rustyms 0.10.0

A library to handle proteomic mass spectrometry data and match peptides to spectra.
Documentation
use std::path::{Path, PathBuf};

//Spectrum	Spectrum.File	Peptide	Modified sequence	Extended.Peptide	Prev.AA	Next.AA	Peptide.Length	Charge	Retention	Observed.Mass	Calibrated.Observed.Mass	Observed.M.Z	Calibrated.Observed.M.Z	Calculated.Peptide.Mass	Calculated.M.Z	Delta.Mass	Expectation	Hyperscore	Nextscore	PeptideProphet.Probability	Number.of.Enzymatic.Termini	Number.of.Missed.Cleavages	Protein.Start	Protein.End	Intensity	Assigned.Modifications	Observed.Modifications	Purity	Is.Unique	Protein	Protein.ID	Entry.Name	Gene	Protein.Description	Mapped.Genes	Mapped.Proteins	condition	group
use crate::{
    error::{Context, CustomError},
    helper_functions::explain_number_error,
    identification::SpectrumId,
    ontologies::CustomDatabase,
    peptidoform::{SemiAmbiguous, SloppyParsingParameters},
    system::{usize::Charge, Mass, MassOverCharge, Time},
    Peptidoform,
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use super::{
    common_parser::{Location, OptionalColumn, OptionalLocation},
    csv::{parse_csv, CsvLine},
    fasta::FastaIdentifier,
    BoxedIdentifiedPeptideIter, IdentifiedPeptide, IdentifiedPeptideSource, MetaData,
};

static NUMBER_ERROR: (&str, &str) = (
    "Invalid MSFragger line",
    "This column is not a number but it is required to be a number in this MSFragger format",
);
static IDENTIFIER_ERROR: (&str, &str) = (
    "Invalid MSFragger line",
    "This column is not a fasta identifier but is required to be one in this MSFragger format",
);
static BOOL_ERROR: (&str, &str) = (
    "Invalid MSFragger line",
    "This column is not a boolean but it is required to be a boolean ('true' or 'false') in this MSFragger format",
);

format_family!(
    /// The format for any MSFragger file
    MSFraggerFormat,
    /// The data from any MSFragger file
    MSFraggerData,
    MSFraggerVersion, [&V21, &V22], b'\t', None;
    required {
        scan: SpectrumId, |location: Location, _| Ok(SpectrumId::Native(location.get_string()));
        spectrum_file: PathBuf, |location: Location, _| Ok(location.get_string().into());
        peptide: Option<Peptidoform<SemiAmbiguous>>, |location: Location, custom_database: Option<&CustomDatabase>| location.or_empty().parse_with(|location| Peptidoform::sloppy_pro_forma(
            location.full_line(),
            location.location.clone(),
            custom_database,
            &SloppyParsingParameters {ignore_prefix_lowercase_n: true, ..Default::default()},
        ));
        extended_peptide: Box<[Option<Peptidoform<SemiAmbiguous>>; 3]>, |location: Location, custom_database: Option<&CustomDatabase>| {
            let peptides = location.clone().array('.').map(|l| l.or_empty().parse_with(|location| Peptidoform::sloppy_pro_forma(
                location.full_line(),
                location.location.clone(),
                custom_database,
                &SloppyParsingParameters {ignore_prefix_lowercase_n: true, ..Default::default()},
            ))).collect::<Result<Vec<_>,_>>()?;
            if peptides.len() == 3 {
                Ok(Box::new([peptides[0].clone(), peptides[1].clone(), peptides[2].clone()]))
            } else {
                Err(CustomError::error("Invalid extened peptide", "The extended peptide should contain the prefix.peptide.suffix for all peptides.", location.context()))
            }
        };
        z: Charge, |location: Location, _| location.parse::<usize>(NUMBER_ERROR).map(Charge::new::<crate::system::e>);
        rt: Time, |location: Location, _| location.parse::<f64>(NUMBER_ERROR).map(Time::new::<crate::system::time::s>);
        /// Experimental mass
        mass: Mass, |location: Location, _| location.parse::<f64>(NUMBER_ERROR).map(Mass::new::<crate::system::dalton>);
        calibrated_experimental_mass: Mass, |location: Location, _| location.parse::<f64>(NUMBER_ERROR).map(Mass::new::<crate::system::dalton>);
        /// Experimental mz
        mz: MassOverCharge, |location: Location, _| location.parse::<f64>(NUMBER_ERROR).map(MassOverCharge::new::<crate::system::mz>);
        calibrated_experimental_mz: MassOverCharge, |location: Location, _| location.parse::<f64>(NUMBER_ERROR).map(MassOverCharge::new::<crate::system::mz>);
        expectation: f64, |location: Location, _| location.parse(NUMBER_ERROR);
        hyperscore: f64, |location: Location, _| location.parse(NUMBER_ERROR).map(|s: f64| s / 100.0);
        next_score: f64, |location: Location, _| location.parse(NUMBER_ERROR).map(|s: f64| s / 100.0);
        peptide_prophet_probability: f64, |location: Location, _| location.parse(NUMBER_ERROR);
        enzymatic_termini: usize, |location: Location, _| location.parse(NUMBER_ERROR);
        missed_cleavages: usize, |location: Location, _| location.parse(NUMBER_ERROR);
        protein_start: usize, |location: Location, _| location.parse(NUMBER_ERROR);
        protein_end: usize, |location: Location, _| location.parse(NUMBER_ERROR);
        intensity: f64, |location: Location, _| location.parse(NUMBER_ERROR);
        assigned_modifications: String, |location: Location, _| Ok(location.get_string());
        purity: f64, |location: Location, _| location.parse(NUMBER_ERROR);
        is_unique: bool, |location: Location, _| location.parse(BOOL_ERROR);
        protein: FastaIdentifier<String>, |location: Location, _| location.parse(IDENTIFIER_ERROR);
        protein_id: String, |location: Location, _| Ok(location.get_string());
        entry_name: String, |location: Location, _| Ok(location.get_string());
        gene: String, |location: Location, _| Ok(location.get_string());
        protein_description: String, |location: Location, _| Ok(location.get_string());
        mapped_genes: Vec<String>, |location: Location, _| Ok(location.get_string().split(',').map(|s| s.trim().to_string()).collect_vec());
        mapped_proteins: Vec<String>, |location: Location, _| Ok(location.get_string().split(',').map(|s| s.trim().to_string()).collect_vec());
    }
    optional {
        raw_file: PathBuf, |location: Location, _| Ok(Some(location.get_string().into()));
        condition: String, |location: Location, _| Ok(Some(location.get_string()));
        group: String, |location: Location, _| Ok(Some(location.get_string()));
    }

    fn post_process(_source: &CsvLine, mut parsed: Self, _custom_database: Option<&CustomDatabase>) -> Result<Self, CustomError> {
        if let SpectrumId::Native(native) = &parsed.scan {
            if let Some(m) = IDENTIFER_REGEX
                .get_or_init(|| regex::Regex::new(r"([^/]+)\.(\d+)\.\d+.\d+").unwrap())
                .captures(native)
            {
                parsed.raw_file = Some(m.get(1).unwrap().as_str().into());
                parsed.scan =
                    SpectrumId::Number(m.get(2).unwrap().as_str().parse::<usize>().unwrap());
            }
        }
        if parsed.peptide.is_none() {
            parsed.peptide.clone_from(&parsed.extended_peptide[1]);
        }
        Ok(parsed)
    }
);

/// The Regex to match against MSFragger scan fields
static IDENTIFER_REGEX: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();

impl From<MSFraggerData> for IdentifiedPeptide {
    fn from(value: MSFraggerData) -> Self {
        Self {
            score: Some(value.hyperscore),
            local_confidence: None,
            metadata: MetaData::MSFragger(value),
        }
    }
}

/// All possible MSFragger versions
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize)]
pub enum MSFraggerVersion {
    /// Version 21
    #[default]
    V21,
    /// Version 22
    V22,
}

