use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use log::warn;
use num_traits::Float;
use super::spectrum_types::{CentroidPeakAdapting, DeconvolutedPeakAdapting, SpectrumLike};
use crate::io::traits::SpectrumSource;
use crate::meta::DissociationMethodTerm;
use crate::params::{
AccessionIntCode, ControlledVocabulary, Param, ParamDescribed, ParamLike, ParamValue, Unit,
CURIE,
};
use crate::{curie, impl_param_described, ParamList};
#[derive(Debug, Clone, Copy, Default)]
#[repr(i8)]
pub enum IsolationWindowState {
#[default]
Unknown = 0,
Offset,
Explicit,
Complete,
}
impl Display for IsolationWindowState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IsolationWindow {
pub target: f32,
pub lower_bound: f32,
pub upper_bound: f32,
#[cfg_attr(feature = "serde", serde(skip))]
pub flags: IsolationWindowState,
}
impl IsolationWindow {
pub fn new(
target: f32,
lower_bound: f32,
upper_bound: f32,
flags: IsolationWindowState,
) -> Self {
Self {
target,
lower_bound,
upper_bound,
flags,
}
}
pub fn around(target: f32, width: f32) -> Self {
let lower_bound = target - width;
let upper_bound = target + width;
Self::new(
target,
lower_bound,
upper_bound,
IsolationWindowState::Complete,
)
}
pub fn contains<F: Float>(&self, point: F) -> bool {
let point = point.to_f32().unwrap();
self.lower_bound <= point && self.upper_bound <= point
}
pub fn is_empty(&self) -> bool {
self.lower_bound == 0.0 && self.upper_bound == 0.0
}
}
impl PartialEq for IsolationWindow {
fn eq(&self, other: &Self) -> bool {
self.target == other.target
&& self.lower_bound == other.lower_bound
&& self.upper_bound == other.upper_bound
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanWindow {
pub lower_bound: f32,
pub upper_bound: f32,
}
impl ScanWindow {
pub fn new(lower_bound: f32, upper_bound: f32) -> Self {
Self {
lower_bound,
upper_bound,
}
}
pub fn contains<F: Float>(&self, point: F) -> bool {
let point = point.to_f32().unwrap();
self.lower_bound <= point && self.upper_bound <= point
}
pub fn is_empty(&self) -> bool {
self.lower_bound == 0.0 && self.upper_bound == 0.0
}
}
type ScanWindowList = Vec<ScanWindow>;
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanEvent {
pub start_time: f64,
pub injection_time: f32,
pub scan_windows: ScanWindowList,
pub instrument_configuration_id: u32,
pub params: Option<Box<ParamList>>,
pub spectrum_reference: Option<Box<str>>,
}
pub(crate) const ION_MOBILITY_SCAN_TERMS: [CURIE; 4] = [
curie!(MS:1002476),
curie!(MS:1002815),
curie!(MS:1001581),
curie!(MS:1003371),
];
pub trait IonMobilityMeasure: ParamDescribed {
fn ion_mobility(&'_ self) -> Option<f64> {
for u in ION_MOBILITY_SCAN_TERMS {
if let Some(v) = self.get_param_by_curie(&u).map(|p| p.value()) {
return v.to_f64().map(Some).unwrap_or_else(|e| {
warn!("Failed to parse ion mobility {u} value {v}: {e}");
None
});
}
}
None
}
fn has_ion_mobility(&self) -> bool {
self.ion_mobility().is_some()
}
fn ion_mobility_type(&self) -> Option<&Param> {
for u in ION_MOBILITY_SCAN_TERMS {
if let Some(v) = self.get_param_by_curie(&u) {
return Some(v);
}
}
None
}
}
pub(crate) const PRESET_SCAN_CONFIGURATION: CURIE = curie!(MS:1000616);
pub(crate) const MASS_RESOLUTION: CURIE = curie!(MS:1000011);
pub(crate) const FILTER_STRING: CURIE = curie!(MS:1000512);
pub(crate) const SCAN_TITLE: CURIE = curie!(MS:1000499);
impl ScanEvent {
pub fn new(
start_time: f64,
injection_time: f32,
scan_windows: ScanWindowList,
instrument_configuration_id: u32,
params: Option<Box<ParamList>>,
) -> Self {
Self {
start_time,
injection_time,
scan_windows,
instrument_configuration_id,
params,
spectrum_reference: None,
}
}
crate::find_param_method!(
filter_string,
&FILTER_STRING,
|p| { p.as_str() },
Option<Cow<'_, str>>
);
crate::find_param_method!(resolution, &MASS_RESOLUTION);
crate::find_param_method!(scan_configuration, &PRESET_SCAN_CONFIGURATION);
}
impl IonMobilityMeasure for ScanEvent {}
type ScanEventList = Vec<ScanEvent>;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScanCombination {
#[default]
NoCombination,
Sum,
Median,
}
impl Display for ScanCombination {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl ScanCombination {
pub fn from_accession(
controlled_vocabulary: ControlledVocabulary,
accession: AccessionIntCode,
) -> Option<ScanCombination> {
match controlled_vocabulary {
ControlledVocabulary::MS => match accession {
1000795 => Some(Self::NoCombination),
1000571 => Some(Self::Sum),
1000573 => Some(Self::Median),
_ => None,
},
_ => None,
}
}
pub const fn name(&self) -> &str {
match self {
ScanCombination::NoCombination => "no combination",
ScanCombination::Sum => "sum of spectra",
ScanCombination::Median => "median of spectra",
}
}
pub const fn accession(&self) -> AccessionIntCode {
match self {
ScanCombination::NoCombination => 1000795,
ScanCombination::Sum => 1000571,
ScanCombination::Median => 1000573,
}
}
pub fn to_param(&self) -> Param {
Param {
name: self.name().to_string(),
value: Default::default(),
accession: Some(self.accession()),
controlled_vocabulary: Some(ControlledVocabulary::MS),
unit: Unit::Unknown,
}
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Acquisition {
pub scans: ScanEventList,
pub combination: ScanCombination,
pub params: Option<Box<ParamList>>,
}
impl Acquisition {
pub fn start_time(&self) -> f64 {
self.first_scan().map(|v| v.start_time).unwrap_or_default()
}
pub fn first_scan(&self) -> Option<&ScanEvent> {
self.scans.first()
}
pub fn first_scan_mut(&mut self) -> Option<&mut ScanEvent> {
if self.scans.is_empty() {
self.scans.push(ScanEvent::default());
}
self.scans.first_mut()
}
pub fn last_scan(&self) -> Option<&ScanEvent> {
self.scans.last()
}
pub fn last_scan_mut(&mut self) -> Option<&mut ScanEvent> {
if self.scans.is_empty() {
self.scans.push(ScanEvent::default());
}
self.scans.last_mut()
}
pub fn instrument_configuration_ids(&self) -> Vec<u32> {
self.scans
.iter()
.map(|s| s.instrument_configuration_id)
.collect()
}
pub fn len(&self) -> usize {
self.scans.len()
}
pub fn is_empty(&self) -> bool {
self.scans.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, ScanEvent> {
self.scans.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, ScanEvent> {
self.scans.iter_mut()
}
}
pub trait IonProperties {
fn mz(&self) -> f64;
fn neutral_mass(&self) -> f64;
fn charge(&self) -> Option<i32>;
fn has_charge(&self) -> bool {
self.charge().is_some()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SelectedIon {
pub mz: f64,
pub intensity: f32,
pub charge: Option<i32>,
pub params: Option<Box<ParamList>>,
}
impl IonProperties for SelectedIon {
#[inline]
fn mz(&self) -> f64 {
self.mz
}
#[inline]
fn neutral_mass(&self) -> f64 {
crate::utils::neutral_mass(self.mz, self.charge.unwrap_or(1))
}
#[inline]
fn charge(&self) -> Option<i32> {
self.charge
}
}
impl IonMobilityMeasure for SelectedIon {}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Activation {
_methods: Vec<DissociationMethodTerm>,
pub energy: f32,
pub params: ParamList,
}
impl Activation {
pub fn method(&self) -> Option<&DissociationMethodTerm> {
self._methods.first()
}
pub fn method_mut(&mut self) -> Option<&mut DissociationMethodTerm> {
self._methods.first_mut()
}
pub fn methods(&self) -> &[DissociationMethodTerm] {
&self._methods
}
pub fn methods_mut(&mut self) -> &mut Vec<DissociationMethodTerm> {
&mut self._methods
}
pub fn is_combined(&self) -> bool {
self._methods.len() > 1
}
pub fn is_param_activation<P: ParamLike>(p: &P) -> bool {
if p.is_controlled() && p.controlled_vocabulary().unwrap() == ControlledVocabulary::MS {
Self::accession_to_activation(p.accession().unwrap())
} else {
false
}
}
pub fn accession_to_activation(accession: AccessionIntCode) -> bool {
DissociationMethodTerm::from_accession(accession).is_some()
}
pub fn _extract_methods_from_params(&mut self) {
let mut methods = Vec::with_capacity(1);
let mut rest = Vec::with_capacity(self.params.len());
for p in self.params.drain(..) {
if Self::is_param_activation(&p) {
methods.push(p.into())
} else {
rest.push(p)
}
}
self.params = rest;
self._methods = methods;
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Precursor {
pub ions: Vec<SelectedIon>,
pub isolation_window: IsolationWindow,
pub precursor_id: Option<String>,
pub product_id: Option<String>,
pub activation: Activation,
}
impl Precursor {
pub fn precursor_spectrum<C, D, S, R>(&self, source: &mut R) -> Option<S>
where
C: CentroidPeakAdapting,
D: DeconvolutedPeakAdapting,
S: SpectrumLike<C, D>,
R: SpectrumSource<C, D, S>,
{
match self.precursor_id.as_ref() {
Some(id) => source.get_spectrum_by_id(id),
None => None,
}
}
pub fn has_ions(&self) -> bool {
self.ions.is_empty()
}
pub fn product_spectrum<C, D, S, R>(&self, source: &mut R) -> Option<S>
where
C: CentroidPeakAdapting,
D: DeconvolutedPeakAdapting,
S: SpectrumLike<C, D>,
R: SpectrumSource<C, D, S>,
{
match self.product_id.as_ref() {
Some(id) => source.get_spectrum_by_id(id),
None => None,
}
}
}
pub trait PrecursorSelection {
fn ion(&self) -> Option<&SelectedIon>;
fn isolation_window(&self) -> &IsolationWindow;
fn precursor_id(&self) -> Option<&String>;
fn product_id(&self) -> Option<&String>;
fn activation(&self) -> &Activation;
fn iter(&self) -> impl Iterator<Item = &SelectedIon>;
fn has_ions(&self) -> bool {
self.iter().count() > 0
}
fn last_ion(&self) -> Option<&SelectedIon> {
self.iter().last()
}
fn iter_mut(&mut self) -> impl Iterator<Item = &mut SelectedIon>;
fn ion_mut(&mut self) -> Option<&mut SelectedIon>;
fn activation_mut(&mut self) -> &mut Activation;
fn isolation_window_mut(&mut self) -> &mut IsolationWindow;
fn add_ion(&mut self, ion: SelectedIon);
fn last_ion_mut(&mut self) -> Option<&mut SelectedIon> {
self.iter_mut().last()
}
}
impl PrecursorSelection for Precursor {
fn ion(&self) -> Option<&SelectedIon> {
self.ions.first()
}
fn isolation_window(&self) -> &IsolationWindow {
&self.isolation_window
}
fn precursor_id(&self) -> Option<&String> {
self.precursor_id.as_ref()
}
fn product_id(&self) -> Option<&String> {
self.product_id.as_ref()
}
fn activation(&self) -> &Activation {
&self.activation
}
fn ion_mut(&mut self) -> Option<&mut SelectedIon> {
if self.ions.is_empty() {
self.ions.push(SelectedIon::default())
}
self.ions.first_mut()
}
fn activation_mut(&mut self) -> &mut Activation {
&mut self.activation
}
fn isolation_window_mut(&mut self) -> &mut IsolationWindow {
&mut self.isolation_window
}
fn iter(&self) -> impl Iterator<Item = &SelectedIon> {
self.ions.iter()
}
fn iter_mut(&mut self) -> impl Iterator<Item = &mut SelectedIon> {
self.ions.iter_mut()
}
fn add_ion(&mut self, ion: SelectedIon) {
self.ions.push(ion);
}
fn last_ion_mut(&mut self) -> Option<&mut SelectedIon> {
if self.ions.is_empty() {
self.ions.push(SelectedIon::default())
}
self.iter_mut().last()
}
}
#[repr(i8)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScanPolarity {
#[default]
Unknown = 0,
Positive = 1,
Negative = -1,
}
impl ScanPolarity {
pub fn sign(&self) -> i32 {
match self {
ScanPolarity::Unknown => 1,
ScanPolarity::Positive => 1,
ScanPolarity::Negative => -1,
}
}
}
impl Display for ScanPolarity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Hash, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SignalContinuity {
#[default]
Unknown = 0,
Centroid = 3,
Profile = 5,
}
impl SignalContinuity {
pub const fn is_profile(&self) -> bool {
matches!(self, Self::Profile)
}
pub const fn is_centroid(&self) -> bool {
matches!(self, Self::Centroid)
}
}
impl Display for SignalContinuity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug)]
pub enum AsPrecursorCollection {
Single(Option<Precursor>),
Multiple(Vec<Precursor>)
}
impl From<AsPrecursorCollection> for Vec<Precursor> {
fn from(value: AsPrecursorCollection) -> Self {
match value {
AsPrecursorCollection::Single(precursor) => precursor.map(|v| vec![v]).unwrap_or_default(),
AsPrecursorCollection::Multiple(precursors) => precursors,
}
}
}
impl From<Precursor> for AsPrecursorCollection {
fn from(value: Precursor) -> Self {
Self::Single(Some(value))
}
}
impl From<Option<Precursor>> for AsPrecursorCollection {
fn from(value: Option<Precursor>) -> Self {
Self::Single(value)
}
}
impl From<Vec<Precursor>> for AsPrecursorCollection {
fn from(value: Vec<Precursor>) -> Self {
Self::Multiple(value)
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SpectrumDescription {
pub id: String,
pub index: usize,
pub ms_level: u8,
pub polarity: ScanPolarity,
pub signal_continuity: SignalContinuity,
pub params: ParamList,
pub acquisition: Acquisition,
pub precursor: Vec<Precursor>,
}
impl SpectrumDescription {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
index: usize,
ms_level: u8,
polarity: ScanPolarity,
signal_continuity: SignalContinuity,
params: ParamList,
acquisition: Acquisition,
precursor: impl Into<AsPrecursorCollection>,
) -> Self {
Self {
id,
index,
ms_level,
polarity,
signal_continuity,
params,
acquisition,
precursor: precursor.into().into(),
}
}
crate::find_param_method!(title, &SCAN_TITLE, |p| p.as_str(), Option<Cow<'_, str>>);
pub fn spectrum_type(&self) -> Option<crate::meta::SpectrumType> {
const SPECTRUM_TYPES: &[(crate::meta::SpectrumType, crate::params::ParamCow<'static>)] = crate::meta::SpectrumType::all_types();
let conv_table: HashMap<CURIE, crate::meta::SpectrumType> = SPECTRUM_TYPES
.iter()
.map(|(t, v)| (v.curie().unwrap(), *t))
.collect();
for param in self.params().iter() {
if let Some(c) = param.curie() {
if let Some(t) = conv_table.get(&c) {
return Some(*t);
}
}
}
None
}
pub fn set_spectrum_type(&mut self, spectrum_type: crate::meta::SpectrumType) {
const SPECTRUM_TYPES: &[(crate::meta::SpectrumType, crate::params::ParamCow<'static>)] = crate::meta::SpectrumType::all_types();
let to_insert: crate::params::ParamCow<'_> = spectrum_type.to_param();
let conv_table: HashSet<CURIE> = SPECTRUM_TYPES
.iter()
.map(|(_, v)| v.curie().unwrap())
.collect();
for param in self.params_mut().iter_mut() {
if let Some(c) = param.curie() {
if conv_table.contains(&c) {
*param = to_insert.into();
return;
}
}
}
self.add_param(to_insert.into());
}
}
impl_param_described!(Activation, SpectrumDescription);
impl_param_described_deferred!(SelectedIon, Acquisition, ScanEvent);
#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ChromatogramType {
#[default]
Unknown,
TotalIonCurrentChromatogram,
BasePeakChromatogram,
SelectedIonCurrentChromatogram,
SelectedIonMonitoringChromatogram,
SelectedReactionMonitoringChromatogram,
AbsorptionChromatogram,
EmissionChromatogram,
FlowRateChromatogram,
PressureChromatogram,
TemperatureChromatogram,
ElectromagneticRadiationChromatogram,
}
impl ChromatogramType {
pub fn from_accession(accession: AccessionIntCode) -> Option<Self> {
let tp = match accession {
1000235 => Self::TotalIonCurrentChromatogram,
1000628 => Self::BasePeakChromatogram,
1000627 => Self::SelectedIonCurrentChromatogram,
1000472 => Self::SelectedIonMonitoringChromatogram,
1000473 => Self::SelectedReactionMonitoringChromatogram,
1000812 => Self::AbsorptionChromatogram,
1000813 => Self::EmissionChromatogram,
1003020 => Self::FlowRateChromatogram,
1003019 => Self::PressureChromatogram,
1000811 => Self::ElectromagneticRadiationChromatogram,
1000626 => Self::Unknown,
1002715 => Self::TemperatureChromatogram,
_ => return None,
};
Some(tp)
}
pub fn is_electromagnetic_radiation(&self) -> bool {
matches!(
self,
Self::AbsorptionChromatogram | Self::EmissionChromatogram | Self::ElectromagneticRadiationChromatogram
)
}
pub fn is_aggregate(&self) -> bool {
matches!(
self,
Self::TotalIonCurrentChromatogram
| Self::BasePeakChromatogram
| Self::PressureChromatogram
| Self::FlowRateChromatogram
)
}
pub fn is_ion_current(&self) -> bool {
matches!(
self,
Self::SelectedIonCurrentChromatogram
| Self::SelectedIonMonitoringChromatogram
| Self::SelectedReactionMonitoringChromatogram
| Self::TotalIonCurrentChromatogram
| Self::BasePeakChromatogram
)
}
pub fn to_curie(&self) -> CURIE {
match self {
Self::TotalIonCurrentChromatogram => CURIE::new(ControlledVocabulary::MS, 1000235),
Self::BasePeakChromatogram => CURIE::new(ControlledVocabulary::MS, 1000628),
Self::SelectedIonCurrentChromatogram => CURIE::new(ControlledVocabulary::MS, 1000627),
Self::SelectedIonMonitoringChromatogram => {
CURIE::new(ControlledVocabulary::MS, 1000472)
}
Self::SelectedReactionMonitoringChromatogram => {
CURIE::new(ControlledVocabulary::MS, 1000473)
}
Self::AbsorptionChromatogram => CURIE::new(ControlledVocabulary::MS, 1000812),
Self::ElectromagneticRadiationChromatogram => CURIE::new(ControlledVocabulary::MS, 1000811),
Self::EmissionChromatogram => CURIE::new(ControlledVocabulary::MS, 1000813),
Self::FlowRateChromatogram => CURIE::new(ControlledVocabulary::MS, 1003020),
Self::PressureChromatogram => CURIE::new(ControlledVocabulary::MS, 1003019),
Self::TemperatureChromatogram => CURIE::new(ControlledVocabulary::MS, 1002715),
Self::Unknown => CURIE::new(ControlledVocabulary::MS, 1000626),
}
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChromatogramDescription {
pub id: String,
pub index: usize,
pub ms_level: Option<u8>,
pub polarity: ScanPolarity,
pub chromatogram_type: ChromatogramType,
pub params: ParamList,
pub precursor: Vec<Precursor>,
}
impl ChromatogramDescription {
pub fn is_aggregate(&self) -> bool {
self.chromatogram_type.is_aggregate()
}
pub fn is_electromagnetic_radiation(&self) -> bool {
self.chromatogram_type.is_electromagnetic_radiation()
}
pub fn is_ion_current(&self) -> bool {
self.chromatogram_type.is_ion_current()
}
}
impl_param_described!(ChromatogramDescription);