Skip to main content

salmon_model/
dumps.rs

1//! Aux-output dump helpers shared by reads-mode and alignment-mode quant.
2//!
3//! salmon writes several `aux_info` files in raw little-endian binary, gzipped:
4//! `fld.gz` (fragment-length sample histogram), the legacy simple-count seq-bias
5//! model (`observed_bias`/`observed_bias_3p`/`expected_bias`), and — under bias
6//! correction — the per-model observed/expected tables. The Rust port computes
7//! the `SBModel`/GC/positional models (dumped here as a documented Rust format:
8//! gzip of raw LE arrays; positional files carry a small header) but not salmon's
9//! legacy simple-count model, which is written as a documented stub for
10//! file-presence parity. `libParams/flenDist.txt` is the text PMF.
11
12use std::io::Write;
13use std::path::Path;
14
15/// Flattened observed/expected bias-model tables captured for the dump files.
16/// Each group is empty when its correction was not enabled. Seq tables are the
17/// [`SBModel`](crate::seqbias::SBModel) transition tables; GC the `cond×gc`
18/// matrices; pos the per-length-class bin masses.
19#[derive(Debug, Clone, Default)]
20pub struct BiasDump {
21    pub obs5_seq: Vec<f64>,
22    pub obs3_seq: Vec<f64>,
23    pub exp5_seq: Vec<f64>,
24    pub exp3_seq: Vec<f64>,
25    pub obs_gc: Vec<f64>,
26    pub exp_gc: Vec<f64>,
27    pub obs5_pos: Vec<Vec<f64>>,
28    pub obs3_pos: Vec<Vec<f64>>,
29    pub exp5_pos: Vec<Vec<f64>>,
30    pub exp3_pos: Vec<Vec<f64>>,
31}
32
33/// Write the observed/expected bias-model tables to a human-readable text file
34/// (`--dumpBiasModels`). One line per row: `<name> [<lc>] v0 v1 ...`. Empty
35/// groups (corrections not enabled) are skipped. Intended for debugging and
36/// C++↔Rust parity comparison, not as a stable machine format.
37pub fn dump_bias_models_to_file(path: &Path, d: &BiasDump) -> std::io::Result<()> {
38    let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
39    let flat = |f: &mut std::io::BufWriter<std::fs::File>, name: &str, v: &[f64]| {
40        if v.is_empty() {
41            return Ok(());
42        }
43        write!(f, "{name}")?;
44        for x in v {
45            write!(f, " {x:.6}")?;
46        }
47        writeln!(f)
48    };
49    let per_lc = |f: &mut std::io::BufWriter<std::fs::File>, name: &str, v: &[Vec<f64>]| {
50        for (lc, row) in v.iter().enumerate() {
51            write!(f, "{name} {lc}")?;
52            for x in row {
53                write!(f, " {x:.6}")?;
54            }
55            writeln!(f)?;
56        }
57        Ok::<(), std::io::Error>(())
58    };
59    flat(&mut f, "obs5_seq", &d.obs5_seq)?;
60    flat(&mut f, "obs3_seq", &d.obs3_seq)?;
61    flat(&mut f, "exp5_seq", &d.exp5_seq)?;
62    flat(&mut f, "exp3_seq", &d.exp3_seq)?;
63    flat(&mut f, "obs_gc", &d.obs_gc)?;
64    flat(&mut f, "exp_gc", &d.exp_gc)?;
65    per_lc(&mut f, "obs5_pos", &d.obs5_pos)?;
66    per_lc(&mut f, "obs3_pos", &d.obs3_pos)?;
67    per_lc(&mut f, "exp5_pos", &d.exp5_pos)?;
68    per_lc(&mut f, "exp3_pos", &d.exp3_pos)?;
69    f.flush()
70}
71
72/// gzip a raw byte buffer to `path` (level 6, matching salmon's aux dumps).
73pub fn gz_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
74    let f = std::fs::File::create(path)?;
75    let mut enc = flate2::write::GzEncoder::new(f, flate2::Compression::new(6));
76    enc.write_all(bytes)?;
77    enc.finish()?;
78    Ok(())
79}
80
81/// gzip of a raw little-endian `f64` array.
82pub fn write_f64_gz(path: &Path, vals: &[f64]) -> std::io::Result<()> {
83    let mut b = Vec::with_capacity(vals.len() * 8);
84    for v in vals {
85        b.extend_from_slice(&v.to_le_bytes());
86    }
87    gz_write(path, &b)
88}
89
90/// gzip of a raw little-endian `i32` array.
91pub fn write_i32_gz(path: &Path, vals: &[i32]) -> std::io::Result<()> {
92    let mut b = Vec::with_capacity(vals.len() * 4);
93    for v in vals {
94        b.extend_from_slice(&v.to_le_bytes());
95    }
96    gz_write(path, &b)
97}
98
99/// gzip of a per-length-class positional model: header `[u32 num_models][u32
100/// bins_per_model]` then the models' bin masses as `f64` LE, row-major.
101pub fn write_pos_gz(path: &Path, models: &[Vec<f64>]) -> std::io::Result<()> {
102    let bins = models.first().map(|m| m.len()).unwrap_or(0) as u32;
103    let mut b = Vec::new();
104    b.extend_from_slice(&(models.len() as u32).to_le_bytes());
105    b.extend_from_slice(&bins.to_le_bytes());
106    for m in models {
107        for v in m {
108            b.extend_from_slice(&v.to_le_bytes());
109        }
110    }
111    gz_write(path, &b)
112}
113
114/// `aux_info/fld.gz`: per-length sample histogram. salmon draws 10,000 samples
115/// from the log-PMF and writes the per-length `i32` counts; we write the
116/// deterministic expected histogram `round(10000 * pmf[len])` (same type/layout).
117pub fn write_fld_dump(path: &Path, pmf: &[f64]) -> std::io::Result<()> {
118    const N_SAMPLES: f64 = 10000.0;
119    let hist: Vec<i32> = pmf
120        .iter()
121        .map(|&p| (p * N_SAMPLES).round() as i32)
122        .collect();
123    write_i32_gz(path, &hist)
124}
125
126/// `libParams/flenDist.txt`: the normalized fragment-length PMF as a single line
127/// of tab-separated scientific-notation values (salmon's format).
128pub fn write_flen_dist(path: &Path, pmf: &[f64]) -> std::io::Result<()> {
129    if let Some(parent) = path.parent() {
130        std::fs::create_dir_all(parent)?;
131    }
132    let mut s = String::with_capacity(pmf.len() * 14);
133    for (i, p) in pmf.iter().enumerate() {
134        if i > 0 {
135            s.push('\t');
136        }
137        s.push_str(&format!("{p:e}"));
138    }
139    s.push('\n');
140    std::fs::write(path, s)
141}
142
143/// Write the `aux_info` bias dumps into `aux_dir`: documented stubs for salmon's
144/// legacy simple-count model (not implemented in the port), plus the computed
145/// seq/GC/pos observed+expected tables present in `dump`.
146pub fn write_aux_bias_dumps(aux_dir: &Path, dump: &BiasDump) -> std::io::Result<()> {
147    // Legacy simple-count seq-bias model (the port uses SBModel instead): stubs.
148    write_i32_gz(&aux_dir.join("observed_bias.gz"), &[0])?;
149    write_i32_gz(&aux_dir.join("observed_bias_3p.gz"), &[0])?;
150    write_f64_gz(&aux_dir.join("expected_bias.gz"), &[1.0])?;
151
152    if !dump.obs5_seq.is_empty() {
153        write_f64_gz(&aux_dir.join("obs5_seq.gz"), &dump.obs5_seq)?;
154        write_f64_gz(&aux_dir.join("obs3_seq.gz"), &dump.obs3_seq)?;
155        write_f64_gz(&aux_dir.join("exp5_seq.gz"), &dump.exp5_seq)?;
156        write_f64_gz(&aux_dir.join("exp3_seq.gz"), &dump.exp3_seq)?;
157    }
158    if !dump.obs_gc.is_empty() {
159        write_f64_gz(&aux_dir.join("obs_gc.gz"), &dump.obs_gc)?;
160        write_f64_gz(&aux_dir.join("exp_gc.gz"), &dump.exp_gc)?;
161    }
162    if !dump.obs5_pos.is_empty() {
163        write_pos_gz(&aux_dir.join("obs5_pos.gz"), &dump.obs5_pos)?;
164        write_pos_gz(&aux_dir.join("obs3_pos.gz"), &dump.obs3_pos)?;
165        write_pos_gz(&aux_dir.join("exp5_pos.gz"), &dump.exp5_pos)?;
166        write_pos_gz(&aux_dir.join("exp3_pos.gz"), &dump.exp3_pos)?;
167    }
168    Ok(())
169}