1fn log_ratio(y: f64) -> f64 {
17 if y.abs() < 1e-3 {
18 1.0 - y / 2.0 + y * y / 3.0 - y * y * y / 4.0
19 } else {
20 y.ln_1p() / y
21 }
22}
23
24fn variance_ratio(y: f64) -> f64 {
26 if y.abs() < 1e-3 {
27 0.5 - y / 6.0 + y * y / 12.0 - y * y * y / 20.0
28 } else {
29 ((1.0 + y) * y.ln_1p() - y) / (y * y)
30 }
31}
32
33#[must_use]
37pub fn uneven_transmission(depth: f64, variance: f64) -> (f64, f64, f64) {
38 let y = variance * depth;
39 let transmission = (-depth * log_ratio(y)).exp();
40 let per_depth = transmission / (1.0 + y);
41 let per_variance = per_depth * depth * depth * variance_ratio(y);
42 (transmission, per_depth, per_variance)
43}
44
45#[must_use]
51pub fn interacting_per_depth(depth: f64, variance: f64) -> (f64, f64, f64) {
52 if depth < 1e-5 && variance * depth < 1e-5 {
53 let (a, b) = (
54 1.0 + variance,
55 1.0 + 3.0 * variance + 2.0 * variance * variance,
56 );
57 return (
58 1.0 - a * depth / 2.0 + b * depth * depth / 6.0,
59 -a / 2.0 + b * depth / 3.0,
60 -depth / 2.0 + (3.0 + 4.0 * variance) * depth * depth / 6.0,
61 );
62 }
63 let (_, per_depth, per_variance) = uneven_transmission(depth, variance);
64 let absorbed = -(-depth * log_ratio(variance * depth)).exp_m1();
65 (
66 absorbed / depth,
67 (per_depth * depth - absorbed) / (depth * depth),
68 -per_variance / depth,
69 )
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 fn gamma_mean(depth: f64, variance: f64) -> f64 {
79 let shape = 1.0 / variance;
80 let log_norm = -libm_lgamma(shape) - shape * variance.ln();
81 let (steps, top) = (400_000, 1.0 + 40.0 * variance.sqrt() + 40.0 * variance);
82 let h = top / f64::from(steps);
83 (0..=steps)
84 .map(|i| {
85 let x = h * f64::from(i);
86 let weight = if i == 0 || i == steps { 0.5 } else { 1.0 };
87 if x == 0.0 {
88 return 0.0;
89 }
90 let density = (log_norm + (shape - 1.0) * x.ln() - x / variance).exp();
91 weight * h * density * (-x * depth).exp()
92 })
93 .sum()
94 }
95
96 fn libm_lgamma(z: f64) -> f64 {
97 const G: [f64; 9] = [
99 0.999_999_999_999_809_9,
100 676.520_368_121_885_1,
101 -1_259.139_216_722_402_8,
102 771.323_428_777_653_1,
103 -176.615_029_162_140_6,
104 12.507_343_278_686_905,
105 -0.138_571_095_265_720_12,
106 9.984_369_578_019_572e-6,
107 1.505_632_735_149_311_6e-7,
108 ];
109 let z = z - 1.0;
110 let mut a = G[0];
111 let t = z + 7.5;
112 for (i, g) in G.iter().enumerate().skip(1) {
113 a += g / (z + i as f64);
114 }
115 0.5 * (2.0 * std::f64::consts::PI).ln() + (z + 0.5) * t.ln() - t + a.ln()
116 }
117
118 #[test]
119 fn the_mean_transmission_is_the_gamma_average_of_exp_minus_x_d() {
120 for (depth, variance) in [(0.3, 0.02), (2.0, 0.05), (8.0, 0.1), (20.0, 0.3)] {
121 let (t, _, _) = uneven_transmission(depth, variance);
122 let oracle = gamma_mean(depth, variance);
123 assert!(
124 (t / oracle - 1.0).abs() <= 1e-6,
125 "D {depth} v {variance}: {t} vs {oracle}"
126 );
127 }
128 }
129
130 #[test]
131 fn the_interacting_fraction_is_one_minus_t_over_d_with_its_slopes() {
132 for variance in [0.0, 0.05, 0.4] {
133 for depth in [0.0, 5e-6, 9.99e-6, 1.001e-5, 3e-3, 0.7, 12.0] {
134 let (q, per_depth, per_variance) = interacting_per_depth(depth, variance);
135 let (t, _, _) = uneven_transmission(depth, variance);
136 if depth > 0.0 {
137 assert!(
138 (q * depth - (1.0 - t)).abs() <= 1e-12 * (1.0 - t).max(1e-300) + 1e-15,
139 "{depth} {variance}"
140 );
141 } else {
142 assert_eq!(q, 1.0);
143 }
144 let (hd, hv) = (1e-5 * depth.max(1e-2), 1e-4 * variance.max(1e-3));
145 let along_depth = (interacting_per_depth(depth + hd, variance).0
146 - interacting_per_depth((depth - hd).max(0.0), variance).0)
147 / (depth + hd - (depth - hd).max(0.0));
148 let along_variance = (interacting_per_depth(depth, variance + hv).0
149 - interacting_per_depth(depth, (variance - hv).max(0.0)).0)
150 / (variance + hv - (variance - hv).max(0.0));
151 assert!(
152 (per_depth - along_depth).abs() <= 1e-6,
153 "{depth} {variance}"
154 );
155 assert!(
156 (per_variance - along_variance).abs() <= 1e-6,
157 "{depth} {variance}"
158 );
159 }
160 }
161 }
162
163 #[test]
164 fn a_large_variance_over_a_small_depth_keeps_the_interacting_fraction() {
165 for (depth, variance) in [(5e-6, 1e6), (9e-6, 2e3)] {
166 let (q, per_depth, per_variance) = interacting_per_depth(depth, variance);
167 let exact = -(-(variance * depth).ln_1p() / variance).exp_m1() / depth;
168 assert!((q / exact - 1.0).abs() <= 1e-12, "{q} vs {exact}");
169 let (hd, hv) = (1e-6 * depth, 1e-6 * variance);
170 let along_depth = (interacting_per_depth(depth + hd, variance).0
171 - interacting_per_depth(depth - hd, variance).0)
172 / (2.0 * hd);
173 let along_variance = (interacting_per_depth(depth, variance + hv).0
174 - interacting_per_depth(depth, variance - hv).0)
175 / (2.0 * hv);
176 assert!((per_depth / along_depth - 1.0).abs() <= 1e-6, "{per_depth}");
177 assert!(
178 (per_variance / along_variance - 1.0).abs() <= 1e-6,
179 "{per_variance}"
180 );
181 }
182 }
183
184 #[test]
185 fn a_uniform_sample_transmits_exp_minus_d_and_the_slopes_are_its_derivatives() {
186 for depth in [0.0, 0.5, 5.0, 30.0] {
187 let (t, per_depth, per_variance) = uneven_transmission(depth, 0.0);
188 assert_eq!(t.to_bits(), (-depth).exp().to_bits());
189 assert_eq!(per_depth.to_bits(), t.to_bits());
190 assert!((per_variance - 0.5 * depth * depth * t).abs() <= 1e-15 * (1.0 + t));
191 }
192 for (depth, variance) in [(0.4, 1e-5), (0.4, 2e-3), (6.0, 0.05), (25.0, 0.4)] {
193 let (_, per_depth, per_variance) = uneven_transmission(depth, variance);
194 let (hd, hv) = (1e-6 * depth, 1e-4 * variance.max(1e-3));
195 let along_depth = (uneven_transmission(depth + hd, variance).0
196 - uneven_transmission(depth - hd, variance).0)
197 / (2.0 * hd);
198 let along_variance = (uneven_transmission(depth, variance + hv).0
199 - uneven_transmission(depth, variance - hv).0)
200 / (2.0 * hv);
201 assert!(
202 (per_depth + along_depth).abs() <= 1e-7 * per_depth,
203 "{depth} {variance}"
204 );
205 assert!(
206 (per_variance - along_variance).abs() <= 1e-6 * per_variance.abs().max(1e-300),
207 "{depth} {variance}: {per_variance} vs {along_variance}"
208 );
209 }
210 }
211}