1use crate::constants::*;
6
7#[derive(Debug, Clone)]
9pub struct Cosmology {
10 pub t_cmb: f64,
12 pub omega_b: f64,
14 pub omega_cdm: f64,
16 pub h: f64,
18 pub n_eff: f64,
20 pub y_p: f64,
22
23 cached_h0: f64,
25 cached_omega_b_frac: f64,
26 cached_omega_cdm_frac: f64,
27 cached_omega_m: f64,
28 cached_omega_gamma: f64,
29 cached_omega_rel: f64,
30 cached_omega_lambda: f64,
31 cached_n_h_prefactor: f64,
33 cached_f_he: f64,
35 cached_baryon_photon_prefactor: f64,
37 cached_rho_gamma_0: f64,
39}
40
41impl Cosmology {
42 pub fn validate(&self) -> Result<(), String> {
48 if !self.t_cmb.is_finite() || self.t_cmb <= 0.0 {
49 return Err(format!(
50 "t_cmb must be positive and finite, got {}",
51 self.t_cmb
52 ));
53 }
54 if !self.omega_b.is_finite() || self.omega_b <= 0.0 {
55 return Err(format!(
56 "omega_b must be positive and finite, got {}",
57 self.omega_b
58 ));
59 }
60 if !self.omega_cdm.is_finite() || self.omega_cdm < 0.0 {
61 return Err(format!(
62 "omega_cdm must be non-negative and finite, got {}",
63 self.omega_cdm
64 ));
65 }
66 if !self.h.is_finite() || self.h <= 0.0 {
67 return Err(format!("h must be positive and finite, got {}", self.h));
68 }
69 if self.h > 10.0 {
70 return Err(format!(
71 "h={} implies H0={} km/s/Mpc, which is nonsensical",
72 self.h,
73 self.h * 100.0
74 ));
75 }
76 if !self.n_eff.is_finite() || self.n_eff < 0.0 || self.n_eff > 20.0 {
77 return Err(format!("n_eff must be in [0, 20], got {}", self.n_eff));
78 }
79 if !self.y_p.is_finite() || self.y_p < 0.0 || self.y_p >= 1.0 {
80 return Err(format!("y_p must be in [0, 1), got {}", self.y_p));
81 }
82 Ok(())
83 }
84
85 pub fn new(
90 t_cmb: f64,
91 omega_b: f64,
92 omega_cdm: f64,
93 h: f64,
94 n_eff: f64,
95 y_p: f64,
96 ) -> Result<Self, String> {
97 let tmp = Cosmology {
99 t_cmb,
100 omega_b,
101 omega_cdm,
102 h,
103 n_eff,
104 y_p,
105 cached_h0: 0.0,
106 cached_omega_b_frac: 0.0,
107 cached_omega_cdm_frac: 0.0,
108 cached_omega_m: 0.0,
109 cached_omega_gamma: 0.0,
110 cached_omega_rel: 0.0,
111 cached_omega_lambda: 0.0,
112 cached_n_h_prefactor: 0.0,
113 cached_f_he: 0.0,
114 cached_baryon_photon_prefactor: 0.0,
115 cached_rho_gamma_0: 0.0,
116 };
117 tmp.validate()?;
118 Ok(Self::new_unchecked(
119 t_cmb, omega_b, omega_cdm, h, n_eff, y_p,
120 ))
121 }
122
123 pub fn new_unchecked(
130 t_cmb: f64,
131 omega_b: f64,
132 omega_cdm: f64,
133 h: f64,
134 n_eff: f64,
135 y_p: f64,
136 ) -> Self {
137 let h0 = 100.0 * h * KM_PER_MPC;
138 let h2 = h * h;
139 let omega_b_frac = omega_b / h2;
140 let omega_cdm_frac = omega_cdm / h2;
141 let omega_m = omega_b_frac + omega_cdm_frac;
142
143 let rho_gamma = std::f64::consts::PI.powi(2) / 15.0 * K_BOLTZMANN.powi(4) * t_cmb.powi(4)
144 / (HBAR.powi(3) * C_LIGHT.powi(3));
145 let rho_crit = 3.0 * h0 * h0 * C_LIGHT * C_LIGHT / (8.0 * std::f64::consts::PI * G_NEWTON);
146 let omega_gamma = rho_gamma / rho_crit;
147 let omega_rel =
148 omega_gamma * (1.0 + n_eff * (7.0 / 8.0) * (4.0_f64 / 11.0).powf(4.0 / 3.0));
149 let omega_lambda = 1.0 - omega_m - omega_rel;
150
151 let rho_crit_mass = 3.0 * h0 * h0 / (8.0 * std::f64::consts::PI * G_NEWTON);
154 let rho_b0 = omega_b_frac * rho_crit_mass;
155 let n_h_prefactor = (1.0 - y_p) * rho_b0 / M_PROTON;
156
157 let f_he = y_p / (4.0 * (1.0 - y_p));
158 let baryon_photon_prefactor = 3.0 * omega_b_frac / (4.0 * omega_gamma);
159
160 Cosmology {
161 t_cmb,
162 omega_b,
163 omega_cdm,
164 h,
165 n_eff,
166 y_p,
167 cached_h0: h0,
168 cached_omega_b_frac: omega_b_frac,
169 cached_omega_cdm_frac: omega_cdm_frac,
170 cached_omega_m: omega_m,
171 cached_omega_gamma: omega_gamma,
172 cached_omega_rel: omega_rel,
173 cached_omega_lambda: omega_lambda,
174 cached_n_h_prefactor: n_h_prefactor,
175 cached_f_he: f_he,
176 cached_baryon_photon_prefactor: baryon_photon_prefactor,
177 cached_rho_gamma_0: rho_gamma,
178 }
179 }
180
181 pub fn planck2015() -> Self {
183 Self::new_unchecked(2.726, 0.02225, 0.1198, 0.6727, 3.046, 0.2467)
184 }
185
186 pub fn planck2018() -> Self {
188 Self::new_unchecked(2.7255, 0.02237, 0.1200, 0.6736, 3.044, 0.2454)
191 }
192
193 #[inline]
195 pub fn h0(&self) -> f64 {
196 self.cached_h0
197 }
198
199 #[inline]
201 pub fn omega_b_frac(&self) -> f64 {
202 self.cached_omega_b_frac
203 }
204
205 #[inline]
207 pub fn omega_cdm_frac(&self) -> f64 {
208 self.cached_omega_cdm_frac
209 }
210
211 #[inline]
213 pub fn omega_m(&self) -> f64 {
214 self.cached_omega_m
215 }
216
217 #[inline]
219 pub fn omega_gamma(&self) -> f64 {
220 self.cached_omega_gamma
221 }
222
223 #[inline]
225 pub fn omega_rel(&self) -> f64 {
226 self.cached_omega_rel
227 }
228
229 #[inline]
231 pub fn omega_lambda(&self) -> f64 {
232 self.cached_omega_lambda
233 }
234
235 #[inline]
237 pub fn e_of_z(&self, z: f64) -> f64 {
238 let opz = 1.0 + z;
239 (self.cached_omega_m * opz.powi(3)
240 + self.cached_omega_rel * opz.powi(4)
241 + self.cached_omega_lambda)
242 .sqrt()
243 }
244
245 #[inline]
248 pub fn theta_z(&self, z: f64) -> f64 {
249 crate::constants::K_BOLTZMANN * self.t_cmb * (1.0 + z) / crate::constants::M_E_C2
250 }
251
252 #[inline]
254 pub fn hubble(&self, z: f64) -> f64 {
255 self.cached_h0 * self.e_of_z(z)
256 }
257
258 pub fn dt_dz(&self, z: f64) -> f64 {
260 -1.0 / (self.hubble(z) * (1.0 + z))
261 }
262
263 pub fn z_eq(&self) -> f64 {
265 self.cached_omega_m / self.cached_omega_rel - 1.0
266 }
267
268 #[inline]
271 pub fn n_h(&self, z: f64) -> f64 {
272 self.cached_n_h_prefactor * (1.0 + z).powi(3)
273 }
274
275 #[inline]
278 pub fn n_he(&self, z: f64) -> f64 {
279 self.cached_f_he * self.n_h(z)
280 }
281
282 #[inline]
284 pub fn f_he(&self) -> f64 {
285 self.cached_f_he
286 }
287
288 #[inline]
290 pub fn n_e(&self, z: f64, x_e: f64) -> f64 {
291 x_e * self.n_h(z)
292 }
293
294 #[inline]
296 pub fn t_compton(&self, z: f64, x_e: f64) -> f64 {
297 1.0 / (SIGMA_THOMSON * self.n_e(z, x_e) * C_LIGHT)
298 }
299
300 #[inline]
302 pub fn rho_gamma(&self, z: f64) -> f64 {
303 self.cached_rho_gamma_0 * (1.0 + z).powi(4)
304 }
305
306 pub fn n_gamma(&self, z: f64) -> f64 {
308 let kt_over_hbar_c = K_BOLTZMANN * self.t_cmb * (1.0 + z) / (HBAR * C_LIGHT);
310 2.0 * ZETA_3 / std::f64::consts::PI.powi(2) * kt_over_hbar_c.powi(3)
311 }
312
313 #[inline]
318 pub fn baryon_photon_ratio(&self, z: f64) -> f64 {
319 self.cached_baryon_photon_prefactor / (1.0 + z)
320 }
321
322 pub fn compton_y_parameter(&self, z: f64) -> f64 {
332 if z < 1e-6 {
333 return 0.0;
334 }
335 let recomb = crate::recombination::RecombinationHistory::new(self);
336 self.compton_y_parameter_with_recomb(z, &recomb)
337 }
338
339 pub fn compton_y_parameter_with_recomb(
344 &self,
345 z: f64,
346 recomb: &crate::recombination::RecombinationHistory,
347 ) -> f64 {
348 if z < 1e-6 {
349 return 0.0;
350 }
351
352 let ln_min = 0.0_f64; let ln_max = (1.0 + z).ln();
354
355 let n = 128;
356 let h = (ln_max - ln_min) / (n as f64);
357 let mut result = 0.0;
358
359 for i in 0..n {
360 let u = ln_min + (i as f64 + 0.5) * h;
361 let zp = u.exp() - 1.0;
362 let opz = 1.0 + zp;
363
364 let x_e = recomb.x_e(zp);
365 let n_e = self.n_e(zp, x_e);
366 const Z_DEC: f64 = 200.0;
373 let theta_e = if zp > Z_DEC {
374 K_BOLTZMANN * self.t_cmb * opz / M_E_C2
375 } else {
376 let ratio = opz / (1.0 + Z_DEC);
378 K_BOLTZMANN * self.t_cmb * (1.0 + Z_DEC) * ratio * ratio / M_E_C2
379 };
380
381 result += theta_e * SIGMA_THOMSON * C_LIGHT * n_e / self.hubble(zp) * h;
384 }
385
386 result
387 }
388
389 pub fn cosmic_time(&self, z: f64) -> f64 {
393 let u_low = (1.0 + z).ln();
394 let u_high = (1.0 + 1.0e9_f64).ln();
395
396 let n = 64;
397 let h = (u_high - u_low) / (n as f64);
398 let mut result = 0.0;
399 for i in 0..n {
400 let u = u_low + (i as f64 + 0.5) * h;
401 let zp = u.exp() - 1.0;
402 let dzp_du = 1.0 + zp;
403 result += (1.0 / (self.hubble(zp) * (1.0 + zp))) * dzp_du * h;
404 }
405 result
406 }
407}
408
409impl Default for Cosmology {
410 fn default() -> Self {
420 Self::new_unchecked(
421 T_CMB_0,
422 0.044 * 0.71 * 0.71, (0.26 - 0.044) * 0.71 * 0.71,
424 0.71,
425 N_EFF,
426 Y_P,
427 )
428 }
429}
430
431#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_default_params() {
438 let cosmo = Cosmology::default();
439 assert!(
440 (cosmo.omega_m() - 0.26).abs() < 0.001,
441 "Omega_m = {}, expected ~0.26",
442 cosmo.omega_m()
443 );
444 assert!(
445 (cosmo.omega_b_frac() - 0.044).abs() < 0.001,
446 "Omega_b = {}, expected ~0.044",
447 cosmo.omega_b_frac()
448 );
449 }
450
451 #[test]
452 fn test_hubble_today() {
453 let cosmo = Cosmology::default();
454 let h0 = cosmo.hubble(0.0);
455 let h0_expected = 100.0 * 0.71 * KM_PER_MPC;
456 assert!(
457 (h0 - h0_expected).abs() / h0_expected < 1e-10,
458 "H(0) = {h0:.3e}, expected {h0_expected:.3e}"
459 );
460 }
461
462 #[test]
463 fn test_z_eq() {
464 let cosmo = Cosmology::default();
465 let z_eq = cosmo.z_eq();
466 assert!(
468 z_eq > 3000.0 && z_eq < 3600.0,
469 "z_eq = {z_eq}, expected ~3300 for default cosmology"
470 );
471 }
472
473 #[test]
474 fn test_e_of_z_today() {
475 let cosmo = Cosmology::default();
476 assert!((cosmo.e_of_z(0.0) - 1.0).abs() < 1e-10);
477 }
478
479 #[test]
480 fn test_radiation_dominated() {
481 let cosmo = Cosmology::default();
482 let z = 1.0e6;
483 let e = cosmo.e_of_z(z);
484 let e_rad = cosmo.omega_rel().sqrt() * (1.0 + z).powi(2);
485 assert!(
486 (e - e_rad).abs() / e_rad < 0.01,
487 "E({z}) = {e:.3e}, radiation approx = {e_rad:.3e}"
488 );
489 }
490
491 #[test]
492 fn test_n_h_positive_and_scaling() {
493 let cosmo = Cosmology::default();
494 assert!(cosmo.n_h(0.0) > 0.0);
495 assert!(cosmo.n_h(1000.0) > cosmo.n_h(0.0));
496 let ratio = cosmo.n_h(1000.0) / cosmo.n_h(0.0);
498 let expected = 1001.0_f64.powi(3);
499 assert!(
500 (ratio / expected - 1.0).abs() < 1e-10,
501 "n_H should scale as (1+z)³: ratio = {ratio:.6e}, expected = {expected:.6e}"
502 );
503 }
504
505 #[test]
506 fn test_cosmic_time_order_and_magnitude() {
507 let cosmo = Cosmology::default();
508 let t1 = cosmo.cosmic_time(1000.0);
509 let t2 = cosmo.cosmic_time(100.0);
510 assert!(t2 > t1, "t(z=100) should be > t(z=1000)");
511 let t0 = cosmo.cosmic_time(0.0);
513 assert!(
514 t0 > 3e17 && t0 < 5e17,
515 "t(z=0) = {t0:.3e} s, expected ~4.3e17 s (13.7 Gyr)"
516 );
517 }
518
519 #[test]
520 fn test_compton_y_parameter_high_z() {
521 let cosmo = Cosmology::default();
522 let yc = cosmo.compton_y_parameter(1e5);
524 assert!(yc > 0.1, "y_C(1e5) = {yc:.3e}, should be > 0.1");
525 let yc_deep = cosmo.compton_y_parameter(1e6);
527 assert!(
528 yc_deep > 10.0,
529 "y_C(1e6) = {yc_deep:.3e}, should be >> 1 (deep thermalization era)"
530 );
531 }
532
533 #[test]
534 fn test_compton_y_parameter_low_z() {
535 let cosmo = Cosmology::default();
536 let yc = cosmo.compton_y_parameter(500.0);
538 assert!(
539 yc < 0.1,
540 "y_C(500) = {yc:.3e}, should be << 1 (post-recombination)"
541 );
542 }
543
544 #[test]
545 fn test_compton_y_parameter_monotonic() {
546 let cosmo = Cosmology::default();
547 let y1 = cosmo.compton_y_parameter(500.0);
548 let y2 = cosmo.compton_y_parameter(2000.0);
549 let y3 = cosmo.compton_y_parameter(1e5);
550 assert!(
551 y3 > y2 && y2 > y1,
552 "y_C should be monotonically increasing: {y1:.3e} < {y2:.3e} < {y3:.3e}"
553 );
554 }
555
556 #[test]
557 fn test_baryon_photon_ratio() {
558 let cosmo = Cosmology::default();
559 let r_0 = cosmo.baryon_photon_ratio(0.0);
561 assert!(
562 r_0 > 100.0,
563 "R(0) = {r_0:.1}, expected >> 1 (matter dominated today)"
564 );
565
566 let r_1100 = cosmo.baryon_photon_ratio(1100.0);
567 assert!(
569 r_1100 > 0.1 && r_1100 < 2.0,
570 "R(1100) = {r_1100:.3}, expected ~0.6"
571 );
572
573 let ratio = r_0 / r_1100;
575 assert!(
576 (ratio - 1101.0).abs() / 1101.0 < 0.01,
577 "R should scale as 1/(1+z)"
578 );
579 }
580
581 #[test]
582 fn test_density_accessors() {
583 let cosmo = Cosmology::default();
584 assert!(cosmo.omega_gamma() > 0.0);
586 assert!(cosmo.omega_rel() > cosmo.omega_gamma()); assert!(cosmo.omega_lambda() > 0.0);
588 assert!(cosmo.omega_cdm_frac() > 0.0);
589 assert!(cosmo.rho_gamma(0.0) > 0.0);
590 assert!(cosmo.n_he(0.0) > 0.0);
591 assert!(cosmo.n_e(0.0, 1.0) > 0.0);
592 assert!(cosmo.t_compton(1e4, 1.0) > 0.0);
593 assert!(cosmo.dt_dz(1e4) < 0.0); let n_gam = cosmo.n_gamma(0.0);
598 assert!(
599 (n_gam - 4.1e8).abs() / 4.1e8 < 0.02,
600 "n_gamma(0) = {n_gam:.3e}, expected ~4.1e8 /m³"
601 );
602 let n_h = cosmo.n_h(0.0);
604 assert!(
605 (n_h - 0.19).abs() / 0.19 < 0.05,
606 "n_H(0) = {n_h:.4}, expected ~0.19 /m³"
607 );
608 }
609
610 #[test]
611 fn test_planck2015_preset() {
612 let cosmo = Cosmology::planck2015();
613 assert!((cosmo.h - 0.6727).abs() < 0.001);
614 assert!((cosmo.omega_b - 0.02225).abs() < 1e-5);
615 assert!((cosmo.y_p - 0.2467).abs() < 1e-4);
616 }
617
618 #[test]
619 fn test_planck2018_preset() {
620 let cosmo = Cosmology::planck2018();
621 assert!(
622 (cosmo.h - 0.6736).abs() < 0.001,
623 "h={}, expected 0.6736",
624 cosmo.h
625 );
626 assert!(
627 (cosmo.omega_b - 0.02237).abs() < 1e-5,
628 "omega_b={}, expected 0.02237",
629 cosmo.omega_b
630 );
631 assert!(
632 (cosmo.omega_cdm - 0.1200).abs() < 1e-4,
633 "omega_cdm={}, expected 0.1200",
634 cosmo.omega_cdm
635 );
636 assert!(
637 (cosmo.t_cmb - 2.7255).abs() < 0.001,
638 "t_cmb={}, expected 2.7255",
639 cosmo.t_cmb
640 );
641 assert!(
642 (cosmo.y_p - 0.2454).abs() < 1e-4,
643 "y_p={}, expected 0.2454",
644 cosmo.y_p
645 );
646 assert!(
647 (cosmo.n_eff - 3.044).abs() < 0.01,
648 "n_eff={}, expected 3.044",
649 cosmo.n_eff
650 );
651 assert!(cosmo.hubble(0.0) > 0.0);
653 assert!(cosmo.n_h(0.0) > 0.0);
654 }
655
656 #[test]
657 fn test_cached_f_he() {
658 let cosmo = Cosmology::default();
659 let expected = cosmo.y_p / (4.0 * (1.0 - cosmo.y_p));
660 assert!((cosmo.f_he() - expected).abs() < 1e-15);
661 }
662
663 #[test]
664 fn test_cosmology_new_custom_params() {
665 let cosmo = Cosmology::new(2.725, 0.022, 0.12, 0.67, 3.046, 0.245).unwrap();
668
669 assert!((cosmo.t_cmb - 2.725).abs() < 1e-10);
671 assert!((cosmo.omega_b - 0.022).abs() < 1e-10);
672 assert!((cosmo.h - 0.67).abs() < 1e-10);
673 assert!((cosmo.y_p - 0.245).abs() < 1e-10);
674
675 let expected_f_he = 0.245 / (4.0 * (1.0 - 0.245));
677 assert!(
678 (cosmo.f_he() - expected_f_he).abs() < 1e-12,
679 "f_he = {}, expected {expected_f_he}",
680 cosmo.f_he()
681 );
682
683 let n_h_0 = cosmo.n_h(0.0);
685 assert!(n_h_0 > 0.0 && n_h_0.is_finite());
686
687 let cosmo2 = Cosmology::new(2.725, 0.044, 0.12, 0.67, 3.046, 0.245).unwrap();
689 let n_h_0_2 = cosmo2.n_h(0.0);
690 let ratio = n_h_0_2 / n_h_0;
692 assert!(
693 (ratio - 2.0).abs() < 0.01,
694 "Doubling omega_b should double n_H: ratio={ratio:.4}"
695 );
696
697 let h_z0 = cosmo.hubble(0.0);
699 assert!(h_z0 > 0.0 && h_z0.is_finite());
700 }
701}