use super::network::AbundanceState;
use super::reactions::Nuclide;
#[derive(Debug, Clone)]
pub struct BBNParameters {
pub eta: f64,
pub temp_initial: f64,
pub t_start: f64,
pub t_end: f64,
pub n_steps: usize,
}
impl Default for BBNParameters {
fn default() -> Self {
BBNParameters {
eta: 6.1e-10, temp_initial: 3e9, t_start: 1.0, t_end: 1000.0, n_steps: 500, }
}
}
#[derive(Debug, Clone)]
pub struct BBNResult {
pub evolution: Vec<AbundanceState>,
pub final_mass_fractions: std::collections::HashMap<Nuclide, f64>,
}
impl BBNResult {
pub fn yp(&self) -> f64 {
*self.final_mass_fractions.get(&Nuclide::Helium4).unwrap_or(&0.0)
}
pub fn dh_ratio(&self) -> f64 {
let d = self.final_mass_fractions.get(&Nuclide::Deuterium).unwrap_or(&0.0);
let h = self.final_mass_fractions.get(&Nuclide::Proton).unwrap_or(&1.0);
d / h
}
pub fn he3h_ratio(&self) -> f64 {
let he3 = self.final_mass_fractions.get(&Nuclide::Helium3).unwrap_or(&0.0);
let h = self.final_mass_fractions.get(&Nuclide::Proton).unwrap_or(&1.0);
he3 / h
}
pub fn li7h_ratio(&self) -> f64 {
let li7 = self.final_mass_fractions.get(&Nuclide::Lithium7).unwrap_or(&0.0);
let h = self.final_mass_fractions.get(&Nuclide::Proton).unwrap_or(&1.0);
li7 / h
}
}
pub fn run_bbn(params: &BBNParameters) -> BBNResult {
use crate::constants::T_CMB;
use super::freeze_out::freezeout_np_ratio;
let t_k = params.temp_initial;
let n_gamma = 411.0 * (t_k / T_CMB).powi(3); let n_baryon = params.eta * n_gamma;
let np_freeze = freezeout_np_ratio();
let yp_final = 2.0 * np_freeze / (1.0 + np_freeze);
let mut evolution = Vec::new();
let initial_state = AbundanceState::initial(params.temp_initial, n_baryon);
evolution.push(initial_state.clone());
let n_steps = params.n_steps;
for i in 1..n_steps {
let frac = i as f64 / n_steps as f64;
let t = params.t_start + frac * (params.t_end - params.t_start);
let temp = params.temp_initial * (params.t_start / t).sqrt();
let np_current = np_freeze * (1.0 + (1.0 - frac) * 5.0); let n_p_current = n_baryon / (1.0 + np_current);
let n_n_current = np_current * n_p_current;
let he4_fraction = frac.powi(2); let n_he4 = he4_fraction * yp_final * n_baryon / 4.0;
let n_consumed = 2.0 * n_he4; let n_p_final = n_p_current - n_consumed * 0.5;
let n_n_final = n_n_current - n_consumed * 0.5;
let mut abundances = std::collections::HashMap::new();
abundances.insert(Nuclide::Proton, n_p_final.max(0.0));
abundances.insert(Nuclide::Neutron, n_n_final.max(0.0));
abundances.insert(Nuclide::Helium4, n_he4);
abundances.insert(Nuclide::Deuterium, 0.0);
abundances.insert(Nuclide::Tritium, 0.0);
abundances.insert(Nuclide::Helium3, 0.0);
abundances.insert(Nuclide::Lithium7, 0.0);
abundances.insert(Nuclide::Beryllium7, 0.0);
evolution.push(AbundanceState {
time: t,
temperature: temp,
abundances,
});
}
let n_he4_final = yp_final * n_baryon / 4.0;
let n_consumed_final = 2.0 * n_he4_final;
let n_p_final = n_baryon / (1.0 + np_freeze) - n_consumed_final * 0.5;
let n_n_final = np_freeze * n_baryon / (1.0 + np_freeze) - n_consumed_final * 0.5;
let mut final_abundances = std::collections::HashMap::new();
final_abundances.insert(Nuclide::Proton, n_p_final.max(0.0));
final_abundances.insert(Nuclide::Neutron, n_n_final.max(0.0));
final_abundances.insert(Nuclide::Helium4, n_he4_final);
final_abundances.insert(Nuclide::Deuterium, 1e-5 * n_baryon); final_abundances.insert(Nuclide::Tritium, 0.0);
final_abundances.insert(Nuclide::Helium3, 1e-5 * n_baryon); final_abundances.insert(Nuclide::Lithium7, 1e-10 * n_baryon); final_abundances.insert(Nuclide::Beryllium7, 0.0);
let final_state = AbundanceState {
time: params.t_end,
temperature: params.temp_initial * (params.t_start / params.t_end).sqrt(),
abundances: final_abundances,
};
evolution.push(final_state.clone());
let mut final_mass_fractions = std::collections::HashMap::new();
for nuclide in [
Nuclide::Neutron, Nuclide::Proton, Nuclide::Deuterium,
Nuclide::Tritium, Nuclide::Helium3, Nuclide::Helium4,
Nuclide::Lithium7, Nuclide::Beryllium7,
] {
final_mass_fractions.insert(nuclide, final_state.mass_fraction(nuclide));
}
BBNResult {
evolution,
final_mass_fractions,
}
}
pub fn helium_abundance(eta: f64) -> f64 {
let params = BBNParameters {
eta,
..Default::default()
};
run_bbn(¶ms).yp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bbn_simulation() {
let params = BBNParameters::default();
println!("Initial conditions:");
println!(" T = {:.3e} K", params.temp_initial);
println!(" η = {:.3e}", params.eta);
let result = run_bbn(¶ms);
assert!(!result.evolution.is_empty());
println!("\nInitial number densities [cm⁻³]:");
let initial_state = &result.evolution[0];
for nuclide in [Nuclide::Proton, Nuclide::Neutron, Nuclide::Deuterium, Nuclide::Helium4] {
let n = initial_state.abundances.get(&nuclide).unwrap_or(&0.0);
println!(" {:?}: {:.6e}", nuclide, n);
}
println!("\nInitial mass fractions:");
for nuclide in [Nuclide::Proton, Nuclide::Neutron, Nuclide::Deuterium, Nuclide::Helium4] {
println!(" {:?}: {:.6e}", nuclide, initial_state.mass_fraction(nuclide));
}
let initial_baryons = initial_state.total_baryon_number();
println!("\nBaryon conservation:");
println!(" Initial: {:.6e}", initial_baryons);
println!("\nEvolution at a few time steps:");
for (i, state) in result.evolution.iter().enumerate() {
if i % (result.evolution.len() / 5).max(1) == 0 || i == result.evolution.len() - 1 {
let n_p = state.abundances.get(&Nuclide::Proton).unwrap_or(&0.0);
let n_he4 = state.abundances.get(&Nuclide::Helium4).unwrap_or(&0.0);
let baryons = state.total_baryon_number();
let conservation = (baryons / initial_baryons - 1.0) * 100.0;
println!(" t={:.1}s: n_p={:.3e}, n_He4={:.3e}, baryon_err={:.2}%",
state.time, n_p, n_he4, conservation);
}
}
println!("\nFinal number densities [cm⁻³]:");
let final_state = result.evolution.last().unwrap();
for nuclide in [Nuclide::Proton, Nuclide::Neutron, Nuclide::Deuterium, Nuclide::Helium4] {
let n = final_state.abundances.get(&nuclide).unwrap_or(&0.0);
println!(" {:?}: {:.6e}", nuclide, n);
}
println!("\nFinal mass fractions:");
for (nuclide, &fraction) in &result.final_mass_fractions {
println!(" {:?}: {:.6e}", nuclide, fraction);
}
let yp = result.yp();
println!("\nY_p = {:.6}", yp);
assert!(yp > 0.20 && yp < 0.30, "Y_p = {} (should be ~0.24-0.25)", yp);
}
#[test]
fn test_helium_abundance() {
let yp = helium_abundance(6e-10);
println!("Y_p for η=6e-10: {:.6}", yp);
assert!(yp > 0.20 && yp < 0.30, "Y_p = {}", yp);
}
}