1#![allow(dead_code)]
29#![allow(clippy::too_many_arguments)]
30
31use std::f64::consts::PI;
32
33const G_ACC: f64 = 9.81;
39
40const GAMMA_W: f64 = 9.81;
42
43const ABS_ZERO_C: f64 = -273.15;
45
46#[inline]
52fn clamp_f64(v: f64, lo: f64, hi: f64) -> f64 {
53 if v < lo {
54 lo
55 } else if v > hi {
56 hi
57 } else {
58 v
59 }
60}
61
62#[inline]
64fn mean_stress(s1: f64, s2: f64, s3: f64) -> f64 {
65 (s1 + s2 + s3) / 3.0
66}
67
68#[inline]
70fn deviatoric_q(s1: f64, s2: f64, s3: f64) -> f64 {
71 let t1 = (s1 - s2).powi(2);
72 let t2 = (s2 - s3).powi(2);
73 let t3 = (s3 - s1).powi(2);
74 ((t1 + t2 + t3) / 2.0).sqrt()
75}
76
77#[derive(Debug, Clone)]
90pub struct ModifiedCamClay {
91 pub m_slope: f64,
93 pub lambda: f64,
95 pub kappa: f64,
97 pub pc0: f64,
99 pub pc: f64,
101 pub v0: f64,
103 pub poisson: f64,
105}
106
107impl ModifiedCamClay {
108 pub fn new(m_slope: f64, lambda: f64, kappa: f64, pc0: f64, v0: f64, poisson: f64) -> Self {
110 Self {
111 m_slope,
112 lambda,
113 kappa,
114 pc0,
115 pc: pc0,
116 v0,
117 poisson,
118 }
119 }
120
121 pub fn yield_function(&self, p: f64, q: f64) -> f64 {
126 q * q / (self.m_slope * self.m_slope) + p * (p - self.pc)
127 }
128
129 pub fn is_yielding(&self, p: f64, q: f64) -> bool {
131 self.yield_function(p, q) >= -1e-12
132 }
133
134 pub fn bulk_modulus(&self, p: f64) -> f64 {
136 self.v0 * p / self.kappa
137 }
138
139 pub fn shear_modulus(&self, p: f64) -> f64 {
141 let k = self.bulk_modulus(p);
142 3.0 * k * (1.0 - 2.0 * self.poisson) / (2.0 * (1.0 + self.poisson))
143 }
144
145 pub fn harden(&mut self, d_eps_v_p: f64) {
148 let factor = self.v0 / (self.lambda - self.kappa);
149 self.pc *= (factor * d_eps_v_p).exp();
150 }
151
152 pub fn stress_ratio(p: f64, q: f64) -> f64 {
154 if p.abs() < 1e-15 { 0.0 } else { q / p }
155 }
156
157 pub fn ocr(&self, p: f64) -> f64 {
159 if p.abs() < 1e-15 {
160 f64::INFINITY
161 } else {
162 self.pc / p
163 }
164 }
165
166 pub fn ncl_volumetric_strain(&self, p0: f64, p: f64) -> f64 {
168 if p0 <= 0.0 || p <= 0.0 {
169 return 0.0;
170 }
171 (self.lambda / self.v0) * (p / p0).ln()
172 }
173
174 pub fn elastic_volumetric_strain(&self, p0: f64, p: f64) -> f64 {
176 if p0 <= 0.0 || p <= 0.0 {
177 return 0.0;
178 }
179 (self.kappa / self.v0) * (p / p0).ln()
180 }
181
182 pub fn yield_surface_size(&self) -> f64 {
184 self.pc
185 }
186
187 pub fn critical_state_p(&self, q: f64) -> f64 {
189 q / self.m_slope
190 }
191
192 pub fn reset(&mut self) {
194 self.pc = self.pc0;
195 }
196}
197
198#[derive(Debug, Clone)]
206pub struct MohrCoulombModel {
207 pub cohesion: f64,
209 pub friction_angle: f64,
211 pub dilation_angle: f64,
213 pub tensile_strength: f64,
215}
216
217impl MohrCoulombModel {
218 pub fn new(cohesion: f64, friction_deg: f64, dilation_deg: f64, tensile_strength: f64) -> Self {
221 Self {
222 cohesion,
223 friction_angle: friction_deg.to_radians(),
224 dilation_angle: dilation_deg.to_radians(),
225 tensile_strength,
226 }
227 }
228
229 pub fn shear_strength(&self, sigma_n: f64) -> f64 {
231 self.cohesion + sigma_n * self.friction_angle.tan()
232 }
233
234 pub fn yield_function(&self, sigma_n: f64, tau: f64) -> f64 {
237 tau.abs() - self.shear_strength(sigma_n)
238 }
239
240 pub fn is_failed_principals(&self, sigma1: f64, sigma3: f64) -> bool {
242 let sin_phi = self.friction_angle.sin();
243 let lhs = (sigma1 - sigma3) / 2.0;
244 let rhs = (sigma1 + sigma3) / 2.0 * sin_phi + self.cohesion * self.friction_angle.cos();
245 lhs >= rhs - 1e-12
246 }
247
248 pub fn max_deviator(&self, sigma3: f64) -> f64 {
250 let sin_phi = self.friction_angle.sin();
251 let _cos_phi = self.friction_angle.cos();
252 let n_phi = (1.0 + sin_phi) / (1.0 - sin_phi);
253 2.0 * self.cohesion * n_phi.sqrt() + sigma3 * (n_phi - 1.0)
254 }
255
256 pub fn tension_cutoff(&self, sigma3: f64) -> bool {
258 sigma3 < -self.tensile_strength
259 }
260
261 pub fn dilation_factor(&self) -> f64 {
263 self.dilation_angle.sin()
264 }
265
266 pub fn from_ucs(ucs: f64, cohesion: f64) -> f64 {
268 let mut phi = 30.0_f64.to_radians();
271 for _ in 0..20 {
272 let sp = phi.sin();
273 let cp = phi.cos();
274 let f_val = ucs * (1.0 - sp) - 2.0 * cohesion * cp;
275 let df = -ucs * cp + 2.0 * cohesion * sp;
276 if df.abs() < 1e-30 {
277 break;
278 }
279 phi -= f_val / df;
280 phi = clamp_f64(phi, 0.001, PI / 2.0 - 0.001);
281 }
282 phi
283 }
284}
285
286#[derive(Debug, Clone)]
294pub struct CapPlasticityModel {
295 pub alpha: f64,
297 pub k_cohesion: f64,
299 pub cap_r: f64,
301 pub x0: f64,
303 pub x_cap: f64,
305 pub hardening_w: f64,
307 pub hardening_d: f64,
309}
310
311impl CapPlasticityModel {
312 pub fn new(
314 alpha: f64,
315 k_cohesion: f64,
316 cap_r: f64,
317 x0: f64,
318 hardening_w: f64,
319 hardening_d: f64,
320 ) -> Self {
321 Self {
322 alpha,
323 k_cohesion,
324 cap_r,
325 x0,
326 x_cap: x0,
327 hardening_w,
328 hardening_d,
329 }
330 }
331
332 pub fn shear_yield(&self, p: f64, q: f64) -> f64 {
334 q - self.alpha * p - self.k_cohesion
335 }
336
337 pub fn cap_yield(&self, p: f64, q: f64) -> f64 {
340 let l = self.cap_l();
341 let rq = self.cap_r * q;
342 let dp = p - l;
343 (dp * dp + rq * rq).sqrt() - (self.x_cap - l)
344 }
345
346 fn cap_l(&self) -> f64 {
348 self.x_cap - self.cap_r * (self.alpha * self.x_cap + self.k_cohesion)
349 }
350
351 pub fn is_elastic(&self, p: f64, q: f64) -> bool {
353 self.shear_yield(p, q) < 1e-12 && self.cap_yield(p, q) < 1e-12
354 }
355
356 pub fn harden_cap(&mut self, d_eps_v_p: f64) {
358 let eps_total = self.current_plastic_vol_strain() + d_eps_v_p;
360 self.x_cap = self.x0
361 - (1.0 / self.hardening_d) * (1.0 - eps_total / self.hardening_w).ln().max(-50.0);
362 }
363
364 pub fn current_plastic_vol_strain(&self) -> f64 {
366 self.hardening_w * (1.0 - (-self.hardening_d * (self.x_cap - self.x0)).exp())
367 }
368
369 pub fn reset(&mut self) {
371 self.x_cap = self.x0;
372 }
373}
374
375#[derive(Debug, Clone)]
383pub struct HoekBrownRock {
384 pub sigma_ci: f64,
386 pub m_i: f64,
388 pub gsi: f64,
390 pub disturbance: f64,
392 pub m_b: f64,
394 pub s: f64,
396 pub a: f64,
398}
399
400impl HoekBrownRock {
401 pub fn new(sigma_ci: f64, m_i: f64, gsi: f64, disturbance: f64) -> Self {
403 let m_b = m_i * ((gsi - 100.0) / (28.0 - 14.0 * disturbance)).exp();
404 let s = ((gsi - 100.0) / (9.0 - 3.0 * disturbance)).exp();
405 let a = 0.5 + ((-gsi / 15.0_f64).exp() - (-20.0 / 3.0_f64).exp()) / 6.0;
406 Self {
407 sigma_ci,
408 m_i,
409 gsi,
410 disturbance,
411 m_b,
412 s,
413 a,
414 }
415 }
416
417 pub fn sigma1_at_failure(&self, sigma3: f64) -> f64 {
419 let ratio = sigma3 / self.sigma_ci;
420 let inner = self.m_b * ratio + self.s;
421 if inner <= 0.0 {
422 sigma3
423 } else {
424 sigma3 + self.sigma_ci * inner.powf(self.a)
425 }
426 }
427
428 pub fn ucs_rock_mass(&self) -> f64 {
430 self.sigma_ci * self.s.powf(self.a)
431 }
432
433 pub fn tensile_strength(&self) -> f64 {
435 -self.s * self.sigma_ci / self.m_b
436 }
437
438 pub fn equivalent_mc(&self, sig3_min: f64, sig3_max: f64) -> (f64, f64) {
443 let n = 10;
444 let ds = (sig3_max - sig3_min) / n as f64;
445 let mut sum_tau = 0.0;
446 let mut sum_sigma = 0.0;
447 let mut sum_st = 0.0;
448 let mut sum_s2 = 0.0;
449 let nf = (n + 1) as f64;
450 for i in 0..=n {
451 let s3 = sig3_min + i as f64 * ds;
452 let s1 = self.sigma1_at_failure(s3);
453 let sigma_n = (s1 + s3) / 2.0;
454 let tau = (s1 - s3) / 2.0;
455 sum_sigma += sigma_n;
456 sum_tau += tau;
457 sum_st += sigma_n * tau;
458 sum_s2 += sigma_n * sigma_n;
459 }
460 let denom = nf * sum_s2 - sum_sigma * sum_sigma;
461 if denom.abs() < 1e-30 {
462 return (0.0, 0.0);
463 }
464 let tan_phi = (nf * sum_st - sum_sigma * sum_tau) / denom;
465 let c = (sum_tau - tan_phi * sum_sigma) / nf;
466 (c.max(0.0), tan_phi.atan().max(0.0))
467 }
468
469 pub fn deformation_modulus(&self) -> f64 {
471 let e_i = self.sigma_ci * 1000.0; e_i * (0.02
473 + (1.0 - self.disturbance / 2.0)
474 / (1.0 + ((60.0 + 15.0 * self.disturbance - self.gsi) / 11.0).exp()))
475 }
476}
477
478#[derive(Debug, Clone)]
486pub struct KozenyCarmanPermeability {
487 pub shape_factor: f64,
489 pub d10: f64,
491 pub viscosity: f64,
493}
494
495impl KozenyCarmanPermeability {
496 pub fn new(shape_factor: f64, d10: f64, viscosity: f64) -> Self {
498 Self {
499 shape_factor,
500 d10,
501 viscosity,
502 }
503 }
504
505 pub fn default_water(d10: f64) -> Self {
507 Self::new(1.0 / 180.0, d10, 1.002e-3)
508 }
509
510 pub fn intrinsic_permeability(&self, e: f64) -> f64 {
512 if e <= 0.0 {
513 return 0.0;
514 }
515 self.shape_factor * (e.powi(3) / (1.0 + e)) * self.d10.powi(2)
516 }
517
518 pub fn hydraulic_conductivity(&self, e: f64) -> f64 {
520 let k_intr = self.intrinsic_permeability(e);
521 k_intr * GAMMA_W * 1000.0 / self.viscosity }
523
524 pub fn permeability_ratio(&self, e0: f64, e1: f64) -> f64 {
526 if e0 <= 0.0 {
527 return 0.0;
528 }
529 let k0 = self.intrinsic_permeability(e0);
530 let k1 = self.intrinsic_permeability(e1);
531 if k0.abs() < 1e-30 { 0.0 } else { k1 / k0 }
532 }
533
534 pub fn porosity(e: f64) -> f64 {
536 e / (1.0 + e)
537 }
538
539 pub fn void_ratio_from_porosity(n: f64) -> f64 {
541 if n >= 1.0 {
542 return f64::INFINITY;
543 }
544 n / (1.0 - n)
545 }
546}
547
548#[derive(Debug, Clone)]
557pub struct TerzaghiConsolidation {
558 pub cv: f64,
560 pub h_dr: f64,
562 pub u0: f64,
564}
565
566impl TerzaghiConsolidation {
567 pub fn new(cv: f64, h_dr: f64, u0: f64) -> Self {
569 Self { cv, h_dr, u0 }
570 }
571
572 pub fn time_factor(&self, t: f64) -> f64 {
574 self.cv * t / (self.h_dr * self.h_dr)
575 }
576
577 pub fn degree_of_consolidation(&self, t: f64) -> f64 {
579 let tv = self.time_factor(t);
580 if tv <= 0.0 {
581 return 0.0;
582 }
583 let mut u = 0.0;
586 for m in 0..50 {
587 let big_m = PI / 2.0 * (2.0 * m as f64 + 1.0);
588 let term = (2.0 / (big_m * big_m)) * (-big_m * big_m * tv).exp();
589 u += term;
590 if term.abs() < 1e-15 {
591 break;
592 }
593 }
594 clamp_f64(1.0 - u, 0.0, 1.0)
595 }
596
597 pub fn pore_pressure(&self, z: f64, t: f64) -> f64 {
599 let tv = self.time_factor(t);
600 if tv <= 0.0 {
601 return self.u0;
602 }
603 let mut u = 0.0;
604 for m in 0..50 {
605 let big_m = PI / 2.0 * (2.0 * m as f64 + 1.0);
606 let term = (2.0 * self.u0 / big_m)
607 * (big_m * z / self.h_dr).sin()
608 * (-big_m * big_m * tv).exp();
609 u += term;
610 if term.abs() < 1e-15 {
611 break;
612 }
613 }
614 clamp_f64(u, 0.0, self.u0)
615 }
616
617 pub fn settlement(&self, t: f64, s_final: f64) -> f64 {
619 self.degree_of_consolidation(t) * s_final
620 }
621
622 pub fn time_for_consolidation(&self, u_target: f64) -> f64 {
625 let u_c = clamp_f64(u_target, 0.0, 0.999);
626 let tv = if u_c < 0.6 {
628 PI / 4.0 * u_c * u_c
629 } else {
630 -0.9332 * (1.0 - u_c).ln() - 0.0851
631 };
632 tv * self.h_dr * self.h_dr / self.cv
633 }
634
635 pub fn mv_from_cc(cc: f64, e0: f64, sigma0: f64, delta_sigma: f64) -> f64 {
638 if delta_sigma <= 0.0 || sigma0 <= 0.0 {
639 return 0.0;
640 }
641 let de = cc * ((sigma0 + delta_sigma) / sigma0).log10();
642 de / ((1.0 + e0) * delta_sigma)
643 }
644}
645
646#[derive(Debug, Clone)]
654pub struct SwellingClayModel {
655 pub swelling_index: f64,
657 pub shrinkage_limit_suction: f64,
659 pub e0: f64,
661 pub suction0: f64,
663 pub max_swell_strain: f64,
665}
666
667impl SwellingClayModel {
668 pub fn new(
670 swelling_index: f64,
671 shrinkage_limit_suction: f64,
672 e0: f64,
673 suction0: f64,
674 max_swell_strain: f64,
675 ) -> Self {
676 Self {
677 swelling_index,
678 shrinkage_limit_suction,
679 e0,
680 suction0,
681 max_swell_strain,
682 }
683 }
684
685 pub fn volumetric_strain(&self, s_new: f64) -> f64 {
688 if s_new <= 0.0 || self.suction0 <= 0.0 {
689 return 0.0;
690 }
691 let de = -self.swelling_index * (s_new / self.suction0).log10();
692 let eps_v = de / (1.0 + self.e0);
693 clamp_f64(eps_v, -self.max_swell_strain, self.max_swell_strain)
694 }
695
696 pub fn void_ratio_at(&self, s_new: f64) -> f64 {
698 if s_new <= 0.0 || self.suction0 <= 0.0 {
699 return self.e0;
700 }
701 let de = -self.swelling_index * (s_new / self.suction0).log10();
702 (self.e0 + de).max(0.0)
703 }
704
705 pub fn swell_pressure(&self) -> f64 {
708 self.shrinkage_limit_suction
711 }
712
713 pub fn is_at_shrinkage_limit(&self, suction: f64) -> bool {
715 suction >= self.shrinkage_limit_suction
716 }
717
718 pub fn linear_swell_from_pi(plasticity_index: f64) -> f64 {
721 2.16e-3 * plasticity_index.powf(2.44)
723 }
724}
725
726#[derive(Debug, Clone)]
735pub struct LiquefactionCriteria {
736 pub magnitude: f64,
738 pub pga: f64,
740 pub sigma_v: f64,
742 pub sigma_v_eff: f64,
744 pub n1_60: f64,
746 pub fines_content: f64,
748}
749
750impl LiquefactionCriteria {
751 pub fn new(
753 magnitude: f64,
754 pga: f64,
755 sigma_v: f64,
756 sigma_v_eff: f64,
757 n1_60: f64,
758 fines_content: f64,
759 ) -> Self {
760 Self {
761 magnitude,
762 pga,
763 sigma_v,
764 sigma_v_eff,
765 n1_60,
766 fines_content,
767 }
768 }
769
770 pub fn csr(&self) -> f64 {
772 if self.sigma_v_eff <= 0.0 {
773 return 0.0;
774 }
775 let rd = self.depth_reduction_factor();
776 0.65 * (self.sigma_v / self.sigma_v_eff) * self.pga * rd
777 }
778
779 fn depth_reduction_factor(&self) -> f64 {
781 let _depth = self.sigma_v / 18_000.0;
783 let z = self.sigma_v / 18_000.0;
784 if z <= 9.15 {
785 1.0 - 0.00765 * z
786 } else if z <= 23.0 {
787 1.174 - 0.0267 * z
788 } else {
789 0.5 }
791 }
792
793 pub fn crr_clean_sand(&self) -> f64 {
795 let n = self.n1_60_cs();
796 if n >= 30.0 {
797 return f64::INFINITY; }
799 let a = n / 34.8;
801 let b = n / 135.0;
802 let c = 50.0 / (10.0 * n + 45.0).powi(2);
803 1.0 / (a + b + c) * 0.048 + 0.004 * n
804 }
805
806 pub fn n1_60_cs(&self) -> f64 {
808 let fc = self.fines_content;
809 let alpha = if fc <= 5.0 {
810 0.0
811 } else if fc >= 35.0 {
812 5.0
813 } else {
814 (fc - 5.0) / 6.0
815 };
816 let beta = if fc <= 5.0 {
817 1.0
818 } else if fc >= 35.0 {
819 1.2
820 } else {
821 1.0 + 0.2 * (fc - 5.0) / 30.0
822 };
823 alpha + beta * self.n1_60
824 }
825
826 pub fn magnitude_scaling_factor(&self) -> f64 {
828 10.0_f64.powf(2.24) / self.magnitude.powf(2.56)
829 }
830
831 pub fn factor_of_safety(&self) -> f64 {
833 let csr = self.csr();
834 if csr <= 0.0 {
835 return f64::INFINITY;
836 }
837 let crr = self.crr_clean_sand();
838 let msf = self.magnitude_scaling_factor();
839 crr * msf / csr
840 }
841
842 pub fn is_liquefiable(&self) -> bool {
844 self.factor_of_safety() < 1.0
845 }
846}
847
848#[derive(Debug, Clone)]
857pub struct RockfillModel {
858 pub phi_base: f64,
860 pub delta_phi: f64,
862 pub sigma_ref: f64,
864 pub modulus_number: f64,
866 pub modulus_exponent: f64,
868 pub p_atm: f64,
870 pub e_max: f64,
872 pub e_min: f64,
874}
875
876impl RockfillModel {
877 pub fn new(
879 phi_base_deg: f64,
880 delta_phi_deg: f64,
881 sigma_ref: f64,
882 modulus_number: f64,
883 modulus_exponent: f64,
884 ) -> Self {
885 Self {
886 phi_base: phi_base_deg.to_radians(),
887 delta_phi: delta_phi_deg.to_radians(),
888 sigma_ref,
889 modulus_number,
890 modulus_exponent,
891 p_atm: 101_325.0,
892 e_max: 0.9,
893 e_min: 0.4,
894 }
895 }
896
897 pub fn friction_angle(&self, sigma3: f64) -> f64 {
899 if sigma3 <= 0.0 {
900 return self.phi_base;
901 }
902 let reduction = self.delta_phi * (sigma3 / self.sigma_ref).log10().max(0.0);
903 (self.phi_base - reduction).max(0.1_f64.to_radians())
904 }
905
906 pub fn youngs_modulus(&self, sigma3: f64) -> f64 {
908 if sigma3 <= 0.0 {
909 return self.modulus_number * self.p_atm;
910 }
911 self.modulus_number * self.p_atm * (sigma3 / self.p_atm).powf(self.modulus_exponent)
912 }
913
914 pub fn shear_strength(&self, sigma3: f64) -> f64 {
916 sigma3 * self.friction_angle(sigma3).tan()
917 }
918
919 pub fn relative_density(&self, e: f64) -> f64 {
921 if (self.e_max - self.e_min).abs() < 1e-15 {
922 return 0.5;
923 }
924 clamp_f64((self.e_max - e) / (self.e_max - self.e_min), 0.0, 1.0)
925 }
926
927 pub fn breakage_index(&self, sigma3: f64) -> f64 {
929 if sigma3 <= self.p_atm {
931 return 0.0;
932 }
933 let b = 0.15 * (sigma3 / self.p_atm).log10();
934 clamp_f64(b, 0.0, 1.0)
935 }
936}
937
938#[derive(Debug, Clone)]
946pub struct FrozenSoilModel {
947 pub c_unfrozen: f64,
949 pub phi_unfrozen: f64,
951 pub ice_strength_coeff: f64,
953 pub creep_n: f64,
955 pub creep_a: f64,
957 pub freeze_temp: f64,
959 pub unfrozen_alpha: f64,
961 pub unfrozen_beta: f64,
963}
964
965impl FrozenSoilModel {
966 pub fn new(
968 c_unfrozen: f64,
969 phi_unfrozen_deg: f64,
970 ice_strength_coeff: f64,
971 creep_n: f64,
972 creep_a: f64,
973 ) -> Self {
974 Self {
975 c_unfrozen,
976 phi_unfrozen: phi_unfrozen_deg.to_radians(),
977 ice_strength_coeff,
978 creep_n,
979 creep_a,
980 freeze_temp: 0.0,
981 unfrozen_alpha: 0.1,
982 unfrozen_beta: -0.5,
983 }
984 }
985
986 pub fn cohesion(&self, temp_c: f64) -> f64 {
988 if temp_c >= self.freeze_temp {
989 return self.c_unfrozen;
990 }
991 let dt = self.freeze_temp - temp_c;
992 self.c_unfrozen + self.ice_strength_coeff * dt
993 }
994
995 pub fn friction_angle(&self, temp_c: f64) -> f64 {
998 if temp_c >= self.freeze_temp {
999 return self.phi_unfrozen;
1000 }
1001 let dt = self.freeze_temp - temp_c;
1002 (self.phi_unfrozen - 0.005 * dt).max(5.0_f64.to_radians())
1004 }
1005
1006 pub fn ucs(&self, temp_c: f64) -> f64 {
1008 let c = self.cohesion(temp_c);
1009 let phi = self.friction_angle(temp_c);
1010 2.0 * c * phi.cos() / (1.0 - phi.sin())
1011 }
1012
1013 pub fn creep_rate(&self, q: f64, temp_c: f64) -> f64 {
1015 if temp_c >= self.freeze_temp || q <= 0.0 {
1016 return 0.0;
1017 }
1018 let dt = (self.freeze_temp - temp_c).max(0.01);
1020 let temp_factor = (-0.05 * dt).exp(); self.creep_a * (q / 1e6).powf(self.creep_n) * temp_factor
1022 }
1023
1024 pub fn unfrozen_water_content(&self, temp_c: f64) -> f64 {
1026 if temp_c >= self.freeze_temp {
1027 return 1.0; }
1029 let dt = (self.freeze_temp - temp_c).max(0.01);
1030 clamp_f64(self.unfrozen_alpha * dt.powf(self.unfrozen_beta), 0.0, 1.0)
1031 }
1032
1033 pub fn thermal_conductivity(&self, temp_c: f64, k_soil: f64, k_ice: f64, k_water: f64) -> f64 {
1035 let wu = self.unfrozen_water_content(temp_c);
1036 let wi = 1.0 - wu;
1037 let n = 0.4;
1040 k_soil.powf(1.0 - n) * k_water.powf(n * wu) * k_ice.powf(n * wi)
1041 }
1042
1043 pub fn is_frozen(&self, temp_c: f64) -> bool {
1045 temp_c < self.freeze_temp
1046 }
1047}
1048
1049pub fn mohr_coulomb_to_drucker_prager(cohesion: f64, friction_deg: f64) -> (f64, f64) {
1057 let phi = friction_deg.to_radians();
1058 let sin_phi = phi.sin();
1059 let cos_phi = phi.cos();
1060 let alpha = 6.0 * sin_phi / (3.0 - sin_phi);
1061 let k = 6.0 * cohesion * cos_phi / (3.0 - sin_phi);
1062 (alpha, k)
1063}
1064
1065pub fn effective_stress(total_stress: f64, pore_pressure: f64) -> f64 {
1067 total_stress - pore_pressure
1068}
1069
1070pub fn void_ratio_from_density(dry_density: f64, specific_gravity: f64) -> f64 {
1072 let rho_w = 1000.0; specific_gravity * rho_w / dry_density - 1.0
1074}
1075
1076pub fn compression_index_from_ll(liquid_limit: f64) -> f64 {
1078 0.009 * (liquid_limit - 10.0)
1079}
1080
1081pub fn k0_jaky(friction_deg: f64) -> f64 {
1083 1.0 - friction_deg.to_radians().sin()
1084}
1085
1086pub fn terzaghi_bearing_capacity(
1090 cohesion: f64,
1091 friction_deg: f64,
1092 gamma: f64,
1093 depth: f64,
1094 width: f64,
1095) -> f64 {
1096 let phi = friction_deg.to_radians();
1097 let tan_phi = phi.tan();
1098 let nq = if phi.abs() < 1e-10 {
1100 1.0
1101 } else {
1102 (PI * tan_phi).exp() * (PI / 4.0 + phi / 2.0).tan().powi(2)
1103 };
1104 let nc = if phi.abs() < 1e-10 {
1105 5.14
1106 } else {
1107 (nq - 1.0) / tan_phi
1108 };
1109 let ngamma = 2.0 * (nq + 1.0) * tan_phi;
1110 cohesion * nc + gamma * depth * nq + 0.5 * gamma * width * ngamma
1111}
1112
1113pub fn ka_rankine(friction_deg: f64) -> f64 {
1115 let phi = friction_deg.to_radians();
1116 let t = (PI / 4.0 - phi / 2.0).tan();
1117 t * t
1118}
1119
1120pub fn kp_rankine(friction_deg: f64) -> f64 {
1122 let phi = friction_deg.to_radians();
1123 let t = (PI / 4.0 + phi / 2.0).tan();
1124 t * t
1125}
1126
1127pub fn specific_surface(d: f64) -> f64 {
1129 if d <= 0.0 {
1130 return 0.0;
1131 }
1132 6.0 / d
1133}
1134
1135#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 const TOL: f64 = 1e-6;
1144
1145 #[test]
1148 fn test_mcc_yield_inside() {
1149 let mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1150 let f = mcc.yield_function(100.0, 0.0);
1152 assert!(f < 0.0, "Should be inside yield surface, f = {f}");
1153 }
1154
1155 #[test]
1156 fn test_mcc_yield_on_surface() {
1157 let mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1158 let f = mcc.yield_function(200.0, 0.0);
1160 assert!(f.abs() < TOL, "Should be on yield surface, f = {f}");
1161 }
1162
1163 #[test]
1164 fn test_mcc_yield_outside() {
1165 let mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1166 let f = mcc.yield_function(50.0, 200.0);
1168 assert!(f > 0.0, "Should be outside yield surface, f = {f}");
1169 }
1170
1171 #[test]
1172 fn test_mcc_hardening() {
1173 let mut mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1174 let old_pc = mcc.pc;
1175 mcc.harden(0.01);
1176 assert!(mcc.pc > old_pc, "pc should increase after compression");
1177 }
1178
1179 #[test]
1180 fn test_mcc_ocr() {
1181 let mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1182 let ocr = mcc.ocr(100.0);
1183 assert!((ocr - 2.0).abs() < TOL, "OCR should be 2.0, got {ocr}");
1184 }
1185
1186 #[test]
1187 fn test_mcc_bulk_modulus() {
1188 let mcc = ModifiedCamClay::new(1.0, 0.1, 0.02, 200.0, 2.0, 0.3);
1189 let k = mcc.bulk_modulus(100.0);
1190 assert!((k - 10_000.0).abs() < TOL, "K should be 10000, got {k}");
1192 }
1193
1194 #[test]
1197 fn test_mc_shear_strength() {
1198 let mc = MohrCoulombModel::new(50_000.0, 30.0, 0.0, 0.0);
1199 let tau = mc.shear_strength(100_000.0);
1200 let expected = 50_000.0 + 100_000.0 * 30.0_f64.to_radians().tan();
1201 assert!(
1202 (tau - expected).abs() < 1.0,
1203 "tau = {tau}, expected = {expected}"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_mc_failure_check() {
1209 let mc = MohrCoulombModel::new(0.0, 30.0, 0.0, 0.0);
1210 let n_phi = (1.0 + 30.0_f64.to_radians().sin()) / (1.0 - 30.0_f64.to_radians().sin());
1212 let sigma3 = 100_000.0;
1213 let sigma1 = sigma3 * n_phi;
1214 assert!(
1215 mc.is_failed_principals(sigma1, sigma3),
1216 "Should be at failure"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_mc_elastic_state() {
1222 let mc = MohrCoulombModel::new(100_000.0, 30.0, 0.0, 0.0);
1223 assert!(
1224 !mc.is_failed_principals(150_000.0, 100_000.0),
1225 "Should be elastic with high cohesion"
1226 );
1227 }
1228
1229 #[test]
1232 fn test_cap_elastic() {
1233 let cap = CapPlasticityModel::new(0.3, 20_000.0, 0.5, 200_000.0, 0.1, 0.001);
1240 assert!(cap.is_elastic(180_000.0, 5_000.0), "Should be elastic");
1245 }
1246
1247 #[test]
1248 fn test_cap_shear_yield() {
1249 let cap = CapPlasticityModel::new(0.5, 50_000.0, 0.5, -200_000.0, 0.1, 0.001);
1250 let f = cap.shear_yield(-10_000.0, 200_000.0);
1251 assert!(f > 0.0, "Should exceed shear yield, f = {f}");
1252 }
1253
1254 #[test]
1255 fn test_cap_hardening() {
1256 let mut cap = CapPlasticityModel::new(0.5, 50_000.0, 0.5, -200_000.0, 0.1, 0.001);
1257 let old_x = cap.x_cap;
1258 cap.harden_cap(0.01);
1259 assert!(
1260 (cap.x_cap - old_x).abs() > 1e-10,
1261 "Cap should move after hardening"
1262 );
1263 }
1264
1265 #[test]
1268 fn test_hb_ucs_intact() {
1269 let hb = HoekBrownRock::new(100e6, 25.0, 100.0, 0.0);
1271 let ucs = hb.ucs_rock_mass();
1272 assert!(
1274 (ucs - 100e6).abs() / 100e6 < 0.05,
1275 "UCS_rm should be ~ sigma_ci for GSI=100, got {ucs}"
1276 );
1277 }
1278
1279 #[test]
1280 fn test_hb_sigma1_at_failure() {
1281 let hb = HoekBrownRock::new(100e6, 25.0, 75.0, 0.0);
1282 let s1 = hb.sigma1_at_failure(10e6);
1283 assert!(s1 > 10e6, "sigma1 at failure must exceed sigma3");
1284 }
1285
1286 #[test]
1287 fn test_hb_tensile_strength() {
1288 let hb = HoekBrownRock::new(100e6, 25.0, 75.0, 0.0);
1289 let ts = hb.tensile_strength();
1290 assert!(ts < 0.0, "Tensile strength should be negative (tension)");
1291 }
1292
1293 #[test]
1296 fn test_kc_permeability_increases_with_e() {
1297 let kc = KozenyCarmanPermeability::default_water(0.001);
1298 let k1 = kc.intrinsic_permeability(0.5);
1299 let k2 = kc.intrinsic_permeability(1.0);
1300 assert!(
1301 k2 > k1,
1302 "Permeability should increase with void ratio: k1={k1}, k2={k2}"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_kc_zero_void_ratio() {
1308 let kc = KozenyCarmanPermeability::default_water(0.001);
1309 let k = kc.intrinsic_permeability(0.0);
1310 assert!(k.abs() < TOL, "Zero void ratio -> zero permeability");
1311 }
1312
1313 #[test]
1314 fn test_kc_porosity_conversion() {
1315 let n = KozenyCarmanPermeability::porosity(1.0);
1316 assert!((n - 0.5).abs() < TOL, "e=1 -> n=0.5, got {n}");
1317 let e = KozenyCarmanPermeability::void_ratio_from_porosity(0.5);
1318 assert!((e - 1.0).abs() < TOL, "n=0.5 -> e=1.0, got {e}");
1319 }
1320
1321 #[test]
1324 fn test_terzaghi_initial() {
1325 let tc = TerzaghiConsolidation::new(1e-7, 5.0, 100_000.0);
1326 let u = tc.degree_of_consolidation(0.0);
1327 assert!(u.abs() < TOL, "U at t=0 should be 0, got {u}");
1328 }
1329
1330 #[test]
1331 fn test_terzaghi_long_time() {
1332 let tc = TerzaghiConsolidation::new(1e-7, 1.0, 100_000.0);
1333 let u = tc.degree_of_consolidation(1e12);
1335 assert!(
1336 (u - 1.0).abs() < 0.01,
1337 "U should approach 1.0 at long time, got {u}"
1338 );
1339 }
1340
1341 #[test]
1342 fn test_terzaghi_settlement() {
1343 let tc = TerzaghiConsolidation::new(1e-7, 1.0, 100_000.0);
1344 let s = tc.settlement(1e12, 0.5);
1345 assert!(
1346 (s - 0.5).abs() < 0.01,
1347 "Final settlement should be 0.5 m, got {s}"
1348 );
1349 }
1350
1351 #[test]
1354 fn test_swell_wetting() {
1355 let sc = SwellingClayModel::new(0.1, 1e6, 0.8, 500_000.0, 0.2);
1356 let eps = sc.volumetric_strain(100_000.0);
1358 assert!(eps > 0.0, "Wetting should cause positive swell, got {eps}");
1359 }
1360
1361 #[test]
1362 fn test_swell_drying() {
1363 let sc = SwellingClayModel::new(0.1, 1e6, 0.8, 500_000.0, 0.2);
1364 let eps = sc.volumetric_strain(1_000_000.0);
1366 assert!(eps < 0.0, "Drying should cause shrinkage, got {eps}");
1367 }
1368
1369 #[test]
1372 fn test_liquefaction_csr() {
1373 let liq = LiquefactionCriteria::new(7.5, 0.3, 100_000.0, 60_000.0, 10.0, 5.0);
1374 let csr = liq.csr();
1375 assert!(csr > 0.0, "CSR should be positive, got {csr}");
1376 assert!(csr < 1.0, "CSR should be < 1 for typical conditions");
1377 }
1378
1379 #[test]
1380 fn test_liquefaction_dense_sand() {
1381 let liq = LiquefactionCriteria::new(7.5, 0.15, 100_000.0, 80_000.0, 35.0, 5.0);
1383 assert!(
1384 !liq.is_liquefiable(),
1385 "Dense sand (N1_60=35) should not liquefy"
1386 );
1387 }
1388
1389 #[test]
1392 fn test_rockfill_phi_reduction() {
1393 let rf = RockfillModel::new(45.0, 8.0, 100_000.0, 500.0, 0.5);
1394 let phi_low = rf.friction_angle(100_000.0);
1395 let phi_high = rf.friction_angle(1_000_000.0);
1396 assert!(
1397 phi_high < phi_low,
1398 "Friction should decrease with stress: low={phi_low:.4}, high={phi_high:.4}"
1399 );
1400 }
1401
1402 #[test]
1403 fn test_rockfill_modulus() {
1404 let rf = RockfillModel::new(45.0, 8.0, 100_000.0, 500.0, 0.5);
1405 let e1 = rf.youngs_modulus(100_000.0);
1406 let e2 = rf.youngs_modulus(400_000.0);
1407 assert!(e2 > e1, "Modulus should increase with stress");
1408 }
1409
1410 #[test]
1413 fn test_frozen_cohesion_increase() {
1414 let fs = FrozenSoilModel::new(20_000.0, 25.0, 50_000.0, 3.0, 1e-12);
1415 let c_warm = fs.cohesion(5.0); let c_cold = fs.cohesion(-10.0); assert!(
1418 c_cold > c_warm,
1419 "Cohesion should increase when frozen: warm={c_warm}, cold={c_cold}"
1420 );
1421 }
1422
1423 #[test]
1424 fn test_frozen_ucs() {
1425 let fs = FrozenSoilModel::new(20_000.0, 25.0, 50_000.0, 3.0, 1e-12);
1426 let ucs_warm = fs.ucs(5.0);
1427 let ucs_cold = fs.ucs(-20.0);
1428 assert!(
1429 ucs_cold > ucs_warm,
1430 "UCS should increase when frozen: warm={ucs_warm}, cold={ucs_cold}"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_frozen_creep_rate() {
1436 let fs = FrozenSoilModel::new(20_000.0, 25.0, 50_000.0, 3.0, 1e-12);
1437 let rate = fs.creep_rate(1e6, -5.0);
1438 assert!(rate > 0.0, "Creep rate should be positive at sub-zero temp");
1439 }
1440
1441 #[test]
1442 fn test_frozen_no_creep_above_zero() {
1443 let fs = FrozenSoilModel::new(20_000.0, 25.0, 50_000.0, 3.0, 1e-12);
1444 let rate = fs.creep_rate(1e6, 5.0);
1445 assert!(rate.abs() < TOL, "No creep above freezing, got {rate}");
1446 }
1447
1448 #[test]
1451 fn test_dp_conversion() {
1452 let (alpha, k) = mohr_coulomb_to_drucker_prager(50_000.0, 30.0);
1453 assert!(alpha > 0.0, "alpha should be positive");
1454 assert!(k > 0.0, "k should be positive");
1455 }
1456
1457 #[test]
1458 fn test_effective_stress() {
1459 let es = effective_stress(200_000.0, 50_000.0);
1460 assert!(
1461 (es - 150_000.0).abs() < TOL,
1462 "Effective stress should be 150 kPa, got {es}"
1463 );
1464 }
1465
1466 #[test]
1467 fn test_k0_jaky_value() {
1468 let k0 = k0_jaky(30.0);
1469 let expected = 1.0 - 30.0_f64.to_radians().sin();
1470 assert!(
1471 (k0 - expected).abs() < TOL,
1472 "K0 = {k0}, expected {expected}"
1473 );
1474 }
1475
1476 #[test]
1477 fn test_bearing_capacity_positive() {
1478 let q = terzaghi_bearing_capacity(50_000.0, 30.0, 18_000.0, 1.0, 2.0);
1479 assert!(q > 0.0, "Bearing capacity should be positive, got {q}");
1480 }
1481
1482 #[test]
1483 fn test_rankine_ka_kp_product() {
1484 let ka = ka_rankine(30.0);
1485 let kp = kp_rankine(30.0);
1486 assert!(
1488 (ka * kp - 1.0).abs() < 0.01,
1489 "Ka * Kp should be ~1.0, got {:.6}",
1490 ka * kp
1491 );
1492 }
1493
1494 #[test]
1495 fn test_mean_deviatoric_stress() {
1496 let p = mean_stress(300.0, 200.0, 100.0);
1497 assert!((p - 200.0).abs() < TOL, "Mean stress should be 200");
1498 let q = deviatoric_q(300.0, 200.0, 100.0);
1499 let expected = (30000.0_f64).sqrt();
1502 assert!((q - expected).abs() < 0.1, "q = {q}, expected ~ {expected}");
1503 }
1504}