1use std::borrow::Cow;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Display;
4
5use log::warn;
6use num_traits::Float;
7
8use crate::params::{
11 AccessionIntCode, CURIE, ControlledVocabulary, Param, ParamDescribed, ParamLike, ParamValue,
12 Unit,
13};
14use mzdata_meta::DissociationMethodTerm;
15use mzdata_param::{
16 ParamList, curie, find_param_method, impl_param_described, impl_param_described_deferred,
17};
18
19#[derive(Debug, Clone, Copy, Default)]
23#[repr(i8)]
24pub enum IsolationWindowState {
25 #[default]
27 Unknown = 0,
28 Offset,
31 Explicit,
34 Complete,
37 NoIsolation,
40}
41
42impl Display for IsolationWindowState {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{:?}", self)
45 }
46}
47
48#[derive(Debug)]
49pub struct IsolationWindowBuilder<'a>(pub &'a mut IsolationWindow);
71
72impl<'a> IsolationWindowBuilder<'a> {
73
74 pub const fn flags(&self) -> IsolationWindowState {
76 self.0.flags
77 }
78
79 pub const fn target(self, value: f32) -> Self {
81 self.0.target = value;
82 self.0.flags = match self.0.flags {
83 IsolationWindowState::Unknown => IsolationWindowState::Complete,
84 IsolationWindowState::Explicit => IsolationWindowState::Complete,
85 IsolationWindowState::Offset => {
86 if self.0.lower_bound != 0.0 {
87 self.0.lower_bound = self.0.target - self.0.lower_bound;
88 }
89 if self.0.upper_bound != 0.0 {
90 self.0.upper_bound += self.0.target;
91 }
92 IsolationWindowState::Complete
93 }
94 IsolationWindowState::Complete => IsolationWindowState::Complete,
95 IsolationWindowState::NoIsolation => IsolationWindowState::NoIsolation,
96 };
97 self
98 }
99
100 pub const fn lower_offset(self, lower_bound: f32) -> Self {
105 match self.0.flags {
106 IsolationWindowState::Unknown => {
107 self.0.flags = IsolationWindowState::Offset;
108 self.0.lower_bound = lower_bound;
109 }
110 IsolationWindowState::Offset => {
112 self.0.lower_bound = lower_bound;
113 }
114 IsolationWindowState::Complete => {
115 self.0.lower_bound = self.0.target - lower_bound;
116 }
117 IsolationWindowState::Explicit => {
118 if self.0.target == 0.0 {
119 panic!("Mixed relative and absolute isolation window bounds could not be resolvd. Explicit and relative bounds specified before target");
120 } else {
121 self.0.lower_bound = self.0.target - lower_bound;
122 }
123 }
124 _ => {}
125 }
126 self
127 }
128
129 pub const fn upper_offset(self, upper_bound: f32) -> Self {
134 match self.0.flags {
135 IsolationWindowState::Unknown => {
136 self.0.flags = IsolationWindowState::Offset;
137 self.0.upper_bound = upper_bound;
138 }
139 IsolationWindowState::Offset => {
140 self.0.upper_bound = upper_bound;
141 }
142 IsolationWindowState::Complete => {
143 self.0.upper_bound = self.0.target + upper_bound;
144 }
145 IsolationWindowState::Explicit => {
146 if self.0.target == 0.0 {
147 panic!("Mixed relative and absolute isolation window bounds could not be resolvd. Explicit and relative bounds specified before target");
148 } else {
149 self.0.upper_bound = self.0.target + upper_bound;
150 }
151 }
152 _ => {}
153 }
154 self
155 }
156
157 pub const fn lower_limit(self, lower_bound: f32) -> Self {
159 match self.0.flags {
160 IsolationWindowState::Unknown | IsolationWindowState::Explicit | IsolationWindowState::Complete | IsolationWindowState::Offset => {
161 self.0.flags = IsolationWindowState::Explicit;
162 self.0.lower_bound = lower_bound;
163 if self.0.upper_bound != 0.0 && self.0.target != 0.0 {
164 self.0.flags = IsolationWindowState::Complete
165 }
166 },
167 IsolationWindowState::NoIsolation => {},
168 }
169 self
170 }
171
172 pub const fn upper_limit(self, upper_bound: f32) -> Self {
174 if matches!(
175 self.0.flags,
176 IsolationWindowState::Unknown
177 | IsolationWindowState::Explicit
178 | IsolationWindowState::Offset
179 | IsolationWindowState::Complete
180 ) {
181 self.0.flags = IsolationWindowState::Explicit;
182 self.0.upper_bound = upper_bound;
183 if self.0.lower_bound != 0.0 && self.0.target != 0.0 {
184 self.0.flags = IsolationWindowState::Complete
185 }
186 }
187 self
188 }
189
190 pub const fn no_isolation(self) -> Self {
192 self.0.flags = IsolationWindowState::NoIsolation;
193 self
194 }
195
196 pub const fn is_complete(&self) -> bool {
198 matches!(
199 self.0.flags,
200 IsolationWindowState::Complete | IsolationWindowState::NoIsolation
201 )
202 }
203
204 pub const fn unit(self, unit: Unit) -> Self {
206 if unit.is_unknown() {
207 return self
208 }
209 *self.0.unit_mut() = unit;
210 self
211 }
212}
213
214#[derive(Default, Debug, Clone)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216pub struct IsolationWindow {
223 pub target: f32,
225 pub lower_bound: f32,
227 pub upper_bound: f32,
229 #[cfg_attr(feature = "serde", serde(skip))]
232 pub flags: IsolationWindowState,
233 unit: Unit
234}
235
236impl IsolationWindow {
237 pub const fn new(
238 target: f32,
239 lower_bound: f32,
240 upper_bound: f32,
241 flags: IsolationWindowState,
242 ) -> Self {
243 Self {
244 target,
245 lower_bound,
246 upper_bound,
247 flags,
248 unit: Unit::Unknown
249 }
250 }
251
252 pub const fn unit(&self) -> Unit {
254 self.unit
255 }
256
257 pub const fn unit_mut(&mut self) -> &mut Unit {
259 &mut self.unit
260 }
261
262 pub const fn full() -> Self {
264 Self::new(0.0, 0.0, 0.0, IsolationWindowState::NoIsolation)
265 }
266
267 pub const fn has_isolation(&self) -> bool {
269 !matches!(self.flags, IsolationWindowState::NoIsolation)
270 }
271
272 pub const fn around(target: f32, radius: f32) -> Self {
275 let lower_bound = target - radius;
276 let upper_bound = target + radius;
277 Self::new(
278 target,
279 lower_bound,
280 upper_bound,
281 IsolationWindowState::Complete,
282 )
283 }
284
285 pub fn contains<F: Float>(&self, point: F) -> bool {
287 let point = point.to_f32().unwrap();
288 self.lower_bound <= point && point <= self.upper_bound
289 }
290
291 pub const fn is_empty(&self) -> bool {
295 self.lower_bound == 0.0 && self.upper_bound == 0.0 && self.has_isolation()
296 }
297}
298
299impl PartialEq for IsolationWindow {
300 fn eq(&self, other: &Self) -> bool {
301 self.target == other.target
302 && self.lower_bound == other.lower_bound
303 && self.upper_bound == other.upper_bound
304 }
305}
306
307#[derive(Default, Debug, Clone, PartialEq)]
309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
310pub struct ScanWindow {
311 pub lower_bound: f32,
313 pub upper_bound: f32,
315}
316
317impl ScanWindow {
318 pub const fn new(lower_bound: f32, upper_bound: f32) -> Self {
319 Self {
320 lower_bound,
321 upper_bound,
322 }
323 }
324
325 pub fn contains<F: Float>(&self, point: F) -> bool {
327 let point = point.to_f32().unwrap();
328 self.lower_bound <= point && point <= self.upper_bound
329 }
330
331 pub fn is_empty(&self) -> bool {
333 self.lower_bound == 0.0 && self.upper_bound == 0.0
334 }
335}
336
337type ScanWindowList = Vec<ScanWindow>;
338
339#[derive(Default, Debug, Clone, PartialEq)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341pub struct ScanEvent {
344 pub start_time: f64,
346 pub injection_time: f32,
348 pub scan_windows: ScanWindowList,
350 pub instrument_configuration_id: u32,
353 pub params: Option<Box<ParamList>>,
355 pub spectrum_reference: Option<Box<str>>,
357}
358
359pub(crate) const ION_MOBILITY_SCAN_TERMS: [CURIE; 4] = [
360 curie!(MS:1002476),
362 curie!(MS:1002815),
364 curie!(MS:1001581),
366 curie!(MS:1003371),
368];
369
370pub trait IonMobilityMeasure: ParamDescribed {
379
380 fn ion_mobility(&'_ self) -> Option<f64> {
382 for u in ION_MOBILITY_SCAN_TERMS {
383 if let Some(v) = self.get_param_by_curie(&u).map(|p| p.value()) {
384 return v.to_f64().map(Some).unwrap_or_else(|e| {
385 warn!("Failed to parse ion mobility {u} value {v}: {e}");
386 None
387 });
388 }
389 }
390 None
391 }
392
393 fn has_ion_mobility(&self) -> bool {
395 self.ion_mobility().is_some()
396 }
397
398 fn ion_mobility_type(&self) -> Option<&Param> {
401 for u in ION_MOBILITY_SCAN_TERMS {
402 if let Some(v) = self.get_param_by_curie(&u) {
403 return Some(v);
404 }
405 }
406 None
407 }
408}
409
410pub(crate) const PRESET_SCAN_CONFIGURATION: CURIE = curie!(MS:1000616);
411pub(crate) const MASS_RESOLUTION: CURIE = curie!(MS:1000011);
412pub(crate) const FILTER_STRING: CURIE = curie!(MS:1000512);
413pub(crate) const SCAN_TITLE: CURIE = curie!(MS:1000499);
414
415impl ScanEvent {
416 pub fn new(
417 start_time: f64,
418 injection_time: f32,
419 scan_windows: ScanWindowList,
420 instrument_configuration_id: u32,
421 params: Option<Box<ParamList>>,
422 ) -> Self {
423 Self {
424 start_time,
425 injection_time,
426 scan_windows,
427 instrument_configuration_id,
428 params,
429 spectrum_reference: None,
430 }
431 }
432
433 find_param_method!(
434 filter_string,
435 &FILTER_STRING,
436 |p| { p.as_str() },
437 Option<Cow<'_, str>>
438 );
439 find_param_method!(resolution, &MASS_RESOLUTION);
440 find_param_method!(scan_configuration, &PRESET_SCAN_CONFIGURATION);
441}
442
443impl IonMobilityMeasure for ScanEvent {}
444
445type ScanEventList = Vec<ScanEvent>;
446
447#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
450#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
451pub enum ScanCombination {
452 #[default]
454 NoCombination,
455 Sum,
457 Median,
459}
460
461impl Display for ScanCombination {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 write!(f, "{:?}", self)
464 }
465}
466
467impl ScanCombination {
468 pub fn from_accession(
469 controlled_vocabulary: ControlledVocabulary,
470 accession: AccessionIntCode,
471 ) -> Option<ScanCombination> {
472 match controlled_vocabulary {
473 ControlledVocabulary::MS => match accession {
474 1000795 => Some(Self::NoCombination),
475 1000571 => Some(Self::Sum),
476 1000573 => Some(Self::Median),
477 _ => None,
478 },
479 _ => None,
480 }
481 }
482
483 pub const fn name(&self) -> &str {
484 match self {
485 ScanCombination::NoCombination => "no combination",
486 ScanCombination::Sum => "sum of spectra",
487 ScanCombination::Median => "median of spectra",
488 }
489 }
490
491 pub const fn accession(&self) -> AccessionIntCode {
492 match self {
493 ScanCombination::NoCombination => 1000795,
494 ScanCombination::Sum => 1000571,
495 ScanCombination::Median => 1000573,
496 }
497 }
498
499 pub fn to_param(&self) -> Param {
500 Param {
501 name: self.name().to_string(),
502 value: Default::default(),
503 accession: Some(self.accession()),
504 controlled_vocabulary: Some(ControlledVocabulary::MS),
505 unit: Unit::Unknown,
506 }
507 }
508}
509
510#[derive(Default, Debug, Clone, PartialEq)]
511#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
512pub struct Acquisition {
515 pub scans: ScanEventList,
517 pub combination: ScanCombination,
519 pub params: Option<Box<ParamList>>,
521}
522
523impl Acquisition {
524 pub fn start_time(&self) -> f64 {
526 self.first_scan().map(|v| v.start_time).unwrap_or_default()
527 }
528
529 pub fn first_scan(&self) -> Option<&ScanEvent> {
531 self.scans.first()
532 }
533
534 pub fn first_scan_mut(&mut self) -> Option<&mut ScanEvent> {
537 if self.scans.is_empty() {
538 self.scans.push(ScanEvent::default());
539 }
540 self.scans.first_mut()
541 }
542
543 pub fn last_scan(&self) -> Option<&ScanEvent> {
545 self.scans.last()
546 }
547
548 pub fn last_scan_mut(&mut self) -> Option<&mut ScanEvent> {
551 if self.scans.is_empty() {
552 self.scans.push(ScanEvent::default());
553 }
554 self.scans.last_mut()
555 }
556
557 pub fn instrument_configuration_ids(&self) -> Vec<u32> {
559 self.scans
560 .iter()
561 .map(|s| s.instrument_configuration_id)
562 .collect()
563 }
564
565 pub const fn len(&self) -> usize {
567 self.scans.len()
568 }
569
570 pub fn is_empty(&self) -> bool {
572 self.scans.is_empty()
573 }
574
575 pub fn iter(&self) -> std::slice::Iter<'_, ScanEvent> {
577 self.scans.iter()
578 }
579
580 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, ScanEvent> {
582 self.scans.iter_mut()
583 }
584}
585
586pub trait IonProperties {
588 fn mz(&self) -> f64;
590 fn neutral_mass(&self) -> f64;
592 fn charge(&self) -> Option<i32>;
594 fn has_charge(&self) -> bool {
595 self.charge().is_some()
596 }
597}
598
599#[derive(Debug, Clone, Default, PartialEq)]
600#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
601pub struct SelectedIon {
603 pub mz: f64,
605 pub intensity: f32,
607 pub charge: Option<i32>,
610 pub params: Option<Box<ParamList>>,
612}
613
614impl IonProperties for SelectedIon {
615 #[inline]
616 fn mz(&self) -> f64 {
617 self.mz
618 }
619
620 #[inline]
621 fn neutral_mass(&self) -> f64 {
622 mzdata_bindata::utils::neutral_mass(self.mz, self.charge.unwrap_or(1))
623 }
624
625 #[inline]
626 fn charge(&self) -> Option<i32> {
627 self.charge
628 }
629}
630
631impl IonMobilityMeasure for SelectedIon {}
632
633#[derive(Debug, Default, Clone, PartialEq)]
634#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
635pub struct Activation {
637 _methods: Vec<DissociationMethodTerm>,
638 pub energy: f32,
640 pub params: ParamList,
642}
643
644impl Activation {
645 pub fn method(&self) -> Option<&DissociationMethodTerm> {
647 self._methods.first()
648 }
649
650 pub fn method_mut(&mut self) -> Option<&mut DissociationMethodTerm> {
652 self._methods.first_mut()
653 }
654
655 pub fn methods(&self) -> &[DissociationMethodTerm] {
657 &self._methods
658 }
659
660 pub fn methods_mut(&mut self) -> &mut Vec<DissociationMethodTerm> {
663 &mut self._methods
664 }
665
666 pub fn is_combined(&self) -> bool {
668 self._methods.len() > 1
669 }
670
671 pub fn is_param_activation<P: ParamLike>(p: &P) -> bool {
673 if p.is_controlled() && p.controlled_vocabulary().unwrap() == ControlledVocabulary::MS {
674 Self::accession_to_activation(p.accession().unwrap())
675 } else {
676 false
677 }
678 }
679
680 pub fn accession_to_activation(accession: AccessionIntCode) -> bool {
683 DissociationMethodTerm::from_accession(accession).is_some()
684 }
685
686 pub fn _extract_methods_from_params(&mut self) {
687 let mut methods = Vec::with_capacity(1);
688 let mut rest = Vec::with_capacity(self.params.len());
689 for p in self.params.drain(..) {
690 if Self::is_param_activation(&p) {
691 methods.push(p.into())
692 } else {
693 rest.push(p)
694 }
695 }
696 self.params = rest;
697 self._methods = methods;
698 }
699
700 pub fn energy(&self) -> f32 {
702 self.energy
703 }
704}
705
706#[derive(Debug, Default, Clone, PartialEq)]
707#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
708pub struct Precursor {
710 pub ions: Vec<SelectedIon>,
712 pub isolation_window: IsolationWindow,
714 pub precursor_id: Option<String>,
716 pub product_id: Option<String>,
718 pub activation: Activation,
720}
721
722impl Precursor {
723 pub fn has_ions(&self) -> bool {
742 self.ions.is_empty()
743 }
744
745 }
761
762pub trait PrecursorSelection {
766 fn ion(&self) -> Option<&SelectedIon>;
768 fn isolation_window(&self) -> &IsolationWindow;
770 fn precursor_id(&self) -> Option<&String>;
772 fn product_id(&self) -> Option<&String>;
774 fn activation(&self) -> &Activation;
776
777 fn iter(&self) -> impl Iterator<Item = &SelectedIon>;
778
779 fn has_ions(&self) -> bool {
781 self.iter().count() > 0
782 }
783
784 fn last_ion(&self) -> Option<&SelectedIon> {
785 self.iter().last()
786 }
787
788 fn iter_mut(&mut self) -> impl Iterator<Item = &mut SelectedIon>;
789 fn ion_mut(&mut self) -> Option<&mut SelectedIon>;
790 fn activation_mut(&mut self) -> &mut Activation;
791 fn isolation_window_mut(&mut self) -> &mut IsolationWindow;
792 fn add_ion(&mut self, ion: SelectedIon);
793 fn last_ion_mut(&mut self) -> Option<&mut SelectedIon> {
794 self.iter_mut().last()
795 }
796}
797
798impl PrecursorSelection for Precursor {
799 fn ion(&self) -> Option<&SelectedIon> {
800 self.ions.first()
801 }
802
803 fn isolation_window(&self) -> &IsolationWindow {
804 &self.isolation_window
805 }
806
807 fn precursor_id(&self) -> Option<&String> {
808 self.precursor_id.as_ref()
809 }
810
811 fn product_id(&self) -> Option<&String> {
812 self.product_id.as_ref()
813 }
814
815 fn activation(&self) -> &Activation {
816 &self.activation
817 }
818
819 fn ion_mut(&mut self) -> Option<&mut SelectedIon> {
820 if self.ions.is_empty() {
821 self.ions.push(SelectedIon::default())
822 }
823 self.ions.first_mut()
824 }
825
826 fn activation_mut(&mut self) -> &mut Activation {
827 &mut self.activation
828 }
829
830 fn isolation_window_mut(&mut self) -> &mut IsolationWindow {
831 &mut self.isolation_window
832 }
833
834 fn iter(&self) -> impl Iterator<Item = &SelectedIon> {
835 self.ions.iter()
836 }
837
838 fn iter_mut(&mut self) -> impl Iterator<Item = &mut SelectedIon> {
839 self.ions.iter_mut()
840 }
841
842 fn add_ion(&mut self, ion: SelectedIon) {
843 self.ions.push(ion);
844 }
845
846 fn last_ion_mut(&mut self) -> Option<&mut SelectedIon> {
847 if self.ions.is_empty() {
848 self.ions.push(SelectedIon::default())
849 }
850 self.iter_mut().last()
851 }
852}
853
854#[repr(i8)]
859#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Default)]
860#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
861pub enum ScanPolarity {
862 #[default]
863 Unknown = 0,
865 Positive = 1,
867 Negative = -1,
870}
871
872impl ScanPolarity {
873 pub fn sign(&self) -> i32 {
878 match self {
879 ScanPolarity::Unknown => 1,
880 ScanPolarity::Positive => 1,
881 ScanPolarity::Negative => -1,
882 }
883 }
884}
885
886impl Display for ScanPolarity {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 write!(f, "{:?}", self)
889 }
890}
891
892#[repr(u8)]
899#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Hash, Eq)]
900#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
901pub enum SignalContinuity {
902 #[default]
903 Unknown = 0,
904 Centroid = 3,
908 Profile = 5,
911}
912
913impl SignalContinuity {
914 pub const fn is_profile(&self) -> bool {
915 matches!(self, Self::Profile)
916 }
917
918 pub const fn is_centroid(&self) -> bool {
919 matches!(self, Self::Centroid)
920 }
921}
922
923impl Display for SignalContinuity {
924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925 write!(f, "{:?}", self)
926 }
927}
928
929#[derive(Debug)]
932pub enum AsPrecursorCollection {
933 Single(Option<Precursor>),
935 Multiple(Vec<Precursor>),
937}
938
939impl From<AsPrecursorCollection> for Vec<Precursor> {
940 fn from(value: AsPrecursorCollection) -> Self {
941 match value {
942 AsPrecursorCollection::Single(precursor) => {
943 precursor.map(|v| vec![v]).unwrap_or_default()
944 }
945 AsPrecursorCollection::Multiple(precursors) => precursors,
946 }
947 }
948}
949
950impl From<Precursor> for AsPrecursorCollection {
951 fn from(value: Precursor) -> Self {
952 Self::Single(Some(value))
953 }
954}
955
956impl From<Option<Precursor>> for AsPrecursorCollection {
957 fn from(value: Option<Precursor>) -> Self {
958 Self::Single(value)
959 }
960}
961
962impl From<Vec<Precursor>> for AsPrecursorCollection {
963 fn from(value: Vec<Precursor>) -> Self {
964 Self::Multiple(value)
965 }
966}
967
968#[derive(Debug, Default, Clone, PartialEq)]
974#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
975pub struct SpectrumDescription {
976 pub id: String,
978 pub index: usize,
980 pub ms_level: u8,
982
983 pub polarity: ScanPolarity,
985
986 pub signal_continuity: SignalContinuity,
989
990 pub params: ParamList,
993
994 pub acquisition: Acquisition,
996
997 pub precursor: Vec<Precursor>,
999}
1000
1001impl SpectrumDescription {
1002 #[allow(clippy::too_many_arguments)]
1003 pub fn new(
1004 id: String,
1005 index: usize,
1006 ms_level: u8,
1007 polarity: ScanPolarity,
1008 signal_continuity: SignalContinuity,
1009 params: ParamList,
1010 acquisition: Acquisition,
1011 precursor: impl Into<AsPrecursorCollection>,
1012 ) -> Self {
1013 Self {
1014 id,
1015 index,
1016 ms_level,
1017 polarity,
1018 signal_continuity,
1019 params,
1020 acquisition,
1021 precursor: precursor.into().into(),
1022 }
1023 }
1024
1025 find_param_method!(title, &SCAN_TITLE, |p| p.as_str(), Option<Cow<'_, str>>);
1026
1027 pub fn spectrum_type(&self) -> Option<mzdata_meta::SpectrumType> {
1033 const SPECTRUM_TYPES: &[(mzdata_meta::SpectrumType, crate::params::ParamCow<'static>)] =
1034 mzdata_meta::SpectrumType::all_types();
1035
1036 let conv_table: HashMap<CURIE, mzdata_meta::SpectrumType> = SPECTRUM_TYPES
1037 .iter()
1038 .map(|(t, v)| (v.curie().unwrap(), *t))
1039 .collect();
1040
1041 for param in self.params().iter() {
1042 if let Some(c) = param.curie() {
1043 if let Some(t) = conv_table.get(&c) {
1044 return Some(*t);
1045 }
1046 }
1047 }
1048 None
1049 }
1050
1051 pub fn set_spectrum_type(&mut self, spectrum_type: mzdata_meta::SpectrumType) {
1053 const SPECTRUM_TYPES: &[(mzdata_meta::SpectrumType, crate::params::ParamCow<'static>)] =
1054 mzdata_meta::SpectrumType::all_types();
1055
1056 let to_insert: crate::params::ParamCow<'_> = spectrum_type.to_param();
1057
1058 let conv_table: HashSet<CURIE> = SPECTRUM_TYPES
1059 .iter()
1060 .map(|(_, v)| v.curie().unwrap())
1061 .collect();
1062
1063 for param in self.params_mut().iter_mut() {
1064 if let Some(c) = param.curie() {
1065 if conv_table.contains(&c) {
1066 *param = to_insert.into();
1067 return;
1068 }
1069 }
1070 }
1071
1072 self.add_param(to_insert.into());
1073 }
1074}
1075
1076impl_param_described!(Activation, SpectrumDescription);
1077impl_param_described_deferred!(SelectedIon, Acquisition, ScanEvent);
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
1081#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1082pub enum ChromatogramType {
1083 #[default]
1084 Unknown,
1085 TotalIonCurrentChromatogram,
1087 BasePeakChromatogram,
1089 SelectedIonCurrentChromatogram,
1091 SelectedIonMonitoringChromatogram,
1093 SelectedReactionMonitoringChromatogram,
1095 AbsorptionChromatogram,
1096 EmissionChromatogram,
1097 FlowRateChromatogram,
1099 PressureChromatogram,
1101 TemperatureChromatogram,
1103 ElectromagneticRadiationChromatogram,
1105}
1106
1107impl ChromatogramType {
1108 pub fn from_accession(accession: AccessionIntCode) -> Option<Self> {
1110 let tp = match accession {
1111 1000235 => Self::TotalIonCurrentChromatogram,
1112 1000628 => Self::BasePeakChromatogram,
1113 1000627 => Self::SelectedIonCurrentChromatogram,
1114 1001472 => Self::SelectedIonMonitoringChromatogram,
1115 1001473 => Self::SelectedReactionMonitoringChromatogram,
1116 1000812 => Self::AbsorptionChromatogram,
1117 1000813 => Self::EmissionChromatogram,
1118 1003020 => Self::FlowRateChromatogram,
1119 1003019 => Self::PressureChromatogram,
1120 1000811 => Self::ElectromagneticRadiationChromatogram,
1121 1000626 => Self::Unknown,
1122 1002715 => Self::TemperatureChromatogram,
1123 _ => return None,
1124 };
1125 Some(tp)
1126 }
1127
1128 pub fn from_curie(curie: CURIE) -> Option<Self> {
1130 match curie.controlled_vocabulary() {
1131 ControlledVocabulary::MS => Self::from_accession(curie.accession_int()),
1132 _ => None,
1133 }
1134 }
1135
1136 pub fn is_electromagnetic_radiation(&self) -> bool {
1138 matches!(
1139 self,
1140 Self::AbsorptionChromatogram
1141 | Self::EmissionChromatogram
1142 | Self::ElectromagneticRadiationChromatogram
1143 )
1144 }
1145
1146 pub fn is_aggregate(&self) -> bool {
1149 matches!(
1150 self,
1151 Self::TotalIonCurrentChromatogram
1152 | Self::BasePeakChromatogram
1153 )
1154 }
1155
1156 pub fn is_ion_current(&self) -> bool {
1158 matches!(
1159 self,
1160 Self::SelectedIonCurrentChromatogram
1161 | Self::SelectedIonMonitoringChromatogram
1162 | Self::SelectedReactionMonitoringChromatogram
1163 | Self::TotalIonCurrentChromatogram
1164 | Self::BasePeakChromatogram
1165 )
1166 }
1167
1168 pub fn to_curie(&self) -> CURIE {
1170 match self {
1171 Self::TotalIonCurrentChromatogram => CURIE::new(ControlledVocabulary::MS, 1000235),
1172 Self::BasePeakChromatogram => CURIE::new(ControlledVocabulary::MS, 1000628),
1173 Self::SelectedIonCurrentChromatogram => CURIE::new(ControlledVocabulary::MS, 1000627),
1174 Self::SelectedIonMonitoringChromatogram => {
1175 CURIE::new(ControlledVocabulary::MS, 1000472)
1176 }
1177 Self::SelectedReactionMonitoringChromatogram => {
1178 CURIE::new(ControlledVocabulary::MS, 1000473)
1179 }
1180 Self::AbsorptionChromatogram => CURIE::new(ControlledVocabulary::MS, 1000812),
1181 Self::ElectromagneticRadiationChromatogram => {
1182 CURIE::new(ControlledVocabulary::MS, 1000811)
1183 }
1184 Self::EmissionChromatogram => CURIE::new(ControlledVocabulary::MS, 1000813),
1185 Self::FlowRateChromatogram => CURIE::new(ControlledVocabulary::MS, 1003020),
1186 Self::PressureChromatogram => CURIE::new(ControlledVocabulary::MS, 1003019),
1187 Self::TemperatureChromatogram => CURIE::new(ControlledVocabulary::MS, 1002715),
1188 Self::Unknown => CURIE::new(ControlledVocabulary::MS, 1000626),
1189 }
1190 }
1191}
1192
1193#[derive(Debug, Default, Clone, PartialEq)]
1196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1197pub struct Product {
1198 pub isolation_window: IsolationWindow,
1200}
1201
1202impl From<IsolationWindow> for Product {
1203 fn from(isolation_window: IsolationWindow) -> Self {
1204 Self { isolation_window }
1205 }
1206}
1207
1208impl Product {
1209 pub fn new(isolation_window: IsolationWindow) -> Self {
1210 Self { isolation_window }
1211 }
1212}
1213
1214#[derive(Debug, Default, Clone, PartialEq)]
1220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1221pub struct ChromatogramDescription {
1222 pub id: String,
1224 pub index: usize,
1226 pub ms_level: Option<u8>,
1228 pub polarity: ScanPolarity,
1230 pub chromatogram_type: ChromatogramType,
1232
1233 pub params: ParamList,
1235 pub precursor: Vec<Precursor>,
1237 pub products: Vec<Product>,
1239}
1240
1241impl ChromatogramDescription {
1242 pub fn is_aggregate(&self) -> bool {
1244 self.chromatogram_type.is_aggregate()
1245 }
1246
1247 pub fn is_electromagnetic_radiation(&self) -> bool {
1249 self.chromatogram_type.is_electromagnetic_radiation()
1250 }
1251
1252 pub fn is_ion_current(&self) -> bool {
1254 self.chromatogram_type.is_ion_current()
1255 }
1256}
1257
1258impl_param_described!(ChromatogramDescription);
1259
1260#[cfg(test)]
1261mod contains_tests {
1262 use num_traits::Zero;
1263
1264use super::*;
1265
1266 #[test]
1267 fn isolation_window_builder_target_only() {
1268 let mut this = IsolationWindow::default();
1269 let builder = IsolationWindowBuilder(&mut this);
1270 builder.target(500.0);
1271 assert_eq!(this.target, 500.0);
1272 assert!(this.upper_bound.is_zero());
1273 assert!(this.lower_bound.is_zero());
1274
1275 this = IsolationWindow::default();
1276 let builder = IsolationWindowBuilder(&mut this);
1277 builder.target(500.0).lower_offset(2.0);
1278 assert_eq!(this.target, 500.0);
1279 assert!(this.upper_bound.is_zero());
1280 assert_eq!(this.lower_bound, 498.0);
1281 }
1282
1283 #[test]
1288 fn isolation_window_contains_is_inclusive_range() {
1289 let window = IsolationWindow::around(810.789, 1.0); assert!(window.contains(window.target), "target must be inside");
1291 assert!(
1292 window.contains(window.lower_bound),
1293 "lower bound is inclusive"
1294 );
1295 assert!(
1296 window.contains(window.upper_bound),
1297 "upper bound is inclusive"
1298 );
1299 assert!(window.contains(810.0_f64), "interior point");
1300 assert!(!window.contains(809.0_f64), "below the lower bound");
1301 assert!(!window.contains(812.0_f64), "above the upper bound");
1302 }
1303
1304 #[test]
1305 fn scan_window_contains_is_inclusive_range() {
1306 let window = ScanWindow::new(200.0, 2000.0);
1307 assert!(window.contains(1100.0_f64), "interior point");
1308 assert!(window.contains(200.0_f64), "lower bound is inclusive");
1309 assert!(window.contains(2000.0_f64), "upper bound is inclusive");
1310 assert!(!window.contains(199.0_f64), "below the lower bound");
1311 assert!(
1312 !window.contains(2500.0_f64),
1313 "above the upper bound (the old bug returned true here)"
1314 );
1315 }
1316}