spectroxide/
constants.rs

1//! Physical and cosmological constants in SI units.
2//!
3//! All values from CODATA 2018 / Particle Data Group unless noted.
4//!
5//! Grouped as:
6//! - **Fundamental**: `C_LIGHT`, `HBAR`, `HPLANCK`, `K_BOLTZMANN`, `M_ELECTRON`,
7//!   `M_PROTON`, `SIGMA_THOMSON`, `ALPHA_FS` — feed Compton scattering rates,
8//!   Planck normalisations, and DC/BR emission prefactors.
9//! - **Unit conversions**: `EV_IN_JOULES`, `MPC_IN_METERS`.
10//! - **Atomic physics**: `E_RYDBERG_*`, `E_HE_*`, `LAMBDA_LYA`, `LAMBDA_2S1S` —
11//!   used by [`crate::recombination`] for the Peebles 3-level atom.
12//! - **Derived**: `LAMBDA_ELECTRON`, `M_E_C2`, `M_E_C2_EV`.
13//! - **Cosmology**: `T_CMB_0`, `Y_P`, `N_EFF`, and Planck spectral integrals
14//!   (`G1_PLANCK = π²/6`, `G2_PLANCK = 2ζ(3)`, `G3_PLANCK = π⁴/15`,
15//!   `I4_PLANCK = 4 G₃`), plus the derived μ-channel coefficients
16//!   `BETA_MU`, `KAPPA_C`, `ALPHA_RHO`, and the era-boundary redshifts
17//!   `Z_MU` (μ-distortion freeze-out) and `Z_MU_Y` (μ→y transition).
18//!   These are used by both the PDE solver and the Green's-function
19//!   visibility functions.
20
21// Fundamental constants
22pub const C_LIGHT: f64 = 2.997_924_58e8; // m/s (exact)
23pub const HBAR: f64 = 1.054_571_817e-34; // J·s
24pub const HPLANCK: f64 = 6.626_070_15e-34; // J·s (exact)
25pub const K_BOLTZMANN: f64 = 1.380_649e-23; // J/K (exact)
26pub const G_NEWTON: f64 = 6.674_30e-11; // m³/(kg·s²)
27pub const M_ELECTRON: f64 = 9.109_383_7015e-31; // kg
28pub const M_PROTON: f64 = 1.672_621_923_69e-27; // kg
29pub const SIGMA_THOMSON: f64 = 6.652_458_7321e-29; // m²
30pub const ALPHA_FS: f64 = 7.297_352_5693e-3; // fine structure constant
31
32// Unit conversions
33/// 1 eV in Joules (exact by 2019 SI redefinition)
34pub const EV_IN_JOULES: f64 = 1.602_176_634e-19;
35/// 1 Mpc in meters
36pub const MPC_IN_METERS: f64 = 3.085_677_581e22;
37
38// Hydrogen atomic physics
39/// Hydrogen ionization energy (1s ground state), in eV.
40pub const E_RYDBERG_EV: f64 = 13.605_693_122_994;
41/// Hydrogen ionization energy, in J.
42pub const E_RYDBERG: f64 = E_RYDBERG_EV * EV_IN_JOULES;
43/// Ionization energy from n=2 level (E_Rydberg / 4), in J.
44pub const E_ION_N2: f64 = E_RYDBERG_EV / 4.0 * EV_IN_JOULES;
45/// Lyman-alpha wavelength, in m.
46pub const LAMBDA_LYA: f64 = 1.215_670e-7;
47/// 2s→1s two-photon decay rate [s⁻¹]
48pub const LAMBDA_2S1S: f64 = 8.2245809;
49
50// Mathematical constants
51/// Riemann zeta function ζ(3) = 1.202...
52pub const ZETA_3: f64 = 1.202_056_903_159_594_3;
53
54// Helium atomic physics
55/// He²⁺ ionization energy (He II → He I), in eV.
56pub const E_HE_II_ION_EV: f64 = 54.4178;
57/// He⁺ ionization energy (He I → He), in eV.
58pub const E_HE_I_ION_EV: f64 = 24.5874;
59
60// Derived constants
61/// Electron Compton wavelength: h / (m_e * c)
62pub const LAMBDA_ELECTRON: f64 = HPLANCK / (M_ELECTRON * C_LIGHT);
63
64/// m_e c^2 in Joules
65pub const M_E_C2: f64 = M_ELECTRON * C_LIGHT * C_LIGHT;
66
67/// m_e c^2 in eV
68pub const M_E_C2_EV: f64 = 0.510_998_950e6;
69
70// Cosmological constants
71/// Default CMB temperature today, in K.
72///
73/// Value 2.726 K (Mather et al. 1999) chosen for compatibility with
74/// CosmoTherm v1.0.3 reference data used in validation. Differs by 0.02%
75/// from the Planck-era Fixsen (2009) value 2.7255 K; the
76/// [`crate::cosmology::Cosmology::planck2018`] preset overrides this.
77pub const T_CMB_0: f64 = 2.726;
78
79/// Helium mass fraction
80pub const Y_P: f64 = 0.24;
81
82/// Helium number fraction relative to hydrogen: f_He = Y_p / (4*(1-Y_p))
83pub const F_HE: f64 = Y_P / (4.0 * (1.0 - Y_P));
84
85/// Effective number of neutrino species
86pub const N_EFF: f64 = 3.046;
87
88/// km/s/Mpc → 1/s
89pub const KM_PER_MPC: f64 = 3.240_779_29e-20;
90
91// Spectral integral constants (for Planck distribution)
92/// G_1 = ∫₀^∞ x n_pl(x) dx = π²/6 = ζ(2)
93pub const G1_PLANCK: f64 = 1.644_934_066_848_226_4; // π²/6
94
95/// G_2 = ∫₀^∞ x² n_pl(x) dx = 2ζ(3)
96pub const G2_PLANCK: f64 = 2.404_113_806_319_188_6; // 2*ζ(3)
97
98/// G_3 = ∫₀^∞ x³ n_pl(x) dx = π⁴/15
99pub const G3_PLANCK: f64 = 6.493_939_402_266_829; // π⁴/15
100
101/// I_4 = ∫₀^∞ x⁴ n_pl (1+n_pl) dx = 4π⁴/15
102pub const I4_PLANCK: f64 = 4.0 * G3_PLANCK;
103
104/// β_μ = 3ζ(3)/ζ(2) ≈ 2.1923 — frequency of μ-distortion zero crossing
105pub const BETA_MU: f64 = 3.0 * ZETA_3 / G1_PLANCK;
106
107/// κ_c = 3 ∫x³ M(x) dx / G₃ = 12/β_μ − 9G₂/G₃ ≈ 2.1419
108///
109/// Derived analytically using ∫₀^∞ xⁿ eˣ/(eˣ−1)² dx = n·G_{n−1}:
110///   ∫x³ M(x) dx = ∫x³ (x/β_μ − 1) g_bb/x dx = (4G₃/β_μ) − 3G₂
111///   κ_c = 3[(4G₃/β_μ) − 3G₂]/G₃ = 12/β_μ − 9G₂/G₃
112pub const KAPPA_C: f64 = 12.0 / BETA_MU - 9.0 * G2_PLANCK / G3_PLANCK;
113
114/// α_μ = 1/β_μ = π²/(18ζ(3)) — relates μ to energy
115pub const ALPHA_MU: f64 = 1.0 / BETA_MU;
116
117/// κ_γ = 8π / λ_e³ — photon phase space density prefactor.
118///
119/// The photon energy density per electron is:
120///   ρ̃_γ = κ_γ × θ_z⁴ × G₃ / n_e
121///
122/// where λ_e is the electron Compton wavelength.
123/// This enters the expansion correction Λ in the quasi-stationary T_e equation.
124pub const KAPPA_GAMMA: f64 =
125    8.0 * std::f64::consts::PI / (LAMBDA_ELECTRON * LAMBDA_ELECTRON * LAMBDA_ELECTRON);
126
127/// α_ρ = G₂/G₃ — ratio of photon number to energy spectral integrals.
128///
129/// This constant determines the critical frequency x₀ = 4/(3α_ρ) at which
130/// photon injection produces zero μ-distortion. Below x₀, injection produces
131/// negative μ; above x₀, positive μ.
132///
133/// Reference: Chluba (2015), arXiv:1506.06582, Eq. 30
134pub const ALPHA_RHO: f64 = G2_PLANCK / G3_PLANCK; // ≈ 0.3702
135
136/// x₀ = 4/(3α_ρ) ≈ 3.60 — balanced injection frequency.
137///
138/// Photon injection at x = x₀ produces zero net μ-distortion because the
139/// energy-per-photon exactly matches the mean energy of the background.
140///
141/// Reference: Chluba (2015), arXiv:1506.06582, Eq. 31
142pub const X_BALANCED: f64 = 4.0 / (3.0 * ALPHA_RHO); // ≈ 3.60
143
144// Thermalization redshifts (approximate)
145/// μ-era thermalization redshift (Chluba 2013, MNRAS 436, 2232)
146pub const Z_MU: f64 = 1.98e6;
147/// μ-y transition redshift
148pub const Z_MU_Y: f64 = 5.0e4;
149
150/// Dimensionless temperature at the *default* CMB temperature T_CMB_0 = 2.726 K.
151///
152/// Convenience helper for tests and quick calculations. Production code must
153/// use [`crate::cosmology::Cosmology::theta_z`] so that a user-supplied T_CMB
154/// (e.g. Planck 2018's 2.7255 K) is honoured.
155#[doc(hidden)]
156#[inline]
157pub fn theta_z(z: f64) -> f64 {
158    K_BOLTZMANN * T_CMB_0 * (1.0 + z) / M_E_C2
159}
160
161/// Photon temperature at redshift `z` assuming the default T_CMB_0.
162///
163/// Convenience helper; see the note on [`theta_z`] for production usage.
164#[doc(hidden)]
165#[inline]
166pub fn t_z(z: f64) -> f64 {
167    T_CMB_0 * (1.0 + z)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_theta_z_value() {
176        // θ_z ≈ 4.60e-10 * (1+z) at z=0
177        let theta_0 = theta_z(0.0);
178        assert!(
179            (theta_0 - 4.60e-10).abs() < 0.05e-10,
180            "theta_z(0) = {theta_0:.3e}, expected ~4.60e-10"
181        );
182    }
183
184    #[test]
185    fn test_beta_mu() {
186        // Literature value
187        assert!(
188            (BETA_MU - 2.1923).abs() < 0.001,
189            "beta_mu = {BETA_MU}, expected ~2.1923"
190        );
191        // Identity: β_μ ≡ 3ζ(3)/ζ(2) = 3·ZETA_3 / (π²/6) to machine precision
192        let expected = 3.0 * ZETA_3 / (std::f64::consts::PI.powi(2) / 6.0);
193        assert!(
194            (BETA_MU - expected).abs() / expected < 1e-14,
195            "BETA_MU = {BETA_MU}, 3ζ(3)/ζ(2) = {expected}"
196        );
197    }
198
199    #[test]
200    fn test_g1_g2_identities() {
201        // G₁ = π²/6 = ζ(2) exact
202        let g1_exact = std::f64::consts::PI.powi(2) / 6.0;
203        assert!(
204            (G1_PLANCK - g1_exact).abs() / g1_exact < 1e-14,
205            "G1 = {G1_PLANCK}, π²/6 = {g1_exact}"
206        );
207        // G₂ = 2ζ(3) exact
208        let g2_exact = 2.0 * ZETA_3;
209        assert!(
210            (G2_PLANCK - g2_exact).abs() / g2_exact < 1e-14,
211            "G2 = {G2_PLANCK}, 2ζ(3) = {g2_exact}"
212        );
213        // I₄ = 4·G₃ exact
214        assert!(
215            (I4_PLANCK - 4.0 * G3_PLANCK).abs() / I4_PLANCK < 1e-14,
216            "I4 = {I4_PLANCK}, 4·G₃ = {}",
217            4.0 * G3_PLANCK
218        );
219    }
220
221    #[test]
222    fn test_g3_planck() {
223        let pi = std::f64::consts::PI;
224        let expected = pi.powi(4) / 15.0;
225        assert!(
226            (G3_PLANCK - expected).abs() / expected < 1e-14,
227            "G3 = {G3_PLANCK}, expected {expected}"
228        );
229    }
230
231    #[test]
232    fn test_alpha_rho() {
233        // Numerical transcription guard: α_ρ = G₂/G₃ = 30ζ(3)/π⁴ ≈ 0.37020884...
234        // (Evaluating 30·ZETA_3/π⁴ against ALPHA_RHO in code would be tautological —
235        // ALPHA_RHO is defined as G2_PLANCK/G3_PLANCK which reduces to that ratio.)
236        assert!(
237            (ALPHA_RHO - 0.370_208_84).abs() < 1e-6,
238            "alpha_rho = {ALPHA_RHO}, expected ~0.37020884 (from 30ζ(3)/π⁴)"
239        );
240    }
241
242    #[test]
243    fn test_x_balanced() {
244        // x₀ = 4/(3α_ρ) ≈ 3.60
245        assert!(
246            (X_BALANCED - 3.602).abs() < 0.01,
247            "x_balanced = {X_BALANCED}, expected ~3.602"
248        );
249        // Cross-check: x₀ = 4G₃/(3G₂)
250        let x0_alt = 4.0 * G3_PLANCK / (3.0 * G2_PLANCK);
251        assert!(
252            (X_BALANCED - x0_alt).abs() < 1e-14,
253            "x_balanced definitions disagree"
254        );
255    }
256
257    #[test]
258    fn test_f_he() {
259        // Y_p=0.24: f_He = 0.24/(4*0.76) ≈ 0.0789
260        assert!((F_HE - 0.07895).abs() < 0.001);
261    }
262
263    /// Verify κ_c against the analytical formula and a numerical quadrature.
264    ///
265    /// κ_c = 12/β_μ − 9G₂/G₃ ≈ 2.1419
266    ///
267    /// Also verified via numerical integration: κ_c = 3∫x³M(x)dx / G₃
268    /// where M(x) = (x/β_μ − 1) G_bb(x) / x.
269    #[test]
270    fn test_kappa_c_analytical_and_numerical() {
271        // Analytical form (how KAPPA_C is defined in constants.rs)
272        let kappa_from_formula = 12.0 / BETA_MU - 9.0 * G2_PLANCK / G3_PLANCK;
273        assert!(
274            (KAPPA_C - kappa_from_formula).abs() < 1e-14,
275            "KAPPA_C mismatch: stored={KAPPA_C}, computed={kappa_from_formula}"
276        );
277
278        // Literature value: κ_c ≈ 2.1419
279        assert!(
280            (KAPPA_C - 2.1419).abs() < 0.001,
281            "KAPPA_C = {KAPPA_C}, expected ~2.1419"
282        );
283
284        // Numerical quadrature: κ_c = 3 ∫x³ M(x) dx / G₃
285        // where M(x) = (x/β_μ − 1) G_bb(x)/x and G_bb(x) = x eˣ/(eˣ−1)².
286        // So M(x) = (x/β_μ − 1) eˣ/(eˣ−1)².
287        //
288        // Using ∫xⁿ eˣ/(eˣ−1)² dx = n Gₙ₋₁ (integration by parts):
289        //   ∫x³ M(x) dx = (1/β_μ)×4G₃ − 3G₂
290        //   κ_c = 3(4G₃/β_μ − 3G₂)/G₃ = 12/β_μ − 9G₂/G₃
291        let n = 10000;
292        let x_min = 0.001_f64;
293        let x_max = 30.0_f64;
294        let dx = (x_max - x_min) / n as f64;
295        let mut integral = 0.0;
296        for i in 0..n {
297            let x = x_min + (i as f64 + 0.5) * dx;
298            let ex = x.exp();
299            // M(x) = (x/β_μ − 1) eˣ/(eˣ−1)²
300            let m_x = (x / BETA_MU - 1.0) * ex / ((ex - 1.0) * (ex - 1.0));
301            integral += x * x * x * m_x * dx;
302        }
303        let kappa_numerical = 3.0 * integral / G3_PLANCK;
304        let rel_err = (KAPPA_C - kappa_numerical).abs() / KAPPA_C;
305        assert!(
306            rel_err < 0.001,
307            "κ_c numerical = {kappa_numerical:.6}, analytical = {KAPPA_C:.6}, err = {rel_err:.2e}"
308        );
309    }
310}