use std::{num::NonZeroU16, ops::Range, sync::Arc};
use context_error::*;
use itertools::Itertools;
use mzcv::AccessionCode;
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};
use crate::{
ParserResult,
chemistry::{Element, MolecularFormula},
glycan::{GlycanStructure, MonoSaccharide},
helper_functions::*,
ontology::{Ontologies, Ontology},
sequence::{
AmbiguousLookup, AmbiguousLookupEntry, CrossLinkLookup, CrossLinkName, MassTag,
PlacementRule, SimpleModification, SimpleModificationInner,
},
system::{Mass, dalton},
};
impl SimpleModificationInner {
pub fn pro_forma<'a>(
line: &'a str,
ambiguous_lookup: &mut AmbiguousLookup,
cross_link_lookup: &mut CrossLinkLookup,
ontologies: &Ontologies,
) -> ParserResult<'a, (ReturnModification, MUPSettings), BasicKind> {
Self::pro_forma_inner(
&Context::default().lines(0, line),
line,
0..line.len(),
ambiguous_lookup,
cross_link_lookup,
ontologies,
)
}
pub fn pro_forma_inner<'a>(
base_context: &Context<'a>,
line: &'a str,
range: Range<usize>,
ambiguous_lookup: &mut AmbiguousLookup,
cross_link_lookup: &mut CrossLinkLookup,
ontologies: &Ontologies,
) -> ParserResult<'a, (ReturnModification, MUPSettings), BasicKind> {
Self::pro_forma_main::<false>(
base_context,
line,
range,
ambiguous_lookup,
cross_link_lookup,
ontologies,
)
}
pub(crate) fn pro_forma_main<'a, const STRICT: bool>(
base_context: &Context<'a>,
line: &'a str,
range: Range<usize>,
ambiguous_lookup: &mut AmbiguousLookup,
cross_link_lookup: &mut CrossLinkLookup,
ontologies: &Ontologies,
) -> ParserResult<'a, (ReturnModification, MUPSettings), BasicKind> {
let mut errors = Vec::new();
let mut modification = None;
let mut settings = MUPSettings::default();
let mut offset = range.start;
for part in line[range].split('|') {
match parse_single_modification::<STRICT>(
base_context,
line,
offset..offset + part.len(),
ambiguous_lookup,
cross_link_lookup,
ontologies,
) {
Ok((result, w)) => {
combine_errors(&mut errors, w);
match result {
SingleReturnModification::None => (),
SingleReturnModification::Modification(m) => {
if modification.as_ref().is_none_or(|mo| m > *mo) {
modification = Some(m);
}
}
SingleReturnModification::Positions(p) => settings.position = Some(p),
SingleReturnModification::Limit(l) => settings.limit = Some(l),
SingleReturnModification::CoMKP(s) => {
settings.comkp = s;
}
SingleReturnModification::CoMUP(s) => {
settings.comup = s;
}
}
}
Err(e) => combine_errors(&mut errors, e),
}
offset += part.len() + 1;
}
if let Some(ReturnModification::Ambiguous(id, _, true)) = &modification {
ambiguous_lookup[*id].copy_settings(&settings);
}
if errors.iter().any(|e| e.get_kind().is_error(())) {
Err(errors)
} else {
Ok((
(
modification.unwrap_or(ReturnModification::Defined(Arc::new(Self::Mass(
MassTag::None,
Mass::default().into(),
None,
)))),
settings,
),
errors,
))
}
}
}
type SliceWithRange<'a> = (&'a str, Range<usize>);
fn tokenise_mod(
text: &str,
range: Range<usize>,
) -> (
SliceWithRange<'_>,
Option<SliceWithRange<'_>>,
Option<SliceWithRange<'_>>,
Option<SliceWithRange<'_>>,
) {
let (m, label) = text[range.clone()]
.split_once('#')
.map_or_else(|| (&text[range.clone()], None), |(m, l)| (m, Some(l)));
let (tag, name) =
m.split_once(':')
.map_or(((m, range.start..range.start + m.len()), None), |(t, n)| {
(
(t, range.start..t.len()),
Some((n, range.start + t.len() + 1..range.start + m.len())),
)
});
let (label, score) = label.map_or((None, None), |label| {
label.strip_suffix(')').and_then(|label| label.split_once('(')).map_or(
(Some((label, range.start + m.len() + 1..range.end)), None),
|(label, score)| {
(
Some((
label,
range.start + m.len() + 1..range.start + m.len() + 1 + label.len(),
)),
Some((
score,
range.start + m.len() + 2 + label.len()..range.end - 1,
)),
)
},
)
});
(tag, name, label, score)
}
enum SingleReturnModification {
None,
Modification(ReturnModification),
Positions(Vec<PlacementRule>),
Limit(usize),
CoMKP(bool),
CoMUP(bool),
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct MUPSettings {
pub position: Option<Vec<PlacementRule>>,
pub limit: Option<usize>,
pub comkp: bool,
pub comup: bool,
}
impl Default for MUPSettings {
fn default() -> Self {
Self {
position: None,
limit: None,
comkp: true,
comup: true,
}
}
}
fn parse_single_modification<'error, const STRICT: bool>(
base_context: &Context<'error>,
line: &'error str,
range: Range<usize>,
ambiguous_lookup: &mut AmbiguousLookup,
cross_link_lookup: &mut CrossLinkLookup,
ontologies: &Ontologies,
) -> ParserResult<'error, SingleReturnModification, BasicKind> {
fn single_name_resolution<'a>(
ontologies: &Ontologies,
ontology: Ontology,
name: &'a str,
base_context: &Context<'a>,
range: Range<usize>,
) -> ParserResult<'a, SimpleModification, BasicKind> {
name_resolution(
ontologies,
&[ontology],
&[ontology],
name,
base_context,
range,
)
}
fn name_resolution<'a>(
ontologies: &Ontologies,
match_selection: &[Ontology],
search_selection: &[Ontology],
name: &'a str,
base_context: &Context<'a>,
range: Range<usize>,
) -> ParserResult<'a, SimpleModification, BasicKind> {
if let Some((by_name, modification)) =
ontologies.get_by_name_or_synonym(match_selection, name)
{
if by_name {
Ok((modification, Vec::new()))
} else {
Ok((modification.clone(), vec![BoxedError::new(
BasicKind::Warning,
"Used modification synonym",
"The name of the modification should be used to unambiguously identify a modification instead of a synonym",
base_context
.clone()
.add_highlight((0, range, format!("use {modification}"))),
)]))
}
} else {
Err(vec![
BoxedError::new(
BasicKind::Error,
"Invalid modification",
format!(
"The modification could not be found in {}",
if match_selection.len() == 1 {
match_selection[0].to_string()
} else if match_selection.len() == 2 {
format!("{} or {}", match_selection[0], match_selection[1])
} else if match_selection.is_empty() {
"Unimod, PSI-MOD, XL-MOD, GNOme, RESID, or Custom".to_string()
} else {
format!(
"{}, or {}",
match_selection[..match_selection.len() - 1]
.iter()
.map(ToString::to_string)
.join(","),
match_selection[match_selection.len() - 1]
)
}
),
base_context.clone().add_highlight((0, range)),
)
.suggestions(
ontologies.search(search_selection, name).into_iter().map(|(m, s)| {
s.map_or_else(|| m.to_string(), |s| format!("`{s}` synonym for `{m}`"))
}),
),
])
}
}
let mut errors = Vec::new();
let (head, tail, label, score) = tokenise_mod(line, range.clone());
let full = (&line[range.clone()], range);
let modification = if let Some(tail) = tail {
let basic_error = BoxedError::new(
BasicKind::Error,
"Invalid modification",
"..",
base_context.clone().add_highlight((0, tail.1.clone())),
);
let sign_warning = BoxedError::new(
BasicKind::Warning,
"Improper modification",
"A numerical modification should always be specified with a sign (+/-) to help it be recognised as a mass modification and not a modification index.",
base_context.clone().add_highlight((0, tail.1.clone())),
);
let lowercase = head.0.to_ascii_lowercase();
match (lowercase.as_str(), tail.0) {
("unimod", tail) => {
let id = tail
.parse::<u32>()
.map_err(|_| vec![basic_error.clone().long_description("Unimod accession number should be a number")])?;
ontologies.unimod().get_by_index(&AccessionCode::Numeric(id)).map(Some).ok_or_else(|| {
vec![
basic_error
.clone()
.long_description("The supplied Unimod accession number is not an existing modification"),
]
})
}
("mod", tail) => {
let id = tail
.parse::<u32>()
.map_err(|_| vec![basic_error.clone().long_description("PSI-MOD accession number should be a number")])?;
ontologies.psimod().get_by_index(&AccessionCode::Numeric(id)).map(Some).ok_or_else(|| {
vec![
basic_error
.clone()
.long_description("The supplied PSI-MOD accession number is not an existing modification"),
]
})
}
("resid", tail) => {
let id = tail.parse::<AccessionCode>().map_err(|_| {
vec![
basic_error
.clone()
.long_description("RESID accession number should be a number prefixed with 'AA'"),
]
})?;
ontologies.resid().get_by_index(&id).map(Some).ok_or_else(|| {
vec![
basic_error
.clone()
.long_description("The supplied RESID accession number is not an existing modification"),
]
})
}
("xlmod", tail) => {
let id = tail
.parse::<u32>()
.map_err(|_| vec![basic_error.clone().long_description("XLMOD accession number should be a number")])?;
ontologies.xlmod().get_by_index(&AccessionCode::Numeric(id)).map(Some).ok_or_else(|| {
vec![
basic_error
.clone()
.long_description("The supplied XLMOD accession number is not an existing modification"),
]
})
}
("custom", tail) => {
let id = tail
.parse::<u32>()
.map_err(|_| vec![basic_error.clone().long_description("Custom accession number should be a number")])?;
ontologies.custom().get_by_index(&AccessionCode::Numeric(id)).map(Some).ok_or_else(|| {
vec![
basic_error
.clone()
.long_description("The supplied Custom accession number is not an existing modification"),
]
})
}
("u", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Unimod), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Unimod, name, base_context, tail.1.clone()))
))),
("m", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Psimod), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Psimod, name, base_context, tail.1.clone()))
))),
("r", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Resid), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Resid, name, base_context, tail.1.clone()))
))),
("x", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Xlmod), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Xlmod, name, base_context, tail.1.clone()))
))),
("c", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Custom), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Custom, name, base_context, tail.1.clone()))
))),
("gno" | "g", name) => Ok(Some(handle!(
errors,
numerical_mod(MassTag::Ontology(Ontology::Gnome), name)
.map(|(sign, m)| (m, if STRICT && !sign { vec![sign_warning.clone()] } else { Vec::new() }))
.or_else(|_| single_name_resolution(ontologies, Ontology::Gnome, name, base_context, tail.1.clone()))
))),
("formula", _) => Ok(Some(Arc::new(SimpleModificationInner::Formula(
MolecularFormula::pro_forma_inner::<true, false>(base_context, line, tail.1.clone()).map_err(|e| vec![e])?,
)))),
("glycan", _) => Ok(Some(Arc::new(SimpleModificationInner::Glycan(handle!(
errors,
MonoSaccharide::pro_forma_composition_inner::<STRICT>(base_context, line, tail.1.clone())
.map(|(v, w)| (
v,
w.into_iter()
.map(|e| e.convert::<BasicKind, BoxedError<'error, BasicKind>>(Into::into))
.collect::<Vec<_>>()
))
.map_err(|e| e
.into_iter()
.map(|e| e.convert::<BasicKind, BoxedError<'error, BasicKind>>(Into::into))
.collect::<Vec<_>>())
))))),
("glycanstructure", _) => GlycanStructure::parse(line, tail.1.clone())
.map(|g| Some(Arc::new(SimpleModificationInner::GlycanStructure(g))))
.map_err(|e| vec![e]),
("info", tail) => Ok(Some(Arc::new(SimpleModificationInner::Info(tail.to_string())))),
("obs", tail) => numerical_mod(MassTag::Observed, tail)
.map(|(sign, m)| {
if STRICT && !sign {
combine_error(&mut errors, sign_warning.clone());
}
Some(m)
})
.map_err(|_| vec![basic_error.long_description("This modification cannot be read as a numerical modification")]),
("position", _) => match super::parse::parse_placement_rules(base_context, line, tail.1.clone()) {
Ok(rules) => return Ok((SingleReturnModification::Positions(rules), errors)),
Err(e) => Err(vec![e]),
},
("limit", tail) => {
match tail.parse::<usize>().map_err(|error| {
basic_error.long_description(format!(
"Invalid limit for modification of unknown position, the number is {}",
explain_number_error(&error)
))
}) {
Ok(l) => return Ok((SingleReturnModification::Limit(l), errors)),
Err(e) => Err(vec![e]),
}
}
("colocalisemodificationsofknownposition" | "comkp", tail) => {
match tail.parse::<bool>().map_err(|error| {
basic_error.long_description(format!(
"Invalid setting for colocalise placed modifications for modification of unknown position, the boolean is {error}",
))
}) {
Ok(s) => return Ok((SingleReturnModification::CoMKP(s), errors)),
Err(e) => Err(vec![e]),
}
}
("colocalisemodificationsofunknownposition" | "comup", tail) => {
match tail.parse::<bool>().map_err(|error| {
basic_error.long_description(format!(
"Invalid setting for colocalise modifications of unknown position for modification of unknown position, the boolean is {error}",
))
}) {
Ok(s) => return Ok((SingleReturnModification::CoMUP(s), errors)),
Err(e) => Err(vec![e]),
}
}
(_, _) => Ok(Some(handle!(
errors,
name_resolution(
ontologies,
&[Ontology::Unimod, Ontology::Psimod],
&[],
&line[head.1.start..tail.1.end],
base_context,
head.1.start..tail.1.end
)
))),
}
} else if head.0.is_empty() {
Ok(None)
} else if full.0.eq_ignore_ascii_case("colocalisemodificationsofknownposition")
|| full.0.eq_ignore_ascii_case("comkp")
{
return Ok((SingleReturnModification::CoMKP(true), errors));
} else if full.0.eq_ignore_ascii_case("colocalisemodificationsofunknownposition")
|| full.0.eq_ignore_ascii_case("comup")
{
return Ok((SingleReturnModification::CoMUP(true), errors));
} else {
Ok(Some(handle!(
errors,
numerical_mod(MassTag::None, head.0)
.map(|(sign, m)| (m, if STRICT && !sign {vec![BoxedError::new(
BasicKind::Warning,
"Improper modification",
"A numerical modification should always be specified with a sign (+/-) to help it be recognised as a mass modification and not a modification index.",
base_context
.clone()
.add_highlight((0, head.1.clone())),
)]} else {Vec::new()}))
.or_else(|_| name_resolution(
ontologies,
&[Ontology::Unimod, Ontology::Psimod],
&[],
head.0,
base_context,
head.1.clone()
))
)))
};
let modification = modification?;
if let Some(modification) = &modification
&& let Some(description) = modification.description()
&& description.obsolete
{
combine_error(
&mut errors,
BoxedError::new(
BasicKind::Warning,
"Obsolete modification",
"The used modification is marked obsolete",
base_context.clone().add_highlight((
0,
full.1.start..full.1.end,
description.description.to_string(),
)),
),
);
}
if let Some(group) = label {
if group.0.chars().any(|c| !c.is_ascii_alphanumeric()) {
combine_error(
&mut errors,
BoxedError::new(
BasicKind::Error,
"Invalid character in label",
"A label can only contain alphanumeric characters",
base_context.clone().add_highlight((0, group.1.clone())),
),
);
}
if group.0.eq_ignore_ascii_case("branch") {
let index = cross_link_lookup
.iter()
.position(|c| c.0 == CrossLinkName::Branch)
.unwrap_or_else(|| {
let index = cross_link_lookup.len();
cross_link_lookup.push((CrossLinkName::Branch, None));
index
});
if let Some(linker) = modification {
if cross_link_lookup[index].1.as_ref().is_some_and(|l| *l != linker) {
return Err(vec![BoxedError::new(
BasicKind::Error,
"Invalid branch definition",
"A branch definition has to be identical at both sites, or only defined at one site.",
base_context.clone().add_highlight((0, full.1)),
)]);
}
cross_link_lookup[index].1 = Some(linker);
}
Ok((
SingleReturnModification::Modification(ReturnModification::CrossLinkReferenced(
index,
)),
errors,
))
} else if let Some(name) = group.0.to_ascii_lowercase().strip_prefix("xl") {
let name = CrossLinkName::Name(name.to_string().into_boxed_str());
let index = cross_link_lookup.iter().position(|c| c.0 == name).unwrap_or_else(|| {
let index = cross_link_lookup.len();
cross_link_lookup.push((name, None));
index
});
if let Some(linker) = modification {
if cross_link_lookup[index].1.as_ref().is_some_and(|l| *l != linker) {
return Err(vec![BoxedError::new(
BasicKind::Error,
"Invalid cross-link definition",
"A cross-link definition has to be identical at both sites, or only defined at one site.",
base_context.clone().add_highlight((0, full.1)),
)]);
}
cross_link_lookup[index].1 = Some(linker);
}
Ok((
SingleReturnModification::Modification(ReturnModification::CrossLinkReferenced(
index,
)),
errors,
))
} else {
let score = match score.map(|s| {
s.0.parse::<f64>().map_err(|_| {
BoxedError::new(
BasicKind::Error,
"Invalid modification localisation score",
"The ambiguous modification localisation score needs to be a valid number",
base_context.clone().add_highlight((0, s.1.clone())),
)
})
}) {
Some(Ok(score)) => Some(OrderedFloat(score)),
Some(Err(err)) => {
combine_error(&mut errors, err);
None
}
None => None,
};
handle_ambiguous_modification(
modification,
group,
score,
ambiguous_lookup,
base_context.clone().add_highlight((0, full.1)),
)
.map(|(m, w)| {
combine_errors(&mut errors, w);
(m, errors)
})
}
} else {
Ok((
modification.map_or(SingleReturnModification::None, |m| {
SingleReturnModification::Modification(ReturnModification::Defined(m))
}),
errors,
))
}
}
fn handle_ambiguous_modification<'a>(
modification: Option<SimpleModification>,
group: SliceWithRange<'a>,
localisation_score: Option<OrderedFloat<f64>>,
ambiguous_lookup: &mut AmbiguousLookup,
context: Context<'a>,
) -> ParserResult<'a, SingleReturnModification, BasicKind> {
let found_definition = ambiguous_lookup
.iter()
.enumerate()
.find(|(_, entry)| entry.name.eq_ignore_ascii_case(group.0))
.map(|(index, entry)| (index, entry.modification.as_ref()));
match (modification, found_definition) {
(Some(m), Some((index, Some(f)))) => {
if *m == **f {
Ok((
SingleReturnModification::Modification(ReturnModification::Ambiguous(
index,
localisation_score,
false,
)),
vec![BoxedError::new(
BasicKind::Warning,
"Invalid ambiguous modification",
"An ambiguous modification cannot be placed twice (for one of the modifications leave out the modification and only provide the group name)",
context,
)],
))
} else {
Err(vec![BoxedError::new(
BasicKind::Error,
"Invalid ambiguous modification",
"An ambiguous modification cannot be placed twice (for one of the modifications leave out the modification and only provide the group name)",
context,
)])
}
}
(Some(m), Some((index, None))) => {
ambiguous_lookup[index].modification = Some(m);
Ok((
SingleReturnModification::Modification(ReturnModification::Ambiguous(
index,
localisation_score,
true,
)),
Vec::new(),
))
}
(None, Some((index, _))) => Ok((
SingleReturnModification::Modification(ReturnModification::Ambiguous(
index,
localisation_score,
false,
)),
Vec::new(),
)),
(m, None) => {
let index = ambiguous_lookup.len();
let preferred = m.is_some();
ambiguous_lookup.push(AmbiguousLookupEntry::new(group.0.to_string(), m));
Ok((
SingleReturnModification::Modification(ReturnModification::Ambiguous(
index,
localisation_score,
preferred,
)),
Vec::new(),
))
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum ReturnModification {
Defined(SimpleModification),
Ambiguous(usize, Option<OrderedFloat<f64>>, bool),
CrossLinkReferenced(usize),
}
impl ReturnModification {
#[must_use]
pub fn defined(self) -> Option<SimpleModification> {
match self {
Self::Defined(modification) => Some(modification),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum GlobalModification {
Isotope(Element, Option<NonZeroU16>),
Fixed(PlacementRule, SimpleModification),
}
pub(super) fn numerical_mod(
tag: MassTag,
text: &str,
) -> Result<(bool, SimpleModification), String> {
text.parse().map_or_else(
|_| Err("Invalid number".to_string()),
|n| {
Ok((
text.starts_with('-') || text.starts_with('+'),
Arc::new(SimpleModificationInner::Mass(
tag,
Mass::new::<dalton>(n).into(),
float_digits(text),
)),
))
},
)
}