uno-ms 0.1.0

MS1-only peptide and protein identification from LC-MS features, with target-decoy FDR
//! `uno quant`: DirectMS1Quant-style two-condition contrast inside uno.
//!
//! Native mode (`--s1/--s2`) reads ms1searchpy tables and reproduces the pinned
//! reference. Uno mode (`--samples`) reads uno per-run searches
//! (`--out-proteins`, `--out-quant-evidence`); with no option flags it is the
//! `reference` adaptation that the former `uno-quant` binary implemented.
use anyhow::Result;
use clap::Args;
use std::path::PathBuf;
use uno_core::quant::directms1::{
    self as dms1,
    input::{self, ReferenceOptions},
    uno_adapter, Config,
};

#[derive(Debug, Args)]
pub struct QuantArgs {
    #[arg(long,num_args=1..,required_unless_present="samples",conflicts_with="samples")]
    s1: Vec<PathBuf>,
    #[arg(long,num_args=1..,required_unless_present="samples",conflicts_with="samples")]
    s2: Vec<PathBuf>,
    /// Uno manifest: sample, condition (S1/S2), evidence, proteins. Experimental adaptation.
    #[arg(long, conflicts_with_all=["all_proteins","all_pfms","allowed_proteins","allowed_peptides"])]
    samples: Option<PathBuf>,
    /// Reject features supporting more than one admitted precursor identity (symmetric target/decoy).
    #[arg(long, requires = "samples")]
    exclusive_features: bool,
    /// Require this many observed runs in EACH condition before reference statistics.
    #[arg(long, requires = "samples", default_value_t = 0)]
    min_per_condition: usize,
    /// Give each feature only to its best row by per-run protein binomial score (the matrix-path rule).
    #[arg(long, requires = "samples")]
    feature_competition: bool,
    /// Keep only confident assignments from the manifest's `peptide_scores` (uno --out-peptide-scores).
    #[arg(long, requires = "samples", default_value = "none")]
    peptide_confidence: uno_adapter::PeptideConfidence,
    /// Per-run target-decoy q-value threshold on `ml` for `--peptide-confidence qvalue`.
    #[arg(long, requires = "samples", default_value_t = 0.01)]
    peptide_qvalue: f64,
    /// `apex` (exported intensity) or `integrated` (intensitySum from the manifest's `features`).
    #[arg(long, requires = "samples", default_value = "apex")]
    intensity: uno_adapter::IntensityKind,
    /// Representative observation per precursor and run: `max-intensity` or `min-deviation`.
    #[arg(long, requires = "samples", default_value = "max-intensity")]
    reduction: uno_adapter::Reduction,
    #[arg(long, default_value_t = 0.01)]
    max_protein_qvalue: f64,
    #[arg(long)]
    out: PathBuf,
    #[arg(long, default_value_t = 0)]
    min_samples: usize,
    #[arg(long, default_value_t = 7)]
    min_length: usize,
    #[arg(long, default_value_t = 2)]
    normalization: u8,
    #[arg(long, default_value_t = 2.)]
    fold_change: f64,
    #[arg(long)]
    absolute_fold_change: bool,
    #[arg(long, default_value_t = 0.05)]
    qvalue: f64,
    #[arg(long, default_value = "DECOY_")]
    decoy_prefix: String,
    #[arg(long, default_value_t = 42)]
    seed: u32,
    #[arg(long)]
    all_proteins: bool,
    #[arg(long)]
    all_pfms: bool,
    #[arg(long)]
    allowed_proteins: Option<PathBuf>,
    #[arg(long)]
    allowed_peptides: Option<PathBuf>,
}

pub fn run(a: QuantArgs) -> Result<()> {
    let cfg = Config {
        min_samples: a.min_samples,
        min_length: a.min_length,
        normalization: a.normalization,
        fold_change: a.fold_change,
        absolute_fold_change: a.absolute_fold_change,
        qvalue: a.qvalue,
        decoy_prefix: a.decoy_prefix,
        seed: a.seed,
    };
    let options = ReferenceOptions {
        all_proteins: a.all_proteins,
        all_pfms: a.all_pfms,
        allowed_proteins: a.allowed_proteins,
        allowed_peptides: a.allowed_peptides,
    };
    let (data, hashes, mode, admission) = if let Some(path) = &a.samples {
        let policy = uno_adapter::AdmissionPolicy {
            exclusive_features: a.exclusive_features,
            min_per_condition: a.min_per_condition,
            feature_competition: a.feature_competition,
            peptide_confidence: a.peptide_confidence,
            peptide_qvalue: if a.peptide_confidence == uno_adapter::PeptideConfidence::Qvalue {
                a.peptide_qvalue
            } else {
                0.
            },
            intensity: a.intensity,
            reduction: a.reduction,
        };
        let (data, hashes, audit) =
            uno_adapter::load_with_policy(path, &cfg, a.max_protein_qvalue, &policy)?;
        (data, hashes, "uno_ms1_adaptation", Some(audit))
    } else {
        let (data, hashes) = input::reference(&a.s1, &a.s2, &cfg, &options)?;
        (data, hashes, "native_directms1quant", None)
    };
    log::info!("quant input: {} precursor rows", data.rows.len());
    let analysis = dms1::analyze(&data, &cfg)?;
    log::info!("quant analysis done");
    dms1::output::write_with_admission(&a.out, &analysis, &hashes, mode, admission.as_ref())?;
    eprintln!(
        "{} eligible precursors; {} target proteins; {} significant (experimental, unvalidated)",
        analysis.eligible_precursors,
        analysis.proteins.len(),
        analysis
            .proteins
            .iter()
            .filter(|p| p.bh_pass && p.fc_pass)
            .count()
    );
    Ok(())
}