Skip to content

Custom oxonium ions via the monosaccharide diagnostic-ion column#2665

Open
trishorts wants to merge 8 commits into
smith-chem-wisc:masterfrom
trishorts:customOxoniumIons
Open

Custom oxonium ions via the monosaccharide diagnostic-ion column#2665
trishorts wants to merge 8 commits into
smith-chem-wisc:masterfrom
trishorts:customOxoniumIons

Conversation

@trishorts

Copy link
Copy Markdown
Contributor

Custom oxonium ions via the monosaccharide diagnostic-ion column

Summary

Lets users turn the diagnostic ions they attach to a custom monosaccharide into a strict diagnostic filter during glyco search. The data source is the existing DiagnosticIonMasses column (column 4) of Glycan_Mods/MonosaccharidesCustom.tsv added in #2660 — there is no new file and no new registry. When the glyco Oxonium Ion Filter is enabled, each such ion must co-occur with its linked monosaccharide: an ion observed without its monosaccharide (or a monosaccharide present without its ion) rejects the candidate.

Default behavior is unchanged. The shipped TSV registers no custom monosaccharides, so the new code path is gated off and the combined oxonium-ion array is the built-in array itself.

Builds on #2660 (now merged). Supersedes #2664, which was closed pending the #2660 merge; this PR is the same work rebased cleanly onto master.

How to add a custom oxonium ion

  1. Open Glycan_Mods/MonosaccharidesCustom.tsv in the MetaMorpheus data directory.
  2. Add (or uncomment) a monosaccharide row and list one or more diagnostic ion m/z values in column 4 (DiagnosticIonMasses), comma-separated.
  3. Restart MetaMorpheus and run a glyco search with Oxonium Ion Filter enabled.

TSV format (column 4)

Column Name Notes
1 Monosaccharide name built-in or custom
2 Single-char code unique ASCII letter
3 Monoisotopic mass (Da) residue mass
4 DiagnosticIonMasses comma-separated observed m/z; these are the custom oxonium ions
5 Description free text

Worked example

Name	SingleCharCode	MonoisotopicMass	DiagnosticIonMasses	Description
HexA	U	176.03209	175.02482,157.01425	Hexuronic acid (GlcA, GalA)

With the oxonium filter on, a candidate glycopeptide is rejected if 175.02482 or 157.01425 is observed but the candidate has no HexA, or if it has HexA but neither ion is observed.

What the strict filter does

For each registered custom diagnostic ion, with the oxonium filter enabled:

  • ion observed and linked monosaccharide present → keep
  • ion observed but monosaccharide absent → reject
  • monosaccharide present but ion absent → reject
  • ion absent and monosaccharide absent → keep

"Observed" uses the same presence rule as the built-in 204 check (a peak matched within the product mass tolerance). The filter is gated by the existing Oxonium Ion Filter parameter; with it off, column-4 ions keep their prior scoring-only role.

What changed under the hood

  • GlycoSearch/OxoniumIonReservedIndices.cs (new): named constants for the six built-in oxonium-array indices the engine had hardcoded (4, 5, 9, 10, 12, 14).
  • GlycoSearch/GlycoSearchEngine.cs: the hardcoded indices now use those constants (pure rename).
  • GlycoSearch/Glycan.cs: HasCustomOxoniumIons, CustomOxoniumIons ((m/z, Kind-index) pairs), and AllOxoniumIonsIncludingCustoms (built-ins + appended customs; returns the built-in array unchanged when there are no customs).
  • GlycoSearch/GlycoPeptides.cs: ScanOxoniumIonFilter probes the combined ion list; DiagonsticFilter applies the strict rule after the unchanged built-in rules, gated on HasCustomOxoniumIons; helpers CheckOxoniumPresence and ApplyStrictMonoFilter.
  • Glycan_Mods/MonosaccharidesCustom.tsv: banner documents the column-4 ions' new strict-filter role.

Tests

  1. Ordering-freeze guard for the six reserved indices.
  2. Accessor behavior: empty default (combined array is the built-in array reference); registration appears in CustomOxoniumIons and the combined array.
  3. Helper units: ApplyStrictMonoFilter (four cases), CheckOxoniumPresence.
  4. Strict filter matrix: the four observed×present combinations, a two-custom single-mismatch reject, and a built-in-reject test proving the legacy path is intact with no customs.
  5. Defensive guards: undersized intensity/Kind inputs treated as absent without throwing.
  6. Round-trip: LoadCustomMonosaccharides column-4 ions appear in CustomOxoniumIons.
  7. Performance bounds ([Explicit], Category=Performance): negligible per-call overhead at zero customs; under 10 µs/call at 50 custom ions.

