use core::f64::consts::PI;
use metrology::{C, EV, G, HBAR, K_B};
use crate::{H100, integrate_log};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cosmology {
pub h0: f64,
pub omega_m: f64,
pub omega_de: f64,
pub w0: f64,
pub wa: f64,
pub t_cmb: f64,
pub n_eff: f64,
pub m_nu: [f64; 3],
}
pub const PLANCK18: Cosmology = Cosmology { h0: 0.6766 * H100, omega_m: 0.30966, omega_de: 0.688_846_270_597_353_6, w0: -1.0, wa: 0.0, t_cmb: 2.7255, n_eff: 3.046, m_nu: [0.0, 0.0, 0.06 * EV] };
impl Cosmology {
const fn flat(h0: f64, omega_m: f64) -> Self {
Self { h0, omega_m, omega_de: 1.0 - omega_m, w0: -1.0, wa: 0.0, t_cmb: 0.0, n_eff: 0.0, m_nu: [0.0; 3] }
}
fn omega_photons(&self) -> f64 {
PI * PI / 15.0 * (K_B * self.t_cmb).powi(4) / (HBAR * C).powi(3) * 8.0 * PI * G / (3.0 * self.h0 * self.h0 * C * C)
}
fn neutrino_ratio(&self, z: f64) -> f64 {
let t_nu = (4.0f64 / 11.0).cbrt() * self.t_cmb * (1.0 + z);
let fermi = |m: f64| {
let y = m / (K_B * t_nu);
if y == 0.0 { 1.0 } else { integrate_log(1e-6, 60.0, 600, |x| x * x * (x * x + y * y).sqrt() / (x.exp() + 1.0)) / (7.0 * PI.powi(4) / 120.0) }
};
7.0 / 8.0 * (4.0f64 / 11.0).powf(4.0 / 3.0) * self.n_eff / 3.0 * self.m_nu.iter().map(|&m| fermi(m)).sum::<f64>()
}
fn radiation(&self, z: f64) -> f64 {
if self.t_cmb == 0.0 { 0.0 } else { self.omega_photons() * (1.0 + self.neutrino_ratio(z)) }
}
pub fn curvature(&self) -> f64 {
1.0 - self.omega_m - self.omega_de - self.radiation(0.0)
}
pub fn e(&self, z: f64) -> f64 {
self.e_with(z, self.curvature())
}
fn e_with(&self, z: f64, omega_k: f64) -> f64 {
let x = 1.0 + z;
let de = x.powf(3.0 * (1.0 + self.w0 + self.wa)) * (-3.0 * self.wa * z / x).exp();
(x * x * (self.omega_m * x + omega_k + self.radiation(z) * x * x) + self.omega_de * de).sqrt()
}
pub fn comoving_distance(&self, z1: f64, z2: f64) -> f64 {
let omega_k = self.curvature();
C / self.h0 * integrate_log(1.0 + z1, 1.0 + z2, 2000, |x| 1.0 / self.e_with(x - 1.0, omega_k))
}
pub fn transverse_distance(&self, z1: f64, z2: f64) -> f64 {
let (omega_k, dh) = (self.curvature(), C / self.h0);
let chi = self.comoving_distance(z1, z2) / dh;
let s = omega_k.abs().sqrt();
dh * if omega_k > 0.0 { (s * chi).sinh() / s } else if omega_k < 0.0 { (s * chi).sin() / s } else { chi }
}
pub fn angular_diameter_distance(&self, z1: f64, z2: f64) -> f64 {
self.transverse_distance(z1, z2) / (1.0 + z2)
}
pub fn luminosity_distance(&self, z: f64) -> f64 {
(1.0 + z) * self.transverse_distance(0.0, z)
}
pub fn time_delay_distance(&self, zd: f64, zs: f64) -> f64 {
let (dd, ds, dds) = (self.angular_diameter_distance(0.0, zd), self.angular_diameter_distance(0.0, zs), self.angular_diameter_distance(zd, zs));
(1.0 + zd) * dd * ds / dds
}
}
pub fn comoving_distance(z: f64, h0: f64, omega_m: f64) -> f64 {
Cosmology::flat(h0, omega_m).comoving_distance(0.0, z)
}
pub fn angular_diameter_distance(z1: f64, z2: f64, h0: f64, omega_m: f64) -> f64 {
Cosmology::flat(h0, omega_m).angular_diameter_distance(z1, z2)
}
pub fn time_delay_distance(zd: f64, zs: f64, h0: f64, omega_m: f64) -> f64 {
Cosmology::flat(h0, omega_m).time_delay_distance(zd, zs)
}
#[cfg(test)]
mod tests {
use super::*;
use metrology::MPC;
#[test]
fn the_two_closed_forms() {
let h0 = 0.7 * H100;
for z in [0.1, 1.0, 3.0] {
let eds = 2.0 * C / h0 * (1.0 - 1.0 / (1.0f64 + z).sqrt());
assert!((comoving_distance(z, h0, 1.0) / eds - 1.0).abs() < 1e-13, "z = {z}");
assert!((comoving_distance(z, h0, 0.0) / (C * z / h0) - 1.0).abs() < 1e-14, "z = {z}");
}
assert_eq!(comoving_distance(0.0, h0, 0.3), 0.0);
}
#[test]
fn h0licow() {
let lenses = [
(0.6304, 1.394, 5156.0, 296.0, 236.0),
(0.295, 0.654, 2096.0, 98.0, 83.0),
(0.4546, 1.693, 2707.0, 183.0, 168.0),
(0.745, 1.789, 5769.0, 589.0, 471.0),
(0.6575, 1.662, 4784.0, 399.0, 248.0),
(0.311, 1.722, 1470.0, 137.0, 127.0),
];
let (mut sum, mut weights) = (0.0, 0.0);
for (zd, zs, ddt, up, down) in lenses {
let h = 70.0 * time_delay_distance(zd, zs, 0.7 * H100, 0.3) / MPC / ddt;
let sigma = h * 0.5 * (up + down) / ddt;
sum += h / (sigma * sigma);
weights += 1.0 / (sigma * sigma);
}
let (h0, sigma) = (sum / weights, weights.powf(-0.5));
assert!((h0 - 73.3).abs() < 0.1 && (sigma - 1.75).abs() < 0.1, "{h0} ± {sigma}");
}
fn lcdm(h: f64, omega_m: f64, omega_de: f64) -> Cosmology {
Cosmology { omega_de, ..Cosmology::flat(h * H100, omega_m) }
}
#[test]
fn external_codes_in_astropys_tests() {
for (om, ol, dm) in [(0.3, 0.7, 3303.8288), (0.3, 0.1, 2979.346), (2.0, 0.1, 2152.7954)] {
let got = lcdm(0.7, om, ol).transverse_distance(0.0, 1.0) / MPC;
assert!((got / dm - 1.0).abs() < 5e-8, "{om} {ol}: {got}");
}
let da = lcdm(0.704, 0.272, 0.728).angular_diameter_distance(1.0, 2.0) / MPC;
assert!((da / 646.229_686_628_220_2 - 1.0).abs() < 1e-14, "{da}");
let cpl = Cosmology { w0: -1.1, wa: 0.2, ..lcdm(0.7, 0.2, 0.8) };
for (z, dl) in [(0.2, 1004.0), (0.4, 2268.62), (0.9, 6265.76), (1.2, 9061.84)] {
assert!((cpl.luminosity_distance(z) / MPC / dl - 1.0).abs() < 3e-6, "z = {z}");
}
let scale = (cpl.e(0.5).powi(2) - 0.2 * 1.5f64.powi(3)) / 0.8;
assert!((scale - 0.924_631_066_952_902_1).abs() < 1e-15, "{scale}");
}
#[test]
fn exact_neutrinos() {
let nu = Cosmology { t_cmb: 3.0, n_eff: 3.0, m_nu: [0.25 * EV; 3], ..lcdm(0.75, 0.25, 0.75) };
let per = 7.0 / 8.0 * (4.0f64 / 11.0).powf(4.0 / 3.0) * 3.0;
for (z, ratio) in [(0.0, 429.924), (1.0, 214.964), (2.0, 143.312), (10.0, 39.1005), (1000.0, 1.11086)] {
assert!((nu.neutrino_ratio(z) / per / ratio - 1.0).abs() < 2e-6, "z = {z}");
}
assert!((Cosmology { m_nu: [0.0; 3], ..nu }.neutrino_ratio(5.0) / per - 1.0).abs() < 1e-15);
}
#[test]
fn planck18() {
let p = PLANCK18;
assert!(p.curvature().abs() < 1e-15, "{}", p.curvature());
let omega_nu = p.omega_photons() * p.neutrino_ratio(0.0);
assert!((p.omega_photons() / 5.402015e-5 - 1.0).abs() < 1e-6 && (omega_nu / 1.439709e-3 - 1.0).abs() < 1e-6);
assert!((p.omega_m + omega_nu - 0.3111).abs() < 5e-5);
let theta = |c: Cosmology| 100.0 * 144.57 * MPC / c.transverse_distance(0.0, 1089.80);
assert!((theta(p) - 1.04124).abs() < 5e-6, "{}", theta(p));
assert!((theta(Cosmology::flat(p.h0, 0.30966 + 0.0014)) - 1.03639).abs() < 5e-6);
assert!((p.comoving_distance(0.0, 1.0) / MPC - 3395.6346).abs() < 1e-4);
}
#[test]
fn curvature_and_the_free_functions() {
for om in [0.3, 1.0, 2.0] {
let c = lcdm(0.7, om, 0.0);
for z in [0.5, 2.0, 5.0] {
let mattig = C / c.h0 * 2.0 * (2.0 - om * (1.0 - z) - (2.0 - om) * (1.0 + om * z).sqrt()) / (om * om * (1.0 + z));
assert!((c.transverse_distance(0.0, z) / mattig - 1.0).abs() < 1e-13, "{om} {z}");
}
}
let closed = lcdm(0.7, 3.0, 0.0);
assert!((closed.angular_diameter_distance(2.0, 50.0) / MPC - 40.5883).abs() < 1e-4);
let (dh, ok) = (C / closed.h0, closed.curvature());
let (m1, m2) = (closed.transverse_distance(0.0, 2.0), closed.transverse_distance(0.0, 50.0));
let hogg = (m2 * (1.0 + ok * m1 * m1 / (dh * dh)).sqrt() - m1 * (1.0 + ok * m2 * m2 / (dh * dh)).sqrt()) / 51.0;
assert!((hogg / MPC - 30.0828).abs() < 1e-4);
let flat = lcdm(0.7, 0.3, 0.7);
assert_eq!(flat.curvature(), 0.0);
assert_eq!(time_delay_distance(0.295, 0.654, flat.h0, 0.3), flat.time_delay_distance(0.295, 0.654));
assert_eq!(comoving_distance(1.0, flat.h0, 0.3), flat.transverse_distance(0.0, 1.0));
}
}