Skip to content
Open

Fd #1042

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
17df34a
flash deconv infrastructure
trishorts Apr 8, 2026
3219ef0
development and msalign writer
trishorts Apr 8, 2026
16e138a
binning
trishorts Apr 8, 2026
a8b22f0
test edge cases
trishorts Apr 8, 2026
35d2111
meat on the bone
trishorts Apr 8, 2026
b5aae03
real data tests vs real flashdeconv
trishorts Apr 8, 2026
ef60c75
deduplication
trishorts Apr 8, 2026
40fc819
nuff for today
trishorts Apr 8, 2026
8d7cfe3
now with q-values
trishorts Apr 9, 2026
31bc460
pause here to create generic scorer
trishorts Apr 9, 2026
f6be318
add new protease subtilisin
trishorts May 11, 2026
70b9e4c
feat: add subtilisin|p protease and unit test
trishorts May 11, 2026
49557b1
Merge remote-tracking branch 'upstream/master'
trishorts May 13, 2026
ce24771
Accept newer-TopFD column names in Ms1Feature
trishorts May 13, 2026
3aa2fe0
Drops a 4-row fixture (Ms1Feature_TopFDvLatest_ms1.feature) captured
trishorts May 13, 2026
3d734cc
Promote consensus mass-tracing types out of Development
trishorts May 13, 2026
6102940
Add Ms1FeatureFile.FromMassFeatures factory
trishorts May 13, 2026
9bc26a2
Cover Ms1FeatureFile.FromMassFeatures with a round-trip test
trishorts May 13, 2026
f1de124
Add [Explicit] end-to-end driver from mzML to _ms1.feature
trishorts May 13, 2026
bc3f6d5
Parameterise consensus driver via TestCaseSource
trishorts May 13, 2026
e73c261
Normalise feature-file RT to minutes when loading
trishorts May 13, 2026
d593c3f
Merge upstream/master into consensus-to-ms1feature (resolve PR #1069 …
trishorts May 15, 2026
43d9ded
Merge branch 'master' into consensus-to-ms1feature
trishorts May 20, 2026
d2a9bbf
feat(decon): split resolvable co-grouped species in trace correction
trishorts May 20, 2026
883f7c9
test(decon): add TD/BU feature generators + per-feature decon scoring
trishorts May 20, 2026
0dc12ca
Merge remote-tracking branch 'upstream/master' into consensus-to-ms1f…
trishorts May 24, 2026
fbf9c14
fix(readers): preserve gapped charge sets on _ms1.feature round-trip
trishorts May 25, 2026
9ffdb85
refactor(consensus): use Constants.C13MinusC12 for isotope spacing
trishorts May 26, 2026
083d086
fix(readers): only lazy-load Results when the file exists
trishorts May 26, 2026
36bc80f
test(consensus): add XIC-vs-MassTrace grouping comparison harness
trishorts May 26, 2026
4c8e021
Merge upstream/master into FD; adapt to new DeconvolutionParameters c…
trishorts May 26, 2026
d6af51b
refactor(decon): rename FLASHDeconv* types to MetaFlashDecon*
trishorts May 26, 2026
da9033e
test(decon): align MetaFlashDecon real-data invariant with Qscore sem…
trishorts May 26, 2026
8369a16
Merge remote-tracking branch 'origin/consensus-to-ms1feature' into FD
trishorts May 26, 2026
b699dd9
feat(decon): pluggable IMassFeatureTracer + consensus-pipeline adapter
trishorts May 26, 2026
99ab51a
feat(decon): MetaFlashDecon native neutral-mass feature tracer (Phase…
trishorts May 26, 2026
c6be23c
feat(decon): whole-file _ms1.feature driver, RT in seconds (Phase 4)
trishorts May 26, 2026
50153f2
perf(decon): parallelize per-scan deconvolution in Ms1FeatureGenerator
trishorts May 26, 2026
a424d5e
feat(decon): align MetaFlashDecon candidate selection with FLASHDecon…
trishorts May 26, 2026
aedbbeb
feat(decon): faithful getNoisePeakPower_ noise model (rep charge, Qsc…
trishorts May 26, 2026
786b56d
fix(decon): differential-test + correct getNoisePeakPower_ noise model
trishorts May 27, 2026
31e3c7c
feat(decon): Plan B getCosine port, differential-tested vs OpenMS
trishorts May 27, 2026
203fad6
feat(decon): Plan B recruitAllPeaksInSpectrum port, differential-tested
trishorts May 27, 2026
446ea8f
feat(decon): Plan B per-charge info + SNR ports, differential-tested
trishorts May 27, 2026
905a441
feat(decon): Plan B chargeRange/chargeFit/monoMass ports, differentia…
trishorts May 27, 2026
69a58fc
feat(decon): Plan B averagine trim/normalize/counts, differential-tested
trishorts May 27, 2026
c1a532f
feat(decon): Plan B averagine model + updatePerChargeCos_, differenti…
trishorts May 27, 2026
e367e2e
feat(decon): Plan B getIsotopeCosineAndDetermineIsotopeIndex, differe…
trishorts May 27, 2026
d317aa5
feat(decon): Plan B current-OpenMS Qscore, differential-tested
trishorts May 27, 2026
f0aa22c
feat(decon): Plan B updateQscore orchestrator + removeChargeError, di…
trishorts May 27, 2026
5533973
feat(decon): Plan B wiring - faithful PeakGroup pipeline in MetaFlash…
trishorts May 27, 2026
ab46263
test(decon): per-spectrum MetaFlashDecon vs real FLASHDeconv (_ms1.ms…
trishorts May 27, 2026
67e11f8
feat(decon): faithful OpenMS candidate generation (recall 2%->84% vs …
trishorts May 27, 2026
fd39140
feat(decon): port getCandidatePeakGroups_ candidate filters + mono-ma…
trishorts May 27, 2026
da306ca
test(decon): candidate-gen differential snip -> port is faithful; div…
trishorts May 27, 2026
09e5570
fix(decon): faithful overlap-dedup window (tol*tol_div_factor*1.5 = 3…
trishorts May 27, 2026
3e6d33e
test(decon): dense-real-scan candidate-gen snip -> candidate gen fait…
trishorts May 27, 2026
ea0710b
test(decon): scoring-stage survivor counts -> over-production is in S…
trishorts May 27, 2026
c3d5189
test(decon): classify dense-scan extras vs real + removeChargeError i…
trishorts May 28, 2026
598e4c8
test(decon): averagine vs OpenMS (linked extract) shows averagine is …
trishorts May 28, 2026
b872937
test(decon): real OpenMS FLASHDeconv on scan 2301 = 17 masses vs ours…
trishorts May 28, 2026
36dad05
test(decon): ours-vs-OpenMS-truth on scan 2301 -> candidate gen FORMS…
trishorts May 28, 2026
064df65
fix(decon): 3 candidate-gen transcription bugs caught by instrumented…
trishorts May 28, 2026
696d078
fix(decon): use narrowed tol (ppm/tol_div_factor) in getCandidatePeak…
trishorts May 28, 2026
dc6d0b3
fix(decon): use narrowed tol (ppm/tol_div_factor) in scoring -> 229 -…
trishorts May 28, 2026
8af4e4d
fix(decon): drop MinIsotopicPeakCount=3 gate -> empty-only check (Ope…
trishorts May 28, 2026
30a954c
feat(decon): pre-loop getIsotopeCosineAndDetermineIsotopeIndex offset…
trishorts May 28, 2026
3badfcb
fix(decon): iso_da_distance = ISOTOPE_MASSDIFF_55K_U (1.002371), not …
trishorts May 28, 2026
7cdd35f
test(decon): extend DenseScan diagnostics — scored/afterCE dumps + pe…
trishorts May 28, 2026
9d2f821
test(decon): deep diagnostics narrow remaining 2 missed truths to ONE…
trishorts May 28, 2026
4dde094
test(decon): mono-mass convergence drift localised to 0.005 Da -> iso…
trishorts May 28, 2026
b17124d
test(decon): per-iter mono trace localises drift to candidate-gen (NO…
trishorts May 28, 2026
1ec5383
fix(decon): faithful OpenMS-style avgDelta from per-iso-bin actual ma…
trishorts May 28, 2026
548a242
Revert "fix(decon): faithful OpenMS-style avgDelta from per-iso-bin a…
trishorts May 28, 2026
9d04448
test(decon): env-gated FD_MITRACE_CS dump of mass_intensities at 2790…
trishorts May 28, 2026
ff93c10
Reapply "fix(decon): faithful OpenMS-style avgDelta from per-iso-bin …
trishorts May 28, 2026
d6c89c4
Revert "Reapply "fix(decon): faithful OpenMS-style avgDelta from per-…
trishorts May 28, 2026
6d93cbf
fix(decon): float-typed mono summation matches OpenMS -> 17/17 truths
trishorts May 29, 2026
72abe33
test(decon): default per-spectrum comparison to ~160 scans
trishorts May 29, 2026
09e343d
docs(decon): lock in OpenMS-fidelity invariants with warning comments
trishorts May 29, 2026
3e12348
feat(decon): faithful feature-level isotope-cosine filter in mass tracer
trishorts May 29, 2026
b6f5424
feat(decon): faithful MassTraceDetection port in native mass tracer
trishorts May 29, 2026
5777816
test(decon): verify CollapseToNeutralMassPeaks is faithful (1:1) on r…
trishorts May 29, 2026
0aa8148
test(decon): per-spectrum fidelity diagnostics vs flashdeconv_snip + …
trishorts May 29, 2026
ee69d38
test(decon): env-gated FD_CGTRACE_CS candidate-mono dump (8166 cand-g…
trishorts May 30, 2026
f4ced77
fix(decon): exact OpenMS avgDelta(50) for mass_bin_min -> recovers sp…
trishorts May 30, 2026
c27e3cf
fix(decon): use FLASHDeconv's real TOPP params, not class defaults / …
trishorts May 30, 2026
64677c5
fix(decon): set trace_termination_outliers to 20 (FLASHDeconv.cpp:570)
trishorts May 30, 2026
35e6eea
fix(decon): write one _ms1.feature row per feature (FLASHDeconv-faith…
trishorts May 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using MassSpectrometry;
using NUnit.Framework;
using UsefulProteomicsDatabases;
Expand Down Expand Up @@ -47,13 +47,15 @@ static StandardDeconvolutionTest()
List<DeconvolutionParameters> topDownDeconvolutionParametersToTest =
[
new ClassicDeconvolutionParameters(1, 60, 4, 3),
new IsoDecDeconvolutionParameters()
new IsoDecDeconvolutionParameters(),
new MetaFlashDeconParameters(minCharge: 1, maxCharge: 60)
];

List<DeconvolutionParameters> bottomUpDeconvolutionParametersToTest =
[
new ClassicDeconvolutionParameters(1, 12, 4, 3),
new IsoDecDeconvolutionParameters()
new IsoDecDeconvolutionParameters(),
new MetaFlashDeconParameters(minCharge: 1, maxCharge: 12)
];


Expand Down
1,090 changes: 1,090 additions & 0 deletions mzLib/MassSpectrometry/Deconvolution/Algorithms/MetaFlashDeconAlgorithm.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// MetaFlashDeconAveragine.cs
//
// Plan B: faithful port of OpenMS PrecalculatedAveragine's per-mass isotope-distribution
// post-processing (FLASHDeconvHelperStructs.cpp:36-106) — trim the isotope pattern to cover 99.99%
// of its power, L2-normalise it (so FLASHDeconvAlgorithm::getCosine, which divides only by |a|, is a
// true cosine), and record apex / left-count-from-apex / last index (used as recruitment scan bounds
// and the per-charge cosine target b).
//
// The RAW isotope distribution generator (OpenMS CoarseIsotopePatternGenerator vs our
// IsotopicDistribution) is the one differential-testing boundary — it is too entangled to extract
// into a standalone snip — so TrimAndNormalize takes the raw isotope-indexed intensities as input and
// is itself differential-tested (trim_cpp) with a fixed raw distribution.

using System;
using System.Collections.Concurrent;
using Chemistry; // Constants.C13MinusC12

namespace MassSpectrometry
{
internal sealed class MetaFlashDeconAveragine
{
// ── Model: OpenMS-style trimmed/normalised isotope distributions over the mass range ──
// Wraps an existing AverageResidue (our raw isotope generator) and applies TrimAndNormalize
// per averagine index, lazily + thread-safely (per-scan deconvolution runs in parallel).
private readonly AverageResidue _source;
private readonly double _isoDa;
private readonly ConcurrentDictionary<int, Entry> _cache = new();

private readonly record struct Entry(double[] B, int Apex, int LeftFromApex, int RightFromApex, double AverageMassDelta);

internal MetaFlashDeconAveragine(AverageResidue source, double isoDa)
{
_source = source;
_isoDa = isoDa;
}

// TEST SEAM ONLY — a constant averagine (mass-independent apex/left/right/avgDelta), so the
// candidate-generation logic can be differential-tested vs the C++ on IDENTICAL averagine
// inputs (isolating logic bugs from the isotope-generator boundary). b is unused by candidate
// generation, so it is empty here. ⚠ Never use this ctor in production — it returns the same
// envelope shape for every mass and would destroy scoring; it exists purely for the diff tests.
private readonly bool _isConstant;
private readonly Entry _constantEntry;
internal MetaFlashDeconAveragine(int apex, int left, int right, double avgDelta)
: this(null, Constants.C13MinusC12)
{
_isConstant = true;
_constantEntry = new Entry(Array.Empty<double>(), apex, left, right, avgDelta);
}

// Shared instances so the per-mass trim cache persists across the (per-scan, parallel)
// deconvolution calls — a fresh algorithm is created per spectrum, but the averagine model is
// immutable and reusable.
private static readonly ConcurrentDictionary<(AverageResidue, double), MetaFlashDeconAveragine> _instances = new();
internal static MetaFlashDeconAveragine For(AverageResidue source, double isoDa)
=> _instances.GetOrAdd((source, isoDa), k => new MetaFlashDeconAveragine(k.Item1, k.Item2));

private Entry GetEntry(double mass)
{
if (_isConstant) return _constantEntry;
int idx = _source.GetMostIntenseMassIndex(mass);
return _cache.GetOrAdd(idx, i =>
{
double[] masses = _source.GetAllTheoreticalMasses(i);
double[] intens = _source.GetAllTheoreticalIntensities(i);
// arrays are intensity-descending: [0] is the most-abundant peak.
double monoMass = masses[0] - _source.GetDiffToMonoisotopic(i);

int maxIso = 0;
for (int k = 0; k < masses.Length; k++)
{
int iso = (int)Math.Round((masses[k] - monoMass) / _isoDa, MidpointRounding.AwayFromZero);
if (iso > maxIso) maxIso = iso;
}
var raw = new double[maxIso + 1];
for (int k = 0; k < masses.Length; k++)
{
int iso = (int)Math.Round((masses[k] - monoMass) / _isoDa, MidpointRounding.AwayFromZero);
if (iso >= 0 && iso <= maxIso) raw[iso] += intens[k];
}
double[] b = TrimAndNormalize(raw, out int apex, out int left, out int right);
// average_mono_mass_difference_: intensity-weighted mean isotope offset (Da from mono).
double sw = 0, swi = 0;
for (int k = 0; k < b.Length; k++) { sw += b[k]; swi += b[k] * k; }
double avgDelta = sw > 0 ? swi / sw * _isoDa : 0;
return new Entry(b, apex, left, right, avgDelta);
});
}

/// <summary>Trimmed + L2-normalised isotope distribution (OpenMS <c>avg.get(mass)</c>).</summary>
internal double[] Get(double mass) => GetEntry(mass).B;
/// <summary>OpenMS <c>avg.getApexIndex(mass)</c>.</summary>
internal int GetApexIndex(double mass) => GetEntry(mass).Apex;
/// <summary>OpenMS <c>avg.getLeftCountFromApex(mass)</c>.</summary>
internal int GetLeftCountFromApex(double mass) => GetEntry(mass).LeftFromApex;
/// <summary>OpenMS <c>avg.getRightCountFromApex(mass)</c>.</summary>
internal int GetRightCountFromApex(double mass) => GetEntry(mass).RightFromApex;
/// <summary>OpenMS <c>avg.getLastIndex(mass)</c> = apex + right-count-from-apex.</summary>
internal int GetLastIndex(double mass) { var e = GetEntry(mass); return e.Apex + e.RightFromApex; }
/// <summary>OpenMS <c>avg.getAverageMassDelta(mass)</c> (intensity-weighted mean isotope offset, Da).</summary>
internal double GetAverageMassDelta(double mass) => GetEntry(mass).AverageMassDelta;

/// <summary>
/// OpenMS <c>PrecalculatedAveragine::getAverageMassDelta</c> at the FLASHDeconv minimum mass (50 Da),
/// from its <c>CoarseIsotopePatternGenerator</c> (FLASHDeconvHelperStructs.cpp:107). ⚠ This is the ONE
/// averagine value mzLib cannot reproduce locally: mzLib's <c>IsotopicDistribution</c> + its isotope
/// abundances give ~0.0251-0.0255 for the mass-50 averagine, vs OpenMS's coarse generator 0.02514582
/// — the un-snippable isotope-generator boundary. Verified against the OpenMS-linked
/// <c>avg_snip</c> / <c>FD_SCALARTRACE</c> (full-precision double). Used ONLY to set
/// <c>mass_bin_min</c>; an exact match removes the ~2-bin axis shift that mis-assigns sparse-scan
/// candidate charge ranges. See STATUS 2026-05-29.
/// </summary>
internal const double OpenMsAvgDeltaAtMass50 = 0.025145816488311823;

/// <summary>
/// avgDelta for the bin-axis origin (<c>mass_bin_min = log(min_mass - this)</c>). Returns the
/// exact-OpenMS value at the FLASHDeconv default min_mass (50); falls back to the model elsewhere.
/// (min_mass is 50 in every standard top-down run; a non-50 min_mass would need a faithful port of
/// CoarseIsotopePatternGenerator to be axis-exact.)
/// </summary>
internal double GetMassBinMinAvgDelta(double minMass)
=> Math.Abs(minMass - 50.0) < 1e-6 ? OpenMsAvgDeltaAtMass50 : GetAverageMassDelta(minMass);

/// <summary>
/// Faithful port of the per-mass loop body in OpenMS
/// <c>PrecalculatedAveragine::PrecalculatedAveragine</c> (FLASHDeconvHelperStructs.cpp:36-106).
/// Given the raw isotope-indexed intensities (isotope 0 = monoisotopic), greedily trim the
/// lower-intensity end peaks while keeping ≥99.99% of the summed power, drop trailing
/// near-zero peaks (<c>trimRight(1e-10)</c>), and L2-normalise by the trimmed power. Returns
/// the normalised distribution <c>b</c> and the apex / left-from-apex / right-from-apex counts
/// (each floored at 2), exactly as OpenMS stores them.
/// </summary>
internal static double[] TrimAndNormalize(
double[] rawIso, out int apexIndex, out int leftCountFromApex, out int rightCountFromApex)
{
const double minPwr = 0.9999;
const int minIsoLength = 2;
const int minLeftRightCount = 2;

var iso = (double[])rawIso.Clone();
double totalPwr = 0.0;
int mostAbundantIndex = 0;
double mostAbundantInt = 0.0;
for (int k = 0; k < iso.Length; k++)
{
totalPwr += iso[k] * iso[k];
if (mostAbundantInt >= iso[k]) continue; // first-max-wins on ties (OpenMS `>=` continue)
mostAbundantInt = iso[k];
mostAbundantIndex = k;
}

int leftCount = 0;
int rightCount = iso.Length - 1;
int trimCount = 0;
while (iso.Length - trimCount > minIsoLength && leftCount < rightCount)
{
double lint = iso[leftCount];
double rint = iso[rightCount];
double pwr;
bool trimLeft = true;
if (lint < rint) { pwr = lint * lint; }
else { pwr = rint * rint; trimLeft = false; }

if (totalPwr - pwr < totalPwr * minPwr) break;
totalPwr -= pwr;
trimCount++;
if (trimLeft) { iso[leftCount] = 0.0; leftCount++; }
else { iso[rightCount] = 0.0; rightCount--; }
}

leftCount = mostAbundantIndex - leftCount;
rightCount = rightCount - mostAbundantIndex;

// trimRight(1e-10): drop trailing near-zero entries (the right-trimmed ones).
int newLen = iso.Length;
while (newLen > 0 && iso[newLen - 1] <= 1e-10) newLen--;

// ⚠ L2 normalisation (divide by sqrt of summed power), NOT L1. OpenMS getCosine divides only
// by |a| and assumes the averagine target `b` is already unit-L2; an L1 (sum) norm here would
// silently bias every isotope-cosine. This whole function is differential-tested (trim_cpp).
double norm = Math.Sqrt(totalPwr);
var b = new double[newLen];
for (int k = 0; k < newLen; k++) b[k] = iso[k] / norm;

leftCount = Math.Max(leftCount, minLeftRightCount);
rightCount = Math.Max(rightCount, minLeftRightCount);

apexIndex = mostAbundantIndex;
leftCountFromApex = leftCount;
rightCountFromApex = rightCount;
return b;
}
}
}
Loading
Loading