1#![allow(dead_code)]
15#![allow(clippy::too_many_arguments)]
16
17use std::f64::consts::PI;
18
19#[derive(Debug, Clone, PartialEq)]
25pub enum NuclearFuel {
26 UO2,
28 MixedOxide,
30 UN,
32 UC,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub enum Actinide {
39 Uranium,
41 Plutonium,
43 Thorium,
45 Neptunium,
47 Americium,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum ZircaloyGrade {
54 Zircaloy2,
56 Zircaloy4,
58 Zirlo,
60 M5,
62}
63
64#[allow(dead_code)]
75#[derive(Debug, Clone)]
76pub struct RadiationDamage {
77 pub displacement_energy: f64,
79 pub pka_energy: f64,
81 pub atomic_number: u32,
83 pub atomic_mass: f64,
85}
86
87impl RadiationDamage {
88 pub fn new(
96 displacement_energy: f64,
97 pka_energy: f64,
98 atomic_number: u32,
99 atomic_mass: f64,
100 ) -> Self {
101 Self {
102 displacement_energy,
103 pka_energy,
104 atomic_number,
105 atomic_mass,
106 }
107 }
108
109 pub fn nrt_dpa(&self, fluence: f64) -> f64 {
117 let nu = self.kinchin_pease(self.pka_energy) as f64;
118 0.8 * nu * fluence / (2.0 * self.displacement_energy)
119 }
120
121 pub fn kinchin_pease(&self, energy: f64) -> usize {
131 let ed = self.displacement_energy;
132 if energy < ed {
133 0
134 } else if energy < 2.0 * ed {
135 1
136 } else {
137 (energy / (2.0 * ed)) as usize
138 }
139 }
140
141 pub fn cascade_size(&self, energy: f64) -> usize {
148 if energy <= self.displacement_energy {
149 return 1;
150 }
151 let ratio = energy / self.displacement_energy;
152 ratio.powf(0.75).round() as usize
153 }
154
155 pub fn recombination_fraction(&self) -> f64 {
159 let nu = self.kinchin_pease(self.pka_energy) as f64;
160 if nu < 1.0 {
161 return 0.0;
162 }
163 let alpha = 0.15_f64;
164 1.0 - 1.0 / (1.0 + alpha * nu)
165 }
166
167 pub fn swelling(&self, dpa: f64) -> f64 {
177 let dpa_inc = 5.0_f64;
178 let a = 1.5e-3_f64;
179 let n = 1.5_f64;
180 if dpa <= dpa_inc {
181 0.0
182 } else {
183 a * (dpa - dpa_inc).powf(n)
184 }
185 }
186}
187
188#[allow(dead_code)]
198#[derive(Debug, Clone)]
199pub struct FuelPellet {
200 pub material: NuclearFuel,
202 pub burnup: f64,
204 pub temperature: f64,
206 pub radius: f64,
208}
209
210impl FuelPellet {
211 pub fn new(material: NuclearFuel, burnup: f64, temperature: f64, radius: f64) -> Self {
219 Self {
220 material,
221 burnup,
222 temperature,
223 radius,
224 }
225 }
226
227 pub fn thermal_conductivity(&self) -> f64 {
233 let t = self.temperature;
234 let base = match self.material {
235 NuclearFuel::UO2 => {
236 let a = 0.0452_f64;
238 let b = 2.46e-4_f64;
239 let lattice_term = 1.0 / (a + b * t);
240 let electron_term = 5.47e9 * t.powf(-2.5) * (-16350.0 / t).exp();
241 lattice_term + electron_term
242 }
243 NuclearFuel::MixedOxide => {
244 let a = 0.0452_f64;
245 let b = 2.46e-4_f64;
246 0.80 / (a + b * t)
247 }
248 NuclearFuel::UN => 1.864e-2 * t.powf(0.5) + 12.0,
249 NuclearFuel::UC => 20.0 * (t / 1000.0).powf(-0.2),
250 };
251 let bu_factor = 1.0 - 0.02 * (self.burnup / 10_000.0).min(1.0);
253 base * bu_factor
254 }
255
256 pub fn fission_gas_release(&self) -> f64 {
261 let beta = 3.0e-12_f64;
262 let raw = 1.0 - (-beta * self.burnup * self.temperature * self.temperature).exp();
263 raw.clamp(0.0, 1.0)
264 }
265
266 pub fn pellet_cladding_gap(&self) -> f64 {
271 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);
275 let delta_r = self.radius * (alpha_th * delta_t + beta_sw * self.burnup);
276 (initial_gap - delta_r).max(0.0)
277 }
278
279 pub fn centerline_temperature(&self, q_linear: f64) -> f64 {
286 let k = self.thermal_conductivity();
287 let t_surface = self.temperature;
288 t_surface + q_linear / (4.0 * PI * k)
289 }
290}
291
292#[allow(dead_code)]
301#[derive(Debug, Clone)]
302pub struct ActinideMaterial {
303 pub element: Actinide,
305 pub oxidation_state: i32,
307 pub temperature: f64,
309}
310
311impl ActinideMaterial {
312 pub fn new(element: Actinide, oxidation_state: i32, temperature: f64) -> Self {
319 Self {
320 element,
321 oxidation_state,
322 temperature,
323 }
324 }
325
326 pub fn lattice_parameter(&self) -> f64 {
330 let a0 = match self.element {
331 Actinide::Uranium => 2.854, Actinide::Plutonium => 3.159, Actinide::Thorium => 5.084, Actinide::Neptunium => 6.663, Actinide::Americium => 3.468, };
337 let alpha = match self.element {
338 Actinide::Uranium => 14e-6,
339 Actinide::Plutonium => 60e-6, Actinide::Thorium => 11e-6,
341 Actinide::Neptunium => 15e-6,
342 Actinide::Americium => 12e-6,
343 };
344 a0 * (1.0 + alpha * (self.temperature - 293.0))
345 }
346
347 pub fn bulk_modulus(&self) -> f64 {
352 let b0 = match self.element {
353 Actinide::Uranium => 115.0,
354 Actinide::Plutonium => 35.0,
355 Actinide::Thorium => 58.0,
356 Actinide::Neptunium => 67.0,
357 Actinide::Americium => 30.0,
358 };
359 let db_dt = -0.02_f64; (b0 + db_dt * (self.temperature - 300.0)).max(0.0)
361 }
362
363 pub fn magnetic_moment(&self) -> f64 {
368 match self.element {
369 Actinide::Uranium => 0.0, Actinide::Plutonium => 0.0, Actinide::Thorium => 0.0, Actinide::Neptunium => 0.3, Actinide::Americium => 0.0, }
375 }
376
377 pub fn electronic_configuration(&self) -> &str {
381 match self.element {
382 Actinide::Thorium => "[Rn] 6d2 7s2",
383 Actinide::Uranium => "[Rn] 5f3 6d1 7s2",
384 Actinide::Neptunium => "[Rn] 5f4 6d1 7s2",
385 Actinide::Plutonium => "[Rn] 5f6 7s2",
386 Actinide::Americium => "[Rn] 5f7 7s2",
387 }
388 }
389}
390
391#[allow(dead_code)]
401#[derive(Debug, Clone)]
402pub struct ZircaloyClad {
403 pub composition: ZircaloyGrade,
405 pub temperature: f64,
407 pub fluence: f64,
409}
410
411impl ZircaloyClad {
412 pub fn new(composition: ZircaloyGrade, temperature: f64, fluence: f64) -> Self {
419 Self {
420 composition,
421 temperature,
422 fluence,
423 }
424 }
425
426 pub fn yield_strength(&self) -> f64 {
431 let sigma_y0 = match self.composition {
432 ZircaloyGrade::Zircaloy2 => 380.0,
433 ZircaloyGrade::Zircaloy4 => 400.0,
434 ZircaloyGrade::Zirlo => 480.0,
435 ZircaloyGrade::M5 => 420.0,
436 };
437 let a_irr = 2.5e-10_f64;
439 let delta_sigma = a_irr * self.fluence.sqrt();
440 let t_ref = 900.0_f64;
441 (sigma_y0 + delta_sigma) * (self.temperature / t_ref).powf(-0.3)
442 }
443
444 pub fn creep_rate(&self) -> f64 {
449 let a = match self.composition {
450 ZircaloyGrade::Zircaloy2 | ZircaloyGrade::Zircaloy4 => 1.0e-25,
451 ZircaloyGrade::Zirlo | ZircaloyGrade::M5 => 5.0e-26,
452 };
453 let sigma_ref = 100.0_f64; let n = 3.0_f64;
455 let q = 230_000.0_f64; let r = 8.314_f64;
457 a * sigma_ref.powf(n) * self.fluence * (-q / (r * self.temperature)).exp()
458 }
459
460 pub fn oxidation_rate(&self) -> f64 {
465 let (a, q) = match self.composition {
466 ZircaloyGrade::Zircaloy2 => (2.3e4_f64, 96_000.0_f64),
467 ZircaloyGrade::Zircaloy4 => (2.0e4_f64, 96_000.0_f64),
468 ZircaloyGrade::Zirlo => (1.2e4_f64, 99_000.0_f64),
469 ZircaloyGrade::M5 => (1.0e4_f64, 102_000.0_f64),
470 };
471 let r = 8.314_f64;
472 a * (-q / (r * self.temperature)).exp()
473 }
474
475 pub fn hydrogen_pickup(&self, time: f64) -> f64 {
483 let f_h = match self.composition {
484 ZircaloyGrade::Zircaloy2 => 0.15, ZircaloyGrade::Zircaloy4 => 0.10,
486 ZircaloyGrade::Zirlo => 0.08,
487 ZircaloyGrade::M5 => 0.05, };
489 let kc = self.oxidation_rate() * 1e9; let oxide_thickness = kc * time.powf(1.0 / 3.0); f_h * oxide_thickness * 0.4
494 }
495}
496
497pub fn lindhard_electronic_stopping(energy: f64, z1: f64, z2: f64, a1: f64, a2: f64) -> f64 {
517 let a_tf = 0.529 * 0.8853 / (z1.powf(2.0 / 3.0) + z2.powf(2.0 / 3.0)).sqrt();
519 let m12 = a1 * a2 / (a1 + a2);
521 let epsilon = energy * a2 * a_tf / ((a1 + a2) * z1 * z2 * 14.4);
523 let k_l = 0.0793 * z1.powf(2.0 / 3.0) * z2.powf(1.0 / 2.0) * (a1 + a2).powf(3.0 / 2.0)
525 / ((z1.powf(2.0 / 3.0) + z2.powf(2.0 / 3.0)).powf(3.0 / 4.0)
526 * a1.powf(3.0 / 2.0)
527 * a2.powf(1.0 / 2.0));
528 let _ = m12; k_l * epsilon.sqrt() * a_tf
530}
531
532pub fn orowan_strengthening(spacing: f64, radius: f64, shear_mod: f64, burgers: f64) -> f64 {
552 let taylor_factor = 3.06_f64; let l_eff = (spacing * spacing - 4.0 * radius * radius)
555 .max(spacing * 0.01)
556 .sqrt();
557 let delta_tau = 0.84 * taylor_factor * shear_mod * burgers / l_eff;
558 delta_tau * 1e-6 }
560
561#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
572 fn test_kinchin_pease_below_threshold() {
573 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
574 assert_eq!(rd.kinchin_pease(20.0), 0);
575 }
576
577 #[test]
578 fn test_kinchin_pease_at_threshold() {
579 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
580 assert_eq!(rd.kinchin_pease(40.0), 1);
581 }
582
583 #[test]
584 fn test_kinchin_pease_above_threshold() {
585 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
586 assert_eq!(rd.kinchin_pease(400.0), 5);
588 }
589
590 #[test]
591 fn test_nrt_dpa_positive() {
592 let rd = RadiationDamage::new(40.0, 200.0, 26, 55.85);
593 let dpa = rd.nrt_dpa(1e22);
594 assert!(dpa > 0.0);
595 }
596
597 #[test]
598 fn test_nrt_dpa_scales_with_fluence() {
599 let rd = RadiationDamage::new(40.0, 200.0, 26, 55.85);
600 let dpa1 = rd.nrt_dpa(1e22);
601 let dpa2 = rd.nrt_dpa(2e22);
602 assert!((dpa2 / dpa1 - 2.0).abs() < 1e-10);
603 }
604
605 #[test]
606 fn test_cascade_size_below_threshold() {
607 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
608 assert_eq!(rd.cascade_size(30.0), 1);
609 }
610
611 #[test]
612 fn test_cascade_size_increases_with_energy() {
613 let rd = RadiationDamage::new(25.0, 1000.0, 26, 55.85);
614 let s1 = rd.cascade_size(100.0);
615 let s2 = rd.cascade_size(1000.0);
616 assert!(s2 > s1);
617 }
618
619 #[test]
620 fn test_recombination_fraction_range() {
621 let rd = RadiationDamage::new(40.0, 400.0, 26, 55.85);
622 let f = rd.recombination_fraction();
623 assert!((0.0..=1.0).contains(&f));
624 }
625
626 #[test]
627 fn test_swelling_below_incubation() {
628 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
629 assert_eq!(rd.swelling(3.0), 0.0);
630 }
631
632 #[test]
633 fn test_swelling_above_incubation() {
634 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
635 assert!(rd.swelling(20.0) > 0.0);
636 }
637
638 #[test]
639 fn test_swelling_monotone() {
640 let rd = RadiationDamage::new(40.0, 100.0, 26, 55.85);
641 assert!(rd.swelling(30.0) > rd.swelling(20.0));
642 }
643
644 #[test]
647 fn test_uo2_thermal_conductivity_positive() {
648 let p = FuelPellet::new(NuclearFuel::UO2, 10_000.0, 800.0, 4.1e-3);
649 assert!(p.thermal_conductivity() > 0.0);
650 }
651
652 #[test]
653 fn test_mox_lower_conductivity_than_uo2() {
654 let uo2 = FuelPellet::new(NuclearFuel::UO2, 0.0, 800.0, 4.1e-3);
655 let mox = FuelPellet::new(NuclearFuel::MixedOxide, 0.0, 800.0, 4.1e-3);
656 assert!(mox.thermal_conductivity() < uo2.thermal_conductivity());
657 }
658
659 #[test]
660 fn test_un_thermal_conductivity_reasonable() {
661 let p = FuelPellet::new(NuclearFuel::UN, 5_000.0, 900.0, 4.1e-3);
662 let k = p.thermal_conductivity();
663 assert!(k > 1.0 && k < 50.0);
664 }
665
666 #[test]
667 fn test_fission_gas_release_at_low_temp_burnup() {
668 let p = FuelPellet::new(NuclearFuel::UO2, 100.0, 500.0, 4.1e-3);
669 let fgr = p.fission_gas_release();
670 assert!((0.0..=1.0).contains(&fgr));
671 }
672
673 #[test]
674 fn test_fission_gas_release_increases_with_burnup() {
675 let p1 = FuelPellet::new(NuclearFuel::UO2, 5_000.0, 1000.0, 4.1e-3);
676 let p2 = FuelPellet::new(NuclearFuel::UO2, 50_000.0, 1000.0, 4.1e-3);
677 assert!(p2.fission_gas_release() >= p1.fission_gas_release());
678 }
679
680 #[test]
681 fn test_pellet_cladding_gap_nonnegative() {
682 let p = FuelPellet::new(NuclearFuel::UO2, 60_000.0, 1200.0, 4.1e-3);
683 assert!(p.pellet_cladding_gap() >= 0.0);
684 }
685
686 #[test]
687 fn test_centerline_temperature_above_surface() {
688 let p = FuelPellet::new(NuclearFuel::UO2, 10_000.0, 700.0, 4.1e-3);
689 let t_cl = p.centerline_temperature(20_000.0);
690 assert!(t_cl > p.temperature);
691 }
692
693 #[test]
696 fn test_uranium_lattice_parameter_positive() {
697 let am = ActinideMaterial::new(Actinide::Uranium, 0, 293.0);
698 assert!(am.lattice_parameter() > 0.0);
699 }
700
701 #[test]
702 fn test_plutonium_lattice_expands_with_temperature() {
703 let am1 = ActinideMaterial::new(Actinide::Plutonium, 0, 300.0);
704 let am2 = ActinideMaterial::new(Actinide::Plutonium, 0, 600.0);
705 assert!(am2.lattice_parameter() > am1.lattice_parameter());
706 }
707
708 #[test]
709 fn test_thorium_bulk_modulus_decreases_with_temperature() {
710 let am1 = ActinideMaterial::new(Actinide::Thorium, 0, 300.0);
711 let am2 = ActinideMaterial::new(Actinide::Thorium, 0, 800.0);
712 assert!(am2.bulk_modulus() < am1.bulk_modulus());
713 }
714
715 #[test]
716 fn test_neptunium_magnetic_moment_nonzero() {
717 let am = ActinideMaterial::new(Actinide::Neptunium, 0, 293.0);
718 assert!(am.magnetic_moment() > 0.0);
719 }
720
721 #[test]
722 fn test_uranium_magnetic_moment_zero() {
723 let am = ActinideMaterial::new(Actinide::Uranium, 4, 293.0);
724 assert_eq!(am.magnetic_moment(), 0.0);
725 }
726
727 #[test]
728 fn test_electronic_configuration_uranium() {
729 let am = ActinideMaterial::new(Actinide::Uranium, 4, 293.0);
730 assert!(am.electronic_configuration().contains("5f3"));
731 }
732
733 #[test]
734 fn test_electronic_configuration_thorium() {
735 let am = ActinideMaterial::new(Actinide::Thorium, 4, 293.0);
736 assert!(am.electronic_configuration().contains("6d2"));
737 }
738
739 #[test]
742 fn test_yield_strength_positive() {
743 let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 1e25);
744 assert!(zr.yield_strength() > 0.0);
745 }
746
747 #[test]
748 fn test_yield_strength_increases_with_fluence() {
749 let zr1 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 0.0);
750 let zr2 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 600.0, 1e26);
751 assert!(zr2.yield_strength() > zr1.yield_strength());
752 }
753
754 #[test]
755 fn test_creep_rate_positive() {
756 let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy2, 620.0, 1e25);
757 assert!(zr.creep_rate() > 0.0);
758 }
759
760 #[test]
761 fn test_oxidation_rate_positive() {
762 let zr = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
763 assert!(zr.oxidation_rate() > 0.0);
764 }
765
766 #[test]
767 fn test_m5_lower_oxidation_than_zircaloy4() {
768 let zr4 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 620.0, 1e25);
769 let m5 = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
770 assert!(m5.oxidation_rate() < zr4.oxidation_rate());
771 }
772
773 #[test]
774 fn test_hydrogen_pickup_increases_with_time() {
775 let zr = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 620.0, 1e25);
776 let h1 = zr.hydrogen_pickup(1e6);
777 let h2 = zr.hydrogen_pickup(1e8);
778 assert!(h2 > h1);
779 }
780
781 #[test]
782 fn test_m5_lower_hydrogen_pickup_than_zircaloy2() {
783 let zr2 = ZircaloyClad::new(ZircaloyGrade::Zircaloy2, 620.0, 1e25);
784 let m5 = ZircaloyClad::new(ZircaloyGrade::M5, 620.0, 1e25);
785 assert!(m5.hydrogen_pickup(1e7) < zr2.hydrogen_pickup(1e7));
786 }
787
788 #[test]
791 fn test_lindhard_stopping_positive() {
792 let s = lindhard_electronic_stopping(1e6, 36.0, 92.0, 84.0, 238.0);
794 assert!(s > 0.0);
795 }
796
797 #[test]
798 fn test_lindhard_stopping_increases_with_energy() {
799 let s1 = lindhard_electronic_stopping(1e5, 18.0, 26.0, 40.0, 55.85);
801 let s2 = lindhard_electronic_stopping(1e6, 18.0, 26.0, 40.0, 55.85);
802 assert!(s2 > s1);
803 }
804
805 #[test]
806 fn test_orowan_strengthening_positive() {
807 let delta = orowan_strengthening(50e-9, 2e-9, 80e9, 0.286e-9);
809 assert!(delta > 0.0);
810 }
811
812 #[test]
813 fn test_orowan_strengthening_decreases_with_spacing() {
814 let d1 = orowan_strengthening(20e-9, 1e-9, 80e9, 0.286e-9);
815 let d2 = orowan_strengthening(100e-9, 1e-9, 80e9, 0.286e-9);
816 assert!(d1 > d2);
817 }
818
819 #[test]
820 fn test_uc_thermal_conductivity() {
821 let p = FuelPellet::new(NuclearFuel::UC, 0.0, 1000.0, 4.1e-3);
822 assert!(p.thermal_conductivity() > 0.0);
823 }
824
825 #[test]
826 fn test_americium_electronic_config() {
827 let am = ActinideMaterial::new(Actinide::Americium, 3, 300.0);
828 assert!(am.electronic_configuration().contains("5f7"));
829 }
830
831 #[test]
832 fn test_zirlo_yield_strength() {
833 let zr = ZircaloyClad::new(ZircaloyGrade::Zirlo, 600.0, 1e25);
834 assert!(zr.yield_strength() > 0.0);
835 }
836
837 #[test]
838 fn test_plutonium_magnetic_moment_zero() {
839 let am = ActinideMaterial::new(Actinide::Plutonium, 0, 300.0);
840 assert_eq!(am.magnetic_moment(), 0.0);
841 }
842}