require 'bsearch' module Mspire module SpectrumLike include Enumerable attr_accessor :products attr_accessor :precursors attr_accessor :scans attr_accessor :ms_level # boolean for if the spectrum represents centroided data or not attr_accessor :centroided # The underlying data store. methods are implemented so that data_arrays[0] is # the m/z's and data_arrays[1] is intensities attr_accessor :data_arrays def centroided?() centroided end # @return [Mspire::Spectrum] # @param [Array] data two element array of mzs and intensities # @param [Boolean] centroided is the spectrum centroided or not def initialize(data_arrays, centroided=true) @data_arrays = data_arrays @centroided = centroided end # found by querying the size of the data store. This should almost always # be 2 (m/z and intensities) def size @data_arrays.size end def ==(other) mzs == other.mzs && intensities == other.intensities end # An array of the mz data. def mzs @data_arrays[0] end def mzs=(ar) @data_arrays[0] = ar end # An array of the intensities data, corresponding to mzs. def intensities @data_arrays[1] end def intensities=(ar) @data_arrays[1] = ar end def mzs_and_intensities [@data_arrays[0], @data_arrays[1]] end # retrieve an m/z and intensity doublet at that index def [](array_index) [@data_arrays[0][array_index], @data_arrays[1][array_index]] end # yields(mz, inten) across the spectrum, or array of doublets if no block. # Note that each peak is merely an array of m/z and intensity. For a # genuine def peaks(&block) @data_arrays[0].zip(@data_arrays[1], &block) end alias_method :each, :peaks alias_method :each_peak, :peaks # returns a bonafide Peaklist object (i.e., each peak is cast as a # Mspire::Peak object). If peak_id is defined, each peak will be cast # as a TaggedPeak object with the given peak_id def to_peaklist(peak_id=nil) # realize this isn't dry, but it is in such an inner loop it needs to be # as fast as possible. pl = Peaklist.new if peak_id peaks.each_with_index do |peak,i| pl[i] = Mspire::TaggedPeak.new( peak, peak_id ) end else peaks.each_with_index do |peak,i| pl[i] = Mspire::Peak.new( peak ) end end pl end # if the mzs and intensities are the same then the spectra are considered # equal def ==(other) mzs == other.mzs && intensities == other.intensities end # returns a new spectrum whose intensities have been normalized by the tic # of another given value def normalize(norm_by=:tic) norm_by = tic if norm_by == :tic Mspire::Spectrum.new([self.mzs, self.intensities.map {|v| v / norm_by }]) end def tic self.intensities.reduce(:+) end # ensures that the m/z values are monotonically ascending (some # instruments are bad about this) # returns self def sort! _peaks = peaks.to_a _peaks.sort! _peaks.each_with_index {|(mz,int), i| @data_arrays[0][i] = mz ; @data_arrays[1][i] = int } self end # the range begin value will also be excluded on an exact match if # exclude_begin is true. (respects the ranges exclude_end? value for the # end). def select_indices(range, exclude_begin=false) indices = [] _mzs = mzs lo_i = _mzs.bsearch_lower_boundary {|v| v <=> range.begin } return indices unless lo_i hi_i = nil (lo_i..._mzs.size).each do |i| break unless range === _mzs[i] indices << i end indices.shift if exclude_begin && range.begin == _mzs[indices.first] indices end # returns the m/z that is closest to the value, favoring the lower m/z in # the case of a tie. Uses a binary search. def find_nearest(val) mzs[find_nearest_index(val)] end # same as find_nearest but returns the index of the point def find_nearest_index(val) find_all_nearest_index(val).first end def find_all_nearest_index(val) _mzs = mzs index = _mzs.bsearch_lower_boundary {|v| v <=> val } if index == _mzs.size [_mzs.size-1] else # if the previous m/z diff is smaller, use it if index == 0 [index] else case (val - _mzs[index-1]).abs <=> (_mzs[index] - val).abs when -1 [index-1] when 0 [index-1, index] when 1 [index] end end end end def find_all_nearest(val) find_all_nearest_index(val).map {|i| mzs[i] } end end end