mzident 0.2.0

Handle all kinds of PSM files.
Documentation
use std::{
    borrow::Cow,
    marker::PhantomData,
    ops::Range,
    path::{Path, PathBuf},
};

use mzcore::{
    csv::{CsvLine, parse_csv},
    ontology::Ontologies,
    sequence::{
        FlankingSequence, Peptidoform, PeptidoformIonSet, SemiAmbiguous, SloppyParsingParameters,
    },
    system::{Mass, MassOverCharge, Time, isize::Charge},
};
use serde::{Deserialize, Serialize};

use crate::{
    BoxedIdentifiedPeptideIter, KnownFileFormat, PSM, PSMData, PSMFileFormatVersion, PSMMetaData,
    PSMSource, PeptidoformPresent, SpectrumId, SpectrumIds,
    common_parser::{Location, OptionalColumn},
};

static NUMBER_ERROR: (&str, &str) = (
    "Invalid PowerNovo line",
    "This column is not a number but it is required to be a number in this format",
);

format_family!(
    PowerNovo,
    SemiAmbiguous, PeptidoformPresent, [&POWERNOVO_V1_0_17], b',', None;
    required {
        title: String, |location: Location, _| Ok(location.get_string());
        peptide: Peptidoform<SemiAmbiguous>, |location: Location, ontologies: &Ontologies| Peptidoform::sloppy_pro_forma_inner(
            &location.base_context(),
            location.full_line(),
            location.range.clone(),
            ontologies,
            &SloppyParsingParameters::default(),
        ).map_err(BoxedError::to_owned);
        score: f64, |location: Location, _| location.parse::<f64>(NUMBER_ERROR);
        local_confidence: Vec<f64>, |location: Location, _| location.array(' ')
            .map(|l| l.parse::<f64>(NUMBER_ERROR))
            .collect::<Result<Vec<_>, _>>();
    }
    optional {
        raw_file: PathBuf, |location: Location, _| Ok(Path::new(&location.get_string()).to_owned());
        scan: usize, |location: Location, _| location.parse::<usize>(NUMBER_ERROR);
    }

    fn post_process(_source: &CsvLine, mut parsed: Self, _ontologies: &Ontologies) -> Result<Self, BoxedError<'static, BasicKind>> {
        if let Some((file, id)) = parsed.title.split_once(":index=")
        {
            parsed.raw_file = Some(PathBuf::from(file));
            parsed.scan = Some(id.parse::<usize>().map_err(|err| BoxedError::new(BasicKind::Error, "Invalid PowerNovo ID", format!("The scan number {}", crate::helper_functions::explain_number_error(&err)), Context::default().lines(0, &parsed.title).add_highlight((0, file.len() + 7, id.len())).to_owned()))?);
        }
        Ok(parsed)
    }
);

/// The only known version of PowerNovo
pub const POWERNOVO_V1_0_17: PowerNovoFormat = PowerNovoFormat {
    version: PowerNovoVersion::V1_0_17,
    scan: OptionalColumn::NotAvailable,
    raw_file: OptionalColumn::NotAvailable,
    title: "spectrum name",
    peptide: "powernovo peptides",
    score: "powernovo score",
    local_confidence: "powernovo aascore",
};

/// All possible PowerNovo versions
#[derive(
    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize,
)]
pub enum PowerNovoVersion {
    #[default]
    /// PowerNovo version 1.0.1
    V1_0_17,
}

impl std::fmt::Display for PowerNovoVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.name())
    }
}

impl PSMFileFormatVersion<PowerNovoFormat> for PowerNovoVersion {
    fn format(self) -> PowerNovoFormat {
        match self {
            Self::V1_0_17 => POWERNOVO_V1_0_17,
        }
    }

    fn name(self) -> &'static str {
        match self {
            Self::V1_0_17 => "v1.0.17",
        }
    }
}

impl PSMMetaData for PowerNovoPSM {
    type Protein = crate::NoProtein;
    #[cfg(feature = "mzannotate")]
    type SpectrumOutputMode = mzcore::chemistry::OutputMolecularFormula;

    fn peptidoform_ion_set(&self) -> Option<Cow<'_, PeptidoformIonSet>> {
        Some(Cow::Owned(self.peptide.clone().into()))
    }

    fn format(&self) -> KnownFileFormat {
        KnownFileFormat::PowerNovo(self.version)
    }

    fn numerical_id(&self) -> Option<usize> {
        self.scan
    }

    fn id(&self) -> String {
        self.scan.as_ref().map_or_else(|| "-".to_string(), ToString::to_string)
    }

    fn search_engine(&self) -> Option<mzcv::Term> {
        None
    }

    fn confidence(&self) -> Option<f64> {
        Some(self.score)
    }

    fn local_confidence(&self) -> Option<Cow<'_, [f64]>> {
        Some(Cow::Borrowed(self.local_confidence.as_slice()))
    }

    fn original_confidence(&self) -> Option<(f64, mzcv::Term)> {
        Some((
            self.score,
            mzcv::term!(MS:1001153|search engine specific score),
        ))
    }

    fn original_local_confidence(&self) -> Option<&[f64]> {
        Some(self.local_confidence.as_slice())
    }

    fn charge(&self) -> Option<Charge> {
        None
    }

    fn mode(&self) -> Option<Cow<'_, str>> {
        None
    }

    fn retention_time(&self) -> Option<Time> {
        None
    }

    fn scans(&self) -> SpectrumIds {
        self.scan.as_ref().map_or(SpectrumIds::None, |scan| {
            self.raw_file.clone().map_or_else(
                || SpectrumIds::FileNotKnown(vec![SpectrumId::Index(*scan)]),
                |raw_file| SpectrumIds::FileKnown(vec![(raw_file, vec![SpectrumId::Index(*scan)])]),
            )
        })
    }

    fn experimental_mz(&self) -> Option<MassOverCharge> {
        None
    }

    fn experimental_mass(&self) -> Option<Mass> {
        None
    }

    fn protein_location(&self) -> Option<Range<u16>> {
        None
    }

    fn flanking_sequences(&self) -> (&FlankingSequence, &FlankingSequence) {
        (&FlankingSequence::Unknown, &FlankingSequence::Unknown)
    }

    fn database(&self) -> Option<(&str, Option<&str>)> {
        None
    }

    fn unique(&self) -> Option<bool> {
        None
    }

    fn reliability(&self) -> Option<crate::Reliability> {
        None
    }

    fn uri(&self) -> Option<String> {
        None
    }
}