From 2c9a7efb1d25b24faa29d587fb207df462f66629 Mon Sep 17 00:00:00 2001 From: Ryan Fellers Date: Fri, 25 Oct 2024 12:39:34 -0500 Subject: [PATCH 1/3] manually carry through monoisotopic mass and m/z on isotopic envelopes (don't assume it's the first mass), updates some vulnerable nuget packages --- .../ChargedIsotopicDistribution.cs | 147 ++++++------ .../IChargedIsotopicDistribution.cs | 3 + .../MassSpectrometry/IIsotopicDistribution.cs | 3 + .../MassSpectrometry/IsotopicDistribution.cs | 214 +++++++++--------- .../Tools/FineStructureIsotopicGenerator.cs | 4 +- src/TopDownProteomics/Tools/Mercury7.cs | 4 +- .../IsotopicDistributionTest.cs | 23 +- .../TopDownProteomics.Tests.csproj | 2 + 8 files changed, 217 insertions(+), 183 deletions(-) diff --git a/src/TopDownProteomics/MassSpectrometry/ChargedIsotopicDistribution.cs b/src/TopDownProteomics/MassSpectrometry/ChargedIsotopicDistribution.cs index 1bb0569..1d08126 100644 --- a/src/TopDownProteomics/MassSpectrometry/ChargedIsotopicDistribution.cs +++ b/src/TopDownProteomics/MassSpectrometry/ChargedIsotopicDistribution.cs @@ -1,99 +1,94 @@ using System; using System.Linq; -namespace TopDownProteomics.MassSpectrometry -{ - /// - /// Isotopic Distribution with a given charge. - /// - /// - public class ChargedIsotopicDistribution : IMzIntensityData, IChargedIsotopicDistribution - { - private readonly double[] _intensity; - private readonly double[] _mz; +namespace TopDownProteomics.MassSpectrometry; - /// - /// Initializes a new instance of the class. - /// - /// The mz. - /// The intensity. - /// The charge. - /// The charge carrier. - public ChargedIsotopicDistribution(double[] mz, double[] intensity, int charge, double chargeCarrier) - { - _intensity = intensity; - _mz = mz; +/// +/// Isotopic Distribution with a given charge. +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The mono m/z. +/// The mz. +/// The intensity. +/// The charge. +/// The charge carrier. +public class ChargedIsotopicDistribution(double monoMz, double[] mz, double[] intensity, int charge, double chargeCarrier) : IMzIntensityData, IChargedIsotopicDistribution +{ + private readonly double[] _intensity = intensity; + private readonly double[] _mz = mz; - this.Charge = charge; - this.ChargeCarrier = chargeCarrier; - } + /// The charge. + public int Charge { get; } = charge; - /// The charge. - public int Charge { get; } + /// The mass of the charge carrier. + public double ChargeCarrier { get; } = chargeCarrier; - /// The mass of the charge carrier. - public double ChargeCarrier { get; } + /// + /// Gets the mz. + /// + public double[] GetMz() => _mz; - /// - /// Gets the mz. - /// - public double[] GetMz() => _mz; + /// + /// Gets the first m/z. + /// + public double FirstMz => _mz[0]; - /// - /// Gets the first m/z. - /// - public double FirstMz => _mz[0]; + /// The monoisotopic m/z. + public double MonoisotopicMz { get; } = monoMz; - /// - /// Gets the last m/z. - /// - public double LastMz => _mz[_mz.Length - 1]; + /// + /// Gets the last m/z. + /// + public double LastMz => _mz[_mz.Length - 1]; - /// - /// Gets the intensity. - /// - public double[] GetIntensity() => _intensity; + /// + /// Gets the intensity. + /// + public double[] GetIntensity() => _intensity; - /// - /// Gets the length. - /// - public int Length => _mz.Length; + /// + /// Gets the length. + /// + public int Length => _mz.Length; - /// - /// Clones the distribution with a subset of the most intense points. - /// - /// The number of points to keep. - /// - public IChargedIsotopicDistribution CloneWithMostIntensePoints(int numberOfPoints) + /// + /// Clones the distribution with a subset of the most intense points. + /// + /// The number of points to keep. + /// + public IChargedIsotopicDistribution CloneWithMostIntensePoints(int numberOfPoints) + { + for (int i = 0; i < _mz.Length - numberOfPoints; i++) { - for (int i = 0; i < _mz.Length - numberOfPoints; i++) + if (_intensity[i] > _intensity[i + numberOfPoints]) { - if (_intensity[i] > _intensity[i + numberOfPoints]) - { - // Moving any more would only make things less intense ... stop - return new ChargedIsotopicDistribution(_mz.SubSequence(i, i + numberOfPoints - 1).ToArray(), - _intensity.SubSequence(i, i + numberOfPoints - 1).ToArray(), this.Charge, this.ChargeCarrier); - } + // Moving any more would only make things less intense ... stop + return new ChargedIsotopicDistribution(this.MonoisotopicMz, _mz.SubSequence(i, i + numberOfPoints - 1).ToArray(), + _intensity.SubSequence(i, i + numberOfPoints - 1).ToArray(), this.Charge, this.ChargeCarrier); } - - throw new Exception($"Cannot find most {numberOfPoints} intense points."); } - /// - /// Clones the distribution and shifts it by an m/z (Th) value. - /// - /// The shift m/z in thomsons (Th). - /// - public IChargedIsotopicDistribution CloneAndShift(double shiftMz) - { - double[] mz = new double[this.Length]; + throw new Exception($"Cannot find most {numberOfPoints} intense points."); + } - for (int i = 0; i < this.Length; i++) - { - mz[i] = _mz[i] + shiftMz; - } + /// + /// Clones the distribution and shifts it by an m/z (Th) value. + /// + /// The shift m/z in Thomsons (Th). + /// + public IChargedIsotopicDistribution CloneAndShift(double shiftMz) + { + double[] mz = new double[this.Length]; + double monoMz = this.MonoisotopicMz + shiftMz; - return new ChargedIsotopicDistribution(mz, (double[])_intensity.Clone(), this.Charge, this.ChargeCarrier); + for (int i = 0; i < this.Length; i++) + { + mz[i] = _mz[i] + shiftMz; } + + return new ChargedIsotopicDistribution(monoMz, mz, (double[])_intensity.Clone(), this.Charge, this.ChargeCarrier); } } \ No newline at end of file diff --git a/src/TopDownProteomics/MassSpectrometry/IChargedIsotopicDistribution.cs b/src/TopDownProteomics/MassSpectrometry/IChargedIsotopicDistribution.cs index 15c1e32..d7ee090 100644 --- a/src/TopDownProteomics/MassSpectrometry/IChargedIsotopicDistribution.cs +++ b/src/TopDownProteomics/MassSpectrometry/IChargedIsotopicDistribution.cs @@ -11,6 +11,9 @@ public interface IChargedIsotopicDistribution : IMzIntensityData /// The mass of the charge carrier. double ChargeCarrier { get; } + /// Gets the monoisotopic m/z. + double MonoisotopicMz { get; } + /// /// Clones the distribution with a subset of the most intense points. /// diff --git a/src/TopDownProteomics/MassSpectrometry/IIsotopicDistribution.cs b/src/TopDownProteomics/MassSpectrometry/IIsotopicDistribution.cs index a396b67..2e7b22d 100644 --- a/src/TopDownProteomics/MassSpectrometry/IIsotopicDistribution.cs +++ b/src/TopDownProteomics/MassSpectrometry/IIsotopicDistribution.cs @@ -13,6 +13,9 @@ public interface IIsotopicDistribution /// int Length { get; } + /// The monoisotopic mass. + double MonoisotopicMass { get; } + /// /// Gets the masses. /// diff --git a/src/TopDownProteomics/MassSpectrometry/IsotopicDistribution.cs b/src/TopDownProteomics/MassSpectrometry/IsotopicDistribution.cs index 8636e57..4bb8582 100644 --- a/src/TopDownProteomics/MassSpectrometry/IsotopicDistribution.cs +++ b/src/TopDownProteomics/MassSpectrometry/IsotopicDistribution.cs @@ -1,114 +1,124 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace TopDownProteomics.MassSpectrometry +namespace TopDownProteomics.MassSpectrometry; + +/// Neutral distribution of isotopes. +public class IsotopicDistribution : IIsotopicDistribution { + private readonly double[] _abundances; + private readonly double[] _masses; + + /// + /// Initializes a new instance of the class. + /// + /// The monoisotopic mass. + /// The masses. + /// The abundances. + public IsotopicDistribution(double monoisotopicMass, IList masses, IList abundances) + { + this.MonoisotopicMass = monoisotopicMass; + _masses = masses.ToArray(); + _abundances = abundances.ToArray(); + this.Length = _masses.Length; + } + /// - /// Neutral distribution of isotopes. + /// Initializes a new instance of the class. /// - public class IsotopicDistribution : IIsotopicDistribution + /// The monoisotopic mass. + /// The masses. + /// The abundances. + public IsotopicDistribution(double monoisotopicMass, double[] masses, double[] abundances) { - private readonly double[] _abundances; - private readonly double[] _masses; - - /// - /// Initializes a new instance of the class. - /// - /// The masses. - /// The abundances. - public IsotopicDistribution(IList masses, IList abundances) - { - _masses = masses.ToArray(); - _abundances = abundances.ToArray(); - this.Length = _masses.Length; - } - - /// - /// Initializes a new instance of the class. - /// - /// The masses. - /// The abundances. - public IsotopicDistribution(double[] masses, double[] abundances) - { - _masses = masses; - _abundances = abundances; - this.Length = _masses.Length; - } - - /// - /// Gets the length. - /// - public int Length { get; } - - /// - /// Gets the masses. - /// - public IList Masses => _masses; - - /// - /// Gets the intensities. - /// - public IList Intensities => _abundances; - - private double[] GetMz(int charge, bool positiveCharge) - { - double[] mz = new double[this.Length]; - - for (int i = 0; i < this.Length; i++) + this.MonoisotopicMass = monoisotopicMass; + _masses = masses; + _abundances = abundances; + this.Length = _masses.Length; + } + + /// + /// Gets the length. + /// + public int Length { get; } + + /// The monoisotopic mass. + public double MonoisotopicMass { get; } + + /// + /// Gets the masses. + /// + public IList Masses => _masses; + + /// + /// Gets the intensities. + /// + public IList Intensities => _abundances; + + private double[] GetMz(int charge, bool positiveCharge) + { + double[] mz = new double[this.Length]; + + for (int i = 0; i < this.Length; i++) #pragma warning disable CS0618 // Type or member is obsolete - mz[i] = Utility.ConvertMassToMz(_masses[i], charge, positiveCharge); + mz[i] = Utility.ConvertMassToMz(_masses[i], charge, positiveCharge); #pragma warning restore CS0618 // Type or member is obsolete - return mz; - } - - private double[] GetMz(int charge, double chargeCarrier) - { - double[] mz = new double[this.Length]; - - for (int i = 0; i < this.Length; i++) - mz[i] = Utility.ConvertMassToMz(_masses[i], charge, chargeCarrier); - - return mz; - } - - /// - /// Creates a charged distribution. - /// - /// The charge. - /// if set to true [positive charge]. - /// - public IChargedIsotopicDistribution CreateChargedDistribution(int charge, bool positiveCharge) - { - Debug.Assert(charge > 0, "Charge must be greater than 0."); - - return new ChargedIsotopicDistribution(this.GetMz(charge, positiveCharge), _abundances, charge, Utility.Proton); - } - - /// - /// Creates a charged isotopic distribution. - /// - /// The charge. - /// The charge carrier. - /// - /// A charged isotopic distribution with the same abundances. - /// - public IChargedIsotopicDistribution CreateChargedDistribution(int charge, double chargeCarrier = Utility.Proton) - { - Debug.Assert(chargeCarrier > 0, "Charge carrier must be greater than 0."); - - return new ChargedIsotopicDistribution(this.GetMz(charge, chargeCarrier), _abundances, charge, chargeCarrier); - } - - /// - /// Clones the distribution and shifts it by a constant m/z value. - /// - /// The shift. - /// - public IIsotopicDistribution CloneAndShift(double shift) - { - return new IsotopicDistribution(_masses.Select(x => x + shift), new List(_abundances)); - } + return mz; + } + + private double[] GetMz(int charge, double chargeCarrier) + { + double[] mz = new double[this.Length]; + + for (int i = 0; i < this.Length; i++) + mz[i] = Utility.ConvertMassToMz(_masses[i], charge, chargeCarrier); + + return mz; + } + + /// + /// Creates a charged distribution. + /// + /// The charge. + /// if set to true [positive charge]. + /// + [Obsolete("Use CreateChargedDistribution(int charge, double chargeCarrier) instead.")] + public IChargedIsotopicDistribution CreateChargedDistribution(int charge, bool positiveCharge) + { + Debug.Assert(charge > 0, "Charge must be greater than 0."); + + double monoMz = Utility.ConvertMassToMz(this.MonoisotopicMass, charge, positiveCharge); + + return new ChargedIsotopicDistribution(monoMz, this.GetMz(charge, positiveCharge), _abundances, charge, Utility.Proton); + } + + /// + /// Creates a charged isotopic distribution. + /// + /// The charge. + /// The charge carrier. + /// + /// A charged isotopic distribution with the same abundances. + /// + public IChargedIsotopicDistribution CreateChargedDistribution(int charge, double chargeCarrier = Utility.Proton) + { + Debug.Assert(chargeCarrier > 0, "Charge carrier must be greater than 0."); + + double monoMz = Utility.ConvertMassToMz(this.MonoisotopicMass, charge, chargeCarrier); + + return new ChargedIsotopicDistribution(monoMz, this.GetMz(charge, chargeCarrier), _abundances, charge, chargeCarrier); + } + + /// + /// Clones the distribution and shifts it by a constant m/z value. + /// + /// The shift. + /// + public IIsotopicDistribution CloneAndShift(double shift) + { + return new IsotopicDistribution(this.MonoisotopicMass + shift, _masses.Select(x => x + shift), new List(_abundances)); } } \ No newline at end of file diff --git a/src/TopDownProteomics/Tools/FineStructureIsotopicGenerator.cs b/src/TopDownProteomics/Tools/FineStructureIsotopicGenerator.cs index cca7714..769e033 100644 --- a/src/TopDownProteomics/Tools/FineStructureIsotopicGenerator.cs +++ b/src/TopDownProteomics/Tools/FineStructureIsotopicGenerator.cs @@ -138,7 +138,9 @@ private IsotopicDistribution CalculateFineGrain(List> elementa i++; } - return new IsotopicDistribution(masses, intensities); + double monoMass = masses[0]; // No pruning, always the first peak + + return new IsotopicDistribution(monoMass, masses, intensities); } private List MultiplyFinePolynomial(List> elementalComposition, double _fineResolution, double _mwResolution, double _fineMinProb) { diff --git a/src/TopDownProteomics/Tools/Mercury7.cs b/src/TopDownProteomics/Tools/Mercury7.cs index 36473d8..2ae62d3 100644 --- a/src/TopDownProteomics/Tools/Mercury7.cs +++ b/src/TopDownProteomics/Tools/Mercury7.cs @@ -158,7 +158,9 @@ private IIsotopicDistribution Mercury(ChemicalFormula cf, double limit) if (msaMz == null || msaAbundance == null) throw new Exception("msa Arrays must not be empty."); - return new IsotopicDistribution(msaMz, msaAbundance); + double monoMass = cf.GetMass(MassType.Monoisotopic); + + return new IsotopicDistribution(monoMass, msaMz, msaAbundance); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs index cc0a29d..83f7c13 100644 --- a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs +++ b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs @@ -55,12 +55,12 @@ public void MercuryChargeCarrierTest() ChemicalFormula formula = ChemicalFormula.ParseString("C6H12O6".AsSpan(), _elementProvider); IChargedIsotopicDistribution result = mercury.GenerateChargedIsotopicDistribution(formula, 1); - Assert.AreEqual(formula.GetMass(MassType.Monoisotopic) + chargeCarrier, result.FirstMz, 0.001); + Assert.AreEqual(formula.GetMass(MassType.Monoisotopic) + chargeCarrier, result.MonoisotopicMz, 0.001); // Test negative charge result = mercury.GenerateChargedIsotopicDistribution(formula, -1); - Assert.AreEqual(formula.GetMass(MassType.Monoisotopic) - chargeCarrier, result.FirstMz, 0.001); + Assert.AreEqual(formula.GetMass(MassType.Monoisotopic) - chargeCarrier, result.MonoisotopicMz, 0.001); } [Test] @@ -69,7 +69,7 @@ public void MercuryResultCloneTest() double[] mz = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; double[] intensity = { 1.1, 2.1, 3.1, 4.2, 5.1, 6.1, 5.2, 4.1, 3.1, 2.1, 1.1 }; - ChargedIsotopicDistribution result = new(mz, intensity, 1, Utility.Proton); + ChargedIsotopicDistribution result = new(mz[0], mz, intensity, 1, Utility.Proton); IChargedIsotopicDistribution clone = result.CloneWithMostIntensePoints(1); double[] cloneMz = clone.GetMz(); @@ -137,4 +137,21 @@ public void HighPruningLimit() result = calc.GenerateChargedIsotopicDistribution(chemicalFormula, 1); Assert.AreEqual(3, result.Length); } + + [Test] + public void MonoMzMassTest() + { + double chargeCarrier = 10d; + var mercury = new Mercury7(1e-5, chargeCarrier); + + ChemicalFormula chemicalFormula = ChemicalFormula.ParseString("C1000H1500O1000".AsSpan(), _elementProvider); + IChargedIsotopicDistribution result = mercury.GenerateChargedIsotopicDistribution(chemicalFormula, 1); + + // Given this high mass and mercury's pruning limit, the mono m/z is filtered out and no longer matches the fist m/z + + double expectedMonoMz = chemicalFormula.GetMass(MassType.Monoisotopic) + chargeCarrier; + + Assert.False(Math.Abs(expectedMonoMz - result.FirstMz) < 0.0001); // shouldn't match the mono m/z + Assert.AreEqual(expectedMonoMz, result.MonoisotopicMz, 0.0001); + } } \ No newline at end of file diff --git a/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj b/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj index 19cea81..10b857b 100644 --- a/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj +++ b/tests/TopDownProteomics.Tests/TopDownProteomics.Tests.csproj @@ -32,12 +32,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + all runtime; build; native; contentfiles; analyzers; buildtransitive + From 6ef904d5cb227ec8d5ee10a6f7c300110d4b56ba Mon Sep 17 00:00:00 2001 From: Ryan Fellers Date: Mon, 28 Oct 2024 10:17:08 -0500 Subject: [PATCH 2/3] adds clone and shift unit test --- .../IsotopicDistributionTest.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs index 83f7c13..8906531 100644 --- a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs +++ b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using System; using System.Collections; +using System.Linq; using TopDownProteomics.Chemistry; using TopDownProteomics.MassSpectrometry; using TopDownProteomics.Tools; @@ -154,4 +155,26 @@ public void MonoMzMassTest() Assert.False(Math.Abs(expectedMonoMz - result.FirstMz) < 0.0001); // shouldn't match the mono m/z Assert.AreEqual(expectedMonoMz, result.MonoisotopicMz, 0.0001); } + + [Test] + public void CloneAndShiftTest() + { + const int shift = 42; + double[] mass = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; + double[] intensity = { 1.1, 2.1, 3.1, 4.2, 5.1, 6.1, 5.2, 4.1, 3.1, 2.1, 1.1 }; + + var isotopicDistribution = new IsotopicDistribution(mass[0], mass, intensity); + + var shift42 = isotopicDistribution.CloneAndShift(shift); + + Assert.AreEqual(isotopicDistribution.MonoisotopicMass + shift, shift42.MonoisotopicMass); + CollectionAssert.AreEquivalent((ICollection)mass.Select(x => x + shift).ToArray(), (ICollection)shift42.Masses); + + var chargedIsotopicDistribution = isotopicDistribution.CreateChargedDistribution(1); + var chargedShift42 = chargedIsotopicDistribution.CloneAndShift(shift); + + Assert.AreEqual(chargedIsotopicDistribution.MonoisotopicMz + shift, chargedShift42.MonoisotopicMz); + CollectionAssert.AreEquivalent((ICollection)chargedIsotopicDistribution.GetMz().Select(x => x + shift).ToArray(), + (ICollection)chargedShift42.GetMz()); + } } \ No newline at end of file From bd989e84fa189026e011b8b184c0a193be0f0e1e Mon Sep 17 00:00:00 2001 From: Ryan Fellers Date: Mon, 28 Oct 2024 10:38:55 -0500 Subject: [PATCH 3/3] continue to unit test an obsolete method --- .../MassSpectrometry/IsotopicDistributionTest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs index 8906531..d382756 100644 --- a/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs +++ b/tests/TopDownProteomics.Tests/MassSpectrometry/IsotopicDistributionTest.cs @@ -30,6 +30,10 @@ public void MercuryTest() IChargedIsotopicDistribution charge1 = dist.CreateChargedDistribution(1); CollectionAssert.AreEquivalent((ICollection)mz, (ICollection)charge1.GetMz()); + + // Continue to test obsolete method + IChargedIsotopicDistribution charge2 = dist.CreateChargedDistribution(1, true); + CollectionAssert.AreEquivalent((ICollection)mz, (ICollection)charge2.GetMz()); } [Test] @@ -154,6 +158,7 @@ public void MonoMzMassTest() Assert.False(Math.Abs(expectedMonoMz - result.FirstMz) < 0.0001); // shouldn't match the mono m/z Assert.AreEqual(expectedMonoMz, result.MonoisotopicMz, 0.0001); + Assert.IsTrue(result.LastMz > result.FirstMz); } [Test]