Skip to main content

koth_ms/io/
mod.rs

1pub mod mzml;
2pub mod prefetch;
3pub mod tims_calibration;
4
5#[cfg(feature = "tdf")]
6pub mod bruker;
7
8#[cfg(feature = "thermo")]
9pub mod thermo;
10
11use std::path::Path;
12
13use crate::config::FileConfig;
14use crate::error::KothError;
15use crate::models::Spectrum;
16
17/// Fallible MS1 stream. Native readers index only metadata before decoding;
18/// errors must be checked even after spectra have already been yielded.
19pub type SpectrumStream = Box<dyn Iterator<Item = Result<Spectrum, KothError>> + Send>;
20
21/// Stream MS1 spectra from any supported format. Bruker uses its own bounded
22/// parallel producer; callers may prefetch mzML and Thermo iterators as needed.
23pub fn stream_spectra(path: &Path, file: &FileConfig) -> Result<SpectrumStream, KothError> {
24    match detect_format(path) {
25        InputFormat::Mzml | InputFormat::MzmlGz => mzml::stream_mzml_results(path),
26        #[cfg(feature = "tdf")]
27        InputFormat::BrukerD => Ok(Box::new(bruker::stream_bruker(path, file))),
28        #[cfg(feature = "thermo")]
29        InputFormat::ThermoRaw => thermo::stream_thermo(path),
30        // Reuse the existing actionable errors for unsupported/build-disabled
31        // formats. No supported reader takes this collecting fallback.
32        _ => read_spectra(path, file)
33            .map(|spectra| Box::new(spectra.into_iter().map(Ok)) as SpectrumStream),
34    }
35}
36
37/// Classification of an input path into one of koth_ff's supported raw formats.
38///
39/// The variant reflects the *shape* of the input, independent of which cargo
40/// features are compiled in — `BrukerD`/`ThermoRaw` are returned even in a build
41/// without `tdf`/`thermo`, and the caller is responsible for surfacing the
42/// "rebuild with --features …" error in that case.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum InputFormat {
45    /// Plain mzML (`.mzml`, case-insensitive).
46    Mzml,
47    /// Gzip-compressed mzML (`.mzml.gz`, case-insensitive).
48    MzmlGz,
49    /// Bruker timsTOF `.d` folder (requires the `tdf` feature to read).
50    BrukerD,
51    /// Thermo Fisher `.raw` file (requires the `thermo` feature to read).
52    ThermoRaw,
53    /// Nothing koth_ff can read.
54    Unsupported,
55}
56
57/// Detect a path's input format from its name (and, for Bruker, directory-ness).
58///
59/// **Canonical predicates** — this is the single source of truth for the
60/// `.mzML`/`.d`/`.raw` decision; every dispatch site routes through it so the
61/// edge cases stay consistent. All extension tests are **case-insensitive**:
62///
63/// * `MzmlGz`   — file name ends `.mzml.gz`.
64/// * `Mzml`     — file name ends `.mzml` (and not `.mzml.gz`).
65/// * `BrukerD`  — the path **is a directory** AND its name ends `.d`. A `.d`
66///   name that is not a directory, or any directory whose name does not end
67///   `.d`, is **not** Bruker.
68/// * `ThermoRaw`— file name ends `.raw`.
69/// * `Unsupported` — anything else.
70pub fn detect_format(path: &Path) -> InputFormat {
71    let name = path
72        .file_name()
73        .and_then(|n| n.to_str())
74        .unwrap_or("")
75        .to_lowercase();
76
77    if name.ends_with(".mzml.gz") {
78        InputFormat::MzmlGz
79    } else if name.ends_with(".mzml") {
80        InputFormat::Mzml
81    } else if name.ends_with(".d") && path.is_dir() {
82        InputFormat::BrukerD
83    } else if name.ends_with(".raw") {
84        InputFormat::ThermoRaw
85    } else {
86        InputFormat::Unsupported
87    }
88}
89
90/// Detect input format and read all MS1 spectra.
91///
92/// Supported formats:
93/// - `.mzML` / `.mzml` / `.mzML.gz` → mzML via mzdata
94/// - directory ending in `.d` → Bruker timsTOF TDF (requires "tdf" feature)
95/// - `.raw` → native Thermo Fisher reader (pure-Rust `opentfraw`, default "thermo" feature)
96pub fn read_spectra(path: &Path, file: &FileConfig) -> Result<Vec<Spectrum>, KothError> {
97    match detect_format(path) {
98        InputFormat::Mzml | InputFormat::MzmlGz => mzml::read_mzml(path),
99        InputFormat::BrukerD => read_bruker_inner(path, file),
100        InputFormat::ThermoRaw => read_thermo_inner(path),
101        InputFormat::Unsupported => Err(KothError::UnsupportedFormat(format!(
102            "'{}' is not a supported input format. Use .mzML, a Bruker .d directory, or a Thermo .raw file.",
103            path.display()
104        ))),
105    }
106}
107
108#[cfg(feature = "thermo")]
109fn read_thermo_inner(path: &Path) -> Result<Vec<Spectrum>, KothError> {
110    thermo::read_thermo(path)
111}
112
113#[cfg(not(feature = "thermo"))]
114fn read_thermo_inner(_path: &Path) -> Result<Vec<Spectrum>, KothError> {
115    Err(KothError::UnsupportedFormat(
116        "Thermo .raw support requires the 'thermo' feature flag. Rebuild with the default features (or --features thermo)."
117            .into(),
118    ))
119}
120
121#[cfg(feature = "tdf")]
122fn read_bruker_inner(path: &Path, file: &FileConfig) -> Result<Vec<Spectrum>, KothError> {
123    bruker::read_bruker(path, file)
124}
125
126#[cfg(not(feature = "tdf"))]
127fn read_bruker_inner(_path: &Path, _file: &FileConfig) -> Result<Vec<Spectrum>, KothError> {
128    Err(KothError::UnsupportedFormat(
129        "Bruker .d support requires the 'tdf' feature flag. Rebuild with --features tdf.".into(),
130    ))
131}