/// <summary> /// Apply the filter to a list of spectra. In "normal" operation /// this list has a length of one. For ion mobility data it /// may be a list of spectra with the same retention time but /// different ion mobility values. For Agilent Mse data it may be /// a list of MS2 spectra that need averaging (or even a list /// of MS2 spectra with mixed retention and ion mobility values). Averaging /// is done by unique retention time count, rather than by spectrum /// count, so that ion mobility data ion counts are additive (we're /// trying to measure ions per injection, basically). /// </summary> private ExtractedSpectrum FilterSpectrumList(MsDataSpectrum[] spectra, SpectrumProductFilter[] productFilters, bool highAcc, bool useIonMobilityHighEnergyOffset) { int targetCount = 1; if (Q1 == 0) { highAcc = false; // No mass error for all-ions extraction } else { if (productFilters.Length == 0) { return(null); } targetCount = productFilters.Length; } float[] extractedIntensities = new float[targetCount]; float[] massErrors = highAcc ? new float[targetCount] : null; double[] meanErrors = highAcc ? new double[targetCount] : null; double minIonMobilityHighEnergyOffset = useIonMobilityHighEnergyOffset ? productFilters.Select(f => f.HighEnergyIonMobilityValueOffset).Min() : 0; double maxIonMobilityHighEnergyOffset = useIonMobilityHighEnergyOffset ? productFilters.Select(f => f.HighEnergyIonMobilityValueOffset).Max() : 0; int spectrumCount = 0; int rtCount = 0; double lastRT = 0; var specIndexFirst = 0; var specIndexLast = spectra.Length; if (specIndexLast > 1 && MinIonMobilityValue.HasValue) { // Only inspect the range of spectra that match our ion mobility window var im0 = spectra[0].IonMobility.Mobility; var im1 = spectra[1].IonMobility.Mobility; if (im0.HasValue && im1.HasValue) { if (im0 < im1) { // Binary search for first spectrum in ascending ion mobility range (as in drift time) var im = MinIonMobilityValue.Value + minIonMobilityHighEnergyOffset; specIndexFirst = CollectionUtil.BinarySearch(spectra, s => (s.IonMobility.Mobility ?? 0).CompareTo(im), true); } else if (im0 > im1) { // Binary search for first spectrum in descending ion mobility range (as in TIMS) var im = MaxIonMobilityValue.Value + maxIonMobilityHighEnergyOffset; specIndexFirst = CollectionUtil.BinarySearch(spectra, s => im.CompareTo(s.IonMobility.Mobility ?? 0), true); } if (specIndexFirst < 0) { specIndexFirst = ~specIndexFirst; } } } for (var specIndex = specIndexFirst; specIndex < specIndexLast; specIndex++) { var spectrum = spectra[specIndex]; // If these are spectra from distinct retention times, average them. // Note that for ion mobility data we will see fewer retention time changes // than the total spectra count - ascending DT (or descending 1/K0) within each RT. Within a // single retention time the ions are additive. var rt = spectrum.RetentionTime ?? 0; if (lastRT != rt) { rtCount++; lastRT = rt; } // Filter on scan polarity if (Q1.IsNegative != spectrum.NegativeCharge) { continue; } spectrumCount++; // Filter on ion mobility, if any - gross check before we look at individual fragment high energy offsets if (spectrum.IonMobilities == null && // Not for 3D spectra !ContainsIonMobilityValue(spectrum.IonMobility, maxIonMobilityHighEnergyOffset) && !ContainsIonMobilityValue(spectrum.IonMobility, minIonMobilityHighEnergyOffset)) { if (specIndex > specIndexFirst && specIndexFirst > 0) { break; // We have left the range of interesting ion mobilities } continue; } var mzArray = spectrum.Mzs; if ((mzArray == null) || (mzArray.Length == 0)) { continue; } // It's not unusual for mzarray and centerArray to have no overlap, esp. with ion mobility data if (Q1 != 0) { var lastProductFilter = productFilters[targetCount - 1]; if ((lastProductFilter.TargetMz.Value + lastProductFilter.FilterWidth / 2) < mzArray[0]) { continue; } } var intensityArray = spectrum.Intensities; var imsArray = spectrum.IonMobilities; // Search for matching peaks for each Q3 filter // Use binary search to get to the first m/z value to be considered more quickly // This should help MS1 where isotope distributions will be very close in m/z // It should also help MS/MS when more selective, larger fragment ions are used, // since then a lot of less selective, smaller peaks must be skipped int iPeak = 0; for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) { var productFilter = productFilters[targetIndex]; // If fragments have individual high energy ion mobility offsets, recheck // but only if there is no IonMobilities array. Otherwise IMS filtering is // performed during extraction if (spectrum.IonMobilities == null && productFilter.HighEnergyIonMobilityValueOffset != 0 && !ContainsIonMobilityValue(spectrum.IonMobility, productFilter.HighEnergyIonMobilityValueOffset)) { continue; } // Look for the first peak that is greater than the start of the filter double targetMz = 0, endFilter = double.MaxValue; if (Q1 != 0) { targetMz = productFilter.TargetMz; double filterWindow = productFilter.FilterWidth; double startFilter = targetMz - filterWindow / 2; endFilter = startFilter + filterWindow; if (iPeak < mzArray.Length) { iPeak = Array.BinarySearch(mzArray, iPeak, mzArray.Length - iPeak, startFilter); if (iPeak < 0) { iPeak = ~iPeak; } } if (iPeak >= mzArray.Length) { break; // No further overlap } } // Add the intensity values of all peaks that pass the filter double totalIntensity = extractedIntensities[targetIndex]; // Start with the value from the previous spectrum, if any double meanError = highAcc ? meanErrors[targetIndex] : 0; for (int iNext = iPeak; iNext < mzArray.Length && mzArray[iNext] < endFilter; iNext++) { double mz = mzArray[iNext]; double intensity = intensityArray[iNext]; // Avoid adding points that are not within the allowed ion mobility range if (imsArray != null && !ContainsIonMobilityValue(imsArray[iNext], productFilter.HighEnergyIonMobilityValueOffset)) { continue; } if (Extractor == ChromExtractor.summed) { totalIntensity += intensity; } else if (intensity > totalIntensity) { totalIntensity = intensity; meanError = 0; } // Accumulate weighted mean mass error for summed, or take a single // mass error of the most intense peak for base peak. if (highAcc && (Extractor == ChromExtractor.summed || meanError == 0)) { if (totalIntensity > 0.0) { double deltaPeak = mz - targetMz; meanError += (deltaPeak - meanError) * intensity / totalIntensity; } } } extractedIntensities[targetIndex] = (float)totalIntensity; if (meanErrors != null) { meanErrors[targetIndex] = meanError; } } } if (spectrumCount == 0) { return(null); } if (meanErrors != null) { for (int i = 0; i < targetCount; i++) { massErrors[i] = (float)SequenceMassCalc.GetPpm(productFilters[i].TargetMz, meanErrors[i]); } } // If we summed across spectra of different retention times, scale per // unique retention time (but not per ion mobility value) if ((Extractor == ChromExtractor.summed) && (rtCount > 1)) { float scale = (float)(1.0 / rtCount); for (int i = 0; i < targetCount; i++) { extractedIntensities[i] *= scale; } } var dtFilter = GetIonMobilityWindow(); return(new ExtractedSpectrum(ModifiedSequence, PeptideColor, Q1, dtFilter, Extractor, Id, productFilters, extractedIntensities, massErrors)); }
/// <summary> /// Apply the filter to a list of spectra. In "normal" operation /// this list has a length of one. For ion mobility data it /// may be a list of spectra with the same retention time but /// different ion mobility values. For Agilent Mse data it may be /// a list of MS2 spectra that need averaging (or even a list /// of MS2 spectra with mixed retention and ion mobility values). Averaging /// is done by unique retention time count, rather than by spectrum /// count, so that ion mobility data ion counts are additive (we're /// trying to measure ions per injection, basically). /// </summary> private ExtractedSpectrum FilterSpectrumList(IEnumerable <MsDataSpectrum> spectra, SpectrumProductFilter[] productFilters, bool highAcc, bool useDriftTimeHighEnergyOffset) { int targetCount = 1; if (Q1 == 0) { highAcc = false; // No mass error for all-ions extraction } else { if (productFilters.Length == 0) { return(null); } targetCount = productFilters.Length; } float[] extractedIntensities = new float[targetCount]; float[] massErrors = highAcc ? new float[targetCount] : null; double[] meanErrors = highAcc ? new double[targetCount] : null; int spectrumCount = 0; int rtCount = 0; double lastRT = 0; foreach (var spectrum in spectra) { // If these are spectra from distinct retention times, average them. // Note that for ion mobility data we will see fewer retention time changes // than the total spectra count - ascending DT within each RT. Within a // single retention time the ions are additive. var rt = spectrum.RetentionTime ?? 0; if (lastRT != rt) { rtCount++; lastRT = rt; } // Filter on scan polarity if (Q1.IsNegative != spectrum.NegativeCharge) { continue; } spectrumCount++; // Filter on ion mobility, if any if (!ContainsIonMobilityValue(spectrum.IonMobility, useDriftTimeHighEnergyOffset)) { continue; } var mzArray = spectrum.Mzs; if ((mzArray == null) || (mzArray.Length == 0)) { continue; } // It's not unusual for mzarray and centerArray to have no overlap, esp. with ion mobility data if (Q1 != 0) { var lastProductFilter = productFilters[targetCount - 1]; if ((lastProductFilter.TargetMz.Value + lastProductFilter.FilterWidth / 2) < mzArray[0]) { continue; } } var intensityArray = spectrum.Intensities; // Search for matching peaks for each Q3 filter // Use binary search to get to the first m/z value to be considered more quickly // This should help MS1 where isotope distributions will be very close in m/z // It should also help MS/MS when more selective, larger fragment ions are used, // since then a lot of less selective, smaller peaks must be skipped int iPeak = 0; for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) { // Look for the first peak that is greater than the start of the filter double targetMz = 0, endFilter = double.MaxValue; if (Q1 != 0) { var productFilter = productFilters[targetIndex]; targetMz = productFilter.TargetMz; double filterWindow = productFilter.FilterWidth; double startFilter = targetMz - filterWindow / 2; endFilter = startFilter + filterWindow; if (iPeak < mzArray.Length) { iPeak = Array.BinarySearch(mzArray, iPeak, mzArray.Length - iPeak, startFilter); if (iPeak < 0) { iPeak = ~iPeak; } } if (iPeak >= mzArray.Length) { break; // No further overlap } } // Add the intensity values of all peaks that pass the filter double totalIntensity = extractedIntensities[targetIndex]; // Start with the value from the previous spectrum, if any double meanError = highAcc ? meanErrors[targetIndex] : 0; for (int iNext = iPeak; iNext < mzArray.Length && mzArray[iNext] < endFilter; iNext++) { double mz = mzArray[iNext]; double intensity = intensityArray[iNext]; if (Extractor == ChromExtractor.summed) { totalIntensity += intensity; } else if (intensity > totalIntensity) { totalIntensity = intensity; meanError = 0; } // Accumulate weighted mean mass error for summed, or take a single // mass error of the most intense peak for base peak. if (highAcc && (Extractor == ChromExtractor.summed || meanError == 0)) { if (totalIntensity > 0.0) { double deltaPeak = mz - targetMz; meanError += (deltaPeak - meanError) * intensity / totalIntensity; } } } extractedIntensities[targetIndex] = (float)totalIntensity; if (meanErrors != null) { meanErrors[targetIndex] = meanError; } } } if (spectrumCount == 0) { return(null); } if (meanErrors != null) { for (int i = 0; i < targetCount; i++) { massErrors[i] = (float)SequenceMassCalc.GetPpm(productFilters[i].TargetMz, meanErrors[i]); } } // If we summed across spectra of different retention times, scale per // unique retention time (but not per ion mobility value) if ((Extractor == ChromExtractor.summed) && (rtCount > 1)) { float scale = (float)(1.0 / rtCount); for (int i = 0; i < targetCount; i++) { extractedIntensities[i] *= scale; } } var dtFilter = GetIonMobilityWindow(useDriftTimeHighEnergyOffset); return(new ExtractedSpectrum(ModifiedSequence, PeptideColor, Q1, dtFilter, Extractor, Id, productFilters, extractedIntensities, massErrors)); }
/// <summary> /// Apply the filter to a list of spectra. In "normal" operation /// this list has a length of one. For ion mobility data it /// may be a list of spectra with the same retention time but /// different ion mobility values. For Agilent Mse data it may be /// a list of MS2 spectra that need averaging (or even a list /// of MS2 spectra with mixed retention and ion mobility values). Averaging /// is done by unique retention time count, rather than by spectrum /// count, so that ion mobility data ion counts are additive (we're /// trying to measure ions per injection, basically). /// </summary> private ExtractedSpectrum FilterSpectrumList(MsDataSpectrum[] spectra, SpectrumProductFilter[] productFilters, bool highAcc, bool useIonMobilityHighEnergyOffset) { int targetCount = 1; if (Q1 == 0) { highAcc = false; // No mass error for all-ions extraction } else { if (productFilters.Length == 0) { return(null); } targetCount = productFilters.Length; } float[] extractedIntensities = new float[targetCount]; float[] massErrors = highAcc ? new float[targetCount] : null; double[] meanErrors = highAcc ? new double[targetCount] : null; int spectrumCount = 0; int rtCount = 0; double lastRT = 0; var imRangeHelper = new IonMobilityRangeHelper(spectra, useIonMobilityHighEnergyOffset ? productFilters : null, MinIonMobilityValue, MaxIonMobilityValue); if (imRangeHelper.IndexFirst >= spectra.Length) { // No ion mobility match - record a zero intensity unless IM value is outside the // machine's measured range, or if this is a polarity mismatch if (!IsOutsideSpectraRangeIM(spectra, MinIonMobilityValue, MaxIonMobilityValue) && spectra.Any(s => Equals(s.NegativeCharge, Q1.IsNegative))) { spectrumCount++; // Our flag to process this as zero rather than null } } // if (spectra.Length > 1) // Console.Write(string.Empty); for (int specIndex = imRangeHelper.IndexFirst; specIndex < spectra.Length; specIndex++) { var spectrum = spectra[specIndex]; if (imRangeHelper.IsBeyondRange(spectrum)) { break; } // If these are spectra from distinct retention times, average them. // Note that for ion mobility data we will see fewer retention time changes // than the total spectra count - ascending DT (or descending 1/K0) within each RT. Within a // single retention time the ions are additive. var rt = spectrum.RetentionTime ?? 0; if (lastRT != rt) { rtCount++; lastRT = rt; } // Filter on scan polarity if (Q1.IsNegative != spectrum.NegativeCharge) { continue; } spectrumCount++; var mzArray = spectrum.Mzs; if (mzArray == null || mzArray.Length == 0) { continue; } // It's not unusual for mzarray and centerArray to have no overlap, esp. with ion mobility data if (Q1 != 0) { var lastProductFilter = productFilters[targetCount - 1]; if (lastProductFilter.TargetMz.Value + lastProductFilter.FilterWidth / 2 < mzArray[0]) { continue; } } var intensityArray = spectrum.Intensities; var imsArray = spectrum.IonMobilities; // Search for matching peaks for each Q3 filter // Use binary search to get to the first m/z value to be considered more quickly // This should help MS1 where isotope distributions will be very close in m/z // It should also help MS/MS when more selective, larger fragment ions are used, // since then a lot of less selective, smaller peaks must be skipped int iPeak = 0; for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) { var productFilter = productFilters[targetIndex]; // Ensure uncombined IM spectra are within range if (spectrum.IonMobilities == null && !ContainsIonMobilityValue(spectrum.IonMobility, useIonMobilityHighEnergyOffset ? productFilter.HighEnergyIonMobilityValueOffset : 0)) { continue; } // Look for the first peak that is greater than the start of the filter double targetMz = 0, endFilter = double.MaxValue; if (Q1 != 0) { targetMz = productFilter.TargetMz; double filterWindow = productFilter.FilterWidth; double startFilter = targetMz - filterWindow / 2; endFilter = startFilter + filterWindow; if (iPeak < mzArray.Length) { iPeak = Array.BinarySearch(mzArray, iPeak, mzArray.Length - iPeak, startFilter); if (iPeak < 0) { iPeak = ~iPeak; } } if (iPeak >= mzArray.Length) { break; // No further overlap } } // Add the intensity values of all peaks that pass the filter double totalIntensity = extractedIntensities[targetIndex]; // Start with the value from the previous spectrum, if any double meanError = highAcc ? meanErrors[targetIndex] : 0; for (int iNext = iPeak; iNext < mzArray.Length && mzArray[iNext] < endFilter; iNext++) { double mz = mzArray[iNext]; double intensity = intensityArray[iNext]; // Avoid adding points that are not within the allowed ion mobility range if (imsArray != null && !ContainsIonMobilityValue(imsArray[iNext], useIonMobilityHighEnergyOffset ? productFilter.HighEnergyIonMobilityValueOffset : 0)) { continue; } if (Extractor == ChromExtractor.summed) { totalIntensity += intensity; } else if (intensity > totalIntensity) { totalIntensity = intensity; meanError = 0; } // Accumulate weighted mean mass error for summed, or take a single // mass error of the most intense peak for base peak. if (highAcc && (Extractor == ChromExtractor.summed || meanError == 0)) { if (totalIntensity > 0.0) { double deltaPeak = mz - targetMz; meanError += (deltaPeak - meanError) * intensity / totalIntensity; } } } extractedIntensities[targetIndex] = (float)totalIntensity; if (meanErrors != null) { meanErrors[targetIndex] = meanError; } } } if (spectrumCount == 0) { return(null); } if (meanErrors != null) { for (int i = 0; i < targetCount; i++) { massErrors[i] = (float)SequenceMassCalc.GetPpm(productFilters[i].TargetMz, meanErrors[i]); } } // If we summed across spectra of different retention times, scale per // unique retention time (but not per ion mobility value) if (Extractor == ChromExtractor.summed && rtCount > 1) { float scale = (float)(1.0 / rtCount); for (int i = 0; i < targetCount; i++) { extractedIntensities[i] *= scale; } } var dtFilter = GetIonMobilityWindow(); return(new ExtractedSpectrum(ModifiedSequence, PeptideColor, Q1, dtFilter, Extractor, Id, productFilters, extractedIntensities, massErrors)); }