spectroxide/
dark_photon.rs1use crate::constants::*;
18use crate::cosmology::Cosmology;
19use crate::recombination::ionization_fraction;
20
21pub fn plasma_frequency_ev(z: f64, cosmo: &Cosmology) -> f64 {
25 let hbar_ev_s = HBAR / EV_IN_JOULES;
26 let x_e = ionization_fraction(z, cosmo);
27 let n_e = cosmo.n_e(z, x_e);
28 let factor = 4.0 * std::f64::consts::PI * ALPHA_FS * HBAR * C_LIGHT / M_ELECTRON;
29 hbar_ev_s * (n_e * factor).sqrt()
30}
31
32pub fn resonance_redshift(m_ev: f64, cosmo: &Cosmology) -> Option<f64> {
37 let z_min = 10.0_f64;
38 let z_max = 3.0e7_f64;
39 let f = |z: f64| plasma_frequency_ev(z, cosmo) - m_ev;
40 let (f_lo, f_hi) = (f(z_min), f(z_max));
41 if !f_lo.is_finite() || !f_hi.is_finite() || f_lo * f_hi > 0.0 {
42 return None;
43 }
44 let mut lo = z_min;
45 let mut hi = z_max;
46 let mut f_lo = f_lo;
47 for _ in 0..80 {
48 let mid = 0.5 * (lo + hi);
49 let f_mid = f(mid);
50 if (hi - lo) / mid < 1e-8 {
51 return Some(mid);
52 }
53 if f_lo * f_mid <= 0.0 {
54 hi = mid;
55 } else {
56 lo = mid;
57 f_lo = f_mid;
58 }
59 }
60 Some(0.5 * (lo + hi))
61}
62
63pub fn dln_omega_pl_sq_dlna(z: f64, cosmo: &Cosmology) -> f64 {
65 let dz = (z * 1.0e-4).max(0.1);
66 let x_e = ionization_fraction(z, cosmo);
67 if x_e <= 1e-30 {
68 return 3.0;
69 }
70 let x_e_plus = ionization_fraction(z + dz, cosmo);
71 let x_e_minus = ionization_fraction(z - dz, cosmo);
72 let dlnxe_dz = (x_e_plus - x_e_minus) / (2.0 * dz * x_e);
73 ((1.0 + z) * dlnxe_dz + 3.0).abs()
74}
75
76pub fn gamma_con(epsilon: f64, m_ev: f64, cosmo: &Cosmology) -> Option<(f64, f64)> {
84 let z_res = resonance_redshift(m_ev, cosmo)?;
85 let t_cmb_ev = K_BOLTZMANN * cosmo.t_cmb * (1.0 + z_res) / EV_IN_JOULES;
86 let hbar_ev_s = HBAR / EV_IN_JOULES;
87 let h_ev = hbar_ev_s * cosmo.hubble(z_res);
88 let d = dln_omega_pl_sq_dlna(z_res, cosmo);
89 let gc = std::f64::consts::PI * epsilon * epsilon * m_ev * m_ev / (d * t_cmb_ev * h_ev);
90 Some((gc, z_res))
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use approx::assert_relative_eq;
97
98 #[test]
99 fn plasma_frequency_matches_first_principles() {
100 let cosmo = Cosmology::default();
106
107 let z = 1.0e5_f64;
109 let x_e = ionization_fraction(z, &cosmo);
110 assert!(x_e > 0.99, "z=1e5 should be fully ionized, got X_e={x_e}");
111 let n_e = cosmo.n_e(z, x_e);
112 let hbar_ev_s = HBAR / EV_IN_JOULES;
113 let expected = hbar_ev_s
114 * (n_e * 4.0 * std::f64::consts::PI * ALPHA_FS * HBAR * C_LIGHT / M_ELECTRON).sqrt();
115 let omega = plasma_frequency_ev(z, &cosmo);
116 let rel_err = (omega - expected).abs() / expected;
117 assert!(
118 rel_err < 1e-12,
119 "ω_pl(1e5) = {omega:.4e} vs first-principles {expected:.4e}, rel_err={rel_err:.2e}"
120 );
121
122 let z1 = 5.0e4_f64;
125 let z2 = 2.0e5_f64;
126 let ratio = plasma_frequency_ev(z2, &cosmo) / plasma_frequency_ev(z1, &cosmo);
127 let expected_ratio = ((1.0 + z2) / (1.0 + z1)).powf(1.5);
128 let scale_err = (ratio - expected_ratio).abs() / expected_ratio;
129 assert!(
130 scale_err < 1e-6,
131 "ω_pl(1+z)^1.5 scaling violated: ratio={ratio:.6}, expected={expected_ratio:.6}"
132 );
133 }
134
135 #[test]
136 fn resonance_round_trip() {
137 let cosmo = Cosmology::default();
138 for m_ev in [3e-8, 1e-7, 1e-6, 1e-5, 1e-4] {
139 let z_res = resonance_redshift(m_ev, &cosmo).expect("no resonance");
140 let omega = plasma_frequency_ev(z_res, &cosmo);
141 assert_relative_eq!(omega, m_ev, max_relative = 1e-5);
142 }
143 }
144
145 #[test]
146 fn gamma_con_scales_as_epsilon_squared() {
147 let cosmo = Cosmology::default();
148 let (g1, _) = gamma_con(1e-9, 1e-6, &cosmo).unwrap();
149 let (g2, _) = gamma_con(2e-9, 1e-6, &cosmo).unwrap();
150 assert_relative_eq!(g2 / g1, 4.0, max_relative = 1e-10);
151 }
152
153 #[test]
154 fn gamma_con_matches_chluba_cyr() {
155 let cosmo = Cosmology::default();
165 let (gc, z_res) = gamma_con(1.0, 1e-7, &cosmo).unwrap();
166 assert!(
167 (z_res - 3.21e4).abs() / 3.21e4 < 0.05,
168 "z_res = {z_res:.3e}, expected ~3.21e4 (±5%) from ω_pl ∝ (1+z)^1.5"
169 );
170 assert!(
174 (gc - 9.3e10).abs() / 9.3e10 < 0.2,
175 "γ_con/ε² = {gc:.3e}, expected ~9.3e10 from Chluba & Cyr 2024 Eq. 6 (±20%)"
176 );
177 }
178}