Skip to main content

oxiphysics_materials/eos/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::R_UNIVERSAL;
6#[allow(unused_imports)]
7use super::functions::*;
8
9/// Rankine-Hugoniot shock jump conditions.
10///
11/// Relates shock velocity U_s to particle velocity U_p via the linear fit:
12/// U_s = c₀ + s * U_p
13#[derive(Debug, Clone, Copy)]
14pub struct ShockHugoniot {
15    /// Reference density ρ₀ (kg/m³).
16    pub rho0: f64,
17    /// Reference bulk sound speed c₀ (m/s).
18    pub c0: f64,
19    /// Linear Hugoniot slope s.
20    pub s: f64,
21    /// Reference pressure p₀ (Pa).
22    pub p0: f64,
23}
24impl ShockHugoniot {
25    /// Create a Hugoniot.
26    pub fn new(rho0: f64, c0: f64, s: f64) -> Self {
27        Self {
28            rho0,
29            c0,
30            s,
31            p0: 0.0,
32        }
33    }
34    /// Aluminium: ρ₀=2700, c₀=5240 m/s, s=1.4.
35    pub fn aluminum() -> Self {
36        Self::new(2700.0, 5240.0, 1.4)
37    }
38    /// Iron: ρ₀=7874, c₀=3574 m/s, s=1.92.
39    pub fn iron() -> Self {
40        Self::new(7874.0, 3574.0, 1.92)
41    }
42    /// Shock velocity from particle velocity.
43    pub fn shock_velocity(&self, u_p: f64) -> f64 {
44        self.c0 + self.s * u_p
45    }
46    /// Hugoniot pressure from particle velocity.
47    ///
48    /// p_H = ρ₀ * U_s * U_p
49    pub fn hugoniot_pressure(&self, u_p: f64) -> f64 {
50        let u_s = self.shock_velocity(u_p);
51        self.rho0 * u_s * u_p + self.p0
52    }
53    /// Post-shock density from particle velocity.
54    ///
55    /// ρ = ρ₀ * U_s / (U_s - U_p)
56    pub fn post_shock_density(&self, u_p: f64) -> f64 {
57        let u_s = self.shock_velocity(u_p);
58        if (u_s - u_p).abs() < 1e-10 {
59            return f64::INFINITY;
60        }
61        self.rho0 * u_s / (u_s - u_p)
62    }
63    /// Hugoniot specific internal energy from particle velocity.
64    ///
65    /// e_H = 0.5 * (p₀ + p_H) * (V₀ - V)
66    pub fn hugoniot_energy(&self, u_p: f64) -> f64 {
67        let p_h = self.hugoniot_pressure(u_p);
68        let rho_h = self.post_shock_density(u_p);
69        0.5 * (self.p0 + p_h) * (1.0 / self.rho0 - 1.0 / rho_h)
70    }
71}
72/// Vinet (Universal) equation of state for solids.
73///
74/// p(V) = 3 K₀ * x^(-2) * (1 - x) * exp(η(1-x))
75/// where x = (V/V₀)^(1/3), η = (3/2)(K₀' - 1).
76#[derive(Debug, Clone, Copy)]
77pub struct VinetEos {
78    /// Reference specific volume V₀ (m³/kg).
79    pub v0: f64,
80    /// Bulk modulus at zero pressure K₀ (Pa).
81    pub k0: f64,
82    /// Pressure derivative K₀'.
83    pub k0_prime: f64,
84}
85impl VinetEos {
86    /// Create a Vinet EOS.
87    pub fn new(v0: f64, k0: f64, k0_prime: f64) -> Self {
88        Self { v0, k0, k0_prime }
89    }
90    /// Diamond (cubic): V₀ = 1/3515, K₀ = 443 GPa, K₀' = 3.6.
91    pub fn diamond() -> Self {
92        Self::new(1.0 / 3515.0, 443.0e9, 3.6)
93    }
94    /// Copper: V₀ = 1/8960, K₀ = 136.7 GPa, K₀' = 5.3.
95    pub fn copper() -> Self {
96        Self::new(1.0 / 8960.0, 136.7e9, 5.3)
97    }
98    /// Pressure from specific volume.
99    pub fn pressure_from_volume(&self, v: f64) -> f64 {
100        let x = (v / self.v0).powf(1.0 / 3.0);
101        if (x - 1.0).abs() < 1e-15 {
102            return 0.0;
103        }
104        let eta = 1.5 * (self.k0_prime - 1.0);
105        3.0 * self.k0 * (1.0 - x) / (x * x) * (eta * (1.0 - x)).exp()
106    }
107    /// Volume from pressure.
108    pub fn volume(&self, pressure: f64) -> f64 {
109        let mut lo = self.v0 * 0.3;
110        let mut hi = self.v0 * 2.0;
111        for _ in 0..80 {
112            let mid = 0.5 * (lo + hi);
113            if self.pressure_from_volume(mid) > pressure {
114                lo = mid;
115            } else {
116                hi = mid;
117            }
118        }
119        0.5 * (lo + hi)
120    }
121}
122/// Jones-Wilkins-Lee equation of state for detonation products.
123///
124/// p = A*(1 - ω/(R1*v))*exp(-R1*v) + B*(1 - ω/(R2*v))*exp(-R2*v) + ω*e/v
125///
126/// where v = ρ₀/ρ (relative specific volume = 1/η).
127#[derive(Debug, Clone, Copy)]
128#[allow(dead_code)]
129pub struct JwlEos {
130    /// JWL coefficient A (Pa).
131    pub big_a: f64,
132    /// JWL coefficient B (Pa).
133    pub big_b: f64,
134    /// JWL constant R1.
135    pub r1: f64,
136    /// JWL constant R2.
137    pub r2: f64,
138    /// Grüneisen-like constant ω.
139    pub omega: f64,
140    /// Reference detonation energy E0 (J/m³).
141    pub e0: f64,
142    /// Reference density ρ₀ (kg/m³).
143    pub rho0: f64,
144}
145#[allow(dead_code)]
146impl JwlEos {
147    /// Create a JWL EOS.
148    #[allow(clippy::too_many_arguments)]
149    pub fn new(big_a: f64, big_b: f64, r1: f64, r2: f64, omega: f64, e0: f64, rho0: f64) -> Self {
150        Self {
151            big_a,
152            big_b,
153            r1,
154            r2,
155            omega,
156            e0,
157            rho0,
158        }
159    }
160    /// TNT parameters (approximate literature values).
161    pub fn tnt() -> Self {
162        Self::new(3.712e11, 3.231e9, 4.15, 0.95, 0.30, 7.0e9, 1630.0)
163    }
164    /// Pressure from density `rho` (kg/m³) and specific internal energy `e` (J/m³).
165    ///
166    /// v = ρ₀/ρ (relative volume)
167    /// p = A*(1 - ω/(R1*v))*exp(-R1*v) + B*(1 - ω/(R2*v))*exp(-R2*v) + ω*e/v
168    pub fn pressure(&self, rho: f64, e: f64) -> f64 {
169        let v = self.rho0 / rho.max(1e-30);
170        let term1 = self.big_a * (1.0 - self.omega / (self.r1 * v)) * (-self.r1 * v).exp();
171        let term2 = self.big_b * (1.0 - self.omega / (self.r2 * v)) * (-self.r2 * v).exp();
172        let term3 = self.omega * e / v;
173        term1 + term2 + term3
174    }
175}
176/// Polynomial EOS commonly used in hydrocode simulations.
177///
178/// p = a₁μ + a₂μ² + a₃μ³ + (b₀ + b₁μ + b₂μ²)ρe  (compressed)
179/// p = t₁μ + t₂μ²                                  (expanded, e<0)
180/// where μ = ρ/ρ₀ - 1.
181#[derive(Debug, Clone, Copy)]
182pub struct PolynomialEos {
183    /// Reference density ρ₀ (kg/m³).
184    pub rho0: f64,
185    /// Compression coefficients a₁, a₂, a₃ (Pa).
186    pub a: [f64; 3],
187    /// Energy coupling b₀, b₁, b₂.
188    pub b: [f64; 3],
189    /// Tension coefficients t₁, t₂ (Pa).
190    pub t: [f64; 2],
191}
192impl PolynomialEos {
193    /// Create a polynomial EOS.
194    pub fn new(rho0: f64, a: [f64; 3], b: [f64; 3], t: [f64; 2]) -> Self {
195        Self { rho0, a, b, t }
196    }
197    /// Compression μ = ρ/ρ₀ - 1.
198    pub fn mu(&self, density: f64) -> f64 {
199        density / self.rho0 - 1.0
200    }
201    /// Pressure from density and energy.
202    pub fn pressure_energy(&self, density: f64, energy: f64) -> f64 {
203        let mu = self.mu(density);
204        if mu >= 0.0 {
205            let poly = self.a[0] * mu + self.a[1] * mu * mu + self.a[2] * mu * mu * mu;
206            let energy_term = (self.b[0] + self.b[1] * mu + self.b[2] * mu * mu) * density * energy;
207            poly + energy_term
208        } else {
209            self.t[0] * mu + self.t[1] * mu * mu
210        }
211    }
212}
213/// Volume-dependent Grüneisen parameter model.
214///
215/// Γ(V) = Γ₀ * (V/V₀)^q  (power-law, common approximation).
216#[derive(Debug, Clone, Copy)]
217pub struct GruneisenParameter {
218    /// Grüneisen parameter at reference volume.
219    pub gamma_0: f64,
220    /// Reference specific volume V₀.
221    pub v0: f64,
222    /// Exponent q (often taken as 1 or 2/3).
223    pub q: f64,
224}
225impl GruneisenParameter {
226    /// Create a Grüneisen parameter model.
227    pub fn new(gamma_0: f64, v0: f64, q: f64) -> Self {
228        Self { gamma_0, v0, q }
229    }
230    /// Evaluate Γ(V).
231    pub fn evaluate(&self, v: f64) -> f64 {
232        self.gamma_0 * (v / self.v0).powf(self.q)
233    }
234    /// Thermal pressure contribution: p_th = Γ(V) * ρ * cv * (T - T₀).
235    pub fn thermal_pressure(&self, density: f64, cv: f64, temperature: f64, t0: f64) -> f64 {
236        let v = 1.0 / density;
237        self.evaluate(v) * density * cv * (temperature - t0)
238    }
239}
240/// Peng-Robinson equation of state.
241///
242/// P = R*T/(V_m - b) - a(T) / \[V_m*(V_m + b) + b*(V_m - b)\]
243#[derive(Debug, Clone, Copy)]
244#[allow(dead_code)]
245pub struct PengRobinson {
246    /// Critical temperature Tc (K).
247    pub tc: f64,
248    /// Critical pressure Pc (Pa).
249    pub pc: f64,
250    /// Acentric factor ω.
251    pub omega: f64,
252}
253#[allow(dead_code)]
254impl PengRobinson {
255    /// Create a Peng-Robinson EOS.
256    pub fn new(tc: f64, pc: f64, omega: f64) -> Self {
257        Self { tc, pc, omega }
258    }
259    /// PR `b` parameter: b = 0.07780 * R * Tc / Pc
260    pub fn b_param(&self) -> f64 {
261        0.07780 * R_UNIVERSAL * self.tc / self.pc
262    }
263    /// PR `a` parameter at critical: a_c = 0.45724 * R² * Tc² / Pc
264    pub fn a_critical(&self) -> f64 {
265        0.45724 * R_UNIVERSAL * R_UNIVERSAL * self.tc * self.tc / self.pc
266    }
267    /// α(T) correction function for Peng-Robinson.
268    ///
269    /// α = \[1 + κ*(1 - sqrt(T/Tc))\]²
270    /// κ = 0.37464 + 1.54226*ω - 0.26992*ω²
271    pub fn acentric_factor_correction(&self, t: f64) -> f64 {
272        let kappa = 0.37464 + 1.54226 * self.omega - 0.26992 * self.omega * self.omega;
273
274        (1.0 + kappa * (1.0 - (t / self.tc).sqrt())).powi(2)
275    }
276    /// Temperature-dependent `a(T) = a_c * α(T)`.
277    pub fn a_param(&self, t: f64) -> f64 {
278        self.a_critical() * self.acentric_factor_correction(t)
279    }
280    /// Pressure at molar volume `v_mol` and temperature `t`.
281    pub fn pressure(&self, t: f64, v_mol: f64) -> f64 {
282        let b = self.b_param();
283        let a_t = self.a_param(t);
284        if (v_mol - b).abs() < 1e-20 {
285            return f64::INFINITY;
286        }
287        R_UNIVERSAL * t / (v_mol - b) - a_t / (v_mol * (v_mol + b) + b * (v_mol - b))
288    }
289}
290/// Murnaghan equation of state (first-order pressure derivative of bulk modulus).
291///
292/// P(V) = (K₀/n) * \[ (V₀/V)^n − 1 \]
293///
294/// This is the original Murnaghan 1944 form, simpler than Birch-Murnaghan.
295#[derive(Debug, Clone, Copy)]
296#[allow(dead_code)]
297pub struct MurnaghanEos {
298    /// Reference specific volume V₀ (m³/kg).
299    pub v0: f64,
300    /// Bulk modulus at zero pressure K₀ (Pa).
301    pub k0: f64,
302    /// Pressure derivative n = K₀' (dimensionless, typically 3–5).
303    pub n: f64,
304}
305#[allow(dead_code)]
306impl MurnaghanEos {
307    /// Create a Murnaghan EOS.
308    pub fn new(v0: f64, k0: f64, n: f64) -> Self {
309        Self { v0, k0, n }
310    }
311    /// Aluminium: V₀ = 1/2700, K₀ = 76 GPa, n = 4.5.
312    pub fn aluminum() -> Self {
313        Self::new(1.0 / 2700.0, 76.0e9, 4.5)
314    }
315    /// Iron: V₀ = 1/7874, K₀ = 170 GPa, n = 4.9.
316    pub fn iron() -> Self {
317        Self::new(1.0 / 7874.0, 170.0e9, 4.9)
318    }
319    /// Pressure from specific volume V (m³/kg).
320    ///
321    /// P = (K₀/n) * \[(V₀/V)^n − 1\]
322    pub fn pressure_from_volume(&self, v: f64) -> f64 {
323        if self.n.abs() < f64::EPSILON {
324            return 0.0;
325        }
326        (self.k0 / self.n) * ((self.v0 / v).powf(self.n) - 1.0)
327    }
328    /// Volume from pressure (analytical inversion).
329    ///
330    /// V = V₀ / (1 + n*P/K₀)^(1/n)
331    pub fn volume_from_pressure(&self, pressure: f64) -> f64 {
332        let x = 1.0 + self.n * pressure / self.k0;
333        if x <= 0.0 {
334            return self.v0 * 0.01;
335        }
336        self.v0 / x.powf(1.0 / self.n)
337    }
338    /// Bulk modulus at volume V: K(V) = K₀ * (V₀/V)^n.
339    pub fn bulk_modulus(&self, v: f64) -> f64 {
340        self.k0 * (self.v0 / v).powf(self.n)
341    }
342}
343/// Tait equation of state in bulk-modulus / isothermal form.
344///
345/// Often used for water and other liquids:
346/// P = K0/K0' * \[ (ρ/ρ0)^K0' - 1 \]
347///
348/// Different from `TaitEos` which uses the c₀-based SPH form.
349#[derive(Debug, Clone, Copy)]
350#[allow(dead_code)]
351pub struct TaitBulkEos {
352    /// Reference bulk modulus K₀ (Pa).
353    pub k0: f64,
354    /// Pressure derivative of bulk modulus K₀' (dimensionless).
355    pub k0_prime: f64,
356    /// Reference density ρ₀ (kg/m³).
357    pub rho0: f64,
358}
359#[allow(dead_code)]
360impl TaitBulkEos {
361    /// Create a Tait bulk-modulus EOS.
362    pub fn new(k0: f64, k0_prime: f64, rho0: f64) -> Self {
363        Self { k0, k0_prime, rho0 }
364    }
365    /// Water at 300 K: K₀ = 2.2 GPa, K₀' = 6.0, ρ₀ = 997 kg/m³.
366    pub fn water() -> Self {
367        Self::new(2.2e9, 6.0, 997.0)
368    }
369    /// Pressure from density ρ (kg/m³).
370    ///
371    /// P = K0/K0' * \[ (ρ/ρ0)^K0' - 1 \]
372    ///
373    /// Returns 0 at reference density.
374    pub fn pressure(&self, rho: f64) -> f64 {
375        let ratio = rho / self.rho0;
376        self.k0 / self.k0_prime * (ratio.powf(self.k0_prime) - 1.0)
377    }
378    /// Sound speed: c = sqrt(K₀ * (ρ/ρ₀)^(K₀'-1) / ρ₀) \[approx\].
379    #[allow(dead_code)]
380    pub fn sound_speed(&self, rho: f64) -> f64 {
381        let ratio = rho / self.rho0;
382        (self.k0 * ratio.powf(self.k0_prime - 1.0) / self.rho0).sqrt()
383    }
384}
385/// Ideal gas equation of state: p = ρ R_specific T.
386#[derive(Debug, Clone, Copy)]
387pub struct IdealGasEos {
388    /// Adiabatic index γ = Cp/Cv.
389    pub gamma: f64,
390    /// Specific gas constant R_specific (J/(kg·K)).
391    pub specific_gas_constant: f64,
392}
393impl IdealGasEos {
394    /// Create a new ideal gas EOS.
395    pub fn new(gamma: f64, specific_gas_constant: f64) -> Self {
396        Self {
397            gamma,
398            specific_gas_constant,
399        }
400    }
401    /// Dry air (γ=1.4, R=287 J/(kg·K)).
402    pub fn air() -> Self {
403        Self::new(1.4, 287.0)
404    }
405    /// Diatomic hydrogen (γ≈1.4, R=4157 J/(kg·K)).
406    pub fn hydrogen() -> Self {
407        Self::new(1.4, 4157.0)
408    }
409    /// Compute pressure at a given density and temperature.
410    pub fn pressure_at_temperature(&self, density: f64, temperature: f64) -> f64 {
411        density * self.specific_gas_constant * temperature
412    }
413    /// Temperature from density and pressure.
414    pub fn temperature(&self, density: f64, pressure: f64) -> f64 {
415        if density.abs() < f64::EPSILON {
416            return 0.0;
417        }
418        pressure / (density * self.specific_gas_constant)
419    }
420}
421/// 3rd-order Birch-Murnaghan equation of state for solids.
422///
423/// p(V) = (3/2) * K₀ * \[ (V₀/V)^(7/3) - (V₀/V)^(5/3) \]
424///          * { 1 + (3/4)*(K₀' - 4)*\[ (V₀/V)^(2/3) - 1 \] }
425#[derive(Debug, Clone, Copy)]
426pub struct BirchMurnaghan3Eos {
427    /// Reference volume V₀ (m³/kg, i.e. specific volume).
428    pub v0: f64,
429    /// Isothermal bulk modulus K₀ at zero pressure (Pa).
430    pub k0: f64,
431    /// Pressure derivative of K₀: dK/dP at zero pressure.
432    pub k0_prime: f64,
433}
434impl BirchMurnaghan3Eos {
435    /// Create a 3rd-order BM EOS.
436    pub fn new(v0: f64, k0: f64, k0_prime: f64) -> Self {
437        Self { v0, k0, k0_prime }
438    }
439    /// Iron at ambient conditions (approximate).
440    pub fn iron() -> Self {
441        Self::new(1.0 / 7874.0, 170.0e9, 4.9)
442    }
443    /// MgO (periclase): V₀ = 1/3580, K₀ = 160.2 GPa, K₀' = 3.99.
444    pub fn mgo() -> Self {
445        Self::new(1.0 / 3580.0, 160.2e9, 3.99)
446    }
447    /// Pressure from specific volume V (m³/kg).
448    pub fn pressure_from_volume(&self, v: f64) -> f64 {
449        let x = self.v0 / v;
450        let x23 = x.powf(2.0 / 3.0);
451        let x73 = x.powf(7.0 / 3.0);
452        let x53 = x.powf(5.0 / 3.0);
453        let correction = 1.0 + 0.75 * (self.k0_prime - 4.0) * (x23 - 1.0);
454        1.5 * self.k0 * (x73 - x53) * correction
455    }
456    /// Volume from pressure (numerical bisection).
457    pub fn volume(&self, pressure: f64) -> f64 {
458        let mut lo = self.v0 * 0.1;
459        let mut hi = self.v0 * 10.0;
460        for _ in 0..80 {
461            let mid = 0.5 * (lo + hi);
462            if self.pressure_from_volume(mid) > pressure {
463                lo = mid;
464            } else {
465                hi = mid;
466            }
467        }
468        0.5 * (lo + hi)
469    }
470    /// Isothermal bulk modulus K_T(V) at volume V.
471    pub fn bulk_modulus(&self, v: f64) -> f64 {
472        let h = v * 1e-6;
473        let dp = self.pressure_from_volume(v - h) - self.pressure_from_volume(v + h);
474        v * dp / (2.0 * h)
475    }
476}
477/// Van der Waals equation of state in molar form.
478///
479/// (P + a/V_m²)(V_m - b) = R * T
480/// P = R*T/(V_m - b) - a/V_m²
481///
482/// Different from `VanDerWaalsEos` (which works with mass density).
483#[derive(Debug, Clone, Copy)]
484#[allow(dead_code)]
485pub struct VanDerWaals {
486    /// Attractive interaction constant a (Pa·m⁶/mol²).
487    pub a: f64,
488    /// Excluded volume constant b (m³/mol).
489    pub b: f64,
490    /// Universal gas constant R (J/(mol·K)).
491    pub r_gas: f64,
492}
493#[allow(dead_code)]
494impl VanDerWaals {
495    /// Create a new VdW EOS.  `R` is the universal gas constant (8.314 J/mol/K).
496    pub fn new(a: f64, b: f64, r_gas: f64) -> Self {
497        Self { a, b, r_gas }
498    }
499    /// Standard instance with R = 8.314 J/(mol·K).
500    pub fn with_r(a: f64, b: f64) -> Self {
501        Self::new(a, b, 8.314_462_618)
502    }
503    /// Pressure: P = R*T/(V_m - b) - a/V_m²
504    ///
505    /// # Arguments
506    /// * `t`   - Temperature (K).
507    /// * `v`   - Molar volume (m³/mol).
508    /// * `_n`  - Amount of substance (mol) — unused in molar form, kept for API.
509    pub fn pressure(&self, t: f64, v: f64, _n: f64) -> f64 {
510        if (v - self.b).abs() < 1e-20 {
511            return f64::INFINITY;
512        }
513        self.r_gas * t / (v - self.b) - self.a / (v * v)
514    }
515    /// Compressibility factor Z = P V_m / (R T).
516    ///
517    /// # Arguments
518    /// * `t` - Temperature (K).
519    /// * `p` - Pressure (Pa).
520    /// * `n` - Amount (mol), used as hint to select molar volume root via bisection.
521    pub fn compressibility(&self, t: f64, p: f64, _n: f64) -> f64 {
522        let rt = self.r_gas * t;
523        if rt.abs() < 1e-30 {
524            return 0.0;
525        }
526        let mut lo = self.b + 1e-10;
527        let mut hi = rt / p.max(1.0) * 100.0 + 1.0;
528        hi = hi.max(lo * 1000.0);
529        for _ in 0..80 {
530            let mid = 0.5 * (lo + hi);
531            if self.pressure(t, mid, 1.0) > p {
532                lo = mid;
533            } else {
534                hi = mid;
535            }
536        }
537        let v_m = 0.5 * (lo + hi);
538        p * v_m / rt
539    }
540}
541/// Mie-Grüneisen equation of state for shock-compressed solids.
542///
543/// p = p_H + Γ*ρ*(e - e_H)
544/// where p_H and e_H are the Hugoniot pressure and energy.
545#[derive(Debug, Clone, Copy)]
546pub struct MieGruneisenEos {
547    /// Reference density ρ₀ (kg/m³).
548    pub rho0: f64,
549    /// Grüneisen parameter Γ (dimensionless).
550    pub gamma_0: f64,
551    /// Linear shock velocity coefficient c₀ (m/s).
552    pub c0: f64,
553    /// Slope of shock velocity - particle velocity (S parameter).
554    pub s: f64,
555}
556impl MieGruneisenEos {
557    /// Create a Mie-Grüneisen EOS.
558    pub fn new(rho0: f64, gamma_0: f64, c0: f64, s: f64) -> Self {
559        Self {
560            rho0,
561            gamma_0,
562            c0,
563            s,
564        }
565    }
566    /// Copper (ρ₀=8930, Γ=2.0, c₀=3933, S=1.5).
567    pub fn copper() -> Self {
568        Self::new(8930.0, 2.0, 3933.0, 1.5)
569    }
570    /// Aluminium (ρ₀=2785, Γ=2.0, c₀=5386, S=1.339).
571    pub fn aluminum() -> Self {
572        Self::new(2785.0, 2.0, 5386.0, 1.339)
573    }
574    /// Iron (ρ₀=7896, Γ=1.81, c₀=3574, S=1.92).
575    pub fn iron() -> Self {
576        Self::new(7896.0, 1.81, 3574.0, 1.92)
577    }
578    /// Volumetric compression η = 1 - ρ₀/ρ.
579    pub fn compression(&self, density: f64) -> f64 {
580        1.0 - self.rho0 / density
581    }
582    /// Hugoniot pressure (reference Rankine-Hugoniot curve).
583    pub fn hugoniot_pressure(&self, density: f64) -> f64 {
584        let mu = density / self.rho0 - 1.0;
585        if mu <= 0.0 {
586            return 0.0;
587        }
588        let denom = (1.0 - self.s * mu).powi(2);
589        if denom.abs() < f64::EPSILON {
590            return 0.0;
591        }
592        self.rho0 * self.c0 * self.c0 * mu / denom
593    }
594    /// Pressure from density and specific internal energy (J/kg).
595    pub fn pressure_from_energy(&self, density: f64, energy: f64) -> f64 {
596        let ph = self.hugoniot_pressure(density);
597        let eh = if density > self.rho0 {
598            ph * 0.5 * (1.0 / self.rho0 - 1.0 / density)
599        } else {
600            0.0
601        };
602        ph + self.gamma_0 * density * (energy - eh)
603    }
604}
605/// Tait equation of state, commonly used for weakly-compressible fluids (e.g. water in SPH).
606///
607/// pressure = B * ((ρ/ρ₀)^γ − 1) + p_ref
608/// where B = ρ₀ c₀² / γ
609#[derive(Debug, Clone, Copy)]
610pub struct TaitEos {
611    /// Reference density ρ₀ (kg/m³).
612    pub reference_density: f64,
613    /// Reference pressure p_ref (Pa).
614    pub reference_pressure: f64,
615    /// Reference speed of sound c₀ (m/s).
616    pub speed_of_sound: f64,
617    /// Polytropic exponent γ (typically 7 for water).
618    pub gamma: f64,
619}
620impl TaitEos {
621    /// Create a new Tait equation of state.
622    pub fn new(
623        reference_density: f64,
624        reference_pressure: f64,
625        speed_of_sound: f64,
626        gamma: f64,
627    ) -> Self {
628        Self {
629            reference_density,
630            reference_pressure,
631            speed_of_sound,
632            gamma,
633        }
634    }
635    /// Water at standard conditions: ρ₀=1000, p_ref=0, c₀=1500, γ=7.
636    pub fn water() -> Self {
637        Self::new(1000.0, 0.0, 1500.0, 7.0)
638    }
639    /// B coefficient = ρ₀ c₀² / γ.
640    pub fn b_coefficient(&self) -> f64 {
641        self.reference_density * self.speed_of_sound.powi(2) / self.gamma
642    }
643}
644/// Redlich-Kwong equation of state.
645///
646/// P = R*T/(V_m - b) - a / (T^0.5 * V_m * (V_m + b))
647///
648/// Uses R = 8.314 J/(mol·K).
649#[derive(Debug, Clone, Copy)]
650#[allow(dead_code)]
651pub struct RedlichKwong {
652    /// RK attraction parameter a (Pa·m⁶·K^0.5/mol²).
653    pub a: f64,
654    /// RK co-volume parameter b (m³/mol).
655    pub b: f64,
656}
657#[allow(dead_code)]
658impl RedlichKwong {
659    /// Create a Redlich-Kwong EOS.
660    pub fn new(a: f64, b: f64) -> Self {
661        Self { a, b }
662    }
663    /// Compute a and b from critical properties.
664    ///
665    /// a = 0.42748 * R² * Tc^2.5 / Pc
666    /// b = 0.08664 * R * Tc / Pc
667    pub fn from_critical(tc: f64, pc: f64) -> Self {
668        let a = 0.42748 * R_UNIVERSAL * R_UNIVERSAL * tc.powf(2.5) / pc;
669        let b = 0.08664 * R_UNIVERSAL * tc / pc;
670        Self { a, b }
671    }
672    /// Pressure at molar volume `v_mol` and temperature `t`.
673    pub fn pressure(&self, t: f64, v_mol: f64) -> f64 {
674        if (v_mol - self.b).abs() < 1e-20 {
675            return f64::INFINITY;
676        }
677        R_UNIVERSAL * t / (v_mol - self.b) - self.a / (t.sqrt() * v_mol * (v_mol + self.b))
678    }
679}
680/// Van der Waals equation of state for real gases.
681///
682/// (p + a*ρ²/M²)(1/ρ - b/M) = R*T/M
683/// where a and b are van der Waals constants.
684#[derive(Debug, Clone, Copy)]
685pub struct VanDerWaalsEos {
686    /// Attractive interaction constant a (Pa·m⁶/mol²).
687    pub a: f64,
688    /// Excluded volume constant b (m³/mol).
689    pub b: f64,
690    /// Molar mass M (kg/mol).
691    pub molar_mass: f64,
692    /// Universal gas constant R (8.314 J/(mol·K)).
693    pub r_gas: f64,
694}
695impl VanDerWaalsEos {
696    /// Create a van der Waals EOS.
697    pub fn new(a: f64, b: f64, molar_mass: f64) -> Self {
698        Self {
699            a,
700            b,
701            molar_mass,
702            r_gas: 8.314,
703        }
704    }
705    /// Carbon dioxide (a=0.3658, b=42.9e-6, M=0.044).
706    pub fn co2() -> Self {
707        Self::new(0.3658, 42.9e-6, 0.044)
708    }
709    /// Water vapour (a=0.5537, b=30.5e-6, M=0.018).
710    pub fn water_vapor() -> Self {
711        Self::new(0.5537, 30.5e-6, 0.018)
712    }
713    /// Nitrogen (a=0.1370, b=38.7e-6, M=0.028).
714    pub fn nitrogen() -> Self {
715        Self::new(0.1370, 38.7e-6, 0.028)
716    }
717    /// Pressure at given density and temperature.
718    pub fn pressure_at_temperature(&self, density: f64, temperature: f64) -> f64 {
719        let specific_volume = 1.0 / density;
720        let molar_volume = specific_volume * self.molar_mass;
721        if (molar_volume - self.b).abs() < 1e-15 {
722            return f64::INFINITY;
723        }
724        self.r_gas * temperature / (molar_volume - self.b) - self.a / (molar_volume * molar_volume)
725    }
726}
727/// Tillotson equation of state for hypervelocity impact and planetary science.
728///
729/// Used for rocks, minerals, and ices under extreme shock conditions.
730#[derive(Debug, Clone, Copy)]
731pub struct TillotsonEos {
732    /// Reference density ρ₀ (kg/m³).
733    pub rho0: f64,
734    /// Reference internal energy E₀ (J/kg).
735    pub e0: f64,
736    /// Cold pressure coefficient a.
737    pub a: f64,
738    /// Cold pressure coefficient b.
739    pub b: f64,
740    /// Bulk modulus-like coefficient A (Pa).
741    pub big_a: f64,
742    /// Bulk modulus-like coefficient B (Pa).
743    pub big_b: f64,
744    /// Incipient vaporization energy E_iv (J/kg).
745    pub e_iv: f64,
746    /// Complete vaporization energy E_cv (J/kg).
747    pub e_cv: f64,
748    /// Expansion parameters α and β.
749    pub alpha: f64,
750    /// Expansion parameter β.
751    pub beta: f64,
752}
753impl TillotsonEos {
754    /// Create a Tillotson EOS.
755    #[allow(clippy::too_many_arguments)]
756    pub fn new(
757        rho0: f64,
758        e0: f64,
759        a: f64,
760        b: f64,
761        big_a: f64,
762        big_b: f64,
763        e_iv: f64,
764        e_cv: f64,
765        alpha: f64,
766        beta: f64,
767    ) -> Self {
768        Self {
769            rho0,
770            e0,
771            a,
772            b,
773            big_a,
774            big_b,
775            e_iv,
776            e_cv,
777            alpha,
778            beta,
779        }
780    }
781    /// Granite (approximate parameters).
782    pub fn granite() -> Self {
783        Self::new(
784            2680.0, 16.0e6, 0.5, 1.3, 18.0e9, 18.0e9, 3.5e6, 18.0e6, 5.0, 5.0,
785        )
786    }
787    /// Basalt (approximate parameters).
788    pub fn basalt() -> Self {
789        Self::new(
790            2860.0, 49.0e6, 0.49, 1.39, 26.7e9, 26.7e9, 4.72e6, 14.0e6, 5.0, 5.0,
791        )
792    }
793    /// Compression ratio η = ρ/ρ₀.
794    pub fn eta(&self, density: f64) -> f64 {
795        density / self.rho0
796    }
797    /// Pressure in compressed/hot region.
798    pub fn pressure_compressed(&self, density: f64, energy: f64) -> f64 {
799        let eta = self.eta(density);
800        let mu = eta - 1.0;
801        let z = energy / (self.e0 * eta * eta);
802        (self.a + self.b / (z + 1.0)) * density * energy + self.big_a * mu + self.big_b * mu * mu
803    }
804    /// Pressure in expanded region (gas-like).
805    pub fn pressure_expanded(&self, density: f64, energy: f64) -> f64 {
806        let eta = self.eta(density);
807        let mu = eta - 1.0;
808        let z = energy / (self.e0 * eta * eta);
809        let term1 = self.a * density * energy;
810        let term2 = (self.b * density * energy / (z + 1.0)
811            + self.big_a * mu * (-self.beta * (1.0 / eta - 1.0)).exp())
812            * (-self.alpha * (1.0 / eta - 1.0).powi(2)).exp();
813        term1 + term2
814    }
815    /// Full pressure from density and specific internal energy.
816    pub fn pressure_from_energy(&self, density: f64, energy: f64) -> f64 {
817        let eta = self.eta(density);
818        if eta >= 1.0 || energy <= self.e_iv {
819            self.pressure_compressed(density, energy)
820        } else if energy >= self.e_cv {
821            self.pressure_expanded(density, energy)
822        } else {
823            let t = (energy - self.e_iv) / (self.e_cv - self.e_iv);
824            let pc = self.pressure_compressed(density, energy);
825            let pe = self.pressure_expanded(density, energy);
826            (1.0 - t) * pc + t * pe
827        }
828    }
829}
830/// Soave-Redlich-Kwong equation of state.
831///
832/// P = R*T/(V_m - b) - a*α(T) / \[V_m*(V_m + b)\]
833/// α(T) = \[1 + m*(1 - sqrt(T/Tc))\]²
834/// m = 0.480 + 1.574*ω - 0.176*ω²
835#[derive(Debug, Clone, Copy)]
836#[allow(dead_code)]
837pub struct SoaveRedlichKwong {
838    /// SRK attraction parameter a (Pa·m⁶/mol²).
839    pub a: f64,
840    /// SRK co-volume b (m³/mol).
841    pub b: f64,
842    /// Acentric factor ω.
843    pub omega: f64,
844    /// Critical temperature Tc (K).
845    pub tc: f64,
846}
847#[allow(dead_code)]
848impl SoaveRedlichKwong {
849    /// Create a Soave-Redlich-Kwong EOS.
850    pub fn new(a: f64, b: f64, omega: f64, tc: f64) -> Self {
851        Self { a, b, omega, tc }
852    }
853    /// Construct from critical properties.
854    ///
855    /// a = 0.42748 * R² * Tc² / Pc
856    /// b = 0.08664 * R * Tc / Pc
857    pub fn from_critical(tc: f64, pc: f64, omega: f64) -> Self {
858        let a = 0.42748 * R_UNIVERSAL * R_UNIVERSAL * tc * tc / pc;
859        let b = 0.08664 * R_UNIVERSAL * tc / pc;
860        Self { a, b, omega, tc }
861    }
862    /// α(T) correction: α = \[1 + m*(1 - sqrt(T/Tc))\]²
863    fn alpha(&self, t: f64) -> f64 {
864        let m = 0.480 + 1.574 * self.omega - 0.176 * self.omega * self.omega;
865        (1.0 + m * (1.0 - (t / self.tc).sqrt())).powi(2)
866    }
867    /// Pressure at molar volume `v_mol` and temperature `t`.
868    pub fn pressure(&self, t: f64, v_mol: f64) -> f64 {
869        let a_t = self.a * self.alpha(t);
870        if (v_mol - self.b).abs() < 1e-20 {
871            return f64::INFINITY;
872        }
873        R_UNIVERSAL * t / (v_mol - self.b) - a_t / (v_mol * (v_mol + self.b))
874    }
875}
876/// Stiffened gas EOS: p + p_inf = (γ-1) ρ e.
877///
878/// Generalises ideal gas to account for liquid-like stiffness.
879/// Used for water shock and cavitation in underwater explosions.
880#[derive(Debug, Clone, Copy)]
881pub struct StiffenedGasEos {
882    /// Adiabatic index γ.
883    pub gamma: f64,
884    /// Stiffness pressure p_inf (Pa). For ideal gas, p_inf = 0.
885    pub p_inf: f64,
886    /// Reference density ρ₀ (kg/m³).
887    pub reference_density: f64,
888    /// Reference energy e₀ (J/kg).
889    pub reference_energy: f64,
890}
891impl StiffenedGasEos {
892    /// Create a new stiffened gas EOS.
893    pub fn new(gamma: f64, p_inf: f64, reference_density: f64, reference_energy: f64) -> Self {
894        Self {
895            gamma,
896            p_inf,
897            reference_density,
898            reference_energy,
899        }
900    }
901    /// Water (Noble-Abel stiffened gas): γ=6.59, p_inf=4.02e8 Pa.
902    pub fn water() -> Self {
903        Self::new(6.59, 4.02e8, 1000.0, 0.0)
904    }
905    /// Air treated as stiffened gas (reduces to ideal gas with p_inf=0).
906    pub fn air() -> Self {
907        Self::new(1.4, 0.0, 1.225, 0.0)
908    }
909    /// Pressure from density and internal energy.
910    pub fn pressure_from_energy(&self, density: f64, energy: f64) -> f64 {
911        (self.gamma - 1.0) * density * (energy - self.reference_energy) - self.p_inf
912    }
913}
914/// Noble-Abel equation of state for dense propellant gases.
915///
916/// P = ρ R T / (1 - b ρ)
917///
918/// Accounts for co-volume b (repulsive hard-core interactions).
919#[derive(Debug, Clone, Copy)]
920#[allow(dead_code)]
921pub struct NobleAbelEos {
922    /// Specific gas constant R (J/(kg·K)).
923    pub r_specific: f64,
924    /// Co-volume b (m³/kg).
925    pub b_covolume: f64,
926}
927#[allow(dead_code)]
928impl NobleAbelEos {
929    /// Create a Noble-Abel EOS.
930    pub fn new(r_specific: f64, b_covolume: f64) -> Self {
931        Self {
932            r_specific,
933            b_covolume,
934        }
935    }
936    /// Gunpowder propellant gases (approximate).
937    pub fn propellant() -> Self {
938        Self::new(290.0, 0.001)
939    }
940    /// Pressure at density and temperature.
941    pub fn pressure_at_temperature(&self, density: f64, temperature: f64) -> f64 {
942        let denom = 1.0 - self.b_covolume * density;
943        if denom <= 0.0 {
944            return f64::INFINITY;
945        }
946        density * self.r_specific * temperature / denom
947    }
948    /// Density from pressure and temperature.
949    pub fn density_at_temperature(&self, pressure: f64, temperature: f64) -> f64 {
950        let rt = self.r_specific * temperature;
951        if rt.abs() < 1e-30 {
952            return 0.0;
953        }
954        pressure / (rt + pressure * self.b_covolume)
955    }
956}
957/// Simple P-V-T EOS combining Murnaghan cold compression with Grüneisen thermal.
958///
959/// p(V,T) = p_cold(V) + Γ(V)/V * cv * (T - T₀)
960pub struct PvtEos {
961    /// Cold compression EOS (Birch-Murnaghan 3rd order).
962    pub cold_eos: BirchMurnaghan3Eos,
963    /// Grüneisen parameter model.
964    pub gruneisen: GruneisenParameter,
965    /// Specific heat at constant volume cv (J/(kg·K)).
966    pub cv: f64,
967    /// Reference temperature T₀ (K).
968    pub t0: f64,
969}
970impl PvtEos {
971    /// Create a P-V-T EOS.
972    pub fn new(
973        cold_eos: BirchMurnaghan3Eos,
974        gruneisen: GruneisenParameter,
975        cv: f64,
976        t0: f64,
977    ) -> Self {
978        Self {
979            cold_eos,
980            gruneisen,
981            cv,
982            t0,
983        }
984    }
985    /// Iron P-V-T EOS (simplified).
986    pub fn iron() -> Self {
987        let cold = BirchMurnaghan3Eos::iron();
988        let v0 = cold.v0;
989        let gruneisen = GruneisenParameter::new(1.81, v0, 1.0);
990        Self::new(cold, gruneisen, 450.0, 300.0)
991    }
992    /// Pressure at specific volume V and temperature T.
993    pub fn pressure(&self, v: f64, temperature: f64) -> f64 {
994        let density = 1.0 / v;
995        let p_cold = self.cold_eos.pressure_from_volume(v);
996        let p_th = self
997            .gruneisen
998            .thermal_pressure(density, self.cv, temperature, self.t0);
999        p_cold + p_th
1000    }
1001    /// Density from pressure and temperature (numerical bisection).
1002    pub fn density(&self, pressure: f64, temperature: f64) -> f64 {
1003        let mut lo = 1.0 / (self.cold_eos.v0 * 3.0);
1004        let mut hi = 1.0 / (self.cold_eos.v0 * 0.3);
1005        for _ in 0..80 {
1006            let mid = 0.5 * (lo + hi);
1007            if self.pressure(1.0 / mid, temperature) < pressure {
1008                lo = mid;
1009            } else {
1010                hi = mid;
1011            }
1012        }
1013        0.5 * (lo + hi)
1014    }
1015}