use std::{
fmt::Write,
hash::Hash,
num::NonZeroU16,
ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
};
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};
use thin_vec::ThinVec;
use crate::{
chemistry::{Element, MassMode, MassOutputType},
glycan::{GlycanPosition, MonoSaccharide},
sequence::{AminoAcid, CrossLinkName, SequencePosition},
space::{Space, UsedSpace},
system::{Mass, f64},
};
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Clone, Default, Deserialize, Hash, Ord, PartialOrd, Serialize)]
pub struct MolecularFormula {
pub(in super::super) elements: ThinVec<(Element, Option<NonZeroU16>, i32)>,
pub(in super::super) additional_mass: OrderedFloat<f64>,
#[serde(default)]
pub(in super::super) labels: ThinVec<AmbiguousLabel>,
}
impl PartialEq for MolecularFormula {
fn eq(&self, other: &Self) -> bool {
self.elements == other.elements && self.additional_mass == other.additional_mass
}
}
impl Eq for MolecularFormula {}
impl std::fmt::Debug for MolecularFormula {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use itertools::Itertools;
write!(
f,
"{} [{}]",
self.hill_notation(),
self.labels.iter().map(ToString::to_string).join(",")
)
}
}
impl std::fmt::Display for MolecularFormula {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.hill_notation())
}
}
impl Space for MolecularFormula {
fn space(&self) -> UsedSpace {
(self.elements.space() + self.additional_mass.space() + self.labels.space())
.set_total::<Self>()
}
}
impl MassOutputType for MolecularFormula {
fn labels(&self) -> &[AmbiguousLabel] {
&self.labels
}
fn with_label(mut self, label: AmbiguousLabel) -> Self {
self.labels.push(label);
self
}
fn with_labels(mut self, labels: &[AmbiguousLabel]) -> Self {
self.labels.extend_from_slice(labels);
self
}
fn with_global_isotope_modifications(
self,
substitutions: &[(Element, Option<NonZeroU16>)],
) -> Option<Self> {
if substitutions.is_empty() {
Some(self.clone())
} else if substitutions.iter().all(|e| e.0.is_valid(e.1)) {
let mut new_elements = self.elements.clone();
for item in &mut new_elements {
for (substitute_element, substitute_species) in substitutions {
if item.0 == *substitute_element {
item.1 = *substitute_species;
}
}
}
let result = Self {
elements: new_elements,
additional_mass: self.additional_mass,
labels: self.labels.clone(),
};
Some(result.simplify())
} else {
None
}
}
fn charge(&self) -> crate::system::isize::Charge {
-self
.elements
.iter()
.find(|el| el.0 == Element::Electron)
.map_or_else(crate::system::isize::Charge::default, |el| {
crate::system::isize::Charge::new::<crate::system::charge::e>(el.2 as isize)
})
}
fn contains_negative_amount(&self) -> bool {
self.elements().iter().any(|e| e.0 != Element::Electron && e.2 < 0)
}
fn as_formula(&self) -> MolecularFormula {
self.clone()
}
fn mass(&self, mode: MassMode) -> Mass {
match mode {
MassMode::Monoisotopic => self.monoisotopic_mass(),
MassMode::Average => self.average_weight(),
#[cfg(feature = "isotopes")]
MassMode::MostAbundant => self.most_abundant_mass(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum AmbiguousLabel {
AminoAcid {
option: AminoAcid,
sequence_index: usize,
peptidoform_index: usize,
peptidoform_ion_index: usize,
},
Modification {
id: usize,
sequence_index: SequencePosition,
peptidoform_index: usize,
peptidoform_ion_index: usize,
},
ChargeCarrier(MolecularFormula),
CrossLinkBound(CrossLinkName),
CrossLinkBroken(CrossLinkName, MolecularFormula),
GlycanFragment(Vec<GlycanPosition>),
GlycanFragmentComposition(Vec<(MonoSaccharide, isize)>),
}
impl Space for AmbiguousLabel {
fn space(&self) -> UsedSpace {
(UsedSpace::stack(1)
+ match self {
Self::AminoAcid {
option,
sequence_index,
peptidoform_index,
peptidoform_ion_index,
} => {
option.space()
+ sequence_index.space()
+ peptidoform_index.space()
+ peptidoform_ion_index.space()
}
Self::Modification {
id,
sequence_index,
peptidoform_index,
peptidoform_ion_index,
} => {
id.space()
+ sequence_index.space()
+ peptidoform_index.space()
+ peptidoform_ion_index.space()
}
Self::ChargeCarrier(f) => f.space(),
Self::CrossLinkBound(n) => n.space(),
Self::CrossLinkBroken(n, f) => n.space() + f.space(),
Self::GlycanFragment(f) => f.space(),
Self::GlycanFragmentComposition(f) => f.space(),
})
.set_total::<Self>()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MolecularFormulaError {
ElementWithoutDefinedMass,
IsotopeWithoutDefinedMass,
Overflow,
}
impl MolecularFormulaError {
pub const fn reason(self) -> &'static str {
match self {
Self::ElementWithoutDefinedMass => "An element without a defined mass was used",
Self::IsotopeWithoutDefinedMass => "An isotope without a defined mass was used",
Self::Overflow => {
"The total amount for this element overflowed the underlying storage type"
}
}
}
}
impl std::fmt::Display for MolecularFormulaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.reason())
}
}
impl MolecularFormula {
pub fn new(
elements: &[(Element, Option<NonZeroU16>, i32)],
labels: &[AmbiguousLabel],
) -> Option<Self> {
if elements.iter().any(|e| !e.0.is_valid(e.1)) {
None
} else {
let result = Self {
elements: elements.into(),
additional_mass: 0.0.into(),
labels: labels.into(),
};
Some(result.simplify())
}
}
#[must_use]
pub(super) fn simplify(mut self) -> Self {
self.elements.retain(|el| el.2 != 0);
self.elements.sort_by(|a, b| {
if a.0 == b.0 {
a.1.cmp(&b.1)
} else {
a.0.cmp(&b.0)
}
});
let mut max = self.elements.len().saturating_sub(1);
let mut index = 0;
while index < max {
let this = self.elements[index];
let next = self.elements[index + 1];
if this.0 == next.0 && this.1 == next.1 {
self.elements[index].2 += next.2;
self.elements.remove(index + 1);
max = max.saturating_sub(1);
} else {
index += 1;
}
}
self.elements
.retain(|el: &(Element, Option<std::num::NonZero<u16>>, i32)| el.2 != 0);
self
}
#[must_use]
pub fn with_additional_mass(additional_mass: f64) -> Self {
Self {
elements: ThinVec::new(),
additional_mass: OrderedFloat(additional_mass),
labels: ThinVec::new(),
}
}
pub fn add(
&mut self,
element: (Element, Option<NonZeroU16>, i32),
) -> Result<(), MolecularFormulaError> {
if element.0.is_valid(element.1) {
let mut index = 0;
let mut done = false;
let (el, i, n) = element;
while !done {
let base = self.elements.get(index).copied();
if let Some((re, ri, _)) = base {
if el > re || (el == re && i > ri) {
index += 1;
} else if el == re && i == ri {
if let Some(n) = self.elements[index].2.checked_add(n) {
self.elements[index].2 = n;
} else {
return Err(MolecularFormulaError::Overflow);
}
done = true;
} else {
self.elements.insert(index, (el, i, n));
done = true;
}
} else {
self.elements.push((el, i, n));
done = true;
}
}
Ok(())
} else {
Err(if element.1.is_some() {
MolecularFormulaError::IsotopeWithoutDefinedMass
} else {
MolecularFormulaError::ElementWithoutDefinedMass
})
}
}
pub fn add_mass(&mut self, mass: OrderedFloat<f64>) {
self.additional_mass += mass;
}
pub fn elements(&self) -> &[(Element, Option<NonZeroU16>, i32)] {
&self.elements
}
pub fn elements_mut(&mut self) -> &mut [(Element, Option<NonZeroU16>, i32)] {
&mut self.elements
}
pub const fn additional_mass(&self) -> OrderedFloat<f64> {
self.additional_mass
}
pub fn set_charge(&mut self, charge: crate::system::isize::Charge) {
if let Some(el) = self.elements.iter_mut().find(|e| e.0 == Element::Electron) {
el.2 = -charge.value as i32;
} else {
self.elements.push((Element::Electron, None, -charge.value as i32));
}
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty() && self.additional_mass == 0.0
}
pub(in super::super) fn hill_notation_generic(
&self,
f: impl Fn(&(Element, Option<NonZeroU16>, i32), &mut String),
separator: &str,
show_mass: bool,
show_charge: bool,
) -> String {
let mut buffer = String::new();
if let Some(carbon) = self.elements.iter().find(|e| e.0 == Element::C && e.1.is_none()) {
if carbon.2 != 0 {
f(carbon, &mut buffer);
}
if let Some(hydrogen) =
self.elements.iter().find(|e| e.0 == Element::H && e.1.is_none())
&& hydrogen.2 != 0
{
if !buffer.is_empty() {
buffer.push_str(separator);
}
f(hydrogen, &mut buffer);
}
for element in self.elements.iter().filter(|e| {
!((e.0 == Element::H || e.0 == Element::C || e.0 == Element::Electron)
&& e.1.is_none())
&& e.2 != 0
}) {
if !buffer.is_empty() {
buffer.push_str(separator);
}
f(element, &mut buffer);
}
} else {
for element in &self.elements {
if element.2 != 0 && element.0 != Element::Electron {
if !buffer.is_empty() {
buffer.push_str(separator);
}
f(element, &mut buffer);
}
}
}
if show_mass && self.additional_mass != 0.0 {
write!(&mut buffer, "{:+}", self.additional_mass).unwrap();
}
if show_charge && self.charge().value != 0 {
write!(&mut buffer, ":z{:+}", self.charge().value).unwrap();
}
buffer
}
}
impl Neg for &MolecularFormula {
type Output = MolecularFormula;
fn neg(self) -> Self::Output {
let mut res = self.clone();
for element in &mut res.elements {
element.2 = -element.2;
}
res
}
}
impl Neg for MolecularFormula {
type Output = Self;
fn neg(mut self) -> Self::Output {
for element in &mut self.elements {
element.2 = -element.2;
}
self
}
}
impl Add<&MolecularFormula> for &MolecularFormula {
type Output = MolecularFormula;
fn add(self, rhs: &MolecularFormula) -> Self::Output {
self.clone().checked_add(rhs).expect("Overflow in adding MolecularFormula")
}
}
impl Sub<&MolecularFormula> for &MolecularFormula {
type Output = MolecularFormula;
fn sub(self, rhs: &MolecularFormula) -> Self::Output {
let mut result = (*self).clone();
result.labels.extend_from_slice(&rhs.labels);
let mut index_result = 0;
let mut index_rhs = 0;
result.additional_mass -= rhs.additional_mass;
while index_rhs < rhs.elements.len() {
let (el, i, n) = rhs.elements[index_rhs];
if index_result < result.elements.len() {
let (re, ri, _) = result.elements[index_result];
if el > re || (el == re && i > ri) {
index_result += 1;
} else if el == re && i == ri {
result.elements[index_result].2 -= n;
index_rhs += 1;
} else {
result.elements.insert(index_result, (el, i, -n));
index_rhs += 1;
}
} else {
result.elements.push((el, i, -n));
index_rhs += 1;
}
}
result.elements.retain(|el| el.2 != 0);
result
}
}
impl Mul<&isize> for &MolecularFormula {
type Output = MolecularFormula;
fn mul(self, rhs: &isize) -> Self::Output {
self.checked_mul_isize(*rhs)
.expect("Overflow in multiplying MolecularFormula")
}
}
impl Mul<&i32> for &MolecularFormula {
type Output = MolecularFormula;
fn mul(self, rhs: &i32) -> Self::Output {
self.checked_mul_i32(*rhs)
.expect("Overflow in multiplying MolecularFormula")
}
}
impl Mul<&u16> for &MolecularFormula {
type Output = MolecularFormula;
fn mul(self, rhs: &u16) -> Self::Output {
self.checked_mul_u16(*rhs)
.expect("Overflow in multiplying MolecularFormula")
}
}
impl MolecularFormula {
pub fn checked_mul_isize(&self, rhs: isize) -> Option<Self> {
self.checked_mul_i32(i32::try_from(rhs).ok()?)
}
pub fn checked_mul_i32(&self, rhs: i32) -> Option<Self> {
Some(Self {
additional_mass: self.additional_mass * f64::from(rhs),
elements: self
.elements
.iter()
.copied()
.map(|part| Some((part.0, part.1, part.2.checked_mul(rhs)?)))
.collect::<Option<_>>()?,
labels: self.labels.clone(),
})
}
pub fn checked_mul_u16(&self, rhs: u16) -> Option<Self> {
Some(Self {
additional_mass: self.additional_mass * f64::from(rhs),
elements: self
.elements
.iter()
.copied()
.map(|part| Some((part.0, part.1, part.2.checked_mul(i32::from(rhs))?)))
.collect::<Option<_>>()?,
labels: self.labels.clone(),
})
}
pub fn checked_add(mut self, rhs: &Self) -> Option<Self> {
self.ref_mut_checked_add(rhs).map(|()| self)
}
pub fn ref_mut_checked_add(&mut self, rhs: &Self) -> Option<()> {
self.labels.extend_from_slice(&rhs.labels);
let mut index_result = 0;
let mut index_rhs = 0;
self.additional_mass += rhs.additional_mass;
while index_rhs < rhs.elements.len() {
let (el, i, n) = rhs.elements[index_rhs];
if index_result < self.elements.len() {
let (re, ri, _) = self.elements[index_result];
if el > re || (el == re && i > ri) {
index_result += 1;
} else if el == re && i == ri {
self.elements[index_result].2 = self.elements[index_result].2.checked_add(n)?;
index_rhs += 1;
} else {
self.elements.insert(index_result, (el, i, n));
index_rhs += 1;
}
} else {
self.elements.push((el, i, n));
index_rhs += 1;
}
}
self.elements.retain(|el| el.2 != 0);
Some(())
}
pub fn checked_sub(mut self, rhs: &Self) -> Option<Self> {
self.ref_mut_checked_sub(rhs).map(|()| self)
}
pub fn ref_mut_checked_sub(&mut self, rhs: &Self) -> Option<()> {
self.labels.extend_from_slice(&rhs.labels);
let mut index_result = 0;
let mut index_rhs = 0;
self.additional_mass -= rhs.additional_mass;
while index_rhs < rhs.elements.len() {
let (el, i, n) = rhs.elements[index_rhs];
if index_result < self.elements.len() {
let (re, ri, _) = self.elements[index_result];
if el > re || (el == re && i > ri) {
index_result += 1;
} else if el == re && i == ri {
self.elements[index_result].2 = self.elements[index_result].2.checked_sub(n)?;
index_rhs += 1;
} else {
self.elements.insert(index_result, (el, i, -n));
index_rhs += 1;
}
} else {
self.elements.push((el, i, -n));
index_rhs += 1;
}
}
self.elements.retain(|el| el.2 != 0);
Some(())
}
}
impl Mul<&i8> for &MolecularFormula {
type Output = MolecularFormula;
fn mul(self, rhs: &i8) -> Self::Output {
MolecularFormula {
additional_mass: self.additional_mass * f64::from(*rhs),
elements: self
.elements
.iter()
.copied()
.map(|part| (part.0, part.1, part.2 * i32::from(*rhs)))
.collect(),
labels: self.labels.clone(),
}
}
}
impl_binop_ref_cases!(impl Add, add for MolecularFormula, MolecularFormula, MolecularFormula);
impl_binop_ref_cases!(impl Sub, sub for MolecularFormula, MolecularFormula, MolecularFormula);
impl_binop_ref_cases!(impl Mul, mul for MolecularFormula, isize, MolecularFormula);
impl_binop_ref_cases!(impl Mul, mul for MolecularFormula, i32, MolecularFormula);
impl_binop_ref_cases!(impl Mul, mul for MolecularFormula, u16, MolecularFormula);
impl_binop_ref_cases!(impl Mul, mul for MolecularFormula, i8, MolecularFormula);
impl AddAssign<&Self> for MolecularFormula {
fn add_assign(&mut self, rhs: &Self) {
self.ref_mut_checked_add(rhs).expect("Overflow in adding MolecularFormula");
}
}
impl SubAssign<&Self> for MolecularFormula {
fn sub_assign(&mut self, rhs: &Self) {
self.ref_mut_checked_sub(rhs)
.expect("Overflow in subtracting MolecularFormula");
}
}
impl AddAssign<Self> for MolecularFormula {
fn add_assign(&mut self, rhs: Self) {
*self += &rhs;
}
}
impl SubAssign<Self> for MolecularFormula {
fn sub_assign(&mut self, rhs: Self) {
*self -= &rhs;
}
}
impl std::iter::Sum<Self> for MolecularFormula {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
let mut res = Self::default();
iter.for_each(|v| res += v);
res
}
}
#[macro_export]
macro_rules! molecular_formula {
($($tail:tt)*) => {
$crate::formula_internal!([$($tail)*] -> [])
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! formula_internal {
([$e:ident $n:literal $($tail:tt)*] -> [$($output:tt)*]) => {
$crate::formula_internal!([$($tail)*] -> [$($output)*($crate::chemistry::Element::$e, None, $n),])
};
([$e:ident $($tail:tt)*] -> [$($output:tt)*]) => {
$crate::formula_internal!([$($tail)*] -> [$($output)*($crate::chemistry::Element::$e, None, 1),])
};
([[$i:literal $e:ident $n:literal] $($tail:tt)*] -> [$($output:tt)*]) => {
$crate::formula_internal!([$($tail)*] -> [$($output)*($crate::chemistry::Element::$e, Some(std::num::NonZeroU16::new($i).unwrap()), $n),])
};
([$e:ident $n:expr] -> [$($output:tt)*]) =>{
$crate::formula_internal!([] -> [$($output)*($crate::chemistry::Element::$e, None, $n),])
};
([$e:ident] -> [$($output:tt)*]) =>{
$crate::formula_internal!([] -> [$($output)*($crate::chemistry::Element::$e, None, 1),])
};
([[$i:literal $e:ident] $n:expr] -> [$($output:tt)*]) =>{
$crate::formula_internal!([] -> [$($output)*($crate::chemistry::Element::$e, Some(std::num::NonZeroU16::new($i).unwrap()), $n),])
};
([:z+$charge:literal] -> [$($output:tt)*]) =>{
$crate::formula_internal!([] -> [$($output)*($crate::chemistry::Element::Electron, None, -$charge),])
};
([:z-$charge:literal] -> [$($output:tt)*]) =>{
$crate::formula_internal!([] -> [$($output)*($crate::chemistry::Element::Electron, None, $charge),])
};
([] -> [$($output:tt)*]) =>{
$crate::chemistry::MolecularFormula::new(&[$($output)*], &[]).unwrap()
};
([($($l:expr),*)] -> [$($output:tt)*]) =>{
$crate::chemistry::MolecularFormula::new(&[$($output)*], &[$($l),*]).unwrap()
};
}
#[derive(
Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize, Default,
)]
pub enum SatelliteLabel {
#[default]
None,
A,
B,
}
impl std::fmt::Display for SatelliteLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::None => "",
Self::A => "a",
Self::B => "b",
})
}
}