Skip to main content

phyz_particle/
material.rs

1//! Material constitutive models for MPM.
2
3use phyz_math::Mat3;
4
5/// Material constitutive model.
6#[derive(Debug, Clone, Copy)]
7pub enum Material {
8    /// Linear elastic material (Neo-Hookean).
9    Elastic {
10        /// Young's modulus (Pa).
11        e: f64,
12        /// Poisson's ratio (dimensionless).
13        nu: f64,
14    },
15    /// Elastic-plastic material with von Mises yield.
16    Plastic {
17        /// Young's modulus (Pa).
18        e: f64,
19        /// Yield stress (Pa).
20        yield_stress: f64,
21    },
22    /// Granular material with Drucker-Prager yield.
23    Granular {
24        /// Internal friction angle (radians).
25        phi: f64,
26    },
27    /// Fluid material with viscosity and equation of state.
28    Fluid {
29        /// Dynamic viscosity (Pa·s).
30        viscosity: f64,
31        /// Equation of state parameters.
32        eos: EquationOfState,
33    },
34}
35
36/// Equation of state for fluids.
37#[derive(Debug, Clone, Copy)]
38pub enum EquationOfState {
39    /// Water-like EOS: p = gamma * (rho/rho0)^7.
40    Water {
41        /// Reference density (kg/m³).
42        rho0: f64,
43        /// Pressure coefficient.
44        gamma: f64,
45    },
46    /// Ideal gas: p = rho0 * cs^2 * (J - 1).
47    IdealGas {
48        /// Reference density (kg/m³).
49        rho0: f64,
50        /// Speed of sound (m/s).
51        cs: f64,
52    },
53}
54
55impl Material {
56    /// Compute first Piola-Kirchhoff stress from deformation gradient.
57    ///
58    /// Returns stress tensor P = ∂ψ/∂F where ψ is the strain energy density.
59    pub fn compute_stress(&self, f: &Mat3, j: f64) -> Mat3 {
60        match self {
61            Material::Elastic { e, nu } => {
62                // Neo-Hookean model
63                // ψ(F) = μ/2 (|F|²_F - 3 - 2 ln J) + λ/2 (ln J)²
64                // P = μ (F - F^-T) + λ ln(J) F^-T
65
66                // Prevent singular deformation
67                let j_safe = j.max(0.01);
68
69                let mu = e / (2.0 * (1.0 + nu));
70                let lambda = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
71
72                // Use safe inverse
73                let f_inv_t = if let Some(inv) = f.try_inverse() {
74                    inv.transpose()
75                } else {
76                    // If F is singular, return zero stress
77                    return Mat3::zeros();
78                };
79
80                let ln_j = j_safe.ln();
81
82                mu * (f - f_inv_t) + lambda * ln_j * f_inv_t
83            }
84            Material::Plastic { e, yield_stress } => {
85                // Simplified J-plasticity: treat as elastic for now
86                // (Full plastic update requires tracking F_p over time)
87                let j_safe = j.max(0.01);
88                let nu = 0.3; // Assume typical Poisson ratio
89                let mu = e / (2.0 * (1.0 + nu));
90                let lambda = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
91
92                let f_inv_t = if let Some(inv) = f.try_inverse() {
93                    inv.transpose()
94                } else {
95                    return Mat3::zeros();
96                };
97
98                let ln_j = j_safe.ln();
99
100                // Apply yield criterion (von Mises on deviatoric stress)
101                let p_elastic = mu * (f - f_inv_t) + lambda * ln_j * f_inv_t;
102
103                // Compute deviatoric stress magnitude
104                let dev_stress = p_elastic - (p_elastic.trace() / 3.0) * Mat3::identity();
105                let dev_norm = dev_stress.norm();
106
107                // If yield exceeded, scale back
108                if dev_norm > *yield_stress {
109                    let scale = yield_stress / dev_norm;
110                    p_elastic * scale
111                } else {
112                    p_elastic
113                }
114            }
115            Material::Granular { phi } => {
116                // Drucker-Prager yield criterion
117                // Simplified: use friction coefficient from angle
118                let j_safe = j.max(0.01);
119                let mu_eff = phi.tan();
120
121                // Elastic response (simplified)
122                let e = 1e6; // Typical granular Young's modulus
123                let nu = 0.3;
124                let mu = e / (2.0 * (1.0 + nu));
125                let lambda = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
126
127                let f_inv_t = if let Some(inv) = f.try_inverse() {
128                    inv.transpose()
129                } else {
130                    return Mat3::zeros();
131                };
132
133                let ln_j = j_safe.ln();
134
135                let p_elastic = mu * (f - f_inv_t) + lambda * ln_j * f_inv_t;
136
137                // Apply Drucker-Prager yield
138                let dev_stress = p_elastic - (p_elastic.trace() / 3.0) * Mat3::identity();
139                let dev_norm = dev_stress.norm();
140                let mean_stress = p_elastic.trace() / 3.0;
141
142                let yield_fn = dev_norm - 3.0_f64.sqrt() * mu_eff * mean_stress.abs();
143
144                if yield_fn > 0.0 && dev_norm > 1e-12 {
145                    // Scale back to yield surface
146                    let scale = (3.0_f64.sqrt() * mu_eff * mean_stress.abs()) / dev_norm;
147                    let dev_scaled = dev_stress * scale;
148                    dev_scaled + mean_stress * Mat3::identity()
149                } else {
150                    p_elastic
151                }
152            }
153            Material::Fluid { viscosity: _, eos } => {
154                // Fluid pressure from equation of state
155                let j_safe = j.max(0.01);
156                let pressure = match eos {
157                    EquationOfState::Water { rho0: _, gamma } => {
158                        // p = gamma * (rho/rho0)^7 = gamma * J^(-7)
159                        gamma * j_safe.powf(-7.0)
160                    }
161                    EquationOfState::IdealGas { rho0, cs } => {
162                        // p = rho0 * cs^2 * (J - 1)
163                        rho0 * cs * cs * (j_safe - 1.0)
164                    }
165                };
166
167                // Pressure contribution: P = -p * J * F^-T
168                let f_inv_t = if let Some(inv) = f.try_inverse() {
169                    inv.transpose()
170                } else {
171                    return Mat3::zeros();
172                };
173
174                -pressure * j_safe * f_inv_t
175
176                // Note: viscous stress would require velocity gradient
177                // For now, just pressure response
178            }
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_elastic_stress() {
189        let mat = Material::Elastic { e: 1e6, nu: 0.3 };
190        let f = Mat3::identity(); // No deformation
191        let j = 1.0;
192        let stress = mat.compute_stress(&f, j);
193
194        // Should be near zero for identity deformation
195        assert!(stress.norm() < 1e-6);
196    }
197
198    #[test]
199    fn test_fluid_pressure() {
200        let mat = Material::Fluid {
201            viscosity: 1e-3,
202            eos: EquationOfState::IdealGas {
203                rho0: 1000.0,
204                cs: 100.0,
205            },
206        };
207
208        // Compression: J < 1 (volume decreased)
209        let f = Mat3::identity() * 0.9; // 10% linear compression
210        let j = 0.9_f64.powi(3); // J ≈ 0.729
211
212        let stress = mat.compute_stress(&f, j);
213
214        // For ideal gas: p = rho0 * cs^2 * (J - 1)
215        // J < 1 means p < 0 (tension)
216        // Stress = -p * J * F^-T, so negative p gives positive trace
217        // Actually for compression (J<1), we get positive pressure in physics
218        // but the formula gives negative p, and then stress = -p*J*F^-T
219        // Let's just check that stress is computed (not zero)
220        assert!(stress.norm() > 1.0);
221    }
222}