uno-ms-core 0.1.0

Search, scoring, FDR and quantitative contrast library behind the uno MS1-only search engine
Documentation
use super::{input::InputHash, Analysis};
use anyhow::{Context, Result};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::path::Path;
fn writer(path: &Path) -> Result<csv::Writer<std::fs::File>> {
    Ok(csv::WriterBuilder::new().delimiter(b'\t').from_path(path)?)
}
fn tf(v: bool) -> String {
    if v { "True" } else { "False" }.into()
}
pub fn write(out: &Path, a: &Analysis, inputs: &[InputHash], mode: &str) -> Result<()> {
    write_with_admission(out, a, inputs, mode, None)
}
pub fn write_with_admission(
    out: &Path,
    a: &Analysis,
    inputs: &[InputHash],
    mode: &str,
    admission: Option<&super::uno_adapter::AdmissionAudit>,
) -> Result<()> {
    // Exclusive directory creation prevents accidental replacement of a previous analysis.
    std::fs::create_dir(out)
        .with_context(|| format!("create a NEW output directory {}", out.display()))?;
    let mut p = writer(&out.join("peptides.tsv"))?;
    let mut headers = vec![
        "proteins",
        "origseq",
        "peptide",
        "decoy",
        "nummissing",
        "nonmissing_S1",
        "nonmissing_S2",
        "intensity_median",
        "iq",
        "FC_raw",
        "FC",
        "FC_corrected",
        "p-value",
        "FC_gr_mean",
        "bp",
        "sign",
        "up",
        "down",
    ]
    .into_iter()
    .map(str::to_owned)
    .collect::<Vec<_>>();
    headers.extend(a.samples.clone());
    p.write_record(headers)?;
    for x in &a.peptides {
        let mut row = vec![
            x.protein.clone(),
            x.sequence.clone(),
            x.peptide.clone(),
            tf(x.decoy),
            x.missing.to_string(),
            x.observed_s1.to_string(),
            x.observed_s2.to_string(),
            x.intensity.to_string(),
            x.bin.to_string(),
            x.raw_fc.to_string(),
            x.fc.to_string(),
            x.corrected_fc.to_string(),
            x.pvalue.to_string(),
            x.running_fc.to_string(),
            x.better.to_string(),
            tf(x.significant),
            tf(x.up),
            tf(x.down),
        ];
        row.extend(x.intensities.iter().map(ToString::to_string));
        p.write_record(row)?;
    }
    p.flush()?;
    let headers = [
        "score",
        "p-value",
        "dbname",
        "log2FoldChange(S2/S1)",
        "differentially expressed peptides",
        "identified peptides",
        "log2FoldChange(S2/S1) no normalization",
        "log2FoldChange(S2/S1) using all peptides",
        "log2FoldChange(S2/S1) using all peptides and no normalization",
        "protein_quant_group",
        "FC_pass",
        "FDR_pass",
        "BH_pass",
    ];
    let mut full = writer(&out.join("proteins.tsv"))?;
    full.write_record(headers)?;
    let mut significant = writer(&out.join("significant.tsv"))?;
    significant.write_record(headers)?;
    for x in &a.proteins {
        let row = [
            x.score.to_string(),
            x.pvalue.to_string(),
            x.name.clone(),
            x.fc.to_string(),
            x.n_changed.to_string(),
            x.n_peptides.to_string(),
            x.raw_fc.to_string(),
            x.all_fc.to_string(),
            x.all_raw_fc.to_string(),
            x.group.to_string(),
            tf(x.fc_pass),
            x.fdr_pass.map(tf).unwrap_or_default(),
            tf(x.bh_pass),
        ];
        full.write_record(&row)?;
        if x.bh_pass && x.fc_pass {
            significant.write_record(&row)?;
        }
    }
    full.flush()?;
    significant.flush()?;
    let mut files = std::collections::BTreeMap::new();
    for name in ["peptides.tsv", "proteins.tsv", "significant.tsv"] {
        files.insert(
            name,
            format!("{:x}", Sha256::digest(std::fs::read(out.join(name))?)),
        );
    }
    let manifest = json!({"schema_version":1,"admission_audit":admission,"executable_sha256":format!("{:x}",Sha256::digest(std::fs::read(std::env::current_exe()?)?)),"peptide_sample_columns":"normalized intensities with reference minimum-intensity filling; not raw observed measurements","status":"experimental_not_biologically_validated","mode":mode,"reference_commit":"9df4a941a901aac657b1c9ab5229cb49671e3c7d","reference_sha256":"cb15b7c43b276eb45e77da2b8cf7cd97cd20765f95264202283212fde85cfc90","crate_version":env!("CARGO_PKG_VERSION"),"arguments":std::env::args().collect::<Vec<_>>(),"config":a.config,"samples":a.samples,"condition_1_count":a.n_s1,"inputs":inputs,"outputs":files,"normalization_factors":a.normalization_factors,"calibration_mean":a.calibration_mean,"calibration_std":a.calibration_std,"fold_change_threshold":a.fold_change_threshold,"decoy_probability":a.decoy_probability,"decoy_sequences":a.n_decoy_sequences,"input_rows":a.input_rows,"eligible_precursors":a.eligible_precursors,"protein_rows":a.proteins.len(),"significant_proteins":a.proteins.iter().filter(|p|p.bh_pass&&p.fc_pass).count(),"semantics":"FDR_pass and BH_pass reproduce reference decisions, not newly validated q-values. Scalar NumPy 1.26 quicksort tie ordering; reference CPU SIMD sort must be disabled. Group numbers are opaque. Uno mode uses protein-admitted RT/mass assignments, not reference PFM-ML admission."});
    std::fs::write(
        out.join("manifest.json"),
        serde_json::to_string_pretty(&manifest)?,
    )?;
    Ok(())
}