Skip to main content

nereids_physics/
thickness.rs

1//! Transmission of a sample whose areal density varies over the pixels a
2//! count sums.
3//!
4//! A region's pixels see the areal density `x·n`, `x` distributed with mean 1
5//! and relative variance `v` over the beam that reaches them.  Their summed
6//! counts transmit the mean `⟨exp(−x·D)⟩`, `D = Σ n σ` the optical depth at the
7//! mean density.  For `x` Gamma distributed the mean is
8//!
9//! ```text
10//! T(D, v) = (1 + v·D)^(−1/v) = exp(−D · ln(1 + v·D)/(v·D))
11//! ```
12//!
13//! exact for that distribution only.  At `v = 0` it is `exp(−D)`.
14
15/// `ln(1 + y)/y`, 1 at `y = 0`.
16fn 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
24/// `((1 + y)·ln(1 + y) − y)/y²`, 1/2 at `y = 0`.
25fn 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/// The mean transmission `T`, `−∂T/∂D` and `∂T/∂v` at optical depth `depth`
34/// (`D`, 0 or more) and relative variance `variance` (`v`, 0 or more) of the
35/// areal density.
36#[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/// `q = (1 − T)/D`, the fraction of the beam that interacts per unit optical
46/// depth, with `∂q/∂D` and `∂q/∂v`, at optical depth `depth` (`D`, 0 or more)
47/// and relative variance `variance` (`v`, 0 or more): `Σ n σ_x · q` is the
48/// chance that a neutron's first collision in the sample is of kind `x`.
49/// `q` is 1 at `D = 0`.
50#[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    /// `⟨exp(−x·D)⟩` over the Gamma density of mean 1 and variance `v`, by the
77    /// trapezoid rule in `x` on a fine grid wide enough to hold the density.
78    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        // ln Γ(z) by the Lanczos series with g = 7 and nine coefficients.
98        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}