Skip to content

feat: implement iter_chromatograms (TIC + BPC) for mzML export#26

Open
Nabejo wants to merge 1 commit into
Sigilweaver:mainfrom
Nabejo:iter-chromatograms-opentimstdf
Open

feat: implement iter_chromatograms (TIC + BPC) for mzML export#26
Nabejo wants to merge 1 commit into
Sigilweaver:mainfrom
Nabejo:iter-chromatograms-opentimstdf

Conversation

@Nabejo

@Nabejo Nabejo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #25.

Summary

TdfSource / OwnedTdfSource previously used SpectrumSource::iter_chromatograms's default empty implementation, so OpenTimsTDF's mzML output had no <chromatogramList> (the openmassspec-core writer only emits it when the source yields something). This wires up the two chromatogram traces that can be built from data Reader::frames already reads, following the scope/format of the referenced OpenWRaw#9 precedent (map existing decoded data into openmassspec_core::ChromatogramRecord, omit channels that can't be confidently mapped rather than guessing).

Both traces are whole-run, one point per frame in acquisition order, in seconds. No extra SQL beyond a single added column, no peak decode.

TIC (decoded-but-unused data)

  • "total ion current chromatogram" (MS:1000235) from Frame::summed_intensities (Frames.SummedIntensities), the accumulation-normalized per-frame intensity sum documented in docs/docs/format/01-tdf-sqlite-schema.md and already cross-checked against the decoded peak sum by tests/roundtrip.rs. This column was previously decoded but read nowhere outside a test mock.

BPC (small additive decode)

  • "basepeak chromatogram" (MS:1000628) from Frames.MaxIntensity. This adds MaxIntensity to both Frames SELECTs, a new Frame::max_intensity field, and its mapping in frame_from_row. The column is already documented in the public schema's observed-columns block; a detailed table row for it was added here.

A trace whose source column is absent on every frame is omitted rather than emitted empty, matching the writer's contract of only emitting a <chromatogramList> when the source yields something.

SRM/PRM: not included (suggested follow-up)

SRM/PRM transition chromatograms are intentionally left out. As the issue notes, producing an SRM-shaped ChromatogramRecord per PrmTarget means walking msms_type == 10 frames, decoding the peak payload restricted to each target's scan range, and summing intensity per frame - genuine per-target cross-frame aggregation, materially larger than wiring existing columns. It would fit well as its own follow-up issue/PR. This is distinct from #13 (PRM-PASEF spectra skipped in the spectrum projection): that is about spectra, this would be about the chromatogram trace; neither fixes the other.

Also

  • Exposed max_intensity on the Python Frame (kept symmetric with summed_intensities), and documented it in docs/docs/reference/python-api.md.

Testing

  • cargo fmt --all -- --check: clean.
  • cargo clippy --all-targets -- -D warnings: clean.
  • cargo test --all: all pass, including 4 new unit tests in mzml.rs covering TIC+BPC construction, seconds-preserving retention time, skip-frames-missing-a-column (arrays stay parallel), omit-empty-trace, and all-absent -> no records. The real-.d-bundle roundtrip/conformance tests are cache-gated and skip when the PRIDE cache is absent; correctness of the added MaxIntensity SELECT is otherwise argued from the public schema.

Clean-room

Per CONTRIBUTING.md: correctness is argued from the schema doc, the existing roundtrip/self-consistency invariants, and the PSI-MS accessions the openmassspec-core writer expects. No vendor software was used.

Contributed by @Nabejo.

Wire SpectrumSource::iter_chromatograms on TdfSource / OwnedTdfSource,
which previously fell back to the trait's default empty iterator, so
OpenTimsTDF's mzML output now carries a <chromatogramList>.

Two whole-run traces are emitted, one point per frame in acquisition
order, built purely from columns Reader::frames already reads (no extra
SQL, no peak decode):

- TIC ("total ion current chromatogram", MS:1000235) from
  Frames.SummedIntensities (Frame::summed_intensities), the
  accumulation-normalized per-frame intensity sum.
- BPC ("basepeak chromatogram", MS:1000628) from Frames.MaxIntensity,
  a new additive column added to both Frames SELECTs and mapped onto a
  new Frame::max_intensity field (also exposed on the Python Frame).

A trace whose source column is absent on every frame is omitted rather
than emitted empty, matching the writer's contract of only emitting a
chromatogramList when the source yields something.

SRM/PRM transition chromatograms are intentionally left out: they need
per-target cross-frame aggregation of the decoded peak stream, not just
column wiring. Distinct from Sigilweaver#13 (PRM spectra), which this does not
touch.

Closes Sigilweaver#25.

Claude-Session: https://claude.ai/code/session_01TLSifJpmaN7RvW3xi2uWgo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement iter_chromatograms: TIC is decoded-but-unused, BPC/SRM need small-to-larger new work

1 participant