1use salmon_core::atomic::AtomicF64;
13use salmon_core::math::{log_add, LOG_0, LOG_EPSILON};
14use statrs::distribution::{Binomial, ContinuousCDF, Discrete, Normal};
15use std::sync::atomic::{AtomicUsize, Ordering};
16
17#[derive(Debug)]
19pub struct FragmentLengthDistribution {
20 kernel: Vec<f64>,
22 hist: Vec<AtomicF64>,
24 tot_mass: AtomicF64,
26 sum: AtomicF64,
28 min: AtomicUsize,
30 bin_size: usize,
32
33 cached_pmf: Vec<f64>,
35 cached_cmf: Vec<f64>,
37 have_cache: bool,
38}
39
40impl FragmentLengthDistribution {
41 pub fn new(
51 alpha: f64,
52 max_val: usize,
53 prior_mu: f64,
54 prior_sigma: f64,
55 kernel_n: usize,
56 kernel_p: f64,
57 bin_size: usize,
58 ) -> Self {
59 assert!(bin_size >= 1, "bin_size must be >= 1");
60 let max_val = max_val / bin_size;
61 let kernel_n = kernel_n / bin_size;
62 assert!(
63 kernel_n.is_multiple_of(2),
64 "kernel_n must be even after binning"
65 );
66
67 let tot = alpha.ln();
68 let hist: Vec<AtomicF64>;
69 let mut sum = LOG_0;
70 let mut tot_mass;
71
72 if prior_mu > 0.0 {
73 let norm = Normal::new(
74 prior_mu / bin_size as f64,
75 prior_sigma / (bin_size * bin_size) as f64,
76 )
77 .expect("valid normal prior");
78 hist = (0..=max_val).map(|_| AtomicF64::new(LOG_0)).collect();
79 tot_mass = LOG_0;
80 for (i, slot) in hist.iter().enumerate() {
81 let norm_mass = norm.cdf(i as f64 + 0.5) - norm.cdf(i as f64 - 0.5);
82 let mass = if norm_mass != 0.0 {
83 tot + norm_mass.ln()
84 } else {
85 LOG_EPSILON
86 };
87 slot.store(mass);
88 sum = log_add(sum, (i as f64).ln() + mass);
89 tot_mass = log_add(tot_mass, mass);
90 }
91 } else {
92 let per = tot - (max_val as f64).ln();
94 hist = (0..=max_val).map(|_| AtomicF64::new(per)).collect();
95 hist[0].store(LOG_0);
96 let h1 = hist.get(1).map(|a| a.load()).unwrap_or(per);
97 sum = h1 + ((max_val * (max_val + 1)) as f64).ln() - 2.0_f64.ln();
98 tot_mass = tot;
99 }
100
101 let binom = Binomial::new(kernel_p, kernel_n as u64).expect("valid binomial kernel");
103 let kernel: Vec<f64> = (0..=kernel_n).map(|i| binom.pmf(i as u64).ln()).collect();
104
105 Self {
106 kernel,
107 hist,
108 tot_mass: AtomicF64::new(tot_mass),
109 sum: AtomicF64::new(sum),
110 min: AtomicUsize::new(max_val),
111 bin_size,
112 cached_pmf: Vec::new(),
113 cached_cmf: Vec::new(),
114 have_cache: false,
115 }
116 }
117
118 pub fn default_for_paired() -> Self {
121 Self::new(1.0, 1000, 0.0, 0.0, 4, 0.5, 1)
122 }
123
124 pub fn max_val(&self) -> usize {
125 (self.hist.len() - 1) * self.bin_size
126 }
127
128 pub fn min_val(&self) -> usize {
129 let m = self.min.load(Ordering::Relaxed);
130 if m == self.hist.len() - 1 {
131 1
132 } else {
133 m
134 }
135 }
136
137 pub fn add_val(&self, len: usize, mass: f64) {
141 let mut len = len / self.bin_size;
142 let max_v = self.max_val() / self.bin_size;
143 if len > max_v {
144 len = max_v;
145 }
146 self.min.fetch_min(len, Ordering::Relaxed);
147
148 let half = self.kernel.len() / 2;
149 let mut offset = len as isize - half as isize;
151 for &k in &self.kernel {
152 if offset > 0 && (offset as usize) < self.hist.len() {
153 let o = offset as usize;
154 let k_mass = mass + k;
155 self.hist[o].log_add_assign(k_mass);
156 self.sum.log_add_assign((o as f64).ln() + k_mass);
157 self.tot_mass.log_add_assign(k_mass);
158 }
159 offset += 1;
160 }
161 }
162
163 pub fn pmf(&self, len: usize) -> f64 {
165 if self.have_cache {
166 return *self
167 .cached_pmf
168 .get(len)
169 .unwrap_or_else(|| self.cached_pmf.last().unwrap());
170 }
171 let mut l = len / self.bin_size;
172 let max_v = self.max_val() / self.bin_size;
173 if l > max_v {
174 l = max_v;
175 }
176 self.hist[l].load() - self.tot_mass.load()
177 }
178
179 pub fn cmf(&self, len: usize) -> f64 {
181 if self.have_cache {
182 return *self
183 .cached_cmf
184 .get(len)
185 .unwrap_or_else(|| self.cached_cmf.last().unwrap());
186 }
187 let mut l = len / self.bin_size;
188 let max_v = self.max_val() / self.bin_size;
189 if l > max_v {
190 l = max_v;
191 }
192 let mut cum = LOG_0;
193 for i in 0..=l {
194 cum = log_add(cum, self.hist[i].load());
195 }
196 cum - self.tot_mass.load()
197 }
198
199 pub fn tot_mass(&self) -> f64 {
201 self.tot_mass.load()
202 }
203
204 pub fn mean(&self) -> f64 {
206 (self.sum.load() - self.tot_mass.load()).exp()
207 }
208
209 pub fn sd(&self) -> f64 {
212 let lp = self.log_pmf();
213 if lp.is_empty() {
214 return 0.0;
215 }
216 let mut mean = 0.0;
217 for (l, &p) in lp.iter().enumerate() {
218 mean += (l as f64) * p.exp();
219 }
220 let mut var = 0.0;
221 for (l, &p) in lp.iter().enumerate() {
222 let d = l as f64 - mean;
223 var += d * d * p.exp();
224 }
225 var.max(0.0).sqrt()
226 }
227
228 pub fn cache(&mut self) {
231 if self.have_cache {
232 return;
233 }
234 let max_v = self.max_val();
235 let mut pmf = Vec::with_capacity(max_v + 1);
237 let mut tot = LOG_0;
238 for i in 0..=max_v {
239 let p = self.pmf(i);
240 pmf.push(p);
241 tot = log_add(tot, p);
242 }
243 for p in &mut pmf {
244 *p -= tot;
245 }
246 let mut cmf = Vec::with_capacity(pmf.len());
248 let mut cum = LOG_0;
249 for &p in &pmf {
250 cum = log_add(cum, p);
251 cmf.push(cum);
252 }
253 self.cached_pmf = pmf;
254 self.cached_cmf = cmf;
255 self.have_cache = true;
256 }
257
258 pub fn log_pmf(&self) -> &[f64] {
260 debug_assert!(self.have_cache, "call cache() before log_pmf()");
261 &self.cached_pmf
262 }
263
264 pub fn conditional_means(&self) -> Vec<f64> {
274 debug_assert!(self.have_cache, "call cache() before conditional_means()");
275 let n = self.cached_pmf.len();
276 let mut cms = vec![0.0f64; n];
277 let mut vals = 0.0; let mut mult = 0.0; for i in 0..n {
280 let p = self.cached_pmf[i].exp();
281 vals += (i as f64) * p;
282 mult += p;
283 cms[i] = if mult > 0.0 { vals / mult } else { 0.0 };
284 }
285 cms
286 }
287}
288
289pub fn smoothed_effective_length(cond_means: &[f64], ref_len: usize) -> f64 {
297 if cond_means.is_empty() {
298 return ref_len as f64;
299 }
300 let max_len = cond_means.len();
301 let cf = if ref_len >= max_len {
302 cond_means[max_len - 1]
303 } else {
304 cond_means[ref_len]
305 };
306 let eff = ref_len as f64 - cf;
307 if eff < 1.0 {
308 ref_len as f64
309 } else {
310 eff
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn uniform_prior_pmf_normalizes() {
320 let mut fld = FragmentLengthDistribution::new(1.0, 200, 0.0, 0.0, 4, 0.5, 1);
321 fld.cache();
322 let total: f64 = fld.log_pmf().iter().map(|p| p.exp()).sum();
323 assert!((total - 1.0).abs() < 1e-9, "pmf sums to {total}");
324 }
325
326 #[test]
327 fn gaussian_prior_mean_is_near_mu() {
328 let fld = FragmentLengthDistribution::new(1000.0, 1000, 250.0, 25.0, 4, 0.5, 1);
329 let m = fld.mean();
330 assert!((m - 250.0).abs() < 5.0, "mean {m} not near 250");
331 }
332
333 #[test]
334 fn observations_shift_the_distribution() {
335 let mut fld = FragmentLengthDistribution::new(1.0, 1000, 250.0, 25.0, 4, 0.5, 1);
336 for _ in 0..100_000 {
338 fld.add_val(400, 0.0); }
340 let m = fld.mean();
341 assert!(m > 300.0, "mean {m} did not move toward 400");
342 fld.cache();
343 let p400 = fld.pmf(400);
345 let p250 = fld.pmf(250);
346 assert!(p400 > p250, "p(400)={p400} not > p(250)={p250}");
347 }
348
349 #[test]
350 fn smoothed_efflen_shrinks_short_transcripts() {
351 let mut fld = FragmentLengthDistribution::new(1000.0, 1000, 250.0, 25.0, 4, 0.5, 1);
355 fld.cache();
356 let cm = fld.conditional_means();
357 for w in cm.windows(2) {
359 assert!(
360 w[1] >= w[0] - 1e-9,
361 "cond means not monotonic: {} < {}",
362 w[1],
363 w[0]
364 );
365 }
366 let short = smoothed_effective_length(&cm, 201);
367 assert!(
368 short < 201.0 && short > 1.0,
369 "short effLen {short} not shrunk"
370 );
371 let long = smoothed_effective_length(&cm, 5000);
373 assert!(long > 4000.0, "long effLen {long} shrunk too much");
374 let tiny = smoothed_effective_length(&cm, 2);
376 assert_eq!(tiny, 2.0, "tiny transcript should fall back to refLen");
377 }
378
379 #[test]
380 fn cmf_is_monotonic() {
381 let mut fld = FragmentLengthDistribution::new(1000.0, 500, 200.0, 30.0, 4, 0.5, 1);
382 fld.cache();
383 let mut prev = f64::NEG_INFINITY;
384 for l in 0..=500 {
385 let c = fld.cmf(l);
386 assert!(c >= prev - 1e-9, "cmf decreased at {l}: {c} < {prev}");
387 prev = c;
388 }
389 assert!((prev - 0.0).abs() < 1e-6, "cmf endpoint {prev} != log(1)");
390 }
391}