Compatibility / rollout

  • No new files, no schema change, no GUI change. Built-in AllOxoniumIons ordering is unchanged and guarded by a test.
  • With the shipped (no-customs) TSV, search output is byte-identical to the base branch.
  • Builds on Support user-defined custom monosaccharides via TSV #2660 (custom monosaccharides). A GUI surface for managing these ions can follow in a later PR.

🤖 Generated with Claude Code

trishorts and others added 6 commits May 26, 2026 08:04
Problem
-------
GlycoSearchEngine.cs and GlycoPeptides.DiagonsticFilter index
Glycan.AllOxoniumIons by bare integer literals (4, 5, 9, 10, 12, 14).
Each slot's meaning lives only in scattered comments, and reordering
AllOxoniumIons would silently break the diagnostic logic with no
compile-time or test-time signal.

Fix
---
- Add OxoniumIonReservedIndices with named int constants R138, R144,
  HexNAc204, NeuAc274, NeuAc292, HexHexNAc366 for the frozen slots.
- GlycoSearchEngine.cs: OxoniumIon204Index now forwards to
  OxoniumIonReservedIndices.HexNAc204; R138/R144 literals replaced in
  the 138/144 ratio computations (two sites).
- GlycoPeptides.DiagonsticFilter: [4]/[10]/[12]/[14] replaced with the
  named constants. Pure rename; filter logic unchanged.

Tests
-----
- Reserved_Indices_Match_AllOxoniumIons_Positions: asserts the exact
  scaled-int mass at each reserved index, failing loudly if anyone
  reorders or edits Glycan.AllOxoniumIons.

Files changed
-------------
  EngineLayer\GlycoSearch\OxoniumIonReservedIndices.cs
  EngineLayer\GlycoSearch\GlycoSearchEngine.cs
  EngineLayer\GlycoSearch\GlycoPeptides.cs
  Test\OxoniumIonReservedIndicesTests.cs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problem
-------
PR smith-chem-wisc#2660 lets a custom monosaccharide carry diagnostic-ion m/z values
(MonosaccharidesCustom.tsv column 4), but those ions only contribute to
DiagnosticIonScore -- they never gate a candidate. Users had no way to
require that a given oxonium ion co-occur with its linked monosaccharide,
which is the diagnostic-filter behavior wanted for the custom-oxonium feature.

Fix
---
Reuse the per-monosaccharide diagnostic ions already registered by
PR smith-chem-wisc#2660 (single source of truth; no second TSV or registry).
- Glycan.cs: add HasCustomOxoniumIons (cheap gate), CustomOxoniumIons
  (m/z-scaled + Kind-index pairs in deterministic order) and
  AllOxoniumIonsIncludingCustoms (built-ins + appended customs; returns
  the built-in array unchanged when no customs -> byte-identical default).
- GlycoPeptides.ScanOxoniumIonFilter: probe intensities over the combined
  list so custom ions get a slot after the built-ins.
- GlycoPeptides.DiagonsticFilter: after the unchanged built-in rules, when
  customs exist, iterate CustomOxoniumIons and reject on strict mismatch
  (ion observed XOR linked monosaccharide present). Gated on
  HasCustomOxoniumIons, so the default no-customs path is untouched.
- Helpers CheckOxoniumPresence (intensity > 0, mirrors the built-in 204
  check) and ApplyStrictMonoFilter (hasSignal == hasMono).

Tests
-----
- CustomOxoniumIons_DefaultNoCustoms_EmptyAndArrayIsByteIdentical: combined
  array is the same reference as AllOxoniumIons when no customs.
- RegisterCustomMonosaccharide_WithDiagnosticIons_AppearsInCustomOxoniumIonsAndCombinedArray.
- ApplyStrictMonoFilter_AcceptsOnlyWhenSignalMatchesMonoPresence (4 cases).
- CheckOxoniumPresence_PositiveIntensityIsPresentZeroIsAbsent.
- DiagonsticFilter_NoCustoms_BuiltInRejectStillApplies (legacy path intact).
- DiagonsticFilter_CustomIon Observed/Absent x Mono present/absent: the
  four strict accept/reject combinations.
- DiagonsticFilter_TwoCustoms_OneMismatch_Rejects.
- LoadCustomMonosaccharides_DiagnosticIonsColumn_RoundTripsIntoCustomOxoniumIons.

Files changed
-------------
  EngineLayer\GlycoSearch\Glycan.cs
  EngineLayer\GlycoSearch\GlycoPeptides.cs
  Test\CustomOxoniumFilterTests.cs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problem
-------
The strict custom-oxonium branch added to DiagonsticFilter should add no
meaningful cost when no customs are registered (the default) and should
scale acceptably with the number of custom ions. Nothing measured this.

