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}