impl std::fmt::Display for MSFraggerVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
        write!(
            f,
            "{}",
            match self {
                Self::V21 => "v21",
                Self::V22 => "v22",
            }
        )
    }
}

/// v21
pub const V21: MSFraggerFormat = MSFraggerFormat {
    version: MSFraggerVersion::V21,
    scan: "spectrum",
    raw_file: OptionalColumn::NotAvailable,
    spectrum_file: "spectrum file",
    peptide: "modified peptide",
    extended_peptide: "extended peptide",
    z: "charge",
    rt: "retention",
    mass: "observed mass",
    calibrated_experimental_mass: "calibrated observed mass",
    mz: "observed m/z",
    calibrated_experimental_mz: "calibrated observed m/z",
    expectation: "expectation",
    hyperscore: "hyperscore",
    next_score: "nextscore",
    peptide_prophet_probability: "peptideprophet probability",
    enzymatic_termini: "number of enzymatic termini",
    missed_cleavages: "number of missed cleavages",
    protein_start: "protein start",
    protein_end: "protein end",
    intensity: "intensity",
    assigned_modifications: "assigned modifications",
    purity: "purity",
    is_unique: "is unique",
    protein: "protein",
    protein_id: "protein id",
    entry_name: "entry name",
    gene: "gene",
    protein_description: "protein description",
    mapped_genes: "mapped genes",
    mapped_proteins: "mapped proteins",
    condition: OptionalColumn::Optional("condition"),
    group: OptionalColumn::Optional("group"),
};

