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}