use core::f64::consts::PI;
pub const ALPHA: f64 = 7.297_352_569_3e-3;
pub const E2: f64 = 1.439_964_548;
pub const KEV: f64 = 1.602_176_634e-16;
pub const AMU_KEV: f64 = 931_494.102_42;
pub const M_P: f64 = 938_272.088_16;
pub const M_D: f64 = 1_875_612.942_4;
pub const M_T: f64 = 2_808_921.132_5;
pub const M_HE3: f64 = 2_808_391.607_4;
pub const M_B11: f64 = 11.009_305_166 * AMU_KEV - 5.0 * 510.998_95 + 0.6704;
pub fn reduced_mass(m1: f64, m2: f64) -> f64 {
m1 * m2 / (m1 + m2)
}
pub fn coulomb_barrier(z1: f64, z2: f64, r_fm: f64) -> f64 {
E2 * 1e3 * z1 * z2 / r_fm
}
pub fn sommerfeld(z1: f64, z2: f64, mr: f64, e: f64) -> f64 {
z1 * z2 * ALPHA * (mr / (2.0 * e)).sqrt()
}
pub fn tunnelling(z1: f64, z2: f64, mr: f64, e: f64) -> f64 {
(-2.0 * PI * sommerfeld(z1, z2, mr, e)).exp()
}
pub fn gamow_energy(z1: f64, z2: f64, mr: f64) -> f64 {
2.0 * mr * (PI * ALPHA * z1 * z2).powi(2)
}
pub fn gamow_peak(e_g: f64, kt: f64) -> (f64, f64) {
let e0 = (e_g * kt * kt / 4.0).cbrt();
(e0, 4.0 * (e0 * kt / 3.0).sqrt())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reaction {
DT,
DDn,
DDp,
DHe3,
}
fn bh(r: Reaction) -> (f64, f64, [f64; 7], (f64, f64)) {
match r {
Reaction::DT => (34.3827, 1_124_656.0, [1.17302e-9, 1.51361e-2, 7.51886e-2, 4.60643e-3, 1.35000e-2, -1.06750e-4, 1.36600e-5], (0.2, 100.0)),
Reaction::DDn => (31.3970, 937_814.0, [5.43360e-12, 5.85778e-3, 7.68222e-3, 0.0, -2.96400e-6, 0.0, 0.0], (0.2, 100.0)),
Reaction::DDp => (31.3970, 937_814.0, [5.65718e-12, 3.41267e-3, 1.99167e-3, 0.0, 1.05060e-5, 0.0, 0.0], (0.2, 100.0)),
Reaction::DHe3 => (68.7508, 1_124_572.0, [5.51036e-10, 6.41918e-3, -2.02896e-3, -1.91080e-5, 1.35776e-4, 0.0, 0.0], (0.5, 190.0)),
}
}
fn bosch_hale_form(e_g: f64, mr: f64, c: &[f64; 7], t: f64) -> f64 {
let theta = t / (1.0 - t * (c[1] + t * (c[3] + t * c[5])) / (1.0 + t * (c[2] + t * (c[4] + t * c[6]))));
let xi = (e_g / (4.0 * theta)).cbrt();
c[0] * theta * (xi / (mr * t.powi(3))).sqrt() * (-3.0 * xi).exp()
}
pub fn bosch_hale(r: Reaction, t: f64) -> Option<f64> {
let (b_g, mr, c, (lo, hi)) = bh(r);
(lo..=hi).contains(&t).then(|| 1e-6 * bosch_hale_form(b_g * b_g, mr, &c, t))
}
pub fn bosch_hale_sigma(r: Reaction, e: f64) -> f64 {
let (b_g, a, b): (f64, [f64; 5], [f64; 4]) = match r {
Reaction::DT => (34.3827, [6.927e4, 7.454e8, 2.050e6, 5.2002e4, 0.0], [6.38e1, -9.95e-1, 6.981e-5, 1.728e-4]),
Reaction::DDp => (31.3970, [5.5576e4, 2.1054e2, -3.2638e-2, 1.4987e-6, 1.8181e-10], [0.0; 4]),
Reaction::DDn => (31.3970, [5.3701e4, 3.3027e2, -1.2706e-1, 2.9327e-5, -2.5151e-9], [0.0; 4]),
Reaction::DHe3 => (68.7508, [5.7501e6, 2.5226e3, 4.5566e1, 0.0, 0.0], [-3.1995e-3, -8.5530e-6, 5.9014e-8, 0.0]),
};
let s = (a[0] + e * (a[1] + e * (a[2] + e * (a[3] + e * a[4])))) / (1.0 + e * (b[0] + e * (b[1] + e * (b[2] + e * b[3]))));
s / (e * (b_g / e.sqrt()).exp())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pb11Fit {
NevinsSwain,
TentoriBelloni,
}
pub fn pb11_reactivity(fit: Pb11Fit, t: f64) -> Option<f64> {
let (c, lo) = match fit {
Pb11Fit::NevinsSwain => ([4.4467e-14, -5.9357e-2, 2.0165e-1, 1.0404e-3, 2.7621e-3, -9.1653e-6, 9.8305e-7], 50.0),
Pb11Fit::TentoriBelloni => ([9.7827e-14, -5.1610e-2, 1.3240e-1, 3.8446e-4, 1.2499e-3, -5.6715e-6, 1.1615e-6], 70.0),
};
(lo..=500.0).contains(&t).then(|| bosch_hale_form(22_589.0, 859_526.0, &c, t))
}
pub const C_BREMS: f64 = 5.34e-37;
pub fn lawson(t: f64, q: f64) -> Option<f64> {
let sv = bosch_hale(Reaction::DT, t)?;
let den = (3.5 / 17.6 + 1.0 / q) * sv * 17.6e3 * KEV / 4.0 - C_BREMS * t.sqrt();
(den > 0.0).then(|| 3.0 * t * KEV / den)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Screening {
FirstOrder,
Exact,
}
pub fn screening(z1: f64, z2: f64, mr: f64, e: f64, ue: f64, form: Screening) -> f64 {
match form {
Screening::FirstOrder => (PI * sommerfeld(z1, z2, mr, e) * ue / e).exp(),
Screening::Exact => tunnelling(z1, z2, mr, e + ue) / tunnelling(z1, z2, mr, e),
}
}
pub fn salpeter(z1z2: f64, zeta: f64, rho: f64, t6: f64) -> f64 {
(0.188 * z1z2 * zeta * rho.sqrt() * t6.powf(-1.5)).exp()
}
pub const MUON_LIFETIME: f64 = 2.196_981_1e-6;
pub fn muon_yield(sticking: f64, cycle_rate: f64) -> f64 {
1.0 / (sticking + 1.0 / (MUON_LIFETIME * cycle_rate))
}
pub fn muon_breakeven(muon_cost_mev: f64) -> f64 {
muon_cost_mev / 17.6
}
pub fn dd_per_watt() -> f64 {
1.0 / ((3.268 + 4.033) / 2.0 * 1e3 * KEV)
}
pub fn dd_neutron_deficit(watts: f64, claimed: f64) -> (f64, f64) {
let expected = watts * dd_per_watt() / 2.0;
(expected, expected / claimed)
}
pub fn fusion_power(n1: f64, n2: f64, sv: f64, q_mev: f64, identical: bool) -> f64 {
n1 * n2 * sv * q_mev * 1e3 * KEV / if identical { 2.0 } else { 1.0 }
}
pub fn charged_sphere(q: f64, r: f64) -> (f64, f64, f64) {
const K: f64 = 8.987_551_792_3e9;
(K * q / (r * r), K * q / r, K * q * q / (2.0 * r))
}
pub fn spinning_shell_field(q: f64, r: f64, omega: f64) -> f64 {
const MU0: f64 = 1.256_637_062_12e-6;
2.0 / 3.0 * MU0 * q * omega / (4.0 * PI * r)
}
pub fn widom_larsen_rate(beta: f64, beta0: f64) -> f64 {
1.86e-3 * (beta - beta0).powi(2)
}
pub fn lane_emden(n: f64) -> (f64, f64, f64) {
let h = 1e-4;
let mut xi = 1e-3;
let (mut th, mut dth) = (1.0 - xi * xi / 6.0, -xi / 3.0);
let f = |xi: f64, th: f64, dth: f64| -> (f64, f64) { (dth, -th.max(0.0).powf(n) - 2.0 * dth / xi) };
loop {
let (k1a, k1b) = f(xi, th, dth);
let (k2a, k2b) = f(xi + h / 2.0, th + h / 2.0 * k1a, dth + h / 2.0 * k1b);
let (k3a, k3b) = f(xi + h / 2.0, th + h / 2.0 * k2a, dth + h / 2.0 * k2b);
let (k4a, k4b) = f(xi + h, th + h * k3a, dth + h * k3b);
let (nth, ndth) = (th + h / 6.0 * (k1a + 2.0 * k2a + 2.0 * k3a + k4a), dth + h / 6.0 * (k1b + 2.0 * k2b + 2.0 * k3b + k4b));
if nth <= 0.0 {
let frac = th / (th - nth);
let (x1, d1) = (xi + frac * h, dth + frac * (ndth - dth));
return (x1, -x1 * x1 * d1, -x1 / (3.0 * d1));
}
(xi, th, dth) = (xi + h, nth, ndth);
}
}
pub fn kelvin_helmholtz(m: f64, r: f64, l: f64) -> f64 {
6.674_30e-11 * m * m / (2.0 * r * l)
}
pub fn virial_temperature(m: f64, r: f64, mu: f64) -> f64 {
6.674_30e-11 * m * mu * 1.660_539_066_60e-27 / (1.380_649e-23 * r)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, rel: f64) -> bool {
(a / b - 1.0).abs() < rel
}
#[test]
fn barriers_and_tunnelling() {
assert!(close(coulomb_barrier(1.0, 1.0, 4.0), 360.0, 1e-3));
assert!(close(coulomb_barrier(1.0, 1.0, 3.243), 444.0, 1e-3));
assert!(close(coulomb_barrier(1.0, 5.0, 4.514), 1595.0, 1e-3));
let dt = reduced_mass(M_D, M_T);
assert!(close(sommerfeld(1.0, 1.0, dt, 10.0), 1.7304, 1e-4));
assert!(close(tunnelling(1.0, 1.0, dt, 10.0), 1.8969e-5, 1e-3));
let dd = reduced_mass(M_D, M_D);
assert!(close(tunnelling(1.0, 1.0, dd, 1.0), 2.3147e-14, 1e-3));
let pb = reduced_mass(M_P, M_B11);
assert!(close(tunnelling(1.0, 5.0, pb, 10.0), 2.2848e-21, 2e-3));
assert!(close(gamow_energy(1.0, 1.0, dt), 1182.2, 1e-4));
assert!(close(gamow_energy(1.0, 1.0, dt).sqrt(), 34.3827, 1e-5));
assert!(close(gamow_energy(1.0, 5.0, pb), 22589.1, 1e-4));
let (e0, w) = gamow_peak(gamow_energy(1.0, 1.0, dt), 10.0);
assert!(close(e0, 30.918, 1e-4) && close(w, 40.607, 1e-4));
let (e0, w) = gamow_peak(gamow_energy(1.0, 1.0, reduced_mass(M_P, M_P)), 1.3357);
assert!(close(e0, 6.036, 1e-3) && close(w, 6.557, 1e-3));
}
#[test]
fn bosch_hale_reproduces_its_tables() {
let table = [
(1.0, [6.86e-21, 9.93e-23, 1.02e-22, 3.05e-26]),
(10.0, [1.14e-16, 6.02e-19, 5.78e-19, 2.12e-19]),
(100.0, [8.45e-16, 2.68e-17, 2.24e-17, 1.72e-16]),
];
for (t, row) in table {
for (r, v) in [Reaction::DT, Reaction::DDn, Reaction::DDp, Reaction::DHe3].iter().zip(row) {
let sv = bosch_hale(*r, t).unwrap() * 1e6;
assert!(close(sv, v, 4e-3), "{r:?} {t}: {sv:e}");
}
}
assert!(close(bosch_hale(Reaction::DT, 10.0).unwrap(), 1.1362e-22, 1e-4));
assert!(close(bosch_hale(Reaction::DDn, 0.1 * 2.0).unwrap() / bosch_hale(Reaction::DDn, 0.2).unwrap(), 1.0, 1e-12));
assert!(bosch_hale(Reaction::DT, 150.0).is_none() && bosch_hale(Reaction::DHe3, 150.0).is_some());
assert!(close(bosch_hale_sigma(Reaction::DT, 10.0), 27.02, 1e-3));
let peak = (100..1300).map(|k| bosch_hale_sigma(Reaction::DT, f64::from(k) * 0.1)).fold(0.0, f64::max);
assert!(close(peak, 5065.7, 1e-3));
assert!(close(bosch_hale_sigma(Reaction::DDn, 50.0), 16.487, 1e-3));
assert!(close(bosch_hale_sigma(Reaction::DDp, 50.0), 15.573, 1e-3));
}
#[test]
fn cross_sections_average_to_the_reactivity() {
let mr = 1_124_656.0 * KEV / (299_792_458.0f64).powi(2);
let t = 10.0;
let n = 20_000;
let e_max = 40.0 * t;
let mut sum = 0.0;
for k in 1..n {
let e = e_max * k as f64 / n as f64;
sum += bosch_hale_sigma(Reaction::DT, e) * 1e-31 * e * (-e / t).exp();
}
let sv = (8.0 / (PI * mr)).sqrt() * (t * KEV).powf(-1.5) * sum * (e_max / n as f64) * KEV * KEV;
assert!(close(sv, bosch_hale(Reaction::DT, t).unwrap(), 0.012), "{sv:e}");
}
#[test]
fn proton_boron() {
let tb = |t| pb11_reactivity(Pb11Fit::TentoriBelloni, t).unwrap();
let ns = |t| pb11_reactivity(Pb11Fit::NevinsSwain, t).unwrap();
assert!(close(tb(100.0), 7.0650e-23, 2e-3) && close(tb(300.0), 4.3870e-22, 2e-3) && close(tb(500.0), 5.6190e-22, 2e-3));
assert!(close(ns(75.0), 2.455e-23, 2e-3) && close(ns(500.0), 3.741e-22, 2e-3));
assert!((tb(300.0) / ns(300.0) - 1.296).abs() < 0.005 && (tb(500.0) / ns(500.0) - 1.502).abs() < 0.005);
assert!(pb11_reactivity(Pb11Fit::TentoriBelloni, 60.0).is_none());
}
#[test]
fn lawson_criterion() {
let ts: Vec<f64> = (0..1981).map(|k| 1.0 + 0.05 * f64::from(k)).filter(|t| *t <= 100.0).collect();
let (t_min, ntt) = ts.iter().filter_map(|&t| lawson(t, f64::INFINITY).map(|n| (t, n * t))).min_by(|a, b| a.1.total_cmp(&b.1)).unwrap();
assert!(close(ntt, 2.968e21, 2e-3) && (t_min - 14.25).abs() < 0.06, "{t_min} {ntt:e}");
let (t1, nt1) = ts.iter().filter_map(|&t| lawson(t, 1.0).map(|n| (t, n * t))).min_by(|a, b| a.1.total_cmp(&b.1)).unwrap();
assert!(close(nt1, 4.677e20, 2e-3) && (t1 - 13.65).abs() < 0.06);
assert!(lawson(4.25, f64::INFINITY).is_none() && lawson(4.35, f64::INFINITY).is_some());
}
#[test]
fn screening_and_stars() {
let dd = reduced_mass(M_D, M_D);
assert!(close(screening(1.0, 1.0, dd, 10.0, 0.025, Screening::FirstOrder), 1.0125, 1e-3));
assert!(close(screening(1.0, 1.0, dd, 1.0, 0.3, Screening::FirstOrder), 111.0, 2e-3));
assert!(close(screening(1.0, 1.0, dd, 1.0, 0.3, Screening::Exact), 47.47, 2e-3));
let dhe = reduced_mass(M_D, M_HE3);
assert!(close(screening(1.0, 2.0, dhe, 1.0, 0.219, Screening::FirstOrder), 1862.0, 3e-3));
assert!(close(salpeter(1.0, 1.291, 150.0, 15.7), 1.0489, 1e-4));
let (xi, mass, conc) = lane_emden(3.0);
assert!(close(xi, 6.897, 1e-3) && close(mass, 2.018, 1e-3) && close(conc, 54.18, 2e-3), "{xi} {mass} {conc}");
assert!(close(kelvin_helmholtz(1.989e30, 6.957e8, 3.828e26) / 3.156e13, 15.7, 0.01));
assert!(close(virial_temperature(1.989e30, 6.957e8, 0.61), 1.40e7, 0.01));
}
#[test]
fn muons_and_claims() {
assert!(close(muon_yield(0.0047, 2.5287e8), 153.85, 1e-3));
assert!(close(muon_breakeven(4700.0), 267.0, 2e-3) && close(muon_breakeven(6000.0), 341.0, 2e-3));
assert!(close(dd_per_watt(), 1.710e12, 2e-3));
let (expected, deficit) = dd_neutron_deficit(1.751, 4e4);
assert!(close(expected, 1.497e12, 2e-3) && close(deficit, 3.74e7, 2e-3));
let p = fusion_power(1e20, 1e20, 5.002e-24, 3.651, true);
assert!(close(p, 1.463e4, 2e-3) && close(p * 4.0 / 3.0 * PI * 0.15f64.powi(3), 207.0, 0.01));
let (field, volts, energy) = charged_sphere(1.0, 0.15);
assert!(close(field, 3.99e11, 2e-3) && close(volts, 5.99e10, 2e-3) && close(energy, 3.00e10, 2e-3));
assert!(close(spinning_shell_field(1.0, 0.15, 1e4), 4.44e-3, 2e-3));
assert!(close(widom_larsen_rate(20.0, 2.59), 0.564, 2e-3));
}
}