Repository navigation
Conversation
…onour it
A glyco search identifies the backbone naked and only then makes up the precursor-mass
difference with glycans, arranging them to maximise matched fragment ions. Until now
AMINO-ACID MOTIF WAS THE ONLY THING SAYING WHERE A GLYCAN MAY GO -- no occupancy prior,
no known-glycosite handling, nothing from the enzyme. Confirmed by reading every
constraint in the tree; there is no prior art for this anywhere in the repo.
But a glycoprotease already proved something the spectrum is being asked to guess. If
OpeRATOR cut this peptide out, its first residue carried a glycan. That is not a
hypothesis to score, it is a consequence of the peptide existing at all.
LocalizationGraph gains obligated sites and enforces them inside the dynamic program,
not afterwards. The DAG's child boxes hold the modifications placed at or before each
site, so "this site is occupied" is exactly "the incoming edge increases the count" --
an edge predicate, needing no new data structure. Applied on both the cached and
uncached paths, plus the first column, which has no incoming edge.
Deliberately NOT done by pruning the candidate-site list, which would say the wrong
thing ("j is not a site" rather than "k is occupied") and would break GraphCheck's
count-feasibility test. And deliberately NOT by pre-placing a modification, which is
the trap here: GetPossibleModSites skips any residue that already carries one, so the
obligated site would VANISH from the graph -- the exact inverse of the intent.
GraphCheck learns the obligation too. A box with fewer glycans than the peptide has
obligated sites cannot fill them, and without screening it the terminal node becomes
unreachable and FinishGraph throws -- the case its own comment says never happens in a
search.
An obligation that is not among the candidate sites is IGNORED, not treated as
unsatisfiable. GetPossibleModSites drops already-modified residues, so a site can be
missing for reasons unrelated to the protease, and refusing the peptide there would
delete a correct answer. Under-constraining forgoes a refinement; over-constraining
forbids the truth.
Cost is nothing when it does not apply: HasObligatedSites short-circuits both loops,
and every ordinary protease leaves it false. When it does apply it strictly shrinks the
route set, and since the localization level (1/1b/2/3) is a function of how many routes
survive and the probabilities are re-summed over the same reduced set, it converts
Level 3 assignments into Level 2 or 1 with the probabilities staying correct.
The four-argument constructor is kept, so the five existing test call sites are
untouched. One production construction site changes.
REQUIRES AN UNRELEASED mzLib: DigestionProduct.GetCleavageObligatedSites, added in
mzLib smith-chem-wisc#1317 (commit cbb749c0). MetaMorpheus consumes mzLib as a package, currently
1.0.591, so this cannot build in CI until smith-chem-wisc#1317 and its parents merge and mzLib ships.
EngineLayer was compiled clean locally against that mzLib via temporary project
references; the MetaMorpheus TEST suite could not be run, because the glyco tests need
the glycan databases and spectra the mzLib package ships. The two new tests are
therefore unexecuted, and are written with relational assertions rather than exact
route counts for that reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…al outcome
Both surfaced on the first end-to-end run against PXD035775 (OpeRATOR on PSGL1,
Orbitrap Eclipse, HCD-pd-EThcD). Neither was caught by the unit tests, because both
need a box the obligation cannot satisfy and the test fixtures only used boxes it
could.
1. The obligation filter could strip a node of EVERY predecessor and the node was
stored anyway. NodeCheck and the valid chart used to guarantee at least one source
for any stored node; the obligation is the first filter that can remove the last
one. GetFirstPath then stepped back into it and called First() on an empty source
list -- "Sequence contains no elements", 12 scans in.
2. FinishGraph threw when the terminal node was unreachable, on the premise that it
could only mean the caller skipped GraphCheck. An obligation makes "no arrangement
puts a glycan where the enzyme proves one must be" an ORDINARY answer about a real
spectrum. GraphCheck cannot screen it out in advance either: comparing counts is not
enough, because an obligated site whose motif does not match the box's glycans is
unsatisfiable for a reason no count reveals. The graph now reports HasRoute and the
engine skips the box.
Skipping rather than scoring zero matters: a zero-scoring graph can still be the
best one when every box fails, and GetFirstPath would then walk a graph with no path.
Result on real data, same file, obligation off vs on:
target PSMs 177 -> 177
O-glyco PSMs 169 -> 169
Level 1 O-glyco PSMs 5 -> 29
localizations at residue 1 5/5 -> 33/33
Identical identifications, 5.8x more confident localizations, every one at residue 1 --
which is OpeRATOR's published rule. The constraint does not move a glycan, it makes the
assignment certain, which is the mechanism this was built on.
133 glyco tests still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…at digestion Found by running a real OpeRATOR file: with RespectCleavagePromotingModifications ON, the peptide index went from 2,864 entries to 0, and the task reported SUCCESS with zero PSMs. Nothing distinguishes that from a bad database or a bad spectra file. The cause is structural rather than a bug. A glyco search identifies the backbone naked and only afterwards makes up the precursor-mass difference with glycans, so there is no glycan at the moment digestion decides where to cut. Every site a glycoprotease has then looks infeasible, the site list empties, and nothing survives to be searched. The setting is not overridden, because silently ignoring what a user asked for is its own trap; it is explained instead, naming the protease and saying what to do. And what the requirement can do for this search it already does without the flag -- the localizer reads the peptide's protease provenance directly, so glycans are still pinned to the sites the enzyme proves they occupy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…poken for The obligation said WHERE a glycan must be. It can now say WHICH glycans belong there, which is the only place a composition-level rule can act in a glyco search: no glycan exists at digestion, so the composition has nothing to constrain until localization, where the real glycans are. OpeRATOR does not merely require a glycan at P1'. It requires at least core 1 and is blocked again by core 2, so a Tn antigen on that residue is not a worse arrangement -- it is an impossible one. A glycan IS a Modification, so each candidate goes straight through CleavageRequirement.IsSatisfiedBy. Glycan now carries its own MonosaccharideComposition, parsed from the same GetKindString shorthand it already produces, so the two vocabularies cannot drift. Deliberately permissive wherever the glycan cannot be identified -- an unknown id, an unloaded glycan list, a composition the database never recorded. An unidentifiable glycan is not evidence that a rule is broken, and refusing it would delete arrangements over missing metadata rather than over chemistry. A rule naming no composition behaves exactly as before. 133 glyco tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clean merge; no conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GetCleavageObligations summary was inserted between GraphCheck's doc comment and GraphCheck, so GraphCheck's docs attached to the wrong member and GetCleavageObligations carried two summaries. Move them back and document the obligatedSiteCount parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zation-obligation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
A glyco search identifies the peptide backbone naked, then makes up the precursor-mass difference with combinations of glycans and tries arrangements, scoring each by matched fragment ions.
Amino-acid motif is the only thing that says where a glycan may go. I checked every constraint in the glyco tree: motif (
ModificationLocalization.ModFits), a skip for residues that already carry a modification, a per-box motif count, and the spectral score — which ranks arrangements but never forbids one. There is no occupancy prior, no known-glycosite handling, and nothing from the enzyme.KnownSite,Obligated,SiteConstraintmatch nothing in the repo, and no open PR or issue attempts it.But when the enzyme is a glycoprotease, it has already proved part of what the spectrum is being asked to guess. If OpeRATOR cut this peptide out, its first residue carried a glycan. That is not a hypothesis to be scored — it is a consequence of the peptide existing.
What lands
LocalizationGraphgains obligated sites and enforces them inside the dynamic program, not afterwards.The DAG's child boxes hold the modifications placed at or before each site, so "this site is occupied" is exactly "the incoming edge increases the modification count" — an edge predicate, needing no new data structure. It is applied on the cached and uncached paths, and at the first column, which has no incoming edge.
GraphChecklearns it too: a box with fewer glycans than the peptide has obligated sites cannot fill them, and without screening it the terminal node becomes unreachable andFinishGraphthrows — precisely the case its own comment says never happens in a search.Two things deliberately not done
Not by pruning the candidate-site list. That expresses the wrong thing — deleting a site says "j is not a candidate", not "k is occupied" — and it breaks
GraphCheck'smodPos.Count >= NumberOfModsfeasibility test, wrongly rejecting multi-glycan boxes.Not by pre-placing a modification. This is the trap.
GetPossibleModSitesskips any residue that already carries a modification, so pre-placing one makes the obligated site vanish from the graph — the exact inverse of the intent.Relatedly, an obligation that is not among the candidate sites is ignored, not treated as unsatisfiable: a site can be missing for reasons unrelated to the protease, and refusing the peptide there would delete a correct answer. Under-constraining forgoes a refinement; over-constraining forbids the truth.
Cost, and what it buys
HasObligatedSitesshort-circuits both loops, and it is false for every ordinary protease, so a non-glyco or non-glycoprotease run pays nothing.When it does apply the constraint strictly shrinks the route set — including in
GetAllPaths_CalP, the exponential step. And because the localization level (1 / 1b / 2 / 3) is a function of how many routes survive, and the probabilities are re-summed over the same reduced set, constraining the routes is precisely what turns a Level 3 into a Level 2 or 1, with the probabilities staying correct.Blast radius
One production construction site (
GlycoSearchEngine.cs:735). The four-argumentLocalizationGraphconstructor is kept, so the five existing test call sites are untouched.Verification
Run against mzLib packed locally from smith-chem-wisc#1317 (
9.9.1191) and restored into this branch, which is what CI will do once smith-chem-wisc#1317 ships.OGlycoTest_ObligatedSiteConstrainsTheRoutesOGlycoTest_AnObligationNotAmongTheCandidateSitesIsIgnoredThe first test carries a vacuity guard — it asserts that the unconstrained graph admits a route leaving the obligated site empty — so its pass proves the constraint actually eliminated routes rather than finding nothing to do. Assertions are relational (fewer routes; every survivor occupies the obligated site) rather than exact route counts, so they do not re-encode the glycan database.
All five pre-existing
LocalizationGraphcall sites are in that 133 and unchanged.Counterpart
mzLib smith-chem-wisc#1317 adds
DigestionProduct.GetCleavageObligatedSites, the derivation this consumes. It is derived rather than stored on purpose: the peptide index is cached with a field-based, schema-rigid serializer whose reuse check carries no assembly version, so a new serialized field would silently corrupt indices users already have.🤖 Generated with Claude Code