Companion to Sigilweaver/OpenMassSpecCore#1 (the shared mzML writer already emits <chromatogramList> whenever SpectrumSource::iter_chromatograms yields anything, landed 2026-07-15) and precedent-setter Sigilweaver/OpenWRaw#9 (closed 2026-07-15, commit 574fe09), which did the equivalent wiring for Waters. OpenTFRaw currently has no iter_chromatograms override, so OpenTfRawSource (crates/opentfraw/src/mzml.rs:498-556) falls through to the trait's empty default and no TIC/BPC/SRM chromatograms reach mzML output.
Finding: this is a "wire up existing decoded data" issue, not new decode work. OpenTFRaw already fully decodes the per-scan chromatogram fields, and even has convenience methods sitting unused:
ScanIndexEntry (crates/opentfraw/src/scan_index.rs:7-20, parsed at :40-80) already carries start_time (RT, minutes), total_current (TIC), base_intensity (base peak intensity), and base_mz (base peak m/z) for every scan, decoded straight off disk.
RawFileReader already exposes retention_times() (reader.rs:1082-1084), tic_chromatogram() -> Vec<(f64, f64)> (reader.rs:1087-1092), and bpc_chromatogram() -> Vec<(f64, f64, f64)> (reader.rs:1095-1099), all built directly from the ScanIndexEntry fields above. None of these three methods are called anywhere outside reader.rs itself (no callers in mzml.rs, examples/, or opentfraw-py) - fully decoded, currently dead-ended.
- For SRM files (
flat_peaks mode, v63/v66), RawFileReader also already builds srm_q1_by_event: HashMap<u16, f64>, srm_q3_windows: HashMap<u16, Vec<(f32, f32)>>, and srm_ce_by_event: HashMap<u16, f64> (reader.rs:301-312, populated at :664-778), keyed by scan_event. These are already used to build scan-filter strings (reader.rs:1019-1076) and give everything needed to group scans into per-transition SRM chromatograms without any new binary parsing - just grouping/summing over already-decoded scans.
Proposed scope:
- Implement
OpenTfRawSource::iter_chromatograms (crates/opentfraw/src/mzml.rs) returning at minimum a TIC (chromatogram_type = "total ion current chromatogram") and a BPC (chromatogram_type = "basepeak chromatogram") built from tic_chromatogram()/bpc_chromatogram()/retention_times(), mapped into openmassspec_core::ChromatogramRecord (src/types.rs ~line 163). Note start_time is in minutes; ChromatogramRecord.time_sec needs * 60.0.
- Optionally (can be split into a follow-up if scope creeps): for
flat_peaks SRM files, group scans by scan_event using the existing srm_q1_by_event/srm_q3_windows maps and emit one chromatogram_type = "selected reaction monitoring chromatogram" record per transition, with precursor_mz/product_mz populated from the Q1/Q3 maps.
- This alone won't produce output until callers pick it up through
OpenMassSpecCore's mzML writer - it's already wired to call iter_chromatograms per OpenMassSpecCore#1, so no downstream change should be needed there.
Clean-room note: per CONTRIBUTING.md, correctness must be argued from open references only (PSI-MS mzML schema, roundtrip/self-consistency checks, independent open-source format checkers) - do not validate the new TIC/BPC/SRM output against vendor software (e.g. Xcalibur, msconvert) or any tool that reads .raw through the vendor SDK.
Companion to Sigilweaver/OpenMassSpecCore#1 (the shared mzML writer already emits
<chromatogramList>wheneverSpectrumSource::iter_chromatogramsyields anything, landed 2026-07-15) and precedent-setter Sigilweaver/OpenWRaw#9 (closed 2026-07-15, commit 574fe09), which did the equivalent wiring for Waters. OpenTFRaw currently has noiter_chromatogramsoverride, soOpenTfRawSource(crates/opentfraw/src/mzml.rs:498-556) falls through to the trait's empty default and no TIC/BPC/SRM chromatograms reach mzML output.Finding: this is a "wire up existing decoded data" issue, not new decode work. OpenTFRaw already fully decodes the per-scan chromatogram fields, and even has convenience methods sitting unused:
ScanIndexEntry(crates/opentfraw/src/scan_index.rs:7-20, parsed at :40-80) already carriesstart_time(RT, minutes),total_current(TIC),base_intensity(base peak intensity), andbase_mz(base peak m/z) for every scan, decoded straight off disk.RawFileReaderalready exposesretention_times()(reader.rs:1082-1084),tic_chromatogram() -> Vec<(f64, f64)>(reader.rs:1087-1092), andbpc_chromatogram() -> Vec<(f64, f64, f64)>(reader.rs:1095-1099), all built directly from theScanIndexEntryfields above. None of these three methods are called anywhere outsidereader.rsitself (no callers in mzml.rs, examples/, or opentfraw-py) - fully decoded, currently dead-ended.flat_peaksmode, v63/v66),RawFileReaderalso already buildssrm_q1_by_event: HashMap<u16, f64>,srm_q3_windows: HashMap<u16, Vec<(f32, f32)>>, andsrm_ce_by_event: HashMap<u16, f64>(reader.rs:301-312, populated at :664-778), keyed byscan_event. These are already used to build scan-filter strings (reader.rs:1019-1076) and give everything needed to group scans into per-transition SRM chromatograms without any new binary parsing - just grouping/summing over already-decoded scans.Proposed scope:
OpenTfRawSource::iter_chromatograms(crates/opentfraw/src/mzml.rs) returning at minimum a TIC (chromatogram_type= "total ion current chromatogram") and a BPC (chromatogram_type= "basepeak chromatogram") built fromtic_chromatogram()/bpc_chromatogram()/retention_times(), mapped intoopenmassspec_core::ChromatogramRecord(src/types.rs ~line 163). Notestart_timeis in minutes;ChromatogramRecord.time_secneeds* 60.0.flat_peaksSRM files, group scans byscan_eventusing the existingsrm_q1_by_event/srm_q3_windowsmaps and emit onechromatogram_type= "selected reaction monitoring chromatogram" record per transition, withprecursor_mz/product_mzpopulated from the Q1/Q3 maps.OpenMassSpecCore's mzML writer - it's already wired to calliter_chromatogramsper OpenMassSpecCore#1, so no downstream change should be needed there.Clean-room note: per CONTRIBUTING.md, correctness must be argued from open references only (PSI-MS mzML schema, roundtrip/self-consistency checks, independent open-source format checkers) - do not validate the new TIC/BPC/SRM output against vendor software (e.g. Xcalibur, msconvert) or any tool that reads
.rawthrough the vendor SDK.