Skip to content

Commit 659fd68

Browse files
trishortsclaude
andcommitted
refactor(most-abundant): source isotope spacing from deconvolution params
Addresses @nbollis round-1 review on PR #2671. - Replace the hardcoded Constants.C13MinusC12 in both most-abundant mass-diff acceptors with an ExpectedIsotopeSpacing arg, threaded from the existing DeconvolutionParameters.ExpectedIsotopeSpacing at every construction site (Calibration / GPTMD / Search). Removes the carbon-only spacing assumption the reviewer flagged for RNA/decoy runs; defaults to C13MinusC12 so no existing caller or test changes. - Revert the unrelated GPTMD filter-serialization feature (GptmdFilterTypes / GetActiveFilters / CreateFilter / [TomlIgnore]) out of this PR to be resubmitted on its own branch. No property was renamed; the interface-typed GptmdFilters never round-tripped through Nett, so old tomls are unaffected. - Expand the PrecursorMassToMatch doc to explain why it stays separate from the monoisotopic PrecursorMass (fragment-bin math and mass-error reporting remain monoisotopic-vs-monoisotopic, uncoupled from the per-notch apex offset). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 05ae907 commit 659fd68

9 files changed

Lines changed: 55 additions & 108 deletions

File tree

MetaMorpheus/EngineLayer/Ms2ScanWithSpecificMass.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ public Ms2ScanWithSpecificMass(MsDataScan mzLibScan, double precursorMonoisotopi
5454
/// The precursor mass used to select theoretical candidates via the <see cref="EngineLayer.MassDiffAcceptor"/>.
5555
/// Equals <see cref="PrecursorMass"/> in monoisotopic mode; in most-abundant mode it is the
5656
/// envelope's most-abundant (or average, if unresolved) observed neutral mass.
57+
/// <para>
58+
/// This is deliberately a separate property rather than an overwrite of <see cref="PrecursorMass"/>:
59+
/// candidate selection is the only step that should key off the most-abundant peak. Fragment-bin
60+
/// math and the reported precursor mass error must stay on the monoisotopic mass so that (a) the
61+
/// error is the monoisotopic-vs-monoisotopic value comparable across search modes, and (b) it is
62+
/// not conflated with the ±k-neutron apex offset the acceptor already models per-notch. Collapsing
63+
/// the two would report error against a peak that is intentionally N neutrons off the monoisotopic.
64+
/// </para>
5765
/// </summary>
5866
public double PrecursorMassToMatch { get; }
5967
public int PrecursorCharge { get; }

MetaMorpheus/EngineLayer/PrecursorSearchModes/MostAbundantDotMassDiffAcceptor.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,25 @@ public class MostAbundantDotMassDiffAcceptor : MassDiffAcceptor
3333
public int MaxApexOffsetNeutrons { get; }
3434
private readonly int[] ApexOffsetsInNeutrons;
3535

36+
/// <summary>
37+
/// Mass spacing between adjacent isotopologues (the "+1 neutron" step) used to place the apex
38+
/// notches. Sourced from the deconvolution's
39+
/// <see cref="MassSpectrometry.DeconvolutionParameters.ExpectedIsotopeSpacing"/> so the acceptor
40+
/// matches the spacing the envelope was built with — the C-13/C-12 difference for peptides/
41+
/// proteoforms, but overridable for decoy runs (~0.9444 Da) or non-carbon-dominated polymers.
42+
/// </summary>
43+
private readonly double ExpectedIsotopeSpacing;
44+
3645
public MostAbundantDotMassDiffAcceptor(string fileNameAddition, IEnumerable<double> acceptableMassShifts,
37-
Tolerance tol, AverageResidue averagine, int maxApexOffsetNeutrons = 2) : base(fileNameAddition)
46+
Tolerance tol, AverageResidue averagine, int maxApexOffsetNeutrons = 2,
47+
double expectedIsotopeSpacing = Constants.C13MinusC12) : base(fileNameAddition)
3848
{
3949
Tolerance = tol;
4050
Averagine = averagine;
4151
SortedMassShifts = acceptableMassShifts.OrderBy(Math.Abs).ThenBy(p => p < 0).ToArray();
4252
ShiftNotches = SortedMassShifts.Select(s => (int)Math.Round(s * NotchScalar)).ToArray();
4353
MaxApexOffsetNeutrons = maxApexOffsetNeutrons;
54+
ExpectedIsotopeSpacing = expectedIsotopeSpacing;
4455

4556
var offsets = new List<int> { 0 };
4657
for (int k = 1; k <= maxApexOffsetNeutrons; k++) { offsets.Add(-k); offsets.Add(k); }
@@ -64,7 +75,7 @@ public override int Accepts(double scanPrecursorMass, double peptideMass)
6475
double apex = shiftedMono + ApexOffset(shiftedMono);
6576
foreach (int k in ApexOffsetsInNeutrons)
6677
{
67-
if (Tolerance.Within(scanPrecursorMass, apex + k * Constants.C13MinusC12))
78+
if (Tolerance.Within(scanPrecursorMass, apex + k * ExpectedIsotopeSpacing))
6879
{
6980
return ShiftNotches[j];
7081
}
@@ -81,7 +92,7 @@ public override IEnumerable<AllowedIntervalWithNotch> GetAllowedPrecursorMassInt
8192
double apex = shiftedMono + ApexOffset(shiftedMono);
8293
foreach (int k in ApexOffsetsInNeutrons)
8394
{
84-
double mass = apex + k * Constants.C13MinusC12;
95+
double mass = apex + k * ExpectedIsotopeSpacing;
8596
yield return new AllowedIntervalWithNotch(Tolerance.GetMinimumValue(mass), Tolerance.GetMaximumValue(mass), ShiftNotches[j]);
8697
}
8798
}
@@ -96,7 +107,7 @@ public override IEnumerable<AllowedIntervalWithNotch> GetAllowedPrecursorMassInt
96107
{
97108
foreach (int k in ApexOffsetsInNeutrons)
98109
{
99-
double mass = monoApprox - SortedMassShifts[j] - k * Constants.C13MinusC12;
110+
double mass = monoApprox - SortedMassShifts[j] - k * ExpectedIsotopeSpacing;
100111
yield return new AllowedIntervalWithNotch(Tolerance.GetMinimumValue(mass), Tolerance.GetMaximumValue(mass), ShiftNotches[j]);
101112
}
102113
}

MetaMorpheus/EngineLayer/PrecursorSearchModes/MostAbundantMassDiffAcceptor.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace EngineLayer
2323
/// yeast top-down data: a single tight-ppm point at the predicted apex under-identifies badly,
2424
/// because one neutron is ~67 ppm at 15 kDa). To tolerate that apex misprediction while keeping
2525
/// each match at tight ppm (and FDR controlled per-notch), this acceptor emits a small set of
26-
/// notches at <c>apex + k·C13</c> for k in [−<see cref="MaxApexOffsetNeutrons"/> ..
26+
/// notches at <c>apex + k·<see cref="ExpectedIsotopeSpacing"/></c> for k in [−<see cref="MaxApexOffsetNeutrons"/> ..
2727
/// +<see cref="MaxApexOffsetNeutrons"/>]. k = 0 is the confident on-apex match; nonzero k are the
2828
/// apex-misprediction cases. Each k maps to a contiguous non-negative notch index (its 0-based
2929
/// position in the apex-offset set), so notches are distinct per k, are always ≥ 0 (a negative
@@ -42,18 +42,29 @@ public class MostAbundantMassDiffAcceptor : MassDiffAcceptor
4242
private readonly Tolerance Tolerance;
4343
private readonly AverageResidue Averagine;
4444

45+
/// <summary>
46+
/// Mass spacing between adjacent isotopologues (the "+1 neutron" step) used to place the apex
47+
/// notches. Sourced from the deconvolution's
48+
/// <see cref="MassSpectrometry.DeconvolutionParameters.ExpectedIsotopeSpacing"/> so the acceptor
49+
/// matches the spacing the envelope was built with — the C-13/C-12 difference for peptides/
50+
/// proteoforms, but overridable for decoy runs (~0.9444 Da) or non-carbon-dominated polymers.
51+
/// </summary>
52+
private readonly double ExpectedIsotopeSpacing;
53+
4554
/// <summary>Maximum apex misprediction, in neutrons, tolerated on either side of the averagine-predicted apex.</summary>
4655
public int MaxApexOffsetNeutrons { get; }
4756

4857
// k values ordered by ascending |k| (then sign) so the on-apex (k = 0) match is preferred.
4958
private readonly int[] ApexOffsetsInNeutrons;
5059

51-
public MostAbundantMassDiffAcceptor(string fileNameAddition, Tolerance tol, AverageResidue averagine, int maxApexOffsetNeutrons = 2)
60+
public MostAbundantMassDiffAcceptor(string fileNameAddition, Tolerance tol, AverageResidue averagine, int maxApexOffsetNeutrons = 2,
61+
double expectedIsotopeSpacing = Constants.C13MinusC12)
5262
: base(fileNameAddition)
5363
{
5464
Tolerance = tol;
5565
Averagine = averagine;
5666
MaxApexOffsetNeutrons = maxApexOffsetNeutrons;
67+
ExpectedIsotopeSpacing = expectedIsotopeSpacing;
5768

5869
var offsets = new List<int> { 0 };
5970
for (int k = 1; k <= maxApexOffsetNeutrons; k++)
@@ -80,7 +91,7 @@ public override int Accepts(double scanPrecursorMass, double peptideMass)
8091
double apex = peptideMass + ApexOffset(peptideMass);
8192
foreach (int k in ApexOffsetsInNeutrons) // ordered to prefer k = 0
8293
{
83-
if (Tolerance.Within(scanPrecursorMass, apex + k * Constants.C13MinusC12))
94+
if (Tolerance.Within(scanPrecursorMass, apex + k * ExpectedIsotopeSpacing))
8495
{
8596
return NotchFor(k);
8697
}
@@ -93,7 +104,7 @@ public override IEnumerable<AllowedIntervalWithNotch> GetAllowedPrecursorMassInt
93104
double apex = peptideMonoisotopicMass + ApexOffset(peptideMonoisotopicMass);
94105
foreach (int k in ApexOffsetsInNeutrons)
95106
{
96-
double mass = apex + k * Constants.C13MinusC12;
107+
double mass = apex + k * ExpectedIsotopeSpacing;
97108
yield return new AllowedIntervalWithNotch(Tolerance.GetMinimumValue(mass), Tolerance.GetMaximumValue(mass), NotchFor(k));
98109
}
99110
}
@@ -108,7 +119,7 @@ public override IEnumerable<AllowedIntervalWithNotch> GetAllowedPrecursorMassInt
108119
double monoApprox = observedMostAbundantMass - ApexOffset(observedMostAbundantMass);
109120
foreach (int k in ApexOffsetsInNeutrons)
110121
{
111-
double mass = monoApprox - k * Constants.C13MinusC12;
122+
double mass = monoApprox - k * ExpectedIsotopeSpacing;
112123
yield return new AllowedIntervalWithNotch(Tolerance.GetMinimumValue(mass), Tolerance.GetMaximumValue(mass), NotchFor(k));
113124
}
114125
}

MetaMorpheus/GUI/TaskWindows/GPTMDTaskWindow.xaml.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,7 @@ private void UpdateFieldsFromTask(GptmdTask task)
212212

213213
foreach (var filter in FilterOptions)
214214
{
215-
filter.IsSelected = (TheTask.GptmdParameters.GptmdFilters?.Any(f => f.GetType() == filter.Filter.GetType()) ?? false)
216-
|| (TheTask.GptmdParameters.GptmdFilterTypes?.Contains(filter.Filter.GetType().Name) ?? false);
215+
filter.IsSelected = TheTask.GptmdParameters.GptmdFilters.Any(f => f.GetType() == filter.Filter.GetType());
217216
}
218217
}
219218

