#![allow(dead_code)]
use std::f64::consts::PI;
pub fn mean_stress(sigma: &[f64; 6]) -> f64 {
(sigma[0] + sigma[1] + sigma[2]) / 3.0
}
pub fn deviatoric_stress(sigma: &[f64; 6]) -> [f64; 6] {
let p = mean_stress(sigma);
[
sigma[0] - p,
sigma[1] - p,
sigma[2] - p,
sigma[3],
sigma[4],
sigma[5],
]
}
pub fn von_mises_stress(sigma: &[f64; 6]) -> f64 {
let s = deviatoric_stress(sigma);
let j2 =
0.5 * (s[0] * s[0] + s[1] * s[1] + s[2] * s[2]) + s[3] * s[3] + s[4] * s[4] + s[5] * s[5];
(3.0 * j2).sqrt()
}
pub fn principal_stresses_2d(sxx: f64, syy: f64, txy: f64) -> (f64, f64, f64) {
let avg = (sxx + syy) / 2.0;
let r = (((sxx - syy) / 2.0).powi(2) + txy.powi(2)).sqrt();
let sigma1 = avg + r;
let sigma2 = avg - r;
let theta_p = 0.5 * txy.atan2((sxx - syy) / 2.0);
(sigma1, sigma2, theta_p)
}
#[derive(Debug, Clone)]
pub struct LinearElasticSoil {
pub young_modulus: f64,
pub poisson_ratio: f64,
}
impl LinearElasticSoil {
pub fn new(young_modulus: f64, poisson_ratio: f64) -> Self {
Self {
young_modulus,
poisson_ratio,
}
}
pub fn bulk_modulus(&self) -> f64 {
self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
}
pub fn shear_modulus(&self) -> f64 {
self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
}
pub fn strain_from_stress(&self, sigma: &[f64; 6]) -> [f64; 6] {
let e = self.young_modulus;
let nu = self.poisson_ratio;
let g = self.shear_modulus();
[
(sigma[0] - nu * (sigma[1] + sigma[2])) / e,
(sigma[1] - nu * (sigma[0] + sigma[2])) / e,
(sigma[2] - nu * (sigma[0] + sigma[1])) / e,
sigma[3] / (2.0 * g),
sigma[4] / (2.0 * g),
sigma[5] / (2.0 * g),
]
}
pub fn stress_from_strain(&self, eps: &[f64; 6]) -> [f64; 6] {
let e = self.young_modulus;
let nu = self.poisson_ratio;
let g = self.shear_modulus();
let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
let _ev = eps[0] + eps[1] + eps[2];
[
factor * ((1.0 - nu) * eps[0] + nu * (eps[1] + eps[2])),
factor * ((1.0 - nu) * eps[1] + nu * (eps[0] + eps[2])),
factor * ((1.0 - nu) * eps[2] + nu * (eps[0] + eps[1])),
2.0 * g * eps[3],
2.0 * g * eps[4],
2.0 * g * eps[5],
]
}
pub fn settlement_1d(&self, delta_sigma_v: f64, h: f64) -> f64 {
let nu = self.poisson_ratio;
let e_oed = self.young_modulus * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
delta_sigma_v * h / e_oed
}
}
#[derive(Debug, Clone)]
pub struct MohrCoulomb {
pub cohesion: f64,
pub friction_angle: f64,
}
impl MohrCoulomb {
pub fn new(cohesion: f64, friction_angle: f64) -> Self {
Self {
cohesion,
friction_angle,
}
}
pub fn shear_strength(&self, sigma_n: f64) -> f64 {
self.cohesion + sigma_n * self.friction_angle.tan()
}
pub fn is_failed(&self, sigma_n: f64, tau: f64) -> bool {
tau >= self.shear_strength(sigma_n)
}
pub fn yield_function(&self, sigma1: f64, sigma3: f64) -> f64 {
let sin_phi = self.friction_angle.sin();
let n_phi = (1.0 + sin_phi) / (1.0 - sin_phi);
sigma1 - sigma3 * n_phi - 2.0 * self.cohesion * n_phi.sqrt()
}
pub fn ucs(&self) -> f64 {
let phi = self.friction_angle;
2.0 * self.cohesion * phi.cos() / (1.0 - phi.sin())
}
pub fn tensile_strength(&self) -> f64 {
let phi = self.friction_angle;
2.0 * self.cohesion * phi.cos() / (1.0 + phi.sin())
}
pub fn to_drucker_prager_inner(&self) -> (f64, f64) {
let sin_phi = self.friction_angle.sin();
let alpha = 2.0 * sin_phi / (3.0_f64.sqrt() * (3.0 - sin_phi));
let k =
6.0 * self.cohesion * self.friction_angle.cos() / (3.0_f64.sqrt() * (3.0 - sin_phi));
(alpha, k)
}
pub fn at_rest_pressure_coefficient(poisson_ratio: f64) -> f64 {
poisson_ratio / (1.0 - poisson_ratio)
}
pub fn active_pressure_coefficient(&self) -> f64 {
let angle = PI / 4.0 - self.friction_angle / 2.0;
angle.tan().powi(2)
}
pub fn passive_pressure_coefficient(&self) -> f64 {
let angle = PI / 4.0 + self.friction_angle / 2.0;
angle.tan().powi(2)
}
}
#[derive(Debug, Clone)]
pub struct DruckerPragerModel {
pub k: f64,
pub alpha: f64,
pub young_modulus: f64,
pub poisson_ratio: f64,
}
impl DruckerPragerModel {
pub fn new(k: f64, alpha: f64, young_modulus: f64, poisson_ratio: f64) -> Self {
Self {
k,
alpha,
young_modulus,
poisson_ratio,
}
}
pub fn from_mohr_coulomb(mc: &MohrCoulomb, young_modulus: f64, poisson_ratio: f64) -> Self {
let (alpha, k) = mc.to_drucker_prager_inner();
Self::new(k, alpha, young_modulus, poisson_ratio)
}
pub fn yield_function(&self, sigma: &[f64; 6]) -> f64 {
let i1 = sigma[0] + sigma[1] + sigma[2];
let s = deviatoric_stress(sigma);
let j2 = 0.5 * (s[0] * s[0] + s[1] * s[1] + s[2] * s[2])
+ s[3] * s[3]
+ s[4] * s[4]
+ s[5] * s[5];
self.alpha * i1 + j2.sqrt() - self.k
}
pub fn is_yielded(&self, sigma: &[f64; 6]) -> bool {
self.yield_function(sigma) > 0.0
}
pub fn flow_direction(&self, sigma: &[f64; 6]) -> [f64; 6] {
let s = deviatoric_stress(sigma);
let j2 = 0.5 * (s[0] * s[0] + s[1] * s[1] + s[2] * s[2])
+ s[3] * s[3]
+ s[4] * s[4]
+ s[5] * s[5];
let sqrt_j2 = j2.sqrt().max(1e-20);
let factor = 1.0 / (2.0 * sqrt_j2);
[
self.alpha + factor * s[0],
self.alpha + factor * s[1],
self.alpha + factor * s[2],
factor * s[3],
factor * s[4],
factor * s[5],
]
}
pub fn return_mapping(&self, sigma_trial: &[f64; 6]) -> [f64; 6] {
if !self.is_yielded(sigma_trial) {
return *sigma_trial;
}
let e = self.young_modulus;
let nu = self.poisson_ratio;
let k_bulk = e / (3.0 * (1.0 - 2.0 * nu));
let g = e / (2.0 * (1.0 + nu));
let i1_trial = sigma_trial[0] + sigma_trial[1] + sigma_trial[2];
let s_trial = deviatoric_stress(sigma_trial);
let j2_trial = 0.5
* (s_trial[0] * s_trial[0] + s_trial[1] * s_trial[1] + s_trial[2] * s_trial[2])
+ s_trial[3] * s_trial[3]
+ s_trial[4] * s_trial[4]
+ s_trial[5] * s_trial[5];
let sqrt_j2 = j2_trial.sqrt().max(1e-20);
let f_trial = self.alpha * i1_trial + sqrt_j2 - self.k;
let d_gamma = f_trial / (g + 9.0 * self.alpha * self.alpha * k_bulk);
let scale = 1.0 - g * d_gamma / sqrt_j2;
if scale <= 0.0 {
let p_apex = self.k / (3.0 * self.alpha.max(1e-30));
return [p_apex, p_apex, p_apex, 0.0, 0.0, 0.0];
}
let p_new = i1_trial / 3.0 - 3.0 * self.alpha * k_bulk * d_gamma;
[
p_new + scale * s_trial[0],
p_new + scale * s_trial[1],
p_new + scale * s_trial[2],
scale * s_trial[3],
scale * s_trial[4],
scale * s_trial[5],
]
}
}
#[derive(Debug, Clone)]
pub struct HoekBrown {
pub sigma_ci: f64,
pub mb: f64,
pub s: f64,
pub a: f64,
}
impl HoekBrown {
pub fn new(sigma_ci: f64, mb: f64, s: f64, a: f64) -> Self {
Self { sigma_ci, mb, s, a }
}
pub fn intact_rock(sigma_ci: f64, mi: f64) -> Self {
Self::new(sigma_ci, mi, 1.0, 0.5)
}
pub fn sigma1_at_failure(&self, sigma3: f64) -> f64 {
let term = self.mb * sigma3 / self.sigma_ci + self.s;
if term < 0.0 {
sigma3
} else {
sigma3 + self.sigma_ci * term.powf(self.a)
}
}
pub fn ucs_mass(&self) -> f64 {
self.sigma_ci * self.s.powf(self.a)
}
pub fn tensile_strength(&self) -> f64 {
let disc = self.mb * self.mb + 4.0 * self.s;
self.sigma_ci / 2.0 * (self.mb - disc.sqrt())
}
pub fn gsi_from_s(&self) -> f64 {
9.0 * self.s.ln() + 100.0
}
pub fn equivalent_friction_angle(&self, sigma3: f64) -> f64 {
let h = self.sigma_ci
* (self.mb * sigma3 / self.sigma_ci + self.s).powf(self.a - 1.0)
* self.mb
* self.a;
let sin_phi = (h - 1.0) / (h + 1.0);
sin_phi.asin().max(0.0)
}
pub fn equivalent_cohesion(&self, sigma3: f64) -> f64 {
let phi = self.equivalent_friction_angle(sigma3);
let sigma1 = self.sigma1_at_failure(sigma3);
let tau = (sigma1 - sigma3) / 2.0;
let sigma_n = (sigma1 + sigma3) / 2.0;
(tau - sigma_n * phi.tan()).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct GriffithCriterion {
pub tensile_strength: f64,
}
impl GriffithCriterion {
pub fn new(tensile_strength: f64) -> Self {
Self { tensile_strength }
}
pub fn yield_function(&self, sigma1: f64, sigma3: f64) -> f64 {
let t0 = self.tensile_strength;
if sigma1 + 3.0 * sigma3 < 0.0 {
-(sigma3 + t0)
} else {
(sigma1 - sigma3).powi(2) - 8.0 * t0 * (sigma1 + sigma3)
}
}
pub fn is_failed(&self, sigma1: f64, sigma3: f64) -> bool {
self.yield_function(sigma1, sigma3) >= 0.0
}
pub fn ucs(&self) -> f64 {
8.0 * self.tensile_strength
}
pub fn stress_intensity_factor(&self, crack_half_length: f64) -> f64 {
self.tensile_strength * (PI * crack_half_length).sqrt()
}
}
pub fn effective_stress(total_stress: f64, pore_pressure: f64) -> f64 {
total_stress - pore_pressure
}
pub fn hydrostatic_pore_pressure(depth_below_water_table: f64, unit_weight_water: f64) -> f64 {
unit_weight_water * depth_below_water_table
}
pub fn overburden_stress(layers: &[(f64, f64)]) -> f64 {
layers.iter().map(|(gamma, h)| gamma * h).sum()
}
#[derive(Debug, Clone)]
pub struct TerzaghiConsolidation {
pub cv: f64,
pub drainage_path: f64,
pub initial_excess_pore_pressure: f64,
}
impl TerzaghiConsolidation {
pub fn new(cv: f64, drainage_path: f64, initial_excess_pore_pressure: f64) -> Self {
Self {
cv,
drainage_path,
initial_excess_pore_pressure,
}
}
pub fn time_factor(&self, t: f64) -> f64 {
self.cv * t / (self.drainage_path * self.drainage_path)
}
pub fn average_degree_of_consolidation(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
let tv = self.time_factor(t);
let mut u = 0.0f64;
for m in 0..=50 {
let big_m = PI * (2 * m + 1) as f64 / 2.0;
u += (2.0 / (big_m * big_m)) * (-big_m * big_m * tv).exp();
}
1.0 - u
}
pub fn average_doc_approx(&self, t: f64) -> f64 {
let tv = self.time_factor(t);
if tv <= 0.217 {
(4.0 * tv / PI).sqrt()
} else {
1.0 - (-1.781 * tv + 0.933_f64.ln()).exp().min(1.0)
}
}
pub fn excess_pore_pressure(&self, z: f64, t: f64) -> f64 {
let tv = self.time_factor(t);
let h = self.drainage_path;
let mut u = 0.0f64;
for m in 0..=50 {
let big_m = PI * (2 * m + 1) as f64 / 2.0;
u += (4.0 / ((2 * m + 1) as f64 * PI))
* (big_m * z / h).sin()
* (-big_m * big_m * tv).exp();
}
self.initial_excess_pore_pressure * u
}
pub fn settlement_at_time(&self, t: f64, final_settlement: f64) -> f64 {
self.average_degree_of_consolidation(t) * final_settlement
}
}
#[derive(Debug, Clone)]
pub struct BiotPoroelastic {
pub bulk_modulus_drained: f64,
pub shear_modulus: f64,
pub biot_coefficient: f64,
pub biot_modulus: f64,
pub permeability: f64,
pub fluid_viscosity: f64,
}
impl BiotPoroelastic {
#[allow(clippy::too_many_arguments)]
pub fn new(
bulk_modulus_drained: f64,
shear_modulus: f64,
biot_coefficient: f64,
biot_modulus: f64,
permeability: f64,
fluid_viscosity: f64,
) -> Self {
Self {
bulk_modulus_drained,
shear_modulus,
biot_coefficient,
biot_modulus,
permeability,
fluid_viscosity,
}
}
pub fn bulk_modulus_undrained(&self) -> f64 {
self.bulk_modulus_drained
+ self.biot_coefficient * self.biot_coefficient * self.biot_modulus
}
pub fn skempton_b(&self) -> f64 {
let ku = self.bulk_modulus_undrained();
if ku < 1e-20 {
0.0
} else {
self.biot_coefficient * self.biot_modulus / ku
}
}
pub fn hydraulic_diffusivity(&self) -> f64 {
let s = 1.0 / self.biot_modulus;
self.permeability / (self.fluid_viscosity * s)
}
pub fn undrained_pore_pressure_response(&self, delta_sigma_mean: f64) -> f64 {
self.skempton_b() * delta_sigma_mean
}
pub fn effective_stress_voigt(&self, sigma: &[f64; 6], pore_pressure: f64) -> [f64; 6] {
let alpha = self.biot_coefficient;
[
sigma[0] - alpha * pore_pressure,
sigma[1] - alpha * pore_pressure,
sigma[2] - alpha * pore_pressure,
sigma[3],
sigma[4],
sigma[5],
]
}
}
#[derive(Debug, Clone)]
pub struct LiquefactionAssessment {
pub amax: f64,
pub magnitude: f64,
pub total_vertical_stress: f64,
pub effective_vertical_stress: f64,
pub n60: f64,
pub fines_content: f64,
}
impl LiquefactionAssessment {
#[allow(clippy::too_many_arguments)]
pub fn new(
amax: f64,
magnitude: f64,
total_vertical_stress: f64,
effective_vertical_stress: f64,
n60: f64,
fines_content: f64,
) -> Self {
Self {
amax,
magnitude,
total_vertical_stress,
effective_vertical_stress,
n60,
fines_content,
}
}
pub fn stress_reduction_coefficient(depth_m: f64) -> f64 {
if depth_m <= 9.15 {
1.0 - 0.00765 * depth_m
} else if depth_m <= 23.0 {
1.174 - 0.0267 * depth_m
} else {
0.564
}
}
pub fn cyclic_stress_ratio(&self, depth_m: f64) -> f64 {
let rd = Self::stress_reduction_coefficient(depth_m);
0.65 * (self.total_vertical_stress / self.effective_vertical_stress) * self.amax * rd
}
pub fn magnitude_scaling_factor(&self) -> f64 {
10.0_f64.powf(2.24) / self.magnitude.powf(2.56)
}
pub fn normalized_n1_60(&self) -> f64 {
let cn = (100_000.0 / self.effective_vertical_stress).sqrt().min(1.7);
cn * self.n60
}
pub fn corrected_n1_60cs(&self) -> f64 {
let n1_60 = self.normalized_n1_60();
let delta_n = if self.fines_content < 5.0 {
0.0
} else if self.fines_content < 35.0 {
-17.0 + 0.76 * (1600.0 / (self.fines_content + 9.0) + 17.0_f64.powi(2)).sqrt()
} else {
5.0
};
n1_60 + delta_n
}
pub fn cyclic_resistance_ratio_7_5(&self) -> f64 {
let n = self.corrected_n1_60cs();
if n >= 30.0 {
return f64::INFINITY; }
1.0 / (34.0 - n) + n / 135.0 + 50.0 / (10.0 * n + 45.0).powi(2) - 1.0 / 200.0
}
pub fn factor_of_safety(&self, depth_m: f64) -> f64 {
let crr = self.cyclic_resistance_ratio_7_5();
if crr.is_infinite() {
return f64::INFINITY;
}
let csr = self.cyclic_stress_ratio(depth_m);
let msf = self.magnitude_scaling_factor();
crr * msf / csr
}
pub fn is_liquefiable(&self, depth_m: f64) -> bool {
self.factor_of_safety(depth_m) < 1.0
}
}
pub fn darcy_flow(permeability: f64, viscosity: f64, pressure_gradient: f64) -> f64 {
-permeability / viscosity * pressure_gradient
}
pub fn kozeny_carman_permeability(void_ratio: f64, particle_diameter: f64) -> f64 {
let cs = 1.0 / 180.0;
let e = void_ratio;
cs * particle_diameter * particle_diameter * e * e * e / (1.0 + e)
}
pub fn hydraulic_conductivity(
permeability: f64,
fluid_density: f64,
gravity: f64,
viscosity: f64,
) -> f64 {
permeability * fluid_density * gravity / viscosity
}
#[derive(Debug, Clone)]
pub struct SoilCompressibility {
pub compression_index: f64,
pub recompression_index: f64,
pub initial_void_ratio: f64,
pub preconsolidation_pressure: f64,
}
impl SoilCompressibility {
pub fn new(
compression_index: f64,
recompression_index: f64,
initial_void_ratio: f64,
preconsolidation_pressure: f64,
) -> Self {
Self {
compression_index,
recompression_index,
initial_void_ratio,
preconsolidation_pressure,
}
}
pub fn cc_from_liquid_limit(liquid_limit_percent: f64) -> f64 {
0.009 * (liquid_limit_percent - 10.0).max(0.0)
}
pub fn cc_from_void_ratio(initial_void_ratio: f64) -> f64 {
(0.54 * (initial_void_ratio - 0.35)).max(0.0)
}
pub fn consolidation_settlement(
&self,
layer_thickness: f64,
initial_effective_stress: f64,
final_effective_stress: f64,
) -> f64 {
let h = layer_thickness;
let e0 = self.initial_void_ratio;
let sigma0 = initial_effective_stress;
let sigmap = self.preconsolidation_pressure;
let sigmaf = final_effective_stress;
if sigmaf <= sigmap {
self.recompression_index * h / (1.0 + e0) * (sigmaf / sigma0).log10()
} else if sigma0 >= sigmap {
self.compression_index * h / (1.0 + e0) * (sigmaf / sigma0).log10()
} else {
let s1 = self.recompression_index * h / (1.0 + e0) * (sigmap / sigma0).log10();
let s2 = self.compression_index * h / (1.0 + e0) * (sigmaf / sigmap).log10();
s1 + s2
}
}
pub fn ocr(&self, initial_effective_stress: f64) -> f64 {
self.preconsolidation_pressure / initial_effective_stress
}
pub fn mv(&self, effective_stress: f64) -> f64 {
self.compression_index / (2.303 * (1.0 + self.initial_void_ratio) * effective_stress)
}
}
#[derive(Debug, Clone)]
pub struct UndrainedShearStrength;
impl UndrainedShearStrength {
pub fn skempton_nc(effective_overburden: f64, plasticity_index: f64) -> f64 {
effective_overburden * (0.11 + 0.0037 * plasticity_index)
}
pub fn vane_corrected(su_vane: f64, plasticity_index: f64) -> f64 {
let mu = (1.05 - 0.005 * plasticity_index).clamp(0.5, 1.0);
mu * su_vane
}
pub fn shansep(effective_vertical_stress: f64, ocr: f64) -> f64 {
let s = 0.22;
let m = 0.80;
effective_vertical_stress * s * ocr.powf(m)
}
pub fn from_triaxial(sigma1: f64, sigma3: f64) -> f64 {
(sigma1 - sigma3) / 2.0
}
}
pub fn immediate_settlement_circular(
applied_pressure: f64,
diameter: f64,
young_modulus: f64,
poisson_ratio: f64,
) -> f64 {
let b = diameter;
let i_s = PI / 4.0;
applied_pressure * b * (1.0 - poisson_ratio * poisson_ratio) / young_modulus * i_s
}
pub fn immediate_settlement_rectangular(
applied_pressure: f64,
width: f64,
length: f64,
young_modulus: f64,
poisson_ratio: f64,
) -> f64 {
let m = length / width;
let i_w = 0.5 * (m * m + 1.0).sqrt() / m * (m + (m * m + 1.0).sqrt()).ln() / PI;
applied_pressure * width * (1.0 - poisson_ratio * poisson_ratio) / young_modulus * i_w * 4.0
}
pub fn secondary_compression_settlement(
secondary_compression_index: f64,
layer_thickness: f64,
void_ratio_at_end_of_primary: f64,
t1: f64,
t2: f64,
) -> f64 {
if t2 <= t1 {
return 0.0;
}
secondary_compression_index * layer_thickness / (1.0 + void_ratio_at_end_of_primary)
* (t2 / t1).log10()
}
#[allow(clippy::too_many_arguments)]
pub fn terzaghi_bearing_capacity_strip(
cohesion: f64,
surcharge: f64,
unit_weight: f64,
width: f64,
friction_angle: f64,
) -> f64 {
let phi = friction_angle;
let nq = (PI * phi.tan()).exp() * ((PI / 4.0 + phi / 2.0).tan()).powi(2);
let nc = if phi < 1e-9 {
PI + 2.0
} else {
(nq - 1.0) / phi.tan()
};
let ng = 2.0 * (nq + 1.0) * phi.tan();
cohesion * nc + surcharge * nq + 0.5 * unit_weight * width * ng
}
pub fn meyerhof_bearing_factors(friction_angle: f64) -> (f64, f64, f64) {
let phi = friction_angle;
let nq = (PI * phi.tan()).exp() * ((PI / 4.0 + phi / 2.0).tan()).powi(2);
let nc = if phi < 1e-9 {
PI + 2.0
} else {
(nq - 1.0) / phi.tan()
};
let ng = (nq - 1.0) * (1.4 * phi).tan();
(nc, nq, ng)
}
pub fn triaxial_stress_path(sigma1: f64, sigma3: f64) -> (f64, f64) {
let p = (sigma1 + 2.0 * sigma3) / 3.0;
let q = sigma1 - sigma3;
(p, q)
}
pub fn k0_stress(unit_weight: f64, depth: f64, k0: f64) -> (f64, f64) {
let sigma_v = unit_weight * depth;
let sigma_h = k0 * sigma_v;
(sigma_v, sigma_h)
}
pub fn rqd_from_jv(jv: f64) -> f64 {
(115.0 - 3.3 * jv).clamp(0.0, 100.0)
}
pub fn rqd_from_frequency(lambda: f64) -> f64 {
100.0 * (-0.1 * lambda).exp() * (0.1 * lambda + 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-8;
#[test]
fn test_mean_stress_hydrostatic() {
let sigma = [100.0, 100.0, 100.0, 0.0, 0.0, 0.0];
assert!((mean_stress(&sigma) - 100.0).abs() < EPS);
}
#[test]
fn test_deviatoric_stress_hydrostatic_is_zero() {
let sigma = [200.0, 200.0, 200.0, 0.0, 0.0, 0.0];
let s = deviatoric_stress(&sigma);
for &v in &s[0..3] {
assert!(v.abs() < EPS, "deviatoric not zero: {}", v);
}
}
#[test]
fn test_von_mises_uniaxial() {
let sigma = [300.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let q = von_mises_stress(&sigma);
assert!((q - 300.0).abs() < 1e-6, "von Mises = {}", q);
}
#[test]
fn test_principal_stresses_2d_pure_shear() {
let (s1, s2, _theta) = principal_stresses_2d(0.0, 0.0, 100.0);
assert!((s1 - 100.0).abs() < EPS);
assert!((s2 + 100.0).abs() < EPS);
}
#[test]
fn test_principal_stresses_2d_uniaxial() {
let (s1, s2, _) = principal_stresses_2d(200.0, 0.0, 0.0);
assert!((s1 - 200.0).abs() < EPS);
assert!(s2.abs() < EPS);
}
#[test]
fn test_linear_elastic_bulk_modulus() {
let soil = LinearElasticSoil::new(1e6, 0.3);
let k = soil.bulk_modulus();
assert!((k - 1e6 / 1.2).abs() < 1.0);
}
#[test]
fn test_linear_elastic_strain_stress_roundtrip() {
let soil = LinearElasticSoil::new(2e6, 0.25);
let eps = [1e-3, 0.0, 0.0, 0.0, 0.0, 0.0];
let sigma = soil.stress_from_strain(&eps);
let eps2 = soil.strain_from_stress(&sigma);
for k in 0..6 {
assert!(
(eps[k] - eps2[k]).abs() < 1e-10,
"roundtrip failed at {}: {} vs {}",
k,
eps[k],
eps2[k]
);
}
}
#[test]
fn test_linear_elastic_settlement_positive() {
let soil = LinearElasticSoil::new(5e6, 0.3);
let s = soil.settlement_1d(50e3, 2.0);
assert!(s > 0.0, "settlement = {}", s);
}
#[test]
fn test_mohr_coulomb_shear_strength() {
let mc = MohrCoulomb::new(10e3, PI / 6.0); let tau = mc.shear_strength(100e3);
assert!((tau - (10000.0 + 100000.0 * (PI / 6.0).tan())).abs() < 1e-4);
}
#[test]
fn test_mohr_coulomb_yield_function_below_yield() {
let mc = MohrCoulomb::new(20e3, 30.0_f64.to_radians());
let f = mc.yield_function(100e3, 100e3);
assert!(f <= 0.0, "yield function = {}", f);
}
#[test]
fn test_mohr_coulomb_ka_kp_product() {
let mc = MohrCoulomb::new(0.0, 30.0_f64.to_radians());
let ka = mc.active_pressure_coefficient();
let kp = mc.passive_pressure_coefficient();
assert!((ka * kp - 1.0).abs() < 1e-9, "Ka*Kp = {}", ka * kp);
}
#[test]
fn test_mohr_coulomb_ucs_positive() {
let mc = MohrCoulomb::new(25e3, 35.0_f64.to_radians());
assert!(mc.ucs() > 0.0);
}
#[test]
fn test_mohr_coulomb_to_drucker_prager() {
let mc = MohrCoulomb::new(10e3, 30.0_f64.to_radians());
let (alpha, k) = mc.to_drucker_prager_inner();
assert!(alpha > 0.0, "alpha = {}", alpha);
assert!(k > 0.0, "k = {}", k);
}
#[test]
fn test_drucker_prager_yield_hydrostatic() {
let mc = MohrCoulomb::new(20e3, 30.0_f64.to_radians());
let dp = DruckerPragerModel::from_mohr_coulomb(&mc, 10e6, 0.3);
let sigma = [100e3, 100e3, 100e3, 0.0, 0.0, 0.0];
let f = dp.yield_function(&sigma);
let expected = dp.alpha * 300e3 - dp.k;
assert!((f - expected).abs() < 1e-4, "F = {}", f);
}
#[test]
fn test_drucker_prager_return_mapping_stress_on_yield() {
let dp = DruckerPragerModel::new(50e3, 0.1, 10e6, 0.3);
let sigma_trial = [500e3, 200e3, 200e3, 50e3, 0.0, 0.0];
let sigma_ret = dp.return_mapping(&sigma_trial);
let f = dp.yield_function(&sigma_ret);
assert!(f <= 1e-3, "after return mapping F = {}", f);
}
#[test]
fn test_drucker_prager_elastic_no_change() {
let dp = DruckerPragerModel::new(1e9, 0.0, 10e6, 0.3);
let sigma = [10e3, 5e3, 5e3, 1e3, 0.0, 0.0];
let sigma_ret = dp.return_mapping(&sigma);
for k in 0..6 {
assert!((sigma[k] - sigma_ret[k]).abs() < 1e-6);
}
}
#[test]
fn test_hoek_brown_uniaxial_compression() {
let hb = HoekBrown::intact_rock(100e6, 7.0);
let sigma1 = hb.sigma1_at_failure(0.0);
assert!((sigma1 - 100e6).abs() < 1.0, "sigma1 = {}", sigma1);
}
#[test]
fn test_hoek_brown_ucs_mass() {
let hb = HoekBrown::new(100e6, 5.0, 0.25, 0.5);
let ucs = hb.ucs_mass();
assert!((ucs - 100e6 * 0.5).abs() < 1.0);
}
#[test]
fn test_hoek_brown_tensile_strength_negative() {
let hb = HoekBrown::intact_rock(50e6, 7.0);
let st = hb.tensile_strength();
assert!(st < 0.0, "tensile = {}", st);
}
#[test]
fn test_griffith_tensile_failure() {
let g = GriffithCriterion::new(10e6);
assert!(g.is_failed(5e6, -10e6));
}
#[test]
fn test_griffith_ucs() {
let g = GriffithCriterion::new(10e6);
assert!((g.ucs() - 80e6).abs() < 1.0);
}
#[test]
fn test_griffith_no_failure_below_threshold() {
let g = GriffithCriterion::new(5e6);
assert!(!g.is_failed(100e6, 50e6));
}
#[test]
fn test_effective_stress_basic() {
let sigma_eff = effective_stress(200e3, 80e3);
assert!((sigma_eff - 120e3).abs() < EPS);
}
#[test]
fn test_hydrostatic_pore_pressure() {
let u = hydrostatic_pore_pressure(10.0, 9810.0);
assert!((u - 98100.0).abs() < EPS);
}
#[test]
fn test_overburden_stress() {
let layers = vec![(18000.0, 2.0), (20000.0, 3.0)];
let sigma_v = overburden_stress(&layers);
assert!((sigma_v - (36000.0 + 60000.0)).abs() < EPS);
}
#[test]
fn test_terzaghi_doc_at_t_zero() {
let c = TerzaghiConsolidation::new(1e-7, 5.0, 100e3);
let u = c.average_degree_of_consolidation(0.0);
assert!(u.abs() < 1e-8, "U(0) = {}", u);
}
#[test]
fn test_terzaghi_doc_increases_with_time() {
let c = TerzaghiConsolidation::new(1e-7, 3.0, 100e3);
let u1 = c.average_degree_of_consolidation(1e7);
let u2 = c.average_degree_of_consolidation(5e7);
assert!(u2 > u1, "U should increase with time");
}
#[test]
fn test_terzaghi_time_factor() {
let c = TerzaghiConsolidation::new(1e-8, 2.0, 50e3);
let tv = c.time_factor(1e8);
assert!((tv - 1e-8 * 1e8 / 4.0).abs() < EPS);
}
#[test]
fn test_terzaghi_settlement_monotone() {
let c = TerzaghiConsolidation::new(1e-7, 5.0, 100e3);
let s1 = c.settlement_at_time(1e6, 0.1);
let s2 = c.settlement_at_time(1e8, 0.1);
assert!(s2 >= s1);
}
#[test]
fn test_biot_skempton_b_range() {
let biot = BiotPoroelastic::new(1e9, 3e8, 0.8, 5e9, 1e-12, 1e-3);
let b = biot.skempton_b();
assert!((0.0..=1.0).contains(&b), "B = {}", b);
}
#[test]
fn test_biot_undrained_bulk_ge_drained() {
let biot = BiotPoroelastic::new(1e9, 3e8, 0.7, 2e9, 1e-12, 1e-3);
let ku = biot.bulk_modulus_undrained();
assert!(ku >= biot.bulk_modulus_drained, "Ku < Kd");
}
#[test]
fn test_biot_effective_stress() {
let biot = BiotPoroelastic::new(1e9, 3e8, 1.0, 2e9, 1e-12, 1e-3);
let sigma = [200e3, 200e3, 200e3, 0.0, 0.0, 0.0];
let sigma_eff = biot.effective_stress_voigt(&sigma, 50e3);
assert!((sigma_eff[0] - 150e3).abs() < EPS);
}
#[test]
fn test_liquefaction_csr_positive() {
let liq = LiquefactionAssessment::new(0.3, 7.0, 150e3, 80e3, 10.0, 5.0);
let csr = liq.cyclic_stress_ratio(5.0);
assert!(csr > 0.0);
}
#[test]
fn test_liquefaction_msf_gt_one_for_small_mw() {
let liq = LiquefactionAssessment::new(0.3, 6.0, 150e3, 80e3, 10.0, 5.0);
let msf = liq.magnitude_scaling_factor();
assert!(msf > 1.0, "MSF = {}", msf);
}
#[test]
fn test_liquefaction_high_n_not_liquefiable() {
let liq = LiquefactionAssessment::new(0.2, 7.5, 120e3, 80e3, 35.0, 2.0);
assert!(!liq.is_liquefiable(5.0));
}
#[test]
fn test_kozeny_carman_increases_with_void_ratio() {
let k1 = kozeny_carman_permeability(0.5, 0.1e-3);
let k2 = kozeny_carman_permeability(1.0, 0.1e-3);
assert!(k2 > k1);
}
#[test]
fn test_darcy_flow_direction() {
let q = darcy_flow(1e-12, 1e-3, 1000.0);
assert!(q < 0.0);
}
#[test]
fn test_cc_from_liquid_limit() {
let cc = SoilCompressibility::cc_from_liquid_limit(50.0);
assert!((cc - 0.009 * 40.0).abs() < EPS);
}
#[test]
fn test_consolidation_settlement_nc() {
let sc = SoilCompressibility::new(0.3, 0.05, 0.8, 50e3);
let s = sc.consolidation_settlement(5.0, 100e3, 200e3);
assert!(s > 0.0, "settlement = {}", s);
}
#[test]
fn test_consolidation_settlement_oc() {
let sc = SoilCompressibility::new(0.3, 0.05, 0.8, 200e3);
let s = sc.consolidation_settlement(5.0, 50e3, 100e3);
assert!(s > 0.0);
}
#[test]
fn test_consolidation_settlement_cross_preconsolidation() {
let sc = SoilCompressibility::new(0.3, 0.05, 0.8, 100e3);
let s = sc.consolidation_settlement(5.0, 50e3, 200e3);
let s_nc = sc.consolidation_settlement(5.0, 50e3, 200e3);
assert!(s > 0.0 && s_nc > 0.0);
}
#[test]
fn test_ocr_calculation() {
let sc = SoilCompressibility::new(0.3, 0.05, 0.8, 200e3);
let ocr = sc.ocr(100e3);
assert!((ocr - 2.0).abs() < EPS);
}
#[test]
fn test_shansep_increases_with_ocr() {
let su1 = UndrainedShearStrength::shansep(100e3, 1.0);
let su2 = UndrainedShearStrength::shansep(100e3, 2.0);
assert!(su2 > su1);
}
#[test]
fn test_su_from_triaxial() {
let su = UndrainedShearStrength::from_triaxial(300e3, 100e3);
assert!((su - 100e3).abs() < EPS);
}
#[test]
fn test_terzaghi_bearing_capacity_cohesive() {
let qu = terzaghi_bearing_capacity_strip(50e3, 0.0, 0.0, 1.0, 0.0);
let expected = 50e3 * (PI + 2.0);
assert!((qu - expected).abs() < 1e-2, "qu = {}", qu);
}
#[test]
fn test_meyerhof_factors_phi30() {
let (nc, nq, ng) = meyerhof_bearing_factors(30.0_f64.to_radians());
assert!(nc > 0.0 && nq > 0.0 && ng > 0.0);
assert!(nq > 1.0, "Nq should be > 1 for φ > 0");
}
#[test]
fn test_triaxial_stress_path() {
let (p, q) = triaxial_stress_path(300e3, 100e3);
assert!((p - (300e3 + 200e3) / 3.0).abs() < EPS);
assert!((q - 200e3).abs() < EPS);
}
#[test]
fn test_k0_stress() {
let (sv, sh) = k0_stress(18000.0, 5.0, 0.5);
assert!((sv - 90000.0).abs() < EPS);
assert!((sh - 45000.0).abs() < EPS);
}
#[test]
fn test_rqd_from_jv_clamped() {
assert!((rqd_from_jv(0.0) - 115.0_f64.min(100.0)).abs() < EPS);
assert!((rqd_from_jv(50.0)).abs() < EPS);
}
#[test]
fn test_rqd_from_frequency_positive() {
let rqd = rqd_from_frequency(10.0);
assert!(rqd > 0.0 && rqd <= 100.0);
}
#[test]
fn test_secondary_compression_increases_with_time() {
let s1 = secondary_compression_settlement(0.02, 3.0, 1.2, 1e6, 1e7);
let s2 = secondary_compression_settlement(0.02, 3.0, 1.2, 1e6, 1e8);
assert!(s2 > s1);
}
#[test]
fn test_secondary_compression_zero_for_backward_time() {
let s = secondary_compression_settlement(0.02, 3.0, 1.2, 1e7, 1e6);
assert_eq!(s, 0.0);
}
}