Skip to main content

streaming_api/
streaming_api.rs

1//! In-process feature-finding API — usage for the two real downstream tools.
2//!
3//! Run against any mzML (or Bruker `.d` with `--features tdf`):
4//!
5//! ```sh
6//! cargo run --release -p koth-ms --example streaming_api -- path/to/run.mzML
7//! ```
8//!
9//! None of the consumers below writes or reads any intermediate parquet/tsv
10//! file: hills and features are obtained directly as owned Rust structs in
11//! memory. The third consumer (`tracer_with_ms2`) also pulls the DIA MS2
12//! fragment hills, partitioned by isolation window, in the same in-process call
13//! — supported on mzML, Bruker **diaPASEF** `.d` (`--features tdf`, windows read
14//! straight from the raw quadrupole settings), and Thermo **DIA** `.raw`
15//! (`--features thermo`, one MS2 spectrum per scan stamped with its precursor
16//! isolation window). On DDA acquisitions (ddaPASEF `.d` / DDA `.raw`) the MS2
17//! set is empty (with a warning) and the MS1 side is unaffected.
18
19use std::path::PathBuf;
20
21use koth_ms::{
22    config::KothConfig, group_ms2_hills_by_window, run_pipeline, run_pipeline_streaming,
23    run_pipeline_with_ms2, Feature, Hill, IsolationWindow, PipelineOptions, PipelineSink,
24    ScoredFeature,
25};
26
27// ---------------------------------------------------------------------------
28// uno-style consumer: collect the final feature list into memory and search it.
29// ---------------------------------------------------------------------------
30fn uno_style(path: &std::path::Path, config: &KothConfig) {
31    // One call, everything owned in memory. No files touched.
32    let out = run_pipeline(path, config, &PipelineOptions::default()).expect("pipeline");
33
34    // uno searches MS1 features by neutral mass + charge + RT.
35    let searchable: Vec<&ScoredFeature> = out
36        .features
37        .iter()
38        .filter(|f| f.feature.charge > 0) // same set the features file would hold
39        .collect();
40
41    println!(
42        "[uno] {} hills, {} charged features available for MS1 search",
43        out.hills.len(),
44        searchable.len()
45    );
46    if let Some(top) = searchable.iter().max_by(|a, b| {
47        a.feature
48            .total_intensity()
49            .partial_cmp(&b.feature.total_intensity())
50            .unwrap()
51    }) {
52        println!(
53            "[uno]   most intense: mass {:?} z{} score {:.3}",
54            top.monoisotopic_neutral_mass(koth_ms::Polarity::Positive),
55            top.feature.charge,
56            top.combined_score
57        );
58    }
59}
60
61// ---------------------------------------------------------------------------
62// koth_tracer-style consumer: take ownership of the hill set to build a trace
63// index, then fold features incrementally without holding the whole vector.
64// ---------------------------------------------------------------------------
65#[derive(Default)]
66struct TracerSink {
67    hills: Vec<Hill>, // owned once, up front — used to trace precursor<->fragment
68    n_features: usize,
69    total_intensity: f64,
70}
71
72impl PipelineSink for TracerSink {
73    fn on_hills(&mut self, hills: Vec<Hill>) {
74        // koth_tracer indexes the finalized hill set here (m/z, RT, IM) so it
75        // can attribute fragment hills to precursor features as they arrive.
76        println!("[tracer] indexing {} finalized hills", hills.len());
77        self.hills = hills;
78    }
79
80    fn on_feature(&mut self, feature: ScoredFeature) {
81        // Consume-and-drop: fold each feature into whatever the tracer keeps.
82        // The full feature vector is never materialized on the consumer side.
83        let f: &Feature = &feature.feature;
84        self.total_intensity += f.total_intensity();
85        self.n_features += 1;
86        // `feature` is dropped here, freeing its memory before the next arrives.
87    }
88}
89
90fn tracer_style(path: &std::path::Path, config: &KothConfig) {
91    let mut sink = TracerSink::default();
92    run_pipeline_streaming(path, config, &PipelineOptions::default(), &mut sink)
93        .expect("streaming pipeline");
94    println!(
95        "[tracer] streamed {} features (sum intensity {:.3e}) over {} indexed hills",
96        sink.n_features,
97        sink.total_intensity,
98        sink.hills.len()
99    );
100}
101
102// ---------------------------------------------------------------------------
103// koth_tracer with MS2: get MS1 precursor features AND the DIA fragment hills,
104// partitioned by isolation window, in one in-process call — no files. Then map
105// each precursor to the window(s) that isolated it. MS2 side: mzML + Bruker
106// diaPASEF `.d` + Thermo DIA `.raw`.
107// ---------------------------------------------------------------------------
108fn tracer_with_ms2(path: &std::path::Path, config: &KothConfig) {
109    // `run_pipeline_with_ms2` forces MS2 emission on; equivalently, set
110    // `PipelineOptions { emit_ms2: true, ..Default::default() }` on any entry.
111    let out = run_pipeline_with_ms2(path, config, &PipelineOptions::default())
112        .expect("pipeline with ms2");
113
114    // Group the flat MS2 hill set into DIA channels (each hill already carries
115    // its window in `hill.isolation_window`; this is just the convenience view).
116    let windows: Vec<(IsolationWindow, Vec<Hill>)> = group_ms2_hills_by_window(out.ms2_hills);
117    println!(
118        "[tracer+ms2] {} MS1 features, {} DIA isolation windows",
119        out.features.len(),
120        windows.len()
121    );
122
123    // Precursor -> window mapping: a precursor feature is isolated by every
124    // window whose [lower, upper] contains its (charge-1) precursor m/z.
125    for f in out.features.iter().filter(|f| f.feature.charge > 0).take(3) {
126        let mz = f.feature.monoisotopic_mz(); // precursor m/z of the feature
127        let isolating: Vec<&(IsolationWindow, Vec<Hill>)> = windows
128            .iter()
129            .filter(|(w, _)| w.lower <= mz && mz <= w.upper)
130            .collect();
131        let n_frag: usize = isolating.iter().map(|(_, hs)| hs.len()).sum();
132        println!(
133            "[tracer+ms2]   precursor m/z {:.4} z{} -> {} window(s), {} candidate fragment hills",
134            mz,
135            f.feature.charge,
136            isolating.len(),
137            n_frag
138        );
139    }
140}
141
142fn main() {
143    let path: PathBuf = std::env::args()
144        .nth(1)
145        .map(PathBuf::from)
146        .expect("usage: streaming_api <input.mzML | input.d>");
147    let config = KothConfig::default();
148
149    tracer_style(&path, &config);
150    uno_style(&path, &config);
151    tracer_with_ms2(&path, &config);
152}