Skip to content

Commit 7c691ce

Browse files
authored
Merge branch 'master' into mzIdentMlCoverage
2 parents 4f91f1a + 4d81586 commit 7c691ce

3 files changed

Lines changed: 197 additions & 2 deletions

File tree

mzLib/MassSpectrometry/Deconvolution/AverageResidue/AverageResidue.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ public abstract class AverageResidue : IEquatable<AverageResidue>
1616
public abstract double[] GetAllTheoreticalIntensities(int index);
1717
public abstract double GetDiffToMonoisotopic(int index);
1818

19+
// Terminology for the offsets composed from this model:
20+
// • most-abundant offset = GetDiffToMonoisotopic(GetMostIntenseMassIndex(mono)) — the gap from the
21+
// monoisotopic mass to the single tallest (most abundant) isotopologue. This is what the resolved
22+
// most-abundant search uses; it is composed from the abstract methods above, so no dedicated
23+
// method is needed here.
24+
// • average / centroid offset = the gap from the monoisotopic mass to the intensity-weighted mean
25+
// of the whole envelope, for isotopically unresolved (high-mass) species. That helper
26+
// (GetAverageOffset) is introduced separately with the unresolved-envelope work, not here.
27+
1928
#region IEquatable<AverageResidue>
2029

2130
public bool Equals(AverageResidue? other)

mzLib/MassSpectrometry/MzSpectra/IsotopicEnvelope.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,43 @@ public class IsotopicEnvelope : IHasMass, IEquatable<IsotopicEnvelope>
1212
public double MonoisotopicMass { get; private set; }
1313

1414
/// <summary>
15-
/// Mass of most abundant observed isotopic peak, not accounting for addition or subtraction or protons due to ESI charge state induction
15+
/// Most abundant observed isotopic peak as m/z × |charge|, i.e. <b>not</b> proton-corrected to a
16+
/// neutral mass (it does not subtract the charge-carrier protons added during ESI). This is a
17+
/// charge-scaled m/z, not a neutral mass; for the proton-corrected neutral mass use
18+
/// <see cref="MostAbundantObservedNeutralMass"/>. Retained for existing deconvolution-quality tests.
19+
///
20+
/// <b>Sentinel:</b> this value is only meaningful for an envelope that carries observed isotopic
21+
/// peaks. Constructors that have no observed envelope (e.g. a neutral mass read from a
22+
/// pre-deconvoluted file) set it to <c>-1</c> to mark "no most-abundant peak available", rather
23+
/// than a misleading zero or a synthetic value.
1624
/// </summary>
17-
internal double MostAbundantObservedIsotopicMass { get; private set; }
25+
public double MostAbundantObservedIsotopicMass { get; private set; }
26+
27+
/// <summary>
28+
/// The <b>most-abundant observed neutral mass</b>: the neutral mass of the single most intense
29+
/// (tallest) observed isotopic peak of this charge-state envelope — the highest-signal, directly
30+
/// measured point of the envelope. The most intense peak's m/z is converted to a neutral mass via
31+
/// the envelope charge (i.e. proton-corrected, <c>mz.ToMass(Charge)</c>), so it is directly
32+
/// comparable to a theoretical proteoform neutral mass. It is the precursor mass used for candidate
33+
/// selection in most-abundant mode. Computed from <see cref="Peaks"/> and <see cref="Charge"/>
34+
/// (both readonly).
35+
///
36+
/// Terminology (the three masses are distinct):
37+
/// • <b>most-abundant (observed neutral) mass</b> — this property; the tallest single isotopic
38+
/// peak, proton-corrected to a neutral mass.
39+
/// • <b>monoisotopic mass</b> (<see cref="MonoisotopicMass"/>) — the all-light-isotope mass; for
40+
/// large proteoforms this isotopologue is rare and often undetectable.
41+
/// • <b>average / centroid mass</b> — the intensity-weighted mean over the whole envelope, used
42+
/// for isotopically unresolved (high-mass) species. That property is added separately, with the
43+
/// unresolved-envelope work, and is not part of this resolved most-abundant feature.
44+
/// This is the neutral-mass form of <see cref="MostAbundantObservedIsotopicMass"/> (which is the
45+
/// SAME tallest peak expressed as m/z × |charge|, NOT proton-corrected): the two differ by exactly
46+
/// <c>|charge| × ProtonMass</c>. Deriving it from that single source means the two values can never
47+
/// disagree, and it carries the same <c>-1</c> "no observed peak" sentinel.
48+
/// </summary>
49+
public double MostAbundantObservedNeutralMass =>
50+
MostAbundantObservedIsotopicMass < 0 ? -1 : MostAbundantObservedIsotopicMass - Charge * Constants.ProtonMass;
51+
1852
public readonly int Charge;
1953
public readonly double TotalIntensity;
2054
public readonly int PrecursorId;
@@ -60,6 +94,9 @@ public IsotopicEnvelope(double monoisotopicMass, double intensity, int charge)
6094
TotalIntensity = intensity;
6195
Score = double.MaxValue;
6296
Peaks = [(monoisotopicMass.ToMz(charge), intensity)];
97+
// A neutral mass read from a pre-deconvoluted file has no observed isotopic envelope, so
98+
// there is no most-abundant observed peak to report — use the -1 sentinel.
99+
MostAbundantObservedIsotopicMass = -1;
63100
}
64101