Fix
---
Add two NUnit tests, both [Explicit] + [Category("Performance")] so they
are skipped by the normal suite (timing assertions are noisy) and run only
via the new Test\perf\run-perf.ps1 (filters on Category=Performance).
- Filter_NoCustoms_OverheadOverLegacyIsNegligible: times DiagonsticFilter
  at zero customs against a captured pre-Phase-3 baseline. Asserts the
  absolute per-call addition stays small; reports ns/call.
- Filter_FiftyCustomIons_PerCallUnderTenMicroseconds: registers 50 custom
  ions (5 monosaccharides x 10 ions), forces the worst case (all match so
  the loop runs fully), asserts mean < 10 us/call.

Measured on the dev box: ~9 ns/call added at zero customs (a single
Dictionary.Count check; ScanOxoniumIonFilter returns the built-in array
unchanged with no allocation), and ~1.36 us/call at 50 custom ions.

Tests
-----
- Filter_NoCustoms_OverheadOverLegacyIsNegligible
- Filter_FiftyCustomIons_PerCallUnderTenMicroseconds

Files changed
-------------
  Test\CustomOxoniumPerformanceTests.cs
  Test\perf\run-perf.ps1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problem
-------
The strict custom-oxonium loop in DiagonsticFilter has two defensive
guards -- the custom ion index may fall past the end of the intensity
array, and the custom monosaccharide index may fall past the end of
glycanBox.Kind. The happy-path tests always pass correctly sized inputs,
so the false branches of both guards were never exercised.

Fix
---
Add a test that registers a custom monosaccharide, then calls
DiagonsticFilter with an intensity array that has no slot for the custom
ion and a Kind array shorter than the custom monosaccharide's index. Both
guards must treat the entry as absent and the call must not throw.

Coverage of the patch after this test (local ReportGenerator, merged from
the integration run and the fast unit run): every added or changed line in
Glycan.cs, GlycoPeptides.cs, GlycoSearchEngine.cs and
OxoniumIonReservedIndices.cs is covered. Class line coverage: Glycan 98.8%,
GlycoSearchEngine 97.3%, GlycoPeptides 90.2% (remaining gaps are
pre-existing methods outside this patch).

Tests
-----
- DiagonsticFilter_UndersizedInputs_GuardsTreatAsAbsentAndDoNotThrow

Files changed
-------------
  Test\CustomOxoniumFilterTests.cs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problem
-------
The MonosaccharidesCustom.tsv banner described column 4 (DiagnosticIonMasses)
only as ions added to the diagnostic-ion SET (scoring). After the strict
custom-oxonium filter, those same ions also gate candidates when the oxonium
filter is on, but the banner did not say so.

Fix
---
Extend the column 4 documentation to state that the listed ions also act as
strict custom oxonium ions when the Oxonium Ion Filter is enabled (observed
without the monosaccharide, or monosaccharide present without the ion, rejects
the candidate), with the standard caveat that ion prevalence is acquisition
dependent and should be validated before use as a filter.

Tests
-----
- None (documentation/banner text only).

Files changed
-------------
  EngineLayer\Glycan_Mods\MonosaccharidesCustom.tsv

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c ions

Problem
-------
The custom-monosaccharide feature and the strict custom-oxonium filter are
configured through MonosaccharidesCustom.tsv, but there was no end-user
documentation explaining how to define a monosaccharide, reference it in a
glycan database, and attach diagnostic/oxonium ions (including how the strict
OxoniumIonFilt gate behaves).

Fix
---
Add docs/Custom-Monosaccharides-and-Diagnostic-Ions.md, a wiki-style guide:
file location and format, the built-in monosaccharide reference table, how to
define and then use a custom sugar in O-glycan (structure) and N-glycan
(composition) databases, the dual role of column-4 diagnostic ions (scoring vs
strict filter), an end-to-end worked example, the loader error messages, tips
and an FAQ.

Tests
-----
- None (documentation only).

Files changed
-------------
  docs\Custom-Monosaccharides-and-Diagnostic-Ions.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.61905% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.30%. Comparing base (3c8e0b8) to head (d6c347e).

Files with missing lines Patch % Lines
...pheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #2665   +/-   ##
=======================================
  Coverage   93.29%   93.30%           
=======================================
  Files         196      196           
  Lines       21320    21350   +30     
  Branches     3994     4003    +9     
=======================================
+ Hits        19891    19921   +30     
  Misses        893      893           
  Partials      536      536           
Flag Coverage Δ
unittests 93.30% <97.61%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
MetaMorpheus/EngineLayer/GlycoSearch/Glycan.cs 96.25% <100.00%> (+0.18%) ⬆️
...aMorpheus/EngineLayer/GlycoSearch/GlycoPeptides.cs 89.62% <100.00%> (+0.73%) ⬆️
...pheus/EngineLayer/GlycoSearch/GlycoSearchEngine.cs 97.56% <80.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants