use serde::{Deserialize, Serialize};
use tracing::{instrument, warn};
use crate::cosmology::friedmann::CosmologicalParameters;
use crate::error::{MimamsaError, ensure_finite, require_finite};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CosmicPhase {
RadiationDominated,
MatterDominated,
DarkEnergyDominated,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct FixedPointState {
pub intensity: f64,
pub unity_param: f64,
pub phase: CosmicPhase,
pub entropy_ratio: f64,
}
impl FixedPointState {
#[instrument(level = "debug", skip(params), ret)]
pub fn at_redshift(params: &CosmologicalParameters, z: f64) -> Result<Self, MimamsaError> {
let phase = cosmic_phase(params, z)?;
let e_ratio = entropy_ratio(params, z)?;
let intensity = manifestation_intensity(params, z)?;
let unity = unity_parameter(intensity)?;
Ok(Self {
intensity,
unity_param: unity,
phase,
entropy_ratio: e_ratio,
})
}
}
#[instrument(level = "trace", skip(params))]
pub fn cosmic_phase(params: &CosmologicalParameters, z: f64) -> Result<CosmicPhase, MimamsaError> {
require_finite(z, "cosmic_phase")?;
if z < -1.0 {
warn!(z, "cosmic_phase: z < -1 is unphysical");
return Err(MimamsaError::InvalidCosmology(
"z < -1 is unphysical".to_string(),
));
}
let z1 = 1.0 + z;
let rho_r = params.omega_r * z1.powi(4);
let rho_m = params.omega_m * z1.powi(3);
let rho_lambda = params.omega_lambda;
if rho_r >= rho_m && rho_r >= rho_lambda {
Ok(CosmicPhase::RadiationDominated)
} else if rho_m >= rho_lambda {
Ok(CosmicPhase::MatterDominated)
} else {
Ok(CosmicPhase::DarkEnergyDominated)
}
}
#[instrument(level = "trace", skip(params))]
pub fn entropy_ratio(params: &CosmologicalParameters, z: f64) -> Result<f64, MimamsaError> {
require_finite(z, "entropy_ratio")?;
if z < -1.0 {
warn!(z, "entropy_ratio: z < -1 is unphysical");
return Err(MimamsaError::InvalidCosmology(
"entropy_ratio: z < -1 is unphysical".to_string(),
));
}
let z1 = 1.0 + z;
let z2 = z1 * z1;
let z3 = z2 * z1;
let z4 = z3 * z1;
let denom =
params.omega_r * z4 + params.omega_m * z3 + params.omega_k * z2 + params.omega_lambda;
if denom <= 0.0 {
warn!(denom, "entropy_ratio: density sum non-positive");
return Err(MimamsaError::Computation(
"entropy_ratio: density sum non-positive".to_string(),
));
}
let ratio = params.omega_lambda / denom;
let clamped = ratio.clamp(0.0, 1.0);
ensure_finite(clamped, "entropy_ratio")
}
#[instrument(level = "trace", skip(params))]
pub fn manifestation_intensity(
params: &CosmologicalParameters,
z: f64,
) -> Result<f64, MimamsaError> {
let ratio = entropy_ratio(params, z)?;
ensure_finite(1.0 - ratio, "manifestation_intensity")
}
#[instrument(level = "trace")]
#[inline]
pub fn unity_parameter(intensity: f64) -> Result<f64, MimamsaError> {
require_finite(intensity, "unity_parameter")?;
if !(0.0..=1.0).contains(&intensity) {
warn!(intensity, "unity_parameter: intensity out of [0, 1]");
return Err(MimamsaError::Computation(
"unity_parameter: intensity must be in [0, 1]".to_string(),
));
}
ensure_finite(1.0 - intensity, "unity_parameter")
}
#[cfg(test)]
mod tests {
use super::*;
fn planck() -> CosmologicalParameters {
CosmologicalParameters::planck2018()
}
#[test]
fn test_cosmic_phase_radiation_early() {
assert_eq!(
cosmic_phase(&planck(), 10000.0).unwrap(),
CosmicPhase::RadiationDominated
);
}
#[test]
fn test_cosmic_phase_matter_intermediate() {
assert_eq!(
cosmic_phase(&planck(), 10.0).unwrap(),
CosmicPhase::MatterDominated
);
}
#[test]
fn test_cosmic_phase_dark_energy_now() {
assert_eq!(
cosmic_phase(&planck(), 0.0).unwrap(),
CosmicPhase::DarkEnergyDominated
);
}
#[test]
fn test_entropy_ratio_z0() {
let ratio = entropy_ratio(&planck(), 0.0).unwrap();
assert!(
(ratio - planck().omega_lambda).abs() < 0.01,
"entropy_ratio(z=0) = {ratio}"
);
}
#[test]
fn test_entropy_ratio_monotonic_decreasing_with_z() {
let p = planck();
let r0 = entropy_ratio(&p, 0.0).unwrap();
let r1 = entropy_ratio(&p, 1.0).unwrap();
let r10 = entropy_ratio(&p, 10.0).unwrap();
assert!(r0 > r1, "ratio should decrease with z: r0={r0}, r1={r1}");
assert!(r1 > r10, "ratio should decrease with z: r1={r1}, r10={r10}");
}
#[test]
fn test_entropy_ratio_bounds() {
let p = planck();
for z in [0.0, 0.5, 1.0, 5.0, 100.0, 1100.0] {
let r = entropy_ratio(&p, z).unwrap();
assert!(
(0.0..=1.0).contains(&r),
"entropy_ratio({z}) = {r} out of [0,1]"
);
}
}
#[test]
fn test_manifestation_intensity_z0() {
let p = planck();
let intensity = manifestation_intensity(&p, 0.0).unwrap();
assert!(
(intensity - (1.0 - p.omega_lambda)).abs() < 0.01,
"intensity(z=0) = {intensity}"
);
}
#[test]
fn test_manifestation_intensity_increases_with_z() {
let p = planck();
let i0 = manifestation_intensity(&p, 0.0).unwrap();
let i1 = manifestation_intensity(&p, 1.0).unwrap();
let i10 = manifestation_intensity(&p, 10.0).unwrap();
assert!(i1 > i0);
assert!(i10 > i1);
}
#[test]
fn test_unity_parameter_complement() {
let p = planck();
let intensity = manifestation_intensity(&p, 0.0).unwrap();
let unity = unity_parameter(intensity).unwrap();
assert!((intensity + unity - 1.0).abs() < 1e-12);
}
#[test]
fn test_unity_parameter_rejects_out_of_range() {
assert!(unity_parameter(1.5).is_err());
assert!(unity_parameter(-0.1).is_err());
}
#[test]
fn test_fixed_point_state_at_z0() {
let state = FixedPointState::at_redshift(&planck(), 0.0).unwrap();
assert_eq!(state.phase, CosmicPhase::DarkEnergyDominated);
assert!((state.intensity + state.unity_param - 1.0).abs() < 1e-12);
assert!((state.entropy_ratio + state.intensity - 1.0).abs() < 1e-12);
}
#[test]
fn test_unphysical_z_rejected() {
assert!(cosmic_phase(&planck(), -2.0).is_err());
assert!(entropy_ratio(&planck(), -2.0).is_err());
}
#[test]
fn test_entropy_ratio_zero_density() {
let params = CosmologicalParameters {
h0: 67.4,
omega_m: 0.0,
omega_r: 0.0,
omega_lambda: 0.0,
omega_k: 0.0,
};
assert!(entropy_ratio(¶ms, 0.0).is_err());
}
}