use std::f64::consts::PI;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
use crate::constants::{G, MPC_KM};
use crate::error::BrahmandaError;
#[derive(Serialize, Deserialize)]
pub struct Cosmology {
pub h: f64,
pub omega_m: f64,
pub omega_b: f64,
pub omega_k: f64,
pub omega_r: f64,
pub n_s: f64,
pub sigma_8: f64,
pub w0: f64,
pub wa: f64,
pub t_cmb: f64,
pub h0_si: f64,
pub rho_crit: f64,
pub omega_lambda: f64,
#[serde(skip)]
a_s_cache: OnceLock<f64>,
}
impl core::fmt::Debug for Cosmology {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Cosmology")
.field("h", &self.h)
.field("omega_m", &self.omega_m)
.field("omega_b", &self.omega_b)
.field("omega_k", &self.omega_k)
.field("omega_r", &self.omega_r)
.field("n_s", &self.n_s)
.field("sigma_8", &self.sigma_8)
.field("w0", &self.w0)
.field("wa", &self.wa)
.field("t_cmb", &self.t_cmb)
.field("h0_si", &self.h0_si)
.field("rho_crit", &self.rho_crit)
.field("omega_lambda", &self.omega_lambda)
.finish()
}
}
impl Clone for Cosmology {
fn clone(&self) -> Self {
Self {
h: self.h,
omega_m: self.omega_m,
omega_b: self.omega_b,
omega_k: self.omega_k,
omega_r: self.omega_r,
n_s: self.n_s,
sigma_8: self.sigma_8,
w0: self.w0,
wa: self.wa,
t_cmb: self.t_cmb,
h0_si: self.h0_si,
rho_crit: self.rho_crit,
omega_lambda: self.omega_lambda,
a_s_cache: OnceLock::new(),
}
}
}
impl PartialEq for Cosmology {
fn eq(&self, other: &Self) -> bool {
self.h == other.h
&& self.omega_m == other.omega_m
&& self.omega_b == other.omega_b
&& self.omega_k == other.omega_k
&& self.omega_r == other.omega_r
&& self.n_s == other.n_s
&& self.sigma_8 == other.sigma_8
&& self.w0 == other.w0
&& self.wa == other.wa
&& self.t_cmb == other.t_cmb
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FilterFunction {
TopHat,
Gaussian,
SharpK,
}
impl FilterFunction {
#[inline]
#[must_use]
pub fn window(self, x: f64) -> f64 {
match self {
Self::TopHat => {
if x.abs() < 1e-6 {
1.0 - x * x / 10.0
} else {
3.0 * (x.sin() - x * x.cos()) / (x * x * x)
}
}
Self::Gaussian => (-x * x / 2.0).exp(),
Self::SharpK => {
if x < 1.0 {
1.0
} else {
0.0
}
}
}
}
}
impl Cosmology {
#[allow(clippy::too_many_arguments)]
pub fn new(
h: f64,
omega_m: f64,
omega_b: f64,
omega_k: f64,
omega_r: f64,
n_s: f64,
sigma_8: f64,
w0: f64,
wa: f64,
t_cmb: f64,
) -> Result<Self, BrahmandaError> {
if h <= 0.0 || !h.is_finite() {
return Err(BrahmandaError::InvalidStructure(
"Cosmology: h must be positive and finite".to_string(),
));
}
if omega_m < 0.0 || !omega_m.is_finite() {
return Err(BrahmandaError::InvalidStructure(
"Cosmology: omega_m must be non-negative and finite".to_string(),
));
}
if omega_b < 0.0 || omega_b > omega_m || !omega_b.is_finite() {
return Err(BrahmandaError::InvalidStructure(
"Cosmology: omega_b must be in [0, omega_m]".to_string(),
));
}
if sigma_8 <= 0.0 || !sigma_8.is_finite() {
return Err(BrahmandaError::InvalidStructure(
"Cosmology: sigma_8 must be positive and finite".to_string(),
));
}
if t_cmb <= 0.0 || !t_cmb.is_finite() {
return Err(BrahmandaError::InvalidStructure(
"Cosmology: t_cmb must be positive and finite".to_string(),
));
}
let h0_si = h * 100.0 / MPC_KM;
let rho_crit = 3.0 * h0_si * h0_si / (8.0 * PI * G);
let omega_lambda = 1.0 - omega_m - omega_k - omega_r;
Ok(Self {
h,
omega_m,
omega_b,
omega_k,
omega_r,
n_s,
sigma_8,
w0,
wa,
t_cmb,
h0_si,
rho_crit,
omega_lambda,
a_s_cache: OnceLock::new(),
})
}
#[must_use]
pub fn planck2018() -> Self {
Self::new(
0.674, 0.315, 0.049, 0.0, 9.14e-5, 0.965, 0.811, -1.0, 0.0, 2.725,
)
.expect("Planck 2018 parameters are valid")
}
#[must_use]
pub fn wmap9() -> Self {
Self::new(
0.693, 0.286, 0.047, 0.0, 8.5e-5, 0.965, 0.82, -1.0, 0.0, 2.725,
)
.expect("WMAP9 parameters are valid")
}
pub fn power_spectrum_amplitude(&self) -> f64 {
*self.a_s_cache.get_or_init(|| {
let ln_k_min = -4.0 * std::f64::consts::LN_10; let ln_k_max = 100.0_f64.ln();
let integrand = |u: f64| -> f64 {
let k = u.exp();
let t = crate::power_spectrum::transfer_function_eh(
k,
self.omega_m,
self.omega_b,
self.h,
)
.unwrap_or(0.0);
let w = FilterFunction::TopHat.window(k * 8.0);
k.powi(3) * k.powf(self.n_s) * t * t * w * w / (2.0 * PI * PI)
};
let sigma8_raw_sq =
hisab::calc::integral_adaptive_simpson(integrand, ln_k_min, ln_k_max, 1e-8)
.expect("sigma8 normalization integral must converge");
if sigma8_raw_sq > 0.0 {
self.sigma_8 * self.sigma_8 / sigma8_raw_sq
} else {
1.0
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_planck2018_preset() {
let c = Cosmology::planck2018();
assert!((c.h - 0.674).abs() < 1e-10);
assert!((c.omega_m - 0.315).abs() < 1e-10);
assert!((c.omega_b - 0.049).abs() < 1e-10);
assert!((c.omega_lambda - 0.6849086).abs() < 1e-4);
assert!(c.h0_si > 0.0);
assert!(c.rho_crit > 0.0);
}
#[test]
fn test_wmap9_preset() {
let c = Cosmology::wmap9();
assert!((c.h - 0.693).abs() < 1e-10);
assert!((c.omega_m - 0.286).abs() < 1e-10);
}
#[test]
fn test_invalid_h() {
assert!(Cosmology::new(0.0, 0.3, 0.04, 0.0, 0.0, 0.96, 0.8, -1.0, 0.0, 2.7).is_err());
assert!(Cosmology::new(-1.0, 0.3, 0.04, 0.0, 0.0, 0.96, 0.8, -1.0, 0.0, 2.7).is_err());
assert!(Cosmology::new(f64::NAN, 0.3, 0.04, 0.0, 0.0, 0.96, 0.8, -1.0, 0.0, 2.7).is_err());
}
#[test]
fn test_invalid_omega_b() {
assert!(Cosmology::new(0.7, 0.3, 0.5, 0.0, 0.0, 0.96, 0.8, -1.0, 0.0, 2.7).is_err());
}
#[test]
fn test_invalid_sigma_8() {
assert!(Cosmology::new(0.7, 0.3, 0.04, 0.0, 0.0, 0.96, 0.0, -1.0, 0.0, 2.7).is_err());
assert!(Cosmology::new(0.7, 0.3, 0.04, 0.0, 0.0, 0.96, -1.0, -1.0, 0.0, 2.7).is_err());
}
#[test]
fn test_power_spectrum_amplitude_positive() {
let c = Cosmology::planck2018();
let a_s = c.power_spectrum_amplitude();
assert!(a_s > 0.0, "A_s = {a_s}");
}
#[test]
fn test_filter_tophat_at_zero() {
assert!((FilterFunction::TopHat.window(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_filter_gaussian_at_zero() {
assert!((FilterFunction::Gaussian.window(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_filter_sharpk() {
assert!((FilterFunction::SharpK.window(0.5) - 1.0).abs() < 1e-10);
assert!((FilterFunction::SharpK.window(1.5)).abs() < 1e-10);
}
#[test]
fn test_clone_fresh_cache() {
let c = Cosmology::planck2018();
let _ = c.power_spectrum_amplitude(); let c2 = c.clone();
let a1 = c.power_spectrum_amplitude();
let a2 = c2.power_spectrum_amplitude();
assert!((a1 - a2).abs() / a1 < 1e-6);
}
#[test]
fn test_serde_roundtrip() {
let c = Cosmology::planck2018();
let json = serde_json::to_string(&c).unwrap();
let back: Cosmology = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
}