@@ -588,8 +587,6 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
588587
TheTask.GptmdParameters.ListOfModsGptmd.AddRange(heh.Children.Where(b => b.Use).Select(b => (b.Parent.DisplayName, b.ModName)));
589588
}
590589
TheTask.GptmdParameters.GptmdFilters = FilterOptions.Where(f => f.IsSelected).Select(f => f.Filter).ToList();
591-
// Also persist the selection in toml-serializable form so it survives a save/load round-trip and CMD runs.
592-
TheTask.GptmdParameters.GptmdFilterTypes = FilterOptions.Where(f => f.IsSelected).Select(f => f.Filter.GetType().Name).ToList();
593590
TheTask.GptmdParameters.WriteDecoys = WriteDecoysCheckBox.IsChecked.Value;
594591
TheTask.CommonParameters = commonParamsToSave;
595592

MetaMorpheus/TaskLayer/CalibrationTask/CalibrationTask.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ private DataPointAquisitionResults GetDataAcquisitionResults(MsDataFile myMsData
209209
&& combinedParameters.DoPrecursorDeconvolution)
210210
{
211211
searchMode = new MostAbundantMassDiffAcceptor("mostAbundant", combinedParameters.PrecursorMassTolerance,
212-
combinedParameters.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine());
212+
combinedParameters.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine(),
213+
expectedIsotopeSpacing: combinedParameters.PrecursorDeconvolutionParameters?.ExpectedIsotopeSpacing ?? Chemistry.Constants.C13MinusC12);
213214
}
214215
else
215216
{
Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
using System.Collections.Generic;
1+
using System.Collections.Generic;
22
using System.Linq;
33
using EngineLayer.Gptmd;
4-
using Nett;
54

65
namespace EngineLayer
76
{
@@ -13,60 +12,16 @@ public class GptmdParameters
1312
public GptmdParameters()
1413
{
1514
GptmdFilters = new();
16-
GptmdFilterTypes = new();
1715
ListOfModsGptmd = GlobalVariables.AllModsKnown.Where(b =>
1816
b.ModificationType.Equals("Common Artifact")
1917
|| b.ModificationType.Equals("Common Biological")
20-
|| b.ModificationType.Equals("Metal")
18+
|| b.ModificationType.Equals("Metal")
2119
//|| b.ModificationType.Equals("Less Common")
2220
).Select(b => (b.ModificationType, b.IdWithMotif)).ToList();
2321
}
2422

2523
public List<(string, string)> ListOfModsGptmd { get; set; }
26-
27-
/// <summary>
28-
/// Runtime list of G-PTM-D acceptance filters (e.g. <see cref="ImprovedScoreFilter"/>). This is a
29-
/// list of interface instances and cannot round-trip through the task toml; the toml-serializable
30-
/// representation is <see cref="GptmdFilterTypes"/>. The GUI sets this directly for in-memory runs.
31-
/// Use <see cref="GetActiveFilters"/> to obtain the effective set (this list plus any named in the toml).
32-
/// </summary>
33-
[TomlIgnore]
34-
public List<IGptmdFilter> GptmdFilters { get; set; }
35-
36-
/// <summary>
37-
/// Toml-serializable filter selection by type name (e.g. "ImprovedScoreFilter"). Lets headless/CMD
38-
/// runs enable G-PTM-D acceptance filters that previously could only be set in the GUI. A common
39-
/// choice is "ImprovedScoreFilter", which only adds a modification when it improves the match score
40-
/// (greatly reducing the number of modifications added).
41-
/// </summary>
42-
public List<string> GptmdFilterTypes { get; set; }
43-
24+
public List<IGptmdFilter> GptmdFilters { get; set; }
4425
public bool WriteDecoys { get; set; } = true;
45-
46-
/// <summary>
47-
/// The effective filter set: the in-memory <see cref="GptmdFilters"/> plus any filters named in
48-
/// <see cref="GptmdFilterTypes"/> (deduplicated by type). Unknown names are ignored.
49-
/// </summary>
50-
public List<IGptmdFilter> GetActiveFilters()
51-
{
52-
var active = new List<IGptmdFilter>(GptmdFilters ?? new List<IGptmdFilter>());
53-
foreach (var name in GptmdFilterTypes ?? new List<string>())
54-
{
55-
var filter = CreateFilter(name);
56-
if (filter != null && active.All(f => f.GetType() != filter.GetType()))
57-
active.Add(filter);
58-
}
59-
return active;
60-
}
61-
62-
/// <summary>Creates a filter instance from its type name; returns null for an unrecognized name.</summary>
63-
public static IGptmdFilter CreateFilter(string typeName) => typeName switch
64-
{
65-
nameof(ImprovedScoreFilter) => new ImprovedScoreFilter(),
66-
nameof(DualDirectionalIonCoverageFilter) => new DualDirectionalIonCoverageFilter(),
67-
nameof(UniDirectionalIonCoverageFilter) => new UniDirectionalIonCoverageFilter(),
68-
nameof(FlankingIonCoverageFilter) => new FlankingIonCoverageFilter(),
69-
_ => null
70-
};
7126
}
72-
}
27+
}

