1#![allow(dead_code)]
26#![allow(clippy::too_many_arguments)]
27
28use std::f64::consts::PI;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ZircaloyGrade {
37 Zircaloy2,
39 Zircaloy4,
41 Zirlo,
43 M5,
45}
46
47#[derive(Debug, Clone)]
49pub struct ZircaloyClad {
50 pub grade: ZircaloyGrade,
52 pub outer_diameter: f64,
54 pub wall_thickness: f64,
56 pub oxide_thickness: f64,
58 pub fluence: f64,
60 pub temperature: f64,
62}
63
64impl ZircaloyClad {
65 pub fn new(
67 grade: ZircaloyGrade,
68 outer_diameter: f64,
69 wall_thickness: f64,
70 temperature: f64,
71 ) -> Self {
72 Self {
73 grade,
74 outer_diameter,
75 wall_thickness,
76 oxide_thickness: 0.0,
77 fluence: 0.0,
78 temperature,
79 }
80 }
81
82 fn cubic_rate_constant(&self) -> f64 {
86 let ea_j = match self.grade {
87 ZircaloyGrade::Zircaloy2 => 1.42e4_f64,
88 ZircaloyGrade::Zircaloy4 => 1.35e4_f64,
89 ZircaloyGrade::Zirlo => 1.20e4_f64,
90 ZircaloyGrade::M5 => 1.10e4_f64,
91 };
92 let a0 = 2.88e-3_f64; let r = 8.314_f64;
95 a0 * (-(ea_j) / (r * self.temperature)).exp()
96 }
97
98 pub fn oxide_weight_gain_cubic(&self, time_s: f64) -> f64 {
102 let a = self.cubic_rate_constant();
103 (a * time_s).cbrt()
104 }
105
106 pub fn linear_rate_constant(&self) -> f64 {
110 let ea_j = 1.10e4_f64;
112 let b0 = 6.0e-6_f64;
113 let r = 8.314_f64;
114 b0 * (-(ea_j) / (r * self.temperature)).exp()
115 }
116
117 pub fn breakaway_time(&self) -> f64 {
121 let wt = match self.grade {
123 ZircaloyGrade::Zircaloy2 => 0.30_f64,
124 ZircaloyGrade::Zircaloy4 => 0.30_f64,
125 ZircaloyGrade::Zirlo => 0.40_f64,
126 ZircaloyGrade::M5 => 0.50_f64,
127 };
128 wt * wt * wt / self.cubic_rate_constant()
129 }
130
131 pub fn hydrogen_pickup_fraction(&self) -> f64 {
136 let f0 = match self.grade {
138 ZircaloyGrade::Zircaloy2 => 0.15_f64,
139 ZircaloyGrade::Zircaloy4 => 0.10_f64,
140 ZircaloyGrade::Zirlo => 0.08_f64,
141 ZircaloyGrade::M5 => 0.07_f64,
142 };
143 let ea = 2500.0_f64; let r = 8.314_f64;
145 let fluence_factor = 1.0 + 2.0e-26 * self.fluence;
146 (f0 * (ea / (r * self.temperature)).exp() * fluence_factor).min(1.0)
147 }
148
149 pub fn hydrogen_concentration_wppm(&self, oxide_wg: f64) -> f64 {
154 let rho_zr = 6520.0_f64; let wt = self.wall_thickness;
158 let h_gen = oxide_wg * 1.11e4 / (rho_zr * wt);
159 h_gen * self.hydrogen_pickup_fraction()
160 }
161
162 pub fn yield_strength(&self) -> f64 {
166 let sigma_0 = match self.grade {
168 ZircaloyGrade::Zircaloy2 => 450e6_f64,
169 ZircaloyGrade::Zircaloy4 => 480e6_f64,
170 ZircaloyGrade::Zirlo => 500e6_f64,
171 ZircaloyGrade::M5 => 510e6_f64,
172 };
173 let t0 = 293.0_f64;
175 let softening = 1.0 - 5.0e-4 * (self.temperature - t0);
176 let k_irr = 1.2e-12_f64;
178 let irr_hardening = k_irr * self.fluence.sqrt();
179 (sigma_0 * softening + irr_hardening).max(0.0)
180 }
181
182 pub fn irradiation_creep_rate(&self, sigma_pa: f64, flux_n_m2_s: f64) -> f64 {
186 let b0 = 3.0e-27_f64;
188 b0 * sigma_pa * flux_n_m2_s
189 }
190
191 pub fn hydride_ductility_factor(&self, h_wppm: f64) -> f64 {
195 (1.0 - h_wppm / 1000.0).max(0.05)
197 }
198}
199
200#[derive(Debug, Clone)]
206pub struct Uo2Pellet {
207 pub diameter: f64,
209 pub height: f64,
211 pub burnup: f64,
213 pub temperature_centre: f64,
215 pub gap_width: f64,
217 pub enrichment: f64,
219}
220
221impl Uo2Pellet {
222 pub fn new(diameter: f64, height: f64, enrichment: f64) -> Self {
224 Self {
225 diameter,
226 height,
227 burnup: 0.0,
228 temperature_centre: 900.0,
229 gap_width: 1.0e-4,
230 enrichment,
231 }
232 }
233
234 pub fn thermal_conductivity(&self, temperature_k: f64) -> f64 {
239 let t = temperature_k;
241 let k0 = 1.0 / (0.0452 + 2.46e-4 * t) + 88.0e9 / (t * t) * (-18531.7 / t).exp();
242
243 let bu = self.burnup.min(100.0);
245 let f_d = 1.0 / (1.0 + 0.019 * bu / (3.0 - 0.019 * bu));
246
247 let f_p = 1.0 - 0.2 / (1.0 + (t - 900.0).powi(2) / 40000.0) * (bu / 100.0);
249
250 let f_r = 1.0 - 0.18 / (1.0 + (t - 900.0).powi(2) / 40000.0) * (bu / 100.0);
252
253 let porosity = 0.05_f64;
255 let f_por = (1.0 - porosity).powf(2.5);
256
257 k0 * f_d * f_p * f_r * f_por
258 }
259
260 pub fn fission_gas_release_booth(&self, temperature_k: f64, time_s: f64) -> f64 {
265 let d0 = 5.0e-8_f64; let ea = 40200.0_f64; let r = 8.314_f64;
269 let d = d0 * (-(ea) / (r * temperature_k)).exp();
270 let radius = self.diameter / 2.0;
272 let tau = d * time_s / (radius * radius);
273 if tau < 0.02 {
275 6.0 * (tau / PI).sqrt() - 3.0 * tau
276 } else {
277 1.0 - (6.0 / (PI * PI))
278 * (1..=20)
279 .map(|n| (-(n as f64).powi(2) * PI * PI * tau).exp() / (n as f64).powi(2))
280 .sum::<f64>()
281 }
282 .clamp(0.0, 1.0)
283 }
284
285 pub fn cracking_conductivity_factor(&self) -> f64 {
289 let bu = self.burnup;
291 1.0 / (1.0 + 0.005 * bu)
292 }
293
294 pub fn fission_product_swelling(&self) -> f64 {
298 0.98e-2 * self.burnup / 10.0
299 }
300
301 pub fn gap_conductance(&self, fill_gas_conductivity: f64) -> f64 {
305 let k_gas = fill_gas_conductivity.max(0.01);
306 let gap = self.gap_width.max(1e-7);
307 let t_clad = self.temperature_centre - 400.0; let sigma = 5.67e-8_f64;
310 let emissivity = 0.85_f64;
311 let radiation = 4.0 * sigma * emissivity * t_clad.powi(3);
312 k_gas / gap + radiation
313 }
314
315 pub fn linear_heat_rate(&self, specific_power_w_per_kgu: f64) -> f64 {
317 let rho_uo2 = 10_400.0_f64; let area = PI * (self.diameter / 2.0).powi(2);
319 let u_mass_fraction = 238.03 / (238.03 + 2.0 * 16.0); let m_u_per_m = rho_uo2 * area * u_mass_fraction;
322 specific_power_w_per_kgu * m_u_per_m / 1000.0
323 }
324}
325
326#[derive(Debug, Clone, Copy)]
332pub struct VoidSwellingParams {
333 pub incubation_dose_dpa: f64,
335 pub swelling_rate_pct_per_dpa: f64,
337 pub peak_temperature_k: f64,
339 pub temperature_width_k: f64,
341}
342
343impl Default for VoidSwellingParams {
344 fn default() -> Self {
345 Self {
347 incubation_dose_dpa: 10.0,
348 swelling_rate_pct_per_dpa: 1.0,
349 peak_temperature_k: 773.0,
350 temperature_width_k: 80.0,
351 }
352 }
353}
354
355pub fn garner_void_swelling(dose_dpa: f64, temperature_k: f64, params: &VoidSwellingParams) -> f64 {
360 if dose_dpa <= params.incubation_dose_dpa {
361 return 0.0;
362 }
363 let excess_dpa = dose_dpa - params.incubation_dose_dpa;
364 let dt = temperature_k - params.peak_temperature_k;
366 let f_t = (-(dt * dt) / (2.0 * params.temperature_width_k * params.temperature_width_k)).exp();
367 let swelling_pct = params.swelling_rate_pct_per_dpa * excess_dpa * f_t;
368 swelling_pct / 100.0
369}
370
371pub fn void_number_density(swelling: f64, mean_void_radius_m: f64) -> f64 {
373 if mean_void_radius_m < 1e-15 {
374 return 0.0;
375 }
376 swelling / ((4.0 / 3.0) * PI * mean_void_radius_m.powi(3))
377}
378
379pub fn irradiation_creep_rate(
390 stress_pa: f64,
391 temperature_k: f64,
392 flux_dpa_per_s: f64,
393 b_irr: f64,
394 a_thermal: f64,
395 n_creep: f64,
396 q_creep: f64,
397) -> f64 {
398 let r = 8.314_f64;
399 let eps_irr = b_irr * stress_pa * flux_dpa_per_s;
400 let eps_th = a_thermal * stress_pa.powf(n_creep) * (-(q_creep) / (r * temperature_k)).exp();
401 eps_irr + eps_th
402}
403
404pub fn irradiation_stress_relaxation(sigma_0: f64, b_irr: f64, dose_dpa: f64) -> f64 {
408 sigma_0 * (-(b_irr * dose_dpa)).exp()
409}
410
411#[derive(Debug, Clone, Copy)]
419pub struct RisParams {
420 pub bulk_concentration: f64,
422 pub binding_energy_ev: f64,
424 pub temperature_k: f64,
426 pub dose_rate_dpa_s: f64,
428 pub recombination_coeff: f64,
430}
431
432impl Default for RisParams {
433 fn default() -> Self {
434 Self {
435 bulk_concentration: 0.18, binding_energy_ev: 0.15,
437 temperature_k: 573.0,
438 dose_rate_dpa_s: 1e-8,
439 recombination_coeff: 1e17,
440 }
441 }
442}
443
444pub fn ris_grain_boundary_concentration(params: &RisParams) -> f64 {
449 let kb = 8.617e-5_f64; let kbt = kb * params.temperature_k;
451 let binding_factor = (params.binding_energy_ev / kbt).exp();
453 let defect_ratio = (params.dose_rate_dpa_s / params.recombination_coeff).sqrt();
455 let delta_c = params.bulk_concentration * (1.0 - binding_factor * defect_ratio);
457 (params.bulk_concentration + delta_c).clamp(0.0, 1.0)
458}
459
460#[derive(Debug, Clone, Copy, PartialEq)]
466pub enum RpvSteelGrade {
467 A508Class3,
469 A533GradeB,
471 Vver15Kh2Mfa,
473}
474
475#[derive(Debug, Clone)]
477pub struct RpvCharpy {
478 pub grade: RpvSteelGrade,
480 pub use_unirradiated: f64,
482 pub rt_ndt_0: f64,
484 pub fluence: f64,
486 pub copper_wt: f64,
488 pub nickel_wt: f64,
490 pub phosphorus_wt: f64,
492}
493
494impl RpvCharpy {
495 pub fn new(
497 grade: RpvSteelGrade,
498 use_unirradiated: f64,
499 rt_ndt_0: f64,
500 fluence: f64,
501 copper_wt: f64,
502 nickel_wt: f64,
503 phosphorus_wt: f64,
504 ) -> Self {
505 Self {
506 grade,
507 use_unirradiated,
508 rt_ndt_0,
509 fluence,
510 copper_wt,
511 nickel_wt,
512 phosphorus_wt,
513 }
514 }
515
516 pub fn use_drop(&self) -> f64 {
521 if self.fluence < 1e18 {
522 return 0.0;
523 }
524 let phi = self.fluence / 1e19_f64; let a =
527 0.11 * (1.0 + 1.58 * self.copper_wt.max(0.0)) * (1.0 + 3.77 * self.nickel_wt.max(0.0));
528 let exponent = 0.28 - 0.10 * phi.log10();
529 let fraction = a * phi.powf(exponent);
530 fraction * self.use_unirradiated
531 }
532
533 pub fn use_irradiated(&self) -> f64 {
535 (self.use_unirradiated - self.use_drop()).max(0.0)
536 }
537
538 pub fn rtndt_shift(&self) -> f64 {
542 if self.fluence < 1e18 {
543 return 0.0;
544 }
545 let phi = self.fluence / 1e19_f64;
546 let cf = chemistry_factor(self.copper_wt, self.nickel_wt);
547 let exponent = 0.28 - 0.10 * phi.log10();
548 cf * phi.powf(exponent)
549 }
550
551 pub fn rtndt_irradiated(&self) -> f64 {
553 self.rt_ndt_0 + self.rtndt_shift()
554 }
555
556 pub fn fracture_toughness_kic(&self, test_temperature_c: f64) -> f64 {
560 let t0 = self.rtndt_irradiated() - 33.0;
561 30.0 + 70.0 * (0.019 * (test_temperature_c - t0)).exp()
562 }
563}
564
565pub fn chemistry_factor(copper_wt: f64, nickel_wt: f64) -> f64 {
569 let cu = copper_wt.clamp(0.0, 0.40);
570 let ni = nickel_wt.clamp(0.0, 1.2);
571 let cf_cu = if cu < 0.10 { 0.0 } else { (cu - 0.10) * 250.0 };
573 let cf_ni = ni * 20.0;
574 (cf_cu + cf_ni).max(0.0)
575}
576
577#[derive(Debug, Clone, Copy)]
585pub struct HeliumEmbrittlement {
586 pub bubble_density: f64,
588 pub bubble_radius: f64,
590 pub grain_size: f64,
592}
593
594impl HeliumEmbrittlement {
595 pub fn new(bubble_density: f64, bubble_radius: f64, grain_size: f64) -> Self {
597 Self {
598 bubble_density,
599 bubble_radius,
600 grain_size,
601 }
602 }
603
604 pub fn coverage_fraction(&self) -> f64 {
608 (self.bubble_density * PI * self.bubble_radius * self.bubble_radius).min(1.0)
609 }
610
611 pub fn strength_reduction_factor(&self) -> f64 {
616 let a_he = 0.8_f64;
617 (1.0 - a_he * self.coverage_fraction()).max(0.0)
618 }
619
620 pub fn helium_appm(
624 &self,
625 cross_section_m2: f64,
626 thermal_flux: f64,
627 time_s: f64,
628 atom_density: f64,
629 ) -> f64 {
630 cross_section_m2 * thermal_flux * time_s * atom_density * 1.0e6
631 }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq)]
640pub enum GraphiteGrade {
641 PileGradeA,
643 Ig110,
645 H327,
647 Nbg18,
649}
650
651#[derive(Debug, Clone)]
653pub struct GraphiteBlock {
654 pub grade: GraphiteGrade,
656 pub fluence: f64,
658 pub temperature: f64,
660 pub initial_density: f64,
662}
663
664impl GraphiteBlock {
665 pub fn new(grade: GraphiteGrade, temperature: f64) -> Self {
667 let density = match grade {
668 GraphiteGrade::PileGradeA => 1750.0,
669 GraphiteGrade::Ig110 => 1770.0,
670 GraphiteGrade::H327 => 1740.0,
671 GraphiteGrade::Nbg18 => 1850.0,
672 };
673 Self {
674 grade,
675 fluence: 0.0,
676 temperature,
677 initial_density: density,
678 }
679 }
680
681 pub fn thermal_conductivity_unirradiated(&self) -> f64 {
683 match self.grade {
684 GraphiteGrade::PileGradeA => 170.0,
685 GraphiteGrade::Ig110 => 130.0,
686 GraphiteGrade::H327 => 80.0,
687 GraphiteGrade::Nbg18 => 140.0,
688 }
689 }
690
691 pub fn thermal_conductivity_irradiated(&self) -> f64 {
696 let k0 = self.thermal_conductivity_unirradiated();
697 let phi = self.fluence / 1.0e25_f64; let a = match self.grade {
700 GraphiteGrade::PileGradeA => 0.10,
701 GraphiteGrade::Ig110 => 0.12,
702 GraphiteGrade::H327 => 0.15,
703 GraphiteGrade::Nbg18 => 0.11,
704 };
705 let degradation = 1.0 / (1.0 + a * phi);
706 k0 * degradation
707 }
708
709 pub fn dimensional_change_axial(&self) -> f64 {
713 let phi = self.fluence / 1.0e25_f64;
714 let phi_t = match self.grade {
716 GraphiteGrade::PileGradeA => 3.0,
717 GraphiteGrade::Ig110 => 5.0,
718 GraphiteGrade::H327 => 4.0,
719 GraphiteGrade::Nbg18 => 4.5,
720 };
721 let contraction_rate = -0.03_f64;
722 let expansion_rate = 0.02_f64;
723 if phi < phi_t {
724 contraction_rate * phi
725 } else {
726 contraction_rate * phi_t + expansion_rate * (phi - phi_t)
727 }
728 }
729
730 pub fn youngs_modulus(&self) -> f64 {
734 let e0 = match self.grade {
735 GraphiteGrade::PileGradeA => 8.0e9_f64,
736 GraphiteGrade::Ig110 => 9.8e9_f64,
737 GraphiteGrade::H327 => 11.0e9_f64,
738 GraphiteGrade::Nbg18 => 12.0e9_f64,
739 };
740 let phi = self.fluence / 1.0e25_f64;
741 let factor = 1.0 + 0.20 * phi * (-phi / 2.0).exp();
742 e0 * factor
743 }
744
745 pub fn wigner_energy_j_per_kg(&self) -> f64 {
749 let e_sat = 2700.0_f64;
751 let phi = self.fluence / 1.0e25_f64;
752 let t_factor = if self.temperature < 473.0 {
753 1.0 - (self.temperature - 300.0) / 173.0 * 0.5
754 } else {
755 0.5
756 };
757 e_sat * (1.0 - (-phi * 0.5).exp()) * t_factor.clamp(0.0, 1.0)
758 }
759}
760
761#[derive(Debug, Clone, Copy, PartialEq)]
767pub enum SfrSteelGrade {
768 Ss316,
770 Ht9,
772 D9,
774 Ep450,
776}
777
778#[derive(Debug, Clone)]
780pub struct SfrMaterial {
781 pub grade: SfrSteelGrade,
783 pub temperature: f64,
785 pub dose_dpa: f64,
787}
788
789impl SfrMaterial {
790 pub fn new(grade: SfrSteelGrade, temperature: f64) -> Self {
792 Self {
793 grade,
794 temperature,
795 dose_dpa: 0.0,
796 }
797 }
798
799 pub fn tensile_strength_unirradiated(&self) -> f64 {
801 match self.grade {
802 SfrSteelGrade::Ss316 => 515.0,
803 SfrSteelGrade::Ht9 => 690.0,
804 SfrSteelGrade::D9 => 550.0,
805 SfrSteelGrade::Ep450 => 680.0,
806 }
807 }
808
809 pub fn irradiation_hardening_mpa(&self) -> f64 {
813 let a = match self.grade {
814 SfrSteelGrade::Ss316 => 50.0,
815 SfrSteelGrade::Ht9 => 30.0, SfrSteelGrade::D9 => 45.0,
817 SfrSteelGrade::Ep450 => 28.0,
818 };
819 a * self.dose_dpa.sqrt()
820 }
821
822 pub fn void_swelling_pct(&self) -> f64 {
824 match self.grade {
825 SfrSteelGrade::Ss316 | SfrSteelGrade::D9 => {
826 let params = VoidSwellingParams::default();
827 garner_void_swelling(self.dose_dpa, self.temperature, ¶ms) * 100.0
828 }
829 SfrSteelGrade::Ht9 | SfrSteelGrade::Ep450 => {
831 0.002 * self.dose_dpa }
833 }
834 }
835
836 pub fn thermal_creep_rate(&self, stress_mpa: f64) -> f64 {
838 let (a, n, q) = match self.grade {
840 SfrSteelGrade::Ss316 => (1.5e-32_f64, 5.0_f64, 280_000.0_f64),
841 SfrSteelGrade::Ht9 => (3.0e-28_f64, 4.5_f64, 260_000.0_f64),
842 SfrSteelGrade::D9 => (1.2e-32_f64, 5.0_f64, 275_000.0_f64),
843 SfrSteelGrade::Ep450 => (2.5e-28_f64, 4.5_f64, 255_000.0_f64),
844 };
845 let r = 8.314_f64;
846 a * stress_mpa.powf(n) * (-(q) / (r * self.temperature)).exp()
847 }
848
849 pub fn fatigue_life_cycles(&self, strain_amplitude: f64) -> f64 {
853 let (eps_f, c) = match self.grade {
854 SfrSteelGrade::Ss316 | SfrSteelGrade::D9 => (0.3_f64, -0.60_f64),
855 SfrSteelGrade::Ht9 | SfrSteelGrade::Ep450 => (0.25_f64, -0.57_f64),
856 };
857 if strain_amplitude < 1e-9 {
858 return f64::MAX;
859 }
860 let two_nf = (strain_amplitude / eps_f).powf(1.0 / c);
861 (two_nf / 2.0).max(1.0)
862 }
863
864 pub fn creep_fatigue_interaction(
869 &self,
870 delta_t_s: f64,
871 rupture_time_s: f64,
872 cycles: f64,
873 n_f: f64,
874 ) -> bool {
875 let d_c = delta_t_s / rupture_time_s.max(1e-9);
876 let d_f = cycles / n_f.max(1.0);
877 (d_c + d_f) >= 1.0
878 }
879}
880
881#[derive(Debug, Clone, Copy)]
889pub struct DecayChain {
890 pub lambda: f64,
892 pub n0_parent: f64,
894}
895
896impl DecayChain {
897 pub fn from_half_life(t_half_s: f64, n0_parent: f64) -> Self {
899 Self {
900 lambda: 2.0_f64.ln() / t_half_s,
901 n0_parent,
902 }
903 }
904
905 pub fn parent_count(&self, t: f64) -> f64 {
907 self.n0_parent * (-self.lambda * t).exp()
908 }
909
910 pub fn daughter_count(&self, t: f64) -> f64 {
912 self.n0_parent * (1.0 - (-self.lambda * t).exp())
913 }
914
915 pub fn activity_bq(&self, t: f64) -> f64 {
917 self.lambda * self.parent_count(t)
918 }
919
920 pub fn specific_activity(&self, molar_mass_g_per_mol: f64, n0_mass_kg: f64) -> f64 {
922 let avogadro = 6.022e23_f64;
923 let n0 = n0_mass_kg * 1000.0 / molar_mass_g_per_mol * avogadro;
924 let chain = Self {
925 lambda: self.lambda,
926 n0_parent: n0,
927 };
928 chain.activity_bq(0.0)
929 }
930}
931
932pub fn control_rod_worth(sigma_a_rod_m2: f64, nu_sigma_f_m2: f64) -> f64 {
936 if nu_sigma_f_m2 < 1e-40 {
937 return 0.0;
938 }
939 -sigma_a_rod_m2 / nu_sigma_f_m2
940}
941
942#[derive(Debug, Clone, Copy)]
948pub struct PciParams {
949 pub pellet_radius: f64,
951 pub clad_inner_radius: f64,
953 pub clad_youngs_modulus: f64,
955 pub clad_poisson: f64,
957 pub clad_thickness: f64,
959}
960
961impl Default for PciParams {
962 fn default() -> Self {
963 Self {
964 pellet_radius: 4.1e-3,
965 clad_inner_radius: 4.18e-3,
966 clad_youngs_modulus: 75e9,
967 clad_poisson: 0.37,
968 clad_thickness: 0.57e-3,
969 }
970 }
971}
972
973pub fn pci_contact_pressure(params: &PciParams, pellet_swelling_fraction: f64) -> f64 {
977 let delta = params.pellet_radius * pellet_swelling_fraction
978 - (params.clad_inner_radius - params.pellet_radius);
979 if delta <= 0.0 {
980 return 0.0;
981 }
982 let e = params.clad_youngs_modulus;
984 let nu = params.clad_poisson;
985 let ri = params.clad_inner_radius;
986 let t = params.clad_thickness;
987 e * delta * t / ((1.0 - nu) * ri * ri)
989}
990
991pub fn power_to_burnup(specific_power_w_per_kgu: f64, time_days: f64) -> f64 {
997 specific_power_w_per_kgu * time_days / 1e6
998}
999
1000pub fn burnup_to_dpa_clad(burnup_mwd_per_kgu: f64) -> f64 {
1004 burnup_mwd_per_kgu / 10.0
1005}
1006
1007pub fn fission_density(burnup_mwd_per_kgu: f64, uo2_density_kg_per_m3: f64) -> f64 {
1009 let u_fraction = 238.03 / (238.03 + 32.0); let mhm_per_m3 = uo2_density_kg_per_m3 * u_fraction;
1012 let burnup_j = burnup_mwd_per_kgu * 1e6 * 86400.0;
1014 mhm_per_m3 * burnup_j / 3.2e-11
1015}
1016
1017#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 #[test]
1026 fn test_zircaloy_clad_construction() {
1027 let clad = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1028 assert_eq!(clad.grade, ZircaloyGrade::Zircaloy4);
1029 assert_eq!(clad.oxide_thickness, 0.0);
1030 }
1031
1032 #[test]
1033 fn test_cubic_oxide_weight_gain_zero_time() {
1034 let clad = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1035 let wg = clad.oxide_weight_gain_cubic(0.0);
1036 assert!(wg.abs() < 1e-15, "zero time → zero weight gain");
1037 }
1038
1039 #[test]
1040 fn test_cubic_oxide_weight_gain_increases_with_time() {
1041 let clad = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1042 let wg1 = clad.oxide_weight_gain_cubic(1e6);
1043 let wg2 = clad.oxide_weight_gain_cubic(1e7);
1044 assert!(wg2 > wg1, "longer time should give greater weight gain");
1045 }
1046
1047 #[test]
1048 fn test_hydrogen_pickup_fraction_range() {
1049 let clad = ZircaloyClad::new(ZircaloyGrade::M5, 9.5e-3, 0.57e-3, 580.0);
1050 let f = clad.hydrogen_pickup_fraction();
1051 assert!(
1052 (0.0..=1.0).contains(&f),
1053 "pickup fraction must be [0,1]: {}",
1054 f
1055 );
1056 }
1057
1058 #[test]
1059 fn test_hydrogen_pickup_m5_lower_than_zry4() {
1060 let clad4 = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1061 let cladm5 = ZircaloyClad::new(ZircaloyGrade::M5, 9.5e-3, 0.57e-3, 600.0);
1062 assert!(
1063 cladm5.hydrogen_pickup_fraction() < clad4.hydrogen_pickup_fraction(),
1064 "M5 should have lower pickup than Zry-4"
1065 );
1066 }
1067
1068 #[test]
1069 fn test_yield_strength_increases_with_fluence() {
1070 let mut clad = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1071 let ys0 = clad.yield_strength();
1072 clad.fluence = 1e25;
1073 let ys1 = clad.yield_strength();
1074 assert!(ys1 > ys0, "irradiation should harden Zircaloy");
1075 }
1076
1077 #[test]
1078 fn test_irradiation_creep_rate_proportional_to_flux() {
1079 let clad = ZircaloyClad::new(ZircaloyGrade::Zircaloy4, 9.5e-3, 0.57e-3, 600.0);
1080 let r1 = clad.irradiation_creep_rate(100e6, 1e13);
1081 let r2 = clad.irradiation_creep_rate(100e6, 2e13);
1082 assert!(
1083 (r2 / r1 - 2.0).abs() < 1e-9,
1084 "creep rate should scale linearly with flux"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_uo2_thermal_conductivity_positive() {
1090 let pellet = Uo2Pellet::new(8.19e-3, 10e-3, 0.0315);
1091 let k = pellet.thermal_conductivity(900.0);
1092 assert!(k > 0.0, "thermal conductivity must be positive, got {}", k);
1093 }
1094
1095 #[test]
1096 fn test_uo2_thermal_conductivity_decreases_with_burnup() {
1097 let mut pellet = Uo2Pellet::new(8.19e-3, 10e-3, 0.0315);
1098 let k0 = pellet.thermal_conductivity(900.0);
1099 pellet.burnup = 50.0;
1100 let k50 = pellet.thermal_conductivity(900.0);
1101 assert!(k50 < k0, "conductivity should decrease with burnup");
1102 }
1103
1104 #[test]
1105 fn test_fission_gas_release_zero_at_t0() {
1106 let pellet = Uo2Pellet::new(8.19e-3, 10e-3, 0.0315);
1107 let fgr = pellet.fission_gas_release_booth(1200.0, 0.0);
1108 assert!(fgr.abs() < 1e-9, "FGR at t=0 must be 0");
1109 }
1110
1111 #[test]
1112 fn test_fission_gas_release_bounded() {
1113 let pellet = Uo2Pellet::new(8.19e-3, 10e-3, 0.0315);
1114 let fgr = pellet.fission_gas_release_booth(1400.0, 1e10);
1115 assert!((0.0..=1.0).contains(&fgr), "FGR must be in [0,1]: {}", fgr);
1116 }
1117
1118 #[test]
1119 fn test_pellet_swelling_increases_with_burnup() {
1120 let mut p = Uo2Pellet::new(8.19e-3, 10e-3, 0.0315);
1121 p.burnup = 0.0;
1122 let s0 = p.fission_product_swelling();
1123 p.burnup = 50.0;
1124 let s50 = p.fission_product_swelling();
1125 assert!(s50 > s0, "swelling increases with burnup");
1126 }
1127
1128 #[test]
1129 fn test_garner_void_swelling_below_incubation_zero() {
1130 let params = VoidSwellingParams::default();
1131 let s = garner_void_swelling(5.0, 773.0, ¶ms);
1132 assert_eq!(s, 0.0, "no swelling before incubation dose");
1133 }
1134
1135 #[test]
1136 fn test_garner_void_swelling_above_incubation_positive() {
1137 let params = VoidSwellingParams::default();
1138 let s = garner_void_swelling(50.0, 773.0, ¶ms);
1139 assert!(s > 0.0, "swelling should be positive above incubation dose");
1140 }
1141
1142 #[test]
1143 fn test_garner_void_swelling_peak_temperature() {
1144 let params = VoidSwellingParams::default();
1145 let s_peak = garner_void_swelling(50.0, params.peak_temperature_k, ¶ms);
1146 let s_off = garner_void_swelling(50.0, params.peak_temperature_k + 100.0, ¶ms);
1147 assert!(
1148 s_peak > s_off,
1149 "peak temperature should give maximum swelling"
1150 );
1151 }
1152
1153 #[test]
1154 fn test_void_number_density_zero_for_zero_swelling() {
1155 let n = void_number_density(0.0, 5e-9);
1156 assert_eq!(n, 0.0);
1157 }
1158
1159 #[test]
1160 fn test_irradiation_creep_additive() {
1161 let r = irradiation_creep_rate(200e6, 773.0, 1e-8, 3e-27, 1e-33, 5.0, 280000.0);
1162 assert!(r > 0.0, "creep rate must be positive");
1163 }
1164
1165 #[test]
1166 fn test_stress_relaxation_decreases_with_dose() {
1167 let sigma = irradiation_stress_relaxation(200e6, 0.01, 100.0);
1168 assert!(sigma < 200e6, "stress should relax with dose");
1169 assert!(sigma > 0.0, "stress must remain positive");
1170 }
1171
1172 #[test]
1173 fn test_ris_concentration_bounded() {
1174 let params = RisParams::default();
1175 let c = ris_grain_boundary_concentration(¶ms);
1176 assert!(
1177 (0.0..=1.0).contains(&c),
1178 "RIS concentration out of [0,1]: {}",
1179 c
1180 );
1181 }
1182
1183 #[test]
1184 fn test_rpv_charpy_use_drop_low_fluence() {
1185 let charpy = RpvCharpy::new(
1186 RpvSteelGrade::A508Class3,
1187 100.0,
1188 -20.0,
1189 1e17,
1190 0.05,
1191 0.7,
1192 0.01,
1193 );
1194 let drop = charpy.use_drop();
1195 assert_eq!(drop, 0.0, "no USE drop below fluence threshold");
1196 }
1197
1198 #[test]
1199 fn test_rpv_charpy_use_irradiated_non_negative() {
1200 let charpy = RpvCharpy::new(
1201 RpvSteelGrade::A533GradeB,
1202 100.0,
1203 -30.0,
1204 1e20,
1205 0.20,
1206 0.8,
1207 0.02,
1208 );
1209 assert!(
1210 charpy.use_irradiated() >= 0.0,
1211 "irradiated USE must be non-negative"
1212 );
1213 }
1214
1215 #[test]
1216 fn test_rpv_rtndt_shift_positive_copper() {
1217 let charpy = RpvCharpy::new(
1218 RpvSteelGrade::A508Class3,
1219 100.0,
1220 -20.0,
1221 5e19,
1222 0.20,
1223 0.7,
1224 0.01,
1225 );
1226 let shift = charpy.rtndt_shift();
1227 assert!(shift > 0.0, "RTNDT shift must be positive with high copper");
1228 }
1229
1230 #[test]
1231 fn test_fracture_toughness_increases_with_temperature() {
1232 let charpy = RpvCharpy::new(
1233 RpvSteelGrade::A508Class3,
1234 100.0,
1235 -20.0,
1236 1e19,
1237 0.1,
1238 0.7,
1239 0.01,
1240 );
1241 let kic_cold = charpy.fracture_toughness_kic(-100.0);
1242 let kic_warm = charpy.fracture_toughness_kic(50.0);
1243 assert!(
1244 kic_warm > kic_cold,
1245 "fracture toughness increases above RTNDT"
1246 );
1247 }
1248
1249 #[test]
1250 fn test_helium_coverage_fraction_bounded() {
1251 let he = HeliumEmbrittlement::new(1e16, 2e-9, 20e-6);
1252 let f = he.coverage_fraction();
1253 assert!(
1254 (0.0..=1.0).contains(&f),
1255 "coverage fraction out of [0,1]: {}",
1256 f
1257 );
1258 }
1259
1260 #[test]
1261 fn test_helium_strength_reduction_non_negative() {
1262 let he = HeliumEmbrittlement::new(1e16, 5e-9, 20e-6);
1263 let sr = he.strength_reduction_factor();
1264 assert!(
1265 sr >= 0.0,
1266 "strength reduction factor must be non-negative: {}",
1267 sr
1268 );
1269 }
1270
1271 #[test]
1272 fn test_graphite_conductivity_decreases_with_fluence() {
1273 let mut g = GraphiteBlock::new(GraphiteGrade::Ig110, 673.0);
1274 let k0 = g.thermal_conductivity_irradiated();
1275 g.fluence = 1e26;
1276 let k_hi = g.thermal_conductivity_irradiated();
1277 assert!(
1278 k_hi < k0,
1279 "graphite conductivity should decrease with fluence"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_graphite_wigner_zero_at_zero_fluence() {
1285 let g = GraphiteBlock::new(GraphiteGrade::PileGradeA, 400.0);
1286 let w = g.wigner_energy_j_per_kg();
1287 assert!(w.abs() < 1e-9, "zero fluence → zero Wigner energy");
1288 }
1289
1290 #[test]
1291 fn test_sfr_void_swelling_316_positive() {
1292 let mut mat = SfrMaterial::new(SfrSteelGrade::Ss316, 773.0);
1293 mat.dose_dpa = 50.0;
1294 let s = mat.void_swelling_pct();
1295 assert!(s >= 0.0, "void swelling must be non-negative");
1296 }
1297
1298 #[test]
1299 fn test_sfr_ht9_swelling_lower_than_316() {
1300 let mut mat_316 = SfrMaterial::new(SfrSteelGrade::Ss316, 773.0);
1301 let mut mat_ht9 = SfrMaterial::new(SfrSteelGrade::Ht9, 773.0);
1302 mat_316.dose_dpa = 100.0;
1303 mat_ht9.dose_dpa = 100.0;
1304 assert!(
1305 mat_ht9.void_swelling_pct() < mat_316.void_swelling_pct(),
1306 "HT-9 should swell less than 316 SS"
1307 );
1308 }
1309
1310 #[test]
1311 fn test_thermal_creep_rate_increases_with_temperature() {
1312 let mat_lo = SfrMaterial::new(SfrSteelGrade::Ss316, 700.0);
1313 let mat_hi = SfrMaterial::new(SfrSteelGrade::Ss316, 900.0);
1314 let r_lo = mat_lo.thermal_creep_rate(100.0);
1315 let r_hi = mat_hi.thermal_creep_rate(100.0);
1316 assert!(r_hi > r_lo, "creep rate should increase with temperature");
1317 }
1318
1319 #[test]
1320 fn test_fatigue_life_decreases_with_strain() {
1321 let mat = SfrMaterial::new(SfrSteelGrade::Ss316, 773.0);
1322 let n1 = mat.fatigue_life_cycles(0.001);
1323 let n2 = mat.fatigue_life_cycles(0.01);
1324 assert!(
1325 n1 > n2,
1326 "higher strain amplitude gives shorter fatigue life"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_decay_chain_parent_count_at_zero() {
1332 let dc = DecayChain::from_half_life(1e9, 1e20);
1333 assert!((dc.parent_count(0.0) - 1e20).abs() < 1.0);
1334 }
1335
1336 #[test]
1337 fn test_decay_chain_conservation() {
1338 let dc = DecayChain::from_half_life(3600.0, 1e20);
1339 let t = 3600.0 * 3.0;
1340 let total = dc.parent_count(t) + dc.daughter_count(t);
1341 assert!(
1342 (total - 1e20).abs() / 1e20 < 1e-9,
1343 "atom count must be conserved"
1344 );
1345 }
1346
1347 #[test]
1348 fn test_pci_contact_pressure_zero_gap() {
1349 let params = PciParams::default();
1350 let p = pci_contact_pressure(¶ms, 0.0);
1351 assert_eq!(p, 0.0, "no contact pressure when gap is open");
1352 }
1353
1354 #[test]
1355 fn test_pci_contact_pressure_positive_on_swelling() {
1356 let params = PciParams::default();
1357 let p = pci_contact_pressure(¶ms, 0.01);
1358 assert!(p >= 0.0, "contact pressure must be non-negative");
1359 }
1360
1361 #[test]
1362 fn test_burnup_to_dpa_scaling() {
1363 let dpa = burnup_to_dpa_clad(50.0);
1364 assert!((dpa - 5.0).abs() < 1e-9, "50 MWd/kgU → 5 dpa");
1365 }
1366
1367 #[test]
1368 fn test_fission_density_positive() {
1369 let fd = fission_density(50.0, 10400.0);
1370 assert!(fd > 0.0, "fission density must be positive");
1371 }
1372
1373 #[test]
1374 fn test_chemistry_factor_zero_low_cu() {
1375 let cf = chemistry_factor(0.05, 0.0);
1376 assert_eq!(cf, 0.0, "no CF for very low copper");
1377 }
1378}