Skip to content

Commit 89de5ee

Browse files
committed
WIP extension of mzTab to support ambiguous modifications
1 parent 052d299 commit 89de5ee

5 files changed

Lines changed: 225 additions & 61 deletions

File tree

rustyms/src/identification/file_format.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66
use crate::{
77
identification::*,
88
ontology::CustomDatabase,
9-
sequence::{Linked, SemiAmbiguous},
9+
sequence::{Linked, SemiAmbiguous, SimpleLinear},
1010
};
1111

1212
/// A file format that is fully known
@@ -193,7 +193,7 @@ impl FileFormat {
193193
>,
194194
> = Box::new(sequences.into_iter().map(|p| {
195195
p.map(|p| {
196-
IdentifiedPeptidoform::<SemiAmbiguous, MaybePeptidoform>::from(p).cast()
196+
IdentifiedPeptidoform::<SimpleLinear, MaybePeptidoform>::from(p).cast()
197197
})
198198
}));
199199
b

rustyms/src/identification/formats/mztab.rs

Lines changed: 209 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ use crate::{
1919
FastaIdentifier, IdentifiedPeptidoform, IdentifiedPeptidoformData, KnownFileFormat,
2020
MaybePeptidoform, MetaData, SpectrumId, SpectrumIds,
2121
},
22-
ontology::CustomDatabase,
23-
prelude::CompoundPeptidoformIon,
22+
ontology::{CustomDatabase, Ontology},
23+
prelude::{CompoundPeptidoformIon, MolecularFormula},
2424
quantities::Tolerance,
2525
sequence::{
26-
AminoAcid, PeptideModificationSearch, Peptidoform, ReturnModification, SemiAmbiguous,
27-
SimpleModification, SimpleModificationInner, SloppyParsingParameters,
26+
AminoAcid, MUPSettings, PeptideModificationSearch, Peptidoform, ReturnModification,
27+
SimpleLinear, SimpleModification, SimpleModificationInner, SloppyParsingParameters,
2828
},
2929
system::{Mass, MassOverCharge, Time, isize::Charge},
3030
};
@@ -33,7 +33,7 @@ use crate::{
3333
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
3434
pub struct MZTabData {
3535
/// The peptide's sequence corresponding to the PSM
36-
pub peptide: Option<Peptidoform<SemiAmbiguous>>,
36+
pub peptide: Option<Peptidoform<SimpleLinear>>,
3737
/// A unique identifier for a PSM within the file. If a PSM can be matched to
3838
/// multiple proteins, the same PSM should be represented on multiple rows with
3939
/// different accessions and the same PSM_ID.
@@ -84,10 +84,8 @@ impl MZTabData {
8484
pub fn parse_file(
8585
path: impl AsRef<std::path::Path>,
8686
custom_database: Option<&CustomDatabase>,
87-
) -> Result<
88-
Box<dyn Iterator<Item = Result<Self, BoxedError<'static>>> + '_>,
89-
BoxedError<'static>,
90-
> {
87+
) -> Result<Box<dyn Iterator<Item = Result<Self, BoxedError<'static>>> + '_>, BoxedError<'static>>
88+
{
9189
let file = File::open(path.as_ref()).map_err(|e| {
9290
BoxedError::error(
9391
"Could not open file",
@@ -285,37 +283,24 @@ impl MZTabData {
285283
.required_column("modifications")
286284
.map_err(BoxedError::to_owned)?;
287285
let mut mod_index = mod_range.start;
288-
let modifications: Vec<(usize, SimpleModification)> = mod_column
289-
.split(',')
290-
.flat_map(|definition| {
291-
let pair = definition
292-
.split_once('-')
293-
.map(|(pos, _)| Ok((
294-
pos.parse::<usize>().map_err(|err| BoxedError::error(
295-
"Invalid modification position",
296-
format!("The position {}", explain_number_error(&err)),
297-
Context::line_range(Some(line.line_index as u32), line.line, mod_range.clone()),
298-
))?,
299-
SimpleModificationInner::parse_pro_forma(
300-
line.line,
301-
mod_index+1+pos.len()..mod_index+definition.len(),
302-
&mut Vec::new(),
303-
&mut Vec::new(),
304-
custom_database)?.0.defined()
305-
.ok_or_else(
306-
|| BoxedError::error(
307-
"Invalid modification",
308-
"A modification should be a fully defined modification, no cross-link or ambiguous modification",
309-
Context::line_range(Some(line.line_index as u32), line.line, mod_range.clone())))?)))
310-
.ok_or_else(
311-
|| BoxedError::error(
312-
"Invalid modification",
313-
"A modification should be the position followed by a hyphen ('-') followed by the modification",
314-
Context::line_range(Some(line.line_index as u32), line.line, mod_range.clone())));
315-
mod_index += definition.len() + 1;
316-
pair
317-
})
318-
.collect::<Result<Vec<_>, BoxedError>>()?;
286+
let modifications: Vec<MZTabReturnModification> =
287+
if mod_column.eq_ignore_ascii_case("null") || mod_column == "0" {
288+
Vec::new()
289+
} else {
290+
mod_column
291+
.split(',')
292+
.map(|definition| {
293+
let res = parse_modification(
294+
line.line,
295+
mod_index..mod_index + definition.len(),
296+
custom_database,
297+
line.line_index as u32,
298+
);
299+
mod_index = mod_index + definition.len() + 1;
300+
res
301+
})
302+
.collect::<Result<Vec<_>, BoxedError>>()?
303+
};
319304

320305
let mut result = Self {
321306
peptide: {
@@ -327,24 +312,38 @@ impl MZTabData {
327312
if range.is_empty() {
328313
None
329314
} else {
330-
let mut peptide = Peptidoform::sloppy_pro_forma(
315+
let mut peptide: Peptidoform<SimpleLinear> = Peptidoform::sloppy_pro_forma(
331316
line.line,
332317
range,
333318
custom_database,
334319
&SloppyParsingParameters {
335320
allow_unwrapped_modifications: true,
336321
..Default::default()
337322
},
338-
)?;
339-
for (location, modification) in modifications {
340-
match location {
341-
0 => peptide.add_simple_n_term(modification),
342-
c if c == peptide.len() + 1 => {
343-
peptide.add_simple_c_term(modification);
323+
)?
324+
.into();
325+
for modification in modifications {
326+
match modification {
327+
MZTabReturnModification::Defined(location, modification) => {
328+
match location {
329+
0 => peptide.add_simple_n_term(modification),
330+
c if c == peptide.len() + 1 => {
331+
peptide.add_simple_c_term(modification);
332+
}
333+
i => {
334+
peptide.sequence_mut()[i - 1]
335+
.add_simple_modification(modification);
336+
}
337+
}
344338
}
345-
i => {
346-
peptide.sequence_mut()[i - 1].add_simple_modification(modification);
339+
MZTabReturnModification::GlobalAmbiguous(modification) => {
340+
let _possible = peptide.add_unknown_position_modification(
341+
modification,
342+
0..peptide.len(),
343+
&MUPSettings::default(),
344+
);
347345
}
346+
_ => todo!(),
348347
}
349348
}
350349
Some(
@@ -695,6 +694,165 @@ impl MZTabData {
695694
}
696695
}
697696

697+
enum MZTabReturnModification {
698+
GlobalAmbiguous(SimpleModification),
699+
Ambiguous(Vec<(usize, Option<f64>)>, SimpleModification),
700+
Defined(usize, SimpleModification),
701+
}
702+
703+
fn parse_modification<'a>(
704+
line: &'a str,
705+
range: Range<usize>,
706+
custom_database: Option<&CustomDatabase>,
707+
line_index: u32,
708+
) -> Result<MZTabReturnModification, BoxedError<'a>> {
709+
if let Some((pos, modification)) = line[range.clone()].split_once('-') {
710+
let position = if pos.eq_ignore_ascii_case("null") {
711+
Ok(None)
712+
} else {
713+
// TODO: handle multiple and handle probabilities
714+
pos.parse::<usize>()
715+
.map_err(|err| {
716+
BoxedError::error(
717+
"Invalid modification position",
718+
format!("The position {}", explain_number_error(&err)),
719+
Context::line_range(
720+
Some(line_index),
721+
line,
722+
range.start..range.start + pos.len(),
723+
),
724+
)
725+
})
726+
.map(Some)
727+
}?;
728+
729+
let modification = if let Some((tag, value)) = modification.split_once(':') {
730+
let value_range = range.start + pos.len() + tag.len() + 2..range.end;
731+
let value_context = Context::line_range(Some(line_index), line, value_range.clone());
732+
if tag.eq_ignore_ascii_case("unimod") {
733+
Ontology::Unimod
734+
.find_id(
735+
value.parse::<usize>().map_err(|err| {
736+
BoxedError::error(
737+
"Invalid unimod code",
738+
format!("The unimod modification {}", explain_number_error(&err)),
739+
value_context.clone(),
740+
)
741+
})?,
742+
None,
743+
)
744+
.ok_or_else(|| {
745+
BoxedError::error(
746+
"Invalid unimod code",
747+
"The given unimod modification does not exist",
748+
value_context.clone(),
749+
)
750+
})?
751+
} else if tag.eq_ignore_ascii_case("mod") {
752+
Ontology::Psimod
753+
.find_id(
754+
value.parse::<usize>().map_err(|err| {
755+
BoxedError::error(
756+
"Invalid PSI-MOD code",
757+
format!("The PSI-MOD modification {}", explain_number_error(&err)),
758+
value_context.clone(),
759+
)
760+
})?,
761+
None,
762+
)
763+
.ok_or_else(|| {
764+
BoxedError::error(
765+
"Invalid PSI-MOD code",
766+
"The given PSI-MOD modification does not exist",
767+
value_context.clone(),
768+
)
769+
})?
770+
} else if tag.eq_ignore_ascii_case("custom") {
771+
Ontology::Custom
772+
.find_id(
773+
value.parse::<usize>().map_err(|err| {
774+
BoxedError::error(
775+
"Invalid custom code",
776+
format!("The custom modification {}", explain_number_error(&err)),
777+
value_context.clone(),
778+
)
779+
})?,
780+
custom_database,
781+
)
782+
.ok_or_else(|| {
783+
BoxedError::error(
784+
"Invalid custom code",
785+
"The given custom modification does not exist",
786+
value_context.clone(),
787+
)
788+
})?
789+
} else if tag.eq_ignore_ascii_case("chemmod") {
790+
if let Ok(mass) = value.parse::<f64>() {
791+
SimpleModificationInner::Mass(Mass::new::<crate::system::dalton>(mass).into())
792+
} else {
793+
let factor = match line.as_bytes()[value_range.start] {
794+
b'-' => -1,
795+
b'+' => 1,
796+
_ => {
797+
return Err(BoxedError::error(
798+
"Invalid mzTab modification",
799+
"A chemmod formula modification should be prepended by a sign",
800+
Context::line_range(
801+
Some(line_index),
802+
line,
803+
range.start + pos.len() + 1
804+
..range.start + pos.len() + 1 + tag.len(),
805+
),
806+
));
807+
}
808+
};
809+
MolecularFormula::from_pro_forma(
810+
line,
811+
value_range.start + 1..value_range.end,
812+
false,
813+
false,
814+
true,
815+
true,
816+
)
817+
.map(|f| SimpleModificationInner::Formula(f * factor))?
818+
}
819+
.into()
820+
} else {
821+
return Err(BoxedError::error(
822+
"Invalid mzTab modification",
823+
"The modification should be prepended by a tag describing the kind of modification, the possible tags are: 'unimod', 'mod', and 'chemmod'",
824+
Context::line_range(
825+
Some(line_index),
826+
line,
827+
range.start + pos.len() + 1..range.start + pos.len() + 1 + tag.len(),
828+
),
829+
));
830+
}
831+
} else {
832+
return Err(BoxedError::error(
833+
"Invalid mzTab modification",
834+
"An mzTab modification should be in format 'tag:value' but the colon (':') is missing",
835+
Context::line_range(
836+
Some(line_index),
837+
line,
838+
range.start + pos.len() + 1..range.end,
839+
),
840+
));
841+
};
842+
843+
Ok(match position {
844+
None => MZTabReturnModification::GlobalAmbiguous(modification),
845+
Some(pos) => MZTabReturnModification::Defined(pos, modification),
846+
})
847+
} else {
848+
Err(BoxedError::error(
849+
"Invalid modification",
850+
"A modification should be the position followed by a hyphen ('-') followed by the modification",
851+
Context::line_range(Some(line_index), line, range),
852+
))
853+
}
854+
}
855+
698856
#[derive(Clone, Copy, Debug)]
699857
struct PSMLine<'a> {
700858
line_index: usize,
@@ -704,7 +862,7 @@ struct PSMLine<'a> {
704862
}
705863

706864
impl<'a> PSMLine<'a> {
707-
/// Form a indexable line out of a set of fields
865+
/// Form an indexable line out of a set of fields
708866
/// # Errors
709867
/// When there is no header or the line has a different number of columns
710868
fn new(
@@ -760,7 +918,7 @@ impl<'a> PSMLine<'a> {
760918
}
761919
}
762920

763-
impl From<MZTabData> for IdentifiedPeptidoform<SemiAmbiguous, MaybePeptidoform> {
921+
impl From<MZTabData> for IdentifiedPeptidoform<SimpleLinear, MaybePeptidoform> {
764922
fn from(value: MZTabData) -> Self {
765923
Self {
766924
score: (!value.search_engine.is_empty())

rustyms/src/identification/formats/mztab_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
identification::{
88
IdentifiedPeptidoform, MZTabData, MaybePeptidoform, test_identified_peptidoform,
99
},
10-
sequence::SemiAmbiguous,
10+
sequence::SimpleLinear,
1111
};
1212

1313
#[test]
@@ -99,7 +99,7 @@ fn contranovo_v1_0_0() {
9999
fn open_file(reader: impl BufRead) -> Result<usize, BoxedError<'static>> {
100100
let mut peptides = 0;
101101
for read in MZTabData::parse_reader(reader, None) {
102-
let peptide: IdentifiedPeptidoform<SemiAmbiguous, MaybePeptidoform> =
102+
let peptide: IdentifiedPeptidoform<SimpleLinear, MaybePeptidoform> =
103103
read.map_err(BoxedError::to_owned)?.into();
104104
peptides += 1;
105105

rustyms/src/identification/general.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use custom_error::*;
55
use crate::{
66
identification::*,
77
ontology::CustomDatabase,
8-
sequence::{Linked, SemiAmbiguous},
8+
sequence::{Linked, SemiAmbiguous, SimpleLinear},
99
};
1010

1111
// TODO:
@@ -123,7 +123,7 @@ pub fn open_identified_peptidoforms_file<'a>(
123123
Some("mztab") => MZTabData::parse_file(path, custom_database).map(|peptides| {
124124
let a: Box<dyn Iterator<Item = Result<IdentifiedPeptidoform<Linked, MaybePeptidoform>, BoxedError>> + 'a>
125125
= Box::new(peptides.into_iter().map(|p| p.map(|p| {
126-
IdentifiedPeptidoform::<SemiAmbiguous, MaybePeptidoform>::from(p).cast()
126+
IdentifiedPeptidoform::<SimpleLinear, MaybePeptidoform>::from(p).cast()
127127
})));
128128
a
129129
}),

0 commit comments

Comments
 (0)