use std::f64::consts::PI;
const GAMMA_W: f64 = 9810.0;
pub struct BiotConsolidation {
pub n_nodes: usize,
pub n_elements: usize,
pub e: f64,
pub nu: f64,
pub k_perm: f64,
pub alpha: f64,
pub m_biot: f64,
}
impl BiotConsolidation {
pub fn new(
n_nodes: usize,
n_elements: usize,
e: f64,
nu: f64,
k_perm: f64,
alpha: f64,
m_biot: f64,
) -> Self {
Self {
n_nodes,
n_elements,
e,
nu,
k_perm,
alpha,
m_biot,
}
}
pub fn oedometric_modulus(&self) -> f64 {
self.e * (1.0 - self.nu) / ((1.0 + self.nu) * (1.0 - 2.0 * self.nu))
}
pub fn cv(&self) -> f64 {
consolidation_coefficient(self.k_perm, self.e, self.nu)
}
}
pub fn biot_effective_stress(total_stress: [f64; 6], pore_pressure: f64, alpha: f64) -> [f64; 6] {
[
total_stress[0] - alpha * pore_pressure,
total_stress[1] - alpha * pore_pressure,
total_stress[2] - alpha * pore_pressure,
total_stress[3],
total_stress[4],
total_stress[5],
]
}
pub fn consolidation_coefficient(k: f64, e: f64, nu: f64) -> f64 {
let denom = (1.0 + nu) * (1.0 - 2.0 * nu) * GAMMA_W;
if denom.abs() < 1e-300 {
return 0.0;
}
k * e * (1.0 - nu) / denom
}
pub fn terzaghi_1d_solution(z: f64, t: f64, h: f64, cv: f64, n_terms: usize) -> f64 {
if h.abs() < 1e-300 {
return 0.0;
}
let tv = cv * t / (h * h);
let mut sum = 0.0;
for m in 0..n_terms {
let m_f = m as f64;
let n_val = 2.0 * m_f + 1.0;
let sin_term = (n_val * PI * z / (2.0 * h)).sin();
let exp_term = (-(n_val * n_val) * PI * PI * tv / 4.0).exp();
sum += (1.0 / n_val) * sin_term * exp_term;
}
(4.0 / PI) * sum
}
pub struct SoilLayer {
pub thickness: f64,
pub cv: f64,
pub initial_excess_pressure: f64,
}
impl SoilLayer {
pub fn new(thickness: f64, cv: f64, initial_excess_pressure: f64) -> Self {
Self {
thickness,
cv,
initial_excess_pressure,
}
}
pub fn time_factor(&self, t: f64) -> f64 {
if self.thickness.abs() < 1e-300 {
return 0.0;
}
self.cv * t / (self.thickness * self.thickness)
}
}
pub fn degree_of_consolidation_1d(t: f64, h: f64, cv: f64) -> f64 {
if h.abs() < 1e-300 {
return 1.0;
}
let tv = cv * t / (h * h);
let n_terms = 20usize;
let mut sum = 0.0;
for m in 0..n_terms {
let m_f = m as f64;
let big_m = (2.0 * m_f + 1.0) * PI / 2.0;
sum += (2.0 / (big_m * big_m)) * (-(big_m * big_m) * tv).exp();
}
1.0 - sum
}
pub struct MohrCoulombCriterion {
pub cohesion: f64,
pub friction_angle: f64,
}
impl MohrCoulombCriterion {
pub fn new(cohesion: f64, friction_angle: f64) -> Self {
Self {
cohesion,
friction_angle,
}
}
pub fn yield_function(&self, sigma1: f64, sigma3: f64) -> f64 {
let sin_phi = self.friction_angle.sin();
let cos_phi = self.friction_angle.cos();
(sigma1 - sigma3) - (sigma1 + sigma3) * sin_phi - 2.0 * self.cohesion * cos_phi
}
pub fn is_yielded(&self, s1: f64, s3: f64) -> bool {
self.yield_function(s1, s3) >= 0.0
}
pub fn shear_strength(&self, normal_stress: f64) -> f64 {
self.cohesion + normal_stress * self.friction_angle.tan()
}
}
pub fn active_earth_pressure_ka(friction_angle: f64) -> f64 {
let t = (PI / 4.0 - friction_angle / 2.0).tan();
t * t
}
pub fn passive_earth_pressure_kp(friction_angle: f64) -> f64 {
let t = (PI / 4.0 + friction_angle / 2.0).tan();
t * t
}
pub struct FoundationBearing {
pub width: f64,
pub depth: f64,
pub cohesion: f64,
pub phi: f64,
pub gamma: f64,
}
impl FoundationBearing {
pub fn new(width: f64, depth: f64, cohesion: f64, phi: f64, gamma: f64) -> Self {
Self {
width,
depth,
cohesion,
phi,
gamma,
}
}
pub fn ultimate_capacity(&self) -> f64 {
let (nc, nq, ngamma) = bearing_factors(self.phi);
self.cohesion * nc + self.gamma * self.depth * nq + 0.5 * self.gamma * self.width * ngamma
}
}
pub fn bearing_factors(phi: f64) -> (f64, f64, f64) {
let nq = (PI * phi.tan()).exp() * (PI / 4.0 + phi / 2.0).tan().powi(2);
let nc = if phi.abs() < 1e-9 {
PI + 2.0
} else {
(nq - 1.0) / phi.tan()
};
let ngamma = 2.0 * (nq + 1.0) * phi.tan();
(nc, nq, ngamma)
}
pub struct SlopeStability {
pub height: f64,
pub beta: f64,
pub c: f64,
pub phi: f64,
pub gamma: f64,
}
impl SlopeStability {
pub fn new(height: f64, beta: f64, c: f64, phi: f64, gamma: f64) -> Self {
Self {
height,
beta,
c,
phi,
gamma,
}
}
pub fn factor_of_safety_infinite_slope(&self) -> f64 {
let driving = self.gamma * self.height * self.beta.sin() * self.beta.cos();
if driving.abs() < 1e-300 {
return f64::INFINITY;
}
let resisting =
self.c + self.gamma * self.height * self.beta.cos().powi(2) * self.phi.tan();
resisting / driving
}
}
pub fn settlement_elastic(q: f64, b: f64, e_mod: f64, nu: f64, depth_factor: f64) -> f64 {
if e_mod.abs() < 1e-300 {
return 0.0;
}
q * b * (1.0 - nu * nu) * depth_factor / e_mod
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_biot_consolidation_new() {
let bc = BiotConsolidation::new(10, 5, 1e6, 0.3, 1e-5, 1.0, 1e8);
assert_eq!(bc.n_nodes, 10);
assert_eq!(bc.n_elements, 5);
}
#[test]
fn test_biot_oedometric_modulus_positive() {
let bc = BiotConsolidation::new(1, 1, 1e6, 0.3, 1e-5, 1.0, 1e8);
let m_oed = bc.oedometric_modulus();
assert!(m_oed > 0.0, "Oedometric modulus must be positive");
}
#[test]
fn test_biot_cv_positive() {
let bc = BiotConsolidation::new(1, 1, 1e6, 0.3, 1e-5, 1.0, 1e8);
let cv = bc.cv();
assert!(cv > 0.0, "Cv must be positive");
}
#[test]
fn test_effective_stress_reduces_normal() {
let sigma = [100.0, 80.0, 60.0, 0.0, 0.0, 0.0];
let eff = biot_effective_stress(sigma, 20.0, 1.0);
assert!((eff[0] - 80.0).abs() < 1e-10);
assert!((eff[1] - 60.0).abs() < 1e-10);
assert!((eff[2] - 40.0).abs() < 1e-10);
}
#[test]
fn test_effective_stress_shear_unchanged() {
let sigma = [0.0, 0.0, 0.0, 15.0, 20.0, 25.0];
let eff = biot_effective_stress(sigma, 10.0, 1.0);
assert!((eff[3] - 15.0).abs() < 1e-10);
assert!((eff[4] - 20.0).abs() < 1e-10);
assert!((eff[5] - 25.0).abs() < 1e-10);
}
#[test]
fn test_effective_stress_zero_pore_pressure() {
let sigma = [50.0, 60.0, 70.0, 5.0, 6.0, 7.0];
let eff = biot_effective_stress(sigma, 0.0, 1.0);
for (a, b) in sigma.iter().zip(eff.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_effective_stress_alpha_partial() {
let sigma = [100.0, 100.0, 100.0, 0.0, 0.0, 0.0];
let eff = biot_effective_stress(sigma, 10.0, 0.5);
assert!((eff[0] - 95.0).abs() < 1e-10);
}
#[test]
fn test_consolidation_coefficient_positive() {
let cv = consolidation_coefficient(1e-5, 1e6, 0.3);
assert!(cv > 0.0);
}
#[test]
fn test_consolidation_coefficient_zero_k() {
let cv = consolidation_coefficient(0.0, 1e6, 0.3);
assert_eq!(cv, 0.0);
}
#[test]
fn test_terzaghi_1d_zero_time() {
let u = terzaghi_1d_solution(0.5, 0.0, 1.0, 1e-4, 50);
assert!(u > 0.9, "At t=0, u/u₀ should be near 1, got {u}");
}
#[test]
fn test_terzaghi_1d_large_time_approaches_zero() {
let u = terzaghi_1d_solution(0.5, 1e10, 1.0, 1e-4, 50);
assert!(u.abs() < 1e-6, "At t→∞, u/u₀ should → 0, got {u}");
}
#[test]
fn test_terzaghi_1d_zero_height_returns_zero() {
let u = terzaghi_1d_solution(0.5, 1.0, 0.0, 1e-4, 10);
assert_eq!(u, 0.0);
}
#[test]
fn test_degree_of_consolidation_zero_time() {
let u = degree_of_consolidation_1d(0.0, 1.0, 1e-4);
assert!(u < 0.02, "U(0) should be near 0, got {u}");
}
#[test]
fn test_degree_of_consolidation_large_time_approaches_one() {
let u = degree_of_consolidation_1d(1e12, 1.0, 1e-4);
assert!((u - 1.0).abs() < 1e-6, "U(∞) should be ~1, got {u}");
}
#[test]
fn test_degree_of_consolidation_monotone() {
let cv = 1e-4;
let h = 1.0;
let u1 = degree_of_consolidation_1d(1000.0, h, cv);
let u2 = degree_of_consolidation_1d(5000.0, h, cv);
assert!(u2 > u1, "Consolidation should increase with time");
}
#[test]
fn test_soil_layer_time_factor() {
let layer = SoilLayer::new(5.0, 2e-3, 50e3);
let tv = layer.time_factor(100.0);
let expected = 2e-3 * 100.0 / 25.0;
assert!((tv - expected).abs() < 1e-12);
}
#[test]
fn test_mohr_coulomb_no_yield_below_failure() {
let mc = MohrCoulombCriterion::new(10e3, 30.0_f64.to_radians());
assert!(!mc.is_yielded(20e3, 18e3));
}
#[test]
fn test_mohr_coulomb_yield_at_failure() {
let phi = 0.0_f64; let c = 100.0;
let mc = MohrCoulombCriterion::new(c, phi);
let f = mc.yield_function(200.0, 0.0);
assert!((f - 0.0).abs() < 1.0); }
#[test]
fn test_shear_strength_cohesion_only() {
let mc = MohrCoulombCriterion::new(50.0, 0.0);
assert!((mc.shear_strength(100.0) - 50.0).abs() < 1e-10);
}
#[test]
fn test_shear_strength_increases_with_normal_stress() {
let mc = MohrCoulombCriterion::new(10.0, 30.0_f64.to_radians());
let tau1 = mc.shear_strength(100.0);
let tau2 = mc.shear_strength(200.0);
assert!(tau2 > tau1);
}
#[test]
fn test_ka_zero_friction() {
let ka = active_earth_pressure_ka(0.0);
assert!((ka - 1.0).abs() < 1e-10, "Ka at φ=0 should be 1.0");
}
#[test]
fn test_kp_zero_friction() {
let kp = passive_earth_pressure_kp(0.0);
assert!((kp - 1.0).abs() < 1e-10, "Kp at φ=0 should be 1.0");
}
#[test]
fn test_ka_kp_product_unity_for_zero_phi() {
let phi = 0.0;
assert!(
(active_earth_pressure_ka(phi) * passive_earth_pressure_kp(phi) - 1.0).abs() < 1e-10
);
}
#[test]
fn test_ka_30_degrees() {
let phi = 30.0_f64.to_radians();
let ka = active_earth_pressure_ka(phi);
assert!(
(ka - 1.0 / 3.0).abs() < 1e-4,
"Ka at φ=30° ≈ 0.333, got {ka}"
);
}
#[test]
fn test_kp_30_degrees() {
let phi = 30.0_f64.to_radians();
let kp = passive_earth_pressure_kp(phi);
assert!((kp - 3.0).abs() < 1e-4, "Kp at φ=30° ≈ 3.0, got {kp}");
}
#[test]
fn test_ka_less_than_one() {
let phi = 20.0_f64.to_radians();
assert!(active_earth_pressure_ka(phi) < 1.0);
}
#[test]
fn test_kp_greater_than_one() {
let phi = 20.0_f64.to_radians();
assert!(passive_earth_pressure_kp(phi) > 1.0);
}
#[test]
fn test_ka_kp_reciprocal() {
let phi = 25.0_f64.to_radians();
let ka = active_earth_pressure_ka(phi);
let kp = passive_earth_pressure_kp(phi);
assert!(ka < 1.0);
assert!(kp > 1.0);
}
#[test]
fn test_bearing_nc_zero_phi() {
let (nc, _nq, _ngamma) = bearing_factors(0.0);
assert!((nc - (PI + 2.0)).abs() < 0.01, "Nc at φ=0 ≈ 5.14, got {nc}");
}
#[test]
fn test_bearing_nq_unity_at_zero_phi() {
let (_nc, nq, _ngamma) = bearing_factors(0.0);
assert!(
(nq - 1.0).abs() < 1e-10,
"Nq at φ=0 should be 1.0, got {nq}"
);
}
#[test]
fn test_bearing_factors_positive() {
let (nc, nq, ngamma) = bearing_factors(30.0_f64.to_radians());
assert!(nc > 0.0 && nq > 0.0 && ngamma > 0.0);
}
#[test]
fn test_foundation_bearing_positive_capacity() {
let fb = FoundationBearing::new(2.0, 1.0, 10e3, 30.0_f64.to_radians(), 18e3);
assert!(fb.ultimate_capacity() > 0.0);
}
#[test]
fn test_foundation_bearing_increases_with_cohesion() {
let fb1 = FoundationBearing::new(2.0, 1.0, 10e3, 20.0_f64.to_radians(), 18e3);
let fb2 = FoundationBearing::new(2.0, 1.0, 50e3, 20.0_f64.to_radians(), 18e3);
assert!(fb2.ultimate_capacity() > fb1.ultimate_capacity());
}
#[test]
fn test_foundation_bearing_increases_with_width() {
let fb1 = FoundationBearing::new(1.0, 1.0, 10e3, 25.0_f64.to_radians(), 18e3);
let fb2 = FoundationBearing::new(4.0, 1.0, 10e3, 25.0_f64.to_radians(), 18e3);
assert!(fb2.ultimate_capacity() > fb1.ultimate_capacity());
}
#[test]
fn test_slope_stability_beta_equals_phi() {
let phi = 30.0_f64.to_radians();
let ss = SlopeStability::new(5.0, phi, 0.0, phi, 18e3);
let fs = ss.factor_of_safety_infinite_slope();
assert!(
(fs - 1.0).abs() < 1e-8,
"FS should be 1 when beta=phi, c=0; got {fs}"
);
}
#[test]
fn test_slope_stability_fs_greater_one_for_gentle_slope() {
let phi = 30.0_f64.to_radians();
let beta = 15.0_f64.to_radians();
let ss = SlopeStability::new(5.0, beta, 0.0, phi, 18e3);
assert!(ss.factor_of_safety_infinite_slope() > 1.0);
}
#[test]
fn test_slope_stability_cohesion_increases_fs() {
let phi = 20.0_f64.to_radians();
let beta = 25.0_f64.to_radians();
let ss1 = SlopeStability::new(5.0, beta, 0.0, phi, 18e3);
let ss2 = SlopeStability::new(5.0, beta, 10e3, phi, 18e3);
assert!(ss2.factor_of_safety_infinite_slope() > ss1.factor_of_safety_infinite_slope());
}
#[test]
fn test_settlement_elastic_positive() {
let s = settlement_elastic(100e3, 2.0, 10e6, 0.3, 0.85);
assert!(s > 0.0);
}
#[test]
fn test_settlement_elastic_zero_modulus() {
let s = settlement_elastic(100e3, 2.0, 0.0, 0.3, 0.85);
assert_eq!(s, 0.0);
}
#[test]
fn test_settlement_elastic_increases_with_load() {
let s1 = settlement_elastic(100e3, 2.0, 10e6, 0.3, 0.85);
let s2 = settlement_elastic(200e3, 2.0, 10e6, 0.3, 0.85);
assert!(s2 > s1);
}
}