1#![allow(dead_code)]
11
12#[derive(Debug, Clone)]
20pub struct ThermoMechanical {
21 pub youngs_modulus: f64,
23 pub poisson_ratio: f64,
25 pub cte: f64,
27 pub thermal_conductivity: f64,
29 pub density: f64,
31 pub specific_heat: f64,
33 pub t_ref: f64,
35}
36
37impl ThermoMechanical {
38 pub fn steel() -> Self {
40 Self {
41 youngs_modulus: 200e9,
42 poisson_ratio: 0.3,
43 cte: 12e-6,
44 thermal_conductivity: 50.0,
45 density: 7850.0,
46 specific_heat: 500.0,
47 t_ref: 293.15,
48 }
49 }
50
51 #[allow(clippy::too_many_arguments)]
53 pub fn new(
54 youngs_modulus: f64,
55 poisson_ratio: f64,
56 cte: f64,
57 thermal_conductivity: f64,
58 density: f64,
59 specific_heat: f64,
60 t_ref: f64,
61 ) -> Self {
62 Self {
63 youngs_modulus,
64 poisson_ratio,
65 cte,
66 thermal_conductivity,
67 density,
68 specific_heat,
69 t_ref,
70 }
71 }
72
73 pub fn thermal_strain(&self, temperature: f64) -> f64 {
75 self.cte * (temperature - self.t_ref)
76 }
77
78 pub fn thermal_stress_uniaxial(&self, temperature: f64) -> f64 {
80 -self.youngs_modulus * self.thermal_strain(temperature)
81 }
82
83 pub fn thermal_diffusivity(&self) -> f64 {
85 self.thermal_conductivity / (self.density * self.specific_heat)
86 }
87
88 pub fn lame_lambda(&self) -> f64 {
90 self.youngs_modulus * self.poisson_ratio
91 / ((1.0 + self.poisson_ratio) * (1.0 - 2.0 * self.poisson_ratio))
92 }
93
94 pub fn lame_mu(&self) -> f64 {
96 self.youngs_modulus / (2.0 * (1.0 + self.poisson_ratio))
97 }
98
99 pub fn hydrostatic_thermal_stress(&self, temperature: f64) -> f64 {
101 let k_bulk = self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio));
102 -3.0 * k_bulk * self.cte * (temperature - self.t_ref)
103 }
104
105 pub fn thermoelastic_dissipation(&self, strain_rate: f64, temperature: f64) -> f64 {
107 let k_bulk = self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio));
108 -3.0 * k_bulk * self.cte * temperature * strain_rate
109 }
110}
111
112#[derive(Debug, Clone)]
121pub struct PiezoElectric {
122 pub compliance: [f64; 4],
124 pub d_matrix: [f64; 3],
126 pub permittivity: [f64; 2],
128 pub name: String,
130}
131
132impl PiezoElectric {
133 pub fn pzt5h() -> Self {
135 Self {
136 compliance: [16.5e-12, -4.78e-12, 20.7e-12, 43.5e-12],
137 d_matrix: [-274e-12, 593e-12, 741e-12],
138 permittivity: [3130.0 * 8.854e-12, 3400.0 * 8.854e-12],
139 name: "PZT-5H".to_string(),
140 }
141 }
142
143 pub fn pvdf() -> Self {
145 Self {
146 compliance: [365e-12, -145e-12, 472e-12, 1600e-12],
147 d_matrix: [23e-12, -33e-12, -27e-12],
148 permittivity: [12.0 * 8.854e-12, 12.0 * 8.854e-12],
149 name: "PVDF".to_string(),
150 }
151 }
152
153 pub fn direct_effect_d33(&self, stress_33: f64) -> f64 {
155 self.d_matrix[1] * stress_33
156 }
157
158 pub fn converse_effect_d33(&self, e_field_3: f64) -> f64 {
160 self.d_matrix[1] * e_field_3
161 }
162
163 pub fn coupling_k33(&self) -> f64 {
165 let d33 = self.d_matrix[1];
166 let s33 = self.compliance[2];
167 let eps33 = self.permittivity[1];
168 d33 / (s33 * eps33).sqrt()
169 }
170
171 pub fn stiffened_modulus_33(&self) -> f64 {
173 let d33 = self.d_matrix[1];
174 let s33 = self.compliance[2];
175 let eps33 = self.permittivity[1];
176 (s33 - d33 * d33 / eps33).recip()
177 }
178}
179
180#[derive(Debug, Clone)]
188pub struct Magnetostrictive {
189 pub lambda_s: f64,
191 pub m_saturation: f64,
193 pub youngs_modulus: f64,
195 pub d_coeff: f64,
197 pub coercive_field: f64,
199}
200
201impl Magnetostrictive {
202 pub fn terfenol_d() -> Self {
204 Self {
205 lambda_s: 1750e-6,
206 m_saturation: 7.65e5,
207 youngs_modulus: 30e9,
208 d_coeff: 1.67e-8,
209 coercive_field: 10e3,
210 }
211 }
212
213 pub fn magnetostriction(&self, magnetization: f64) -> f64 {
215 let m_norm = (magnetization / self.m_saturation).clamp(-1.0, 1.0);
216 1.5 * self.lambda_s * m_norm * m_norm
217 }
218
219 pub fn villari_effect(&self, stress: f64, h_field: f64) -> f64 {
223 self.d_coeff * stress * h_field / self.coercive_field
225 }
226
227 pub fn preisach_hysteron(&self, h: f64, alpha: f64, beta: f64, state: bool) -> bool {
231 if h > alpha {
232 true
233 } else if h < beta {
234 false
235 } else {
236 state
237 }
238 }
239
240 pub fn magnetization_curve(&self, h_field: f64) -> f64 {
242 let a = self.coercive_field;
243 let x = h_field / a;
244 self.m_saturation * x.tanh()
246 }
247}
248
249#[derive(Debug, Clone)]
257pub struct ElectrochemicalMaterial {
258 pub exchange_current_density: f64,
260 pub alpha: f64,
262 pub diffusion_coefficient: f64,
264 pub partial_molar_volume: f64,
266 pub youngs_modulus: f64,
268 pub poisson_ratio: f64,
270 pub faraday: f64,
272}
273
274impl ElectrochemicalMaterial {
275 const F: f64 = 96485.0;
277 const R: f64 = 8.314;
279
280 pub fn lithium_graphite() -> Self {
282 Self {
283 exchange_current_density: 2.0,
284 alpha: 0.5,
285 diffusion_coefficient: 3.9e-14,
286 partial_molar_volume: 3.497e-6,
287 youngs_modulus: 10e9,
288 poisson_ratio: 0.3,
289 faraday: Self::F,
290 }
291 }
292
293 pub fn butler_volmer(&self, overpotential: f64, temperature: f64) -> f64 {
295 let f_rt = Self::F / (Self::R * temperature);
296 self.exchange_current_density
297 * ((self.alpha * f_rt * overpotential).exp()
298 - (-(1.0 - self.alpha) * f_rt * overpotential).exp())
299 }
300
301 pub fn diffusion_flux(&self, concentration_gradient: f64) -> f64 {
303 -self.diffusion_coefficient * concentration_gradient
304 }
305
306 pub fn intercalation_stress(&self, concentration: f64, c_avg: f64) -> f64 {
308 let prefactor =
309 self.partial_molar_volume * self.youngs_modulus / (3.0 * (1.0 - self.poisson_ratio));
310 -prefactor * (concentration - c_avg)
311 }
312
313 pub fn nernst_shift(&self, c_norm: f64, temperature: f64) -> f64 {
315 let c = c_norm.clamp(1e-10, 1.0 - 1e-10);
316 Self::R * temperature / Self::F * (c / (1.0 - c)).ln()
317 }
318}
319
320#[derive(Debug, Clone)]
328pub struct PoroElastic {
329 pub youngs_modulus: f64,
331 pub poisson_ratio: f64,
333 pub biot_coefficient: f64,
335 pub permeability: f64,
337 pub fluid_viscosity: f64,
339 pub biot_modulus: f64,
341}
342
343impl PoroElastic {
344 pub fn saturated_clay() -> Self {
346 Self {
347 youngs_modulus: 20e6,
348 poisson_ratio: 0.35,
349 biot_coefficient: 0.9,
350 permeability: 1e-13,
351 fluid_viscosity: 1e-3,
352 biot_modulus: 1e9,
353 }
354 }
355
356 pub fn sandstone() -> Self {
358 Self {
359 youngs_modulus: 15e9,
360 poisson_ratio: 0.25,
361 biot_coefficient: 0.7,
362 permeability: 1e-13,
363 fluid_viscosity: 1e-3,
364 biot_modulus: 20e9,
365 }
366 }
367
368 pub fn consolidation_coefficient(&self) -> f64 {
370 self.permeability * self.biot_modulus / self.fluid_viscosity
371 }
372
373 pub fn effective_stress(&self, total_stress: f64, pore_pressure: f64) -> f64 {
375 total_stress - self.biot_coefficient * pore_pressure
376 }
377
378 pub fn darcy_velocity(&self, pressure_gradient: f64) -> f64 {
380 -(self.permeability / self.fluid_viscosity) * pressure_gradient
381 }
382
383 pub fn skempton_coefficient(&self) -> f64 {
387 let k_bulk = self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio));
388 let b = self.biot_coefficient;
389 let m = self.biot_modulus;
390 b * m / (k_bulk + b * b * m)
391 }
392}
393
394#[derive(Debug, Clone)]
400pub struct SwellingMaterial {
401 pub hygroscopic_coeff: f64,
403 pub osmotic_coeff: f64,
405 pub crosslink_density: f64,
407 pub flory_chi: f64,
409 pub temperature: f64,
411}
412
413impl SwellingMaterial {
414 const R: f64 = 8.314;
416
417 pub fn hydrogel() -> Self {
419 Self {
420 hygroscopic_coeff: 0.002,
421 osmotic_coeff: 2479.0, crosslink_density: 100.0,
423 flory_chi: 0.5,
424 temperature: 298.15,
425 }
426 }
427
428 pub fn hygroscopic_strain(&self, delta_rh: f64) -> f64 {
430 self.hygroscopic_coeff * delta_rh
431 }
432
433 pub fn osmotic_pressure(&self, concentration: f64) -> f64 {
435 concentration * Self::R * self.temperature
436 }
437
438 pub fn flory_rehner_elastic_energy(&self, swelling_ratio: f64) -> f64 {
442 let nu = self.crosslink_density;
443 let kt = Self::R * self.temperature / 6.022e23;
444 1.5 * nu * kt * (swelling_ratio * swelling_ratio - 1.0 - swelling_ratio.ln())
445 }
446
447 pub fn swelling_equilibrium_condition(&self, phi: f64) -> f64 {
449 let phi = phi.clamp(1e-5, 1.0 - 1e-5);
451 let mixing = (1.0 - phi).ln() + phi + self.flory_chi * phi * phi;
452 let elastic = self.crosslink_density * (phi.powf(1.0 / 3.0) - 0.5 * phi);
453 mixing + elastic
454 }
455}
456
457#[derive(Debug, Clone)]
465pub struct RadiationDamage {
466 pub threshold_energy_ev: f64,
468 pub atomic_density: f64,
470 pub yield_strength_initial: f64,
472 pub dpa: f64,
474 pub void_fraction: f64,
476 pub helium_conc_appm: f64,
478}
479
480impl RadiationDamage {
481 pub fn ss316l() -> Self {
483 Self {
484 threshold_energy_ev: 40.0,
485 atomic_density: 8.5e28,
486 yield_strength_initial: 220e6,
487 dpa: 0.0,
488 void_fraction: 0.0,
489 helium_conc_appm: 0.0,
490 }
491 }
492
493 pub fn compute_dpa(&self, flux: f64, cross_section: f64, time: f64) -> f64 {
497 flux * cross_section * time
498 / (self.atomic_density * 2.0 * self.threshold_energy_ev * 1.6e-19)
499 }
500
501 pub fn irradiation_hardening(&self, k_hardening: f64) -> f64 {
505 k_hardening * self.dpa.sqrt()
506 }
507
508 pub fn yield_strength(&self, k_hardening: f64) -> f64 {
510 self.yield_strength_initial + self.irradiation_hardening(k_hardening)
511 }
512
513 pub fn void_swelling_rate(&self, temperature_k: f64, a_coeff: f64, b_coeff: f64) -> f64 {
515 a_coeff * (-b_coeff / temperature_k).exp()
516 }
517
518 pub fn bubble_radius(&self, bubble_density: f64) -> f64 {
520 let c_he = self.helium_conc_appm * 1e-6 * self.atomic_density;
521 (3.0 * c_he / (4.0 * std::f64::consts::PI * bubble_density)).cbrt()
522 }
523
524 pub fn dbtt_shift_k(&self) -> f64 {
526 20.0 * self.dpa
527 }
528}
529
530#[derive(Debug, Clone)]
538pub struct PhaseTransformMaterial {
539 pub ea: f64,
541 pub em: f64,
543 pub max_strain: f64,
545 pub as_temp: f64,
547 pub af_temp: f64,
549 pub ms_temp: f64,
551 pub mf_temp: f64,
553 pub xi: f64,
555}
556
557impl PhaseTransformMaterial {
558 pub fn nitinol() -> Self {
560 Self {
561 ea: 75e9,
562 em: 30e9,
563 max_strain: 0.08,
564 as_temp: 291.0,
565 af_temp: 307.0,
566 ms_temp: 291.0,
567 mf_temp: 271.0,
568 xi: 0.0,
569 }
570 }
571
572 pub fn effective_modulus(&self) -> f64 {
574 self.ea + self.xi * (self.em - self.ea)
575 }
576
577 pub fn martensite_fraction_cooling(&self, temperature: f64) -> f64 {
579 if temperature >= self.ms_temp {
580 0.0
581 } else if temperature <= self.mf_temp {
582 1.0
583 } else {
584 let b_m = std::f64::consts::PI / (self.ms_temp - self.mf_temp);
585 0.5 * (1.0 - (b_m * (temperature - (self.ms_temp + self.mf_temp) / 2.0)).cos())
586 }
587 }
588
589 pub fn martensite_fraction_heating(&self, temperature: f64) -> f64 {
591 if temperature >= self.af_temp {
592 0.0
593 } else if temperature <= self.as_temp {
594 self.xi } else {
596 let b_a = std::f64::consts::PI / (self.af_temp - self.as_temp);
597 self.xi * 0.5 * (1.0 + (b_a * (temperature - self.as_temp)).cos())
598 }
599 }
600
601 pub fn transformation_stress(&self, strain: f64) -> f64 {
603 let eps = strain.clamp(0.0, self.max_strain);
604 let xi_new = eps / self.max_strain;
605 let e_eff = self.ea + xi_new * (self.em - self.ea);
606 e_eff * (eps - xi_new * self.max_strain)
607 }
608}
609
610#[derive(Debug, Clone)]
616pub struct ElectroMagnetic {
617 pub rel_permeability: f64,
619 pub conductivity: f64,
621 pub steinmetz_k_h: f64,
623 pub steinmetz_n: f64,
625 pub eddy_current_k_e: f64,
627 pub frequency: f64,
629}
630
631impl ElectroMagnetic {
632 const MU0: f64 = 1.256_637_062e-6;
634
635 pub fn silicon_steel() -> Self {
637 Self {
638 rel_permeability: 5000.0,
639 conductivity: 2e6,
640 steinmetz_k_h: 40.0,
641 steinmetz_n: 1.8,
642 eddy_current_k_e: 0.5,
643 frequency: 50.0,
644 }
645 }
646
647 pub fn permeability(&self) -> f64 {
649 Self::MU0 * self.rel_permeability
650 }
651
652 pub fn skin_depth(&self) -> f64 {
654 let omega = 2.0 * std::f64::consts::PI * self.frequency;
655 (2.0 / (omega * self.conductivity * self.permeability())).sqrt()
656 }
657
658 pub fn hysteresis_loss(&self, b_max: f64) -> f64 {
660 self.steinmetz_k_h * self.frequency * b_max.powf(self.steinmetz_n)
661 }
662
663 pub fn eddy_current_loss(&self, b_max: f64) -> f64 {
665 self.eddy_current_k_e * self.frequency.powi(2) * b_max.powi(2)
666 }
667
668 pub fn total_core_loss(&self, b_max: f64) -> f64 {
670 self.hysteresis_loss(b_max) + self.eddy_current_loss(b_max)
671 }
672
673 pub fn inductance_per_length(&self, n_turns_per_m: f64, area_m2: f64) -> f64 {
675 self.permeability() * n_turns_per_m * n_turns_per_m * area_m2
676 }
677}
678
679pub struct CoupledSolver {
687 pub tolerance: f64,
689 pub max_iter: usize,
691 pub relaxation: f64,
693}
694
695impl CoupledSolver {
696 pub fn new(tolerance: f64, max_iter: usize, relaxation: f64) -> Self {
698 Self {
699 tolerance,
700 max_iter,
701 relaxation,
702 }
703 }
704
705 pub fn operator_split<FA, FB>(
710 &self,
711 a0: f64,
712 b0: f64,
713 solve_a: FA,
714 solve_b: FB,
715 ) -> (f64, f64, usize)
716 where
717 FA: Fn(f64) -> f64,
718 FB: Fn(f64) -> f64,
719 {
720 let mut a = a0;
721 let mut b = b0;
722 for iter in 0..self.max_iter {
723 let a_new = solve_a(b);
724 let b_new = solve_b(a_new);
725 let err = ((a_new - a).powi(2) + (b_new - b).powi(2)).sqrt();
726 a = a * (1.0 - self.relaxation) + a_new * self.relaxation;
727 b = b * (1.0 - self.relaxation) + b_new * self.relaxation;
728 if err < self.tolerance {
729 return (a, b, iter + 1);
730 }
731 }
732 (a, b, self.max_iter)
733 }
734
735 pub fn staggered<F>(&self, state0: &[f64], step_fn: F) -> (Vec<f64>, usize)
739 where
740 F: Fn(&[f64]) -> Vec<f64>,
741 {
742 let mut state = state0.to_vec();
743 for iter in 0..self.max_iter {
744 let state_new = step_fn(&state);
745 let err = state
746 .iter()
747 .zip(state_new.iter())
748 .map(|(a, b)| (a - b).powi(2))
749 .sum::<f64>()
750 .sqrt();
751 let state_relaxed: Vec<f64> = state
753 .iter()
754 .zip(state_new.iter())
755 .map(|(a, b)| a * (1.0 - self.relaxation) + b * self.relaxation)
756 .collect();
757 state = state_relaxed;
758 if err < self.tolerance {
759 return (state, iter + 1);
760 }
761 }
762 (state, self.max_iter)
763 }
764
765 pub fn monolithic_newton<F>(&self, x0: &[f64], residual: F) -> (Vec<f64>, usize)
769 where
770 F: Fn(&[f64]) -> Vec<f64>,
771 {
772 let n = x0.len();
773 let mut x = x0.to_vec();
774 let eps = 1e-7;
775
776 for iter in 0..self.max_iter {
777 let r = residual(&x);
778 let r_norm = r.iter().map(|v| v * v).sum::<f64>().sqrt();
779 if r_norm < self.tolerance {
780 return (x, iter);
781 }
782 let mut j = vec![0.0_f64; n * n];
784 for j_col in 0..n {
785 let mut x_pert = x.clone();
786 x_pert[j_col] += eps;
787 let r_pert = residual(&x_pert);
788 for i in 0..n {
789 j[i * n + j_col] = (r_pert[i] - r[i]) / eps;
790 }
791 }
792 let dx = self.gaussian_solve(&j, &r.iter().map(|&v| -v).collect::<Vec<_>>(), n);
794 for i in 0..n {
795 x[i] += self.relaxation * dx[i];
796 }
797 }
798 (x, self.max_iter)
799 }
800
801 fn gaussian_solve(&self, a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
803 let mut aug: Vec<f64> = (0..n)
804 .flat_map(|i| {
805 let mut row: Vec<f64> = a[i * n..(i + 1) * n].to_vec();
806 row.push(b[i]);
807 row
808 })
809 .collect();
810
811 for col in 0..n {
812 let mut max_row = col;
814 let mut max_val = aug[col * (n + 1) + col].abs();
815 for row in col + 1..n {
816 let v = aug[row * (n + 1) + col].abs();
817 if v > max_val {
818 max_val = v;
819 max_row = row;
820 }
821 }
822 for j in 0..=n {
824 aug.swap(col * (n + 1) + j, max_row * (n + 1) + j);
825 }
826 let pivot = aug[col * (n + 1) + col];
827 if pivot.abs() < 1e-15 {
828 continue;
829 }
830 for row in col + 1..n {
831 let factor = aug[row * (n + 1) + col] / pivot;
832 for j in col..=n {
833 let v = aug[col * (n + 1) + j];
834 aug[row * (n + 1) + j] -= factor * v;
835 }
836 }
837 }
838
839 let mut x = vec![0.0_f64; n];
841 for i in (0..n).rev() {
842 let mut sum = aug[i * (n + 1) + n];
843 for j in i + 1..n {
844 sum -= aug[i * (n + 1) + j] * x[j];
845 }
846 let diag = aug[i * (n + 1) + i];
847 x[i] = if diag.abs() > 1e-15 { sum / diag } else { 0.0 };
848 }
849 x
850 }
851}
852
853#[cfg(test)]
858mod tests {
859 use super::*;
860
861 #[test]
862 fn test_thermo_mechanical_thermal_strain() {
863 let mat = ThermoMechanical::steel();
864 let eps = mat.thermal_strain(393.15); assert!((eps - 12e-6 * 100.0).abs() < 1e-15);
866 }
867
868 #[test]
869 fn test_thermo_mechanical_diffusivity() {
870 let mat = ThermoMechanical::steel();
871 let diff = mat.thermal_diffusivity();
872 assert!((diff - 50.0 / (7850.0 * 500.0)).abs() < 1e-15);
873 }
874
875 #[test]
876 fn test_thermo_mechanical_lame() {
877 let mat = ThermoMechanical::steel();
878 assert!(mat.lame_lambda() > 0.0);
879 assert!(mat.lame_mu() > 0.0);
880 }
881
882 #[test]
883 fn test_thermo_mechanical_hydrostatic_stress() {
884 let mat = ThermoMechanical::steel();
885 let sigma = mat.hydrostatic_thermal_stress(393.15);
886 assert!(sigma < 0.0); }
888
889 #[test]
890 fn test_piezo_pzt5h_coupling() {
891 let pz = PiezoElectric::pzt5h();
892 let k33 = pz.coupling_k33();
893 assert!(k33 > 0.0 && k33 < 1.0);
894 }
895
896 #[test]
897 fn test_piezo_direct_effect() {
898 let pz = PiezoElectric::pzt5h();
899 let d = pz.direct_effect_d33(1e6); assert!(d.abs() > 0.0);
901 }
902
903 #[test]
904 fn test_piezo_converse_effect() {
905 let pz = PiezoElectric::pzt5h();
906 let s = pz.converse_effect_d33(1e6); assert!(s.abs() > 0.0);
908 }
909
910 #[test]
911 fn test_piezo_pvdf_name() {
912 let pz = PiezoElectric::pvdf();
913 assert_eq!(pz.name, "PVDF");
914 }
915
916 #[test]
917 fn test_magnetostrictive_magnetostriction() {
918 let ms = Magnetostrictive::terfenol_d();
919 let eps = ms.magnetostriction(ms.m_saturation);
920 assert!((eps - 1.5 * ms.lambda_s).abs() < 1e-15);
921 }
922
923 #[test]
924 fn test_magnetostrictive_curve() {
925 let ms = Magnetostrictive::terfenol_d();
926 let m = ms.magnetization_curve(0.0);
927 assert!(m.abs() < 1e-10); }
929
930 #[test]
931 fn test_electrochemical_butler_volmer() {
932 let ec = ElectrochemicalMaterial::lithium_graphite();
933 let j = ec.butler_volmer(0.0, 298.0);
934 assert!(j.abs() < 1e-10); }
936
937 #[test]
938 fn test_electrochemical_butler_volmer_positive_eta() {
939 let ec = ElectrochemicalMaterial::lithium_graphite();
940 let j = ec.butler_volmer(0.05, 298.0);
941 assert!(j > 0.0); }
943
944 #[test]
945 fn test_electrochemical_intercalation_stress() {
946 let ec = ElectrochemicalMaterial::lithium_graphite();
947 let sigma = ec.intercalation_stress(1000.0, 800.0);
948 assert!(sigma < 0.0); }
950
951 #[test]
952 fn test_poroelastic_consolidation_coeff() {
953 let pe = PoroElastic::saturated_clay();
954 let cv = pe.consolidation_coefficient();
955 assert!(cv > 0.0);
956 }
957
958 #[test]
959 fn test_poroelastic_effective_stress() {
960 let pe = PoroElastic::saturated_clay();
961 let sigma_eff = pe.effective_stress(-100e3, 50e3);
962 assert!((sigma_eff - (-100e3 - 0.9 * 50e3)).abs() < 1.0);
964 }
965
966 #[test]
967 fn test_poroelastic_darcy_velocity() {
968 let pe = PoroElastic::saturated_clay();
969 let q = pe.darcy_velocity(1000.0); assert!(q < 0.0); }
972
973 #[test]
974 fn test_swelling_hygroscopic_strain() {
975 let sw = SwellingMaterial::hydrogel();
976 let eps = sw.hygroscopic_strain(0.5); assert!((eps - 0.002 * 0.5).abs() < 1e-15);
978 }
979
980 #[test]
981 fn test_swelling_osmotic_pressure() {
982 let sw = SwellingMaterial::hydrogel();
983 let pi = sw.osmotic_pressure(1.0); assert!((pi - 1.0 * 8.314 * 298.15).abs() < 1.0);
985 }
986
987 #[test]
988 fn test_radiation_damage_dpa() {
989 let rd = RadiationDamage::ss316l();
990 let dpa = rd.compute_dpa(1e15, 1e-28, 3.15e7); assert!(dpa > 0.0);
992 }
993
994 #[test]
995 fn test_radiation_damage_hardening() {
996 let mut rd = RadiationDamage::ss316l();
997 rd.dpa = 10.0;
998 let delta_sy = rd.irradiation_hardening(100e6);
999 assert!((delta_sy - 100e6 * 10.0_f64.sqrt()).abs() < 1.0);
1000 }
1001
1002 #[test]
1003 fn test_radiation_damage_dbtt() {
1004 let mut rd = RadiationDamage::ss316l();
1005 rd.dpa = 5.0;
1006 let shift = rd.dbtt_shift_k();
1007 assert!((shift - 100.0).abs() < 1e-10);
1008 }
1009
1010 #[test]
1011 fn test_phase_transform_martensite_fraction() {
1012 let sma = PhaseTransformMaterial::nitinol();
1013 let xi = sma.martensite_fraction_cooling(261.0); assert!((xi - 1.0).abs() < 1e-10);
1015 let xi2 = sma.martensite_fraction_cooling(310.0); assert!(xi2.abs() < 1e-10);
1017 }
1018
1019 #[test]
1020 fn test_phase_transform_effective_modulus() {
1021 let mut sma = PhaseTransformMaterial::nitinol();
1022 sma.xi = 0.0;
1023 assert!((sma.effective_modulus() - sma.ea).abs() < 1.0);
1024 sma.xi = 1.0;
1025 assert!((sma.effective_modulus() - sma.em).abs() < 1.0);
1026 }
1027
1028 #[test]
1029 fn test_electromagnetic_skin_depth() {
1030 let em = ElectroMagnetic::silicon_steel();
1031 let delta = em.skin_depth();
1032 assert!(delta > 0.0 && delta < 1.0); }
1034
1035 #[test]
1036 fn test_electromagnetic_hysteresis_loss() {
1037 let em = ElectroMagnetic::silicon_steel();
1038 let ph = em.hysteresis_loss(1.5); assert!(ph > 0.0);
1040 }
1041
1042 #[test]
1043 fn test_electromagnetic_total_loss_increases_with_b() {
1044 let em = ElectroMagnetic::silicon_steel();
1045 let p1 = em.total_core_loss(1.0);
1046 let p2 = em.total_core_loss(1.5);
1047 assert!(p2 > p1);
1048 }
1049
1050 #[test]
1051 fn test_coupled_solver_operator_split() {
1052 let solver = CoupledSolver::new(1e-6, 100, 1.0);
1053 let (a, b, _iter) = solver.operator_split(0.0, 0.0, |b| b + 1.0, |a| a - 1.0);
1055 assert!((a - b - 1.0).abs() < 1e-5);
1056 }
1057
1058 #[test]
1059 fn test_coupled_solver_staggered() {
1060 let solver = CoupledSolver::new(1e-8, 100, 0.5);
1061 let (state, _iter) = solver.staggered(&[1.0, 1.0], |s| vec![s[0] * 0.5, s[1] * 0.5]);
1062 assert!(state[0].abs() < 0.01);
1064 assert!(state[1].abs() < 0.01);
1065 }
1066
1067 #[test]
1068 fn test_coupled_solver_newton_linear() {
1069 let solver = CoupledSolver::new(1e-10, 20, 1.0);
1070 let (x, _) = solver.monolithic_newton(&[0.0], |s| vec![s[0] - 3.0]);
1072 assert!((x[0] - 3.0).abs() < 1e-6);
1073 }
1074}