MetaMorpheus/TaskLayer/GPTMDTask/GPTMDTask.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List<DbForTask
8181
// temporary search type for writing prose
8282
// the actual search type is technically file-specific but we don't allow file-specific notches, so it's safe to do this
8383
MassDiffAcceptor tempSearchMode = CommonParameters.PrecursorMassMatchMode == PrecursorMassMatchMode.MostAbundant
84-
? new MostAbundantDotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), CommonParameters.PrecursorMassTolerance, CommonParameters.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine())
84+
? new MostAbundantDotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), CommonParameters.PrecursorMassTolerance, CommonParameters.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine(), expectedIsotopeSpacing: CommonParameters.PrecursorDeconvolutionParameters?.ExpectedIsotopeSpacing ?? Chemistry.Constants.C13MinusC12)
8585
: new DotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), CommonParameters.PrecursorMassTolerance);
8686
ProseCreatedWhileRunning.Append("precursor mass tolerance(s) = {" + tempSearchMode.ToProseString() + "}; ");
8787

@@ -116,7 +116,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List<DbForTask
116116

117117
CommonParameters combinedParams = SetAllFileSpecificCommonParams(CommonParameters, fileSettingsList[spectraFileIndex]);
118118
MassDiffAcceptor searchMode = combinedParams.PrecursorMassMatchMode == PrecursorMassMatchMode.MostAbundant
119-
? new MostAbundantDotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), combinedParams.PrecursorMassTolerance, combinedParams.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine())
119+
? new MostAbundantDotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), combinedParams.PrecursorMassTolerance, combinedParams.PrecursorDeconvolutionParameters?.AverageResidueModel ?? new Averagine(), expectedIsotopeSpacing: combinedParams.PrecursorDeconvolutionParameters?.ExpectedIsotopeSpacing ?? Chemistry.Constants.C13MinusC12)
120120
: new DotMassDiffAcceptor("", GetAcceptableMassShifts(fixedModifications, variableModifications, gptmdModifications, combos), combinedParams.PrecursorMassTolerance);
121121

