kira-spliceqc 0.2.0

Deterministic, explainable splicing QC for single-cell expression data.
Documentation
use crate::model::collapse::{SpliceosomeCollapseMetrics, SpliceosomeCollapseStatus};
use crate::model::cryptic_risk::CrypticSplicingRiskMetrics;
use crate::model::sis::{SpliceIntegrityClass, SpliceIntegrityMetrics};
use crate::stats::robust::median;

pub fn format_summary(
    metrics: &SpliceIntegrityMetrics,
    cryptic: Option<&CrypticSplicingRiskMetrics>,
    collapse: Option<&SpliceosomeCollapseMetrics>,
    cell_cycle_confounded: Option<&[bool]>,
) -> String {
    let n_cells = metrics.sis.len();
    let mut intact = 0usize;
    let mut stressed = 0usize;
    let mut impaired = 0usize;
    let mut broken = 0usize;

    for class in &metrics.class {
        match class {
            SpliceIntegrityClass::Intact => intact += 1,
            SpliceIntegrityClass::Stressed => stressed += 1,
            SpliceIntegrityClass::Impaired => impaired += 1,
            SpliceIntegrityClass::Broken => broken += 1,
        }
    }

    let med = median(&metrics.sis);
    let fail = impaired + broken;
    let pct = |count: usize| {
        if n_cells == 0 {
            0.0
        } else {
            (count as f32 / n_cells as f32) * 100.0
        }
    };

    let cryptic_pct = cryptic.map(|metrics| {
        let mut flagged = 0usize;
        let mut total = 0usize;
        for value in &metrics.cryptic_risk {
            if value.is_finite() {
                total += 1;
                if *value > 0.7 {
                    flagged += 1;
                }
            }
        }
        if total == 0 {
            None
        } else {
            Some((flagged as f32 / total as f32) * 100.0)
        }
    });

    let collapse_pct = collapse.map(|metrics| {
        let mut flagged = 0usize;
        let mut total = 0usize;
        for status in &metrics.collapse_status {
            if *status != SpliceosomeCollapseStatus::Inconclusive {
                total += 1;
                if *status == SpliceosomeCollapseStatus::Collapse {
                    flagged += 1;
                }
            }
        }
        if total == 0 {
            None
        } else {
            Some((flagged as f32 / total as f32) * 100.0)
        }
    });

    let cell_cycle_pct = cell_cycle_confounded.map(|flags| {
        let mut flagged = 0usize;
        for value in flags {
            if *value {
                flagged += 1;
            }
        }
        if flags.is_empty() {
            None
        } else {
            Some((flagged as f32 / flags.len() as f32) * 100.0)
        }
    });

    format!(
        "kira-spliceqc summary\n---------------------\nCells analyzed: {}\n\nIntegrity classes:\n  Intact:    {:>4} ({:.1}%)\n  Stressed:  {:>4} ({:.1}%)\n  Impaired:  {:>4} ({:.1}%)\n  Broken:    {:>4} ({:.1}%)\n\nMedian SIS: {:.2}\nFailure fraction (Impaired+Broken): {:.1}%\n\nCryptic splicing risk > 0.7: {}\nSpliceosome collapse: {}\nCell-cycle confounded: {}\n",
        n_cells,
        intact,
        pct(intact),
        stressed,
        pct(stressed),
        impaired,
        pct(impaired),
        broken,
        pct(broken),
        med,
        pct(fail),
        fmt_pct(cryptic_pct),
        fmt_pct(collapse_pct),
        fmt_pct(cell_cycle_pct)
    )
}

fn fmt_pct(value: Option<Option<f32>>) -> String {
    match value {
        Some(Some(pct)) => format!("{pct:.1}%"),
        _ => "N/A".to_string(),
    }
}