mzident 0.2.0

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

use context_error::{BasicKind, BoxedError, FullErrorContent};
use itertools::Itertools;
use mzannotate::prelude::AnnotatedSpectrum;
use mzcore::{
    chemistry::OutputMolecularFormula,
    ontology::Ontologies,
    prelude::*,
    sequence::{FlankingSequence, Linked},
    system::{Mass, MassOverCharge, Time, isize::Charge},
};

use crate::{
    GeneralPSMs, KnownFileFormat, MaybePeptidoform, PSM, PSMData, PSMMetaData, SpectrumId,
    SpectrumIds,
};

impl<Mode: MassOutputMode> PSMMetaData for AnnotatedSpectrum<Mode> {
    type Protein = crate::NoProtein;
    type SpectrumOutputMode = Mode;

    fn peptidoform_ion_set(&self) -> Option<Cow<'_, PeptidoformIonSet>> {
        let cpi: PeptidoformIonSet = self
            .analytes
            .iter()
            .filter_map(|a| match &a.target {
                mzannotate::mzspeclib::AnalyteTarget::PeptidoformIon(pep) => Some(pep.clone()),
                _ => None,
            })
            .collect();
        cpi.peptidoform_ions().is_empty().not().then_some(Cow::Owned(cpi))
    }

    fn format(&self) -> KnownFileFormat {
        KnownFileFormat::AnnotatedSpectrum
    }

    fn numerical_id(&self) -> Option<usize> {
        Some(self.description.index)
    }

    fn id(&self) -> String {
        self.description.index.to_string()
    }

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

    fn confidence(&self) -> Option<f64> {
        self.interpretations
            .iter()
            .filter_map(|i| i.probability)
            .exactly_one()
            .ok()
    }

    fn local_confidence(&self) -> Option<Cow<'_, [f64]>> {
        None
    }

    fn original_confidence(&self) -> Option<(f64, mzcv::Term)> {
        self.interpretations
            .iter()
            .filter_map(|i| i.probability)
            .exactly_one()
            .ok()
            .map(|v| (v, mzcv::term!(MS:1001153|search engine specific score)))
    }

    fn original_local_confidence(&self) -> Option<&[f64]> {
        None
    }

    fn charge(&self) -> Option<Charge> {
        self.analytes
            .iter()
            .filter_map(|a| match &a.target {
                mzannotate::mzspeclib::AnalyteTarget::PeptidoformIon(pep) => Some(pep),
                _ => None,
            })
            .exactly_one()
            .ok()
            .and_then(|p| p.get_charge_carriers().map(MolecularCharge::charge))
    }

    #[allow(clippy::redundant_closure_for_method_calls)] // Do not want to have to reference mzdata
    fn mode(&self) -> Option<Cow<'_, str>> {
        self.description
            .precursor
            .first()
            .map(|p| Cow::Owned(p.activation.methods().iter().map(|d| d.name()).join("+")))
    }

    fn fragmentation_model(
        &self,
    ) -> Option<mzannotate::annotation::model::BuiltInFragmentationModel> {
        self.description.precursor.first().map(|p| p.activation.methods().into())
    }

    fn retention_time(&self) -> Option<Time> {
        self.description
            .acquisition
            .scans
            .first()
            .map(|s| Time::new::<mzcore::system::time::s>(s.start_time))
    }

    fn scans(&self) -> SpectrumIds {
        self.description
            .params
            .iter()
            .find(|p| {
                p.controlled_vocabulary.is_some_and(|cv| cv.prefix() == "MS")
                    && p.accession == Some(1003203)
            })
            .map_or_else(
                || SpectrumIds::FileNotKnown(vec![SpectrumId::Index(self.description.index)]),
                |rawfile| {
                    SpectrumIds::FileKnown(vec![(rawfile.value.to_string().into(), vec![
                        SpectrumId::Index(self.description.index),
                    ])])
                },
            )
    }

    fn experimental_mz(&self) -> Option<MassOverCharge> {
        self.description
            .precursor
            .first()
            .and_then(|p| p.ions.first())
            .map(|i| MassOverCharge::new::<mzcore::system::mass_over_charge::thomson>(i.mz))
    }

    fn experimental_mass(&self) -> Option<Mass> {
        self.description
            .precursor
            .first()
            .and_then(|p| p.ions.first())
            .and_then(|i| {
                i.charge
                    .map(|c| Mass::new::<mzcore::system::mass::dalton>(i.mz * f64::from(c)))
            })
    }

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

    fn flanking_sequences(&self) -> (&FlankingSequence, &FlankingSequence) {
        (
            self.analytes
                .iter()
                .flat_map(|a| &a.proteins)
                .map(|p| &p.flanking_sequences.0)
                .exactly_one()
                .unwrap_or(FlankingSequence::UNKNOWN),
            self.analytes
                .iter()
                .flat_map(|a| &a.proteins)
                .map(|p| &p.flanking_sequences.1)
                .exactly_one()
                .unwrap_or(FlankingSequence::UNKNOWN),
        )
    }

    fn database(&self) -> Option<(&str, Option<&str>)> {
        self.analytes
            .iter()
            .flat_map(|a| &a.proteins)
            .map(|p| p.database_name.as_deref())
            .exactly_one()
            .ok()
            .flatten()
            .map(|name| {
                (
                    name,
                    self.analytes
                        .iter()
                        .flat_map(|a| &a.proteins)
                        .map(|p| p.database_version.as_deref())
                        .exactly_one()
                        .ok()
                        .flatten(),
                )
            })
    }

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

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

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

    fn annotated_spectrum(&self) -> Option<Cow<'_, AnnotatedSpectrum<Mode>>> {
        Some(Cow::Borrowed(self))
    }

    fn has_annotated_spectrum(&self) -> bool {
        true
    }
}

impl From<AnnotatedSpectrum<OutputMolecularFormula>> for PSM<Linked, MaybePeptidoform> {
    fn from(value: AnnotatedSpectrum<OutputMolecularFormula>) -> Self {
        Self {
            score: value.confidence(),
            local_confidence: value.local_confidence().map(|v| v.to_vec()),
            data: PSMData::AnnotatedSpectrum(value),
            complexity_marker: PhantomData,
            peptidoform_availability_marker: PhantomData,
        }
    }
}

/// Parse an mzSpecLib file into a general PSM iterator.
/// # Errors
/// If the file is not a valid mzSpecLib file.
pub(crate) fn parse_mzspeclib<'a>(
    path: &Path,
    ontologies: &'a Ontologies,
) -> Result<GeneralPSMs<'a>, BoxedError<'static, BasicKind>> {
    mzannotate::mzspeclib::MzSpecLibTextParser::open_file(path, ontologies)
        .map(move |parser| {
            let b: Box<
                dyn Iterator<
                    Item = Result<PSM<Linked, MaybePeptidoform>, BoxedError<'static, BasicKind>>,
                >,
            > = Box::new(
                parser.map(move |s| s.map(Into::into).map_err(|e| e.convert(|_| BasicKind::Error))),
            );
            b
        })
        .map_err(|e| e.convert(|_| BasicKind::Error))
}