#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub enum NuclearFuel {
UO2,
MixedOxide,
UN,
UC,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Actinide {
Uranium,
Plutonium,
Thorium,
Neptunium,
Americium,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ZircaloyGrade {
Zircaloy2,
Zircaloy4,
Zirlo,
M5,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RadiationDamage {
pub displacement_energy: f64,
pub pka_energy: f64,
pub atomic_number: u32,
pub atomic_mass: f64,
}
impl RadiationDamage {
pub fn new(
displacement_energy: f64,
pka_energy: f64,
atomic_number: u32,
atomic_mass: f64,
) -> Self {
Self {
displacement_energy,
pka_energy,
atomic_number,
atomic_mass,
}
}
pub fn nrt_dpa(&self, fluence: f64) -> f64 {
let nu = self.kinchin_pease(self.pka_energy) as f64;
0.8 * nu * fluence / (2.0 * self.displacement_energy)
}
pub fn kinchin_pease(&self, energy: f64) -> usize {
let ed = self.displacement_energy;
if energy < ed {
0
} else if energy < 2.0 * ed {
1
} else {
(energy / (2.0 * ed)) as usize
}
}
pub fn cascade_size(&self, energy: f64) -> usize {
if energy <= self.displacement_energy {
return 1;
}
let ratio = energy / self.displacement_energy;
ratio.powf(0.75).round() as usize
}
pub fn recombination_fraction(&self) -> f64 {
let nu = self.kinchin_pease(self.pka_energy) as f64;
if nu < 1.0 {
return 0.0;
}
let alpha = 0.15_f64;
1.0 - 1.0 / (1.0 + alpha * nu)
}
pub fn swelling(&self, dpa: f64) -> f64 {
let dpa_inc = 5.0_f64;
let a = 1.5e-3_f64;
let n = 1.5_f64;
if dpa <= dpa_inc {
0.0
} else {
a * (dpa - dpa_inc).powf(n)
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FuelPellet {
pub material: NuclearFuel,
pub burnup: f64,
pub temperature: f64,
pub radius: f64,
}
impl FuelPellet {
pub fn new(material: NuclearFuel, burnup: f64, temperature: f64, radius: f64) -> Self {
Self {
material,
burnup,
temperature,
radius,
}
}
pub fn thermal_conductivity(&self) -> f64 {
let t = self.temperature;
let base = match self.material {
NuclearFuel::UO2 => {
let a = 0.0452_f64;
let b = 2.46e-4_f64;
let lattice_term = 1.0 / (a + b * t);
let electron_term = 5.47e9 * t.powf(-2.5) * (-16350.0 / t).exp();
lattice_term + electron_term
}
NuclearFuel::MixedOxide => {
let a = 0.0452_f64;
let b = 2.46e-4_f64;
0.80 / (a + b * t)
}
NuclearFuel::UN => 1.864e-2 * t.powf(0.5) + 12.0,
NuclearFuel::UC => 20.0 * (t / 1000.0).powf(-0.2),
};
let bu_factor = 1.0 - 0.02 * (self.burnup / 10_000.0).min(1.0);
base * bu_factor
}
pub fn fission_gas_release(&self) -> f64 {
let beta = 3.0e-12_f64;
let raw = 1.0 - (-beta * self.burnup * self.temperature * self.temperature).exp();
raw.clamp(0.0, 1.0)
}
pub fn pellet_cladding_gap(&self) -> f64 {
let initial_gap = 80e-6_f64; let alpha_th = 10e-6_f64; let beta_sw = 0.5e-9_f64; let delta_t = (self.temperature - 300.0).max(0.0);
let delta_r = self.radius * (alpha_th * delta_t + beta_sw * self.burnup);
(initial_gap - delta_r).max(0.0)
}
pub fn centerline_temperature(&self, q_linear: f64) -> f64 {
let k = self.thermal_conductivity();
let t_surface = self.temperature;
t_surface + q_linear / (4.0 * PI * k)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ActinideMaterial {
pub element: Actinide,
pub oxidation_state: i32,
pub temperature: f64,
}
impl ActinideMaterial {
pub fn new(element: Actinide, oxidation_state: i32, temperature: f64) -> Self {
Self {
element,
oxidation_state,
temperature,
}
}
pub fn lattice_parameter(&self) -> f64 {
let a0 = match self.element {
Actinide::Uranium => 2.854, Actinide::Plutonium => 3.159, Actinide::Thorium => 5.084, Actinide::Neptunium => 6.663, Actinide::Americium => 3.468, };
let alpha = match self.element {
Actinide::Uranium => 14e-6,
Actinide::Plutonium => 60e-6, Actinide::Thorium => 11e-6,
Actinide::Neptunium => 15e-6,
Actinide::Americium => 12e-6,
};
a0 * (1.0 + alpha * (self.temperature - 293.0))
}
pub fn bulk_modulus(&self) -> f64 {
let b0 = match self.element {
Actinide::Uranium => 115.0,
Actinide::Plutonium => 35.0,
Actinide::Thorium => 58.0,
Actinide::Neptunium => 67.0,
Actinide::Americium => 30.0,
};
let db_dt = -0.02_f64; (b0 + db_dt * (self.temperature - 300.0)).max(0.0)
}
pub fn magnetic_moment(&self) -> f64 {
match self.element {
Actinide::Uranium => 0.0, Actinide::Plutonium => 0.0, Actinide::Thorium => 0.0, Actinide::Neptunium => 0.3, Actinide::Americium => 0.0, }
}
pub fn electronic_configuration(&self) -> &str {
match self.element {
Actinide::Thorium => "[Rn] 6d2 7s2",
Actinide::Uranium => "[Rn] 5f3 6d1 7s2",
Actinide::Neptunium => "[Rn] 5f4 6d1 7s2",
Actinide::Plutonium => "[Rn] 5f6 7s2",
Actinide::Americium => "[Rn] 5f7 7s2",
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ZircaloyClad {
pub composition: ZircaloyGrade,
pub temperature: f64,
pub fluence: f64,
}
impl ZircaloyClad {
pub fn new(composition: ZircaloyGrade, temperature: f64, fluence: f64) -> Self {
Self {
composition,
temperature,
fluence,
}
}
pub fn yield_strength(&self) -> f64 {
let sigma_y0 = match self.composition {
ZircaloyGrade::Zircaloy2 => 380.0,
ZircaloyGrade::Zircaloy4 => 400.0,
ZircaloyGrade::Zirlo => 480.0,
ZircaloyGrade::M5 => 420.0,
};
let a_irr = 2.5e-10_f64;
let delta_sigma = a_irr * self.fluence.sqrt();
let t_ref = 900.0_f64;
(sigma_y0 + delta_sigma) * (self.temperature / t_ref).powf(-0.3)
}
pub fn creep_rate(&self) -> f64 {
let a = match self.composition {
ZircaloyGrade::Zircaloy2 | ZircaloyGrade::Zircaloy4 => 1.0e-25,
ZircaloyGrade::Zirlo | ZircaloyGrade::M5 => 5.0e-26,
};
let sigma_ref = 100.0_f64; let n = 3.0_f64;
let q = 230_000.0_f64; let r = 8.314_f64;
a * sigma_ref.powf(n) * self.fluence * (-q / (r * self.temperature)).exp()
}
pub fn oxidation_rate(&self) -> f64 {
let (a, q) = match self.composition {
ZircaloyGrade::Zircaloy2 => (2.3e4_f64, 96_000.0_f64),
ZircaloyGrade::Zircaloy4 => (2.0e4_f64, 96_000.0_f64),
ZircaloyGrade::Zirlo => (1.2e4_f64, 99_000.0_f64),
ZircaloyGrade::M5 => (1.0e4_f64, 102_000.0_f64),
};
let r = 8.314_f64;
a * (-q / (r * self.temperature)).exp()
}
pub fn hydrogen_pickup(&self, time: f64) -> f64 {
let f_h = match self.composition {
ZircaloyGrade::Zircaloy2 => 0.15, ZircaloyGrade::Zircaloy4 => 0.10,
ZircaloyGrade::Zirlo => 0.08,
ZircaloyGrade::M5 => 0.05, };
let kc = self.oxidation_rate() * 1e9; let oxide_thickness = kc * time.powf(1.0 / 3.0); f_h * oxide_thickness * 0.4
}
}
pub fn lindhard_electronic_stopping(energy: f64, z1: f64, z2: f64, a1: f64, a2: f64) -> f64 {
let a_tf = 0.529 * 0.8853 / (z1.powf(2.0 / 3.0) + z2.powf(2.0 / 3.0)).sqrt();
let m12 = a1 * a2 / (a1 + a2);
let epsilon = energy * a2 * a_tf / ((a1 + a2) * z1 * z2 * 14.4);
let k_l = 0.0793 * z1.powf(2.0 / 3.0) * z2.powf(1.0 / 2.0) * (a1 + a2).powf(3.0 / 2.0)
/ ((z1.powf(2.0 / 3.0) + z2.powf(2.0 / 3.0)).powf(3.0 / 4.0)
* a1.powf(3.0 / 2.0)
* a2.powf(1.0 / 2.0));
let _ = m12; k_l * epsilon.sqrt() * a_tf
}
pub fn orowan_strengthening(spacing: f64, radius: f64, shear_mod: f64, burgers: f64) -> f64 {
let taylor_factor = 3.06_f64; let l_eff = (spacing * spacing - 4.0 * radius * radius)
.max(spacing * 0.01)
.sqrt();
let delta_tau = 0.84 * taylor_factor * shear_mod * burgers / l_eff;
delta_tau * 1e-6 }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kinchin_pease_below_threshold() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert_eq!(rd.kinchin_pease(20.0), 0);
}
#[test]
fn test_kinchin_pease_at_threshold() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert_eq!(rd.kinchin_pease(40.0), 1);
}
#[test]
fn test_kinchin_pease_above_threshold() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert_eq!(rd.kinchin_pease(400.0), 5);
}
#[test]
fn test_nrt_dpa_positive() {
let rd = RadiationDamage::new(40.0, 200.0, 26, 55.85);
let dpa = rd.nrt_dpa(1e22);
assert!(dpa > 0.0);
}
#[test]
fn test_nrt_dpa_scales_with_fluence() {
let rd = RadiationDamage::new(40.0, 200.0, 26, 55.85);
let dpa1 = rd.nrt_dpa(1e22);
let dpa2 = rd.nrt_dpa(2e22);
assert!((dpa2 / dpa1 - 2.0).abs() < 1e-10);
}
#[test]
fn test_cascade_size_below_threshold() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert_eq!(rd.cascade_size(30.0), 1);
}
#[test]
fn test_cascade_size_increases_with_energy() {
let rd = RadiationDamage::new(25.0, 1000.0, 26, 55.85);
let s1 = rd.cascade_size(100.0);
let s2 = rd.cascade_size(1000.0);
assert!(s2 > s1);
}
#[test]
fn test_recombination_fraction_range() {
let rd = RadiationDamage::new(40.0, 400.0, 26, 55.85);
let f = rd.recombination_fraction();
assert!((0.0..=1.0).contains(&f));
}
#[test]
fn test_swelling_below_incubation() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert_eq!(rd.swelling(3.0), 0.0);
}
#[test]
fn test_swelling_above_incubation() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert!(rd.swelling(20.0) > 0.0);
}
#[test]
fn test_swelling_monotone() {
let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
assert!(rd.swelling(30.0) > rd.swelling(20.0));
}
#[test]
fn test_uo2_thermal_conductivity_positive() {
let p = FuelPellet::new(NuclearFuel::UO2, 10_000.0, 800.0, 4.1e-3);
assert!(p.thermal_conductivity() > 0.0);
}
#[test]
fn test_mox_lower_conductivity_than_uo2() {
let uo2 = FuelPellet::new(NuclearFuel::UO2, 0.0, 800.0, 4.1e-3);
let mox = FuelPellet::new(NuclearFuel::MixedOxide, 0.0, 800.0, 4.1e-3);
assert!(mox.thermal_conductivity() < uo2.thermal_conductivity());
}
#[test]
fn test_un_thermal_conductivity_reasonable() {
let p = FuelPellet::new(NuclearFuel::UN, 5_000.0, 900.0, 4.1e-3);
let k = p.thermal_conductivity();
assert!(k > 1.0 && k < 50.0);
}
#[test]
fn test_fission_gas_release_at_low_temp_burnup() {
let p = FuelPellet::new(NuclearFuel::UO2, 100.0, 500.0, 4.1e-3);
let fgr = p.fission_gas_release();
assert!((0.0..=1.0).contains(&fgr));
}
#[test]
fn test_fission_gas_release_increases_with_burnup() {
let p1 = FuelPellet::new(NuclearFuel::UO2, 5_000.0, 1000.0, 4.1e-3);
let p2 = FuelPellet::new(NuclearFuel::UO2, 50_000.0, 1000.0, 4.1e-3);
assert!(p2.fission_gas_release() >= p1.fission_gas_release());
}
#[test]
fn test_pellet_cladding_gap_nonnegative() {
let p = FuelPellet::new(NuclearFuel::UO2, 60_000.0, 1200.0, 4.1e-3);
assert!(p.pellet_cladding_gap() >= 0.0);
}
#[test]
fn test_centerline_temperature_above_surface() {
let p = FuelPellet::new(NuclearFuel::UO2, 10_000.0, 700.0, 4.1e-3);
let t_cl = p.centerline_temperature(20_000.0);
assert!(t_cl > p.temperature);
}
#[test]
fn test_uranium_lattice_parameter_positive() {
let am = ActinideMaterial::new(Actinide::Uranium, 0, 293.0);
assert!(am.lattice_parameter() > 0.0);
}
#[test]
fn test_plutonium_lattice_expands_with_temperature() {
let am1 = ActinideMaterial::new(Actinide::Plutonium, 0, 300.0);
let am2 = ActinideMaterial::new(Actinide::Plutonium, 0, 600.0);
assert!(am2.lattice_parameter() > am1.lattice_parameter());
}
#[test]
fn test_thorium_bulk_modulus_decreases_with_temperature() {
let am1 = ActinideMaterial::new(Actinide::Thorium, 0, 300.0);
let am2 = ActinideMaterial::new(Actinide::Thorium, 0, 800.0);
assert!(am2.bulk_modulus() < am1.bulk_modulus());
}
#[test]
fn test_neptunium_magnetic_moment_nonzero() {
let am = ActinideMaterial::new(Actinide::Neptunium, 0, 293.0);
assert!(am.magnetic_moment() > 0.0);
}
#[test]
fn test_uranium_magnetic_moment_zero() {
let am = ActinideMaterial::new(Actinide::Uranium, 4, 293.0);
assert_eq!(am.magnetic_moment(), 0.0);
}
#[test]
fn test_electronic_configuration_uranium() {
let am = ActinideMaterial::new(Actinide::Uranium, 4, 293.0);
assert!(am.electronic_configuration().contains("5f3"));
}
#[test]
fn test_electronic_configuration_thorium() {
let am = ActinideMaterial::new(Actinide::Thorium, 4, 293.0);
assert!(am.electronic_configuration().contains("6d2"));
}
#[test]
fn test_yield_strength_positive() {
let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 1e25);
assert!(zr.yield_strength() > 0.0);
}
#[test]
fn test_yield_strength_increases_with_fluence() {
let zr1 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 0.0);
let zr2 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 1e26);
assert!(zr2.yield_strength() > zr1.yield_strength());
}
#[test]
fn test_creep_rate_positive() {
let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy2, 620.0, 1e25);
assert!(zr.creep_rate() > 0.0);
}
#[test]
fn test_oxidation_rate_positive() {
let zr = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
assert!(zr.oxidation_rate() > 0.0);
}
#[test]
fn test_m5_lower_oxidation_than_zircaloy4() {
let zr4 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 620.0, 1e25);
let m5 = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
assert!(m5.oxidation_rate() < zr4.oxidation_rate());
}
#[test]
fn test_hydrogen_pickup_increases_with_time() {
let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 620.0, 1e25);
let h1 = zr.hydrogen_pickup(1e6);
let h2 = zr.hydrogen_pickup(1e8);
assert!(h2 > h1);
}
#[test]
fn test_m5_lower_hydrogen_pickup_than_zircaloy2() {
let zr2 = ZircaloyClad::new(ZircaloyGrade::Zircaloy2, 620.0, 1e25);
let m5 = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
assert!(m5.hydrogen_pickup(1e7) < zr2.hydrogen_pickup(1e7));
}
#[test]
fn test_lindhard_stopping_positive() {
let s = lindhard_electronic_stopping(1e6, 36.0, 92.0, 84.0, 238.0);
assert!(s > 0.0);
}
#[test]
fn test_lindhard_stopping_increases_with_energy() {
let s1 = lindhard_electronic_stopping(1e5, 18.0, 26.0, 40.0, 55.85);
let s2 = lindhard_electronic_stopping(1e6, 18.0, 26.0, 40.0, 55.85);
assert!(s2 > s1);
}
#[test]
fn test_orowan_strengthening_positive() {
let delta = orowan_strengthening(50e-9, 2e-9, 80e9, 0.286e-9);
assert!(delta > 0.0);
}
#[test]
fn test_orowan_strengthening_decreases_with_spacing() {
let d1 = orowan_strengthening(20e-9, 1e-9, 80e9, 0.286e-9);
let d2 = orowan_strengthening(100e-9, 1e-9, 80e9, 0.286e-9);
assert!(d1 > d2);
}
#[test]
fn test_uc_thermal_conductivity() {
let p = FuelPellet::new(NuclearFuel::UC, 0.0, 1000.0, 4.1e-3);
assert!(p.thermal_conductivity() > 0.0);
}
#[test]
fn test_americium_electronic_config() {
let am = ActinideMaterial::new(Actinide::Americium, 3, 300.0);
assert!(am.electronic_configuration().contains("5f7"));
}
#[test]
fn test_zirlo_yield_strength() {
let zr = ZircaloyClad::new(ZircaloyGrade::Zirlo, 600.0, 1e25);
assert!(zr.yield_strength() > 0.0);
}
#[test]
fn test_plutonium_magnetic_moment_zero() {
let am = ActinideMaterial::new(Actinide::Plutonium, 0, 300.0);
assert_eq!(am.magnetic_moment(), 0.0);
}
}