Summary
openmassspec-core's SpectrumSource::iter_chromatograms has a default empty-iterator impl, so OpenTimsTDF's mzML output has no <chromatogramList> (the writer only emits it when a source yields something - openmassspec-core src/mzml.rs, landed in OpenMassSpecCore#1). OpenWRaw#9 wired an existing decoded-but-unused chroms.rs into iter_chromatograms for the same reason and is a good model for scope/format here.
TdfSource / OwnedTdfSource in crates/opentimstdf/src/mzml.rs (impls at lines 572 and 581) currently only implement iter_spectra. This issue is about implementing iter_chromatograms against openmassspec_core::types::ChromatogramRecord.
What's already decoded and unused (TIC - small)
Frame (crates/opentimstdf/src/types.rs lines 1-21) already carries:
pub time: f64, // retention time, seconds
pub summed_intensities: Option<u64>, // normalized per-frame intensity sum
Reader::frames() (crates/opentimstdf/src/reader.rs lines 248-258) already runs:
SELECT Id, Time, NumScans, NumPeaks, TimsId, ScanMode, MsMsType,
MzCalibration, AccumulationTime, SummedIntensities
FROM Frames ORDER BY Id ASC
and frame_from_row (crates/opentimstdf/src/codec.rs line 19) maps SummedIntensities into Frame.summed_intensities. Per docs/docs/format/01-tdf-sqlite-schema.md line 53, this column is sum(raw_intensity) * 100.0 / AccumulationTime_ms, verified against the decoded payload sum on PXD022216 (ratio 1.08460 +/- 0.00001 across 57,886 frames) - so it's a trustworthy, already-decoded TIC value per frame.
Searching the crate, summed_intensities is read nowhere except a test-mock Frame in mzml.rs (line 632, sets it to None). So (frame.time, frame.summed_intensities) across reader.frames() is a fully decoded TIC trace sitting unused - wiring this into one ChromatogramRecord (chromatogram_type = "total ion current chromatogram", time_sec/intensity built from existing Frame fields) is small, no new SQL or binary decode needed.
What needs a small net-new decode (BPC)
Frames.MaxIntensity is documented in the schema (docs/docs/format/01-tdf-sqlite-schema.md line 34, listed in the observed-columns block) but is not in either SELECT in reader.rs (lines 238-239 and 251-252) and there's no field for it on Frame in types.rs. Building a basepeak chromatogram needs: add MaxIntensity to both queries, add a max_intensity: Option<u64> field to Frame, map it in frame_from_row. Small, additive, no clean-room risk since the column and its role are already documented from public schema analysis.
What needs real net-new work (SRM/PRM traces)
PrmTargets and PrmFrameMsMsInfo already have per-frame/per-target lookups (Reader::prm_msms_info_for_frame, Reader::prm_target, reader.rs lines 357 and 395), but nothing aggregates a target's isolation-window intensity across frames into a time series. Producing an SRM-shaped ChromatogramRecord (chromatogram_type = "selected reaction monitoring chromatogram", precursor_mz/product_mz populated) per PRM target would mean, for each PrmTarget, walking msms_type == 10 frames, decoding the peak payload (reusing the existing per-frame decode path used for spectra) restricted to the target's scan-number range, and summing intensity per frame. That's genuinely new aggregation logic, not just wiring - larger than the TIC/BPC pieces. Related but distinct from #13 (PRM-PASEF frames decoded but skipped in the mzML spectrum projection): that issue is about spectra, this is about the chromatogram trace: fixing #13 does not fix this, and vice versa.
Suggested scope
Given the size difference, this issue is written broadly but a PR need not do all three at once - TIC is the cheapest, self-contained win (existing data, one new ChromatogramRecord), BPC is a small additive follow-on, SRM is optional/larger and could be split out if preferred.
Clean-room note
Per CONTRIBUTING.md, correctness here should be argued from self-consistency (e.g. TIC values matching Frames.SummedIntensities, roundtrip invariants, the PSI-MS mzML schema) - not by comparing output against vendor software or msconvert.
Summary
openmassspec-core'sSpectrumSource::iter_chromatogramshas a default empty-iterator impl, so OpenTimsTDF's mzML output has no<chromatogramList>(the writer only emits it when a source yields something -openmassspec-coresrc/mzml.rs, landed in OpenMassSpecCore#1). OpenWRaw#9 wired an existing decoded-but-unusedchroms.rsintoiter_chromatogramsfor the same reason and is a good model for scope/format here.TdfSource/OwnedTdfSourceincrates/opentimstdf/src/mzml.rs(impls at lines 572 and 581) currently only implementiter_spectra. This issue is about implementingiter_chromatogramsagainstopenmassspec_core::types::ChromatogramRecord.What's already decoded and unused (TIC - small)
Frame(crates/opentimstdf/src/types.rslines 1-21) already carries:Reader::frames()(crates/opentimstdf/src/reader.rslines 248-258) already runs:and
frame_from_row(crates/opentimstdf/src/codec.rsline 19) mapsSummedIntensitiesintoFrame.summed_intensities. Perdocs/docs/format/01-tdf-sqlite-schema.mdline 53, this column issum(raw_intensity) * 100.0 / AccumulationTime_ms, verified against the decoded payload sum on PXD022216 (ratio 1.08460 +/- 0.00001 across 57,886 frames) - so it's a trustworthy, already-decoded TIC value per frame.Searching the crate,
summed_intensitiesis read nowhere except a test-mockFrameinmzml.rs(line 632, sets it toNone). So(frame.time, frame.summed_intensities)acrossreader.frames()is a fully decoded TIC trace sitting unused - wiring this into oneChromatogramRecord(chromatogram_type= "total ion current chromatogram",time_sec/intensitybuilt from existingFramefields) is small, no new SQL or binary decode needed.What needs a small net-new decode (BPC)
Frames.MaxIntensityis documented in the schema (docs/docs/format/01-tdf-sqlite-schema.mdline 34, listed in the observed-columns block) but is not in eitherSELECTinreader.rs(lines 238-239 and 251-252) and there's no field for it onFrameintypes.rs. Building a basepeak chromatogram needs: addMaxIntensityto both queries, add amax_intensity: Option<u64>field toFrame, map it inframe_from_row. Small, additive, no clean-room risk since the column and its role are already documented from public schema analysis.What needs real net-new work (SRM/PRM traces)
PrmTargetsandPrmFrameMsMsInfoalready have per-frame/per-target lookups (Reader::prm_msms_info_for_frame,Reader::prm_target,reader.rslines 357 and 395), but nothing aggregates a target's isolation-window intensity across frames into a time series. Producing an SRM-shapedChromatogramRecord(chromatogram_type= "selected reaction monitoring chromatogram",precursor_mz/product_mzpopulated) per PRM target would mean, for eachPrmTarget, walkingmsms_type == 10frames, decoding the peak payload (reusing the existing per-frame decode path used for spectra) restricted to the target's scan-number range, and summing intensity per frame. That's genuinely new aggregation logic, not just wiring - larger than the TIC/BPC pieces. Related but distinct from #13 (PRM-PASEF frames decoded but skipped in the mzML spectrum projection): that issue is about spectra, this is about the chromatogram trace: fixing #13 does not fix this, and vice versa.Suggested scope
Given the size difference, this issue is written broadly but a PR need not do all three at once - TIC is the cheapest, self-contained win (existing data, one new
ChromatogramRecord), BPC is a small additive follow-on, SRM is optional/larger and could be split out if preferred.Clean-room note
Per CONTRIBUTING.md, correctness here should be argued from self-consistency (e.g. TIC values matching
Frames.SummedIntensities, roundtrip invariants, the PSI-MS mzML schema) - not by comparing output against vendor software or msconvert.