use std::path::Path;
use crate::config::KothConfig;
use crate::error::KothError;
use crate::models::{Feature, Hill, IsolationWindow, ScoredFeature, Spectrum};
#[derive(Debug, Clone)]
pub struct PipelineOptions {
pub scoring: bool,
pub emit_ms2: bool,
}
impl Default for PipelineOptions {
fn default() -> Self {
Self {
scoring: true,
emit_ms2: false,
}
}
}
pub trait PipelineSink {
fn on_hills(&mut self, hills: Vec<Hill>) {
let _ = hills;
}
fn on_feature(&mut self, feature: ScoredFeature) {
let _ = feature;
}
fn on_ms2_hills(&mut self, hills: Vec<Hill>) {
let _ = hills;
}
}
pub fn group_ms2_hills_by_window(hills: Vec<Hill>) -> Vec<(IsolationWindow, Vec<Hill>)> {
use std::collections::BTreeMap;
let mut groups: BTreeMap<(i64, i64, i64), (IsolationWindow, Vec<Hill>)> = BTreeMap::new();
for h in hills {
let Some(iw) = h.isolation_window else {
continue;
};
groups
.entry(iw.key())
.or_insert_with(|| (iw, Vec::new()))
.1
.push(h);
}
groups.into_values().collect()
}
#[derive(Debug, Default)]
pub struct FeatureFindingOutput {
pub hills: Vec<Hill>,
pub features: Vec<ScoredFeature>,
pub ms2_hills: Vec<Hill>,
}
pub(crate) fn wrap_unscored(f: Feature) -> ScoredFeature {
ScoredFeature {
cosine_score: f.cosine_score,
feature: f,
neutron_offset: 0,
isotope_score: 0.0,
combined_score: 0.0,
theoretical_pattern: Vec::new(),
}
}
fn drive_from_hills<S: PipelineSink>(
hills: Vec<Hill>,
config: &KothConfig,
opts: &PipelineOptions,
sink: &mut S,
) -> Result<(), KothError> {
let features = crate::run_features(&hills, &config.features, &config.file)?;
let scored: Vec<ScoredFeature> = if opts.scoring {
crate::run_scoring(
&features,
&config.scoring,
&config.features,
config.file.polarity,
)
} else {
features.into_iter().map(wrap_unscored).collect()
};
sink.on_hills(hills);
for sf in scored {
sink.on_feature(sf);
}
Ok(())
}
pub fn run_pipeline_streaming<S: PipelineSink>(
path: &Path,
config: &KothConfig,
opts: &PipelineOptions,
sink: &mut S,
) -> Result<(), KothError> {
let hills = crate::run_hills_streaming(path, &config.hills, &config.file)?;
drive_from_hills(hills, config, opts, sink)?;
if opts.emit_ms2 {
let ms2 = crate::run_ms2_hills_streaming(path, &config.ms2_hills(), &config.file)?;
sink.on_ms2_hills(ms2);
}
Ok(())
}
pub fn run_pipeline_streaming_from_spectra<I, S>(
spectra: I,
config: &KothConfig,
opts: &PipelineOptions,
sink: &mut S,
) -> Result<(), KothError>
where
I: Iterator<Item = Spectrum>,
S: PipelineSink,
{
if opts.emit_ms2 {
let buf: Vec<Spectrum> = spectra.collect();
let ms2 = crate::hills::detect_ms2_hills_from_iter(
buf.iter().cloned(),
&config.ms2_hills(),
&config.file,
);
let ms1: Vec<Spectrum> = buf.into_iter().filter(|s| s.ms_level != 2).collect();
let ms1 = crate::shuffle_if_decoy(ms1, &config.file);
let hills =
crate::hills::detect_hills_from_iter(ms1.into_iter(), &config.hills, &config.file);
drive_from_hills(hills, config, opts, sink)?;
sink.on_ms2_hills(ms2);
return Ok(());
}
let hills = if config.file.decoy_mode {
let buf = crate::shuffle_if_decoy(spectra.collect(), &config.file);
crate::hills::detect_hills_from_iter(buf.into_iter(), &config.hills, &config.file)
} else {
crate::hills::detect_hills_from_iter(spectra, &config.hills, &config.file)
};
drive_from_hills(hills, config, opts, sink)
}
#[derive(Debug, Default)]
struct CollectingSink {
out: FeatureFindingOutput,
}
impl PipelineSink for CollectingSink {
fn on_hills(&mut self, hills: Vec<Hill>) {
self.out.hills = hills;
}
fn on_feature(&mut self, feature: ScoredFeature) {
self.out.features.push(feature);
}
fn on_ms2_hills(&mut self, hills: Vec<Hill>) {
self.out.ms2_hills = hills;
}
}
pub fn run_pipeline(
path: &Path,
config: &KothConfig,
opts: &PipelineOptions,
) -> Result<FeatureFindingOutput, KothError> {
let mut sink = CollectingSink::default();
run_pipeline_streaming(path, config, opts, &mut sink)?;
Ok(sink.out)
}
pub fn run_pipeline_from_spectra<I>(
spectra: I,
config: &KothConfig,
opts: &PipelineOptions,
) -> Result<FeatureFindingOutput, KothError>
where
I: Iterator<Item = Spectrum>,
{
let mut sink = CollectingSink::default();
run_pipeline_streaming_from_spectra(spectra, config, opts, &mut sink)?;
Ok(sink.out)
}
pub fn run_pipeline_with_ms2(
path: &Path,
config: &KothConfig,
opts: &PipelineOptions,
) -> Result<FeatureFindingOutput, KothError> {
let opts = PipelineOptions {
emit_ms2: true,
..opts.clone()
};
let mut sink = CollectingSink::default();
run_pipeline_streaming(path, config, &opts, &mut sink)?;
Ok(sink.out)
}
#[cfg(test)]
#[path = "pipeline_tests.rs"]
mod tests;