Skip to main content

qualia_core_db/specialized_libs/engineering_analysis/
survival.rs

1// ─── Survival-engineering kernels: stress / drag / fatigue ───────────────────────
2//
3// The rapid-deployment / mobile-infrastructure scope (camper trailers, pop-up
4// habitations, harsh-environment survival). The big FEA scaffolding above is the type
5// machinery; these are the actual continuum-mechanics / fluid-dynamics / fatigue
6// COMPUTATIONS. All zero-heap (fixed-size tensors / caller slices).
7
8/// The reduced state of a 3×3 Cauchy stress tensor.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct StressState {
11    /// von Mises equivalent stress (yield criterion).
12    pub von_mises: f64,
13    /// Principal stresses σ1 ≥ σ2 ≥ σ3 (the tensor's eigenvalues).
14    pub principal: [f64; 3],
15    /// Maximum shear stress = (σ1 − σ3) / 2 (Tresca).
16    pub max_shear: f64,
17    /// Hydrostatic (mean) stress = trace / 3.
18    pub hydrostatic: f64,
19}
20
21/// Principal stresses of a symmetric 3×3 stress tensor by the closed-form
22/// (Smith 1961) eigenvalue solution, returned σ1 ≥ σ2 ≥ σ3.
23/// Principal stresses = eigenvalues of the symmetric Cauchy stress tensor, sorted
24/// descending. The closed-form symmetric-3×3 eigensolver lives once in the engine
25/// (`solvers::linear_algebra::eigen`); this marshals the tensor and calls it.
26fn principal_stresses(t: &[[f64; 3]; 3]) -> [f64; 3] {
27    let a = [
28        t[0][0], t[0][1], t[0][2], t[1][0], t[1][1], t[1][2], t[2][0], t[2][1], t[2][2],
29    ];
30    crate::solvers::linear_algebra::eigen::symmetric_eigen_3x3(&a)
31}
32
33/// Analyse a 3×3 Cauchy stress tensor (e.g. chassis shear on an off-road camper):
34/// von Mises equivalent stress, principal stresses, maximum shear, hydrostatic stress.
35pub fn cauchy_stress_analysis(tensor: &[[f64; 3]; 3]) -> StressState {
36    let (sxx, syy, szz) = (tensor[0][0], tensor[1][1], tensor[2][2]);
37    let (txy, tyz, tzx) = (tensor[0][1], tensor[1][2], tensor[2][0]);
38    let von_mises = (0.5 * ((sxx - syy).powi(2) + (syy - szz).powi(2) + (szz - sxx).powi(2))
39        + 3.0 * (txy * txy + tyz * tyz + tzx * tzx))
40        .sqrt();
41    let principal = principal_stresses(tensor);
42    StressState {
43        von_mises,
44        principal,
45        max_shear: (principal[0] - principal[2]) / 2.0,
46        hydrostatic: (sxx + syy + szz) / 3.0,
47    }
48}
49
50/// Aerodynamic drag / wind-load force (N): `F = ½·ρ·v²·C_d·A` — wind-load on a
51/// rapid-deployment structure or drag on a moving camper.
52pub fn drag_force(
53    air_density_kg_m3: f64,
54    velocity_m_s: f64,
55    drag_coefficient: f64,
56    area_m2: f64,
57) -> f64 {
58    0.5 * air_density_kg_m3 * velocity_m_s * velocity_m_s * drag_coefficient * area_m2
59}
60
61/// Reynolds number `Re = ρ·v·L / μ` — laminar/turbulent regime for the wind-load model.
62pub fn reynolds_number(
63    density: f64,
64    velocity: f64,
65    char_length_m: f64,
66    dynamic_viscosity: f64,
67) -> f64 {
68    if dynamic_viscosity == 0.0 {
69        return f64::INFINITY;
70    }
71    density * velocity * char_length_m / dynamic_viscosity
72}
73
74/// Cycles-to-failure under a constant stress amplitude via Basquin's law
75/// `σ_a = σ_f'·(2N)^b`  ⇒  `N = ½·(σ_a/σ_f')^(1/b)` (`b` is the negative fatigue
76/// strength exponent). Below the endurance behaviour this is huge (effectively
77/// infinite life). Feeds the probabilistic failure-prediction model.
78pub fn fatigue_cycles_basquin(
79    stress_amplitude: f64,
80    fatigue_strength_coeff: f64,
81    fatigue_strength_exponent: f64,
82) -> f64 {
83    if stress_amplitude <= 0.0 || fatigue_strength_coeff <= 0.0 || fatigue_strength_exponent == 0.0
84    {
85        return f64::INFINITY;
86    }
87    0.5 * (stress_amplitude / fatigue_strength_coeff).powf(1.0 / fatigue_strength_exponent)
88}
89
90/// Palmgren–Miner cumulative fatigue damage `D = Σ nᵢ/Nᵢ` over load blocks
91/// `(applied_cycles, allowable_cycles)`. Failure is predicted when `D ≥ 1`. Zero-heap.
92pub fn miner_cumulative_damage(blocks: &[(f64, f64)]) -> f64 {
93    let mut d = 0.0;
94    for &(applied, allowable) in blocks {
95        if allowable > 0.0 {
96            d += applied / allowable;
97        }
98    }
99    d
100}
101
102#[cfg(test)]
103mod survival_engineering_tests {
104    use super::*;
105
106    #[test]
107    fn uniaxial_stress_state() {
108        // Pure uniaxial tension of 100 MPa along x.
109        let t = [[100.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
110        let s = cauchy_stress_analysis(&t);
111        assert!((s.von_mises - 100.0).abs() < 1e-6);
112        assert!((s.principal[0] - 100.0).abs() < 1e-6 && s.principal[2].abs() < 1e-6);
113        assert!((s.max_shear - 50.0).abs() < 1e-6);
114        assert!((s.hydrostatic - 100.0 / 3.0).abs() < 1e-6);
115    }
116
117    #[test]
118    fn pure_shear_stress_state() {
119        // Pure shear τ = 50 → principal {50, 0, −50}, von Mises = √3·50 ≈ 86.6.
120        let t = [[0.0, 50.0, 0.0], [50.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
121        let s = cauchy_stress_analysis(&t);
122        assert!(
123            (s.von_mises - 3f64.sqrt() * 50.0).abs() < 1e-6,
124            "vm {}",
125            s.von_mises
126        );
127        assert!(
128            (s.principal[0] - 50.0).abs() < 1e-6,
129            "σ1 {}",
130            s.principal[0]
131        );
132        assert!(
133            (s.principal[2] + 50.0).abs() < 1e-6,
134            "σ3 {}",
135            s.principal[2]
136        );
137        assert!((s.max_shear - 50.0).abs() < 1e-6);
138    }
139
140    #[test]
141    fn drag_and_reynolds() {
142        // 10 m/s wind on 2 m² flat-ish panel (Cd≈1) in sea-level air (ρ=1.225).
143        assert!((drag_force(1.225, 10.0, 1.0, 2.0) - 122.5).abs() < 1e-6);
144        // Re for 1 m chord at 10 m/s in air (μ≈1.8e-5) → ~6.8e5 (turbulent).
145        let re = reynolds_number(1.225, 10.0, 1.0, 1.8e-5);
146        assert!(re > 6.0e5 && re < 7.0e5, "Re {re}");
147    }
148
149    #[test]
150    fn fatigue_life_and_cumulative_damage() {
151        // Lower stress amplitude ⇒ more cycles to failure (Basquin, b<0).
152        let n_low = fatigue_cycles_basquin(100.0, 900.0, -0.085);
153        let n_high = fatigue_cycles_basquin(300.0, 900.0, -0.085);
154        assert!(n_low > n_high, "lower stress should give longer life");
155        // Miner: two blocks each at half their allowable → D = 1.0 (failure threshold).
156        let d = miner_cumulative_damage(&[(500.0, 1000.0), (250.0, 500.0)]);
157        assert!((d - 1.0).abs() < 1e-9, "D {d}");
158        assert!(
159            miner_cumulative_damage(&[(100.0, 1000.0)]) < 1.0,
160            "safe block < 1"
161        );
162    }
163}