65102
/// <summary>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Chemistry;
4+
using MassSpectrometry;
5+
using NUnit.Framework;
6+
7+
namespace Test
8+
{
9+
/// <summary>
10+
/// Unit tests for the resolved "most abundant mass" precursor-selection support (Strategy B).
11+
///
12+
/// Terminology pinned by these tests:
13+
/// • most-abundant mass = the neutral mass of the single most intense (tallest) isotopic peak,
14+
/// proton-corrected (<see cref="IsotopicEnvelope.MostAbundantObservedNeutralMass"/>);
15+
/// • most-abundant offset = GetDiffToMonoisotopic(GetMostIntenseMassIndex(mono)) on the averagine
16+
/// model — the gap from the monoisotopic mass to that tallest isotopologue.
17+
/// The intensity-weighted average (centroid) mass and the isotopically-unresolved path are a
18+
/// separate change and are tested with that work, not here. All tests build synthetic envelopes
19+
/// from the Averagine model — no deconvolution is run.
20+
/// </summary>
21+
[TestFixture]
22+
public sealed class TestMostAbundantMass
23+
{
24+
private static readonly AverageResidue Model = new Averagine();
25+
26+
// Most-abundant offset for a monoisotopic mass = the averagine diff-to-monoisotopic of the
27+
// nearest mass bin (the mass-keyed composition consumers use in place of a dedicated method).
28+
private static double MostAbundantOffset(double monoMass) => Model.GetDiffToMonoisotopic(Model.GetMostIntenseMassIndex(monoMass));
29+
30+
/// <summary>
31+
/// Builds a perfect synthetic envelope: peaks at exact theoretical m/z with
32+
/// Averagine-proportional intensities. (Same construction as TestDeconvolutionScorerUnit.)
33+
/// </summary>
34+
private static List<(double mz, double intensity)> BuildPerfectPeaks(double monoMass, int charge, double baseIntens = 1e6)
35+
{
36+
int avgIdx = Model.GetMostIntenseMassIndex(monoMass);
37+
double[] rawMasses = Model.GetAllTheoreticalMasses(avgIdx);
38+
double[] rawIntens = Model.GetAllTheoreticalIntensities(avgIdx);
39+
40+
var sorted = rawMasses.Zip(rawIntens).OrderBy(p => p.First).ToArray();
41+
42+
double isotopeStep = Constants.C13MinusC12 / charge;
43+
double monoMz = monoMass.ToMz(charge);
44+
var peaks = new List<(double mz, double intensity)>();
45+
for (int n = 0; n < sorted.Length; n++)
46+
{
47+
double intensity = baseIntens * sorted[n].Second;
48+
if (intensity < baseIntens * 0.001) continue;
49+
peaks.Add((monoMz + n * isotopeStep, intensity));
50+
}
51+
return peaks;
52+
}
53+
54+
private static IsotopicEnvelope BuildPerfectEnvelope(double monoMass, int charge, double baseIntens = 1e6)
55+
{
56+
var peaks = BuildPerfectPeaks(monoMass, charge, baseIntens);
57+
return new IsotopicEnvelope(0, peaks, monoMass, charge, peaks.Sum(p => p.intensity), 0.999);
58+
}
59+
60+
// ── AverageResidue most-abundant offset ─────────────────────────────────
61+
62+
[Test]
63+
public void MostAbundantOffset_IsNonNegativeAndNonDecreasingWithMass()
64+
{
65+
double[] masses = { 500, 2000, 5000, 10000, 20000, 40000 };
66+
double prev = double.NegativeInfinity;
67+
foreach (double m in masses)
68+
{
69+
double offset = MostAbundantOffset(m);
70+
Assert.That(offset, Is.GreaterThanOrEqualTo(-1e-6)); // ~0 at tiny mass (mono IS most abundant)
71+
Assert.That(offset, Is.GreaterThanOrEqualTo(prev - 1e-6), $"offset decreased at mass {m}");
72+
prev = offset;
73+
}
74+
}
75+
76+
[Test]
77+
public void MostAbundantOffset_IsNearZeroForSmallMass()
78+
{
79+
// A small peptide's monoisotopic peak is (nearly) the most abundant.
80+
Assert.That(MostAbundantOffset(500), Is.LessThan(0.5));
81+
}
82+
83+
[Test]
84+
public void MostAbundantOffset_GrowsRoughlyOneNeutronPer1600Da()
85+
{
86+
// ~1 13C neutron (~1.00235 Da) per ~1.6 kDa. Assert the offset at 16 kDa is in a
87+
// physically plausible band (≈ 9–11 Da) rather than an exact value.
88+
double offset = MostAbundantOffset(16000);
89+
Assert.That(offset, Is.GreaterThan(8.0).And.LessThan(12.0));
90+
}
91+
92+
// ── IsotopicEnvelope most-abundant observed mass ────────────────────────
93+
94+
[Test]
95+
public void MostAbundantObservedNeutralMass_IsProtonCorrectedNeutralMass()
96+
{
97+
const int charge = 10;
98+
var env = BuildPerfectEnvelope(15000, charge);
99+
100+
// The proton-corrected neutral mass equals the most intense peak's m/z .ToMass(charge)...
101+
double mostIntenseMz = env.Peaks.MaxBy(p => p.intensity).mz;
102+
Assert.That(env.MostAbundantObservedNeutralMass, Is.EqualTo(mostIntenseMz.ToMass(charge)).Within(1e-6));
103+
104+
// ...and it differs from the un-proton-corrected mz*|z| field by exactly z proton masses.
105+
Assert.That(env.MostAbundantObservedIsotopicMass - env.MostAbundantObservedNeutralMass,
106+
Is.EqualTo(charge * Constants.ProtonMass).Within(1e-6));
107+
}
108+
109+
[Test]
110+
public void MostAbundantObservedNeutralMass_MatchesMonoPlusAveragineOffset()
111+
{
112+
// Ties the two features together: the observed most-abundant neutral mass of a perfect
113+
// envelope ≈ candidate monoisotopic + averagine most-abundant offset.
114+
const double mono = 12000;
115+
const int charge = 12;
116+
var env = BuildPerfectEnvelope(mono, charge);
117+
118+
double predicted = mono + MostAbundantOffset(mono);
119+
Assert.That(env.MostAbundantObservedNeutralMass, Is.EqualTo(predicted).Within(0.15));
120+
}
121+
122+
[Test]
123+
public void DeconvolutionConstructor_ComputesMostAbundantObservedNeutralMass()
124+
{
125+
// The 5-arg mzLib-deconvolution constructor must compute the most-abundant observed mass.
126+
const double mono = 12000;
127+
const int charge = 12;
128+
var peaks = BuildPerfectPeaks(mono, charge);
129+
var env = new IsotopicEnvelope(peaks, mono, charge, peaks.Sum(p => p.intensity), 0.5);
130+
131+
double mostIntenseMz = peaks.MaxBy(p => p.intensity).mz;
132+
Assert.That(env.MostAbundantObservedNeutralMass, Is.EqualTo(mostIntenseMz.ToMass(charge)).Within(1e-6));
133+
}
134+
135+
[Test]
136+
public void FileReadEnvelope_HasNoMostAbundantPeak_ReturnsSentinel()
137+
{
138+
// The file-read constructor carries a neutral mass but no observed isotopic envelope, so the
139+
// most-abundant observed mass is undefined: both the m/z×|charge| form and the proton-corrected
140+
// form report the -1 sentinel rather than a synthetic value.
141+
const double mono = 8000;
142+
const int charge = 8;
143+
var env = new IsotopicEnvelope(mono, 1e6, charge);
144+
145+
Assert.That(env.MostAbundantObservedIsotopicMass, Is.EqualTo(-1));
146+
Assert.That(env.MostAbundantObservedNeutralMass, Is.EqualTo(-1));
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)