Skip to main content

oxiphysics_materials/
foam_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Foam and cellular material models.
5//!
6//! Provides Gibson-Ashby scaling laws for open/closed-cell foams,
7//! Kelvin cell geometry, energy absorption (plateau stress, densification),
8//! viscoelastic foam (Prony series), crushable foam plasticity,
9//! foaming kinetics, thermal conductivity models, and impact attenuation.
10
11#![allow(dead_code)]
12#![allow(clippy::too_many_arguments)]
13
14use std::f64::consts::PI;
15
16// ===========================================================================
17// Cell type enum
18// ===========================================================================
19
20/// Type of foam cell structure.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum CellType {
23    /// Open-cell foam (interconnected pores).
24    Open,
25    /// Closed-cell foam (sealed pores with gas).
26    Closed,
27    /// Mixed cell structure with a given fraction closed (0..1).
28    Mixed,
29}
30
31// ===========================================================================
32// Gibson-Ashby scaling laws
33// ===========================================================================
34
35/// Gibson-Ashby scaling law parameters for cellular solids.
36///
37/// Predicts effective modulus, strength, and thermal conductivity from
38/// relative density using power-law scaling.
39#[derive(Debug, Clone, PartialEq)]
40pub struct GibsonAshby {
41    /// Density of the solid cell-wall material \[kg/m³\].
42    pub rho_solid: f64,
43    /// Young's modulus of the solid cell-wall material \[Pa\].
44    pub e_solid: f64,
45    /// Compressive strength of the solid cell-wall material \[Pa\].
46    pub sigma_solid: f64,
47    /// Thermal conductivity of the solid material \[W/(m·K)\].
48    pub k_solid: f64,
49    /// Cell type.
50    pub cell_type: CellType,
51    /// Fraction of closed cells (only used when `cell_type == Mixed`).
52    pub closed_fraction: f64,
53}
54
55impl GibsonAshby {
56    /// Create a new Gibson-Ashby model.
57    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    /// Relative density (foam density / solid density).
76    pub fn relative_density(&self, rho_foam: f64) -> f64 {
77        rho_foam / self.rho_solid
78    }
79
80    /// Effective Young's modulus for open-cell foam.
81    ///
82    /// E* / E_s = C1 * (rho*/rho_s)^2   (C1 ≈ 1)
83    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    /// Effective Young's modulus for closed-cell foam.
89    ///
90    /// E* / E_s ≈ phi^2 * (rho*/rho_s)^2 + (1-phi) * (rho*/rho_s)
91    /// where phi is fraction of solid in cell edges (~0.6 typical).
92    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    /// Effective modulus based on cell type.
98    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    /// Compressive (collapse) strength for open-cell foam.
112    ///
113    /// sigma* / sigma_s = C2 * (rho*/rho_s)^(3/2)   (C2 ≈ 0.3)
114    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    /// Compressive strength for closed-cell foam.
120    ///
121    /// sigma* / sigma_s = C3 * (phi * rho*/rho_s)^(3/2) + (1-phi) * (rho*/rho_s)
122    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    /// Effective compressive strength based on cell type.
128    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    /// Gibson-Ashby thermal conductivity scaling.
142    ///
143    /// k* / k_s ≈ (1/3) * (rho*/rho_s)  for open-cell
144    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    /// Gibson-Ashby Poisson's ratio estimate for open-cell foam.
150    ///
151    /// For open-cell foams, Poisson's ratio is approximately 1/3.
152    pub fn poisson_ratio_open(&self) -> f64 {
153        1.0 / 3.0
154    }
155
156    /// Fracture toughness scaling for open-cell foam.
157    ///
158    /// K_Ic* / (sigma_s * sqrt(l)) ≈ C * (rho*/rho_s)^(3/2)
159    /// where l is cell size and C ≈ 0.65.
160    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// ===========================================================================
167// Kelvin cell model
168// ===========================================================================
169
170/// Kelvin cell (truncated octahedron) foam geometry model.
171///
172/// The Kelvin cell is a space-filling polyhedron (truncated octahedron) that
173/// provides a good approximation to real foam cells.
174#[derive(Debug, Clone, PartialEq)]
175pub struct KelvinCell {
176    /// Cell edge length \[m\].
177    pub edge_length: f64,
178    /// Cell wall thickness \[m\].
179    pub wall_thickness: f64,
180    /// Solid material density \[kg/m³\].
181    pub rho_solid: f64,
182    /// Solid material modulus \[Pa\].
183    pub e_solid: f64,
184}
185
186impl KelvinCell {
187    /// Create a new Kelvin cell model.
188    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    /// Volume of a single Kelvin cell (truncated octahedron).
198    ///
199    /// V = 8 * sqrt(2) * a^3 where a is the edge length.
200    pub fn cell_volume(&self) -> f64 {
201        8.0 * 2.0_f64.sqrt() * self.edge_length.powi(3)
202    }
203
204    /// Surface area of a single Kelvin cell.
205    ///
206    /// A = (6 + 12*sqrt(3)) * a^2
207    pub fn cell_surface_area(&self) -> f64 {
208        (6.0 + 12.0 * 3.0_f64.sqrt()) * self.edge_length.powi(2)
209    }
210
211    /// Number of faces: 8 hexagonal + 6 square = 14.
212    pub fn num_faces(&self) -> usize {
213        14
214    }
215
216    /// Number of edges of a Kelvin cell = 36.
217    pub fn num_edges(&self) -> usize {
218        36
219    }
220
221    /// Number of vertices of a Kelvin cell = 24.
222    pub fn num_vertices(&self) -> usize {
223        24
224    }
225
226    /// Relative density for closed-cell Kelvin foam.
227    ///
228    /// Approximation: rho*/rho_s ≈ surface_area * t / cell_volume
229    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    /// Effective foam density \[kg/m³\].
236    pub fn foam_density(&self) -> f64 {
237        self.rho_solid * self.relative_density()
238    }
239
240    /// Effective modulus via Gibson-Ashby with Kelvin cell relative density.
241    pub fn effective_modulus(&self) -> f64 {
242        let rd = self.relative_density();
243        self.e_solid * rd * rd
244    }
245
246    /// Characteristic cell diameter (equivalent sphere diameter).
247    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    /// Strut cross-section area for open-cell Kelvin model.
253    ///
254    /// For open-cell, material is concentrated in the 36 edges.
255    /// A_strut ≈ (rho*/rho_s) * V_cell / (N_edges * L_edge)
256    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// ===========================================================================
263// Energy absorption model
264// ===========================================================================
265
266/// Energy absorption model for foam impact.
267///
268/// Describes the three characteristic regions of foam compression:
269/// linear elastic, plateau (constant stress), and densification.
270#[derive(Debug, Clone, PartialEq)]
271pub struct EnergyAbsorption {
272    /// Plateau stress \[Pa\].
273    pub plateau_stress: f64,
274    /// Densification strain (typically 0.5–0.9).
275    pub densification_strain: f64,
276    /// Young's modulus in the linear elastic region \[Pa\].
277    pub elastic_modulus: f64,
278    /// Yield strain (transition from elastic to plateau).
279    pub yield_strain: f64,
280    /// Foam density \[kg/m³\].
281    pub foam_density: f64,
282    /// Foam thickness \[m\].
283    pub thickness: f64,
284}
285
286impl EnergyAbsorption {
287    /// Create a new energy absorption model.
288    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    /// Stress at a given engineering strain (simplified tri-linear model).
307    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            // Linear elastic region
313            self.elastic_modulus * strain
314        } else if strain <= self.densification_strain {
315            // Plateau region
316            self.plateau_stress
317        } else {
318            // Densification: exponential rise
319            let excess = strain - self.densification_strain;
320            let stiffening_factor = 10.0; // empirical
321            self.plateau_stress * (1.0 + stiffening_factor * excess * excess)
322        }
323    }
324
325    /// Energy absorbed per unit volume up to a given strain \[J/m³\].
326    ///
327    /// Integrates the stress-strain curve from 0 to `strain`.
328    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            // Triangular area under linear elastic
334            0.5 * self.elastic_modulus * strain * strain
335        } else if strain <= self.densification_strain {
336            // Elastic area + plateau rectangle
337            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            // Elastic + plateau + densification
342            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            // Integral of sigma_p * (1 + k * x^2) dx from 0 to excess
348            let densification_energy = self.plateau_stress * (excess + k * excess.powi(3) / 3.0);
349            elastic_energy + plateau_energy + densification_energy
350        }
351    }
352
353    /// Total energy absorbed through full plateau region \[J/m³\].
354    pub fn plateau_energy_density(&self) -> f64 {
355        self.energy_density(self.densification_strain)
356    }
357
358    /// Efficiency at a given strain.
359    ///
360    /// eta = W(strain) / (sigma(strain) * strain)
361    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    /// Ideal energy absorption efficiency (rectangular stress-strain).
374    ///
375    /// eta_ideal = 1 for perfect energy absorber.
376    pub fn ideal_efficiency(&self) -> f64 {
377        // For the plateau region only
378        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    /// Energy absorbed per unit mass up to a given strain \[J/kg\].
387    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    /// Maximum deceleration during impact for given drop parameters.
395    ///
396    /// G = sigma_plateau / (rho_foam * h)
397    /// where h is the drop height.
398    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    /// Cushion factor = peak_stress / energy_density at that strain.
406    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// ===========================================================================
416// Viscoelastic foam (Prony series)
417// ===========================================================================
418
419/// Single Prony series term for viscoelastic relaxation.
420#[derive(Debug, Clone, PartialEq)]
421pub struct PronyTerm {
422    /// Relative modulus weight g_i (dimensionless).
423    pub g_i: f64,
424    /// Relaxation time tau_i \[s\].
425    pub tau_i: f64,
426}
427
428/// Viscoelastic foam model using a Prony series representation.
429///
430/// The relaxation modulus is:
431///   E(t) = E_inf + sum_i (E_0 * g_i * exp(-t / tau_i))
432#[derive(Debug, Clone, PartialEq)]
433pub struct ViscoelasticFoam {
434    /// Instantaneous modulus E_0 \[Pa\].
435    pub e_instantaneous: f64,
436    /// Long-term (equilibrium) modulus E_inf \[Pa\].
437    pub e_equilibrium: f64,
438    /// Prony series terms.
439    pub prony_terms: Vec<PronyTerm>,
440    /// Foam density \[kg/m³\].
441    pub density: f64,
442    /// Poisson's ratio (typically ~0.0 for low-density foams).
443    pub poisson_ratio: f64,
444}
445
446impl ViscoelasticFoam {
447    /// Create a new viscoelastic foam model.
448    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    /// Create a single-term Maxwell model.
465    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    /// Relaxation modulus at time t.
477    ///
478    /// E(t) = E_inf + E_0 * sum_i g_i * exp(-t / tau_i)
479    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    /// Stress response to a constant-rate strain loading.
488    ///
489    /// For strain(t) = rate * t:
490    ///   sigma(t) = rate * integral_0^t E(t - s) ds
491    pub fn stress_constant_rate(&self, strain_rate: f64, t: f64) -> f64 {
492        // Analytical integration for Prony series under constant rate
493        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            // integral_0^t g_i * E_0 * exp(-(t-s)/tau) * rate ds
498            // = rate * E_0 * g_i * tau * (1 - exp(-t/tau))
499            sigma += strain_rate * self.e_instantaneous * g * tau * (1.0 - (-t / tau).exp());
500        }
501        sigma
502    }
503
504    /// Creep compliance at time t (reciprocal relationship, approximate).
505    ///
506    /// J(t) ≈ 1 / E(t)   (approximate for linear viscoelastic).
507    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    /// Loss tangent (tan delta) at angular frequency omega.
516    ///
517    /// tan(delta) = E''(omega) / E'(omega)
518    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    /// Storage and loss moduli at angular frequency omega.
527    ///
528    /// E'(omega) = E_inf + E_0 * sum g_i * (omega*tau_i)^2 / (1 + (omega*tau_i)^2)
529    /// E''(omega) = E_0 * sum g_i * omega*tau_i / (1 + (omega*tau_i)^2)
530    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    /// Half-time for stress relaxation.
544    ///
545    /// Approximate: uses the longest relaxation time.
546    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    /// Bulk modulus from E and nu.
556    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    /// Shear modulus from E and nu.
563    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// ===========================================================================
571// Crushable foam plasticity
572// ===========================================================================
573
574/// Crushable foam plasticity model.
575///
576/// Uses a volumetric yield surface (elliptical in p-q space).
577/// Suitable for metallic foams, polymeric foams under large compressive loads.
578#[derive(Debug, Clone, PartialEq)]
579pub struct CrushableFoam {
580    /// Initial yield stress in uniaxial compression \[Pa\].
581    pub sigma_c0: f64,
582    /// Initial yield stress in hydrostatic compression \[Pa\].
583    pub p_c0: f64,
584    /// Ratio of hydrostatic tensile strength to compressive (k_t).
585    pub tension_cutoff_ratio: f64,
586    /// Hardening curve: pairs of (plastic volumetric strain, yield stress).
587    pub hardening: Vec<(f64, f64)>,
588    /// Elastic modulus \[Pa\].
589    pub elastic_modulus: f64,
590    /// Poisson's ratio.
591    pub poisson_ratio: f64,
592}
593
594impl CrushableFoam {
595    /// Create a new crushable foam model.
596    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    /// Add a point to the hardening curve.
614    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    /// Yield surface value f(p, q) for the crushable foam model.
619    ///
620    /// f = q^2 + alpha^2 * (p - p0)^2 - alpha^2 * (p_c - p0)^2
621    /// where p0 is the center and alpha is the shape factor.
622    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    /// Shape factor alpha = q_c / p_c.
631    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    /// Center of the yield ellipse in pressure space.
639    pub fn yield_surface_center(&self) -> f64 {
640        let k_t = self.tension_cutoff_ratio;
641        // p_0 = (p_c - k_t * p_c) / 2 = p_c * (1 - k_t) / 2
642        self.p_c0 * (1.0 - k_t) / 2.0
643    }
644
645    /// Check if a stress state (p, q) is inside the yield surface.
646    pub fn is_elastic(&self, pressure: f64, von_mises: f64) -> bool {
647        self.yield_function(pressure, von_mises) < 0.0
648    }
649
650    /// Current yield stress from hardening table by interpolation.
651    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        // Linear interpolation
663        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    /// Bulk modulus.
675    pub fn bulk_modulus(&self) -> f64 {
676        self.elastic_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
677    }
678
679    /// Shear modulus.
680    pub fn shear_modulus(&self) -> f64 {
681        self.elastic_modulus / (2.0 * (1.0 + self.poisson_ratio))
682    }
683
684    /// Plastic Poisson's ratio (typically ~0 for foams).
685    pub fn plastic_poisson_ratio(&self) -> f64 {
686        0.0
687    }
688}
689
690// ===========================================================================
691// Foaming kinetics
692// ===========================================================================
693
694/// Foaming kinetics model.
695///
696/// Describes the nucleation, growth, and stabilization of bubbles
697/// during foam processing.
698#[derive(Debug, Clone, PartialEq)]
699pub struct FoamingKinetics {
700    /// Initial gas concentration \[mol/m³\].
701    pub c0_gas: f64,
702    /// Surface tension of the foaming liquid \[N/m\].
703    pub surface_tension: f64,
704    /// Viscosity of the foaming liquid \[Pa·s\].
705    pub viscosity: f64,
706    /// Temperature \[K\].
707    pub temperature: f64,
708    /// Gas constant R = 8.314 J/(mol·K).
709    pub gas_constant: f64,
710    /// Activation energy for nucleation \[J/mol\].
711    pub activation_energy: f64,
712}
713
714impl FoamingKinetics {
715    /// Create a new foaming kinetics model.
716    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    /// Classical nucleation theory: critical bubble radius.
734    ///
735    /// R_crit = 2 * gamma / delta_P
736    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    /// Nucleation energy barrier.
744    ///
745    /// W_crit = (16 * pi * gamma^3) / (3 * delta_P^2)
746    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    /// Nucleation rate (classical nucleation theory).
754    ///
755    /// J = J0 * exp(-W_crit / (k_B * T))
756    /// Using k_B = R/N_A, simplified here with prefactor.
757    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; // Boltzmann * T
760        prefactor * (-w_crit / k_b_t).exp()
761    }
762
763    /// Bubble growth by diffusion (Epstein-Plesset model, simplified).
764    ///
765    /// R(t) = sqrt(R0^2 + 2 * D * c * t / rho_g)
766    /// D is diffusivity, c is supersaturation, rho_g is gas density.
767    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    /// Viscous limitation on bubble growth rate.
786    ///
787    /// dR/dt = R * delta_P / (4 * mu)
788    /// where mu is viscosity and delta_P is the internal pressure excess.
789    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    /// Avrami equation for volume fraction of foam as function of time.
797    ///
798    /// X(t) = 1 - exp(-k * t^n)
799    pub fn avrami_fraction(&self, k: f64, n: f64, time: f64) -> f64 {
800        1.0 - (-k * time.powf(n)).exp()
801    }
802
803    /// Foam expansion ratio at equilibrium.
804    ///
805    /// phi = V_foam / V_liquid ≈ 1 + (c0 * R * T) / P_atm
806    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// ===========================================================================
815// Thermal conductivity of foams
816// ===========================================================================
817
818/// Thermal conductivity model for foam materials.
819///
820/// Accounts for contributions from solid conduction, gas conduction,
821/// radiation, and convection.
822#[derive(Debug, Clone, PartialEq)]
823pub struct FoamThermalConductivity {
824    /// Thermal conductivity of solid cell wall material \[W/(m·K)\].
825    pub k_solid: f64,
826    /// Thermal conductivity of the gas in cells \[W/(m·K)\].
827    pub k_gas: f64,
828    /// Relative density of the foam.
829    pub relative_density: f64,
830    /// Mean cell diameter \[m\].
831    pub cell_diameter: f64,
832    /// Foam temperature \[K\].
833    pub temperature: f64,
834    /// Cell type (open/closed).
835    pub cell_type: CellType,
836    /// Emissivity of cell walls (for radiation).
837    pub emissivity: f64,
838}
839
840impl FoamThermalConductivity {
841    /// Create a new foam thermal conductivity model.
842    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    /// Solid conduction contribution.
863    ///
864    /// k_s_eff = (2/3) * rho_rel * k_solid   (open-cell)
865    /// k_s_eff = rho_rel * k_solid            (closed-cell, simplified)
866    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    /// Gas conduction contribution.
874    ///
875    /// k_g_eff = (1 - rho_rel) * k_gas
876    pub fn gas_conduction(&self) -> f64 {
877        (1.0 - self.relative_density) * self.k_gas
878    }
879
880    /// Radiation contribution (Rosseland approximation).
881    ///
882    /// k_rad = 16 * sigma_SB * T^3 * d / (3 * K_ext)
883    /// where K_ext ≈ (1 - porosity) / d for simplicity.
884    pub fn radiation_contribution(&self) -> f64 {
885        let sigma_sb = 5.670374419e-8; // Stefan-Boltzmann
886        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        // Extinction coefficient approximation
891        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    /// Total effective thermal conductivity.
900    pub fn total_conductivity(&self) -> f64 {
901        self.solid_conduction() + self.gas_conduction() + self.radiation_contribution()
902    }
903
904    /// Knudsen effect correction for gas conductivity in small cells.
905    ///
906    /// k_gas_eff = k_gas / (1 + 2 * beta * Kn)
907    /// where Kn = lambda / d is the Knudsen number,
908    /// lambda is the mean free path, beta ≈ 1.64.
909    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    /// R-value (thermal resistance) for a given foam thickness.
919    ///
920    /// R = thickness / k_total \[m²·K/W\]
921    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// ===========================================================================
931// Impact attenuation
932// ===========================================================================
933
934/// Impact attenuation model for foam packaging/helmets.
935///
936/// Predicts peak acceleration, HIC, and energy absorption for
937/// drop impact scenarios.
938#[derive(Debug, Clone, PartialEq)]
939pub struct ImpactAttenuation {
940    /// Foam plateau stress \[Pa\].
941    pub plateau_stress: f64,
942    /// Foam densification strain.
943    pub densification_strain: f64,
944    /// Foam thickness \[m\].
945    pub thickness: f64,
946    /// Foam contact area \[m²\].
947    pub contact_area: f64,
948    /// Object mass \[kg\].
949    pub impactor_mass: f64,
950}
951
952impl ImpactAttenuation {
953    /// Create a new impact attenuation model.
954    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    /// Maximum energy the foam can absorb before densification \[J\].
971    pub fn max_energy_capacity(&self) -> f64 {
972        self.plateau_stress * self.contact_area * self.thickness * self.densification_strain
973    }
974
975    /// Impact velocity that saturates the foam capacity \[m/s\].
976    ///
977    /// 0.5 * m * v^2 = E_max  =>  v = sqrt(2 * E_max / m)
978    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    /// Peak deceleration in g's for a given impact velocity.
987    ///
988    /// a_peak = sigma_p * A / (m * g)   when v < v_crit
989    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    /// Head Injury Criterion (HIC) estimate.
998    ///
999    /// HIC = ((1/(t2-t1)) * integral_t1^t2 a(t) dt)^2.5 * (t2 - t1)
1000    /// For constant deceleration a: HIC = a^2.5 * dt
1001    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        // Impact duration: dt = m * v / (sigma_p * A)
1007        let dt = self.impactor_mass * impact_velocity / (self.plateau_stress * self.contact_area);
1008        // HIC = (a_g * g)^2.5 * dt  (with acceleration in m/s^2)
1009        let a_ms2 = a_g * 9.81;
1010        a_ms2.powf(2.5) * dt
1011    }
1012
1013    /// Compression depth for a given impact velocity \[m\].
1014    ///
1015    /// delta = 0.5 * m * v^2 / (sigma_p * A)
1016    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    /// Whether the foam is fully crushed at a given velocity.
1025    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    /// Rebound velocity (coefficient of restitution ≈ 0 for ideal foam).
1031    ///
1032    /// For a real foam with given COR:
1033    ///   v_rebound = cor * v_impact
1034    pub fn rebound_velocity(&self, impact_velocity: f64, cor: f64) -> f64 {
1035        cor * impact_velocity
1036    }
1037
1038    /// Drop height corresponding to a given impact velocity.
1039    ///
1040    /// h = v^2 / (2 * g)
1041    pub fn drop_height_from_velocity(impact_velocity: f64) -> f64 {
1042        impact_velocity * impact_velocity / (2.0 * 9.81)
1043    }
1044
1045    /// Impact velocity from a given drop height.
1046    ///
1047    /// v = sqrt(2 * g * h)
1048    pub fn velocity_from_drop_height(height: f64) -> f64 {
1049        (2.0 * 9.81 * height).sqrt()
1050    }
1051}
1052
1053// ===========================================================================
1054// Foam aging / degradation
1055// ===========================================================================
1056
1057/// Simple foam aging/degradation model.
1058///
1059/// Models the reduction in foam properties over time due to gas diffusion,
1060/// cell wall degradation, and UV exposure.
1061#[derive(Debug, Clone, PartialEq)]
1062pub struct FoamAging {
1063    /// Initial modulus \[Pa\].
1064    pub e_initial: f64,
1065    /// Initial strength \[Pa\].
1066    pub sigma_initial: f64,
1067    /// Gas loss rate constant \[1/s\].
1068    pub gas_loss_rate: f64,
1069    /// Modulus degradation rate constant \[1/s\].
1070    pub modulus_degradation_rate: f64,
1071    /// Strength degradation rate constant \[1/s\].
1072    pub strength_degradation_rate: f64,
1073}
1074
1075impl FoamAging {
1076    /// Create a new foam aging model.
1077    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    /// Gas retention fraction at time t.
1094    pub fn gas_retention(&self, time: f64) -> f64 {
1095        (-self.gas_loss_rate * time).exp()
1096    }
1097
1098    /// Modulus at time t with aging.
1099    pub fn modulus_at_time(&self, time: f64) -> f64 {
1100        self.e_initial * (-self.modulus_degradation_rate * time).exp()
1101    }
1102
1103    /// Strength at time t with aging.
1104    pub fn strength_at_time(&self, time: f64) -> f64 {
1105        self.sigma_initial * (-self.strength_degradation_rate * time).exp()
1106    }
1107
1108    /// Half-life of the modulus.
1109    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// ===========================================================================
1118// Foam cell size distribution
1119// ===========================================================================
1120
1121/// Foam cell size distribution statistics.
1122#[derive(Debug, Clone, PartialEq)]
1123pub struct CellSizeDistribution {
1124    /// Mean cell diameter \[m\].
1125    pub mean_diameter: f64,
1126    /// Standard deviation of cell diameter \[m\].
1127    pub std_diameter: f64,
1128    /// Number of cells per unit volume \[1/m³\].
1129    pub cell_density: f64,
1130}
1131
1132impl CellSizeDistribution {
1133    /// Create a new cell size distribution.
1134    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    /// Coefficient of variation.
1143    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    /// Approximate porosity from cell density and mean diameter.
1151    ///
1152    /// phi ≈ N * (pi/6) * d^3, capped at 1.0.
1153    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    /// Specific surface area (surface per unit volume) for spherical cells.
1159    ///
1160    /// S_v = N * pi * d^2
1161    pub fn specific_surface_area(&self) -> f64 {
1162        self.cell_density * PI * self.mean_diameter.powi(2)
1163    }
1164
1165    /// Characteristic cell wall thickness for closed-cell foam.
1166    ///
1167    /// t ≈ d * (1 - (1 - rho_rel)^(1/3))
1168    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// ===========================================================================
1178// Multi-layer foam stack
1179// ===========================================================================
1180
1181/// Multi-layer foam stack for graded energy absorption.
1182#[derive(Debug, Clone, PartialEq)]
1183pub struct FoamStack {
1184    /// Layers: each described by (thickness, plateau_stress, densification_strain, modulus).
1185    pub layers: Vec<FoamLayer>,
1186}
1187
1188/// A single layer in a foam stack.
1189#[derive(Debug, Clone, PartialEq)]
1190pub struct FoamLayer {
1191    /// Thickness \[m\].
1192    pub thickness: f64,
1193    /// Plateau stress \[Pa\].
1194    pub plateau_stress: f64,
1195    /// Densification strain.
1196    pub densification_strain: f64,
1197    /// Elastic modulus \[Pa\].
1198    pub elastic_modulus: f64,
1199}
1200
1201impl FoamLayer {
1202    /// Create a new foam layer.
1203    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    /// Maximum energy this layer can absorb \[J/m²\].
1218    pub fn max_energy_per_area(&self) -> f64 {
1219        self.plateau_stress * self.thickness * self.densification_strain
1220    }
1221}
1222
1223impl FoamStack {
1224    /// Create a new empty foam stack.
1225    pub fn new() -> Self {
1226        Self { layers: Vec::new() }
1227    }
1228
1229    /// Add a layer to the stack.
1230    pub fn add_layer(&mut self, layer: FoamLayer) {
1231        self.layers.push(layer);
1232    }
1233
1234    /// Total thickness of the stack.
1235    pub fn total_thickness(&self) -> f64 {
1236        self.layers.iter().map(|l| l.thickness).sum()
1237    }
1238
1239    /// Total maximum energy absorption per unit area \[J/m²\].
1240    pub fn total_energy_capacity(&self) -> f64 {
1241        self.layers.iter().map(|l| l.max_energy_per_area()).sum()
1242    }
1243
1244    /// Effective (series) modulus of the stack.
1245    ///
1246    /// 1/E_eff = sum (t_i / E_i) / sum(t_i)
1247    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    /// Number of layers.
1270    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// ===========================================================================
1282// Tests
1283// ===========================================================================
1284
1285#[cfg(test)]
1286mod tests {
1287    use super::*;
1288
1289    const EPS: f64 = 1.0e-6;
1290
1291    // 1. Gibson-Ashby open-cell modulus scaling
1292    #[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; // relative density = 0.1
1296        let e = ga.modulus_open(rho_foam);
1297        let expected = 1.0e9 * 0.01; // (0.1)^2
1298        assert!(
1299            (e - expected).abs() < EPS * expected,
1300            "Open-cell modulus: got {e}, expected {expected}"
1301        );
1302    }
1303
1304    // 2. Gibson-Ashby closed-cell modulus
1305    #[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    // 3. Gibson-Ashby open-cell strength
1319    #[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    // 4. Kelvin cell volume
1332    #[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    // 5. Kelvin cell face count
1344    #[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    // 6. Energy absorption - elastic region
1353    #[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    // 7. Energy absorption - plateau region
1366    #[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; // in plateau region
1370        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    // 8. Energy absorption - densification rises
1379    #[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    // 9. Energy density monotonically increases
1391    #[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    // 10. Viscoelastic foam relaxation modulus at t=0
1407    #[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        // At t=0: E = E_inf + E_0 * g = E_inf + (E_0 - E_inf) = E_0
1412        assert!(
1413            (e0 - 1.0e6).abs() < EPS,
1414            "At t=0, modulus should be E_instantaneous, got {e0}"
1415        );
1416    }
1417
1418    // 11. Viscoelastic foam relaxation modulus at t→∞
1419    #[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    // 12. Viscoelastic loss tangent is positive
1430    #[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    // 13. Crushable foam yield surface
1441    #[test]
1442    fn test_crushable_foam_yield() {
1443        let cf = CrushableFoam::new(1.0e5, 2.0e5, 0.1, 1.0e7, 0.3);
1444        // At the origin, should be elastic
1445        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    // 14. Crushable foam - large stress is plastic
1450    #[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    // 15. Foaming kinetics critical radius
1461    #[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    // 16. Foaming kinetics Avrami
1473    #[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    // 17. Thermal conductivity gas + solid
1486    #[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    // 18. R-value increases with thickness
1497    #[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    // 19. Impact attenuation critical velocity
1514    #[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    // 20. Impact bottoming out detection
1527    #[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    // 21. Foam aging modulus decreases over time
1543    #[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    // 22. Cell size distribution porosity
1556    #[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    // 23. Foam stack energy capacity
1567    #[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    // 24. Foam stack effective modulus (series)
1582    #[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        // Two identical layers → effective modulus = single layer modulus
1588        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    // 25. Gibson-Ashby relative density
1596    #[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    // 26. Viscoelastic dynamic moduli at zero frequency
1607    #[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    // 27. Kelvin cell relative density bounded
1622    #[test]
1623    fn test_kelvin_relative_density_bounded() {
1624        let kc = KelvinCell::new(0.001, 0.5, 1000.0, 1.0e9); // very thick walls
1625        let rd = kc.relative_density();
1626        assert!(rd <= 1.0, "Relative density capped at 1.0, got {rd}");
1627    }
1628
1629    // 28. Energy absorption efficiency in plateau
1630    #[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); // well into plateau
1634        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}