1use crate::sim::containers::{
2 FragmentIonSim, FrameToWindowGroupSim, FramesSim, IonScalar, IonSim, MobilityEnv,
3 PeptideScalar, PeptidesSim, ScansSim, SignalDistribution, WindowGroupSettingsSim,
4};
5use mscore::data::peptide::{FragmentType, PeptideProductIonSeriesCollection, PeptideSequence};
6use mscore::data::spectrum::{MsType, MzSpectrum};
7use mscore::simulation::annotation::MzSpectrumAnnotated;
8use mscore::timstof::collision::{TimsTofCollisionEnergy, TimsTofCollisionEnergyDIA};
9use mscore::timstof::quadrupole::{IonTransmission, PASEFMeta, TimsTransmissionDDA, TimsTransmissionDIA};
10use rayon::prelude::*;
11use rayon::ThreadPoolBuilder;
12use rusqlite::Connection;
13use std::collections::{BTreeMap, BTreeSet, HashSet};
14use std::path::Path;
15
16pub fn resolve_fragment_ce_key<V>(
34 map: &BTreeMap<(u32, i8, i32), V>,
35 peptide_id: u32,
36 charge: i8,
37 applied_ce_ev: f64,
38) -> Option<i32> {
39 let base = (applied_ce_ev * 1e1).round() as i32;
40 [base, base - 1, base + 1]
41 .into_iter()
42 .find(|&k| map.contains_key(&(peptide_id, charge, k)))
43}
44
45pub fn fragment_prefix_exists<V>(
51 map: &std::collections::BTreeMap<(u32, i8, i32), V>,
52 peptide_id: u32,
53 charge: i8,
54) -> bool {
55 map.range((peptide_id, charge, i32::MIN)..=(peptide_id, charge, i32::MAX))
56 .next()
57 .is_some()
58}
59
60fn mean_consecutive_diff(xs: &[f64]) -> f64 {
63 if xs.len() < 2 {
64 return 0.0;
65 }
66 let total: f64 = xs.windows(2).map(|w| w[1] - w[0]).sum();
67 total / (xs.len() - 1) as f64
68}
69
70#[derive(Debug, Clone)]
75pub struct PredictionSet {
76 pub prediction_set_id: i64,
77 pub predictor_model: Option<String>,
78 pub instrument: String,
79 pub acquisition_type: String,
80 pub activation_method: String,
81 pub energy_unit: String,
82 pub collision_energy_encoding: String,
86}
87
88impl PredictionSet {
89 pub fn legacy_bruker() -> Self {
92 PredictionSet {
93 prediction_set_id: 0,
94 predictor_model: None,
95 instrument: "bruker_timstof".to_string(),
96 acquisition_type: "unknown".to_string(),
97 activation_method: "hcd".to_string(),
98 energy_unit: "ev".to_string(),
99 collision_energy_encoding: "normalized_div100".to_string(),
100 }
101 }
102
103 pub fn assert_render_compatible(&self) -> Result<(), String> {
107 if self.collision_energy_encoding != "normalized_div100" {
108 return Err(format!(
109 "prediction set {} has collision_energy_encoding='{}' but the \
110 renderer expects 'normalized_div100'; the stored fragments were \
111 built for a different instrument/encoding (model={:?}, instrument={})",
112 self.prediction_set_id,
113 self.collision_energy_encoding,
114 self.predictor_model,
115 self.instrument,
116 ));
117 }
118 Ok(())
119 }
120}
121
122#[derive(Debug)]
123pub struct TimsTofSyntheticsDataHandle {
124 pub connection: Connection,
125}
126
127impl TimsTofSyntheticsDataHandle {
128 pub fn new(path: &Path) -> rusqlite::Result<Self> {
129 let connection = Connection::open(path)?;
130 Ok(Self { connection })
131 }
132
133 pub fn read_frames(&self) -> rusqlite::Result<Vec<FramesSim>> {
134 let mut stmt = self.connection.prepare(
135 "SELECT frame_id, time, ms_type FROM frames"
136 )?;
137 let frames_iter = stmt.query_map([], |row| {
138 Ok(FramesSim::new(
139 row.get("frame_id")?,
140 row.get("time")?,
141 row.get("ms_type")?,
142 ))
143 })?;
144 let mut frames = Vec::new();
145 for frame in frames_iter {
146 frames.push(frame?);
147 }
148 Ok(frames)
149 }
150
151 pub fn read_scans(&self) -> rusqlite::Result<Vec<ScansSim>> {
152 let mut stmt = self.connection.prepare(
153 "SELECT scan, mobility FROM scans ORDER BY scan"
154 )?;
155 let scans_iter = stmt.query_map([], |row| {
156 Ok(ScansSim::new(
157 row.get("scan")?,
158 row.get("mobility")?,
159 ))
160 })?;
161 let mut scans = Vec::new();
162 for scan in scans_iter {
163 scans.push(scan?);
164 }
165 Ok(scans)
166 }
167
168 pub fn read_peptides(&self) -> rusqlite::Result<Vec<PeptidesSim>> {
169 let mut stmt = self.connection.prepare("SELECT * FROM peptides ORDER BY peptide_id")?;
170 let peptides_iter = stmt.query_map([], |row| {
171 Self::peptide_from_row(row)
172 })?;
173 let mut peptides = Vec::new();
174 for peptide in peptides_iter {
175 peptides.push(peptide?);
176 }
177 Ok(peptides)
178 }
179
180 fn peptide_from_row(row: &rusqlite::Row) -> rusqlite::Result<PeptidesSim> {
182 let frame_occurrence_str: String = row.get("frame_occurrence")?;
183 let frame_abundance_str: String = row.get("frame_abundance")?;
184
185 let frame_occurrence: Vec<u32> = serde_json::from_str(&frame_occurrence_str)
186 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
187 0, rusqlite::types::Type::Text, Box::new(e),
188 ))?;
189
190 let frame_abundance: Vec<f32> = match serde_json::from_str(&frame_abundance_str) {
191 Ok(value) => value,
192 Err(_) => vec![0.0; frame_occurrence.len()],
193 };
194
195 let frame_distribution =
196 SignalDistribution::new(0.0, 0.0, 0.0, frame_occurrence, frame_abundance);
197
198 let peptide_id: u32 = row.get("peptide_id")?;
199
200 Ok(PeptidesSim {
201 protein_id: row.get("protein_id")?,
202 peptide_id,
203 sequence: PeptideSequence::new(row.get("sequence")?, Some(peptide_id as i32)),
204 proteins: row.get("protein")?,
205 decoy: row.get("decoy")?,
206 missed_cleavages: row.get("missed_cleavages")?,
207 n_term: row.get("n_term")?,
208 c_term: row.get("c_term")?,
209 mono_isotopic_mass: row.get("monoisotopic-mass")?,
210 retention_time: row.get("retention_time_gru_predictor")?,
211 events: row.get("events")?,
212 frame_start: row.get("frame_occurrence_start")?,
213 frame_end: row.get("frame_occurrence_end")?,
214 frame_distribution,
215 })
216 }
217
218 fn ion_from_row(row: &rusqlite::Row) -> rusqlite::Result<IonSim> {
220 let simulated_spectrum_str: String = row.get("simulated_spectrum")?;
221 let scan_occurrence_str: String = row.get("scan_occurrence")?;
222 let scan_abundance_str: String = row.get("scan_abundance")?;
223
224 let simulated_spectrum: MzSpectrum = serde_json::from_str(&simulated_spectrum_str)
225 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
226 0, rusqlite::types::Type::Text, Box::new(e),
227 ))?;
228
229 let scan_occurrence: Vec<u32> = serde_json::from_str(&scan_occurrence_str)
230 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
231 0, rusqlite::types::Type::Text, Box::new(e),
232 ))?;
233
234 let scan_abundance: Vec<f32> = serde_json::from_str(&scan_abundance_str)
235 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
236 0, rusqlite::types::Type::Text, Box::new(e),
237 ))?;
238
239 Ok(IonSim::new(
240 row.get("ion_id")?,
241 row.get("peptide_id")?,
242 row.get("sequence")?,
243 row.get("charge")?,
244 row.get("relative_abundance")?,
245 row.get("inv_mobility_gru_predictor")?,
246 simulated_spectrum,
247 scan_occurrence,
248 scan_abundance,
249 ))
250 }
251
252 fn fragment_ion_from_row(row: &rusqlite::Row) -> rusqlite::Result<FragmentIonSim> {
254 let indices_string: String = row.get("indices")?;
255 let values_string: String = row.get("values")?;
256
257 let indices: Vec<u32> = serde_json::from_str(&indices_string)
258 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
259 0, rusqlite::types::Type::Text, Box::new(e),
260 ))?;
261
262 let values: Vec<f64> = serde_json::from_str(&values_string)
263 .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
264 0, rusqlite::types::Type::Text, Box::new(e),
265 ))?;
266
267 Ok(FragmentIonSim::new(
268 row.get("peptide_id")?,
269 row.get("ion_id")?,
270 row.get("collision_energy")?,
271 row.get("charge")?,
272 indices,
273 values,
274 ))
275 }
276
277 pub fn read_ions(&self) -> rusqlite::Result<Vec<IonSim>> {
278 let mut stmt = self.connection.prepare("SELECT * FROM ions ORDER BY ion_id")?;
279 let ions_iter = stmt.query_map([], |row| Self::ion_from_row(row))?;
280 let mut ions = Vec::new();
281 for ion in ions_iter {
282 ions.push(ion?);
283 }
284 Ok(ions)
285 }
286
287 fn assert_ident(name: &str) -> rusqlite::Result<()> {
300 let ok = !name.is_empty()
301 && name.chars().next().map_or(false, |c| c.is_ascii_alphabetic() || c == '_')
302 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
303 if ok {
304 Ok(())
305 } else {
306 Err(rusqlite::Error::InvalidParameterName(format!(
307 "unsafe SQL identifier: {name:?}"
308 )))
309 }
310 }
311
312 fn table_has_column(&self, table: &str, column: &str) -> rusqlite::Result<bool> {
315 Self::assert_ident(table)?;
316 let mut stmt = self
317 .connection
318 .prepare(&format!("PRAGMA table_info({})", table))?;
319 let mut found = false;
320 let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
321 for name in rows {
322 if name? == column {
323 found = true;
324 }
325 }
326 Ok(found)
327 }
328
329 pub fn read_mobility_env(&self) -> rusqlite::Result<MobilityEnv> {
334 let complete = self.table_has_column("experiment_conditions", "drift_gas_mass")?
335 && self.table_has_column("experiment_conditions", "temperature_c")?
336 && self.table_has_column("experiment_conditions", "t_diff")?;
337 if !complete {
338 return Ok(MobilityEnv::default());
339 }
340 let mut stmt = self.connection.prepare(
341 "SELECT drift_gas_mass, temperature_c, t_diff FROM experiment_conditions LIMIT 1",
342 )?;
343 let mut rows = stmt.query_map([], |row| {
344 Ok(MobilityEnv {
345 gas_mass: row.get(0)?,
346 temp_c: row.get(1)?,
347 t_diff: row.get(2)?,
348 })
349 })?;
350 match rows.next() {
351 Some(env) => env,
352 None => Ok(MobilityEnv::default()),
353 }
354 }
355
356 fn peptide_scalar_from_row(
358 row: &rusqlite::Row,
359 has_condition: bool,
360 has_rt_mu: bool,
361 ) -> rusqlite::Result<PeptideScalar> {
362 let peptide_id: u32 = row.get("peptide_id")?;
363 let condition_id = if has_condition {
364 row.get::<_, Option<i64>>("condition_id")?
365 } else {
366 None
367 };
368 let retention_time: f32 = row.get("retention_time_gru_predictor")?;
369 let rt_mu: f64 = if has_rt_mu { row.get("rt_mu")? } else { retention_time as f64 };
373 Ok(PeptideScalar {
374 protein_id: row.get("protein_id")?,
375 peptide_id,
376 sequence: PeptideSequence::new(row.get("sequence")?, Some(peptide_id as i32)),
377 proteins: row.get("protein")?,
378 decoy: row.get("decoy")?,
379 missed_cleavages: row.get("missed_cleavages")?,
380 n_term: row.get("n_term")?,
381 c_term: row.get("c_term")?,
382 mono_isotopic_mass: row.get("monoisotopic-mass")?,
383 retention_time,
384 rt_mu,
385 rt_sigma: row.get("rt_sigma")?,
386 rt_lambda: row.get("rt_lambda")?,
387 events: row.get("events")?,
388 condition_id,
389 })
390 }
391
392 fn ion_scalar_from_row(
393 row: &rusqlite::Row,
394 env: &MobilityEnv,
395 has_ccs: bool,
396 has_condition: bool,
397 has_inv_std: bool,
398 ) -> rusqlite::Result<IonScalar> {
399 let simulated_spectrum_str: String = row.get("simulated_spectrum")?;
400 let simulated_spectrum: MzSpectrum = serde_json::from_str(&simulated_spectrum_str)
401 .map_err(|e| {
402 rusqlite::Error::FromSqlConversionFailure(
403 0,
404 rusqlite::types::Type::Text,
405 Box::new(e),
406 )
407 })?;
408 let charge: i8 = row.get("charge")?;
409 let mz: f64 = row.get("mz")?;
410 let ccs: f64 = if has_ccs {
411 row.get("ccs")?
412 } else {
413 let one_over_k0: f64 = row.get("inv_mobility_gru_predictor")?;
414 env.ccs_from_inv_mobility(one_over_k0, mz, charge)
415 };
416 let inv_mobility_std: f64 = if has_inv_std {
419 row.get("inv_mobility_gru_predictor_std")?
420 } else {
421 0.0
422 };
423 let condition_id = if has_condition {
424 row.get::<_, Option<i64>>("condition_id")?
425 } else {
426 None
427 };
428 Ok(IonScalar {
429 ion_id: row.get("ion_id")?,
430 peptide_id: row.get("peptide_id")?,
431 sequence: row.get("sequence")?,
432 charge,
433 relative_abundance: row.get("relative_abundance")?,
434 mz,
435 ccs,
436 inv_mobility_std,
437 simulated_spectrum,
438 condition_id,
439 })
440 }
441
442 pub fn read_peptides_scalar(&self) -> rusqlite::Result<Vec<PeptideScalar>> {
443 let has_condition = self.table_has_column("peptides", "condition_id")?;
444 let has_rt_mu = self.table_has_column("peptides", "rt_mu")?;
445 let mut stmt = self.connection.prepare("SELECT * FROM peptides ORDER BY peptide_id")?;
446 let iter = stmt.query_map([], |row| {
447 Self::peptide_scalar_from_row(row, has_condition, has_rt_mu)
448 })?;
449 let mut out = Vec::new();
450 for p in iter {
451 out.push(p?);
452 }
453 Ok(out)
454 }
455
456 pub fn read_peptides_scalar_for_ids(
460 &self,
461 peptide_ids: &[u32],
462 ) -> rusqlite::Result<Vec<PeptideScalar>> {
463 if peptide_ids.is_empty() {
464 return Ok(Vec::new());
465 }
466 let has_condition = self.table_has_column("peptides", "condition_id")?;
467 let has_rt_mu = self.table_has_column("peptides", "rt_mu")?;
468 const CHUNK_SIZE: usize = 500;
469 let mut out = Vec::new();
470 for chunk in peptide_ids.chunks(CHUNK_SIZE) {
471 let placeholders: String = chunk.iter().map(|_| "?").collect::<Vec<_>>().join(",");
472 let sql = format!(
473 "SELECT * FROM peptides WHERE peptide_id IN ({}) ORDER BY peptide_id",
474 placeholders
475 );
476 let mut stmt = self.connection.prepare(&sql)?;
477 let iter = stmt.query_map(rusqlite::params_from_iter(chunk.iter()), |row| {
478 Self::peptide_scalar_from_row(row, has_condition, has_rt_mu)
479 })?;
480 for p in iter {
481 out.push(p?);
482 }
483 }
484 Ok(out)
485 }
486
487 pub fn read_ions_scalar(&self, env: &MobilityEnv) -> rusqlite::Result<Vec<IonScalar>> {
491 let has_ccs = self.table_has_column("ions", "ccs")?;
492 let has_condition = self.table_has_column("ions", "condition_id")?;
493 let has_inv_std = self.table_has_column("ions", "inv_mobility_gru_predictor_std")?;
494 let mut stmt = self.connection.prepare("SELECT * FROM ions ORDER BY ion_id")?;
495 let env = *env;
496 let iter = stmt.query_map([], move |row| {
497 Self::ion_scalar_from_row(row, &env, has_ccs, has_condition, has_inv_std)
498 })?;
499 let mut out = Vec::new();
500 for i in iter {
501 out.push(i?);
502 }
503 Ok(out)
504 }
505
506 pub fn read_ions_scalar_for_peptides(
512 &self,
513 peptide_ids: &[u32],
514 env: &MobilityEnv,
515 ) -> rusqlite::Result<Vec<IonScalar>> {
516 if peptide_ids.is_empty() {
517 return Ok(Vec::new());
518 }
519 let has_ccs = self.table_has_column("ions", "ccs")?;
520 let has_condition = self.table_has_column("ions", "condition_id")?;
521 let has_inv_std = self.table_has_column("ions", "inv_mobility_gru_predictor_std")?;
522 let env = *env;
523 const CHUNK_SIZE: usize = 500;
524 let mut out = Vec::new();
525 for chunk in peptide_ids.chunks(CHUNK_SIZE) {
526 let placeholders: String = chunk.iter().map(|_| "?").collect::<Vec<_>>().join(",");
527 let sql = format!(
528 "SELECT * FROM ions WHERE peptide_id IN ({}) ORDER BY peptide_id, ion_id",
529 placeholders
530 );
531 let mut stmt = self.connection.prepare(&sql)?;
532 let iter = stmt.query_map(rusqlite::params_from_iter(chunk.iter()), move |row| {
533 Self::ion_scalar_from_row(row, &env, has_ccs, has_condition, has_inv_std)
534 })?;
535 for i in iter {
536 out.push(i?);
537 }
538 }
539 Ok(out)
540 }
541
542 pub fn read_peptides_with_source(
552 &self,
553 source: &crate::sim::projector::DistributionSource,
554 ) -> rusqlite::Result<Vec<PeptidesSim>> {
555 use crate::sim::projector::DistributionSource;
556 let (mode, params) = match source {
557 DistributionSource::Columns => return self.read_peptides(),
558 DistributionSource::Projector { mode, params, .. } => (*mode, *params),
559 };
560 let scalars = self.read_peptides_scalar()?;
561 self.project_peptide_scalars(scalars, mode, params)
562 }
563
564 fn abundance_for_mode(
572 a: f64,
573 mode: crate::sim::projector::ProjectionMode,
574 num_decimals: u32,
575 ) -> f32 {
576 match mode {
577 crate::sim::projector::ProjectionMode::LegacyCompat => {
578 let scale = 10f64.powi(num_decimals as i32);
579 ((a * scale).round_ties_even() / scale) as f32
583 }
584 crate::sim::projector::ProjectionMode::Accurate => a as f32,
585 }
586 }
587
588 fn project_peptide_scalars(
595 &self,
596 scalars: Vec<PeptideScalar>,
597 mode: crate::sim::projector::ProjectionMode,
598 params: crate::sim::projector::ProjectionParams,
599 ) -> rusqlite::Result<Vec<PeptidesSim>> {
600 use crate::sim::projector::ProjectionMode;
601 let mut frames = self.read_frames()?;
603 frames.sort_by_key(|f| f.frame_id);
604 let frame_ids: Vec<u32> = frames.iter().map(|f| f.frame_id).collect();
605 let frame_times: Vec<f64> = frames.iter().map(|f| f.time as f64).collect();
606 if frame_times.len() < 2 {
607 return Err(rusqlite::Error::InvalidQuery);
608 }
609 let rt_cycle = mean_consecutive_diff(&frame_times);
610 let mus: Vec<f64> = scalars.iter().map(|p| p.rt_mu).collect();
611 let sigmas: Vec<f64> = scalars.iter().map(|p| p.rt_sigma).collect();
612 let lambdas: Vec<f64> = scalars.iter().map(|p| p.rt_lambda).collect();
613
614 let projected: Vec<Vec<(u32, f64)>> = match mode {
615 ProjectionMode::LegacyCompat => crate::sim::projector::project_time_legacy(
616 &mus, &sigmas, &lambdas, &frame_ids, &frame_times, rt_cycle, params.target_p,
617 params.frame_step_size, params.n_steps, params.remove_epsilon, params.num_threads,
618 ),
619 ProjectionMode::Accurate => {
620 let mut starts = Vec::with_capacity(frame_times.len());
623 let mut ends = Vec::with_capacity(frame_times.len());
624 for (i, &t) in frame_times.iter().enumerate() {
625 let prev = if i > 0 { frame_times[i - 1] } else { t - (frame_times[1] - frame_times[0]) };
626 starts.push(prev);
627 ends.push(t);
628 }
629 let intervals: Vec<(f64, f64)> = starts.into_iter().zip(ends).collect();
630 let proj = mscore::algorithm::utility::project_emg_over_events_par(
631 &intervals, mus.clone(), sigmas.clone(), lambdas.clone(), params.target_p,
632 params.frame_step_size, params.num_threads.max(1), params.n_steps,
633 );
634 proj.into_iter()
635 .map(|pairs| {
636 pairs.into_iter()
637 .filter(|(_, a)| *a > params.remove_epsilon)
638 .map(|(i, a)| (frame_ids[i], a))
639 .collect()
640 })
641 .collect()
642 }
643 };
644
645 Ok(scalars
646 .into_iter()
647 .zip(projected)
648 .map(|(p, pairs)| {
649 let occ: Vec<u32> = pairs.iter().map(|(f, _)| *f).collect();
650 let ab: Vec<f32> = pairs.iter().map(|(_, a)| Self::abundance_for_mode(*a, mode, params.num_decimals)).collect();
651 let frame_start = occ.first().copied().unwrap_or(0);
652 let frame_end = occ.last().copied().unwrap_or(0);
653 PeptidesSim::new(
654 p.protein_id, p.peptide_id, p.sequence.sequence.clone(), p.proteins, p.decoy,
655 p.missed_cleavages, p.n_term, p.c_term, p.mono_isotopic_mass, p.retention_time,
656 p.events, frame_start, frame_end, occ, ab,
657 )
658 })
659 .collect())
660 }
661
662 pub fn read_ions_with_source(
664 &self,
665 source: &crate::sim::projector::DistributionSource,
666 ) -> rusqlite::Result<Vec<IonSim>> {
667 use crate::sim::projector::DistributionSource;
668 let (mode, env, params) = match source {
669 DistributionSource::Columns => return self.read_ions(),
670 DistributionSource::Projector { mode, env, params } => (*mode, *env, *params),
671 };
672 let scalars = self.read_ions_scalar(&env)?;
673 self.project_ion_scalars(scalars, mode, env, params)
674 }
675
676 fn project_ion_scalars(
681 &self,
682 scalars: Vec<IonScalar>,
683 mode: crate::sim::projector::ProjectionMode,
684 env: MobilityEnv,
685 params: crate::sim::projector::ProjectionParams,
686 ) -> rusqlite::Result<Vec<IonSim>> {
687 use crate::sim::projector::ProjectionMode;
688 let mut scans = self.read_scans()?;
690 scans.sort_by(|a, b| a.mobility.partial_cmp(&b.mobility).unwrap_or(std::cmp::Ordering::Equal));
691 let scan_ids: Vec<u32> = scans.iter().map(|s| s.scan).collect();
692 let scan_mob: Vec<f64> = scans.iter().map(|s| s.mobility as f64).collect();
693 if scan_mob.len() < 2 {
694 return Err(rusqlite::Error::InvalidQuery);
695 }
696 let im_cycle = mean_consecutive_diff(&scan_mob);
697 let means: Vec<f64> = scalars.iter().map(|i| i.inv_mobility(&env)).collect();
698 let sigmas: Vec<f64> = scalars.iter().map(|i| i.inv_mobility_std).collect();
699
700 let projected: Vec<Vec<(i32, f64)>> = match mode {
701 ProjectionMode::LegacyCompat => crate::sim::projector::project_mobility_legacy_par(
702 &means, &sigmas, &scan_ids, &scan_mob, im_cycle, params.target_p,
703 params.scan_step_size, params.num_threads,
704 ),
705 ProjectionMode::Accurate => {
706 let acc = crate::sim::projector::project_mobility_accurate_par(
708 &means, &sigmas, &scan_mob, params.target_p, params.scan_step_size,
709 params.num_threads,
710 );
711 acc.into_iter()
712 .map(|pairs| pairs.into_iter().map(|(idx, a)| (scan_ids[idx as usize] as i32, a)).collect())
713 .collect()
714 }
715 };
716
717 Ok(scalars
718 .into_iter()
719 .zip(projected)
720 .map(|(ion, pairs)| {
721 let occ: Vec<u32> = pairs.iter().map(|(s, _)| *s as u32).collect();
722 let ab: Vec<f32> = pairs.iter().map(|(_, a)| Self::abundance_for_mode(*a, mode, params.num_decimals)).collect();
723 let mobility = ion.inv_mobility(&env) as f32;
724 IonSim::new(
725 ion.ion_id, ion.peptide_id, ion.sequence, ion.charge, ion.relative_abundance,
726 mobility, ion.simulated_spectrum, occ, ab,
727 )
728 })
729 .collect())
730 }
731
732 pub fn read_prediction_set(&self) -> rusqlite::Result<PredictionSet> {
738 let has_table: bool = self
739 .connection
740 .query_row(
741 "SELECT 1 FROM sqlite_master WHERE type='table' AND name='prediction_sets'",
742 [],
743 |_| Ok(true),
744 )
745 .unwrap_or(false);
746 if !has_table {
747 return Ok(PredictionSet::legacy_bruker());
748 }
749 self.connection.query_row(
750 "SELECT prediction_set_id, predictor_model, instrument, acquisition_type, \
751 activation_method, energy_unit, collision_energy_encoding \
752 FROM prediction_sets ORDER BY prediction_set_id LIMIT 1",
753 [],
754 |row| {
755 Ok(PredictionSet {
756 prediction_set_id: row.get(0)?,
757 predictor_model: row.get(1)?,
758 instrument: row.get(2)?,
759 acquisition_type: row.get(3)?,
760 activation_method: row.get(4)?,
761 energy_unit: row.get(5)?,
762 collision_energy_encoding: row.get(6)?,
763 })
764 },
765 )
766 }
767
768 fn candidate_peptide_ids_for_frame_range(
774 &self,
775 frame_min: u32,
776 frame_max: u32,
777 ) -> rusqlite::Result<Vec<u32>> {
778 let mut stmt = self.connection.prepare(
779 "SELECT peptide_id FROM peptides WHERE frame_occurrence_start <= ?1 AND frame_occurrence_end >= ?2 ORDER BY peptide_id",
780 )?;
781 let iter = stmt.query_map([frame_max, frame_min], |row| row.get::<_, u32>(0))?;
782 let mut out = Vec::new();
783 for id in iter {
784 out.push(id?);
785 }
786 Ok(out)
787 }
788
789 pub fn read_peptides_for_frame_range_with_source(
794 &self,
795 frame_min: u32,
796 frame_max: u32,
797 source: &crate::sim::projector::DistributionSource,
798 ) -> rusqlite::Result<Vec<PeptidesSim>> {
799 use crate::sim::projector::DistributionSource;
800 let (mode, params) = match source {
801 DistributionSource::Columns => {
802 return self.read_peptides_for_frame_range(frame_min, frame_max)
803 }
804 DistributionSource::Projector { mode, params, .. } => (*mode, *params),
805 };
806 if matches!(mode, crate::sim::projector::ProjectionMode::Accurate) {
820 return Err(rusqlite::Error::InvalidParameterName(
821 "lazy loading with projection_mode='accurate' is not yet supported \
822 (the per-batch candidate index uses legacy occurrence columns and \
823 would omit peptides whose accurate RT support crosses a batch \
824 boundary); use eager loading for accurate projection, or \
825 projection_mode='legacy_compat' with lazy loading"
826 .to_string(),
827 ));
828 }
829 let candidate_ids = self.candidate_peptide_ids_for_frame_range(frame_min, frame_max)?;
830 let scalars = self.read_peptides_scalar_for_ids(&candidate_ids)?;
832 self.project_peptide_scalars(scalars, mode, params)
833 }
834
835 pub fn read_ions_for_peptides_with_source(
839 &self,
840 peptide_ids: &[u32],
841 source: &crate::sim::projector::DistributionSource,
842 ) -> rusqlite::Result<Vec<IonSim>> {
843 use crate::sim::projector::DistributionSource;
844 let (mode, env, params) = match source {
845 DistributionSource::Columns => return self.read_ions_for_peptides(peptide_ids),
846 DistributionSource::Projector { mode, env, params } => (*mode, *env, *params),
847 };
848 let scalars = self.read_ions_scalar_for_peptides(peptide_ids, &env)?;
851 self.project_ion_scalars(scalars, mode, env, params)
852 }
853
854 pub fn read_window_group_settings(&self) -> rusqlite::Result<Vec<WindowGroupSettingsSim>> {
855 let mut stmt = self.connection.prepare("SELECT * FROM dia_ms_ms_windows")?;
856 let window_group_settings_iter = stmt.query_map([], |row| {
857 Ok(WindowGroupSettingsSim::new(
858 row.get("window_group")?,
859 row.get("scan_start")?,
860 row.get("scan_end")?,
861 row.get("isolation_mz")?,
862 row.get("isolation_width")?,
863 row.get("collision_energy")?,
864 ))
865 })?;
866 let mut window_group_settings = Vec::new();
867 for window_group_setting in window_group_settings_iter {
868 window_group_settings.push(window_group_setting?);
869 }
870 Ok(window_group_settings)
871 }
872
873 pub fn read_frame_to_window_group(&self) -> rusqlite::Result<Vec<FrameToWindowGroupSim>> {
874 let mut stmt = self.connection.prepare("SELECT * FROM dia_ms_ms_info")?;
875 let frame_to_window_group_iter = stmt.query_map([], |row| {
876 Ok(FrameToWindowGroupSim::new(
877 row.get("frame")?,
878 row.get("window_group")?,
879 ))
880 })?;
881
882 let mut frame_to_window_groups: Vec<FrameToWindowGroupSim> = Vec::new();
883 for frame_to_window_group in frame_to_window_group_iter {
884 frame_to_window_groups.push(frame_to_window_group?);
885 }
886
887 Ok(frame_to_window_groups)
888 }
889
890 pub fn read_pasef_meta(&self) -> rusqlite::Result<Vec<PASEFMeta>> {
891 let mut stmt = self.connection.prepare(
892 "SELECT frame, scan_start, scan_end, isolation_mz, isolation_width, collision_energy, precursor FROM pasef_meta"
893 )?;
894 let pasef_meta_iter = stmt.query_map([], |row| {
895 Ok(PASEFMeta::new(
896 row.get("frame")?,
897 row.get("scan_start")?,
898 row.get("scan_end")?,
899 row.get("isolation_mz")?,
900 row.get("isolation_width")?,
901 row.get("collision_energy")?,
902 row.get("precursor")?,
903 ))
904 })?;
905
906 let mut pasef_meta: Vec<PASEFMeta> = Vec::new();
907
908 for pasef_meta_entry in pasef_meta_iter {
909 pasef_meta.push(pasef_meta_entry?);
910 }
911
912 Ok(pasef_meta)
913 }
914
915 pub fn read_peptides_for_frame_range(
918 &self,
919 frame_min: u32,
920 frame_max: u32,
921 ) -> rusqlite::Result<Vec<PeptidesSim>> {
922 let mut stmt = self.connection.prepare(
923 "SELECT * FROM peptides WHERE frame_occurrence_start <= ?1 AND frame_occurrence_end >= ?2 ORDER BY peptide_id"
924 )?;
925
926 let peptides_iter = stmt.query_map([frame_max, frame_min], |row| {
927 Self::peptide_from_row(row)
928 })?;
929
930 let mut peptides = Vec::new();
931 for peptide in peptides_iter {
932 peptides.push(peptide?);
933 }
934 Ok(peptides)
935 }
936
937 pub fn read_ions_for_peptides(&self, peptide_ids: &[u32]) -> rusqlite::Result<Vec<IonSim>> {
940 if peptide_ids.is_empty() {
941 return Ok(Vec::new());
942 }
943
944 let mut all_ions = Vec::new();
945
946 const CHUNK_SIZE: usize = 500;
948
949 for chunk in peptide_ids.chunks(CHUNK_SIZE) {
950 let placeholders: String = chunk.iter()
951 .map(|_| "?")
952 .collect::<Vec<_>>()
953 .join(",");
954
955 let sql = format!(
962 "SELECT * FROM ions WHERE peptide_id IN ({}) ORDER BY peptide_id, ion_id",
963 placeholders
964 );
965
966 let mut stmt = self.connection.prepare(&sql)?;
967
968 let ions_iter = stmt.query_map(
969 rusqlite::params_from_iter(chunk.iter()),
970 |row| Self::ion_from_row(row),
971 )?;
972
973 for ion in ions_iter {
974 all_ions.push(ion?);
975 }
976 }
977
978 Ok(all_ions)
979 }
980
981 pub fn read_fragment_ions_for_peptides(&self, peptide_ids: &[u32]) -> rusqlite::Result<Vec<FragmentIonSim>> {
984 if peptide_ids.is_empty() {
985 return Ok(Vec::new());
986 }
987
988 let mut all_fragment_ions = Vec::new();
989
990 const CHUNK_SIZE: usize = 500;
992
993 for chunk in peptide_ids.chunks(CHUNK_SIZE) {
994 let placeholders: String = chunk.iter()
995 .map(|_| "?")
996 .collect::<Vec<_>>()
997 .join(",");
998
999 let sql = format!(
1000 "SELECT * FROM fragment_ions WHERE peptide_id IN ({}) ORDER BY peptide_id",
1001 placeholders
1002 );
1003
1004 let mut stmt = self.connection.prepare(&sql)?;
1005
1006 let fragment_ion_iter = stmt.query_map(
1007 rusqlite::params_from_iter(chunk.iter()),
1008 |row| Self::fragment_ion_from_row(row),
1009 )?;
1010
1011 for fragment_ion in fragment_ion_iter {
1012 all_fragment_ions.push(fragment_ion?);
1013 }
1014 }
1015
1016 Ok(all_fragment_ions)
1017 }
1018
1019 pub fn read_fragment_ions(&self) -> rusqlite::Result<Vec<FragmentIonSim>> {
1020 let mut stmt = self.connection.prepare("SELECT * FROM fragment_ions")?;
1021
1022 let fragment_ion_sim_iter = stmt.query_map([], |row| {
1023 Self::fragment_ion_from_row(row)
1024 })?;
1025
1026 let mut fragment_ion_sim = Vec::new();
1027 for fragment_ion in fragment_ion_sim_iter {
1028 fragment_ion_sim.push(fragment_ion?);
1029 }
1030
1031 Ok(fragment_ion_sim)
1032 }
1033
1034 pub fn get_transmission_dia(&self) -> TimsTransmissionDIA {
1035 let frame_to_window_group = self.read_frame_to_window_group().unwrap();
1036 let window_group_settings = self.read_window_group_settings().unwrap();
1037
1038 TimsTransmissionDIA::new(
1039 frame_to_window_group
1040 .iter()
1041 .map(|x| x.frame_id as i32)
1042 .collect(),
1043 frame_to_window_group
1044 .iter()
1045 .map(|x| x.window_group as i32)
1046 .collect(),
1047 window_group_settings
1048 .iter()
1049 .map(|x| x.window_group as i32)
1050 .collect(),
1051 window_group_settings
1052 .iter()
1053 .map(|x| x.scan_start as i32)
1054 .collect(),
1055 window_group_settings
1056 .iter()
1057 .map(|x| x.scan_end as i32)
1058 .collect(),
1059 window_group_settings
1060 .iter()
1061 .map(|x| x.isolation_mz as f64)
1062 .collect(),
1063 window_group_settings
1064 .iter()
1065 .map(|x| x.isolation_width as f64)
1066 .collect(),
1067 None,
1068 )
1069 }
1070
1071 pub fn get_transmission_dda(&self) -> TimsTransmissionDDA {
1072 let pasef_meta = self.read_pasef_meta().unwrap();
1073 TimsTransmissionDDA::new(
1074 pasef_meta,
1075 None,
1076 )
1077 }
1078
1079 pub fn get_collision_energy_dia(&self) -> TimsTofCollisionEnergyDIA {
1080 let frame_to_window_group = self.read_frame_to_window_group().unwrap();
1081 let window_group_settings = self.read_window_group_settings().unwrap();
1082
1083 TimsTofCollisionEnergyDIA::new(
1084 frame_to_window_group
1085 .iter()
1086 .map(|x| x.frame_id as i32)
1087 .collect(),
1088 frame_to_window_group
1089 .iter()
1090 .map(|x| x.window_group as i32)
1091 .collect(),
1092 window_group_settings
1093 .iter()
1094 .map(|x| x.window_group as i32)
1095 .collect(),
1096 window_group_settings
1097 .iter()
1098 .map(|x| x.scan_start as i32)
1099 .collect(),
1100 window_group_settings
1101 .iter()
1102 .map(|x| x.scan_end as i32)
1103 .collect(),
1104 window_group_settings
1105 .iter()
1106 .map(|x| x.collision_energy as f64)
1107 .collect(),
1108 )
1109 }
1110
1111 fn ion_map_fn_dda(
1112 ion: IonSim,
1113 peptide_map: &BTreeMap<u32, PeptidesSim>,
1114 _precursor_frames: &HashSet<u32>,
1115 transmission: &TimsTransmissionDDA,
1116 ) -> BTreeSet<(u32, u32, String, i8, i32)> {
1117 let peptide = peptide_map.get(&ion.peptide_id).unwrap();
1118 let mut ret_tree: BTreeSet<(u32, u32, String, i8, i32)> = BTreeSet::new();
1119
1120 let selections = transmission.get_selections_for_precursor(ion.ion_id as i32);
1123
1124 for (_, collision_energy) in selections {
1125 let quantized_energy = (collision_energy * 100.0).round() as i32;
1128
1129 ret_tree.insert((
1130 ion.peptide_id,
1131 ion.ion_id,
1132 peptide.sequence.sequence.clone(),
1133 ion.charge,
1134 quantized_energy,
1135 ));
1136 }
1137
1138 ret_tree
1139 }
1140
1141 fn ion_map_fn_dia(
1142 ion: IonSim,
1143 peptide_map: &BTreeMap<u32, PeptidesSim>,
1144 precursor_frames: &HashSet<u32>,
1145 transmission: &TimsTransmissionDIA,
1146 collision_energy: &TimsTofCollisionEnergyDIA,
1147 ) -> BTreeSet<(u32, u32, String, i8, i32)> {
1148 let peptide = peptide_map.get(&ion.peptide_id).unwrap();
1149 let mut ret_tree: BTreeSet<(u32, u32, String, i8, i32)> = BTreeSet::new();
1150
1151 for frame in peptide.frame_distribution.occurrence.iter() {
1153 if !precursor_frames.contains(frame) {
1155 for scan in &ion.scan_distribution.occurrence {
1157 let precursor_spec = &ion.simulated_spectrum;
1160
1161 if transmission.any_transmitted(
1162 *frame as i32,
1163 *scan as i32,
1164 &precursor_spec.mz,
1165 Some(0.5),
1166 ) {
1167 let collision_energy =
1168 collision_energy.get_collision_energy(*frame as i32, *scan as i32);
1169 let quantized_energy = (collision_energy * 100.0).round() as i32;
1170
1171 ret_tree.insert((
1172 ion.peptide_id,
1173 ion.ion_id,
1174 peptide.sequence.sequence.clone(),
1175 ion.charge,
1176 quantized_energy,
1177 ));
1178 }
1179 }
1180 }
1181 }
1182 ret_tree
1183 }
1184
1185 pub fn get_transmitted_ions(
1187 &self,
1188 num_threads: usize,
1189 dda_mode: bool,
1190 ) -> (Vec<i32>, Vec<i32>, Vec<String>, Vec<i8>, Vec<f32>) {
1191
1192 let thread_pool = ThreadPoolBuilder::new()
1193 .num_threads(num_threads)
1194 .build()
1195 .unwrap();
1196
1197 let peptides = self.read_peptides().unwrap();
1198
1199 let peptide_map = TimsTofSyntheticsDataHandle::build_peptide_map(&peptides);
1200
1201 let precursor_frames =
1202 TimsTofSyntheticsDataHandle::build_precursor_frame_id_set(&self.read_frames().unwrap());
1203
1204 let ions = self.read_ions().unwrap();
1205
1206 let trees = match dda_mode {
1207 true => {
1208 let transmission = self.get_transmission_dda();
1209 thread_pool.install(|| {
1210 ions.par_iter()
1211 .map(|ion| {
1212 TimsTofSyntheticsDataHandle::ion_map_fn_dda(
1213 ion.clone(),
1214 &peptide_map,
1215 &precursor_frames,
1216 &transmission,
1217 )
1218 })
1219 .collect::<Vec<_>>()
1220 })
1221 },
1222 false => {
1223 let transmission = self.get_transmission_dia();
1224 let collision_energy = self.get_collision_energy_dia();
1225 thread_pool.install(|| {
1226 ions.par_iter()
1227 .map(|ion| {
1228 TimsTofSyntheticsDataHandle::ion_map_fn_dia(
1229 ion.clone(),
1230 &peptide_map,
1231 &precursor_frames,
1232 &transmission,
1233 &collision_energy,
1234 )
1235 })
1236 .collect::<Vec<_>>()
1237 })
1238 },
1239 };
1240
1241 let mut ret_tree: BTreeSet<(u32, u32, String, i8, i32)> = BTreeSet::new();
1242 for tree in trees {
1243 ret_tree.extend(tree);
1244 }
1245
1246 let mut ret_peptide_id = Vec::new();
1247 let mut ret_ion_id = Vec::new();
1248 let mut ret_sequence = Vec::new();
1249 let mut ret_charge = Vec::new();
1250 let mut ret_energy = Vec::new();
1251
1252 for (peptide_id, ion_id, sequence, charge, energy) in ret_tree {
1253 ret_peptide_id.push(peptide_id as i32);
1254 ret_ion_id.push(ion_id as i32);
1255 ret_sequence.push(sequence);
1256 ret_charge.push(charge);
1257 ret_energy.push(energy as f32 / 100.0);
1258 }
1259
1260 (
1261 ret_peptide_id,
1262 ret_ion_id,
1263 ret_sequence,
1264 ret_charge,
1265 ret_energy,
1266 )
1267 }
1268
1269 pub fn get_transmitted_ions_for_frame_range(
1284 &self,
1285 frame_min: u32,
1286 frame_max: u32,
1287 num_threads: usize,
1288 dda_mode: bool,
1289 ) -> (Vec<i32>, Vec<i32>, Vec<String>, Vec<i8>, Vec<f32>) {
1290
1291 let thread_pool = ThreadPoolBuilder::new()
1292 .num_threads(num_threads)
1293 .build()
1294 .unwrap();
1295
1296 let peptides = self.read_peptides_for_frame_range(frame_min, frame_max).unwrap();
1298
1299 if peptides.is_empty() {
1300 return (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
1301 }
1302
1303 let peptide_ids: Vec<u32> = peptides.iter().map(|p| p.peptide_id).collect();
1304 let peptide_map = TimsTofSyntheticsDataHandle::build_peptide_map(&peptides);
1305
1306 let precursor_frames =
1307 TimsTofSyntheticsDataHandle::build_precursor_frame_id_set(&self.read_frames().unwrap());
1308
1309 let ions = self.read_ions_for_peptides(&peptide_ids).unwrap();
1311
1312 if ions.is_empty() {
1313 return (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
1314 }
1315
1316 let trees = match dda_mode {
1317 true => {
1318 let transmission = self.get_transmission_dda();
1319 thread_pool.install(|| {
1320 ions.par_iter()
1321 .map(|ion| {
1322 TimsTofSyntheticsDataHandle::ion_map_fn_dda(
1323 ion.clone(),
1324 &peptide_map,
1325 &precursor_frames,
1326 &transmission,
1327 )
1328 })
1329 .collect::<Vec<_>>()
1330 })
1331 },
1332 false => {
1333 let transmission = self.get_transmission_dia();
1334 let collision_energy = self.get_collision_energy_dia();
1335 thread_pool.install(|| {
1336 ions.par_iter()
1337 .map(|ion| {
1338 TimsTofSyntheticsDataHandle::ion_map_fn_dia(
1339 ion.clone(),
1340 &peptide_map,
1341 &precursor_frames,
1342 &transmission,
1343 &collision_energy,
1344 )
1345 })
1346 .collect::<Vec<_>>()
1347 })
1348 },
1349 };
1350
1351 let mut ret_tree: BTreeSet<(u32, u32, String, i8, i32)> = BTreeSet::new();
1352 for tree in trees {
1353 ret_tree.extend(tree);
1354 }
1355
1356 let mut ret_peptide_id = Vec::new();
1357 let mut ret_ion_id = Vec::new();
1358 let mut ret_sequence = Vec::new();
1359 let mut ret_charge = Vec::new();
1360 let mut ret_energy = Vec::new();
1361
1362 for (peptide_id, ion_id, sequence, charge, energy) in ret_tree {
1363 ret_peptide_id.push(peptide_id as i32);
1364 ret_ion_id.push(ion_id as i32);
1365 ret_sequence.push(sequence);
1366 ret_charge.push(charge);
1367 ret_energy.push(energy as f32 / 100.0);
1368 }
1369
1370 (
1371 ret_peptide_id,
1372 ret_ion_id,
1373 ret_sequence,
1374 ret_charge,
1375 ret_energy,
1376 )
1377 }
1378
1379 pub fn build_peptide_to_ion_map(ions: &Vec<IonSim>) -> BTreeMap<u32, Vec<IonSim>> {
1381 let mut ion_map = BTreeMap::new();
1382 for ion in ions.iter() {
1383 let ions = ion_map.entry(ion.peptide_id).or_insert_with(Vec::new);
1384 ions.push(ion.clone());
1385 }
1386 ion_map
1387 }
1388
1389 pub fn build_peptide_map(peptides: &Vec<PeptidesSim>) -> BTreeMap<u32, PeptidesSim> {
1391 let mut peptide_map = BTreeMap::new();
1392 for peptide in peptides.iter() {
1393 peptide_map.insert(peptide.peptide_id, peptide.clone());
1394 }
1395 peptide_map
1396 }
1397
1398 pub fn build_precursor_frame_id_set(frames: &Vec<FramesSim>) -> HashSet<u32> {
1400 frames
1401 .iter()
1402 .filter(|frame| frame.parse_ms_type() == MsType::Precursor)
1403 .map(|frame| frame.frame_id)
1404 .collect()
1405 }
1406
1407 pub fn build_peptide_to_events(peptides: &Vec<PeptidesSim>) -> BTreeMap<u32, f32> {
1409 let mut peptide_to_events = BTreeMap::new();
1410 for peptide in peptides.iter() {
1411 peptide_to_events.insert(peptide.peptide_id, peptide.events);
1412 }
1413 peptide_to_events
1414 }
1415
1416 pub fn build_frame_to_rt(frames: &Vec<FramesSim>) -> BTreeMap<u32, f32> {
1418 let mut frame_to_rt = BTreeMap::new();
1419 for frame in frames.iter() {
1420 frame_to_rt.insert(frame.frame_id, frame.time);
1421 }
1422 frame_to_rt
1423 }
1424
1425 pub fn build_scan_to_mobility(scans: &Vec<ScansSim>) -> BTreeMap<u32, f32> {
1427 let mut scan_to_mobility = BTreeMap::new();
1428 for scan in scans.iter() {
1429 scan_to_mobility.insert(scan.scan, scan.mobility);
1430 }
1431 scan_to_mobility
1432 }
1433 pub fn build_frame_to_abundances(
1434 peptides: &Vec<PeptidesSim>,
1435 ) -> BTreeMap<u32, (Vec<u32>, Vec<f32>)> {
1436 let mut frame_to_abundances = BTreeMap::new();
1437
1438 for peptide in peptides.iter() {
1439 let peptide_id = peptide.peptide_id;
1440 for (frame_id, abundance) in peptide
1445 .frame_distribution
1446 .occurrence
1447 .iter()
1448 .zip(peptide.frame_distribution.abundance.iter())
1449 {
1450 if *abundance > 1e-6 {
1452 let (occurrences, abundances) = frame_to_abundances
1453 .entry(*frame_id)
1454 .or_insert((vec![], vec![]));
1455 occurrences.push(peptide_id);
1456 abundances.push(*abundance);
1457 }
1458 }
1459 }
1460
1461 frame_to_abundances
1462 }
1463 pub fn build_peptide_to_ions(
1464 ions: &Vec<IonSim>,
1465 ) -> BTreeMap<
1466 u32,
1467 (
1468 Vec<f32>,
1469 Vec<Vec<u32>>,
1470 Vec<Vec<f32>>,
1471 Vec<i8>,
1472 Vec<MzSpectrum>,
1473 ),
1474 > {
1475 let mut peptide_to_ions = BTreeMap::new();
1476
1477 for ion in ions.iter() {
1478 let peptide_id = ion.peptide_id;
1479 let abundance = ion.relative_abundance;
1480 let scan_occurrence = ion.scan_distribution.occurrence.clone();
1481 let scan_abundance = ion.scan_distribution.abundance.clone();
1482 let charge = ion.charge;
1483 let spectrum = ion.simulated_spectrum.clone();
1484
1485 let (abundances, scan_occurrences, scan_abundances, charges, spectra) = peptide_to_ions
1486 .entry(peptide_id)
1487 .or_insert((vec![], vec![], vec![], vec![], vec![]));
1488 abundances.push(abundance);
1489 scan_occurrences.push(scan_occurrence);
1490 scan_abundances.push(scan_abundance);
1491 charges.push(charge);
1492 spectra.push(spectrum);
1493 }
1494
1495 peptide_to_ions
1496 }
1497 pub fn build_fragment_ions(
1498 peptides_sim: &BTreeMap<u32, PeptidesSim>,
1499 fragment_ions: &Vec<FragmentIonSim>,
1500 num_threads: usize,
1501 ) -> BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrum>)> {
1502 let thread_pool = ThreadPoolBuilder::new()
1503 .num_threads(num_threads)
1504 .build()
1505 .unwrap();
1506 let fragment_ion_map = thread_pool.install(|| {
1507 fragment_ions
1508 .par_iter()
1509 .map(|fragment_ion| {
1510 let key = (
1511 fragment_ion.peptide_id,
1512 fragment_ion.charge,
1513 (fragment_ion.collision_energy * 1e3).round() as i32,
1514 );
1515
1516 let value = peptides_sim
1517 .get(&fragment_ion.peptide_id)
1518 .unwrap()
1519 .sequence
1520 .associate_with_predicted_intensities(
1521 fragment_ion.charge as i32,
1522 FragmentType::B,
1523 fragment_ion.to_dense(174),
1524 true,
1525 true,
1526 );
1527
1528 let fragment_ions: Vec<MzSpectrum> = value
1529 .peptide_ions
1530 .par_iter()
1531 .map(|ion_series| {
1532 ion_series.generate_isotopic_spectrum(1e-2, 1e-3, 100, 1e-5)
1533 })
1534 .collect();
1535 (key, (value, fragment_ions))
1536 })
1537 .collect::<BTreeMap<_, _>>() });
1539
1540 fragment_ion_map
1541 }
1542
1543 pub fn build_fragment_ions_annotated(
1544 peptides_sim: &BTreeMap<u32, PeptidesSim>,
1545 fragment_ions: &Vec<FragmentIonSim>,
1546 num_threads: usize,
1547 ) -> BTreeMap<(u32, i8, i32), (PeptideProductIonSeriesCollection, Vec<MzSpectrumAnnotated>)>
1548 {
1549 let thread_pool = ThreadPoolBuilder::new()
1550 .num_threads(num_threads)
1551 .build()
1552 .unwrap();
1553 let fragment_ion_map = thread_pool.install(|| {
1554 fragment_ions
1555 .par_iter()
1556 .map(|fragment_ion| {
1557 let key = (
1558 fragment_ion.peptide_id,
1559 fragment_ion.charge,
1560 (fragment_ion.collision_energy * 1e3).round() as i32,
1561 );
1562
1563 let value = peptides_sim
1564 .get(&fragment_ion.peptide_id)
1565 .unwrap()
1566 .sequence
1567 .associate_with_predicted_intensities(
1568 fragment_ion.charge as i32,
1569 FragmentType::B,
1570 fragment_ion.to_dense(174),
1571 true,
1572 true,
1573 );
1574
1575 let fragment_ions: Vec<MzSpectrumAnnotated> = value
1576 .peptide_ions
1577 .par_iter()
1578 .map(|ion_series| {
1579 ion_series.generate_isotopic_spectrum_annotated(1e-2, 1e-3, 100, 1e-5)
1580 })
1581 .collect();
1582 (key, (value, fragment_ions))
1583 })
1584 .collect::<BTreeMap<_, _>>() });
1586
1587 fragment_ion_map
1588 }
1589
1590 pub fn build_fragment_ions_with_transmission_data(
1612 peptides_sim: &BTreeMap<u32, PeptidesSim>,
1613 fragment_ions: &Vec<FragmentIonSim>,
1614 num_threads: usize,
1615 ) -> BTreeMap<(u32, i8, i32), FragmentIonsWithComplementary> {
1616 let thread_pool = ThreadPoolBuilder::new()
1617 .num_threads(num_threads)
1618 .build()
1619 .unwrap();
1620
1621 let fragment_ion_map = thread_pool.install(|| {
1622 fragment_ions
1623 .par_iter()
1624 .map(|fragment_ion| {
1625 let key = (
1626 fragment_ion.peptide_id,
1627 fragment_ion.charge,
1628 (fragment_ion.collision_energy * 1e3).round() as i32,
1629 );
1630
1631 let peptide_sim = peptides_sim.get(&fragment_ion.peptide_id).unwrap();
1632
1633 let precursor_composition = peptide_sim.sequence.atomic_composition();
1635
1636 let precursor_composition_owned: std::collections::HashMap<String, i32> =
1638 precursor_composition.iter().map(|(k, v)| (k.to_string(), *v)).collect();
1639 let precursor_isotope_distribution = mscore::algorithm::isotope::generate_isotope_distribution(
1640 &precursor_composition_owned,
1641 1e-3,
1642 1e-8,
1643 100,
1644 ).into_iter().filter(|&(_, abundance)| abundance > 1e-10).collect();
1645
1646 let ion_series_collection = peptide_sim
1647 .sequence
1648 .associate_with_predicted_intensities(
1649 fragment_ion.charge as i32,
1650 FragmentType::B,
1651 fragment_ion.to_dense(174),
1652 true,
1653 true,
1654 );
1655
1656 let mut fragment_spectra: Vec<MzSpectrum> = Vec::new();
1658 let mut per_fragment_data: Vec<Vec<FragmentIonTransmissionData>> = Vec::new();
1659
1660 for ion_series in &ion_series_collection.peptide_ions {
1661 let spectrum = ion_series.generate_isotopic_spectrum(1e-2, 1e-3, 100, 1e-5);
1663 fragment_spectra.push(spectrum);
1664
1665 let mut series_fragment_data: Vec<FragmentIonTransmissionData> = Vec::new();
1667
1668 for n_ion in &ion_series.n_ions {
1670 let frag_dist = n_ion.isotope_distribution(1e-3, 1e-8, 100, 1e-10);
1671 let comp_dist = n_ion.complementary_isotope_distribution(
1672 &precursor_composition,
1673 1e-3,
1674 1e-8,
1675 100,
1676 );
1677
1678 series_fragment_data.push(FragmentIonTransmissionData {
1679 fragment_distribution: frag_dist,
1680 complementary_distribution: comp_dist,
1681 predicted_intensity: n_ion.ion.intensity,
1682 });
1683 }
1684
1685 for c_ion in &ion_series.c_ions {
1687 let frag_dist = c_ion.isotope_distribution(1e-3, 1e-8, 100, 1e-10);
1688 let comp_dist = c_ion.complementary_isotope_distribution(
1689 &precursor_composition,
1690 1e-3,
1691 1e-8,
1692 100,
1693 );
1694
1695 series_fragment_data.push(FragmentIonTransmissionData {
1696 fragment_distribution: frag_dist,
1697 complementary_distribution: comp_dist,
1698 predicted_intensity: c_ion.ion.intensity,
1699 });
1700 }
1701
1702 per_fragment_data.push(series_fragment_data);
1703 }
1704
1705 let data = FragmentIonsWithComplementary {
1706 ion_series_collection,
1707 fragment_spectra,
1708 precursor_isotope_distribution,
1709 per_fragment_data,
1710 };
1711
1712 (key, data)
1713 })
1714 .collect::<BTreeMap<_, _>>()
1715 });
1716
1717 fragment_ion_map
1718 }
1719}
1720
1721#[derive(Debug, Clone)]
1723pub struct FragmentIonTransmissionData {
1724 pub fragment_distribution: Vec<(f64, f64)>,
1726 pub complementary_distribution: Vec<(f64, f64)>,
1728 pub predicted_intensity: f64,
1730}
1731
1732#[derive(Debug, Clone)]
1738pub struct FragmentIonsWithComplementary {
1739 pub ion_series_collection: PeptideProductIonSeriesCollection,
1741 pub fragment_spectra: Vec<MzSpectrum>,
1743 pub precursor_isotope_distribution: Vec<(f64, f64)>,
1745 pub per_fragment_data: Vec<Vec<FragmentIonTransmissionData>>,
1748}
1749
1750#[cfg(test)]
1751mod scalar_reader_tests {
1752 use super::*;
1753 use rusqlite::Connection;
1754
1755 #[test]
1756 fn prediction_set_compatibility() {
1757 assert!(PredictionSet::legacy_bruker().assert_render_compatible().is_ok());
1759
1760 let mut astral = PredictionSet::legacy_bruker();
1765 astral.instrument = "orbitrap_astral".to_string();
1766 astral.energy_unit = "nce".to_string();
1767 astral.predictor_model = Some("prosit_hcd".to_string());
1768 assert!(astral.assert_render_compatible().is_ok());
1770
1771 let mut bad_encoding = PredictionSet::legacy_bruker();
1774 bad_encoding.collision_energy_encoding = "nce_raw".to_string();
1775 assert!(bad_encoding.assert_render_compatible().is_err());
1776 }
1777
1778 #[test]
1779 fn resolve_fragment_ce_key_tolerates_quantization_noise() {
1780 let mut map: BTreeMap<(u32, i8, i32), ()> = BTreeMap::new();
1782 map.insert((1, 2, 330), ()); assert_eq!(resolve_fragment_ce_key(&map, 1, 2, 33.00), Some(330));
1786 assert_eq!(resolve_fragment_ce_key(&map, 1, 2, 33.0499), Some(330));
1788 assert_eq!(resolve_fragment_ce_key(&map, 1, 2, 32.95), Some(330));
1790 assert_eq!(resolve_fragment_ce_key(&map, 1, 2, 35.0), None);
1792 assert_eq!(resolve_fragment_ce_key(&map, 9, 2, 33.0), None);
1794 }
1795
1796 #[test]
1800 fn scalar_readers_on_legacy_db() {
1801 let path = std::env::temp_dir().join(format!(
1802 "rustdf_scalar_test_{}.db",
1803 std::process::id()
1804 ));
1805 let _ = std::fs::remove_file(&path);
1806 let one_over_k0 = 0.85_f64;
1807 let (mz, charge) = (500.25_f64, 2_i8);
1808 {
1809 let conn = Connection::open(&path).unwrap();
1810 conn.execute_batch(
1811 "CREATE TABLE peptides (
1812 protein_id INTEGER, peptide_id INTEGER, sequence TEXT, protein TEXT,
1813 decoy INTEGER, missed_cleavages INTEGER, n_term, c_term,
1814 \"monoisotopic-mass\" REAL,
1815 retention_time_gru_predictor REAL, rt_sigma REAL, rt_lambda REAL, events REAL
1816 );
1817 CREATE TABLE ions (
1818 ion_id INTEGER, peptide_id INTEGER, sequence TEXT, charge INTEGER,
1819 relative_abundance REAL, mz REAL, inv_mobility_gru_predictor REAL,
1820 inv_mobility_gru_predictor_std REAL, simulated_spectrum TEXT
1821 );",
1822 )
1823 .unwrap();
1824 conn.execute(
1825 "INSERT INTO peptides VALUES (1,1,'PEPTIDEK','P',0,0,NULL,NULL,930.5,123.4,1.2,0.3,5.0)",
1826 [],
1827 )
1828 .unwrap();
1829 conn.execute(
1830 "INSERT INTO ions VALUES (1,1,'PEPTIDEK',?1,1.0,?2,?3,0.02,'{\"mz\":[500.25],\"intensity\":[1.0]}')",
1831 rusqlite::params![charge, mz, one_over_k0],
1832 )
1833 .unwrap();
1834 }
1835
1836 let handle = TimsTofSyntheticsDataHandle::new(&path).unwrap();
1837
1838 let env = handle.read_mobility_env().unwrap();
1840 assert_eq!(env, MobilityEnv::default());
1841
1842 let peptides = handle.read_peptides_scalar().unwrap();
1843 assert_eq!(peptides.len(), 1);
1844 assert_eq!(peptides[0].rt_sigma, 1.2);
1845 assert_eq!(peptides[0].condition_id, None);
1846
1847 let ions = handle.read_ions_scalar(&env).unwrap();
1848 assert_eq!(ions.len(), 1);
1849 let back = ions[0].inv_mobility(&env);
1852 assert!((back - one_over_k0).abs() < 1e-9, "1/K0 round-trip: {back}");
1853 assert_eq!(ions[0].condition_id, None);
1854
1855 let _ = std::fs::remove_file(&path);
1856 }
1857
1858 #[test]
1862 fn source_aware_reads_both_paths() {
1863 use crate::sim::containers::MobilityEnv;
1864 use crate::sim::projector::{DistributionSource, ProjectionMode, ProjectionParams};
1865 let path = std::env::temp_dir().join(format!("rustdf_p4a0_{}.db", std::process::id()));
1866 let _ = std::fs::remove_file(&path);
1867 {
1868 let conn = Connection::open(&path).unwrap();
1869 conn.execute_batch(
1870 "CREATE TABLE frames (frame_id INTEGER, time REAL, ms_type INTEGER);
1871 CREATE TABLE scans (scan INTEGER, mobility REAL);
1872 CREATE TABLE peptides (
1873 protein_id INTEGER, peptide_id INTEGER, sequence TEXT, protein TEXT,
1874 decoy INTEGER, missed_cleavages INTEGER, n_term, c_term,
1875 \"monoisotopic-mass\" REAL, retention_time_gru_predictor REAL, rt_mu REAL,
1876 rt_sigma REAL, rt_lambda REAL, events REAL, frame_occurrence TEXT,
1877 frame_abundance TEXT, frame_occurrence_start INTEGER, frame_occurrence_end INTEGER);
1878 CREATE TABLE ions (
1879 ion_id INTEGER, peptide_id INTEGER, sequence TEXT, charge INTEGER,
1880 relative_abundance REAL, mz REAL, inv_mobility_gru_predictor REAL,
1881 inv_mobility_gru_predictor_std REAL, simulated_spectrum TEXT,
1882 scan_occurrence TEXT, scan_abundance TEXT);",
1883 )
1884 .unwrap();
1885 for fid in 1..=40 {
1887 conn.execute(
1888 "INSERT INTO frames VALUES (?1, ?2, 0)",
1889 rusqlite::params![fid, fid as f64 * 0.1],
1890 )
1891 .unwrap();
1892 }
1893 for s in 0..20 {
1894 conn.execute(
1895 "INSERT INTO scans VALUES (?1, ?2)",
1896 rusqlite::params![s, 1.3 - s as f64 * 0.01],
1897 )
1898 .unwrap();
1899 }
1900 conn.execute(
1901 "INSERT INTO peptides VALUES (0,1,'PEPTIDEK','P',0,0,NULL,NULL,930.5,2.0,2.0,0.3,0.6,5.0,'[1]','[0.0]',1,1)",
1902 [],
1903 ).unwrap();
1904 conn.execute(
1905 "INSERT INTO ions VALUES (1,1,'PEPTIDEK',2,1.0,500.0,1.1,0.02,'{\"mz\":[500.0],\"intensity\":[1.0]}','[5]','[1.0]')",
1906 [],
1907 ).unwrap();
1908 }
1909 let handle = TimsTofSyntheticsDataHandle::new(&path).unwrap();
1910
1911 let pc = handle.read_peptides_with_source(&DistributionSource::Columns).unwrap();
1913 let ic = handle.read_ions_with_source(&DistributionSource::Columns).unwrap();
1914 assert_eq!(pc.len(), 1);
1915 assert_eq!(ic.len(), 1);
1916 assert_eq!(pc[0].frame_distribution.occurrence, vec![1]); let src = DistributionSource::Projector {
1920 mode: ProjectionMode::LegacyCompat,
1921 env: MobilityEnv::default(),
1922 params: ProjectionParams::default(),
1923 };
1924 let pp = handle.read_peptides_with_source(&src).unwrap();
1925 let ip = handle.read_ions_with_source(&src).unwrap();
1926 assert_eq!(pp.len(), 1);
1927 assert_eq!(ip.len(), 1);
1928 assert!(
1929 pp[0].frame_distribution.occurrence.len() > 1,
1930 "projector should populate multiple frames for a mid-gradient peptide"
1931 );
1932 assert!(!ip[0].scan_distribution.occurrence.is_empty(), "projector should populate scans");
1933 let src_acc = DistributionSource::Projector {
1935 mode: ProjectionMode::Accurate,
1936 env: MobilityEnv::default(),
1937 params: ProjectionParams::default(),
1938 };
1939 assert_eq!(handle.read_peptides_with_source(&src_acc).unwrap().len(), 1);
1940 assert_eq!(handle.read_ions_with_source(&src_acc).unwrap().len(), 1);
1941
1942 let _ = std::fs::remove_file(&path);
1943 }
1944}