1use crate::sampling::lut::LutF64;
26
27pub const DEFAULT_RESOLUTION: usize = 1000;
29
30#[polydat::polydat_node(category = Conversions)]
47fn unit_interval(input: u64) -> f64 {
48 input as f64 / u64::MAX as f64
49}
50
51#[polydat::polydat_node(category = Conversions)]
69fn clamp_f64(
70 input: f64,
71 #[poly_default(f64::MIN)] min: Const<f64>,
72 #[poly_default(f64::MAX)] max: Const<f64>,
73) -> f64 {
74 input.clamp(*min, *max)
75}
76
77fn probit(p: f64) -> f64 {
84 if p <= 0.0 {
85 return f64::NEG_INFINITY;
86 }
87 if p >= 1.0 {
88 return f64::INFINITY;
89 }
90
91 let t = if p < 0.5 {
92 (-2.0 * p.ln()).sqrt()
93 } else {
94 (-2.0 * (1.0 - p).ln()).sqrt()
95 };
96
97 let c0 = 2.515517;
98 let c1 = 0.802853;
99 let c2 = 0.010328;
100 let d1 = 1.432788;
101 let d2 = 0.189269;
102 let d3 = 0.001308;
103
104 let result = t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t);
105
106 if p < 0.5 { -result } else { result }
107}
108
109fn ln_gamma(x: f64) -> f64 {
115 let g = 7.0;
116 let c = [
117 0.999_999_999_999_809_9,
118 676.5203681218851,
119 -1259.1392167224028,
120 771.323_428_777_653_1,
121 -176.615_029_162_140_6,
122 12.507343278686905,
123 -0.13857109526572012,
124 9.984_369_578_019_572e-6,
125 1.5056327351493116e-7,
126 ];
127
128 if x < 0.5 {
129 let pi = std::f64::consts::PI;
130 return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
131 }
132
133 let x = x - 1.0;
134 let mut sum = c[0];
135 for (i, &coeff) in c[1..].iter().enumerate() {
136 sum += coeff / (x + i as f64 + 1.0);
137 }
138
139 let t = x + g + 0.5;
140 0.5 * (2.0 * std::f64::consts::PI).ln() + (t.ln() * (x + 0.5)) - t + sum.ln()
141}
142
143fn regularized_beta(x: f64, a: f64, b: f64) -> f64 {
145 if x <= 0.0 {
146 return 0.0;
147 }
148 if x >= 1.0 {
149 return 1.0;
150 }
151
152 if x > (a + 1.0) / (a + b + 2.0) {
154 return 1.0 - regularized_beta(1.0 - x, b, a);
155 }
156
157 let ln_prefix = ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln();
158 let prefix = ln_prefix.exp();
159
160 let mut sum = 0.0;
162 let mut term = 1.0;
163 for n in 0..300 {
164 sum += term;
165 term *= x * (a + b + n as f64) / (a + 1.0 + n as f64);
166 if term.abs() < 1e-15 * sum.abs() {
167 break;
168 }
169 }
170
171 (prefix * sum / a).clamp(0.0, 1.0)
172}
173
174fn inv_regularized_beta(p: f64, a: f64, b: f64) -> f64 {
176 if p <= 0.0 {
177 return 0.0;
178 }
179 if p >= 1.0 {
180 return 1.0;
181 }
182
183 let mut lo = 0.0_f64;
184 let mut hi = 1.0_f64;
185 for _ in 0..100 {
186 let mid = (lo + hi) / 2.0;
187 if regularized_beta(mid, a, b) < p {
188 lo = mid;
189 } else {
190 hi = mid;
191 }
192 }
193 (lo + hi) / 2.0
194}
195
196fn regularized_gamma_p(a: f64, x: f64) -> f64 {
198 if x <= 0.0 {
199 return 0.0;
200 }
201 if x > a + 50.0 {
202 return 1.0;
203 } let mut sum = 1.0 / a;
206 let mut term = 1.0 / a;
207 for n in 1..300 {
208 term *= x / (a + n as f64);
209 sum += term;
210 if term.abs() < 1e-14 * sum.abs() {
211 break;
212 }
213 }
214 (a * x.ln() - x - ln_gamma(a)).exp() * sum
215}
216
217fn inv_regularized_gamma_p(p: f64, a: f64) -> f64 {
219 if p <= 0.0 {
220 return 0.0;
221 }
222 if p >= 1.0 {
223 return f64::INFINITY;
224 }
225
226 let mut hi = a.max(1.0);
228 while regularized_gamma_p(a, hi) < p {
229 hi *= 2.0;
230 }
231 let mut lo = 0.0_f64;
232
233 for _ in 0..100 {
234 let mid = (lo + hi) / 2.0;
235 if regularized_gamma_p(a, mid) < p {
236 lo = mid;
237 } else {
238 hi = mid;
239 }
240 }
241 (lo + hi) / 2.0
242}
243
244pub fn dist_normal_lut(mean: f64, stddev: f64, resolution: usize) -> LutF64 {
250 LutF64::from_fn(|p| mean + stddev * probit(p), resolution)
251}
252
253pub fn dist_exponential_lut(rate: f64, resolution: usize) -> LutF64 {
255 LutF64::from_fn(|p| -(1.0 - p).ln() / rate, resolution)
256}
257
258pub fn dist_uniform_lut(min: f64, max: f64, resolution: usize) -> LutF64 {
260 LutF64::from_fn(|p| min + p * (max - min), resolution)
261}
262
263pub fn dist_pareto_lut(scale: f64, shape: f64, resolution: usize) -> LutF64 {
265 LutF64::from_fn(|p| scale / (1.0 - p).powf(1.0 / shape), resolution)
266}
267
268pub fn dist_lognormal_lut(mean: f64, stddev: f64, resolution: usize) -> LutF64 {
270 LutF64::from_fn(|p| (mean + stddev * probit(p)).exp(), resolution)
271}
272
273pub fn dist_weibull_lut(shape: f64, scale: f64, resolution: usize) -> LutF64 {
275 LutF64::from_fn(|p| scale * (-(1.0 - p).ln()).powf(1.0 / shape), resolution)
276}
277
278pub fn dist_cauchy_lut(location: f64, scale: f64, resolution: usize) -> LutF64 {
280 LutF64::from_fn(
281 |p| location + scale * (std::f64::consts::PI * (p - 0.5)).tan(),
282 resolution,
283 )
284}
285
286pub fn dist_laplace_lut(location: f64, scale: f64, resolution: usize) -> LutF64 {
288 LutF64::from_fn(
289 |p| {
290 if p <= 0.5 {
291 location + scale * (2.0 * p).ln()
292 } else {
293 location - scale * (2.0 * (1.0 - p)).ln()
294 }
295 },
296 resolution,
297 )
298}
299
300pub fn dist_beta_lut(alpha: f64, beta: f64, resolution: usize) -> LutF64 {
302 LutF64::from_fn(|p| inv_regularized_beta(p, alpha, beta), resolution)
303}
304
305pub fn dist_gamma_lut(shape: f64, scale: f64, resolution: usize) -> LutF64 {
307 LutF64::from_fn(|p| scale * inv_regularized_gamma_p(p, shape), resolution)
308}
309
310pub fn dist_zipf_lut(n: u64, exponent: f64, resolution: usize) -> LutF64 {
320 let harmonic: f64 = (1..=n).map(|k| 1.0 / (k as f64).powf(exponent)).sum();
322 let mut cdf = Vec::with_capacity(n as usize + 1);
323 cdf.push(0.0);
324 let mut cumulative = 0.0;
325 for k in 1..=n {
326 cumulative += (1.0 / (k as f64).powf(exponent)) / harmonic;
327 cdf.push(cumulative);
328 }
329
330 LutF64::from_fn(
332 |p| {
333 let p = p.clamp(0.0, 1.0);
334 match cdf.binary_search_by(|v| v.partial_cmp(&p).unwrap()) {
335 Ok(idx) => idx as f64,
336 Err(idx) => (idx as f64).max(1.0).min(n as f64),
337 }
338 },
339 resolution,
340 )
341}
342
343pub fn dist_poisson_lut(lambda: f64, resolution: usize) -> LutF64 {
347 let upper = (lambda + 6.0 * lambda.sqrt() + 10.0).ceil() as usize;
348
349 let mut cdf = Vec::with_capacity(upper + 2);
351 cdf.push(0.0);
352 let mut cumulative = 0.0;
353 let mut pmf = (-lambda).exp(); for k in 0..=upper {
355 cumulative += pmf;
356 cdf.push(cumulative.min(1.0));
357 pmf *= lambda / (k + 1) as f64;
358 }
359
360 LutF64::from_fn(
361 |p| {
362 let p = p.clamp(0.0, 1.0);
363 match cdf.binary_search_by(|v| v.partial_cmp(&p).unwrap()) {
364 Ok(idx) => idx.saturating_sub(1) as f64,
365 Err(idx) => idx.saturating_sub(1) as f64,
366 }
367 },
368 resolution,
369 )
370}
371
372pub fn dist_binomial_lut(trials: u64, prob: f64, resolution: usize) -> LutF64 {
374 let n = trials as usize;
375
376 let mut cdf = Vec::with_capacity(n + 2);
378 cdf.push(0.0);
379 let mut cumulative = 0.0;
380 let mut pmf = (1.0 - prob).powi(n as i32); for k in 0..=n {
382 cumulative += pmf;
383 cdf.push(cumulative.min(1.0));
384 if k < n {
385 pmf *= prob / (1.0 - prob) * ((n - k) as f64) / ((k + 1) as f64);
386 }
387 }
388
389 LutF64::from_fn(
390 |p| {
391 let p = p.clamp(0.0, 1.0);
392 match cdf.binary_search_by(|v| v.partial_cmp(&p).unwrap()) {
393 Ok(idx) => idx.saturating_sub(1) as f64,
394 Err(idx) => idx.saturating_sub(1) as f64,
395 }
396 },
397 resolution,
398 )
399}
400
401pub fn dist_geometric_lut(prob: f64, resolution: usize) -> LutF64 {
405 let ln_q = (1.0 - prob).ln();
406 LutF64::from_fn(
407 |p| {
408 if p <= 0.0 {
409 return 1.0;
410 }
411 if p >= 1.0 {
412 return f64::INFINITY;
413 }
414 ((1.0 - p).ln() / ln_q).ceil().max(1.0)
415 },
416 resolution,
417 )
418}
419
420pub fn dist_empirical_lut(data: &[f64], resolution: usize) -> LutF64 {
430 assert!(!data.is_empty(), "data must not be empty");
431 let mut sorted = data.to_vec();
432 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
433
434 LutF64::from_fn(
435 |p| {
436 let pos = p * (sorted.len() - 1) as f64;
437 let idx = pos as usize;
438 let idx = idx.min(sorted.len() - 2);
439 let frac = pos - idx as f64;
440 sorted[idx] * (1.0 - frac) + sorted[idx + 1] * frac
441 },
442 resolution,
443 )
444}
445
446pub fn dist_empirical_weighted_lut(values: &[f64], weights: &[f64], resolution: usize) -> LutF64 {
450 assert_eq!(values.len(), weights.len());
451 assert!(!values.is_empty());
452
453 let mut pairs: Vec<(f64, f64)> = values
455 .iter()
456 .copied()
457 .zip(weights.iter().copied())
458 .collect();
459 pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
460
461 let total: f64 = pairs.iter().map(|(_, w)| w).sum();
462 let mut cdf_points: Vec<(f64, f64)> = Vec::new(); let mut cumulative = 0.0;
464 for (val, weight) in &pairs {
465 cumulative += weight / total;
466 cdf_points.push((cumulative, *val));
467 }
468
469 LutF64::from_fn(
471 |p| match cdf_points.binary_search_by(|&(cp, _)| cp.partial_cmp(&p).unwrap()) {
472 Ok(idx) => cdf_points[idx].1,
473 Err(idx) => {
474 if idx >= cdf_points.len() {
475 cdf_points.last().unwrap().1
476 } else {
477 cdf_points[idx].1
478 }
479 }
480 },
481 resolution,
482 )
483}
484
485fn build_normal_lut(mean: f64, stddev: f64) -> LutF64 {
492 dist_normal_lut(mean, stddev, DEFAULT_RESOLUTION)
493}
494
495fn dist_normal_jit_constants(node: &DistNormal) -> Vec<u64> {
499 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
500}
501
502#[polydat::polydat_node(category = Distributions, jit_constants = dist_normal_jit_constants)]
508fn dist_normal(
509 input: f64,
510 mean: Const<f64>,
511 stddev: Const<f64>,
512 #[poly_const(build_normal_lut, from = (mean, stddev))] lut: &LutF64,
513) -> f64 {
514 let _ = mean;
515 let _ = stddev;
516 lut.sample(input)
517}
518
519fn icd_normal_jit_constants(node: &IcdNormal) -> Vec<u64> {
520 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
521}
522
523#[polydat::polydat_node(category = Distributions, jit_constants = icd_normal_jit_constants)]
527fn icd_normal(
528 input: f64,
529 mean: Const<f64>,
530 stddev: Const<f64>,
531 #[poly_const(build_normal_lut, from = (mean, stddev))] lut: &LutF64,
532) -> f64 {
533 let _ = mean;
534 let _ = stddev;
535 lut.sample(input)
536}
537
538fn build_exponential_lut(rate: f64) -> LutF64 {
539 dist_exponential_lut(rate, DEFAULT_RESOLUTION)
540}
541
542fn dist_exponential_jit_constants(node: &DistExponential) -> Vec<u64> {
543 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
544}
545
546#[polydat::polydat_node(category = Distributions, jit_constants = dist_exponential_jit_constants)]
548fn dist_exponential(
549 input: f64,
550 rate: Const<f64>,
551 #[poly_const(build_exponential_lut, from = rate)] lut: &LutF64,
552) -> f64 {
553 let _ = rate;
554 lut.sample(input)
555}
556
557fn icd_exponential_jit_constants(node: &IcdExponential) -> Vec<u64> {
558 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
559}
560
561#[polydat::polydat_node(category = Distributions, jit_constants = icd_exponential_jit_constants)]
563fn icd_exponential(
564 input: f64,
565 rate: Const<f64>,
566 #[poly_const(build_exponential_lut, from = rate)] lut: &LutF64,
567) -> f64 {
568 let _ = rate;
569 lut.sample(input)
570}
571
572fn build_uniform_lut(min: f64, max: f64) -> LutF64 {
573 dist_uniform_lut(min, max, DEFAULT_RESOLUTION)
574}
575
576fn dist_uniform_jit_constants(node: &DistUniform) -> Vec<u64> {
577 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
578}
579
580#[polydat::polydat_node(category = Distributions, jit_constants = dist_uniform_jit_constants)]
582fn dist_uniform(
583 input: f64,
584 min: Const<f64>,
585 max: Const<f64>,
586 #[poly_const(build_uniform_lut, from = (min, max))] lut: &LutF64,
587) -> f64 {
588 let _ = min;
589 let _ = max;
590 lut.sample(input)
591}
592
593fn build_pareto_lut(scale: f64, shape: f64) -> LutF64 {
594 dist_pareto_lut(scale, shape, DEFAULT_RESOLUTION)
595}
596
597fn dist_pareto_jit_constants(node: &DistPareto) -> Vec<u64> {
598 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
599}
600
601#[polydat::polydat_node(category = Distributions, jit_constants = dist_pareto_jit_constants)]
603fn dist_pareto(
604 input: f64,
605 scale: Const<f64>,
606 shape: Const<f64>,
607 #[poly_const(build_pareto_lut, from = (scale, shape))] lut: &LutF64,
608) -> f64 {
609 let _ = scale;
610 let _ = shape;
611 lut.sample(input)
612}
613
614fn build_zipf_lut(n: u64, exponent: f64) -> LutF64 {
615 dist_zipf_lut(n, exponent, DEFAULT_RESOLUTION)
616}
617
618fn dist_zipf_jit_constants(node: &DistZipf) -> Vec<u64> {
619 vec![node.lut.as_ptr() as u64, node.lut.len() as u64]
620}
621
622#[polydat::polydat_node(category = Distributions, jit_constants = dist_zipf_jit_constants)]
624fn dist_zipf(
625 input: f64,
626 n: Const<u64>,
627 exponent: Const<f64>,
628 #[poly_const(build_zipf_lut, from = (n, exponent))] lut: &LutF64,
629) -> f64 {
630 let _ = n;
631 let _ = exponent;
632 lut.sample(input)
633}
634
635#[cfg(test)]
643mod tests {
644 use super::*;
645 use polydat::ast::{PolydatNode, Value};
646
647 #[test]
648 fn unit_interval_range() {
649 let node = UnitInterval::new();
650 let mut out = [Value::None];
651 node.eval(&[Value::U64(0)], &mut out);
652 assert_eq!(out[0].as_f64(), 0.0);
653 node.eval(&[Value::U64(u64::MAX)], &mut out);
654 assert!((0.999..=1.0).contains(&out[0].as_f64()));
655 }
656
657 #[test]
658 fn normal_symmetry() {
659 let lut = dist_normal_lut(0.0, 1.0, 1000);
660 assert!(lut.sample(0.5).abs() < 0.01);
661 assert!((lut.sample(0.25) + lut.sample(0.75)).abs() < 0.01);
662 }
663
664 #[test]
665 fn normal_mean_stddev() {
666 let lut = dist_normal_lut(100.0, 10.0, 1000);
667 assert!((lut.sample(0.5) - 100.0).abs() < 0.5);
668 }
669
670 #[test]
671 fn exponential_median() {
672 let lut = dist_exponential_lut(1.0, 1000);
673 assert!((lut.sample(0.5) - 0.693).abs() < 0.01);
674 }
675
676 #[test]
677 fn exponential_positive() {
678 let lut = dist_exponential_lut(1.0, 1000);
679 for i in 1..1000 {
680 assert!(lut.sample(i as f64 / 1000.0) >= 0.0);
681 }
682 }
683
684 #[test]
685 fn uniform_linear() {
686 let lut = dist_uniform_lut(10.0, 20.0, 1000);
687 assert!((lut.sample(0.0) - 10.0).abs() < 0.1);
688 assert!((lut.sample(0.5) - 15.0).abs() < 0.1);
689 assert!((lut.sample(0.999) - 20.0).abs() < 0.1);
690 }
691
692 #[test]
693 fn pareto_heavy_tail() {
694 let lut = dist_pareto_lut(1.0, 1.0, 1000);
695 assert!((lut.sample(0.5) - 2.0).abs() < 0.1);
696 assert!(lut.sample(0.99) > 50.0);
697 }
698
699 #[test]
700 fn cauchy_symmetric() {
701 let lut = dist_cauchy_lut(0.0, 1.0, 1000);
702 assert!(lut.sample(0.5).abs() < 0.1);
703 assert!((lut.sample(0.25) + lut.sample(0.75)).abs() < 0.1);
704 }
705
706 #[test]
707 fn laplace_symmetric() {
708 let lut = dist_laplace_lut(5.0, 2.0, 1000);
709 assert!((lut.sample(0.5) - 5.0).abs() < 0.1);
710 }
711
712 #[test]
713 fn beta_bounded_01() {
714 let lut = dist_beta_lut(2.0, 5.0, 1000);
715 for i in 0..=1000 {
716 let v = lut.sample(i as f64 / 1000.0);
717 assert!((0.0..=1.0).contains(&v), "beta out of [0,1]: {v}");
718 }
719 }
720
721 #[test]
722 fn beta_symmetric_at_half() {
723 let lut = dist_beta_lut(2.0, 2.0, 1000);
725 assert!(
726 (lut.sample(0.5) - 0.5).abs() < 0.1,
727 "beta(2,2) median={}, expected ~0.5",
728 lut.sample(0.5)
729 );
730 }
731
732 #[test]
733 fn gamma_positive() {
734 let lut = dist_gamma_lut(2.0, 1.0, 1000);
735 for i in 1..1000 {
736 assert!(lut.sample(i as f64 / 1000.0) > 0.0);
737 }
738 }
739
740 #[test]
741 fn gamma_mean() {
742 let lut = dist_gamma_lut(3.0, 2.0, 1000);
744 assert!((lut.sample(0.5) - 5.0).abs() < 1.5); }
746
747 #[test]
748 fn weibull_positive() {
749 let lut = dist_weibull_lut(2.0, 1.0, 1000);
750 for i in 1..1000 {
751 assert!(lut.sample(i as f64 / 1000.0) >= 0.0);
752 }
753 }
754
755 #[test]
756 fn zipf_range() {
757 let lut = dist_zipf_lut(100, 1.0, 1000);
758 for i in 1..1000 {
759 let v = lut.sample(i as f64 / 1000.0);
760 assert!((1.0..=100.0).contains(&v), "zipf out of [1,100]: {v}");
761 }
762 }
763
764 #[test]
765 fn zipf_skewed() {
766 let lut = dist_zipf_lut(100, 1.0, 1000);
768 let low_quantile = lut.sample(0.5);
769 assert!(
770 low_quantile < 20.0,
771 "median of Zipf(100,1) should be low, got {low_quantile}"
772 );
773 }
774
775 #[test]
776 fn poisson_mean() {
777 let lut = dist_poisson_lut(5.0, 1000);
779 let median = lut.sample(0.5);
780 assert!(
781 (median - 5.0).abs() < 1.0,
782 "poisson median={median}, expected ~5"
783 );
784 }
785
786 #[test]
787 fn poisson_nonnegative() {
788 let lut = dist_poisson_lut(3.0, 1000);
789 for i in 0..=1000 {
790 assert!(lut.sample(i as f64 / 1000.0) >= 0.0);
791 }
792 }
793
794 #[test]
795 fn binomial_range() {
796 let lut = dist_binomial_lut(20, 0.5, 1000);
797 for i in 0..=1000 {
798 let v = lut.sample(i as f64 / 1000.0);
799 assert!((0.0..=20.0).contains(&v), "binomial out of [0,20]: {v}");
800 }
801 }
802
803 #[test]
804 fn binomial_mean() {
805 let lut = dist_binomial_lut(20, 0.5, 1000);
807 let median = lut.sample(0.5);
808 assert!(
809 (median - 10.0).abs() < 1.5,
810 "binomial median={median}, expected ~10"
811 );
812 }
813
814 #[test]
815 fn geometric_starts_at_one() {
816 let lut = dist_geometric_lut(0.5, 1000);
817 assert!(lut.sample(0.001) >= 1.0);
818 }
819
820 #[test]
821 fn geometric_mean() {
822 let lut = dist_geometric_lut(0.5, 1000);
824 let median = lut.sample(0.5);
825 assert!(
826 (median - 1.0).abs() < 1.0,
827 "geometric median={median}, expected ~1-2"
828 );
829 }
830
831 #[test]
832 fn dist_normal_node_eval() {
833 let node = DistNormal::new(0.0, 1.0);
834 let mut out = [Value::None];
835 node.eval(&[Value::F64(0.5)], &mut out);
836 assert!(out[0].as_f64().abs() < 0.01);
837 }
838
839 #[test]
840 fn full_pipeline_hash_normalize_sample() {
841 use xxhash_rust::xxh3::xxh3_64;
842
843 let lut = dist_normal_lut(72.0, 5.0, 1000);
844 let mut values = Vec::new();
845 for i in 0..10_000u64 {
846 let hashed = xxh3_64(&i.to_le_bytes());
847 let u = hashed as f64 / u64::MAX as f64;
848 values.push(lut.sample(u));
849 }
850 let mean = values.iter().sum::<f64>() / values.len() as f64;
851 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
852 let stddev = variance.sqrt();
853 assert!((mean - 72.0).abs() < 0.5, "mean={mean}");
854 assert!((stddev - 5.0).abs() < 0.5, "stddev={stddev}");
855 }
856}