xcell-rust 0.1.0

Pure-Rust port of xCell (Aran et al. 2017) cell-type enrichment — ssGSEA, spillover-corrected — validated for numeric parity against the R xCell package. Built on gsva-rust.
Documentation
//! Stage 2 of xCell: `transformScores`.
//!
//! Calibrates each cell type's raw enrichment with a per-cell-type power curve.

use gsva::EnrichmentResult;

use crate::data::SpillModel;

/// Mirrors xCell 1.1.0 `transformScores`. For each cell type present in `fv`:
/// `t = max(0, (raw - rowMin) / 5000)`, then `t^V2 / (V3 * 2)`. With
/// `scale = false`, `V3` is forced to 1 (so the divisor is exactly 2).
pub fn transform_scores(
    raw: &EnrichmentResult,
    spill: &SpillModel,
    scale: bool,
) -> EnrichmentResult {
    let nsamp = raw.samples.len();
    let mut out_types = Vec::new();
    let mut scores = Vec::new();

    for (i, ct) in raw.gene_sets.iter().enumerate() {
        // rows <- rownames(scores)[rownames(scores) %in% rownames(fit.vals)]
        let Some(v2) = spill.v2(ct) else { continue };
        let v3 = if scale {
            spill.v3(ct).expect("fv has V2 but not V3")
        } else {
            1.0
        };
        let row = &raw.scores[i * nsamp..(i + 1) * nsamp];
        let mn = row.iter().copied().fold(f64::INFINITY, f64::min);
        let denom = v3 * 2.0;

        out_types.push(ct.clone());
        for &x in row {
            let mut t = (x - mn) / 5000.0;
            if t < 0.0 {
                t = 0.0;
            }
            scores.push(t.powf(v2) / denom);
        }
    }

    EnrichmentResult {
        gene_sets: out_types,
        samples: raw.samples.clone(),
        scores,
    }
}