pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("KOTH_GIT_SHA"), ")");
pub mod alignment;
pub mod config;
pub mod error;
pub mod input;
pub mod io;
pub mod lfq;
pub mod output;
pub mod pipeline;
pub use koth_core::{features, hills, mem, models, scoring, stats};
pub use koth_core::{run_features, run_hills, run_scoring};
pub use pipeline::{
group_ms2_hills_by_window, run_pipeline, run_pipeline_from_spectra, run_pipeline_streaming,
run_pipeline_streaming_from_spectra, run_pipeline_with_ms2, FeatureFindingOutput,
PipelineOptions, PipelineSink,
};
pub use models::{Feature, Hill, IsolationWindow, Polarity, ScoredFeature, Spectrum};
use std::path::Path;
use config::{FileConfig, HillsConfig};
use error::KothError;
use koth_core::shuffle_if_decoy;
pub fn read_spectra(path: &Path, file: &FileConfig) -> Result<Vec<Spectrum>, KothError> {
io::read_spectra(path, file)
}
pub fn run_hills_streaming(
path: &Path,
config: &HillsConfig,
file: &FileConfig,
) -> Result<Vec<Hill>, KothError> {
hills_streaming_inner(path, config, file)
}
pub fn run_ms2_hills_streaming(
path: &Path,
config: &HillsConfig,
file: &FileConfig,
) -> Result<Vec<Hill>, KothError> {
let fmt = io::detect_format(path);
#[cfg(feature = "tdf")]
if fmt == io::InputFormat::BrukerD {
let spectra = io::bruker::read_bruker_ms2(path, file)?;
if spectra.is_empty() {
return Ok(Vec::new());
}
log::info!(
"Streaming MS2 hill detection from {} ({} Bruker diaPASEF MS2 spectra)",
path.display(),
spectra.len()
);
return Ok(hills::detect_ms2_hills_from_iter(
spectra.into_iter(),
config,
file,
));
}
#[cfg(feature = "thermo")]
if fmt == io::InputFormat::ThermoRaw {
let spectra = io::thermo::read_thermo_ms2(path)?;
if spectra.is_empty() {
return Ok(Vec::new());
}
log::info!(
"Streaming MS2 hill detection from {} ({} Thermo DIA MS2 spectra)",
path.display(),
spectra.len()
);
return Ok(hills::detect_ms2_hills_from_iter(
spectra.into_iter(),
config,
file,
));
}
if !matches!(fmt, io::InputFormat::Mzml | io::InputFormat::MzmlGz) {
log::warn!(
"MS2 hill detection supports mzML, Bruker diaPASEF .d, and Thermo DIA .raw \
(.raw needs --features thermo) inputs only; skipping '{}'",
path.display()
);
return Ok(Vec::new());
}
let (iter, status) = io::mzml::stream_mzml_ms2_checked(path)?;
log::info!("Streaming MS2 hill detection from {}", path.display());
let hills = hills::detect_ms2_hills_from_iter(iter, config, file);
status.check()?;
Ok(hills)
}
fn hills_streaming_inner(
path: &Path,
config: &HillsConfig,
file: &FileConfig,
) -> Result<Vec<Hill>, KothError> {
let iter = io::stream_spectra(path, file)?;
if file.decoy_mode {
let spectra = shuffle_if_decoy(iter.collect::<Result<Vec<_>, _>>()?, file);
return Ok(hills::detect_hills_from_iter(
spectra.into_iter(),
config,
file,
));
}
log::info!("Streaming MS1 hill detection from {}", path.display());
let iter: io::SpectrumStream = if io::detect_format(path) == io::InputFormat::BrukerD {
iter
} else {
Box::new(io::prefetch::prefetch(iter, PREFETCH_CAPACITY))
};
detect_hills_from_results(iter, config, file)
}
fn detect_hills_from_results(
iter: io::SpectrumStream,
config: &HillsConfig,
file: &FileConfig,
) -> Result<Vec<Hill>, KothError> {
let mut error = None;
let spectra = iter.map_while(|item| match item {
Ok(spectrum) => Some(spectrum),
Err(e) => {
error = Some(e);
None
}
});
let hills = hills::detect_hills_from_iter(spectra, config, file);
match error {
Some(error) => Err(error),
None => Ok(hills),
}
}
const PREFETCH_CAPACITY: usize = 64;
#[cfg(test)]
mod ms1_streaming_tests {
use super::*;
#[test]
fn reader_error_discards_partial_detection() {
let items = vec![
Ok(Spectrum {
scan_index: 0,
retention_time: 1.0,
peaks: vec![],
ms_level: 1,
isolation_window: None,
faims_cv: None,
}),
Err(KothError::ThermoError("decode failure".into())),
];
let result = detect_hills_from_results(
Box::new(items.into_iter()),
&HillsConfig::default(),
&FileConfig::default(),
);
assert!(matches!(result, Err(KothError::ThermoError(_))));
}
}