Export every annotation candidate, and say "A or B" in mzTab-M - #793
Merged
Conversation
MS-DIAL keeps up to NUMBER_OF_ANNOTATION_RESULTS threshold-passing results per annotator, and alignment carries all of them from the representative peak into the spot. Every text export then published only the representative, so a search that could not choose between two references whose spectra are indistinguishable came out looking like a search that had chosen. A product-ion spectrum reports structure indirectly, so that situation is the common case, not the corner case. On the FastLC demo, of 1919 annotated alignment spots only 335 have a single candidate: 115 have two and 1469 have three. Four fifths of the annotations were published as unambiguous when the run had not established that. AlignResult-*.mdcandidate.tsv reports one row per spot and candidate, ranked by the same precedence that picks the representative, so rank 1 is the representative in every row of that file. It carries the annotator, the library entry, the resolved reference fields and the full score block, which is what a downstream record needs in order to state an annotation claim and the evidence behind it. The export is off by default and requested with "Annotation candidates: True" in a Console parameter file. Conventions follow the .mdprovenance.tsv sidecar rather than the .mdalign columns, because the consumer is the same audit pipeline: an empty cell wherever the run established nothing, and round-trip precision rather than display rounding. A spectral score is written only when a comparison actually happened, and that condition now lives in one place, AnnotationScoreFormat.IsComputed, which the .mdalign and .mdpeak columns already used. The five non-spectral similarity terms have no equivalent of the -1 sentinel, so an unused term is stored as 0 and cannot be told apart from a term scored 0; the file says so rather than implying a measurement. The annotator identifier is an absolute library path in a Console run, which is what .mdalign already records in its Comment column and what the parameter file records. It is kept verbatim because it is the join key back to those artifacts; database_id beside it carries the short database name for anything that should not repeat a local path. Six tests, asserting whole lines rather than substrings: the ranked order, that a reference match outranks a higher-priority suggestion, that an uncompared spectrum publishes no score, that an unannotated spot and a "set unknown" assignment contribute no row, and that an unresolvable reference leaves the reference columns empty rather than invented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mzTab-M already has a way to report that a search did not choose: evidence rows that share an evidence_input_id came from the same input spectrum, rank orders them, and the feature row that references them carries ambiguity code 1. The exporter wrote one evidence row per feature, hardcoded rank to 1 and the ambiguity code to null, so a file could not say what MS-DIAL had actually found. On the FastLC demo the evidence section goes from 848 rows to 2228, and 714 of the 848 features that carry an identification turn out to have alternatives the search kept and the file used to drop. One of them, alignment feature 108, matches three separate RIKEN N-VS1 records at the same nominal mass; the third is the one a different run file independently chose, which the old file could not have shown. Evidence identifiers are now assigned in a layout pass before the feature section is written. The reference set on a feature row therefore comes from the same table as the rows themselves and cannot outlive a row that was never written; the two conditions used to be spelled out separately and did not agree, the feature side omitting the MS/MS-assigned, blank-filtered and internal-standard tests. The identifiers are file-unique and consecutive within one input, which the alignment identifier they used to reuse was not: an ion-mobility drift spot is written with its parent's metadata dictionary and so reported the parent's number. For the same reason evidence_input_id now comes from the spot the row describes. A drift row and its parent are different input spectra and must not be grouped as alternatives for one another. Each row's database identifier, name, formula, SMILES, theoretical m/z and score block are resolved from its own candidate. theoretical_mass_to_charge was read from the representative's metadata dictionary under a key that only the LC and IM accessors provide, so a GC-MS file reported the number 0 for it; it is now resolved through the same reference lookup for every row, and an unresolvable reference is the null token rather than a mass of zero. spectra_ref keeps its existing rule, the member files whose own top annotation is this library entry, applied to the row's own candidate: 79 of the 1380 lower-ranked rows have one, and the rest report null because no file independently chose them. Each evidence row is now written as a line. The previous form wrote the row without a terminator and let the caller close it, which appended a trailing separator to every one of them: the built-in mzTab-M structural check reports "SME column count 27 differs from its header count 26" on all 848 rows of a current master run and reports nothing on this one. With more than one candidate per feature that form would also have run them together on a single physical line. Measured against master at 6b8e3e9, same demo and method file: the 14 per-file .mdpeak and .mdmsp, the .mdalign, the alignment .mdmsp, the .mdpeakid.tsv and the .qa.tsv are byte-identical. Inside the mzTab the metadata section differs only in the run-identifier line, which is a timestamp, and the summary section is byte-identical. Only two feature-row fields change, the evidence reference list and the ambiguity code, and nothing else on those rows moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
MS-DIAL keeps up to
NUMBER_OF_ANNOTATION_RESULTSthreshold-passing results per annotator, and alignment carries all of them from the representative peak into the spot. Every export then published only the representative. A product-ion spectrum reports structure indirectly, so a search that cannot separate two references is the common case rather than the corner case, and dropping the alternatives at the file boundary turns "the search did not choose" into "the search chose".On the FastLC demo, of the 1919 annotated alignment spots only 335 have a single candidate: 115 have two and 1469 have three. Four fifths of the annotations were published as unambiguous when the run had not established that.
This adds the two exports that let a file say otherwise: a per-candidate audit sidecar, and the mzTab-M mechanism that already exists for exactly this.
AlignResult-*.mdcandidate.tsvOne row per spot and candidate, ranked by the same precedence that picks the representative, so rank 1 is the representative in every row of the file. 36 columns: the annotator and database, the library entry, the resolved reference fields, and the full score block. That is what a downstream record needs in order to state an annotation claim and the evidence behind it.
Off by default, requested with
Annotation candidates: Truein a Console parameter file, alongside the existingDetailed alignment provenance.Conventions follow the
.mdprovenance.tsvsidecar rather than the.mdaligncolumns, because the consumer is the same audit pipeline: an empty cell wherever the run established nothing, and round-trip precision instead of display rounding. A score that reads0.977in.mdalignreads0.977049112here; same measurement.Three things the file is careful about:
AnnotationScoreFormat.IsComputed, which the.mdalignand.mdpeakcolumns already went through. On the demo, 2744 of the 4972 rows are precursor-only suggestions and carry five empty score cells rather than five zeros.-1sentinel. An unused term is stored as0and cannot be told apart from a term evaluated as0. The file says so in a comment rather than implying a measurement it cannot support..mdalign's Comment column and to the parameter file, which both already record it.database_idbeside it carries the short database name (LbmDB) for anything that should not repeat a local path.mzTab-M:
SME_ID_REF_ambiguity_code, real ranks, one row per candidatemzTab-M already has a way to report that a search did not choose: evidence rows sharing an
evidence_input_idcame from the same input spectrum,rankorders them, and the feature row that references them carries ambiguity code 1. The exporter wrote one evidence row per feature, hardcodedrankto"1"and the ambiguity code to"null".On the demo the evidence section goes from 848 rows to 2228, and 714 of the 848 identified features have alternatives the search kept and the file used to drop. Feature 108 is a good example: three separate RIKEN N-VS1 records at the same nominal mass, and the third is the one a different run file independently chose, which the old file had no way to show.
Identifiers are laid out before the sections are written
The feature section is written before the evidence section but has to reference it, so a layout pass assigns the evidence identifiers first. Two things follow.
A reference cannot outlive a row that was never written. The two conditions used to be spelled out separately and did not agree: the feature side omitted the MS/MS-assigned, blank-filtered and internal-standard tests, so a feature could reference an evidence row the evidence loop had skipped. The demo does not happen to exercise the difference; sourcing both from one table removes the possibility, and a test asserts the two sets are equal for every row of the file.
The identifiers are file-unique. They used to be the alignment identifier, which is not unique across an ion-mobility run: a drift spot is written with its parent's metadata dictionary and so reported the parent's number. For the same reason
evidence_input_idnow comes from the spot the row describes — a drift row and its parent are different input spectra and must not be grouped as alternatives for one another.Each row is resolved from its own candidate
Database identifier, name, formula, SMILES, theoretical m/z and the score block. Two notes:
theoretical_mass_to_chargewas read from the representative's metadata dictionary under a key only the LC and IM accessors provide, so a GC-MS file reported the number0for it. It now goes through the same reference lookup for every row, and an unresolvable reference is the null token rather than a mass of zero. This is visible in the updated golden file, where0becomes301.03000.spectra_refkeeps its existing rule — the member files whose own top annotation is this library entry — applied to the row's own candidate. 79 of the 1380 lower-ranked rows have one; the rest report null because no file independently chose them. That the alternatives were scored against the same query spectrum is already stated byevidence_input_id.A trailing separator on every evidence row, removed
The row used to be written without a terminator, the caller closing it afterwards, which appended a trailing tab to every one. The built-in mzTab-M structural check reports
on a current
masterrun of this demo, and reports nothing on this branch. With more than one candidate per feature that form would also have run them together on a single physical line.Verification
Measured against
masterat6b8e3e9d3, same demo and method file:.mdpeak, 7 per-file.mdmsp.mdalign, alignment.mdmsp,.mdpeakid.tsv,.qa.tsv.mzTabMTD sectionmzTab-IDline, which is the run timestamp.mzTabSML section.mzTabSMF section.mzTabSME sectionmzTab-M validation:
passedon this branch,warningonmaster.Alignment-light mode produces the same 4972 candidate rows and the same 2228 evidence rows, and its mzTab validates. It diverges from regular mode on 6 of 4972 candidate rows, which are the same 4 spots on which the
.mdalignfiles of the two modes already diverge onmaster— a pre-existing representative-selection difference, not something this introduces.Release,net48, 0 errors, includingMsdialGuiApp.MsdialCoreTests: 325/325 (316 before, plus six for the candidate exporter and three for the mzTab evidence section).MsdialCoreTestAppTests: 14/14, including the new config key.The new tests assert whole lines, not substrings, for the reason the provenance exporter tests do: a substring assertion cannot see a ragged field count, and this change adds another hand-maintained column list.
Deliberately not in this PR
.mdalignand.mdpeak. Those are widely parsed fixed-column formats; the sidecar carries the alternatives without moving anything in them.🤖 Generated with Claude Code