#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::LN_2;
#[derive(Debug, Clone)]
pub struct RadiationMaterial {
pub name: String,
pub atomic_number: u32,
pub density: f64,
pub mass_attenuation: f64,
pub neutron_cross_section: f64,
}
impl RadiationMaterial {
pub fn new(
name: impl Into<String>,
atomic_number: u32,
density: f64,
mass_attenuation: f64,
neutron_cross_section: f64,
) -> Self {
Self {
name: name.into(),
atomic_number,
density,
mass_attenuation,
neutron_cross_section,
}
}
pub fn lead() -> Self {
Self::new("Lead", 82, 11_340.0, 7.1e-3, 0.171)
}
pub fn concrete() -> Self {
Self::new("Concrete", 14, 2_300.0, 6.5e-3, 0.17)
}
pub fn water() -> Self {
Self::new("Water", 1, 1_000.0, 6.7e-3, 0.333)
}
pub fn polyethylene() -> Self {
Self::new("Polyethylene", 6, 950.0, 2.0e-3, 0.333)
}
}
pub fn linear_attenuation(density: f64, mass_attenuation: f64) -> f64 {
density * mass_attenuation
}
pub fn narrow_beam_transmission(mu: f64, thickness: f64) -> f64 {
(-mu * thickness).exp()
}
pub fn half_value_layer(mu: f64) -> f64 {
if mu.abs() < 1e-300 {
f64::INFINITY
} else {
LN_2 / mu
}
}
pub fn tenth_value_layer(mu: f64) -> f64 {
if mu.abs() < 1e-300 {
f64::INFINITY
} else {
10.0_f64.ln() / mu
}
}
pub fn hvls_for_transmission(transmission: f64) -> f64 {
assert!(transmission > 0.0 && transmission <= 1.0);
-transmission.log2()
}
pub fn buildup_factor(mu_x: f64, cap_a: f64, alpha1: f64, alpha2: f64) -> f64 {
cap_a * (alpha1 * mu_x).exp() + (1.0 - cap_a) * (alpha2 * mu_x).exp()
}
pub fn berger_buildup_factor(mu_x: f64, c_coeff: f64, d_coeff: f64) -> f64 {
1.0 + c_coeff * mu_x * (d_coeff * mu_x).exp()
}
pub fn dose_rate(fluence_rate: f64, energy_j: f64, mu_en_over_rho_air: f64) -> f64 {
fluence_rate * energy_j * mu_en_over_rho_air
}
pub fn shielded_dose_rate(
fluence_rate_0: f64,
energy_j: f64,
mu_en_over_rho_air: f64,
mu: f64,
thickness: f64,
buildup: f64,
) -> f64 {
let unshielded = dose_rate(fluence_rate_0, energy_j, mu_en_over_rho_air);
unshielded * buildup * (-mu * thickness).exp()
}
#[derive(Debug, Clone, Copy)]
pub struct ActivationResult {
pub saturation_activity: f64,
pub activity_at_eoi: f64,
pub activity_after_cooling: f64,
pub decay_constant: f64,
}
pub fn activation_analysis(
mass_kg: f64,
molar_mass: f64,
cross_section_m2: f64,
neutron_flux: f64,
half_life_s: f64,
irradiation_time_s: f64,
cooling_time_s: f64,
) -> ActivationResult {
const AVOGADRO: f64 = 6.022_140_76e23;
let n_atoms = (mass_kg / molar_mass) * AVOGADRO;
let lambda = LN_2 / half_life_s;
let saturation_activity = n_atoms * cross_section_m2 * neutron_flux * lambda;
let activity_at_eoi = saturation_activity * (1.0 - (-lambda * irradiation_time_s).exp());
let activity_after_cooling = activity_at_eoi * (-lambda * cooling_time_s).exp();
ActivationResult {
saturation_activity,
activity_at_eoi,
activity_after_cooling,
decay_constant: lambda,
}
}
pub fn effective_dose_rate(activity_bq: f64, dcf: f64) -> f64 {
activity_bq * dcf
}
pub fn macroscopic_removal_cross_section(
density: f64,
molar_mass: f64,
removal_cross_section_m2: f64,
) -> f64 {
const AVOGADRO: f64 = 6.022_140_76e23;
let number_density = (density / molar_mass) * AVOGADRO;
number_density * removal_cross_section_m2
}
pub fn mean_free_path(macroscopic_cross_section: f64) -> f64 {
1.0 / macroscopic_cross_section
}
pub fn required_thickness(reduction_factor: f64, mu: f64) -> f64 {
assert!(reduction_factor > 1.0);
reduction_factor.ln() / mu
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_linear_attenuation_lead() {
let mat = RadiationMaterial::lead();
let mu = linear_attenuation(mat.density, mat.mass_attenuation);
assert!(
(mu - 80.514).abs() < 0.5,
"Lead μ should be ~80.5, got {mu}"
);
}
#[test]
fn test_hvl_zero_mu() {
assert_eq!(half_value_layer(0.0), f64::INFINITY);
}
#[test]
fn test_hvl_identity() {
let mu = 50.0;
let hvl = half_value_layer(mu);
assert!((hvl * mu - LN_2).abs() < EPS, "HVL * μ must equal ln2");
}
#[test]
fn test_narrow_beam_half_at_hvl() {
let mu = 30.0;
let hvl = half_value_layer(mu);
let t = narrow_beam_transmission(mu, hvl);
assert!(
(t - 0.5).abs() < EPS,
"Transmission at HVL must be 0.5, got {t}"
);
}
#[test]
fn test_narrow_beam_zero_thickness() {
let t = narrow_beam_transmission(100.0, 0.0);
assert!((t - 1.0).abs() < EPS);
}
#[test]
fn test_transmission_monotone() {
let mu = 20.0;
let t1 = narrow_beam_transmission(mu, 0.01);
let t2 = narrow_beam_transmission(mu, 0.05);
let t3 = narrow_beam_transmission(mu, 0.1);
assert!(
t1 > t2 && t2 > t3,
"Transmission must decrease with thickness"
);
}
#[test]
fn test_tvl_identity() {
let mu = 80.0;
let tvl = tenth_value_layer(mu);
assert!(
(tvl * mu - 10.0_f64.ln()).abs() < EPS,
"TVL * μ must equal ln(10)"
);
}
#[test]
fn test_tvl_vs_hvl_ratio() {
let mu = 45.0;
let hvl = half_value_layer(mu);
let tvl = tenth_value_layer(mu);
let ratio = tvl / hvl;
let expected = 10.0_f64.log2(); assert!(
(ratio - expected).abs() < EPS,
"TVL/HVL must equal log2(10), got {ratio}"
);
}
#[test]
fn test_taylor_buildup_at_zero() {
let b = buildup_factor(0.0, 0.5, -0.1, 0.05);
assert!((b - 1.0).abs() < EPS, "Buildup at μx=0 must be 1, got {b}");
}
#[test]
fn test_berger_buildup_at_zero() {
let b = berger_buildup_factor(0.0, 2.0, 0.3);
assert!(
(b - 1.0).abs() < EPS,
"Berger buildup at μx=0 must be 1, got {b}"
);
}
#[test]
fn test_berger_buildup_gte_one() {
for &mu_x in &[0.5, 1.0, 2.0, 5.0, 10.0] {
let b = berger_buildup_factor(mu_x, 1.5, 0.2);
assert!(b >= 1.0, "Berger buildup must be ≥ 1, got {b} at μx={mu_x}");
}
}
#[test]
fn test_dose_rate_linear_fluence() {
let e_j = 1.6e-13; let mu_en = 2.7e-3;
let d1 = dose_rate(1e6, e_j, mu_en);
let d2 = dose_rate(2e6, e_j, mu_en);
assert!(
(d2 / d1 - 2.0).abs() < EPS,
"Dose rate must scale linearly with fluence"
);
}
#[test]
fn test_dose_rate_zero_fluence() {
let d = dose_rate(0.0, 1.6e-13, 2.7e-3);
assert_eq!(d, 0.0);
}
#[test]
fn test_shielded_dose_no_shield() {
let d_unshielded = dose_rate(1e8, 1.6e-13, 2.7e-3);
let d_shielded = shielded_dose_rate(1e8, 1.6e-13, 2.7e-3, 80.0, 0.0, 1.0);
assert!((d_shielded - d_unshielded).abs() < EPS * d_unshielded);
}
#[test]
fn test_shielded_dose_decreases_with_thickness() {
let (phi0, e, mu_en, mu) = (1e10, 1.6e-13, 2.7e-3, 80.0);
let d1 = shielded_dose_rate(phi0, e, mu_en, mu, 0.01, 1.0);
let d2 = shielded_dose_rate(phi0, e, mu_en, mu, 0.05, 1.0);
assert!(d1 > d2, "Dose rate must decrease with shield thickness");
}
#[test]
fn test_activation_saturation_scales_with_flux() {
let r1 = activation_analysis(1e-3, 0.059, 1e-28, 1e14, 3.7e3, 1e6, 0.0);
let r2 = activation_analysis(1e-3, 0.059, 1e-28, 2e14, 3.7e3, 1e6, 0.0);
let ratio = r2.saturation_activity / r1.saturation_activity;
assert!(
(ratio - 2.0).abs() < 1e-6,
"Saturation activity must scale with flux, ratio={ratio}"
);
}
#[test]
fn test_activation_approaches_saturation() {
let half_life = 600.0; let result =
activation_analysis(1e-3, 0.059, 1e-28, 1e14, half_life, 100.0 * half_life, 0.0);
let ratio = result.activity_at_eoi / result.saturation_activity;
assert!(
(ratio - 1.0).abs() < 1e-4,
"Long irradiation should approach saturation, ratio={ratio}"
);
}
#[test]
fn test_activation_decay_after_cooling() {
let half_life = 600.0;
let result = activation_analysis(
1e-3,
0.059,
1e-28,
1e14,
half_life,
10.0 * half_life,
100.0 * half_life,
);
assert!(
result.activity_after_cooling < result.activity_at_eoi * 1e-25,
"Activity should be negligible after 100 half-lives"
);
}
#[test]
fn test_decay_constant() {
let half_life = 1234.5;
let result = activation_analysis(1e-3, 0.059, 1e-28, 1e14, half_life, 1.0, 0.0);
let expected_lambda = LN_2 / half_life;
assert!(
(result.decay_constant - expected_lambda).abs() < EPS,
"Decay constant mismatch: {} vs {}",
result.decay_constant,
expected_lambda
);
}
#[test]
fn test_required_thickness_round_trip() {
let mu = 80.0;
let target_reduction = 1000.0;
let x = required_thickness(target_reduction, mu);
let actual_reduction = 1.0 / narrow_beam_transmission(mu, x);
assert!(
(actual_reduction - target_reduction).abs() < 1e-6,
"Round-trip failed: {actual_reduction} vs {target_reduction}"
);
}
#[test]
fn test_hvls_for_half_transmission() {
let n = hvls_for_transmission(0.5);
assert!(
(n - 1.0).abs() < EPS,
"Need exactly 1 HVL for 50% transmission, got {n}"
);
}
#[test]
fn test_hvls_for_quarter_transmission() {
let n = hvls_for_transmission(0.25);
assert!(
(n - 2.0).abs() < EPS,
"Need 2 HVLs for 25% transmission, got {n}"
);
}
#[test]
fn test_macroscopic_removal_cross_section_positive() {
let sigma = macroscopic_removal_cross_section(1000.0, 0.018, 3e-30);
assert!(
sigma > 0.0,
"Macroscopic cross-section must be positive, got {sigma}"
);
}
#[test]
fn test_mean_free_path_reciprocal() {
let sigma_t = 100.0; let mfp = mean_free_path(sigma_t);
assert!(
(mfp - 0.01).abs() < EPS,
"MFP must be 1/Σ_t = 0.01 m, got {mfp}"
);
}
#[test]
fn test_water_preset_density() {
let water = RadiationMaterial::water();
assert_eq!(water.density, 1000.0);
}
#[test]
fn test_density_ordering() {
let lead = RadiationMaterial::lead();
let concrete = RadiationMaterial::concrete();
assert!(
lead.density > concrete.density,
"Lead must be denser than concrete"
);
}
#[test]
fn test_effective_dose_rate_linear() {
let dcf = 1e-15; let d1 = effective_dose_rate(1e6, dcf);
let d2 = effective_dose_rate(3e6, dcf);
assert!(
(d2 / d1 - 3.0).abs() < EPS,
"Effective dose rate must scale with activity"
);
}
#[test]
fn test_required_thickness_monotone() {
let mu = 50.0;
let x10 = required_thickness(10.0, mu);
let x100 = required_thickness(100.0, mu);
let x1000 = required_thickness(1000.0, mu);
assert!(
x10 < x100 && x100 < x1000,
"Required thickness must increase with reduction factor"
);
}
#[test]
fn test_taylor_buildup_gte_one_positive_alphas() {
let (cap_a, a1, a2) = (0.4, 0.1, 0.05);
for &mu_x in &[0.0, 0.5, 1.0, 2.0, 5.0] {
let b = buildup_factor(mu_x, cap_a, a1, a2);
assert!(b >= 1.0, "Buildup must be ≥ 1 at μx={mu_x}, got {b}");
}
}
#[test]
fn test_polyethylene_density() {
let pe = RadiationMaterial::polyethylene();
let concrete = RadiationMaterial::concrete();
assert!(
pe.density < concrete.density,
"Polyethylene must be lighter than concrete"
);
}
}