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}