use std::f64::consts::PI;
pub struct BiotCoeff {
pub alpha: f64,
pub modulus: f64,
}
impl BiotCoeff {
pub fn new(alpha: f64, modulus: f64) -> Self {
Self { alpha, modulus }
}
pub fn compute_from_bulk(k_dry: f64, k_s: f64, k_f: f64, phi: f64) -> Self {
let alpha = 1.0 - k_dry / k_s;
let inv_m = phi / k_f + (alpha - phi) / k_s;
let modulus = if inv_m.abs() < 1e-300 {
f64::INFINITY
} else {
1.0 / inv_m
};
Self { alpha, modulus }
}
pub fn storage_coefficient(&self, phi: f64, k_f: f64) -> f64 {
self.alpha * self.alpha / self.modulus + phi / k_f
}
pub fn undrained_bulk(k_dry: f64, alpha: f64, modulus: f64) -> f64 {
k_dry + alpha * alpha * modulus
}
pub fn skempton_b(k_dry: f64, alpha: f64, modulus: f64) -> f64 {
let k_u = Self::undrained_bulk(k_dry, alpha, modulus);
if k_u.abs() < 1e-300 {
0.0
} else {
alpha * modulus / k_u
}
}
}
pub struct PorousElement {
pub node_ids: Vec<usize>,
pub permeability: [f64; 9],
pub porosity: f64,
pub solid_stiffness: f64,
}
impl PorousElement {
pub fn new(
node_ids: Vec<usize>,
permeability: [f64; 9],
porosity: f64,
solid_stiffness: f64,
) -> Self {
Self {
node_ids,
permeability,
porosity,
solid_stiffness,
}
}
pub fn compute_coupling_matrix(&self) -> Vec<f64> {
let n = self.node_ids.len();
let mut coupling = vec![0.0_f64; 3 * n];
let n_f64 = n as f64;
for i in 0..n {
coupling[3 * i] = 1.0 / n_f64;
coupling[3 * i + 1] = 1.0 / n_f64;
coupling[3 * i + 2] = 1.0 / n_f64;
}
coupling
}
pub fn k_xx(&self) -> f64 {
self.permeability[0]
}
pub fn isotropic_permeability(&self) -> f64 {
(self.permeability[0] + self.permeability[4] + self.permeability[8]) / 3.0
}
}
pub struct ConsolidationSolver {
pub u_dofs: Vec<f64>,
pub p_dofs: Vec<f64>,
pub k_uu: Vec<f64>,
pub k_up: Vec<f64>,
pub k_pp: Vec<f64>,
pub h_pp: Vec<f64>,
pub time: f64,
}
impl ConsolidationSolver {
pub fn new(n_u: usize, n_p: usize) -> Self {
Self {
u_dofs: vec![0.0; n_u],
p_dofs: vec![0.0; n_p],
k_uu: vec![0.0; n_u * n_u],
k_up: vec![0.0; n_u * n_p],
k_pp: vec![0.0; n_p],
h_pp: vec![0.0; n_p],
time: 0.0,
}
}
pub fn step(&mut self, dt: f64) {
let n_p = self.p_dofs.len();
for i in 0..n_p {
let lhs = self.k_pp[i] + dt * self.h_pp[i];
if lhs.abs() > 1e-300 {
self.p_dofs[i] = self.k_pp[i] * self.p_dofs[i] / lhs;
}
}
self.time += dt;
}
pub fn n_u(&self) -> usize {
self.u_dofs.len()
}
pub fn n_p(&self) -> usize {
self.p_dofs.len()
}
}
pub struct TerzaghiConsolidation {
pub drainage_height: f64,
pub cv: f64,
}
impl TerzaghiConsolidation {
pub fn new(drainage_height: f64, cv: f64) -> Self {
Self {
drainage_height,
cv,
}
}
pub fn time_factor(&self, time: f64) -> f64 {
self.cv * time / (self.drainage_height * self.drainage_height)
}
pub fn degree_of_consolidation(&self, time: f64) -> f64 {
let tv = self.time_factor(time);
terzaghi_u(tv)
}
pub fn settlement(&self, time: f64, s_final: f64) -> f64 {
self.degree_of_consolidation(time) * s_final
}
pub fn time_to_consolidation(&self, u_target: f64) -> f64 {
let tv = if u_target <= 0.6 {
PI / 4.0 * u_target * u_target
} else {
-0.9332 * (1.0 - u_target).ln() - 0.0851
};
tv * self.drainage_height * self.drainage_height / self.cv
}
}
pub struct DarcyFlow {
pub permeability: f64,
pub viscosity: f64,
pub hydraulic_gradient: [f64; 3],
}
impl DarcyFlow {
pub fn new(permeability: f64, viscosity: f64, hydraulic_gradient: [f64; 3]) -> Self {
Self {
permeability,
viscosity,
hydraulic_gradient,
}
}
pub fn darcy_velocity(&self) -> [f64; 3] {
let factor = -self.permeability / self.viscosity;
[
factor * self.hydraulic_gradient[0],
factor * self.hydraulic_gradient[1],
factor * self.hydraulic_gradient[2],
]
}
pub fn darcy_speed(&self) -> f64 {
let q = self.darcy_velocity();
(q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt()
}
pub fn hydraulic_conductivity(&self, density: f64, g: f64) -> f64 {
self.permeability * density * g / self.viscosity
}
pub fn reynolds_number(&self, density: f64, particle_diameter: f64) -> f64 {
density * self.darcy_speed() * particle_diameter / self.viscosity
}
}
pub fn terzaghi_u(tv: f64) -> f64 {
if tv <= 0.0 {
return 0.0;
}
let mut sum = 0.0_f64;
for m in 0_u32..50 {
let two_m1 = (2 * m + 1) as f64;
let coeff = 8.0 / (two_m1 * two_m1 * PI * PI);
let exponent = -two_m1 * two_m1 * PI * PI * tv / 4.0;
sum += coeff * exponent.exp();
}
(1.0 - sum).clamp(0.0, 1.0)
}
pub fn consolidation_time(tv: f64, cv: f64, h: f64) -> f64 {
tv * h * h / cv
}
pub fn effective_stress(total: f64, pore_pressure: f64) -> f64 {
total - pore_pressure
}
pub fn consolidation_coefficient(k: f64, mu: f64, m_v: f64) -> f64 {
if mu.abs() < 1e-300 || m_v.abs() < 1e-300 {
return 0.0;
}
k / (mu * m_v)
}
pub fn pore_pressure_ratio(z: f64, h: f64, tv: f64) -> f64 {
if h < 1e-300 {
return 0.0;
}
let mut sum = 0.0_f64;
for m in 0_u32..50 {
let two_m1 = (2 * m + 1) as f64;
let coeff = 4.0 / (two_m1 * PI);
let spatial = (two_m1 * PI * z / (2.0 * h)).sin();
let temporal = (-two_m1 * two_m1 * PI * PI * tv / 4.0).exp();
sum += coeff * spatial * temporal;
}
sum.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_biot_new() {
let b = BiotCoeff::new(0.8, 1e9);
assert!((b.alpha - 0.8).abs() < 1e-12);
assert!((b.modulus - 1e9).abs() < 1.0);
}
#[test]
fn test_biot_compute_from_bulk_incompressible_solid() {
let b = BiotCoeff::compute_from_bulk(0.0, 1e12, 2.2e9, 0.4);
assert!((b.alpha - 1.0).abs() < 1e-6);
}
#[test]
fn test_biot_compute_from_bulk_typical_sandstone() {
let b = BiotCoeff::compute_from_bulk(10e9, 36e9, 2.2e9, 0.2);
assert!(b.alpha > 0.0 && b.alpha < 1.0);
assert!(b.modulus > 0.0);
}
#[test]
fn test_biot_alpha_range() {
let b = BiotCoeff::compute_from_bulk(5e9, 36e9, 2.2e9, 0.3);
assert!((0.0..=1.0).contains(&b.alpha));
}
#[test]
fn test_biot_storage_coefficient() {
let b = BiotCoeff::new(1.0, 1e9);
let s = b.storage_coefficient(0.4, 2.2e9);
assert!(s > 0.0);
}
#[test]
fn test_biot_undrained_bulk() {
let k_u = BiotCoeff::undrained_bulk(10e9, 0.8, 5e9);
assert!(k_u > 10e9);
}
#[test]
fn test_biot_skempton_b() {
let b_val = BiotCoeff::skempton_b(10e9, 0.8, 5e9);
assert!(b_val > 0.0 && b_val <= 1.0);
}
#[test]
fn test_porous_element_new() {
let k = [1e-12, 0.0, 0.0, 0.0, 1e-12, 0.0, 0.0, 0.0, 1e-12];
let elem = PorousElement::new(vec![0, 1, 2, 3], k, 0.3, 1e7);
assert_eq!(elem.node_ids.len(), 4);
assert!((elem.porosity - 0.3).abs() < 1e-12);
}
#[test]
fn test_porous_element_coupling_matrix_length() {
let k = [1e-12; 9];
let elem = PorousElement::new(vec![0, 1, 2, 3], k, 0.3, 1e7);
let coupling = elem.compute_coupling_matrix();
assert_eq!(coupling.len(), 12); }
#[test]
fn test_porous_element_coupling_matrix_values() {
let k = [1e-12; 9];
let elem = PorousElement::new(vec![0, 1, 2, 3], k, 0.3, 1e7);
let coupling = elem.compute_coupling_matrix();
for &v in &coupling {
assert!((v - 0.25).abs() < 1e-12);
}
}
#[test]
fn test_porous_element_k_xx() {
let mut k = [0.0_f64; 9];
k[0] = 2e-12;
let elem = PorousElement::new(vec![0], k, 0.2, 1e8);
assert!((elem.k_xx() - 2e-12).abs() < 1e-300);
}
#[test]
fn test_porous_element_isotropic_permeability() {
let k = [1e-12, 0.0, 0.0, 0.0, 2e-12, 0.0, 0.0, 0.0, 3e-12];
let elem = PorousElement::new(vec![0], k, 0.2, 1e8);
assert!((elem.isotropic_permeability() - 2e-12).abs() < 1e-24);
}
#[test]
fn test_consolidation_solver_new() {
let solver = ConsolidationSolver::new(6, 2);
assert_eq!(solver.n_u(), 6);
assert_eq!(solver.n_p(), 2);
assert!((solver.time - 0.0).abs() < 1e-12);
}
#[test]
fn test_consolidation_solver_step_time() {
let mut solver = ConsolidationSolver::new(3, 1);
solver.step(0.1);
assert!((solver.time - 0.1).abs() < 1e-12);
solver.step(0.1);
assert!((solver.time - 0.2).abs() < 1e-12);
}
#[test]
fn test_consolidation_solver_pressure_decay() {
let mut solver = ConsolidationSolver::new(3, 1);
solver.p_dofs[0] = 100.0;
solver.k_pp[0] = 1.0;
solver.h_pp[0] = 1.0;
solver.step(1.0);
assert!((solver.p_dofs[0] - 50.0).abs() < 1e-10);
}
#[test]
fn test_consolidation_solver_pressure_zero_kpp() {
let mut solver = ConsolidationSolver::new(3, 1);
solver.p_dofs[0] = 100.0;
solver.k_pp[0] = 0.0;
solver.h_pp[0] = 1.0;
solver.step(0.5);
assert!((solver.p_dofs[0]).abs() < 1e-12);
}
#[test]
fn test_terzaghi_time_factor_zero() {
let tc = TerzaghiConsolidation::new(1.0, 1e-6);
assert!((tc.time_factor(0.0)).abs() < 1e-12);
}
#[test]
fn test_terzaghi_time_factor() {
let cv = 1e-6;
let h = 2.0;
let tc = TerzaghiConsolidation::new(h, cv);
let t = 1e5;
let tv = cv * t / (h * h);
assert!((tc.time_factor(t) - tv).abs() < 1e-20);
}
#[test]
fn test_terzaghi_degree_zero_time() {
let tc = TerzaghiConsolidation::new(1.0, 1e-6);
assert!((tc.degree_of_consolidation(0.0)).abs() < 1e-10);
}
#[test]
fn test_terzaghi_degree_large_time() {
let tc = TerzaghiConsolidation::new(1.0, 1e-6);
let u = tc.degree_of_consolidation(1e20);
assert!((u - 1.0).abs() < 1e-6);
}
#[test]
fn test_terzaghi_degree_monotonic() {
let tc = TerzaghiConsolidation::new(1.0, 1e-7);
let u1 = tc.degree_of_consolidation(1e4);
let u2 = tc.degree_of_consolidation(2e4);
assert!(u2 >= u1);
}
#[test]
fn test_terzaghi_settlement() {
let tc = TerzaghiConsolidation::new(1.0, 1e-6);
let s = tc.settlement(1e20, 0.1);
assert!((s - 0.1).abs() < 1e-5);
}
#[test]
fn test_terzaghi_time_to_consolidation_50pct() {
let cv = 1e-6;
let h = 1.0;
let tc = TerzaghiConsolidation::new(h, cv);
let t50 = tc.time_to_consolidation(0.5);
let u_check = tc.degree_of_consolidation(t50);
assert!((u_check - 0.5).abs() < 0.05);
}
#[test]
fn test_darcy_velocity_1d() {
let flow = DarcyFlow::new(1e-12, 1e-3, [1.0, 0.0, 0.0]);
let v = flow.darcy_velocity();
assert!((v[0] - (-1e-9)).abs() < 1e-21);
assert!((v[1]).abs() < 1e-30);
assert!((v[2]).abs() < 1e-30);
}
#[test]
fn test_darcy_velocity_zero_gradient() {
let flow = DarcyFlow::new(1e-12, 1e-3, [0.0, 0.0, 0.0]);
let v = flow.darcy_velocity();
assert!(v[0].abs() < 1e-30 && v[1].abs() < 1e-30 && v[2].abs() < 1e-30);
}
#[test]
fn test_darcy_speed() {
let flow = DarcyFlow::new(1e-12, 1e-3, [1.0, 0.0, 0.0]);
let speed = flow.darcy_speed();
assert!((speed - 1e-9).abs() < 1e-21);
}
#[test]
fn test_darcy_hydraulic_conductivity() {
let flow = DarcyFlow::new(1e-12, 1e-3, [0.0; 3]);
let k_cond = flow.hydraulic_conductivity(1000.0, 9.81);
let expected = 1e-12 * 1000.0 * 9.81 / 1e-3;
assert!((k_cond - expected).abs() / expected < 1e-10);
}
#[test]
fn test_darcy_reynolds_number() {
let flow = DarcyFlow::new(1e-12, 1e-3, [1.0, 0.0, 0.0]);
let re = flow.reynolds_number(1000.0, 1e-3);
assert!(re >= 0.0);
}
#[test]
fn test_terzaghi_u_zero() {
assert!((terzaghi_u(0.0)).abs() < 1e-12);
}
#[test]
fn test_terzaghi_u_large() {
assert!((terzaghi_u(100.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_terzaghi_u_half() {
let u = terzaghi_u(0.197);
assert!((u - 0.5).abs() < 0.02);
}
#[test]
fn test_terzaghi_u_monotonic() {
let u1 = terzaghi_u(0.1);
let u2 = terzaghi_u(0.3);
let u3 = terzaghi_u(1.0);
assert!(u1 <= u2 && u2 <= u3);
}
#[test]
fn test_consolidation_time_roundtrip() {
let cv = 2e-8;
let h = 3.0;
let tv = 0.5;
let t = consolidation_time(tv, cv, h);
let tv_back = cv * t / (h * h);
assert!((tv_back - tv).abs() < 1e-12);
}
#[test]
fn test_effective_stress_positive() {
let sigma_eff = effective_stress(100e3, 30e3);
assert!((sigma_eff - 70e3).abs() < 1e-8);
}
#[test]
fn test_effective_stress_zero_pore() {
assert!((effective_stress(50e3, 0.0) - 50e3).abs() < 1e-10);
}
#[test]
fn test_effective_stress_negative() {
let s = effective_stress(10e3, 50e3);
assert!((s - (-40e3)).abs() < 1e-8);
}
#[test]
fn test_consolidation_coefficient_basic() {
let cc = consolidation_coefficient(1e-9, 1e-3, 1e-7);
assert!(cc > 0.0);
}
#[test]
fn test_consolidation_coefficient_zero_viscosity() {
let cc = consolidation_coefficient(1e-9, 0.0, 1e-7);
assert!((cc).abs() < 1e-12);
}
#[test]
fn test_pore_pressure_ratio_surface() {
let r = pore_pressure_ratio(0.0, 1.0, 1.0);
assert!(r.abs() < 0.01);
}
#[test]
fn test_pore_pressure_ratio_zero_time() {
let r = pore_pressure_ratio(1.0, 1.0, 0.0);
assert!(r > 0.9);
}
#[test]
fn test_pore_pressure_ratio_range() {
for i in 0..10 {
let tv = i as f64 * 0.1;
let r = pore_pressure_ratio(0.5, 1.0, tv);
assert!((0.0..=1.0).contains(&r));
}
}
}