/// v22
pub const V22: MSFraggerFormat = MSFraggerFormat {
    version: MSFraggerVersion::V22,
    scan: "spectrum",
    raw_file: OptionalColumn::NotAvailable,
    spectrum_file: "spectrum file",
    peptide: "modified peptide",
    extended_peptide: "extended peptide",
    z: "charge",
    rt: "retention",
    mass: "observed mass",
    calibrated_experimental_mass: "calibrated observed mass",
    mz: "observed m/z",
    calibrated_experimental_mz: "calibrated observed m/z",
    expectation: "expectation",
    hyperscore: "hyperscore",
    next_score: "nextscore",
    peptide_prophet_probability: "probability",
    enzymatic_termini: "number of enzymatic termini",
    missed_cleavages: "number of missed cleavages",
    protein_start: "protein start",
    protein_end: "protein end",
    intensity: "intensity",
    assigned_modifications: "assigned modifications",
    purity: "purity",
    is_unique: "is unique",
    protein: "protein",
    protein_id: "protein id",
    entry_name: "entry name",
    gene: "gene",
    protein_description: "protein description",
    mapped_genes: "mapped genes",
    mapped_proteins: "mapped proteins",
    condition: OptionalColumn::Optional("condition"),
    group: OptionalColumn::Optional("group"),
};

/// The scans identifier for a MSFragger identification
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize)]
pub struct MSFraggerID {
    /// The file, if defined
    pub file: PathBuf,
    /// The scan number triplet
    pub scan: (usize, usize, usize),
}

impl std::fmt::Display for MSFraggerID {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}.{}.{}.{}",
            self.file.to_string_lossy(),
            self.scan.0,
            self.scan.1,
            self.scan.2,
        )
    }
}

impl std::str::FromStr for MSFraggerID {
    type Err = CustomError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let split = s.rsplitn(4, '.').collect_vec();
        if split.len() == 4 {
            Ok(Self {
                file: Path::new(&split[3]).to_owned(),
                scan: (
                    split[2].parse().map_err(|err| {
                        CustomError::error(
                            "Invalid MSFragger ID",
                            format!("The scan number {}", explain_number_error(&err)),
                            Context::None,
                        )
                    })?,
                    split[1].parse().map_err(|err| {
                        CustomError::error(
                            "Invalid MSFragger ID",
                            format!("The scan number {}", explain_number_error(&err)),
                            Context::None,
                        )
                    })?,
                    split[0].parse().map_err(|err| {
                        CustomError::error(
                            "Invalid MSFragger ID",
                            format!("The scan number {}", explain_number_error(&err)),
                            Context::None,
                        )
                    })?,
                ),
            })
        } else {
            Err(CustomError::error(
                "Invalid MSFragger ID",
                "An MSFragger ID should consist of 4 parts separated by dots (file.scan.scan.scan)",
                Context::None,
            ))
        }
    }
}