1#![allow(dead_code)]
12#![allow(clippy::too_many_arguments)]
13
14use std::f64::consts::PI;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum CellType {
23 Open,
25 Closed,
27 Mixed,
29}
30
31#[derive(Debug, Clone, PartialEq)]
40pub struct GibsonAshby {
41 pub rho_solid: f64,
43 pub e_solid: f64,
45 pub sigma_solid: f64,
47 pub k_solid: f64,
49 pub cell_type: CellType,
51 pub closed_fraction: f64,
53}
54
55impl GibsonAshby {
56 pub fn new(
58 rho_solid: f64,
59 e_solid: f64,
60 sigma_solid: f64,
61 k_solid: f64,
62 cell_type: CellType,
63 closed_fraction: f64,
64 ) -> Self {
65 Self {
66 rho_solid,
67 e_solid,
68 sigma_solid,
69 k_solid,
70 cell_type,
71 closed_fraction,
72 }
73 }
74
75 pub fn relative_density(&self, rho_foam: f64) -> f64 {
77 rho_foam / self.rho_solid
78 }
79
80 pub fn modulus_open(&self, rho_foam: f64) -> f64 {
84 let rd = self.relative_density(rho_foam);
85 self.e_solid * rd * rd
86 }
87
88 pub fn modulus_closed(&self, rho_foam: f64, phi_edge: f64) -> f64 {
93 let rd = self.relative_density(rho_foam);
94 self.e_solid * (phi_edge * phi_edge * rd * rd + (1.0 - phi_edge) * rd)
95 }
96
97 pub fn effective_modulus(&self, rho_foam: f64) -> f64 {
99 match self.cell_type {
100 CellType::Open => self.modulus_open(rho_foam),
101 CellType::Closed => self.modulus_closed(rho_foam, 0.6),
102 CellType::Mixed => {
103 let e_open = self.modulus_open(rho_foam);
104 let e_closed = self.modulus_closed(rho_foam, 0.6);
105 let f = self.closed_fraction;
106 f * e_closed + (1.0 - f) * e_open
107 }
108 }
109 }
110
111 pub fn strength_open(&self, rho_foam: f64) -> f64 {
115 let rd = self.relative_density(rho_foam);
116 0.3 * self.sigma_solid * rd.powf(1.5)
117 }
118
119 pub fn strength_closed(&self, rho_foam: f64, phi_edge: f64) -> f64 {
123 let rd = self.relative_density(rho_foam);
124 self.sigma_solid * (0.3 * (phi_edge * rd).powf(1.5) + (1.0 - phi_edge) * rd)
125 }
126
127 pub fn effective_strength(&self, rho_foam: f64) -> f64 {
129 match self.cell_type {
130 CellType::Open => self.strength_open(rho_foam),
131 CellType::Closed => self.strength_closed(rho_foam, 0.6),
132 CellType::Mixed => {
133 let s_open = self.strength_open(rho_foam);
134 let s_closed = self.strength_closed(rho_foam, 0.6);
135 let f = self.closed_fraction;
136 f * s_closed + (1.0 - f) * s_open
137 }
138 }
139 }
140
141 pub fn thermal_conductivity_open(&self, rho_foam: f64) -> f64 {
145 let rd = self.relative_density(rho_foam);
146 self.k_solid * rd / 3.0
147 }
148
149 pub fn poisson_ratio_open(&self) -> f64 {
153 1.0 / 3.0
154 }
155
156 pub fn fracture_toughness_open(&self, rho_foam: f64, cell_size: f64) -> f64 {
161 let rd = self.relative_density(rho_foam);
162 0.65 * self.sigma_solid * cell_size.sqrt() * rd.powf(1.5)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
175pub struct KelvinCell {
176 pub edge_length: f64,
178 pub wall_thickness: f64,
180 pub rho_solid: f64,
182 pub e_solid: f64,
184}
185
186impl KelvinCell {
187 pub fn new(edge_length: f64, wall_thickness: f64, rho_solid: f64, e_solid: f64) -> Self {
189 Self {
190 edge_length,
191 wall_thickness,
192 rho_solid,
193 e_solid,
194 }
195 }
196
197 pub fn cell_volume(&self) -> f64 {
201 8.0 * 2.0_f64.sqrt() * self.edge_length.powi(3)
202 }
203
204 pub fn cell_surface_area(&self) -> f64 {
208 (6.0 + 12.0 * 3.0_f64.sqrt()) * self.edge_length.powi(2)
209 }
210
211 pub fn num_faces(&self) -> usize {
213 14
214 }
215
216 pub fn num_edges(&self) -> usize {
218 36
219 }
220
221 pub fn num_vertices(&self) -> usize {
223 24
224 }
225
226 pub fn relative_density(&self) -> f64 {
230 let a = self.cell_surface_area();
231 let v = self.cell_volume();
232 (a * self.wall_thickness / v).min(1.0)
233 }
234
235 pub fn foam_density(&self) -> f64 {
237 self.rho_solid * self.relative_density()
238 }
239
240 pub fn effective_modulus(&self) -> f64 {
242 let rd = self.relative_density();
243 self.e_solid * rd * rd
244 }
245
246 pub fn equivalent_diameter(&self) -> f64 {
248 let v = self.cell_volume();
249 (6.0 * v / PI).powf(1.0 / 3.0)
250 }
251
252 pub fn strut_area(&self, relative_density: f64) -> f64 {
257 let v = self.cell_volume();
258 relative_density * v / (self.num_edges() as f64 * self.edge_length)
259 }
260}
261
262#[derive(Debug, Clone, PartialEq)]
271pub struct EnergyAbsorption {
272 pub plateau_stress: f64,
274 pub densification_strain: f64,
276 pub elastic_modulus: f64,
278 pub yield_strain: f64,
280 pub foam_density: f64,
282 pub thickness: f64,
284}
285
286impl EnergyAbsorption {
287 pub fn new(
289 plateau_stress: f64,
290 densification_strain: f64,
291 elastic_modulus: f64,
292 yield_strain: f64,
293 foam_density: f64,
294 thickness: f64,
295 ) -> Self {
296 Self {
297 plateau_stress,
298 densification_strain,
299 elastic_modulus,
300 yield_strain,
301 foam_density,
302 thickness,
303 }
304 }
305
306 pub fn stress_at_strain(&self, strain: f64) -> f64 {
308 if strain < 0.0 {
309 return 0.0;
310 }
311 if strain <= self.yield_strain {
312 self.elastic_modulus * strain
314 } else if strain <= self.densification_strain {
315 self.plateau_stress
317 } else {
318 let excess = strain - self.densification_strain;
320 let stiffening_factor = 10.0; self.plateau_stress * (1.0 + stiffening_factor * excess * excess)
322 }
323 }
324
325 pub fn energy_density(&self, strain: f64) -> f64 {
329 if strain <= 0.0 {
330 return 0.0;
331 }
332 if strain <= self.yield_strain {
333 0.5 * self.elastic_modulus * strain * strain
335 } else if strain <= self.densification_strain {
336 let elastic_energy = 0.5 * self.elastic_modulus * self.yield_strain * self.yield_strain;
338 let plateau_energy = self.plateau_stress * (strain - self.yield_strain);
339 elastic_energy + plateau_energy
340 } else {
341 let elastic_energy = 0.5 * self.elastic_modulus * self.yield_strain * self.yield_strain;
343 let plateau_energy =
344 self.plateau_stress * (self.densification_strain - self.yield_strain);
345 let excess = strain - self.densification_strain;
346 let k = 10.0;
347 let densification_energy = self.plateau_stress * (excess + k * excess.powi(3) / 3.0);
349 elastic_energy + plateau_energy + densification_energy
350 }
351 }
352
353 pub fn plateau_energy_density(&self) -> f64 {
355 self.energy_density(self.densification_strain)
356 }
357
358 pub fn efficiency(&self, strain: f64) -> f64 {
362 if strain <= 1.0e-12 {
363 return 0.0;
364 }
365 let w = self.energy_density(strain);
366 let sigma = self.stress_at_strain(strain);
367 if sigma.abs() < 1.0e-30 {
368 return 0.0;
369 }
370 w / (sigma * strain)
371 }
372
373 pub fn ideal_efficiency(&self) -> f64 {
377 let w = self.plateau_energy_density();
379 let sigma_max = self.stress_at_strain(self.densification_strain);
380 if sigma_max.abs() < 1.0e-30 {
381 return 0.0;
382 }
383 w / (sigma_max * self.densification_strain)
384 }
385
386 pub fn specific_energy(&self, strain: f64) -> f64 {
388 if self.foam_density.abs() < 1.0e-30 {
389 return 0.0;
390 }
391 self.energy_density(strain) / self.foam_density
392 }
393
394 pub fn peak_g_force(&self, drop_height: f64) -> f64 {
399 if self.foam_density.abs() < 1.0e-30 || self.thickness.abs() < 1.0e-30 {
400 return 0.0;
401 }
402 self.plateau_stress / (self.foam_density * drop_height)
403 }
404
405 pub fn cushion_factor(&self, strain: f64) -> f64 {
407 let w = self.energy_density(strain);
408 if w.abs() < 1.0e-30 {
409 return 0.0;
410 }
411 self.stress_at_strain(strain) / w
412 }
413}
414
415#[derive(Debug, Clone, PartialEq)]
421pub struct PronyTerm {
422 pub g_i: f64,
424 pub tau_i: f64,
426}
427
428#[derive(Debug, Clone, PartialEq)]
433pub struct ViscoelasticFoam {
434 pub e_instantaneous: f64,
436 pub e_equilibrium: f64,
438 pub prony_terms: Vec<PronyTerm>,
440 pub density: f64,
442 pub poisson_ratio: f64,
444}
445
446impl ViscoelasticFoam {
447 pub fn new(
449 e_instantaneous: f64,
450 e_equilibrium: f64,
451 prony_terms: Vec<PronyTerm>,
452 density: f64,
453 poisson_ratio: f64,
454 ) -> Self {
455 Self {
456 e_instantaneous,
457 e_equilibrium,
458 prony_terms,
459 density,
460 poisson_ratio,
461 }
462 }
463
464 pub fn single_term(e_inst: f64, e_eq: f64, tau: f64, density: f64) -> Self {
466 let g = (e_inst - e_eq) / e_inst;
467 Self::new(
468 e_inst,
469 e_eq,
470 vec![PronyTerm { g_i: g, tau_i: tau }],
471 density,
472 0.0,
473 )
474 }
475
476 pub fn relaxation_modulus(&self, t: f64) -> f64 {
480 let mut e = self.e_equilibrium;
481 for term in &self.prony_terms {
482 e += self.e_instantaneous * term.g_i * (-t / term.tau_i).exp();
483 }
484 e
485 }
486
487 pub fn stress_constant_rate(&self, strain_rate: f64, t: f64) -> f64 {
492 let mut sigma = self.e_equilibrium * strain_rate * t;
494 for term in &self.prony_terms {
495 let tau = term.tau_i;
496 let g = term.g_i;
497 sigma += strain_rate * self.e_instantaneous * g * tau * (1.0 - (-t / tau).exp());
500 }
501 sigma
502 }
503
504 pub fn creep_compliance(&self, t: f64) -> f64 {
508 let e = self.relaxation_modulus(t);
509 if e.abs() < 1.0e-30 {
510 return f64::INFINITY;
511 }
512 1.0 / e
513 }
514
515 pub fn loss_tangent(&self, omega: f64) -> f64 {
519 let (e_storage, e_loss) = self.dynamic_moduli(omega);
520 if e_storage.abs() < 1.0e-30 {
521 return 0.0;
522 }
523 e_loss / e_storage
524 }
525
526 pub fn dynamic_moduli(&self, omega: f64) -> (f64, f64) {
531 let mut e_storage = self.e_equilibrium;
532 let mut e_loss = 0.0;
533 for term in &self.prony_terms {
534 let wt = omega * term.tau_i;
535 let wt2 = wt * wt;
536 let denom = 1.0 + wt2;
537 e_storage += self.e_instantaneous * term.g_i * wt2 / denom;
538 e_loss += self.e_instantaneous * term.g_i * wt / denom;
539 }
540 (e_storage, e_loss)
541 }
542
543 pub fn relaxation_half_time(&self) -> f64 {
547 let max_tau = self
548 .prony_terms
549 .iter()
550 .map(|t| t.tau_i)
551 .fold(0.0_f64, f64::max);
552 max_tau * 2.0_f64.ln()
553 }
554
555 pub fn bulk_modulus(&self, t: f64) -> f64 {
557 let e = self.relaxation_modulus(t);
558 let nu = self.poisson_ratio;
559 e / (3.0 * (1.0 - 2.0 * nu))
560 }
561
562 pub fn shear_modulus(&self, t: f64) -> f64 {
564 let e = self.relaxation_modulus(t);
565 let nu = self.poisson_ratio;
566 e / (2.0 * (1.0 + nu))
567 }
568}
569
570#[derive(Debug, Clone, PartialEq)]
579pub struct CrushableFoam {
580 pub sigma_c0: f64,
582 pub p_c0: f64,
584 pub tension_cutoff_ratio: f64,
586 pub hardening: Vec<(f64, f64)>,
588 pub elastic_modulus: f64,
590 pub poisson_ratio: f64,
592}
593
594impl CrushableFoam {
595 pub fn new(
597 sigma_c0: f64,
598 p_c0: f64,
599 tension_cutoff_ratio: f64,
600 elastic_modulus: f64,
601 poisson_ratio: f64,
602 ) -> Self {
603 Self {
604 sigma_c0,
605 p_c0,
606 tension_cutoff_ratio,
607 hardening: Vec::new(),
608 elastic_modulus,
609 poisson_ratio,
610 }
611 }
612
613 pub fn add_hardening_point(&mut self, plastic_vol_strain: f64, yield_stress: f64) {
615 self.hardening.push((plastic_vol_strain, yield_stress));
616 }
617
618 pub fn yield_function(&self, pressure: f64, von_mises: f64) -> f64 {
623 let alpha = self.shape_factor();
624 let p0 = self.yield_surface_center();
625 let p_c = self.p_c0;
626 von_mises * von_mises + alpha * alpha * (pressure - p0).powi(2)
627 - alpha * alpha * (p_c - p0).powi(2)
628 }
629
630 pub fn shape_factor(&self) -> f64 {
632 if self.p_c0.abs() < 1.0e-30 {
633 return 1.0;
634 }
635 self.sigma_c0 / self.p_c0
636 }
637
638 pub fn yield_surface_center(&self) -> f64 {
640 let k_t = self.tension_cutoff_ratio;
641 self.p_c0 * (1.0 - k_t) / 2.0
643 }
644
645 pub fn is_elastic(&self, pressure: f64, von_mises: f64) -> bool {
647 self.yield_function(pressure, von_mises) < 0.0
648 }
649
650 pub fn current_yield_stress(&self, plastic_vol_strain: f64) -> f64 {
652 if self.hardening.is_empty() {
653 return self.sigma_c0;
654 }
655 if plastic_vol_strain <= self.hardening[0].0 {
656 return self.hardening[0].1;
657 }
658 let last = self.hardening.len() - 1;
659 if plastic_vol_strain >= self.hardening[last].0 {
660 return self.hardening[last].1;
661 }
662 for i in 0..last {
664 let (e0, s0) = self.hardening[i];
665 let (e1, s1) = self.hardening[i + 1];
666 if plastic_vol_strain >= e0 && plastic_vol_strain <= e1 {
667 let t = (plastic_vol_strain - e0) / (e1 - e0);
668 return s0 + t * (s1 - s0);
669 }
670 }
671 self.sigma_c0
672 }
673
674 pub fn bulk_modulus(&self) -> f64 {
676 self.elastic_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
677 }
678
679 pub fn shear_modulus(&self) -> f64 {
681 self.elastic_modulus / (2.0 * (1.0 + self.poisson_ratio))
682 }
683
684 pub fn plastic_poisson_ratio(&self) -> f64 {
686 0.0
687 }
688}
689
690#[derive(Debug, Clone, PartialEq)]
699pub struct FoamingKinetics {
700 pub c0_gas: f64,
702 pub surface_tension: f64,
704 pub viscosity: f64,
706 pub temperature: f64,
708 pub gas_constant: f64,
710 pub activation_energy: f64,
712}
713
714impl FoamingKinetics {
715 pub fn new(
717 c0_gas: f64,
718 surface_tension: f64,
719 viscosity: f64,
720 temperature: f64,
721 activation_energy: f64,
722 ) -> Self {
723 Self {
724 c0_gas,
725 surface_tension,
726 viscosity,
727 temperature,
728 gas_constant: 8.314,
729 activation_energy,
730 }
731 }
732
733 pub fn critical_radius(&self, pressure_drop: f64) -> f64 {
737 if pressure_drop.abs() < 1.0e-30 {
738 return f64::INFINITY;
739 }
740 2.0 * self.surface_tension / pressure_drop
741 }
742
743 pub fn nucleation_barrier(&self, pressure_drop: f64) -> f64 {
747 if pressure_drop.abs() < 1.0e-30 {
748 return f64::INFINITY;
749 }
750 16.0 * PI * self.surface_tension.powi(3) / (3.0 * pressure_drop.powi(2))
751 }
752
753 pub fn nucleation_rate(&self, pressure_drop: f64, prefactor: f64) -> f64 {
758 let w_crit = self.nucleation_barrier(pressure_drop);
759 let k_b_t = 1.380649e-23 * self.temperature; prefactor * (-w_crit / k_b_t).exp()
761 }
762
763 pub fn bubble_radius_diffusion(
768 &self,
769 r0: f64,
770 diffusivity: f64,
771 supersaturation: f64,
772 gas_density: f64,
773 time: f64,
774 ) -> f64 {
775 if gas_density.abs() < 1.0e-30 {
776 return r0;
777 }
778 let r_sq = r0 * r0 + 2.0 * diffusivity * supersaturation * time / gas_density;
779 if r_sq < 0.0 {
780 return r0;
781 }
782 r_sq.sqrt()
783 }
784
785 pub fn growth_rate_viscous(&self, radius: f64, pressure_excess: f64) -> f64 {
790 if self.viscosity.abs() < 1.0e-30 {
791 return 0.0;
792 }
793 radius * pressure_excess / (4.0 * self.viscosity)
794 }
795
796 pub fn avrami_fraction(&self, k: f64, n: f64, time: f64) -> f64 {
800 1.0 - (-k * time.powf(n)).exp()
801 }
802
803 pub fn expansion_ratio(&self, atmospheric_pressure: f64) -> f64 {
807 if atmospheric_pressure.abs() < 1.0e-30 {
808 return 1.0;
809 }
810 1.0 + self.c0_gas * self.gas_constant * self.temperature / atmospheric_pressure
811 }
812}
813
814#[derive(Debug, Clone, PartialEq)]
823pub struct FoamThermalConductivity {
824 pub k_solid: f64,
826 pub k_gas: f64,
828 pub relative_density: f64,
830 pub cell_diameter: f64,
832 pub temperature: f64,
834 pub cell_type: CellType,
836 pub emissivity: f64,
838}
839
840impl FoamThermalConductivity {
841 pub fn new(
843 k_solid: f64,
844 k_gas: f64,
845 relative_density: f64,
846 cell_diameter: f64,
847 temperature: f64,
848 cell_type: CellType,
849 emissivity: f64,
850 ) -> Self {
851 Self {
852 k_solid,
853 k_gas,
854 relative_density,
855 cell_diameter,
856 temperature,
857 cell_type,
858 emissivity,
859 }
860 }
861
862 pub fn solid_conduction(&self) -> f64 {
867 match self.cell_type {
868 CellType::Open => (2.0 / 3.0) * self.relative_density * self.k_solid,
869 CellType::Closed | CellType::Mixed => self.relative_density * self.k_solid,
870 }
871 }
872
873 pub fn gas_conduction(&self) -> f64 {
877 (1.0 - self.relative_density) * self.k_gas
878 }
879
880 pub fn radiation_contribution(&self) -> f64 {
885 let sigma_sb = 5.670374419e-8; let porosity = 1.0 - self.relative_density;
887 if self.cell_diameter.abs() < 1.0e-30 || porosity.abs() < 1.0e-30 {
888 return 0.0;
889 }
890 let k_ext = self.relative_density / self.cell_diameter;
892 if k_ext.abs() < 1.0e-30 {
893 return 0.0;
894 }
895 16.0 * sigma_sb * self.emissivity * self.temperature.powi(3) * self.cell_diameter
896 / (3.0 * k_ext * self.cell_diameter)
897 }
898
899 pub fn total_conductivity(&self) -> f64 {
901 self.solid_conduction() + self.gas_conduction() + self.radiation_contribution()
902 }
903
904 pub fn knudsen_corrected_gas(&self, mean_free_path: f64) -> f64 {
910 if self.cell_diameter.abs() < 1.0e-30 {
911 return 0.0;
912 }
913 let kn = mean_free_path / self.cell_diameter;
914 let beta = 1.64;
915 self.k_gas / (1.0 + 2.0 * beta * kn)
916 }
917
918 pub fn r_value(&self, thickness: f64) -> f64 {
922 let k = self.total_conductivity();
923 if k.abs() < 1.0e-30 {
924 return f64::INFINITY;
925 }
926 thickness / k
927 }
928}
929
930#[derive(Debug, Clone, PartialEq)]
939pub struct ImpactAttenuation {
940 pub plateau_stress: f64,
942 pub densification_strain: f64,
944 pub thickness: f64,
946 pub contact_area: f64,
948 pub impactor_mass: f64,
950}
951
952impl ImpactAttenuation {
953 pub fn new(
955 plateau_stress: f64,
956 densification_strain: f64,
957 thickness: f64,
958 contact_area: f64,
959 impactor_mass: f64,
960 ) -> Self {
961 Self {
962 plateau_stress,
963 densification_strain,
964 thickness,
965 contact_area,
966 impactor_mass,
967 }
968 }
969
970 pub fn max_energy_capacity(&self) -> f64 {
972 self.plateau_stress * self.contact_area * self.thickness * self.densification_strain
973 }
974
975 pub fn critical_velocity(&self) -> f64 {
979 if self.impactor_mass.abs() < 1.0e-30 {
980 return 0.0;
981 }
982 let e_max = self.max_energy_capacity();
983 (2.0 * e_max / self.impactor_mass).sqrt()
984 }
985
986 pub fn peak_g(&self, _impact_velocity: f64) -> f64 {
990 let g = 9.81;
991 if self.impactor_mass.abs() < 1.0e-30 {
992 return 0.0;
993 }
994 self.plateau_stress * self.contact_area / (self.impactor_mass * g)
995 }
996
997 pub fn hic_estimate(&self, impact_velocity: f64) -> f64 {
1002 let a_g = self.peak_g(impact_velocity);
1003 if a_g.abs() < 1.0e-30 || self.plateau_stress.abs() < 1.0e-30 {
1004 return 0.0;
1005 }
1006 let dt = self.impactor_mass * impact_velocity / (self.plateau_stress * self.contact_area);
1008 let a_ms2 = a_g * 9.81;
1010 a_ms2.powf(2.5) * dt
1011 }
1012
1013 pub fn compression_depth(&self, impact_velocity: f64) -> f64 {
1017 if self.plateau_stress.abs() < 1.0e-30 || self.contact_area.abs() < 1.0e-30 {
1018 return 0.0;
1019 }
1020 0.5 * self.impactor_mass * impact_velocity * impact_velocity
1021 / (self.plateau_stress * self.contact_area)
1022 }
1023
1024 pub fn is_bottomed_out(&self, impact_velocity: f64) -> bool {
1026 let depth = self.compression_depth(impact_velocity);
1027 depth >= self.thickness * self.densification_strain
1028 }
1029
1030 pub fn rebound_velocity(&self, impact_velocity: f64, cor: f64) -> f64 {
1035 cor * impact_velocity
1036 }
1037
1038 pub fn drop_height_from_velocity(impact_velocity: f64) -> f64 {
1042 impact_velocity * impact_velocity / (2.0 * 9.81)
1043 }
1044
1045 pub fn velocity_from_drop_height(height: f64) -> f64 {
1049 (2.0 * 9.81 * height).sqrt()
1050 }
1051}
1052
1053#[derive(Debug, Clone, PartialEq)]
1062pub struct FoamAging {
1063 pub e_initial: f64,
1065 pub sigma_initial: f64,
1067 pub gas_loss_rate: f64,
1069 pub modulus_degradation_rate: f64,
1071 pub strength_degradation_rate: f64,
1073}
1074
1075impl FoamAging {
1076 pub fn new(
1078 e_initial: f64,
1079 sigma_initial: f64,
1080 gas_loss_rate: f64,
1081 modulus_degradation_rate: f64,
1082 strength_degradation_rate: f64,
1083 ) -> Self {
1084 Self {
1085 e_initial,
1086 sigma_initial,
1087 gas_loss_rate,
1088 modulus_degradation_rate,
1089 strength_degradation_rate,
1090 }
1091 }
1092
1093 pub fn gas_retention(&self, time: f64) -> f64 {
1095 (-self.gas_loss_rate * time).exp()
1096 }
1097
1098 pub fn modulus_at_time(&self, time: f64) -> f64 {
1100 self.e_initial * (-self.modulus_degradation_rate * time).exp()
1101 }
1102
1103 pub fn strength_at_time(&self, time: f64) -> f64 {
1105 self.sigma_initial * (-self.strength_degradation_rate * time).exp()
1106 }
1107
1108 pub fn modulus_half_life(&self) -> f64 {
1110 if self.modulus_degradation_rate.abs() < 1.0e-30 {
1111 return f64::INFINITY;
1112 }
1113 2.0_f64.ln() / self.modulus_degradation_rate
1114 }
1115}
1116
1117#[derive(Debug, Clone, PartialEq)]
1123pub struct CellSizeDistribution {
1124 pub mean_diameter: f64,
1126 pub std_diameter: f64,
1128 pub cell_density: f64,
1130}
1131
1132impl CellSizeDistribution {
1133 pub fn new(mean_diameter: f64, std_diameter: f64, cell_density: f64) -> Self {
1135 Self {
1136 mean_diameter,
1137 std_diameter,
1138 cell_density,
1139 }
1140 }
1141
1142 pub fn coefficient_of_variation(&self) -> f64 {
1144 if self.mean_diameter.abs() < 1.0e-30 {
1145 return 0.0;
1146 }
1147 self.std_diameter / self.mean_diameter
1148 }
1149
1150 pub fn estimated_porosity(&self) -> f64 {
1154 let vol_per_cell = PI / 6.0 * self.mean_diameter.powi(3);
1155 (self.cell_density * vol_per_cell).min(1.0)
1156 }
1157
1158 pub fn specific_surface_area(&self) -> f64 {
1162 self.cell_density * PI * self.mean_diameter.powi(2)
1163 }
1164
1165 pub fn wall_thickness(&self, relative_density: f64) -> f64 {
1169 let x = 1.0 - relative_density;
1170 if x <= 0.0 {
1171 return self.mean_diameter;
1172 }
1173 self.mean_diameter * (1.0 - x.powf(1.0 / 3.0))
1174 }
1175}
1176
1177#[derive(Debug, Clone, PartialEq)]
1183pub struct FoamStack {
1184 pub layers: Vec<FoamLayer>,
1186}
1187
1188#[derive(Debug, Clone, PartialEq)]
1190pub struct FoamLayer {
1191 pub thickness: f64,
1193 pub plateau_stress: f64,
1195 pub densification_strain: f64,
1197 pub elastic_modulus: f64,
1199}
1200
1201impl FoamLayer {
1202 pub fn new(
1204 thickness: f64,
1205 plateau_stress: f64,
1206 densification_strain: f64,
1207 elastic_modulus: f64,
1208 ) -> Self {
1209 Self {
1210 thickness,
1211 plateau_stress,
1212 densification_strain,
1213 elastic_modulus,
1214 }
1215 }
1216
1217 pub fn max_energy_per_area(&self) -> f64 {
1219 self.plateau_stress * self.thickness * self.densification_strain
1220 }
1221}
1222
1223impl FoamStack {
1224 pub fn new() -> Self {
1226 Self { layers: Vec::new() }
1227 }
1228
1229 pub fn add_layer(&mut self, layer: FoamLayer) {
1231 self.layers.push(layer);
1232 }
1233
1234 pub fn total_thickness(&self) -> f64 {
1236 self.layers.iter().map(|l| l.thickness).sum()
1237 }
1238
1239 pub fn total_energy_capacity(&self) -> f64 {
1241 self.layers.iter().map(|l| l.max_energy_per_area()).sum()
1242 }
1243
1244 pub fn effective_modulus(&self) -> f64 {
1248 let total_t = self.total_thickness();
1249 if total_t.abs() < 1.0e-30 {
1250 return 0.0;
1251 }
1252 let compliance: f64 = self
1253 .layers
1254 .iter()
1255 .map(|l| {
1256 if l.elastic_modulus.abs() < 1.0e-30 {
1257 0.0
1258 } else {
1259 l.thickness / l.elastic_modulus
1260 }
1261 })
1262 .sum();
1263 if compliance.abs() < 1.0e-30 {
1264 return 0.0;
1265 }
1266 total_t / compliance
1267 }
1268
1269 pub fn num_layers(&self) -> usize {
1271 self.layers.len()
1272 }
1273}
1274
1275impl Default for FoamStack {
1276 fn default() -> Self {
1277 Self::new()
1278 }
1279}
1280
1281#[cfg(test)]
1286mod tests {
1287 use super::*;
1288
1289 const EPS: f64 = 1.0e-6;
1290
1291 #[test]
1293 fn test_gibson_ashby_modulus_open() {
1294 let ga = GibsonAshby::new(1000.0, 1.0e9, 1.0e6, 0.2, CellType::Open, 0.0);
1295 let rho_foam = 100.0; let e = ga.modulus_open(rho_foam);
1297 let expected = 1.0e9 * 0.01; assert!(
1299 (e - expected).abs() < EPS * expected,
1300 "Open-cell modulus: got {e}, expected {expected}"
1301 );
1302 }
1303
1304 #[test]
1306 fn test_gibson_ashby_modulus_closed() {
1307 let ga = GibsonAshby::new(1000.0, 1.0e9, 1.0e6, 0.2, CellType::Closed, 0.0);
1308 let rho_foam = 100.0;
1309 let e = ga.modulus_closed(rho_foam, 0.6);
1310 let rd = 0.1;
1311 let expected = 1.0e9 * (0.36 * rd * rd + 0.4 * rd);
1312 assert!(
1313 (e - expected).abs() < EPS * expected,
1314 "Closed-cell modulus: got {e}, expected {expected}"
1315 );
1316 }
1317
1318 #[test]
1320 fn test_gibson_ashby_strength_open() {
1321 let ga = GibsonAshby::new(1000.0, 1.0e9, 1.0e6, 0.2, CellType::Open, 0.0);
1322 let rho_foam = 100.0;
1323 let s = ga.strength_open(rho_foam);
1324 let expected = 0.3 * 1.0e6 * 0.1_f64.powf(1.5);
1325 assert!(
1326 (s - expected).abs() < EPS * expected,
1327 "Open-cell strength: got {s}, expected {expected}"
1328 );
1329 }
1330
1331 #[test]
1333 fn test_kelvin_cell_volume() {
1334 let kc = KelvinCell::new(0.001, 0.0001, 1000.0, 1.0e9);
1335 let v = kc.cell_volume();
1336 let expected = 8.0 * 2.0_f64.sqrt() * 0.001_f64.powi(3);
1337 assert!(
1338 (v - expected).abs() < EPS * expected,
1339 "Kelvin cell volume: got {v}, expected {expected}"
1340 );
1341 }
1342
1343 #[test]
1345 fn test_kelvin_cell_faces() {
1346 let kc = KelvinCell::new(0.001, 0.0001, 1000.0, 1.0e9);
1347 assert_eq!(kc.num_faces(), 14);
1348 assert_eq!(kc.num_edges(), 36);
1349 assert_eq!(kc.num_vertices(), 24);
1350 }
1351
1352 #[test]
1354 fn test_energy_absorption_elastic() {
1355 let ea = EnergyAbsorption::new(1.0e5, 0.7, 1.0e6, 0.05, 50.0, 0.05);
1356 let strain = 0.03;
1357 let stress = ea.stress_at_strain(strain);
1358 let expected_stress = 1.0e6 * 0.03;
1359 assert!(
1360 (stress - expected_stress).abs() < EPS,
1361 "Elastic stress: got {stress}"
1362 );
1363 }
1364
1365 #[test]
1367 fn test_energy_absorption_plateau() {
1368 let ea = EnergyAbsorption::new(1.0e5, 0.7, 1.0e6, 0.05, 50.0, 0.05);
1369 let strain = 0.3; let stress = ea.stress_at_strain(strain);
1371 assert!(
1372 (stress - 1.0e5).abs() < EPS,
1373 "Plateau stress should be {}, got {stress}",
1374 1.0e5
1375 );
1376 }
1377
1378 #[test]
1380 fn test_energy_absorption_densification() {
1381 let ea = EnergyAbsorption::new(1.0e5, 0.7, 1.0e6, 0.05, 50.0, 0.05);
1382 let stress_plateau = ea.stress_at_strain(0.7);
1383 let stress_dense = ea.stress_at_strain(0.8);
1384 assert!(
1385 stress_dense > stress_plateau,
1386 "Densification stress should exceed plateau"
1387 );
1388 }
1389
1390 #[test]
1392 fn test_energy_density_monotonic() {
1393 let ea = EnergyAbsorption::new(1.0e5, 0.7, 1.0e6, 0.05, 50.0, 0.05);
1394 let mut prev = 0.0;
1395 for i in 1..=20 {
1396 let strain = i as f64 * 0.05;
1397 let w = ea.energy_density(strain);
1398 assert!(
1399 w >= prev,
1400 "Energy density should be monotonic at strain={strain}"
1401 );
1402 prev = w;
1403 }
1404 }
1405
1406 #[test]
1408 fn test_viscoelastic_modulus_t0() {
1409 let foam = ViscoelasticFoam::single_term(1.0e6, 0.5e6, 1.0, 50.0);
1410 let e0 = foam.relaxation_modulus(0.0);
1411 assert!(
1413 (e0 - 1.0e6).abs() < EPS,
1414 "At t=0, modulus should be E_instantaneous, got {e0}"
1415 );
1416 }
1417
1418 #[test]
1420 fn test_viscoelastic_modulus_inf() {
1421 let foam = ViscoelasticFoam::single_term(1.0e6, 0.5e6, 1.0, 50.0);
1422 let e_inf = foam.relaxation_modulus(1.0e6);
1423 assert!(
1424 (e_inf - 0.5e6).abs() < 1.0,
1425 "At t→∞, modulus should be E_equilibrium, got {e_inf}"
1426 );
1427 }
1428
1429 #[test]
1431 fn test_viscoelastic_loss_tangent() {
1432 let foam = ViscoelasticFoam::single_term(1.0e6, 0.5e6, 1.0, 50.0);
1433 let tan_delta = foam.loss_tangent(1.0);
1434 assert!(
1435 tan_delta > 0.0,
1436 "Loss tangent should be positive, got {tan_delta}"
1437 );
1438 }
1439
1440 #[test]
1442 fn test_crushable_foam_yield() {
1443 let cf = CrushableFoam::new(1.0e5, 2.0e5, 0.1, 1.0e7, 0.3);
1444 let f = cf.yield_function(0.0, 0.0);
1446 assert!(f < 0.0, "Origin should be inside yield surface, f={f}");
1447 }
1448
1449 #[test]
1451 fn test_crushable_foam_plastic() {
1452 let cf = CrushableFoam::new(1.0e5, 2.0e5, 0.1, 1.0e7, 0.3);
1453 let f = cf.yield_function(1.0e6, 1.0e6);
1454 assert!(
1455 f > 0.0,
1456 "Large stress should be outside yield surface, f={f}"
1457 );
1458 }
1459
1460 #[test]
1462 fn test_nucleation_critical_radius() {
1463 let fk = FoamingKinetics::new(100.0, 0.03, 1.0, 400.0, 50000.0);
1464 let r_crit = fk.critical_radius(1.0e5);
1465 let expected = 2.0 * 0.03 / 1.0e5;
1466 assert!(
1467 (r_crit - expected).abs() < EPS * expected,
1468 "Critical radius: got {r_crit}, expected {expected}"
1469 );
1470 }
1471
1472 #[test]
1474 fn test_avrami_limits() {
1475 let fk = FoamingKinetics::new(100.0, 0.03, 1.0, 400.0, 50000.0);
1476 let x0 = fk.avrami_fraction(0.1, 2.0, 0.0);
1477 assert!((x0).abs() < EPS, "Avrami at t=0 should be 0, got {x0}");
1478 let x_inf = fk.avrami_fraction(0.1, 2.0, 100.0);
1479 assert!(
1480 (x_inf - 1.0).abs() < 0.01,
1481 "Avrami at t→∞ should be ~1, got {x_inf}"
1482 );
1483 }
1484
1485 #[test]
1487 fn test_thermal_conductivity_components() {
1488 let tc = FoamThermalConductivity::new(0.2, 0.026, 0.05, 0.001, 300.0, CellType::Open, 0.9);
1489 let k_s = tc.solid_conduction();
1490 let k_g = tc.gas_conduction();
1491 assert!(k_s > 0.0, "Solid conduction should be positive");
1492 assert!(k_g > 0.0, "Gas conduction should be positive");
1493 assert!(k_g > k_s, "For low-density foam, gas conduction dominates");
1494 }
1495
1496 #[test]
1498 fn test_r_value_thickness() {
1499 let tc =
1500 FoamThermalConductivity::new(0.2, 0.026, 0.05, 0.001, 300.0, CellType::Closed, 0.9);
1501 let r1 = tc.r_value(0.05);
1502 let r2 = tc.r_value(0.10);
1503 assert!(
1504 r2 > r1,
1505 "R-value should increase with thickness: r1={r1}, r2={r2}"
1506 );
1507 assert!(
1508 (r2 / r1 - 2.0).abs() < 0.01,
1509 "R-value should double when thickness doubles"
1510 );
1511 }
1512
1513 #[test]
1515 fn test_impact_critical_velocity() {
1516 let ia = ImpactAttenuation::new(1.0e5, 0.7, 0.05, 0.01, 5.0);
1517 let v_crit = ia.critical_velocity();
1518 let e_max = ia.max_energy_capacity();
1519 let expected = (2.0 * e_max / 5.0).sqrt();
1520 assert!(
1521 (v_crit - expected).abs() < EPS,
1522 "Critical velocity: got {v_crit}, expected {expected}"
1523 );
1524 }
1525
1526 #[test]
1528 fn test_impact_bottoming_out() {
1529 let ia = ImpactAttenuation::new(1.0e5, 0.7, 0.05, 0.01, 5.0);
1530 let v_low = 0.1;
1531 let v_high = 100.0;
1532 assert!(
1533 !ia.is_bottomed_out(v_low),
1534 "Low velocity should not bottom out"
1535 );
1536 assert!(
1537 ia.is_bottomed_out(v_high),
1538 "High velocity should bottom out"
1539 );
1540 }
1541
1542 #[test]
1544 fn test_foam_aging() {
1545 let aging = FoamAging::new(1.0e6, 1.0e5, 1.0e-8, 1.0e-7, 1.0e-7);
1546 let e0 = aging.modulus_at_time(0.0);
1547 let e1 = aging.modulus_at_time(1.0e6);
1548 assert!(
1549 (e0 - 1.0e6).abs() < EPS,
1550 "Initial modulus should be E_initial"
1551 );
1552 assert!(e1 < e0, "Modulus should decrease with aging");
1553 }
1554
1555 #[test]
1557 fn test_cell_size_porosity() {
1558 let csd = CellSizeDistribution::new(0.001, 0.0002, 1.0e8);
1559 let phi = csd.estimated_porosity();
1560 assert!(
1561 (0.0..=1.0).contains(&phi),
1562 "Porosity should be in [0,1], got {phi}"
1563 );
1564 }
1565
1566 #[test]
1568 fn test_foam_stack_energy() {
1569 let mut stack = FoamStack::new();
1570 stack.add_layer(FoamLayer::new(0.02, 5.0e4, 0.7, 5.0e5));
1571 stack.add_layer(FoamLayer::new(0.03, 1.0e5, 0.6, 1.0e6));
1572 let e_total = stack.total_energy_capacity();
1573 let e1 = 5.0e4 * 0.02 * 0.7;
1574 let e2 = 1.0e5 * 0.03 * 0.6;
1575 assert!(
1576 (e_total - (e1 + e2)).abs() < EPS,
1577 "Stack energy should be sum of layers"
1578 );
1579 }
1580
1581 #[test]
1583 fn test_foam_stack_modulus() {
1584 let mut stack = FoamStack::new();
1585 stack.add_layer(FoamLayer::new(0.01, 1.0e5, 0.7, 1.0e6));
1586 stack.add_layer(FoamLayer::new(0.01, 1.0e5, 0.7, 1.0e6));
1587 let e_eff = stack.effective_modulus();
1589 assert!(
1590 (e_eff - 1.0e6).abs() < EPS,
1591 "Two identical layers: E_eff should equal E_layer, got {e_eff}"
1592 );
1593 }
1594
1595 #[test]
1597 fn test_relative_density() {
1598 let ga = GibsonAshby::new(1000.0, 1.0e9, 1.0e6, 0.2, CellType::Open, 0.0);
1599 let rd = ga.relative_density(100.0);
1600 assert!(
1601 (rd - 0.1).abs() < EPS,
1602 "Relative density should be 0.1, got {rd}"
1603 );
1604 }
1605
1606 #[test]
1608 fn test_dynamic_moduli_zero_freq() {
1609 let foam = ViscoelasticFoam::single_term(1.0e6, 0.5e6, 1.0, 50.0);
1610 let (e_s, e_l) = foam.dynamic_moduli(0.0);
1611 assert!(
1612 (e_s - 0.5e6).abs() < 1.0,
1613 "Storage modulus at omega=0 should equal E_equilibrium, got {e_s}"
1614 );
1615 assert!(
1616 e_l.abs() < EPS,
1617 "Loss modulus at omega=0 should be zero, got {e_l}"
1618 );
1619 }
1620
1621 #[test]
1623 fn test_kelvin_relative_density_bounded() {
1624 let kc = KelvinCell::new(0.001, 0.5, 1000.0, 1.0e9); let rd = kc.relative_density();
1626 assert!(rd <= 1.0, "Relative density capped at 1.0, got {rd}");
1627 }
1628
1629 #[test]
1631 fn test_efficiency_in_plateau() {
1632 let ea = EnergyAbsorption::new(1.0e5, 0.7, 1.0e6, 0.05, 50.0, 0.05);
1633 let eff = ea.efficiency(0.4); assert!(eff > 0.5, "Efficiency in plateau should be high, got {eff}");
1635 assert!(eff <= 1.0, "Efficiency should not exceed 1.0, got {eff}");
1636 }
1637}