use std::{cmp::Ordering, collections::BTreeSet};
use context_error::*;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
chemistry::{DiagnosticIon, MolecularFormula, NeutralLoss},
parse_json::{ParseJson, use_serde},
sequence::PlacementRule,
space::{Space, UsedSpace},
};
/// Indicate the cross-link side, it contains a set of all placement rules that apply for the placed
/// location to find all possible ways of breaking and/or neutral losses. These numbers are the
/// index into the [`LinkerSpecificity`] rules.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CrossLinkSide {
/// The cross-link is symmetric, or if asymmetric it can be placed in both orientations
Symmetric(BTreeSet<usize>),
/// The cross-link is asymmetric and this is the 'left' side
Left(BTreeSet<usize>),
/// The cross-link is asymmetric and this is the 'right' side
Right(BTreeSet<usize>),
}
impl PartialOrd for CrossLinkSide {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CrossLinkSide {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Self::Symmetric(_), Self::Symmetric(_)) | (Self::Left(_), Self::Left(_)) => {
Ordering::Equal
}
(Self::Symmetric(_), _) => Ordering::Greater,
(_, Self::Symmetric(_)) => Ordering::Less,
(Self::Left(_), _) => Ordering::Greater,
(_, Self::Left(_)) => Ordering::Less,
(Self::Right(_), Self::Right(_)) => Ordering::Equal,
}
}
}
impl std::hash::Hash for CrossLinkSide {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let (i, r) = match self {
Self::Symmetric(r) => (0, r),
Self::Left(r) => (1, r),
Self::Right(r) => (2, r),
};
state.write_u8(i);
state.write(
&r.iter()
.sorted()
.flat_map(|r| r.to_ne_bytes())
.collect_vec(),
);
}
}
impl ParseJson for CrossLinkSide {
fn from_json_value(value: Value) -> Result<Self, BoxedError<'static, BasicKind>> {
use_serde(value)
}
}
/// The name of a cross-link
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum CrossLinkName {
/// A branch
Branch,
/// A cross-link
Name(Box<str>),
}
impl Space for CrossLinkName {
fn space(&self) -> UsedSpace {
match self {
Self::Branch => UsedSpace::default(),
Self::Name(n) => n.space(),
}
.set_total::<Self>()
}
}
impl ParseJson for CrossLinkName {
fn from_json_value(value: Value) -> Result<Self, BoxedError<'static, BasicKind>> {
use_serde(value)
}
}
/// The linker position specificities for a linker
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum LinkerSpecificity {
/// A symmetric specificity where both ends have the same specificity.
Symmetric {
/// The placement rules for both ends.
rules: Vec<PlacementRule>,
/// All stubs that can be left after cleaving or breaking of the cross-link.
stubs: Vec<(MolecularFormula, MolecularFormula)>,
/// All possible neutral losses from the intact cross-linker.
neutral_losses: Vec<NeutralLoss>,
/// All diagnostic ions from the cross-linker
diagnostic: Vec<DiagnosticIon>,
},
/// An asymmetric specificity where both ends have a different specificity.
Asymmetric {
/// The placement rules for both ends, these can be asymmetric thus are provided for 'right' and 'left' separately.
rules: (Vec<PlacementRule>, Vec<PlacementRule>),
/// All stubs that can be left after cleaving or breaking of the cross-link. The stubs are specific for right and left in the same orientation as the rules.
stubs: Vec<(MolecularFormula, MolecularFormula)>,
/// All possible neutral losses from the intact cross-linker.
neutral_losses: Vec<NeutralLoss>,
/// All diagnostic ions from the cross-linker
diagnostic: Vec<DiagnosticIon>,
},
}
impl Space for LinkerSpecificity {
fn space(&self) -> UsedSpace {
(UsedSpace::stack(1)
+ match self {
Self::Symmetric {
rules,
stubs,
neutral_losses,
diagnostic,
} => rules.space() + stubs.space() + neutral_losses.space() + diagnostic.space(),
Self::Asymmetric {
rules,
stubs,
neutral_losses,
diagnostic,
} => rules.space() + stubs.space() + neutral_losses.space() + diagnostic.space(),
})
.set_total::<Self>()
}
}
impl ParseJson for LinkerSpecificity {
fn from_json_value(value: Value) -> Result<Self, BoxedError<'static, BasicKind>> {
if let Value::Object(map) = value {
let (key, value) = map.into_iter().next().unwrap();
match key.as_str() {
"Symmetric" => {
if let Value::Object(mut map) = value {
Ok(Self::Symmetric {
rules: Vec::<PlacementRule>::from_json_value(
map.remove("rules").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'rules' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
stubs: Vec::<(MolecularFormula, MolecularFormula)>::from_json_value(
map.remove("stubs").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'stubs' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
neutral_losses: Vec::<NeutralLoss>::from_json_value(
map.remove("neutral_losses").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'neutral_losses' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
diagnostic: Vec::<DiagnosticIon>::from_json_value(
map.remove("diagnostic").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'diagnostic' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
})
} else if let Value::Array(mut arr) = value {
if arr.len() == 3 {
let diagnostic =
Vec::<DiagnosticIon>::from_json_value(arr.pop().unwrap())?;
let stubs =
Vec::<(MolecularFormula, MolecularFormula)>::from_json_value(
arr.pop().unwrap(),
)?;
let rules = Vec::<PlacementRule>::from_json_value(arr.pop().unwrap())?;
Ok(Self::Symmetric {
rules,
stubs,
neutral_losses: Vec::new(),
diagnostic,
})
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid NeutralLoss",
"The Symmetric is a sequence but does not have 3 children",
Context::show(arr.iter().join(",")),
))
}
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The Symmetric value has to be a map or a sequence",
Context::show(value.to_string()),
))
}
}
"Asymmetric" => {
if let Value::Object(mut map) = value {
Ok(Self::Asymmetric {
rules: <(Vec<PlacementRule>, Vec<PlacementRule>)>::from_json_value(
map.remove("rules").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'rules' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
stubs: Vec::<(MolecularFormula, MolecularFormula)>::from_json_value(
map.remove("stubs").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'stubs' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
neutral_losses: Vec::<NeutralLoss>::from_json_value(
map.remove("neutral_losses").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'neutral_losses' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
diagnostic: Vec::<DiagnosticIon>::from_json_value(
map.remove("diagnostic").ok_or_else(|| {
BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The required property 'diagnostic' is missing",
Context::show(
map.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.join(","),
),
)
})?,
)?,
})
} else if let Value::Array(mut arr) = value {
if arr.len() == 3 {
let diagnostic =
Vec::<DiagnosticIon>::from_json_value(arr.pop().unwrap())?;
let stubs =
Vec::<(MolecularFormula, MolecularFormula)>::from_json_value(
arr.pop().unwrap(),
)?;
let rules =
<(Vec<PlacementRule>, Vec<PlacementRule>)>::from_json_value(
arr.pop().unwrap(),
)?;
Ok(Self::Asymmetric {
rules,
stubs,
neutral_losses: Vec::new(),
diagnostic,
})
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid NeutralLoss",
"The Asymmetric is a sequence but does not have 3 children",
Context::show(arr.iter().join(",")),
))
}
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The Asymmetric value has to be a map or a sequence",
Context::show(value.to_string()),
))
}
}
_ => Err(BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The tag has to be Symmetric/Asymmetric",
Context::show(key),
)),
}
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid LinkerSpecificity",
"The JSON value has to be a map",
Context::show(value.to_string()),
))
}
}
}
#[cfg(test)]
#[allow(clippy::missing_panics_doc)]
mod tests {
use crate::{
chemistry::{DiagnosticIon, NeutralLoss},
molecular_formula,
parse_json::ParseJson,
sequence::{AminoAcid, LinkerSpecificity, PlacementRule, Position},
};
#[test]
fn deserialise_json() {
let old = r#"{"Asymmetric":[[[{"AminoAcid":[["Selenocysteine"],"AnyCTerm"]},{"AminoAcid":[["GlutamicAcid"],"Anywhere"]}],[{"AminoAcid":[["Selenocysteine"],"AnyNTerm"]}]],[[{"elements":[["U",null,1]],"additional_mass":0.0},{"elements":[["U",null,1]],"additional_mass":0.0}]],[{"elements":[["Te",null,1]],"additional_mass":0.0},{"elements":[["Ne",null,1]],"additional_mass":0.0},{"elements":[["H",null,2],["He",null,3]],"additional_mass":0.0},{"elements":[["H",null,1],["He",null,2]],"additional_mass":0.0},{"elements":[["I",null,1],["Er",null,1]],"additional_mass":0.0},{"elements":[["H",null,12],["C",null,12],["O",null,1]],"additional_mass":0.0}]]}"#;
let current = LinkerSpecificity::Symmetric {
rules: vec![
PlacementRule::Terminal(Position::AnyNTerm),
PlacementRule::AminoAcid(
vec![AminoAcid::Alanine, AminoAcid::Leucine].into(),
Position::Anywhere,
),
],
stubs: vec![(molecular_formula!(H 2 O 1), molecular_formula!(H 2 O 1))],
neutral_losses: vec![NeutralLoss::Gain(2, molecular_formula!(H 2 O 1))],
diagnostic: vec![DiagnosticIon(molecular_formula!(H 2 O 1))],
};
let current_text =
serde_json::to_string(¤t).expect("Could not serialise linker specificity");
let _old = LinkerSpecificity::from_json(old).expect("Could not deserialise old json");
let current_back = LinkerSpecificity::from_json(¤t_text)
.expect("Could not deserialise current json");
assert_eq!(current, current_back);
}
}