Skip to main content

qualia_core_db/specialized_libs/engineering_analysis/
mod.rs

1//! Engineering Analysis Library - Structural, Mechanical, and Systems Engineering Analysis
2//!
3//! This module provides high-performance engineering analysis operations leveraging Phase 2 enhancements:
4//! - Linear Algebra Library for matrix computations and finite element analysis
5//! - Physics Simulation Library for structural dynamics and thermal analysis
6//! - Hardware-Sympathetic Storage (ZNS) for zero-copy engineering data
7//! - Statistical Computing Library for reliability analysis and optimization
8
9use super::linear_algebra::LinearAlgebraLibrary;
10use super::physics_simulation::PhysicsSimulationLibrary;
11use super::statistical_computing::StatisticalComputingLibrary;
12use crate::zns_storage::ZnsZoneManager;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16
17/// Standard normal random sample via the Box–Muller transform, using two uniform
18/// draws from `rand::random()`. Returns a single N(0,1) value. Used by the Monte
19/// Carlo reliability kernel — this is NOT a hot path (engineering analysis is a
20/// planning/analysis module, not the evaluator loop), so `Vec`/`rand` are fine.
21fn standard_normal_sample() -> f64 {
22    // Draw two independent uniforms in (0, 1]; reject exact 0 to avoid log(0).
23    let mut u1: f64 = rand::random();
24    while u1 <= 0.0 {
25        u1 = rand::random();
26    }
27    let u2: f64 = rand::random();
28    let r = (-2.0 * u1.ln()).sqrt();
29    let theta = 2.0 * std::f64::consts::PI * u2;
30    r * theta.cos()
31}
32
33/// Approximate inverse of the standard normal CDF (Φ⁻¹) via the Acklam/Wichura
34/// rational approximation. Given a failure probability `p` ∈ (0, 1), returns the
35/// reliability index β = −Φ⁻¹(p). Clamps `p` away from 0/1 to keep the result
36/// finite.
37fn inverse_normal_cdf(p: f64) -> f64 {
38    let p = p.clamp(1e-12, 1.0 - 1e-12);
39    // Acklam's algorithm.
40    let a = [
41        -3.969_683_028_665_376e+01,
42        2.209_460_984_245_205e+02,
43        -2.759_285_104_469_687e+02,
44        1.383_577_518_672_69e+02,
45        -3.066_479_806_617_929e+01,
46        2.506_628_277_459_239e+00,
47    ];
48    let b = [
49        -5.447_609_879_822_406e+01,
50        1.615_858_368_580_409e+02,
51        -1.556_989_798_598_866e+02,
52        6.680_131_188_771_972e+01,
53        -1.328_068_155_288_362e+01,
54    ];
55    let c = [
56        -7.784_894_002_430_993e-03,
57        -3.223_964_580_411_365e-01,
58        -2.400_758_277_161_838e+00,
59        -2.549_732_539_349_742e+00,
60        4.374_664_141_464_968e+00,
61        2.938_163_982_698_783e+00,
62    ];
63    let d = [
64        7.784_695_709_041_462e-03,
65        3.224_671_290_700_398e-01,
66        2.445_134_137_232_851e+00,
67        3.754_408_661_907_416e+00,
68    ];
69
70    let plow = 0.02425;
71    let phigh = 1.0 - plow;
72    if p < plow {
73        let q = (-2.0 * p.ln()).sqrt();
74        (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
75            / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
76    } else if p <= phigh {
77        let q = p - 0.5;
78        let r = q * q;
79        (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
80            / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
81    } else {
82        let q = (-2.0 * (1.0 - p).ln()).sqrt();
83        -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
84            / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
85    }
86}
87
88/// Standard normal CDF Φ(x) via the Abramowitz & Stegun 7.1.26 approximation
89/// (maximum absolute error < 7.5e-8). Used to compute failure probability
90/// from the reliability index: P(fail) = Φ(−β).
91fn normal_cdf(x: f64) -> f64 {
92    // Φ(x) = ½ [1 + erf(x / √2)]
93    let z = x / std::f64::consts::SQRT_2;
94    let erf = if z >= 0.0 {
95        // erf(z) for z ≥ 0 via A&S 7.1.26
96        let t = 1.0 / (1.0 + 0.3275911 * z);
97        let poly = t
98            * (0.254829592
99                + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
100        1.0 - poly * (-z * z).exp()
101    } else {
102        // erf(-z) = -erf(z)
103        let az = -z;
104        let t = 1.0 / (1.0 + 0.3275911 * az);
105        let poly = t
106            * (0.254829592
107                + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
108        -(1.0 - poly * (-az * az).exp())
109    };
110    0.5 * (1.0 + erf)
111}
112
113/// Solve the undamped free-vibration generalized eigenproblem `K φ = ω² M φ` for a
114/// symmetric stiffness matrix `stiffness` (row-major `n×n`) and a **lumped
115/// (diagonal)** mass matrix given by its `n` positive diagonal entries `mass_diag`.
116/// Returns `(ω, φ)` pairs sorted by ascending natural angular frequency ω (rad/s).
117///
118/// Method (mass pre/post-scaling to standard form): let `M^{-1/2}` be the diagonal
119/// matrix of `1/√mᵢ`. Then `Ã = M^{-1/2} K M^{-1/2}` is symmetric and
120/// `Ã ψ = ω² ψ` with `ψ = M^{1/2} φ`. The standard symmetric eigenproblem is solved
121/// by the crate's Jacobi eigensolver
122/// (`solvers::linear_algebra::eigen::symmetric_eigen`) — **no eigen algorithm is
123/// re-derived here** — and the physical mode is recovered as `φ = M^{-1/2} ψ`.
124/// Eigenvalues that come out marginally negative from round-off are clamped to 0
125/// before the square root. Mode shapes are scaled to unit maximum component.
126fn solve_modal_eigen(
127    stiffness: &[f64],
128    mass_diag: &[f64],
129    n: usize,
130) -> Result<Vec<(f64, Vec<f64>)>, EngineeringError> {
131    if n == 0 {
132        return Err(EngineeringError::InsufficientData(
133            "system has zero degrees of freedom".to_string(),
134        ));
135    }
136    if stiffness.len() != n * n {
137        return Err(EngineeringError::ValidationError(format!(
138            "stiffness must have n*n = {} entries, got {}",
139            n * n,
140            stiffness.len()
141        )));
142    }
143    if mass_diag.len() != n {
144        return Err(EngineeringError::ValidationError(format!(
145            "mass diagonal must have n = {} entries, got {}",
146            n,
147            mass_diag.len()
148        )));
149    }
150    if mass_diag.iter().any(|&m| !(m > 0.0)) {
151        return Err(EngineeringError::ValidationError(
152            "all lumped masses must be positive".to_string(),
153        ));
154    }
155
156    // Ã = M^{-1/2} K M^{-1/2}.
157    let inv_sqrt_m: Vec<f64> = mass_diag.iter().map(|&m| 1.0 / m.sqrt()).collect();
158    let mut a = vec![0.0_f64; n * n];
159    for i in 0..n {
160        for j in 0..n {
161            a[i * n + j] = stiffness[i * n + j] * inv_sqrt_m[i] * inv_sqrt_m[j];
162        }
163    }
164
165    let mut eigvecs = vec![0.0_f64; n * n];
166    crate::solvers::linear_algebra::eigen::symmetric_eigen(n, &mut a, &mut eigvecs).map_err(
167        |e| EngineeringError::SolverError(format!("symmetric eigensolver failed: {:?}", e)),
168    )?;
169
170    // Diagonal of the transformed matrix now holds the eigenvalues λ = ω²; column
171    // `j` of `eigvecs` is the corresponding ψ.
172    let mut modes: Vec<(f64, Vec<f64>)> = Vec::with_capacity(n);
173    for j in 0..n {
174        let lambda = a[j * n + j];
175        let omega = lambda.max(0.0).sqrt();
176        // Physical mode φ = M^{-1/2} ψ (column j of eigvecs).
177        let mut phi: Vec<f64> = (0..n).map(|i| eigvecs[i * n + j] * inv_sqrt_m[i]).collect();
178        let max_abs = phi.iter().fold(0.0_f64, |m, &v| m.max(v.abs()));
179        if max_abs > 0.0 {
180            for v in phi.iter_mut() {
181                *v /= max_abs;
182            }
183        }
184        modes.push((omega, phi));
185    }
186    modes.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(core::cmp::Ordering::Equal));
187    Ok(modes)
188}
189
190/// Real 1-D steady-state heat-conduction solver (Fourier's law, finite-difference
191/// + tridiagonal Thomas algorithm) backing `perform_thermal_analysis`. Split into
192/// its own library submodule (PROJECT RULE §11); carries its own correctness tests
193/// against the analytic conduction solutions.
194pub mod thermal_conduction;
195
196/// Real 2-D incompressible Navier–Stokes finite-volume solver (Chorin projection
197/// method on a staggered Cartesian grid). Backs `perform_fluid_analysis`. Split
198/// into its own library submodule (PROJECT RULE §11); carries its own correctness
199/// tests (lid-driven cavity, channel flow, pressure outlet).
200pub mod cfd;
201
202/// Real finite-element subsystem (element library, global assembly, static solve,
203/// Newmark-β time integration, Newton–Raphson nonlinear solve). Backs the structural
204/// `NonlinearStatic` / `LinearDynamic` / `NonlinearDynamic` analysis types. Split into
205/// its own library submodule (PROJECT RULE §11); carries its own reference tests
206/// (cantilever tip deflection, axial/two-bar truss, SDOF Newmark, cubic-spring Newton).
207pub mod fem;
208
209// ── Library-ized submodules (PROJECT RULE §11: mechanical code-motion split of a
210// ~6.7k-line mod.rs into single-purpose siblings; no logic/signature change). Each
211// submodule uses `use super::*` for shared types and helper fns; `mod.rs` re-exports
212// the full public surface via `pub use <name>::*` so every existing external path
213// (`crate::specialized_libs::engineering_analysis::<Item>`) resolves exactly as before.
214mod buckling;
215mod dynamics;
216mod errors;
217mod fluid;
218mod library;
219mod mechanical;
220mod model;
221mod reliability;
222mod structural;
223mod survival;
224mod thermal;
225mod vibration;
226
227pub use buckling::*;
228pub use dynamics::*;
229pub use errors::*;
230pub use fluid::*;
231pub use library::*;
232pub use mechanical::*;
233pub use model::*;
234pub use reliability::*;
235pub use structural::*;
236pub use survival::*;
237pub use thermal::*;
238pub use vibration::*;
239
240#[cfg(test)]
241mod tests;