From 7b6c7a7fdbf40781af2a8becb771e186f08661e1 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Tue, 1 Oct 2024 16:00:20 -0700 Subject: [PATCH 01/30] initial branch commit --- README.md | 32 +++++-- src/lib.rs | 3 +- .../raw_peak_agg/multi_chromatogram_agg.rs | 96 +++++++++++++------ src/models/elution_group.rs | 2 +- .../quad_splitted_transposed_index.rs | 15 ++- .../queriable_tims_data.rs | 6 +- 6 files changed, 112 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f752d01..ed47c00 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,34 @@ # Timsquery -UNDER CONSTRUCTION +## Where are we in the life cycle? -... The idea is to have a library that allows querying TIMS data in a generic way. +- The library is in a very early stage of development. + - I cannot assure stability or bug-freeness. + - We are still deciding what the API should be and what the scope of the project overall is. -The main design is to have modular aggregators, indices and queries. -Thus, depending on the access pattern, the data can be queried in +1. Push to main. +2. Branched but fast moving <- **We are here** +3. Stable api but features might be dropped without notice. +4. Stable api and deprecations on changes. + +## What is this? + +Timsquery is meant to be a library and command line tool that allows querying TIMS data in a generic way. +Basically, pick a way in which you want your data to be aggregated, pick how you want to query it and pick +your file, and you get back results that match those three things! + +More explicitly: +- The main design is to have modular components: + - aggregators + - indices + - queries + - tolerances + +Thus, depending on the access pattern and purpose, the data can be queried in different ways and aggregated in different ways (if you need random -acces, use the index that works for that, if you need bulk +access, use the index that works for that, if you need bulk sequential, use that). -Ideally we will also have a sane CLI (think ... msaccess-like). +## What does the cli look like right now? + diff --git a/src/lib.rs b/src/lib.rs index 21cd7b5..c7ab4fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ // Re-export main structures pub use crate::models::elution_group::ElutionGroup; +pub use crate::models::indices::transposed_quad_index::QuadSplittedTransposedIndex; pub use crate::queriable_tims_data::queriable_tims_data::QueriableTimsData; // Re-export traits @@ -15,5 +16,3 @@ pub mod models; pub mod queriable_tims_data; pub mod traits; pub mod utils; - -// Any library-wide code, documentation, or additional exports can go here diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index 297437c..551f4b4 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -16,6 +16,8 @@ use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter} pub struct MultiCMGStats { pub scan_tof_mapping: MappingCollection<(FH, u32), ScanTofStatsCalculatorPair>, pub converters: (Tof2MzConverter, Scan2ImConverter), + pub uniq_rts: HashSet, + pub uniq_ids: HashSet, pub id: u64, } @@ -30,6 +32,8 @@ impl MultiCMGStatsFactory { MultiCMGStats { scan_tof_mapping: MappingCollection::new(), converters: (self.converters.0, self.converters.1), + uniq_rts: HashSet::new(), + uniq_ids: HashSet::new(), id, } } @@ -120,6 +124,13 @@ impl NaturalFinalizedMultiCMGSt &lazy_hyperscore, 1 + (other.retention_time_miliseconds.len() / 10), ); + // Set 0 the NANs + // Q: Can I do this in-place? Will the comnpiler do it for me? + let lazy_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline + .into_iter() + .map(|x| if x.is_nan() { 0.0 } else { x }) + .collect(); + let mut apex_hyperscore_index = 0; let mut max_hyperscore = 0.0f64; for (i, val) in lazy_hyperscore_vs_baseline.iter().enumerate() { @@ -195,6 +206,10 @@ impl From>(); let mut unique_rts = unique_rts.into_iter().collect::>(); unique_rts.sort(); + + // Q: Is this the most efficient way to do this? + // I think having the btrees as the unit of integration might not be the best idea. + // ... If I want to preserve the sparsity, I can use a hashmap and the sort it. let mut summed_intensity_tree: BTreeMap = BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); let mut npeaks_tree: BTreeMap = @@ -231,6 +246,7 @@ impl From Aggregator<(RawPeak, FH)> let u64_intensity = peak.intensity as u64; let rt_miliseconds = (peak.retention_time * 1000.0) as u32; + self.uniq_rts.insert(rt_miliseconds); + self.uniq_ids.insert(transition.clone()); + self.scan_tof_mapping .entry((transition.clone(), rt_miliseconds)) .and_modify(|curr| { @@ -279,39 +298,30 @@ impl Aggregator<(RawPeak, FH)> fn finalize(self) -> NaturalFinalizedMultiCMGStatsArrays { let mut transition_stats = MappingCollection::new(); - for ((transition, rt_ms), scan_tof_mapping) in self.scan_tof_mapping.into_iter() { - transition_stats - .entry(transition) - .and_modify(|curr: &mut ChromatomobilogramStatsArrays| { - curr.retention_time_miliseconds.push(rt_ms); - curr.scan_index_means - .push(scan_tof_mapping.scan.mean().unwrap()); - curr.scan_index_sds - .push(scan_tof_mapping.scan.standard_deviation().unwrap()); - curr.tof_index_means - .push(scan_tof_mapping.tof.mean().unwrap()); - curr.tof_index_sds - .push(scan_tof_mapping.tof.standard_deviation().unwrap()); - curr.intensities.push(scan_tof_mapping.tof.weight()); - }) - .or_insert_with(|| { - let mut out = ChromatomobilogramStatsArrays::new(); - out.retention_time_miliseconds.push(rt_ms); - out.scan_index_means + + for id_key in self.uniq_ids.iter() { + let mut id_cmgs = ChromatomobilogramStatsArrays::new(); + for rt_key in self.uniq_rts.iter() { + let scan_tof_mapping = self.scan_tof_mapping.get(&(id_key.clone(), *rt_key)); + if let Some(scan_tof_mapping) = scan_tof_mapping { + id_cmgs.retention_time_miliseconds.push(*rt_key); + id_cmgs + .scan_index_means .push(scan_tof_mapping.scan.mean().unwrap()); - out.scan_index_sds + id_cmgs + .scan_index_sds .push(scan_tof_mapping.scan.standard_deviation().unwrap()); - out.tof_index_means + id_cmgs + .tof_index_means .push(scan_tof_mapping.tof.mean().unwrap()); - out.tof_index_sds + id_cmgs + .tof_index_sds .push(scan_tof_mapping.tof.standard_deviation().unwrap()); - out.intensities.push(scan_tof_mapping.tof.weight()); - out - }); - } - - for (_k, v) in transition_stats.iter_mut() { - v.sort_by_rt(); + id_cmgs.intensities.push(scan_tof_mapping.tof.weight()); + } + } + id_cmgs.sort_by_rt(); + transition_stats.insert(id_key.clone(), id_cmgs); } let tmp = MultiCMGStatsArrays { @@ -325,3 +335,31 @@ impl Aggregator<(RawPeak, FH)> ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_value_vs_baseline() { + let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; + let baseline_window_size = 3; + let baseline = rolling_median(&vals, baseline_window_size, f64::NAN); + let out = calculate_value_vs_baseline(&vals, baseline_window_size); + let expect_val = vec![f64::NAN, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, f64::NAN]; + let all_close = out + .iter() + .zip(expect_val.iter()) + .filter(|(a, b)| ((!a.is_nan()) && (!b.is_nan()))) + .all(|(a, b)| (a - b).abs() < 1e-6); + + let all_match_nan = out + .iter() + .zip(expect_val.iter()) + .filter(|(a, b)| ((a.is_nan()) || (b.is_nan()))) + .all(|(a, b)| a.is_nan() && b.is_nan()); + + assert!(all_close, "Expected {:?}, got {:?}", expect_val, out); + assert!(all_match_nan, "Expected {:?}, got {:?}", expect_val, out); + } +} diff --git a/src/models/elution_group.rs b/src/models/elution_group.rs index 24cd811..0fec0f9 100644 --- a/src/models/elution_group.rs +++ b/src/models/elution_group.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::hash::Hash; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ElutionGroup { pub id: u64, pub mobility: f32, diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 16c358e..260347b 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -1,7 +1,7 @@ use super::quad_index::{ FrameRTTolerance, PeakInQuad, TransposedQuadIndex, TransposedQuadIndexBuilder, }; - +use crate::models::elution_group::ElutionGroup; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::expand_quad_settings; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; @@ -11,13 +11,12 @@ use crate::traits::indexed_data::IndexedData; use crate::utils::compress_explode::explode_vec; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; - -use crate::models::elution_group::ElutionGroup; use log::{debug, info, trace}; use rayon::prelude::*; use serde::Serialize; use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; use std::sync::Arc; @@ -42,6 +41,16 @@ pub struct QuadSplittedTransposedIndex { metadata: Metadata, } +impl Debug for QuadSplittedTransposedIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "QuadSplittedTransposedIndex(num_quads: {})", + self.flat_quad_settings.len() + ) + } +} + impl QuadSplittedTransposedIndex { pub fn query_peaks( &self, diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index e2fc840..982c377 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -127,7 +127,11 @@ where microseconds_per_query ); - aggregators.into_par_iter().map(|x| x.finalize()).collect() + let start = Instant::now(); + let out = aggregators.into_par_iter().map(|x| x.finalize()).collect(); + let elapsed = start.elapsed(); + info!("Aggregation took {:#?}", elapsed); + out } pub fn query_indexed( From 9c9f6caeffaed3731bc5899e1f92f6016e182a61 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Thu, 17 Oct 2024 15:08:20 -0700 Subject: [PATCH 02/30] feat: added precursor index to transposed --- Taskfile.yml | 2 +- src/main.rs | 4 +- .../raw_peak_agg/multi_chromatogram_agg.rs | 103 ++++++- .../aggregators/streaming_aggregator.rs | 35 ++- src/models/frames/expanded_frame.rs | 125 +++++--- src/models/frames/expanded_window_group.rs | 2 +- src/models/frames/single_quad_settings.rs | 19 +- src/models/indices/expanded_raw_index/mod.rs | 0 .../transposed_quad_index/peak_bucket.rs | 22 +- .../transposed_quad_index/quad_index.rs | 280 ++++++++++++++---- .../quad_splitted_transposed_index.rs | 138 +++++---- src/utils/math.rs | 8 + src/utils/sorting.rs | 17 +- 13 files changed, 564 insertions(+), 191 deletions(-) create mode 100644 src/models/indices/expanded_raw_index/mod.rs diff --git a/Taskfile.yml b/Taskfile.yml index bf23144..9ac1af4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -29,7 +29,7 @@ tasks: fmt: cmds: - - cargo fmt + - cargo +nightly fmt clippy: cmds: diff --git a/src/main.rs b/src/main.rs index 8ff2ed4..1dc7c05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ use timsquery::queriable_tims_data::queriable_tims_data::query_multi_group; use timsquery::traits::tolerance::DefaultTolerance; use timsquery::{ models::aggregators::{ - ChromatomobilogramStats, ExtractedIonChromatomobilogram, MultiCMGStats, - MultiCMGStatsFactory, RawPeakIntensityAggregator, RawPeakVectorAggregator, + ChromatomobilogramStats, ExtractedIonChromatomobilogram, MultiCMGStatsFactory, + RawPeakIntensityAggregator, RawPeakVectorAggregator, }, models::indices::raw_file_index::RawFileIndex, models::indices::transposed_quad_index::QuadSplittedTransposedIndex, diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index 551f4b4..b9baf0d 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -5,7 +5,8 @@ use crate::models::aggregators::rolling_calculators::rolling_median; use crate::models::aggregators::streaming_aggregator::RunningStatsCalculator; use crate::models::frames::raw_peak::RawPeak; use crate::traits::aggregator::Aggregator; -use crate::utils::math::lnfact; +use crate::utils::math::{lnfact, lnfact_float}; +use log::{debug, warn}; use serde::Serialize; use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; @@ -50,6 +51,9 @@ pub struct FinalizedMultiCMGStatsArrays, pub weighted_scan_index_mean: Vec, pub summed_intensity: Vec, + + // This should be the same as the sum of log intensities. + pub log_intensity_products: Vec, pub npeaks: Vec, pub scan_index_means: MappingCollection>, pub tof_index_means: MappingCollection>, @@ -68,10 +72,14 @@ pub struct NaturalFinalizedMultiCMGStatsArrays, pub lazy_hyperscore: Vec, pub lazy_hyperscore_vs_baseline: Vec, + pub norm_hyperscore_vs_baseline: Vec, + pub lazyerscore: Vec, + pub lazyerscore_vs_baseline: Vec, + pub norm_lazyerscore_vs_baseline: Vec, pub transition_mobilities: MappingCollection>, pub transition_mzs: MappingCollection>, pub transition_intensities: MappingCollection>, - pub apex_hyperscore_index: usize, + pub apex_primary_score_index: usize, pub id: u64, } @@ -120,23 +128,55 @@ impl NaturalFinalizedMultiCMGSt mobility_converter: &Scan2ImConverter, ) -> Self { let lazy_hyperscore = calculate_lazy_hyperscore(&other.npeaks, &other.summed_intensity); - let lazy_hyperscore_vs_baseline = calculate_value_vs_baseline( - &lazy_hyperscore, - 1 + (other.retention_time_miliseconds.len() / 10), - ); + let basline_window_len = 1 + (other.retention_time_miliseconds.len() / 10); + let lazy_hyperscore_vs_baseline = + calculate_value_vs_baseline(&lazy_hyperscore, basline_window_len); + let lazyerscore: Vec = other + .log_intensity_products + .iter() + .map(|x| lnfact_float(*x)) + .collect(); + let lazyerscore_vs_baseline = calculate_value_vs_baseline(&lazyerscore, basline_window_len); + // Set 0 the NANs // Q: Can I do this in-place? Will the comnpiler do it for me? let lazy_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline .into_iter() .map(|x| if x.is_nan() { 0.0 } else { x }) .collect(); + let lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline + .into_iter() + .map(|x| if x.is_nan() { 0.0 } else { x }) + .collect(); + + // Calculate the standard deviation of the lazyscores v baseline + let mut sd_calculator_hyperscore = RunningStatsCalculator::new(1, 0.0); + let mut sd_calculator_lazyscore = RunningStatsCalculator::new(1, 0.0); + + (0..lazyerscore_vs_baseline.len()).for_each(|i| { + sd_calculator_hyperscore.add(lazy_hyperscore_vs_baseline[i], 1); + sd_calculator_lazyscore.add(lazyerscore_vs_baseline[i], 1); + }); + let sd_hyperscore = sd_calculator_hyperscore.standard_deviation().unwrap(); + let sd_lazyscore = sd_calculator_lazyscore.standard_deviation().unwrap(); + + // Calculate the normalized scores + let norm_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline + .iter() + .map(|x| x / sd_hyperscore) + .collect(); + let norm_lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline + .iter() + .map(|x| x / sd_lazyscore) + .collect(); - let mut apex_hyperscore_index = 0; - let mut max_hyperscore = 0.0f64; - for (i, val) in lazy_hyperscore_vs_baseline.iter().enumerate() { - if max_hyperscore.is_nan() || *val > max_hyperscore { - max_hyperscore = *val; - apex_hyperscore_index = i; + let mut apex_primary_score_index = 0; + let mut max_primary_score = 0.0f64; + let primary_scores = &norm_lazyerscore_vs_baseline; + for (i, val) in primary_scores.iter().enumerate() { + if max_primary_score.is_nan() || *val > max_primary_score { + max_primary_score = *val; + apex_primary_score_index = i; } } @@ -150,7 +190,13 @@ impl NaturalFinalizedMultiCMGSt average_mobility: other .weighted_scan_index_mean .into_iter() - .map(|x| mobility_converter.convert(x)) + .map(|x| { + let out = mobility_converter.convert(x); + if out < 0.5 || out > 2.0 { + debug!("Bad mobility value: {:?}, input was {:?}", out, x); + } + out + }) .collect(), summed_intensity: other.summed_intensity, npeaks: other.npeaks, @@ -176,7 +222,11 @@ impl NaturalFinalizedMultiCMGSt transition_intensities: other.intensities, lazy_hyperscore, lazy_hyperscore_vs_baseline, - apex_hyperscore_index, + apex_primary_score_index, + lazyerscore, + lazyerscore_vs_baseline, + norm_hyperscore_vs_baseline, + norm_lazyerscore_vs_baseline, id: other.id, } } @@ -195,6 +245,7 @@ impl From From = BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); + let mut intensity_logsums_tree: BTreeMap = + BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0.0))); let mut npeaks_tree: BTreeMap = BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); let mut weighted_tof_index_mean_tree: BTreeMap = @@ -230,11 +283,19 @@ impl From 100 { + npeaks_tree.entry(rt).and_modify(|curr| *curr += 1); + } tmp_tree.entry(rt).or_insert((scan, tof, inten)); summed_intensity_tree .entry(rt) .and_modify(|curr| *curr += inten); - npeaks_tree.entry(rt).and_modify(|curr| *curr += 1); + + intensity_logsums_tree.entry(rt).and_modify(|curr| { + if inten > 10 { + *curr += (inten as f64).ln() + } + }); weighted_tof_index_mean_tree .entry(rt) .and_modify(|curr| curr.add(tof, inten)) @@ -265,8 +326,18 @@ impl From 1000.0 { + warn!("Bad mobility value: {:?}, input was {:?}", out, x); + } + + out + }) .collect(); + + // Note: The log of products is the same as the sum of logs. + out.log_intensity_products = intensity_logsums_tree.into_values().collect(); out } } diff --git a/src/models/aggregators/streaming_aggregator.rs b/src/models/aggregators/streaming_aggregator.rs index 785584f..2ab67b2 100644 --- a/src/models/aggregators/streaming_aggregator.rs +++ b/src/models/aggregators/streaming_aggregator.rs @@ -22,6 +22,8 @@ pub struct RunningStatsCalculator { weight: u64, mean_n: f64, d_: f64, + min: f64, + max: f64, // count: u64, } @@ -30,7 +32,9 @@ impl RunningStatsCalculator { Self { weight, mean_n: mean, - d_: 0.0, + d_: 1.0, + min: mean, + max: mean, // count: 0, } } @@ -52,6 +56,35 @@ impl RunningStatsCalculator { // Update the weight self.weight += weight; + + // That should be the end of it but I seem to be getting consistently some + // values outside of the min and max observed values. Which might be a + // float issue ... TODO investigate. + + // In the meantime I will just squeeze the mean to the min/max observed values. + self.min = self.min.min(value); + self.max = self.max.max(value); + + self.mean_n = self.mean_n.min(self.max).max(self.min); + + assert!( + self.mean_n <= self.max, + "high mean_n: {} max: {} curr_sd: {} weight_ratio: {} {:?}", + self.mean_n, + self.max, + self.standard_deviation().unwrap(), + weight_ratio, + self + ); + assert!( + self.mean_n >= self.min, + "low mean_n: {} min: {} curr_sd: {} weight_ratio: {} {:?}", + self.mean_n, + self.min, + self.standard_deviation().unwrap(), + weight_ratio, + self + ); } pub fn mean(&self) -> Result { diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index f218f70..a58d555 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,12 +1,16 @@ use std::sync::Arc; -use super::single_quad_settings::{expand_quad_settings, SingleQuadrupoleSetting}; +use super::single_quad_settings::{ + expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, +}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; use crate::sort_by_indices_multi; +use crate::utils::compress_explode::explode_vec; use crate::utils::sorting::argsort_by; /// A frame after expanding the mobility data and re-sorting it by tof. +#[derive(Debug, Clone)] pub struct ExpandedFrame { pub tof_indices: Vec, pub scan_numbers: Vec, @@ -20,6 +24,7 @@ pub struct ExpandedFrame { pub window_group: u8, } +#[derive(Debug, Clone)] pub struct ExpandedFrameSlice { pub tof_indices: Vec, // I could Arc<[u32]> if I didnt want to sort by it ... pub scan_numbers: Vec, @@ -28,7 +33,7 @@ pub struct ExpandedFrameSlice { pub rt: f64, pub acquisition_type: AcquisitionType, pub ms_level: MSLevel, - pub quadrupole_settings: SingleQuadrupoleSetting, + pub quadrupole_settings: Option, pub intensity_correction_factor: f64, pub window_group: u8, pub window_subindex: u8, @@ -44,37 +49,79 @@ impl ExpandedFrameSlice { &mut self.intensities ); } + + pub fn len(&self) -> usize { + self.tof_indices.len() + } + + pub fn is_empty(&self) -> bool { + self.tof_indices.is_empty() + } } -fn expand_and_split_frame(frame: &Frame) -> Vec { - let quad_settings = &frame.quadrupole_settings; - let expanded_quad_settings = expand_quad_settings(quad_settings); +fn trim_scan_edges(scan_start: usize, scan_end: usize) -> (usize, usize) { + // Poor man's fix to different quad windows bleeding onto each other ... + // I will trim the smallest of 20 scans or 10% of the range size. + // + // old dumber implementation + // let peak_start = frame.scan_offsets[qs.ranges.scan_start + 10]; + // let peak_end = frame.scan_offsets[qs.ranges.scan_end - 10]; + // (peak_start, peak_end) + // + let scan_range = scan_end - scan_start; + assert!(scan_range > 0, "Expected scan range to be positive..."); + let offset_use = (scan_range / 10).min(20); + + let new_start = scan_start + offset_use; + let new_end = scan_end - offset_use; + + assert!(new_start < new_end, "Expected new_start < new_end"); + + (new_start, new_end) +} + +fn expand_unfragmented_frame(frame: Frame) -> ExpandedFrameSlice { + let scan_numbers = explode_vec(&frame.scan_offsets); + let intensities = frame.intensities; + let tof_indices = frame.tof_indices; + let mut curr_slice = ExpandedFrameSlice { + tof_indices, + scan_numbers, + intensities, + frame_index: frame.index, + rt: frame.rt, + acquisition_type: frame.acquisition_type, + ms_level: frame.ms_level, + quadrupole_settings: None, + intensity_correction_factor: frame.intensity_correction_factor, + window_group: frame.window_group, + window_subindex: 0u8, + }; + curr_slice.sort_by_tof(); + curr_slice +} - let mut out = Vec::with_capacity(quad_settings.scan_ends.len()); - for (i, qs) in expanded_quad_settings.into_iter().enumerate() { +fn expand_fragmented_frame( + frame: Frame, + quads: Vec, +) -> Vec { + let mut out = Vec::with_capacity(quads.len()); + let exploded_scans = explode_vec(&frame.scan_offsets); + for (i, qs) in quads.into_iter().enumerate() { // This whole block is kind of ugly but I think its good enough for now ... - let slice_scan_start = qs.ranges.scan_start; - let slice_scan_end = qs.ranges.scan_end; + let (slice_scan_start, slice_scan_end) = + trim_scan_edges(qs.ranges.scan_start, qs.ranges.scan_end); + let tof_index_index_slice_start = frame.scan_offsets[slice_scan_start]; let tof_index_index_slice_end = frame.scan_offsets[slice_scan_end]; - let mut slice_tof_indices = + let slice_tof_indices = frame.tof_indices[tof_index_index_slice_start..tof_index_index_slice_end].to_vec(); - let mut slice_scan_numbers = explode_scan_offsets( - &frame.scan_offsets[slice_scan_start..slice_scan_end], - slice_scan_start, - ); - let mut slice_intensities = + let slice_scan_numbers = + exploded_scans[tof_index_index_slice_start..tof_index_index_slice_end].to_vec(); + let slice_intensities = frame.intensities[tof_index_index_slice_start..tof_index_index_slice_end].to_vec(); - let mut indices = argsort_by(&slice_tof_indices, |x| *x); - sort_by_indices_multi!( - &mut indices, - &mut slice_tof_indices, - &mut slice_scan_numbers, - &mut slice_intensities - ); - let mut curr_slice = ExpandedFrameSlice { tof_indices: slice_tof_indices, scan_numbers: slice_scan_numbers, @@ -83,34 +130,35 @@ fn expand_and_split_frame(frame: &Frame) -> Vec { rt: frame.rt, acquisition_type: frame.acquisition_type, ms_level: frame.ms_level, - quadrupole_settings: qs, + quadrupole_settings: Some(qs), intensity_correction_factor: frame.intensity_correction_factor, window_group: frame.window_group, window_subindex: i as u8, }; + // Q: is this sorting twice? since sort before creating the expanded frame slices. + // TODO: Use the state type pattern to make sure only one sort happens. curr_slice.sort_by_tof(); out.push(curr_slice); } out } -fn explode_scan_offsets(scan_offsets: &[usize], offset_val: usize) -> Vec { - let num_vals = scan_offsets.last().unwrap(); - let mut exploded_offsets = vec![0; *num_vals]; - for i in 0..scan_offsets.len() - 1 { - let start = scan_offsets[i]; - let end = scan_offsets[i + 1]; - for pos in exploded_offsets.iter_mut().take(end).skip(start) { - *pos = i + offset_val; - } +pub fn expand_and_split_frame(frame: Frame) -> Vec { + let quad_settings = &frame.quadrupole_settings; + // Q: Can I save a vec allocation if I make the expanded quad_settings + // To return a vec of builders? or Enum(settings|frame_slice)? + let expanded_quad_settings = expand_quad_settings(quad_settings); + + match expanded_quad_settings { + ExpandedFrameQuadSettings::Unfragmented => vec![expand_unfragmented_frame(frame)], + ExpandedFrameQuadSettings::Fragmented(quads) => expand_fragmented_frame(frame, quads), } - exploded_offsets } impl ExpandedFrame { pub fn from_frame(frame: Frame) -> Self { let mut tof_indices = frame.tof_indices; - let mut scan_numbers = explode_scan_offsets(&frame.scan_offsets, 0); + let mut scan_numbers = explode_vec(&frame.scan_offsets); let mut intensities = frame.intensities; let mut indices = argsort_by(&tof_indices, |x| *x); @@ -140,13 +188,6 @@ impl ExpandedFrame { mod tests { use super::*; - #[test] - fn test_explode_scan_offsets() { - let scan_offsets = vec![0, 2, 4, 8]; - let exploded_offsets = explode_scan_offsets(&scan_offsets, 0); - assert_eq!(exploded_offsets, vec![0, 0, 1, 1, 2, 2, 2, 2]); - } - #[test] fn test_expanded_frame_from_frame() { let frame = Frame { diff --git a/src/models/frames/expanded_window_group.rs b/src/models/frames/expanded_window_group.rs index 1ac3c2b..261b7e9 100644 --- a/src/models/frames/expanded_window_group.rs +++ b/src/models/frames/expanded_window_group.rs @@ -12,7 +12,7 @@ pub struct ExpandedWindowGroup { // pub frame_indices: Vec, pub acquisition_type: AcquisitionType, pub ms_level: MSLevel, - pub quadrupole_settings: SingleQuadrupoleSetting, + pub quadrupole_settings: Option, // pub intensity_correction_factor: f64, pub window_group: u8, } diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index a27eb54..67f4cfd 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -36,6 +36,11 @@ impl Display for SingleQuadrupoleSettingRanges { } } +/// A single quadrupole setting. +/// +/// NOTE: Only the index is used for the hashing of this struct. +/// So make sure that int he buildingn process there is no generation +/// of settings with the same index. #[derive(Debug, Clone, Copy)] pub struct SingleQuadrupoleSetting { pub index: SingleQuadrupoleSettingIndex, @@ -81,7 +86,17 @@ impl PartialEq for SingleQuadrupoleSetting { impl Eq for SingleQuadrupoleSetting {} -pub fn expand_quad_settings(quad_settings: &QuadrupoleSettings) -> Vec { +#[derive(Debug, Clone, Hash, PartialEq)] +pub enum ExpandedFrameQuadSettings { + Unfragmented, + Fragmented(Vec), +} + +pub fn expand_quad_settings(quad_settings: &QuadrupoleSettings) -> ExpandedFrameQuadSettings { + if quad_settings.scan_ends.is_empty() { + return ExpandedFrameQuadSettings::Unfragmented; + } + let mut out = Vec::with_capacity(quad_settings.scan_ends.len()); for i in 0..quad_settings.scan_ends.len() { let isolation_width = quad_settings.isolation_width[i]; @@ -108,5 +123,5 @@ pub fn expand_quad_settings(quad_settings: &QuadrupoleSettings) -> Vec PeakBucket { let mut indices = argsort_by(&self.scan_offsets, |x| *x); sort_by_indices_multi!( @@ -102,7 +113,16 @@ impl PeakBucketBuilder { &mut self.intensities ); // TODO consider if I really need to compress this. - let out = if self.scan_offsets.len() > 1000 { + // Options: + // 1. Change the compression of scans (I use the default in timsrust but im sure we can do + // better) ... + // - tuple of val-offset to prevent the long runs of non changing scans. + // - btree of offsets? + // 2. Not compressing makes queries <10% slower, but building the index 10% faster ... + // compressing all makes building the index 3x slower. + // + // let out = if false { + let out = if self.scan_offsets.len() > 500 { let compressed = compress_vec(&self.scan_offsets); PeakBucket { intensities: self.intensities, diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 30bfb0e..162eea9 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -1,9 +1,12 @@ use super::peak_bucket::PeakBucketBuilder; use super::peak_bucket::{PeakBucket, PeakInBucket}; +use crate::models::frames::expanded_frame::{expand_and_split_frame, ExpandedFrameSlice}; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; +use crate::sort_by_indices_multi; use crate::utils::display::{glimpse_vec, GlimpseConfig}; - +use crate::utils::sorting::{argsort_by, par_argsort_by}; +use log::debug; use log::info; use log::trace; use rayon::prelude::*; @@ -14,7 +17,7 @@ use timsrust::converters::{ConvertableDomain, Frame2RtConverter}; #[derive(Debug)] pub struct TransposedQuadIndex { - pub quad_settings: SingleQuadrupoleSetting, + pub quad_settings: Option, pub frame_rts: Vec, pub frame_indices: Vec, pub peak_buckets: BTreeMap, @@ -60,7 +63,7 @@ impl Display for TransposedQuadIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "TransposedQuadIndex\n quad_settings: {}\n frame_indices: {}\n frame_rts: {}\n peak_buckets: {}\n", + "TransposedQuadIndex\n quad_settings: {:?}\n frame_indices: {}\n frame_rts: {}\n peak_buckets: {}\n", self.quad_settings, glimpse_vec(&self.frame_indices, Some(GlimpseConfig { max_items: 10, padding: 2, new_line: true })), glimpse_vec(&self.frame_rts, Some(GlimpseConfig { max_items: 10, padding: 2, new_line: true })), @@ -192,7 +195,7 @@ impl FrameRTTolerance { #[derive(Debug, Clone)] pub struct TransposedQuadIndexBuilder { - quad_settings: SingleQuadrupoleSetting, + quad_settings: Option, int_slices: Vec>, tof_slices: Vec>, scan_slices: Vec>, @@ -201,7 +204,7 @@ pub struct TransposedQuadIndexBuilder { } impl TransposedQuadIndexBuilder { - pub fn new(quad_settings: SingleQuadrupoleSetting) -> Self { + pub fn new(quad_settings: Option) -> Self { Self { quad_settings, int_slices: Vec::new(), @@ -220,34 +223,29 @@ impl TransposedQuadIndexBuilder { self.frame_rts.extend(other.frame_rts); } - pub fn add_frame_slice( - &mut self, - int_slice: Vec, - tof_slice: Vec, - expanded_scan_slice: Vec, - frame_index: usize, - frame_rt: f64, - ) { - assert!(int_slice.len() == tof_slice.len()); - assert!(int_slice.len() == expanded_scan_slice.len()); - - self.int_slices.push(int_slice); - self.tof_slices.push(tof_slice); - self.scan_slices.push(expanded_scan_slice); - self.frame_indices.push(frame_index); - self.frame_rts.push(frame_rt); + pub fn add_frame_slice(&mut self, slice: ExpandedFrameSlice) { + self.int_slices.push(slice.intensities); + self.tof_slices.push(slice.tof_indices); + self.scan_slices.push(slice.scan_numbers); + self.frame_indices.push(slice.frame_index); + self.frame_rts.push(slice.rt); } pub fn build(self) -> TransposedQuadIndex { + // TODO: Refactor this function, its getting pretty large. let st = Instant::now(); + info!( + "TransposedQuadIndex::build start ... quad_settings = {:?}", + self.quad_settings + ); let max_tof = *self .tof_slices .iter() .map(|x| x.iter().max().unwrap()) .max() .unwrap(); - let mut tof_counts = vec![0; max_tof as usize + 1]; - let mut tot_peaks = 0; + let mut tof_counts = vec![0usize; max_tof as usize + 1]; + let mut tot_peaks = 0u64; for tof_slice in self.tof_slices.iter() { for tof in tof_slice.iter() { tof_counts[*tof as usize] += 1; @@ -264,41 +262,17 @@ impl TransposedQuadIndexBuilder { continue; } } - let out_rts = self.frame_rts.clone(); let out_indices = self.frame_indices.clone(); - let mut added_peaks = 0; + let quad_settings = self.quad_settings; let aps = Instant::now(); - for slice_ind in 0..self.frame_indices.len() { - let int_slice = &self.int_slices[slice_ind]; - let tof_slice = &self.tof_slices[slice_ind]; - let scan_slice = &self.scan_slices[slice_ind]; - // let frame_index = self.frame_indices[slice_ind]; - let frame_rt = self.frame_rts[slice_ind] as f32; - - // Q: is it worth to sort and add peaks in slices? - - for ((inten, tof), scan) in int_slice - .iter() - .zip(tof_slice.iter()) - .zip(scan_slice.iter()) - { - peak_buckets - .get_mut(tof) - .expect("Should have just added it...") - .add_peak(*scan, *inten, frame_rt); - - added_peaks += 1; - } - } - let aps = aps.elapsed(); - - if added_peaks != tot_peaks { - println!("TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", tot_peaks, added_peaks); - panic!("TransposedQuadIndex::add_frame_slice failed at peak count check"); - } + peak_buckets = if tot_peaks > 10_000_000 { + self.batched_build_inner(peak_buckets, tot_peaks) + } else { + self.build_inner_ref(peak_buckets, tot_peaks) + }; for (tof, count) in tof_counts.into_iter().enumerate() { if count > 0 { @@ -316,28 +290,212 @@ impl TransposedQuadIndexBuilder { } let bb_st = Instant::now(); - let peak_bucket = BTreeMap::from_par_iter( - peak_buckets - .into_par_iter() - .map(|(tof, pb)| (tof, pb.build())), - ); + + // FYI par iter here makes no difference. + let peak_bucket: BTreeMap = peak_buckets + .into_iter() + .map(|(tof, pb)| (tof, pb.build())) + .collect(); let bbe = bb_st.elapsed(); let elapsed = st.elapsed(); info!( "TransposedQuadIndex::add_frame_slice adding peaks took {:#?} for {} peaks", - aps, tot_peaks + aps.elapsed(), + tot_peaks ); info!( "TransposedQuadIndex::add_frame_slice building buckets took {:#?}", bbe ); - info!("TransposedQuadIndex::build took {:#?}", elapsed); + info!( + "TransposedQuadIndex::build quad_settings={:?} took {:#?}", + quad_settings, elapsed + ); TransposedQuadIndex { - quad_settings: self.quad_settings, + quad_settings, frame_rts: out_rts, frame_indices: out_indices, peak_buckets: peak_bucket, } } + + fn build_inner_ref( + self, + mut peak_buckets: HashMap, + tot_peaks: u64, + ) -> HashMap { + let mut added_peaks = 0; + + for slice_ind in 0..self.frame_indices.len() { + let int_slice = &self.int_slices[slice_ind]; + let tof_slice = &self.tof_slices[slice_ind]; + let scan_slice = &self.scan_slices[slice_ind]; + let frame_rt = self.frame_rts[slice_ind] as f32; + + // Q: is it worth to sort and add peaks in slices? If we do it has to be one level + // higher, since within each slice, most tof indices would not repeat. + // A: Nope ... this is faster. + // + // In theory there are 3 things to consider: + // 1. Can we hold a vec that large? (u32::MAX in teory for a 32 bit system) + // - For According to the playground ... 268_435_455 is the max number of + // u32's that can be allocated ... in some of our data the MS1s have 500M peaks. + // so even if we do it, we would need to batch it ... + // as a note most MS2s are ~20M + // 2. Is the sorting slower than querying the hash map that many times? + // - Sorting is slower ... + // 3. I am assuming that sequential import would let us grow each vec only once + // per chunk addition instead of once per ... peak in the worst case (although vecs + // grow in chunks of +25% ish ...) + // - Might be true but we allocate the vecs with capacity, so they dont grow while + // we iterate + // + for ((inten, tof), scan) in int_slice + .iter() + .zip(tof_slice.iter()) + .zip(scan_slice.iter()) + { + peak_buckets + .get_mut(tof) + .expect("Should have just added it...") + .add_peak(*scan, *inten, frame_rt); + + added_peaks += 1; + if added_peaks % 500_000 == 0 { + debug!( + "TransposedQuadIndex::build quad_settings={:?} added_peaks={:?}/{:?}", + self.quad_settings, added_peaks, tot_peaks, + ); + } + } + } + + if added_peaks != tot_peaks { + println!("TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", tot_peaks, added_peaks); + panic!("TransposedQuadIndex::add_frame_slice failed at peak count check"); + } + + peak_buckets + } + + fn batched_build_inner( + self, + mut peak_buckets: HashMap, + tot_peaks: u64, + ) -> HashMap { + let info_prefix = format!("BatchedBuild: quad_settings={:?} ", self.quad_settings); + info!("{} start", info_prefix); + let num_slices = self.frame_indices.len(); + let mut added_peaks = 0; + + let mut peaks_in_chunk = 0; + let mut start = 0; + let mut end = 0; + + while start < self.frame_indices.len() { + while peaks_in_chunk < 100_000_000 && end < self.frame_indices.len() { + peaks_in_chunk += self.int_slices[end].len(); + end += 1; + } + + let concat_st = Instant::now(); + let mut int_slice = self.int_slices[start..end].concat(); + let mut tof_slice = self.tof_slices[start..end].concat(); + let mut scan_slice = self.scan_slices[start..end].concat(); + let mut rt_slice: Vec = self.frame_rts[start..end] + .iter() + .zip(self.tof_slices[start..end].iter()) + .flat_map(|(rt, tofslice)| vec![*rt as f32; tofslice.len()]) + .collect(); + + let concat_elapsed = concat_st.elapsed(); + + let sorting_st = Instant::now(); + // let mut indices = argsort_by(&tof_slice, |x| *x); + let mut indices = par_argsort_by(&tof_slice, |x| *x); + + let argsort_elapsed = sorting_st.elapsed(); + + sort_by_indices_multi!( + &mut indices, + &mut tof_slice, + &mut scan_slice, + &mut int_slice, + &mut rt_slice + ); + + let sorting_elapsed = sorting_st.elapsed(); + + let insertion_st = Instant::now(); + let mut slice_start = 0; + let mut slice_start_val = tof_slice[0]; + while slice_start < int_slice.len() { + // // Binary search the current value + 1 + // let slice_end = tof_slice.binary_search(&(slice_start_val + 1)); + + // // let local_slice_end = slice_end.unwrap_or_else(|x| x); + // let local_slice_end = match slice_end { + // Ok(x) => { + // // If the value is found, we need to walk back to find the first instance + // // of the value ... + // let mut local_slice_end = x; + // while tof_slice[local_slice_end - 1] > slice_start_val { + // local_slice_end -= 1; + // } + // local_slice_end + // } + // Err(x) => x, + // }; + // + let mut local_slice_end = slice_start; + while tof_slice[local_slice_end] == slice_start_val { + local_slice_end += 1; + // There has to be a way to add this to the conditional above ... + // But alas ... this works ... + if local_slice_end == tof_slice.len() { + break; + } + } + + let range_use = slice_start..local_slice_end; + + peak_buckets + .get_mut(&slice_start_val) + .expect("Tof should have been added during the build") + .extend_peaks( + &scan_slice[range_use.clone()], + &int_slice[range_use.clone()], + &rt_slice[range_use.clone()], + ); + + added_peaks += range_use.len() as u64; + + assert!(slice_start_val == tof_slice[slice_start]); + + if local_slice_end == tof_slice.len() { + break; + } + assert!(slice_start_val < tof_slice[local_slice_end]); + + slice_start = local_slice_end; + slice_start_val = tof_slice[slice_start]; + } + + let insertion_elapsed = insertion_st.elapsed(); + info!( + "BatchedBuild: quad_settings={:?} start={:?} end={:?}/{} peaks {}/{} concat took {:#?} sorting took arg: {:#?} / {:#?} insertion took {:#?}", + self.quad_settings, start, end, num_slices, added_peaks, tot_peaks, concat_elapsed, argsort_elapsed, sorting_elapsed, insertion_elapsed, + ); + start = end; + peaks_in_chunk = 0; + } + + if added_peaks != tot_peaks { + println!("TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", tot_peaks, added_peaks); + panic!("TransposedQuadIndex::add_frame_slice failed at peak count check"); + } + + peak_buckets + } } diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 260347b..f41e963 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -2,6 +2,7 @@ use super::quad_index::{ FrameRTTolerance, PeakInQuad, TransposedQuadIndex, TransposedQuadIndexBuilder, }; use crate::models::elution_group::ElutionGroup; +use crate::models::frames::expanded_frame::{expand_and_split_frame, ExpandedFrameSlice}; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::expand_quad_settings; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; @@ -33,7 +34,8 @@ use timsrust::TimsRustError; // - JSP: 2024-11-19 pub struct QuadSplittedTransposedIndex { - indices: HashMap, TransposedQuadIndex>, + precursor_index: TransposedQuadIndex, + fragment_indices: HashMap, flat_quad_settings: Vec, rt_converter: Frame2RtConverter, pub mz_converter: Tof2MzConverter, @@ -74,7 +76,7 @@ impl QuadSplittedTransposedIndex { let rt_range = rt_range.map(|x| x.to_frame_index_range(&self.rt_converter)); matching_quads.into_iter().flat_map(move |qs| { - let tqi = self.indices.get(&qs).unwrap(); + let tqi = self.fragment_indices.get(&qs).unwrap(); tqi.query_peaks(tof_range, scan_range, rt_range) }) } @@ -91,14 +93,36 @@ impl QuadSplittedTransposedIndex { && (qs.ranges.isolation_high >= precursor_mz_range.0) }) .filter(move |qs| { - if let Some(scan_range) = scan_range { - // This is done for sanity tbh ... sometimes they get flipped - // bc the lowest scan is actually the highest 1/k0. - let min_scan = qs.ranges.scan_start.min(qs.ranges.scan_end); - let max_scan = qs.ranges.scan_start.max(qs.ranges.scan_end); - (min_scan <= scan_range.1) && (scan_range.0 <= max_scan) - } else { - true + // if let Some(scan_range) = scan_range { + // // This is done for sanity tbh ... sometimes they get flipped + // // bc the lowest scan is actually the highest 1/k0. + // } else { + // true + // } + // + match scan_range { + Some((min_scan, max_scan)) => { + assert!(qs.ranges.scan_start <= qs.ranges.scan_end); + assert!(min_scan <= max_scan); + + // Above quad + // Quad [----------] + // Query [------] + let above_quad = qs.ranges.scan_end < min_scan; + + // Below quad + // Quad [------] + // Query [------] + let below_quad = qs.ranges.scan_start > max_scan; + + if above_quad || below_quad { + // This quad is completely outside the scan range + false + } else { + true + } + } + None => true, } }) .cloned() @@ -190,13 +214,19 @@ impl Display for QuadSplittedTransposedIndex { new_line: true, }), )); + + disp_str.push_str("precursor_index: \n"); + disp_str.push_str(&format!(" -- {}\n", self.precursor_index)); let mut num_shown = 0; - for (qs, tqi) in self.indices.iter() { - disp_str.push_str(&format!(" - {}: \n", qs)); + for (qs, tqi) in self.fragment_indices.iter() { + disp_str.push_str(&format!(" - {:?}: \n", qs)); disp_str.push_str(&format!(" -- {}\n", tqi)); num_shown += 1; if num_shown > 5 { - disp_str.push_str(&format!(" ........ len = {}\n", self.indices.len())); + disp_str.push_str(&format!( + " ........ len = {}\n", + self.fragment_indices.len() + )); break; } } @@ -218,8 +248,9 @@ impl QuadSplittedTransposedIndex { } } +#[derive(Debug, Clone, Default)] pub struct QuadSplittedTransposedIndexBuilder { - indices: HashMap, + indices: HashMap, TransposedQuadIndexBuilder>, rt_converter: Option, mz_converter: Option, im_converter: Option, @@ -231,56 +262,32 @@ pub struct QuadSplittedTransposedIndexBuilder { } impl QuadSplittedTransposedIndexBuilder { - fn new() -> Self { - Self { - indices: HashMap::new(), - rt_converter: None, - mz_converter: None, - im_converter: None, - metadata: None, - added_peaks: 0, - } + pub fn new() -> Self { + Self::default() } fn add_frame(&mut self, frame: Frame) { - let expanded_quad_settings = expand_quad_settings(&frame.quadrupole_settings); - let exploded_scans = explode_vec(&frame.scan_offsets); + let expanded_slices = expand_and_split_frame(frame); - for qs in expanded_quad_settings { + for es in expanded_slices.into_iter() { // Add key if it doesnt exist ... - if let Entry::Vacant(e) = self.indices.entry(qs) { - let max_tof = frame.tof_indices.iter().max().unwrap(); - trace!( - "Adding new transposed quad index for qs {:?} with max tof {}", - qs, - max_tof - ); - let new_index = TransposedQuadIndexBuilder::new(qs); - e.insert(new_index); - } - - let peak_start = frame.scan_offsets[qs.ranges.scan_start]; - let peak_end = frame.scan_offsets[qs.ranges.scan_end + 1]; - - let int_slice = frame.intensities[peak_start..peak_end].to_vec(); - let tof_slice = frame.tof_indices[peak_start..peak_end].to_vec(); - let expanded_scan_slice = exploded_scans[peak_start..peak_end].to_vec(); - - let frame_index = frame.index; - let frame_rt = frame.rt; - self.added_peaks += int_slice.len() as u64; + self.indices + .entry(es.quadrupole_settings) + .or_insert(TransposedQuadIndexBuilder::new(es.quadrupole_settings)); - self.indices.get_mut(&qs).unwrap().add_frame_slice( - int_slice, - tof_slice, - expanded_scan_slice, - frame_index, - frame_rt, - ); + self.added_peaks += es.len() as u64; + self.add_frame_slice(es); } } + fn add_frame_slice(&mut self, frame_slice: ExpandedFrameSlice) { + self.indices + .get_mut(&frame_slice.quadrupole_settings) + .unwrap() + .add_frame_slice(frame_slice); + } + fn from_path(path: &str) -> Result { let file_reader = FrameReader::new(path)?; @@ -297,9 +304,9 @@ impl QuadSplittedTransposedIndexBuilder { added_peaks: 0, }; - let out2: Result, TimsRustError> = file_reader - .get_all() + let out2: Result, TimsRustError> = (0..file_reader.len()) .into_par_iter() + .map(|id| file_reader.get(id)) .chunks(100) .map(|frames| { let mut out = Self::new(); @@ -320,7 +327,7 @@ impl QuadSplittedTransposedIndexBuilder { Ok(final_out) } - fn fold(&mut self, other: Self) { + pub fn fold(&mut self, other: Self) { for (qs, builder) in other.indices.into_iter() { self.indices .entry(qs) @@ -330,19 +337,23 @@ impl QuadSplittedTransposedIndexBuilder { self.added_peaks += other.added_peaks; } - fn build(self) -> QuadSplittedTransposedIndex { + pub fn build(self) -> QuadSplittedTransposedIndex { let mut indices = HashMap::new(); let mut flat_quad_settings = Vec::new(); - let built: Vec<(TransposedQuadIndex, SingleQuadrupoleSetting)> = self + let built: Vec<(TransposedQuadIndex, Option)> = self .indices .into_par_iter() .map(|(qs, builder)| (builder.build(), qs)) .collect(); + let mut precursor_index: Option = None; for (qi, qs) in built.into_iter() { - let qa: Arc = Arc::new(qs); - indices.insert(qa.clone(), qi); - flat_quad_settings.push(qs); + if let Some(qs) = qs { + indices.insert(qs, qi); + flat_quad_settings.push(qs); + } else { + precursor_index = Some(qi); + } } flat_quad_settings.sort_by(|a, b| { @@ -353,7 +364,8 @@ impl QuadSplittedTransposedIndexBuilder { }); QuadSplittedTransposedIndex { - indices, + precursor_index: precursor_index.expect("Precursor peaks should be present"), + fragment_indices: indices, flat_quad_settings, rt_converter: self.rt_converter.unwrap(), mz_converter: self.mz_converter.unwrap(), diff --git a/src/utils/math.rs b/src/utils/math.rs index b55af29..029fd70 100644 --- a/src/utils/math.rs +++ b/src/utils/math.rs @@ -10,3 +10,11 @@ pub fn lnfact(n: u16) -> f64 { n * n.ln() - n + 0.5 * n.ln() + 0.5 * (std::f64::consts::PI * 2.0 * n).ln() } } + +pub fn lnfact_float(n: f64) -> f64 { + if n < 1.0 { + 0.0 + } else { + n * n.ln() - n + 0.5 * n.ln() + 0.5 * (std::f64::consts::PI * 2.0 * n).ln() + } +} diff --git a/src/utils/sorting.rs b/src/utils/sorting.rs index 53fd838..2f528eb 100644 --- a/src/utils/sorting.rs +++ b/src/utils/sorting.rs @@ -1,3 +1,5 @@ +use rayon::prelude::*; + fn place_at_indices(original: &mut [T], indices: &mut [usize]) { for i in 0..indices.len() { while i != indices[i] { @@ -26,7 +28,20 @@ where K: Ord, { let mut indices: Vec = (0..v.len()).collect(); - indices.sort_by_key(|&i| key(&v[i])); + // indices.sort_by_key(|&i| key(&v[i])); + indices.sort_unstable_by_key(|&i| key(&v[i])); + indices +} + +pub fn par_argsort_by(v: &[T], key: F) -> Vec +where + F: Fn(&T) -> K + Sync + Send, + K: Ord + Sync + Send, + T: Sync + Send, +{ + let mut indices: Vec = (0..v.len()).collect(); + // indices.sort_by_key(|&i| key(&v[i])); + indices.par_sort_unstable_by_key(|&i| key(&v[i])); indices } From 3263f919cb061a610e3714c92bb557124fcbd9dd Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Thu, 17 Oct 2024 15:11:10 -0700 Subject: [PATCH 03/30] chore: clippy and format --- src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs | 4 ++-- src/models/indices/transposed_quad_index/quad_index.rs | 4 ++-- .../transposed_quad_index/quad_splitted_transposed_index.rs | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index b9baf0d..ad20484 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -192,7 +192,7 @@ impl NaturalFinalizedMultiCMGSt .into_iter() .map(|x| { let out = mobility_converter.convert(x); - if out < 0.5 || out > 2.0 { + if !(0.5..=2.0).contains(&out) { debug!("Bad mobility value: {:?}, input was {:?}", out, x); } out @@ -328,7 +328,7 @@ impl From 1000.0 { + if !(0.0..=1000.0).contains(&out) { warn!("Bad mobility value: {:?}, input was {:?}", out, x); } diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 162eea9..beaf7cf 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -1,11 +1,11 @@ use super::peak_bucket::PeakBucketBuilder; use super::peak_bucket::{PeakBucket, PeakInBucket}; -use crate::models::frames::expanded_frame::{expand_and_split_frame, ExpandedFrameSlice}; +use crate::models::frames::expanded_frame::ExpandedFrameSlice; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::sort_by_indices_multi; use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::sorting::{argsort_by, par_argsort_by}; +use crate::utils::sorting::par_argsort_by; use log::debug; use log::info; use log::trace; diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index f41e963..ec489b7 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -4,23 +4,19 @@ use super::quad_index::{ use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{expand_and_split_frame, ExpandedFrameSlice}; use crate::models::frames::raw_peak::RawPeak; -use crate::models::frames::single_quad_settings::expand_quad_settings; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::models::queries::FragmentGroupIndexQuery; use crate::models::queries::PrecursorIndexQuery; use crate::traits::indexed_data::IndexedData; -use crate::utils::compress_explode::explode_vec; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; use log::{debug, info, trace}; use rayon::prelude::*; use serde::Serialize; -use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; -use std::sync::Arc; use std::time::Instant; use timsrust::converters::{ ConvertableDomain, Frame2RtConverter, Scan2ImConverter, Tof2MzConverter, From 3564fcc0e5dbe34331c08af99c0c58fa54673428 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Tue, 22 Oct 2024 07:40:05 -0700 Subject: [PATCH 04/30] feat(wip)!: addition of simpler index n ...a lot --- src/models/frames/expanded_frame.rs | 163 ++++++++++++++ src/models/indices/expanded_raw_index/mod.rs | 1 + .../indices/expanded_raw_index/model.rs | 77 +++++++ src/models/indices/mod.rs | 1 + .../transposed_quad_index/quad_index.rs | 1 - .../quad_splitted_transposed_index.rs | 185 ++++++++++++---- src/utils/frame_processing.rs | 199 ++++++++++++++++++ src/utils/mod.rs | 2 + src/utils/tolerance_ranges.rs | 38 ++++ 9 files changed, 626 insertions(+), 41 deletions(-) create mode 100644 src/models/indices/expanded_raw_index/model.rs create mode 100644 src/utils/frame_processing.rs create mode 100644 src/utils/tolerance_ranges.rs diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index a58d555..1570d8a 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,13 +1,21 @@ +use std::collections::HashMap; use std::sync::Arc; use super::single_quad_settings::{ expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, }; +use itertools::Itertools; +use rayon::iter::IntoParallelIterator; +use rayon::prelude::*; +use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; use crate::sort_by_indices_multi; use crate::utils::compress_explode::explode_vec; +use crate::utils::frame_processing::lazy_centroid_weighted_frame; use crate::utils::sorting::argsort_by; +use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; +use log::debug; /// A frame after expanding the mobility data and re-sorting it by tof. #[derive(Debug, Clone)] @@ -184,6 +192,161 @@ impl ExpandedFrame { } } +pub fn expand_and_arrange_frames( + frames: Vec, +) -> HashMap, Vec> { + let mut out = HashMap::new(); + let split: Vec = frames + .into_par_iter() + .flat_map(|frame| expand_and_split_frame(frame)) + .collect(); + for es in split { + out.entry(es.quadrupole_settings) + .or_insert(Vec::new()) + .push(es); + } + + // Finally sort each of them internally by retention time. + for (_, es) in out.iter_mut() { + es.sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); + } + out +} + +pub fn par_expand_and_centroid_frames( + frames: Vec, + ims_tol_pct: f64, + mz_tol_ppm: f64, + ims_converter: &Scan2ImConverter, + mz_converter: &Tof2MzConverter, +) -> HashMap, Vec> { + let split_frames = expand_and_arrange_frames(frames); + + let out: HashMap, Vec> = split_frames + .into_iter() + .map(|(qs, frameslices)| { + // NOTE: Since the the centroiding runs in paralel over the windows, its ok if this + // outer loop is done in series. + let start_peaks: usize = frameslices.iter().map(|x| x.len()).sum(); + let centroided = par_lazy_centroid_frameslices( + &frameslices, + 3, + ims_tol_pct, + mz_tol_ppm, + ims_converter, + mz_converter, + ); + let end_peaks: usize = centroided.iter().map(|x| x.len()).sum(); + debug!( + "Peak counts for quad {:?}: raw={}/centroid={}", + qs, start_peaks, end_peaks + ); + (qs, centroided) + }) + .collect(); + + out +} + +fn centroid_frameslice_window( + frameslices: &[ExpandedFrameSlice], + ims_tol_pct: f64, + mz_tol_ppm: f64, + ims_converter: &Scan2ImConverter, + mz_converter: &Tof2MzConverter, +) -> ExpandedFrameSlice { + assert!(frameslices.len() > 1, "Expected at least 2 frameslices"); + let reference_index = frameslices.len() / 2; + + // this is A LOT of cloning ... look into whether it is needed. + + let mut tof_array: Vec = frameslices + .iter() + .flat_map(|x| x.tof_indices.clone()) + .collect(); + + let mut ims_array: Vec = frameslices + .iter() + .flat_map(|x| x.scan_numbers.clone()) + .collect(); + let mut weight_array: Vec = frameslices + .iter() + .flat_map(|x| x.intensities.clone()) + .collect(); + let mut intensity_array: Vec = frameslices + .iter() + .enumerate() + .flat_map(|(i, x)| { + if i == reference_index { + x.intensities.clone() + } else { + vec![0u32; x.tof_indices.len()] + } + }) + .collect(); + + let mut tof_order = argsort_by(&tof_array, |x| *x); + sort_by_indices_multi!( + &mut tof_order, + &mut tof_array, + &mut ims_array, + &mut weight_array, + &mut intensity_array + ); + + let ((mzs, intensities), imss) = lazy_centroid_weighted_frame( + &tof_array, + &ims_array, + &weight_array, + &intensity_array, + |&tof| tof_tol_range(tof, mz_tol_ppm, mz_converter), + |&scan| scan_tol_range(scan, ims_tol_pct, ims_converter), + ); + + ExpandedFrameSlice { + tof_indices: mzs, + scan_numbers: imss, + intensities, + frame_index: frameslices[reference_index].frame_index, + rt: frameslices[reference_index].rt, + acquisition_type: frameslices[reference_index].acquisition_type, + ms_level: frameslices[reference_index].ms_level, + quadrupole_settings: frameslices[reference_index].quadrupole_settings, + intensity_correction_factor: frameslices[reference_index].intensity_correction_factor, + window_group: frameslices[reference_index].window_group, + window_subindex: frameslices[reference_index].window_subindex, + } +} + +pub fn par_lazy_centroid_frameslices( + frameslices: &[ExpandedFrameSlice], + window_width: usize, + ims_tol_pct: f64, + mz_tol_ppm: f64, + ims_converter: &Scan2ImConverter, + mz_converter: &Tof2MzConverter, +) -> Vec { + assert!( + frameslices + .iter() + .tuple_windows() + .map(|(a, b)| { a.rt < b.rt && a.quadrupole_settings == b.quadrupole_settings }) + .all(|x| x), + "All frames should be sorted by rt and have the same quad settings" + ); + + assert!(frameslices.len() > window_width); + + let local_lambda = |fss: &[ExpandedFrameSlice]| { + centroid_frameslice_window(fss, ims_tol_pct, mz_tol_ppm, ims_converter, mz_converter) + }; + + frameslices + .par_windows(window_width) + .map(|window| local_lambda(window)) + .collect() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/models/indices/expanded_raw_index/mod.rs b/src/models/indices/expanded_raw_index/mod.rs index e69de29..ee2d47a 100644 --- a/src/models/indices/expanded_raw_index/mod.rs +++ b/src/models/indices/expanded_raw_index/mod.rs @@ -0,0 +1 @@ +mod model; diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs new file mode 100644 index 0000000..10155f6 --- /dev/null +++ b/src/models/indices/expanded_raw_index/model.rs @@ -0,0 +1,77 @@ +use crate::models::frames::expanded_frame::ExpandedFrame; +use crate::models::frames::expanded_frame::{ + expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, +}; +use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; +use log::debug; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; +use timsrust::converters::{ + ConvertableDomain, Frame2RtConverter, Scan2ImConverter, Tof2MzConverter, +}; +use timsrust::readers::{FrameReader, FrameReaderError, MetadataReader}; +use timsrust::{QuadrupoleSettings, TimsRustError}; + +type QuadSettingsIndex = usize; + +#[derive(Debug, Default)] +struct ExpandedRawFrameIndex { + bundled_ms1_frames: Vec, + bundled_frames: HashMap>, + flat_quad_settings: Vec, + rt_converter: Frame2RtConverter, + pub mz_converter: Tof2MzConverter, + pub im_converter: Scan2ImConverter, +} + +impl ExpandedRawFrameIndex { + pub fn from_tdf_path(path: &str) -> Result { + let file_reader = FrameReader::new(path)?; + + let sql_path = std::path::Path::new(path).join("analysis.tdf"); + let meta_converters = MetadataReader::new(&sql_path)?; + + let st = Instant::now(); + let all_frames = file_reader.get_all().into_iter().flatten().collect(); + let read_elap = st.elapsed(); + debug!("Reading all frames took {:#?}", read_elap); + let st = Instant::now(); + let centroided_split_frames = par_expand_and_centroid_frames( + all_frames, + 1.5, + 15.0, + &meta_converters.im_converter, + &meta_converters.mz_converter, + ); + let centroided_elap = st.elapsed(); + debug!("Centroiding took {:#?}", centroided_elap); + + let mut out_ms2_frames = HashMap::new(); + let mut out_ms1_frames: Option> = None; + + centroided_split_frames + .into_iter() + .for_each(|(q, frameslices)| match q { + None => { + out_ms1_frames = Some(frameslices); + } + Some(q) => { + out_ms2_frames.insert(q, frameslices); + } + }); + + let mut flat_quad_settings = out_ms2_frames.keys().cloned().collect(); + + let out = Self { + bundled_ms1_frames: out_ms1_frames.expect("At least one ms1 frame should be present"), + bundled_frames: out_ms2_frames, + flat_quad_settings, + rt_converter: meta_converters.rt_converter, + mz_converter: meta_converters.mz_converter, + im_converter: meta_converters.im_converter, + }; + + Ok(out) + } +} diff --git a/src/models/indices/mod.rs b/src/models/indices/mod.rs index f912a50..b8cb6e1 100644 --- a/src/models/indices/mod.rs +++ b/src/models/indices/mod.rs @@ -1,2 +1,3 @@ +pub mod expanded_raw_index; pub mod raw_file_index; pub mod transposed_quad_index; diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index beaf7cf..8b4f2cf 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -9,7 +9,6 @@ use crate::utils::sorting::par_argsort_by; use log::debug; use log::info; use log::trace; -use rayon::prelude::*; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::time::Instant; diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index ec489b7..bdc718c 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -2,7 +2,9 @@ use super::quad_index::{ FrameRTTolerance, PeakInQuad, TransposedQuadIndex, TransposedQuadIndexBuilder, }; use crate::models::elution_group::ElutionGroup; -use crate::models::frames::expanded_frame::{expand_and_split_frame, ExpandedFrameSlice}; +use crate::models::frames::expanded_frame::{ + expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, +}; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::models::queries::FragmentGroupIndexQuery; @@ -50,13 +52,16 @@ impl Debug for QuadSplittedTransposedIndex { } impl QuadSplittedTransposedIndex { - pub fn query_peaks( + pub fn query_peaks( &self, tof_range: (u32, u32), precursor_mz_range: (f64, f64), scan_range: Option<(usize, usize)>, rt_range: Option, - ) -> impl Iterator + '_ { + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { trace!( "QuadSplittedTransposedIndex::query_peaks tof_range: {:?}, scan_range: {:?}, rt_range: {:?}, precursor_mz_range: {:?}", tof_range, @@ -68,13 +73,29 @@ impl QuadSplittedTransposedIndex { .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); trace!("matching_quads: {:?}", matching_quads); + self.query_precursor_peaks(&matching_quads, tof_range, scan_range, rt_range, f); + } + fn query_precursor_peaks( + &self, + matching_quads: &[SingleQuadrupoleSetting], + tof_range: (u32, u32), + scan_range: Option<(usize, usize)>, + rt_range: Option, + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { let rt_range = rt_range.map(|x| x.to_frame_index_range(&self.rt_converter)); - matching_quads.into_iter().flat_map(move |qs| { - let tqi = self.fragment_indices.get(&qs).unwrap(); + for quad in matching_quads { + let tqi = self + .fragment_indices + .get(quad) + .expect("Only existing quads should be queried."); tqi.query_peaks(tof_range, scan_range, rt_range) - }) + .for_each(&mut *f); + } } fn get_matching_quad_settings( @@ -89,13 +110,6 @@ impl QuadSplittedTransposedIndex { && (qs.ranges.isolation_high >= precursor_mz_range.0) }) .filter(move |qs| { - // if let Some(scan_range) = scan_range { - // // This is done for sanity tbh ... sometimes they get flipped - // // bc the lowest scan is actually the highest 1/k0. - // } else { - // true - // } - // match scan_range { Some((min_scan, max_scan)) => { assert!(qs.ranges.scan_start <= qs.ranges.scan_end); @@ -242,6 +256,20 @@ impl QuadSplittedTransposedIndex { debug!("{}", out); Ok(out) } + + pub fn from_path_centroided(path: &str) -> Result { + let st = Instant::now(); + info!( + "Building CENTROIDED transposed quad index from path {}", + path + ); + let tmp = QuadSplittedTransposedIndexBuilder::from_path_centroided(path)?; + let out = tmp.build(); + let elapsed = st.elapsed(); + info!("Transposed CENTROIDED quad index built in {:#?}", elapsed); + debug!("{}", out); + Ok(out) + } } #[derive(Debug, Clone, Default)] @@ -266,24 +294,28 @@ impl QuadSplittedTransposedIndexBuilder { let expanded_slices = expand_and_split_frame(frame); for es in expanded_slices.into_iter() { - // Add key if it doesnt exist ... - - self.indices - .entry(es.quadrupole_settings) - .or_insert(TransposedQuadIndexBuilder::new(es.quadrupole_settings)); - - self.added_peaks += es.len() as u64; self.add_frame_slice(es); } } fn add_frame_slice(&mut self, frame_slice: ExpandedFrameSlice) { + // Add key if it doesnt exist ... + self.indices + .entry(frame_slice.quadrupole_settings) + .or_insert(TransposedQuadIndexBuilder::new( + frame_slice.quadrupole_settings, + )); + + self.added_peaks += frame_slice.len() as u64; self.indices .get_mut(&frame_slice.quadrupole_settings) .unwrap() .add_frame_slice(frame_slice); } + // TODO: I think i should split this into two functions, one that starts the builder + // and one that adds the frameslices, maybe even have a config struct that dispatches + // the right preprocessing steps. fn from_path(path: &str) -> Result { let file_reader = FrameReader::new(path)?; @@ -323,6 +355,64 @@ impl QuadSplittedTransposedIndexBuilder { Ok(final_out) } + fn from_path_centroided(path: &str) -> Result { + let file_reader = FrameReader::new(path)?; + + let sql_path = std::path::Path::new(path).join("analysis.tdf"); + let meta_converters = MetadataReader::new(&sql_path)?; + + let out_meta_converters = meta_converters.clone(); + let mut final_out = Self { + indices: HashMap::new(), + rt_converter: Some(meta_converters.rt_converter), + mz_converter: Some(meta_converters.mz_converter), + im_converter: Some(meta_converters.im_converter), + metadata: Some(out_meta_converters), + added_peaks: 0, + }; + + let st = Instant::now(); + let all_frames = file_reader.get_all().into_iter().flatten().collect(); + let read_elap = st.elapsed(); + debug!("Reading all frames took {:#?}", read_elap); + let st = Instant::now(); + let centroided_split_frames = par_expand_and_centroid_frames( + all_frames, + 1.5, + 15.0, + &meta_converters.im_converter, + &meta_converters.mz_converter, + ); + let centroided_elap = st.elapsed(); + debug!("Centroiding took {:#?}", centroided_elap); + + let st = Instant::now(); + let out2: Result, TimsRustError> = centroided_split_frames + .into_par_iter() + .map(|(q, frameslices)| { + let mut out = Self::new(); + for frameslice in frameslices { + out.add_frame_slice(frameslice); + } + Ok(out) + }) + .collect(); + + let out2 = out2?.into_iter().fold(Self::new(), |mut x, y| { + x.fold(y); + x + }); + final_out.fold(out2); + let build_elap = st.elapsed(); + + info!( + "Reading all frames took {:#?}, centroiding took {:#?}, building took {:#?}", + read_elap, centroided_elap, build_elap + ); + + Ok(final_out) + } + pub fn fold(&mut self, other: Self) { for (qs, builder) in other.indices.into_iter() { self.indices @@ -388,13 +478,15 @@ impl .mz_index_ranges .iter() .flat_map(|(_, tof_range)| { + let mut local_vec = vec![]; self.query_peaks( *tof_range, precursor_mz_range, scan_range, frame_index_range, - ) - .map(RawPeak::from) + &mut |x| local_vec.push(RawPeak::from(x)), + ); + local_vec.into_iter() }) .collect() } @@ -422,8 +514,8 @@ impl precursor_mz_range, scan_range, frame_index_range, - ) - .for_each(|peak| aggregator.add(&RawPeak::from(peak))); + &mut |peak| aggregator.add(&RawPeak::from(peak)), + ); }) } @@ -448,8 +540,13 @@ impl )); for (_, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_peaks(tof_range, precursor_mz_range, scan_range, frame_index_range) - .for_each(|peak| agg.add(&RawPeak::from(peak))); + self.query_peaks( + tof_range, + precursor_mz_range, + scan_range, + frame_index_range, + &mut |peak| agg.add(&RawPeak::from(peak)), + ); } }); } @@ -475,17 +572,16 @@ impl .mz_index_ranges .iter() .flat_map(|(fh, tof_range)| { - let out: Vec<(RawPeak, FH)> = self - .query_peaks( - *tof_range, - precursor_mz_range, - scan_range, - frame_index_range, - ) - .map(|x| (RawPeak::from(x), *fh)) - .collect(); + let mut local_vec: Vec<(RawPeak, FH)> = vec![]; + self.query_peaks( + *tof_range, + precursor_mz_range, + scan_range, + frame_index_range, + &mut |x| local_vec.push((RawPeak::from(x), *fh)), + ); - out + local_vec }) .collect() } @@ -513,8 +609,8 @@ impl precursor_mz_range, scan_range, frame_index_range, - ) - .for_each(|peak| aggregator.add(&(RawPeak::from(peak), *fh))); + &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), + ); }) } @@ -538,9 +634,18 @@ impl fragment_query.precursor_query.frame_index_range, )); + let local_quad_vec: Vec = self + .get_matching_quad_settings(precursor_mz_range, scan_range) + .collect(); + for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_peaks(tof_range, precursor_mz_range, scan_range, frame_index_range) - .for_each(|peak| agg.add(&(RawPeak::from(peak), fh))); + self.query_precursor_peaks( + &local_quad_vec, + tof_range, + scan_range, + frame_index_range, + &mut |peak| agg.add(&(RawPeak::from(peak), fh)), + ); } }); } diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs new file mode 100644 index 0000000..d6f832a --- /dev/null +++ b/src/utils/frame_processing.rs @@ -0,0 +1,199 @@ +use log::warn; +use std::cmp::Ordering; +use std::ops::RangeInclusive; +use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; + +pub fn squash_frame( + mz_array: &[f32], + intensity_array: &[f32], + tol_ppm: f32, +) -> (Vec, Vec) { + // Make sure the mz array is sorted + assert!(mz_array.windows(2).all(|x| x[0] <= x[1])); + + let arr_len = mz_array.len(); + let mut touched = vec![false; arr_len]; + let mut global_num_touched = 0; + + let mut order: Vec = (0..arr_len).collect(); + order.sort_unstable_by(|&a, &b| { + intensity_array[b] + .partial_cmp(&intensity_array[a]) + .unwrap_or(Ordering::Equal) + }); + + let mut agg_mz = vec![0.0; arr_len]; + let mut agg_intensity = vec![0.0; arr_len]; + + let utol = tol_ppm / 1e6; + + for &idx in &order { + if touched[idx] { + continue; + } + + let mz = mz_array[idx]; + let da_tol = mz * utol; + let left_e = mz - da_tol; + let right_e = mz + da_tol; + + let ss_start = mz_array.partition_point(|&x| x < left_e); + let ss_end = mz_array.partition_point(|&x| x <= right_e); + + let slice_width = ss_end - ss_start; + let local_num_touched = touched[ss_start..ss_end].iter().filter(|&&x| x).count(); + let local_num_untouched = slice_width - local_num_touched; + + if local_num_touched == slice_width { + continue; + } + + let mut curr_intensity = 0.0; + let mut curr_weighted_mz = 0.0; + + for i in ss_start..ss_end { + if !touched[i] && intensity_array[i] > 0.0 { + curr_intensity += intensity_array[i]; + curr_weighted_mz += mz_array[i] * intensity_array[i]; + } + } + + if curr_intensity > 0.0 { + curr_weighted_mz /= curr_intensity; + + agg_intensity[idx] = curr_intensity; + agg_mz[idx] = curr_weighted_mz; + + touched[ss_start..ss_end].iter_mut().for_each(|x| *x = true); + global_num_touched += local_num_untouched; + } + + if global_num_touched == arr_len { + break; + } + } + + // Drop the zeros and sort + let mut result: Vec<(f32, f32)> = agg_mz + .into_iter() + .zip(agg_intensity.into_iter()) + .filter(|&(mz, intensity)| mz > 0.0 && intensity > 0.0) + .collect(); + + result.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + + result.into_iter().unzip() +} + +pub type TofIntensityVecs = (Vec, Vec); +pub type CentroidedVecs = (TofIntensityVecs, Vec); + +pub fn lazy_centroid_weighted_frame( + tof_array: &[u32], + ims_array: &[usize], + weight_array: &[u32], + intensity_array: &[u32], + tof_tol_range_fn: impl Fn(&u32) -> RangeInclusive, + ims_tol_range_fn: impl Fn(&usize) -> RangeInclusive, +) -> CentroidedVecs { + let arr_len = tof_array.len(); + const MIN_WEIGHT_PRESERVE: u64 = 50; + + // TODO make the asserts optional at compile time. + assert!( + tof_array.windows(2).all(|x| x[0] <= x[1]), + "Expected tof array to be sorted" + ); + assert_eq!( + arr_len, + intensity_array.len(), + "Expected tof and intensity arrays to be the same length" + ); + assert_eq!( + arr_len, + weight_array.len(), + "Expected tof and weight arrays to be the same length" + ); + assert_eq!( + arr_len, + ims_array.len(), + "Expected tof and ims arrays to be the same length" + ); + + let mut touched = vec![false; arr_len]; + let mut global_num_touched = 0; + + // We will be iterating in decreasing order of intensity + let mut order: Vec = (0..arr_len).collect(); + order.sort_unstable_by(|&a, &b| { + weight_array[b] + .partial_cmp(&weight_array[a]) + .unwrap_or(Ordering::Equal) + }); + + let mut agg_tof = vec![0; arr_len]; + let mut agg_intensity = vec![0; arr_len]; + // We will not be returning the weights. + // let mut agg_weight = vec![0; arr_len]; + let mut agg_ims = vec![0; arr_len]; + + for idx in order { + if touched[idx] { + continue; + } + + let tof = tof_array[idx]; + let ims = ims_array[idx]; + let tof_range = tof_tol_range_fn(&tof); + let ims_range = ims_tol_range_fn(&ims); + + let ss_start = tof_array.partition_point(|x| x < tof_range.start()); + let ss_end = tof_array.partition_point(|x| x <= tof_range.end()); + + let mut curr_intensity = 0u64; + let mut curr_weight = 0u64; + let mut curr_agg_tof = 0u64; + let mut curr_agg_ims = 0u64; + + for i in ss_start..ss_end { + if !touched[i] && intensity_array[i] > 0 && ims_range.contains(&ims_array[i]) { + let local_weight = weight_array[i] as u64; + curr_intensity += intensity_array[i] as u64; + curr_agg_tof += tof_array[i] as u64 * local_weight; + curr_agg_ims += ims_array[i] as u64 * local_weight; + curr_weight += local_weight; + } + } + + if curr_intensity > 0 && curr_weight > 0 { + agg_intensity[idx] = u32::try_from(curr_intensity).expect("Expected to fit in u32"); + agg_tof[idx] = (curr_agg_tof / curr_weight) as u32; + agg_ims[idx] = (curr_agg_ims / curr_weight) as usize; + + for i in ss_start..ss_end { + touched[i] = true; + global_num_touched += 1; + } + } + + if global_num_touched == arr_len { + break; + } + } + + // Drop the zeros and sort by mz (tof) + let mut res: Vec<((u32, u32), usize)> = agg_tof + .into_iter() + .zip(agg_intensity.into_iter()) + .zip(agg_ims.into_iter()) + .filter(|&((_, intensity), ims)| (intensity > 0) & (ims > 0)) + .collect(); + + res.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + let output_len = res.len(); + if arr_len > 500 && (output_len == arr_len) { + warn!("Output length is the same as input length, this is probably a bug"); + } + + res.into_iter().unzip() +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4807e91..1d8244c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,6 @@ pub mod compress_explode; pub mod display; +pub mod frame_processing; pub mod math; pub mod sorting; +pub mod tolerance_ranges; diff --git a/src/utils/tolerance_ranges.rs b/src/utils/tolerance_ranges.rs new file mode 100644 index 0000000..dcb3978 --- /dev/null +++ b/src/utils/tolerance_ranges.rs @@ -0,0 +1,38 @@ +use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; + +use std::ops::RangeInclusive; + +pub fn ppm_tol_range(elem: f64, tol_ppm: f64) -> RangeInclusive { + let utol = tol_ppm / 1e6; + let left_e = elem - utol; + let right_e = elem + utol; + left_e..=right_e +} + +pub fn tof_tol_range(tof: u32, tol_ppm: f64, converter: &Tof2MzConverter) -> RangeInclusive { + let mz = converter.convert(tof); + let mz_range = ppm_tol_range(mz, tol_ppm); + RangeInclusive::new( + converter.invert(*mz_range.start()).round() as u32, + converter.invert(*mz_range.end()).round() as u32, + ) +} + +pub fn scan_tol_range( + scan: usize, + tol_pct: f64, + converter: &Scan2ImConverter, +) -> RangeInclusive { + let im = converter.convert(scan as f64); + let im_range = ppm_tol_range(im, tol_pct); + let scan_min = converter.invert(*im_range.start()).round() as usize; + let scan_max = converter.invert(*im_range.end()).round() as usize; + // Note I need to do this here bc the conversion between scan numbers and ion + // mobilities is not monotonically increasing. IN OTHER WORDS, lower scan numbers + // are higher 1/k0.... But im not sure if they are ALWAYS inversely proportional. + if scan_min > scan_max { + scan_max..=scan_min + } else { + scan_min..=scan_max + } +} From 833bea0b16198dc7ca800c683cfdb8f7f734cd55 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Wed, 23 Oct 2024 17:43:49 -0700 Subject: [PATCH 05/30] (chore,feat): Added new index and refactored benchmarks --- Cargo.lock | 186 +--------- Cargo.toml | 8 +- benches/benchmark_indices.rs | 313 +++++++++-------- src/models/adapters.rs | 90 +++++ src/models/frames/expanded_frame.rs | 162 ++++++--- src/models/frames/expanded_window_group.rs | 6 +- src/models/frames/mod.rs | 1 + src/models/frames/peak_in_quad.rs | 20 ++ src/models/frames/single_quad_settings.rs | 40 +++ src/models/indices/expanded_raw_index/mod.rs | 2 + .../indices/expanded_raw_index/model.rs | 326 +++++++++++++++++- .../transposed_quad_index/quad_index.rs | 36 +- .../quad_splitted_transposed_index.rs | 144 +------- src/models/mod.rs | 1 + 14 files changed, 773 insertions(+), 562 deletions(-) create mode 100644 src/models/adapters.rs create mode 100644 src/models/frames/peak_in_quad.rs diff --git a/Cargo.lock b/Cargo.lock index 78f1f39..2bdd6b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,12 +61,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.15" @@ -273,12 +267,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.1.7" @@ -307,33 +295,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clap" version = "4.5.17" @@ -428,42 +389,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -619,12 +544,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "humantime" version = "2.1.0" @@ -683,32 +602,12 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" -[[package]] -name = "is-terminal" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -981,12 +880,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "oorandom" -version = "11.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" - [[package]] name = "ordered-float" version = "2.10.1" @@ -1039,34 +932,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "plotters" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" - -[[package]] -name = "plotters-svg" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" -dependencies = [ - "plotters-backend", -] - [[package]] name = "portable-atomic" version = "1.7.0" @@ -1230,15 +1095,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "semver" version = "1.0.23" @@ -1365,10 +1221,9 @@ name = "timsquery" version = "0.4.0" dependencies = [ "clap", - "criterion", "env_logger", "indicatif", - "itertools 0.13.0", + "itertools", "log", "rand", "rand_chacha", @@ -1406,16 +1261,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "twox-hash" version = "1.6.3" @@ -1456,16 +1301,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1526,25 +1361,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -dependencies = [ - "windows-sys", -] - [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 5b5ae85..c65ac8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,6 @@ log = "0.4.22" env_logger = "0.11.5" -[dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } - [features] clap = ["dep:clap"] build-binary = ["clap"] @@ -33,10 +30,9 @@ build-binary = ["clap"] name = "timsquery" required-features = ["build-binary"] -[[bench]] +[[bin]] name = "benchmark_indices" -harness = false - +path = "benches/benchmark_indices.rs" [profile.release] opt-level = 3 diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 79f0adc..84a355e 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -1,29 +1,64 @@ -use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use serde::Serialize; use std::collections::HashMap; - -use std::hint::black_box; +use std::fs::File; +use std::time::{Duration, Instant}; use timsquery::{ models::{ aggregators::RawPeakIntensityAggregator, indices::{ - raw_file_index::RawFileIndex, transposed_quad_index::QuadSplittedTransposedIndex, + expanded_raw_index::ExpandedRawFrameIndex, raw_file_index::RawFileIndex, + transposed_quad_index::QuadSplittedTransposedIndex, }, }, - queriable_tims_data::queriable_tims_data::{query_indexed, query_multi_group}, + queriable_tims_data::queriable_tims_data::query_multi_group, traits::tolerance::DefaultTolerance, ElutionGroup, }; +const NUM_ELUTION_GROUPS: usize = 500; +const NUM_ITERATIONS: usize = 1; + +#[derive(Debug, Serialize)] +struct BenchmarkIteration { + iteration: usize, + duration_seconds: f64, +} + +#[derive(Debug, Serialize)] +struct BenchmarkResult { + name: String, + context: String, + iterations: Vec, + mean_duration_seconds: f64, +} + +#[derive(Debug, Serialize)] +struct BenchmarkReport { + settings: BenchmarkSettings, + results: Vec, + metadata: BenchmarkMetadata, +} + +#[derive(Debug, Serialize)] +struct BenchmarkSettings { + num_elution_groups: usize, + num_iterations: usize, +} + +#[derive(Debug, Serialize)] +struct BenchmarkMetadata { + basename: String, +} + +fn duration_to_seconds(duration: Duration) -> f64 { + duration.as_secs() as f64 + duration.subsec_nanos() as f64 * 1e-9 +} + fn get_file_from_env() -> (String, String) { - // Read from environment variable the raw file path to use - // env var name: TIMS_DATA_FILE - let raw_file_path = std::env::var("TIMS_DATA_FILE"); - let raw_file_path = match raw_file_path { - Ok(path) => path, - Err(_) => panic!("TIMS_DATA_FILE environment variable not set"), - }; + let raw_file_path = + std::env::var("TIMS_DATA_FILE").expect("TIMS_DATA_FILE environment variable not set"); let basename = std::path::Path::new(&raw_file_path) .file_stem() @@ -35,8 +70,7 @@ fn get_file_from_env() -> (String, String) { (raw_file_path, basename) } -const NUM_ELUTION_GROUPS: usize = 500; -fn build_elution_groups(raw_file_path: String) -> Vec> { +fn build_elution_groups() -> Vec> { const NUM_FRAGMENTS: usize = 10; const MAX_RT: f32 = 22.0 * 60.0; const MAX_MOBILITY: f32 = 1.5; @@ -44,187 +78,148 @@ fn build_elution_groups(raw_file_path: String) -> Vec> { const MAX_MZ: f64 = 1000.0; const MIN_MZ: f64 = 300.0; - let mut out_egs: Vec> = Vec::with_capacity(NUM_ELUTION_GROUPS); + let mut out_egs = Vec::with_capacity(NUM_ELUTION_GROUPS); let mut rng = ChaCha8Rng::seed_from_u64(43u64); for i in 1..NUM_ELUTION_GROUPS { - // Rand f32/64 are number from 0-1 let rt = rng.gen::() * MAX_RT; let mobility = rng.gen::() * (MAX_MOBILITY - MIN_MOBILITY) + MIN_MOBILITY; let mz = rng.gen::() * (MAX_MZ - MIN_MZ) + MIN_MZ; let mut fragment_mzs = HashMap::with_capacity(NUM_FRAGMENTS); - for ii in 0..NUM_FRAGMENTS { let fragment_mz = rng.gen::() * (MAX_MZ - MIN_MZ) + MIN_MZ; - // let fragment_charge = rng.gen::() * 3 + 1; fragment_mzs.insert(ii as u64, fragment_mz); } - // rand u8 is number from 0-255 ... which is not amazing for us ... - // let precursor_charge = rng.gen::() * 3 + 1; - let precursor_charge = 2; - out_egs.push(ElutionGroup { id: i as u64, rt_seconds: rt, mobility, precursor_mz: mz, - precursor_charge, + precursor_charge: 2, fragment_mzs, }); } out_egs } -fn criterion_benchmark(c: &mut Criterion) { - let (raw_file_path, basename) = get_file_from_env(); - let mut group = c.benchmark_group("Encoding Time"); - - group.sample_size(10); - group.bench_function( - BenchmarkId::new("TransposedQuadIndex", basename.clone()), - |b| { - b.iter_batched( - || {}, - |()| { - QuadSplittedTransposedIndex::from_path(&black_box(raw_file_path.clone())) - .unwrap() - }, - BatchSize::SmallInput, - ) - }, - ); - group.bench_function(BenchmarkId::new("RawFileIndex", basename.clone()), |b| { - b.iter_batched( - || {}, - |()| RawFileIndex::from_path(&black_box(raw_file_path.clone())).unwrap(), - BatchSize::SmallInput, - ) +fn with_benchmark(name: &str, context: &str, f: impl Fn()) -> BenchmarkResult { + let mut iterations = Vec::with_capacity(NUM_ITERATIONS); + for i in 0..NUM_ITERATIONS { + println!("{name} iteration {i}"); + let start = Instant::now(); + f(); + let duration = start.elapsed(); + + iterations.push(BenchmarkIteration { + iteration: i + 1, + duration_seconds: duration_to_seconds(duration), + }); + } + + let mean = iterations.iter().map(|i| i.duration_seconds).sum::() / NUM_ITERATIONS as f64; + BenchmarkResult { + name: name.to_string(), + context: context.to_string(), + iterations, + mean_duration_seconds: mean, + } +} + +fn run_encoding_benchmark(raw_file_path: &str) -> Vec { + let rfi = with_benchmark("RawFileIndex", "Encoding", || { + RawFileIndex::from_path(raw_file_path).unwrap(); + }); + let erfi = with_benchmark("ExpandedRawFileIndex", "Encoding", || { + ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); + }); + let tqi = with_benchmark("TransposedQuadIndex", "Encoding", || { + QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); }); - group.finish(); + vec![tqi, rfi, erfi] } -macro_rules! add_bench_random { - ($group:expr, $raw_file_path:expr, $basename:expr, $name:literal, $index_type:ty, $tolerance_type:ty, ) => { - $group.bench_function(BenchmarkId::new($name, $basename.clone()), |b| { - b.iter_batched( - || { - ( - <$index_type>::from_path(&($raw_file_path.clone())).unwrap(), - build_elution_groups($raw_file_path.clone()), - <$tolerance_type>::default(), - ) - }, - |(index, query_groups, tolerance)| { - let local_lambda = |elution_group: &ElutionGroup| { - query_indexed( - &index, - &RawPeakIntensityAggregator::new, - &index, - &tolerance, - &elution_group, - ) - }; - for elution_group in query_groups { - let query_results = local_lambda(&elution_group); - black_box((|_query_results| false)(query_results)); - } - }, - BatchSize::PerIteration, - ) - }); - }; +fn run_batch_access_benchmark(raw_file_path: &str) -> Vec { + let query_groups = build_elution_groups(); + let tolerance = DefaultTolerance::default(); + + let rfi_index = RawFileIndex::from_path(raw_file_path).unwrap(); + let rfi = with_benchmark("RawFileIndex", "BatchAccess", || { + let _ = query_multi_group( + &rfi_index, + &rfi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + }); + std::mem::drop(rfi_index); + + let erfi_index = ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); + let erfi = with_benchmark("ExpandedRawFileIndex", "BatchAccess", || { + let _ = query_multi_group( + &erfi_index, + &erfi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + }); + std::mem::drop(erfi_index); + + let tqi_index = QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); + let tqi = with_benchmark("TransposedQuadIndex", "BatchAccess", || { + let _ = query_multi_group( + &tqi_index, + &tqi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + }); + std::mem::drop(tqi_index); + + vec![tqi, rfi, erfi] } -macro_rules! add_bench_optim { - ($group:expr, $raw_file_path:expr, $basename:expr, $name:literal, $index_type:ty, $tolerance_type:ty, $query_func:expr,) => { - $group.bench_function(BenchmarkId::new($name, $basename.clone()), |b| { - b.iter_batched( - || { - ( - <$index_type>::from_path(&($raw_file_path.clone())).unwrap(), - build_elution_groups($raw_file_path.clone()), - <$tolerance_type>::default(), - ) - }, - |(index, query_groups, tolerance)| { - let qr = query_multi_group( - &index, - &index, - &tolerance, - &query_groups, - &RawPeakIntensityAggregator::new, - ); - black_box((|_qr| false)(qr)); - }, - BatchSize::PerIteration, - ) - }); + +fn write_results(results: Vec, basename: &str) -> std::io::Result<()> { + let report = BenchmarkReport { + settings: BenchmarkSettings { + num_elution_groups: NUM_ELUTION_GROUPS, + num_iterations: NUM_ITERATIONS, + }, + results, + metadata: BenchmarkMetadata { + basename: basename.to_string(), + }, }; + + let file = File::create(format!("benchmark_results_{}.json", basename))?; + serde_json::to_writer_pretty(file, &report)?; + Ok(()) } -fn thoughput_benchmark_random(c: &mut Criterion) { +fn main() { let (raw_file_path, basename) = get_file_from_env(); - let mut group = c.benchmark_group("RandomAccessThroughput"); - group.significance_level(0.05).sample_size(10); - group.throughput(criterion::Throughput::Elements(NUM_ELUTION_GROUPS as u64)); - - add_bench_random!( - group, - raw_file_path, - basename, - "TransposedQuadIndex", - QuadSplittedTransposedIndex, - DefaultTolerance, - ); + let mut all_results: Vec = vec![]; - add_bench_random!( - group, - raw_file_path, - basename, - "RawFileIndex", - RawFileIndex, - DefaultTolerance, + println!( + "Run Settings: Elution groups={}, Iterations={}, basename={}", + NUM_ELUTION_GROUPS, NUM_ITERATIONS, basename ); - group.finish(); -} - -fn thoughput_benchmark_optim(c: &mut Criterion) { - env_logger::init(); - let (raw_file_path, basename) = get_file_from_env(); - let mut group = c.benchmark_group("BatchAccessThroughput"); - group.significance_level(0.05).sample_size(10); - group.throughput(criterion::Throughput::Elements(NUM_ELUTION_GROUPS as u64)); - - add_bench_optim!( - group, - raw_file_path, - basename, - "TransposedQuadIndex", - QuadSplittedTransposedIndex, - DefaultTolerance, - query_multi_group, - ); + println!("Running encoding benchmarks..."); + let encoding_results = run_encoding_benchmark(&raw_file_path); + all_results.extend(encoding_results); - add_bench_optim!( - group, - raw_file_path, - basename, - "RawFileIndex", - RawFileIndex, - DefaultTolerance, - query_multi_group, - ); + println!("Running batch access benchmarks..."); + let batch_results = run_batch_access_benchmark(&raw_file_path); + all_results.extend(batch_results); - group.finish(); + match write_results(all_results, &basename) { + Ok(_) => println!("Results written to benchmark_results_{}.json", basename), + Err(e) => eprintln!("Error writing results: {}", e), + } } - -criterion_group!( - benches, - thoughput_benchmark_optim, - criterion_benchmark, - thoughput_benchmark_random, -); -criterion_main!(benches); diff --git a/src/models/adapters.rs b/src/models/adapters.rs new file mode 100644 index 0000000..61df79f --- /dev/null +++ b/src/models/adapters.rs @@ -0,0 +1,90 @@ +use crate::models::elution_group::ElutionGroup; +use crate::models::queries::{FragmentGroupIndexQuery, PrecursorIndexQuery}; +use crate::ToleranceAdapter; +use serde::Serialize; +use std::hash::Hash; +use timsrust::converters::ConvertableDomain; +use timsrust::Metadata; + +#[derive(Debug, Default)] +pub struct FragmentIndexAdapter { + pub metadata: Metadata, +} + +impl From for FragmentIndexAdapter { + fn from(metadata: Metadata) -> Self { + Self { metadata } + } +} + +impl + ToleranceAdapter, ElutionGroup> for FragmentIndexAdapter +{ + fn query_from_elution_group( + &self, + tol: &dyn crate::traits::tolerance::Tolerance, + elution_group: &ElutionGroup, + ) -> FragmentGroupIndexQuery { + let rt_range = tol.rt_range(elution_group.rt_seconds); + let mobility_range = tol.mobility_range(elution_group.mobility); + let precursor_mz_range = tol.mz_range(elution_group.precursor_mz); + let quad_range = tol.quad_range(elution_group.precursor_mz, elution_group.precursor_charge); + + let mz_index_range = ( + self.metadata.mz_converter.invert(precursor_mz_range.0) as u32, + self.metadata.mz_converter.invert(precursor_mz_range.1) as u32, + ); + let mobility_range = match mobility_range { + Some(mobility_range) => mobility_range, + None => (self.metadata.lower_im as f32, self.metadata.upper_im as f32), + }; + let mobility_index_range = ( + self.metadata.im_converter.invert(mobility_range.0) as usize, + self.metadata.im_converter.invert(mobility_range.1) as usize, + ); + let rt_range = match rt_range { + Some(rt_range) => rt_range, + None => (self.metadata.lower_rt as f32, self.metadata.upper_rt as f32), + }; + let frame_index_range = ( + self.metadata.rt_converter.invert(rt_range.0) as usize, + self.metadata.rt_converter.invert(rt_range.1) as usize, + ); + + assert!(frame_index_range.0 <= frame_index_range.1); + assert!(mz_index_range.0 <= mz_index_range.1); + // Since mobilities get mixed up bc low scan ranges are high 1/k0, I + // Just make sure they are sorted here. + let mobility_index_range = ( + mobility_index_range.0.min(mobility_index_range.1), + mobility_index_range.1.max(mobility_index_range.0), + ); + + let precursor_query = PrecursorIndexQuery { + frame_index_range, + mz_index_range, + mobility_index_range, + isolation_mz_range: quad_range, + }; + + let fqs = elution_group + .fragment_mzs + .iter() + .map(|(k, v)| { + let mz_range = tol.mz_range(*v); + ( + *k, + ( + self.metadata.mz_converter.invert(mz_range.0) as u32, + self.metadata.mz_converter.invert(mz_range.1) as u32, + ), + ) + }) + .collect(); + + FragmentGroupIndexQuery { + mz_index_ranges: fqs, + precursor_query, + } + } +} diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 1570d8a..a55af8c 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::marker::PhantomData; use std::sync::Arc; use super::single_quad_settings::{ @@ -10,6 +11,7 @@ use rayon::prelude::*; use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; +use super::peak_in_quad::PeakInQuad; use crate::sort_by_indices_multi; use crate::utils::compress_explode::explode_vec; use crate::utils::frame_processing::lazy_centroid_weighted_frame; @@ -32,8 +34,17 @@ pub struct ExpandedFrame { pub window_group: u8, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UnsortedState {} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SortedState {} + +pub trait SortingStateTrait {} +impl SortingStateTrait for UnsortedState {} +impl SortingStateTrait for SortedState {} + #[derive(Debug, Clone)] -pub struct ExpandedFrameSlice { +pub struct ExpandedFrameSlice { pub tof_indices: Vec, // I could Arc<[u32]> if I didnt want to sort by it ... pub scan_numbers: Vec, pub intensities: Vec, @@ -45,10 +56,11 @@ pub struct ExpandedFrameSlice { pub intensity_correction_factor: f64, pub window_group: u8, pub window_subindex: u8, + _sorting_state: PhantomData, } -impl ExpandedFrameSlice { - pub fn sort_by_tof(&mut self) { +impl ExpandedFrameSlice { + pub fn sort_by_tof(mut self) -> ExpandedFrameSlice { let mut indices = argsort_by(&self.tof_indices, |x| *x); sort_by_indices_multi!( &mut indices, @@ -56,6 +68,20 @@ impl ExpandedFrameSlice { &mut self.scan_numbers, &mut self.intensities ); + ExpandedFrameSlice { + tof_indices: self.tof_indices, + scan_numbers: self.scan_numbers, + intensities: self.intensities, + frame_index: self.frame_index, + rt: self.rt, + acquisition_type: self.acquisition_type, + ms_level: self.ms_level, + quadrupole_settings: self.quadrupole_settings, + intensity_correction_factor: self.intensity_correction_factor, + window_group: self.window_group, + window_subindex: self.window_subindex, + _sorting_state: PhantomData, + } } pub fn len(&self) -> usize { @@ -67,6 +93,52 @@ impl ExpandedFrameSlice { } } +impl ExpandedFrameSlice { + pub fn query_peaks( + &self, + tof_range: (u32, u32), + scan_range: Option<(usize, usize)>, + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + let peak_range = { + let peak_ind_start = self.tof_indices.partition_point(|x| x < &tof_range.0); + let peak_ind_end = self.tof_indices.partition_point(|x| x <= &tof_range.1); + peak_ind_start..peak_ind_end + }; + + match scan_range { + Some((min_scan, max_scan)) => { + assert!(min_scan <= max_scan); + } + None => {} + }; + + for peak_ind in peak_range { + let scan_index = self.scan_numbers[peak_ind]; + match scan_range { + Some((min_scan, max_scan)) => { + if scan_index < min_scan || scan_index > max_scan { + continue; + } + } + None => {} + }; + + let intensity = self.intensities[peak_ind]; + let tof_index = self.tof_indices[peak_ind]; + let retention_time = self.rt; + f(PeakInQuad { + scan_index, + intensity, + tof_index, + retention_time: retention_time as f32, + }); + } + } +} + fn trim_scan_edges(scan_start: usize, scan_end: usize) -> (usize, usize) { // Poor man's fix to different quad windows bleeding onto each other ... // I will trim the smallest of 20 scans or 10% of the range size. @@ -88,11 +160,11 @@ fn trim_scan_edges(scan_start: usize, scan_end: usize) -> (usize, usize) { (new_start, new_end) } -fn expand_unfragmented_frame(frame: Frame) -> ExpandedFrameSlice { +fn expand_unfragmented_frame(frame: Frame) -> ExpandedFrameSlice { let scan_numbers = explode_vec(&frame.scan_offsets); let intensities = frame.intensities; let tof_indices = frame.tof_indices; - let mut curr_slice = ExpandedFrameSlice { + let curr_slice = ExpandedFrameSlice { tof_indices, scan_numbers, intensities, @@ -104,15 +176,16 @@ fn expand_unfragmented_frame(frame: Frame) -> ExpandedFrameSlice { intensity_correction_factor: frame.intensity_correction_factor, window_group: frame.window_group, window_subindex: 0u8, + _sorting_state: PhantomData::, }; - curr_slice.sort_by_tof(); - curr_slice + + curr_slice.sort_by_tof() } fn expand_fragmented_frame( frame: Frame, quads: Vec, -) -> Vec { +) -> Vec> { let mut out = Vec::with_capacity(quads.len()); let exploded_scans = explode_vec(&frame.scan_offsets); for (i, qs) in quads.into_iter().enumerate() { @@ -130,7 +203,7 @@ fn expand_fragmented_frame( let slice_intensities = frame.intensities[tof_index_index_slice_start..tof_index_index_slice_end].to_vec(); - let mut curr_slice = ExpandedFrameSlice { + let curr_slice = ExpandedFrameSlice { tof_indices: slice_tof_indices, scan_numbers: slice_scan_numbers, intensities: slice_intensities, @@ -142,16 +215,16 @@ fn expand_fragmented_frame( intensity_correction_factor: frame.intensity_correction_factor, window_group: frame.window_group, window_subindex: i as u8, + _sorting_state: PhantomData::, }; // Q: is this sorting twice? since sort before creating the expanded frame slices. // TODO: Use the state type pattern to make sure only one sort happens. - curr_slice.sort_by_tof(); - out.push(curr_slice); + out.push(curr_slice.sort_by_tof()); } out } -pub fn expand_and_split_frame(frame: Frame) -> Vec { +pub fn expand_and_split_frame(frame: Frame) -> Vec> { let quad_settings = &frame.quadrupole_settings; // Q: Can I save a vec allocation if I make the expanded quad_settings // To return a vec of builders? or Enum(settings|frame_slice)? @@ -194,9 +267,9 @@ impl ExpandedFrame { pub fn expand_and_arrange_frames( frames: Vec, -) -> HashMap, Vec> { +) -> HashMap, Vec>> { let mut out = HashMap::new(); - let split: Vec = frames + let split: Vec> = frames .into_par_iter() .flat_map(|frame| expand_and_split_frame(frame)) .collect(); @@ -219,42 +292,43 @@ pub fn par_expand_and_centroid_frames( mz_tol_ppm: f64, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, -) -> HashMap, Vec> { +) -> HashMap, Vec>> { let split_frames = expand_and_arrange_frames(frames); - let out: HashMap, Vec> = split_frames - .into_iter() - .map(|(qs, frameslices)| { - // NOTE: Since the the centroiding runs in paralel over the windows, its ok if this - // outer loop is done in series. - let start_peaks: usize = frameslices.iter().map(|x| x.len()).sum(); - let centroided = par_lazy_centroid_frameslices( - &frameslices, - 3, - ims_tol_pct, - mz_tol_ppm, - ims_converter, - mz_converter, - ); - let end_peaks: usize = centroided.iter().map(|x| x.len()).sum(); - debug!( - "Peak counts for quad {:?}: raw={}/centroid={}", - qs, start_peaks, end_peaks - ); - (qs, centroided) - }) - .collect(); + let out: HashMap, Vec>> = + split_frames + .into_iter() + .map(|(qs, frameslices)| { + // NOTE: Since the the centroiding runs in paralel over the windows, its ok if this + // outer loop is done in series. + let start_peaks: usize = frameslices.iter().map(|x| x.len()).sum(); + let centroided = par_lazy_centroid_frameslices( + &frameslices, + 3, + ims_tol_pct, + mz_tol_ppm, + ims_converter, + mz_converter, + ); + let end_peaks: usize = centroided.iter().map(|x| x.len()).sum(); + debug!( + "Peak counts for quad {:?}: raw={}/centroid={}", + qs, start_peaks, end_peaks + ); + (qs, centroided) + }) + .collect(); out } fn centroid_frameslice_window( - frameslices: &[ExpandedFrameSlice], + frameslices: &[ExpandedFrameSlice], ims_tol_pct: f64, mz_tol_ppm: f64, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, -) -> ExpandedFrameSlice { +) -> ExpandedFrameSlice { assert!(frameslices.len() > 1, "Expected at least 2 frameslices"); let reference_index = frameslices.len() / 2; @@ -303,6 +377,9 @@ fn centroid_frameslice_window( |&scan| scan_tol_range(scan, ims_tol_pct, ims_converter), ); + // Make sure everything is sorted ... + assert!(mzs.windows(2).all(|x| x[0] <= x[1])); + ExpandedFrameSlice { tof_indices: mzs, scan_numbers: imss, @@ -315,17 +392,18 @@ fn centroid_frameslice_window( intensity_correction_factor: frameslices[reference_index].intensity_correction_factor, window_group: frameslices[reference_index].window_group, window_subindex: frameslices[reference_index].window_subindex, + _sorting_state: PhantomData::, } } pub fn par_lazy_centroid_frameslices( - frameslices: &[ExpandedFrameSlice], + frameslices: &[ExpandedFrameSlice], window_width: usize, ims_tol_pct: f64, mz_tol_ppm: f64, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, -) -> Vec { +) -> Vec> { assert!( frameslices .iter() @@ -337,7 +415,7 @@ pub fn par_lazy_centroid_frameslices( assert!(frameslices.len() > window_width); - let local_lambda = |fss: &[ExpandedFrameSlice]| { + let local_lambda = |fss: &[ExpandedFrameSlice]| { centroid_frameslice_window(fss, ims_tol_pct, mz_tol_ppm, ims_converter, mz_converter) }; diff --git a/src/models/frames/expanded_window_group.rs b/src/models/frames/expanded_window_group.rs index 261b7e9..4283c2a 100644 --- a/src/models/frames/expanded_window_group.rs +++ b/src/models/frames/expanded_window_group.rs @@ -1,6 +1,6 @@ use std::iter::repeat; -use super::expanded_frame::ExpandedFrameSlice; +use super::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; use super::single_quad_settings::SingleQuadrupoleSetting; use timsrust::{AcquisitionType, MSLevel}; @@ -18,7 +18,9 @@ pub struct ExpandedWindowGroup { } impl ExpandedWindowGroup { - pub fn from_expanded_frame_slices(expanded_frame_slices: Vec) -> Self { + pub fn from_expanded_frame_slices( + expanded_frame_slices: Vec>, + ) -> Self { let num_peaks = expanded_frame_slices .iter() .map(|x| x.tof_indices.len()) diff --git a/src/models/frames/mod.rs b/src/models/frames/mod.rs index 7cffa4e..49c123f 100644 --- a/src/models/frames/mod.rs +++ b/src/models/frames/mod.rs @@ -1,5 +1,6 @@ pub mod expanded_frame; pub mod expanded_window_group; +pub mod peak_in_quad; pub mod raw_frames; pub mod raw_peak; pub mod single_quad_settings; diff --git a/src/models/frames/peak_in_quad.rs b/src/models/frames/peak_in_quad.rs new file mode 100644 index 0000000..7803399 --- /dev/null +++ b/src/models/frames/peak_in_quad.rs @@ -0,0 +1,20 @@ +use super::raw_peak::RawPeak; + +#[derive(Debug, Clone, Copy)] +pub struct PeakInQuad { + pub scan_index: usize, + pub intensity: u32, + pub retention_time: f32, + pub tof_index: u32, +} + +impl From for RawPeak { + fn from(peak_in_quad: PeakInQuad) -> Self { + RawPeak { + scan_index: peak_in_quad.scan_index, + tof_index: peak_in_quad.tof_index, + intensity: peak_in_quad.intensity, + retention_time: peak_in_quad.retention_time, + } + } +} diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index 67f4cfd..4747e3d 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -125,3 +125,43 @@ pub fn expand_quad_settings(quad_settings: &QuadrupoleSettings) -> ExpandedFrame } ExpandedFrameQuadSettings::Fragmented(out) } + +pub fn get_matching_quad_settings( + flat_quad_settings: &[SingleQuadrupoleSetting], + precursor_mz_range: (f64, f64), + scan_range: Option<(usize, usize)>, +) -> impl Iterator + '_ { + flat_quad_settings + .iter() + .filter(move |qs| { + (qs.ranges.isolation_low <= precursor_mz_range.1) + && (qs.ranges.isolation_high >= precursor_mz_range.0) + }) + .filter(move |qs| { + match scan_range { + Some((min_scan, max_scan)) => { + assert!(qs.ranges.scan_start <= qs.ranges.scan_end); + assert!(min_scan <= max_scan); + + // Above quad + // Quad [----------] + // Query [------] + let above_quad = qs.ranges.scan_end < min_scan; + + // Below quad + // Quad [------] + // Query [------] + let below_quad = qs.ranges.scan_start > max_scan; + + if above_quad || below_quad { + // This quad is completely outside the scan range + false + } else { + true + } + } + None => true, + } + }) + .map(|e| e.index) +} diff --git a/src/models/indices/expanded_raw_index/mod.rs b/src/models/indices/expanded_raw_index/mod.rs index ee2d47a..15ec9da 100644 --- a/src/models/indices/expanded_raw_index/mod.rs +++ b/src/models/indices/expanded_raw_index/mod.rs @@ -1 +1,3 @@ mod model; + +pub use model::ExpandedRawFrameIndex; diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index 10155f6..73cac2d 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -1,10 +1,22 @@ +use crate::models::adapters::FragmentIndexAdapter; +use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::ExpandedFrame; use crate::models::frames::expanded_frame::{ - expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, + expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, SortedState, }; -use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; +use crate::models::frames::peak_in_quad::PeakInQuad; +use crate::models::frames::raw_peak::RawPeak; +use crate::models::frames::single_quad_settings::{ + get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, +}; +use crate::models::queries::FragmentGroupIndexQuery; +use crate::traits::indexed_data::IndexedData; +use crate::ToleranceAdapter; use log::debug; +use rayon::prelude::*; +use serde::Serialize; use std::collections::HashMap; +use std::hash::Hash; use std::sync::Arc; use std::time::Instant; use timsrust::converters::{ @@ -15,18 +27,95 @@ use timsrust::{QuadrupoleSettings, TimsRustError}; type QuadSettingsIndex = usize; -#[derive(Debug, Default)] -struct ExpandedRawFrameIndex { - bundled_ms1_frames: Vec, - bundled_frames: HashMap>, +#[derive(Debug)] +pub struct ExpandedRawFrameIndex { + bundled_ms1_frames: ExpandedSliceBundle, + bundled_frames: HashMap, flat_quad_settings: Vec, rt_converter: Frame2RtConverter, pub mz_converter: Tof2MzConverter, pub im_converter: Scan2ImConverter, + adapter: FragmentIndexAdapter, +} + +#[derive(Debug, Clone)] +pub struct ExpandedSliceBundle { + slices: Vec>, + rts: Vec, + frame_indices: Vec, +} + +impl ExpandedSliceBundle { + pub fn new(mut slices: Vec>) -> Self { + slices.sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); + let rts = slices.iter().map(|x| x.rt).collect(); + let frame_indices = slices.iter().map(|x| x.frame_index).collect(); + Self { + slices, + rts, + frame_indices, + } + } + + pub fn query_peaks( + &self, + tof_range: (u32, u32), + scan_range: Option<(usize, usize)>, + frame_index_range: (usize, usize), + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + // Binary search the rt if needed. + let frame_indices = self.frame_indices.as_slice(); + let low = frame_indices.partition_point(|x| x < &frame_index_range.0); + let high = frame_indices.partition_point(|x| x <= &frame_index_range.1); + + for i in low..high { + let slice = &self.slices[i]; + slice.query_peaks(tof_range, scan_range, f); + } + } } impl ExpandedRawFrameIndex { - pub fn from_tdf_path(path: &str) -> Result { + pub fn query_peaks( + &self, + tof_range: (u32, u32), + precursor_mz_range: (f64, f64), + scan_range: Option<(usize, usize)>, + frame_index_range: (usize, usize), + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + let matching_quads: Vec = + get_matching_quad_settings(&self.flat_quad_settings, precursor_mz_range, scan_range) + .collect(); + self.query_precursor_peaks(&matching_quads, tof_range, scan_range, frame_index_range, f); + } + + fn query_precursor_peaks( + &self, + matching_quads: &[SingleQuadrupoleSettingIndex], + tof_range: (u32, u32), + scan_range: Option<(usize, usize)>, + frame_index_range: (usize, usize), + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + for quad in matching_quads { + let tqi = self + .bundled_frames + .get(quad) + .expect("Only existing quads should be queried."); + + tqi.query_peaks(tof_range, scan_range, frame_index_range, &mut *f) + } + } + + pub fn from_path(path: &str) -> Result { let file_reader = FrameReader::new(path)?; let sql_path = std::path::Path::new(path).join("analysis.tdf"); @@ -48,20 +137,22 @@ impl ExpandedRawFrameIndex { debug!("Centroiding took {:#?}", centroided_elap); let mut out_ms2_frames = HashMap::new(); - let mut out_ms1_frames: Option> = None; + let mut out_ms1_frames: Option = None; + let mut flat_quad_settings = Vec::new(); centroided_split_frames .into_iter() .for_each(|(q, frameslices)| match q { None => { - out_ms1_frames = Some(frameslices); + out_ms1_frames = Some(ExpandedSliceBundle::new(frameslices)); } Some(q) => { - out_ms2_frames.insert(q, frameslices); + flat_quad_settings.push(q); + out_ms2_frames.insert(q.index, ExpandedSliceBundle::new(frameslices)); } }); - let mut flat_quad_settings = out_ms2_frames.keys().cloned().collect(); + let adapter = FragmentIndexAdapter::from(meta_converters.clone()); let out = Self { bundled_ms1_frames: out_ms1_frames.expect("At least one ms1 frame should be present"), @@ -70,8 +161,221 @@ impl ExpandedRawFrameIndex { rt_converter: meta_converters.rt_converter, mz_converter: meta_converters.mz_converter, im_converter: meta_converters.im_converter, + adapter, }; Ok(out) } } + +impl + IndexedData, (RawPeak, FH)> for ExpandedRawFrameIndex +{ + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { + todo!(); + // let precursor_mz_range = ( + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // ); + // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + // let frame_index_range = Some(FrameRTTolerance::FrameIndex( + // fragment_query.precursor_query.frame_index_range, + // )); + + // fragment_query + // .mz_index_ranges + // .iter() + // .flat_map(|(fh, tof_range)| { + // let mut local_vec: Vec<(RawPeak, FH)> = vec![]; + // self.query_peaks( + // *tof_range, + // precursor_mz_range, + // scan_range, + // frame_index_range, + // &mut |x| local_vec.push((RawPeak::from(x), *fh)), + // ); + + // local_vec + // }) + // .collect() + } + + fn add_query>( + &self, + fragment_query: &FragmentGroupIndexQuery, + aggregator: &mut AG, + ) { + todo!(); + // let precursor_mz_range = ( + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // ); + // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + // let frame_index_range = Some(FrameRTTolerance::FrameIndex( + // fragment_query.precursor_query.frame_index_range, + // )); + + // fragment_query + // .mz_index_ranges + // .iter() + // .for_each(|(fh, tof_range)| { + // self.query_peaks( + // *tof_range, + // precursor_mz_range, + // scan_range, + // frame_index_range, + // &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), + // ); + // }) + } + + fn add_query_multi_group>( + &self, + fragment_queries: &[FragmentGroupIndexQuery], + aggregator: &mut [AG], + ) { + fragment_queries + .par_iter() + .zip(aggregator.par_iter_mut()) + .for_each(|(fragment_query, agg)| { + let precursor_mz_range = ( + fragment_query.precursor_query.isolation_mz_range.0 as f64, + fragment_query.precursor_query.isolation_mz_range.1 as f64, + ); + assert!(precursor_mz_range.0 <= precursor_mz_range.1); + assert!(precursor_mz_range.0 > 0.0); + let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + let frame_index_range = fragment_query.precursor_query.frame_index_range; + + let local_quad_vec: Vec = get_matching_quad_settings( + &self.flat_quad_settings, + precursor_mz_range, + scan_range, + ) + .collect(); + + for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { + self.query_precursor_peaks( + &local_quad_vec, + tof_range, + scan_range, + frame_index_range, + &mut |peak| agg.add(&(RawPeak::from(peak), fh)), + ); + } + }); + } +} + +impl + IndexedData, RawPeak> for ExpandedRawFrameIndex +{ + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { + todo!(); + // let precursor_mz_range = ( + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // ); + // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + // let frame_index_range = Some(FrameRTTolerance::FrameIndex( + // fragment_query.precursor_query.frame_index_range, + // )); + + // fragment_query + // .mz_index_ranges + // .iter() + // .flat_map(|(fh, tof_range)| { + // let mut local_vec: Vec<(RawPeak, FH)> = vec![]; + // self.query_peaks( + // *tof_range, + // precursor_mz_range, + // scan_range, + // frame_index_range, + // &mut |x| local_vec.push((RawPeak::from(x), *fh)), + // ); + + // local_vec + // }) + // .collect() + } + + fn add_query>( + &self, + fragment_query: &FragmentGroupIndexQuery, + aggregator: &mut AG, + ) { + todo!(); + // let precursor_mz_range = ( + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // ); + // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + // let frame_index_range = Some(FrameRTTolerance::FrameIndex( + // fragment_query.precursor_query.frame_index_range, + // )); + + // fragment_query + // .mz_index_ranges + // .iter() + // .for_each(|(fh, tof_range)| { + // self.query_peaks( + // *tof_range, + // precursor_mz_range, + // scan_range, + // frame_index_range, + // &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), + // ); + // }) + } + + fn add_query_multi_group>( + &self, + fragment_queries: &[FragmentGroupIndexQuery], + aggregator: &mut [AG], + ) { + fragment_queries + .par_iter() + .zip(aggregator.par_iter_mut()) + .for_each(|(fragment_query, agg)| { + let precursor_mz_range = ( + fragment_query.precursor_query.isolation_mz_range.0 as f64, + fragment_query.precursor_query.isolation_mz_range.1 as f64, + ); + assert!(precursor_mz_range.0 <= precursor_mz_range.1); + assert!(precursor_mz_range.0 > 0.0); + let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + let frame_index_range = fragment_query.precursor_query.frame_index_range; + + let local_quad_vec: Vec = get_matching_quad_settings( + &self.flat_quad_settings, + precursor_mz_range, + scan_range, + ) + .collect(); + + for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { + self.query_precursor_peaks( + &local_quad_vec, + tof_range, + scan_range, + frame_index_range, + &mut |peak| agg.add(&RawPeak::from(peak)), + ); + } + }); + } +} + +// ============================================================================ + +impl + ToleranceAdapter, ElutionGroup> for ExpandedRawFrameIndex +{ + fn query_from_elution_group( + &self, + tol: &dyn crate::traits::tolerance::Tolerance, + elution_group: &ElutionGroup, + ) -> FragmentGroupIndexQuery { + self.adapter.query_from_elution_group(tol, elution_group) + } +} diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 8b4f2cf..6642517 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -1,6 +1,7 @@ use super::peak_bucket::PeakBucketBuilder; use super::peak_bucket::{PeakBucket, PeakInBucket}; -use crate::models::frames::expanded_frame::ExpandedFrameSlice; +use crate::models::frames::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; +use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::sort_by_indices_multi; @@ -77,17 +78,10 @@ impl TransposedQuadIndex { tof_range: (u32, u32), scan_range: Option<(usize, usize)>, rt_range: Option, - // ) -> Vec { ) -> impl Iterator + '_ { - trace!( - "TransposedQuadIndex::query_peaks tof_range: {:?}, scan_range: {:?}, rt_range: {:?}", - tof_range, - scan_range, - rt_range - ); - // TODO reimplement as an iterator ... // This version is not compatible with the borrow checker unless I collect the vec... // which will do for now for prototyping. + // TODO: make this a single type and convert upstream. let frame_index_range = self.convert_to_local_frame_range(rt_range); self.peak_buckets @@ -96,8 +90,6 @@ impl TransposedQuadIndex { pb.query_peaks(scan_range, frame_index_range) .map(move |p| PeakInQuad::from_peak_in_bucket(p, *tof_index)) }) - // Coult I just return an Arc<[intensities]> + ... - // If I made the peak buckets sparse, I could make it ... not be an option. } fn convert_to_local_frame_range( @@ -141,25 +133,6 @@ impl TransposedQuadIndex { } } -#[derive(Debug, Clone, Copy)] -pub struct PeakInQuad { - pub scan_index: usize, - pub intensity: u32, - pub retention_time: f32, - pub tof_index: u32, -} - -impl From for RawPeak { - fn from(peak_in_quad: PeakInQuad) -> Self { - RawPeak { - scan_index: peak_in_quad.scan_index, - tof_index: peak_in_quad.tof_index, - intensity: peak_in_quad.intensity, - retention_time: peak_in_quad.retention_time, - } - } -} - impl PeakInQuad { pub fn from_peak_in_bucket(peak_in_bucket: PeakInBucket, tof_index: u32) -> Self { Self { @@ -170,7 +143,6 @@ impl PeakInQuad { } } } - // Q: Do I use this? Should I just have it as the frame index as // an option? #[derive(Debug, Clone, Copy)] @@ -222,7 +194,7 @@ impl TransposedQuadIndexBuilder { self.frame_rts.extend(other.frame_rts); } - pub fn add_frame_slice(&mut self, slice: ExpandedFrameSlice) { + pub fn add_frame_slice(&mut self, slice: ExpandedFrameSlice) { self.int_slices.push(slice.intensities); self.tof_slices.push(slice.tof_indices); self.scan_slices.push(slice.scan_numbers); diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index bdc718c..0c213fa 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -1,12 +1,15 @@ -use super::quad_index::{ - FrameRTTolerance, PeakInQuad, TransposedQuadIndex, TransposedQuadIndexBuilder, -}; +use super::quad_index::{FrameRTTolerance, TransposedQuadIndex, TransposedQuadIndexBuilder}; +use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ - expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, + expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, SortingStateTrait, }; +use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; -use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; + +use crate::models::frames::single_quad_settings::{ + get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, +}; use crate::models::queries::FragmentGroupIndexQuery; use crate::models::queries::PrecursorIndexQuery; use crate::traits::indexed_data::IndexedData; @@ -33,12 +36,12 @@ use timsrust::TimsRustError; pub struct QuadSplittedTransposedIndex { precursor_index: TransposedQuadIndex, - fragment_indices: HashMap, + fragment_indices: HashMap, flat_quad_settings: Vec, rt_converter: Frame2RtConverter, pub mz_converter: Tof2MzConverter, pub im_converter: Scan2ImConverter, - metadata: Metadata, + adapter: FragmentIndexAdapter, } impl Debug for QuadSplittedTransposedIndex { @@ -62,14 +65,7 @@ impl QuadSplittedTransposedIndex { ) where F: FnMut(PeakInQuad), { - trace!( - "QuadSplittedTransposedIndex::query_peaks tof_range: {:?}, scan_range: {:?}, rt_range: {:?}, precursor_mz_range: {:?}", - tof_range, - scan_range, - rt_range, - precursor_mz_range, - ); - let matching_quads: Vec = self + let matching_quads: Vec = self .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); trace!("matching_quads: {:?}", matching_quads); @@ -78,7 +74,7 @@ impl QuadSplittedTransposedIndex { fn query_precursor_peaks( &self, - matching_quads: &[SingleQuadrupoleSetting], + matching_quads: &[SingleQuadrupoleSettingIndex], tof_range: (u32, u32), scan_range: Option<(usize, usize)>, rt_range: Option, @@ -86,8 +82,6 @@ impl QuadSplittedTransposedIndex { ) where F: FnMut(PeakInQuad), { - let rt_range = rt_range.map(|x| x.to_frame_index_range(&self.rt_converter)); - for quad in matching_quads { let tqi = self .fragment_indices @@ -102,108 +96,8 @@ impl QuadSplittedTransposedIndex { &self, precursor_mz_range: (f64, f64), scan_range: Option<(usize, usize)>, - ) -> impl Iterator + '_ { - self.flat_quad_settings - .iter() - .filter(move |qs| { - (qs.ranges.isolation_low <= precursor_mz_range.1) - && (qs.ranges.isolation_high >= precursor_mz_range.0) - }) - .filter(move |qs| { - match scan_range { - Some((min_scan, max_scan)) => { - assert!(qs.ranges.scan_start <= qs.ranges.scan_end); - assert!(min_scan <= max_scan); - - // Above quad - // Quad [----------] - // Query [------] - let above_quad = qs.ranges.scan_end < min_scan; - - // Below quad - // Quad [------] - // Query [------] - let below_quad = qs.ranges.scan_start > max_scan; - - if above_quad || below_quad { - // This quad is completely outside the scan range - false - } else { - true - } - } - None => true, - } - }) - .cloned() - } - - fn queries_from_elution_elements_impl( - &self, - tol: &dyn crate::traits::tolerance::Tolerance, - elution_group: &crate::models::elution_group::ElutionGroup, - ) -> FragmentGroupIndexQuery { - let rt_range = tol.rt_range(elution_group.rt_seconds); - let mobility_range = tol.mobility_range(elution_group.mobility); - let precursor_mz_range = tol.mz_range(elution_group.precursor_mz); - let quad_range = tol.quad_range(elution_group.precursor_mz, elution_group.precursor_charge); - - let mz_index_range = ( - self.mz_converter.invert(precursor_mz_range.0) as u32, - self.mz_converter.invert(precursor_mz_range.1) as u32, - ); - let mobility_range = match mobility_range { - Some(mobility_range) => mobility_range, - None => (self.metadata.lower_im as f32, self.metadata.upper_im as f32), - }; - let mobility_index_range = ( - self.im_converter.invert(mobility_range.0) as usize, - self.im_converter.invert(mobility_range.1) as usize, - ); - let rt_range = match rt_range { - Some(rt_range) => rt_range, - None => (self.metadata.lower_rt as f32, self.metadata.upper_rt as f32), - }; - let frame_index_range = ( - self.rt_converter.invert(rt_range.0) as usize, - self.rt_converter.invert(rt_range.1) as usize, - ); - - assert!(frame_index_range.0 <= frame_index_range.1); - assert!(mz_index_range.0 <= mz_index_range.1); - // Since mobilities get mixed up bc low scan ranges are high 1/k0, I - // Just make sure they are sorted here. - let mobility_index_range = ( - mobility_index_range.0.min(mobility_index_range.1), - mobility_index_range.1.max(mobility_index_range.0), - ); - - let precursor_query = PrecursorIndexQuery { - frame_index_range, - mz_index_range, - mobility_index_range, - isolation_mz_range: quad_range, - }; - - let fqs = elution_group - .fragment_mzs - .iter() - .map(|(k, v)| { - let mz_range = tol.mz_range(*v); - ( - k.clone(), - ( - self.mz_converter.invert(mz_range.0) as u32, - self.mz_converter.invert(mz_range.1) as u32, - ), - ) - }) - .collect(); - - FragmentGroupIndexQuery { - mz_index_ranges: fqs, - precursor_query, - } + ) -> impl Iterator + '_ { + get_matching_quad_settings(&self.flat_quad_settings, precursor_mz_range, scan_range) } } @@ -298,7 +192,7 @@ impl QuadSplittedTransposedIndexBuilder { } } - fn add_frame_slice(&mut self, frame_slice: ExpandedFrameSlice) { + fn add_frame_slice(&mut self, frame_slice: ExpandedFrameSlice) { // Add key if it doesnt exist ... self.indices .entry(frame_slice.quadrupole_settings) @@ -435,7 +329,7 @@ impl QuadSplittedTransposedIndexBuilder { let mut precursor_index: Option = None; for (qi, qs) in built.into_iter() { if let Some(qs) = qs { - indices.insert(qs, qi); + indices.insert(qs.index, qi); flat_quad_settings.push(qs); } else { precursor_index = Some(qi); @@ -456,7 +350,7 @@ impl QuadSplittedTransposedIndexBuilder { rt_converter: self.rt_converter.unwrap(), mz_converter: self.mz_converter.unwrap(), im_converter: self.im_converter.unwrap(), - metadata: self.metadata.unwrap(), + adapter: FragmentIndexAdapter::from(self.metadata.unwrap()), } } } @@ -634,7 +528,7 @@ impl fragment_query.precursor_query.frame_index_range, )); - let local_quad_vec: Vec = self + let local_quad_vec: Vec = self .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); @@ -662,6 +556,6 @@ impl tol: &dyn crate::traits::tolerance::Tolerance, elution_group: &ElutionGroup, ) -> FragmentGroupIndexQuery { - self.queries_from_elution_elements_impl(tol, elution_group) + self.adapter.query_from_elution_group(tol, elution_group) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 174f68d..e43662d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ +pub mod adapters; pub mod aggregators; pub mod elution_group; pub mod frames; From 1ad2454dc110e5b5930f45eb93fb1c10534c4203 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Thu, 24 Oct 2024 14:03:47 -0700 Subject: [PATCH 06/30] (refactor,feat) added benchmark plotting and cleanup --- benches/benchmark_indices.rs | 249 ++++++++++++++---- benches/plot_bench.py | 45 ++++ data/get_data.bash | 9 + src/lib.rs | 3 - src/models/frames/expanded_frame.rs | 96 +++++-- .../indices/expanded_raw_index/model.rs | 62 ++++- src/utils/sorting.rs | 61 +++++ 7 files changed, 437 insertions(+), 88 deletions(-) create mode 100644 benches/plot_bench.py create mode 100644 data/get_data.bash diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 84a355e..71a3520 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -3,6 +3,7 @@ use rand_chacha::ChaCha8Rng; use serde::Serialize; use std::collections::HashMap; use std::fs::File; +use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use timsquery::{ models::{ @@ -13,11 +14,13 @@ use timsquery::{ }, }, queriable_tims_data::queriable_tims_data::query_multi_group, - traits::tolerance::DefaultTolerance, + traits::tolerance::{ + DefaultTolerance, MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance, + }, ElutionGroup, }; -const NUM_ELUTION_GROUPS: usize = 500; +const NUM_ELUTION_GROUPS: usize = 1000; const NUM_ITERATIONS: usize = 1; #[derive(Debug, Serialize)] @@ -32,6 +35,8 @@ struct BenchmarkResult { context: String, iterations: Vec, mean_duration_seconds: f64, + mean_duration_human_readable: String, + note: Option, } #[derive(Debug, Serialize)] @@ -39,6 +44,7 @@ struct BenchmarkReport { settings: BenchmarkSettings, results: Vec, metadata: BenchmarkMetadata, + full_benchmark_time_seconds: f64, } #[derive(Debug, Serialize)] @@ -104,105 +110,237 @@ fn build_elution_groups() -> Vec> { out_egs } -fn with_benchmark(name: &str, context: &str, f: impl Fn()) -> BenchmarkResult { +fn with_benchmark(name: &str, context: &str, f: impl Fn() -> Option) -> BenchmarkResult { let mut iterations = Vec::with_capacity(NUM_ITERATIONS); - for i in 0..NUM_ITERATIONS { + let mut durations = Vec::with_capacity(NUM_ITERATIONS); + let mut extras = None; + let glob_start = Instant::now(); + let mut i = 0; + while (glob_start.elapsed().as_millis() < 1000) || (i < NUM_ITERATIONS) { println!("{name} iteration {i}"); let start = Instant::now(); - f(); + if let Some(out) = f() { + extras = Some(out); + } + let duration = start.elapsed(); iterations.push(BenchmarkIteration { iteration: i + 1, duration_seconds: duration_to_seconds(duration), }); + durations.push(duration); + i += 1; } - let mean = iterations.iter().map(|i| i.duration_seconds).sum::() / NUM_ITERATIONS as f64; + let mean = iterations.iter().map(|i| i.duration_seconds).sum::() / (i as f64); + let avg_duration: Duration = durations.iter().sum::() / i as u32; + let mean_duration_human_readable = format!("{:?}", avg_duration); + BenchmarkResult { name: name.to_string(), context: context.to_string(), iterations, mean_duration_seconds: mean, + mean_duration_human_readable, + note: extras, } } fn run_encoding_benchmark(raw_file_path: &str) -> Vec { + let mut out = vec![]; let rfi = with_benchmark("RawFileIndex", "Encoding", || { RawFileIndex::from_path(raw_file_path).unwrap(); + None + }); + out.push(rfi); + let erfic = with_benchmark("ExpandedRawFileIndexCentroided", "Encoding", || { + ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); + None }); + out.push(erfic); let erfi = with_benchmark("ExpandedRawFileIndex", "Encoding", || { ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); + None }); + out.push(erfi); let tqi = with_benchmark("TransposedQuadIndex", "Encoding", || { QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); + None }); + out.push(tqi); - vec![tqi, rfi, erfi] + let tqic = with_benchmark("TransposedQuadIndexCentroided", "Encoding", || { + QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(); + None + }); + out.push(tqic); + + out } fn run_batch_access_benchmark(raw_file_path: &str) -> Vec { + let mut out = vec![]; let query_groups = build_elution_groups(); - let tolerance = DefaultTolerance::default(); + let tolerance_with_rt = DefaultTolerance { + ms: MzToleramce::Ppm((20.0, 20.0)), + rt: RtTolerance::Absolute((5.0, 5.0)), + mobility: MobilityTolerance::Pct((3.0, 3.0)), + quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + }; + let tolerance_with_nort = DefaultTolerance { + ms: MzToleramce::Ppm((20.0, 20.0)), + rt: RtTolerance::None, + mobility: MobilityTolerance::Pct((3.0, 3.0)), + quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + }; + let tolerances = [ + (tolerance_with_rt, "narrow_rt"), + (tolerance_with_nort, "full_rt"), + ]; + + // TODO: Refactor this ... there is a lot of code duplication but over different types ... int + // ion thoty this is a good place for a macro. let rfi_index = RawFileIndex::from_path(raw_file_path).unwrap(); - let rfi = with_benchmark("RawFileIndex", "BatchAccess", || { - let _ = query_multi_group( - &rfi_index, - &rfi_index, - &tolerance, - &query_groups, - &RawPeakIntensityAggregator::new, - ); - }); + for (tolerance, tol_name) in tolerances.clone() { + if tol_name == "full_rt" { + println!("Skipping full_rt"); + continue; + } + let rfi = with_benchmark("RawFileIndex", &format!("BatchAccess_{}", tol_name), || { + let tmp = query_multi_group( + &rfi_index, + &rfi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!("RawFileIndex::query_multi_group aggregated {}", tot); + println!("{}", out); + Some(out) + }); + out.push(rfi); + } std::mem::drop(rfi_index); let erfi_index = ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); - let erfi = with_benchmark("ExpandedRawFileIndex", "BatchAccess", || { - let _ = query_multi_group( - &erfi_index, - &erfi_index, - &tolerance, - &query_groups, - &RawPeakIntensityAggregator::new, + for (tolerance, tol_name) in tolerances.clone() { + let erfi = with_benchmark( + "ExpandedRawFileIndex", + &format!("BatchAccess_{}", tol_name), + || { + let tmp = query_multi_group( + &erfi_index, + &erfi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!("ExpandedRawFileIndex::query_multi_group aggregated {}", tot); + println!("{}", out); + Some(out) + }, ); - }); + out.push(erfi); + } std::mem::drop(erfi_index); + let erfic_index = ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(); + for (tolerance, tol_name) in tolerances.clone() { + let erfic = with_benchmark( + "ExpandedRawFileIndexCentroided", + &format!("BatchAccess_{}", tol_name), + || { + let tmp = query_multi_group( + &erfic_index, + &erfic_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!( + "ExpandedRawFileIndexCentroided::query_multi_group aggregated {}", + tot + ); + println!("{}", out); + Some(out) + }, + ); + out.push(erfic); + } + std::mem::drop(erfic_index); + let tqi_index = QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); - let tqi = with_benchmark("TransposedQuadIndex", "BatchAccess", || { - let _ = query_multi_group( - &tqi_index, - &tqi_index, - &tolerance, - &query_groups, - &RawPeakIntensityAggregator::new, + for (tolerance, tol_name) in tolerances.clone() { + let tqi = with_benchmark( + "TransposedQuadIndex", + &format!("BatchAccess_{}", tol_name), + || { + let tmp = query_multi_group( + &tqi_index, + &tqi_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!("TransposedQuadIndex::query_multi_group aggregated {}", tot); + println!("{}", out); + Some(out) + }, ); - }); + out.push(tqi); + } std::mem::drop(tqi_index); - vec![tqi, rfi, erfi] + let tqic_index = QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(); + for (tolerance, tol_name) in tolerances.clone() { + let tqi = with_benchmark( + "TransposedQuadIndexCentroided", + &format!("BatchAccess_{}", tol_name), + || { + let tmp = query_multi_group( + &tqic_index, + &tqic_index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!("TransposedQuadIndex::query_multi_group aggregated {}", tot); + println!("{}", out); + Some(out) + }, + ); + out.push(tqi); + } + std::mem::drop(tqic_index); + out } -fn write_results(results: Vec, basename: &str) -> std::io::Result<()> { - let report = BenchmarkReport { - settings: BenchmarkSettings { - num_elution_groups: NUM_ELUTION_GROUPS, - num_iterations: NUM_ITERATIONS, - }, - results, - metadata: BenchmarkMetadata { - basename: basename.to_string(), - }, - }; - - let file = File::create(format!("benchmark_results_{}.json", basename))?; +fn write_results( + report: BenchmarkReport, + basename: &str, + parent: &Path, +) -> std::io::Result<(PathBuf, String)> { + let filepath = parent.join(format!("benchmark_results_{}.json", basename)); + let file = File::create(&filepath)?; serde_json::to_writer_pretty(file, &report)?; - Ok(()) + let out = serde_json::to_string_pretty(&report)?; + Ok((filepath, out)) } fn main() { + env_logger::init(); + let st = Instant::now(); let (raw_file_path, basename) = get_file_from_env(); + let file_parent = std::path::Path::new(&raw_file_path) + .parent() + .expect("Expected to find a parent directory"); let mut all_results: Vec = vec![]; println!( @@ -217,9 +355,20 @@ fn main() { println!("Running batch access benchmarks..."); let batch_results = run_batch_access_benchmark(&raw_file_path); all_results.extend(batch_results); + let report = BenchmarkReport { + settings: BenchmarkSettings { + num_elution_groups: NUM_ELUTION_GROUPS, + num_iterations: NUM_ITERATIONS, + }, + results: all_results, + metadata: BenchmarkMetadata { + basename: basename.to_string(), + }, + full_benchmark_time_seconds: st.elapsed().as_secs_f64(), + }; - match write_results(all_results, &basename) { - Ok(_) => println!("Results written to benchmark_results_{}.json", basename), + match write_results(report, &basename, file_parent) { + Ok((fp, res)) => println!("Results written to {} \n{}", fp.to_string_lossy(), res), Err(e) => eprintln!("Error writing results: {}", e), } } diff --git a/benches/plot_bench.py b/benches/plot_bench.py new file mode 100644 index 0000000..e6e3bb0 --- /dev/null +++ b/benches/plot_bench.py @@ -0,0 +1,45 @@ +# /// script +# dependencies = [ +# "altair", +# "polars", +# "vl-convert-python", +# ] +# /// + +from pathlib import Path +import polars as pl +import altair as alt +import argparse + + +parser = argparse.ArgumentParser() +parser.add_argument("benchmark_file") +args = parser.parse_args() + +data = ( + pl.read_json(args.benchmark_file) + .explode("results") + .with_columns( + bench=pl.col("results").struct.field("name"), + context=pl.col("results").struct.field("context"), + iterations=pl.col("results").struct.field("iterations"), + ) + .explode("iterations") + .with_columns(time_seconds=pl.col("iterations").struct.field("duration_seconds")) +) +print(data) + +for context, sdf in data.group_by("context"): + context = context[0] + print(context) + # Sample to max 100 points on each bench + sdf = sdf.filter(pl.int_range(pl.len()).shuffle().over("bench") < 100) + alt.Chart(sdf).mark_point().encode( + x="bench", + y=alt.Y("time_seconds", scale=alt.Scale(type="log")), + color="bench", + ).properties( + width=600, + height=600, + title=f"{context}", + ).save(f"{context}.png") diff --git a/data/get_data.bash b/data/get_data.bash new file mode 100644 index 0000000..44bb605 --- /dev/null +++ b/data/get_data.bash @@ -0,0 +1,9 @@ + +# LFQ_timsTOFPro_diaPASEF_Ecoli_03.d.zip + +docker run --platform linux/amd64 -v ${PWD}:/data \ + ghcr.io/pride-archive/aspera \ + ascp -i /home/aspera/.aspera/cli/etc/asperaweb_id_dsa.openssh \ + -TQ -P33001 \ + prd_ascp@fasp.ebi.ac.uk:/pride/data/archive/2022/02/PXD028735/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d.zip \ + /data diff --git a/src/lib.rs b/src/lib.rs index c7ab4fd..8ca3d26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,6 @@ pub use crate::traits::aggregator::Aggregator; pub use crate::traits::indexed_data::IndexedData; pub use crate::traits::tolerance::{HasIntegerID, Tolerance, ToleranceAdapter}; -// Re-export utility functions -pub use crate::utils::sorting::sort_multiple_by; - // Declare modules pub mod models; pub mod queriable_tims_data; diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index a55af8c..3263c58 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,23 +1,24 @@ -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; - use super::single_quad_settings::{ expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, }; use itertools::Itertools; use rayon::iter::IntoParallelIterator; use rayon::prelude::*; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; +use std::time::Instant; use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; use super::peak_in_quad::PeakInQuad; use crate::sort_by_indices_multi; +use crate::sort_vecs_by_first; use crate::utils::compress_explode::explode_vec; use crate::utils::frame_processing::lazy_centroid_weighted_frame; use crate::utils::sorting::argsort_by; use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; -use log::debug; +use log::{debug, info, trace}; /// A frame after expanding the mobility data and re-sorting it by tof. #[derive(Debug, Clone)] @@ -59,19 +60,48 @@ pub struct ExpandedFrameSlice { _sorting_state: PhantomData, } +fn sort_by_tof2( + tof_indices: Vec, + scan_numbers: Vec, + intensities: Vec, +) -> ((Vec, Vec), Vec) { + let mut combined: Vec<((u32, usize), u32)> = tof_indices + .iter() + .zip(scan_numbers.iter()) + .zip(intensities.iter()) + .map(|((tof, scan), inten)| ((*tof, *scan), *inten)) + .collect(); + combined.sort_unstable_by(|a, b| a.0 .0.partial_cmp(&b.0 .0).unwrap()); + let ((tof_indices, scan_numbers), intensities) = combined.into_iter().unzip(); + ((tof_indices, scan_numbers), intensities) +} + +// Example of how to use it with your specific types +fn sort_by_tof_macro( + tof_indices: Vec, + scan_numbers: Vec, + intensities: Vec, +) -> (Vec, Vec, Vec) { + sort_vecs_by_first!(tof_indices, scan_numbers, intensities) +} + impl ExpandedFrameSlice { pub fn sort_by_tof(mut self) -> ExpandedFrameSlice { - let mut indices = argsort_by(&self.tof_indices, |x| *x); - sort_by_indices_multi!( - &mut indices, - &mut self.tof_indices, - &mut self.scan_numbers, - &mut self.intensities - ); + // let mut indices = argsort_by(&self.tof_indices, |x| *x); + // sort_by_indices_multi!( + // &mut indices, + // &mut self.tof_indices, + // &mut self.scan_numbers, + // &mut self.intensities + // ); + + let (tof_indices, scan_numbers, intensities) = + sort_by_tof_macro(self.tof_indices, self.scan_numbers, self.intensities); + ExpandedFrameSlice { - tof_indices: self.tof_indices, - scan_numbers: self.scan_numbers, - intensities: self.intensities, + tof_indices, + scan_numbers, + intensities, frame_index: self.frame_index, rt: self.rt, acquisition_type: self.acquisition_type, @@ -268,6 +298,8 @@ impl ExpandedFrame { pub fn expand_and_arrange_frames( frames: Vec, ) -> HashMap, Vec>> { + info!("Expanding and arranging frames"); + let start = Instant::now(); let mut out = HashMap::new(); let split: Vec> = frames .into_par_iter() @@ -283,6 +315,8 @@ pub fn expand_and_arrange_frames( for (_, es) in out.iter_mut() { es.sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); } + let end = start.elapsed(); + info!("Expanding and arranging frames took {:#?}", end); out } @@ -311,9 +345,11 @@ pub fn par_expand_and_centroid_frames( mz_converter, ); let end_peaks: usize = centroided.iter().map(|x| x.len()).sum(); - debug!( + trace!( "Peak counts for quad {:?}: raw={}/centroid={}", - qs, start_peaks, end_peaks + qs, + start_peaks, + end_peaks ); (qs, centroided) }) @@ -334,20 +370,20 @@ fn centroid_frameslice_window( // this is A LOT of cloning ... look into whether it is needed. - let mut tof_array: Vec = frameslices + let tof_array: Vec = frameslices .iter() .flat_map(|x| x.tof_indices.clone()) .collect(); - let mut ims_array: Vec = frameslices + let ims_array: Vec = frameslices .iter() .flat_map(|x| x.scan_numbers.clone()) .collect(); - let mut weight_array: Vec = frameslices + let weight_array: Vec = frameslices .iter() .flat_map(|x| x.intensities.clone()) .collect(); - let mut intensity_array: Vec = frameslices + let intensity_array: Vec = frameslices .iter() .enumerate() .flat_map(|(i, x)| { @@ -359,14 +395,16 @@ fn centroid_frameslice_window( }) .collect(); - let mut tof_order = argsort_by(&tof_array, |x| *x); - sort_by_indices_multi!( - &mut tof_order, - &mut tof_array, - &mut ims_array, - &mut weight_array, - &mut intensity_array - ); + // let mut tof_order = argsort_by(&tof_array, |x| *x); + // sort_by_indices_multi!( + // &mut tof_order, + // &mut tof_array, + // &mut ims_array, + // &mut weight_array, + // &mut intensity_array + // ); + let (tof_array, ims_array, weight_array, intensity_array) = + sort_vecs_by_first!(tof_array, ims_array, weight_array, intensity_array); let ((mzs, intensities), imss) = lazy_centroid_weighted_frame( &tof_array, diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index 73cac2d..b61788b 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -2,7 +2,8 @@ use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::ExpandedFrame; use crate::models::frames::expanded_frame::{ - expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, SortedState, + expand_and_arrange_frames, expand_and_split_frame, par_expand_and_centroid_frames, + ExpandedFrameSlice, SortedState, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; @@ -12,12 +13,11 @@ use crate::models::frames::single_quad_settings::{ use crate::models::queries::FragmentGroupIndexQuery; use crate::traits::indexed_data::IndexedData; use crate::ToleranceAdapter; -use log::debug; +use log::{debug, info}; use rayon::prelude::*; use serde::Serialize; use std::collections::HashMap; use std::hash::Hash; -use std::sync::Arc; use std::time::Instant; use timsrust::converters::{ ConvertableDomain, Frame2RtConverter, Scan2ImConverter, Tof2MzConverter, @@ -115,7 +115,7 @@ impl ExpandedRawFrameIndex { } } - pub fn from_path(path: &str) -> Result { + pub fn from_path_centroided(path: &str) -> Result { let file_reader = FrameReader::new(path)?; let sql_path = std::path::Path::new(path).join("analysis.tdf"); @@ -124,8 +124,9 @@ impl ExpandedRawFrameIndex { let st = Instant::now(); let all_frames = file_reader.get_all().into_iter().flatten().collect(); let read_elap = st.elapsed(); - debug!("Reading all frames took {:#?}", read_elap); + info!("Reading all frames took {:#?}", read_elap); let st = Instant::now(); + // TODO: Expose this parameter as a config option. let centroided_split_frames = par_expand_and_centroid_frames( all_frames, 1.5, @@ -134,7 +135,56 @@ impl ExpandedRawFrameIndex { &meta_converters.mz_converter, ); let centroided_elap = st.elapsed(); - debug!("Centroiding took {:#?}", centroided_elap); + info!("Centroiding took {:#?}", centroided_elap); + + let mut out_ms2_frames = HashMap::new(); + let mut out_ms1_frames: Option = None; + + let mut flat_quad_settings = Vec::new(); + centroided_split_frames + .into_iter() + .for_each(|(q, frameslices)| match q { + None => { + out_ms1_frames = Some(ExpandedSliceBundle::new(frameslices)); + } + Some(q) => { + flat_quad_settings.push(q); + out_ms2_frames.insert(q.index, ExpandedSliceBundle::new(frameslices)); + } + }); + + let adapter = FragmentIndexAdapter::from(meta_converters.clone()); + + let out = Self { + bundled_ms1_frames: out_ms1_frames.expect("At least one ms1 frame should be present"), + bundled_frames: out_ms2_frames, + flat_quad_settings, + rt_converter: meta_converters.rt_converter, + mz_converter: meta_converters.mz_converter, + im_converter: meta_converters.im_converter, + adapter, + }; + + Ok(out) + } + + pub fn from_path(path: &str) -> Result { + // NOTE: I am just copy-pasting the centroided version. If I keep both I will + // abstract it and make dispatch in a config ... + + let file_reader = FrameReader::new(path)?; + + let sql_path = std::path::Path::new(path).join("analysis.tdf"); + let meta_converters = MetadataReader::new(&sql_path)?; + + let st = Instant::now(); + let all_frames = file_reader.get_all().into_iter().flatten().collect(); + let read_elap = st.elapsed(); + info!("Reading all frames took {:#?}", read_elap); + let st = Instant::now(); + let centroided_split_frames = expand_and_arrange_frames(all_frames); + let centroided_elap = st.elapsed(); + info!("Splitting took {:#?}", centroided_elap); let mut out_ms2_frames = HashMap::new(); let mut out_ms1_frames: Option = None; diff --git a/src/utils/sorting.rs b/src/utils/sorting.rs index 2f528eb..2caa86f 100644 --- a/src/utils/sorting.rs +++ b/src/utils/sorting.rs @@ -111,6 +111,28 @@ macro_rules! sort_by_indices_multi { }}; } +#[macro_export] +macro_rules! sort_vecs_by_first { + ($first:expr $(,$rest:expr)*) => {{ + let first_vec = $first; + let len = first_vec.len(); + + // Create and sort indices + let mut indices: Vec<_> = (0..len).collect(); + indices.sort_unstable_by_key(|&i| &first_vec[i]); + + // Reorder first vector + let sorted_first: Vec<_> = indices.iter().map(|&i| first_vec[i]).collect(); + + // Reorder all other vectors + (sorted_first, $( { + let other_vec = $rest; + assert_eq!(other_vec.len(), len, "All vectors must have the same length"); + indices.iter().map(|&i| other_vec[i]).collect::>() + }, )*) + }}; +} + #[cfg(test)] mod tests { use super::*; @@ -156,4 +178,43 @@ mod tests { assert_eq!(scan_numbers, vec![0, 2, 0, 2, 1, 1]); assert_eq!(intensities, vec![10, 50, 20, 60, 30, 40]); } + + #[test] + fn test_sort_two_vecs() { + let v1 = vec![3, 1, 4, 1, 5]; + let v2 = vec!['a', 'b', 'c', 'd', 'e']; + + let (sorted_v1, sorted_v2) = sort_vecs_by_first!(v1, v2); + + assert_eq!(sorted_v1, vec![1, 1, 3, 4, 5]); + assert_eq!(sorted_v2, vec!['b', 'd', 'a', 'c', 'e']); + } + + #[test] + fn test_sort_three_vecs() { + let v1 = vec![3, 1, 4]; + let v2 = vec!['x', 'y', 'z']; + let v3 = vec![true, false, true]; + + let (sorted_v1, sorted_v2, sorted_v3) = sort_vecs_by_first!(v1, v2, v3); + + assert_eq!(sorted_v1, vec![1, 3, 4]); + assert_eq!(sorted_v2, vec!['y', 'x', 'z']); + assert_eq!(sorted_v3, vec![false, true, true]); + } + + #[test] + fn test_sort_four_vecs() { + let v1 = vec![3, 1, 4]; + let v2 = vec!['x', 'y', 'z']; + let v3 = vec![true, false, true]; + let v4 = vec![1.0, 2.0, 3.0]; + + let (sorted_v1, sorted_v2, sorted_v3, sorted_v4) = sort_vecs_by_first!(v1, v2, v3, v4); + + assert_eq!(sorted_v1, vec![1, 3, 4]); + assert_eq!(sorted_v2, vec!['y', 'x', 'z']); + assert_eq!(sorted_v3, vec![false, true, true]); + assert_eq!(sorted_v4, vec![2.0, 1.0, 3.0]); + } } From f549bfae8e8d59d27daf290e39a30d2b501162a3 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Fri, 25 Oct 2024 17:54:59 -0700 Subject: [PATCH 07/30] feat: first benchmark results and refactor trait --- benches/benchmark_indices.rs | 405 ++++++++++++------ benches/plot_bench.py | 12 +- data/.gitignore | 4 + ..._A_Sample_Alpha_02_BatchAccess_full_rt.png | Bin 0 -> 47274 bytes ..._Sample_Alpha_02_BatchAccess_narrow_rt.png | Bin 0 -> 52663 bytes data/get_data.bash | 5 +- src/lib.rs | 2 +- .../raw_peak_agg/chromatogram_agg.rs | 6 +- .../raw_peak_agg/multi_chromatogram_agg.rs | 5 +- .../aggregators/raw_peak_agg/point_agg.rs | 6 +- src/models/frames/expanded_frame.rs | 318 +++++++++++--- .../indices/expanded_raw_index/model.rs | 110 ++--- src/models/indices/raw_file_index.rs | 16 +- .../transposed_quad_index/quad_index.rs | 4 +- .../quad_splitted_transposed_index.rs | 102 ++--- src/models/queries.rs | 2 +- .../queriable_tims_data.rs | 26 +- src/traits/aggregator.rs | 6 +- src/traits/indexed_data.rs | 14 +- src/utils/frame_processing.rs | 270 ++++++++---- src/utils/tolerance_ranges.rs | 11 +- 21 files changed, 856 insertions(+), 468 deletions(-) create mode 100644 data/.gitignore create mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png create mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_narrow_rt.png diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 71a3520..21c86d8 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -2,6 +2,7 @@ use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use serde::Serialize; use std::collections::HashMap; +use std::env; use std::fs::File; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; @@ -23,6 +24,74 @@ use timsquery::{ const NUM_ELUTION_GROUPS: usize = 1000; const NUM_ITERATIONS: usize = 1; +#[derive(Debug, Clone)] +struct EnvConfig { + tims_data_file: PathBuf, + file_stem: String, + skip_highmem: bool, + skip_slow: bool, + skip_build: bool, + skip_query: bool, +} + +impl EnvConfig { + fn new() -> Self { + let mut tims_data_file = String::new(); + let mut skip_highmem = false; + // let mut skip_slow = false; + let mut skip_slow = true; + let mut skip_build = false; + let mut skip_query = false; + + for (key, value) in env::vars() { + match key.as_str() { + "TIMS_DATA_FILE" => tims_data_file = value, + "SKIP_HIGHMEM" => skip_highmem = value == "1", + // "SKIP_SLOW" => skip_slow = value == "1", + // I hate this double negative but I want to make slow benches + // opt-in rather than opt-out. + "RUN_SLOW" => skip_slow = value != "1", + "SKIP_BUILD" => skip_build = value == "1", + "SKIP_QUERY" => skip_query = value == "1", + _ => {} + } + } + + if tims_data_file.is_empty() { + panic!("TIMS_DATA_FILE environment variable not set"); + } + + // Check that the file exists and is readable + + let tims_data_file = Path::new(&tims_data_file); + if !tims_data_file.exists() { + panic!("TIMS_DATA_FILE={} does not exist", tims_data_file.display()); + } + let file_stem = tims_data_file.file_stem().unwrap().to_str().unwrap(); + + let out = Self { + tims_data_file: tims_data_file.to_path_buf(), + file_stem: file_stem.to_string(), + skip_highmem, + skip_slow, + skip_build, + skip_query, + }; + + println!("Env config: {:#?}", out); + + out + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BenchmarkTag { + HighMem, + Slow, + Build, + Query, +} + #[derive(Debug, Serialize)] struct BenchmarkIteration { iteration: usize, @@ -33,9 +102,12 @@ struct BenchmarkIteration { struct BenchmarkResult { name: String, context: String, - iterations: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + iterations: Option>, mean_duration_seconds: f64, mean_duration_human_readable: String, + setup_time_seconds: f64, + setup_time_human_readable: String, note: Option, } @@ -62,20 +134,6 @@ fn duration_to_seconds(duration: Duration) -> f64 { duration.as_secs() as f64 + duration.subsec_nanos() as f64 * 1e-9 } -fn get_file_from_env() -> (String, String) { - let raw_file_path = - std::env::var("TIMS_DATA_FILE").expect("TIMS_DATA_FILE environment variable not set"); - - let basename = std::path::Path::new(&raw_file_path) - .file_stem() - .unwrap() - .to_str() - .unwrap() - .to_string(); - - (raw_file_path, basename) -} - fn build_elution_groups() -> Vec> { const NUM_FRAGMENTS: usize = 10; const MAX_RT: f32 = 22.0 * 60.0; @@ -110,76 +168,153 @@ fn build_elution_groups() -> Vec> { out_egs } -fn with_benchmark(name: &str, context: &str, f: impl Fn() -> Option) -> BenchmarkResult { - let mut iterations = Vec::with_capacity(NUM_ITERATIONS); - let mut durations = Vec::with_capacity(NUM_ITERATIONS); - let mut extras = None; - let glob_start = Instant::now(); - let mut i = 0; - while (glob_start.elapsed().as_millis() < 1000) || (i < NUM_ITERATIONS) { - println!("{name} iteration {i}"); - let start = Instant::now(); - if let Some(out) = f() { - extras = Some(out); +impl EnvConfig { + fn with_benchmark( + &self, + name: &str, + context: &str, + setup_fn: impl Fn() -> T, + f: impl Fn(&T, usize) -> Option, + tags: &[BenchmarkTag], + ) -> Option { + if self.skip_slow && tags.contains(&BenchmarkTag::Slow) { + println!("Skipping slow benchmark for {} {}", name, context); + return None; } - let duration = start.elapsed(); + if self.skip_highmem && tags.contains(&BenchmarkTag::HighMem) { + println!("Skipping highmem benchmark {} {}", name, context); + return None; + } - iterations.push(BenchmarkIteration { - iteration: i + 1, - duration_seconds: duration_to_seconds(duration), - }); - durations.push(duration); - i += 1; - } + if self.skip_build && tags.contains(&BenchmarkTag::Build) { + println!("Skipping build benchmark {} {}", name, context); + return None; + } + + if self.skip_query && tags.contains(&BenchmarkTag::Query) { + println!("Skipping query benchmark {} {}", name, context); + return None; + } - let mean = iterations.iter().map(|i| i.duration_seconds).sum::() / (i as f64); - let avg_duration: Duration = durations.iter().sum::() / i as u32; - let mean_duration_human_readable = format!("{:?}", avg_duration); - - BenchmarkResult { - name: name.to_string(), - context: context.to_string(), - iterations, - mean_duration_seconds: mean, - mean_duration_human_readable, - note: extras, + println!("{} {} Tagged with {:?}", name, context, tags); + let sst = Instant::now(); + println!("Setting up {} {}", name, context); + let setup = setup_fn(); + let setup_elap = sst.elapsed(); + println!("Setup took {:#?}", setup_elap); + + let mut iterations = Vec::with_capacity(NUM_ITERATIONS); + let mut durations = Vec::with_capacity(NUM_ITERATIONS); + let mut extras = None; + let glob_start = Instant::now(); + let mut i = 0; + while (glob_start.elapsed().as_millis() < 1000) || (i < NUM_ITERATIONS) { + if i < 10 || i % 100 == 0 { + println!("{name} iteration {i}"); + } + let start = Instant::now(); + if let Some(out) = f(&setup, i) { + if i < 3 { + println!("{}", out); + } + extras = Some(out); + } + + let duration = start.elapsed(); + + iterations.push(BenchmarkIteration { + iteration: i + 1, + duration_seconds: duration_to_seconds(duration), + }); + durations.push(duration); + i += 1; + } + + let mean = iterations.iter().map(|i| i.duration_seconds).sum::() / (i as f64); + let avg_duration: Duration = durations.iter().sum::() / i as u32; + let mean_duration_human_readable = format!("{:?}", avg_duration); + + println!("Mean duration: {:?}", mean_duration_human_readable); + + Some(BenchmarkResult { + name: name.to_string(), + context: context.to_string(), + iterations: Some(iterations), + mean_duration_seconds: mean, + mean_duration_human_readable, + setup_time_seconds: setup_elap.as_secs_f64(), + setup_time_human_readable: format!("{:?}", setup_elap), + note: extras, + }) } } -fn run_encoding_benchmark(raw_file_path: &str) -> Vec { +fn run_encoding_benchmark(raw_file_path: &PathBuf, env_config: EnvConfig) -> Vec { + let raw_file_path = raw_file_path.to_str().unwrap(); let mut out = vec![]; - let rfi = with_benchmark("RawFileIndex", "Encoding", || { - RawFileIndex::from_path(raw_file_path).unwrap(); - None - }); + let rfi = env_config.with_benchmark( + "RawFileIndex", + "Encoding", + || {}, + |_, _| { + RawFileIndex::from_path(raw_file_path).unwrap(); + None + }, + &[BenchmarkTag::Build], + ); out.push(rfi); - let erfic = with_benchmark("ExpandedRawFileIndexCentroided", "Encoding", || { - ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); - None - }); + let erfic = env_config.with_benchmark( + "ExpandedRawFileIndexCentroided", + "Encoding", + || {}, + |_, _| { + ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(); + None + }, + &[BenchmarkTag::Build], + ); out.push(erfic); - let erfi = with_benchmark("ExpandedRawFileIndex", "Encoding", || { - ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); - None - }); + + let erfi = env_config.with_benchmark( + "ExpandedRawFileIndex", + "Encoding", + || {}, + |_, _| { + ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); + None + }, + &[BenchmarkTag::HighMem, BenchmarkTag::Build], + ); out.push(erfi); - let tqi = with_benchmark("TransposedQuadIndex", "Encoding", || { - QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); - None - }); + let tqi = env_config.with_benchmark( + "TransposedQuadIndex", + "Encoding", + || {}, + |_, _| { + QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); + None + }, + &[BenchmarkTag::HighMem, BenchmarkTag::Build], + ); out.push(tqi); - - let tqic = with_benchmark("TransposedQuadIndexCentroided", "Encoding", || { - QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(); - None - }); + let tqic = env_config.with_benchmark( + "TransposedQuadIndexCentroided", + "Encoding", + || {}, + |_, _| { + QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(); + None + }, + &[BenchmarkTag::Build], + ); out.push(tqic); - out + out.into_iter().flatten().collect() } -fn run_batch_access_benchmark(raw_file_path: &str) -> Vec { +fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Vec { + let raw_file_path = raw_file_path.to_str().unwrap(); let mut out = vec![]; let query_groups = build_elution_groups(); let tolerance_with_rt = DefaultTolerance { @@ -202,128 +337,131 @@ fn run_batch_access_benchmark(raw_file_path: &str) -> Vec { // TODO: Refactor this ... there is a lot of code duplication but over different types ... int // ion thoty this is a good place for a macro. - let rfi_index = RawFileIndex::from_path(raw_file_path).unwrap(); for (tolerance, tol_name) in tolerances.clone() { - if tol_name == "full_rt" { - println!("Skipping full_rt"); - continue; - } - let rfi = with_benchmark("RawFileIndex", &format!("BatchAccess_{}", tol_name), || { - let tmp = query_multi_group( - &rfi_index, - &rfi_index, - &tolerance, - &query_groups, - &RawPeakIntensityAggregator::new, - ); - let tot: u64 = tmp.into_iter().sum(); - let out = format!("RawFileIndex::query_multi_group aggregated {}", tot); - println!("{}", out); - Some(out) - }); + let local_tags = if tol_name == "full_rt" { + vec![BenchmarkTag::Slow, BenchmarkTag::Query] + } else { + vec![BenchmarkTag::Query] + }; + let rfi = env_config.with_benchmark( + "RawFileIndex", + &format!("BatchAccess_{}", tol_name), + || RawFileIndex::from_path(raw_file_path).unwrap(), + |index, i| { + let tmp = query_multi_group( + index, + index, + &tolerance, + &query_groups, + &RawPeakIntensityAggregator::new, + ); + let tot: u64 = tmp.into_iter().sum(); + let out = format!("RawFileIndex::query_multi_group aggregated {} ", tot,); + Some(out) + }, + &local_tags, + ); out.push(rfi); } - std::mem::drop(rfi_index); - let erfi_index = ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(); for (tolerance, tol_name) in tolerances.clone() { - let erfi = with_benchmark( + let erfi = env_config.with_benchmark( "ExpandedRawFileIndex", &format!("BatchAccess_{}", tol_name), - || { + || ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(), + |index, i| { let tmp = query_multi_group( - &erfi_index, - &erfi_index, + index, + index, &tolerance, &query_groups, &RawPeakIntensityAggregator::new, ); let tot: u64 = tmp.into_iter().sum(); - let out = format!("ExpandedRawFileIndex::query_multi_group aggregated {}", tot); - println!("{}", out); + let out = format!( + "ExpandedRawFileIndex::query_multi_group aggregated {} ", + tot, + ); Some(out) }, + &[BenchmarkTag::HighMem, BenchmarkTag::Query], ); out.push(erfi); } - std::mem::drop(erfi_index); - let erfic_index = ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(); for (tolerance, tol_name) in tolerances.clone() { - let erfic = with_benchmark( + let erfic = env_config.with_benchmark( "ExpandedRawFileIndexCentroided", &format!("BatchAccess_{}", tol_name), - || { + || ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(), + |index, i| { let tmp = query_multi_group( - &erfic_index, - &erfic_index, + index, + index, &tolerance, &query_groups, &RawPeakIntensityAggregator::new, ); let tot: u64 = tmp.into_iter().sum(); let out = format!( - "ExpandedRawFileIndexCentroided::query_multi_group aggregated {}", - tot + "ExpandedRawFileIndexCentroided::query_multi_group aggregated {} ", + tot, ); - println!("{}", out); Some(out) }, + &[BenchmarkTag::Query], ); out.push(erfic); } - std::mem::drop(erfic_index); - let tqi_index = QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(); for (tolerance, tol_name) in tolerances.clone() { - let tqi = with_benchmark( + let tqi = env_config.with_benchmark( "TransposedQuadIndex", &format!("BatchAccess_{}", tol_name), - || { + || QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(), + |index, i| { let tmp = query_multi_group( - &tqi_index, - &tqi_index, + index, + index, &tolerance, &query_groups, &RawPeakIntensityAggregator::new, ); let tot: u64 = tmp.into_iter().sum(); - let out = format!("TransposedQuadIndex::query_multi_group aggregated {}", tot); - println!("{}", out); + let out = format!("TransposedQuadIndex::query_multi_group aggregated {} ", tot,); Some(out) }, + &[BenchmarkTag::HighMem, BenchmarkTag::Query], ); out.push(tqi); } - std::mem::drop(tqi_index); - let tqic_index = QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(); for (tolerance, tol_name) in tolerances.clone() { - let tqi = with_benchmark( + let tqi = env_config.with_benchmark( "TransposedQuadIndexCentroided", &format!("BatchAccess_{}", tol_name), - || { + || QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(), + |index, i| { let tmp = query_multi_group( - &tqic_index, - &tqic_index, + index, + index, &tolerance, &query_groups, &RawPeakIntensityAggregator::new, ); let tot: u64 = tmp.into_iter().sum(); - let out = format!("TransposedQuadIndex::query_multi_group aggregated {}", tot); - println!("{}", out); + let out = format!("TransposedQuadIndex::query_multi_group aggregated {} ", tot,); Some(out) }, + &[BenchmarkTag::Query], ); out.push(tqi); } - std::mem::drop(tqic_index); - out + out.into_iter().flatten().collect() } fn write_results( - report: BenchmarkReport, + mut report: BenchmarkReport, basename: &str, parent: &Path, ) -> std::io::Result<(PathBuf, String)> { @@ -331,13 +469,26 @@ fn write_results( let file = File::create(&filepath)?; serde_json::to_writer_pretty(file, &report)?; let out = serde_json::to_string_pretty(&report)?; - Ok((filepath, out)) + + for result in report.results.iter_mut() { + result.iterations = None; + } + let slim_filepath = parent.join(format!("slim_benchmark_results_{}.json", basename)); + let slim_file = File::create(&slim_filepath)?; + serde_json::to_writer_pretty(slim_file, &report)?; + let slim_out = serde_json::to_string_pretty(&report)?; + + Ok((filepath, slim_out)) } fn main() { env_logger::init(); let st = Instant::now(); - let (raw_file_path, basename) = get_file_from_env(); + + let env_config = EnvConfig::new(); + let raw_file_path = env_config.tims_data_file.clone(); + let basename = env_config.file_stem.clone(); + let file_parent = std::path::Path::new(&raw_file_path) .parent() .expect("Expected to find a parent directory"); @@ -349,11 +500,11 @@ fn main() { ); println!("Running encoding benchmarks..."); - let encoding_results = run_encoding_benchmark(&raw_file_path); + let encoding_results = run_encoding_benchmark(&raw_file_path, env_config.clone()); all_results.extend(encoding_results); println!("Running batch access benchmarks..."); - let batch_results = run_batch_access_benchmark(&raw_file_path); + let batch_results = run_batch_access_benchmark(&raw_file_path, env_config.clone()); all_results.extend(batch_results); let report = BenchmarkReport { settings: BenchmarkSettings { diff --git a/benches/plot_bench.py b/benches/plot_bench.py index e6e3bb0..cc822ed 100644 --- a/benches/plot_bench.py +++ b/benches/plot_bench.py @@ -16,8 +16,12 @@ parser.add_argument("benchmark_file") args = parser.parse_args() +bench_file_path = Path(args.benchmark_file) +target_dir = bench_file_path.parent +file_stem = bench_file_path.stem + data = ( - pl.read_json(args.benchmark_file) + pl.read_json(str(bench_file_path)) .explode("results") .with_columns( bench=pl.col("results").struct.field("name"), @@ -39,7 +43,7 @@ y=alt.Y("time_seconds", scale=alt.Scale(type="log")), color="bench", ).properties( - width=600, - height=600, + width=480, + height=360, title=f"{context}", - ).save(f"{context}.png") + ).save(target_dir / f"{file_stem}_{context}.png") diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..4dba598 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,4 @@ +*.d/ +*.d.zip +*.d.tar + diff --git a/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png b/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png new file mode 100644 index 0000000000000000000000000000000000000000..876a7c629b60e6fe39d5dc8a7d6f76dbfd8af3d3 GIT binary patch literal 47274 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 for ExtractedIonChromatomobilogram { +impl Aggregator for ExtractedIonChromatomobilogram { + type Item = RawPeak; type Output = ChromatomobilogramVectorArrayTuples; fn add(&mut self, peak: &RawPeak) { @@ -162,7 +163,8 @@ impl ChromatomobilogramStatsArrays { } } -impl Aggregator for ChromatomobilogramStats { +impl Aggregator for ChromatomobilogramStats { + type Item = RawPeak; type Output = ChromatomobilogramStatsArrays; fn add(&mut self, peak: &RawPeak) { diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index ad20484..35783c8 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -342,9 +342,8 @@ impl From Aggregator<(RawPeak, FH)> - for MultiCMGStats -{ +impl Aggregator for MultiCMGStats { + type Item = (RawPeak, FH); type Output = NaturalFinalizedMultiCMGStatsArrays; fn add(&mut self, peak: &(RawPeak, FH)) { diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index e1ad36c..9309232 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -14,7 +14,8 @@ impl RawPeakIntensityAggregator { } } -impl Aggregator for RawPeakIntensityAggregator { +impl Aggregator for RawPeakIntensityAggregator { + type Item = RawPeak; type Output = u64; fn add(&mut self, peak: &RawPeak) { @@ -60,7 +61,8 @@ pub struct RawPeakVectorArrays { pub retention_times: Vec, } -impl Aggregator for RawPeakVectorAggregator { +impl Aggregator for RawPeakVectorAggregator { + type Item = RawPeak; type Output = RawPeakVectorArrays; fn add(&mut self, peak: &RawPeak) { diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 3263c58..638cfc4 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -2,7 +2,6 @@ use super::single_quad_settings::{ expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, }; use itertools::Itertools; -use rayon::iter::IntoParallelIterator; use rayon::prelude::*; use std::collections::HashMap; use std::marker::PhantomData; @@ -15,10 +14,14 @@ use super::peak_in_quad::PeakInQuad; use crate::sort_by_indices_multi; use crate::sort_vecs_by_first; use crate::utils::compress_explode::explode_vec; -use crate::utils::frame_processing::lazy_centroid_weighted_frame; +use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; use crate::utils::sorting::argsort_by; use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; -use log::{debug, info, trace}; +use log::{info, trace, warn}; +use timsrust::{ + readers::{FrameReader, FrameReaderError, MetadataReaderError}, + TimsRustError, +}; /// A frame after expanding the mobility data and re-sorting it by tof. #[derive(Debug, Clone)] @@ -86,7 +89,7 @@ fn sort_by_tof_macro( } impl ExpandedFrameSlice { - pub fn sort_by_tof(mut self) -> ExpandedFrameSlice { + pub fn sort_by_tof(self) -> ExpandedFrameSlice { // let mut indices = argsort_by(&self.tof_indices, |x| *x); // sort_by_indices_multi!( // &mut indices, @@ -295,39 +298,239 @@ impl ExpandedFrame { } } -pub fn expand_and_arrange_frames( - frames: Vec, +pub fn par_expand_and_arrange_frames( + frame_iter: impl ParallelIterator, ) -> HashMap, Vec>> { - info!("Expanding and arranging frames"); let start = Instant::now(); - let mut out = HashMap::new(); - let split: Vec> = frames - .into_par_iter() - .flat_map(|frame| expand_and_split_frame(frame)) - .collect(); - for es in split { - out.entry(es.quadrupole_settings) - .or_insert(Vec::new()) - .push(es); - } + let split = frame_iter.flat_map(|frame| expand_and_split_frame(frame)); + // let mut out = HashMap::new(); + // for es in split { + // out.entry(es.quadrupole_settings) + // .or_insert(Vec::new()) + // .push(es); + // } + // + // Attempted implementation so the folding occurs in parrallel. + // Inspired by/Copied from: https://stackoverflow.com/a/70097253/4295016 + let mut out = split + .fold(HashMap::new, |mut acc, x| { + acc.entry(x.quadrupole_settings) + .or_insert(Vec::new()) + .push(x); + acc + }) + .reduce_with(|mut acc1, acc2| { + for (qs, frameslices) in acc2.into_iter() { + acc1.entry(qs).or_insert(Vec::new()).extend(frameslices); + } + acc1 + }) + .expect("At least one frame is iterated over"); // Finally sort each of them internally by retention time. for (_, es) in out.iter_mut() { - es.sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); + es.par_sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); } let end = start.elapsed(); info!("Expanding and arranging frames took {:#?}", end); out } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FrameProcessingConfig { + Centroided { + ims_tol_pct: f64, + mz_tol_ppm: f64, + window_width: usize, + max_ms1_peaks: usize, + max_ms2_peaks: usize, + ims_converter: Option, + mz_converter: Option, + }, + NotCentroided, +} + +impl FrameProcessingConfig { + pub fn with_converters( + self, + ims_converter: Scan2ImConverter, + mz_converter: Tof2MzConverter, + ) -> Self { + match self { + FrameProcessingConfig::Centroided { + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks, + max_ms2_peaks, + .. + } => FrameProcessingConfig::Centroided { + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks, + max_ms2_peaks, + ims_converter: Some(ims_converter), + mz_converter: Some(mz_converter), + }, + FrameProcessingConfig::NotCentroided => FrameProcessingConfig::NotCentroided, + } + } + + pub fn default_centroided() -> Self { + FrameProcessingConfig::Centroided { + ims_tol_pct: 1.5, + mz_tol_ppm: 15.0, + window_width: 3, + max_ms1_peaks: 100_000, + max_ms2_peaks: 10_000, + ims_converter: Default::default(), + mz_converter: Default::default(), + } + } + + pub fn default_not_centroided() -> Self { + FrameProcessingConfig::NotCentroided + } +} + +#[derive(Debug)] +pub enum DataReadingError { + CentroidingError(FrameProcessingConfig), + UnsupportedDataError(String), + TimsRustError(TimsRustError), // Why doesnt timsrust error derive clone? +} + +impl From for DataReadingError { + fn from(e: TimsRustError) -> Self { + DataReadingError::TimsRustError(e) + } +} + +impl From for DataReadingError { + fn from(e: MetadataReaderError) -> Self { + DataReadingError::TimsRustError(TimsRustError::MetadataReaderError(e)) + } +} + +impl From for DataReadingError { + fn from(e: FrameReaderError) -> Self { + DataReadingError::TimsRustError(TimsRustError::FrameReaderError(e)) + } +} + +fn warn_and_skip_badframes( + frame_iter: impl ParallelIterator>, +) -> impl ParallelIterator { + frame_iter.filter_map(|x| { + // Log the info of the frame that broke ... + match x { + Ok(frame) => Some(frame), + Err(e) => { + warn!("Failed to read frame {:?}", e); + None + } + } + }) +} + +pub fn par_read_and_expand_frames( + frame_reader: &FrameReader, + centroiding_config: FrameProcessingConfig, +) -> Result< + HashMap, Vec>>, + DataReadingError, +> { + let dia_windows = match frame_reader.get_dia_windows() { + Some(dia_windows) => dia_windows, + None => { + return Err(DataReadingError::UnsupportedDataError( + "No dia windows found".to_string(), + )) + } + }; + + let mut all_expanded_frames = HashMap::new(); + for dia_window in dia_windows.into_iter() { + info!("Processing dia window: {:?}", dia_window); + let curr_iter = frame_reader.parallel_filter(|x| x.quadrupole_settings == dia_window); + let curr_iter = warn_and_skip_badframes(curr_iter); + + let expanded_frames = match centroiding_config { + FrameProcessingConfig::Centroided { + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks: _max_ms1_peaks, + max_ms2_peaks, + ims_converter, + mz_converter, + } => { + let expanded_frames = par_expand_and_centroid_frames( + curr_iter, + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms2_peaks, + &ims_converter.unwrap(), + &mz_converter.unwrap(), + ); + expanded_frames + } + FrameProcessingConfig::NotCentroided => { + let expanded_frames = par_expand_and_arrange_frames(curr_iter); + expanded_frames + } + }; + + all_expanded_frames.extend(expanded_frames); + } + + info!("Processing MS1 frames"); + let ms1_iter = frame_reader.parallel_filter(|x| x.ms_level == MSLevel::MS1); + let ms1_iter = warn_and_skip_badframes(ms1_iter); + let expanded_ms1_frames = match centroiding_config { + FrameProcessingConfig::Centroided { + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks, + max_ms2_peaks: _max_ms2_peaks, + ims_converter, + mz_converter, + } => { + let expanded_frames = par_expand_and_centroid_frames( + ms1_iter, + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks, + &ims_converter.unwrap(), + &mz_converter.unwrap(), + ); + expanded_frames + } + FrameProcessingConfig::NotCentroided => { + let expanded_frames = par_expand_and_arrange_frames(ms1_iter); + expanded_frames + } + }; + all_expanded_frames.extend(expanded_ms1_frames); + info!("Done reading and expanding frames"); + + Ok(all_expanded_frames) +} + pub fn par_expand_and_centroid_frames( - frames: Vec, + frames: impl ParallelIterator, ims_tol_pct: f64, mz_tol_ppm: f64, + window_width: usize, + max_peaks: usize, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, ) -> HashMap, Vec>> { - let split_frames = expand_and_arrange_frames(frames); + let split_frames = par_expand_and_arrange_frames(frames); let out: HashMap, Vec>> = split_frames @@ -338,9 +541,10 @@ pub fn par_expand_and_centroid_frames( let start_peaks: usize = frameslices.iter().map(|x| x.len()).sum(); let centroided = par_lazy_centroid_frameslices( &frameslices, - 3, + window_width, ims_tol_pct, mz_tol_ppm, + max_peaks, ims_converter, mz_converter, ); @@ -358,70 +562,34 @@ pub fn par_expand_and_centroid_frames( out } -fn centroid_frameslice_window( +fn centroid_frameslice_window2( frameslices: &[ExpandedFrameSlice], ims_tol_pct: f64, mz_tol_ppm: f64, + max_peaks: usize, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, ) -> ExpandedFrameSlice { assert!(frameslices.len() > 1, "Expected at least 2 frameslices"); let reference_index = frameslices.len() / 2; - // this is A LOT of cloning ... look into whether it is needed. - - let tof_array: Vec = frameslices - .iter() - .flat_map(|x| x.tof_indices.clone()) - .collect(); - - let ims_array: Vec = frameslices + let peak_refs: Vec = frameslices .iter() - .flat_map(|x| x.scan_numbers.clone()) - .collect(); - let weight_array: Vec = frameslices - .iter() - .flat_map(|x| x.intensities.clone()) - .collect(); - let intensity_array: Vec = frameslices - .iter() - .enumerate() - .flat_map(|(i, x)| { - if i == reference_index { - x.intensities.clone() - } else { - vec![0u32; x.tof_indices.len()] - } - }) + .map(|x| PeakArrayRefs::new(&x.tof_indices, &x.scan_numbers, &x.intensities)) .collect(); - // let mut tof_order = argsort_by(&tof_array, |x| *x); - // sort_by_indices_multi!( - // &mut tof_order, - // &mut tof_array, - // &mut ims_array, - // &mut weight_array, - // &mut intensity_array - // ); - let (tof_array, ims_array, weight_array, intensity_array) = - sort_vecs_by_first!(tof_array, ims_array, weight_array, intensity_array); - - let ((mzs, intensities), imss) = lazy_centroid_weighted_frame( - &tof_array, - &ims_array, - &weight_array, - &intensity_array, - |&tof| tof_tol_range(tof, mz_tol_ppm, mz_converter), - |&scan| scan_tol_range(scan, ims_tol_pct, ims_converter), + let ((tof_array, intensity_array), ims_array) = lazy_centroid_weighted_frame( + &peak_refs, + reference_index, + max_peaks, + |tof| tof_tol_range(tof, mz_tol_ppm, mz_converter), + |scan| scan_tol_range(scan, ims_tol_pct, ims_converter), ); - // Make sure everything is sorted ... - assert!(mzs.windows(2).all(|x| x[0] <= x[1])); - ExpandedFrameSlice { - tof_indices: mzs, - scan_numbers: imss, - intensities, + tof_indices: tof_array, + scan_numbers: ims_array, + intensities: intensity_array, frame_index: frameslices[reference_index].frame_index, rt: frameslices[reference_index].rt, acquisition_type: frameslices[reference_index].acquisition_type, @@ -439,6 +607,7 @@ pub fn par_lazy_centroid_frameslices( window_width: usize, ims_tol_pct: f64, mz_tol_ppm: f64, + max_peaks: usize, ims_converter: &Scan2ImConverter, mz_converter: &Tof2MzConverter, ) -> Vec> { @@ -454,7 +623,14 @@ pub fn par_lazy_centroid_frameslices( assert!(frameslices.len() > window_width); let local_lambda = |fss: &[ExpandedFrameSlice]| { - centroid_frameslice_window(fss, ims_tol_pct, mz_tol_ppm, ims_converter, mz_converter) + centroid_frameslice_window2( + fss, + ims_tol_pct, + mz_tol_ppm, + max_peaks, + ims_converter, + mz_converter, + ) }; frameslices diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index b61788b..11e85cd 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -1,9 +1,8 @@ use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; -use crate::models::frames::expanded_frame::ExpandedFrame; use crate::models::frames::expanded_frame::{ - expand_and_arrange_frames, expand_and_split_frame, par_expand_and_centroid_frames, - ExpandedFrameSlice, SortedState, + par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, FrameProcessingConfig, + SortedState, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; @@ -11,19 +10,16 @@ use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; -use crate::traits::indexed_data::IndexedData; +use crate::traits::indexed_data::QueriableData; use crate::ToleranceAdapter; -use log::{debug, info}; +use log::info; use rayon::prelude::*; use serde::Serialize; use std::collections::HashMap; use std::hash::Hash; use std::time::Instant; -use timsrust::converters::{ - ConvertableDomain, Frame2RtConverter, Scan2ImConverter, Tof2MzConverter, -}; -use timsrust::readers::{FrameReader, FrameReaderError, MetadataReader}; -use timsrust::{QuadrupoleSettings, TimsRustError}; +use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; +use timsrust::readers::{FrameReader, MetadataReader}; type QuadSettingsIndex = usize; @@ -115,76 +111,32 @@ impl ExpandedRawFrameIndex { } } - pub fn from_path_centroided(path: &str) -> Result { - let file_reader = FrameReader::new(path)?; - - let sql_path = std::path::Path::new(path).join("analysis.tdf"); - let meta_converters = MetadataReader::new(&sql_path)?; - - let st = Instant::now(); - let all_frames = file_reader.get_all().into_iter().flatten().collect(); - let read_elap = st.elapsed(); - info!("Reading all frames took {:#?}", read_elap); - let st = Instant::now(); - // TODO: Expose this parameter as a config option. - let centroided_split_frames = par_expand_and_centroid_frames( - all_frames, - 1.5, - 15.0, - &meta_converters.im_converter, - &meta_converters.mz_converter, - ); - let centroided_elap = st.elapsed(); - info!("Centroiding took {:#?}", centroided_elap); - - let mut out_ms2_frames = HashMap::new(); - let mut out_ms1_frames: Option = None; - - let mut flat_quad_settings = Vec::new(); - centroided_split_frames - .into_iter() - .for_each(|(q, frameslices)| match q { - None => { - out_ms1_frames = Some(ExpandedSliceBundle::new(frameslices)); - } - Some(q) => { - flat_quad_settings.push(q); - out_ms2_frames.insert(q.index, ExpandedSliceBundle::new(frameslices)); - } - }); - - let adapter = FragmentIndexAdapter::from(meta_converters.clone()); - - let out = Self { - bundled_ms1_frames: out_ms1_frames.expect("At least one ms1 frame should be present"), - bundled_frames: out_ms2_frames, - flat_quad_settings, - rt_converter: meta_converters.rt_converter, - mz_converter: meta_converters.mz_converter, - im_converter: meta_converters.im_converter, - adapter, - }; - - Ok(out) + pub fn from_path_centroided(path: &str) -> Result { + let config = FrameProcessingConfig::default_centroided(); + Self::from_path_base(path, config) } - pub fn from_path(path: &str) -> Result { - // NOTE: I am just copy-pasting the centroided version. If I keep both I will - // abstract it and make dispatch in a config ... - + pub fn from_path_base( + path: &str, + centroid_config: FrameProcessingConfig, + ) -> Result { + info!( + "Building ExpandedRawFrameIndex from path {} config {:?}", + path, centroid_config, + ); let file_reader = FrameReader::new(path)?; let sql_path = std::path::Path::new(path).join("analysis.tdf"); let meta_converters = MetadataReader::new(&sql_path)?; + let centroid_config = centroid_config.with_converters( + meta_converters.im_converter.clone(), + meta_converters.mz_converter.clone(), + ); let st = Instant::now(); - let all_frames = file_reader.get_all().into_iter().flatten().collect(); - let read_elap = st.elapsed(); - info!("Reading all frames took {:#?}", read_elap); - let st = Instant::now(); - let centroided_split_frames = expand_and_arrange_frames(all_frames); + let centroided_split_frames = par_read_and_expand_frames(&file_reader, centroid_config)?; let centroided_elap = st.elapsed(); - info!("Splitting took {:#?}", centroided_elap); + info!("Reading + Centroiding took {:#?}", centroided_elap); let mut out_ms2_frames = HashMap::new(); let mut out_ms1_frames: Option = None; @@ -216,10 +168,14 @@ impl ExpandedRawFrameIndex { Ok(out) } + + pub fn from_path(path: &str) -> Result { + Self::from_path_base(path, FrameProcessingConfig::NotCentroided) + } } impl - IndexedData, (RawPeak, FH)> for ExpandedRawFrameIndex + QueriableData, (RawPeak, FH)> for ExpandedRawFrameIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { todo!(); @@ -250,7 +206,7 @@ impl // .collect() } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, @@ -279,7 +235,7 @@ impl // }) } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], @@ -318,7 +274,7 @@ impl } impl - IndexedData, RawPeak> for ExpandedRawFrameIndex + QueriableData, RawPeak> for ExpandedRawFrameIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { todo!(); @@ -349,7 +305,7 @@ impl // .collect() } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, @@ -378,7 +334,7 @@ impl // }) } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index f736e30..60688be 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -4,7 +4,7 @@ use crate::models::frames::raw_frames::frame_elems_matching; use crate::models::frames::raw_peak::RawPeak; use crate::models::queries::{FragmentGroupIndexQuery, NaturalPrecursorQuery, PrecursorIndexQuery}; use crate::traits::aggregator::Aggregator; -use crate::traits::indexed_data::IndexedData; +use crate::traits::indexed_data::QueriableData; use crate::ElutionGroup; use crate::ToleranceAdapter; use log::trace; @@ -60,7 +60,7 @@ impl RawFileIndex { 'c, 'b: 'c, 'a: 'b, - FH: Clone + Eq + Serialize + Hash + Debug + Send + Sync, + FH: Clone + Eq + Serialize + Hash + Debug + Send + Sync + Copy, >( &'a self, fqs: &'b FragmentGroupIndexQuery, @@ -148,7 +148,9 @@ impl RawFileIndex { } } - fn queries_from_elution_elements_impl( + fn queries_from_elution_elements_impl< + FH: Clone + Eq + Serialize + Hash + Send + Sync + Copy, + >( &self, tol: &dyn crate::traits::tolerance::Tolerance, elution_elements: &crate::models::elution_group::ElutionGroup, @@ -183,7 +185,7 @@ impl RawFileIndex { } impl - IndexedData, RawPeak> for RawFileIndex + QueriableData, RawPeak> for RawFileIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { let mut out = Vec::new(); @@ -191,7 +193,7 @@ impl out } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, @@ -199,7 +201,7 @@ impl self.apply_on_query(fragment_query, &mut |peak, _| aggregator.add(&peak)); } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], @@ -211,7 +213,7 @@ impl } } -impl +impl ToleranceAdapter, ElutionGroup> for RawFileIndex { fn query_from_elution_group( diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 6642517..8a4ac88 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -2,14 +2,12 @@ use super::peak_bucket::PeakBucketBuilder; use super::peak_bucket::{PeakBucket, PeakInBucket}; use crate::models::frames::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; use crate::models::frames::peak_in_quad::PeakInQuad; -use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::sort_by_indices_multi; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::utils::sorting::par_argsort_by; use log::debug; use log::info; -use log::trace; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::time::Instant; @@ -212,7 +210,7 @@ impl TransposedQuadIndexBuilder { let max_tof = *self .tof_slices .iter() - .map(|x| x.iter().max().unwrap()) + .filter_map(|x| x.iter().max()) .max() .unwrap(); let mut tof_counts = vec![0usize; max_tof as usize + 1]; diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 0c213fa..f596fe0 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -2,17 +2,16 @@ use super::quad_index::{FrameRTTolerance, TransposedQuadIndex, TransposedQuadInd use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ - expand_and_split_frame, par_expand_and_centroid_frames, ExpandedFrameSlice, SortingStateTrait, + expand_and_split_frame, par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, + FrameProcessingConfig, SortingStateTrait, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; - use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; -use crate::models::queries::PrecursorIndexQuery; -use crate::traits::indexed_data::IndexedData; +use crate::traits::indexed_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; use log::{debug, info, trace}; @@ -23,9 +22,7 @@ use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; use std::time::Instant; -use timsrust::converters::{ - ConvertableDomain, Frame2RtConverter, Scan2ImConverter, Tof2MzConverter, -}; +use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; use timsrust::readers::{FrameReader, MetadataReader}; use timsrust::Frame; use timsrust::Metadata; @@ -140,7 +137,7 @@ impl Display for QuadSplittedTransposedIndex { } impl QuadSplittedTransposedIndex { - pub fn from_path(path: &str) -> Result { + pub fn from_path(path: &str) -> Result { let st = Instant::now(); info!("Building transposed quad index from path {}", path); let tmp = QuadSplittedTransposedIndexBuilder::from_path(path)?; @@ -151,7 +148,7 @@ impl QuadSplittedTransposedIndex { Ok(out) } - pub fn from_path_centroided(path: &str) -> Result { + pub fn from_path_centroided(path: &str) -> Result { let st = Instant::now(); info!( "Building CENTROIDED transposed quad index from path {}", @@ -207,10 +204,22 @@ impl QuadSplittedTransposedIndexBuilder { .add_frame_slice(frame_slice); } + fn from_path(path: &str) -> Result { + Self::from_path_base(path, FrameProcessingConfig::NotCentroided) + } + + fn from_path_centroided(path: &str) -> Result { + let config = FrameProcessingConfig::default_centroided(); + Self::from_path_base(path, config) + } + // TODO: I think i should split this into two functions, one that starts the builder // and one that adds the frameslices, maybe even have a config struct that dispatches // the right preprocessing steps. - fn from_path(path: &str) -> Result { + fn from_path_base( + path: &str, + centroid_config: FrameProcessingConfig, + ) -> Result { let file_reader = FrameReader::new(path)?; let sql_path = std::path::Path::new(path).join("analysis.tdf"); @@ -226,62 +235,17 @@ impl QuadSplittedTransposedIndexBuilder { added_peaks: 0, }; - let out2: Result, TimsRustError> = (0..file_reader.len()) - .into_par_iter() - .map(|id| file_reader.get(id)) - .chunks(100) - .map(|frames| { - let mut out = Self::new(); - for frame in frames { - let frame = frame?; - out.add_frame(frame); - } - Ok(out) - }) - .collect(); - - let out2 = out2?.into_iter().fold(Self::new(), |mut x, y| { - x.fold(y); - x - }); - final_out.fold(out2); + let centroid_config = centroid_config + .with_converters(meta_converters.im_converter, meta_converters.mz_converter); - Ok(final_out) - } - - fn from_path_centroided(path: &str) -> Result { - let file_reader = FrameReader::new(path)?; - - let sql_path = std::path::Path::new(path).join("analysis.tdf"); - let meta_converters = MetadataReader::new(&sql_path)?; - - let out_meta_converters = meta_converters.clone(); - let mut final_out = Self { - indices: HashMap::new(), - rt_converter: Some(meta_converters.rt_converter), - mz_converter: Some(meta_converters.mz_converter), - im_converter: Some(meta_converters.im_converter), - metadata: Some(out_meta_converters), - added_peaks: 0, - }; - - let st = Instant::now(); - let all_frames = file_reader.get_all().into_iter().flatten().collect(); - let read_elap = st.elapsed(); - debug!("Reading all frames took {:#?}", read_elap); let st = Instant::now(); - let centroided_split_frames = par_expand_and_centroid_frames( - all_frames, - 1.5, - 15.0, - &meta_converters.im_converter, - &meta_converters.mz_converter, - ); + let split_frames = par_read_and_expand_frames(&file_reader, centroid_config)?; + let centroided_elap = st.elapsed(); - debug!("Centroiding took {:#?}", centroided_elap); + debug!("Reading + Centroiding took {:#?}", centroided_elap); let st = Instant::now(); - let out2: Result, TimsRustError> = centroided_split_frames + let out2: Result, TimsRustError> = split_frames .into_par_iter() .map(|(q, frameslices)| { let mut out = Self::new(); @@ -300,8 +264,8 @@ impl QuadSplittedTransposedIndexBuilder { let build_elap = st.elapsed(); info!( - "Reading all frames took {:#?}, centroiding took {:#?}, building took {:#?}", - read_elap, centroided_elap, build_elap + "Reading all frames + centroiding took {:#?}, building took {:#?}", + centroided_elap, build_elap ); Ok(final_out) @@ -356,7 +320,7 @@ impl QuadSplittedTransposedIndexBuilder { } impl - IndexedData, RawPeak> for QuadSplittedTransposedIndex + QueriableData, RawPeak> for QuadSplittedTransposedIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { let precursor_mz_range = ( @@ -385,7 +349,7 @@ impl .collect() } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, @@ -413,7 +377,7 @@ impl }) } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], @@ -450,7 +414,7 @@ impl // ============================================================================ impl - IndexedData, (RawPeak, FH)> for QuadSplittedTransposedIndex + QueriableData, (RawPeak, FH)> for QuadSplittedTransposedIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { let precursor_mz_range = ( @@ -480,7 +444,7 @@ impl .collect() } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, @@ -508,7 +472,7 @@ impl }) } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], diff --git a/src/models/queries.rs b/src/models/queries.rs index e4e7679..8813111 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -10,7 +10,7 @@ pub struct PrecursorIndexQuery { } #[derive(Debug, Clone)] -pub struct FragmentGroupIndexQuery { +pub struct FragmentGroupIndexQuery { pub mz_index_ranges: HashMap, pub precursor_query: PrecursorIndexQuery, } diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 982c377..baf2dc1 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -5,7 +5,7 @@ use std::hash::Hash; use std::rc::Rc; use std::time::Instant; -use crate::{Aggregator, ElutionGroup, HasIntegerID, IndexedData, Tolerance, ToleranceAdapter}; +use crate::{Aggregator, ElutionGroup, HasIntegerID, QueriableData, Tolerance, ToleranceAdapter}; /// A struct that can be queried for TIMS data. /// @@ -39,10 +39,12 @@ use crate::{Aggregator, ElutionGroup, HasIntegerID, IndexedData, Tolerance, Tole /// 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). pub struct QueriableTimsData<'a, ID, TA, TL, QF, AE, OE, AG, EG: HasIntegerID> where - AG: Aggregator + Send + Sync, - ID: IndexedData, + AG: Aggregator + Send + Sync, + ID: QueriableData, TA: ToleranceAdapter, TL: Tolerance, + QF: Send + Sync, + AE: Send + Sync + Clone + Copy, { pub indexed_data: &'a ID, pub aggregator_factory: &'a dyn Fn(u64) -> AG, @@ -55,11 +57,13 @@ where impl<'a, ID, TA, TL, QF, AE, OE, AG, EG> QueriableTimsData<'a, ID, TA, TL, QF, AE, OE, AG, EG> where - AG: Aggregator + Send + Sync, - ID: IndexedData, + AG: Aggregator + Send + Sync, + ID: QueriableData, TA: ToleranceAdapter, TL: Tolerance, EG: HasIntegerID, + QF: Send + Sync, + AE: Send + Sync + Clone + Copy, { pub fn query(&self, elution_group: &EG) -> OE { let mut aggregator = (self.aggregator_factory)(elution_group.get_id()); @@ -95,12 +99,14 @@ pub fn query_multi_group<'a, ID, TA, TL, QF, AE, OE, AG, FH>( aggregator_factory: &dyn Fn(u64) -> AG, ) -> Vec where - AG: Aggregator + Send + Sync, - ID: IndexedData, + AG: Aggregator + Send + Sync, + ID: QueriableData, TA: ToleranceAdapter>, TL: Tolerance, OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, + QF: Send + Sync, + AE: Send + Sync + Clone + Copy, { let start = Instant::now(); let mut fragment_queries = Vec::with_capacity(elution_groups.len()); @@ -142,11 +148,13 @@ pub fn query_indexed( elution_group: &ElutionGroup, ) -> OE where - AG: Aggregator + Send + Sync, - ID: IndexedData, + AG: Aggregator + Send + Sync, + ID: QueriableData, TA: ToleranceAdapter>, TL: Tolerance, FH: Clone + Eq + Serialize + Hash + Send + Sync, + QF: Send + Sync, + AE: Send + Sync + Clone + Copy, { let mut aggregator = aggregator_factory(elution_group.id); let prep_query = Rc::new(tolerance_adapter.query_from_elution_group(tolerance, elution_group)); diff --git a/src/traits/aggregator.rs b/src/traits/aggregator.rs index 54a125d..209d444 100644 --- a/src/traits/aggregator.rs +++ b/src/traits/aggregator.rs @@ -1,7 +1,7 @@ -pub trait Aggregator: Send + Sync { +pub trait Aggregator: Send + Sync { + type Item: Send + Sync; type Output: Send + Sync; - fn add(&mut self, item: &I); - // fn fold(&mut self, item: Self); + fn add(&mut self, item: &Self::Item); fn finalize(self) -> Self::Output; } diff --git a/src/traits/indexed_data.rs b/src/traits/indexed_data.rs index 6906a01..a1ce16f 100644 --- a/src/traits/indexed_data.rs +++ b/src/traits/indexed_data.rs @@ -1,9 +1,17 @@ use crate::Aggregator; -pub trait IndexedData { +pub trait QueriableData +where + QF: Send + Sync, + A: Send + Sync + Clone + Copy, +{ fn query(&self, fragment_query: &QF) -> Vec; - fn add_query>(&self, fragment_query: &QF, aggregator: &mut AG); - fn add_query_multi_group>( + fn add_query>( + &self, + fragment_query: &QF, + aggregator: &mut AG, + ); + fn add_query_multi_group>( &self, fragment_queries: &[QF], aggregator: &mut [AG], diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index d6f832a..53cc87a 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -1,7 +1,7 @@ -use log::warn; +use crate::sort_vecs_by_first; +use log::{info, warn}; use std::cmp::Ordering; use std::ops::RangeInclusive; -use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; pub fn squash_frame( mz_array: &[f32], @@ -40,14 +40,6 @@ pub fn squash_frame( let ss_start = mz_array.partition_point(|&x| x < left_e); let ss_end = mz_array.partition_point(|&x| x <= right_e); - let slice_width = ss_end - ss_start; - let local_num_touched = touched[ss_start..ss_end].iter().filter(|&&x| x).count(); - let local_num_untouched = slice_width - local_num_touched; - - if local_num_touched == slice_width { - continue; - } - let mut curr_intensity = 0.0; let mut curr_weighted_mz = 0.0; @@ -55,6 +47,8 @@ pub fn squash_frame( if !touched[i] && intensity_array[i] > 0.0 { curr_intensity += intensity_array[i]; curr_weighted_mz += mz_array[i] * intensity_array[i]; + touched[i] = true; + global_num_touched += 1; } } @@ -65,7 +59,6 @@ pub fn squash_frame( agg_mz[idx] = curr_weighted_mz; touched[ss_start..ss_end].iter_mut().for_each(|x| *x = true); - global_num_touched += local_num_untouched; } if global_num_touched == arr_len { @@ -88,91 +81,188 @@ pub fn squash_frame( pub type TofIntensityVecs = (Vec, Vec); pub type CentroidedVecs = (TofIntensityVecs, Vec); -pub fn lazy_centroid_weighted_frame( - tof_array: &[u32], - ims_array: &[usize], - weight_array: &[u32], - intensity_array: &[u32], - tof_tol_range_fn: impl Fn(&u32) -> RangeInclusive, - ims_tol_range_fn: impl Fn(&usize) -> RangeInclusive, +fn sort_n_check( + agg_intensity: Vec, + agg_tof: Vec, + agg_ims: Vec, +) -> ((Vec, Vec), Vec) { + let (tof_array, ims_array, intensity_array) = + sort_vecs_by_first!(agg_tof, agg_ims, agg_intensity); + + if let Some(x) = intensity_array.last() { + assert!(*x > 0); + let max_tof = tof_array.iter().max().expect("At least one element"); + assert!(*max_tof < (u32::MAX - 1)); + } + ((tof_array, intensity_array), ims_array) +} + +pub struct PeakArrayRefs<'a> { + pub tof_array: &'a [u32], + pub ims_array: &'a [usize], + pub intensity_array: &'a [u32], +} + +impl<'a> PeakArrayRefs<'a> { + pub fn new( + tof_array: &'a [u32], + ims_array: &'a [usize], + intensity_array: &'a [u32], + ) -> PeakArrayRefs<'a> { + // TODO make the asserts optional at compile time. + assert!(tof_array.len() == intensity_array.len()); + assert!(tof_array.len() == ims_array.len()); + assert!( + tof_array.windows(2).all(|x| x[0] <= x[1]), + "Expected tof array to be sorted" + ); + Self { + tof_array, + ims_array, + intensity_array, + } + } + + fn len(&self) -> usize { + self.tof_array.len() + } +} + +/// Splits a global index into a major and minor that +/// matches elements in a collection. +/// +/// As an example if there is a slice of slices, the major index +/// is the index of the slice and the minor index is the index +/// of the element in the slice. +/// +/// Thus if we have [[0,1,2], [3,4,5], [6,7,8]] +/// the major indice are 0,0,0 1,1,1 2,2,2 +/// and the minor 0,1,2 0,1,2 0,1,2 +/// +/// +fn index_split(index: usize, slice_sizes: &[usize]) -> (usize, usize) { + let mut index = index; + for (i, slice_size) in slice_sizes.iter().enumerate() { + if index < *slice_size { + return (i, index); + } + index -= *slice_size; + } + panic!("Index {} is out of bounds", index); +} + +// TODO: Refactor this function +pub fn lazy_centroid_weighted_frame<'a>( + peak_refs: &'a [PeakArrayRefs<'a>], + reference_index: usize, + max_peaks: usize, + tof_tol_range_fn: impl Fn(u32) -> RangeInclusive, + ims_tol_range_fn: impl Fn(usize) -> RangeInclusive, ) -> CentroidedVecs { - let arr_len = tof_array.len(); - const MIN_WEIGHT_PRESERVE: u64 = 50; + let slice_sizes: Vec = peak_refs.iter().map(|x| x.len()).collect(); + let tot_size: usize = slice_sizes.iter().sum(); + let ref_arrays = &peak_refs[reference_index]; + let num_arrays = peak_refs.len(); + assert!(num_arrays > 1); + let arr_len = ref_arrays.len(); + let initial_tot_intensity = ref_arrays + .intensity_array + .iter() + .map(|x| *x as u64) + .sum::(); - // TODO make the asserts optional at compile time. - assert!( - tof_array.windows(2).all(|x| x[0] <= x[1]), - "Expected tof array to be sorted" - ); - assert_eq!( - arr_len, - intensity_array.len(), - "Expected tof and intensity arrays to be the same length" - ); - assert_eq!( - arr_len, - weight_array.len(), - "Expected tof and weight arrays to be the same length" - ); - assert_eq!( - arr_len, - ims_array.len(), - "Expected tof and ims arrays to be the same length" - ); + const MIN_WEIGHT_PRESERVE: u64 = 50; - let mut touched = vec![false; arr_len]; + let mut touched = vec![false; tot_size]; let mut global_num_touched = 0; + let mut num_added = 0; // We will be iterating in decreasing order of intensity - let mut order: Vec = (0..arr_len).collect(); + let mut order: Vec = (0..tot_size).collect(); order.sort_unstable_by(|&a, &b| { - weight_array[b] - .partial_cmp(&weight_array[a]) + let (a_major, a_minor) = index_split(a, &slice_sizes); + let (b_major, b_minor) = index_split(b, &slice_sizes); + + let a_intensity = peak_refs[a_major].intensity_array[a_minor]; + let b_intensity = peak_refs[b_major].intensity_array[b_minor]; + + b_intensity + .partial_cmp(&a_intensity) .unwrap_or(Ordering::Equal) }); - let mut agg_tof = vec![0; arr_len]; - let mut agg_intensity = vec![0; arr_len]; - // We will not be returning the weights. - // let mut agg_weight = vec![0; arr_len]; - let mut agg_ims = vec![0; arr_len]; + assert!({ + let (first_major, first_minor) = index_split(order[0], &slice_sizes); + let (last_major, last_minor) = index_split(order[tot_size - 1], &slice_sizes); + + let first_intensity = peak_refs[first_major].intensity_array[first_minor]; + let last_intensity = peak_refs[last_major].intensity_array[last_minor]; + + first_intensity >= last_intensity + }); + + // TODO explore if making vecs with capacity and appedning is better... + let capacity = max_peaks.min(arr_len); + let mut agg_tof = Vec::with_capacity(capacity); + let mut agg_intensity = Vec::with_capacity(capacity); + let mut agg_ims = Vec::with_capacity(capacity); for idx in order { + let (major_idx, minor_idx) = index_split(idx, &slice_sizes); + if touched[idx] { continue; } - let tof = tof_array[idx]; - let ims = ims_array[idx]; - let tof_range = tof_tol_range_fn(&tof); - let ims_range = ims_tol_range_fn(&ims); - - let ss_start = tof_array.partition_point(|x| x < tof_range.start()); - let ss_end = tof_array.partition_point(|x| x <= tof_range.end()); + let tof = peak_refs[major_idx].tof_array[minor_idx]; + let ims = peak_refs[major_idx].ims_array[minor_idx]; + let this_intensity = peak_refs[major_idx].intensity_array[minor_idx] as u64; + let tof_range = tof_tol_range_fn(tof); + let ims_range = ims_tol_range_fn(ims); let mut curr_intensity = 0u64; let mut curr_weight = 0u64; let mut curr_agg_tof = 0u64; let mut curr_agg_ims = 0u64; - for i in ss_start..ss_end { - if !touched[i] && intensity_array[i] > 0 && ims_range.contains(&ims_array[i]) { - let local_weight = weight_array[i] as u64; - curr_intensity += intensity_array[i] as u64; - curr_agg_tof += tof_array[i] as u64 * local_weight; - curr_agg_ims += ims_array[i] as u64 * local_weight; - curr_weight += local_weight; + let mut local_offset_touched = 0; + for ii in 0..num_arrays { + let local_peak_refs = &peak_refs[ii]; + let ss_start = local_peak_refs + .tof_array + .partition_point(|x| x < tof_range.start()); + let ss_end = local_peak_refs + .tof_array + .partition_point(|x| x <= tof_range.end()); + for i in ss_start..ss_end { + let ti = local_offset_touched + i; + if !touched[ti] && ims_range.contains(&local_peak_refs.ims_array[i]) { + // Peaks are always weighted but not always intense! + let local_intensity = local_peak_refs.intensity_array[i] as u64; + if ii == reference_index { + curr_intensity += local_intensity; + global_num_touched += 1; + } + curr_agg_tof += local_peak_refs.tof_array[i] as u64 * local_intensity; + curr_agg_ims += local_peak_refs.ims_array[i] as u64 * local_intensity; + curr_weight += local_intensity; + touched[ti] = true; + } } + local_offset_touched += local_peak_refs.len(); } - if curr_intensity > 0 && curr_weight > 0 { - agg_intensity[idx] = u32::try_from(curr_intensity).expect("Expected to fit in u32"); - agg_tof[idx] = (curr_agg_tof / curr_weight) as u32; - agg_ims[idx] = (curr_agg_ims / curr_weight) as usize; - - for i in ss_start..ss_end { - touched[i] = true; - global_num_touched += 1; + if curr_intensity > this_intensity { + agg_intensity.push(u32::try_from(curr_intensity).expect("Expected to fit in u32")); + let calc_tof = (curr_agg_tof / curr_weight) as u32; + let calc_ims = (curr_agg_ims / curr_weight) as usize; + assert!(tof_range.contains(&calc_tof)); + assert!(ims_range.contains(&calc_ims)); + agg_tof.push(calc_tof); + agg_ims.push(calc_ims); + num_added += 1; + if num_added == max_peaks { + break; } } @@ -182,18 +272,36 @@ pub fn lazy_centroid_weighted_frame( } // Drop the zeros and sort by mz (tof) - let mut res: Vec<((u32, u32), usize)> = agg_tof - .into_iter() - .zip(agg_intensity.into_iter()) - .zip(agg_ims.into_iter()) - .filter(|&((_, intensity), ims)| (intensity > 0) & (ims > 0)) - .collect(); + let out = sort_n_check(agg_intensity, agg_tof, agg_ims); + let tot_final_intensity = out.0 .1.iter().map(|x| *x as u64).sum::(); + let inten_ratio = tot_final_intensity as f64 / initial_tot_intensity as f64; + assert!(initial_tot_intensity >= tot_final_intensity); + + let output_len = out.0 .0.len(); + let compression_ratio = output_len as f64 / arr_len as f64; + assert!(num_added == output_len); + + // 80% of the intensity being preserved sounds like a good cutoff. + if output_len == max_peaks && inten_ratio < 0.80 { + info!( + "Frame trimmed to max peaks ({}), preserved intensity {}/{} intensity ratio: {} compression_ratio: {} if this is not acceptable consider increasing the parameter.", + max_peaks, tot_final_intensity, initial_tot_intensity, inten_ratio, compression_ratio, + ); + } - res.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); - let output_len = res.len(); - if arr_len > 500 && (output_len == arr_len) { + if arr_len > 5000 && (output_len == arr_len) { warn!("Output length is the same as input length, this is probably a bug"); + warn!("Intensity ratio {:?}", inten_ratio); + warn!("initial_tot_intensity: {:?}", initial_tot_intensity); + warn!("tot_final_intensity: {:?}", tot_final_intensity); + warn!( + "First tof {} -> Range {:?}", + out.0 .0[0], + tof_tol_range_fn(out.0 .0[0]) + ); + panic!(); + // warn!("agg_intensity: {:?}", out.0 .0); } - res.into_iter().unzip() + out } diff --git a/src/utils/tolerance_ranges.rs b/src/utils/tolerance_ranges.rs index dcb3978..faa3d4d 100644 --- a/src/utils/tolerance_ranges.rs +++ b/src/utils/tolerance_ranges.rs @@ -3,7 +3,14 @@ use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter} use std::ops::RangeInclusive; pub fn ppm_tol_range(elem: f64, tol_ppm: f64) -> RangeInclusive { - let utol = tol_ppm / 1e6; + let utol = elem * (tol_ppm / 1e6); + let left_e = elem - utol; + let right_e = elem + utol; + left_e..=right_e +} + +pub fn pct_tol_range(elem: f64, tol_pct: f64) -> RangeInclusive { + let utol = elem * (tol_pct / 100.0); let left_e = elem - utol; let right_e = elem + utol; left_e..=right_e @@ -24,7 +31,7 @@ pub fn scan_tol_range( converter: &Scan2ImConverter, ) -> RangeInclusive { let im = converter.convert(scan as f64); - let im_range = ppm_tol_range(im, tol_pct); + let im_range = pct_tol_range(im, tol_pct); let scan_min = converter.invert(*im_range.start()).round() as usize; let scan_max = converter.invert(*im_range.end()).round() as usize; // Note I need to do this here bc the conversion between scan numbers and ion From 75601e96c3a6def245c77d2df4c9d898a82ecf0d Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sun, 27 Oct 2024 23:58:34 -0700 Subject: [PATCH 08/30] refactor: changed logging to tracing and testing --- Cargo.lock | 308 +++++++++++++++--- Cargo.toml | 18 +- Taskfile.yml | 5 +- benches/benchmark_indices.rs | 30 +- data/get_data.bash | 5 + ..._results_230510_PRTC_13_S1-B1_1_12817.json | 138 ++++++++ ..._diaPASEF_Condition_A_Sample_Alpha_02.json | 57 ++++ deny.toml | 253 ++++++++++++++ metrics/Taskfile.yml | 15 + src/main.rs | 22 +- .../raw_peak_agg/chromatogram_agg.rs | 13 +- .../raw_peak_agg/multi_chromatogram_agg.rs | 4 +- .../aggregators/streaming_aggregator.rs | 2 +- src/models/frames/expanded_frame.rs | 171 ++++------ src/models/frames/raw_frames.rs | 2 +- .../indices/expanded_raw_index/model.rs | 22 +- src/models/indices/raw_file_index.rs | 4 +- .../transposed_quad_index/peak_bucket.rs | 16 +- .../transposed_quad_index/quad_index.rs | 46 +-- .../quad_splitted_transposed_index.rs | 29 +- .../queriable_tims_data.rs | 2 +- src/utils/frame_processing.rs | 290 ++++++++++++----- src/utils/sorting.rs | 163 +-------- 23 files changed, 1136 insertions(+), 479 deletions(-) create mode 100644 data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json create mode 100644 data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json create mode 100644 deny.toml create mode 100644 metrics/Taskfile.yml diff --git a/Cargo.lock b/Cargo.lock index 2bdd6b6..680a5d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,6 +420,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.1.7" @@ -443,29 +452,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "env_filter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -498,6 +484,16 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -544,12 +540,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "iana-time-zone" version = "0.1.60" @@ -608,15 +598,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -770,6 +751,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" @@ -794,6 +784,16 @@ dependencies = [ "adler", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.3" @@ -827,6 +827,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -889,6 +895,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parquet" version = "42.0.0" @@ -926,6 +938,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + [[package]] name = "pkg-config" version = "0.3.30" @@ -938,6 +956,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.18" @@ -1023,8 +1047,17 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1035,9 +1068,15 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.4", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.4" @@ -1139,6 +1178,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1205,6 +1253,16 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "thrift" version = "0.17.0" @@ -1216,15 +1274,43 @@ dependencies = [ "ordered-float", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "timsquery" version = "0.4.0" dependencies = [ "clap", - "env_logger", "indicatif", - "itertools", - "log", "rand", "rand_chacha", "rayon", @@ -1232,6 +1318,10 @@ dependencies = [ "serde", "serde_json", "timsrust", + "tracing", + "tracing-bunyan-formatter", + "tracing-chrome", + "tracing-subscriber", ] [[package]] @@ -1261,6 +1351,108 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash", + "gethostname", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-chrome" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0a738ed5d6450a9fb96e86a23ad808de2b727fd1394585da5cdd6788ffe724" +dependencies = [ + "serde_json", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -1289,6 +1481,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1361,6 +1559,28 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index c65ac8e..38305e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "timsquery" version = "0.4.0" edition = "2021" +license = "Apache-2.0" [dependencies] timsrust = "0.4.1" @@ -13,18 +14,24 @@ serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.122" rmp-serde = "1.3.0" -itertools = "0.13.0" +tracing = { version = "0.1.40", features = ["log"] } +tracing-subscriber = { version = "0.3.18", features = [ + "registry", + "env-filter", +] } +tracing-bunyan-formatter = "0.3.9" -rand = "0.8.5" -rand_chacha = "0.3.1" -log = "0.4.22" -env_logger = "0.11.5" +# These are only used for benchmarks +rand = { version = "0.8.5", optional = true } +rand_chacha = { version = "0.3.1", optional = true } +tracing-chrome = "0.7.2" [features] clap = ["dep:clap"] build-binary = ["clap"] +bench = ["dep:rand", "dep:rand_chacha"] [[bin]] name = "timsquery" @@ -33,6 +40,7 @@ required-features = ["build-binary"] [[bin]] name = "benchmark_indices" path = "benches/benchmark_indices.rs" +required-features = ["bench"] [profile.release] opt-level = 3 diff --git a/Taskfile.yml b/Taskfile.yml index 9ac1af4..bc24eec 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -36,10 +36,9 @@ tasks: - cargo clippy bench: - requires: - vars: [RUST_BACKTRACE, RUST_LOG, TIMS_DATA_FILE] cmds: - - cargo bench + - SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d cargo run --release --features bench --bin benchmark_indices + - uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json templates: sources: diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 21c86d8..d63d531 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -20,6 +20,10 @@ use timsquery::{ }, ElutionGroup, }; +use tracing::subscriber::set_global_default; +use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_chrome::ChromeLayerBuilder; +use tracing_subscriber::{fmt, prelude::*, registry::Registry, EnvFilter, Layer}; const NUM_ELUTION_GROUPS: usize = 1000; const NUM_ITERATIONS: usize = 1; @@ -250,7 +254,7 @@ impl EnvConfig { } } -fn run_encoding_benchmark(raw_file_path: &PathBuf, env_config: EnvConfig) -> Vec { +fn run_encoding_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Vec { let raw_file_path = raw_file_path.to_str().unwrap(); let mut out = vec![]; let rfi = env_config.with_benchmark( @@ -347,7 +351,7 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve "RawFileIndex", &format!("BatchAccess_{}", tol_name), || RawFileIndex::from_path(raw_file_path).unwrap(), - |index, i| { + |index, _i| { let tmp = query_multi_group( index, index, @@ -369,7 +373,7 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve "ExpandedRawFileIndex", &format!("BatchAccess_{}", tol_name), || ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(), - |index, i| { + |index, _i| { let tmp = query_multi_group( index, index, @@ -394,7 +398,7 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve "ExpandedRawFileIndexCentroided", &format!("BatchAccess_{}", tol_name), || ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(), - |index, i| { + |index, _i| { let tmp = query_multi_group( index, index, @@ -419,7 +423,7 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve "TransposedQuadIndex", &format!("BatchAccess_{}", tol_name), || QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(), - |index, i| { + |index, _i| { let tmp = query_multi_group( index, index, @@ -441,7 +445,7 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve "TransposedQuadIndexCentroided", &format!("BatchAccess_{}", tol_name), || QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(), - |index, i| { + |index, _i| { let tmp = query_multi_group( index, index, @@ -468,7 +472,7 @@ fn write_results( let filepath = parent.join(format!("benchmark_results_{}.json", basename)); let file = File::create(&filepath)?; serde_json::to_writer_pretty(file, &report)?; - let out = serde_json::to_string_pretty(&report)?; + let _out = serde_json::to_string_pretty(&report)?; for result in report.results.iter_mut() { result.iterations = None; @@ -482,7 +486,17 @@ fn write_results( } fn main() { - env_logger::init(); + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let formatting_layer = BunyanFormattingLayer::new("timsquery".into(), std::io::stdout); + let (chrome_layer, _guard) = ChromeLayerBuilder::new().build(); + + let subscriber = Registry::default() + .with(env_filter) + .with(chrome_layer) + .with(JsonStorageLayer) + .with(formatting_layer); + + set_global_default(subscriber).expect("Setting default subscriber failed"); let st = Instant::now(); let env_config = EnvConfig::new(); diff --git a/data/get_data.bash b/data/get_data.bash index bd4a39d..046e094 100644 --- a/data/get_data.bash +++ b/data/get_data.bash @@ -1,8 +1,13 @@ # LFQ_timsTOFPro_diaPASEF_Ecoli_03.d.zip +# Long gradient run docker run --rm --platform linux/amd64 -v ${PWD}:/data \ ghcr.io/pride-archive/aspera \ ascp -i /home/aspera/.aspera/cli/etc/asperaweb_id_dsa.openssh \ -TQ -P33001 \ prd_ascp@fasp.ebi.ac.uk:/pride/data/archive/2022/02/PXD028735/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d.zip \ /data + +# 22 min hela from some random day +aws s3 cp s3://terraform-workstations-bucket/jspaezp/20241027_PRTC/230510_PRTC_13.d.tar . +tar -xf 230510_PRTC_13.d.tar diff --git a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json new file mode 100644 index 0000000..c42d54c --- /dev/null +++ b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json @@ -0,0 +1,138 @@ +{ + "settings": { + "num_elution_groups": 1000, + "num_iterations": 1 + }, + "results": [ + { + "name": "RawFileIndex", + "context": "Encoding", + "mean_duration_seconds": 0.006061387115151513, + "mean_duration_human_readable": "6.061387ms", + "setup_time_seconds": 1.75e-6, + "setup_time_human_readable": "1.75µs", + "note": null + }, + { + "name": "ExpandedRawFileIndexCentroided", + "context": "Encoding", + "mean_duration_seconds": 102.017141791, + "mean_duration_human_readable": "102.017141791s", + "setup_time_seconds": 9.58e-7, + "setup_time_human_readable": "958ns", + "note": null + }, + { + "name": "ExpandedRawFileIndex", + "context": "Encoding", + "mean_duration_seconds": 7.339009041, + "mean_duration_human_readable": "7.339009041s", + "setup_time_seconds": 1.042e-6, + "setup_time_human_readable": "1.042µs", + "note": null + }, + { + "name": "TransposedQuadIndex", + "context": "Encoding", + "mean_duration_seconds": 64.579125625, + "mean_duration_human_readable": "64.579125625s", + "setup_time_seconds": 1.167e-6, + "setup_time_human_readable": "1.167µs", + "note": null + }, + { + "name": "TransposedQuadIndexCentroided", + "context": "Encoding", + "mean_duration_seconds": 105.091189208, + "mean_duration_human_readable": "105.091189208s", + "setup_time_seconds": 1.25e-6, + "setup_time_human_readable": "1.25µs", + "note": null + }, + { + "name": "RawFileIndex", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 28.889043334, + "mean_duration_human_readable": "28.889043334s", + "setup_time_seconds": 0.077295959, + "setup_time_human_readable": "77.295959ms", + "note": "RawFileIndex::query_multi_group aggregated 71227 " + }, + { + "name": "ExpandedRawFileIndex", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.0023722945118483407, + "mean_duration_human_readable": "2.372294ms", + "setup_time_seconds": 5.8142151250000005, + "setup_time_human_readable": "5.814215125s", + "note": "ExpandedRawFileIndex::query_multi_group aggregated 72731 " + }, + { + "name": "ExpandedRawFileIndex", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 0.363698, + "mean_duration_human_readable": "363.698ms", + "setup_time_seconds": 5.680389416, + "setup_time_human_readable": "5.680389416s", + "note": "ExpandedRawFileIndex::query_multi_group aggregated 11194054 " + }, + { + "name": "ExpandedRawFileIndexCentroided", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.0011996081642685855, + "mean_duration_human_readable": "1.199608ms", + "setup_time_seconds": 98.870238958, + "setup_time_human_readable": "98.870238958s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 21990 " + }, + { + "name": "ExpandedRawFileIndexCentroided", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 0.07626683057142856, + "mean_duration_human_readable": "76.26683ms", + "setup_time_seconds": 114.076896875, + "setup_time_human_readable": "114.076896875s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5012679 " + }, + { + "name": "TransposedQuadIndex", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.005130067953846155, + "mean_duration_human_readable": "5.130067ms", + "setup_time_seconds": 72.976041375, + "setup_time_human_readable": "72.976041375s", + "note": "TransposedQuadIndex::query_multi_group aggregated 69045 " + }, + { + "name": "TransposedQuadIndex", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 0.00552524769060773, + "mean_duration_human_readable": "5.525247ms", + "setup_time_seconds": 87.057384125, + "setup_time_human_readable": "87.057384125s", + "note": "TransposedQuadIndex::query_multi_group aggregated 9971215 " + }, + { + "name": "TransposedQuadIndexCentroided", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.001565241712050078, + "mean_duration_human_readable": "1.565241ms", + "setup_time_seconds": 118.473094709, + "setup_time_human_readable": "118.473094709s", + "note": "TransposedQuadIndex::query_multi_group aggregated 17773 " + }, + { + "name": "TransposedQuadIndexCentroided", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 0.003929947243137255, + "mean_duration_human_readable": "3.929947ms", + "setup_time_seconds": 124.951945458, + "setup_time_human_readable": "124.951945458s", + "note": "TransposedQuadIndex::query_multi_group aggregated 4425627 " + } + ], + "metadata": { + "basename": "230510_PRTC_13_S1-B1_1_12817" + }, + "full_benchmark_time_seconds": 961.0857955 +} \ No newline at end of file diff --git a/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json new file mode 100644 index 0000000..14ff6fe --- /dev/null +++ b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json @@ -0,0 +1,57 @@ +{ + "settings": { + "num_elution_groups": 1000, + "num_iterations": 1 + }, + "results": [ + { + "name": "RawFileIndex", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 3.778817917, + "mean_duration_human_readable": "3.778817917s", + "setup_time_seconds": 0.241755042, + "setup_time_human_readable": "241.755042ms", + "note": "RawFileIndex::query_multi_group aggregated 9144 " + }, + { + "name": "ExpandedRawFileIndexCentroided", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.0005595513605823068, + "mean_duration_human_readable": "559.551µs", + "setup_time_seconds": 619.45208575, + "setup_time_human_readable": "619.45208575s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 873 " + }, + { + "name": "ExpandedRawFileIndexCentroided", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 7.19476525, + "mean_duration_human_readable": "7.19476525s", + "setup_time_seconds": 636.328349375, + "setup_time_human_readable": "636.328349375s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 193555055 " + }, + { + "name": "TransposedQuadIndexCentroided", + "context": "BatchAccess_narrow_rt", + "mean_duration_seconds": 0.0017960869371633765, + "mean_duration_human_readable": "1.796086ms", + "setup_time_seconds": 665.489050917, + "setup_time_human_readable": "665.489050917s", + "note": "TransposedQuadIndex::query_multi_group aggregated 715 " + }, + { + "name": "TransposedQuadIndexCentroided", + "context": "BatchAccess_full_rt", + "mean_duration_seconds": 0.0027502779313186817, + "mean_duration_human_readable": "2.750277ms", + "setup_time_seconds": 663.333964959, + "setup_time_human_readable": "663.333964959s", + "note": "TransposedQuadIndex::query_multi_group aggregated 167889419 " + } + ], + "metadata": { + "basename": "LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02" + }, + "full_benchmark_time_seconds": 2621.0749125 +} \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..4592ef2 --- /dev/null +++ b/deny.toml @@ -0,0 +1,253 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +[graph] + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + { triple = "aarch64-apple-darwin" }, + { triple = "x86_64-apple-darwin" }, + { triple = "i686-pc-windows-gnu" }, + { triple = "i686-pc-windows-msvc" }, + { triple = "x86_64-pc-windows-gnu" }, + { triple = "x86_64-pc-windows-msvc" }, + { triple = "i686-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-musl" }, + # { triple = "wasm32-unknown-unknown" }, + # { triple = "x86_64-unknown-redox" }, +] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = false +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +features = [ + "build-binary", +] + +[output] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +version = 2 +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +yanked = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", + "RUSTSEC-2023-0086", # Lexical-core -> arrow issue, updated in main in Sept-2024, unreleased. +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +version = 2 +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-3-Clause", + "Unicode-DFS-2016", + "Zlib", +] + +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.95 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +[[licenses.clarify]] +name = "webpki" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + + +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ +# Each entry is a crate relative path, and the (opaque) hash of its contents +#{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, + # + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, +] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#name = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite. +skip-tree = [] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = ["https://github.com/lazear/sage"] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +# github = [""] diff --git a/metrics/Taskfile.yml b/metrics/Taskfile.yml new file mode 100644 index 0000000..1ed6ac5 --- /dev/null +++ b/metrics/Taskfile.yml @@ -0,0 +1,15 @@ +version: "3" + +interval: 100ms + +env: + PGO_DATA_DIR: tmp/pgo-data + BIN_EXTRAS: "--features build-binary --bin timsquery" + +dotenv: [".env"] + +tasks: + build: + dir: "{{.TASKFILE_DIR}}" + cmds: + - cargo build $BIN_EXTRAS diff --git a/src/main.rs b/src/main.rs index 1dc7c05..82f5e06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +use clap::{Parser, Subcommand}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Instant; use timsquery::models::elution_group::ElutionGroup; use timsquery::queriable_tims_data::queriable_tims_data::query_multi_group; use timsquery::traits::tolerance::DefaultTolerance; +use timsquery::traits::tolerance::{MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance}; use timsquery::{ models::aggregators::{ ChromatomobilogramStats, ExtractedIonChromatomobilogram, MultiCMGStatsFactory, @@ -11,14 +14,20 @@ use timsquery::{ models::indices::raw_file_index::RawFileIndex, models::indices::transposed_quad_index::QuadSplittedTransposedIndex, }; - -use timsquery::traits::tolerance::{MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance}; - -use clap::{Parser, Subcommand}; -use serde::{Deserialize, Serialize}; +use tracing::instrument; +use tracing::subscriber::set_global_default; +use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_subscriber::{fmt, prelude::*, registry::Registry, EnvFilter, Layer}; fn main() { - env_logger::init(); + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let formatting_layer = BunyanFormattingLayer::new("timsquery".into(), std::io::stdout); + let subscriber = Registry::default() + .with(env_filter) + .with(JsonStorageLayer) + .with(formatting_layer); + + set_global_default(subscriber).expect("Setting default subscriber failed"); let args = Args::parse(); match args.command { @@ -88,6 +97,7 @@ fn template_elution_groups(num: usize) -> Vec> { egs } +#[instrument] fn main_query_index(args: QueryIndexArgs) { let args_clone = args.clone(); diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index c86e22b..36281e4 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -1,8 +1,7 @@ use super::super::streaming_aggregator::RunningStatsCalculator; use crate::models::frames::raw_peak::RawPeak; -use crate::sort_by_indices_multi; +use crate::sort_vecs_by_first; use crate::traits::aggregator::Aggregator; -use crate::utils::sorting::argsort_by; use serde::Serialize; use std::collections::BTreeMap; @@ -150,9 +149,7 @@ impl ChromatomobilogramStatsArrays { } pub fn sort_by_rt(&mut self) { - let mut indices = argsort_by(&self.retention_time_miliseconds, |x| *x); - sort_by_indices_multi!( - &mut indices, + let x = sort_vecs_by_first!( &mut self.retention_time_miliseconds, &mut self.tof_index_means, &mut self.tof_index_sds, @@ -160,6 +157,12 @@ impl ChromatomobilogramStatsArrays { &mut self.scan_index_sds, &mut self.intensities ); + self.retention_time_miliseconds = x.0; + self.tof_index_means = x.1; + self.tof_index_sds = x.2; + self.scan_index_means = x.3; + self.scan_index_sds = x.4; + self.intensities = x.5; } } diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index 35783c8..d446db9 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -6,10 +6,10 @@ use crate::models::aggregators::streaming_aggregator::RunningStatsCalculator; use crate::models::frames::raw_peak::RawPeak; use crate::traits::aggregator::Aggregator; use crate::utils::math::{lnfact, lnfact_float}; -use log::{debug, warn}; use serde::Serialize; use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; +use tracing::{debug, warn}; use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; @@ -414,7 +414,7 @@ mod tests { fn test_value_vs_baseline() { let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; let baseline_window_size = 3; - let baseline = rolling_median(&vals, baseline_window_size, f64::NAN); + let _baseline = rolling_median(&vals, baseline_window_size, f64::NAN); let out = calculate_value_vs_baseline(&vals, baseline_window_size); let expect_val = vec![f64::NAN, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, f64::NAN]; let all_close = out diff --git a/src/models/aggregators/streaming_aggregator.rs b/src/models/aggregators/streaming_aggregator.rs index 2ab67b2..d015879 100644 --- a/src/models/aggregators/streaming_aggregator.rs +++ b/src/models/aggregators/streaming_aggregator.rs @@ -1,4 +1,4 @@ -use log::debug; +use tracing::debug; // Generic streaming aggregator that takes a pair of unsigned ints one with a value // and another with a weight and in a streaming fashion adds the value to the accumulator diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 638cfc4..1cfe84d 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,7 +1,6 @@ use super::single_quad_settings::{ expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, }; -use itertools::Itertools; use rayon::prelude::*; use std::collections::HashMap; use std::marker::PhantomData; @@ -11,17 +10,16 @@ use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; use super::peak_in_quad::PeakInQuad; -use crate::sort_by_indices_multi; use crate::sort_vecs_by_first; use crate::utils::compress_explode::explode_vec; use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; -use crate::utils::sorting::argsort_by; use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; -use log::{info, trace, warn}; use timsrust::{ readers::{FrameReader, FrameReaderError, MetadataReaderError}, TimsRustError, }; +use tracing::instrument; +use tracing::{info, trace, warn}; /// A frame after expanding the mobility data and re-sorting it by tof. #[derive(Debug, Clone)] @@ -63,22 +61,6 @@ pub struct ExpandedFrameSlice { _sorting_state: PhantomData, } -fn sort_by_tof2( - tof_indices: Vec, - scan_numbers: Vec, - intensities: Vec, -) -> ((Vec, Vec), Vec) { - let mut combined: Vec<((u32, usize), u32)> = tof_indices - .iter() - .zip(scan_numbers.iter()) - .zip(intensities.iter()) - .map(|((tof, scan), inten)| ((*tof, *scan), *inten)) - .collect(); - combined.sort_unstable_by(|a, b| a.0 .0.partial_cmp(&b.0 .0).unwrap()); - let ((tof_indices, scan_numbers), intensities) = combined.into_iter().unzip(); - ((tof_indices, scan_numbers), intensities) -} - // Example of how to use it with your specific types fn sort_by_tof_macro( tof_indices: Vec, @@ -141,23 +123,17 @@ impl ExpandedFrameSlice { peak_ind_start..peak_ind_end }; - match scan_range { - Some((min_scan, max_scan)) => { - assert!(min_scan <= max_scan); - } - None => {} - }; + if let Some((min_scan, max_scan)) = scan_range { + assert!(min_scan <= max_scan); + } for peak_ind in peak_range { let scan_index = self.scan_numbers[peak_ind]; - match scan_range { - Some((min_scan, max_scan)) => { - if scan_index < min_scan || scan_index > max_scan { - continue; - } + if let Some((min_scan, max_scan)) = scan_range { + if scan_index < min_scan || scan_index > max_scan { + continue; } - None => {} - }; + } let intensity = self.intensities[peak_ind]; let tof_index = self.tof_indices[peak_ind]; @@ -250,8 +226,6 @@ fn expand_fragmented_frame( window_subindex: i as u8, _sorting_state: PhantomData::, }; - // Q: is this sorting twice? since sort before creating the expanded frame slices. - // TODO: Use the state type pattern to make sure only one sort happens. out.push(curr_slice.sort_by_tof()); } out @@ -271,17 +245,12 @@ pub fn expand_and_split_frame(frame: Frame) -> Vec Self { - let mut tof_indices = frame.tof_indices; - let mut scan_numbers = explode_vec(&frame.scan_offsets); - let mut intensities = frame.intensities; - - let mut indices = argsort_by(&tof_indices, |x| *x); - sort_by_indices_multi!( - &mut indices, - &mut tof_indices, - &mut scan_numbers, - &mut intensities - ); + let scan_numbers = explode_vec(&frame.scan_offsets); + + let sorted = sort_vecs_by_first!(frame.tof_indices, scan_numbers, frame.intensities); + let tof_indices = sorted.0; + let scan_numbers = sorted.1; + let intensities = sorted.2; ExpandedFrame { tof_indices, @@ -298,30 +267,28 @@ impl ExpandedFrame { } } +type QuadBundledFrameslices = + HashMap, Vec>>; + +#[instrument(skip(frame_iter))] pub fn par_expand_and_arrange_frames( frame_iter: impl ParallelIterator, -) -> HashMap, Vec>> { - let start = Instant::now(); - let split = frame_iter.flat_map(|frame| expand_and_split_frame(frame)); - // let mut out = HashMap::new(); - // for es in split { - // out.entry(es.quadrupole_settings) - // .or_insert(Vec::new()) - // .push(es); - // } - // - // Attempted implementation so the folding occurs in parrallel. +) -> QuadBundledFrameslices { + let split = frame_iter.flat_map(expand_and_split_frame); + + // Implementation so the folding occurs in parrallel. // Inspired by/Copied from: https://stackoverflow.com/a/70097253/4295016 - let mut out = split - .fold(HashMap::new, |mut acc, x| { - acc.entry(x.quadrupole_settings) - .or_insert(Vec::new()) - .push(x); - acc - }) + let mut out: QuadBundledFrameslices = split + .fold( + HashMap::new, + |mut acc: QuadBundledFrameslices, x: ExpandedFrameSlice| { + acc.entry(x.quadrupole_settings).or_default().push(x); + acc + }, + ) .reduce_with(|mut acc1, acc2| { for (qs, frameslices) in acc2.into_iter() { - acc1.entry(qs).or_insert(Vec::new()).extend(frameslices); + acc1.entry(qs).or_default().extend(frameslices); } acc1 }) @@ -331,8 +298,6 @@ pub fn par_expand_and_arrange_frames( for (_, es) in out.iter_mut() { es.par_sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); } - let end = start.elapsed(); - info!("Expanding and arranging frames took {:#?}", end); out } @@ -434,6 +399,13 @@ fn warn_and_skip_badframes( }) } +#[instrument( + skip(frame_reader), + fields( + num_frames = %frame_reader.len(), + path = frame_reader.get_path().to_str(), + ) +)] pub fn par_read_and_expand_frames( frame_reader: &FrameReader, centroiding_config: FrameProcessingConfig, @@ -465,22 +437,16 @@ pub fn par_read_and_expand_frames( max_ms2_peaks, ims_converter, mz_converter, - } => { - let expanded_frames = par_expand_and_centroid_frames( - curr_iter, - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms2_peaks, - &ims_converter.unwrap(), - &mz_converter.unwrap(), - ); - expanded_frames - } - FrameProcessingConfig::NotCentroided => { - let expanded_frames = par_expand_and_arrange_frames(curr_iter); - expanded_frames - } + } => par_expand_and_centroid_frames( + curr_iter, + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms2_peaks, + &ims_converter.unwrap(), + &mz_converter.unwrap(), + ), + FrameProcessingConfig::NotCentroided => par_expand_and_arrange_frames(curr_iter), }; all_expanded_frames.extend(expanded_frames); @@ -498,22 +464,16 @@ pub fn par_read_and_expand_frames( max_ms2_peaks: _max_ms2_peaks, ims_converter, mz_converter, - } => { - let expanded_frames = par_expand_and_centroid_frames( - ms1_iter, - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks, - &ims_converter.unwrap(), - &mz_converter.unwrap(), - ); - expanded_frames - } - FrameProcessingConfig::NotCentroided => { - let expanded_frames = par_expand_and_arrange_frames(ms1_iter); - expanded_frames - } + } => par_expand_and_centroid_frames( + ms1_iter, + ims_tol_pct, + mz_tol_ppm, + window_width, + max_ms1_peaks, + &ims_converter.unwrap(), + &mz_converter.unwrap(), + ), + FrameProcessingConfig::NotCentroided => par_expand_and_arrange_frames(ms1_iter), }; all_expanded_frames.extend(expanded_ms1_frames); info!("Done reading and expanding frames"); @@ -521,6 +481,7 @@ pub fn par_read_and_expand_frames( Ok(all_expanded_frames) } +#[instrument(skip(frames))] pub fn par_expand_and_centroid_frames( frames: impl ParallelIterator, ims_tol_pct: f64, @@ -602,6 +563,7 @@ fn centroid_frameslice_window2( } } +#[instrument(skip(frameslices))] pub fn par_lazy_centroid_frameslices( frameslices: &[ExpandedFrameSlice], window_width: usize, @@ -613,9 +575,12 @@ pub fn par_lazy_centroid_frameslices( ) -> Vec> { assert!( frameslices - .iter() - .tuple_windows() - .map(|(a, b)| { a.rt < b.rt && a.quadrupole_settings == b.quadrupole_settings }) + .windows(2) + .map(|window| { + let a = &window[0]; + let b = &window[1]; + a.rt < b.rt && a.quadrupole_settings == b.quadrupole_settings + }) .all(|x| x), "All frames should be sorted by rt and have the same quad settings" ); @@ -635,7 +600,7 @@ pub fn par_lazy_centroid_frameslices( frameslices .par_windows(window_width) - .map(|window| local_lambda(window)) + .map(local_lambda) .collect() } diff --git a/src/models/frames/raw_frames.rs b/src/models/frames/raw_frames.rs index d3e6daa..bb071d6 100644 --- a/src/models/frames/raw_frames.rs +++ b/src/models/frames/raw_frames.rs @@ -1,7 +1,7 @@ use timsrust::{Frame, QuadrupoleSettings}; use super::raw_peak::RawPeak; -use log::trace; +use tracing::trace; pub fn scans_matching_quad( quad_settings: &QuadrupoleSettings, diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index 11e85cd..921c057 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -12,7 +12,6 @@ use crate::models::frames::single_quad_settings::{ use crate::models::queries::FragmentGroupIndexQuery; use crate::traits::indexed_data::QueriableData; use crate::ToleranceAdapter; -use log::info; use rayon::prelude::*; use serde::Serialize; use std::collections::HashMap; @@ -20,8 +19,8 @@ use std::hash::Hash; use std::time::Instant; use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; use timsrust::readers::{FrameReader, MetadataReader}; - -type QuadSettingsIndex = usize; +use tracing::info; +use tracing::instrument; #[derive(Debug)] pub struct ExpandedRawFrameIndex { @@ -111,11 +110,18 @@ impl ExpandedRawFrameIndex { } } + #[instrument(name = "ExpandedRawFrameIndex::from_path_centroided")] pub fn from_path_centroided(path: &str) -> Result { let config = FrameProcessingConfig::default_centroided(); Self::from_path_base(path, config) } + #[instrument(name = "ExpandedRawFrameIndex::from_path")] + pub fn from_path(path: &str) -> Result { + Self::from_path_base(path, FrameProcessingConfig::NotCentroided) + } + + #[instrument(name = "ExpandedRawFrameIndex::from_path_base")] pub fn from_path_base( path: &str, centroid_config: FrameProcessingConfig, @@ -128,10 +134,8 @@ impl ExpandedRawFrameIndex { let sql_path = std::path::Path::new(path).join("analysis.tdf"); let meta_converters = MetadataReader::new(&sql_path)?; - let centroid_config = centroid_config.with_converters( - meta_converters.im_converter.clone(), - meta_converters.mz_converter.clone(), - ); + let centroid_config = centroid_config + .with_converters(meta_converters.im_converter, meta_converters.mz_converter); let st = Instant::now(); let centroided_split_frames = par_read_and_expand_frames(&file_reader, centroid_config)?; @@ -168,10 +172,6 @@ impl ExpandedRawFrameIndex { Ok(out) } - - pub fn from_path(path: &str) -> Result { - Self::from_path_base(path, FrameProcessingConfig::NotCentroided) - } } impl diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index 60688be..133e406 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -7,7 +7,6 @@ use crate::traits::aggregator::Aggregator; use crate::traits::indexed_data::QueriableData; use crate::ElutionGroup; use crate::ToleranceAdapter; -use log::trace; use rayon::iter::ParallelIterator; use serde::Serialize; use std::fmt::Debug; @@ -16,6 +15,7 @@ use timsrust::converters::ConvertableDomain; use timsrust::readers::{FrameReader, FrameReaderError, MetadataReader}; use timsrust::TimsRustError; use timsrust::{Frame, Metadata, QuadrupoleSettings}; +use tracing::trace; pub struct RawFileIndex { file_reader: FrameReader, @@ -168,7 +168,7 @@ impl RawFileIndex { .map(|(k, v)| { let mz_range = tol.mz_range(*v); ( - k.clone(), + *k, ( self.meta_converters.mz_converter.invert(mz_range.0) as u32, self.meta_converters.mz_converter.invert(mz_range.1) as u32, diff --git a/src/models/indices/transposed_quad_index/peak_bucket.rs b/src/models/indices/transposed_quad_index/peak_bucket.rs index d654ad0..d0f0e7d 100644 --- a/src/models/indices/transposed_quad_index/peak_bucket.rs +++ b/src/models/indices/transposed_quad_index/peak_bucket.rs @@ -1,7 +1,6 @@ -use crate::sort_by_indices_multi; +use crate::sort_vecs_by_first; use crate::utils::compress_explode::compress_vec; use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::sorting::argsort_by; use std::fmt::Display; pub struct PeakInBucket { @@ -105,13 +104,12 @@ impl PeakBucketBuilder { } pub fn build(mut self) -> PeakBucket { - let mut indices = argsort_by(&self.scan_offsets, |x| *x); - sort_by_indices_multi!( - &mut indices, - &mut self.scan_offsets, - &mut self.retention_times, - &mut self.intensities - ); + let sorted = + sort_vecs_by_first!(&self.scan_offsets, &self.retention_times, &self.intensities); + self.scan_offsets = sorted.0; + self.retention_times = sorted.1; + self.intensities = sorted.2; + // TODO consider if I really need to compress this. // Options: // 1. Change the compression of scans (I use the default in timsrust but im sure we can do diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 8a4ac88..492acce 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -3,15 +3,14 @@ use super::peak_bucket::{PeakBucket, PeakInBucket}; use crate::models::frames::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; -use crate::sort_by_indices_multi; +use crate::sort_vecs_by_first; use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::sorting::par_argsort_by; -use log::debug; -use log::info; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::time::Instant; use timsrust::converters::{ConvertableDomain, Frame2RtConverter}; +use tracing::instrument; +use tracing::{debug, info}; #[derive(Debug)] pub struct TransposedQuadIndex { @@ -200,13 +199,9 @@ impl TransposedQuadIndexBuilder { self.frame_rts.push(slice.rt); } + #[instrument(skip(self))] pub fn build(self) -> TransposedQuadIndex { // TODO: Refactor this function, its getting pretty large. - let st = Instant::now(); - info!( - "TransposedQuadIndex::build start ... quad_settings = {:?}", - self.quad_settings - ); let max_tof = *self .tof_slices .iter() @@ -267,7 +262,6 @@ impl TransposedQuadIndexBuilder { .collect(); let bbe = bb_st.elapsed(); - let elapsed = st.elapsed(); info!( "TransposedQuadIndex::add_frame_slice adding peaks took {:#?} for {} peaks", aps.elapsed(), @@ -277,10 +271,6 @@ impl TransposedQuadIndexBuilder { "TransposedQuadIndex::add_frame_slice building buckets took {:#?}", bbe ); - info!( - "TransposedQuadIndex::build quad_settings={:?} took {:#?}", - quad_settings, elapsed - ); TransposedQuadIndex { quad_settings, frame_rts: out_rts, @@ -369,10 +359,10 @@ impl TransposedQuadIndexBuilder { } let concat_st = Instant::now(); - let mut int_slice = self.int_slices[start..end].concat(); - let mut tof_slice = self.tof_slices[start..end].concat(); - let mut scan_slice = self.scan_slices[start..end].concat(); - let mut rt_slice: Vec = self.frame_rts[start..end] + let int_slice = self.int_slices[start..end].concat(); + let tof_slice = self.tof_slices[start..end].concat(); + let scan_slice = self.scan_slices[start..end].concat(); + let rt_slice: Vec = self.frame_rts[start..end] .iter() .zip(self.tof_slices[start..end].iter()) .flat_map(|(rt, tofslice)| vec![*rt as f32; tofslice.len()]) @@ -381,18 +371,12 @@ impl TransposedQuadIndexBuilder { let concat_elapsed = concat_st.elapsed(); let sorting_st = Instant::now(); - // let mut indices = argsort_by(&tof_slice, |x| *x); - let mut indices = par_argsort_by(&tof_slice, |x| *x); - - let argsort_elapsed = sorting_st.elapsed(); - sort_by_indices_multi!( - &mut indices, - &mut tof_slice, - &mut scan_slice, - &mut int_slice, - &mut rt_slice - ); + let out = sort_vecs_by_first!(tof_slice, scan_slice, int_slice, rt_slice); + let tof_slice = out.0; + let scan_slice = out.1; + let int_slice = out.2; + let rt_slice = out.3; let sorting_elapsed = sorting_st.elapsed(); @@ -453,8 +437,8 @@ impl TransposedQuadIndexBuilder { let insertion_elapsed = insertion_st.elapsed(); info!( - "BatchedBuild: quad_settings={:?} start={:?} end={:?}/{} peaks {}/{} concat took {:#?} sorting took arg: {:#?} / {:#?} insertion took {:#?}", - self.quad_settings, start, end, num_slices, added_peaks, tot_peaks, concat_elapsed, argsort_elapsed, sorting_elapsed, insertion_elapsed, + "BatchedBuild: quad_settings={:?} start={:?} end={:?}/{} peaks {}/{} concat took {:#?} sorting took: {:#?} insertion took {:#?}", + self.quad_settings, start, end, num_slices, added_peaks, tot_peaks, concat_elapsed, sorting_elapsed, insertion_elapsed, ); start = end; peaks_in_chunk = 0; diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index f596fe0..7c96e34 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -14,7 +14,6 @@ use crate::models::queries::FragmentGroupIndexQuery; use crate::traits::indexed_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; -use log::{debug, info, trace}; use rayon::prelude::*; use serde::Serialize; use std::collections::HashMap; @@ -27,6 +26,8 @@ use timsrust::readers::{FrameReader, MetadataReader}; use timsrust::Frame; use timsrust::Metadata; use timsrust::TimsRustError; +use tracing::instrument; +use tracing::{debug, info, trace}; // TODO break this module apart ... its getting too big for my taste // - JSP: 2024-11-19 @@ -137,6 +138,7 @@ impl Display for QuadSplittedTransposedIndex { } impl QuadSplittedTransposedIndex { + #[instrument(name = "QuadSplittedTransposedIndex::from_path")] pub fn from_path(path: &str) -> Result { let st = Instant::now(); info!("Building transposed quad index from path {}", path); @@ -148,6 +150,7 @@ impl QuadSplittedTransposedIndex { Ok(out) } + #[instrument(name = "QuadSplittedTransposedIndex::from_path_centroided")] pub fn from_path_centroided(path: &str) -> Result { let st = Instant::now(); info!( @@ -181,14 +184,6 @@ impl QuadSplittedTransposedIndexBuilder { Self::default() } - fn add_frame(&mut self, frame: Frame) { - let expanded_slices = expand_and_split_frame(frame); - - for es in expanded_slices.into_iter() { - self.add_frame_slice(es); - } - } - fn add_frame_slice(&mut self, frame_slice: ExpandedFrameSlice) { // Add key if it doesnt exist ... self.indices @@ -204,10 +199,12 @@ impl QuadSplittedTransposedIndexBuilder { .add_frame_slice(frame_slice); } + #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path")] fn from_path(path: &str) -> Result { Self::from_path_base(path, FrameProcessingConfig::NotCentroided) } + #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path_centroided")] fn from_path_centroided(path: &str) -> Result { let config = FrameProcessingConfig::default_centroided(); Self::from_path_base(path, config) @@ -216,6 +213,7 @@ impl QuadSplittedTransposedIndexBuilder { // TODO: I think i should split this into two functions, one that starts the builder // and one that adds the frameslices, maybe even have a config struct that dispatches // the right preprocessing steps. + #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path_base")] fn from_path_base( path: &str, centroid_config: FrameProcessingConfig, @@ -238,13 +236,9 @@ impl QuadSplittedTransposedIndexBuilder { let centroid_config = centroid_config .with_converters(meta_converters.im_converter, meta_converters.mz_converter); - let st = Instant::now(); let split_frames = par_read_and_expand_frames(&file_reader, centroid_config)?; - let centroided_elap = st.elapsed(); - debug!("Reading + Centroiding took {:#?}", centroided_elap); - - let st = Instant::now(); + // TODO use the rayon contructor to fold let out2: Result, TimsRustError> = split_frames .into_par_iter() .map(|(q, frameslices)| { @@ -261,12 +255,6 @@ impl QuadSplittedTransposedIndexBuilder { x }); final_out.fold(out2); - let build_elap = st.elapsed(); - - info!( - "Reading all frames + centroiding took {:#?}, building took {:#?}", - centroided_elap, build_elap - ); Ok(final_out) } @@ -281,6 +269,7 @@ impl QuadSplittedTransposedIndexBuilder { self.added_peaks += other.added_peaks; } + #[instrument(skip(self))] pub fn build(self) -> QuadSplittedTransposedIndex { let mut indices = HashMap::new(); let mut flat_quad_settings = Vec::new(); diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index baf2dc1..2595405 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -1,9 +1,9 @@ -use log::info; use rayon::prelude::*; use serde::Serialize; use std::hash::Hash; use std::rc::Rc; use std::time::Instant; +use tracing::info; use crate::{Aggregator, ElutionGroup, HasIntegerID, QueriableData, Tolerance, ToleranceAdapter}; diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 53cc87a..302af05 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -1,82 +1,7 @@ use crate::sort_vecs_by_first; -use log::{info, warn}; use std::cmp::Ordering; use std::ops::RangeInclusive; - -pub fn squash_frame( - mz_array: &[f32], - intensity_array: &[f32], - tol_ppm: f32, -) -> (Vec, Vec) { - // Make sure the mz array is sorted - assert!(mz_array.windows(2).all(|x| x[0] <= x[1])); - - let arr_len = mz_array.len(); - let mut touched = vec![false; arr_len]; - let mut global_num_touched = 0; - - let mut order: Vec = (0..arr_len).collect(); - order.sort_unstable_by(|&a, &b| { - intensity_array[b] - .partial_cmp(&intensity_array[a]) - .unwrap_or(Ordering::Equal) - }); - - let mut agg_mz = vec![0.0; arr_len]; - let mut agg_intensity = vec![0.0; arr_len]; - - let utol = tol_ppm / 1e6; - - for &idx in &order { - if touched[idx] { - continue; - } - - let mz = mz_array[idx]; - let da_tol = mz * utol; - let left_e = mz - da_tol; - let right_e = mz + da_tol; - - let ss_start = mz_array.partition_point(|&x| x < left_e); - let ss_end = mz_array.partition_point(|&x| x <= right_e); - - let mut curr_intensity = 0.0; - let mut curr_weighted_mz = 0.0; - - for i in ss_start..ss_end { - if !touched[i] && intensity_array[i] > 0.0 { - curr_intensity += intensity_array[i]; - curr_weighted_mz += mz_array[i] * intensity_array[i]; - touched[i] = true; - global_num_touched += 1; - } - } - - if curr_intensity > 0.0 { - curr_weighted_mz /= curr_intensity; - - agg_intensity[idx] = curr_intensity; - agg_mz[idx] = curr_weighted_mz; - - touched[ss_start..ss_end].iter_mut().for_each(|x| *x = true); - } - - if global_num_touched == arr_len { - break; - } - } - - // Drop the zeros and sort - let mut result: Vec<(f32, f32)> = agg_mz - .into_iter() - .zip(agg_intensity.into_iter()) - .filter(|&(mz, intensity)| mz > 0.0 && intensity > 0.0) - .collect(); - - result.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); - - result.into_iter().unzip() -} +use tracing::{error, info, warn}; pub type TofIntensityVecs = (Vec, Vec); pub type CentroidedVecs = (TofIntensityVecs, Vec); @@ -165,14 +90,16 @@ pub fn lazy_centroid_weighted_frame<'a>( let num_arrays = peak_refs.len(); assert!(num_arrays > 1); let arr_len = ref_arrays.len(); + if arr_len == 0 { + error!("No peaks in reference array when centroiding"); + return ((Vec::new(), Vec::new()), Vec::new()); + } let initial_tot_intensity = ref_arrays .intensity_array .iter() .map(|x| *x as u64) .sum::(); - const MIN_WEIGHT_PRESERVE: u64 = 50; - let mut touched = vec![false; tot_size]; let mut global_num_touched = 0; let mut num_added = 0; @@ -252,7 +179,7 @@ pub fn lazy_centroid_weighted_frame<'a>( local_offset_touched += local_peak_refs.len(); } - if curr_intensity > this_intensity { + if curr_weight > this_intensity { agg_intensity.push(u32::try_from(curr_intensity).expect("Expected to fit in u32")); let calc_tof = (curr_agg_tof / curr_weight) as u32; let calc_ims = (curr_agg_ims / curr_weight) as usize; @@ -305,3 +232,208 @@ pub fn lazy_centroid_weighted_frame<'a>( out } + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to create a simple tolerance range for testing + fn test_tof_tolerance(tof: u32) -> RangeInclusive { + let tolerance = 2; + (tof.saturating_sub(tolerance))..=tof.saturating_add(tolerance) + } + + fn test_ims_tolerance(ims: usize) -> RangeInclusive { + let tolerance = 1; + (ims.saturating_sub(tolerance))..=ims.saturating_add(tolerance) + } + + // Helper function to create PeakArrayRefs + fn create_peak_refs<'a>( + tof: &'a [u32], + ims: &'a [usize], + intensity: &'a [u32], + ) -> PeakArrayRefs<'a> { + PeakArrayRefs::new(tof, ims, intensity) + } + + #[test] + fn test_basic_centroid() { + // Test case with two simple peaks that should be merged + let peak_refs = vec![ + create_peak_refs( + &[100, 101, 200, 201], // tof + &[1, 1, 2, 2], // ims + &[1000, 2000, 500, 600], // intensity + ), + create_peak_refs( + &[101, 201], // tof + &[1, 2], // ims + &[900, 600], // intensity + ), + ]; + + let result = lazy_centroid_weighted_frame( + &peak_refs, + 0, // reference_index + 10, // max_peaks + test_tof_tolerance, + test_ims_tolerance, + ); + + let ((tof_array, intensity_array), ims_array) = result; + + assert_eq!( + tof_array.len(), + 2, + "Expected 2 centroids, got {:?}, {:?}, {:?}", + tof_array, + intensity_array, + ims_array + ); + assert_eq!(intensity_array.len(), 2); + assert_eq!(ims_array.len(), 2); + + // Check that the peaks were properly merged and weighted + assert!(tof_array[0] >= 100 && tof_array[0] <= 101); + assert_eq!(ims_array[0], 1); + assert!(intensity_array[0] == 3000); // Should be sum of intensities + assert!(intensity_array[1] == 1100); // Should be sum of intensities + } + + #[test] + fn test_max_peaks_limit() { + // Test that the function respects the max_peaks parameter + let peak_refs = vec![ + create_peak_refs(&[100, 200, 300], &[1, 2, 3], &[1000, 900, 800]), + create_peak_refs(&[101, 201, 301], &[1, 2, 3], &[950, 850, 750]), + ]; + + let result = lazy_centroid_weighted_frame( + &peak_refs, + 0, + 2, // max_peaks set to 2 + test_tof_tolerance, + test_ims_tolerance, + ); + + let ((tof_array, _), _) = result; + assert_eq!( + tof_array.len(), + 2, + "Should only return max_peaks number of peaks" + ); + } + + #[test] + fn test_empty_input() { + // Test handling of empty arrays + let peak_refs = vec![ + create_peak_refs(&[], &[], &[]), + create_peak_refs(&[], &[], &[]), + ]; + + let result = + lazy_centroid_weighted_frame(&peak_refs, 0, 10, test_tof_tolerance, test_ims_tolerance); + + let ((tof_array, intensity_array), ims_array) = result; + assert_eq!(tof_array.len(), 0); + assert_eq!(intensity_array.len(), 0); + assert_eq!(ims_array.len(), 0); + } + + #[test] + fn test_intensity_weighted_centroid() { + // Test that centroids are properly weighted by intensity + let peak_refs = vec![ + create_peak_refs( + &[100, 200], + &[1, 2], + &[1000, 100], // High intensity for first peak + ), + create_peak_refs( + &[102, 202], + &[1, 2], + &[100, 1000], // High intensity for second peak + ), + ]; + + let result = + lazy_centroid_weighted_frame(&peak_refs, 0, 10, test_tof_tolerance, test_ims_tolerance); + + let ((tof_array, _), ims_array) = result; + + // First centroid should be closer to 100 due to higher intensity + assert!(tof_array[0] - 100 < 102 - tof_array[0]); + assert_eq!(ims_array[0], 1); + } + + #[test] + #[should_panic] + fn test_invalid_reference_index() { + let peak_refs = vec![ + create_peak_refs(&[100], &[1], &[1000]), + create_peak_refs(&[101], &[1], &[900]), + ]; + + // This should panic due to invalid reference index + lazy_centroid_weighted_frame( + &peak_refs, + 2, // Invalid index + 10, + test_tof_tolerance, + test_ims_tolerance, + ); + } + + // Pretty decent test suggested by claude but im not sure if I really + // need to suuport it ... since scan indices are usually less than 2_000 + // #[test] + // fn test_large_input_values() { + // // Test handling of large values close to u32::MAX + // let peak_refs = vec![ + // create_peak_refs( + // &[u32::MAX - 10, u32::MAX - 5], + // &[usize::MAX - 10, usize::MAX - 5], + // &[1000, 900], + // ), + // create_peak_refs( + // &[u32::MAX - 9, u32::MAX - 4], + // &[usize::MAX - 9, usize::MAX - 4], + // &[950, 850], + // ), + // ]; + + // let result = + // lazy_centroid_weighted_frame(&peak_refs, 0, 10, test_tof_tolerance, test_ims_tolerance); + + // let ((tof_array, _), _) = result; + // assert!(!tof_array.is_empty(), "Should handle large values properly"); + // } + + #[test] + fn test_reference_frame_intensity() { + // Test that only reference frame intensities are summed + let peak_refs = vec![ + create_peak_refs( + &[100], + &[1], + &[1000], // Reference frame + ), + create_peak_refs( + &[101], + &[1], + &[5000], // Non-reference frame with higher intensity + ), + ]; + + let result = + lazy_centroid_weighted_frame(&peak_refs, 0, 10, test_tof_tolerance, test_ims_tolerance); + + let ((_, intensity_array), _) = result; + assert_eq!( + intensity_array[0], 1000, + "Should only use reference frame intensity" + ); + } +} diff --git a/src/utils/sorting.rs b/src/utils/sorting.rs index 2caa86f..d67d691 100644 --- a/src/utils/sorting.rs +++ b/src/utils/sorting.rs @@ -1,116 +1,27 @@ -use rayon::prelude::*; - -fn place_at_indices(original: &mut [T], indices: &mut [usize]) { - for i in 0..indices.len() { - while i != indices[i] { - let new_i = indices[i]; - indices.swap(i, new_i); - original.swap(i, new_i); - } - } -} - -fn place_at_indices_n(original: &mut [&mut [T]], indices: &mut [usize]) { - for i in 0..indices.len() { - while i != indices[i] { - let new_i = indices[i]; - indices.swap(i, new_i); - for slice in original.iter_mut() { - slice.swap(i, new_i); - } - } - } -} - -pub fn argsort_by(v: &[T], key: F) -> Vec -where - F: Fn(&T) -> K, - K: Ord, -{ - let mut indices: Vec = (0..v.len()).collect(); - // indices.sort_by_key(|&i| key(&v[i])); - indices.sort_unstable_by_key(|&i| key(&v[i])); - indices -} - -pub fn par_argsort_by(v: &[T], key: F) -> Vec -where - F: Fn(&T) -> K + Sync + Send, - K: Ord + Sync + Send, - T: Sync + Send, -{ - let mut indices: Vec = (0..v.len()).collect(); - // indices.sort_by_key(|&i| key(&v[i])); - indices.par_sort_unstable_by_key(|&i| key(&v[i])); - indices -} - -/// Sorts all passed slices by the key function. +/// Macro that sorts an arbitrary number of vecs by a the values +/// first one. /// -/// For a similar macro that works in slices of heterogeneous types, see `sort_by_indices_multi!`. +/// NOTE: This macro creates a new ordered vec for each one. +/// In theory its possible to have this happen in-place (see commit history) +/// but it seems ineficient for the most part when I benchmarked it. /// -/// # Example -/// ``` -/// use timsquery::utils::sorting::sort_multiple_by; -/// let mut va = vec![9, 8, 7]; -/// let mut vb = vec![1, 2, 3]; -/// sort_multiple_by(&mut [&mut va, &mut vb], |x| *x); -/// assert_eq!(va, vec![7, 8, 9]); -/// assert_eq!(vb, vec![3, 2, 1]); -/// ``` -/// -pub fn sort_multiple_by(vectors: &mut [&mut [T]], key: F) -where - F: Fn(&T) -> K, - K: Ord, -{ - let mut indices = argsort_by(vectors[0], |x| key(x)); - place_at_indices_n(vectors, &mut indices); -} - -/// Macro that sorts an arbitrary number of slices by a key function applied to the -/// first slice. +/// TODO: Make a variant that sort in parallel. +/// TODO: Add a parameter to specify ascending or descending. /// /// # Example /// ``` -/// use timsquery::sort_by_indices_multi; -/// use timsquery::utils::sorting::argsort_by; +/// use timsquery::sort_vecs_by_first; /// -/// let mut va = vec![9, 8, 7]; -/// let mut vb = vec![1, 2, 3]; -/// let mut vc = vec!['a', 'b', 'c']; -/// let mut indices = argsort_by(&va, |x| *x); -/// sort_by_indices_multi!( indices, &mut va, &mut vb, &mut vc); +/// let va = vec![9, 8, 7]; +/// let vb = vec![1, 2, 3]; +/// let vc = vec!['a', 'b', 'c']; +/// let out = sort_vecs_by_first!(&va, &vb, &vc); /// -/// assert_eq!(va, vec![7, 8, 9]); -/// assert_eq!(vb, vec![3, 2, 1]); -/// assert_eq!(vc, vec!['c', 'b', 'a']); +/// assert_eq!(out.0, vec![7, 8, 9]); +/// assert_eq!(out.1, vec![3, 2, 1]); +/// assert_eq!(out.2, vec!['c', 'b', 'a']); /// ``` /// -/// -#[macro_export] -macro_rules! sort_by_indices_multi { - ($indices:expr, $($data:expr),+ $(,)?) => {{ - let mut indices = $indices.to_vec(); - for idx in 0..indices.len() { - if indices[idx] != idx { - let mut current_idx = idx; - loop { - let target_idx = indices[current_idx]; - indices[current_idx] = current_idx; - if indices[target_idx] == target_idx { - break; - } - $( - $data.swap(current_idx, target_idx); - )+ - current_idx = target_idx; - } - } - } - }}; -} - #[macro_export] macro_rules! sort_vecs_by_first { ($first:expr $(,$rest:expr)*) => {{ @@ -135,50 +46,6 @@ macro_rules! sort_vecs_by_first { #[cfg(test)] mod tests { - use super::*; - - #[test] - fn test_place_at_indices() { - let mut va = vec![1, 2, 3, 4]; - let mut indices = vec![3, 2, 1, 0]; - place_at_indices(&mut va, &mut indices); - assert_eq!(va, vec![4, 3, 2, 1]); - } - - #[test] - fn test_place_at_indices_n() { - let mut va = vec![1, 2, 3, 4]; - let mut vb = vec![5, 6, 7, 8]; - let mut indices = vec![3, 2, 1, 0]; - place_at_indices_n(&mut [&mut va, &mut vb], &mut indices); - assert_eq!(va, vec![4, 3, 2, 1]); - assert_eq!(vb, vec![8, 7, 6, 5]); - } - - #[test] - fn test_argsort_by() { - let va = vec![1, 2, 3, 4]; - let indices = argsort_by(&va, |x| *x); - assert_eq!(indices, vec![0, 1, 2, 3]); - } - - #[test] - fn test_sort_multiple_slices() { - let mut tof_indices: Vec = vec![1, 2, 3, 4, 1, 2]; - let mut scan_numbers: Vec = vec![0, 0, 1, 1, 2, 2]; - let mut intensities: Vec = vec![10, 20, 30, 40, 50, 60]; - let mut indices = argsort_by(&tof_indices, |x| *x); - sort_by_indices_multi!( - &mut indices, - &mut tof_indices, - &mut scan_numbers, - &mut intensities - ); - assert_eq!(tof_indices, vec![1, 1, 2, 2, 3, 4]); - assert_eq!(scan_numbers, vec![0, 2, 0, 2, 1, 1]); - assert_eq!(intensities, vec![10, 50, 20, 60, 30, 40]); - } - #[test] fn test_sort_two_vecs() { let v1 = vec![3, 1, 4, 1, 5]; From 2fdbfad3fe512fb3c00c884d04279e70f88f01e5 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Mon, 28 Oct 2024 06:00:55 -0700 Subject: [PATCH 09/30] feat(centroiding): 15pct speedup in centroiding --- ..._results_230510_PRTC_13_S1-B1_1_12817.json | 122 +++++++++--------- ..._diaPASEF_Condition_A_Sample_Alpha_02.json | 50 +++---- src/models/frames/expanded_frame.rs | 2 +- src/utils/frame_processing.rs | 98 ++++++-------- 4 files changed, 127 insertions(+), 145 deletions(-) diff --git a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json index c42d54c..542d380 100644 --- a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json +++ b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json @@ -7,132 +7,132 @@ { "name": "RawFileIndex", "context": "Encoding", - "mean_duration_seconds": 0.006061387115151513, - "mean_duration_human_readable": "6.061387ms", - "setup_time_seconds": 1.75e-6, - "setup_time_human_readable": "1.75µs", + "mean_duration_seconds": 0.006079765945454544, + "mean_duration_human_readable": "6.079765ms", + "setup_time_seconds": 1.791e-6, + "setup_time_human_readable": "1.791µs", "note": null }, { "name": "ExpandedRawFileIndexCentroided", "context": "Encoding", - "mean_duration_seconds": 102.017141791, - "mean_duration_human_readable": "102.017141791s", - "setup_time_seconds": 9.58e-7, - "setup_time_human_readable": "958ns", + "mean_duration_seconds": 94.757292084, + "mean_duration_human_readable": "94.757292084s", + "setup_time_seconds": 1e-6, + "setup_time_human_readable": "1µs", "note": null }, { "name": "ExpandedRawFileIndex", "context": "Encoding", - "mean_duration_seconds": 7.339009041, - "mean_duration_human_readable": "7.339009041s", - "setup_time_seconds": 1.042e-6, - "setup_time_human_readable": "1.042µs", + "mean_duration_seconds": 7.356188333, + "mean_duration_human_readable": "7.356188333s", + "setup_time_seconds": 1.167e-6, + "setup_time_human_readable": "1.167µs", "note": null }, { "name": "TransposedQuadIndex", "context": "Encoding", - "mean_duration_seconds": 64.579125625, - "mean_duration_human_readable": "64.579125625s", - "setup_time_seconds": 1.167e-6, - "setup_time_human_readable": "1.167µs", + "mean_duration_seconds": 62.624879917, + "mean_duration_human_readable": "62.624879917s", + "setup_time_seconds": 4e-6, + "setup_time_human_readable": "4µs", "note": null }, { "name": "TransposedQuadIndexCentroided", "context": "Encoding", - "mean_duration_seconds": 105.091189208, - "mean_duration_human_readable": "105.091189208s", - "setup_time_seconds": 1.25e-6, - "setup_time_human_readable": "1.25µs", + "mean_duration_seconds": 96.316570291, + "mean_duration_human_readable": "96.316570291s", + "setup_time_seconds": 7.08e-7, + "setup_time_human_readable": "708ns", "note": null }, { "name": "RawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 28.889043334, - "mean_duration_human_readable": "28.889043334s", - "setup_time_seconds": 0.077295959, - "setup_time_human_readable": "77.295959ms", + "mean_duration_seconds": 29.691199875, + "mean_duration_human_readable": "29.691199875s", + "setup_time_seconds": 0.079093084, + "setup_time_human_readable": "79.093084ms", "note": "RawFileIndex::query_multi_group aggregated 71227 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0023722945118483407, - "mean_duration_human_readable": "2.372294ms", - "setup_time_seconds": 5.8142151250000005, - "setup_time_human_readable": "5.814215125s", + "mean_duration_seconds": 0.0016401144606557367, + "mean_duration_human_readable": "1.640114ms", + "setup_time_seconds": 5.64560575, + "setup_time_human_readable": "5.64560575s", "note": "ExpandedRawFileIndex::query_multi_group aggregated 72731 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.363698, - "mean_duration_human_readable": "363.698ms", - "setup_time_seconds": 5.680389416, - "setup_time_human_readable": "5.680389416s", + "mean_duration_seconds": 0.5222450000000001, + "mean_duration_human_readable": "522.245ms", + "setup_time_seconds": 5.767646542, + "setup_time_human_readable": "5.767646542s", "note": "ExpandedRawFileIndex::query_multi_group aggregated 11194054 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0011996081642685855, - "mean_duration_human_readable": "1.199608ms", - "setup_time_seconds": 98.870238958, - "setup_time_human_readable": "98.870238958s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 21990 " + "mean_duration_seconds": 0.0010444809603340293, + "mean_duration_human_readable": "1.04448ms", + "setup_time_seconds": 89.688057292, + "setup_time_human_readable": "89.688057292s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 28517 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.07626683057142856, - "mean_duration_human_readable": "76.26683ms", - "setup_time_seconds": 114.076896875, - "setup_time_human_readable": "114.076896875s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5012679 " + "mean_duration_seconds": 0.112750301, + "mean_duration_human_readable": "112.750301ms", + "setup_time_seconds": 90.299234708, + "setup_time_human_readable": "90.299234708s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5973230 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.005130067953846155, - "mean_duration_human_readable": "5.130067ms", - "setup_time_seconds": 72.976041375, - "setup_time_human_readable": "72.976041375s", + "mean_duration_seconds": 0.009101093563636368, + "mean_duration_human_readable": "9.101093ms", + "setup_time_seconds": 70.55164475, + "setup_time_human_readable": "70.55164475s", "note": "TransposedQuadIndex::query_multi_group aggregated 69045 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.00552524769060773, - "mean_duration_human_readable": "5.525247ms", - "setup_time_seconds": 87.057384125, - "setup_time_human_readable": "87.057384125s", + "mean_duration_seconds": 0.00893748735714286, + "mean_duration_human_readable": "8.937487ms", + "setup_time_seconds": 64.168985583, + "setup_time_human_readable": "64.168985583s", "note": "TransposedQuadIndex::query_multi_group aggregated 9971215 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.001565241712050078, - "mean_duration_human_readable": "1.565241ms", - "setup_time_seconds": 118.473094709, - "setup_time_human_readable": "118.473094709s", - "note": "TransposedQuadIndex::query_multi_group aggregated 17773 " + "mean_duration_seconds": 0.003934095082031249, + "mean_duration_human_readable": "3.934095ms", + "setup_time_seconds": 99.876973791, + "setup_time_human_readable": "99.876973791s", + "note": "TransposedQuadIndex::query_multi_group aggregated 23806 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.003929947243137255, - "mean_duration_human_readable": "3.929947ms", - "setup_time_seconds": 124.951945458, - "setup_time_human_readable": "124.951945458s", - "note": "TransposedQuadIndex::query_multi_group aggregated 4425627 " + "mean_duration_seconds": 0.0021190172139830504, + "mean_duration_human_readable": "2.119017ms", + "setup_time_seconds": 107.17344675, + "setup_time_human_readable": "107.17344675s", + "note": "TransposedQuadIndex::query_multi_group aggregated 5282090 " } ], "metadata": { "basename": "230510_PRTC_13_S1-B1_1_12817" }, - "full_benchmark_time_seconds": 961.0857955 + "full_benchmark_time_seconds": 847.892203041 } \ No newline at end of file diff --git a/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json index 14ff6fe..edbebb6 100644 --- a/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json +++ b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json @@ -7,51 +7,51 @@ { "name": "RawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 3.778817917, - "mean_duration_human_readable": "3.778817917s", - "setup_time_seconds": 0.241755042, - "setup_time_human_readable": "241.755042ms", + "mean_duration_seconds": 3.923214042, + "mean_duration_human_readable": "3.923214042s", + "setup_time_seconds": 0.236783083, + "setup_time_human_readable": "236.783083ms", "note": "RawFileIndex::query_multi_group aggregated 9144 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0005595513605823068, - "mean_duration_human_readable": "559.551µs", - "setup_time_seconds": 619.45208575, - "setup_time_human_readable": "619.45208575s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 873 " + "mean_duration_seconds": 0.0009338467012138191, + "mean_duration_human_readable": "933.846µs", + "setup_time_seconds": 470.779360458, + "setup_time_human_readable": "470.779360458s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 1505 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 7.19476525, - "mean_duration_human_readable": "7.19476525s", - "setup_time_seconds": 636.328349375, - "setup_time_human_readable": "636.328349375s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 193555055 " + "mean_duration_seconds": 16.197693792, + "mean_duration_human_readable": "16.197693792s", + "setup_time_seconds": 487.496389916, + "setup_time_human_readable": "487.496389916s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 213765098 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0017960869371633765, - "mean_duration_human_readable": "1.796086ms", - "setup_time_seconds": 665.489050917, - "setup_time_human_readable": "665.489050917s", - "note": "TransposedQuadIndex::query_multi_group aggregated 715 " + "mean_duration_seconds": 0.003893406926070039, + "mean_duration_human_readable": "3.893406ms", + "setup_time_seconds": 530.744600625, + "setup_time_human_readable": "530.744600625s", + "note": "TransposedQuadIndex::query_multi_group aggregated 1347 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.0027502779313186817, - "mean_duration_human_readable": "2.750277ms", - "setup_time_seconds": 663.333964959, - "setup_time_human_readable": "663.333964959s", - "note": "TransposedQuadIndex::query_multi_group aggregated 167889419 " + "mean_duration_seconds": 0.004904444668292681, + "mean_duration_human_readable": "4.904444ms", + "setup_time_seconds": 542.031527458, + "setup_time_human_readable": "542.031527458s", + "note": "TransposedQuadIndex::query_multi_group aggregated 185201566 " } ], "metadata": { "basename": "LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02" }, - "full_benchmark_time_seconds": 2621.0749125 + "full_benchmark_time_seconds": 2091.340999875 } \ No newline at end of file diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 1cfe84d..14a1571 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -348,7 +348,7 @@ impl FrameProcessingConfig { mz_tol_ppm: 15.0, window_width: 3, max_ms1_peaks: 100_000, - max_ms2_peaks: 10_000, + max_ms2_peaks: 20_000, ims_converter: Default::default(), mz_converter: Default::default(), } diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 302af05..82c5ce0 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -15,7 +15,11 @@ fn sort_n_check( sort_vecs_by_first!(agg_tof, agg_ims, agg_intensity); if let Some(x) = intensity_array.last() { - assert!(*x > 0); + assert!( + *x > 0, + "Expected all intensities to be positive non-zero, got {:?}", + intensity_array + ); let max_tof = tof_array.iter().max().expect("At least one element"); assert!(*max_tof < (u32::MAX - 1)); } @@ -105,45 +109,48 @@ pub fn lazy_centroid_weighted_frame<'a>( let mut num_added = 0; // We will be iterating in decreasing order of intensity - let mut order: Vec = (0..tot_size).collect(); - order.sort_unstable_by(|&a, &b| { - let (a_major, a_minor) = index_split(a, &slice_sizes); - let (b_major, b_minor) = index_split(b, &slice_sizes); - - let a_intensity = peak_refs[a_major].intensity_array[a_minor]; - let b_intensity = peak_refs[b_major].intensity_array[b_minor]; - - b_intensity - .partial_cmp(&a_intensity) - .unwrap_or(Ordering::Equal) - }); - - assert!({ - let (first_major, first_minor) = index_split(order[0], &slice_sizes); - let (last_major, last_minor) = index_split(order[tot_size - 1], &slice_sizes); + // Pre-calculate indices and intensities for sorting + struct OrderItem { + global_idx: usize, + major_idx: usize, + minor_idx: usize, + intensity: u32, + } - let first_intensity = peak_refs[first_major].intensity_array[first_minor]; - let last_intensity = peak_refs[last_major].intensity_array[last_minor]; + let mut order: Vec = Vec::with_capacity(tot_size); + let mut offset = 0; + for (major_idx, peak_ref) in peak_refs.iter().enumerate() { + for (minor_idx, &intensity) in peak_ref.intensity_array.iter().enumerate() { + order.push(OrderItem { + global_idx: offset + minor_idx, + major_idx, + minor_idx, + intensity, + }); + } + offset += peak_ref.len(); + } - first_intensity >= last_intensity - }); + // Sort by intensity + order.sort_unstable_by(|a, b| b.intensity.cmp(&a.intensity)); + assert!(order[0].intensity > order[tot_size - 1].intensity); - // TODO explore if making vecs with capacity and appedning is better... let capacity = max_peaks.min(arr_len); let mut agg_tof = Vec::with_capacity(capacity); let mut agg_intensity = Vec::with_capacity(capacity); let mut agg_ims = Vec::with_capacity(capacity); - for idx in order { - let (major_idx, minor_idx) = index_split(idx, &slice_sizes); + for item in order { + let major_idx = item.major_idx; + let minor_idx = item.minor_idx; + let this_intensity = item.intensity as u64; - if touched[idx] { + if touched[item.global_idx] { continue; } let tof = peak_refs[major_idx].tof_array[minor_idx]; let ims = peak_refs[major_idx].ims_array[minor_idx]; - let this_intensity = peak_refs[major_idx].intensity_array[minor_idx] as u64; let tof_range = tof_tol_range_fn(tof); let ims_range = ims_tol_range_fn(ims); @@ -153,8 +160,7 @@ pub fn lazy_centroid_weighted_frame<'a>( let mut curr_agg_ims = 0u64; let mut local_offset_touched = 0; - for ii in 0..num_arrays { - let local_peak_refs = &peak_refs[ii]; + for (ii, local_peak_refs) in peak_refs.iter().enumerate() { let ss_start = local_peak_refs .tof_array .partition_point(|x| x < tof_range.start()); @@ -179,12 +185,13 @@ pub fn lazy_centroid_weighted_frame<'a>( local_offset_touched += local_peak_refs.len(); } - if curr_weight > this_intensity { + // This means that at least 2 peaks need to be aggregated. + if curr_weight > this_intensity && curr_intensity >= this_intensity { agg_intensity.push(u32::try_from(curr_intensity).expect("Expected to fit in u32")); let calc_tof = (curr_agg_tof / curr_weight) as u32; let calc_ims = (curr_agg_ims / curr_weight) as usize; - assert!(tof_range.contains(&calc_tof)); - assert!(ims_range.contains(&calc_ims)); + debug_assert!(tof_range.contains(&calc_tof)); + debug_assert!(ims_range.contains(&calc_ims)); agg_tof.push(calc_tof); agg_ims.push(calc_ims); num_added += 1; @@ -198,8 +205,9 @@ pub fn lazy_centroid_weighted_frame<'a>( } } - // Drop the zeros and sort by mz (tof) let out = sort_n_check(agg_intensity, agg_tof, agg_ims); + + // TODO:Make everything below this a separate function and accumulate it. let tot_final_intensity = out.0 .1.iter().map(|x| *x as u64).sum::(); let inten_ratio = tot_final_intensity as f64 / initial_tot_intensity as f64; assert!(initial_tot_intensity >= tot_final_intensity); @@ -410,30 +418,4 @@ mod tests { // let ((tof_array, _), _) = result; // assert!(!tof_array.is_empty(), "Should handle large values properly"); // } - - #[test] - fn test_reference_frame_intensity() { - // Test that only reference frame intensities are summed - let peak_refs = vec![ - create_peak_refs( - &[100], - &[1], - &[1000], // Reference frame - ), - create_peak_refs( - &[101], - &[1], - &[5000], // Non-reference frame with higher intensity - ), - ]; - - let result = - lazy_centroid_weighted_frame(&peak_refs, 0, 10, test_tof_tolerance, test_ims_tolerance); - - let ((_, intensity_array), _) = result; - assert_eq!( - intensity_array[0], 1000, - "Should only use reference frame intensity" - ); - } } From 13aeff169de816c8c831d7f077f54414c4586853 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Tue, 29 Oct 2024 15:39:16 -0700 Subject: [PATCH 10/30] ... a lot .... --- Taskfile.yml | 4 + benches/benchmark_indices.rs | 5 - ..._results_230510_PRTC_13_S1-B1_1_12817.json | 126 +++++----- src/lib.rs | 5 +- src/models/adapters.rs | 1 + .../aggregators/raw_peak_agg/point_agg.rs | 19 +- src/models/elution_group.rs | 13 +- src/models/frames/expanded_frame.rs | 216 +++++++++++++++++- src/models/frames/single_quad_settings.rs | 69 +++--- .../indices/expanded_raw_index/model.rs | 204 +++++++---------- src/models/indices/raw_file_index.rs | 24 +- .../transposed_quad_index/quad_index.rs | 63 ++--- .../quad_splitted_transposed_index.rs | 128 +---------- src/models/queries.rs | 1 + .../queriable_tims_data.rs | 144 ++++-------- src/traits/indexed_data.rs | 19 -- src/traits/mod.rs | 2 +- src/traits/queriable_data.rs | 53 +++++ src/traits/tolerance.rs | 13 +- src/utils/frame_processing.rs | 24 -- src/utils/sorting.rs | 57 +++++ 21 files changed, 651 insertions(+), 539 deletions(-) delete mode 100644 src/traits/indexed_data.rs create mode 100644 src/traits/queriable_data.rs diff --git a/Taskfile.yml b/Taskfile.yml index bc24eec..d08501b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,6 +23,10 @@ tasks: cmds: - cargo build $BIN_EXTRAS + license_check: + cmds: + - cargo deny check + test: cmds: - cargo test diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index d63d531..3afc9e5 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -353,7 +353,6 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve || RawFileIndex::from_path(raw_file_path).unwrap(), |index, _i| { let tmp = query_multi_group( - index, index, &tolerance, &query_groups, @@ -375,7 +374,6 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve || ExpandedRawFrameIndex::from_path(raw_file_path).unwrap(), |index, _i| { let tmp = query_multi_group( - index, index, &tolerance, &query_groups, @@ -400,7 +398,6 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve || ExpandedRawFrameIndex::from_path_centroided(raw_file_path).unwrap(), |index, _i| { let tmp = query_multi_group( - index, index, &tolerance, &query_groups, @@ -425,7 +422,6 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve || QuadSplittedTransposedIndex::from_path(raw_file_path).unwrap(), |index, _i| { let tmp = query_multi_group( - index, index, &tolerance, &query_groups, @@ -447,7 +443,6 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve || QuadSplittedTransposedIndex::from_path_centroided(raw_file_path).unwrap(), |index, _i| { let tmp = query_multi_group( - index, index, &tolerance, &query_groups, diff --git a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json index 542d380..1b5d19d 100644 --- a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json +++ b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json @@ -7,132 +7,132 @@ { "name": "RawFileIndex", "context": "Encoding", - "mean_duration_seconds": 0.006079765945454544, - "mean_duration_human_readable": "6.079765ms", - "setup_time_seconds": 1.791e-6, - "setup_time_human_readable": "1.791µs", + "mean_duration_seconds": 0.006222996397515529, + "mean_duration_human_readable": "6.222996ms", + "setup_time_seconds": 4.833e-6, + "setup_time_human_readable": "4.833µs", "note": null }, { "name": "ExpandedRawFileIndexCentroided", "context": "Encoding", - "mean_duration_seconds": 94.757292084, - "mean_duration_human_readable": "94.757292084s", - "setup_time_seconds": 1e-6, - "setup_time_human_readable": "1µs", + "mean_duration_seconds": 97.626668625, + "mean_duration_human_readable": "97.626668625s", + "setup_time_seconds": 9.17e-7, + "setup_time_human_readable": "917ns", "note": null }, { "name": "ExpandedRawFileIndex", "context": "Encoding", - "mean_duration_seconds": 7.356188333, - "mean_duration_human_readable": "7.356188333s", - "setup_time_seconds": 1.167e-6, - "setup_time_human_readable": "1.167µs", + "mean_duration_seconds": 8.102684125, + "mean_duration_human_readable": "8.102684125s", + "setup_time_seconds": 1.042e-6, + "setup_time_human_readable": "1.042µs", "note": null }, { "name": "TransposedQuadIndex", "context": "Encoding", - "mean_duration_seconds": 62.624879917, - "mean_duration_human_readable": "62.624879917s", - "setup_time_seconds": 4e-6, - "setup_time_human_readable": "4µs", + "mean_duration_seconds": 74.288219334, + "mean_duration_human_readable": "74.288219334s", + "setup_time_seconds": 1e-6, + "setup_time_human_readable": "1µs", "note": null }, { "name": "TransposedQuadIndexCentroided", "context": "Encoding", - "mean_duration_seconds": 96.316570291, - "mean_duration_human_readable": "96.316570291s", - "setup_time_seconds": 7.08e-7, - "setup_time_human_readable": "708ns", + "mean_duration_seconds": 103.221435875, + "mean_duration_human_readable": "103.221435875s", + "setup_time_seconds": 9.58e-7, + "setup_time_human_readable": "958ns", "note": null }, { "name": "RawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 29.691199875, - "mean_duration_human_readable": "29.691199875s", - "setup_time_seconds": 0.079093084, - "setup_time_human_readable": "79.093084ms", + "mean_duration_seconds": 30.1034345, + "mean_duration_human_readable": "30.1034345s", + "setup_time_seconds": 0.080088542, + "setup_time_human_readable": "80.088542ms", "note": "RawFileIndex::query_multi_group aggregated 71227 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0016401144606557367, - "mean_duration_human_readable": "1.640114ms", - "setup_time_seconds": 5.64560575, - "setup_time_human_readable": "5.64560575s", - "note": "ExpandedRawFileIndex::query_multi_group aggregated 72731 " + "mean_duration_seconds": 0.0043137600000000016, + "mean_duration_human_readable": "4.31376ms", + "setup_time_seconds": 5.777069208, + "setup_time_human_readable": "5.777069208s", + "note": "ExpandedRawFileIndex::query_multi_group aggregated 68449 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.5222450000000001, - "mean_duration_human_readable": "522.245ms", - "setup_time_seconds": 5.767646542, - "setup_time_human_readable": "5.767646542s", - "note": "ExpandedRawFileIndex::query_multi_group aggregated 11194054 " + "mean_duration_seconds": 1.389402875, + "mean_duration_human_readable": "1.389402875s", + "setup_time_seconds": 5.838487875, + "setup_time_human_readable": "5.838487875s", + "note": "ExpandedRawFileIndex::query_multi_group aggregated 10744180 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0010444809603340293, - "mean_duration_human_readable": "1.04448ms", - "setup_time_seconds": 89.688057292, - "setup_time_human_readable": "89.688057292s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 28517 " + "mean_duration_seconds": 0.003144791757009347, + "mean_duration_human_readable": "3.144791ms", + "setup_time_seconds": 89.554111875, + "setup_time_human_readable": "89.554111875s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 26179 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.112750301, - "mean_duration_human_readable": "112.750301ms", - "setup_time_seconds": 90.299234708, - "setup_time_human_readable": "90.299234708s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5973230 " + "mean_duration_seconds": 0.2185190752, + "mean_duration_human_readable": "218.519075ms", + "setup_time_seconds": 88.279319125, + "setup_time_human_readable": "88.279319125s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5775312 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.009101093563636368, - "mean_duration_human_readable": "9.101093ms", - "setup_time_seconds": 70.55164475, - "setup_time_human_readable": "70.55164475s", - "note": "TransposedQuadIndex::query_multi_group aggregated 69045 " + "mean_duration_seconds": 0.003314156013245034, + "mean_duration_human_readable": "3.314156ms", + "setup_time_seconds": 62.383771084, + "setup_time_human_readable": "62.383771084s", + "note": "TransposedQuadIndex::query_multi_group aggregated 64676 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.00893748735714286, - "mean_duration_human_readable": "8.937487ms", - "setup_time_seconds": 64.168985583, - "setup_time_human_readable": "64.168985583s", + "mean_duration_seconds": 0.003775450037735844, + "mean_duration_human_readable": "3.77545ms", + "setup_time_seconds": 59.13618275, + "setup_time_human_readable": "59.13618275s", "note": "TransposedQuadIndex::query_multi_group aggregated 9971215 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.003934095082031249, - "mean_duration_human_readable": "3.934095ms", - "setup_time_seconds": 99.876973791, - "setup_time_human_readable": "99.876973791s", - "note": "TransposedQuadIndex::query_multi_group aggregated 23806 " + "mean_duration_seconds": 0.001552562270769231, + "mean_duration_human_readable": "1.552562ms", + "setup_time_seconds": 98.014544042, + "setup_time_human_readable": "98.014544042s", + "note": "TransposedQuadIndex::query_multi_group aggregated 22454 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.0021190172139830504, - "mean_duration_human_readable": "2.119017ms", - "setup_time_seconds": 107.17344675, - "setup_time_human_readable": "107.17344675s", + "mean_duration_seconds": 0.001730233717993079, + "mean_duration_human_readable": "1.730233ms", + "setup_time_seconds": 98.609288084, + "setup_time_human_readable": "98.609288084s", "note": "TransposedQuadIndex::query_multi_group aggregated 5282090 " } ], "metadata": { "basename": "230510_PRTC_13_S1-B1_1_12817" }, - "full_benchmark_time_seconds": 847.892203041 + "full_benchmark_time_seconds": 843.833114208 } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7624462..2ef3d2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,11 @@ // Re-export main structures pub use crate::models::elution_group::ElutionGroup; pub use crate::models::indices::transposed_quad_index::QuadSplittedTransposedIndex; -pub use crate::queriable_tims_data::queriable_tims_data::QueriableTimsData; // Re-export traits pub use crate::traits::aggregator::Aggregator; -pub use crate::traits::indexed_data::QueriableData; -pub use crate::traits::tolerance::{HasIntegerID, Tolerance, ToleranceAdapter}; +pub use crate::traits::queriable_data::QueriableData; +pub use crate::traits::tolerance::{Tolerance, ToleranceAdapter}; // Declare modules pub mod models; diff --git a/src/models/adapters.rs b/src/models/adapters.rs index 61df79f..a4e8cce 100644 --- a/src/models/adapters.rs +++ b/src/models/adapters.rs @@ -62,6 +62,7 @@ impl let precursor_query = PrecursorIndexQuery { frame_index_range, + rt_range_seconds: rt_range, mz_index_range, mobility_index_range, isolation_mz_range: quad_range, diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index 9309232..1e1567d 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -3,23 +3,28 @@ use crate::traits::aggregator::Aggregator; use serde::Serialize; #[derive(Debug, Clone, Copy)] -pub struct RawPeakIntensityAggregator { +pub struct RawPeakIntensityAggregator { pub id: u64, pub intensity: u64, + _phantom: std::marker::PhantomData, } -impl RawPeakIntensityAggregator { +impl RawPeakIntensityAggregator { pub fn new(id: u64) -> Self { - Self { id, intensity: 0 } + Self { + id, + intensity: 0, + _phantom: std::marker::PhantomData, + } } } -impl Aggregator for RawPeakIntensityAggregator { - type Item = RawPeak; +impl Aggregator for RawPeakIntensityAggregator { + type Item = (RawPeak, T); type Output = u64; - fn add(&mut self, peak: &RawPeak) { - self.intensity += peak.intensity as u64; + fn add(&mut self, peak: &(RawPeak, T)) { + self.intensity += peak.0.intensity as u64; } fn finalize(self) -> u64 { diff --git a/src/models/elution_group.rs b/src/models/elution_group.rs index 0fec0f9..fd7d2be 100644 --- a/src/models/elution_group.rs +++ b/src/models/elution_group.rs @@ -1,8 +1,13 @@ -use crate::HasIntegerID; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::hash::Hash; +/// A struct that represents an elution group. +/// +/// The elution group is a single precursor ion that is framented. +/// The fragments m/z values are stored in a hashmap where the key is +/// the generic type `T` and the value is the fragment m/z. +/// #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ElutionGroup { pub id: u64, @@ -12,9 +17,3 @@ pub struct ElutionGroup { pub precursor_charge: u8, pub fragment_mzs: HashMap, } - -impl HasIntegerID for ElutionGroup { - fn get_id(&self) -> u64 { - self.id - } -} diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 14a1571..c9b0ceb 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -5,7 +5,6 @@ use rayon::prelude::*; use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; -use std::time::Instant; use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; @@ -13,6 +12,7 @@ use super::peak_in_quad::PeakInQuad; use crate::sort_vecs_by_first; use crate::utils::compress_explode::explode_vec; use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; +use crate::utils::sorting::top_n; use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; use timsrust::{ readers::{FrameReader, FrameReaderError, MetadataReaderError}, @@ -452,6 +452,14 @@ pub fn par_read_and_expand_frames( all_expanded_frames.extend(expanded_frames); } + let slice_infos: Vec = all_expanded_frames + .values() + .map(|x| ExpandedQuadSliceInfo::new(x)) + .collect(); + + println!("Slice info: {:?}", slice_infos); + panic!(); + info!("Processing MS1 frames"); let ms1_iter = frame_reader.parallel_filter(|x| x.ms_level == MSLevel::MS1); let ms1_iter = warn_and_skip_badframes(ms1_iter); @@ -481,6 +489,212 @@ pub fn par_read_and_expand_frames( Ok(all_expanded_frames) } +#[derive(Debug, Clone, Copy)] +pub struct ExpandedQuadSliceInfo { + pub quad_settings: Option, + pub cycle_time_seconds: f64, + pub peak_width_seconds: Option, +} + +impl ExpandedQuadSliceInfo { + #[instrument(skip(frameslices), ret)] + pub fn new(frameslices: &[ExpandedFrameSlice]) -> Self { + let avg_cycle_time = frameslices + .windows(2) + .map(|x| { + let a = &x[0]; + let b = &x[1]; + let diff = b.rt - a.rt; + assert!(diff > 0.0); + diff + }) + .sum::() + / frameslices.len() as f64; + + let peak_width = Self::estimate_peak_width(frameslices); + + Self { + quad_settings: frameslices[0].quadrupole_settings, + cycle_time_seconds: avg_cycle_time, + peak_width_seconds: peak_width, + } + } + + /// Estimate the peak width of elutions within the quad. + /// + /// This is a pretty dumb algorithm ... basically... + /// 1. picks 100 equally spaced points between the 20% and 80% of the frames. + /// 2. At each of those points finds the top 10 peaks. + /// 3. For each of them iteratively goes forward and backwards in time until + /// the intensity of the peak drops under 1% of its intensity. + /// 4. The time difference between the forward and backward point is the peak width. + /// 5. Finds the median of the peak widths. + /// + /// + fn estimate_peak_width(frameslices: &[ExpandedFrameSlice]) -> Option { + let start_idx = frameslices.len() / 5; + let end_idx = frameslices.len() * 4 / 5; + let step = (end_idx - start_idx) / 100; + + if step == 0 { + warn!("Estimating peak width failed, step is 0"); + return None; + } + + #[derive(Debug, Clone)] + struct Peak { + tof: u32, + patience: u32, + max_rt: f64, + min_rt: f64, + intensity: u32, + local_frame_index: usize, + fwdone: bool, + bwdone: bool, + any_update: bool, + } + + let mut peaks: Vec = Vec::with_capacity(500); + + for i in 0..49 { + let local_idx = start_idx + i * step; + + let local_frame = &frameslices[local_idx]; + let local_rt = local_frame.rt; + let (top_intens, top_indices) = top_n(&local_frame.intensities, 10); + let tofs: Vec<_> = top_indices + .iter() + .zip(top_intens.iter()) + .filter_map(|(tof, inten)| if *inten > 100u32 { Some(tof) } else { None }) + .map(|x| local_frame.tof_indices[*x]) + .collect(); + let tofs: Vec = tofs + .as_slice() + .windows(2) + .filter_map(|x| { + let a = x[0]; + let b = x[1]; + if a.abs_diff(b) > 5 { + Some(a) + } else { + None + } + }) + .collect(); + + let mut local_peaks: Vec = tofs + .iter() + .map(|tof| { + let mut local_inten = 0u32; + local_frame.query_peaks((tof - 1, tof + 1), None, &mut |x| { + local_inten += x.intensity + }); + assert!(local_inten > 0); + Peak { + tof: *tof, + patience: 1, + max_rt: local_rt, + min_rt: local_rt, + intensity: local_inten, + local_frame_index: local_idx, + fwdone: false, + bwdone: false, + any_update: false, + } + }) + .collect(); + + // Forward lookup + for lookup_frame in frameslices.iter().skip(local_idx + 1) { + let local_rt = lookup_frame.rt; + let mut any = false; + for peak in local_peaks.iter_mut() { + if peak.fwdone { + continue; + } + let mut local_int = 0u32; + lookup_frame.query_peaks((peak.tof - 1, peak.tof + 1), None, &mut |x| { + local_int += x.intensity + }); + if local_int > (peak.intensity / 100) { + peak.max_rt = local_rt; + peak.patience = 1; + peak.any_update = true; + } else if peak.patience > 0 { + peak.patience -= 1; + } else { + peak.fwdone = true; + } + + any = true; + } + if !any { + break; + } + } + + // reset patience + for peak in local_peaks.iter_mut() { + peak.patience = 1; + } + + // Backward lookup + let mut j = local_idx - 1; + while j > 1 { + // for j in (0..local_idx).rev() { + let lookup_frame = &frameslices[j]; + let local_rt = lookup_frame.rt; + let mut any = false; + for peak in local_peaks.iter_mut() { + if peak.bwdone { + continue; + } + let mut local_int = 0; + lookup_frame.query_peaks((peak.tof - 1, peak.tof + 1), None, &mut |x| { + local_int += x.intensity + }); + + if local_int > (peak.intensity / 100) { + peak.min_rt = local_rt; + peak.patience = 1; + peak.any_update = true; + } else if peak.patience > 0 { + peak.patience -= 1; + } else { + peak.bwdone = true; + } + + any = true; + } + if !any { + break; + } + j -= 1; + } + + for peak in local_peaks.into_iter() { + println!("peak: {:?}", peak); + if peak.fwdone && peak.bwdone && peak.any_update { + peaks.push(peak); + } + } + } + + if peaks.is_empty() { + return None; + } + + // get median + peaks.sort_by(|x, y| { + (x.max_rt - x.min_rt) + .partial_cmp(&(y.max_rt - y.min_rt)) + .unwrap() + }); + let median = peaks[peaks.len() / 2].max_rt - peaks[peaks.len() / 2].min_rt; + Some(median) + } +} + #[instrument(skip(frames))] pub fn par_expand_and_centroid_frames( frames: impl ParallelIterator, diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index 4747e3d..2225801 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -134,34 +134,47 @@ pub fn get_matching_quad_settings( flat_quad_settings .iter() .filter(move |qs| { - (qs.ranges.isolation_low <= precursor_mz_range.1) - && (qs.ranges.isolation_high >= precursor_mz_range.0) - }) - .filter(move |qs| { - match scan_range { - Some((min_scan, max_scan)) => { - assert!(qs.ranges.scan_start <= qs.ranges.scan_end); - assert!(min_scan <= max_scan); - - // Above quad - // Quad [----------] - // Query [------] - let above_quad = qs.ranges.scan_end < min_scan; - - // Below quad - // Quad [------] - // Query [------] - let below_quad = qs.ranges.scan_start > max_scan; - - if above_quad || below_quad { - // This quad is completely outside the scan range - false - } else { - true - } - } - None => true, - } + matches_iso_window(qs, precursor_mz_range) && matches_scan_range(qs, scan_range) }) .map(|e| e.index) } + +fn matches_iso_window(qs: &SingleQuadrupoleSetting, precursor_mz_range: (f64, f64)) -> bool { + (qs.ranges.isolation_low <= precursor_mz_range.1) + && (qs.ranges.isolation_high >= precursor_mz_range.0) +} + +fn matches_scan_range(qs: &SingleQuadrupoleSetting, scan_range: Option<(usize, usize)>) -> bool { + match scan_range { + Some((min_scan, max_scan)) => { + assert!(qs.ranges.scan_start <= qs.ranges.scan_end); + assert!(min_scan <= max_scan); + + // Above quad + // Quad [----------] + // Query [------] + let above_quad = qs.ranges.scan_end < min_scan; + + // Below quad + // Quad [------] + // Query [------] + let below_quad = qs.ranges.scan_start > max_scan; + + if above_quad || below_quad { + // This quad is completely outside the scan range + false + } else { + true + } + } + None => true, + } +} + +pub fn matches_quad_settings( + a: &SingleQuadrupoleSetting, + precursor_mz_range: (f64, f64), + scan_range: Option<(usize, usize)>, +) -> bool { + matches_iso_window(a, precursor_mz_range) && matches_scan_range(a, scan_range) +} diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index 921c057..a00113e 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -7,10 +7,11 @@ use crate::models::frames::expanded_frame::{ use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::{ - get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, + get_matching_quad_settings, matches_quad_settings, SingleQuadrupoleSetting, + SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; -use crate::traits::indexed_data::QueriableData; +use crate::traits::queriable_data::QueriableData; use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; @@ -240,140 +241,99 @@ impl fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], ) { - fragment_queries - .par_iter() - .zip(aggregator.par_iter_mut()) - .for_each(|(fragment_query, agg)| { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.1 as f64, - ); - assert!(precursor_mz_range.0 <= precursor_mz_range.1); - assert!(precursor_mz_range.0 > 0.0); - let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = fragment_query.precursor_query.frame_index_range; - - let local_quad_vec: Vec = get_matching_quad_settings( - &self.flat_quad_settings, - precursor_mz_range, - scan_range, - ) - .collect(); - - for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_precursor_peaks( - &local_quad_vec, - tof_range, - scan_range, - frame_index_range, - &mut |peak| agg.add(&(RawPeak::from(peak), fh)), - ); - } - }); - } -} - -impl - QueriableData, RawPeak> for ExpandedRawFrameIndex -{ - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { - todo!(); - // let precursor_mz_range = ( - // fragment_query.precursor_query.isolation_mz_range.0 as f64, - // fragment_query.precursor_query.isolation_mz_range.0 as f64, - // ); - // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - // let frame_index_range = Some(FrameRTTolerance::FrameIndex( - // fragment_query.precursor_query.frame_index_range, - // )); + // fragment_queries + // .par_iter() + // .zip(aggregator.par_iter_mut()) + // .for_each(|(fragment_query, agg)| { + // let precursor_mz_range = ( + // fragment_query.precursor_query.isolation_mz_range.0 as f64, + // fragment_query.precursor_query.isolation_mz_range.1 as f64, + // ); + // assert!(precursor_mz_range.0 <= precursor_mz_range.1); + // assert!(precursor_mz_range.0 > 0.0); + // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); + // let frame_index_range = fragment_query.precursor_query.frame_index_range; - // fragment_query - // .mz_index_ranges - // .iter() - // .flat_map(|(fh, tof_range)| { - // let mut local_vec: Vec<(RawPeak, FH)> = vec![]; - // self.query_peaks( - // *tof_range, + // let local_quad_vec: Vec = get_matching_quad_settings( + // &self.flat_quad_settings, // precursor_mz_range, // scan_range, - // frame_index_range, - // &mut |x| local_vec.push((RawPeak::from(x), *fh)), - // ); + // ) + // .collect(); + + // for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { + // self.query_precursor_peaks( + // &local_quad_vec, + // tof_range, + // scan_range, + // frame_index_range, + // &mut |peak| agg.add(&(RawPeak::from(peak), fh)), + // ); + // } + // }); + let prec_mz_ranges = fragment_queries + .iter() + .map(|x| { + ( + x.precursor_query.isolation_mz_range.0 as f64, + x.precursor_query.isolation_mz_range.0 as f64, + ) + }) + .collect::>(); - // local_vec - // }) - // .collect() - } + let scan_ranges = fragment_queries + .iter() + .map(|x| Some(x.precursor_query.mobility_index_range)) + .collect::>(); - fn add_query>( - &self, - fragment_query: &FragmentGroupIndexQuery, - aggregator: &mut AG, - ) { - todo!(); - // let precursor_mz_range = ( - // fragment_query.precursor_query.isolation_mz_range.0 as f64, - // fragment_query.precursor_query.isolation_mz_range.0 as f64, - // ); - // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - // let frame_index_range = Some(FrameRTTolerance::FrameIndex( - // fragment_query.precursor_query.frame_index_range, - // )); + let frame_index_ranges = fragment_queries + .iter() + .map(|x| x.precursor_query.frame_index_range) + .collect::>(); - // fragment_query - // .mz_index_ranges - // .iter() - // .for_each(|(fh, tof_range)| { - // self.query_peaks( - // *tof_range, - // precursor_mz_range, - // scan_range, - // frame_index_range, - // &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), - // ); - // }) - } + for quad_setting in self.flat_quad_settings.iter() { + let local_index = quad_setting.index; - fn add_query_multi_group>( - &self, - fragment_queries: &[FragmentGroupIndexQuery], - aggregator: &mut [AG], - ) { - fragment_queries - .par_iter() - .zip(aggregator.par_iter_mut()) - .for_each(|(fragment_query, agg)| { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.1 as f64, - ); - assert!(precursor_mz_range.0 <= precursor_mz_range.1); - assert!(precursor_mz_range.0 > 0.0); - let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = fragment_query.precursor_query.frame_index_range; - - let local_quad_vec: Vec = get_matching_quad_settings( - &self.flat_quad_settings, - precursor_mz_range, - scan_range, - ) - .collect(); + let tqi = self + .bundled_frames + .get(&local_index) + .expect("Only existing quads should be queried."); - for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_precursor_peaks( - &local_quad_vec, - tof_range, - scan_range, - frame_index_range, - &mut |peak| agg.add(&RawPeak::from(peak)), + // for i in 0..prec_mz_ranges.len() { + // if !matches_quad_settings(quad_setting, prec_mz_ranges[i], scan_ranges[i]) { + // continue; + // } + + // for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { + // let mut local_lambda = |peak| aggregator[i].add(&(RawPeak::from(peak), *fh)); + // tqi.query_peaks( + // *tof_range, + // scan_ranges[i], + // frame_index_ranges[i], + // &mut local_lambda, + // ) + // } + // } + + aggregator.par_iter_mut().enumerate().for_each(|(i, agg)| { + if !matches_quad_settings(quad_setting, prec_mz_ranges[i], scan_ranges[i]) { + return; + } + + for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { + let mut local_lambda = |peak| agg.add(&(RawPeak::from(peak), *fh)); + tqi.query_peaks( + *tof_range, + scan_ranges[i], + frame_index_ranges[i], + &mut local_lambda, ); } }); + } } } -// ============================================================================ - impl ToleranceAdapter, ElutionGroup> for ExpandedRawFrameIndex { diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index 133e406..462ff87 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -1,10 +1,8 @@ -use std::sync::Arc; - use crate::models::frames::raw_frames::frame_elems_matching; use crate::models::frames::raw_peak::RawPeak; use crate::models::queries::{FragmentGroupIndexQuery, NaturalPrecursorQuery, PrecursorIndexQuery}; use crate::traits::aggregator::Aggregator; -use crate::traits::indexed_data::QueriableData; +use crate::traits::queriable_data::QueriableData; use crate::ElutionGroup; use crate::ToleranceAdapter; use rayon::iter::ParallelIterator; @@ -40,6 +38,7 @@ impl RawFileIndex { self.meta_converters.rt_converter.invert(query.rt_range.0) as usize, self.meta_converters.rt_converter.invert(query.rt_range.1) as usize, ), + rt_range_seconds: query.rt_range, mz_index_range: ( self.meta_converters.mz_converter.invert(query.mz_range.0) as u32, self.meta_converters.mz_converter.invert(query.mz_range.1) as u32, @@ -64,7 +63,7 @@ impl RawFileIndex { >( &'a self, fqs: &'b FragmentGroupIndexQuery, - fun: &'c mut dyn for<'r> FnMut(RawPeak, Arc), + fun: &'c mut dyn for<'r> FnMut(RawPeak, FH), ) { trace!("RawFileIndex::apply_on_query"); trace!("FragmentGroupIndexQuery: {:?}", fqs); @@ -77,7 +76,7 @@ impl RawFileIndex { let pq = &fqs.precursor_query; let iso_mz_range = pq.isolation_mz_range; for frame in frames { - for (_, tof_range) in fqs.mz_index_ranges.iter() { + for (fh, tof_range) in fqs.mz_index_ranges.iter() { let scan_range = pq.mobility_index_range; let peaks = frame_elems_matching( &frame, @@ -86,7 +85,7 @@ impl RawFileIndex { Some((iso_mz_range.0 as f64, iso_mz_range.1 as f64)), ); for peak in peaks { - fun(peak, frame.quadrupole_settings.clone()); + fun(peak, *fh); } } } @@ -139,6 +138,7 @@ impl RawFileIndex { self.meta_converters.rt_converter.invert(rt_range.0) as usize, self.meta_converters.rt_converter.invert(rt_range.1) as usize, ), + rt_range_seconds: rt_range, mz_index_range: ( self.meta_converters.mz_converter.invert(mz_range.0) as u32, self.meta_converters.mz_converter.invert(mz_range.1) as u32, @@ -185,23 +185,23 @@ impl RawFileIndex { } impl - QueriableData, RawPeak> for RawFileIndex + QueriableData, (RawPeak, FH)> for RawFileIndex { - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { let mut out = Vec::new(); - self.apply_on_query(fragment_query, &mut |peak, _| out.push(peak)); + self.apply_on_query(fragment_query, &mut |x, y| out.push((x, y))); out } - fn add_query>( + fn add_query>( &self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG, ) { - self.apply_on_query(fragment_query, &mut |peak, _| aggregator.add(&peak)); + self.apply_on_query(fragment_query, &mut |x, y| aggregator.add(&(x, y))); } - fn add_query_multi_group>( + fn add_query_multi_group>( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 492acce..b5b601d 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -74,17 +74,12 @@ impl TransposedQuadIndex { &self, tof_range: (u32, u32), scan_range: Option<(usize, usize)>, - rt_range: Option, + rt_range: Option<(f32, f32)>, ) -> impl Iterator + '_ { - // This version is not compatible with the borrow checker unless I collect the vec... - // which will do for now for prototyping. - // TODO: make this a single type and convert upstream. - let frame_index_range = self.convert_to_local_frame_range(rt_range); - self.peak_buckets .range(tof_range.0..tof_range.1) .flat_map(move |(tof_index, pb)| { - pb.query_peaks(scan_range, frame_index_range) + pb.query_peaks(scan_range, rt_range) .map(move |p| PeakInQuad::from_peak_in_bucket(p, *tof_index)) }) } @@ -199,7 +194,14 @@ impl TransposedQuadIndexBuilder { self.frame_rts.push(slice.rt); } - #[instrument(skip(self))] + #[instrument( + name = "TransposedQuadIndex::build", + skip(self), + fields( + num_frames = %self.frame_indices.len(), + quad_settings = format!("{:?}", self.quad_settings), + ) + )] pub fn build(self) -> TransposedQuadIndex { // TODO: Refactor this function, its getting pretty large. let max_tof = *self @@ -279,6 +281,15 @@ impl TransposedQuadIndexBuilder { } } + #[instrument( + name = "TransposedQuadIndex::build_inner_ref", + skip(self, peak_buckets), + fields( + num_frames = %self.frame_indices.len(), + quad_settings = format!("{:?}", self.quad_settings), + peak_buckets = %peak_buckets.len(), + ) + )] fn build_inner_ref( self, mut peak_buckets: HashMap, @@ -338,6 +349,15 @@ impl TransposedQuadIndexBuilder { peak_buckets } + #[instrument( + name = "TransposedQuadIndex::batched_build_inner", + skip(self, peak_buckets), + fields( + num_frames = %self.frame_indices.len(), + quad_settings = format!("{:?}", self.quad_settings), + peak_buckets = %peak_buckets.len(), + ) + )] fn batched_build_inner( self, mut peak_buckets: HashMap, @@ -384,23 +404,6 @@ impl TransposedQuadIndexBuilder { let mut slice_start = 0; let mut slice_start_val = tof_slice[0]; while slice_start < int_slice.len() { - // // Binary search the current value + 1 - // let slice_end = tof_slice.binary_search(&(slice_start_val + 1)); - - // // let local_slice_end = slice_end.unwrap_or_else(|x| x); - // let local_slice_end = match slice_end { - // Ok(x) => { - // // If the value is found, we need to walk back to find the first instance - // // of the value ... - // let mut local_slice_end = x; - // while tof_slice[local_slice_end - 1] > slice_start_val { - // local_slice_end -= 1; - // } - // local_slice_end - // } - // Err(x) => x, - // }; - // let mut local_slice_end = slice_start; while tof_slice[local_slice_end] == slice_start_val { local_slice_end += 1; @@ -424,12 +427,18 @@ impl TransposedQuadIndexBuilder { added_peaks += range_use.len() as u64; - assert!(slice_start_val == tof_slice[slice_start]); + assert!( + slice_start_val == tof_slice[slice_start], + "Not all elements in slice have the same tof value" + ); if local_slice_end == tof_slice.len() { break; } - assert!(slice_start_val < tof_slice[local_slice_end]); + assert!( + slice_start_val < tof_slice[local_slice_end], + "The next element after this slice should have a higher tof value" + ); slice_start = local_slice_end; slice_start_val = tof_slice[slice_start]; diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 7c96e34..8d1dc80 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -2,8 +2,8 @@ use super::quad_index::{FrameRTTolerance, TransposedQuadIndex, TransposedQuadInd use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ - expand_and_split_frame, par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, - FrameProcessingConfig, SortingStateTrait, + par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, FrameProcessingConfig, + SortingStateTrait, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; @@ -11,7 +11,7 @@ use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; -use crate::traits::indexed_data::QueriableData; +use crate::traits::queriable_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; use rayon::prelude::*; @@ -23,7 +23,6 @@ use std::hash::Hash; use std::time::Instant; use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; use timsrust::readers::{FrameReader, MetadataReader}; -use timsrust::Frame; use timsrust::Metadata; use timsrust::TimsRustError; use tracing::instrument; @@ -58,7 +57,7 @@ impl QuadSplittedTransposedIndex { tof_range: (u32, u32), precursor_mz_range: (f64, f64), scan_range: Option<(usize, usize)>, - rt_range: Option, + rt_range_seconds: Option<(f32, f32)>, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -67,7 +66,7 @@ impl QuadSplittedTransposedIndex { .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); trace!("matching_quads: {:?}", matching_quads); - self.query_precursor_peaks(&matching_quads, tof_range, scan_range, rt_range, f); + self.query_precursor_peaks(&matching_quads, tof_range, scan_range, rt_range_seconds, f); } fn query_precursor_peaks( @@ -75,7 +74,7 @@ impl QuadSplittedTransposedIndex { matching_quads: &[SingleQuadrupoleSettingIndex], tof_range: (u32, u32), scan_range: Option<(usize, usize)>, - rt_range: Option, + rt_range_seconds: Option<(f32, f32)>, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -85,7 +84,7 @@ impl QuadSplittedTransposedIndex { .fragment_indices .get(quad) .expect("Only existing quads should be queried."); - tqi.query_peaks(tof_range, scan_range, rt_range) + tqi.query_peaks(tof_range, scan_range, rt_range_seconds) .for_each(&mut *f); } } @@ -242,6 +241,8 @@ impl QuadSplittedTransposedIndexBuilder { let out2: Result, TimsRustError> = split_frames .into_par_iter() .map(|(q, frameslices)| { + // TODO:Refactor so the internal index is built first and then added. + // This should save a couple of thousand un-necessary hashmap lookups. let mut out = Self::new(); for frameslice in frameslices { out.add_frame_slice(frameslice); @@ -308,100 +309,6 @@ impl QuadSplittedTransposedIndexBuilder { } } -impl - QueriableData, RawPeak> for QuadSplittedTransposedIndex -{ - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.0 as f64, - ); - let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); - - fragment_query - .mz_index_ranges - .iter() - .flat_map(|(_, tof_range)| { - let mut local_vec = vec![]; - self.query_peaks( - *tof_range, - precursor_mz_range, - scan_range, - frame_index_range, - &mut |x| local_vec.push(RawPeak::from(x)), - ); - local_vec.into_iter() - }) - .collect() - } - - fn add_query>( - &self, - fragment_query: &FragmentGroupIndexQuery, - aggregator: &mut AG, - ) { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.0 as f64, - ); - let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); - - fragment_query - .mz_index_ranges - .iter() - .for_each(|(_, tof_range)| { - self.query_peaks( - *tof_range, - precursor_mz_range, - scan_range, - frame_index_range, - &mut |peak| aggregator.add(&RawPeak::from(peak)), - ); - }) - } - - fn add_query_multi_group>( - &self, - fragment_queries: &[FragmentGroupIndexQuery], - aggregator: &mut [AG], - ) { - fragment_queries - .par_iter() - .zip(aggregator.par_iter_mut()) - .for_each(|(fragment_query, agg)| { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.1 as f64, - ); - assert!(precursor_mz_range.0 <= precursor_mz_range.1); - assert!(precursor_mz_range.0 > 0.0); - let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); - - for (_, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_peaks( - tof_range, - precursor_mz_range, - scan_range, - frame_index_range, - &mut |peak| agg.add(&RawPeak::from(peak)), - ); - } - }); - } -} - -// Copy pasting for now ... TODO refactor or delete -// ============================================================================ - impl QueriableData, (RawPeak, FH)> for QuadSplittedTransposedIndex { @@ -411,9 +318,6 @@ impl fragment_query.precursor_query.isolation_mz_range.0 as f64, ); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); fragment_query .mz_index_ranges @@ -424,7 +328,7 @@ impl *tof_range, precursor_mz_range, scan_range, - frame_index_range, + Some(fragment_query.precursor_query.rt_range_seconds), &mut |x| local_vec.push((RawPeak::from(x), *fh)), ); @@ -443,9 +347,6 @@ impl fragment_query.precursor_query.isolation_mz_range.0 as f64, ); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); fragment_query .mz_index_ranges @@ -455,7 +356,7 @@ impl *tof_range, precursor_mz_range, scan_range, - frame_index_range, + Some(fragment_query.precursor_query.rt_range_seconds), &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), ); }) @@ -477,9 +378,6 @@ impl assert!(precursor_mz_range.0 <= precursor_mz_range.1); assert!(precursor_mz_range.0 > 0.0); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let frame_index_range = Some(FrameRTTolerance::FrameIndex( - fragment_query.precursor_query.frame_index_range, - )); let local_quad_vec: Vec = self .get_matching_quad_settings(precursor_mz_range, scan_range) @@ -490,7 +388,7 @@ impl &local_quad_vec, tof_range, scan_range, - frame_index_range, + Some(fragment_query.precursor_query.rt_range_seconds), &mut |peak| agg.add(&(RawPeak::from(peak), fh)), ); } @@ -498,8 +396,6 @@ impl } } -// ============================================================================ - impl ToleranceAdapter, ElutionGroup> for QuadSplittedTransposedIndex diff --git a/src/models/queries.rs b/src/models/queries.rs index 8813111..dc3b9ea 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -4,6 +4,7 @@ use std::hash::Hash; #[derive(Debug, Clone)] pub struct PrecursorIndexQuery { pub frame_index_range: (usize, usize), + pub rt_range_seconds: (f32, f32), pub mz_index_range: (u32, u32), pub mobility_index_range: (usize, usize), pub isolation_mz_range: (f32, f32), diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 2595405..2e4a79a 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -5,103 +5,53 @@ use std::rc::Rc; use std::time::Instant; use tracing::info; -use crate::{Aggregator, ElutionGroup, HasIntegerID, QueriableData, Tolerance, ToleranceAdapter}; - -/// A struct that can be queried for TIMS data. -/// -/// The main idea behind this struct is to provide a generic way to query TIMS data. -/// And by that I mean that with the same interface but different imeplementations -/// on the indexing one can optimize for different access patterns. -/// -/// In the same way different aggregators can be used to aggregate the results. -/// In some cases just adding the intensities might be enough, but in other cases -/// one might want to do more complex operations. (fitting a gaussian to a chromatogram for example) -/// -/// Main Generic parameters: -/// - `ID`: The type of indexed data that will be queried. -/// This can also optimize the access pattern to the data. When the data is queried -/// with the `many` version of the query functions, the indexed data can optimize -/// the access pattern to the data. -/// - `TA`: The type of tolerance adapter that will be used to convert elution groups into queries. -/// - `TL`: The type of tolerance that will be used to define the search space. -/// -/// Additional Generic parameters: -/// - `QP`: The type of precursor query that will be used to query the indexed data. -/// - `QF`: The type of fragment query that will be used to query the indexed data. -/// - `OE`: The type of output element that will be returned by the aggregators. -/// - `AG`: The type of aggregator that will be used to aggregate the output elements. -/// - `AE`: The type of element that will be aggregated. -/// -/// -/// So ... in other words ... -/// 1. `TA` converts `ElutionGroup` with `TL` into `QP` and `QF` queries. -/// 2. `ID` is queried with `QP` and `QF` queries. -/// 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). -pub struct QueriableTimsData<'a, ID, TA, TL, QF, AE, OE, AG, EG: HasIntegerID> -where - AG: Aggregator + Send + Sync, - ID: QueriableData, - TA: ToleranceAdapter, - TL: Tolerance, - QF: Send + Sync, - AE: Send + Sync + Clone + Copy, -{ - pub indexed_data: &'a ID, - pub aggregator_factory: &'a dyn Fn(u64) -> AG, - pub tolerance_adapter: &'a TA, - pub tolerance: &'a TL, - pub _phantom_queries: std::marker::PhantomData, - pub _phatom_agg: std::marker::PhantomData<(AE, OE, AG)>, - pub _phatom_elution_group: std::marker::PhantomData, -} - -impl<'a, ID, TA, TL, QF, AE, OE, AG, EG> QueriableTimsData<'a, ID, TA, TL, QF, AE, OE, AG, EG> -where - AG: Aggregator + Send + Sync, - ID: QueriableData, - TA: ToleranceAdapter, - TL: Tolerance, - EG: HasIntegerID, - QF: Send + Sync, - AE: Send + Sync + Clone + Copy, -{ - pub fn query(&self, elution_group: &EG) -> OE { - let mut aggregator = (self.aggregator_factory)(elution_group.get_id()); - let prep_query = self - .tolerance_adapter - .query_from_elution_group(self.tolerance, elution_group); - - self.indexed_data.add_query(&prep_query, &mut aggregator); - aggregator.finalize() - } - - pub fn add_query_multi_group(&self, elution_groups: &[EG], aggregators: &mut [AG]) { - let mut fragment_queries = Vec::with_capacity(elution_groups.len()); - - for elution_group in elution_groups { - let qf = self - .tolerance_adapter - .query_from_elution_group(self.tolerance, elution_group); - - fragment_queries.push(qf); - } - - self.indexed_data - .add_query_multi_group(&fragment_queries, aggregators); - } -} - -pub fn query_multi_group<'a, ID, TA, TL, QF, AE, OE, AG, FH>( - indexed_data: &'a ID, - tolerance_adapter: &'a TA, +use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter}; + +// TODO: URGENTLY make documentation fot eh functions using this leftover struct docs +// as a reference. +// +// A struct that can be queried for TIMS data. +// +// The main idea behind this struct is to provide a generic way to query TIMS data. +// And by that I mean that with the same interface but different imeplementations +// on the indexing one can optimize for different access patterns. +// +// In the same way different aggregators can be used to aggregate the results. +// In some cases just adding the intensities might be enough, but in other cases +// one might want to do more complex operations. (fitting a gaussian to a chromatogram for example) +// +// Main Generic parameters: +// - `QD`: The type of indexed data that will be queried. +// This can also optimize the access pattern to the data. When the data is queried +// with the `many` version of the query functions, the indexed data can optimize +// the access pattern to the data. +// +// also implements tolerance adapter that will be used to convert elution groups +// into queries. +// - `TL`: The type of tolerance that will be used to define the search space. +// +// Additional Generic parameters: +// - `QP`: The type of precursor query that will be used to query the indexed data. +// - `QF`: The type of fragment query that will be used to query the indexed data. +// - `OE`: The type of output element that will be returned by the aggregators. +// - `AG`: The type of aggregator that will be used to aggregate the output elements. +// - `AE`: The type of element that will be aggregated. +// +// +// So ... in other words ... +// 1. `TA` converts `ElutionGroup` with `TL` into `QP` and `QF` queries. +// 2. `QD` is queried with `QP` and `QF` queries. +// 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). + +pub fn query_multi_group<'a, QD, TL, QF, AE, OE, AG, FH>( + queriable_data: &'a QD, tolerance: &'a TL, elution_groups: &[ElutionGroup], aggregator_factory: &dyn Fn(u64) -> AG, ) -> Vec where AG: Aggregator + Send + Sync, - ID: QueriableData, - TA: ToleranceAdapter>, + QD: QueriableData + ToleranceAdapter>, TL: Tolerance, OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, @@ -113,13 +63,13 @@ where let mut aggregators = Vec::with_capacity(elution_groups.len()); for (i, elution_group) in elution_groups.iter().enumerate() { - let qp = tolerance_adapter.query_from_elution_group(tolerance, elution_group); + let qp = queriable_data.query_from_elution_group(tolerance, elution_group); fragment_queries.push(qp); aggregators.push(aggregator_factory(i as u64)); } - indexed_data.add_query_multi_group(&fragment_queries, &mut aggregators); + queriable_data.add_query_multi_group(&fragment_queries, &mut aggregators); let duration = start.elapsed(); info!("Querying took {:#?}", duration); @@ -140,8 +90,8 @@ where out } -pub fn query_indexed( - indexed_data: &ID, +pub fn query_indexed( + queriable_data: &QD, aggregator_factory: &dyn Fn(u64) -> AG, tolerance_adapter: &TA, tolerance: &TL, @@ -149,7 +99,7 @@ pub fn query_indexed( ) -> OE where AG: Aggregator + Send + Sync, - ID: QueriableData, + QD: QueriableData, TA: ToleranceAdapter>, TL: Tolerance, FH: Clone + Eq + Serialize + Hash + Send + Sync, @@ -159,6 +109,6 @@ where let mut aggregator = aggregator_factory(elution_group.id); let prep_query = Rc::new(tolerance_adapter.query_from_elution_group(tolerance, elution_group)); - indexed_data.add_query(&prep_query, &mut aggregator); + queriable_data.add_query(&prep_query, &mut aggregator); aggregator.finalize() } diff --git a/src/traits/indexed_data.rs b/src/traits/indexed_data.rs deleted file mode 100644 index a1ce16f..0000000 --- a/src/traits/indexed_data.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::Aggregator; - -pub trait QueriableData -where - QF: Send + Sync, - A: Send + Sync + Clone + Copy, -{ - fn query(&self, fragment_query: &QF) -> Vec; - fn add_query>( - &self, - fragment_query: &QF, - aggregator: &mut AG, - ); - fn add_query_multi_group>( - &self, - fragment_queries: &[QF], - aggregator: &mut [AG], - ); -} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 9a7266a..4e3908c 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -1,3 +1,3 @@ pub mod aggregator; -pub mod indexed_data; +pub mod queriable_data; pub mod tolerance; diff --git a/src/traits/queriable_data.rs b/src/traits/queriable_data.rs new file mode 100644 index 0000000..07d5a91 --- /dev/null +++ b/src/traits/queriable_data.rs @@ -0,0 +1,53 @@ +use super::tolerance::ToleranceAdapter; +use crate::models::elution_group::ElutionGroup; +use crate::Aggregator; + +pub trait QueriableData +where + QF: Send + Sync, + A: Send + Sync + Clone + Copy, +{ + fn query(&self, fragment_query: &QF) -> Vec; + fn add_query>( + &self, + fragment_query: &QF, + aggregator: &mut AG, + ); + fn add_query_multi_group>( + &self, + fragment_queries: &[QF], + aggregator: &mut [AG], + ); +} + +// I like this idea but I need a way to set/propagate the tolerance +// +// impl>, QF, A, K> QueriableData for T { +// fn query(&self, fragment_query: &QF) -> Vec { +// let mut out = Vec::new(); +// let qf = self.query_from_elution_group(fragment_query); +// self.add_query(&qf, &mut |x| out.push(x)); +// out +// } +// +// fn add_query>( +// &self, +// fragment_query: &QF, +// aggregator: &mut AG, +// ) { +// let qf = self.query_from_elution_group(fragment_query); +// self.add_query(&qf, aggregator); +// } +// +// fn add_query_multi_group>( +// &self, +// fragment_queries: &[QF], +// aggregator: &mut [AG], +// ) { +// let qfs = fragment_queries +// .iter() +// .map(|x| self.query_from_elution_group(x)) +// .collect(); +// self.add_query_multi_group(&qfs, aggregator); +// } +// } diff --git a/src/traits/tolerance.rs b/src/traits/tolerance.rs index b02dcfa..0e4a2f3 100644 --- a/src/traits/tolerance.rs +++ b/src/traits/tolerance.rs @@ -113,12 +113,11 @@ impl Tolerance for DefaultTolerance { } } -// TODO decide whether to kill this one or to change the interrface -// and how to propagate identifiers. -pub trait HasIntegerID { - fn get_id(&self) -> u64; -} - -pub trait ToleranceAdapter { +/// A trait that can be implemented by types that can convert +/// elution groups into queries. +/// +/// The elution group here is generic but most regularly it will be +/// an `ElutionGroup` struct. +pub trait ToleranceAdapter { fn query_from_elution_group(&self, tol: &dyn Tolerance, elution_group: &T) -> QF; } diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 82c5ce0..6e2c090 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -1,5 +1,4 @@ use crate::sort_vecs_by_first; -use std::cmp::Ordering; use std::ops::RangeInclusive; use tracing::{error, info, warn}; @@ -57,29 +56,6 @@ impl<'a> PeakArrayRefs<'a> { } } -/// Splits a global index into a major and minor that -/// matches elements in a collection. -/// -/// As an example if there is a slice of slices, the major index -/// is the index of the slice and the minor index is the index -/// of the element in the slice. -/// -/// Thus if we have [[0,1,2], [3,4,5], [6,7,8]] -/// the major indice are 0,0,0 1,1,1 2,2,2 -/// and the minor 0,1,2 0,1,2 0,1,2 -/// -/// -fn index_split(index: usize, slice_sizes: &[usize]) -> (usize, usize) { - let mut index = index; - for (i, slice_size) in slice_sizes.iter().enumerate() { - if index < *slice_size { - return (i, index); - } - index -= *slice_size; - } - panic!("Index {} is out of bounds", index); -} - // TODO: Refactor this function pub fn lazy_centroid_weighted_frame<'a>( peak_refs: &'a [PeakArrayRefs<'a>], diff --git a/src/utils/sorting.rs b/src/utils/sorting.rs index d67d691..c35b22a 100644 --- a/src/utils/sorting.rs +++ b/src/utils/sorting.rs @@ -44,8 +44,57 @@ macro_rules! sort_vecs_by_first { }}; } +/// Returns the top n elements of a slice. +/// +/// The indices of the elements are also returned. +/// +/// # Example +/// ``` +/// use timsquery::utils::sorting::top_n; +/// +/// let v = vec![1, 2, 10, 4, 5, 9, 7, 8, 2, 1]; +/// let (top, indices) = top_n(&v, 3); +/// assert_eq!(top, vec![10, 9, 8]); +/// assert_eq!(indices, vec![2, 5, 7]); +/// ``` +/// +/// https://users.rust-lang.org/t/solved-best-way-to-find-largest-three-values-in-unsorted-slice/34754/12 +pub fn top_n(slice: &[u32], n: usize) -> (Vec, Vec) { + let mut result = Vec::with_capacity(n + 1); + let mut result_indices = Vec::with_capacity(n + 1); + for (ind, v) in slice.iter().copied().enumerate() { + result.push(v); + result_indices.push(ind); + + let mut i = result.len() - 1; + while i > 0 { + if result[i] <= result[i - 1] { + break; + } + + // swap + let t = result[i]; + let tind = result_indices[i]; + result[i] = result[i - 1]; + result[i - 1] = t; + result_indices[i] = result_indices[i - 1]; + result_indices[i - 1] = tind; + + i -= 1; + } + + if result.len() > n { + result.pop(); + result_indices.pop(); + } + } + (result, result_indices) +} + #[cfg(test)] mod tests { + use super::*; + #[test] fn test_sort_two_vecs() { let v1 = vec![3, 1, 4, 1, 5]; @@ -84,4 +133,12 @@ mod tests { assert_eq!(sorted_v3, vec![false, true, true]); assert_eq!(sorted_v4, vec![2.0, 1.0, 3.0]); } + + #[test] + fn test_top_n() { + let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let (top, indices) = top_n(&v, 3); + assert_eq!(top, vec![10, 9, 8]); + assert_eq!(indices, vec![9, 8, 7]); + } } From faf46820325a45f74cfa5d3b2770d90f20344cfb Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Wed, 30 Oct 2024 03:31:58 -0700 Subject: [PATCH 11/30] chore(benchmarks): updated benchmarks --- README.md | 4 + Taskfile.yml | 7 +- ..._results_230510_PRTC_13_S1-B1_1_12817.json | 127 ++++++------------ ..._diaPASEF_Condition_A_Sample_Alpha_02.json | 50 +++---- src/models/frames/expanded_frame.rs | 1 - src/utils/frame_processing.rs | 16 ++- 6 files changed, 86 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index ed47c00..8a21978 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,7 @@ sequential, use that). ## What does the cli look like right now? + +## TODO: + +- Add logging levels to instrumentations. diff --git a/Taskfile.yml b/Taskfile.yml index d08501b..cb7d494 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -40,9 +40,14 @@ tasks: - cargo clippy bench: + sources: + - "src/**/*.rs" cmds: - - SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d cargo run --release --features bench --bin benchmark_indices + - cargo b --release --features bench --bin benchmark_indices + - SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices + - SKIP_SLOW=1 SKIP_BUILD=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/230510_PRTC_13_S1-B1_1_12817.d ./target/release/benchmark_indices - uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json + - uv run benches/plot_bench.py data/benchmark_results_230510_PRTC_13_S1-B1_1_12817.json templates: sources: diff --git a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json index 1b5d19d..fbd58df 100644 --- a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json +++ b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json @@ -4,135 +4,90 @@ "num_iterations": 1 }, "results": [ - { - "name": "RawFileIndex", - "context": "Encoding", - "mean_duration_seconds": 0.006222996397515529, - "mean_duration_human_readable": "6.222996ms", - "setup_time_seconds": 4.833e-6, - "setup_time_human_readable": "4.833µs", - "note": null - }, - { - "name": "ExpandedRawFileIndexCentroided", - "context": "Encoding", - "mean_duration_seconds": 97.626668625, - "mean_duration_human_readable": "97.626668625s", - "setup_time_seconds": 9.17e-7, - "setup_time_human_readable": "917ns", - "note": null - }, - { - "name": "ExpandedRawFileIndex", - "context": "Encoding", - "mean_duration_seconds": 8.102684125, - "mean_duration_human_readable": "8.102684125s", - "setup_time_seconds": 1.042e-6, - "setup_time_human_readable": "1.042µs", - "note": null - }, - { - "name": "TransposedQuadIndex", - "context": "Encoding", - "mean_duration_seconds": 74.288219334, - "mean_duration_human_readable": "74.288219334s", - "setup_time_seconds": 1e-6, - "setup_time_human_readable": "1µs", - "note": null - }, - { - "name": "TransposedQuadIndexCentroided", - "context": "Encoding", - "mean_duration_seconds": 103.221435875, - "mean_duration_human_readable": "103.221435875s", - "setup_time_seconds": 9.58e-7, - "setup_time_human_readable": "958ns", - "note": null - }, { "name": "RawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 30.1034345, - "mean_duration_human_readable": "30.1034345s", - "setup_time_seconds": 0.080088542, - "setup_time_human_readable": "80.088542ms", + "mean_duration_seconds": 39.59563725, + "mean_duration_human_readable": "39.59563725s", + "setup_time_seconds": 0.080144208, + "setup_time_human_readable": "80.144208ms", "note": "RawFileIndex::query_multi_group aggregated 71227 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0043137600000000016, - "mean_duration_human_readable": "4.31376ms", - "setup_time_seconds": 5.777069208, - "setup_time_human_readable": "5.777069208s", + "mean_duration_seconds": 0.0029533639321533945, + "mean_duration_human_readable": "2.953363ms", + "setup_time_seconds": 7.448746083, + "setup_time_human_readable": "7.448746083s", "note": "ExpandedRawFileIndex::query_multi_group aggregated 68449 " }, { "name": "ExpandedRawFileIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 1.389402875, - "mean_duration_human_readable": "1.389402875s", - "setup_time_seconds": 5.838487875, - "setup_time_human_readable": "5.838487875s", + "mean_duration_seconds": 0.3531393056666667, + "mean_duration_human_readable": "353.139305ms", + "setup_time_seconds": 6.922591083, + "setup_time_human_readable": "6.922591083s", "note": "ExpandedRawFileIndex::query_multi_group aggregated 10744180 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.003144791757009347, - "mean_duration_human_readable": "3.144791ms", - "setup_time_seconds": 89.554111875, - "setup_time_human_readable": "89.554111875s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 26179 " + "mean_duration_seconds": 0.0017233227263339077, + "mean_duration_human_readable": "1.723322ms", + "setup_time_seconds": 93.177977, + "setup_time_human_readable": "93.177977s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 25111 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.2185190752, - "mean_duration_human_readable": "218.519075ms", - "setup_time_seconds": 88.279319125, - "setup_time_human_readable": "88.279319125s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5775312 " + "mean_duration_seconds": 0.06051568876470587, + "mean_duration_human_readable": "60.515688ms", + "setup_time_seconds": 88.266641959, + "setup_time_human_readable": "88.266641959s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 5714405 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.003314156013245034, - "mean_duration_human_readable": "3.314156ms", - "setup_time_seconds": 62.383771084, - "setup_time_human_readable": "62.383771084s", + "mean_duration_seconds": 0.0015990381517571877, + "mean_duration_human_readable": "1.599038ms", + "setup_time_seconds": 60.422055292, + "setup_time_human_readable": "60.422055292s", "note": "TransposedQuadIndex::query_multi_group aggregated 64676 " }, { "name": "TransposedQuadIndex", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.003775450037735844, - "mean_duration_human_readable": "3.77545ms", - "setup_time_seconds": 59.13618275, - "setup_time_human_readable": "59.13618275s", + "mean_duration_seconds": 0.0022858289497716907, + "mean_duration_human_readable": "2.285828ms", + "setup_time_seconds": 59.177824542, + "setup_time_human_readable": "59.177824542s", "note": "TransposedQuadIndex::query_multi_group aggregated 9971215 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.001552562270769231, - "mean_duration_human_readable": "1.552562ms", - "setup_time_seconds": 98.014544042, - "setup_time_human_readable": "98.014544042s", - "note": "TransposedQuadIndex::query_multi_group aggregated 22454 " + "mean_duration_seconds": 0.0009990033406593406, + "mean_duration_human_readable": "999.003µs", + "setup_time_seconds": 111.4298805, + "setup_time_human_readable": "111.4298805s", + "note": "TransposedQuadIndex::query_multi_group aggregated 25626 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.001730233717993079, - "mean_duration_human_readable": "1.730233ms", - "setup_time_seconds": 98.609288084, - "setup_time_human_readable": "98.609288084s", - "note": "TransposedQuadIndex::query_multi_group aggregated 5282090 " + "mean_duration_seconds": 0.0008020078941459505, + "mean_duration_human_readable": "802.007µs", + "setup_time_seconds": 120.050409959, + "setup_time_human_readable": "120.050409959s", + "note": "TransposedQuadIndex::query_multi_group aggregated 5257955 " } ], "metadata": { "basename": "230510_PRTC_13_S1-B1_1_12817" }, - "full_benchmark_time_seconds": 843.833114208 + "full_benchmark_time_seconds": 619.527993583 } \ No newline at end of file diff --git a/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json index edbebb6..3ae51d7 100644 --- a/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json +++ b/data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json @@ -7,51 +7,51 @@ { "name": "RawFileIndex", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 3.923214042, - "mean_duration_human_readable": "3.923214042s", - "setup_time_seconds": 0.236783083, - "setup_time_human_readable": "236.783083ms", + "mean_duration_seconds": 4.031142541, + "mean_duration_human_readable": "4.031142541s", + "setup_time_seconds": 0.243732333, + "setup_time_human_readable": "243.732333ms", "note": "RawFileIndex::query_multi_group aggregated 9144 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.0009338467012138191, - "mean_duration_human_readable": "933.846µs", - "setup_time_seconds": 470.779360458, - "setup_time_human_readable": "470.779360458s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 1505 " + "mean_duration_seconds": 0.0019490771459143978, + "mean_duration_human_readable": "1.949077ms", + "setup_time_seconds": 487.943830709, + "setup_time_human_readable": "487.943830709s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 1230 " }, { "name": "ExpandedRawFileIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 16.197693792, - "mean_duration_human_readable": "16.197693792s", - "setup_time_seconds": 487.496389916, - "setup_time_human_readable": "487.496389916s", - "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 213765098 " + "mean_duration_seconds": 13.260161042, + "mean_duration_human_readable": "13.260161042s", + "setup_time_seconds": 523.69316175, + "setup_time_human_readable": "523.69316175s", + "note": "ExpandedRawFileIndexCentroided::query_multi_group aggregated 203755782 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_narrow_rt", - "mean_duration_seconds": 0.003893406926070039, - "mean_duration_human_readable": "3.893406ms", - "setup_time_seconds": 530.744600625, - "setup_time_human_readable": "530.744600625s", - "note": "TransposedQuadIndex::query_multi_group aggregated 1347 " + "mean_duration_seconds": 0.003312026207792206, + "mean_duration_human_readable": "3.312026ms", + "setup_time_seconds": 607.35518275, + "setup_time_human_readable": "607.35518275s", + "note": "TransposedQuadIndex::query_multi_group aggregated 1206 " }, { "name": "TransposedQuadIndexCentroided", "context": "BatchAccess_full_rt", - "mean_duration_seconds": 0.004904444668292681, - "mean_duration_human_readable": "4.904444ms", - "setup_time_seconds": 542.031527458, - "setup_time_human_readable": "542.031527458s", - "note": "TransposedQuadIndex::query_multi_group aggregated 185201566 " + "mean_duration_seconds": 0.0074757223134328385, + "mean_duration_human_readable": "7.475722ms", + "setup_time_seconds": 589.906208458, + "setup_time_human_readable": "589.906208458s", + "note": "TransposedQuadIndex::query_multi_group aggregated 184228858 " } ], "metadata": { "basename": "LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02" }, - "full_benchmark_time_seconds": 2091.340999875 + "full_benchmark_time_seconds": 2285.880075541 } \ No newline at end of file diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index c9b0ceb..4016b1c 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -458,7 +458,6 @@ pub fn par_read_and_expand_frames( .collect(); println!("Slice info: {:?}", slice_infos); - panic!(); info!("Processing MS1 frames"); let ms1_iter = frame_reader.parallel_filter(|x| x.ms_level == MSLevel::MS1); diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 6e2c090..71528f9 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -132,9 +132,11 @@ pub fn lazy_centroid_weighted_frame<'a>( let mut curr_intensity = 0u64; let mut curr_weight = 0u64; - let mut curr_agg_tof = 0u64; - let mut curr_agg_ims = 0u64; + // let mut curr_agg_tof = 0u64; + // let mut curr_agg_ims = 0u64; + // This is added to the index within the loop + // To get the touched status of the peaks. let mut local_offset_touched = 0; for (ii, local_peak_refs) in peak_refs.iter().enumerate() { let ss_start = local_peak_refs @@ -152,8 +154,8 @@ pub fn lazy_centroid_weighted_frame<'a>( curr_intensity += local_intensity; global_num_touched += 1; } - curr_agg_tof += local_peak_refs.tof_array[i] as u64 * local_intensity; - curr_agg_ims += local_peak_refs.ims_array[i] as u64 * local_intensity; + // curr_agg_tof += local_peak_refs.tof_array[i] as u64 * local_intensity; + // curr_agg_ims += local_peak_refs.ims_array[i] as u64 * local_intensity; curr_weight += local_intensity; touched[ti] = true; } @@ -164,8 +166,10 @@ pub fn lazy_centroid_weighted_frame<'a>( // This means that at least 2 peaks need to be aggregated. if curr_weight > this_intensity && curr_intensity >= this_intensity { agg_intensity.push(u32::try_from(curr_intensity).expect("Expected to fit in u32")); - let calc_tof = (curr_agg_tof / curr_weight) as u32; - let calc_ims = (curr_agg_ims / curr_weight) as usize; + // let calc_tof = (curr_agg_tof / curr_weight) as u32; + // let calc_ims = (curr_agg_ims / curr_weight) as usize; + let calc_tof = tof; + let calc_ims = ims; debug_assert!(tof_range.contains(&calc_tof)); debug_assert!(ims_range.contains(&calc_ims)); agg_tof.push(calc_tof); From 37e39a54b21a1e06a15821e62620e4de391316f0 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Wed, 30 Oct 2024 16:25:30 -0700 Subject: [PATCH 12/30] refactor!: agg/query traits and cli --- README.md | 1 + src/main.rs | 26 ++++++++------ .../raw_peak_agg/chromatogram_agg.rs | 6 ++-- .../raw_peak_agg/multi_chromatogram_agg.rs | 4 +-- .../aggregators/raw_peak_agg/point_agg.rs | 8 +++-- src/models/frames/expanded_frame.rs | 1 - src/models/frames/raw_peak.rs | 7 ++++ .../indices/expanded_raw_index/model.rs | 20 ++++++----- src/models/indices/mod.rs | 4 +++ src/models/indices/raw_file_index.rs | 21 +++++++----- .../quad_splitted_transposed_index.rs | 24 +++++++------ .../queriable_tims_data.rs | 34 ++++--------------- src/traits/aggregator.rs | 13 +++++-- src/traits/queriable_data.rs | 26 ++++++-------- 14 files changed, 104 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 8a21978..ac30827 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,4 @@ sequential, use that). ## TODO: - Add logging levels to instrumentations. +- Add missing_docks_in_private_items to clippy. diff --git a/src/main.rs b/src/main.rs index 82f5e06..4620335 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,12 @@ use timsquery::{ ChromatomobilogramStats, ExtractedIonChromatomobilogram, MultiCMGStatsFactory, RawPeakIntensityAggregator, RawPeakVectorAggregator, }, - models::indices::raw_file_index::RawFileIndex, - models::indices::transposed_quad_index::QuadSplittedTransposedIndex, + models::indices::{ExpandedRawFrameIndex, QuadSplittedTransposedIndex}, }; use tracing::instrument; use tracing::subscriber::set_global_default; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; -use tracing_subscriber::{fmt, prelude::*, registry::Registry, EnvFilter, Layer}; +use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; fn main() { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); @@ -113,13 +112,14 @@ fn main_query_index(args: QueryIndexArgs) { serde_json::from_str(&std::fs::read_to_string(&elution_groups_path).unwrap()).unwrap(); let index_use = match (index_use, elution_groups.len() > 10) { - (PossibleIndex::RawFileIndex, true) => PossibleIndex::RawFileIndex, + (PossibleIndex::ExpandedRawFrameIndex, true) => PossibleIndex::ExpandedRawFrameIndex, (PossibleIndex::TransposedQuadIndex, true) => PossibleIndex::TransposedQuadIndex, - (PossibleIndex::RawFileIndex, false) => PossibleIndex::RawFileIndex, + (PossibleIndex::ExpandedRawFrameIndex, false) => PossibleIndex::ExpandedRawFrameIndex, (PossibleIndex::TransposedQuadIndex, false) => PossibleIndex::TransposedQuadIndex, (PossibleIndex::Unspecified, true) => PossibleIndex::TransposedQuadIndex, - (PossibleIndex::Unspecified, false) => PossibleIndex::RawFileIndex, + (PossibleIndex::Unspecified, false) => PossibleIndex::ExpandedRawFrameIndex, }; + // ExpandedRawFrameIndex, execute_query( index_use, @@ -162,7 +162,7 @@ pub enum PossibleAggregator { pub enum PossibleIndex { #[default] Unspecified, - RawFileIndex, + ExpandedRawFrameIndex, TransposedQuadIndex, } @@ -242,7 +242,7 @@ pub fn execute_query( macro_rules! execute_query_inner { ($index:expr, $agg:expr) => { - let tmp = query_multi_group(&$index, &$index, &tolerance, &elution_groups, &$agg); + let tmp = query_multi_group(&$index, &tolerance, &elution_groups, &$agg); let mut out = Vec::with_capacity(tmp.len()); for (res, eg) in tmp.into_iter().zip(elution_groups) { @@ -316,8 +316,8 @@ pub fn execute_query( } } } - (PossibleIndex::RawFileIndex, aggregator) => { - let index = RawFileIndex::from_path(&(raw_file_path.clone())).unwrap(); + (PossibleIndex::ExpandedRawFrameIndex, aggregator) => { + let index = ExpandedRawFrameIndex::from_path(&(raw_file_path.clone())).unwrap(); match aggregator { PossibleAggregator::RawPeakIntensityAggregator => { let aggregator = RawPeakIntensityAggregator::new; @@ -336,7 +336,11 @@ pub fn execute_query( execute_query_inner!(index, aggregator); } PossibleAggregator::MultiCMGStats => { - panic!("Not Implemented!"); + let factory = MultiCMGStatsFactory { + converters: (index.mz_converter, index.im_converter), + _phantom: std::marker::PhantomData::, + }; + execute_query_inner!(index, |x| factory.build(x)); } } } diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index 36281e4..53dd953 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -30,7 +30,8 @@ impl Aggregator for ExtractedIonChromatomobilogram { type Item = RawPeak; type Output = ChromatomobilogramVectorArrayTuples; - fn add(&mut self, peak: &RawPeak) { + fn add(&mut self, peak: impl Into) { + let peak = peak.into(); let u64_intensity = peak.intensity as u64; // In theory I could use a power of 2 to have a better preservation of @@ -170,7 +171,8 @@ impl Aggregator for ChromatomobilogramStats { type Item = RawPeak; type Output = ChromatomobilogramStatsArrays; - fn add(&mut self, peak: &RawPeak) { + fn add(&mut self, peak: impl Into) { + let peak = peak.into(); let u64_intensity = peak.intensity as u64; let rt_miliseconds = (peak.retention_time * 1000.0) as u32; diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index d446db9..04ca716 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -346,8 +346,8 @@ impl Aggregator for MultiCMGSta type Item = (RawPeak, FH); type Output = NaturalFinalizedMultiCMGStatsArrays; - fn add(&mut self, peak: &(RawPeak, FH)) { - let (peak, transition) = peak; + fn add(&mut self, peak: impl Into<(RawPeak, FH)>) { + let (peak, transition) = peak.into(); let u64_intensity = peak.intensity as u64; let rt_miliseconds = (peak.retention_time * 1000.0) as u32; diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index 1e1567d..1ddbd34 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -19,11 +19,12 @@ impl RawPeakIntensityAggregator { } } -impl Aggregator for RawPeakIntensityAggregator { +impl Aggregator for RawPeakIntensityAggregator { type Item = (RawPeak, T); type Output = u64; - fn add(&mut self, peak: &(RawPeak, T)) { + fn add(&mut self, peak: impl Into<(RawPeak, T)>) { + let peak = peak.into(); self.intensity += peak.0.intensity as u64; } @@ -70,7 +71,8 @@ impl Aggregator for RawPeakVectorAggregator { type Item = RawPeak; type Output = RawPeakVectorArrays; - fn add(&mut self, peak: &RawPeak) { + fn add(&mut self, peak: impl Into) { + let peak = peak.into(); self.peaks.scans.push(peak.scan_index); self.peaks.tofs.push(peak.tof_index); self.peaks.intensities.push(peak.intensity); diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 4016b1c..ab4bfae 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -672,7 +672,6 @@ impl ExpandedQuadSliceInfo { } for peak in local_peaks.into_iter() { - println!("peak: {:?}", peak); if peak.fwdone && peak.bwdone && peak.any_update { peaks.push(peak); } diff --git a/src/models/frames/raw_peak.rs b/src/models/frames/raw_peak.rs index 9d6a8f7..f4500bf 100644 --- a/src/models/frames/raw_peak.rs +++ b/src/models/frames/raw_peak.rs @@ -1,4 +1,5 @@ use serde::Serialize; +use std::convert::From; #[derive(Debug, Clone, Copy, Serialize)] pub struct RawPeak { @@ -7,3 +8,9 @@ pub struct RawPeak { pub intensity: u32, pub retention_time: f32, } + +impl From<(RawPeak, T)> for RawPeak { + fn from(x: (RawPeak, T)) -> Self { + x.0 + } +} diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index a00113e..bda8b6d 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -11,6 +11,7 @@ use crate::models::frames::single_quad_settings::{ SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; +use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::ToleranceAdapter; use rayon::prelude::*; @@ -207,11 +208,11 @@ impl // .collect() } - fn add_query>( - &self, - fragment_query: &FragmentGroupIndexQuery, - aggregator: &mut AG, - ) { + fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) + where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { todo!(); // let precursor_mz_range = ( // fragment_query.precursor_query.isolation_mz_range.0 as f64, @@ -236,11 +237,14 @@ impl // }) } - fn add_query_multi_group>( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], - ) { + ) where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { // fragment_queries // .par_iter() // .zip(aggregator.par_iter_mut()) @@ -321,7 +325,7 @@ impl } for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { - let mut local_lambda = |peak| agg.add(&(RawPeak::from(peak), *fh)); + let mut local_lambda = |peak| agg.add((RawPeak::from(peak), *fh)); tqi.query_peaks( *tof_range, scan_ranges[i], diff --git a/src/models/indices/mod.rs b/src/models/indices/mod.rs index b8cb6e1..81d43c9 100644 --- a/src/models/indices/mod.rs +++ b/src/models/indices/mod.rs @@ -1,3 +1,7 @@ pub mod expanded_raw_index; pub mod raw_file_index; pub mod transposed_quad_index; + +pub use expanded_raw_index::ExpandedRawFrameIndex; +pub use raw_file_index::RawFileIndex; +pub use transposed_quad_index::QuadSplittedTransposedIndex; diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index 462ff87..23a6977 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -12,7 +12,7 @@ use std::hash::Hash; use timsrust::converters::ConvertableDomain; use timsrust::readers::{FrameReader, FrameReaderError, MetadataReader}; use timsrust::TimsRustError; -use timsrust::{Frame, Metadata, QuadrupoleSettings}; +use timsrust::{Frame, Metadata}; use tracing::trace; pub struct RawFileIndex { @@ -193,19 +193,22 @@ impl out } - fn add_query>( - &self, - fragment_query: &FragmentGroupIndexQuery, - aggregator: &mut AG, - ) { - self.apply_on_query(fragment_query, &mut |x, y| aggregator.add(&(x, y))); + fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) + where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { + self.apply_on_query(fragment_query, &mut |x, y| aggregator.add((x, y))); } - fn add_query_multi_group>( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], - ) { + ) where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { fragment_queries .iter() .zip(aggregator.iter_mut()) diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 8d1dc80..0537410 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -1,4 +1,4 @@ -use super::quad_index::{FrameRTTolerance, TransposedQuadIndex, TransposedQuadIndexBuilder}; +use super::quad_index::{TransposedQuadIndex, TransposedQuadIndexBuilder}; use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ @@ -11,6 +11,7 @@ use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; use crate::models::queries::FragmentGroupIndexQuery; +use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; use crate::ToleranceAdapter; @@ -337,11 +338,11 @@ impl .collect() } - fn add_query>( - &self, - fragment_query: &FragmentGroupIndexQuery, - aggregator: &mut AG, - ) { + fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) + where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { let precursor_mz_range = ( fragment_query.precursor_query.isolation_mz_range.0 as f64, fragment_query.precursor_query.isolation_mz_range.0 as f64, @@ -357,16 +358,19 @@ impl precursor_mz_range, scan_range, Some(fragment_query.precursor_query.rt_range_seconds), - &mut |peak| aggregator.add(&(RawPeak::from(peak), *fh)), + &mut |peak| aggregator.add((RawPeak::from(peak), *fh)), ); }) } - fn add_query_multi_group>( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], - ) { + ) where + A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, + AG: Aggregator, + { fragment_queries .par_iter() .zip(aggregator.par_iter_mut()) @@ -389,7 +393,7 @@ impl tof_range, scan_range, Some(fragment_query.precursor_query.rt_range_seconds), - &mut |peak| agg.add(&(RawPeak::from(peak), fh)), + &mut |peak| agg.add((RawPeak::from(peak), fh)), ); } }); diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 2e4a79a..4252b57 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -1,7 +1,6 @@ use rayon::prelude::*; use serde::Serialize; use std::hash::Hash; -use std::rc::Rc; use std::time::Instant; use tracing::info; @@ -43,20 +42,22 @@ use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter // 2. `QD` is queried with `QP` and `QF` queries. // 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). -pub fn query_multi_group<'a, QD, TL, QF, AE, OE, AG, FH>( +pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH>( queriable_data: &'a QD, tolerance: &'a TL, elution_groups: &[ElutionGroup], aggregator_factory: &dyn Fn(u64) -> AG, ) -> Vec where - AG: Aggregator + Send + Sync, - QD: QueriableData + ToleranceAdapter>, + AG: Aggregator + Send + Sync, + QD: QueriableData + ToleranceAdapter>, TL: Tolerance, OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, QF: Send + Sync, - AE: Send + Sync + Clone + Copy, + // AE: Send + Sync + Clone + Copy, + AE1: Into + Send + Sync + Clone + Copy, + AE2: Send + Sync + Clone + Copy + From, { let start = Instant::now(); let mut fragment_queries = Vec::with_capacity(elution_groups.len()); @@ -89,26 +90,3 @@ where info!("Aggregation took {:#?}", elapsed); out } - -pub fn query_indexed( - queriable_data: &QD, - aggregator_factory: &dyn Fn(u64) -> AG, - tolerance_adapter: &TA, - tolerance: &TL, - elution_group: &ElutionGroup, -) -> OE -where - AG: Aggregator + Send + Sync, - QD: QueriableData, - TA: ToleranceAdapter>, - TL: Tolerance, - FH: Clone + Eq + Serialize + Hash + Send + Sync, - QF: Send + Sync, - AE: Send + Sync + Clone + Copy, -{ - let mut aggregator = aggregator_factory(elution_group.id); - let prep_query = Rc::new(tolerance_adapter.query_from_elution_group(tolerance, elution_group)); - - queriable_data.add_query(&prep_query, &mut aggregator); - aggregator.finalize() -} diff --git a/src/traits/aggregator.rs b/src/traits/aggregator.rs index 209d444..13af0ae 100644 --- a/src/traits/aggregator.rs +++ b/src/traits/aggregator.rs @@ -1,7 +1,16 @@ +/// A trait that defines how to aggregate items. +/// +/// The `Item` type is the type of the item that is being aggregated. +/// The `Output` type is the type of the output of the aggregation. +/// +/// The `add` method takes an item of type `Item` OR a type that +/// imlements `Into`. +/// +/// The `finalize` method returns the output of the aggregation. pub trait Aggregator: Send + Sync { - type Item: Send + Sync; + type Item: Send + Sync + Clone; type Output: Send + Sync; - fn add(&mut self, item: &Self::Item); + fn add(&mut self, item: impl Into); fn finalize(self) -> Self::Output; } diff --git a/src/traits/queriable_data.rs b/src/traits/queriable_data.rs index 07d5a91..92f96de 100644 --- a/src/traits/queriable_data.rs +++ b/src/traits/queriable_data.rs @@ -1,23 +1,19 @@ -use super::tolerance::ToleranceAdapter; -use crate::models::elution_group::ElutionGroup; use crate::Aggregator; -pub trait QueriableData +pub trait QueriableData where QF: Send + Sync, - A: Send + Sync + Clone + Copy, + I: Send + Sync + Clone + Copy, { - fn query(&self, fragment_query: &QF) -> Vec; - fn add_query>( - &self, - fragment_query: &QF, - aggregator: &mut AG, - ); - fn add_query_multi_group>( - &self, - fragment_queries: &[QF], - aggregator: &mut [AG], - ); + fn query(&self, fragment_query: &QF) -> Vec; + fn add_query(&self, fragment_query: &QF, aggregator: &mut AG) + where + A: From + Send + Sync + Clone + Copy, + AG: Aggregator; + fn add_query_multi_group(&self, fragment_queries: &[QF], aggregator: &mut [AG]) + where + A: From + Send + Sync + Clone + Copy, + AG: Aggregator; } // I like this idea but I need a way to set/propagate the tolerance From c4cde153a1542caac3ba9effcf4de389c72cbfb1 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Wed, 30 Oct 2024 17:37:00 -0700 Subject: [PATCH 13/30] refactor(errors)!: refactored errors --- Cargo.toml | 2 +- src/errors.rs | 58 ++++++++ src/lib.rs | 4 + src/models/frames/expanded_frame.rs | 139 +++++++----------- .../indices/expanded_raw_index/model.rs | 20 +-- .../transposed_quad_index/quad_index.rs | 40 ----- .../quad_splitted_transposed_index.rs | 27 ++-- 7 files changed, 129 insertions(+), 161 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.toml b/Cargo.toml index 38305e8..5546743 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,12 @@ tracing-subscriber = { version = "0.3.18", features = [ "env-filter", ] } tracing-bunyan-formatter = "0.3.9" +tracing-chrome = "0.7.2" # These are only used for benchmarks rand = { version = "0.8.5", optional = true } rand_chacha = { version = "0.3.1", optional = true } -tracing-chrome = "0.7.2" [features] diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..4b66808 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,58 @@ +use crate::models::frames::expanded_frame::FrameProcessingConfig; +use std::fmt::Display; +use timsrust::TimsRustError; + +#[derive(Debug)] +pub enum TimsqueryError { + DataReadingError(DataReadingError), + DataProcessingError(DataProcessingError), + Other(String), +} + +pub type Result = std::result::Result; + +impl Display for TimsqueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl TimsqueryError { + pub fn custom(msg: impl Display) -> Self { + Self::Other(msg.to_string()) + } +} + +#[derive(Debug)] +pub enum DataReadingError { + UnsupportedDataError(UnsupportedDataError), + TimsRustError(TimsRustError), // Why doesnt timsrust error derive clone? +} + +impl From for DataReadingError { + fn from(e: UnsupportedDataError) -> Self { + DataReadingError::UnsupportedDataError(e) + } +} + +#[derive(Debug)] +pub enum UnsupportedDataError { + NoMS2DataError, +} + +#[derive(Debug)] +pub enum DataProcessingError { + CentroidingError(FrameProcessingConfig), +} + +impl> From for TimsqueryError { + fn from(e: T) -> Self { + TimsqueryError::DataReadingError(e.into()) + } +} + +impl> From for DataReadingError { + fn from(e: T) -> Self { + DataReadingError::TimsRustError(e.into()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2ef3d2b..500b75e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,11 @@ pub use crate::traits::queriable_data::QueriableData; pub use crate::traits::tolerance::{Tolerance, ToleranceAdapter}; // Declare modules +pub mod errors; pub mod models; pub mod queriable_tims_data; pub mod traits; pub mod utils; + +// Re-export errors +pub use crate::errors::{DataProcessingError, DataReadingError, TimsqueryError}; diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index ab4bfae..c0da9f3 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,23 +1,20 @@ +use super::peak_in_quad::PeakInQuad; use super::single_quad_settings::{ expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, }; +use crate::errors::{Result, UnsupportedDataError}; +use crate::sort_vecs_by_first; +use crate::utils::compress_explode::explode_vec; +use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; +use crate::utils::sorting::top_n; +use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; use rayon::prelude::*; use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; +use timsrust::readers::{FrameReader, FrameReaderError}; use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; - -use super::peak_in_quad::PeakInQuad; -use crate::sort_vecs_by_first; -use crate::utils::compress_explode::explode_vec; -use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; -use crate::utils::sorting::top_n; -use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; -use timsrust::{ - readers::{FrameReader, FrameReaderError, MetadataReaderError}, - TimsRustError, -}; use tracing::instrument; use tracing::{info, trace, warn}; @@ -301,14 +298,31 @@ pub fn par_expand_and_arrange_frames( out } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CentroidingSettings { + ims_tol_pct: f64, + mz_tol_ppm: f64, + window_width: usize, + max_ms1_peaks: usize, + max_ms2_peaks: usize, +} + +impl Default for CentroidingSettings { + fn default() -> Self { + CentroidingSettings { + ims_tol_pct: 1.5, + mz_tol_ppm: 15.0, + window_width: 3, + max_ms1_peaks: 100_000, + max_ms2_peaks: 20_000, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum FrameProcessingConfig { Centroided { - ims_tol_pct: f64, - mz_tol_ppm: f64, - window_width: usize, - max_ms1_peaks: usize, - max_ms2_peaks: usize, + settings: CentroidingSettings, ims_converter: Option, mz_converter: Option, }, @@ -322,33 +336,20 @@ impl FrameProcessingConfig { mz_converter: Tof2MzConverter, ) -> Self { match self { - FrameProcessingConfig::Centroided { - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks, - max_ms2_peaks, - .. - } => FrameProcessingConfig::Centroided { - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks, - max_ms2_peaks, - ims_converter: Some(ims_converter), - mz_converter: Some(mz_converter), - }, + FrameProcessingConfig::Centroided { settings, .. } => { + FrameProcessingConfig::Centroided { + settings, + ims_converter: Some(ims_converter), + mz_converter: Some(mz_converter), + } + } FrameProcessingConfig::NotCentroided => FrameProcessingConfig::NotCentroided, } } pub fn default_centroided() -> Self { FrameProcessingConfig::Centroided { - ims_tol_pct: 1.5, - mz_tol_ppm: 15.0, - window_width: 3, - max_ms1_peaks: 100_000, - max_ms2_peaks: 20_000, + settings: Default::default(), ims_converter: Default::default(), mz_converter: Default::default(), } @@ -359,33 +360,8 @@ impl FrameProcessingConfig { } } -#[derive(Debug)] -pub enum DataReadingError { - CentroidingError(FrameProcessingConfig), - UnsupportedDataError(String), - TimsRustError(TimsRustError), // Why doesnt timsrust error derive clone? -} - -impl From for DataReadingError { - fn from(e: TimsRustError) -> Self { - DataReadingError::TimsRustError(e) - } -} - -impl From for DataReadingError { - fn from(e: MetadataReaderError) -> Self { - DataReadingError::TimsRustError(TimsRustError::MetadataReaderError(e)) - } -} - -impl From for DataReadingError { - fn from(e: FrameReaderError) -> Self { - DataReadingError::TimsRustError(TimsRustError::FrameReaderError(e)) - } -} - fn warn_and_skip_badframes( - frame_iter: impl ParallelIterator>, + frame_iter: impl ParallelIterator>, ) -> impl ParallelIterator { frame_iter.filter_map(|x| { // Log the info of the frame that broke ... @@ -409,16 +385,11 @@ fn warn_and_skip_badframes( pub fn par_read_and_expand_frames( frame_reader: &FrameReader, centroiding_config: FrameProcessingConfig, -) -> Result< - HashMap, Vec>>, - DataReadingError, -> { +) -> Result, Vec>>> { let dia_windows = match frame_reader.get_dia_windows() { Some(dia_windows) => dia_windows, None => { - return Err(DataReadingError::UnsupportedDataError( - "No dia windows found".to_string(), - )) + return Err(UnsupportedDataError::NoMS2DataError.into()); } }; @@ -430,19 +401,15 @@ pub fn par_read_and_expand_frames( let expanded_frames = match centroiding_config { FrameProcessingConfig::Centroided { - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks: _max_ms1_peaks, - max_ms2_peaks, + settings, ims_converter, mz_converter, } => par_expand_and_centroid_frames( curr_iter, - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms2_peaks, + settings.ims_tol_pct, + settings.mz_tol_ppm, + settings.window_width, + settings.max_ms2_peaks, &ims_converter.unwrap(), &mz_converter.unwrap(), ), @@ -464,19 +431,15 @@ pub fn par_read_and_expand_frames( let ms1_iter = warn_and_skip_badframes(ms1_iter); let expanded_ms1_frames = match centroiding_config { FrameProcessingConfig::Centroided { - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks, - max_ms2_peaks: _max_ms2_peaks, + settings, ims_converter, mz_converter, } => par_expand_and_centroid_frames( ms1_iter, - ims_tol_pct, - mz_tol_ppm, - window_width, - max_ms1_peaks, + settings.ims_tol_pct, + settings.mz_tol_ppm, + settings.window_width, + settings.max_ms1_peaks, &ims_converter.unwrap(), &mz_converter.unwrap(), ), diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index bda8b6d..f2c5533 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -1,8 +1,8 @@ +use crate::errors::Result; use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ - par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, FrameProcessingConfig, - SortedState, + par_read_and_expand_frames, ExpandedFrameSlice, FrameProcessingConfig, SortedState, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; @@ -19,7 +19,7 @@ use serde::Serialize; use std::collections::HashMap; use std::hash::Hash; use std::time::Instant; -use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; +use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::readers::{FrameReader, MetadataReader}; use tracing::info; use tracing::instrument; @@ -29,7 +29,6 @@ pub struct ExpandedRawFrameIndex { bundled_ms1_frames: ExpandedSliceBundle, bundled_frames: HashMap, flat_quad_settings: Vec, - rt_converter: Frame2RtConverter, pub mz_converter: Tof2MzConverter, pub im_converter: Scan2ImConverter, adapter: FragmentIndexAdapter, @@ -38,18 +37,15 @@ pub struct ExpandedRawFrameIndex { #[derive(Debug, Clone)] pub struct ExpandedSliceBundle { slices: Vec>, - rts: Vec, frame_indices: Vec, } impl ExpandedSliceBundle { pub fn new(mut slices: Vec>) -> Self { slices.sort_unstable_by(|a, b| a.rt.partial_cmp(&b.rt).unwrap()); - let rts = slices.iter().map(|x| x.rt).collect(); let frame_indices = slices.iter().map(|x| x.frame_index).collect(); Self { slices, - rts, frame_indices, } } @@ -113,21 +109,18 @@ impl ExpandedRawFrameIndex { } #[instrument(name = "ExpandedRawFrameIndex::from_path_centroided")] - pub fn from_path_centroided(path: &str) -> Result { + pub fn from_path_centroided(path: &str) -> Result { let config = FrameProcessingConfig::default_centroided(); Self::from_path_base(path, config) } #[instrument(name = "ExpandedRawFrameIndex::from_path")] - pub fn from_path(path: &str) -> Result { + pub fn from_path(path: &str) -> Result { Self::from_path_base(path, FrameProcessingConfig::NotCentroided) } #[instrument(name = "ExpandedRawFrameIndex::from_path_base")] - pub fn from_path_base( - path: &str, - centroid_config: FrameProcessingConfig, - ) -> Result { + pub fn from_path_base(path: &str, centroid_config: FrameProcessingConfig) -> Result { info!( "Building ExpandedRawFrameIndex from path {} config {:?}", path, centroid_config, @@ -166,7 +159,6 @@ impl ExpandedRawFrameIndex { bundled_ms1_frames: out_ms1_frames.expect("At least one ms1 frame should be present"), bundled_frames: out_ms2_frames, flat_quad_settings, - rt_converter: meta_converters.rt_converter, mz_converter: meta_converters.mz_converter, im_converter: meta_converters.im_converter, adapter, diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index b5b601d..c5d203e 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -83,46 +83,6 @@ impl TransposedQuadIndex { .map(move |p| PeakInQuad::from_peak_in_bucket(p, *tof_index)) }) } - - fn convert_to_local_frame_range( - &self, - rt_range: Option, - ) -> Option<(f32, f32)> { - // TODO consider if I should allow only RT here, since it would in theory - // force me to to the repreatable work beforehand. - let frame_index_range = match rt_range { - Some(FrameRTTolerance::Seconds((rt_low, rt_high))) => { - Some((rt_low as f32, rt_high as f32)) - } - Some(FrameRTTolerance::FrameIndex((frame_low, frame_high))) => { - let frame_id_start = self - .frame_indices - .binary_search_by(|x| x.cmp(&frame_low)) - .unwrap_or_else(|x| x); - - let frame_id_end = self - .frame_indices - .binary_search_by(|x| x.cmp(&frame_high)) - .unwrap_or_else(|x| x); - - // TODO consider throwing a warning if we are - // out of bounds here. - Some(( - self.frame_rts[frame_id_start.min(self.frame_rts.len() - 1)] as f32, - self.frame_rts[frame_id_end.min(self.frame_rts.len() - 1)] as f32, - )) - } - None => None, - }; - - if cfg!(debug_assertions) { - if let Some((low, high)) = frame_index_range { - debug_assert!(low <= high); - } - } - - frame_index_range - } } impl PeakInQuad { diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 0537410..2f003b9 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -1,9 +1,9 @@ use super::quad_index::{TransposedQuadIndex, TransposedQuadIndexBuilder}; +use crate::errors::Result; use crate::models::adapters::FragmentIndexAdapter; use crate::models::elution_group::ElutionGroup; use crate::models::frames::expanded_frame::{ - par_read_and_expand_frames, DataReadingError, ExpandedFrameSlice, FrameProcessingConfig, - SortingStateTrait, + par_read_and_expand_frames, ExpandedFrameSlice, FrameProcessingConfig, SortingStateTrait, }; use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::raw_peak::RawPeak; @@ -22,10 +22,9 @@ use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; use std::time::Instant; -use timsrust::converters::{Frame2RtConverter, Scan2ImConverter, Tof2MzConverter}; +use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; use timsrust::readers::{FrameReader, MetadataReader}; use timsrust::Metadata; -use timsrust::TimsRustError; use tracing::instrument; use tracing::{debug, info, trace}; @@ -36,7 +35,6 @@ pub struct QuadSplittedTransposedIndex { precursor_index: TransposedQuadIndex, fragment_indices: HashMap, flat_quad_settings: Vec, - rt_converter: Frame2RtConverter, pub mz_converter: Tof2MzConverter, pub im_converter: Scan2ImConverter, adapter: FragmentIndexAdapter, @@ -104,7 +102,6 @@ impl Display for QuadSplittedTransposedIndex { let mut disp_str = String::new(); disp_str.push_str("QuadSplittedTransposedIndex\n"); - disp_str.push_str("rt_converter: ... not showing ...\n"); disp_str.push_str(&format!("mz_converter: {:?}\n", self.mz_converter)); disp_str.push_str(&format!("im_converter: {:?}\n", self.im_converter)); disp_str.push_str("flat_quad_settings: \n"); @@ -139,7 +136,7 @@ impl Display for QuadSplittedTransposedIndex { impl QuadSplittedTransposedIndex { #[instrument(name = "QuadSplittedTransposedIndex::from_path")] - pub fn from_path(path: &str) -> Result { + pub fn from_path(path: &str) -> Result { let st = Instant::now(); info!("Building transposed quad index from path {}", path); let tmp = QuadSplittedTransposedIndexBuilder::from_path(path)?; @@ -151,7 +148,7 @@ impl QuadSplittedTransposedIndex { } #[instrument(name = "QuadSplittedTransposedIndex::from_path_centroided")] - pub fn from_path_centroided(path: &str) -> Result { + pub fn from_path_centroided(path: &str) -> Result { let st = Instant::now(); info!( "Building CENTROIDED transposed quad index from path {}", @@ -169,7 +166,6 @@ impl QuadSplittedTransposedIndex { #[derive(Debug, Clone, Default)] pub struct QuadSplittedTransposedIndexBuilder { indices: HashMap, TransposedQuadIndexBuilder>, - rt_converter: Option, mz_converter: Option, im_converter: Option, metadata: Option, @@ -200,12 +196,12 @@ impl QuadSplittedTransposedIndexBuilder { } #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path")] - fn from_path(path: &str) -> Result { + fn from_path(path: &str) -> Result { Self::from_path_base(path, FrameProcessingConfig::NotCentroided) } #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path_centroided")] - fn from_path_centroided(path: &str) -> Result { + fn from_path_centroided(path: &str) -> Result { let config = FrameProcessingConfig::default_centroided(); Self::from_path_base(path, config) } @@ -214,10 +210,7 @@ impl QuadSplittedTransposedIndexBuilder { // and one that adds the frameslices, maybe even have a config struct that dispatches // the right preprocessing steps. #[instrument(name = "QuadSplittedTransposedIndexBuilder::from_path_base")] - fn from_path_base( - path: &str, - centroid_config: FrameProcessingConfig, - ) -> Result { + fn from_path_base(path: &str, centroid_config: FrameProcessingConfig) -> Result { let file_reader = FrameReader::new(path)?; let sql_path = std::path::Path::new(path).join("analysis.tdf"); @@ -226,7 +219,6 @@ impl QuadSplittedTransposedIndexBuilder { let out_meta_converters = meta_converters.clone(); let mut final_out = Self { indices: HashMap::new(), - rt_converter: Some(meta_converters.rt_converter), mz_converter: Some(meta_converters.mz_converter), im_converter: Some(meta_converters.im_converter), metadata: Some(out_meta_converters), @@ -239,7 +231,7 @@ impl QuadSplittedTransposedIndexBuilder { let split_frames = par_read_and_expand_frames(&file_reader, centroid_config)?; // TODO use the rayon contructor to fold - let out2: Result, TimsRustError> = split_frames + let out2: Result> = split_frames .into_par_iter() .map(|(q, frameslices)| { // TODO:Refactor so the internal index is built first and then added. @@ -302,7 +294,6 @@ impl QuadSplittedTransposedIndexBuilder { precursor_index: precursor_index.expect("Precursor peaks should be present"), fragment_indices: indices, flat_quad_settings, - rt_converter: self.rt_converter.unwrap(), mz_converter: self.mz_converter.unwrap(), im_converter: self.im_converter.unwrap(), adapter: FragmentIndexAdapter::from(self.metadata.unwrap()), From 7dbf59289a29750fdead9350c30bebb3eb3855eb Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Wed, 30 Oct 2024 17:52:18 -0700 Subject: [PATCH 14/30] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 680a5d5..473a7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "timsquery" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "indicatif", diff --git a/Cargo.toml b/Cargo.toml index 5546743..fa3b214 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "timsquery" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "Apache-2.0" From 0fd0173800744d1162e0cb3a924c3423409cd3c8 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Thu, 31 Oct 2024 13:04:09 -0700 Subject: [PATCH 15/30] (feat,wip) initial move to ms1 + trait cleanup --- src/main.rs | 2 + src/models/adapters.rs | 35 +++-- src/models/aggregators/mod.rs | 1 - .../raw_peak_agg/chromatogram_agg.rs | 129 +----------------- src/models/aggregators/raw_peak_agg/mod.rs | 1 - .../raw_peak_agg/multi_chromatogram_agg.rs | 12 +- .../aggregators/raw_peak_agg/point_agg.rs | 10 +- src/models/frames/expanded_frame.rs | 40 +++--- src/models/frames/raw_frames.rs | 25 ++-- src/models/frames/single_quad_settings.rs | 29 ++-- .../indices/expanded_raw_index/model.rs | 43 +++--- src/models/indices/raw_file_index.rs | 110 ++------------- .../transposed_quad_index/peak_bucket.rs | 29 ++-- .../transposed_quad_index/quad_index.rs | 9 +- .../quad_splitted_transposed_index.rs | 33 ++--- src/models/queries.rs | 69 ++++++++-- .../queriable_tims_data.rs | 9 +- src/traits/aggregator.rs | 40 ++++++ src/traits/queriable_data.rs | 42 +----- src/traits/tolerance.rs | 94 +++++++++++++ src/utils/frame_processing.rs | 25 ++-- src/utils/tolerance_ranges.rs | 69 ++++++++-- 22 files changed, 430 insertions(+), 426 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4620335..203d25b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,6 +138,8 @@ fn template_tolerance_settings() -> DefaultTolerance { rt: RtTolerance::None, mobility: MobilityTolerance::Pct((20.0, 20.0)), quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + num_ms1_isotopes: 3, + num_ms2_isotopes: 1, } } diff --git a/src/models/adapters.rs b/src/models/adapters.rs index a4e8cce..0ca164e 100644 --- a/src/models/adapters.rs +++ b/src/models/adapters.rs @@ -1,5 +1,6 @@ use crate::models::elution_group::ElutionGroup; use crate::models::queries::{FragmentGroupIndexQuery, PrecursorIndexQuery}; +use crate::utils::tolerance_ranges::IncludedRange; use crate::ToleranceAdapter; use serde::Serialize; use std::hash::Hash; @@ -27,13 +28,21 @@ impl ) -> FragmentGroupIndexQuery { let rt_range = tol.rt_range(elution_group.rt_seconds); let mobility_range = tol.mobility_range(elution_group.mobility); - let precursor_mz_range = tol.mz_range(elution_group.precursor_mz); + let precursor_mzs = + tol.isotope_mzs_mz(elution_group.precursor_mz, elution_group.precursor_charge); let quad_range = tol.quad_range(elution_group.precursor_mz, elution_group.precursor_charge); + let quad_range = IncludedRange::new(quad_range.0, quad_range.1); - let mz_index_range = ( - self.metadata.mz_converter.invert(precursor_mz_range.0) as u32, - self.metadata.mz_converter.invert(precursor_mz_range.1) as u32, - ); + let mz_index_ranges = precursor_mzs + .iter() + .map(|mz| { + let mz_range = tol.mz_range(*mz); + IncludedRange::new( + self.metadata.mz_converter.invert(mz_range.0) as u32, + self.metadata.mz_converter.invert(mz_range.1) as u32, + ) + }) + .collect(); let mobility_range = match mobility_range { Some(mobility_range) => mobility_range, None => (self.metadata.lower_im as f32, self.metadata.upper_im as f32), @@ -46,16 +55,16 @@ impl Some(rt_range) => rt_range, None => (self.metadata.lower_rt as f32, self.metadata.upper_rt as f32), }; - let frame_index_range = ( - self.metadata.rt_converter.invert(rt_range.0) as usize, - self.metadata.rt_converter.invert(rt_range.1) as usize, + let rt_range = IncludedRange::new(rt_range.0, rt_range.1); + let frame_index_range = IncludedRange::new( + self.metadata.rt_converter.invert(rt_range.start()) as usize, + self.metadata.rt_converter.invert(rt_range.end()) as usize, ); - assert!(frame_index_range.0 <= frame_index_range.1); - assert!(mz_index_range.0 <= mz_index_range.1); + assert!(frame_index_range.start() <= frame_index_range.end()); // Since mobilities get mixed up bc low scan ranges are high 1/k0, I // Just make sure they are sorted here. - let mobility_index_range = ( + let mobility_index_range = IncludedRange::new( mobility_index_range.0.min(mobility_index_range.1), mobility_index_range.1.max(mobility_index_range.0), ); @@ -63,7 +72,7 @@ impl let precursor_query = PrecursorIndexQuery { frame_index_range, rt_range_seconds: rt_range, - mz_index_range, + mz_index_ranges, mobility_index_range, isolation_mz_range: quad_range, }; @@ -75,7 +84,7 @@ impl let mz_range = tol.mz_range(*v); ( *k, - ( + IncludedRange::new( self.metadata.mz_converter.invert(mz_range.0) as u32, self.metadata.mz_converter.invert(mz_range.1) as u32, ), diff --git a/src/models/aggregators/mod.rs b/src/models/aggregators/mod.rs index 6498af9..e34cad4 100644 --- a/src/models/aggregators/mod.rs +++ b/src/models/aggregators/mod.rs @@ -3,7 +3,6 @@ pub mod rolling_calculators; pub mod streaming_aggregator; pub use raw_peak_agg::ChromatomobilogramStats; -pub use raw_peak_agg::ExtractedIonChromatomobilogram; pub use raw_peak_agg::MultiCMGStats; pub use raw_peak_agg::MultiCMGStatsArrays; pub use raw_peak_agg::MultiCMGStatsFactory; diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index 53dd953..aa5d091 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -7,68 +7,6 @@ use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; -#[derive(Debug, Clone, Serialize)] -pub struct ExtractedIonChromatomobilogram { - pub rt_tree: BTreeMap, - pub scan_tree: BTreeMap, - pub tof_tree: BTreeMap, - pub id: u64, -} - -impl ExtractedIonChromatomobilogram { - pub fn new(id: u64) -> Self { - Self { - rt_tree: BTreeMap::new(), - scan_tree: BTreeMap::new(), - tof_tree: BTreeMap::new(), - id, - } - } -} - -impl Aggregator for ExtractedIonChromatomobilogram { - type Item = RawPeak; - type Output = ChromatomobilogramVectorArrayTuples; - - fn add(&mut self, peak: impl Into) { - let peak = peak.into(); - let u64_intensity = peak.intensity as u64; - - // In theory I could use a power of 2 to have a better preservation of - // the precision. - // TODO make this macro ... right now it feels very repetitive. - let rt_miliseconds = (peak.retention_time * 1000.0) as u32; - - self.rt_tree - .entry(rt_miliseconds) - .and_modify(|curr| *curr += u64_intensity) - .or_insert(u64_intensity); - - self.scan_tree - .entry(peak.scan_index) - .and_modify(|curr| *curr += u64_intensity) - .or_insert(u64_intensity); - - self.tof_tree - .entry(peak.tof_index) - .and_modify(|curr| *curr += u64_intensity) - .or_insert(u64_intensity); - } - - fn finalize(self) -> ChromatomobilogramVectorArrayTuples { - ChromatomobilogramVectorArrayTuples { - scan_indices: self.scan_tree.into_iter().collect(), - tof_indices: self.tof_tree.into_iter().collect(), - retention_times: self - .rt_tree - .into_iter() - .map(|(k, v)| ((k as f32) / 1000.0, v)) - .collect(), - } - } -} - -// type MappingCollection = BTreeMap; pub type MappingCollection = HashMap; #[derive(Debug, Clone)] @@ -129,14 +67,7 @@ pub struct ChromatomobilogramStatsArrays { impl ChromatomobilogramStatsArrays { // TODO use default instead of new everywhere .. pub fn new() -> Self { - Self { - retention_time_miliseconds: Vec::new(), - tof_index_means: Vec::new(), - tof_index_sds: Vec::new(), - scan_index_means: Vec::new(), - scan_index_sds: Vec::new(), - intensities: Vec::new(), - } + Self::default() } pub fn fold(&mut self, other: Self) { @@ -167,64 +98,6 @@ impl ChromatomobilogramStatsArrays { } } -impl Aggregator for ChromatomobilogramStats { - type Item = RawPeak; - type Output = ChromatomobilogramStatsArrays; - - fn add(&mut self, peak: impl Into) { - let peak = peak.into(); - let u64_intensity = peak.intensity as u64; - let rt_miliseconds = (peak.retention_time * 1000.0) as u32; - - self.scan_tof_mapping - .entry(rt_miliseconds) - .and_modify(|curr| { - curr.add(u64_intensity, peak.scan_index, peak.tof_index); - }) - .or_insert(ScanTofStatsCalculatorPair::new( - u64_intensity, - peak.scan_index, - peak.tof_index, - )); - } - - fn finalize(self) -> ChromatomobilogramStatsArrays { - type VecTuple = (Vec, Vec); - type OutVecTubples = ((VecTuple, VecTuple), Vec); - let ((scan_data, tof_data), intensities): OutVecTubples = self - .scan_tof_mapping - .values() - .map(|pair| { - ( - ( - ( - pair.scan.mean().unwrap(), - pair.scan.standard_deviation().unwrap(), - ), - ( - pair.tof.mean().unwrap(), - pair.tof.standard_deviation().unwrap(), - ), - ), - pair.tof.weight(), - ) - }) - .unzip(); - - let (scan_means, scan_sds) = scan_data; - let (tof_means, tof_sds) = tof_data; - - ChromatomobilogramStatsArrays { - retention_time_miliseconds: self.scan_tof_mapping.keys().cloned().collect::>(), - tof_index_means: tof_means, - tof_index_sds: tof_sds, - scan_index_means: scan_means, - scan_index_sds: scan_sds, - intensities, - } - } -} - #[derive(Debug, Clone, Serialize)] pub struct ChromatomobilogramVectorArrayTuples { pub scan_indices: Vec<(usize, u64)>, diff --git a/src/models/aggregators/raw_peak_agg/mod.rs b/src/models/aggregators/raw_peak_agg/mod.rs index ab32750..dd5ad20 100644 --- a/src/models/aggregators/raw_peak_agg/mod.rs +++ b/src/models/aggregators/raw_peak_agg/mod.rs @@ -3,7 +3,6 @@ pub mod multi_chromatogram_agg; pub mod point_agg; pub use chromatogram_agg::ChromatomobilogramStats; -pub use chromatogram_agg::ExtractedIonChromatomobilogram; pub use multi_chromatogram_agg::MultiCMGStats; pub use multi_chromatogram_agg::MultiCMGStatsArrays; pub use multi_chromatogram_agg::MultiCMGStatsFactory; diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index 04ca716..51b7850 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -342,12 +342,16 @@ impl From Aggregator for MultiCMGStats { - type Item = (RawPeak, FH); +impl Aggregator + for MultiCMGStats +{ + type Item = RawPeak; + type Context = FH; type Output = NaturalFinalizedMultiCMGStatsArrays; - fn add(&mut self, peak: impl Into<(RawPeak, FH)>) { - let (peak, transition) = peak.into(); + fn add(&mut self, peak: impl Into) { + let peak = peak.into(); + let transition = self.get_context(); let u64_intensity = peak.intensity as u64; let rt_miliseconds = (peak.retention_time * 1000.0) as u32; diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index 1ddbd34..f731c91 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -1,5 +1,5 @@ use crate::models::frames::raw_peak::RawPeak; -use crate::traits::aggregator::Aggregator; +use crate::traits::aggregator::{Aggregator, NoContext, ProvidesContext}; use serde::Serialize; #[derive(Debug, Clone, Copy)] @@ -20,12 +20,13 @@ impl RawPeakIntensityAggregator { } impl Aggregator for RawPeakIntensityAggregator { - type Item = (RawPeak, T); + type Item = RawPeak; + type Context = NoContext; type Output = u64; - fn add(&mut self, peak: impl Into<(RawPeak, T)>) { + fn add(&mut self, peak: impl Into) { let peak = peak.into(); - self.intensity += peak.0.intensity as u64; + self.intensity += peak.intensity as u64; } fn finalize(self) -> u64 { @@ -69,6 +70,7 @@ pub struct RawPeakVectorArrays { impl Aggregator for RawPeakVectorAggregator { type Item = RawPeak; + type Context = NoContext; type Output = RawPeakVectorArrays; fn add(&mut self, peak: impl Into) { diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index c0da9f3..9006607 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -7,6 +7,7 @@ use crate::sort_vecs_by_first; use crate::utils::compress_explode::explode_vec; use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; use crate::utils::sorting::top_n; +use crate::utils::tolerance_ranges::IncludedRange; use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; use rayon::prelude::*; use std::collections::HashMap; @@ -108,26 +109,27 @@ impl ExpandedFrameSlice { impl ExpandedFrameSlice { pub fn query_peaks( &self, - tof_range: (u32, u32), - scan_range: Option<(usize, usize)>, + tof_range: IncludedRange, + scan_range: Option>, f: &mut F, ) where F: FnMut(PeakInQuad), { + // TODO abstract this operation... Range -> Range let peak_range = { - let peak_ind_start = self.tof_indices.partition_point(|x| x < &tof_range.0); - let peak_ind_end = self.tof_indices.partition_point(|x| x <= &tof_range.1); - peak_ind_start..peak_ind_end + let peak_ind_start = self.tof_indices.partition_point(|x| *x < tof_range.start()); + let peak_ind_end = self.tof_indices.partition_point(|x| *x <= tof_range.end()); + peak_ind_start..=peak_ind_end }; - if let Some((min_scan, max_scan)) = scan_range { - assert!(min_scan <= max_scan); - } + // if let Some(scan_range) = scan_range { + // assert!(scan_range.start() <= scan_range.end()); + // } for peak_ind in peak_range { let scan_index = self.scan_numbers[peak_ind]; - if let Some((min_scan, max_scan)) = scan_range { - if scan_index < min_scan || scan_index > max_scan { + if let Some(scan_range) = &scan_range { + if !scan_range.contains(scan_index) { continue; } } @@ -548,7 +550,7 @@ impl ExpandedQuadSliceInfo { .iter() .map(|tof| { let mut local_inten = 0u32; - local_frame.query_peaks((tof - 1, tof + 1), None, &mut |x| { + local_frame.query_peaks(IncludedRange::new(tof - 1, tof + 1), None, &mut |x| { local_inten += x.intensity }); assert!(local_inten > 0); @@ -575,9 +577,11 @@ impl ExpandedQuadSliceInfo { continue; } let mut local_int = 0u32; - lookup_frame.query_peaks((peak.tof - 1, peak.tof + 1), None, &mut |x| { - local_int += x.intensity - }); + lookup_frame.query_peaks( + IncludedRange::new(peak.tof - 1, peak.tof + 1), + None, + &mut |x| local_int += x.intensity, + ); if local_int > (peak.intensity / 100) { peak.max_rt = local_rt; peak.patience = 1; @@ -612,9 +616,11 @@ impl ExpandedQuadSliceInfo { continue; } let mut local_int = 0; - lookup_frame.query_peaks((peak.tof - 1, peak.tof + 1), None, &mut |x| { - local_int += x.intensity - }); + lookup_frame.query_peaks( + IncludedRange::new(peak.tof - 1, peak.tof + 1), + None, + &mut |x| local_int += x.intensity, + ); if local_int > (peak.intensity / 100) { peak.min_rt = local_rt; diff --git a/src/models/frames/raw_frames.rs b/src/models/frames/raw_frames.rs index bb071d6..4cc1f9c 100644 --- a/src/models/frames/raw_frames.rs +++ b/src/models/frames/raw_frames.rs @@ -1,12 +1,13 @@ use timsrust::{Frame, QuadrupoleSettings}; use super::raw_peak::RawPeak; +use crate::utils::tolerance_ranges::IncludedRange; use tracing::trace; pub fn scans_matching_quad( quad_settings: &QuadrupoleSettings, - quad_range: (f64, f64), -) -> Option<(usize, usize)> { + quad_range: IncludedRange, +) -> Option> { let mut min_start = usize::MAX; let mut max_end = 0; @@ -15,7 +16,7 @@ pub fn scans_matching_quad( let quad_start = quad_settings.isolation_mz[i] - half_width; let quad_end = quad_settings.isolation_mz[i] + half_width; - if quad_start <= quad_range.1 && quad_end >= quad_range.0 { + if quad_start <= quad_range.end() && quad_end >= quad_range.start() { let start = quad_settings.scan_starts[i]; let end = quad_settings.scan_ends[i]; min_start = min_start.min(start); @@ -26,15 +27,15 @@ pub fn scans_matching_quad( if min_start == usize::MAX { None } else { - Some((min_start, max_end)) + Some((min_start, max_end).into()) } } pub fn frame_elems_matching( frame: &Frame, - tof_range: (u32, u32), - scan_range: (usize, usize), - quad_range: Option<(f64, f64)>, + tof_range: IncludedRange, + scan_range: IncludedRange, + quad_range: Option>, ) -> impl Iterator + '_ { trace!( "frame_elems_matching tof_range: {:?}, scan_range: {:?}, quad_range: {:?}", @@ -52,11 +53,11 @@ pub fn frame_elems_matching( // Only checkinghere bc its common for them to get flipped // bc skill issues. (and the highest scan is actually the lowest 1/k0) - let min_scan = scan_range.0.min(scan_range.1); - let max_scan = scan_range.0.max(scan_range.1); + let min_scan = scan_range.start(); + let max_scan = scan_range.end(); - let min_quad_scan = quad_scan_range.0.min(quad_scan_range.1); - let max_quad_scan = quad_scan_range.0.max(quad_scan_range.1); + let min_quad_scan = quad_scan_range.start(); + let max_quad_scan = quad_scan_range.end(); let (scan_ind_start, scan_ind_end): (usize, usize) = (min_quad_scan.max(min_scan), max_quad_scan.min(max_scan)); @@ -83,7 +84,7 @@ pub fn frame_elems_matching( (tof_ind, intensity, scan_index) }) - .filter(|(tof_ind, _, _)| *tof_ind >= tof_range.0 && *tof_ind < tof_range.1) + .filter(|(tof_ind, _, _)| *tof_ind >= tof_range.start() && *tof_ind < tof_range.end()) .map(|(tof_ind, intensity, scan_index)| RawPeak { scan_index, tof_index: tof_ind, diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index 2225801..12f3dab 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -3,6 +3,8 @@ use std::hash::Hash; use timsrust::QuadrupoleSettings; +use crate::utils::tolerance_ranges::IncludedRange; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SingleQuadrupoleSettingIndex { pub major_index: usize, @@ -128,8 +130,8 @@ pub fn expand_quad_settings(quad_settings: &QuadrupoleSettings) -> ExpandedFrame pub fn get_matching_quad_settings( flat_quad_settings: &[SingleQuadrupoleSetting], - precursor_mz_range: (f64, f64), - scan_range: Option<(usize, usize)>, + precursor_mz_range: IncludedRange, + scan_range: Option>, ) -> impl Iterator + '_ { flat_quad_settings .iter() @@ -139,16 +141,23 @@ pub fn get_matching_quad_settings( .map(|e| e.index) } -fn matches_iso_window(qs: &SingleQuadrupoleSetting, precursor_mz_range: (f64, f64)) -> bool { - (qs.ranges.isolation_low <= precursor_mz_range.1) - && (qs.ranges.isolation_high >= precursor_mz_range.0) +fn matches_iso_window( + qs: &SingleQuadrupoleSetting, + precursor_mz_range: IncludedRange, +) -> bool { + (qs.ranges.isolation_low <= precursor_mz_range.end()) + && (qs.ranges.isolation_high >= precursor_mz_range.start()) } -fn matches_scan_range(qs: &SingleQuadrupoleSetting, scan_range: Option<(usize, usize)>) -> bool { +fn matches_scan_range( + qs: &SingleQuadrupoleSetting, + scan_range: Option>, +) -> bool { match scan_range { - Some((min_scan, max_scan)) => { + Some(tmp_range) => { + let min_scan = tmp_range.start(); + let max_scan = tmp_range.end(); assert!(qs.ranges.scan_start <= qs.ranges.scan_end); - assert!(min_scan <= max_scan); // Above quad // Quad [----------] @@ -173,8 +182,8 @@ fn matches_scan_range(qs: &SingleQuadrupoleSetting, scan_range: Option<(usize, u pub fn matches_quad_settings( a: &SingleQuadrupoleSetting, - precursor_mz_range: (f64, f64), - scan_range: Option<(usize, usize)>, + precursor_mz_range: IncludedRange, + scan_range: Option>, ) -> bool { matches_iso_window(a, precursor_mz_range) && matches_scan_range(a, scan_range) } diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index f2c5533..c9900ae 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -13,6 +13,7 @@ use crate::models::frames::single_quad_settings::{ use crate::models::queries::FragmentGroupIndexQuery; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; +use crate::utils::tolerance_ranges::IncludedRange; use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; @@ -52,21 +53,21 @@ impl ExpandedSliceBundle { pub fn query_peaks( &self, - tof_range: (u32, u32), - scan_range: Option<(usize, usize)>, - frame_index_range: (usize, usize), + tof_range: IncludedRange, + scan_range: Option>, + frame_index_range: IncludedRange, f: &mut F, ) where F: FnMut(PeakInQuad), { // Binary search the rt if needed. let frame_indices = self.frame_indices.as_slice(); - let low = frame_indices.partition_point(|x| x < &frame_index_range.0); - let high = frame_indices.partition_point(|x| x <= &frame_index_range.1); + let low = frame_indices.partition_point(|x| *x < frame_index_range.start()); + let high = frame_indices.partition_point(|x| *x <= frame_index_range.end()); for i in low..high { let slice = &self.slices[i]; - slice.query_peaks(tof_range, scan_range, f); + slice.query_peaks(tof_range.clone(), scan_range.clone(), f); } } } @@ -74,10 +75,10 @@ impl ExpandedSliceBundle { impl ExpandedRawFrameIndex { pub fn query_peaks( &self, - tof_range: (u32, u32), - precursor_mz_range: (f64, f64), - scan_range: Option<(usize, usize)>, - frame_index_range: (usize, usize), + tof_range: IncludedRange, + precursor_mz_range: IncludedRange, + scan_range: Option>, + frame_index_range: IncludedRange, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -91,9 +92,9 @@ impl ExpandedRawFrameIndex { fn query_precursor_peaks( &self, matching_quads: &[SingleQuadrupoleSettingIndex], - tof_range: (u32, u32), - scan_range: Option<(usize, usize)>, - frame_index_range: (usize, usize), + tof_range: IncludedRange, + scan_range: Option>, + frame_index_range: IncludedRange, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -169,7 +170,7 @@ impl ExpandedRawFrameIndex { } impl - QueriableData, (RawPeak, FH)> for ExpandedRawFrameIndex + QueriableData, RawPeak, FH> for ExpandedRawFrameIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { todo!(); @@ -234,8 +235,8 @@ impl fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], ) where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, { // fragment_queries // .par_iter() @@ -271,9 +272,10 @@ impl .iter() .map(|x| { ( - x.precursor_query.isolation_mz_range.0 as f64, - x.precursor_query.isolation_mz_range.0 as f64, + x.precursor_query.isolation_mz_range.start() as f64, + x.precursor_query.isolation_mz_range.end() as f64, ) + .into() }) .collect::>(); @@ -317,12 +319,13 @@ impl } for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { - let mut local_lambda = |peak| agg.add((RawPeak::from(peak), *fh)); + agg.set_context(*fh); + // let mut local_lambda = |peak| agg.add((RawPeak::from(peak), *fh)); tqi.query_peaks( *tof_range, scan_ranges[i], frame_index_ranges[i], - &mut local_lambda, + &mut |x| agg.add(x), ); } }); diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index 23a6977..a65d729 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -1,8 +1,10 @@ +use crate::models::adapters::FragmentIndexAdapter; use crate::models::frames::raw_frames::frame_elems_matching; use crate::models::frames::raw_peak::RawPeak; use crate::models::queries::{FragmentGroupIndexQuery, NaturalPrecursorQuery, PrecursorIndexQuery}; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; +use crate::utils::tolerance_ranges::IncludedRange; use crate::ElutionGroup; use crate::ToleranceAdapter; use rayon::iter::ParallelIterator; @@ -17,7 +19,7 @@ use tracing::trace; pub struct RawFileIndex { file_reader: FrameReader, - meta_converters: Metadata, + adapter: FragmentIndexAdapter, } impl RawFileIndex { @@ -26,35 +28,13 @@ impl RawFileIndex { let sql_path = std::path::Path::new(path).join("analysis.tdf"); let meta_converters = MetadataReader::new(&sql_path)?; + let adapter = meta_converters.into(); Ok(Self { file_reader, - meta_converters, + adapter, }) } - pub fn convert_precursor_query(&self, query: &NaturalPrecursorQuery) -> PrecursorIndexQuery { - PrecursorIndexQuery { - frame_index_range: ( - self.meta_converters.rt_converter.invert(query.rt_range.0) as usize, - self.meta_converters.rt_converter.invert(query.rt_range.1) as usize, - ), - rt_range_seconds: query.rt_range, - mz_index_range: ( - self.meta_converters.mz_converter.invert(query.mz_range.0) as u32, - self.meta_converters.mz_converter.invert(query.mz_range.1) as u32, - ), - mobility_index_range: ( - self.meta_converters - .im_converter - .invert(query.mobility_range.0) as usize, - self.meta_converters - .im_converter - .invert(query.mobility_range.1) as usize, - ), - isolation_mz_range: query.isolation_mz_range, - } - } - fn apply_on_query< 'c, 'b: 'c, @@ -68,7 +48,7 @@ impl RawFileIndex { trace!("RawFileIndex::apply_on_query"); trace!("FragmentGroupIndexQuery: {:?}", fqs); let frames: Vec = self - .read_frames_in_range(&fqs.precursor_query.frame_index_range) + .read_frames_in_range(&fqs.precursor_query.frame_index_range.into()) .filter(|x| x.is_ok()) .map(|x| x.unwrap()) .collect(); @@ -82,7 +62,7 @@ impl RawFileIndex { &frame, *tof_range, scan_range, - Some((iso_mz_range.0 as f64, iso_mz_range.1 as f64)), + Some((iso_mz_range.start() as f64, iso_mz_range.end() as f64).into()), ); for peak in peaks { fun(peak, *fh); @@ -99,55 +79,6 @@ impl RawFileIndex { self.file_reader.parallel_filter(lambda_use) } - fn query_from_elution_group_impl( - &self, - tol: &dyn crate::traits::tolerance::Tolerance, - elution_group: &crate::models::elution_group::ElutionGroup, - ) -> PrecursorIndexQuery { - let rt_range = tol.rt_range(elution_group.rt_seconds); - let mz_range = tol.mz_range(elution_group.precursor_mz); - let quad_range = tol.quad_range(elution_group.precursor_mz, elution_group.precursor_charge); - - let mobility_range = tol.mobility_range(elution_group.mobility); - let mobility_range = match mobility_range { - Some(mobility_range) => mobility_range, - None => ( - self.meta_converters.lower_im as f32, - self.meta_converters.upper_im as f32, - ), - }; - let mut min_scan_index = - self.meta_converters.im_converter.invert(mobility_range.0) as usize; - let mut max_scan_index = - self.meta_converters.im_converter.invert(mobility_range.1) as usize; - - if min_scan_index > max_scan_index { - std::mem::swap(&mut min_scan_index, &mut max_scan_index); - } - - let rt_range = match rt_range { - Some(rt_range) => rt_range, - None => ( - self.meta_converters.lower_rt as f32, - self.meta_converters.upper_rt as f32, - ), - }; - - PrecursorIndexQuery { - frame_index_range: ( - self.meta_converters.rt_converter.invert(rt_range.0) as usize, - self.meta_converters.rt_converter.invert(rt_range.1) as usize, - ), - rt_range_seconds: rt_range, - mz_index_range: ( - self.meta_converters.mz_converter.invert(mz_range.0) as u32, - self.meta_converters.mz_converter.invert(mz_range.1) as u32, - ), - mobility_index_range: (min_scan_index, max_scan_index), - isolation_mz_range: quad_range, - } - } - fn queries_from_elution_elements_impl< FH: Clone + Eq + Serialize + Hash + Send + Sync + Copy, >( @@ -155,32 +86,7 @@ impl RawFileIndex { tol: &dyn crate::traits::tolerance::Tolerance, elution_elements: &crate::models::elution_group::ElutionGroup, ) -> FragmentGroupIndexQuery { - let precursor_query = self.query_from_elution_group_impl(tol, elution_elements); - // TODO: change this unwrap and use explicitly the lack of fragment mzs. - // Does that mean its onlyt a precursor query? - // Why is it an option? - // - // let fragment_mzs = elution_elements.fragment_mzs.as_ref().unwrap(); - - let fqs = elution_elements - .fragment_mzs - .iter() - .map(|(k, v)| { - let mz_range = tol.mz_range(*v); - ( - *k, - ( - self.meta_converters.mz_converter.invert(mz_range.0) as u32, - self.meta_converters.mz_converter.invert(mz_range.1) as u32, - ), - ) - }) - .collect(); - - FragmentGroupIndexQuery { - mz_index_ranges: fqs, - precursor_query, - } + self.adapter.query_from_elution_group(tol, elution_elements) } } diff --git a/src/models/indices/transposed_quad_index/peak_bucket.rs b/src/models/indices/transposed_quad_index/peak_bucket.rs index d0f0e7d..4e6ae76 100644 --- a/src/models/indices/transposed_quad_index/peak_bucket.rs +++ b/src/models/indices/transposed_quad_index/peak_bucket.rs @@ -1,6 +1,7 @@ use crate::sort_vecs_by_first; use crate::utils::compress_explode::compress_vec; use crate::utils::display::{glimpse_vec, GlimpseConfig}; +use crate::utils::tolerance_ranges::IncludedRange; use std::fmt::Display; pub struct PeakInBucket { @@ -155,8 +156,8 @@ impl PeakBucket { pub fn query_peaks( &self, - scan_range: Option<(usize, usize)>, - rt_range: Option<(f32, f32)>, + scan_range: Option>, + rt_range: Option>, ) -> impl Iterator + '_ { match self.mode { PeakBucketMode::Compressed => self @@ -172,11 +173,11 @@ impl PeakBucket { pub fn query_peaks_sorted( &self, - scan_range: Option<(usize, usize)>, - rt_range: Option<(f32, f32)>, + scan_range: Option>, + rt_range: Option>, ) -> impl Iterator + '_ { let (scan_min, scan_max) = match scan_range { - Some((scan_low, scan_high)) => (scan_low, scan_high), + Some(x) => x.into(), None => ( 0, *(self @@ -204,8 +205,8 @@ impl PeakBucket { } let retention_time = self.retention_times[x]; - if let Some((low, high)) = rt_range { - if retention_time < low || retention_time > high { + if let Some(x) = rt_range { + if !x.contains(retention_time) { return None; } } @@ -219,12 +220,12 @@ impl PeakBucket { pub fn query_peaks_compressed( &self, - scan_range: Option<(usize, usize)>, - rt_range: Option<(f32, f32)>, + scan_range: Option>, + rt_range: Option>, ) -> impl Iterator + '_ { let scan_range = match scan_range { - Some((scan_low, scan_high)) => scan_low..scan_high.min(self.scan_offsets.len() - 1), - None => 0..(self.scan_offsets.len() - 1), + Some(x) => x.start()..=x.end().min(self.scan_offsets.len() - 1), + None => 0..=(self.scan_offsets.len() - 1), }; scan_range .flat_map(move |scan_index| { @@ -234,10 +235,10 @@ impl PeakBucket { (peak_index_start..peak_index_end).map(move |peak_index| { let retention_time = self.retention_times[peak_index]; - if let Some((low, high)) = rt_range { - if retention_time < low || retention_time > high { + if let Some(x) = rt_range { + if !x.contains(retention_time) { return None; - } + }; } let intensity = self.intensities[peak_index]; diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index c5d203e..87b62d9 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -5,6 +5,7 @@ use crate::models::frames::peak_in_quad::PeakInQuad; use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; use crate::sort_vecs_by_first; use crate::utils::display::{glimpse_vec, GlimpseConfig}; +use crate::utils::tolerance_ranges::IncludedRange; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::time::Instant; @@ -72,12 +73,12 @@ impl Display for TransposedQuadIndex { impl TransposedQuadIndex { pub fn query_peaks( &self, - tof_range: (u32, u32), - scan_range: Option<(usize, usize)>, - rt_range: Option<(f32, f32)>, + tof_range: IncludedRange, + scan_range: Option>, + rt_range: Option>, ) -> impl Iterator + '_ { self.peak_buckets - .range(tof_range.0..tof_range.1) + .range(tof_range.start()..=tof_range.end()) .flat_map(move |(tof_index, pb)| { pb.query_peaks(scan_range, rt_range) .map(move |p| PeakInQuad::from_peak_in_bucket(p, *tof_index)) diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 2f003b9..1bba871 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -14,6 +14,7 @@ use crate::models::queries::FragmentGroupIndexQuery; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; +use crate::utils::tolerance_ranges::IncludedRange; use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; @@ -53,10 +54,10 @@ impl Debug for QuadSplittedTransposedIndex { impl QuadSplittedTransposedIndex { pub fn query_peaks( &self, - tof_range: (u32, u32), - precursor_mz_range: (f64, f64), - scan_range: Option<(usize, usize)>, - rt_range_seconds: Option<(f32, f32)>, + tof_range: IncludedRange, + precursor_mz_range: IncludedRange, + scan_range: Option>, + rt_range_seconds: Option>, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -71,9 +72,9 @@ impl QuadSplittedTransposedIndex { fn query_precursor_peaks( &self, matching_quads: &[SingleQuadrupoleSettingIndex], - tof_range: (u32, u32), - scan_range: Option<(usize, usize)>, - rt_range_seconds: Option<(f32, f32)>, + tof_range: IncludedRange, + scan_range: Option>, + rt_range_seconds: Option>, f: &mut F, ) where F: FnMut(PeakInQuad), @@ -90,8 +91,8 @@ impl QuadSplittedTransposedIndex { fn get_matching_quad_settings( &self, - precursor_mz_range: (f64, f64), - scan_range: Option<(usize, usize)>, + precursor_mz_range: IncludedRange, + scan_range: Option>, ) -> impl Iterator + '_ { get_matching_quad_settings(&self.flat_quad_settings, precursor_mz_range, scan_range) } @@ -305,7 +306,7 @@ impl QueriableData, (RawPeak, FH)> for QuadSplittedTransposedIndex { fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { - let precursor_mz_range = ( + let precursor_mz_range = IncludedRange::new( fragment_query.precursor_query.isolation_mz_range.0 as f64, fragment_query.precursor_query.isolation_mz_range.0 as f64, ); @@ -334,7 +335,7 @@ impl A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, AG: Aggregator, { - let precursor_mz_range = ( + let precursor_mz_range = IncludedRange::new( fragment_query.precursor_query.isolation_mz_range.0 as f64, fragment_query.precursor_query.isolation_mz_range.0 as f64, ); @@ -366,12 +367,12 @@ impl .par_iter() .zip(aggregator.par_iter_mut()) .for_each(|(fragment_query, agg)| { - let precursor_mz_range = ( - fragment_query.precursor_query.isolation_mz_range.0 as f64, - fragment_query.precursor_query.isolation_mz_range.1 as f64, + let precursor_mz_range = fragment_query.precursor_query.isolation_mz_range; + let precursor_mz_range = IncludedRange::new( + precursor_mz_range.start() as f64, + precursor_mz_range.end() as f64, ); - assert!(precursor_mz_range.0 <= precursor_mz_range.1); - assert!(precursor_mz_range.0 > 0.0); + assert!(precursor_mz_range.start() > 0.0); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); let local_quad_vec: Vec = self diff --git a/src/models/queries.rs b/src/models/queries.rs index dc3b9ea..1d26ca0 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -1,29 +1,72 @@ +use crate::traits::aggregator::ProvidesContext; +use crate::utils::tolerance_ranges::IncludedRange; use std::collections::HashMap; use std::hash::Hash; +use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; #[derive(Debug, Clone)] pub struct PrecursorIndexQuery { - pub frame_index_range: (usize, usize), - pub rt_range_seconds: (f32, f32), - pub mz_index_range: (u32, u32), - pub mobility_index_range: (usize, usize), - pub isolation_mz_range: (f32, f32), + pub frame_index_range: IncludedRange, + pub rt_range_seconds: IncludedRange, + pub mz_index_ranges: Vec>, + pub mobility_index_range: IncludedRange, + pub isolation_mz_range: IncludedRange, +} + +#[derive(Debug, Clone)] +pub enum MsLevelContext { + MS1(T1), + MS2(T2), } #[derive(Debug, Clone)] pub struct FragmentGroupIndexQuery { - pub mz_index_ranges: HashMap, + pub mz_index_ranges: HashMap>, pub precursor_query: PrecursorIndexQuery, } +impl ProvidesContext for FragmentGroupIndexQuery { + type Context = MsLevelContext; +} + +#[derive(Debug, Clone)] pub struct NaturalPrecursorQuery { - pub rt_range: (f32, f32), - pub mobility_range: (f32, f32), - pub mz_range: (f64, f64), - pub isolation_mz_range: (f32, f32), + pub rt_range: IncludedRange, + pub mobility_range: IncludedRange, + pub mz_ranges: Vec>, + pub isolation_mz_range: IncludedRange, } -pub struct NaturalFragmentQuery<'a> { - pub mz_range: (f64, f64), - pub precursor_query: &'a NaturalPrecursorQuery, +impl NaturalPrecursorQuery { + pub fn as_precursor_query( + &self, + mz_converter: &Tof2MzConverter, + im_converter: &Scan2ImConverter, + ) -> PrecursorIndexQuery { + PrecursorIndexQuery { + frame_index_range: ( + im_converter.invert(self.mobility_range.start()).round() as usize, + im_converter.invert(self.mobility_range.end()).round() as usize, + ) + .into(), + rt_range_seconds: self.rt_range.clone(), + mz_index_ranges: self + .mz_ranges + .iter() + .map(|mz_range| { + ( + mz_converter.invert(mz_range.start()).round() as u32, + mz_converter.invert(mz_range.end()).round() as u32, + ) + .into() + }) + .collect(), + mobility_index_range: ( + im_converter.invert(self.mobility_range.start()).round() as usize, + im_converter.invert(self.mobility_range.end()).round() as usize, + ) + .into(), + isolation_mz_range: self.isolation_mz_range.clone(), + } + } } diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 4252b57..c46d4b1 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -4,6 +4,7 @@ use std::hash::Hash; use std::time::Instant; use tracing::info; +use crate::traits::aggregator::ProvidesContext; use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter}; // TODO: URGENTLY make documentation fot eh functions using this leftover struct docs @@ -42,19 +43,19 @@ use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter // 2. `QD` is queried with `QP` and `QF` queries. // 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). -pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH>( +pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH, CTX>( queriable_data: &'a QD, tolerance: &'a TL, elution_groups: &[ElutionGroup], aggregator_factory: &dyn Fn(u64) -> AG, ) -> Vec where - AG: Aggregator + Send + Sync, - QD: QueriableData + ToleranceAdapter>, + AG: Aggregator + Send + Sync, + QD: QueriableData + ToleranceAdapter>, TL: Tolerance, OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, - QF: Send + Sync, + QF: Send + Sync + ProvidesContext, // AE: Send + Sync + Clone + Copy, AE1: Into + Send + Sync + Clone + Copy, AE2: Send + Sync + Clone + Copy + From, diff --git a/src/traits/aggregator.rs b/src/traits/aggregator.rs index 13af0ae..5d1ed90 100644 --- a/src/traits/aggregator.rs +++ b/src/traits/aggregator.rs @@ -2,15 +2,55 @@ /// /// The `Item` type is the type of the item that is being aggregated. /// The `Output` type is the type of the output of the aggregation. +/// The `Context` type is the type of the context that is being aggregated. /// /// The `add` method takes an item of type `Item` OR a type that /// imlements `Into`. /// +/// The `set_context` method sets the context that is being aggregated. +/// Note that if the aggregator does not support contexts, this method +/// should never be called. (thus the default implementation is empty). +/// +/// The `supports_context` method returns true if the aggregator supports +/// contexts. +/// /// The `finalize` method returns the output of the aggregation. pub trait Aggregator: Send + Sync { type Item: Send + Sync + Clone; type Output: Send + Sync; + type Context: Send + Sync + std::fmt::Debug; fn add(&mut self, item: impl Into); fn finalize(self) -> Self::Output; + fn supports_context(&self) -> bool { + false + } + fn set_context(&mut self, context: Self::Context) { + panic!( + "Misconfigured aggregator, context not supported, got {:?}", + context + ); + } + fn get_context(&self) -> Self::Context { + panic!("Misconfigured aggregator, context not supported"); + } +} + +/// Trait purely for the purpose of semantic meaning. +/// +/// Every thread safe type automatically implements this trait. +/// For the context `NoContext`. +pub trait ProvidesContext { + type Context: Send + Sync; + fn provides_context(&self) -> bool { + false + } +} + +/// Dummy type to denote that no context is provided. +#[derive(Debug, Clone, Copy)] +pub enum NoContext {} + +impl ProvidesContext for T { + type Context = NoContext; } diff --git a/src/traits/queriable_data.rs b/src/traits/queriable_data.rs index 92f96de..cee64b7 100644 --- a/src/traits/queriable_data.rs +++ b/src/traits/queriable_data.rs @@ -1,49 +1,17 @@ -use crate::Aggregator; +use crate::traits::aggregator::{Aggregator, ProvidesContext}; -pub trait QueriableData +pub trait QueriableData where - QF: Send + Sync, + QF: Send + Sync + ProvidesContext, I: Send + Sync + Clone + Copy, { fn query(&self, fragment_query: &QF) -> Vec; fn add_query(&self, fragment_query: &QF, aggregator: &mut AG) where A: From + Send + Sync + Clone + Copy, - AG: Aggregator; + AG: Aggregator; fn add_query_multi_group(&self, fragment_queries: &[QF], aggregator: &mut [AG]) where A: From + Send + Sync + Clone + Copy, - AG: Aggregator; + AG: Aggregator; } - -// I like this idea but I need a way to set/propagate the tolerance -// -// impl>, QF, A, K> QueriableData for T { -// fn query(&self, fragment_query: &QF) -> Vec { -// let mut out = Vec::new(); -// let qf = self.query_from_elution_group(fragment_query); -// self.add_query(&qf, &mut |x| out.push(x)); -// out -// } -// -// fn add_query>( -// &self, -// fragment_query: &QF, -// aggregator: &mut AG, -// ) { -// let qf = self.query_from_elution_group(fragment_query); -// self.add_query(&qf, aggregator); -// } -// -// fn add_query_multi_group>( -// &self, -// fragment_queries: &[QF], -// aggregator: &mut [AG], -// ) { -// let qfs = fragment_queries -// .iter() -// .map(|x| self.query_from_elution_group(x)) -// .collect(); -// self.add_query_multi_group(&qfs, aggregator); -// } -// } diff --git a/src/traits/tolerance.rs b/src/traits/tolerance.rs index 0e4a2f3..a15ba2a 100644 --- a/src/traits/tolerance.rs +++ b/src/traits/tolerance.rs @@ -32,6 +32,8 @@ pub struct DefaultTolerance { pub rt: RtTolerance, pub mobility: MobilityTolerance, pub quad: QuadTolerance, + pub num_ms1_isotopes: usize, + pub num_ms2_isotopes: usize, } impl Default for DefaultTolerance { @@ -41,15 +43,47 @@ impl Default for DefaultTolerance { rt: RtTolerance::Absolute((5.0, 5.0)), mobility: MobilityTolerance::Pct((3.0, 3.0)), quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + num_ms1_isotopes: 3, + num_ms2_isotopes: 1, } } } +fn mass_to_isotope_mzs(mass: f64, charge: u8, num_isotopes: usize) -> Vec { + let mut out = Vec::with_capacity(num_isotopes); + const PROTON_MASS: f64 = 1.007276; + for i in 1..(num_isotopes + 1) { + let mass = mass + i as f64 * PROTON_MASS; + let mz = mass / charge as f64; + out.push(mz); + } + out +} + +fn mz_to_isotope_mzs(mz: f64, charge: u8, num_isotopes: usize) -> Vec { + let mut out = Vec::with_capacity(num_isotopes); + const PROTON_MASS: f64 = 1.007276; + let proton_mass_frac = PROTON_MASS / charge as f64; + for i in 0..num_isotopes { + let mz = mz + (i as f64 * proton_mass_frac); + out.push(mz); + } + out +} + pub trait Tolerance { fn mz_range(&self, mz: f64) -> (f64, f64); fn rt_range(&self, rt: f32) -> Option<(f32, f32)>; fn mobility_range(&self, mobility: f32) -> Option<(f32, f32)>; fn quad_range(&self, precursor_mz: f64, precursor_charge: u8) -> (f32, f32); + fn num_ms1_isotopes(&self) -> usize; + fn isotope_mzs_mass(&self, monoisotopic_mass: f64, charge: u8) -> Vec { + mass_to_isotope_mzs(monoisotopic_mass, charge, self.num_ms1_isotopes()) + } + fn isotope_mzs_mz(&self, mz: f64, charge: u8) -> Vec { + mz_to_isotope_mzs(mz, charge, self.num_ms1_isotopes()) + } + fn num_ms2_isotopes(&self) -> usize; } impl Tolerance for DefaultTolerance { @@ -111,6 +145,14 @@ impl Tolerance for DefaultTolerance { } } } + + fn num_ms1_isotopes(&self) -> usize { + self.num_ms1_isotopes + } + + fn num_ms2_isotopes(&self) -> usize { + self.num_ms2_isotopes + } } /// A trait that can be implemented by types that can convert @@ -121,3 +163,55 @@ impl Tolerance for DefaultTolerance { pub trait ToleranceAdapter { fn query_from_elution_group(&self, tol: &dyn Tolerance, elution_group: &T) -> QF; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_isotope_mzs_neutral() { + let test_vals = vec![ + (100.0, 1, vec![101.0, 102.0, 103.0]), + (100.0, 2, vec![100.5, 101.0, 101.5]), + (100.0, 3, vec![100.33333, 100.666666, 101.0]), + ]; + + for (monoisotopic_mass, charge, expected) in test_vals { + let out = mass_to_isotope_mzs(monoisotopic_mass, charge, 3); + assert_eq!(out.len(), expected.len()); + let abs_diff: Vec = out + .iter() + .zip(expected.iter()) + .map(|(a, b)| (a - b).abs()) + .collect(); + for ad in abs_diff.iter() { + // Very tight tolerances here ... + assert!(*ad < 0.01); + } + } + } + + #[test] + fn test_isotope_mzs_mz() { + let test_vals = vec![ + (100.0, 1, vec![10.0, 101.0, 102.0]), + (100.0, 2, vec![100.0, 100.5, 101.5]), + (100.0, 3, vec![100.0, 100.3333, 101.66666]), + ]; + + for (monoisotopic_mass, charge, expected) in test_vals { + let out = mz_to_isotope_mzs(monoisotopic_mass, charge, 3); + assert_eq!(out.len(), expected.len()); + let abs_diff: Vec = out + .iter() + .zip(expected.iter()) + .map(|(a, b)| (a - b).abs()) + .collect(); + + for ad in abs_diff.iter() { + // Very tight tolerances here ... + assert!(*ad < 0.01); + } + } + } +} diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 71528f9..6a0ecfb 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -1,7 +1,8 @@ use crate::sort_vecs_by_first; -use std::ops::RangeInclusive; use tracing::{error, info, warn}; +use super::tolerance_ranges::IncludedRange; + pub type TofIntensityVecs = (Vec, Vec); pub type CentroidedVecs = (TofIntensityVecs, Vec); @@ -61,8 +62,8 @@ pub fn lazy_centroid_weighted_frame<'a>( peak_refs: &'a [PeakArrayRefs<'a>], reference_index: usize, max_peaks: usize, - tof_tol_range_fn: impl Fn(u32) -> RangeInclusive, - ims_tol_range_fn: impl Fn(usize) -> RangeInclusive, + tof_tol_range_fn: impl Fn(u32) -> IncludedRange, + ims_tol_range_fn: impl Fn(usize) -> IncludedRange, ) -> CentroidedVecs { let slice_sizes: Vec = peak_refs.iter().map(|x| x.len()).collect(); let tot_size: usize = slice_sizes.iter().sum(); @@ -141,13 +142,13 @@ pub fn lazy_centroid_weighted_frame<'a>( for (ii, local_peak_refs) in peak_refs.iter().enumerate() { let ss_start = local_peak_refs .tof_array - .partition_point(|x| x < tof_range.start()); + .partition_point(|x| *x < tof_range.start()); let ss_end = local_peak_refs .tof_array - .partition_point(|x| x <= tof_range.end()); + .partition_point(|x| *x <= tof_range.end()); for i in ss_start..ss_end { let ti = local_offset_touched + i; - if !touched[ti] && ims_range.contains(&local_peak_refs.ims_array[i]) { + if !touched[ti] && ims_range.contains(local_peak_refs.ims_array[i]) { // Peaks are always weighted but not always intense! let local_intensity = local_peak_refs.intensity_array[i] as u64; if ii == reference_index { @@ -170,8 +171,8 @@ pub fn lazy_centroid_weighted_frame<'a>( // let calc_ims = (curr_agg_ims / curr_weight) as usize; let calc_tof = tof; let calc_ims = ims; - debug_assert!(tof_range.contains(&calc_tof)); - debug_assert!(ims_range.contains(&calc_ims)); + debug_assert!(tof_range.contains(calc_tof)); + debug_assert!(ims_range.contains(calc_ims)); agg_tof.push(calc_tof); agg_ims.push(calc_ims); num_added += 1; @@ -226,14 +227,14 @@ mod tests { use super::*; // Helper function to create a simple tolerance range for testing - fn test_tof_tolerance(tof: u32) -> RangeInclusive { + fn test_tof_tolerance(tof: u32) -> IncludedRange { let tolerance = 2; - (tof.saturating_sub(tolerance))..=tof.saturating_add(tolerance) + (tof.saturating_sub(tolerance), tof.saturating_add(tolerance)).into() } - fn test_ims_tolerance(ims: usize) -> RangeInclusive { + fn test_ims_tolerance(ims: usize) -> IncludedRange { let tolerance = 1; - (ims.saturating_sub(tolerance))..=ims.saturating_add(tolerance) + (ims.saturating_sub(tolerance), ims.saturating_add(tolerance)).into() } // Helper function to create PeakArrayRefs diff --git a/src/utils/tolerance_ranges.rs b/src/utils/tolerance_ranges.rs index faa3d4d..4bde46d 100644 --- a/src/utils/tolerance_ranges.rs +++ b/src/utils/tolerance_ranges.rs @@ -1,27 +1,68 @@ use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; -use std::ops::RangeInclusive; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct IncludedRange(pub T, pub T); -pub fn ppm_tol_range(elem: f64, tol_ppm: f64) -> RangeInclusive { +// TODO: Implement overlaps ... + +impl IncludedRange +where + T: Copy + PartialOrd, +{ + pub fn new(left: T, right: T) -> Self { + // Swao if left > right + if left > right { + Self(right, left) + } else { + Self(left, right) + } + } + + pub fn contains(&self, x: T) -> bool { + self.0 <= x && x <= self.1 + } + + pub fn start(&self) -> T { + self.0 + } + + pub fn end(&self) -> T { + self.1 + } +} + +impl From<(T, T)> for IncludedRange { + fn from(x: (T, T)) -> Self { + Self::new(x.0, x.1) + } +} + +impl Into<(T, T)> for IncludedRange { + fn into(self) -> (T, T) { + (self.0, self.1) + } +} + +pub fn ppm_tol_range(elem: f64, tol_ppm: f64) -> IncludedRange { let utol = elem * (tol_ppm / 1e6); let left_e = elem - utol; let right_e = elem + utol; - left_e..=right_e + (left_e, right_e).into() } -pub fn pct_tol_range(elem: f64, tol_pct: f64) -> RangeInclusive { +pub fn pct_tol_range(elem: f64, tol_pct: f64) -> IncludedRange { let utol = elem * (tol_pct / 100.0); let left_e = elem - utol; let right_e = elem + utol; - left_e..=right_e + (left_e, right_e).into() } -pub fn tof_tol_range(tof: u32, tol_ppm: f64, converter: &Tof2MzConverter) -> RangeInclusive { +pub fn tof_tol_range(tof: u32, tol_ppm: f64, converter: &Tof2MzConverter) -> IncludedRange { let mz = converter.convert(tof); let mz_range = ppm_tol_range(mz, tol_ppm); - RangeInclusive::new( - converter.invert(*mz_range.start()).round() as u32, - converter.invert(*mz_range.end()).round() as u32, + IncludedRange::new( + converter.invert(mz_range.start()).round() as u32, + converter.invert(mz_range.end()).round() as u32, ) } @@ -29,17 +70,17 @@ pub fn scan_tol_range( scan: usize, tol_pct: f64, converter: &Scan2ImConverter, -) -> RangeInclusive { +) -> IncludedRange { let im = converter.convert(scan as f64); let im_range = pct_tol_range(im, tol_pct); - let scan_min = converter.invert(*im_range.start()).round() as usize; - let scan_max = converter.invert(*im_range.end()).round() as usize; + let scan_min = converter.invert(im_range.start()).round() as usize; + let scan_max = converter.invert(im_range.end()).round() as usize; // Note I need to do this here bc the conversion between scan numbers and ion // mobilities is not monotonically increasing. IN OTHER WORDS, lower scan numbers // are higher 1/k0.... But im not sure if they are ALWAYS inversely proportional. if scan_min > scan_max { - scan_max..=scan_min + (scan_max, scan_min).into() } else { - scan_min..=scan_max + (scan_min, scan_max).into() } } From 68855a633e7ce2eece972f2901e3eb7fb9c5aa0a Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Thu, 31 Oct 2024 18:34:31 -0700 Subject: [PATCH 16/30] feat(wip,trait)!: aggregator context propagation --- Cargo.lock | 2 +- Cargo.toml | 2 +- benches/benchmark_indices.rs | 4 + src/main.rs | 21 +- .../raw_peak_agg/chromatogram_agg.rs | 2 + .../raw_peak_agg/multi_chromatogram_agg.rs | 225 +++++++++++------- .../aggregators/raw_peak_agg/point_agg.rs | 21 +- .../aggregators/streaming_aggregator.rs | 18 ++ src/models/frames/expanded_frame.rs | 6 +- .../indices/expanded_raw_index/model.rs | 120 +++++----- src/models/indices/raw_file_index.rs | 36 +-- .../quad_splitted_transposed_index.rs | 106 ++++++--- src/models/queries.rs | 49 +++- .../queriable_tims_data.rs | 12 +- src/traits/aggregator.rs | 13 +- src/traits/queriable_data.rs | 19 +- src/traits/tolerance.rs | 1 + 17 files changed, 402 insertions(+), 255 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 473a7b2..43c371c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "timsquery" -version = "0.5.0" +version = "0.6.0" dependencies = [ "clap", "indicatif", diff --git a/Cargo.toml b/Cargo.toml index fa3b214..dfd31d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "timsquery" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "Apache-2.0" diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 3afc9e5..006c96e 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -326,12 +326,16 @@ fn run_batch_access_benchmark(raw_file_path: &Path, env_config: EnvConfig) -> Ve rt: RtTolerance::Absolute((5.0, 5.0)), mobility: MobilityTolerance::Pct((3.0, 3.0)), quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + num_ms1_isotopes: 3, + num_ms2_isotopes: 1, }; let tolerance_with_nort = DefaultTolerance { ms: MzToleramce::Ppm((20.0, 20.0)), rt: RtTolerance::None, mobility: MobilityTolerance::Pct((3.0, 3.0)), quad: QuadTolerance::Absolute((0.1, 0.1, 1)), + num_ms1_isotopes: 3, + num_ms2_isotopes: 1, }; let tolerances = [ (tolerance_with_rt, "narrow_rt"), diff --git a/src/main.rs b/src/main.rs index 203d25b..9fc15d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,7 @@ use timsquery::traits::tolerance::DefaultTolerance; use timsquery::traits::tolerance::{MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance}; use timsquery::{ models::aggregators::{ - ChromatomobilogramStats, ExtractedIonChromatomobilogram, MultiCMGStatsFactory, - RawPeakIntensityAggregator, RawPeakVectorAggregator, + MultiCMGStatsFactory, RawPeakIntensityAggregator, RawPeakVectorAggregator, }, models::indices::{ExpandedRawFrameIndex, QuadSplittedTransposedIndex}, }; @@ -155,8 +154,6 @@ pub enum PossibleAggregator { #[default] RawPeakIntensityAggregator, RawPeakVectorAggregator, - ExtractedIonChromatomobilogram, - ChromatoMobilogramStat, MultiCMGStats, } @@ -301,14 +298,6 @@ pub fn execute_query( let aggregator = RawPeakVectorAggregator::new; execute_query_inner!(index, aggregator); } - PossibleAggregator::ChromatoMobilogramStat => { - let aggregator = ChromatomobilogramStats::new; - execute_query_inner!(index, aggregator); - } - PossibleAggregator::ExtractedIonChromatomobilogram => { - let aggregator = ExtractedIonChromatomobilogram::new; - execute_query_inner!(index, aggregator); - } PossibleAggregator::MultiCMGStats => { let factory = MultiCMGStatsFactory { converters: (index.mz_converter, index.im_converter), @@ -329,14 +318,6 @@ pub fn execute_query( let aggregator = RawPeakVectorAggregator::new; execute_query_inner!(index, aggregator); } - PossibleAggregator::ChromatoMobilogramStat => { - let aggregator = ChromatomobilogramStats::new; - execute_query_inner!(index, aggregator); - } - PossibleAggregator::ExtractedIonChromatomobilogram => { - let aggregator = ExtractedIonChromatomobilogram::new; - execute_query_inner!(index, aggregator); - } PossibleAggregator::MultiCMGStats => { let factory = MultiCMGStatsFactory { converters: (index.mz_converter, index.im_converter), diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index aa5d091..6eed3d5 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -9,6 +9,8 @@ use std::collections::HashMap; pub type MappingCollection = HashMap; +/// A struct that can be used to calculate the mean and variance +/// of a stream of weighted tof and scan numbers. #[derive(Debug, Clone)] pub struct ScanTofStatsCalculatorPair { pub scan: RunningStatsCalculator, diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs index 51b7850..62df922 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs @@ -4,6 +4,7 @@ use super::chromatogram_agg::{ use crate::models::aggregators::rolling_calculators::rolling_median; use crate::models::aggregators::streaming_aggregator::RunningStatsCalculator; use crate::models::frames::raw_peak::RawPeak; +use crate::models::queries::MsLevelContext; use crate::traits::aggregator::Aggregator; use crate::utils::math::{lnfact, lnfact_float}; use serde::Serialize; @@ -14,12 +15,62 @@ use tracing::{debug, warn}; use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; #[derive(Debug, Clone)] -pub struct MultiCMGStats { +struct _MultiCMGStats { pub scan_tof_mapping: MappingCollection<(FH, u32), ScanTofStatsCalculatorPair>, - pub converters: (Tof2MzConverter, Scan2ImConverter), pub uniq_rts: HashSet, pub uniq_ids: HashSet, +} + +impl Default for _MultiCMGStats { + fn default() -> Self { + Self { + scan_tof_mapping: MappingCollection::new(), + uniq_rts: HashSet::new(), + uniq_ids: HashSet::new(), + } + } +} + +impl _MultiCMGStats { + fn finalize(self) -> MultiCMGStatsArrays { + let mut transition_stats = MappingCollection::new(); + + for id_key in self.uniq_ids.iter() { + let mut id_cmgs = ChromatomobilogramStatsArrays::new(); + for rt_key in self.uniq_rts.iter() { + let scan_tof_mapping = self.scan_tof_mapping.get(&(id_key.clone(), *rt_key)); + if let Some(scan_tof_mapping) = scan_tof_mapping { + id_cmgs.retention_time_miliseconds.push(*rt_key); + id_cmgs + .scan_index_means + .push(scan_tof_mapping.scan.mean().unwrap()); + id_cmgs + .scan_index_sds + .push(scan_tof_mapping.scan.standard_deviation().unwrap()); + id_cmgs + .tof_index_means + .push(scan_tof_mapping.tof.mean().unwrap()); + id_cmgs + .tof_index_sds + .push(scan_tof_mapping.tof.standard_deviation().unwrap()); + id_cmgs.intensities.push(scan_tof_mapping.tof.weight()); + } + } + id_cmgs.sort_by_rt(); + transition_stats.insert(id_key.clone(), id_cmgs); + } + + MultiCMGStatsArrays { transition_stats } + } +} + +#[derive(Debug, Clone)] +pub struct MultiCMGStats { + pub converters: (Tof2MzConverter, Scan2ImConverter), + pub ms1_stats: _MultiCMGStats, + pub ms2_stats: _MultiCMGStats, pub id: u64, + pub context: Option>, } #[derive(Debug, Clone)] @@ -31,11 +82,11 @@ pub struct MultiCMGStatsFactory impl MultiCMGStatsFactory { pub fn build(&self, id: u64) -> MultiCMGStats { MultiCMGStats { - scan_tof_mapping: MappingCollection::new(), converters: (self.converters.0, self.converters.1), - uniq_rts: HashSet::new(), - uniq_ids: HashSet::new(), + ms1_stats: Default::default(), + ms2_stats: Default::default(), id, + context: None, } } } @@ -43,11 +94,10 @@ impl MultiCMGStatsFactory { #[derive(Debug, Clone, Serialize)] pub struct MultiCMGStatsArrays { pub transition_stats: MappingCollection, - id: u64, } #[derive(Debug, Clone, Serialize)] -pub struct FinalizedMultiCMGStatsArrays { +pub struct _FinalizedMultiCMGStatsArrays { pub retention_time_miliseconds: Vec, pub weighted_scan_index_mean: Vec, pub summed_intensity: Vec, @@ -60,12 +110,18 @@ pub struct FinalizedMultiCMGStatsArrays>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct FinalizedMultiCMGStatsArrays { + pub ms1_stats: _FinalizedMultiCMGStatsArrays, + pub ms2_stats: _FinalizedMultiCMGStatsArrays, pub id: u64, } // This name is starting to get really long ... #[derive(Debug, Clone, Serialize)] -pub struct NaturalFinalizedMultiCMGStatsArrays { +pub struct _NaturalFinalizedMultiCMGStatsArrays { pub retention_time_miliseconds: Vec, pub average_mobility: Vec, pub summed_intensity: Vec, @@ -80,27 +136,14 @@ pub struct NaturalFinalizedMultiCMGStatsArrays>, pub transition_intensities: MappingCollection>, pub apex_primary_score_index: usize, - pub id: u64, } -// Reference python code - -// log1p_intensitties = np.log1p(arrays["summed_intensity"]) -// lazy_hyperscore = LN_FACTORIALS[arrays["npeaks"]] + (2 * log1p_intensitties) -// -// five_pct_len = int(len(arrays["retention_time_miliseconds"]) / 100) * 5 -// lazy_hyperscore_roll_median = ( -// pd.Series(lazy_hyperscore).rolling(window=five_pct_len, center=True).median() -// ).array -// hyperscore_off_baseline = lazy_hyperscore - lazy_hyperscore_roll_median -// hyperscore_off_baseline = np.nan_to_num(hyperscore_off_baseline, 0) -// iqr_hyperscore_off_baseline = np.percentile( -// hyperscore_off_baseline, 75 -// ) - np.percentile(hyperscore_off_baseline, 25) -// -// scaled_hyperscore_off_baseline = hyperscore_off_baseline / iqr_hyperscore_off_baseline -// ) -// ) +#[derive(Debug, Clone, Serialize)] +pub struct NaturalFinalizedMultiCMGStatsArrays { + pub ms1_stats: _NaturalFinalizedMultiCMGStatsArrays, + pub ms2_stats: _NaturalFinalizedMultiCMGStatsArrays, + pub id: u64, +} fn calculate_lazy_hyperscore(npeaks: &[usize], summed_intensity: &[u64]) -> Vec { let mut scores = vec![0.0; npeaks.len()]; @@ -121,9 +164,9 @@ fn calculate_value_vs_baseline(vals: &[f64], baseline_window_size: usize) -> Vec .collect() } -impl NaturalFinalizedMultiCMGStatsArrays { +impl _NaturalFinalizedMultiCMGStatsArrays { pub fn new( - other: FinalizedMultiCMGStatsArrays, + other: _FinalizedMultiCMGStatsArrays, mz_converter: &Tof2MzConverter, mobility_converter: &Scan2ImConverter, ) -> Self { @@ -185,7 +228,7 @@ impl NaturalFinalizedMultiCMGSt "Failed sanity check" ); - NaturalFinalizedMultiCMGStatsArrays { + _NaturalFinalizedMultiCMGStatsArrays { retention_time_miliseconds: other.retention_time_miliseconds, average_mobility: other .weighted_scan_index_mean @@ -227,18 +270,17 @@ impl NaturalFinalizedMultiCMGSt lazyerscore_vs_baseline, norm_hyperscore_vs_baseline, norm_lazyerscore_vs_baseline, - id: other.id, } } } impl From> - for FinalizedMultiCMGStatsArrays + for _FinalizedMultiCMGStatsArrays { fn from(other: MultiCMGStatsArrays) -> Self { // TODO ... maybe refactor this ... RN its king of ugly. - let mut out = FinalizedMultiCMGStatsArrays { + let mut out = _FinalizedMultiCMGStatsArrays { retention_time_miliseconds: Vec::new(), scan_index_means: MappingCollection::new(), tof_index_means: MappingCollection::new(), @@ -247,7 +289,6 @@ impl From Aggregat for MultiCMGStats { type Item = RawPeak; - type Context = FH; + type Context = MsLevelContext; type Output = NaturalFinalizedMultiCMGStatsArrays; fn add(&mut self, peak: impl Into) { let peak = peak.into(); - let transition = self.get_context(); let u64_intensity = peak.intensity as u64; let rt_miliseconds = (peak.retention_time * 1000.0) as u32; - self.uniq_rts.insert(rt_miliseconds); - self.uniq_ids.insert(transition.clone()); + match self.get_context() { + MsLevelContext::MS1(i) => { + self.ms1_stats.uniq_rts.insert(rt_miliseconds); + self.ms1_stats + .scan_tof_mapping + .entry((i, rt_miliseconds)) + .and_modify(|curr| { + curr.add(u64_intensity, peak.scan_index, peak.tof_index); + }) + .or_insert(ScanTofStatsCalculatorPair::new( + u64_intensity, + peak.scan_index, + peak.tof_index, + )); + } + MsLevelContext::MS2(i) => { + self.ms2_stats.uniq_rts.insert(rt_miliseconds); + self.ms2_stats + .scan_tof_mapping + .entry((i, rt_miliseconds)) + .and_modify(|curr| { + curr.add(u64_intensity, peak.scan_index, peak.tof_index); + }) + .or_insert(ScanTofStatsCalculatorPair::new( + u64_intensity, + peak.scan_index, + peak.tof_index, + )); + } + } + } - self.scan_tof_mapping - .entry((transition.clone(), rt_miliseconds)) - .and_modify(|curr| { - curr.add(u64_intensity, peak.scan_index, peak.tof_index); - }) - .or_insert(ScanTofStatsCalculatorPair::new( - u64_intensity, - peak.scan_index, - peak.tof_index, - )); + fn set_context(&mut self, context: Self::Context) { + match &context { + MsLevelContext::MS1(i) => { + self.ms1_stats.uniq_ids.insert(*i); + } + MsLevelContext::MS2(i) => { + self.ms2_stats.uniq_ids.insert(i.clone()); + } + } + self.context = Some(context); } - fn finalize(self) -> NaturalFinalizedMultiCMGStatsArrays { - let mut transition_stats = MappingCollection::new(); + fn supports_context(&self) -> bool { + true + } - for id_key in self.uniq_ids.iter() { - let mut id_cmgs = ChromatomobilogramStatsArrays::new(); - for rt_key in self.uniq_rts.iter() { - let scan_tof_mapping = self.scan_tof_mapping.get(&(id_key.clone(), *rt_key)); - if let Some(scan_tof_mapping) = scan_tof_mapping { - id_cmgs.retention_time_miliseconds.push(*rt_key); - id_cmgs - .scan_index_means - .push(scan_tof_mapping.scan.mean().unwrap()); - id_cmgs - .scan_index_sds - .push(scan_tof_mapping.scan.standard_deviation().unwrap()); - id_cmgs - .tof_index_means - .push(scan_tof_mapping.tof.mean().unwrap()); - id_cmgs - .tof_index_sds - .push(scan_tof_mapping.tof.standard_deviation().unwrap()); - id_cmgs.intensities.push(scan_tof_mapping.tof.weight()); - } - } - id_cmgs.sort_by_rt(); - transition_stats.insert(id_key.clone(), id_cmgs); + fn get_context(&self) -> Self::Context { + match &self.context { + Some(context) => context.clone(), + None => panic!("No context set"), } + } + + fn finalize(self) -> NaturalFinalizedMultiCMGStatsArrays { + let mz_converter = &self.converters.0; + let mobility_converter = &self.converters.1; - let tmp = MultiCMGStatsArrays { - transition_stats, + let ms1_stats = _NaturalFinalizedMultiCMGStatsArrays::new( + _FinalizedMultiCMGStatsArrays::from(self.ms1_stats.finalize()), + mz_converter, + mobility_converter, + ); + let ms2_stats = _NaturalFinalizedMultiCMGStatsArrays::new( + _FinalizedMultiCMGStatsArrays::from(self.ms2_stats.finalize()), + mz_converter, + mobility_converter, + ); + + NaturalFinalizedMultiCMGStatsArrays { + ms2_stats, + ms1_stats, id: self.id, - }; - NaturalFinalizedMultiCMGStatsArrays::new( - FinalizedMultiCMGStatsArrays::from(tmp), - &self.converters.0, - &self.converters.1, - ) + } } } diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index f731c91..1fbfdcc 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -3,23 +3,18 @@ use crate::traits::aggregator::{Aggregator, NoContext, ProvidesContext}; use serde::Serialize; #[derive(Debug, Clone, Copy)] -pub struct RawPeakIntensityAggregator { +pub struct RawPeakIntensityAggregator { pub id: u64, pub intensity: u64, - _phantom: std::marker::PhantomData, } -impl RawPeakIntensityAggregator { +impl RawPeakIntensityAggregator { pub fn new(id: u64) -> Self { - Self { - id, - intensity: 0, - _phantom: std::marker::PhantomData, - } + Self { id, intensity: 0 } } } -impl Aggregator for RawPeakIntensityAggregator { +impl Aggregator for RawPeakIntensityAggregator { type Item = RawPeak; type Context = NoContext; type Output = u64; @@ -32,6 +27,10 @@ impl Aggregator for RawPeakIntensityAggregator { fn finalize(self) -> u64 { self.intensity } + + fn supports_context(&self) -> bool { + false + } } #[derive(Debug, Clone)] @@ -84,4 +83,8 @@ impl Aggregator for RawPeakVectorAggregator { fn finalize(self) -> RawPeakVectorArrays { self.peaks } + + fn supports_context(&self) -> bool { + false + } } diff --git a/src/models/aggregators/streaming_aggregator.rs b/src/models/aggregators/streaming_aggregator.rs index d015879..c85456e 100644 --- a/src/models/aggregators/streaming_aggregator.rs +++ b/src/models/aggregators/streaming_aggregator.rs @@ -12,6 +12,24 @@ pub enum StreamingAggregatorError { type Result = std::result::Result; +/// A struct that can be used to calculate the mean and variance of a stream of numbers. +/// +/// # Example +/// +/// ``` +/// use timsquery::models::aggregators::streaming_aggregator::RunningStatsCalculator; +/// +/// // Create a new calculator with a weight of 10 and a mean of 0.0 +/// let mut calc = RunningStatsCalculator::new(2, 0.0); +/// calc.add(10.0, 2); +/// // So overall this should be the equivalent of the mean for +/// // [0.0, 0.0, 10.0, 10.0] +/// assert_eq!(calc.mean().unwrap(), 5.0); +/// +/// ``` +/// +/// # Notes +/// /// Ref impl in javascript ... /// https://nestedsoftware.com/2018/03/27/calculating-standard-deviation-on-streaming-data-253l.23919.html /// https://nestedsoftware.com/2019/09/26/incremental-average-and-standard-deviation-with-sliding-window-470k.176143.html diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 9006607..ff8d0b0 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -119,13 +119,9 @@ impl ExpandedFrameSlice { let peak_range = { let peak_ind_start = self.tof_indices.partition_point(|x| *x < tof_range.start()); let peak_ind_end = self.tof_indices.partition_point(|x| *x <= tof_range.end()); - peak_ind_start..=peak_ind_end + peak_ind_start..peak_ind_end }; - // if let Some(scan_range) = scan_range { - // assert!(scan_range.start() <= scan_range.end()); - // } - for peak_ind in peak_range { let scan_index = self.scan_numbers[peak_ind]; if let Some(scan_range) = &scan_range { diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index c9900ae..ff08ed6 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -10,7 +10,7 @@ use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, matches_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; -use crate::models::queries::FragmentGroupIndexQuery; +use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::utils::tolerance_ranges::IncludedRange; @@ -67,7 +67,7 @@ impl ExpandedSliceBundle { for i in low..high { let slice = &self.slices[i]; - slice.query_peaks(tof_range.clone(), scan_range.clone(), f); + slice.query_peaks(tof_range, scan_range, f); } } } @@ -89,6 +89,19 @@ impl ExpandedRawFrameIndex { self.query_precursor_peaks(&matching_quads, tof_range, scan_range, frame_index_range, f); } + fn query_ms1_peaks( + &self, + tof_range: IncludedRange, + scan_range: Option>, + frame_index_range: IncludedRange, + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + self.bundled_ms1_frames + .query_peaks(tof_range, scan_range, frame_index_range, f); + } + fn query_precursor_peaks( &self, matching_quads: &[SingleQuadrupoleSettingIndex], @@ -170,9 +183,10 @@ impl ExpandedRawFrameIndex { } impl - QueriableData, RawPeak, FH> for ExpandedRawFrameIndex + QueriableData, RawPeak, MsLevelContext> + for ExpandedRawFrameIndex { - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { todo!(); // let precursor_mz_range = ( // fragment_query.precursor_query.isolation_mz_range.0 as f64, @@ -201,10 +215,14 @@ impl // .collect() } - fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) - where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + fn add_query( + &self, + fragment_query: &FragmentGroupIndexQuery, + aggregator: &mut AG, + ) where + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { todo!(); // let precursor_mz_range = ( @@ -230,44 +248,15 @@ impl // }) } - fn add_query_multi_group( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], ) where - A: From + Send + Sync + Clone + Copy, - AG: Aggregator, + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { - // fragment_queries - // .par_iter() - // .zip(aggregator.par_iter_mut()) - // .for_each(|(fragment_query, agg)| { - // let precursor_mz_range = ( - // fragment_query.precursor_query.isolation_mz_range.0 as f64, - // fragment_query.precursor_query.isolation_mz_range.1 as f64, - // ); - // assert!(precursor_mz_range.0 <= precursor_mz_range.1); - // assert!(precursor_mz_range.0 > 0.0); - // let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - // let frame_index_range = fragment_query.precursor_query.frame_index_range; - - // let local_quad_vec: Vec = get_matching_quad_settings( - // &self.flat_quad_settings, - // precursor_mz_range, - // scan_range, - // ) - // .collect(); - - // for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - // self.query_precursor_peaks( - // &local_quad_vec, - // tof_range, - // scan_range, - // frame_index_range, - // &mut |peak| agg.add(&(RawPeak::from(peak), fh)), - // ); - // } - // }); let prec_mz_ranges = fragment_queries .iter() .map(|x| { @@ -289,6 +278,23 @@ impl .map(|x| x.precursor_query.frame_index_range) .collect::>(); + // Query the ms1 mzs first. + aggregator.iter_mut().enumerate().for_each(|(i, agg)| { + fragment_queries[i] + .iter_ms1_mzs() + .for_each(|(fh, mz_range)| { + if agg.supports_context() { + agg.set_context(fh.into()); + } + self.query_ms1_peaks( + mz_range, + scan_ranges[i], + frame_index_ranges[i], + &mut |x| agg.add(RawPeak::from(x)), + ); + }); + }); + for quad_setting in self.flat_quad_settings.iter() { let local_index = quad_setting.index; @@ -297,36 +303,18 @@ impl .get(&local_index) .expect("Only existing quads should be queried."); - // for i in 0..prec_mz_ranges.len() { - // if !matches_quad_settings(quad_setting, prec_mz_ranges[i], scan_ranges[i]) { - // continue; - // } - - // for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { - // let mut local_lambda = |peak| aggregator[i].add(&(RawPeak::from(peak), *fh)); - // tqi.query_peaks( - // *tof_range, - // scan_ranges[i], - // frame_index_ranges[i], - // &mut local_lambda, - // ) - // } - // } - aggregator.par_iter_mut().enumerate().for_each(|(i, agg)| { if !matches_quad_settings(quad_setting, prec_mz_ranges[i], scan_ranges[i]) { return; } - for (fh, tof_range) in fragment_queries[i].mz_index_ranges.iter() { - agg.set_context(*fh); - // let mut local_lambda = |peak| agg.add((RawPeak::from(peak), *fh)); - tqi.query_peaks( - *tof_range, - scan_ranges[i], - frame_index_ranges[i], - &mut |x| agg.add(x), - ); + for (fh, tof_range) in fragment_queries[i].iter_ms2_mzs() { + if agg.supports_context() { + agg.set_context(fh.into()); + } + tqi.query_peaks(tof_range, scan_ranges[i], frame_index_ranges[i], &mut |x| { + agg.add(RawPeak::from(x)) + }); } }); } diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index a65d729..56f7b82 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -1,7 +1,7 @@ use crate::models::adapters::FragmentIndexAdapter; use crate::models::frames::raw_frames::frame_elems_matching; use crate::models::frames::raw_peak::RawPeak; -use crate::models::queries::{FragmentGroupIndexQuery, NaturalPrecursorQuery, PrecursorIndexQuery}; +use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::utils::tolerance_ranges::IncludedRange; @@ -43,7 +43,7 @@ impl RawFileIndex { >( &'a self, fqs: &'b FragmentGroupIndexQuery, - fun: &'c mut dyn for<'r> FnMut(RawPeak, FH), + fun: &'c mut dyn for<'r> FnMut(RawPeak), ) { trace!("RawFileIndex::apply_on_query"); trace!("FragmentGroupIndexQuery: {:?}", fqs); @@ -56,7 +56,7 @@ impl RawFileIndex { let pq = &fqs.precursor_query; let iso_mz_range = pq.isolation_mz_range; for frame in frames { - for (fh, tof_range) in fqs.mz_index_ranges.iter() { + for (_fh, tof_range) in fqs.mz_index_ranges.iter() { let scan_range = pq.mobility_index_range; let peaks = frame_elems_matching( &frame, @@ -65,7 +65,7 @@ impl RawFileIndex { Some((iso_mz_range.start() as f64, iso_mz_range.end() as f64).into()), ); for peak in peaks { - fun(peak, *fh); + fun(peak); } } } @@ -91,29 +91,35 @@ impl RawFileIndex { } impl - QueriableData, (RawPeak, FH)> for RawFileIndex + QueriableData, RawPeak, MsLevelContext> + for RawFileIndex { - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { let mut out = Vec::new(); - self.apply_on_query(fragment_query, &mut |x, y| out.push((x, y))); + self.apply_on_query(fragment_query, &mut |x| out.push(x)); out } - fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) - where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + fn add_query( + &self, + fragment_query: &FragmentGroupIndexQuery, + aggregator: &mut AG, + ) where + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { - self.apply_on_query(fragment_query, &mut |x, y| aggregator.add((x, y))); + self.apply_on_query(fragment_query, &mut |x| aggregator.add(x)); } - fn add_query_multi_group( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], ) where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { fragment_queries .iter() diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 1bba871..91025ab 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -10,7 +10,7 @@ use crate::models::frames::raw_peak::RawPeak; use crate::models::frames::single_quad_settings::{ get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, }; -use crate::models::queries::FragmentGroupIndexQuery; +use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; use crate::traits::aggregator::Aggregator; use crate::traits::queriable_data::QueriableData; use crate::utils::display::{glimpse_vec, GlimpseConfig}; @@ -52,7 +52,7 @@ impl Debug for QuadSplittedTransposedIndex { } impl QuadSplittedTransposedIndex { - pub fn query_peaks( + pub fn query_ms2_peaks( &self, tof_range: IncludedRange, precursor_mz_range: IncludedRange, @@ -65,11 +65,24 @@ impl QuadSplittedTransposedIndex { let matching_quads: Vec = self .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); - trace!("matching_quads: {:?}", matching_quads); - self.query_precursor_peaks(&matching_quads, tof_range, scan_range, rt_range_seconds, f); + self.query_peaks_in_precursors(&matching_quads, tof_range, scan_range, rt_range_seconds, f); } - fn query_precursor_peaks( + fn query_ms1_peaks( + &self, + tof_range: IncludedRange, + scan_range: Option>, + rt_range_seconds: Option>, + f: &mut F, + ) where + F: FnMut(PeakInQuad), + { + self.precursor_index + .query_peaks(tof_range, scan_range, rt_range_seconds) + .for_each(f); + } + + fn query_peaks_in_precursors( &self, matching_quads: &[SingleQuadrupoleSettingIndex], tof_range: IncludedRange, @@ -303,9 +316,10 @@ impl QuadSplittedTransposedIndexBuilder { } impl - QueriableData, (RawPeak, FH)> for QuadSplittedTransposedIndex + QueriableData, RawPeak, MsLevelContext> + for QuadSplittedTransposedIndex { - fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec<(RawPeak, FH)> { + fn query(&self, fragment_query: &FragmentGroupIndexQuery) -> Vec { let precursor_mz_range = IncludedRange::new( fragment_query.precursor_query.isolation_mz_range.0 as f64, fragment_query.precursor_query.isolation_mz_range.0 as f64, @@ -313,16 +327,15 @@ impl let scan_range = Some(fragment_query.precursor_query.mobility_index_range); fragment_query - .mz_index_ranges - .iter() - .flat_map(|(fh, tof_range)| { - let mut local_vec: Vec<(RawPeak, FH)> = vec![]; - self.query_peaks( - *tof_range, + .iter_ms2_mzs() + .flat_map(|(_fh, tof_range)| { + let mut local_vec: Vec = vec![]; + self.query_ms2_peaks( + tof_range, precursor_mz_range, scan_range, Some(fragment_query.precursor_query.rt_range_seconds), - &mut |x| local_vec.push((RawPeak::from(x), *fh)), + &mut |x| local_vec.push(RawPeak::from(x)), ); local_vec @@ -330,10 +343,14 @@ impl .collect() } - fn add_query(&self, fragment_query: &FragmentGroupIndexQuery, aggregator: &mut AG) - where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + fn add_query( + &self, + fragment_query: &FragmentGroupIndexQuery, + aggregator: &mut AG, + ) where + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { let precursor_mz_range = IncludedRange::new( fragment_query.precursor_query.isolation_mz_range.0 as f64, @@ -341,27 +358,29 @@ impl ); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - fragment_query - .mz_index_ranges - .iter() - .for_each(|(fh, tof_range)| { - self.query_peaks( - *tof_range, - precursor_mz_range, - scan_range, - Some(fragment_query.precursor_query.rt_range_seconds), - &mut |peak| aggregator.add((RawPeak::from(peak), *fh)), - ); - }) + fragment_query.iter_ms2_mzs().for_each(|(fh, tof_range)| { + if aggregator.supports_context() { + aggregator.set_context(fh.into()); + } + + self.query_ms2_peaks( + tof_range, + precursor_mz_range, + scan_range, + Some(fragment_query.precursor_query.rt_range_seconds), + &mut |peak| aggregator.add(RawPeak::from(peak)), + ); + }) } - fn add_query_multi_group( + fn add_query_multi_group( &self, fragment_queries: &[FragmentGroupIndexQuery], aggregator: &mut [AG], ) where - A: From<(RawPeak, FH)> + Send + Sync + Clone + Copy, - AG: Aggregator, + A: From + Send + Sync + Clone + Copy, + AG: Aggregator, + MsLevelContext: Into, { fragment_queries .par_iter() @@ -379,13 +398,28 @@ impl .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); - for (fh, tof_range) in fragment_query.mz_index_ranges.clone().into_iter() { - self.query_precursor_peaks( + for (fh, tof_range) in fragment_query.iter_ms1_mzs() { + if agg.supports_context() { + agg.set_context(fh.into()); + } + self.query_ms1_peaks( + tof_range, + scan_range, + Some(fragment_query.precursor_query.rt_range_seconds), + &mut |peak| agg.add(RawPeak::from(peak)), + ); + } + + for (fh, tof_range) in fragment_query.iter_ms2_mzs() { + if agg.supports_context() { + agg.set_context(fh.into()); + } + self.query_peaks_in_precursors( &local_quad_vec, tof_range, scan_range, Some(fragment_query.precursor_query.rt_range_seconds), - &mut |peak| agg.add((RawPeak::from(peak), fh)), + &mut |peak| agg.add(RawPeak::from(peak)), ); } }); diff --git a/src/models/queries.rs b/src/models/queries.rs index 1d26ca0..83da21d 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -1,4 +1,4 @@ -use crate::traits::aggregator::ProvidesContext; +use crate::traits::aggregator::{NoContext, ProvidesContext}; use crate::utils::tolerance_ranges::IncludedRange; use std::collections::HashMap; use std::hash::Hash; @@ -13,7 +13,14 @@ pub struct PrecursorIndexQuery { pub isolation_mz_range: IncludedRange, } -#[derive(Debug, Clone)] +/// Pretty Generic Definition of the context of a query. +/// +/// The context is used to pass additional information to the aggregator. +/// And this implementation simply says that there will be something passed. +/// if its an MS1 and maybe something else if its an MS2. +/// +/// The aggregator should be able to handle this in its definition. +#[derive(Debug, Clone, Copy)] pub enum MsLevelContext { MS1(T1), MS2(T2), @@ -26,7 +33,41 @@ pub struct FragmentGroupIndexQuery { } impl ProvidesContext for FragmentGroupIndexQuery { - type Context = MsLevelContext; + type Context = MsLevelContext; +} + +#[allow(clippy::from_over_into)] +impl Into for MsLevelContext { + fn into(self) -> NoContext { + NoContext {} + } +} + +impl FragmentGroupIndexQuery { + // TODO find if there is a good way to specify in the type that the + // Only a specific context is returned from each function. + + pub fn iter_ms1_mzs( + &self, + ) -> impl Iterator, IncludedRange)> + '_ { + let out = self + .precursor_query + .mz_index_ranges + .iter() + .enumerate() + .map(|(i, mz_range)| (MsLevelContext::MS1(i), *mz_range)); + out + } + + pub fn iter_ms2_mzs( + &self, + ) -> impl Iterator, IncludedRange)> + '_ { + let out = self + .mz_index_ranges + .iter() + .map(|(i, mz_range)| (MsLevelContext::MS2(*i), *mz_range)); + out + } } #[derive(Debug, Clone)] @@ -49,7 +90,7 @@ impl NaturalPrecursorQuery { im_converter.invert(self.mobility_range.end()).round() as usize, ) .into(), - rt_range_seconds: self.rt_range.clone(), + rt_range_seconds: self.rt_range, mz_index_ranges: self .mz_ranges .iter() diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index c46d4b1..88853fc 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -43,22 +43,24 @@ use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter // 2. `QD` is queried with `QP` and `QF` queries. // 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). -pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH, CTX>( +pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH, CTX1, CTX2, FF>( queriable_data: &'a QD, tolerance: &'a TL, elution_groups: &[ElutionGroup], - aggregator_factory: &dyn Fn(u64) -> AG, + aggregator_factory: &FF, ) -> Vec where - AG: Aggregator + Send + Sync, - QD: QueriableData + ToleranceAdapter>, + AG: Aggregator + Send + Sync, + QD: QueriableData + ToleranceAdapter>, TL: Tolerance, OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, - QF: Send + Sync + ProvidesContext, + QF: Send + Sync + ProvidesContext, // AE: Send + Sync + Clone + Copy, AE1: Into + Send + Sync + Clone + Copy, AE2: Send + Sync + Clone + Copy + From, + CTX1: Into + Send + Sync + Clone + Copy, + FF: Fn(u64) -> AG + Send + Sync, { let start = Instant::now(); let mut fragment_queries = Vec::with_capacity(elution_groups.len()); diff --git a/src/traits/aggregator.rs b/src/traits/aggregator.rs index 5d1ed90..ca44b79 100644 --- a/src/traits/aggregator.rs +++ b/src/traits/aggregator.rs @@ -22,9 +22,7 @@ pub trait Aggregator: Send + Sync { fn add(&mut self, item: impl Into); fn finalize(self) -> Self::Output; - fn supports_context(&self) -> bool { - false - } + fn supports_context(&self) -> bool; fn set_context(&mut self, context: Self::Context) { panic!( "Misconfigured aggregator, context not supported, got {:?}", @@ -48,9 +46,8 @@ pub trait ProvidesContext { } /// Dummy type to denote that no context is provided. +/// +/// Felt more semantically correct to make a type rather +/// than passing `()` to the context. #[derive(Debug, Clone, Copy)] -pub enum NoContext {} - -impl ProvidesContext for T { - type Context = NoContext; -} +pub struct NoContext {} diff --git a/src/traits/queriable_data.rs b/src/traits/queriable_data.rs index cee64b7..55dd7af 100644 --- a/src/traits/queriable_data.rs +++ b/src/traits/queriable_data.rs @@ -1,17 +1,28 @@ use crate::traits::aggregator::{Aggregator, ProvidesContext}; +use rayon::prelude::*; pub trait QueriableData where QF: Send + Sync + ProvidesContext, I: Send + Sync + Clone + Copy, + Self: Send + Sync, { fn query(&self, fragment_query: &QF) -> Vec; - fn add_query(&self, fragment_query: &QF, aggregator: &mut AG) + fn add_query(&self, fragment_query: &QF, aggregator: &mut AG) where A: From + Send + Sync + Clone + Copy, - AG: Aggregator; - fn add_query_multi_group(&self, fragment_queries: &[QF], aggregator: &mut [AG]) + AG: Aggregator, + C: Into; + + fn add_query_multi_group(&self, fragment_queries: &[QF], aggregator: &mut [AG]) where A: From + Send + Sync + Clone + Copy, - AG: Aggregator; + AG: Aggregator, + C: Into, + { + fragment_queries + .par_iter() + .zip(aggregator.par_iter_mut()) + .for_each(|(query, agg)| self.add_query(query, agg)); + } } diff --git a/src/traits/tolerance.rs b/src/traits/tolerance.rs index a15ba2a..a4903ec 100644 --- a/src/traits/tolerance.rs +++ b/src/traits/tolerance.rs @@ -26,6 +26,7 @@ pub enum QuadTolerance { Absolute((f32, f32, u8)), } +// TODO: Rename to something that does not use the 'Default' #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DefaultTolerance { pub ms: MzToleramce, From 8dd51e60eb3db53b7895758947f9c1d93058ab13 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Fri, 1 Nov 2024 03:36:34 -0700 Subject: [PATCH 17/30] feat(wip)!: ms1 quant --- Taskfile.yml | 4 +- data/.gitignore | 2 +- ...C_13_S1-B1_1_12817_BatchAccess_full_rt.png | Bin 0 -> 61560 bytes ...13_S1-B1_1_12817_BatchAccess_narrow_rt.png | Bin 0 -> 68296 bytes ..._230510_PRTC_13_S1-B1_1_12817_Encoding.png | Bin 0 -> 60957 bytes ..._A_Sample_Alpha_02_BatchAccess_full_rt.png | Bin 47274 -> 0 bytes ..._Sample_Alpha_02_BatchAccess_narrow_rt.png | Bin 52663 -> 0 bytes ..._results_230510_PRTC_13_S1-B1_1_12817.json | 90 +++++++++--------- ..._diaPASEF_Condition_A_Sample_Alpha_02.json | 57 ----------- src/models/frames/expanded_frame.rs | 2 +- .../transposed_quad_index/peak_bucket.rs | 4 +- .../transposed_quad_index/quad_index.rs | 7 +- .../quad_splitted_transposed_index.rs | 3 +- 13 files changed, 57 insertions(+), 112 deletions(-) create mode 100644 data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_full_rt.png create mode 100644 data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_narrow_rt.png create mode 100644 data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_Encoding.png delete mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png delete mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_narrow_rt.png delete mode 100644 data/slim_benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json diff --git a/Taskfile.yml b/Taskfile.yml index cb7d494..2b222c4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -44,10 +44,10 @@ tasks: - "src/**/*.rs" cmds: - cargo b --release --features bench --bin benchmark_indices - - SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices - SKIP_SLOW=1 SKIP_BUILD=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/230510_PRTC_13_S1-B1_1_12817.d ./target/release/benchmark_indices - - uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json - uv run benches/plot_bench.py data/benchmark_results_230510_PRTC_13_S1-B1_1_12817.json + - # SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices + - # uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json templates: sources: diff --git a/data/.gitignore b/data/.gitignore index 4dba598..34de35f 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,4 +1,4 @@ *.d/ *.d.zip *.d.tar - +benchmark_results_*.json diff --git a/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_full_rt.png b/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_full_rt.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa47a1b5f49eb83eb8d8c7731dfcb350c76de94 GIT binary patch literal 61560 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 TTWs>bakt*_(~th+fv5i;z-Ec& literal 0 HcmV?d00001 diff --git a/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_narrow_rt.png b/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_narrow_rt.png new file mode 100644 index 0000000000000000000000000000000000000000..b5daf59f812822357d4043535deff708831d501d GIT binary patch literal 68296 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 B*?a&1 literal 0 HcmV?d00001 diff --git a/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_Encoding.png b/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_Encoding.png new file mode 100644 index 0000000000000000000000000000000000000000..b0dee1770f4a28ee3ff8613590d2fd11552d44e6 GIT binary patch literal 60957 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]) -> Self { let avg_cycle_time = frameslices .windows(2) diff --git a/src/models/indices/transposed_quad_index/peak_bucket.rs b/src/models/indices/transposed_quad_index/peak_bucket.rs index 4e6ae76..4ceef58 100644 --- a/src/models/indices/transposed_quad_index/peak_bucket.rs +++ b/src/models/indices/transposed_quad_index/peak_bucket.rs @@ -224,8 +224,8 @@ impl PeakBucket { rt_range: Option>, ) -> impl Iterator + '_ { let scan_range = match scan_range { - Some(x) => x.start()..=x.end().min(self.scan_offsets.len() - 1), - None => 0..=(self.scan_offsets.len() - 1), + Some(x) => x.start()..x.end().min(self.scan_offsets.len() - 1), + None => 0..(self.scan_offsets.len() - 1), }; scan_range .flat_map(move |scan_index| { diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 87b62d9..049fd07 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -162,6 +162,7 @@ impl TransposedQuadIndexBuilder { num_frames = %self.frame_indices.len(), quad_settings = format!("{:?}", self.quad_settings), ) + level = "debug", )] pub fn build(self) -> TransposedQuadIndex { // TODO: Refactor this function, its getting pretty large. @@ -249,7 +250,8 @@ impl TransposedQuadIndexBuilder { num_frames = %self.frame_indices.len(), quad_settings = format!("{:?}", self.quad_settings), peak_buckets = %peak_buckets.len(), - ) + ), + level = "debug", )] fn build_inner_ref( self, @@ -317,7 +319,8 @@ impl TransposedQuadIndexBuilder { num_frames = %self.frame_indices.len(), quad_settings = format!("{:?}", self.quad_settings), peak_buckets = %peak_buckets.len(), - ) + ), + level = "debug", )] fn batched_build_inner( self, diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 91025ab..548b257 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -277,7 +277,7 @@ impl QuadSplittedTransposedIndexBuilder { self.added_peaks += other.added_peaks; } - #[instrument(skip(self))] + #[instrument(skip(self), level = "debug")] pub fn build(self) -> QuadSplittedTransposedIndex { let mut indices = HashMap::new(); let mut flat_quad_settings = Vec::new(); @@ -393,7 +393,6 @@ impl ); assert!(precursor_mz_range.start() > 0.0); let scan_range = Some(fragment_query.precursor_query.mobility_index_range); - let local_quad_vec: Vec = self .get_matching_quad_settings(precursor_mz_range, scan_range) .collect(); From 180ec83881dd0be98cfd234925c7d196dc4ef53e Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sat, 2 Nov 2024 20:43:31 -0700 Subject: [PATCH 18/30] feat: initial plotting snippet --- README.md | 2 + Taskfile.yml | 9 +++ data/sageresults/.gitignore | 5 ++ data/sageresults/build.py | 60 +++++++++++++++++ data/sageresults/check.bash | 13 ++++ data/sageresults/check_config.json | 67 +++++++++++++++++++ data/sageresults/plot.py | 71 +++++++++++++++++++++ data/sageresults/ubb_elution_groups.json | 78 +++++++++++++++++++++++ data/sageresults/ubb_peptide_plot.png | Bin 0 -> 131585 bytes src/traits/tolerance.rs | 1 + 10 files changed, 306 insertions(+) create mode 100644 data/sageresults/.gitignore create mode 100644 data/sageresults/build.py create mode 100644 data/sageresults/check.bash create mode 100644 data/sageresults/check_config.json create mode 100644 data/sageresults/plot.py create mode 100644 data/sageresults/ubb_elution_groups.json create mode 100644 data/sageresults/ubb_peptide_plot.png diff --git a/README.md b/README.md index ac30827..72a9234 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,5 @@ sequential, use that). - Add logging levels to instrumentations. - Add missing_docks_in_private_items to clippy. +- Implement predicate pushdown on the setup of indices. +- Implement predicate pushdown on query execution for raw index. diff --git a/Taskfile.yml b/Taskfile.yml index 2b222c4..cbca819 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,6 +49,15 @@ tasks: - # SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices - # uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json + plot: + sources: + - "src/**/*.rs" + - "data/sageresults/**/*.py" + cmds: + - cargo b --release --features build-binary --bin timsquery + - ./target/release/timsquery query-index --raw-file-path ./data/230510_PRTC_13_S1-B1_1_12817.d --tolerance-settings-path "templates/tolerance_settings_narrow_rt.json" --elution-groups-path "./data/sageresults/ubb_elution_groups.json" --output-path "./data/sageresults/query_results" --index expanded-raw-frame-index --aggregator multi-cmg-stats --format pretty-json + - cd data/sageresults && uv run plot.py + templates: sources: - "src/**/*.rs" diff --git a/data/sageresults/.gitignore b/data/sageresults/.gitignore new file mode 100644 index 0000000..774378f --- /dev/null +++ b/data/sageresults/.gitignore @@ -0,0 +1,5 @@ +lfq.tsv +results.sage.tsv +matched_fragments.sage.tsv +sage +log.log \ No newline at end of file diff --git a/data/sageresults/build.py b/data/sageresults/build.py new file mode 100644 index 0000000..52d9860 --- /dev/null +++ b/data/sageresults/build.py @@ -0,0 +1,60 @@ +# /// script +# dependencies = [ +# "polars", +# ] +# /// + +import polars as pl +import json + +PROTON_MASS = 1.007276 + +ub_peptides_df = ( + pl.scan_csv("./results.sage.tsv", separator="\t") + .filter(pl.col("proteins").str.contains("UBB_"), pl.col("peptide_q") < 0.01) + .group_by("peptide") + .agg(pl.all().sort_by("sage_discriminant_score", descending=True).head(1)) + .explode(pl.all().exclude("peptide")) + .select(["peptide", "charge", "psm_id", "rt", "ion_mobility", "calcmass"]) + .with_columns( + mz=(pl.col("calcmass") + (pl.col("charge") * PROTON_MASS)) / pl.col("charge") + ) + .collect() +) + +psm_ids = ub_peptides_df["psm_id"].unique() + +fragments = ( + pl.scan_csv("matched_fragments.sage.tsv", separator="\t") + .filter(pl.col("psm_id").is_in(psm_ids)) + .select(["psm_id", "fragment_charge", "fragment_mz_calculated"]) + .group_by(["psm_id"]) + .agg(pl.all()) + .collect() +) + +df = ub_peptides_df.join(fragments, on="psm_id", how="inner") +df + +# Convert to json +out = [] +for x in df.iter_rows(named=True): + out.append( + { + "id": x["psm_id"], + "human_id": x["peptide"] + "_" + str(x["charge"]), + "mobility": x["ion_mobility"], + # Note: the rt in sage is minutes ... + "rt_seconds": x["rt"] * 60, + "precursor_mz": x["mz"], + "precursor_charge": x["charge"], + "fragment_mzs": { + str(i): y for i, y in enumerate(x["fragment_mz_calculated"]) + }, + } + ) + +out + +with open("ubb_elution_groups.json", "w") as f: + json.dump(out, f, indent=2) diff --git a/data/sageresults/check.bash b/data/sageresults/check.bash new file mode 100644 index 0000000..dc23fb5 --- /dev/null +++ b/data/sageresults/check.bash @@ -0,0 +1,13 @@ +#!/bin/bash + +set -x +set -e +set -o pipefail + +# This generates the `ubb_elution_groups.json` file. +# RAW_FILE=../230510_PRTC_13_S1-B1_1_12817.d/ +# ./sage --annotate-matches --fasta ~/fasta/20231030_LINEARIZED_UP000005640_9606.fasta check_config.json $RAW_FILE +# uv run build.py + +../../target/release/timsquery query-index --raw-file-path ../230510_PRTC_13_S1-B1_1_12817.d --tolerance-settings-path "../../templates/tolerance_settings_narrow_rt.json" --elution-groups-path "ubb_elution_groups.json" --output-path "query_results" --index expanded-raw-frame-index --aggregator multi-cmg-stats --format pretty-json +uv run plot.py \ No newline at end of file diff --git a/data/sageresults/check_config.json b/data/sageresults/check_config.json new file mode 100644 index 0000000..2b314f1 --- /dev/null +++ b/data/sageresults/check_config.json @@ -0,0 +1,67 @@ +{ + "database": { + "bucket_size": 8192, + "enzyme": { + "missed_cleavages": 2, + "min_len": 7, + "max_len": 35, + "cleave_at": "KR", + "restrict": "P" + }, + "fragment_min_mz": 150.0, + "fragment_max_mz": 2000.0, + "peptide_min_mass": 500.0, + "peptide_max_mass": 5000.0, + "ion_kinds": [ + "b", + "y" + ], + "min_ion_index": 2, + "max_variable_mods": 3, + "static_mods": { + "C": 57.0215 + }, + "variable_mods": { + "M": [15.994] + }, + "decoy_tag": "rev_", + "generate_decoys": true + }, + "precursor_tol": { + "ppm": [ + -20.0, + 20.0 + ] + }, + "fragment_tol": { + "ppm": [ + -20.0, + 20.0 + ] + }, + "quant": { + "lfq": true + }, + "deisotope": true, + "min_peaks": 15, + "max_peaks": 250, + "max_fragment_charge": 1, + "min_matched_peaks": 4, + "predict_rt": true, + "wide_window": true, + "chimera": false, + "report_psms": 1, + "bruker_spectrum_processor": { + "spectrum_processing_params": { + "smoothing_window": 1, + "centroiding_window": 1, + "calibration_tolerance": 0.1, + "calibrate": false + }, + "frame_splitting_params": { + "Quadrupole": { + "UniformMobility": [[0.1, 0.05], null] + } + } + } +} diff --git a/data/sageresults/plot.py b/data/sageresults/plot.py new file mode 100644 index 0000000..ce85153 --- /dev/null +++ b/data/sageresults/plot.py @@ -0,0 +1,71 @@ +# /// script +# dependencies = [ +# "polars", +# "matplotlib", +# ] +# /// + +import json +import numpy as np +import matplotlib.pyplot as plt + +data = json.load(open("query_results/results.json")) +labels = json.load(open("ubb_elution_groups.json")) +id_to_label = {x["id"]: x["human_id"] for x in labels} + +num_elution_groups = len(data) +fig, ax = plt.subplots(ncols=num_elution_groups, nrows=2, figsize=(10, 7)) + +# Top is the MS1 and bottom is the MS2 + +# Getting the data for ms1 is like so: +# x["result"]["ms1_stats"]["retention_time_miliseconds"] +# x["result"]["ms1_stats"]["transition_intensities"][y] + +# Getting the data for ms2 is like so: +# x["result"]["ms2_stat"]["retention_time_miliseconds"] +# x["result"]["ms2_stats"]["transition_intensities"][y] + + +def ms_to_mins(x): + return (x / 1_000) / 60 + + +def infinite_color_cycle(): + colors = ["#308AAD", "#C8102E", "#96D8D8", "#B2EE79"] + while True: + for c in colors: + yield c + + +for i, x in enumerate(data): + ms1_rts = np.array(x["result"]["ms1_stats"]["retention_time_miliseconds"]) + ms1_rts = ms_to_mins(ms1_rts) + ms2_rts = np.array(x["result"]["ms2_stats"]["retention_time_miliseconds"]) + ms2_rts = ms_to_mins(ms2_rts) + + colors = infinite_color_cycle() + sorted_ms1_keys = sorted(x["result"]["ms1_stats"]["transition_intensities"].keys()) + sorted_ms2_keys = sorted(x["result"]["ms2_stats"]["transition_intensities"].keys()) + + for j in sorted_ms1_keys: + j = x["result"]["ms1_stats"]["transition_intensities"][j] + ax[0, i].plot(ms1_rts, j, color=next(colors)) + + for j in sorted_ms2_keys: + j = x["result"]["ms2_stats"]["transition_intensities"][j] + ax[1, i].plot(ms2_rts, j, color=next(colors)) + + # Label axes ... + ax[0, i].set_xlabel("Retention Time (min)") + ax[1, i].set_xlabel("Retention Time (min)") + ax[0, i].set_ylabel("Intensity") + ax[1, i].set_ylabel("Intensity") + + # Label titles + human_lab = id_to_label[x["elution_group"]["id"]] + ax[0, i].set_title(human_lab + " MS1") + ax[1, i].set_title(human_lab + " MS2") + +plt.tight_layout() +plt.savefig("ubb_peptide_plot.png") diff --git a/data/sageresults/ubb_elution_groups.json b/data/sageresults/ubb_elution_groups.json new file mode 100644 index 0000000..0c8970b --- /dev/null +++ b/data/sageresults/ubb_elution_groups.json @@ -0,0 +1,78 @@ +[ + { + "id": 912, + "human_id": "TLSDYNIQK_2", + "mobility": 0.8217308, + "rt_seconds": 316.72206, + "precursor_mz": 541.2797760000001, + "precursor_charge": 2, + "fragment_mzs": { + "0": 215.13902, + "1": 302.17102, + "2": 417.19797, + "3": 694.3042, + "4": 807.38824, + "5": 980.50464, + "6": 867.4206, + "7": 780.38855, + "8": 665.36163, + "9": 502.2983, + "10": 388.25537, + "11": 275.17133 + } + }, + { + "id": 89501, + "human_id": "TITLEVEPSDTIENVK_2", + "mobility": 1.1326923, + "rt_seconds": 745.8906, + "precursor_mz": 894.4673760000001, + "precursor_charge": 2, + "fragment_mzs": { + "0": 215.13902, + "1": 316.1867, + "2": 429.27075, + "3": 558.31335, + "4": 657.3818, + "5": 786.4244, + "6": 883.4771, + "7": 1573.7957, + "8": 1472.7479, + "9": 1359.6638, + "10": 1230.6212, + "11": 1131.5529, + "12": 1002.5102, + "13": 905.45746, + "14": 818.4254, + "15": 703.3985, + "16": 602.3508, + "17": 489.26678, + "18": 360.22418, + "19": 246.18126 + } + }, + { + "id": 8469, + "human_id": "ESTLHLVLR_2", + "mobility": 0.8719231, + "rt_seconds": 483.31707000000006, + "precursor_mz": 534.313976, + "precursor_charge": 2, + "fragment_mzs": { + "0": 217.0819, + "1": 318.12958, + "2": 431.21362, + "3": 568.2725, + "4": 681.35657, + "5": 780.425, + "6": 893.50903, + "7": 938.57806, + "8": 851.546, + "9": 750.49835, + "10": 637.4143, + "11": 500.3554, + "12": 387.27136, + "13": 288.20294 + } + } +] \ No newline at end of file diff --git a/data/sageresults/ubb_peptide_plot.png b/data/sageresults/ubb_peptide_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..1db70a685e13aff081388c39f442f7debdb4d2ec GIT binary patch literal 131585 zcmbrm1z1;U*Dt(Lqy#AuL_%6oO6d*-R6rCg1SCWnM7oh~R76TjxPvVTv5Dq6T=SAVA!Dw z0zCK=m!^mU_?L*y<-0a&mXB=gb)Ohu%DOg>%`9!qjP*|289cEzwzRm&Ey8`_{AnW_ zo5$9oJUr(A`T)1(6GI**mBamT5W>fb_pC9DR2Th&lOdIEjNxF|l}j?}4zK5joi)|F zx22Zant7@5XvnDAlbKU9GZf!U*zoauf7tgWCDi`Q-1WoPL=<*37?g6)d^LUZ`dgRE zi>oH78Hx_sb*BPj*wcw?yKt0sOPXER7sH|}qo3m7oixyi-ld}1tT{e7=S z+XNqRtp}Mb0h2fSMLBS^i{$j5-=bgGX8!rD|FuNixPLxoh4V6z@}D2!_Mou&=l69- zR6SMxK9qL}cD0@GufuwiQA-nw{`32ZLuX_-{yDHF_HSQu^#A=!GAY9vt!J#hedi+g`llpVDF_FO zElGW3p9+}vBuK}hD7bw`n2^B)=##^IfWAC%< zci(y7Q(Ro!V(o=bDxa@iX;L@4=9F{qJ>K5VhF@41X+>paN42}V%kM87yfnlD6JquY z!OvL~dK~tv5B7L)58=c+FNhfN$B4SS=V+DN={864IedSva5~~@NA7*0-IaucOINN0 z$mKJNJMcP)GnCo=GTCsrefz|Q^Y->!zImutg#$TH_3i~vFE6I&ic7`Sdn9d=JLe_0 zzEj4Qz&Ys+6dKQTXH&yBiP|sBs}2%mUf17FYmE~VTODYL6|!EWVq|<^=-k&9Ua7FL zFpPt_AMA`WMO-!C&YNXdio%{NdROmnXOARU_vG9gn$vb&yA`W{A)45Ad-db*A5BT! zCgvlPjg0Pf{^TrwzMdigH%`>{(@~xZkHW%24fjR{28N}L4TVdWa7P>_Ni?fm`o|MS zx0hSR79YEN&ARH!x%a$%%galwHfe9Px1mv>cLXl9>R^9I!+kozb^Rpzubl<41;c{g zQA0yRInIbTmZMcA&KnlF)7kqmnQG}11&s_|>YVi~Vd9=YJJTn7a_=9jjNiLwaN%T^ ziEYRvT)?>vv8?@r5!2p0-LEgtI;?gm!0jSCe%xEe3r}}>qF&G{T_x_5X?~@zzdr$i zkL;3zxZi-hKTFnwrd#f>g{?e;xOjLPC+dSbvhVOXI65ZT&h-$Gk~W^QtautLWckFk zlqC!fZq@7Dov)jfogHR4=swkaa-r0I#1S7>qW!{9L4fTecl{7osdc8~L>;Bph86qU zbH^U6+)H-*vgXKdd)VJ}XVLw@bY+kFfy>jJoE+M=jhT+x1;4KNs3@BG(Q3xYhLGCe zlNS?v=d6BwrJ5fur@k(}jBC%=q)}#bR#sLP{zsBJCtCl_abncxZs*k4kGlH7&CR)A ze5}%G?Nf#0iizgKs3NMqSVcr8y&d9OHBE7f-f0a1Vakyc3%k8+XyJ9`|Dm_{{Oiy|PU zfy8UFF--sSGp3oYOv*>iQBM;SPit1Y(Q~N0u9@x5<}+-i;}+!NA{i{UR2Z#t?Wl5f z_H*NwrR#{QGVad0b%uv$x@xr8lCf%g_FRr;DTVn!;ds3JL;weESK2Xp(bH$oW;U!; zRaIflUpC?;oZrIgQHmEYmDe;dV5{0&zc0QqWNkEDCa|&lzD?3pXdhP8`z`JBC-iH5 zNidHWFDT}RN+}Pvd#fLBuNs>Tejve~#m9&8={G#4uFTaal9iFc38feFg}p}%7g8+3 zu`@zkn^;(Qe7Y^6@fFwow-&>qE}P?iii(ODR44deqFyMM!;b%x41o7 zfplT(?c2Aa?tY>BIUZZ`<9S-;WR#R@b6X9e^g&xI(=2cU-~rj`fBW{Wz0B4$n1=67 z&kCKW4FONZ(xEcjIp4+V1BU(mecOGy9C%ig_KPF+=khB&;Xo4`qwb9ZMWzMzi`w6R z{9rnBCS%0f!NDQ%-8%~B_4$ud`^%)U`bBG<)qCrwwl@|@uV25e6emg*CuZLOPx|rt z{5^F3ci%lEq%OA{l{D*ni=(Tni;C3}l(M#@gpg;xI{81c=DwvK#a28T6J@*}!z!(7 zXwYkYEdmdyG4)#HvsbTtBek6fp*rSwH1a&CqI2Kh;<%lsX}sbx_`%%j*FX{e`bytq z&VHKUTyMT#aj_65C+D-cxRC3TTewiR>%*k>`w~}FtGkGKz4@gzHR;p_rD8oj58+`e z9SV%y+uLgCDtuVf#)xx%1E1H&nxRr_za6*EG-YCJYV~KQfZqG}?@z+Du?q`FW#;5O zr1902++C8x!@{GZjaS@UBopqv)xp^q8n5*`i77ca2p{(N($Z2SVsB=4e)7i;gO#C+ zZQ`}7^ZlJurO_7+FJMt{W-0PJJ$YJ9n2xsdFM-%;FE6iX*hC9E`BmG2=F`jV(Ysr zZ5Pc4KNu_wm*1Iuefjcb!O_tYE3b~9HpI2ON;Wq9lyr2O2c+_!U5%7Rp{g;$wZq*g zZ^YrXva+(Rs;-oLQ}*@i)rQ7KzZ++c+l8g4rou|pXPFM0`c*udx3M-qYP-2K-kfnG*@SL!py*uh_4@}qZoi6p zX-G*)r(hWiXshil=BRU)9)$I*i7$2rCG;a*0C&nJl_2demTbWi^*tIY>TlPUG zetJu*qoZ({UdUub`GlGJ7EMlT>FO-zPBsA}%4p=d^0{(9kfsq@<)_xv-Fb zb8~YH+Dy6HNLbsn^&YO+3!AmsTdRwsjLgq7LSS)bb{-X&rxb9oeQG+P?!C`eY^Sa;lnQB59Wh{QAz+7ct0+fx^Ir-Us6y=z81;7 zxxceWzwdtBv^`Nq;E@FhEk6O&CX(6o#xTY(r~rdXh7zvZ{BC<|y|nKcu$9qLIlp5B z$ByM$pc$Jmn^TT(3!znx8qW060;@!55v0K$*3OB01Zv&nr zrP@gvDznWV78oGmGycYkP1&}3IF_}fC%6T|W;lQU12OdG`u_KZ1#Wvz<(g>dbK=xI zim`BQfJ=aFJO24O%aFCPu`!LHS=8{+^|3LX?^9C~(+TbtHqL~`SAykP7KTdICZTG7 ztcM$fgXe`KW;g%V@z-#<{qyMP04Ro;^6rTpM)UD^(mT5c+1EdXD4&M{@*HZO)WPoi z8&al#OqQB2wvE*VP^xCChloZN`R?XGmr>}=(<-p}d9&QK<6g2tnC~U#%z|A)-VNK2 zA3xfKdnUMT6ShemhyyTy0y>=NR>b||#}6pPmhD^eVGJY;VSa!(`wJ`KfzH693E70R zM?Kf?82yv0v4qAs&}L{8Oh5GCsoJ`3)>A*|+H!PLHJNkHiIfwWaNCL9`8eWW4&67o z?eg9W(C zXOhF-8g($ErA_4XrpFpVBBSoCh?X*Hg||muY_I-QGbMJ;apFt0o^I8h?<;Va%}jp_ zEi8S6X?LlXl!=UB>30*4*lrB8xHk`~-G&a7u3o+R78+E!y%f&agM)2q*eB!+451wH z4u>qOw%Cs`h!a$5FTPB8`fO_UYs5Jt=sO{eQU_btxgWvyG=iHN5>&jpYBRIuRL*h^ zs$!wD_FNG23FjM*uu)jq*v4Zmt7M?iLBqwXsaRjyUYkRJlFw#Fm6e@+0>E%#w9I6N zcBRwDhZjU_;|%58Za^!)4troj; z`PIh&s_WMJjxRSdHbOU9hD!S8-MepRr4BQZP*G9cnzY^ndJrNeY0`DZX0C@MARvH% zknrQnvuaCQ(+Pud=BuZ>dN%I%c3ai$tqO&fId5>o+Vzs!-#i>EXfAv8D$&3P^B};> z!@XNMu=6zj?7Xl>xkr!KM_f0w3Qf9+9=NViO-xKc2@Hm_27@%-Y zBpmD-=0^0JUUb`am>(!QDjIj}f%~2S&>A7Fsxs)LCqzU8YsBW!^8E%`_fWVnRt200Oe&{jR5ZMSa9?s zKB-4}2R5vhy4}xjM=UGXPJDk-+H!meF^!t1hiQO{x$Q11pt5pZYL6c{Ss?62p_yp| z`|^xzPFFdR?f37X9FDc0*@iuv)L~>2y!v)JBk#?dLk0#01jNKPvt0xFjiI_t$^oT%*d>#>3)fT(I;i&&e?%a5BCtx1?U z)4oaC3rR*`5$g6E*j7=Zwwz@)KRuxa_TCRJk?i6rWyqRPVGu328(ciQ&ydOvPzKwU7>RnHvXC}OvPUB1~{ zz3&6(qYtf|M&!xEI3Y%Wq-|`g2u%YNDnN(d{oVArsY%Yr$VlIfPBw;(hbLd-+=fOh zukIlLlmXD{DuC#0uCz%l!xtd_;wODNgM2he4&Sf!+;hbet+RD{d{7->lljqF!GeLm zBO)R`x3ts)VR;@C69`XZ0!qZO{L6;vZav+1_+KVVoeQGB92OE%5BOSbE~K8EMZN|u z1zLc^TK63s4DA3YTVBwUd;Avx3$tI34@OuV$eWy#Q`vB}vYck=lOsHps}xRav-P(I zE&ZWgFi1*D_Eoux{r>&C9rOa5pWjk@P7;SntkdknqW)yWKLKRBuiT#JMR+)$^^}sg zw>SUHhLWN_2@Nl{x4o8qv7xt@`pcIuiYh7%AbUKWpEq;aU2a6|%f;1od7*q!^6h$e z&&E*OlSJQjq0oU+YmSE(PFjpsg&@TP{^~d^0gs-YR)#}43mOBBnB93qX<*su0j@fh zuY3Tv@`i@SSMv_5>IK8&amvy>nkA}GpXZ+|Fl1Vp!BRs%1R&4)!R6VUI4O4qBqkun z>;gT0X~}5QwHQueRI@Dc6XBTpa!0~s=9g|)rG7%`zoD}htA)9inoc9>A`G{#0K|r!vP`BEK?q5i^}jm;AG0e-I6;6rGh8lof?M-*OIjbW zF*xIAerJV+X@G&$Lt(&`lB#~RX1VUReM$i#2*3a|LKbxI-n|?9`7;2{XsDNh&10MG zjy$2L6`^br8PKWrxiw(sp>p}U8=rDaLrRmUTH}sNNlE$jz6qn@J9Ox2VuRc3o~q4> zqyCqeD>f&B1VWd$TwIpxscAs2dUgJuCp^rdJLRB61y091AH%`H=>t|Q^kda6m{E#B zYIo@{JoI=VU3k^;l^5M$f9+TtW?InuV7ya)WE}eZv+!^)_@N{Kt84Mo zu#fM4Fgpo5`t>`lBVvL3&;gmDDvWG`0)Pq(0;GTdw`m*I)YMRo-K{>@I4~K0rEuo> z1a#1b@avKRrPk9u(es1J>YRKon~#CDJbd&>bk#Y#sfnjL7|4VEaG9-Lv5p?Sq)Xp| z;_Db)&t*$->kEan@Az+&- zyk7iF3t$jK+-x#3GDQu00EO(1+TWB?*6aqI;McKs*!qkmE?ZCD?+7_p?@ zSp}_@2m5;^Pks;qLJkZJdC02U4LAB0@|j z{1|JK*r3BsU%W^;>bAq>dyGCXOmaJ;)mkwXQHZf&udUe{mF+=SQ@wW&A!tGX(|#)Hwab%@Xf@g_44qU< zQySNU@>D|=x&zBV70Q;@*!s^-d{9bcy#^E&)kLhPf``AGK`la<5{oJ(&&u^}fPQUW zMGCNp^1ArPkjwq94menW@};WheV zmP7DN{fCA$gBc`fp@d)_ptMZ{@l<(KS63^Ke}X&cRW9QKVk9=UpNo%=Z+$<8;v3C; zk)|zD4;dW4lfyGa8t5PR^z<i8J(*iPVxsD9y|Zp!e&C*INp=->Qi#w<4Xgw6Jh?f!y_*+n$i88qMJSmc zm=O^X(O^ZAsHI&1Eu$Hq(WORwrTL;#x!o_s?9*;#-)@F&9GzdiKQQG2u=AZ`r6Q}I zr>AEW;GI|ZK*mqLnQ3A^{f`KW@=%{P^)c3GH4(^Zs`R#I;dU*KOg*Jo9s;!!$#|&`Q2uX z7RBhc`BaxCtyR*?`~c+o(&;|a%g&@vrEBU%21dK=@_@wjSB(A|EHFyU{y;7kb$D!S z5U}cQCj`hrAN($7Mh*^`X(Z2s1~6Ubf&&5rofLE7kKijP{@p68v7E&NtF2Uu(Pat8 zK|H=?B#AZz54H~yr@>4jr=;|H^M*OLm+Hg`W>Hbvo0-R<$45!IOA_OGDK`UPZ7;Tb z;9su?2=WLR6M4(~XV0FU87jR94i_^Ei`8t`+1Sa@LZ3f=p8B%UEMAFN_!n>Z=%>a; za4PaMW90}6bZGE^sURhP)1|jO(h)@O-D#-9FbY>3nFf8Rf9`2@8N2^{eBlL2Z1@W(vh zd9yGv;bdlJ`UeNsW!=h7R6X-7At4MjEx&=AjI)e{R$6%@3T209SqQC^Llzhrw_E4x z8h}ci#8wmWB{Wy900*Znu$!DYcTOKj*;goQd~M<@#D8`StWq{MHng7Myi5B)QuD#7 z`TqSol00|Tf9b=ffRRXr;EL8g3dAWxxt~^5hBc z<1uMC(L@mQf%kpaJ5F@uQCoZn00>0&6;xDILS!!x`FO@&&F}pA3|1b9bWa+$TqFS{ z13;R5e1BGMkA*3(DPl*_UdEM`DKE2NkA-bUIXLbGYxtK8Yqq*0r2~Z@VZV-p8Yb+z zZ4;t@HI;(-If0v-n_wG09-eHb`V+pMFCxybC2rpdzMAT(k=p=O1<>EG1|T!_;~&)1 zyG_6gEM08+`V~_>Q1m}%T_pJ}u3Z4W?w8w+f(HL30;ZNaxVy_xzjOe1K2u~>9;Qpb zcJ-=fem>8}4Dd=dp#ZcjzbCyxesfF!G_~W@)X4yNmX?-0{QU4k86*M$;MRaJ`Sj^i z*uH1Zo-J_Hx^aUXIs}Hnx)b2cd_USt%@Y0vfowtZ0qN6ZeIW&s3a;jv7UQ3M&Tw)4 zV1Q>~zZ)rs%<6B4{t#6!IR%KQ8KhfII3tlYSJm!MSh z81}5`M~TBSb6`+VX;euQ> zwc8#qtl1omA|e1RQ5Ot|u$tc9;*k%3DgfDiZXwSRK%h$w6|`X`CBm48e6kGKDoCvX z{9RX9*A?~X-rc+YU}ykZ!^Oqb+27p)FI$igRD2om;(!@-q+X*o8>uLjzkl=Q&2~5% zczl=SZl3l zRsC~K;=a03X%^l^plX7w`(*S0Y!rv}enYFxC0$|$aT-8K@^Fs?Ek}sKqo`|a^a3aY zt?v*<6C&^J=f?tP$LP9B{&xk_hi`N^*xSItjvP6H-1J8+F+NbcSj5Cmfq-%u4rPy1 z2FMdE#gqT5fFXx)ch?1O875t6ZsiU823GpjYuAiLIheS;C1aJt_VK(+xVK#dCALqI zO~lad^xKr;R+a$;(OVp;+yu!Y>1Jm82lM+tu*L!Y;bBNRkZ@WZTkjqQ!ACxnE&#dD zEF$o&4l6A}Rsh5hGu)br{rvfJ4amREm2I%IZrr@-3B4J^paq~j0%Q$NcsRNm0GvbC z>h9>l9wVICoOuunss{ochVOrNjCzpuUQTFBQ*b{ltEr|h70WGDKO6%%JMw{B0El6p zjgw45Lex%`3_xu)l^{LPk*UT3r3%BKY_*r$TgDo{0!;4R5Dm*Mh=Q z6fnNB1!ZqSkIwk0)physYoja7-p}y#Y4F&4s}in5v7o5swfUuJR$}A1-y$@cn394E zb{Z!povAL%O7XF+t&N=Hc3EHX=Fx44rkJ{2D3)0oKd<#soAY$?^JY#J?J(e1R zpVV_|mXzJ%V=Cn;vTY#T0KL#vO{m@&%Kg~fT$6;T-F#n{GPBCd2bzK95gNNo&cB3= z37Aqu_+n^wH=W7zBGt!2v_#JI&c>EqfjYxH6T-gx_*TFi8`YW3>>TK_n%+(9Gw!>~ z_>gsQR!WeJbLNI+2f`-wDZv83mjpya6M9*9ZW>nS^MEb*5wHZ<+E1lVzdfbz8oE88 z*9LwJ5@aOu9#m_$S$gtw%n!Fw6dmK%9(hV8OQ#q2?7&XJ=U~v92roR7@IF*vuG$Aw zv~TYOT{f6yH$Q4c&mEg+IZ#Ll5W+t+bSgf`-`_*^%q2TJ!3|9}H%X$SN0}m+rrBlXZ!}80V^hDCU8mfFIz8Nl_iw?fh~eV3kdf`LiM96jC@*%>negx-fjo zu=VwL=tW|$y1Gk+roG4F&vH zSD=@$fKFyw$R;5{kGSC_W*OK|pyWvY!9Im9^*?AAOaXT_u~FK|LB`4g~gWZ!_M&jc-8uYd;QHSIaS z3j)Jku#X{Fp+P{Y zz8=~F3K1wOE7t?u8vpU*h>t8zNE+vr7?47_waUDqPoWwMXHyHNWa%VqNMKD0k00|s zv9?|Y#brFse&i$6zpwRlQf_o8W&(Hh@xqWuFz5ow@*(f~B|bm@@a$E7ALrS0+_=Xy z)Gwo>zhFAh7ftEZvb&pstTurDANc%gYV$Ro$~ti4n%I#~+YGTF8hEB@aEqIf$s(WM zJ6H#VD~gbm)DV=vmCr0;yg>A_mbDXHeiL>=1(9c^;5&>*tfTT;xMS=?S2}P2oSK)f zUJ1_F)(M!4K5z5pSbX>?-2MW!<$iZp3sZ=6LJKNVy^nY8*(l>H!W|7dH<}Bwi=&pl z$2FI5^(zmb|5m5@YXz#J<%R1KYwEd;%IuVs$B);!0O1<4M!@jy`9~-_;;TzD1^4!^ zuz+Us{@%_>hOovVu-tWNeD4)o(069tIwzmqJy2m^Y%yH2c(5Gi&I<@maFOxk$uEEO zBsz;A7R_Il(zF(H(GN=J8j9Cno6Js;aTw1VcCsn7LjvHKukKQxept6K`*X!-FJ5@U zf(ER12>M}G!IT&%hyY0CAee&0S(JBp#L9r-kun5BuaU{_o&aN7Gu6R#d3kwrGE6G5 z+r;Yk_bbqD1xvWD zWMm%TQ_-P(m3>8GB6c1jVeK~{WF*~eD4gdKmgQeQM`KMlQw zTRutkw3HMhEXYZ{*@4zx*zj^jPk)0eAnx#fyB->t@LO>w9N#)FUg`Zf;Ok==ABD zsS-eayL)?a@8a^;mZ9X4{=)70J@nz~*C69Su>wT4INmYH@FbxNKAQM0IB`eOd+EfT zki?6HYDcV?Agt%QvtAG16^1VNqG4+t2)xf?Vg|aDt!W1cGk#=hvxaWpLMYH*t%E?6 zDJ4}`q1`}_+=X=tcUVU+k&h{u8Nmc^kEoV zX>RKjwVAjY@ACV~_vz_D@eK&lkOEi5Jf1xx!##4O-z+Q})FgPivAIqFlmXl~)gz3@ z9v+Y_#~RsWHQ&cKvv%uX|NXmn?T{DD({phh+!FnE;@fXNVlTa>tF2g5qFz~ee z6j?u^a`N*Vo7rd;J(xc7va*MIUkQyk03@NSf$fIkY{)JWww@xMn4F}yIBLB0)fO*f zZ7N!kDc2QizinFAoV-sa>z{14A(Q@Rb*H}ze%W-sbFi{r_NDO}zo&}kRS+i&AlP+Y zvxE%T(Pc$N0+4`wKxu!>cl5-0>C2aoJZgO%0AViiwJtT`$72|9VrnFX00PwSd#ej8 z-9I4UV~l`l$t1J^VTYyDpoNfu2?0dnG#_8bqx@B5BZJvdNrI9GaVy5zU`N=lOJC3B^Jj6T80eoX!QXUXu4$yiO3p6U zRdyV=mwlb#*#nVW4Vnmfzk`i6I|A*f;8Toy_&6FXM_A2^*<|iy zfolsl%U>-sw(c1 z>DNmy!FOQ;@&`!@i$Jkl84i^96%LWN#MR%@m|()Bos%eKV}v5)@hxrHFzGQ4xyHI) z98B9p;HwQv8Gqi(ikVmlIL(;3ReC{y#y4}$ppFFX)bO`+ZH}1d!zr;OHuW>1!X04O zke@h#7p;W;F4YW$7HQpxPs%{d5N5a%C1CohMFhYMIXyi|rg|VK3e5>pjCfwZn#15J zBm%!A3#y+1uMP--W-`}*5KR7)1~a{P;j>)Qx0!J*W0iKF+`X}muhiIB>Wxl8Q^0!! zJ!(UPPO;e=ro{4jyc|{!;DRPt=NV>hKF8N^48*}UGF0iT-heYP=0z$gZDGL;DLF52 zr6wR~Q3p}ElBkmxAB}-fG6Z&ZwAdqlT6Fpl1Ar~15290Fh2upEO3G3c_kzF;%1(kE zS_@JwA24_@EuQcJ{ZW9e2C`@{FrGKp6C^=tIm698rT2IGn13>l&@fL*+5M& z1QcCjKbosIsUukJ6n0|tM08oy#5uMGc zYxsK-xBfcg+-gpK9n4|LaVYT^RUpgxfkz=^9J=R9uH{nLXn3i-{snPcK&tIV{7YaG zfWS#k3)2k9YJhk+ip*-3JjOv87{F}>roG&l$IZ;bE3G(>N6$rk71jTnJq?%RBla`} z#`lm2Ahg}xU6Z=Ei~3==vwd#gw^h5A`UdxHW`ssq;!g3Qur~aMBhmpx4KKd2ThKFk z^Itc6S|T_?vE(R@jeXEK_a?VzhymTv09#LOfqJNd0oH(wNx;(sDW<;nfqO__-(6%c zg5C|nIy-eQ@J0mrA(BxGit{o=gamc7>BC>X^k%#Eir@C4f5e3lh7psM`Zo%uQW!RR z2QTf?ku-0ML^3ECZ@I}Di3+p6s|=lj@>6%EgxN}#bn{+d9rSi9LEtKYhfgyzAA-=t zOlLaj?R;%>Awfv>V@sg5<6s~onWzKdA|)cqy7=EX*D=_A!2dM#=kJv~W&*xk+6aQy z-1et3Ll%o4Z%FX9LK@FKM~>9t}s$FmpFIsC`gg9>^zZ8pq83j#{-bJ zh$~lQmA!PyTRHg4Ls?*I)Rg4pI7zBTl;I$jf>j&4_PaZvUeDsF$hu6-?M#gax9Tfx z&jF-R?rQpKT&)e_GO0ah^A+LPmxn#V?h+)L3~EnQ83l?lseEFenPRq@ClFp|}Iu>tK7MH3f=rE#!U4paf&cHvi9hmD#?K z7?32SJwQ9I0dWXTKE_*UbAbGOKZ>TimKL1>Hz2!t=BEjM>| zpor)~t%QY613kR z+HoNH;vt(friZjED9bE-eB>apf^h+t;GsNT5^?U1XWKLjJ3AiOQt1MC@J)*!0?H>>DANy*3<2FXy3uxqRo zDJdzZ+1OG?1j&1E-A?lDdgtxQMbWt6!+k6JMq0P^{lnmWSSgkEyhsenzR4#jV`xfC zzH!+UAOXc3xy|+G_^b?5nO8SBGgaxBQXDUgaPhf+oUX0y#h1QSAj}iuTmG5Vn{1+l zh^zekI}^+=E+i!lclcN7Lr*sd!z#TNCWio=Z+be1)5bywAojQ( zj{^P136T3ff-Q_DA6DmjNwK9b90`BY2tcWVAt{AYTA=D}g2>855zzkL@H(jIx)1=+ zWUhwgk47tyL51mn5ejndCwPz$u9{W7@4utdOYC^-luWh4A0(J`AtXL~jN3%ZHhd^K z!f{88CnN@~_fpbz`SyBv7&R`+Kd&4S9)8snlIXrMY(LYRMAg9;?pF%)}*z1l#} zbLI^6wM&q>-*^q%7!*&4`W?og`#w!bV2pfySQ`Ep*gY!o;$P}fFo*d@_c|UtB1mDD z*e^0r>+eaL5${NXNmfE?cc1vfXZ&(o`Loc(o;}J?PoCr~Ir=L&@mjaZOH<=qR%m&+ zwY+xOB`c2NvBnE^uBDoupaT6*+!o1~hdf&BBW7{XEwPFDg5)4O=hF(r|OMNix(fr z0fHN7>s#y(Tr(59+Is6rdS&}?OjAo7Z?qD+OMhhzKrRRcy!1z-4hb=z=n>Gqd6NQS zL*KeuPLpDT(eKoT*j|+<(q=VWAu#q;r=_pxmuTPHE1B*$S-=wavsZP6f&e-R*g2Ks zCp7-rtbIWw@t9g8>-)l#rUh;#czFaQBt?#Sf74_PNGuKpNf9$W_@zW2Fd)Fs4kxfV&2d9a93W5i4x4?b;t_RUHyvnMq8f6%@ zgQ#S~t>DO}AA*au4R04G74K(J2GxW5C`kHk_h$(EOfWmv!!k4P_6l*97{&;BOM6^; zjHc;K**F7`C0Rwew?$$V^#U9hoC9YDo|ou&N?MMH!%*2Fu+1gic76#Vc^{bbW$;}v zkH$$|P_P|fc`t)vgGQ*LAd?Q$iD5UA<#Bk!N~F++;?u{20V^LA@&=5Ia(8Hc@ekMo z%!axpP+(o);Ar#_T5HLN3UJ;K>3zlXAQa4)F{Jsy1Pd7>`wrV^EjF(1A!MQyTy?ZK)-qrRz$fN@N9XZ z9&U31zP#-A3C58b<+rrfQ9!s!rA7 zk1NoAm|E_Z_IQxlwlG^^HN~tmO2%7qmejC84XJLc4P}RaJ8pl7TB#jJ=lkd9C{>!< z=qS$cid!oXN^%eF(a9e-uw!)Ciio2 z`0HU6SN!Dces*`Z&_VrL`{h?>7d_tv`VDc)nry%@Bs}+i@iP0xR{CBeeo$WIpsJ1o ze~OBVqV;h*`XAV<(bRvp`#WB}e0dxNpkNI1`Sa&6WcRYgN?|}&BaO(8q%4knHA`TI z-*cfARDe3d5^|e4TrI_0LvZV;t=q&!TbH)TKD(HOUO7)#o>4)^?(5j6%#v~&<|OK% zCzmvW82OnV3?~MNC3`_c9^rN&%aJ;m-zlAxPbLQ;6dD2Y{y&)YdjhKjobx>B?3pvt z5CrihKIvIe@kdO8!5;-kjZ?n|&y1CoRR_XHDE|(*!NWeXy6@Q@?1+&1d zjFh`I^mCvixAft2VcD2iqH>lQb=FXje3q}Zs10r9ny}Bze=tS5OQ934M6)mX%^;Z2 z07v&EgdJ<)(Z7B8g#wu?C?g6ZrqJfcrl$>8V&D|cL|pxwNB$QN?!U0eS&&5m*J|Xx z+R?sznKqU!Pw{mn=K?eB5EGI`j+?~Ak=w*fs1O)8h^tSL+msWQ{Se8NS7w>1Gk_sS z&mB?Nldzm&I0)jRVqop}dvjTbrlhc*zxOUBGggK+fW9j;8);1TmT0GOWN=a#3V1Hu zxWyV8c!pdy=G2X7$FP-Is-dS@K~k%oGfEoqUKw{RBZ)$ZhLUk3Rf zq(-1}{@|l6IQ%#NtuXUL{Qs5vwv~z8H=c5|heS~YPuIz&an{I{p>|exRqh-LxR~>x zngN8yA)jbX?ndxBN+$~&d92u<8suhwZY*!$n@iEME?!M43V2qwJNMw=6J(%ZL{k8n zL@2)wNo?3%_~05S!K49rT*5A!$Wr11f#&m%C&YBZWEd}mXhE#e`<8M=*nM9dq<)mq zuB)$?Ie+3XhK%%4_XB#w2Y~{iDOtn^flp2jJb(7g1C0^g|6ukJM%+DNd@C8`7f5#E zKq5$1BQJ~G##wEsUtq>&c;>HyHXo9BbW_|PcPfkq^pRF*==d&6AMp{chE9i+A(WZc zwACG$j1&$E&^_HAqtM`^EzrC0TU_FJJP?COx}y z{rYt19@vjCT*|@W2ZmNZWW#HxS5Bth{8A5VA^!Q7hrzeA4PYkr#QD25e}-HofU(w| zIZm{&gL4=U&6Eyp$R|HHhGZBhGIhMfldAXWIyCq3xNfB0Zc91l;3G0l-Jp43Rz4gh zr{#BnWA{@OkC5BWv-=8~IIfbh40|LojBy`7|6}2?MGVk3<_~k}BR}tY-W#_f5z~3Y zaHKCET30b#Tf1adKnj)Nb&C37o!*4Q9+7cp7jEiy3z`yB(b(NDY@uaR(XnK-srm3ijHwj`ePDeW@zXjuJL`jy z1)%jZ#7KTEUwbW|EF}Ao_kp65BAcB)4UhGN#rb>(qhF9sn9BKdp;C+ac#YQs>(?*o z`}p`gdUQNf25i}^NRgsWnE_8r;_`=$3f4kuQLA?l9%16)Aw}~z5UU~wZ2(kHA=~$_ zna`dj1bbm98C(*GB^Iah(VYGk-J7_Qy1o%Mb&tYc-)B3i2;uCDdD5@r)~2Ozc^*^O zx=*nohs>lJ_OTxNb2?Ww6;-|qlgKE9L$)=Lpaf<0%ETzrUIL-yE zCQ_p(4P+}OZ{ZY^Ab}m08R>2vQuCszDu|QAsVQ6KOM0rP4FyngfeFJRA1n=;rt0n6 z$Wfvbd4dZA0D?QS+y4@AiWZh*U%w_!y=iP8HdFamoKae7ZVHXrcw+$N|7?9PBZO|E`u&GMl6M&0s%@82X)dIoib+=GGJ@12SuQF^4 zyJJoPc6cU#A%+IQbPvNKJ%)Hq0hO!mS0<^bsr6yzx5Ret1dMrrec%h?miW&6d#rh9 z_V<z8nY`EwG;fI(mjGQR`EllCJOjtE|h+RW&{`TrMP$8KXY zH=C=j$aq!Wf9^`V&xkgL#m68MsE*u>%HEl3=-y3oF;5l3vT{hfJ(K@NlO@`&(VKV* zI9^CGj0KDSn-tR%1q^0>usFYU1;v+Oaw7X8JJ@W1{6kERT4~W19UGBf3AyYBcuB&g zLI<~|)94oiyk|u(Td65eCf^iy*yY#VD}-f(ZveaUztAwO^8bOOsZ09GdmV%VemRZo zPnMn<^6i&LrUYhO{_LY;;DgS^0E!Wqu?PbeP8$&C$x2RCGr4RQTEH^X^@TzrPV~v1=x)Wmi?kF>P<5un4(Jy%M5d6{I)QBvvg_evU!gYN!iYy+nQw_Bv9kT zXfyi|FfM*%V$hsoyKD2Up?cAQWB!nlxiM8EIdjXjJ@bL>9q!1)YA_+kmK_AcKrOOv z6rd#m{|~)s0qO+O=1}N+8lc%FfU_{a4oPQT_k9DJ2{X0vmcDJHi0V5j5`s86h)B z8o}syBaFXb81TJ1XoSma^Uk0N(h!H@RPZrC)X2U?RFy%(=`<{L^pYFs zyI+csu!Pueno_hU#E??b(qv)&3r5rIA^sqdv1WTbAlVg>!DnAC)W_XvI$;V`D`rCh zxyA%m#CKTpLOB;8gIV&>b021Y-atM8y~F}BSC~>hgaLj@>YXE}qN=Y4eoGJUvXKS6 zjiDGhXm;{2hC-dJdIn8-fT8_Q2nvh=kPu>N;9V);_WTJ!B|{+gW+sH79J4jNEGz@X zhL0fehCRe_jz2!#FqiL8ljIBoJ0)j7xjl%Cc6`M8MJ*ayrtSUxkuMX=&w<9s!opJE zxbg_i&%j`u0=#1c37an?B4qxRftwP5Agn591cpHk4S)TlJ)pI=Hos=_OcEY+Tmr}LqbohsOW+7Fqzqk*eko^C#m&0l@whC|xyPd* zuw4BXD`G)XR%zaop$IbYNj-jA466=3Sp0oimgdMIf?#3@p+{boDRJ1p#PT zT3x*a8KTo<1oI7d6Oya`ErRB#HaANnlMY1u*&}~LU}Z~LI}Ifr??_@l1S4%R^XUDE zVYTl#3}JUnUvns1**kM5Bt}L?PD08Cg+4%#IDMQ5?3WgLh#V^D>N0`Wk4bY!PJ%Ea z*xduki__s>$Y^Q5*56I=3eiOpD8P7KN*K?EvHO;K;cq7<82WzBtuOd$PzUIr?~H+r zS9bwnnO|W%t`=A>5u_E$sBKyG2HDx|72F1XOR`XME;4pvKi_>1hK-gEfnIO5GNp`$ z;UFlBrpf+iN8~i71Fy~iL{pz~CH&c+=K8OgT2tKqz4woFVNesL)Y4Sq0^yAdD5VB& z#n>Of4Q3qDcp-jO19FZkybb8AS{mjd2em+^_C&z@&hd9w$^&q0T+ZuZoXcSmpZe5D&3aHbTAySc5@yZn$m^sYl1En5@Dj&p6 zpM*(p7%jhJs088wNkGF;$Yg4`2ydFR)8K0 zw`OY6m*`KMwe1iUu8*Ua5sISu6UE@JW{wsmTkN&PuzGNrH~9nb4r) z@wp~6u{SR9==sKIqL#yd$!SB!H#G97a|^u0BA`;SLAc16)$Z#}bC#JuD%Wcxz&tkK zB|X)KdKhH}?umoJzs01j?p? z(#a$xHOl@7#`FI4V_3HRbx@!G_nDVL_k{n@dY%764E;Yf^VThcXHX-Bp0LcgT#HyA zAaHFa6ot3HKs@auKI7j??2vWn>Xk037x+He6WVRZ?GsIIr^WRAecdVBX(iUM%mc-Y zQ(!=z8N#6OY81ruR-Gh#Nf|=}i3Yjy&$Y@~YGO%A zR8aJPRO1-OEd7PO26VhX-(*!$Z6hdazj-M%y+d-zj(j2m)VE29Vv>TvX%?TKfBnt= zb8*EOUnQ$_FzZjr#i)a)r5ncgc;@SeUvCt9QKp|Qv{U7FPEPw$AgE3mg z$g6`j3j!u^l&)MMWI0Y04Pqnr6c|yu7%#*uj_tX$j9a!-{5XD` zPvOuuAFxcCwQi}|;K~XY7VA$u!S@cjy#2aFCpb0NR_@)t-2gGIh%aBizWYf^|1&&B z?cg(S-j2}ebyt@eaTFN6XZu~RvY@j{^ef_}>kqhH2)I~Ud-~egp`1aKXYMN-!PC&W}Zn1O+ur}P)lZ_giH}d?h=+EWGIoLloV1Vzw_IN`sK^+v%rO>P{YXULB% zqLAW*qGo7l7_D3VX&QBj&ofM;{R0E2L=#6?x6-HF% z=v{a{^zmL_)XH>=QlL8Ja>KzBjV}ERv7;izJ^uCUyLO40nHg{dd5K~QPYO^50Z2~? zlIIw93mJHnUCWdI%c!s6=_z+rcP;!H5ePRS7fGoCqU*lzXETy6C!_{g?(W;xaUh27 z?y_4)M-4$?@^6LOWev0unPY*FQ=*6ulqj4?W+TQ&h$?u8WZnC!&iqPP!=7Bc$774> zZ`HO)snzS+M;#=@9rpw1p*W>Pslxal0{YN@9<;}$R@Z5f!N!MY8RiQ;DsA~oA$v=x zv?AX98&T*pQ_$ph4*kTBd`I}A@Xr$SiRBVRPPoa;4Bk_INiG(`L3(*hbp_^_R#sNX zdeveKP_w z$jShL3WJs^^(}|TvYnBF6zj`vx2fi_)K)&f{*NC=uVjXZaq=9j?Y)F1P=aT;(SOP3 zs1|UsmO?~97)96UT72@{V~aZTpuH&}84rJWzijojt*r28|FTjkDnn%PllU9!ggx1w z&)sRVYfX8q-uk1dZhqH~Ad!>@Uh9@97iTmcn(O6B44xFAsQugp z#{(y~I9OSA`EzxQ#o0dXR6je?ncmp>QZl4}{(GW^W}RZEj4y4clc-1Bvy+O@yhWp{ z@+9vS=&W35U$rh=!)T$cpjOaO;S>kO>A=r$Cmu`(g9Kd@6zVccT3O117CsN_j%#`! zEy4|E$vBtODDBy=NczPpd&MGlTUTDC?I)DmBAck1bE+$Ep!aoY@44B^Gf7-*TYX=e zW~@PBdA{aV^Xr!FBMeA_%8Q5>DXR}FU#3p@%b1YS1+b|57FYtIJ zKD^r>R&=iLI7*bbm?Jl`S9`E$^&6}iaeeN%c!qd5V2rfJz>e@Gz<|8~Q&%6#@Y7a% z{X%r=-=hVD&hYW*!r%P*zSzhjW?t`Sm#L%m7`*o&ce`89UPe~N^o6IMpiHf0?v^K- zQ;&LMj5?^rF5SzB=nq*RUW#*Y)>%X4C!t|dn*-1E>ZY5v&XrarL$MhBHz<*k@ouo0 zNx|tm_~Fo_wnXZRg`cs5{0uwGGk;uyW_#)4Y&>gi>22oyVq?%dNc<1Q6ebPin@-H< zIHu4r42JvbX~ai#U0Uq{9VP4Zj86Xxe>g-q&+vMCC5GkxhTw!4s@dxAf)|_UxBYk& zI8ZBmg2jJG3GZQSE01@DRV=WEHW&qCsH6oQg=iu~M}f6=f$D%W0qsVcsoQRyD_(g2 zABHBPo_CX1z3R)|u`^iAQTm6O5JkJdpO5~aouJRY+ZnSclReww3J z;Z{C-^v!cibD5j9dA_Y0$4L~)5ksAj^0)wb^7&jcc?IlQeU0CE_O12v8!Ti99(-}F zBw#`uw-}eOu$Q zek<*2&g4+eOUG_om$7}UOzg!gp|W%`^mgZSd;8|97=Zy$x`TWn@rUk{cg~P1f`ED5EQZ0On$YejJgclI7Z6HpKU7^tLK^wpzf0qjP|ufzOLWh{L^k--6xmV>N%+z#Pjv(cqC5{>u{L=Z=E;FGe;=J2_7?`VSnKQGyD%7f9(|a(5 zcM{zlQ)sDNAjUc?ySncw{#D|++)BXX4fje6L7hQjjQBG~3QdPXfv*mld4z>DGX+z( zM?mwE3_|)Y5GK^cuZYUaUyZ^b@^-^4-foX!u5+4a&z@C-vso)zkdXpM7Cc*8D8DK2 zZQpr=1E9fL&SN%yYb8_C*r~D=_TMi>Y;$l5e#N5lwl)9e@r5x&YKJ;5D(yX(^Dmi- zt+8~-(pl*ks0Vl z;2L>m;Kuy%@fwMx4;Gi@H!8I}P%4iVd#_#q$l#fd06w{Bhk(_sfQwOQP zFO^)GH4QgF-*+&DXA;#M`fL~>S3yk&a2y)|&Vn-}BbapM20SY~f7-2RPDL0yA3~1N z&*J}~dwB-qctu}T<=f1zw-NR3tM}Mf7IL1NJM+3 z=G+>Ob<;*-F>%^u_wXD*!}EPCUL&MbRx79$xxHs8DXW^}4!R%S8c&_5IG$0$+s{Hu zv?Echt%BbDEExwpVPB1E5FBG|2>#g=8>WPsybxDMeMR4X97s-v`z7Lq6aWvQ}naNekkF*H>>efs2k+(r-P9>Mc@ zqPz7nn%~az`IY&4?=aA#!Cw*V9~eKsE(zKMp3l{wV?ws5M!S?ar5I6i0gmA*zlYxZ zSCvv?w*^kF$IQze7aqQi>i0TqAts{<5}Zo>TcPRyo$V_O=N`h)7O|Bt6R8QBF>cH+ z@(%C|EHQNU;Y_tmJ(l9E)6&xWmd|gZ&w#OEkLT9tH+IoJu!<7W4P_jNfIiHUp22kZ z_5GE-HV_UQ16OERoGzpD+xdFmcU@GtahqrUK;*FV#A8fV$X5mQ0%G8R-lgc;a~5qT zafwmxJGP3G>=9wlgZJq%-lvYO1p;^7VMJGho~qO){;$MRNs`olBO@4?!f@}eoSzPvABOvRv>9!3^ySm*Mkv&WVn1Lc zw%mC+&cNBhf2p4!2J;3h)Tfc-K_SgZAz4I%Kcft3I5`z{nY!&fTThd>KEKuH!F%<| zOQ>8Z6l4b}BuED3k=)%kFvCnihf88d*1}02d&`R(V))>5{-1das;70mPBqAjsZU=G z&F6d37I&bHS6@fpn|-&Lia!0vW%;S{vn?*lE(~+1orp+DlE?!fmx+)nokBeBgaWeg z)!5k^DzzCiAP-*=N4c9aeq$Efe!jm#C>sBWa!~v?D2IQrk#Q8Ksh~g2HK`}}@W)vN zu63J@Z(I@8>OT~wRP}&8PKSXrH_rU2fzD1~XB5}(9dohU+h@@3NrHtkwC^fEM+xlDl|}EB^Av6f$!`u_}V)kx+D}N zbRGR?`h(}zXS($cY2izIN;^!odVB_90}+fNUqHVduu@bDQ8KBJRL{3+(dK^rg6PmC z*tE&?L5-=K`dPgr#V^ObsRe_GCJA~M55C+khbbqSc%Z-_#-*{JxaBd)g?Xp*f(8H5 zr7)?TjmoleNy@$JDr8jA6$tDNKrujkgM$2Dqc7v)f11LH$QjkntsS|bx#a31CWOSs z=*dGicJiU=MBjwy_r9IZGRgbxLvLpc=*8+x3>BiO#E`=sm?)rko*tF+Xo3{CCtm*f zO}AY-S90)No;Y8wB@{#3)c24-2IK8=ecx=43p?dxkpM%2= zp{C*Cl>7I&{<2(ueG~L0xuj${xD%96NOAu`(SZT7K-mxdp5I?s2~czh@0Rk?Xbjwb z?ggtdabI&qeOQ#bK{jrxa+~8*9`7q_leZY7yc^>rFK$nc+Yc5M>%8}Q3J#7!WeFZ; zrz*kdBM3wgYjL0Vea+_K`e?CRukGRo-S*b2ZGrTUYM(H-OZUeoHfuja1r5_N0TrSy zC@JcJR|f;eh<`J3`zJ2MKg&(h%C{7pmtiwN6z&XbApDfPN4Oy394XlA@h_$p4aAcV zzUsa7kc;0qi;lvQ+H$(WKm&bDNMqS)V7XP>?;R#9M(8Mk-#?uL7X!w~MHG<3$I*fD zVn`K^I$a~V3XZkoA006&u|dyI40(ha2J;cjlK%e2b0NPFItFHF;X9gdtG&^zkOf?o zY<+_683zjjvt^PL{eeT2aD&E5h=KIXGuk*@)qQIxt5gk7jkzX1WSRq#c^w%ce*5N$ zXS5HsdCvfjoIg+9bey`o7H!|QF=eQbqgPcqC6>GdXy>y2 zV8IDVVn9LdEOO@mkJ{va>!jRbmz}5itfC4{rVY#@PXIwkC!js+dOo88-x0f zOm889?*h#TXfs0#i6@PSpE98{>L>dVrJ^jxP6ZDy1{$$ z$ezX?KbKoOLZ+p?jI)j!t0eVy2%Wptm^jX6Yts0$?}+@B5{muF?IK6#JAt&YLm~eU zvzD<=_$@#x#JB=xz#H&Lz)yU8pTsqbmk>taKS5k1YY=fir0;Ho_GGse7?h+fMxALOa~A_0G%F^LXRmvx*jc%o5jL z45?e(#u4ZyKYf$kGJLGorBx3a-nrIxsy4FNAY=i)DhHD`dos;8`ZF>pe)bhhMWxld zX(Y1c=iH`!c}UIr^%<)X%WKq}$-w&`N-iC7|2}3pvPXQI%e5Mo&JAC$Qm@SRz5ESx z{J*4n&%;O$7Ds~RTFNSnnsSEGEj!<(TObAfORM}Oi8vx)*VMfBIYIP$Ah*L0KEJF# zewk7gM#gF(5+YMgsj?GV6IC}*?I7Roe+ynmQD+)mPHy{%bf)gR@^SEr;oqSGz2>Sb-~qL*}nZ&A!9tpOSOeC#>^T(-!LsZ*uNb zEbsZIgG%5$K3DTrhmjCk>Y(!<&_Ey_NVIA590c`X)dR6NHX@tGe)1@)^RvFQy%~6S z^G#}z;H#qcw~J2Kzn)8Ze)N<{&aQ<4is*xaf>PUmZi*pnHm7$5;^xDR4wVSX zhu3}CPb7W~8S1#r9&ujs|AHK&VHR_mW8_eZv-b4+LWW+rV$qiT=zjZ~3A8rCAEJa3 zf-H2bl!7;>xvp2g;r1zfdaQ%H+0t-YGg!>S$1I$CwVs@GzAjBqYIpfP9R|_coad@~ zhD~GdL>+rW$xi=+w}q?mqX=5%v+(x3LtKv#??^fev;-1PfRCsaZupBZXk#9>230gn z*XD`<=!+`JCm`Ml!Jub?Y_~eizG9)FS)v~vRdaJ7^L%*_ z9^Dd^qBoTMwI7z5t{aIoqkUh^-FrglVkef*;7XzvdkN|4{sX{5gSHg|XUSThj86JY|B1 zS{gn8*&isCF@ZUYh8;1K^-_W;|XZoSZ#OhIW}B z>&2joU$SDxo8Xr3veZ@MWB=XX`IXJS=QK|tDg^biO%OYTF5rlN6>#exTkJL4w^#1N z-M`b4Ugbh{=xF`+uy?MDHB8o?T6aJC>z&uv7X}Q#qm|rQ*^UQvGSF;`x|y&G{g<u$6(m;#1|2gs*yLw=WXb@eXZ1WCTeAt$j3FOd=zqZ zDJiGQ*J5}qvfIKkt+5ILheJG*JUOgm+w`W61T9wC+ArPmf!f|`ur&1u`Jp!}1g@WM zk$YeFa_F)bWqrM}`9KlgbyYMpsxac`=jWGr3^3s(p<*Z9I|;;sy?<}v3uP@MYkl1` zP~{O*l8ULH7WqcS>nbo@D776h=NbDr+7xESV6Wf81)1UANPdKh-F+ z9Nj*?;jo{e-9#}7-UNsQr*Tr)!I4ic*(E4(nyLmg8?QV+;pxz{SMB#UdX zX$X25`GuzqteYN>e-#xvj{c3PP+_g)`JQFw`)4++bRc6I7)}t126+k{5JJ(b zvtqT!N6Dci)~ z5b_hGP}ET4z*YA{*%^6Vu#Sw5E?iiGy6Y)Yfh5|f8Yq8q=EFM5P_7Ep5^n750|ABX z_#@v|Y@%3*wyMD#i9eOa0dpL-42-MEf;AjnbqC(T5=I=ZB+muMA^~Ck8TMsiVWETu zc59eP`3(D3xm$7vJh{{PtC5`k2|3#VRS>tHVN9 zc<_(<5MgPIekwLK?ZH7YgV_uo$|{cED^e?j>ED{OrerUC_$j#OHUZno?vky>Ox~4E65mB#t$i z4-Km@gblx=yG<8NX|p&pw)R`-A97sY_b$(v_`NG+n8U{;Bj{}4-0D<>FW$4@q}G27 zduac?q(S+MiK`f-FTuwO%d171_qnM2?aV8l-rAYb3NmGf_-P;&NP2ETDYV&YelBV2 zO{N3Ndwiu0(=>nSD$o0g~L|Di8s%;rb1V<4<_!+H~Fnp{eKMM;pt^xJGh?d(IWRQUDvkPFBzYElz z?u|9dGH&l)7F1{^gadxLm%miRty!@U2C3nywh&Xy@eW(p1-4!^yw=^on!qQnwR* z)>?Ixhca30N-xgqk94cc935r*H*8Q`tnySwv0$@#zd1Y$|Y zQ{ELToW1s}=Oe3SfG7+IZ+s0h8D3*D7(HZX53(E{kk_6mzFLcAN@NF)e(#bN9jC3M z=P1-q8JlIU9lRqpv3!r2&<_)%U4Y6R{BmM<{nV-%40RZGDko6&D4mBx8nd^b0y)c? z?_6KQ;P2_LR&iEmzH?!sDEGi0Ut6oJmJnvh|d!{hV^W)mpz4&Evb_ zrA<;I^DB<*xLxu01bjN&7RlV(VgGfljlb#}-obSfhH+l)it5wy)6dt!m&u-ys}2@| zzqL@#|6F}?qx!Mf^S`J+ZDJ2?9C~=sRs{L{?Uv`#8Xvz1wmdm@xk2`vA*J#dEpcIZ z|5}@;e@Qw-M2@`^EPe(~Z3StN;S)ZO{YzX8MiA8pXJ zq@emYCZNv{NB-UfM5#)~^eN{dF1%lFOYcd3lkQ%L5z9qZRl~f^R1G(Z<@pj3d-6S7 zfBaM<@FB;;l=hPHCYa+3NQDanl#YfR~p zY8S)EjkuujpdP_3(lg4qhz`azHcSPjWjd}L z{dKDO!#zgkm))A282dxB87yt(HJZeY;)+JK>Wu?5Ru10%eTnN}%`#@YjFJw;Q__oN z_d0j*Sri9UUTb^w^6W!XFA-?&d%;mf6$0RPs#cOc%=Z7{{#KCY%USU^-fua_PZy}p zdB5qFNB0HzhsrLe{vMRrBPjJFZYnXf)Ud~ea;+@B`#C01yg?4?7F|W>|hZ?ZzG>yRio7i1`6U`IoIX6h{}8c zwt||~GE3-l*Q)a$(4#S0?PsBHU|W@|-8_f1ZZ}kC*tES}CnGP$8RXdWPG++me0Zw7 zuzSro>zBopOeOnrj-=f8R_YkwHa`8dCKhxp&|t_aHTWUO)TdUc{!eBNB)2+MOaK@W zF9K1>Qx-$3Q-5R!79*$yFXonBd4!XQ&1Z30Za|*@2o!GR25}K8|VJ!8?h~VYIqZ0Z!j`tm4x^hfXxB2ZN z=bH0d{U{WNTpsT(yM{R>7ij+}gcN+9v?+MExXi}x7TndgVsRYH=Kn>>mM97G4dQ#^ zOSgXRx3=;7*LgcZ6q&+pgs_J8c7RJrIS;-cS=~X_vyrt`5E(JPfb$q*Q0oan?13Sa zuPpg{QAS8{EMA;{?#QSchTDKEv4T^ZF3t@8HOFYj>!<}tPEKwaWnY#t``p*vYsM9Z z-d3EseszWFFg@4T&u{DPdHsBI%c|tQHHyzfC@l;}kJ&6Yc3(51F~Z79M-g^0Q@75V zKBC`JO8;W-(CapN8(#0CH!RU_{>VMHPU<==Hn8oInL1@08MCdUg%KrU$mbn$P^g_d zA8@|hzF6D(fyI53_j5KADw{6-yc3RNWCv$XEZl$_r+vT&Bgs;-E(lJoYHS%0?wnQG z`JdDsck>hV4cy7ae1bV2K7(Q7x^3%JlKv1Qnsx{1Y3w^YE~chQbbQ3ZwC~((`}Wn( zc*QBDt^qnT*NLIm{5>7#c1|YV@E|9tuElAbg$=16l%Epb%e{y;l z7;j3xU}9uUh7C>pG$A644t(-Lim2e+kxex@=78Z5!pxTg5=#jdohEX#XI)sjg!;X^ zDW|sTIVnGT##*s0nYjuPycRVwM9h{0vH+oTG{Kqv$A&*A=KD5NIVoyR0_q2iPK_zXm&Ia;x8o}y8y>qKSV(K5B8#!*SjAty=o`faOITS6d+++izfeN> z#y;#e$v)R|dn%G81tlBC;tnMj!|dqnkR;}@$7nhhb`^=-(!*2B zIxMS`YNM&<`X0D}3$C=bvN)O)_9!~Xu^HCY6sBcVtkG z&X+iqem7eb2xtD2ZEi+vVb~Tdv@A)hJ|LjdFy_MC9$xZno)aRS81OS zQHTBGz)Y8b3P+eUfd7)%12oDy#7&QDr9-U#-x7&DgQ)aX*b5d0qW~G3kLE8Vh~i|c7#Trz(f2Zf62&4lcezvh#oFGO3xwyeYdiAboVm~+>FM77 z?()$%v@l%91rdt`8UR+`wP~tdKf=FAMP@zX>Du91H6iO&ZIXAn+-w@1i$y3DY2JPV z5PTHX=S32zJ6_bD-Fj1dO;O~Q(&1m*8vHc$B(lQSPTRu=VZA)PeE`Y(sasgO`S^`g zG=e`^7`LNMU;

zH0}qvZmmBdkXhBHgB{9c5BARJ+qED~(U zN$F;@n<#~CDg*S#rQp-%JZ55k_vU2a$kk%Hw(W3m+tkbZjSAezO&wym&Eq{yNZZq4 z`ZQIkH6vvSLr(3VGE0AsKE4_IT93BybGRVExtJm_1iN3fYUMC~m zY_E?ZV&{8o#|{RIX^gn_Lj4=p)L`kJ>6j7RJscm|UjlAY)xa{G$?vxxx~FcxglWX> zEhB}6zRO+qPRvkw%ITmqname-hy3oBsbp*by!dHq`^c)o3-J>b+l1o#U4t!LS~iL}Qtc2(`+D5gG-zcQ~Dsl%?k7Gn)X-0M+?SY%C{<-J`4} z$n+X8JiwAkMq&ZTTP=_|7hrRL$9led@o!kqs{ft!oW)ceDercGlm}7si$Ybv5{Xl7 zVO%6*CgffGa@XK5sdCDJkL*3GxE)LJdQ!tCwRkUl8o6vpvAmSF+TOZbFy9Bv?a1vc zqInwTuDAMKxz6RV*1XzW(W{c)V%-N|cIdOq1w27#9xL2qhqh#Hrv9Yn# zJF+t3{~oYa{Cda}ehM<`#{kje`zKRK8q7HV(dl#GMTQ`qyNmYLj(4(5oAy&fv?fkP z%M9Nrw|p+)6T%e1J`ZMuKwH!VbGBZ$8Red=QUMwc^pu*&58#|c+vGoG?5E^x7}Hww z;}*|Yz^KCKh;185g^p#d=RqqVCIZ5!#%FN}Tdn>tSX}i6%;qR_P5APnzUqtBhNa-MjeI8yctSLGHIPJoVJQknr(w^N;BCPZ#=DC)e`Ol+eA5Q@c zGn`N*bewO#X{2ApGmZ*(EAp@3HKtR2G$Xh=?nrKl^??Kn@YE!KhqKL)i1{Q_%^xc}c-eTfBc8dJ7&kCvYvoWHhrgv z*gp2vQY=`cn&fy4HwwYD_?MXV3Kv>+N5}ObAXS^I-!eI}Vv1-MUYdU9GfUvy;xeVq z6LXoEDCXE%w@~)#Y2zqnuQ}j8EpzzB$7@3jmPgmA7nk#^5F&iZa@$DdWIM6TAkoE$ zjhJ-bm0dzl@+LqKJKFpe_REJCG~M-lE78#4ZR;683pDlkth|74;8xDi>xWpv!w(RY zm!EiSNC0R4U5S97P(VzLRD#D_@Uyb z!y1n+8gP{6%aD=`QIlGmdJvRj!$Wcvk>XWW~Z{|h)!`R4@LV$YQB$4}E!wf~au z!_@dcd64~Oor^r)*oa!oRJj*3sLdVdq03>YxG$mZCZ*8vv&1Ob!&C2E4msNKFHM&` zr^-m=_;uxp9xb`*|uQ)Li1@Ve_&Xw^&|{ z;|?2JcdXT*yO8ZE6BpC^2F)=_!*9PfS)3ZvCZQ>d3%gx*)6&=z`Wv5p&=*h0ZJ&gf zL7eNqMfp5u|BUj%U-j3<9a6etyAbwkl7VVpRTw4o-$(g|=0lqP?fSEhxPRxkj&wEx zU9e$Aac%q|mN}vN$?uA}RRbM6wtsy0g}JhrFa4XY1Do5EiF~fYX@zwoTXY>4@6#K` z2=ng~>L6^aLg*V2ztd3kA-*~7StIc;gqV%|LU;eS*H7U_@PS69+8N|T5~NPwJ@D2k z`uqtl4GV*`fEQwiKfb1}_6YrK;=E{+*Qv&3No(F8G5i0|S@l0M${yuA(~j|QPu9k% z(u6YmIBpal;60f0zEON+i{1w&hlMu|nhPPDV#haUcy(y;n?N>FegpnORuxgmZ6_xu zhh=FTf9_(lg&g(+(?Sa6PogC0U`PgMv|WnOxnU|K*i;GtAQbQ>Z@^(ksG?xzVTYCw zn$iK4&VRd0a=z(Ju=ehKX`CYSCd0Q9d1b;9_tA0F-&uY3#+p;-&(C~3m1EP@7w=~| zA!vBAOrlJZ$)Yx5J&)$vOtI%Yr?)9Ld5=|o5fMtX^Q72~|AgB+tL%V}bM~mOs*jPb zZo#+JF{;(v%2G_Kd8bxWe=F0%IoN5ucYlkIDHbzl)J46fJvw6zc9zA}UJ5mX+EHtc7eMm{H3YoeYZDOl4{d+>Vv-gir3Gu7@c7o(A@gL zU10>6y}q*1JX`S9mk7v3fT&l13@PL>_8-HA#HxmE>h*j6bc+L?0g&aV*vh*T`v%GA^}40602Vo0tlvb` z=`Fq9ylB2vQNh_`KRgC3<5&YJzjaf8r|rF0HNBQx{%g9_CVVru@&{gheO5h#uH~gA zOS7y)j9Ta|t`%l>j_SOAdV(VT?$V#MbN#2Q7HJ2;N`jj6_dXSk)QGrll*- zLB_Q>)YaD3MzdZ8bOIt??Ny+@~&oJr^Oa+eW8wH8z4-Og{WL&|3= zw&INf=gYa!$(=jWeMTt3psHdBeF+hsFAnx;y~K=xSj*|{ zAf#>WGUbtaKlx+U-t0orEOp(M8&)DhrcD+jaHPK z(0yA-^TWw|8ut;#om3zrL=*gVt%5U%&JczPWH(=OF8rlXR1$|7{ALJFD_wkr`GF?)jh-$Cz(c` zeUXq~vQhk-09jmjd=A5US;1)N1?vmpMRdU;w(IanFOu)SwmtY z$?`?h+u1|agxI(Q#hrn5$z+w6FE0bWrSCzwMk?dKyHL3B=<)kd)qx5z+JeO=}wNuXiG-V30< z{rXMl68efdtjNxClJe~d9L>d7${k1B7IF{`WWK7Y0EwWwn3^m;*1~LAxL;qN9XgVu zyV)(E{x9=e`G?`4%XfMu0ku#72S$tVA@pVPo4awbWR$eBH`_S3OqlV+b3~Tt;VQo6C@Nc+K2)?~=un1cP6- zn0tqx8V%h(R@JG=9+-J9xAuH91OUhm1xR%IO&kK4 z&AJ)ky{Q!#2^ZdXfHBlgK7uLC`Rq>`MO7o+l=t(dEnBW*{CPi1#MahU0CNynd#qt1 zBzICtNr|C52l2EckPG5vKi&;HTv%lL!~lly3R=FUCQCHa6BCe}XUfnomD4 ztz=Uub;DUP=&j=%S@Rr&^Ic*QqtwXpJR5%mr z!#5cYJ8&!+Z@>I|SUo!Sy>vrQ>;WDsTeC(oMb)spqjTkm`jG2yO0SXe`3^f&5|&Q1 zNS4c{(lH$7SvxNJYHRkh$Ui;q5h|SM)G$!Z`}uZu!v?xCu71C4Iofq0*Xcj2Whzuf@^h%ugQ~u%Yex`PRSe`|uepk9+PV z;|xxBBOG%2CS&yVIZcC3vr%H}c493~`JMx&LBU1=%2!`{dUz{E)zgh@7OSci2UPs9 znKLRXr9YO+rSaZ>NYN>MLHy-q6ywj zeZGJ5BJWHE!=5+cBR+!p)ups!skY6FUcGvC*St6e`W)Tw>Q3^Vg^{6IK6>j|NqC)b z&;`ut5HLuisS&go-(SEo4pgUJ_6<50;=Qo<4$=B*QV ze35iD*V=VP36QG2lVM~-7^Ol7Z&6Ki%Dr!~40qSo z(B-nxa#8X#FK;oT$Q-O^d5TIg!Fehk-257ig0%*D(OzY7Jb_x1D!?D}CbX2?GC=7H0vuwc-FFWKE)yhNUF z`1a#R3Up;wts30|`{$0`d1c2|q(rLHEBIL&(IFc-8jjXnT>dsY*fhwFu5bIHv&CJ9 zt!{Vq?y&qYUHLd$1;Zv%34E~?&gM?iQ!g#By&$jfv)7iS#AeR-=5sbudy+B>!F_aU zj#iArJVJ?8B1wAn%HQ8!Ix8-6LG9YDeD^nZq`olcwLv+I( zvL3o5Su|3|^XJcC5A-eyp~c2csx#mJ{+zC=(q|pz_2TB`Qz}jt!W<@@U9uS(EX{=< zZ9*pWGHOpf9j5ORrglHFve<`zlIX$}D&F}>wcY7S(m|HO3Z`B&=WK-oIU2)lYMZBz zZJp>U3~A0^_)H>Z_zwKgwsafAUCligL}&f>A=}_r>$cI6Tg%z2?nMrMzP?X1!hy-$ zLz$A7sgr0mf7QX|bGYk-|9H)hdIkI40SOc9Be;@wn_54*qm;9_gD#}O*PPzl%U}y_ z^y`6c92${vx0~zV%6YwRJmF*^w|+QZgB)Y44u!i)Oiv4JNiW){w#D9W7yl7;WlJUB z&-AGxC;>(mGtu&{YM%0uTV^>b%&ws@BFrhF$VA!e7_h>N{6M_`_wx!@=nkq=&jqcZ z_fC3#Ryl`_5|i|JYNyD>Wqa*T@NvXmU)!CwZ0-{hb0yT(^`8B z#sk&wxqYQRnaXEkI$yw?Zbm=PS2_HBl&R`hLs0jLCC#!6@9EXrFTfIU8xBBueOti7j*MR0ziqsV9?pB)A~Pr@OTaVd)IW~WLr3-01Ih;vvWUj_=ZtguXI&l$Yv1C=9HMV|LR2oS zeDL5h+z^+qU8@BIH~`nS|48x~|EX68cRGSd-FV=jWi6H$ngYziZ|TiVegm;}e4mx( zu<<3;&r!Kn_9E}!>6jM=~DoYww()i|QIv$tI|DG*{50fxzvC>(QEW>?O z==SJ}TFCp^@;DEW~wS7Zt@eO(o!5mQD1Plv7TycB!m5FR%M>p~d3(nso<^5_&jYu$mF@*jmo^8tpFfAG@kGzT_F zrvy$tyYDm12f+4jiUbsd#V9ONi=54(;S@v@PB6_`+pD$2Z~;N^+Ccl|>%-N`5{Np4UG&yZKdH zp<{?f;q_l9=Gn&E+QzLnpjIrDMt6Pn>iEzfOyOaGO5B&9`keAzR#4Z7G!Ldd`UEOKRQplgm@MDSN#cDD7&JqN1AJb3dCx(qW9B1j6TSbK~Vq z?fXoC;5_(lxNd+dQyIP z4+tj;y9Fmi8QgiwI3ibdW+d~Jo94iZ#k64E{L)8>r_2~#gI8}lx#u+wmn1yYu53euxeY{zJs}J?*@~lvmJw) zyF3C+mT4u$*koK0(wFzyQuB-}_}gO2RjD6}$9p#@JErb3&vGvTG&{^!ncu_PyM$uN z^n*+2zNW4xiX19dR>NmZ3=A5V+5@kcj#1CPF6Da73=^boXbDG_tA|F+^---wt8h1HyJ4v_#F}_uu@p zJOb1P$a;5HsDl^7chaRd&D4BmK5Jh`O`4eLj=d;b?)< zYu??`b!%d@l9`G{=$qvNeLGj`qoTZo8rWY)WKtw5>b2QBk^I&izQaOFp}hlHeup8a zCHO_eZ=Tv4wNCWOp6L3gAA@Vp46dxn|AHP(qqXq>e6m+cuU@o?$9c)`s%;P3zt}iOI6-6AEw`iGx9@$Z_}S>}P7;ezAoo`Yq5|dO1g|C6kBRNpgFm>>Te-n+HbH^ z$-wO^Zjh*>b$Whwn`4U@OogDY9K$PmgtK_t{I3b3(OiwizNsW~2EW|gbRWCtg#$-K zN%NS#_cjOZ?xVAfSv~JUOfD34?~|U_TWM?N=}@u%yieNi^e^-S1F}r%4_B@f*&AWC z_gZ&DdR3n%`?;-kZ#gTRYR!7m-Z^d(yc%iMdMJn*A+&4hXAj-}^&`e~WlP3IW745# zr}Z4}6ENc4IjVT=dV#oMU(6l@@pgyQ7XwF*SP54KzKJlK^=5`&d$+*Y6CR3p{4)X7 zRyu#5pK;=|qKlg^0*Q)TH~-8D^r6W<=wR2~4*o8Hn9m?y(jWq87M{=7CQn)2yAIH6 zpJyqF6cCRb%#lOT4O!BjH#CsXGSTCi01*X<1^_?dCEzC{t`5yjzqLo&X4{XxaOifY z0Y;Q@?I}@d(5`JZ+P5$I`9Spw$6D^puN>{6#c}&G)~q&UI~;5gLSb!pb1~P#l>!;? zCO(Q=_$?fY^WPnhz?m2Kg1%H-?{+PZ#?^ik!(<~%%Z5{oZ1)dsOb&j8Z65@+R?w&t z<<+Em-Dt0>!d3Q{9bH%Tw%OZK#%_bYOjTlGj776sFOh^=LRo}2AnbJG2 zWxS!bDXdcSDG#r*v}l+IlAJVoj?Ux9vodFyj6{yqwn(lB{dizhquy=hYyK$prY`!m z8#~M9$8TL!vJ+Usggk)8?bNE+lgJ(*d8k4rb1-v?Lv0uY&gdvqX@Uw1#h=Av!^i}% z1=oqv3;A{eYq;gWUbc{)53j@L1E|ouoA$Rz9e`0}QoVHf1vu=bub#mQ*0==MvU4De z-oi$)%_zH&VD|?QJm!~(W?Rk!+Wh|JTdx>HSt^8$RwR@>bt@XFR1c=yXSB=V>UUoO zd$?|v%57^l5&c%Om*Wr21>U(6yb}T-U+xEEzl4l^N>I(c`uxM#}xVVU%he=<{X(y7M%3F&X&*-g$_OVEKh^o>9sQ*TjYz6_1rM}HJkKU ztZ<{0_^A~J-*&Y9cK*QhDev@KDr@saOsb)7t0Uq+682>-M&N`~9 zsO#4t0!pfMD)!x)G$iyH%u1x|>6HcSs3HcXxL;+;!gfyJOsczB`5(I5=mY zz1Ny^t{KnI%e_`b=~w8nS6-*oJ-pjS1(_{g%&%gih#2XsP7`n{Ot34C+Wo)g#I7-F zo%IQ!xZa$wp-p)EM;pywQvyM>!@5AZ}xJzqEi!jeRI>`V6AF+!QB5Wo2d1*$31!0tzRJFm|1mOfB)! z`}!pJo%lfKnoG0x&a?4$WxU%St#aq9rx$);ux5h6&g^t7dz2L4rqP+-7OHC5H*L5Ff zer^`c{OF-Zz;xy$Vf+{eFDN3eY~&oFbm*PVG37={!q}QS0EC~{EVkH^t6N8|IpbYlfHNO-s} zYaG-B#;XGalK~*O2#Ty78+P(ue_xdQ|M!6aU{8_C2UttYg&XXFOb{RIkjbigOZi_~ zt!n3cjCbCa>K^V698_4p`?dY|r_6j7%Kd_+568A7Ee7XllUmZmw{Ji#YVK0(+4-;F z?q_n<9vmf2$A`^EKB*tV&#pwDhGgdqmscP;{}U4i{FI4R_{p)&^GKEP9zqRC!_3Nw z&>9L(NN&5bN*nO&*Q^aRzJKn`b~VZr`wJ&EF~`=*`-!DxnzA||^P-@zaHig#dS_<` z#`wt^!Jp(X{%E$9K2-ye66lx*^+loRoN~sGR8fHd1M3Gpx$ywyTZslZ z$G0s4ck-53V)VX9WnddDuzh_}9)&-6W%8Wxn? z`L~a`^sOa_+E90!IXJlYT)mz%J4;dTcCLM-l@wA}Gk)0U+4wfPHyy3ho%rzH(fKWJ z5qNN@Bd=o+a|R^P(QaQ`tdbT9$c6`eEH!nk3Fb-&U`sfr#UaGcu9+U8&ZyL|Ip7Ph zTr1Vx>%Q9aLn@ll`PUxQFozHGN@oa=p?G$(qu^j_!k|-QE780}7xd110Dm{{{qz76d90j4Yy1BdS;_wQ zA}d);Yj9q_12dVk9e<&?Cf+r%1RAlO{MhQ5-`({a`?_nrseRmyth+`4Pbs3o?ftzK z>8V$A&(6iN*k~04vXehFSj)C3Qc+93+^2_+nS_)ObE`KV%C|$+kPtI9AYv z3|#6paE+hm_AcB6fh;??zpz^MZei&r2p2aZ#7SR50i$FE9L>LhJOixj26$XQ7@`FT zyE8^_f)4z3(Ch>Yke2J#!t_BP3d*4AgKf5}+cvIEow&Fw|IWh)-srPW-bI#I|ExaA z`Shq_*e!4e4TQd0jR#9E@(uCZ5^*YxbS+D(v&&gGM6(fRcGj#fqn)VCl$|TnElHVI zf`yfjn7NnKgJ!E!Ad%=&hZO?(GH*23VtzSv!%i!g-)vsX%1hX^?VBm2W&pf zt|M>Kn!}>SugpXa>#-{3_32GthrYZxxpQ2e62NcP-dBJ8pDVuFl0KEii&oPftPwyA zwIl~A#l-$KXx{8=6(0<_Lnr++w-Ix~dTCJPr^kl|E~cx~{Xowq&GX)!j&lUWg&jjY6p<5-$F_{+m_T3hdn=FPL8%nj57+O;gxMt*Wz- z`A%xc9!cUdSafv5BEg2g&E}lrZRH@7wLfM(?Q!T>C)F=(%E-s)ul7SJehEaUMYXMqKG?|Fi177>eG~P8-nXn&(~ef1oQ+A2WnQ3kXO4CM$y;Q_|oo za+-}=cK+{=qC)p$j}C^WF$ZD1ou0LJ-MRt$@>r<5dVS-?15d4z((z^($-lYU@H8n& zwdlB>il5J#SYM^QYDK+8aII^|{t5B_G#0>b@T#qbb?2fJ*=%HDM;LxPF|KDy6SHd1 z7Ob)RF`CPly2^UF_#|!8tV(;xKPGgs@ie(r@z_?*#dV8p?#yz~K^D(>O_1Aq zO~BRqL~=Fslwfbfu)NdNDmVQSi9QBAX)2IO$5!D3$^V>vtNj))Zfpz_9MO0oD}NWh=X%j3aa60Ms_a;F%F{n#v6h3=OQ>W5+1W=fUYB>EUfsjTj` z_Uy3;f)`dv=HHeXwkLyv+{8yhi0o0mL@~laryb{fuJ^-uO|AGW;Y?XnK;6jYe2;^B zegHEsMVo19eIw)e%Dkb(Saxu4HpU*{jMGht%Vxk*ahAp&{~T}|qdfP8{sT0}TlTw{ zafV6o@-;Cs`!M=$aQjxr^@8MOGzULnIk-7uR;A(1h7*)ztXA6JTS1(_yuV6NO# z-_^BJgHH^TOnO%a3tsKX`{=lLv6;I`8~YWG{_3jy0s&jmhrKh&`mHhL`G?*D$i(w| zIYUlrOphD3WwPqWh!bJg^LgDS_k#qF2Y>sUZNEy_orUkD>OUM5;@- zb6DL4s|HUwQcgW^a7&miX%g+($We?^a}e4c0Ic z_vx$cV8@>uFQR_5h6=!=tCblFMLkuZu$u?mNV=OVX0wn4x!ZKFg4!|QWESgB1v!~;p7=J7FS%m2g2DfZCAA4Z2z>f1Yk%l?I4uEJW!2IP;R=qPRT z{Hi48$4L-4D4Go^GT;_j5^o8Yj+K_-G!YpyPjAh@C_MnaFpZW#m;h~PXDkbUw zX}gna?4^UgtshLP_M88hxUZgYZKj%H?we{~z$Nu`T>1LKp-D+w9RIkl`}JIb0TiJwEMYQN%<{$ut7=F zQuWLGLvh*INYCk9V{x&|n#z82ynlbMr1h^R2t_gl;-sp2 z=4HhwgpI3*%3q36>NI**u9~M_@e@6(3a9a4gpP;I0i(2wTx6P#LX#2 z>7+CBLW{@!b>S6Gt<)lB98>N7+6ra{u z*Oezzp52(hyt!Civ!GL~QiPUD(6h@lLE`V9PELwR_dCp(f1z~xnT^1Y=6>z}E_(Fs zUa`u8+B;NIMHNwN?0snES*K;RgJ*|%cL?mE))bBhT)jb8Lsd+GC z`cFw^&ip$G3`8eY%r#ApNDb2-o3^ejFR7|(ulgJ?*jsJRG~wfIzUak`6o4vOtrozJ zZ*%97mk*YtCfC_tRr6_`Mccclm+wTR_UTK=KODWy*(E$5k&nN2~T)$u3 z$&FWv=aWx=o6qDqFo&n;BTEJ1TldDXKB8xQVI#v~;(0((}Y% z+}-ox+U~3cL(#R8y!Jct=G|S2j^k%cD)fVqeapr!@snHStnD{>Us}OUjvjYN-aLe@ z+5iElGPuodB>W#Y{;uVPg}3L8jWBW6$lRf~z%OVkD<4Ihe~R$AWwjaM6G>x5vsUJq zD~b2Bw_Y#}a-;0B>8E|TIlg6~zgxR$;yhtzB#HDvvY=-$PFwI(2%=02)Isf6uVD)( zvpYMv-Nmgqp~A||$%ZQ{sEhZh3A<2{e=)+@@ZdU*c8EaSvU%_+F~?WTo{8J^1ssB% zb3}ta^_5pe#Ta`AShU^ybomCumpbqP0(%Ch^H%ecx)XbIj&C904)gFN$mi#uhlJ%= zOQSvL>3^xzU@;-&m01J3glc1*`7I-c=Q5(j_z433=~?c%+a3YyVRBHlj%?FL%zpFT zk-_82yG-^_$=W2G_o7<19?pUdx@d7|gGkFg&7vN)AJX3QonH6j3E6fu_N!Gqyd^7I z=&F?BEc@0k=poK*0=l)^XH({RtPx4m^emQ2<~=u;aa%10147p?3dKek7$bdfI1g_e zaICBs;FH6VWE`WX-nJ@&=0DIitspP|Nm}~f$Y2%At=|K&t$x>&QYagSX!qNDu@h*!fuXx}qUItwDE zGYkIsgWAouvD3(mPk6{i!%I~!6fRhgMB+2Ix3=!>`KP&bfV}c;+F>bwcY$Jj%j^=%E>L_Z=A>-Z~E^)-ATE8D6k2~{}b!|fD?`+jD^L+!b~+1`;*m+ z;e1H1sx{LOnB%O)86Tva9{Vc-AA(Wnib~=^#=p(=48%gP%E-RU3yiw#{(K#uctf;W z$v6Cus!GF$|A?`8b@z!rqkCheZ>{kxm!+o~c%^18C=xI+uz>%iCYSzRg6PC?yU8mv zjd5I%fr7*_b|y)WyYRz;WODHi8FRF9mh%)oEX7*1c~{Zs`y(j~ewN@+V@T#|BiY`9 z7;P7P8cEcbaOGU;G3+lMzY?+~*d7$t$_Ov+>uLi6tr?8kV?C!AS_kq=ObN3T6oqf@ zOpdgjlvJGJjby;_Nc~~VC=-48OOUv~x6R)Uh8aCwiS+q-&pD1xZD)c$vv<&<=pWY2 z9rt5Y3ib9&xTtEgv2j-5 zh~YiQws=C*!@kSu0j%WmMw%*Bqb{|*P<2(n8+&ou#7#t;jgum4A^vmHerxHzO#PzaF>5_?Q=z0nSdSa ziysvT9Jz!Kd=^B!zF)L_EKnhcy)cHouzGB&uD{v!yqCkoyHw|ldQ@Oy-*uZ)fgTsU z5ifbq#kGe=nL8Jb6_=-;9=8Bh>``h;=wTNZOXJtNe@5gvGP-{k6j{_TpQI2*ob0?w zJ?+qW{e0&x&vrUWZ7$-{usA$kAR9eJr6?<(2Zb3)^9iz%GNztN*2px4f`;bx) z6N!IIMIvdi>IegE#CVGUi*fchX_?Pt^0k^uw$(myElZuoL|3o&c}!Whc`8+rwlM!? zy6dsa8&RlawM^)zUPrGUx_(fv022f+*qC&@P&^e@KZ9l>C9lSyd(}s&jUbNef<)3a z5##Dt?wb;;-C^Wb`=(9xmGSERwQ$S_bVSJE2byy`WRT`@U6?*bD&sEz-JT-7!S<9JA}fY zGZAH`?td1jIKBTb!i9MA>hn^roP?sQH4gM|C z^+i8trYvu2b|K^2l|Pj%oqn=hT1uA2AS>rlVT)P+!*t6mxH6I3`y*}~Nu02e#~Jcq zWurjT=>zWL*_o5H8%e3QtA?85Z2^w?wCB*;H0J|hO^P6+U%+E?0KYC=gz&BYPDEh6 zCNEB~>Tj+yON&+ubm#;c(kc^a|7a9sGi3ovsT6H*r}z9e-sJL{-7GRvLMjL=6LjfAlC`n6*sBW<|9fVWrV&nY1|sEvb^ z{?b=2Imc03I0IrDy{eg@dULvxha2uI(#~>EuFHdZ7jp~Bue_<9QaxD-G8Rk)>q$D8 z#~YiT`=xnznYiwvILC>Cu^Gvyl1) z-+>L_K`2}MAhw}C+F#0(C-}0|AJ$O{4~pi${(U?pL@C^_YZ8PUB@Aw-?!?axtKL=n zKZSpKa!Zgo67v60h`Pqg`gts!gtkyh^16Il$WpdpqHl(P06SK>a(QRQ!TZX)Q~5^c zH?Gsc=hmJh=$30%xXGi)nPbh}nHuHP^7e;<8vC7oo#U%q(w_Abx$1j_n!UFlSR?Hl zodjlQ%yAm~yBbL9_e+bi+3B98jlc<#(H;#^UkIgrqc4VQCN)1ICig$5(32x0zrE;8 zoNfna3Weoy5{S+lI>lI7`C{IEeFLjt zIP;yUSGN92>B7lKylIui@2-DfgMYHNdX%!Ryjt06-xI|%eeXKzOQ~be5__1iO+iGe zBTDOQ`GDKkqAhl#+MlOhnSat6IP>pT+f!|!RH#RXGmNh%b=bu1yz8T{Fh0g!*>spg_!OwX8fd+vC(DF%x6Oxz6mq` zrbE#55IC+eO7XICGNyKHR42-ej=Uy!Jgyyh*>i)(pFBR#YLaCYPSiFD`enPU_JnsQ zFQD7kIyk(>y;S0(9VQrYGs+4sQ_xsj{^*Kcj|jE+`yp%eplkVJlOvyyWXrwh&mm!3Ub;lp%?=UJDJWr?N@ar-6on_2zTV_h&6gP&wK_C3SLUzWsQ*!eO(N ztgU_dPqKR1zUwro`6H8J=J9>WkjdEC zQ**=jC{Hwj0{VIq)oK##NnffpMobAmIO82(quSq$C6#6%gv}k}Y?hdc?_!s`^IMeU zfjFS^@IqnTW*LS5-(_&Ah_cWMH}9@{XrPhxKcuH!5X;xmM>*h}JN5)UOxbLW({R^K*C&+OF82b}C07x(_dx z&t25d_dZ=lEBlY)I}kQU>Jv;*gpRpoSJqY~4sjhBZX7-{8=`aukv?!I9owk{_!ptA zG{4=?w)g%{rB`*g-!t4@gHEuNBfX+7k?a|XkS+J15BT~8iI z(!2&|NmB0%MX@1|1coP96rEqbQBhF@k-u%OA3w^_IM-iOF)^`o=ZKc}WRI#*P%|E!4Fvc`5h}Qs`UIai0(W%c1^7z=BJ8}O~|9I+__TE!c{?JZ-$0=)tY(=}&6buqNd|%k>+C2F-yt55*%Y!Q-Y=p<# z7a&y%GM92eyJS;}x4oYw1#c4n`5#j51icJ>Y^MBpw4QBanK-wn4{TBFAvdr^D7);f zoJ1Fo)GCOi;o<8hs0p_6Xorz)0spY|D5O0axzfmMeWumyw~+?Jg7jSU$X^=SPy1Nr z5A;5GyC!_tYVzqipt>wl+`e1s7e9^O1G!4|_&D*F=EvG-II5H|MFoXzEYLdlTS0O{ z(+0!SHx}gLYNy4EA|bb=YNHf-jrWI(a(rIU2BD78gzplD_*;lA_Jum3zB%`Nh#40+0!AVRH1&$juU4au;D=~d> ztqNKBp?Z86htb{L9%wBT$K3G@&a}7?+fCYujLoL z?L=h4r%bJHQrv%&V34_+C)b5gU1l4X39zC1&qzZ)vrmO?52!b@{rM^DKi9v;Law_W>!+cs0(t`g#4S$2>LZ5U1f=!9RbN8PPHEXB8xAlie}o zE;@#|Pf=Jd_5uyRS%;7^m87`aZs6!iOLa#s;3(FfyCMJNRT6I$;wuDDiqmmL*@(2X zA)vm45PCBuYs*AOJvi2gFys0)aa`7vSsaso4xNArgveoFd#)~x@t8n)NlC_x- zF_7=f&88Id3Y&RXCW7ismWw?_4PC zI;>h5<(Ut&H;9VqUAfQzbG2fvKF7t0TiE`wyyFx2m9}ki%g(~v`h1(@qd1M0xt3Ls zd9ny0x=13r8d_+0bi9t~+FtP;ZIKi6TZU8+g9X$ko*)vY#G|{R@sfa=$tof8h7cR z;(&{qGcIyo&hn@sREn1}6Ta(CU>?>DeT9TM~ZH<6KFG!9K0r zUg8bEHr*ADd|l%U=NR;qso>3v6*)pD2kzk&4*cz;gwN6nXnqo?`J{NweL*Kyi(yt> z8fq=L16#uC3@-lYpADUVtUuSH4U9ITRGef$mo1LSkP(fM7I1XQ+G4~MJjks!G>|I! z(Q(kFXQBRNk)~iGcZ6E(xxrB+AcxkcLu&P*=-Z;-LiAj6r;6%W@!Pa0$x|MyHKeKn zGmD1?MS(g@4g|NA?Gbd<2Jd4IeRy@1{w2RA``jL z!%Mm$>p2G*c#%0x(#x&JGdcgCvh< zl9fnkbe;2~LhKDtbFCje*=n2cW5dAh{|{mX`+6D*ruAFk)J0jY1VtK6KdMyKZ6TmzP{87uJ;=BQ!- zJ$;w`TQYkiB#^?cxy2_5?Ahwb!j`0Ml3a1%ww<<6qaY6m4|?9Vaj|ml!m{g_aYvt* z-Q9i`Jel#ZEEpy6VT?Kcfn_}#9E=8VAJvb+K2quNzx1E{jhX|5I zD04dazP~<28QF(KYx}cqvqY=vE|eqhg*D?#^)V|cW!N{IXbfDFQ`qFb?-+*9gKuU= zBMYJCBt*n_Z}H%ff`>tdb1$wA|lRBw03BSU7f}E zzCe18!tKGxVc>c;iIKSHx9U^a;R_C5eifHKI+}zlj>}>`+x_`q3TLvCW@Pyv{+60U z(vpn)sNznlz**W**ES~ zZ#7~|l%?o4AfGYo4*qkX=n{>|gaSqINT(fUA5L(H|L5i*=$n;|aw^3v63-&0*=`|% zrz^&mG)!mCOic4pMbtKULcEM6uAG4zHw8 z{>BuLGW04hcL)r43uZGcad3cDynZ%E$PrqUN23=ab~$H(&AZ#ejCMlH9!|Y)4Xqcz zm+WJSjn<{771vP@9n$N@(`{cAz;f15s2mc$!S33?IPJHHR^_9mE&1t*=mLtuk#D{P z){fRnq0Q{DnD{#kv2Nf*++Xtd>8;TmO(E3gbkunWusunV_l5W)b&BM~zSn}Vjk&pY?rJNdZkt2B0)ceKM1KFkuv5FF5RTWM ziV%MqG~6VZYkaZ^N$lrTw|@OP|1o#zIbV^K7tNz*!}!nC4h4$n6d zGNqCJ(V(b4{b?VVlc%DHU4Q=1wmh$ZQ0x81dW@<<(U^^x=(l4x7Vc!4D0t&q2d%FP zg!D<(1%%U4;dsaD(lm&EW<;@OMB5j2|ETy$nOGQ+J4F@Q^0?qsnp4XO2n>i39tP+B zJ>}e>ZB9 z4bo?zCFUR|W@7GE}wEa_H^Y{^@XkN(?L~Zp7aI#ni4OGWNA+nzCRal#fWD- z2KKiZPq=}DPXy>Dn&>Yj=ZqwW72rJ*88DTQNyoUoC+rw1A2S?h#%Y^FkKP# z7rpgOxudFD6k&*%!^?8~H>);D14=G9INH99JQ2~_3FrJbE(%P%cX?d!0xC4&$XhM^ z`_6g96vY7#EUsMs1_ z%3TLE{+;IyB4SRnrkF`@JZP%i`8yM&b&H$!Nil&X-!Cd4U$QD?gEJ0xg*}_)z`#yC zJOn|q*~Mo?6O2l6*vTU*(HM4_ncO@Kz7jq^(ynk%gstEssC-)QAkn^^FW63A8-ckp zC@1oUHdmk+3CE2um{TQ2e?Hy)M}gmV#8JDAxUWixY^?wtyIwbrHt1{l%W)d?xH*5| z;PN~Sa=RiI-iKv#qaw+ON4I}0_%yV`vk+jF8RHurVYy~k9}p0*3`U>UGlLmX7}8h` zQ2>_91CXO+4zuc~mM@+>f0%z8!rhRyuOFF7anVm=)a0n8F4{L`3r7|<7!T%-<&V zK)&X%&5MehgFdi)`dNE@Mkb+$ApZ%=qhN3-Vt+efvWE5M1S3ZE1q(** z#6G4UIS7IOPKevrm#^bx73O^gfgE45TJ0e@=M|hSbVIF31?^?|c2?G~s95^ERq`Fi z>fi?vGNQGktw|0NVKbCl?S=MrZ;GAr#$m~V<8_=&ep2a~gm^KGj_8CR?+a0LtO(F2 zP41Ez%_c3fJx1&b1l7L3Z~VZbFZSBDC-izBgC9qEJBysf*s(o6`PC%5HnDbM`}g;` zM6tLl87YdSM|1hi+L{!{4qLt(((N0 zi2Ne$=t;?T)CoF`o!bfUgk!pXjTv;Bu@tO|MpQesaW!nMj zTBMSPS($2?l795ZkFn=4lJ9-z_FJ&6zL)eIASfyLD9XLVT zfQ$1=`D>u+*viD-@%yfookb388gm?FlZ^=z$&4L#2To)SQI7HH5sdNvE^sr*mmJv4Q%v2wEWW+~8Nc~k;W`F$5|I%qv%c{w>izb&SD`$Ae@fuC6NzBJv z-4NY25hHx@V@%GOl3(OL?(V#!-Qng*J_mT zZbVf%HlAQ0&-Hr6U(=(gl8HFDe~V(!;LjVER}Oa`FdWr0>kN-;WE8eNhxl{1f4n4a zG94QZ@-dBgU>>oXj)?oKn%nv}5}_PO+1{yJIpU%Q;*!OWR!l$K>-Znn0WZ#d?zw_b z-AvvNo(tJe=i$Jo&lCzlcAsvuMQg4sD8=SjxHJKXPBBCy^dEKvKAU|xG)ogoa| zuel`+L(CRkxZqw>A-tw?-QWACp(&>A-COVfjtg{&7VwkO{{XX+1^ET-`D1JHeM{Ig zW}nB{`31>9(W0>YKB?U&(a=>1zxPk(iuJ_|vS^_W;+e@Hr#9?o;xc6Y!ea--_ zDbOrvMz4FmRJHQdcyv$mzAa@pNNI6j%8V&V!*Tr=rouc2eF4cR|CV4quf zlzn~|Ci*75_LDh^YUNBweS!=hXai<5HzlCm$O0IaKLGFMuq_9hbKdniJiAe=GI3BASsu<;#!!BEDUi6 zM(VY^A1=iC0D&V*!fbfX1k9KMJtbejn6q6Cq&_Y6evm>@efR6dZ&tt@+jZUm!Jd** zCjurvOj@Uh#F-dCQ}fuY{K2J(K0D`({GB(OK&ltnb`Sq$A@#qUd&f0dwsXTbSzQ6zZpu9OXvEgsh$qd1y9850g zfM!eNg?w5;0Uc1*3wPr5ip%!E&RmOPJ>8CARR{*e z>grzrCgufw7iNKZ8@e&C0+Cfr;-P05``@~&0z#98s?hQ%EjKyXct4W1bV~&^A7+4 z`rqE=|9JxNx35n@D1aq12BvZN2&LJ;v)@pJ@ghr@pEJNj`wVcs|$$8(uH4 zzi;up<7^lJ@})a-wM@9dzJMpo;{AAEb^#h^U6$cT$HvMviow$K1GAoFgJUnoM(y3e zD&;U<}4_RFgioj6LOm*U|6pOVuJkN^*no@!r3G zKaQ~jX)-*GEvLqMfY`!kH+dBi5n*6p00XGm0j&?JD?Tu#3lwY0)#yX;SuzKk4{W$$ zbZr1|5fK%IjV&q2$qnlc?EtcxF*v7*h>L?!!ao4K)(K{1|BQ}1m(JVuGmu;+Co1AP z)a*s4qn(uGnCZSfYsUo2F`B4sMMd)Dk>Ed zredC4s8rnK-9!J-5T=wfWogv&ZSgihi0gcMSbAb{IbIeUgS`OIlJpe>#1)a!DSLpl z^oR0+=kaH>8BG29v)wtO;WNM8zh3fw^7Ju{`(JS_Xi!%Wr@8|08jK(f5Q{LOEWqDf zvEdHu+<{}@gI$Z=gtBTmY(Nf>MZk7}&BXzQgBMa$2_;w`rKP3SZh_X=ui_(%sr<@$ z*-S?>Gc$ZJ7!Ky3iYf<6a~1!-H3Qv!`p(etAim)N-8iT1bILE%i4#)pIy9YsJN57># z>QtAXs@kr;aoTQy44O5^omjHRFi;L)!En~efJs~|D(U4Aw!v>;9BtmVhh!cMaO=Wb z+Iu<>2_fTsCQ^E>@1k+B8n}64y7MJIkvE?0KvbEKQ}x3*Cffw zUUsK0uvSnotqcWl&OC_FgM%aZY+UYhU}_v9DlT5QySqDF0Z62Is7y^|4{Wp2n1a{o zgscVFBjqr2@c(fuisr>crmWRz(+8p27zLyz-Cj$jMYOU=hnTJDS2-p|5U?^2b z;QqKH!@KsvwU|M}9s=|s<-Y?*Va_}d(E?HM@rnm?;h})M0H~qyxYl=oB*jT8I6bWfZvAUXd&;WyWDLW~8JorhFo^$);YfPwWqLzHBG4&>C3<}EwEqGo z<3i>2SVR=kXK|yw?y|Bg1&-u^I2R=yL)s+WehQCgqhS>qj66n`5guav4 zI^}&kmX=pklnq$hU^Ki5_y{oS3fKmAK6C8nXjm4^$S3~YFxY! z%aR{RV1o_MB(NzSjxNx|b-3JP2PWSU3?OqP9NK&Zv5%sC>(D2Xi>q6(Sd>8+ zrVJx*-U4-;aR6m02C{tPzyTA@=ctxy?~*-Ti%(Yoq5047eMXfWd;i@A6PLi_PZjetv&shP`s;dyKZ7K9t#r~P~Tw`Cak z1#J4ZZ_LXVZbb7nIXQmd6NLjz9H(nCPrWyc!%%IaaZ~(%0$Pi zHVHP94IFP7?0U_aLHGjpnM|xIBdArJo+6Eb&BK#B3wSz%FdjFch{HBWmMIVyg}3QE z^qEXm9L_NOWZVLAzuaE|ZeayT5m>Mu04w^rN=d3^Xt>jh!j;xZ??076e^=eKH&q>Dy-?&{6tp8%jHVAXUY2BSJqHJ;S`=O)@gs#9E6 z12gVPL-#1*Z*}be?X_ID0|{Vo@ov$GI749Qa3C`%mbCUSg%1x#*w#}0latu82gEbhbJ2f^@Zja75!&!nq6E&aqIcx z>j5$t1siZRfZk-{ys>yI#Q)T-*X3b1LpD5OK4NS)5vniUp2Em)F}equn_G}- zO?L_Ecv|`V70m}{vEd5m85pS zX61v(u3&y%>jGph^FZvKmz_jw}DvbjIVkRTtn8!p#W$in;`}60*j@Eq!tofKi_i`W)>MIC*i)8+w zYYM5?wJu;N=;Q;dQ~*F-g+PXxF1RZbsE%VhEvD@7B08>vI3HYoj0QMJ!M>i2g9xh- zgj9v#JdmZu7}v1;6IBTaI!}OG{pV0IcZ^`R2LO2TwLMO{hFh1w@&oDs6JW1T0OLFX zGlZ+Ft4hFo(+mJ!LnbR5+XRpSn*d4*MPgyZ`QQsgiaV2<*2;hdVFp5W-MMKCPH40O zpVPivIR9zDEx`RLTUnLBKCBYp(@f;aXYuaya46%_Ox%=2DLl&m01EdLo>vR1Tk-yP zye~%<3GW1*yO;K0Q5+Dv$^-AEwXwNb0NjTJ$Q1ZGCf$ssJ!Y5GZzc}6Iv>nJ0T^f; z5WTX1Fk`0CU~>MsTeC*lw@)-O|BW-70iLPv8*?7$=kSZM26q%tmT0s1MtQ)`*qZ@l zr5RYiuqU$%WJ^_fQy7cxI>9CeN8?ZaNZY`#G$gVbN~$FSq8IQ50Q2uyQ#)f}1|$UN zczDzaLd8G^1Zvf=lS5&Lh5XfZBZf{u;A?_3)dW-u54NEEF1!nmlEIEfm;l<-hO7zW z-{BF)VKTuri(6;;tq%v*bQ~PY2?IusU}NWlfR`k2OkT43J{btQVasiuE&E{7L3y_v z8^B3Y$;c>&_5aZJ=Fwcg>-(rw8l<9;M3GWLnUk4R%9MFbna7ebb19Kf5h9r)vxG8B z8OkhEhA0Xd6Ov57>+#w9clJK(oU^`b{nq)Twf6RTdwUJfb3gZeUDtixkI$1|h=LQ5 zMO_5{rTI~-_Or=>p5lIO)C&IBhD@tN4j&N|eBI;VrTxO@9%CWLqvg+N!4j2TE+6>1Gn zMO*RAn)^Jyoc{GB;s|xfuv`1t!wDsShdI`^=wvXBj~tM3wH%%AruWRm*n(D)xnjjU zs<^C->`=pVapdAV{UZ6tJg1^O{{Fbs#i{jD26=-mS?!4OcdC0jBmRCqp^LN>kKM)l z`$Z+Y>Y+e;1v2`1xGDb5OUo|NyqbJb@6LT#N1{-s=9#j3EOiBcu;ys{T%NST3BM@PpnoH3Oh3yVAE zHD4O}QL5tg6%I}Sy->vziR5e%Zf?aBjDe2g`i%%LOn(boW>4VAdsJtlvUcs-JDghr zFwBLlL3hX$tUH6M`jxL=zg|7&DCoN9d2=$xdb9uu=(M-mFTDCN>&PKvqj%5lZ7c z`A%s7kEtg(qsTFU!kK~sgW_5(DlBrD+T8fQAwc46@KI4Xksv?cG%~}vq!V%e;6eF_ za#U$=;Q!i|*iYR=e}-NwEaU-AFwcdBr)TCg!$N{+=0Ar9ot~$;F{GP#{xOM@Z(kUP z>0a{_CWra*Z!xxzVS9`izO=E#WA6Jdhe~==c0^W^_s9!=eg@Qf z_{kuOL=5cvs469O^Y1(6r5If8lzUJuAR? z>PL77WDU#7%d5f0WjL#fiMmTsOY3ojutSQRoZJN@@WZH+l>nq#@+p$C4mq>ar+JshCk!sCxNw+>yhF&5tSKfEtGB47H8`7V?h0d(%<#B$9=o z@44u(HVL3jaw+b17=vQeaMmP|^BT)~=BbWK`+=myMA;wq!nsBt9v)owkg_+qAS*|I z81U`NCO&aSNf+)Z?U$di0Jmu`BRsS3Yw=8uU6RhFZb!%qnU*pP#}Mw zbe8(3yujHaFehkn_A3LTH%fTvn#zDZ&jH#!Y9FED9BFA zLvXb#H!ZXH%q@gOu7L+)Lva-Zw;@ww03bPIbjq>an{-`%Bv2p(u8yp$gyrwvR{@x~ z@~1!O%GGGj4jZ70vCE2zitA!f0@T*lntb=f%ogHk^#1+(#+L{ZYf(-jS3DyxzZN*g zs-c;`FAS|ey@=JSGRwexHo!5`%L?-Hr!w2)n}56w-LZ4$N^uTvuCtij+76mn95axJGNfS?IyQZ!SYHv-^@je?nAh_IQ2k`}N_LjDJWHbb{cl#h z&Z?bhMnzE%%jV>a45}}%Vv0Ez#&xOdaoqsb zY{$c~d$NW@%lz`I-waOW{<30FpBE}K%$>CzJ}GNhh4p_B#VElgJ2a%5uOJ-485D28 zTFk?)K0em+g0{9C2H3~X)AniZGye8f)FHF z!yC&0!z+O0FlTqvBBLhZf8=we5zi`=Ptr9J*?M;2=M!HnwmpZhV(nAW$cTuD#yyQF zPgg}LoQ@WA*LELeh3}~X84KoJCgJGlnEeWa9Py3OFl>B%eREA)Cv46(--&M${36BZ z56Jx`&WO0F`@bgNKm3(7)yN+u<6EhRSc&Q!IoDax0f<q6zXH5C zS>QCG08gnvZKhj7+PH;l7RVEQTxMVZG>iC-V}#RBIB@-XUAh=h#q9ay zU(T##kkS3%;MEdgTzUj?A*rW~g~bt+F6+#$6%OPzdHl;y&<{*iE!T1-;p1m3kAvGB4k$qOUHu*j z_?7?8#QqO|#b4GOEy@kB1MVnx6^uxn#Iz{E0>!YJ3V7Q$0J>2`U;+Aw)W(Y#z@ilJ z`B?W<^yKJnFg5VL$8xPgE+c_bjeYFTWWQff&~r?dte=FdD3CzYa$j1A*h0DV7&-@G z20!wwbi7~$&wHbe0Ei(>V9`93-ZeG=41|c@K&EWlv7_SW*LT;UI-hhFarkJ8$*h2D zTD6w&hSwGAH(=3uzx!%7J+TW^2D2^Bf{(?nPAG+jnxAih85)^vSJ&x*A(&0Vt$OG|snxIOlu z;?sK{jN+a>d-h?f2%wZymmV(c(g@-&VJaFhUq|6N@91u@7^3}ELC+CQvHMXloB&IO zYE8l0(-6@~N{W&=%c3|lg-vdq*m7qyCZMs~(`@FX)^W*z9j5GIwZSWz^Dd(EquFra z#BtjErFQ`{+N7KL#1I=B^IJ4}xV3X2Gg_}8^mPpY|2^Hkv0dF)-_cBue~!eD5i9E9 z0((YZUqAcRmZz3@-^jr0|AK70vm|?G>Gya~aU^!oQyic;Q{ZH2*pj47T!v%$#G-=$ z%i5K#-PM?`BS>bt30>U>$7juKPybtHwXqG??s&lXk6um(ron`&0Y>U~@7@6zR1;fV zfL~F#37_g;18$BJJ$qexPEOMgCyT=Dlcrfy=?oCq8ObQ`X|3`0|GNS$r#*2RL$cqCN9sv7os-jIb}1Ec`l0 z5)VX_gWWcQ$e+Grfe)fI-z-{7CHm_sU ztXdOOl{^>vnIB_T8}WR`ItduZm-T!l(?nrwT^lL5#lPZXT7NGJ2|`B#?g)PT_ydeJ zok2Mg6F|$hH1LE1`S{#hK!Sg;IZ0c_$hVrySuKY*5vGpb^ogX2$w4wK6qB?@VVUrV zLxW7Bv+J^cz3PQ!JC{x2m_WVo$Wc%nFuT}v(S2rz=bv}Xgb~B(rJh}{;`n}qxg5H z@DD42J<)EzHjpZcjAi@|Ph-d8A8;by`}+zGZX}ymD5`5}=HQQ9g)gro6eWBR*S!G= zoR1ABy`>oW*SCRzmzcfehifQ|+hA&xYidR)7d_<4S2&cZsoH3c3YbxS#PsVtpUaaa&*Y#M$!=1?Q#u zCnvxz1L}v<5By6Xz^z#@FS^2immTU>!Z{Ea3|gl-|nKR0&}zh&DBZTR!u zmeorW-NZli?4hV~fK5_zavQ-TeL>-C1R#-^(#tnTIAloi9sG0DDG5Xy<}!L~9eXSdjw#GOYC{qI+j*k@2jr4qBmUY<9J`8A&UYt>?+A+_%V1!E6f*5uVF4j>JT&RDPGh%e=kC`KaX@M{)t?6Ab**Dapw zb)EurI(R;O>)%;agQN5}=a8L*I~LOc;w4{5IZvVh}gU*BbZj51xD1X>}kX8}02#;Tc6 z6}eVU>OCiWVoCLn$2kmgkwL`y#jiOx1P+kn7f{rpv});She`M&h36`-yRV%y>xgZ? zhVXTSg++>np69@*V9JS-#os#LG2Qm)>K5#g_Jgmt=(l4yOA1{0lj{Eh*u7o-qqBEH zeBtXBf)xU;YVSz=?cgXJHB1V)-~mjJQ}@!&yLu`3`u4cv0irdnzp*gcB?!9c(AF{L zsD?^)!jY8jc#Ja{)M68@9zMy6hQ^BSbDlrT*6oM^n=}0Eisf~cXrDkL5IQ1}uAQ6G zG`5B9`-Q4Q^4%4*xpspJ6bdK4hX{{MySQtnD;w5}Or2lh{!JU0mgakxDUTUeZlc=4 zRLi{da~mw4_84iA5iJ#hzQ5a>16!f&|=&;!l(dWx4W{G!Gr zT-$ha0tz6X!dBoOPQ!<1f6lpKly`oQbt$raYz7JB(3qKeC1P>U4nB*PBw^0ivH=XI zq@<+$tKXarJ~)Xi*@xeB5x4uP#N9PYean~9dXQag92}|j1)c33Mm()u7i6lWh_Feb&!6-M3$Hq?+PgN~}9-VY4o)M_nRDhj| zRBy$#w}7B7xV6WKZYxJ>>IoM+*qN`s#ougQ{p@V;`@qo z=2pgZ&MVrifBN{v#7gSsA6`o~gbOjF@}VeuIzT_dnZ>^u@K%jcBNu^xCW?fL`Qgr7 z4g`x!S2lydDH(5b>ozdYuZm z6i#wbBKDDt>B{Rbj9&nRN6YP)9he1QL7wC~AvRjIl+{zgx`BIeUs-#*~clKo}lzLv4_u6oDz|!PC>z!dtFD zF;X$+*shcP3#3+iVQ=S_Q<7zOszC$&0<6MIW;kwP!u$kx3eVnm!1A!Aw~)X=bYqY# z%)i*lSI2+#iv$!Tg}f7bbX4U+Z0IY$3p^`ABsC;2FH=u&NX8fvf^Z&H2>7AK)$y=By<@v(Wwrowg^27dIi( zAlHotL#pHJnD{VkZEY`vSHGsJ)jXpuM6gZ`U&HNuR02*G%<;nl}71rCP^ z@7&N>L7QOCK=L{=c?BV{2q_DU#{&SMWuTbZ86-=;V~3il%r^r05j3Xx$M7bGYe|r( zK)kUTRf7sxv#!y8sv!Ekdq1x%gDDMd?YmX|v6k%_V+>CkFmoC(L=@o8)UpR1dw4Lb zg{ZUeWXMDhmtwJ&kRc`Hd;%nixk=4s01tG6G{CSs5XkKIX5`WPJj_^7BuG9_-*yzqR%iNzC~9A7RW$YP|OAJ%;L5o+a=U1WC+Jk zXmPT8GR{loRFd&(-R+nfZfsVJ>EiJ1hBvyXm*?~}=>LaLpVlL-Q#Td3`@2o4V_U7_ zxtyFL^9*D&_n$mD@8~E1`O|8U_}^-{ORcUQ>q|V7-!l1r+e@LoS`*$CXlUf(vK*Fh zug5$mm_!jW$6Gf3c+2ym695cH_}&E~HOFxH?YnkCJ=6J3%%*hF5t-r%GEhehughfb z>)0=auMh^NoWRdLKsiEEMpUKvI=Ae=4l<;aOtj+x{-i#$Axg-Wg+O7rBesd6sm-Ww z^y9gi0R%B^B#C$f(4|`3VHJUez`_pomU>26lVC(rd?J3V2g$Z`5K>((9-fzF4_qxH zG1G&L;U>YJ0Dy?igUI^&gFF{lU5ZN-{t^blvUI0E8z0B*4l4|dzk{E&ofX7*XGFF; zbtZ;0!>wd|P;CUiKN6N802l&=+%7MEm>L?NI>$e&>wPnK2;lW4(4jOXnv57V@y{IA zJAlwPdl484lXO~6s>|YtC9SVSHkya*ARGx~aeE>gaP(?GWwk{96F>I494X~FV1g5+ z%VOX(3;}9j!cunG&P|w!1k>2LEl6C*Y#0D{Q-}-KkmbUeLuI0O4w?7h()@(!a7!{7 ztbqL_^@YM`%^VivfKa?ZgnX3|CD#Plz*)?VYU8V!UkRnMjMRGVufl4dp zw)JW9K(~$oh&6b{)PI<}LoQe|37yGo2kg5Ws`Gc?%ha!2)z^`cSwl87i5( zUt6B&$*?ARl7j=EVOKW;q>;633Xo;h(6wFbzl%FDCT1VsePu(>DP&M#QxH3B2n=nT#NIx@^ zm)X>sj3mT-4I=ie7miY>BB%%8H_{$LG`ZT3CU8hz!-Op|^=n~nN?Kk)#}Ije8yFFc zYRgISIr+x~getey+@WZ>h z;JN3e(}X@LDu@CigMTQUHtp2K*EV|b=`jTMY9IMd22r65HH&C7xcDK4C}qizDqvH= zc&~+Oa~u%0fF!~E1Hzv}-mR0%{4=Z>hf6|A%3Vl1UOCVEWK3Z`TOKlAtp43tpEv9! z>GsOHxrvAZHgxQ0QO?0L^~0DmCahiPfiz4Tn+EP9A;KU`MrBOi%yg2-BeT4rJjQP! zACn&^;hN;8fO>Lj?01xDPb-Se-6Ayg;}ZOJfxmV3lft;l@& z)Zg1}ZEp`M7RK(|{px72@ozg1&nG7i@fs=m z7(LXTXt&g7vl~BQckp(XU7f?{cG37UgHkRm3jh^Vz<=-{yPoyxNE0}R2_*u_-`eum z24BJ}b1->`1Dx7GAfD1)ubHS5iS`OSp{4c6ei;qpS{+2wSUfq4u5msS9L)}57u!pF z|3ct;6(O5$6V=>zZcqBw5zA!e#i7%nMoP=LwK9u-$^D%fslxI%CDj!HdSp{KQ7jF5 zF5MyH!Jro9FD!qBH%Q(#T1#Fyj&+*Mt~&t)OwV-u0;YkgP2>?dP<>^T!{TxgMlC|+ zsfc0aaXopezKKL9Gkm_05a}dwJ!_|O)cl8AGb2g5ohEt~AY#1IHe#fI*IjQgb$ycw zayT&j$ukSLHf;M;>M2g3U)+8_j&i~{kt=|VDUeCD8sSLAGO8EBSCJ^g_HF%wt&W`a z#?70Q2sd4Ei|I#NN%^1n}IW65IK#L zkeqYv`mUU0Qh9Q6vV70#>sZ=k;0s}&h;(xSwW~mov5CpA;tBW6vxCQT;P+1}Z^v7HrJXPVEZBIcF}AKi zc7JCL5WwM0^(gWxFimysP}lm_cIp1p;(wLP8$d7XLh)@<`8t+jk*J`iia6(-JZd;! zgU@+~9Hf?Jy(rHqfLFXe+isMlNgzZZ?+e2kr7U>-%S#i>HXkiXu7oF?kLQE= zmKV#HilLo8AAT(M?BIsd(8QzL{-WMfymVEP#RnBMi5b{B60N3W$!8h==hW$u1~CMoStRh-ebuQi0P8@yNRM+8ZS9 znGPkhs0A@IUeUjy(5xv=o2K*VPU_M>72Fa0y-$Q};^o4;LL~1!>3Dwr^MuE9J=d{ZZ7>adk#&iPCh~Fft|Npx5Bz*h)LHkE@;a_lBByj z2O;`kRKwtuIkG` zKIQ;_I^&AdDi=8{-ybn3W81u32D1wW-nMzDm-c27krzXZe6(0`{p{y#(;zA{)b!dw zSfdPwBlkdLm5bM_o2_}Y6QONn>5GF~{a-N^$QJgvyQ?gDrjJa_LQlwS4VI=82~bi( zVxom(q25WL`nizKRCNN=*BZ&45yh$1{jA}zjJrZ<*vZ97%o8$;NHj+U^+*5!&KM5B zb|{C)yPX;BP{Ay+qF3mBb7>IwSoq_z%*pS_{0n#(eurPkba(qQIH|`C^s{#zL;>5K z$Vi@&cONW3KETp5QSP@bSO22Eeg>{Z;73WrzNrhNDtzfX^b~JxrA75b0trc;BL%OR zAa5LVKl_hN%Xc#w#~qiJmluu|PZ?D84hJ4Mrz@@(AO54{idzj**LOFZziu`Rrq1TI zT39Urt9hqtz}bQWV;I;)b|m?Iejm z$FhJJXM)}Zn!pEL9Q$Q-9$rr-j4{4kQ}YZSe{ofRe?I}~yMEpU`FN|V(1{xZjI6KL z4mHJh4O!r40}1sqxZrp|4UrkdKhNE`oZ0W|_e=4~ z$H&Fpgv#OQ&N;4Z(f14NGNTVqyj=Xicv!~DVr+qZmCMPKF4OE7#cDoL0}8hvX(c$U z!eZ|Da}Vc~eLo$tGn#*J;)3s|wvThll+%u(Po8WVT3(m<>{-rK>-znhAa6$X6uvBb zswvfr#z@msSL2cV9yd$u(zJz*;TC&O7;|I8uevj}?wBmaO=@TS>My3qn{~gjT%PP$!a#eDEE+jC3pR=! zWrqKJSTAMgKCfLA(dw=B8m#EVjCSqaH4x45%A-3_gXMAnxN#XjLHp+6)+0ZY$MGHe zZ}~Ens`Ea_M59|<(>01`?WP(smPgI1IB2tE$pSCmQysHfHKnR+QVvVCz)jnsF1mG? zN_Hc9Q|}GW_RlTE%-L0hMvXc1v$Wx9%4|1_*!e&$WxX7}aY*E?_q7jT>Sacp#g-ZB zm?dm0S1A<_@=_Xq?3ErOm%owHS$RXaxW>xIW!x^Yjq8)w5T}+hJ3P{`?at=6Ct{e* z76L3JirwTAm#~h4DV?!&z`Wrc zos@5qpQd*Pxu~t_mqPLm=^lGi8>6vfj8*gS4!M<`|8`e(%pon;teTcyEP^6vDiGrL8+^2Ca-PJi$*xPN!-)R?O+%fXdrwOc2x z;_%r6I=mEz?J!k`=_}i~!{Sl=s(JVP^k{yuhN9Ky3#-nQ*t1i#7K&TG9-+&UCU519 zX7G;}Eo+88VnS4tR?Ei5gj%e2?eg`F+o-l2i_SJ3nGIap_Pq0Zgp5^I?(Ew2P4q2B zjoY#`r>{t}p=)>OsKP`y7d_&E8GXE@?%S9b6vd~7DQ;|I8@ndY@@i!f9xKSJ(t3F2 z)h)ShHS$WLytAl2%QEhZ>ON)F3781Q126C)_PcM<>{n~(Q5qe+IBH6yQy47=-4_xX zS0NMzJbiBCyP0-O8I|>Q*DK#$V@s+4{j+4CCW}CVG5 zFZs7oyw;YJn93bA#t4d3^9UbByVduN#x}InUY9C=h`wU5xRpSREcgDa6@es4m!Z^J z7`x)x58%KJd0x4s z5ROhllC$&RJ_-8@Cg2UphKpTapD-!Axt37aN6~BuSo1-6I4FK5rl3)Z=8o$n3shcz zrW%e%XU~pq_PWS3M0wbD7t0a1OZ4`4*jC+n=%tvNc*Oaxs!pb3i7;MV{ys6jn-mAM zU6pn&sLOSqI*;x;MkFn%Q)r(&0`V;^P?~N#UB?b%MqztjBB=lxn`QG^m;i%Z`QPX$ zh~WAvFwI|h6~R$abvU&_t`L}7h;cK45r8}Zet$s!o?GkXEihC_`i@eNMkzcGf&oD* z2UWw<>jU(_I*!}qf8R-=?>?gj)dWg48sr>RkN@uQ+r8CZjUt?;Z=Z-a*Q$d1n+mq3 zCt67I7H)rjz-LKt!+jp5{W4XiY2vR9Sj!$JiXXCz99jb^8YwMigHr&^k<-?WpntOZ z9+M~o3N5JdYL7#QMYH+NJI3I!uqyC)?3|qZRrv41eZ^>(`BdP*L4h3U8K!++K98q? z!h^4>rD9~J?^-?!Q=DPbP01FEV!5{wAqHYssEr#}A3E2hnyZ%~U%nYj-HgrMI+2R~ z(+>BS4w};vdN0po{7!v%6{8$wU}M<*j}?0M_Dn%{$Z{tH3M3^IO1iq|Wo2c{g(B-@ zAyh$CI1mmwq3`j^hJb(#;!H~g1(huX#z3VJ5riX;Z-m$Xx9OTeinj6yiqAdqdnmN` zNxUeEtIIrB4#XJOkeO8xK^Jr?%V#M~vEbS^j$1=R_b5KwY@lxZUURzX#@wRE(n#qN zH6{v!67QP77BPx6C!wgQ+l^7oEw7mSET77YBYaNUN7?IzrmmlRvf9#uhhnM!BYQ?F z>TzFno>!W7x2ij5#B1c!w#M20jHI7-W(;i%3*ve0x4C+KeJL3BjT<-Cj-HS}_gq!b z>G3yo>({U!KD?iUqlVEACDni59UB){AnH}$J42c+N?tbd=OvUwd<_SLr-NT^F88^-+n(bl3GGRVKWF%T0>-RPzm!^`PLuS zq#yqA;EB>mc*h4E1A0$!I^;ZjAQ_a(fdC(Rc70RUW43WQDNWCSBYg6f4K;Lep(wJj z@&VIPPMcUkBZ&R8zO=W2Sbm|#BUM$I!F`a#AlfJAra6_@0fR#Mnh#8~qs0B>@?3)_ zMm!VHeBa5Fd%RGXQQvu22CXJR3CWOZ^toOFQ83^@${K=E1GIYS2~hx~6ON;wB;nFP zaW{^zt54m2oh9uLDBLTsouvUXgVi-_?tN^gBkRRfvea!5tT4_Z^Ga)5+@o6lp|AAz zr>Z4LwASil>Xf3BU^}ywS)h->C3&m0MOENlGMaLrLAFuk`Y$8$*T{EM5IA_U0Bkv# z{z*Z`g5m-QmJK}_u%BPhO+*-~sHj{Ar4laYE(|-g8041#p$=WnoF)~YTB`cwC&Qpg z|BDMZ!N$G-VMF?e>}r9tCa0uaM$p6ywI!|R2tZF1OmjRrS$uAVtfN@0c zCb^OVo}o)x2g!8Zw}p9K+t;a&u9@stOMRimCb27vh@5ED9r+T5Sy9d6Px1?yp!g;{ zB28fa;_oiIa3T*3|^WyrbUO>d9jDrJB5 zt7W5Pi5Dj5gl>$WpZ=p=H7fe7T#sZXagT;;V>n(asFeHkE{;15xSaN`>c51_#eSj( zTmAr`TFunRpOEqI{s*6iLg*O6BjpW=@^C<)(U9bZBmVrIooM*=y~eV58K(`w5fq*W z1B!WxSj;vc4IlG(x#=d*HiXSNB* zvb>sm+ZMB$WE}go-;ee^RXy$Jx~0QJ{-1|s@mn%;PN3!P>T^jI-rePQe?xfi0o?E( zNK}n%7FR3j>(@haS`HY8pf&WzFRuZ7wGlG=uV8Z4e1R6;5S^3@*gB-nBDB=?sevr} z9~ZK_CV^YSk>L`VHOs)~yRhI!Ft(lC`c@_Fb=KjG{&-J9@Z(qCk8-#x0#A zC%?<7_<)LBH!@1&n>74>HTMtNN*zLOpT4!xZB5ha(4U(M;@B@x9qDXE5<;F={OO=T z=aLsMwwCrXPc9%4IDrGZgB&Ojs2RV%zu(BjB*GAf3_uwy?kYyRzxh+Ye%0DRQn`_u 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 Nrbd>ACHlYK`X`w0dv*W- literal 0 HcmV?d00001 diff --git a/src/traits/tolerance.rs b/src/traits/tolerance.rs index a4903ec..63246fa 100644 --- a/src/traits/tolerance.rs +++ b/src/traits/tolerance.rs @@ -99,6 +99,7 @@ impl Tolerance for DefaultTolerance { } } + // TODO add an unit ... fn rt_range(&self, rt: f32) -> Option<(f32, f32)> { match self.rt { RtTolerance::Absolute((low, high)) => Some((rt - low, rt + high)), From b262e2914c2f88fc95a12901206c3e639fc69d63 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sat, 2 Nov 2024 21:37:31 -0700 Subject: [PATCH 19/30] feat: updated plot --- data/sageresults/plot.py | 10 ++++++++-- data/sageresults/ubb_peptide_plot.png | Bin 131585 -> 140564 bytes 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/data/sageresults/plot.py b/data/sageresults/plot.py index ce85153..3a28b91 100644 --- a/data/sageresults/plot.py +++ b/data/sageresults/plot.py @@ -49,12 +49,18 @@ def infinite_color_cycle(): sorted_ms2_keys = sorted(x["result"]["ms2_stats"]["transition_intensities"].keys()) for j in sorted_ms1_keys: + col = next(colors) j = x["result"]["ms1_stats"]["transition_intensities"][j] - ax[0, i].plot(ms1_rts, j, color=next(colors)) + ax[0, i].plot(ms1_rts, j, color=col, alpha=0.4) + # Draw the points + ax[0, i].scatter(ms1_rts, j, color=col, s=5) for j in sorted_ms2_keys: + col = next(colors) j = x["result"]["ms2_stats"]["transition_intensities"][j] - ax[1, i].plot(ms2_rts, j, color=next(colors)) + ax[1, i].plot(ms2_rts, j, color=col, alpha=0.4) + # Draw the points + ax[1, i].scatter(ms2_rts, j, color=col, s=5) # Label axes ... ax[0, i].set_xlabel("Retention Time (min)") diff --git a/data/sageresults/ubb_peptide_plot.png b/data/sageresults/ubb_peptide_plot.png index 1db70a685e13aff081388c39f442f7debdb4d2ec..e1fb49efef028eaa097e5e11f17e8ee83c47ccf1 100644 GIT binary patch literal 140564 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%0AViiwJtT`$72|9VrnFX00PwSd#ej8 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%0DRQn`_u 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 Nrbd>ACHlYK`X`w0dv*W- From e1a31161804e820a03ff059b53d04787a1f39537 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sun, 3 Nov 2024 10:02:24 -0800 Subject: [PATCH 20/30] style: new fmt settings --- benches/benchmark_indices.rs | 45 +++- rustfmt.toml | 6 + src/lib.rs | 23 ++- src/main.rs | 62 ++++-- src/models/adapters.rs | 21 +- src/models/aggregators/mod.rs | 14 +- .../raw_peak_agg/chromatogram_agg.rs | 15 +- src/models/aggregators/raw_peak_agg/mod.rs | 15 +- .../multi_chromatogram_agg/base.rs | 89 ++++++++ .../multi_chromatogram_agg/mod.rs | 7 + .../multi_chromatogram_agg.rs | 195 ++++++------------ .../aggregators/raw_peak_agg/point_agg.rs | 14 +- src/models/aggregators/rolling_calculators.rs | 45 ++++ .../aggregators/streaming_aggregator.rs | 21 +- src/models/elution_group.rs | 12 +- src/models/frames/expanded_frame.rs | 82 +++++--- src/models/frames/expanded_window_group.rs | 14 +- src/models/frames/raw_frames.rs | 9 +- src/models/frames/single_quad_settings.rs | 14 +- .../indices/expanded_raw_index/model.rs | 74 ++++--- src/models/indices/raw_file_index.rs | 50 +++-- .../transposed_quad_index/peak_bucket.rs | 15 +- .../transposed_quad_index/quad_index.rs | 97 +++++++-- .../quad_splitted_transposed_index.rs | 92 ++++++--- src/models/queries.rs | 21 +- .../queriable_tims_data.rs | 24 ++- src/traits/queriable_data.rs | 5 +- src/traits/tolerance.rs | 47 +++-- src/utils/correlation.rs | 1 + src/utils/frame_processing.rs | 14 +- src/utils/math.rs | 84 ++++++++ src/utils/mod.rs | 2 + src/utils/scoring.rs | 1 + src/utils/sorting.rs | 1 - src/utils/tolerance_ranges.rs | 6 +- 35 files changed, 877 insertions(+), 360 deletions(-) create mode 100644 rustfmt.toml create mode 100644 src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs create mode 100644 src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/mod.rs rename src/models/aggregators/raw_peak_agg/{ => multi_chromatogram_agg}/multi_chromatogram_agg.rs (72%) create mode 100644 src/utils/correlation.rs create mode 100644 src/utils/scoring.rs diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index 006c96e..d62c9fe 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -1,29 +1,54 @@ -use rand::{Rng, SeedableRng}; +use rand::{ + Rng, + SeedableRng, +}; use rand_chacha::ChaCha8Rng; use serde::Serialize; -use std::collections::HashMap; -use std::env; -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant}; +use std::{ + collections::HashMap, + env, + fs::File, + path::{ + Path, + PathBuf, + }, + time::{ + Duration, + Instant, + }, +}; use timsquery::{ models::{ aggregators::RawPeakIntensityAggregator, indices::{ - expanded_raw_index::ExpandedRawFrameIndex, raw_file_index::RawFileIndex, + expanded_raw_index::ExpandedRawFrameIndex, + raw_file_index::RawFileIndex, transposed_quad_index::QuadSplittedTransposedIndex, }, }, queriable_tims_data::queriable_tims_data::query_multi_group, traits::tolerance::{ - DefaultTolerance, MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance, + DefaultTolerance, + MobilityTolerance, + MzToleramce, + QuadTolerance, + RtTolerance, }, ElutionGroup, }; use tracing::subscriber::set_global_default; -use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_bunyan_formatter::{ + BunyanFormattingLayer, + JsonStorageLayer, +}; use tracing_chrome::ChromeLayerBuilder; -use tracing_subscriber::{fmt, prelude::*, registry::Registry, EnvFilter, Layer}; +use tracing_subscriber::{ + fmt, + prelude::*, + registry::Registry, + EnvFilter, + Layer, +}; const NUM_ELUTION_GROUPS: usize = 1000; const NUM_ITERATIONS: usize = 1; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c67c611 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ + +imports_layout = "Vertical" +imports_granularity = "Crate" +normalize_comments = true +reorder_impl_items = true +version = "Two" diff --git a/src/lib.rs b/src/lib.rs index 500b75e..d0c0ebf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,18 @@ // Re-export main structures -pub use crate::models::elution_group::ElutionGroup; -pub use crate::models::indices::transposed_quad_index::QuadSplittedTransposedIndex; +pub use crate::models::{ + elution_group::ElutionGroup, + indices::transposed_quad_index::QuadSplittedTransposedIndex, +}; // Re-export traits -pub use crate::traits::aggregator::Aggregator; -pub use crate::traits::queriable_data::QueriableData; -pub use crate::traits::tolerance::{Tolerance, ToleranceAdapter}; +pub use crate::traits::{ + aggregator::Aggregator, + queriable_data::QueriableData, + tolerance::{ + Tolerance, + ToleranceAdapter, + }, +}; // Declare modules pub mod errors; @@ -15,4 +22,8 @@ pub mod traits; pub mod utils; // Re-export errors -pub use crate::errors::{DataProcessingError, DataReadingError, TimsqueryError}; +pub use crate::errors::{ + DataProcessingError, + DataReadingError, + TimsqueryError, +}; diff --git a/src/main.rs b/src/main.rs index 9fc15d9..88c6b48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,50 @@ -use clap::{Parser, Subcommand}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Instant; -use timsquery::models::elution_group::ElutionGroup; -use timsquery::queriable_tims_data::queriable_tims_data::query_multi_group; -use timsquery::traits::tolerance::DefaultTolerance; -use timsquery::traits::tolerance::{MobilityTolerance, MzToleramce, QuadTolerance, RtTolerance}; +use clap::{ + Parser, + Subcommand, +}; +use serde::{ + Deserialize, + Serialize, +}; +use std::{ + collections::HashMap, + time::Instant, +}; use timsquery::{ - models::aggregators::{ - MultiCMGStatsFactory, RawPeakIntensityAggregator, RawPeakVectorAggregator, + models::{ + aggregators::{ + MultiCMGStatsFactory, + RawPeakIntensityAggregator, + RawPeakVectorAggregator, + }, + elution_group::ElutionGroup, + indices::{ + ExpandedRawFrameIndex, + QuadSplittedTransposedIndex, + }, + }, + queriable_tims_data::queriable_tims_data::query_multi_group, + traits::tolerance::{ + DefaultTolerance, + MobilityTolerance, + MzToleramce, + QuadTolerance, + RtTolerance, }, - models::indices::{ExpandedRawFrameIndex, QuadSplittedTransposedIndex}, }; -use tracing::instrument; -use tracing::subscriber::set_global_default; -use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; -use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; +use tracing::{ + instrument, + subscriber::set_global_default, +}; +use tracing_bunyan_formatter::{ + BunyanFormattingLayer, + JsonStorageLayer, +}; +use tracing_subscriber::{ + prelude::*, + registry::Registry, + EnvFilter, +}; fn main() { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); @@ -57,8 +86,7 @@ fn main_write_template(args: WriteTemplateArgs) { std::fs::write(tolerance_json_path.clone(), tolerance_json).unwrap(); println!( "use as `timsquery query-index --output-path '.' --raw-file-path 'your_file.d' --tolerance-settings-path {:#?} --elution-groups-path {:#?}`", - tolerance_json_path, - egs_json_path, + tolerance_json_path, egs_json_path, ); } diff --git a/src/models/adapters.rs b/src/models/adapters.rs index 0ca164e..fe494d7 100644 --- a/src/models/adapters.rs +++ b/src/models/adapters.rs @@ -1,11 +1,20 @@ -use crate::models::elution_group::ElutionGroup; -use crate::models::queries::{FragmentGroupIndexQuery, PrecursorIndexQuery}; -use crate::utils::tolerance_ranges::IncludedRange; -use crate::ToleranceAdapter; +use crate::{ + models::{ + elution_group::ElutionGroup, + queries::{ + FragmentGroupIndexQuery, + PrecursorIndexQuery, + }, + }, + utils::tolerance_ranges::IncludedRange, + ToleranceAdapter, +}; use serde::Serialize; use std::hash::Hash; -use timsrust::converters::ConvertableDomain; -use timsrust::Metadata; +use timsrust::{ + converters::ConvertableDomain, + Metadata, +}; #[derive(Debug, Default)] pub struct FragmentIndexAdapter { diff --git a/src/models/aggregators/mod.rs b/src/models/aggregators/mod.rs index e34cad4..57e858d 100644 --- a/src/models/aggregators/mod.rs +++ b/src/models/aggregators/mod.rs @@ -2,9 +2,11 @@ pub mod raw_peak_agg; pub mod rolling_calculators; pub mod streaming_aggregator; -pub use raw_peak_agg::ChromatomobilogramStats; -pub use raw_peak_agg::MultiCMGStats; -pub use raw_peak_agg::MultiCMGStatsArrays; -pub use raw_peak_agg::MultiCMGStatsFactory; -pub use raw_peak_agg::RawPeakIntensityAggregator; -pub use raw_peak_agg::RawPeakVectorAggregator; +pub use raw_peak_agg::{ + ChromatomobilogramStats, + MultiCMGStatsAgg, + MultiCMGStatsFactory, + PartitionedCMGArrays, + RawPeakIntensityAggregator, + RawPeakVectorAggregator, +}; diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index 6eed3d5..4c06b9c 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -1,11 +1,15 @@ use super::super::streaming_aggregator::RunningStatsCalculator; -use crate::models::frames::raw_peak::RawPeak; -use crate::sort_vecs_by_first; -use crate::traits::aggregator::Aggregator; +use crate::{ + models::frames::raw_peak::RawPeak, + sort_vecs_by_first, + traits::aggregator::Aggregator, +}; use serde::Serialize; -use std::collections::BTreeMap; -use std::collections::HashMap; +use std::collections::{ + BTreeMap, + HashMap, +}; pub type MappingCollection = HashMap; @@ -27,6 +31,7 @@ impl ScanTofStatsCalculatorPair { tof: RunningStatsCalculator::new(intensity, tof_index), } } + pub fn add(&mut self, intensity: u64, scan_index: usize, tof_index: u32) { self.scan.add(scan_index as f64, intensity); self.tof.add(tof_index as f64, intensity); diff --git a/src/models/aggregators/raw_peak_agg/mod.rs b/src/models/aggregators/raw_peak_agg/mod.rs index dd5ad20..03ec9b5 100644 --- a/src/models/aggregators/raw_peak_agg/mod.rs +++ b/src/models/aggregators/raw_peak_agg/mod.rs @@ -3,8 +3,13 @@ pub mod multi_chromatogram_agg; pub mod point_agg; pub use chromatogram_agg::ChromatomobilogramStats; -pub use multi_chromatogram_agg::MultiCMGStats; -pub use multi_chromatogram_agg::MultiCMGStatsArrays; -pub use multi_chromatogram_agg::MultiCMGStatsFactory; -pub use point_agg::RawPeakIntensityAggregator; -pub use point_agg::RawPeakVectorAggregator; +pub use multi_chromatogram_agg::MultiCMGStatsAgg; +// TODO: reorganize this so I donr use direcly from `base`` +pub use multi_chromatogram_agg::{ + base::PartitionedCMGArrays, + MultiCMGStatsFactory, +}; +pub use point_agg::{ + RawPeakIntensityAggregator, + RawPeakVectorAggregator, +}; diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs new file mode 100644 index 0000000..fc089f8 --- /dev/null +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs @@ -0,0 +1,89 @@ +use super::super::chromatogram_agg::{ + ChromatomobilogramStatsArrays, + MappingCollection, + ScanTofStatsCalculatorPair, +}; +use crate::{ + models::{ + aggregators::{ + rolling_calculators::rolling_median, + streaming_aggregator::RunningStatsCalculator, + }, + frames::raw_peak::RawPeak, + queries::MsLevelContext, + }, + traits::aggregator::Aggregator, + utils::math::{ + lnfact, + lnfact_float, + }, +}; +use serde::Serialize; +use std::{ + collections::{ + BTreeMap, + HashSet, + }, + hash::Hash, +}; +use tracing::{ + debug, + warn, +}; + +#[derive(Debug, Clone)] +pub struct ParitionedCMGAggregator { + pub scan_tof_mapping: MappingCollection<(FH, u32), ScanTofStatsCalculatorPair>, + pub uniq_rts: HashSet, + pub uniq_ids: HashSet, +} + +impl Default for ParitionedCMGAggregator { + fn default() -> Self { + Self { + scan_tof_mapping: MappingCollection::new(), + uniq_rts: HashSet::new(), + uniq_ids: HashSet::new(), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct PartitionedCMGArrays { + pub transition_stats: MappingCollection, +} + +// TODO reimplement ... dont really like how non-idiomatic this finalize is. +// From-Into might be a better way. +impl ParitionedCMGAggregator { + pub fn finalize(self) -> PartitionedCMGArrays { + let mut transition_stats = MappingCollection::new(); + + for id_key in self.uniq_ids.iter() { + let mut id_cmgs = ChromatomobilogramStatsArrays::new(); + for rt_key in self.uniq_rts.iter() { + let scan_tof_mapping = self.scan_tof_mapping.get(&(id_key.clone(), *rt_key)); + if let Some(scan_tof_mapping) = scan_tof_mapping { + id_cmgs.retention_time_miliseconds.push(*rt_key); + id_cmgs + .scan_index_means + .push(scan_tof_mapping.scan.mean().unwrap()); + id_cmgs + .scan_index_sds + .push(scan_tof_mapping.scan.standard_deviation().unwrap()); + id_cmgs + .tof_index_means + .push(scan_tof_mapping.tof.mean().unwrap()); + id_cmgs + .tof_index_sds + .push(scan_tof_mapping.tof.standard_deviation().unwrap()); + id_cmgs.intensities.push(scan_tof_mapping.tof.weight()); + } + } + id_cmgs.sort_by_rt(); + transition_stats.insert(id_key.clone(), id_cmgs); + } + + PartitionedCMGArrays { transition_stats } + } +} diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/mod.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/mod.rs new file mode 100644 index 0000000..fb06f82 --- /dev/null +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/mod.rs @@ -0,0 +1,7 @@ +pub mod base; +pub mod multi_chromatogram_agg; + +pub use multi_chromatogram_agg::{ + MultiCMGStatsAgg, + MultiCMGStatsFactory, +}; diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs similarity index 72% rename from src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs rename to src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs index 62df922..a7583da 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs @@ -1,76 +1,58 @@ -use super::chromatogram_agg::{ - ChromatomobilogramStatsArrays, MappingCollection, ScanTofStatsCalculatorPair, +use super::{ + super::chromatogram_agg::{ + ChromatomobilogramStatsArrays, + MappingCollection, + ScanTofStatsCalculatorPair, + }, + base::{ + ParitionedCMGAggregator, + PartitionedCMGArrays, + }, +}; +use crate::{ + models::{ + aggregators::{ + rolling_calculators::{ + calculate_lazy_hyperscore, + calculate_value_vs_baseline, + }, + streaming_aggregator::RunningStatsCalculator, + }, + frames::raw_peak::RawPeak, + queries::MsLevelContext, + }, + traits::aggregator::Aggregator, + utils::math::{ + lnfact, + lnfact_float, + }, }; -use crate::models::aggregators::rolling_calculators::rolling_median; -use crate::models::aggregators::streaming_aggregator::RunningStatsCalculator; -use crate::models::frames::raw_peak::RawPeak; -use crate::models::queries::MsLevelContext; -use crate::traits::aggregator::Aggregator; -use crate::utils::math::{lnfact, lnfact_float}; use serde::Serialize; -use std::collections::{BTreeMap, HashSet}; -use std::hash::Hash; -use tracing::{debug, warn}; - -use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; - -#[derive(Debug, Clone)] -struct _MultiCMGStats { - pub scan_tof_mapping: MappingCollection<(FH, u32), ScanTofStatsCalculatorPair>, - pub uniq_rts: HashSet, - pub uniq_ids: HashSet, -} - -impl Default for _MultiCMGStats { - fn default() -> Self { - Self { - scan_tof_mapping: MappingCollection::new(), - uniq_rts: HashSet::new(), - uniq_ids: HashSet::new(), - } - } -} - -impl _MultiCMGStats { - fn finalize(self) -> MultiCMGStatsArrays { - let mut transition_stats = MappingCollection::new(); - - for id_key in self.uniq_ids.iter() { - let mut id_cmgs = ChromatomobilogramStatsArrays::new(); - for rt_key in self.uniq_rts.iter() { - let scan_tof_mapping = self.scan_tof_mapping.get(&(id_key.clone(), *rt_key)); - if let Some(scan_tof_mapping) = scan_tof_mapping { - id_cmgs.retention_time_miliseconds.push(*rt_key); - id_cmgs - .scan_index_means - .push(scan_tof_mapping.scan.mean().unwrap()); - id_cmgs - .scan_index_sds - .push(scan_tof_mapping.scan.standard_deviation().unwrap()); - id_cmgs - .tof_index_means - .push(scan_tof_mapping.tof.mean().unwrap()); - id_cmgs - .tof_index_sds - .push(scan_tof_mapping.tof.standard_deviation().unwrap()); - id_cmgs.intensities.push(scan_tof_mapping.tof.weight()); - } - } - id_cmgs.sort_by_rt(); - transition_stats.insert(id_key.clone(), id_cmgs); - } - - MultiCMGStatsArrays { transition_stats } - } -} +use std::{ + collections::{ + BTreeMap, + HashSet, + }, + hash::Hash, +}; +use tracing::{ + debug, + warn, +}; +use timsrust::converters::{ + ConvertableDomain, + Scan2ImConverter, + Tof2MzConverter, +}; #[derive(Debug, Clone)] -pub struct MultiCMGStats { +pub struct MultiCMGStatsAgg { pub converters: (Tof2MzConverter, Scan2ImConverter), - pub ms1_stats: _MultiCMGStats, - pub ms2_stats: _MultiCMGStats, + pub ms1_stats: ParitionedCMGAggregator, + pub ms2_stats: ParitionedCMGAggregator, pub id: u64, pub context: Option>, + pub buffer: Option, } #[derive(Debug, Clone)] @@ -80,24 +62,20 @@ pub struct MultiCMGStatsFactory } impl MultiCMGStatsFactory { - pub fn build(&self, id: u64) -> MultiCMGStats { - MultiCMGStats { + pub fn build(&self, id: u64) -> MultiCMGStatsAgg { + MultiCMGStatsAgg { converters: (self.converters.0, self.converters.1), ms1_stats: Default::default(), ms2_stats: Default::default(), id, context: None, + buffer: None, } } } #[derive(Debug, Clone, Serialize)] -pub struct MultiCMGStatsArrays { - pub transition_stats: MappingCollection, -} - -#[derive(Debug, Clone, Serialize)] -pub struct _FinalizedMultiCMGStatsArrays { +pub struct PartitionedCMGArrayStats { pub retention_time_miliseconds: Vec, pub weighted_scan_index_mean: Vec, pub summed_intensity: Vec, @@ -114,8 +92,8 @@ pub struct _FinalizedMultiCMGStatsArrays { - pub ms1_stats: _FinalizedMultiCMGStatsArrays, - pub ms2_stats: _FinalizedMultiCMGStatsArrays, + pub ms1_stats: PartitionedCMGArrayStats, + pub ms2_stats: PartitionedCMGArrayStats, pub id: u64, } @@ -145,28 +123,9 @@ pub struct NaturalFinalizedMultiCMGStatsArrays Vec { - let mut scores = vec![0.0; npeaks.len()]; - for i in 0..npeaks.len() { - let npeaks_i = npeaks[i]; - let summed_intensity_i = summed_intensity[i]; - let log1p_intensities_i = (summed_intensity_i as f64 + 1.0).ln(); - scores[i] = lnfact(npeaks_i as u16) + (2.0 * log1p_intensities_i); - } - scores -} - -fn calculate_value_vs_baseline(vals: &[f64], baseline_window_size: usize) -> Vec { - let baseline = rolling_median(vals, baseline_window_size, f64::NAN); - vals.iter() - .zip(baseline.iter()) - .map(|(x, y)| x - y) - .collect() -} - impl _NaturalFinalizedMultiCMGStatsArrays { pub fn new( - other: _FinalizedMultiCMGStatsArrays, + other: PartitionedCMGArrayStats, mz_converter: &Tof2MzConverter, mobility_converter: &Scan2ImConverter, ) -> Self { @@ -274,13 +233,13 @@ impl _NaturalFinalizedMultiCMGS } } -impl From> - for _FinalizedMultiCMGStatsArrays +impl From> + for PartitionedCMGArrayStats { - fn from(other: MultiCMGStatsArrays) -> Self { + fn from(other: PartitionedCMGArrays) -> Self { // TODO ... maybe refactor this ... RN its king of ugly. - let mut out = _FinalizedMultiCMGStatsArrays { + let mut out = PartitionedCMGArrayStats { retention_time_miliseconds: Vec::new(), scan_index_means: MappingCollection::new(), tof_index_means: MappingCollection::new(), @@ -384,10 +343,10 @@ impl From Aggregator - for MultiCMGStats + for MultiCMGStatsAgg { - type Item = RawPeak; type Context = MsLevelContext; + type Item = RawPeak; type Output = NaturalFinalizedMultiCMGStatsArrays; fn add(&mut self, peak: impl Into) { @@ -455,12 +414,12 @@ impl Aggregat let mobility_converter = &self.converters.1; let ms1_stats = _NaturalFinalizedMultiCMGStatsArrays::new( - _FinalizedMultiCMGStatsArrays::from(self.ms1_stats.finalize()), + PartitionedCMGArrayStats::from(self.ms1_stats.finalize()), mz_converter, mobility_converter, ); let ms2_stats = _NaturalFinalizedMultiCMGStatsArrays::new( - _FinalizedMultiCMGStatsArrays::from(self.ms2_stats.finalize()), + PartitionedCMGArrayStats::from(self.ms2_stats.finalize()), mz_converter, mobility_converter, ); @@ -472,31 +431,3 @@ impl Aggregat } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_value_vs_baseline() { - let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; - let baseline_window_size = 3; - let _baseline = rolling_median(&vals, baseline_window_size, f64::NAN); - let out = calculate_value_vs_baseline(&vals, baseline_window_size); - let expect_val = vec![f64::NAN, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, f64::NAN]; - let all_close = out - .iter() - .zip(expect_val.iter()) - .filter(|(a, b)| ((!a.is_nan()) && (!b.is_nan()))) - .all(|(a, b)| (a - b).abs() < 1e-6); - - let all_match_nan = out - .iter() - .zip(expect_val.iter()) - .filter(|(a, b)| ((a.is_nan()) || (b.is_nan()))) - .all(|(a, b)| a.is_nan() && b.is_nan()); - - assert!(all_close, "Expected {:?}, got {:?}", expect_val, out); - assert!(all_match_nan, "Expected {:?}, got {:?}", expect_val, out); - } -} diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index 1fbfdcc..931f657 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -1,5 +1,11 @@ -use crate::models::frames::raw_peak::RawPeak; -use crate::traits::aggregator::{Aggregator, NoContext, ProvidesContext}; +use crate::{ + models::frames::raw_peak::RawPeak, + traits::aggregator::{ + Aggregator, + NoContext, + ProvidesContext, + }, +}; use serde::Serialize; #[derive(Debug, Clone, Copy)] @@ -15,8 +21,8 @@ impl RawPeakIntensityAggregator { } impl Aggregator for RawPeakIntensityAggregator { - type Item = RawPeak; type Context = NoContext; + type Item = RawPeak; type Output = u64; fn add(&mut self, peak: impl Into) { @@ -68,8 +74,8 @@ pub struct RawPeakVectorArrays { } impl Aggregator for RawPeakVectorAggregator { - type Item = RawPeak; type Context = NoContext; + type Item = RawPeak; type Output = RawPeakVectorArrays; fn add(&mut self, peak: impl Into) { diff --git a/src/models/aggregators/rolling_calculators.rs b/src/models/aggregators/rolling_calculators.rs index 24bdfad..60a5f31 100644 --- a/src/models/aggregators/rolling_calculators.rs +++ b/src/models/aggregators/rolling_calculators.rs @@ -1,3 +1,7 @@ +use crate::utils::math::{ + lnfact, + lnfact_float, +}; // Rolling median calculator pub struct RollingMedianCalculator { @@ -56,6 +60,24 @@ pub fn rolling_median( out } +pub fn calculate_lazy_hyperscore(npeaks: &[usize], summed_intensity: &[u64]) -> Vec { + let mut scores = vec![0.0; npeaks.len()]; + for i in 0..npeaks.len() { + let npeaks_i = npeaks[i]; + let summed_intensity_i = summed_intensity[i]; + let log1p_intensities_i = (summed_intensity_i as f64 + 1.0).ln(); + scores[i] = lnfact(npeaks_i as u16) + (2.0 * log1p_intensities_i); + } + scores +} + +pub fn calculate_value_vs_baseline(vals: &[f64], baseline_window_size: usize) -> Vec { + let baseline = rolling_median(vals, baseline_window_size, f64::NAN); + vals.iter() + .zip(baseline.iter()) + .map(|(x, y)| x - y) + .collect() +} #[cfg(test)] mod tests { use super::*; @@ -100,4 +122,27 @@ mod tests { } } } + + #[test] + fn test_value_vs_baseline() { + let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; + let baseline_window_size = 3; + let _baseline = rolling_median(&vals, baseline_window_size, f64::NAN); + let out = calculate_value_vs_baseline(&vals, baseline_window_size); + let expect_val = vec![f64::NAN, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, f64::NAN]; + let all_close = out + .iter() + .zip(expect_val.iter()) + .filter(|(a, b)| ((!a.is_nan()) && (!b.is_nan()))) + .all(|(a, b)| (a - b).abs() < 1e-6); + + let all_match_nan = out + .iter() + .zip(expect_val.iter()) + .filter(|(a, b)| ((a.is_nan()) || (b.is_nan()))) + .all(|(a, b)| a.is_nan() && b.is_nan()); + + assert!(all_close, "Expected {:?}, got {:?}", expect_val, out); + assert!(all_match_nan, "Expected {:?}, got {:?}", expect_val, out); + } } diff --git a/src/models/aggregators/streaming_aggregator.rs b/src/models/aggregators/streaming_aggregator.rs index c85456e..d03093f 100644 --- a/src/models/aggregators/streaming_aggregator.rs +++ b/src/models/aggregators/streaming_aggregator.rs @@ -4,6 +4,7 @@ use tracing::debug; // and another with a weight and in a streaming fashion adds the value to the accumulator // to calculate the total, mean and variance. +// TODO: move this to the general errors. #[derive(Debug, Clone, Copy)] pub enum StreamingAggregatorError { DivisionByZero, @@ -20,21 +21,29 @@ type Result = std::result::Result; /// use timsquery::models::aggregators::streaming_aggregator::RunningStatsCalculator; /// /// // Create a new calculator with a weight of 10 and a mean of 0.0 -/// let mut calc = RunningStatsCalculator::new(2, 0.0); -/// calc.add(10.0, 2); +/// let mut calc = RunningStatsCalculator::new(1, 0.0); +/// calc.add(10.0, 1); +/// calc.add(0.0, 1); +/// calc.add(10.0, 1); +/// calc.add(0.0, 1); +/// calc.add(10.0, 1); +/// calc.add(0.0, 1); /// // So overall this should be the equivalent of the mean for -/// // [0.0, 0.0, 10.0, 10.0] -/// assert_eq!(calc.mean().unwrap(), 5.0); -/// +/// // [0.0, 10.0, 0.0, 10.0, 0.0, 10.0] +/// assert_eq!(calc.mean().unwrap(), 5.0, "{calc:#?}"); +/// assert!((4.5..5.5).contains(&calc.standard_deviation().unwrap()), "{calc:#?}"); /// ``` /// /// # Notes /// +/// It is important to know that the calculation of the mean is not +/// perfect. Thus if the initial value passes if very far off the real +/// mean, the final estimate will be off. +/// /// Ref impl in javascript ... /// https://nestedsoftware.com/2018/03/27/calculating-standard-deviation-on-streaming-data-253l.23919.html /// https://nestedsoftware.com/2019/09/26/incremental-average-and-standard-deviation-with-sliding-window-470k.176143.html /// Read the blog ... its amazing. -/// #[derive(Debug, Clone, Copy, Default)] pub struct RunningStatsCalculator { weight: u64, diff --git a/src/models/elution_group.rs b/src/models/elution_group.rs index fd7d2be..1448d97 100644 --- a/src/models/elution_group.rs +++ b/src/models/elution_group.rs @@ -1,13 +1,17 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::hash::Hash; +use serde::{ + Deserialize, + Serialize, +}; +use std::{ + collections::HashMap, + hash::Hash, +}; /// A struct that represents an elution group. /// /// The elution group is a single precursor ion that is framented. /// The fragments m/z values are stored in a hashmap where the key is /// the generic type `T` and the value is the fragment m/z. -/// #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ElutionGroup { pub id: u64, diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index ab0baba..05bae0b 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,23 +1,57 @@ -use super::peak_in_quad::PeakInQuad; -use super::single_quad_settings::{ - expand_quad_settings, ExpandedFrameQuadSettings, SingleQuadrupoleSetting, +use super::{ + peak_in_quad::PeakInQuad, + single_quad_settings::{ + expand_quad_settings, + ExpandedFrameQuadSettings, + SingleQuadrupoleSetting, + }, +}; +use crate::{ + errors::{ + Result, + UnsupportedDataError, + }, + sort_vecs_by_first, + utils::{ + compress_explode::explode_vec, + frame_processing::{ + lazy_centroid_weighted_frame, + PeakArrayRefs, + }, + sorting::top_n, + tolerance_ranges::{ + scan_tol_range, + tof_tol_range, + IncludedRange, + }, + }, }; -use crate::errors::{Result, UnsupportedDataError}; -use crate::sort_vecs_by_first; -use crate::utils::compress_explode::explode_vec; -use crate::utils::frame_processing::{lazy_centroid_weighted_frame, PeakArrayRefs}; -use crate::utils::sorting::top_n; -use crate::utils::tolerance_ranges::IncludedRange; -use crate::utils::tolerance_ranges::{scan_tol_range, tof_tol_range}; use rayon::prelude::*; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; -use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; -use timsrust::readers::{FrameReader, FrameReaderError}; -use timsrust::{AcquisitionType, Frame, MSLevel, QuadrupoleSettings}; -use tracing::instrument; -use tracing::{info, trace, warn}; +use std::{ + collections::HashMap, + marker::PhantomData, + sync::Arc, +}; +use timsrust::{ + converters::{ + Scan2ImConverter, + Tof2MzConverter, + }, + readers::{ + FrameReader, + FrameReaderError, + }, + AcquisitionType, + Frame, + MSLevel, + QuadrupoleSettings, +}; +use tracing::{ + info, + instrument, + trace, + warn, +}; /// A frame after expanding the mobility data and re-sorting it by tof. #[derive(Debug, Clone)] @@ -489,8 +523,6 @@ impl ExpandedQuadSliceInfo { /// the intensity of the peak drops under 1% of its intensity. /// 4. The time difference between the forward and backward point is the peak width. /// 5. Finds the median of the peak widths. - /// - /// fn estimate_peak_width(frameslices: &[ExpandedFrameSlice]) -> Option { let start_idx = frameslices.len() / 5; let end_idx = frameslices.len() * 4 / 5; @@ -534,11 +566,7 @@ impl ExpandedQuadSliceInfo { .filter_map(|x| { let a = x[0]; let b = x[1]; - if a.abs_diff(b) > 5 { - Some(a) - } else { - None - } + if a.abs_diff(b) > 5 { Some(a) } else { None } }) .collect(); @@ -689,9 +717,7 @@ pub fn par_expand_and_centroid_frames( let end_peaks: usize = centroided.iter().map(|x| x.len()).sum(); trace!( "Peak counts for quad {:?}: raw={}/centroid={}", - qs, - start_peaks, - end_peaks + qs, start_peaks, end_peaks ); (qs, centroided) }) diff --git a/src/models/frames/expanded_window_group.rs b/src/models/frames/expanded_window_group.rs index 4283c2a..9cff012 100644 --- a/src/models/frames/expanded_window_group.rs +++ b/src/models/frames/expanded_window_group.rs @@ -1,8 +1,16 @@ use std::iter::repeat; -use super::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; -use super::single_quad_settings::SingleQuadrupoleSetting; -use timsrust::{AcquisitionType, MSLevel}; +use super::{ + expanded_frame::{ + ExpandedFrameSlice, + SortingStateTrait, + }, + single_quad_settings::SingleQuadrupoleSetting, +}; +use timsrust::{ + AcquisitionType, + MSLevel, +}; pub struct ExpandedWindowGroup { pub tof_indices: Vec, diff --git a/src/models/frames/raw_frames.rs b/src/models/frames/raw_frames.rs index 4cc1f9c..c40c6ae 100644 --- a/src/models/frames/raw_frames.rs +++ b/src/models/frames/raw_frames.rs @@ -1,4 +1,7 @@ -use timsrust::{Frame, QuadrupoleSettings}; +use timsrust::{ + Frame, + QuadrupoleSettings, +}; use super::raw_peak::RawPeak; use crate::utils::tolerance_ranges::IncludedRange; @@ -39,9 +42,7 @@ pub fn frame_elems_matching( ) -> impl Iterator + '_ { trace!( "frame_elems_matching tof_range: {:?}, scan_range: {:?}, quad_range: {:?}", - tof_range, - scan_range, - quad_range + tof_range, scan_range, quad_range ); let quad_scan_range = quad_range .and_then(|quad_range| scans_matching_quad(&frame.quadrupole_settings, quad_range)); diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index 12f3dab..a9071ff 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -1,5 +1,7 @@ -use std::fmt::Display; -use std::hash::Hash; +use std::{ + fmt::Display, + hash::Hash, +}; use timsrust::QuadrupoleSettings; @@ -33,7 +35,13 @@ impl Display for SingleQuadrupoleSettingRanges { write!( f, "SingleQuadrupoleSettingRanges {{ scan_start: {}, scan_end: {}, isolation_mz: {}, isolation_width: {}, isolation_high: {}, isolation_low: {}, collision_energy: {} }}", - self.scan_start, self.scan_end, self.isolation_mz, self.isolation_width, self.isolation_high, self.isolation_low, self.collision_energy + self.scan_start, + self.scan_end, + self.isolation_mz, + self.isolation_width, + self.isolation_high, + self.isolation_low, + self.collision_energy ) } } diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index ff08ed6..513ff9e 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -1,29 +1,57 @@ -use crate::errors::Result; -use crate::models::adapters::FragmentIndexAdapter; -use crate::models::elution_group::ElutionGroup; -use crate::models::frames::expanded_frame::{ - par_read_and_expand_frames, ExpandedFrameSlice, FrameProcessingConfig, SortedState, +use crate::{ + errors::Result, + models::{ + adapters::FragmentIndexAdapter, + elution_group::ElutionGroup, + frames::{ + expanded_frame::{ + par_read_and_expand_frames, + ExpandedFrameSlice, + FrameProcessingConfig, + SortedState, + }, + peak_in_quad::PeakInQuad, + raw_peak::RawPeak, + single_quad_settings::{ + get_matching_quad_settings, + matches_quad_settings, + SingleQuadrupoleSetting, + SingleQuadrupoleSettingIndex, + }, + }, + queries::{ + FragmentGroupIndexQuery, + MsLevelContext, + }, + }, + traits::{ + aggregator::Aggregator, + queriable_data::QueriableData, + }, + utils::tolerance_ranges::IncludedRange, + ToleranceAdapter, }; -use crate::models::frames::peak_in_quad::PeakInQuad; -use crate::models::frames::raw_peak::RawPeak; -use crate::models::frames::single_quad_settings::{ - get_matching_quad_settings, matches_quad_settings, SingleQuadrupoleSetting, - SingleQuadrupoleSettingIndex, -}; -use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; -use crate::traits::aggregator::Aggregator; -use crate::traits::queriable_data::QueriableData; -use crate::utils::tolerance_ranges::IncludedRange; -use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; -use std::collections::HashMap; -use std::hash::Hash; -use std::time::Instant; -use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; -use timsrust::readers::{FrameReader, MetadataReader}; -use tracing::info; -use tracing::instrument; +use std::{ + collections::HashMap, + hash::Hash, + time::Instant, +}; +use timsrust::{ + converters::{ + Scan2ImConverter, + Tof2MzConverter, + }, + readers::{ + FrameReader, + MetadataReader, + }, +}; +use tracing::{ + info, + instrument, +}; #[derive(Debug)] pub struct ExpandedRawFrameIndex { diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index 56f7b82..b1076cf 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -1,20 +1,40 @@ -use crate::models::adapters::FragmentIndexAdapter; -use crate::models::frames::raw_frames::frame_elems_matching; -use crate::models::frames::raw_peak::RawPeak; -use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; -use crate::traits::aggregator::Aggregator; -use crate::traits::queriable_data::QueriableData; -use crate::utils::tolerance_ranges::IncludedRange; -use crate::ElutionGroup; -use crate::ToleranceAdapter; +use crate::{ + models::{ + adapters::FragmentIndexAdapter, + frames::{ + raw_frames::frame_elems_matching, + raw_peak::RawPeak, + }, + queries::{ + FragmentGroupIndexQuery, + MsLevelContext, + }, + }, + traits::{ + aggregator::Aggregator, + queriable_data::QueriableData, + }, + utils::tolerance_ranges::IncludedRange, + ElutionGroup, + ToleranceAdapter, +}; use rayon::iter::ParallelIterator; use serde::Serialize; -use std::fmt::Debug; -use std::hash::Hash; -use timsrust::converters::ConvertableDomain; -use timsrust::readers::{FrameReader, FrameReaderError, MetadataReader}; -use timsrust::TimsRustError; -use timsrust::{Frame, Metadata}; +use std::{ + fmt::Debug, + hash::Hash, +}; +use timsrust::{ + converters::ConvertableDomain, + readers::{ + FrameReader, + FrameReaderError, + MetadataReader, + }, + Frame, + Metadata, + TimsRustError, +}; use tracing::trace; pub struct RawFileIndex { diff --git a/src/models/indices/transposed_quad_index/peak_bucket.rs b/src/models/indices/transposed_quad_index/peak_bucket.rs index 4ceef58..a0def3d 100644 --- a/src/models/indices/transposed_quad_index/peak_bucket.rs +++ b/src/models/indices/transposed_quad_index/peak_bucket.rs @@ -1,7 +1,14 @@ -use crate::sort_vecs_by_first; -use crate::utils::compress_explode::compress_vec; -use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::tolerance_ranges::IncludedRange; +use crate::{ + sort_vecs_by_first, + utils::{ + compress_explode::compress_vec, + display::{ + glimpse_vec, + GlimpseConfig, + }, + tolerance_ranges::IncludedRange, + }, +}; use std::fmt::Display; pub struct PeakInBucket { diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 049fd07..08ee99a 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -1,17 +1,43 @@ -use super::peak_bucket::PeakBucketBuilder; -use super::peak_bucket::{PeakBucket, PeakInBucket}; -use crate::models::frames::expanded_frame::{ExpandedFrameSlice, SortingStateTrait}; -use crate::models::frames::peak_in_quad::PeakInQuad; -use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; -use crate::sort_vecs_by_first; -use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::tolerance_ranges::IncludedRange; -use std::collections::{BTreeMap, HashMap}; -use std::fmt::Display; -use std::time::Instant; -use timsrust::converters::{ConvertableDomain, Frame2RtConverter}; -use tracing::instrument; -use tracing::{debug, info}; +use super::peak_bucket::{ + PeakBucket, + PeakBucketBuilder, + PeakInBucket, +}; +use crate::{ + models::frames::{ + expanded_frame::{ + ExpandedFrameSlice, + SortingStateTrait, + }, + peak_in_quad::PeakInQuad, + single_quad_settings::SingleQuadrupoleSetting, + }, + sort_vecs_by_first, + utils::{ + display::{ + glimpse_vec, + GlimpseConfig, + }, + tolerance_ranges::IncludedRange, + }, +}; +use std::{ + collections::{ + BTreeMap, + HashMap, + }, + fmt::Display, + time::Instant, +}; +use timsrust::converters::{ + ConvertableDomain, + Frame2RtConverter, +}; +use tracing::{ + debug, + info, + instrument, +}; #[derive(Debug)] pub struct TransposedQuadIndex { @@ -63,8 +89,22 @@ impl Display for TransposedQuadIndex { f, "TransposedQuadIndex\n quad_settings: {:?}\n frame_indices: {}\n frame_rts: {}\n peak_buckets: {}\n", self.quad_settings, - glimpse_vec(&self.frame_indices, Some(GlimpseConfig { max_items: 10, padding: 2, new_line: true })), - glimpse_vec(&self.frame_rts, Some(GlimpseConfig { max_items: 10, padding: 2, new_line: true })), + glimpse_vec( + &self.frame_indices, + Some(GlimpseConfig { + max_items: 10, + padding: 2, + new_line: true + }) + ), + glimpse_vec( + &self.frame_rts, + Some(GlimpseConfig { + max_items: 10, + padding: 2, + new_line: true + }) + ), display_peak_bucket_map(&self.peak_buckets), ) } @@ -207,7 +247,10 @@ impl TransposedQuadIndexBuilder { let curr_bucket = peak_buckets.get(&(tof as u32)).unwrap(); let real_count = curr_bucket.len(); if real_count != count { - println!("TransposedQuadIndex::build failed at tof bucket count check, expected: {}, real: {}", count, real_count); + println!( + "TransposedQuadIndex::build failed at tof bucket count check, expected: {}, real: {}", + count, real_count + ); println!("Bucket -> {:?}", curr_bucket); panic!("TransposedQuadIndex::build failed at tof bucket count check"); @@ -305,7 +348,10 @@ impl TransposedQuadIndexBuilder { } if added_peaks != tot_peaks { - println!("TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", tot_peaks, added_peaks); + println!( + "TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", + tot_peaks, added_peaks + ); panic!("TransposedQuadIndex::add_frame_slice failed at peak count check"); } @@ -411,14 +457,25 @@ impl TransposedQuadIndexBuilder { let insertion_elapsed = insertion_st.elapsed(); info!( "BatchedBuild: quad_settings={:?} start={:?} end={:?}/{} peaks {}/{} concat took {:#?} sorting took: {:#?} insertion took {:#?}", - self.quad_settings, start, end, num_slices, added_peaks, tot_peaks, concat_elapsed, sorting_elapsed, insertion_elapsed, + self.quad_settings, + start, + end, + num_slices, + added_peaks, + tot_peaks, + concat_elapsed, + sorting_elapsed, + insertion_elapsed, ); start = end; peaks_in_chunk = 0; } if added_peaks != tot_peaks { - println!("TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", tot_peaks, added_peaks); + println!( + "TransposedQuadIndex::add_frame_slice failed at peak count check, expected: {}, real: {}", + tot_peaks, added_peaks + ); panic!("TransposedQuadIndex::add_frame_slice failed at peak count check"); } diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index 548b257..bdc1b58 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -1,33 +1,73 @@ -use super::quad_index::{TransposedQuadIndex, TransposedQuadIndexBuilder}; -use crate::errors::Result; -use crate::models::adapters::FragmentIndexAdapter; -use crate::models::elution_group::ElutionGroup; -use crate::models::frames::expanded_frame::{ - par_read_and_expand_frames, ExpandedFrameSlice, FrameProcessingConfig, SortingStateTrait, +use super::quad_index::{ + TransposedQuadIndex, + TransposedQuadIndexBuilder, }; -use crate::models::frames::peak_in_quad::PeakInQuad; -use crate::models::frames::raw_peak::RawPeak; -use crate::models::frames::single_quad_settings::{ - get_matching_quad_settings, SingleQuadrupoleSetting, SingleQuadrupoleSettingIndex, +use crate::{ + errors::Result, + models::{ + adapters::FragmentIndexAdapter, + elution_group::ElutionGroup, + frames::{ + expanded_frame::{ + par_read_and_expand_frames, + ExpandedFrameSlice, + FrameProcessingConfig, + SortingStateTrait, + }, + peak_in_quad::PeakInQuad, + raw_peak::RawPeak, + single_quad_settings::{ + get_matching_quad_settings, + SingleQuadrupoleSetting, + SingleQuadrupoleSettingIndex, + }, + }, + queries::{ + FragmentGroupIndexQuery, + MsLevelContext, + }, + }, + traits::{ + aggregator::Aggregator, + queriable_data::QueriableData, + }, + utils::{ + display::{ + glimpse_vec, + GlimpseConfig, + }, + tolerance_ranges::IncludedRange, + }, + ToleranceAdapter, }; -use crate::models::queries::{FragmentGroupIndexQuery, MsLevelContext}; -use crate::traits::aggregator::Aggregator; -use crate::traits::queriable_data::QueriableData; -use crate::utils::display::{glimpse_vec, GlimpseConfig}; -use crate::utils::tolerance_ranges::IncludedRange; -use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; -use std::collections::HashMap; -use std::fmt::Debug; -use std::fmt::Display; -use std::hash::Hash; -use std::time::Instant; -use timsrust::converters::{Scan2ImConverter, Tof2MzConverter}; -use timsrust::readers::{FrameReader, MetadataReader}; -use timsrust::Metadata; -use tracing::instrument; -use tracing::{debug, info, trace}; +use std::{ + collections::HashMap, + fmt::{ + Debug, + Display, + }, + hash::Hash, + time::Instant, +}; +use timsrust::{ + converters::{ + Scan2ImConverter, + Tof2MzConverter, + }, + readers::{ + FrameReader, + MetadataReader, + }, + Metadata, +}; +use tracing::{ + debug, + info, + instrument, + trace, +}; // TODO break this module apart ... its getting too big for my taste // - JSP: 2024-11-19 diff --git a/src/models/queries.rs b/src/models/queries.rs index 83da21d..5daf53e 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -1,8 +1,19 @@ -use crate::traits::aggregator::{NoContext, ProvidesContext}; -use crate::utils::tolerance_ranges::IncludedRange; -use std::collections::HashMap; -use std::hash::Hash; -use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; +use crate::{ + traits::aggregator::{ + NoContext, + ProvidesContext, + }, + utils::tolerance_ranges::IncludedRange, +}; +use std::{ + collections::HashMap, + hash::Hash, +}; +use timsrust::converters::{ + ConvertableDomain, + Scan2ImConverter, + Tof2MzConverter, +}; #[derive(Debug, Clone)] pub struct PrecursorIndexQuery { diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 88853fc..8f0f985 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -1,11 +1,21 @@ +use crate::{ + traits::aggregator::ProvidesContext, + Aggregator, + ElutionGroup, + QueriableData, + Tolerance, + ToleranceAdapter, +}; use rayon::prelude::*; use serde::Serialize; -use std::hash::Hash; -use std::time::Instant; -use tracing::info; - -use crate::traits::aggregator::ProvidesContext; -use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter}; +use std::{ + hash::Hash, + time::Instant, +}; +use tracing::{ + info, + instrument, +}; // TODO: URGENTLY make documentation fot eh functions using this leftover struct docs // as a reference. @@ -43,6 +53,7 @@ use crate::{Aggregator, ElutionGroup, QueriableData, Tolerance, ToleranceAdapter // 2. `QD` is queried with `QP` and `QF` queries. // 3. The results are aggregated with `AG` aggregators (which are generated by the factory, when passed a numeric ID). +#[instrument(skip_all)] pub fn query_multi_group<'a, QD, TL, QF, AE1, AE2, OE, AG, FH, CTX1, CTX2, FF>( queriable_data: &'a QD, tolerance: &'a TL, @@ -56,7 +67,6 @@ where OE: Send + Sync, FH: Clone + Eq + Serialize + Hash + Send + Sync, QF: Send + Sync + ProvidesContext, - // AE: Send + Sync + Clone + Copy, AE1: Into + Send + Sync + Clone + Copy, AE2: Send + Sync + Clone + Copy + From, CTX1: Into + Send + Sync + Clone + Copy, diff --git a/src/traits/queriable_data.rs b/src/traits/queriable_data.rs index 55dd7af..e7e509d 100644 --- a/src/traits/queriable_data.rs +++ b/src/traits/queriable_data.rs @@ -1,4 +1,7 @@ -use crate::traits::aggregator::{Aggregator, ProvidesContext}; +use crate::traits::aggregator::{ + Aggregator, + ProvidesContext, +}; use rayon::prelude::*; pub trait QueriableData diff --git a/src/traits/tolerance.rs b/src/traits/tolerance.rs index 63246fa..4e0d110 100644 --- a/src/traits/tolerance.rs +++ b/src/traits/tolerance.rs @@ -1,5 +1,8 @@ use core::f32; -use serde::{Deserialize, Serialize}; +use serde::{ + Deserialize, + Serialize, +}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MzToleramce { @@ -50,11 +53,14 @@ impl Default for DefaultTolerance { } } +const PROTON_MASS: f64 = 1.007276; +const NEUTRON_MASS: f64 = 1.008664; + fn mass_to_isotope_mzs(mass: f64, charge: u8, num_isotopes: usize) -> Vec { let mut out = Vec::with_capacity(num_isotopes); - const PROTON_MASS: f64 = 1.007276; - for i in 1..(num_isotopes + 1) { - let mass = mass + i as f64 * PROTON_MASS; + let real_mass = mass + (charge as f64 * PROTON_MASS); + for i in 0..(num_isotopes) { + let mass = real_mass + (i as f64 * NEUTRON_MASS); let mz = mass / charge as f64; out.push(mz); } @@ -63,7 +69,6 @@ fn mass_to_isotope_mzs(mass: f64, charge: u8, num_isotopes: usize) -> Vec { fn mz_to_isotope_mzs(mz: f64, charge: u8, num_isotopes: usize) -> Vec { let mut out = Vec::with_capacity(num_isotopes); - const PROTON_MASS: f64 = 1.007276; let proton_mass_frac = PROTON_MASS / charge as f64; for i in 0..num_isotopes { let mz = mz + (i as f64 * proton_mass_frac); @@ -127,7 +132,10 @@ impl Tolerance for DefaultTolerance { fn quad_range(&self, precursor_mz: f64, precursor_charge: u8) -> (f32, f32) { // Should this be a recoverable error? if precursor_charge == 0 { - panic!("Precursor charge is 0, inputs: self: {:?}, precursor_mz: {:?}, precursor_charge: {:?}", self, precursor_mz, precursor_charge); + panic!( + "Precursor charge is 0, inputs: self: {:?}, precursor_mz: {:?}, precursor_charge: {:?}", + self, precursor_mz, precursor_charge + ); }; match self.quad { QuadTolerance::Absolute((low, high, num_isotopes)) => { @@ -174,8 +182,9 @@ mod tests { fn test_isotope_mzs_neutral() { let test_vals = vec![ (100.0, 1, vec![101.0, 102.0, 103.0]), - (100.0, 2, vec![100.5, 101.0, 101.5]), - (100.0, 3, vec![100.33333, 100.666666, 101.0]), + // For z2, mass would be 102 (100 + 2 protons).... but since charge is 2 then we divide that + (100.0, 2, vec![51.0, 51.5, 52.0]), + (100.0, 3, vec![34.34, 34.67, 35.0]), ]; for (monoisotopic_mass, charge, expected) in test_vals { @@ -188,7 +197,13 @@ mod tests { .collect(); for ad in abs_diff.iter() { // Very tight tolerances here ... - assert!(*ad < 0.01); + assert!( + *ad < 0.03, + "Expected {:?}, got {:?}, diff {:?}", + expected, + out, + ad + ); } } } @@ -196,9 +211,9 @@ mod tests { #[test] fn test_isotope_mzs_mz() { let test_vals = vec![ - (100.0, 1, vec![10.0, 101.0, 102.0]), - (100.0, 2, vec![100.0, 100.5, 101.5]), - (100.0, 3, vec![100.0, 100.3333, 101.66666]), + (100.0, 1, vec![100.0, 101.0, 102.0]), + (100.0, 2, vec![100.0, 100.5, 101.0]), + (100.0, 3, vec![100.0, 100.3333, 100.66666]), ]; for (monoisotopic_mass, charge, expected) in test_vals { @@ -212,7 +227,13 @@ mod tests { for ad in abs_diff.iter() { // Very tight tolerances here ... - assert!(*ad < 0.01); + assert!( + *ad < 0.03, + "Expected {:?}, got {:?}, diff {:?}", + expected, + out, + ad + ); } } } diff --git a/src/utils/correlation.rs b/src/utils/correlation.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/utils/correlation.rs @@ -0,0 +1 @@ + diff --git a/src/utils/frame_processing.rs b/src/utils/frame_processing.rs index 6a0ecfb..5657bbb 100644 --- a/src/utils/frame_processing.rs +++ b/src/utils/frame_processing.rs @@ -1,5 +1,9 @@ use crate::sort_vecs_by_first; -use tracing::{error, info, warn}; +use tracing::{ + error, + info, + warn, +}; use super::tolerance_ranges::IncludedRange; @@ -189,11 +193,11 @@ pub fn lazy_centroid_weighted_frame<'a>( let out = sort_n_check(agg_intensity, agg_tof, agg_ims); // TODO:Make everything below this a separate function and accumulate it. - let tot_final_intensity = out.0 .1.iter().map(|x| *x as u64).sum::(); + let tot_final_intensity = out.0.1.iter().map(|x| *x as u64).sum::(); let inten_ratio = tot_final_intensity as f64 / initial_tot_intensity as f64; assert!(initial_tot_intensity >= tot_final_intensity); - let output_len = out.0 .0.len(); + let output_len = out.0.0.len(); let compression_ratio = output_len as f64 / arr_len as f64; assert!(num_added == output_len); @@ -212,8 +216,8 @@ pub fn lazy_centroid_weighted_frame<'a>( warn!("tot_final_intensity: {:?}", tot_final_intensity); warn!( "First tof {} -> Range {:?}", - out.0 .0[0], - tof_tol_range_fn(out.0 .0[0]) + out.0.0[0], + tof_tol_range_fn(out.0.0[0]) ); panic!(); // warn!("agg_intensity: {:?}", out.0 .0); diff --git a/src/utils/math.rs b/src/utils/math.rs index 029fd70..7f3e5e5 100644 --- a/src/utils/math.rs +++ b/src/utils/math.rs @@ -18,3 +18,87 @@ pub fn lnfact_float(n: f64) -> f64 { n * n.ln() - n + 0.5 * n.ln() + 0.5 * (std::f64::consts::PI * 2.0 * n).ln() } } + +/// Logarigthmic mean of a slice of values. +pub fn lnmean(vals: &[f64]) -> f64 { + let mut sum = 0.0; + for val in vals { + sum += val.ln(); + } + (sum / vals.len() as f64).exp() +} + +fn cosine_similarity(a: &[f64], b: &[f64]) -> Option { + // Check if vectors have the same length and are not empty + if a.len() != b.len() || a.is_empty() { + return None; + } + + // Calculate dot product (numerator) + let dot_product: f64 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum(); + + // Calculate magnitudes (denominator) + let magnitude_a: f64 = a.iter().map(|&x| x * x).sum::().sqrt(); + + let magnitude_b: f64 = b.iter().map(|&x| x * x).sum::().sqrt(); + + // Avoid division by zero + if magnitude_a == 0.0 || magnitude_b == 0.0 { + return None; + } + + // Calculate cosine similarity + Some(dot_product / (magnitude_a * magnitude_b)) +} + +// Example usage: +fn main() { + let vector_a = vec![1.0, 2.0, 3.0]; + let vector_b = vec![4.0, 5.0, 6.0]; + + match cosine_similarity(&vector_a, &vector_b) { + Some(similarity) => println!("Cosine similarity: {}", similarity), + None => println!("Could not calculate cosine similarity"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cosine_similarity() { + let a = vec![1.0, 2.0, 3.0]; + let b = vec![4.0, 5.0, 6.0]; + let result = cosine_similarity(&a, &b).unwrap(); + assert!((result - 0.974631846).abs() < 1e-8); + } + + #[test] + fn test_identical_vectors() { + let a = vec![1.0, 1.0, 1.0]; + let result = cosine_similarity(&a, &a).unwrap(); + assert!((result - 1.0).abs() < 1e-8); + } + + #[test] + fn test_empty_vectors() { + let a: Vec = vec![]; + let b: Vec = vec![]; + assert!(cosine_similarity(&a, &b).is_none()); + } + + #[test] + fn test_different_lengths() { + let a = vec![1.0, 2.0]; + let b = vec![1.0, 2.0, 3.0]; + assert!(cosine_similarity(&a, &b).is_none()); + } + + #[test] + fn test_zero_vector() { + let a = vec![0.0, 0.0, 0.0]; + let b = vec![1.0, 2.0, 3.0]; + assert!(cosine_similarity(&a, &b).is_none()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1d8244c..225a906 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,8 @@ pub mod compress_explode; +pub mod correlation; pub mod display; pub mod frame_processing; pub mod math; +pub mod scoring; pub mod sorting; pub mod tolerance_ranges; diff --git a/src/utils/scoring.rs b/src/utils/scoring.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/utils/scoring.rs @@ -0,0 +1 @@ + diff --git a/src/utils/sorting.rs b/src/utils/sorting.rs index c35b22a..3e9643f 100644 --- a/src/utils/sorting.rs +++ b/src/utils/sorting.rs @@ -21,7 +21,6 @@ /// assert_eq!(out.1, vec![3, 2, 1]); /// assert_eq!(out.2, vec!['c', 'b', 'a']); /// ``` -/// #[macro_export] macro_rules! sort_vecs_by_first { ($first:expr $(,$rest:expr)*) => {{ diff --git a/src/utils/tolerance_ranges.rs b/src/utils/tolerance_ranges.rs index 4bde46d..5099546 100644 --- a/src/utils/tolerance_ranges.rs +++ b/src/utils/tolerance_ranges.rs @@ -1,4 +1,8 @@ -use timsrust::converters::{ConvertableDomain, Scan2ImConverter, Tof2MzConverter}; +use timsrust::converters::{ + ConvertableDomain, + Scan2ImConverter, + Tof2MzConverter, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct IncludedRange(pub T, pub T); From dd0cb501ca0016eb1efcb7da699a71e79c0b50ce Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sun, 3 Nov 2024 11:24:25 -0800 Subject: [PATCH 21/30] chore: added pre-commit --- .gitignore | 1 - .pre-commit-config.yaml | 31 ++ README.md | 2 +- Taskfile.yml | 6 +- benches/benchmark_indices.rs | 56 ++-- data/sageresults/.gitignore | 2 +- data/sageresults/check.bash | 2 +- data/sageresults/ubb_elution_groups.json | 2 +- ..._results_230510_PRTC_13_S1-B1_1_12817.json | 2 +- rustfmt.toml | 2 +- src/lib.rs | 18 +- src/main.rs | 54 ++- src/models/adapters.rs | 22 +- .../raw_peak_agg/chromatogram_agg.rs | 11 +- src/models/aggregators/raw_peak_agg/mod.rs | 6 +- .../multi_chromatogram_agg/base.rs | 289 ++++++++++++++-- .../multi_chromatogram_agg.rs | 314 +----------------- .../aggregators/raw_peak_agg/point_agg.rs | 11 +- src/models/aggregators/rolling_calculators.rs | 5 +- src/models/elution_group.rs | 6 +- src/models/frames/expanded_frame.rs | 70 ++-- src/models/frames/expanded_window_group.rs | 10 +- src/models/frames/single_quad_settings.rs | 6 +- .../indices/expanded_raw_index/model.rs | 80 ++--- src/models/indices/raw_file_index.rs | 41 +-- .../transposed_quad_index/peak_bucket.rs | 16 +- .../transposed_quad_index/quad_index.rs | 40 +-- .../quad_splitted_transposed_index.rs | 99 +++--- src/models/queries.rs | 18 +- .../queriable_tims_data.rs | 8 +- src/utils/math.rs | 11 - src/utils/tolerance_ranges.rs | 6 +- 32 files changed, 558 insertions(+), 689 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.gitignore b/.gitignore index 926a8d5..c21c2ce 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ /tmp .env results.json - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f2af126 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace +- repo: local + hooks: + - id: fmt + name: fmt + description: Format files with cargo fmt. + entry: cargo +nightly fmt + language: system + types: [rust] + args: ["--"] + - id: cargo-check + name: cargo check + description: Check the package for errors. + entry: cargo check + language: system + types: [rust] + pass_filenames: false + - id: clippy + name: clippy + description: Lint rust sources + entry: cargo clippy + language: system + args: ["--", "-D", "warnings"] + types: [rust] + pass_filenames: false + + diff --git a/README.md b/README.md index 72a9234..a661f33 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ your file, and you get back results that match those three things! More explicitly: - The main design is to have modular components: - aggregators - - indices + - indices - queries - tolerances diff --git a/Taskfile.yml b/Taskfile.yml index cbca819..6335cfa 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -37,7 +37,7 @@ tasks: clippy: cmds: - - cargo clippy + - cargo clippy {{.CLI_ARGS}} bench: sources: @@ -46,8 +46,8 @@ tasks: - cargo b --release --features bench --bin benchmark_indices - SKIP_SLOW=1 SKIP_BUILD=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/230510_PRTC_13_S1-B1_1_12817.d ./target/release/benchmark_indices - uv run benches/plot_bench.py data/benchmark_results_230510_PRTC_13_S1-B1_1_12817.json - - # SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices - - # uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json + - # SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices + - # uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json plot: sources: diff --git a/benches/benchmark_indices.rs b/benches/benchmark_indices.rs index d62c9fe..e37617c 100644 --- a/benches/benchmark_indices.rs +++ b/benches/benchmark_indices.rs @@ -4,48 +4,40 @@ use rand::{ }; use rand_chacha::ChaCha8Rng; use serde::Serialize; -use std::{ - collections::HashMap, - env, - fs::File, - path::{ - Path, - PathBuf, - }, - time::{ - Duration, - Instant, - }, +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::path::{ + Path, + PathBuf, }; -use timsquery::{ - models::{ - aggregators::RawPeakIntensityAggregator, - indices::{ - expanded_raw_index::ExpandedRawFrameIndex, - raw_file_index::RawFileIndex, - transposed_quad_index::QuadSplittedTransposedIndex, - }, - }, - queriable_tims_data::queriable_tims_data::query_multi_group, - traits::tolerance::{ - DefaultTolerance, - MobilityTolerance, - MzToleramce, - QuadTolerance, - RtTolerance, - }, - ElutionGroup, +use std::time::{ + Duration, + Instant, +}; +use timsquery::models::aggregators::RawPeakIntensityAggregator; +use timsquery::models::indices::expanded_raw_index::ExpandedRawFrameIndex; +use timsquery::models::indices::raw_file_index::RawFileIndex; +use timsquery::models::indices::transposed_quad_index::QuadSplittedTransposedIndex; +use timsquery::queriable_tims_data::queriable_tims_data::query_multi_group; +use timsquery::traits::tolerance::{ + DefaultTolerance, + MobilityTolerance, + MzToleramce, + QuadTolerance, + RtTolerance, }; +use timsquery::ElutionGroup; use tracing::subscriber::set_global_default; use tracing_bunyan_formatter::{ BunyanFormattingLayer, JsonStorageLayer, }; use tracing_chrome::ChromeLayerBuilder; +use tracing_subscriber::prelude::*; +use tracing_subscriber::registry::Registry; use tracing_subscriber::{ fmt, - prelude::*, - registry::Registry, EnvFilter, Layer, }; diff --git a/data/sageresults/.gitignore b/data/sageresults/.gitignore index 774378f..4030c7f 100644 --- a/data/sageresults/.gitignore +++ b/data/sageresults/.gitignore @@ -2,4 +2,4 @@ lfq.tsv results.sage.tsv matched_fragments.sage.tsv sage -log.log \ No newline at end of file +log.log diff --git a/data/sageresults/check.bash b/data/sageresults/check.bash index dc23fb5..0213bd4 100644 --- a/data/sageresults/check.bash +++ b/data/sageresults/check.bash @@ -10,4 +10,4 @@ set -o pipefail # uv run build.py ../../target/release/timsquery query-index --raw-file-path ../230510_PRTC_13_S1-B1_1_12817.d --tolerance-settings-path "../../templates/tolerance_settings_narrow_rt.json" --elution-groups-path "ubb_elution_groups.json" --output-path "query_results" --index expanded-raw-frame-index --aggregator multi-cmg-stats --format pretty-json -uv run plot.py \ No newline at end of file +uv run plot.py diff --git a/data/sageresults/ubb_elution_groups.json b/data/sageresults/ubb_elution_groups.json index 0c8970b..4cbd150 100644 --- a/data/sageresults/ubb_elution_groups.json +++ b/data/sageresults/ubb_elution_groups.json @@ -75,4 +75,4 @@ "13": 288.20294 } } -] \ No newline at end of file +] diff --git a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json index 6c29c95..77bed42 100644 --- a/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json +++ b/data/slim_benchmark_results_230510_PRTC_13_S1-B1_1_12817.json @@ -90,4 +90,4 @@ "basename": "230510_PRTC_13_S1-B1_1_12817" }, "full_benchmark_time_seconds": 648.106197917 -} \ No newline at end of file +} diff --git a/rustfmt.toml b/rustfmt.toml index c67c611..9a8eadb 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,6 @@ imports_layout = "Vertical" -imports_granularity = "Crate" +imports_granularity = "Module" normalize_comments = true reorder_impl_items = true version = "Two" diff --git a/src/lib.rs b/src/lib.rs index d0c0ebf..3d254d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,13 @@ // Re-export main structures -pub use crate::models::{ - elution_group::ElutionGroup, - indices::transposed_quad_index::QuadSplittedTransposedIndex, -}; +pub use crate::models::elution_group::ElutionGroup; +pub use crate::models::indices::transposed_quad_index::QuadSplittedTransposedIndex; // Re-export traits -pub use crate::traits::{ - aggregator::Aggregator, - queriable_data::QueriableData, - tolerance::{ - Tolerance, - ToleranceAdapter, - }, +pub use crate::traits::aggregator::Aggregator; +pub use crate::traits::queriable_data::QueriableData; +pub use crate::traits::tolerance::{ + Tolerance, + ToleranceAdapter, }; // Declare modules diff --git a/src/main.rs b/src/main.rs index 88c6b48..5616f0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,45 +6,35 @@ use serde::{ Deserialize, Serialize, }; -use std::{ - collections::HashMap, - time::Instant, +use std::collections::HashMap; +use std::time::Instant; +use timsquery::models::aggregators::{ + MultiCMGStatsFactory, + RawPeakIntensityAggregator, + RawPeakVectorAggregator, }; -use timsquery::{ - models::{ - aggregators::{ - MultiCMGStatsFactory, - RawPeakIntensityAggregator, - RawPeakVectorAggregator, - }, - elution_group::ElutionGroup, - indices::{ - ExpandedRawFrameIndex, - QuadSplittedTransposedIndex, - }, - }, - queriable_tims_data::queriable_tims_data::query_multi_group, - traits::tolerance::{ - DefaultTolerance, - MobilityTolerance, - MzToleramce, - QuadTolerance, - RtTolerance, - }, +use timsquery::models::elution_group::ElutionGroup; +use timsquery::models::indices::{ + ExpandedRawFrameIndex, + QuadSplittedTransposedIndex, }; -use tracing::{ - instrument, - subscriber::set_global_default, +use timsquery::queriable_tims_data::queriable_tims_data::query_multi_group; +use timsquery::traits::tolerance::{ + DefaultTolerance, + MobilityTolerance, + MzToleramce, + QuadTolerance, + RtTolerance, }; +use tracing::instrument; +use tracing::subscriber::set_global_default; use tracing_bunyan_formatter::{ BunyanFormattingLayer, JsonStorageLayer, }; -use tracing_subscriber::{ - prelude::*, - registry::Registry, - EnvFilter, -}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::registry::Registry; +use tracing_subscriber::EnvFilter; fn main() { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); diff --git a/src/models/adapters.rs b/src/models/adapters.rs index fe494d7..013199f 100644 --- a/src/models/adapters.rs +++ b/src/models/adapters.rs @@ -1,20 +1,14 @@ -use crate::{ - models::{ - elution_group::ElutionGroup, - queries::{ - FragmentGroupIndexQuery, - PrecursorIndexQuery, - }, - }, - utils::tolerance_ranges::IncludedRange, - ToleranceAdapter, +use crate::models::elution_group::ElutionGroup; +use crate::models::queries::{ + FragmentGroupIndexQuery, + PrecursorIndexQuery, }; +use crate::utils::tolerance_ranges::IncludedRange; +use crate::ToleranceAdapter; use serde::Serialize; use std::hash::Hash; -use timsrust::{ - converters::ConvertableDomain, - Metadata, -}; +use timsrust::converters::ConvertableDomain; +use timsrust::Metadata; #[derive(Debug, Default)] pub struct FragmentIndexAdapter { diff --git a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs index 4c06b9c..65e3c3f 100644 --- a/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/chromatogram_agg.rs @@ -1,15 +1,8 @@ use super::super::streaming_aggregator::RunningStatsCalculator; -use crate::{ - models::frames::raw_peak::RawPeak, - sort_vecs_by_first, - traits::aggregator::Aggregator, -}; +use crate::sort_vecs_by_first; use serde::Serialize; -use std::collections::{ - BTreeMap, - HashMap, -}; +use std::collections::HashMap; pub type MappingCollection = HashMap; diff --git a/src/models/aggregators/raw_peak_agg/mod.rs b/src/models/aggregators/raw_peak_agg/mod.rs index 03ec9b5..2d034c6 100644 --- a/src/models/aggregators/raw_peak_agg/mod.rs +++ b/src/models/aggregators/raw_peak_agg/mod.rs @@ -5,10 +5,8 @@ pub mod point_agg; pub use chromatogram_agg::ChromatomobilogramStats; pub use multi_chromatogram_agg::MultiCMGStatsAgg; // TODO: reorganize this so I donr use direcly from `base`` -pub use multi_chromatogram_agg::{ - base::PartitionedCMGArrays, - MultiCMGStatsFactory, -}; +pub use multi_chromatogram_agg::base::PartitionedCMGArrays; +pub use multi_chromatogram_agg::MultiCMGStatsFactory; pub use point_agg::{ RawPeakIntensityAggregator, RawPeakVectorAggregator, diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs index fc089f8..405d772 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/base.rs @@ -3,28 +3,22 @@ use super::super::chromatogram_agg::{ MappingCollection, ScanTofStatsCalculatorPair, }; -use crate::{ - models::{ - aggregators::{ - rolling_calculators::rolling_median, - streaming_aggregator::RunningStatsCalculator, - }, - frames::raw_peak::RawPeak, - queries::MsLevelContext, - }, - traits::aggregator::Aggregator, - utils::math::{ - lnfact, - lnfact_float, - }, +use crate::models::aggregators::rolling_calculators::{ + calculate_lazy_hyperscore, + calculate_value_vs_baseline, }; +use crate::models::aggregators::streaming_aggregator::RunningStatsCalculator; +use crate::utils::math::lnfact_float; use serde::Serialize; -use std::{ - collections::{ - BTreeMap, - HashSet, - }, - hash::Hash, +use std::collections::{ + BTreeMap, + HashSet, +}; +use std::hash::Hash; +use timsrust::converters::{ + ConvertableDomain, + Scan2ImConverter, + Tof2MzConverter, }; use tracing::{ debug, @@ -38,6 +32,41 @@ pub struct ParitionedCMGAggregator, } +#[derive(Debug, Clone, Serialize)] +pub struct PartitionedCMGArrayStats { + pub retention_time_miliseconds: Vec, + pub weighted_scan_index_mean: Vec, + pub summed_intensity: Vec, + + // This should be the same as the sum of log intensities. + pub log_intensity_products: Vec, + pub npeaks: Vec, + pub scan_index_means: MappingCollection>, + pub tof_index_means: MappingCollection>, + // TODO consider if I want to add the standard deviations ... RN they dont + // seem to be that useful. + pub intensities: MappingCollection>, +} + +// This name is starting to get really long ... +#[derive(Debug, Clone, Serialize)] +pub struct PartitionedCMGScoredStatsArrays { + pub retention_time_miliseconds: Vec, + pub average_mobility: Vec, + pub summed_intensity: Vec, + pub npeaks: Vec, + pub lazy_hyperscore: Vec, + pub lazy_hyperscore_vs_baseline: Vec, + pub norm_hyperscore_vs_baseline: Vec, + pub lazyerscore: Vec, + pub lazyerscore_vs_baseline: Vec, + pub norm_lazyerscore_vs_baseline: Vec, + pub transition_mobilities: MappingCollection>, + pub transition_mzs: MappingCollection>, + pub transition_intensities: MappingCollection>, + pub apex_primary_score_index: usize, +} + impl Default for ParitionedCMGAggregator { fn default() -> Self { Self { @@ -87,3 +116,223 @@ impl ParitionedCMGAggregator PartitionedCMGScoredStatsArrays { + // TODO: Refactor this function ... its getting pretty big. + pub fn new( + other: PartitionedCMGArrayStats, + mz_converter: &Tof2MzConverter, + mobility_converter: &Scan2ImConverter, + ) -> Self { + let lazy_hyperscore = calculate_lazy_hyperscore(&other.npeaks, &other.summed_intensity); + let basline_window_len = 1 + (other.retention_time_miliseconds.len() / 10); + let lazy_hyperscore_vs_baseline = + calculate_value_vs_baseline(&lazy_hyperscore, basline_window_len); + let lazyerscore: Vec = other + .log_intensity_products + .iter() + .map(|x| lnfact_float(*x)) + .collect(); + let lazyerscore_vs_baseline = calculate_value_vs_baseline(&lazyerscore, basline_window_len); + + // Set 0 the NANs + // Q: Can I do this in-place? Will the comnpiler do it for me? + let lazy_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline + .into_iter() + .map(|x| if x.is_nan() { 0.0 } else { x }) + .collect(); + let lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline + .into_iter() + .map(|x| if x.is_nan() { 0.0 } else { x }) + .collect(); + + // Calculate the standard deviation of the lazyscores v baseline + let mut sd_calculator_hyperscore = RunningStatsCalculator::new(1, 0.0); + let mut sd_calculator_lazyscore = RunningStatsCalculator::new(1, 0.0); + + (0..lazyerscore_vs_baseline.len()).for_each(|i| { + sd_calculator_hyperscore.add(lazy_hyperscore_vs_baseline[i], 1); + sd_calculator_lazyscore.add(lazyerscore_vs_baseline[i], 1); + }); + let sd_hyperscore = sd_calculator_hyperscore.standard_deviation().unwrap(); + let sd_lazyscore = sd_calculator_lazyscore.standard_deviation().unwrap(); + + // Calculate the normalized scores + let norm_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline + .iter() + .map(|x| x / sd_hyperscore) + .collect(); + let norm_lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline + .iter() + .map(|x| x / sd_lazyscore) + .collect(); + + let mut apex_primary_score_index = 0; + let mut max_primary_score = 0.0f64; + let primary_scores = &norm_lazyerscore_vs_baseline; + for (i, val) in primary_scores.iter().enumerate() { + if max_primary_score.is_nan() || *val > max_primary_score { + max_primary_score = *val; + apex_primary_score_index = i; + } + } + + assert!( + lazy_hyperscore_vs_baseline.len() == other.retention_time_miliseconds.len(), + "Failed sanity check" + ); + + PartitionedCMGScoredStatsArrays { + retention_time_miliseconds: other.retention_time_miliseconds, + average_mobility: other + .weighted_scan_index_mean + .into_iter() + .map(|x| { + let out = mobility_converter.convert(x); + if !(0.5..=2.0).contains(&out) { + debug!("Bad mobility value: {:?}, input was {:?}", out, x); + } + out + }) + .collect(), + summed_intensity: other.summed_intensity, + npeaks: other.npeaks, + transition_mobilities: other + .scan_index_means + .into_iter() + .map(|(k, v)| { + let new_v = v + .into_iter() + .map(|x| mobility_converter.convert(x)) + .collect(); + (k, new_v) + }) + .collect(), + transition_mzs: other + .tof_index_means + .into_iter() + .map(|(k, v)| { + let new_v = v.into_iter().map(|x| mz_converter.convert(x)).collect(); + (k, new_v) + }) + .collect(), + transition_intensities: other.intensities, + lazy_hyperscore, + lazy_hyperscore_vs_baseline, + apex_primary_score_index, + lazyerscore, + lazyerscore_vs_baseline, + norm_hyperscore_vs_baseline, + norm_lazyerscore_vs_baseline, + } + } +} + +impl From> + for PartitionedCMGArrayStats +{ + fn from(other: PartitionedCMGArrays) -> Self { + // TODO ... maybe refactor this ... RN its king of ugly. + + let mut out = PartitionedCMGArrayStats { + retention_time_miliseconds: Vec::new(), + scan_index_means: MappingCollection::new(), + tof_index_means: MappingCollection::new(), + weighted_scan_index_mean: Vec::new(), + intensities: MappingCollection::new(), + summed_intensity: Vec::new(), + log_intensity_products: Vec::new(), + npeaks: Vec::new(), + }; + + let unique_rts = other + .transition_stats + .iter() + .flat_map(|(_k, v)| v.retention_time_miliseconds.clone()) + .collect::>(); + let mut unique_rts = unique_rts.into_iter().collect::>(); + unique_rts.sort(); + + // Q: Is this the most efficient way to do this? + // I think having the btrees as the unit of integration might not be the best idea. + // ... If I want to preserve the sparsity, I can use a hashmap and the sort it. + let mut summed_intensity_tree: BTreeMap = + BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); + let mut intensity_logsums_tree: BTreeMap = + BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0.0))); + let mut npeaks_tree: BTreeMap = + BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); + let mut weighted_tof_index_mean_tree: BTreeMap = + BTreeMap::new(); + let mut weighted_scan_index_mean_tree: BTreeMap = + BTreeMap::new(); + + for (k, v) in other.transition_stats.into_iter() { + type ScanTofIntensityTuple = (f64, f64, u64); + type ScanTofIntensityVecs = (Vec, (Vec, Vec)); + let mut tmp_tree: BTreeMap = BTreeMap::new(); + + for i in 0..v.retention_time_miliseconds.len() { + let rt = v.retention_time_miliseconds[i]; + let scan = v.scan_index_means[i]; + let tof = v.tof_index_means[i]; + let inten = v.intensities[i]; + + if inten > 100 { + npeaks_tree.entry(rt).and_modify(|curr| *curr += 1); + } + tmp_tree.entry(rt).or_insert((scan, tof, inten)); + summed_intensity_tree + .entry(rt) + .and_modify(|curr| *curr += inten); + + intensity_logsums_tree.entry(rt).and_modify(|curr| { + if inten > 10 { + *curr += (inten as f64).ln() + } + }); + weighted_tof_index_mean_tree + .entry(rt) + .and_modify(|curr| curr.add(tof, inten)) + .or_insert(RunningStatsCalculator::new(inten, tof)); + weighted_scan_index_mean_tree + .entry(rt) + .and_modify(|curr| curr.add(scan, inten)) + .or_insert(RunningStatsCalculator::new(inten, scan)); + } + + // Now we fill with nans the missing values. + // Q: Do I really need to do this here? + for rt in unique_rts.iter() { + tmp_tree.entry(*rt).or_insert((f64::NAN, f64::NAN, 0)); + } + + let (out_scans, (out_tofs, out_intens)): ScanTofIntensityVecs = tmp_tree + .into_iter() + .map(|(_, (scan, tof, inten))| (scan, (tof, inten))) + .unzip(); + out.scan_index_means.insert(k.clone(), out_scans); + out.tof_index_means.insert(k.clone(), out_tofs); + out.intensities.insert(k.clone(), out_intens); + } + + out.retention_time_miliseconds = unique_rts; + out.summed_intensity = summed_intensity_tree.into_values().collect(); + out.npeaks = npeaks_tree.into_values().collect(); + out.weighted_scan_index_mean = weighted_scan_index_mean_tree + .into_values() + .map(|x| { + let out = x.mean().expect("At least one value should be present"); + if !(0.0..=1000.0).contains(&out) { + warn!("Bad mobility value: {:?}, input was {:?}", out, x); + } + + out + }) + .collect(); + + // Note: The log of products is the same as the sum of logs. + out.log_intensity_products = intensity_logsums_tree.into_values().collect(); + out + } +} diff --git a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs index a7583da..e19d31d 100644 --- a/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs +++ b/src/models/aggregators/raw_peak_agg/multi_chromatogram_agg/multi_chromatogram_agg.rs @@ -1,50 +1,20 @@ -use super::{ - super::chromatogram_agg::{ - ChromatomobilogramStatsArrays, - MappingCollection, - ScanTofStatsCalculatorPair, - }, - base::{ - ParitionedCMGAggregator, - PartitionedCMGArrays, - }, -}; -use crate::{ - models::{ - aggregators::{ - rolling_calculators::{ - calculate_lazy_hyperscore, - calculate_value_vs_baseline, - }, - streaming_aggregator::RunningStatsCalculator, - }, - frames::raw_peak::RawPeak, - queries::MsLevelContext, - }, - traits::aggregator::Aggregator, - utils::math::{ - lnfact, - lnfact_float, - }, +use super::super::chromatogram_agg::ScanTofStatsCalculatorPair; +use super::base::{ + ParitionedCMGAggregator, + PartitionedCMGArrayStats, + PartitionedCMGScoredStatsArrays, }; +use crate::models::frames::raw_peak::RawPeak; +use crate::models::queries::MsLevelContext; +use crate::traits::aggregator::Aggregator; use serde::Serialize; -use std::{ - collections::{ - BTreeMap, - HashSet, - }, - hash::Hash, -}; -use tracing::{ - debug, - warn, -}; +use std::hash::Hash; use timsrust::converters::{ - ConvertableDomain, Scan2ImConverter, Tof2MzConverter, }; + #[derive(Debug, Clone)] pub struct MultiCMGStatsAgg { pub converters: (Tof2MzConverter, Scan2ImConverter), @@ -75,273 +45,19 @@ impl MultiCMGStatsFactory { } #[derive(Debug, Clone, Serialize)] -pub struct PartitionedCMGArrayStats { - pub retention_time_miliseconds: Vec, - pub weighted_scan_index_mean: Vec, - pub summed_intensity: Vec, - - // This should be the same as the sum of log intensities. - pub log_intensity_products: Vec, - pub npeaks: Vec, - pub scan_index_means: MappingCollection>, - pub tof_index_means: MappingCollection>, - // TODO consider if I want to add the standard deviations ... RN they dont - // seem to be that useful. - pub intensities: MappingCollection>, -} - -#[derive(Debug, Clone, Serialize)] -pub struct FinalizedMultiCMGStatsArrays { +pub struct MultiCMGArrayStats { pub ms1_stats: PartitionedCMGArrayStats, pub ms2_stats: PartitionedCMGArrayStats, pub id: u64, } -// This name is starting to get really long ... -#[derive(Debug, Clone, Serialize)] -pub struct _NaturalFinalizedMultiCMGStatsArrays { - pub retention_time_miliseconds: Vec, - pub average_mobility: Vec, - pub summed_intensity: Vec, - pub npeaks: Vec, - pub lazy_hyperscore: Vec, - pub lazy_hyperscore_vs_baseline: Vec, - pub norm_hyperscore_vs_baseline: Vec, - pub lazyerscore: Vec, - pub lazyerscore_vs_baseline: Vec, - pub norm_lazyerscore_vs_baseline: Vec, - pub transition_mobilities: MappingCollection>, - pub transition_mzs: MappingCollection>, - pub transition_intensities: MappingCollection>, - pub apex_primary_score_index: usize, -} - #[derive(Debug, Clone, Serialize)] pub struct NaturalFinalizedMultiCMGStatsArrays { - pub ms1_stats: _NaturalFinalizedMultiCMGStatsArrays, - pub ms2_stats: _NaturalFinalizedMultiCMGStatsArrays, + pub ms1_stats: PartitionedCMGScoredStatsArrays, + pub ms2_stats: PartitionedCMGScoredStatsArrays, pub id: u64, } -impl _NaturalFinalizedMultiCMGStatsArrays { - pub fn new( - other: PartitionedCMGArrayStats, - mz_converter: &Tof2MzConverter, - mobility_converter: &Scan2ImConverter, - ) -> Self { - let lazy_hyperscore = calculate_lazy_hyperscore(&other.npeaks, &other.summed_intensity); - let basline_window_len = 1 + (other.retention_time_miliseconds.len() / 10); - let lazy_hyperscore_vs_baseline = - calculate_value_vs_baseline(&lazy_hyperscore, basline_window_len); - let lazyerscore: Vec = other - .log_intensity_products - .iter() - .map(|x| lnfact_float(*x)) - .collect(); - let lazyerscore_vs_baseline = calculate_value_vs_baseline(&lazyerscore, basline_window_len); - - // Set 0 the NANs - // Q: Can I do this in-place? Will the comnpiler do it for me? - let lazy_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline - .into_iter() - .map(|x| if x.is_nan() { 0.0 } else { x }) - .collect(); - let lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline - .into_iter() - .map(|x| if x.is_nan() { 0.0 } else { x }) - .collect(); - - // Calculate the standard deviation of the lazyscores v baseline - let mut sd_calculator_hyperscore = RunningStatsCalculator::new(1, 0.0); - let mut sd_calculator_lazyscore = RunningStatsCalculator::new(1, 0.0); - - (0..lazyerscore_vs_baseline.len()).for_each(|i| { - sd_calculator_hyperscore.add(lazy_hyperscore_vs_baseline[i], 1); - sd_calculator_lazyscore.add(lazyerscore_vs_baseline[i], 1); - }); - let sd_hyperscore = sd_calculator_hyperscore.standard_deviation().unwrap(); - let sd_lazyscore = sd_calculator_lazyscore.standard_deviation().unwrap(); - - // Calculate the normalized scores - let norm_hyperscore_vs_baseline: Vec = lazy_hyperscore_vs_baseline - .iter() - .map(|x| x / sd_hyperscore) - .collect(); - let norm_lazyerscore_vs_baseline: Vec = lazyerscore_vs_baseline - .iter() - .map(|x| x / sd_lazyscore) - .collect(); - - let mut apex_primary_score_index = 0; - let mut max_primary_score = 0.0f64; - let primary_scores = &norm_lazyerscore_vs_baseline; - for (i, val) in primary_scores.iter().enumerate() { - if max_primary_score.is_nan() || *val > max_primary_score { - max_primary_score = *val; - apex_primary_score_index = i; - } - } - - assert!( - lazy_hyperscore_vs_baseline.len() == other.retention_time_miliseconds.len(), - "Failed sanity check" - ); - - _NaturalFinalizedMultiCMGStatsArrays { - retention_time_miliseconds: other.retention_time_miliseconds, - average_mobility: other - .weighted_scan_index_mean - .into_iter() - .map(|x| { - let out = mobility_converter.convert(x); - if !(0.5..=2.0).contains(&out) { - debug!("Bad mobility value: {:?}, input was {:?}", out, x); - } - out - }) - .collect(), - summed_intensity: other.summed_intensity, - npeaks: other.npeaks, - transition_mobilities: other - .scan_index_means - .into_iter() - .map(|(k, v)| { - let new_v = v - .into_iter() - .map(|x| mobility_converter.convert(x)) - .collect(); - (k, new_v) - }) - .collect(), - transition_mzs: other - .tof_index_means - .into_iter() - .map(|(k, v)| { - let new_v = v.into_iter().map(|x| mz_converter.convert(x)).collect(); - (k, new_v) - }) - .collect(), - transition_intensities: other.intensities, - lazy_hyperscore, - lazy_hyperscore_vs_baseline, - apex_primary_score_index, - lazyerscore, - lazyerscore_vs_baseline, - norm_hyperscore_vs_baseline, - norm_lazyerscore_vs_baseline, - } - } -} - -impl From> - for PartitionedCMGArrayStats -{ - fn from(other: PartitionedCMGArrays) -> Self { - // TODO ... maybe refactor this ... RN its king of ugly. - - let mut out = PartitionedCMGArrayStats { - retention_time_miliseconds: Vec::new(), - scan_index_means: MappingCollection::new(), - tof_index_means: MappingCollection::new(), - weighted_scan_index_mean: Vec::new(), - intensities: MappingCollection::new(), - summed_intensity: Vec::new(), - log_intensity_products: Vec::new(), - npeaks: Vec::new(), - }; - - let unique_rts = other - .transition_stats - .iter() - .flat_map(|(_k, v)| v.retention_time_miliseconds.clone()) - .collect::>(); - let mut unique_rts = unique_rts.into_iter().collect::>(); - unique_rts.sort(); - - // Q: Is this the most efficient way to do this? - // I think having the btrees as the unit of integration might not be the best idea. - // ... If I want to preserve the sparsity, I can use a hashmap and the sort it. - let mut summed_intensity_tree: BTreeMap = - BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); - let mut intensity_logsums_tree: BTreeMap = - BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0.0))); - let mut npeaks_tree: BTreeMap = - BTreeMap::from_iter(unique_rts.iter().map(|x| (*x, 0))); - let mut weighted_tof_index_mean_tree: BTreeMap = - BTreeMap::new(); - let mut weighted_scan_index_mean_tree: BTreeMap = - BTreeMap::new(); - - for (k, v) in other.transition_stats.into_iter() { - type ScanTofIntensityTuple = (f64, f64, u64); - type ScanTofIntensityVecs = (Vec, (Vec, Vec)); - let mut tmp_tree: BTreeMap = BTreeMap::new(); - - for i in 0..v.retention_time_miliseconds.len() { - let rt = v.retention_time_miliseconds[i]; - let scan = v.scan_index_means[i]; - let tof = v.tof_index_means[i]; - let inten = v.intensities[i]; - - if inten > 100 { - npeaks_tree.entry(rt).and_modify(|curr| *curr += 1); - } - tmp_tree.entry(rt).or_insert((scan, tof, inten)); - summed_intensity_tree - .entry(rt) - .and_modify(|curr| *curr += inten); - - intensity_logsums_tree.entry(rt).and_modify(|curr| { - if inten > 10 { - *curr += (inten as f64).ln() - } - }); - weighted_tof_index_mean_tree - .entry(rt) - .and_modify(|curr| curr.add(tof, inten)) - .or_insert(RunningStatsCalculator::new(inten, tof)); - weighted_scan_index_mean_tree - .entry(rt) - .and_modify(|curr| curr.add(scan, inten)) - .or_insert(RunningStatsCalculator::new(inten, scan)); - } - - // Now we fill with nans the missing values. - // Q: Do I really need to do this here? - for rt in unique_rts.iter() { - tmp_tree.entry(*rt).or_insert((f64::NAN, f64::NAN, 0)); - } - - let (out_scans, (out_tofs, out_intens)): ScanTofIntensityVecs = tmp_tree - .into_iter() - .map(|(_, (scan, tof, inten))| (scan, (tof, inten))) - .unzip(); - out.scan_index_means.insert(k.clone(), out_scans); - out.tof_index_means.insert(k.clone(), out_tofs); - out.intensities.insert(k.clone(), out_intens); - } - - out.retention_time_miliseconds = unique_rts; - out.summed_intensity = summed_intensity_tree.into_values().collect(); - out.npeaks = npeaks_tree.into_values().collect(); - out.weighted_scan_index_mean = weighted_scan_index_mean_tree - .into_values() - .map(|x| { - let out = x.mean().expect("At least one value should be present"); - if !(0.0..=1000.0).contains(&out) { - warn!("Bad mobility value: {:?}, input was {:?}", out, x); - } - - out - }) - .collect(); - - // Note: The log of products is the same as the sum of logs. - out.log_intensity_products = intensity_logsums_tree.into_values().collect(); - out - } -} - impl Aggregator for MultiCMGStatsAgg { @@ -413,12 +129,12 @@ impl Aggregat let mz_converter = &self.converters.0; let mobility_converter = &self.converters.1; - let ms1_stats = _NaturalFinalizedMultiCMGStatsArrays::new( + let ms1_stats = PartitionedCMGScoredStatsArrays::new( PartitionedCMGArrayStats::from(self.ms1_stats.finalize()), mz_converter, mobility_converter, ); - let ms2_stats = _NaturalFinalizedMultiCMGStatsArrays::new( + let ms2_stats = PartitionedCMGScoredStatsArrays::new( PartitionedCMGArrayStats::from(self.ms2_stats.finalize()), mz_converter, mobility_converter, diff --git a/src/models/aggregators/raw_peak_agg/point_agg.rs b/src/models/aggregators/raw_peak_agg/point_agg.rs index 931f657..7187a3b 100644 --- a/src/models/aggregators/raw_peak_agg/point_agg.rs +++ b/src/models/aggregators/raw_peak_agg/point_agg.rs @@ -1,10 +1,7 @@ -use crate::{ - models::frames::raw_peak::RawPeak, - traits::aggregator::{ - Aggregator, - NoContext, - ProvidesContext, - }, +use crate::models::frames::raw_peak::RawPeak; +use crate::traits::aggregator::{ + Aggregator, + NoContext, }; use serde::Serialize; diff --git a/src/models/aggregators/rolling_calculators.rs b/src/models/aggregators/rolling_calculators.rs index 60a5f31..b010554 100644 --- a/src/models/aggregators/rolling_calculators.rs +++ b/src/models/aggregators/rolling_calculators.rs @@ -1,7 +1,4 @@ -use crate::utils::math::{ - lnfact, - lnfact_float, -}; +use crate::utils::math::lnfact; // Rolling median calculator pub struct RollingMedianCalculator { diff --git a/src/models/elution_group.rs b/src/models/elution_group.rs index 1448d97..9ed5221 100644 --- a/src/models/elution_group.rs +++ b/src/models/elution_group.rs @@ -2,10 +2,8 @@ use serde::{ Deserialize, Serialize, }; -use std::{ - collections::HashMap, - hash::Hash, -}; +use std::collections::HashMap; +use std::hash::Hash; /// A struct that represents an elution group. /// diff --git a/src/models/frames/expanded_frame.rs b/src/models/frames/expanded_frame.rs index 05bae0b..dbe1aad 100644 --- a/src/models/frames/expanded_frame.rs +++ b/src/models/frames/expanded_frame.rs @@ -1,46 +1,38 @@ -use super::{ - peak_in_quad::PeakInQuad, - single_quad_settings::{ - expand_quad_settings, - ExpandedFrameQuadSettings, - SingleQuadrupoleSetting, - }, +use super::peak_in_quad::PeakInQuad; +use super::single_quad_settings::{ + expand_quad_settings, + ExpandedFrameQuadSettings, + SingleQuadrupoleSetting, }; -use crate::{ - errors::{ - Result, - UnsupportedDataError, - }, - sort_vecs_by_first, - utils::{ - compress_explode::explode_vec, - frame_processing::{ - lazy_centroid_weighted_frame, - PeakArrayRefs, - }, - sorting::top_n, - tolerance_ranges::{ - scan_tol_range, - tof_tol_range, - IncludedRange, - }, - }, +use crate::errors::{ + Result, + UnsupportedDataError, +}; +use crate::sort_vecs_by_first; +use crate::utils::compress_explode::explode_vec; +use crate::utils::frame_processing::{ + lazy_centroid_weighted_frame, + PeakArrayRefs, +}; +use crate::utils::sorting::top_n; +use crate::utils::tolerance_ranges::{ + scan_tol_range, + tof_tol_range, + IncludedRange, }; use rayon::prelude::*; -use std::{ - collections::HashMap, - marker::PhantomData, - sync::Arc, +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; +use timsrust::converters::{ + Scan2ImConverter, + Tof2MzConverter, +}; +use timsrust::readers::{ + FrameReader, + FrameReaderError, }; use timsrust::{ - converters::{ - Scan2ImConverter, - Tof2MzConverter, - }, - readers::{ - FrameReader, - FrameReaderError, - }, AcquisitionType, Frame, MSLevel, @@ -540,7 +532,6 @@ impl ExpandedQuadSliceInfo { max_rt: f64, min_rt: f64, intensity: u32, - local_frame_index: usize, fwdone: bool, bwdone: bool, any_update: bool, @@ -584,7 +575,6 @@ impl ExpandedQuadSliceInfo { max_rt: local_rt, min_rt: local_rt, intensity: local_inten, - local_frame_index: local_idx, fwdone: false, bwdone: false, any_update: false, diff --git a/src/models/frames/expanded_window_group.rs b/src/models/frames/expanded_window_group.rs index 9cff012..d38b5a0 100644 --- a/src/models/frames/expanded_window_group.rs +++ b/src/models/frames/expanded_window_group.rs @@ -1,12 +1,10 @@ use std::iter::repeat; -use super::{ - expanded_frame::{ - ExpandedFrameSlice, - SortingStateTrait, - }, - single_quad_settings::SingleQuadrupoleSetting, +use super::expanded_frame::{ + ExpandedFrameSlice, + SortingStateTrait, }; +use super::single_quad_settings::SingleQuadrupoleSetting; use timsrust::{ AcquisitionType, MSLevel, diff --git a/src/models/frames/single_quad_settings.rs b/src/models/frames/single_quad_settings.rs index a9071ff..7d4e819 100644 --- a/src/models/frames/single_quad_settings.rs +++ b/src/models/frames/single_quad_settings.rs @@ -1,7 +1,5 @@ -use std::{ - fmt::Display, - hash::Hash, -}; +use std::fmt::Display; +use std::hash::Hash; use timsrust::QuadrupoleSettings; diff --git a/src/models/indices/expanded_raw_index/model.rs b/src/models/indices/expanded_raw_index/model.rs index 513ff9e..c8be15d 100644 --- a/src/models/indices/expanded_raw_index/model.rs +++ b/src/models/indices/expanded_raw_index/model.rs @@ -1,52 +1,40 @@ -use crate::{ - errors::Result, - models::{ - adapters::FragmentIndexAdapter, - elution_group::ElutionGroup, - frames::{ - expanded_frame::{ - par_read_and_expand_frames, - ExpandedFrameSlice, - FrameProcessingConfig, - SortedState, - }, - peak_in_quad::PeakInQuad, - raw_peak::RawPeak, - single_quad_settings::{ - get_matching_quad_settings, - matches_quad_settings, - SingleQuadrupoleSetting, - SingleQuadrupoleSettingIndex, - }, - }, - queries::{ - FragmentGroupIndexQuery, - MsLevelContext, - }, - }, - traits::{ - aggregator::Aggregator, - queriable_data::QueriableData, - }, - utils::tolerance_ranges::IncludedRange, - ToleranceAdapter, +use crate::errors::Result; +use crate::models::adapters::FragmentIndexAdapter; +use crate::models::elution_group::ElutionGroup; +use crate::models::frames::expanded_frame::{ + par_read_and_expand_frames, + ExpandedFrameSlice, + FrameProcessingConfig, + SortedState, }; +use crate::models::frames::peak_in_quad::PeakInQuad; +use crate::models::frames::raw_peak::RawPeak; +use crate::models::frames::single_quad_settings::{ + get_matching_quad_settings, + matches_quad_settings, + SingleQuadrupoleSetting, + SingleQuadrupoleSettingIndex, +}; +use crate::models::queries::{ + FragmentGroupIndexQuery, + MsLevelContext, +}; +use crate::traits::aggregator::Aggregator; +use crate::traits::queriable_data::QueriableData; +use crate::utils::tolerance_ranges::IncludedRange; +use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; -use std::{ - collections::HashMap, - hash::Hash, - time::Instant, +use std::collections::HashMap; +use std::hash::Hash; +use std::time::Instant; +use timsrust::converters::{ + Scan2ImConverter, + Tof2MzConverter, }; -use timsrust::{ - converters::{ - Scan2ImConverter, - Tof2MzConverter, - }, - readers::{ - FrameReader, - MetadataReader, - }, +use timsrust::readers::{ + FrameReader, + MetadataReader, }; use tracing::{ info, @@ -307,7 +295,7 @@ impl .collect::>(); // Query the ms1 mzs first. - aggregator.iter_mut().enumerate().for_each(|(i, agg)| { + aggregator.par_iter_mut().enumerate().for_each(|(i, agg)| { fragment_queries[i] .iter_ms1_mzs() .for_each(|(fh, mz_range)| { diff --git a/src/models/indices/raw_file_index.rs b/src/models/indices/raw_file_index.rs index b1076cf..fbb8c1c 100644 --- a/src/models/indices/raw_file_index.rs +++ b/src/models/indices/raw_file_index.rs @@ -1,38 +1,27 @@ +use crate::models::adapters::FragmentIndexAdapter; +use crate::models::frames::raw_frames::frame_elems_matching; +use crate::models::frames::raw_peak::RawPeak; +use crate::models::queries::{ + FragmentGroupIndexQuery, + MsLevelContext, +}; +use crate::traits::aggregator::Aggregator; +use crate::traits::queriable_data::QueriableData; use crate::{ - models::{ - adapters::FragmentIndexAdapter, - frames::{ - raw_frames::frame_elems_matching, - raw_peak::RawPeak, - }, - queries::{ - FragmentGroupIndexQuery, - MsLevelContext, - }, - }, - traits::{ - aggregator::Aggregator, - queriable_data::QueriableData, - }, - utils::tolerance_ranges::IncludedRange, ElutionGroup, ToleranceAdapter, }; use rayon::iter::ParallelIterator; use serde::Serialize; -use std::{ - fmt::Debug, - hash::Hash, +use std::fmt::Debug; +use std::hash::Hash; +use timsrust::readers::{ + FrameReader, + FrameReaderError, + MetadataReader, }; use timsrust::{ - converters::ConvertableDomain, - readers::{ - FrameReader, - FrameReaderError, - MetadataReader, - }, Frame, - Metadata, TimsRustError, }; use tracing::trace; diff --git a/src/models/indices/transposed_quad_index/peak_bucket.rs b/src/models/indices/transposed_quad_index/peak_bucket.rs index a0def3d..7265ae8 100644 --- a/src/models/indices/transposed_quad_index/peak_bucket.rs +++ b/src/models/indices/transposed_quad_index/peak_bucket.rs @@ -1,14 +1,10 @@ -use crate::{ - sort_vecs_by_first, - utils::{ - compress_explode::compress_vec, - display::{ - glimpse_vec, - GlimpseConfig, - }, - tolerance_ranges::IncludedRange, - }, +use crate::sort_vecs_by_first; +use crate::utils::compress_explode::compress_vec; +use crate::utils::display::{ + glimpse_vec, + GlimpseConfig, }; +use crate::utils::tolerance_ranges::IncludedRange; use std::fmt::Display; pub struct PeakInBucket { diff --git a/src/models/indices/transposed_quad_index/quad_index.rs b/src/models/indices/transposed_quad_index/quad_index.rs index 08ee99a..eda101b 100644 --- a/src/models/indices/transposed_quad_index/quad_index.rs +++ b/src/models/indices/transposed_quad_index/quad_index.rs @@ -3,32 +3,24 @@ use super::peak_bucket::{ PeakBucketBuilder, PeakInBucket, }; -use crate::{ - models::frames::{ - expanded_frame::{ - ExpandedFrameSlice, - SortingStateTrait, - }, - peak_in_quad::PeakInQuad, - single_quad_settings::SingleQuadrupoleSetting, - }, - sort_vecs_by_first, - utils::{ - display::{ - glimpse_vec, - GlimpseConfig, - }, - tolerance_ranges::IncludedRange, - }, +use crate::models::frames::expanded_frame::{ + ExpandedFrameSlice, + SortingStateTrait, }; -use std::{ - collections::{ - BTreeMap, - HashMap, - }, - fmt::Display, - time::Instant, +use crate::models::frames::peak_in_quad::PeakInQuad; +use crate::models::frames::single_quad_settings::SingleQuadrupoleSetting; +use crate::sort_vecs_by_first; +use crate::utils::display::{ + glimpse_vec, + GlimpseConfig, }; +use crate::utils::tolerance_ranges::IncludedRange; +use std::collections::{ + BTreeMap, + HashMap, +}; +use std::fmt::Display; +use std::time::Instant; use timsrust::converters::{ ConvertableDomain, Frame2RtConverter, diff --git a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs index bdc1b58..12f6cb9 100644 --- a/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs +++ b/src/models/indices/transposed_quad_index/quad_splitted_transposed_index.rs @@ -2,71 +2,56 @@ use super::quad_index::{ TransposedQuadIndex, TransposedQuadIndexBuilder, }; -use crate::{ - errors::Result, - models::{ - adapters::FragmentIndexAdapter, - elution_group::ElutionGroup, - frames::{ - expanded_frame::{ - par_read_and_expand_frames, - ExpandedFrameSlice, - FrameProcessingConfig, - SortingStateTrait, - }, - peak_in_quad::PeakInQuad, - raw_peak::RawPeak, - single_quad_settings::{ - get_matching_quad_settings, - SingleQuadrupoleSetting, - SingleQuadrupoleSettingIndex, - }, - }, - queries::{ - FragmentGroupIndexQuery, - MsLevelContext, - }, - }, - traits::{ - aggregator::Aggregator, - queriable_data::QueriableData, - }, - utils::{ - display::{ - glimpse_vec, - GlimpseConfig, - }, - tolerance_ranges::IncludedRange, - }, - ToleranceAdapter, +use crate::errors::Result; +use crate::models::adapters::FragmentIndexAdapter; +use crate::models::elution_group::ElutionGroup; +use crate::models::frames::expanded_frame::{ + par_read_and_expand_frames, + ExpandedFrameSlice, + FrameProcessingConfig, + SortingStateTrait, }; +use crate::models::frames::peak_in_quad::PeakInQuad; +use crate::models::frames::raw_peak::RawPeak; +use crate::models::frames::single_quad_settings::{ + get_matching_quad_settings, + SingleQuadrupoleSetting, + SingleQuadrupoleSettingIndex, +}; +use crate::models::queries::{ + FragmentGroupIndexQuery, + MsLevelContext, +}; +use crate::traits::aggregator::Aggregator; +use crate::traits::queriable_data::QueriableData; +use crate::utils::display::{ + glimpse_vec, + GlimpseConfig, +}; +use crate::utils::tolerance_ranges::IncludedRange; +use crate::ToleranceAdapter; use rayon::prelude::*; use serde::Serialize; -use std::{ - collections::HashMap, - fmt::{ - Debug, - Display, - }, - hash::Hash, - time::Instant, +use std::collections::HashMap; +use std::fmt::{ + Debug, + Display, +}; +use std::hash::Hash; +use std::time::Instant; +use timsrust::converters::{ + Scan2ImConverter, + Tof2MzConverter, }; -use timsrust::{ - converters::{ - Scan2ImConverter, - Tof2MzConverter, - }, - readers::{ - FrameReader, - MetadataReader, - }, - Metadata, +use timsrust::readers::{ + FrameReader, + MetadataReader, }; +use timsrust::Metadata; use tracing::{ debug, info, instrument, - trace, }; // TODO break this module apart ... its getting too big for my taste @@ -287,7 +272,7 @@ impl QuadSplittedTransposedIndexBuilder { // TODO use the rayon contructor to fold let out2: Result> = split_frames .into_par_iter() - .map(|(q, frameslices)| { + .map(|(_q, frameslices)| { // TODO:Refactor so the internal index is built first and then added. // This should save a couple of thousand un-necessary hashmap lookups. let mut out = Self::new(); diff --git a/src/models/queries.rs b/src/models/queries.rs index 5daf53e..dbd0cde 100644 --- a/src/models/queries.rs +++ b/src/models/queries.rs @@ -1,14 +1,10 @@ -use crate::{ - traits::aggregator::{ - NoContext, - ProvidesContext, - }, - utils::tolerance_ranges::IncludedRange, -}; -use std::{ - collections::HashMap, - hash::Hash, +use crate::traits::aggregator::{ + NoContext, + ProvidesContext, }; +use crate::utils::tolerance_ranges::IncludedRange; +use std::collections::HashMap; +use std::hash::Hash; use timsrust::converters::{ ConvertableDomain, Scan2ImConverter, @@ -118,7 +114,7 @@ impl NaturalPrecursorQuery { im_converter.invert(self.mobility_range.end()).round() as usize, ) .into(), - isolation_mz_range: self.isolation_mz_range.clone(), + isolation_mz_range: self.isolation_mz_range, } } } diff --git a/src/queriable_tims_data/queriable_tims_data.rs b/src/queriable_tims_data/queriable_tims_data.rs index 8f0f985..f7cdb54 100644 --- a/src/queriable_tims_data/queriable_tims_data.rs +++ b/src/queriable_tims_data/queriable_tims_data.rs @@ -1,5 +1,5 @@ +use crate::traits::aggregator::ProvidesContext; use crate::{ - traits::aggregator::ProvidesContext, Aggregator, ElutionGroup, QueriableData, @@ -8,10 +8,8 @@ use crate::{ }; use rayon::prelude::*; use serde::Serialize; -use std::{ - hash::Hash, - time::Instant, -}; +use std::hash::Hash; +use std::time::Instant; use tracing::{ info, instrument, diff --git a/src/utils/math.rs b/src/utils/math.rs index 7f3e5e5..ac47d1e 100644 --- a/src/utils/math.rs +++ b/src/utils/math.rs @@ -51,17 +51,6 @@ fn cosine_similarity(a: &[f64], b: &[f64]) -> Option { Some(dot_product / (magnitude_a * magnitude_b)) } -// Example usage: -fn main() { - let vector_a = vec![1.0, 2.0, 3.0]; - let vector_b = vec![4.0, 5.0, 6.0]; - - match cosine_similarity(&vector_a, &vector_b) { - Some(similarity) => println!("Cosine similarity: {}", similarity), - None => println!("Could not calculate cosine similarity"), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/utils/tolerance_ranges.rs b/src/utils/tolerance_ranges.rs index 5099546..fcd9ff2 100644 --- a/src/utils/tolerance_ranges.rs +++ b/src/utils/tolerance_ranges.rs @@ -41,9 +41,9 @@ impl From<(T, T)> for IncludedRange { } } -impl Into<(T, T)> for IncludedRange { - fn into(self) -> (T, T) { - (self.0, self.1) +impl From> for (T, T) { + fn from(val: IncludedRange) -> Self { + (val.0, val.1) } } From 10f0b769f5ef34040d14f16265dc8f7aa8783166 Mon Sep 17 00:00:00 2001 From: "J. Sebastian Paez" Date: Sun, 3 Nov 2024 20:05:32 -0800 Subject: [PATCH 22/30] chore: clippy and updated benches --- Taskfile.yml | 28 +++++- ...C_13_S1-B1_1_12817_BatchAccess_full_rt.png | Bin 61560 -> 60633 bytes ...13_S1-B1_1_12817_BatchAccess_narrow_rt.png | Bin 68296 -> 67050 bytes ..._A_Sample_Alpha_02_BatchAccess_full_rt.png | Bin 0 -> 44940 bytes ..._Sample_Alpha_02_BatchAccess_narrow_rt.png | Bin 0 -> 54934 bytes data/sageresults/ubb_peptide_plot.png | Bin 140564 -> 140251 bytes ..._results_230510_PRTC_13_S1-B1_1_12817.json | 76 +++++++-------- .../multi_chromatogram_agg/mod.rs | 1 + .../indices/expanded_raw_index/model.rs | 89 ++++++++---------- .../quad_splitted_transposed_index.rs | 78 +++++++++------ src/queriable_tims_data/mod.rs | 1 + 11 files changed, 155 insertions(+), 118 deletions(-) create mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png create mode 100644 data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_narrow_rt.png diff --git a/Taskfile.yml b/Taskfile.yml index 6335cfa..c0c5216 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -40,14 +40,38 @@ tasks: - cargo clippy {{.CLI_ARGS}} bench: + cmds: + - task: bench-build + - task: bench-small-data + - task: bench-large-data + + bench-build: + deps: [build] sources: - "src/**/*.rs" cmds: - cargo b --release --features bench --bin benchmark_indices + + bench-small-data: + deps: [bench-build] + sources: + - "src/**/*.rs" + - "data/230510_PRTC_13_S1-B1_1_12817.d" + - "benches/plot_bench.py" + + cmds: - SKIP_SLOW=1 SKIP_BUILD=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/230510_PRTC_13_S1-B1_1_12817.d ./target/release/benchmark_indices - uv run benches/plot_bench.py data/benchmark_results_230510_PRTC_13_S1-B1_1_12817.json - - # SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices - - # uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json + + bench-large-data: + deps: [bench-build] + sources: + - "src/**/*.rs" + - "data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d" + - "benches/plot_bench.py" + cmds: + - SKIP_SLOW=1 SKIP_BUILD=1 SKIP_HIGHMEM=1 RUST_BACKTRACE=full TIMS_DATA_FILE=./data/LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.d ./target/release/benchmark_indices + - uv run benches/plot_bench.py data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02.json plot: sources: diff --git a/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_full_rt.png b/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_full_rt.png index 0fa47a1b5f49eb83eb8d8c7731dfcb350c76de94..0c6a7e3801390098d0488f00a8a38635e08d6ea3 100644 GIT binary patch literal 60633 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 E2N89h+W-In literal 61560 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 TTWs>bakt*_(~th+fv5i;z-Ec& diff --git a/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_narrow_rt.png b/data/benchmark_results_230510_PRTC_13_S1-B1_1_12817_BatchAccess_narrow_rt.png index b5daf59f812822357d4043535deff708831d501d..b0140dd8b3c17698a4925ab85f2f907edfd972b7 100644 GIT binary patch literal 67050 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


QRf1bXmDR&j` zBGSt=)f{V5^-l;8x<352;%}gmfTrr8wR0rQS+!}ReAS}uZj`(If|X!u3xWB&mIgXE z6EJld(1)$F@fVd*tnu(V@Lu+PQ|$$ z(*vj1izYP0h*wRQvLHCT9A>|GGoULEoak)>iW+9tvqxOaV7B7T# zIUJ)#F10j-xS%UAWM&qii4|3HC%UjC;R<6UzsMG&AAuZswk#Bypt)bisvb z;*)zueW+>T`gmtaiv1gAN4Myrmtrez@7vOOqpfN&VuRDeW-s*(w8wXruPx^zJ5H?W zaJL|SAq(%zJU)HLB6a5ISNm7);8qx^*pQ|B_pkASxHDUmO&7ekCjR(Nyk7%%6OhMS zj9V}+$<-}C(V~ltz*}zNQtJ|uvjp_Hv*1Dm;_LFYx{(WVMG~VLg>@l3uw5P$FRNz_ z5YrSAG148Y?*8#Cuz-|UH{l#ODl@y6<>(e_m|qw z$6B=p*9!f|SlqhjR=qFAH}Xqp%c+oWwJVDB*Byz&rP!Q~7QF8wy4LB&bNrM z(l_xg%wD}Aqi{kNXF)NN9UlbUZ21qhPUwmn`hC4Hskr)7`54|U!Q|oFp@1ASII?&9 z<)7i&IxXJCN@->YWoF(0+0SaYt*9p%A+6x&MoMMt+HlQxK=rv%-iJ zORhZw8J9;3NT}MpiL$x+=uNA)F?H#av=QL|n-8~{D&bUH zn?!q#Bl-m>I;b#mnDVZejM3-ES1>#tie{}fvNKLaAUH&7VxC;UroCrx1Qwr$ z;1riyl%mV{^m;NI#ouE=dX{}|^rMx;2w_QYltXHP4I+2{1BwLR^v})X3FPWs(K@l_ a!apAU_LP^`UDC4u)?Tyj>hY^?eCQug6ZcL4 literal 68296 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 B*?a&1 diff --git a/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png b/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_full_rt.png new file mode 100644 index 0000000000000000000000000000000000000000..78135c49cc91a2cd7f49467d77153e24c461bcde GIT binary patch literal 44940 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 K`Zw=;^8W!&;D+h| literal 0 HcmV?d00001 diff --git a/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_narrow_rt.png b/data/benchmark_results_LFQ_timsTOFPro_diaPASEF_Condition_A_Sample_Alpha_02_BatchAccess_narrow_rt.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c56e4c1baa86ee9c281ae8067f5e5f09b0af98 GIT binary patch literal 54934 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