122122
NewCollection(Path.GetFileName(origDataFile), new List<string> { taskId, "Individual Spectra Files", origDataFile });
@@ -177,7 +177,7 @@ protected override MyTaskResults RunSpecific(string OutputFolder, List<DbForTask
177177
// GPTMD doesn't work as well if you do FDR on a file-by-file basis. Presumably this is because it takes multiple files to get enough PSMs for all the different notches
178178
new FdrAnalysisEngine(allPsms, tempSearchMode.NumNotches, CommonParameters, this.FileSpecificParameters, new List<string> { taskId }, doPEP: false).Run();
179179
Dictionary<string, HashSet<Tuple<int, Modification>>> allModDictionary = new();
180-
new GptmdEngine(allPsms, gptmdModifications, combos, filePathToPrecursorMassTolerance, CommonParameters, this.FileSpecificParameters, new List<string> { taskId }, allModDictionary, GptmdParameters.GetActiveFilters()).Run();
180+
new GptmdEngine(allPsms, gptmdModifications, combos, filePathToPrecursorMassTolerance, CommonParameters, this.FileSpecificParameters, new List<string> { taskId }, allModDictionary, GptmdParameters.GptmdFilters).Run();
181181

182182
//Move this text after search because proteins don't get loaded until search begins.
183183
ProseCreatedWhileRunning.Append("The combined search database contained " + proteinList.Count(p => !p.IsDecoy) + $" non-decoy {GlobalVariables.AnalyteType.GetBioPolymerLabel().ToLower()} entries including " + proteinList.Count(p => p.IsContaminant) + " contaminant sequences. ");

0 commit comments

Comments
 (0)