1#![allow(dead_code)]
8#![allow(clippy::too_many_arguments)]
9
10use std::f64::consts::PI;
11
12#[derive(Debug, Clone)]
20pub struct SoilModel {
21 pub cohesion: f64,
23 pub friction_angle: f64,
25 pub dilatancy_angle: f64,
27 pub young_modulus: f64,
29 pub poisson_ratio: f64,
31}
32
33impl SoilModel {
34 pub fn new(
36 cohesion: f64,
37 friction_angle_deg: f64,
38 dilatancy_angle_deg: f64,
39 young_modulus: f64,
40 poisson_ratio: f64,
41 ) -> Self {
42 Self {
43 cohesion,
44 friction_angle: friction_angle_deg.to_radians(),
45 dilatancy_angle: dilatancy_angle_deg.to_radians(),
46 young_modulus,
47 poisson_ratio,
48 }
49 }
50
51 pub fn medium_sand() -> Self {
53 Self::new(0.0, 32.0, 5.0, 30e6, 0.3)
54 }
55
56 pub fn stiff_clay() -> Self {
58 Self::new(60e3, 22.0, 0.0, 80e6, 0.35)
59 }
60
61 pub fn yield_function(&self, sigma1: f64, sigma3: f64) -> f64 {
66 let phi = self.friction_angle;
67 (sigma1 - sigma3) - 2.0 * self.cohesion * phi.cos() - (sigma1 + sigma3) * phi.sin()
68 }
69
70 pub fn is_yielding(&self, sigma1: f64, sigma3: f64) -> bool {
72 self.yield_function(sigma1, sigma3) >= 0.0
73 }
74
75 pub fn plastic_multiplier(&self, d_eps_p: f64) -> f64 {
79 d_eps_p / (1.0 + self.dilatancy_angle.sin())
80 }
81
82 pub fn critical_state_line_q(&self, p: f64) -> f64 {
86 let phi = self.friction_angle;
87 let m_cs = 6.0 * phi.sin() / (3.0 - phi.sin());
88 m_cs * p
89 }
90
91 pub fn bulk_modulus(&self) -> f64 {
93 self.young_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio))
94 }
95
96 pub fn shear_modulus(&self) -> f64 {
98 self.young_modulus / (2.0 * (1.0 + self.poisson_ratio))
99 }
100
101 pub fn ucs(&self) -> f64 {
105 let phi = self.friction_angle;
106 2.0 * self.cohesion * phi.cos() / (1.0 - phi.sin())
107 }
108
109 pub fn k0_jaky(&self) -> f64 {
113 1.0 - self.friction_angle.sin()
114 }
115}
116
117#[derive(Debug, Clone)]
125pub struct CamClayModel {
126 pub m: f64,
128 pub lambda: f64,
130 pub kappa: f64,
132 pub n_value: f64,
134 pub e0: f64,
136 pub p_c0: f64,
138}
139
140impl CamClayModel {
141 pub fn new(m: f64, lambda: f64, kappa: f64, n_value: f64, e0: f64, p_c0: f64) -> Self {
143 Self {
144 m,
145 lambda,
146 kappa,
147 n_value,
148 e0,
149 p_c0,
150 }
151 }
152
153 pub fn kaolin_ncc() -> Self {
155 Self::new(0.92, 0.26, 0.05, 3.0, 1.5, 100e3)
156 }
157
158 pub fn e_cs(&self, p: f64) -> f64 {
162 self.n_value - self.lambda * (p / 1e3).ln()
163 }
164
165 pub fn yield_surface_p0(&self, p: f64, q: f64) -> f64 {
169 q * q / (self.m * self.m) + p * (p - self.p_c0)
170 }
171
172 pub fn is_yielding(&self, p: f64, q: f64) -> bool {
174 self.yield_surface_p0(p, q) >= 0.0
175 }
176
177 pub fn preconsolidation_pressure(&self, void_ratio: f64) -> f64 {
181 let ln_p = (self.n_value - void_ratio) / self.lambda;
182 1e3 * ln_p.exp()
183 }
184
185 pub fn volumetric_strain_elastic(&self, dp: f64, p: f64) -> f64 {
189 if p <= 0.0 {
190 return 0.0;
191 }
192 (self.kappa / (1.0 + self.e0)) * dp / p
193 }
194
195 pub fn volumetric_strain_plastic(&self, dp0: f64, p0: f64) -> f64 {
199 if p0 <= 0.0 {
200 return 0.0;
201 }
202 ((self.lambda - self.kappa) / (1.0 + self.e0)) * dp0 / p0
203 }
204
205 pub fn critical_state_p(&self, void_ratio: f64) -> f64 {
207 let exp_arg = (self.n_value - void_ratio) / self.lambda;
208 1e3 * exp_arg.exp()
209 }
210}
211
212#[derive(Debug, Clone)]
218pub struct RockModel {
219 pub ucs: f64,
221 pub mi: f64,
223 pub gsi: f64,
225 pub d: f64,
227}
228
229impl RockModel {
230 pub fn new(ucs: f64, mi: f64, gsi: f64, d: f64) -> Self {
232 Self { ucs, mi, gsi, d }
233 }
234
235 pub fn granite() -> Self {
237 Self::new(200e6, 33.0, 75.0, 0.0)
238 }
239
240 pub fn shale() -> Self {
242 Self::new(20e6, 8.0, 40.0, 0.2)
243 }
244
245 pub fn mb_parameter(&self) -> f64 {
249 self.mi * ((self.gsi - 100.0) / (28.0 - 14.0 * self.d)).exp()
250 }
251
252 pub fn s_parameter(&self) -> f64 {
256 ((self.gsi - 100.0) / (9.0 - 3.0 * self.d)).exp()
257 }
258
259 pub fn a_parameter(&self) -> f64 {
263 0.5 + (1.0 / 6.0) * ((-self.gsi / 15.0).exp() - (-20.0_f64 / 3.0).exp())
264 }
265
266 pub fn yield_hoek_brown(&self, sigma1: f64, sigma3: f64) -> bool {
270 let mb = self.mb_parameter();
271 let s = self.s_parameter();
272 let a = self.a_parameter();
273 let rhs = sigma3 + self.ucs * (mb * sigma3 / self.ucs + s).max(0.0).powf(a);
274 sigma1 >= rhs
275 }
276
277 pub fn uniaxial_compressive_strength_rock(&self) -> f64 {
281 let s = self.s_parameter();
282 let a = self.a_parameter();
283 self.ucs * s.powf(a)
284 }
285
286 pub fn tensile_strength_rock(&self) -> f64 {
290 let mb = self.mb_parameter();
291 let s = self.s_parameter();
292 -(s * self.ucs) / mb
293 }
294
295 pub fn deformation_modulus(&self) -> f64 {
299 let factor = 1.0 - self.d / 2.0;
300 let ucs_mpa = self.ucs / 1e6;
301 let em_gpa = factor * (ucs_mpa / 100.0).sqrt() * 10.0_f64.powf((self.gsi - 10.0) / 40.0);
302 em_gpa * 1e9
303 }
304}
305
306#[derive(Debug, Clone)]
312pub struct SandModel {
313 pub relative_density: f64,
315 pub critical_state_friction_angle: f64,
317 pub d50: f64,
319 pub specific_gravity: f64,
321}
322
323impl SandModel {
324 pub fn new(
326 relative_density: f64,
327 critical_state_friction_angle_deg: f64,
328 d50: f64,
329 specific_gravity: f64,
330 ) -> Self {
331 Self {
332 relative_density,
333 critical_state_friction_angle: critical_state_friction_angle_deg.to_radians(),
334 d50,
335 specific_gravity,
336 }
337 }
338
339 pub fn medium_dense_quartz() -> Self {
341 Self::new(0.6, 32.0, 0.3e-3, 2.65)
342 }
343
344 pub fn loose_sand() -> Self {
346 Self::new(0.3, 30.0, 0.2e-3, 2.65)
347 }
348
349 pub fn peak_friction_angle(&self, dr: f64) -> f64 {
353 let ir = self.dilatancy_index(dr, 100.0);
354 self.critical_state_friction_angle + 5.0_f64.to_radians() * ir
355 }
356
357 pub fn dilatancy_index(&self, dr: f64, _stress_level: f64) -> f64 {
361 let q = 10.0;
362 let r = 1.0;
363 let p_kpa = 100.0_f64; (dr * (q - (p_kpa).ln()) - r).max(0.0)
365 }
366
367 pub fn bolton_dilatancy(&self, dr: f64, p_kpa: f64) -> f64 {
371 let q = 10.0;
372 let r = 1.0;
373 if p_kpa <= 0.0 {
374 return 0.0;
375 }
376 (dr * (q - p_kpa.ln()) - r).max(0.0)
377 }
378
379 pub fn e_max(&self) -> f64 {
381 0.9 }
383
384 pub fn e_min(&self) -> f64 {
386 0.6 }
388
389 pub fn current_void_ratio(&self) -> f64 {
391 self.e_max() - self.relative_density * (self.e_max() - self.e_min())
392 }
393
394 pub fn dry_unit_weight(&self) -> f64 {
396 let e = self.current_void_ratio();
397 self.specific_gravity * 9.81 / (1.0 + e)
398 }
399}
400
401#[derive(Debug, Clone)]
409pub struct PermafrostModel {
410 pub soil_type: PermafrostSoilType,
412 pub total_water_content: f64,
414 pub dry_density: f64,
416 pub lambda_solid: f64,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq)]
422pub enum PermafrostSoilType {
423 SiltyClay,
425 Sand,
427 Gravel,
429}
430
431impl PermafrostModel {
432 pub fn new(
434 soil_type: PermafrostSoilType,
435 total_water_content: f64,
436 dry_density: f64,
437 lambda_solid: f64,
438 ) -> Self {
439 Self {
440 soil_type,
441 total_water_content,
442 dry_density,
443 lambda_solid,
444 }
445 }
446
447 pub fn siberian_silt() -> Self {
449 Self::new(PermafrostSoilType::SiltyClay, 0.35, 1400.0, 2.5)
450 }
451
452 pub fn unfrozen_water_content(&self, t_celsius: f64) -> f64 {
456 if t_celsius >= 0.0 {
457 return self.total_water_content;
458 }
459 let (alpha, beta) = match self.soil_type {
460 PermafrostSoilType::SiltyClay => (0.45, 0.6),
461 PermafrostSoilType::Sand => (0.08, 0.5),
462 PermafrostSoilType::Gravel => (0.05, 0.4),
463 };
464 let t_abs = (-t_celsius).max(1e-6);
465 (alpha * t_abs.powf(-beta)).min(self.total_water_content)
466 }
467
468 pub fn ice_saturation(&self, t_celsius: f64) -> f64 {
470 if t_celsius >= 0.0 {
471 return 0.0;
472 }
473 let theta_u = self.unfrozen_water_content(t_celsius);
474 let theta_i = (self.total_water_content - theta_u).max(0.0);
475 if self.total_water_content > 0.0 {
476 theta_i / self.total_water_content
477 } else {
478 0.0
479 }
480 }
481
482 pub fn bulk_modulus_frozen(&self, t_celsius: f64) -> f64 {
486 let k_base = 1.5e8; let si = self.ice_saturation(t_celsius);
488 k_base * (1.0 + 5.0 * si)
489 }
490
491 pub fn thermal_conductivity(&self, theta_u: f64) -> f64 {
495 let n = 0.4; let lambda_water = 0.56_f64;
497 let lambda_ice = 2.2_f64;
498 let theta_i = (n - theta_u).max(0.0);
499 self.lambda_solid.powf(1.0 - n) * lambda_water.powf(theta_u) * lambda_ice.powf(theta_i)
500 }
501
502 pub fn volumetric_heat_capacity(&self, t_celsius: f64) -> f64 {
504 let theta_u = self.unfrozen_water_content(t_celsius);
505 let theta_i = (self.total_water_content - theta_u).max(0.0);
506 let n = 0.4;
507 2.0e6 * (1.0 - n) + 4.18e6 * theta_u + 1.9e6 * theta_i
509 }
510}
511
512#[derive(Debug, Clone)]
520pub struct SeabedSedimentModel {
521 pub su0: f64,
523 pub k_su: f64,
525 pub compression_index_cc: f64,
527 pub recompression_index_cr: f64,
529 pub cv: f64,
531 pub e0: f64,
533 pub gamma_sat: f64,
535}
536
537impl SeabedSedimentModel {
538 pub fn new(
540 su0: f64,
541 k_su: f64,
542 compression_index_cc: f64,
543 recompression_index_cr: f64,
544 cv: f64,
545 e0: f64,
546 gamma_sat: f64,
547 ) -> Self {
548 Self {
549 su0,
550 k_su,
551 compression_index_cc,
552 recompression_index_cr,
553 cv,
554 e0,
555 gamma_sat,
556 }
557 }
558
559 pub fn gulf_of_mexico_clay() -> Self {
561 Self::new(5e3, 1.5e3, 0.7, 0.07, 2e-8, 2.8, 16.0)
562 }
563
564 pub fn undrained_shear_strength(&self, depth_m: f64) -> f64 {
568 self.su0 + self.k_su * depth_m
569 }
570
571 pub fn settlement_primary(&self, sigma_v0: f64, sigma_vf: f64, h: f64, e0: f64) -> f64 {
575 if sigma_v0 <= 0.0 || sigma_vf <= sigma_v0 {
576 return 0.0;
577 }
578 (self.compression_index_cc / (1.0 + e0)) * h * (sigma_vf / sigma_v0).log10()
579 }
580
581 pub fn time_to_consolidation(&self, cv: f64, h_dr: f64, u: f64) -> f64 {
586 let t_v = if u <= 0.6 {
588 PI * PI * u * u / 4.0
589 } else {
590 -0.933 * (1.0 - u).log10() - 0.085
591 };
592 t_v * h_dr * h_dr / cv
593 }
594
595 pub fn ocr(&self, sigma_v: f64, sigma_p: f64) -> f64 {
597 if sigma_v <= 0.0 {
598 return 1.0;
599 }
600 sigma_p / sigma_v
601 }
602
603 pub fn pore_pressure_ratio(&self, u_excess: f64, sigma_v_eff: f64) -> f64 {
605 if sigma_v_eff <= 0.0 {
606 return 0.0;
607 }
608 u_excess / sigma_v_eff
609 }
610}
611
612#[derive(Debug, Clone)]
620pub struct GranularPressureModel {
621 pub grain_shear_modulus: f64,
623 pub grain_poisson_ratio: f64,
625 pub mean_grain_radius: f64,
627 pub void_ratio: f64,
629}
630
631impl GranularPressureModel {
632 pub fn new(
634 grain_shear_modulus: f64,
635 grain_poisson_ratio: f64,
636 mean_grain_radius: f64,
637 void_ratio: f64,
638 ) -> Self {
639 Self {
640 grain_shear_modulus,
641 grain_poisson_ratio,
642 mean_grain_radius,
643 void_ratio,
644 }
645 }
646
647 pub fn quartz_sand() -> Self {
649 Self::new(44e9, 0.07, 0.15e-3, 0.65)
650 }
651
652 pub fn hertz_mindlin_stiffness(&self, g: f64, nu: f64, r_eff: f64, sigma_n: f64) -> (f64, f64) {
659 let g_eff = g / (2.0 * (1.0 - nu));
660 let r_eff_m = r_eff;
663 let f_n = sigma_n * PI * r_eff_m * r_eff_m;
666 let delta = (3.0 * f_n / (4.0 * g_eff * r_eff_m.sqrt()))
667 .max(0.0)
668 .powf(2.0 / 3.0);
669 let kn = (4.0 / 3.0) * g_eff * (r_eff_m * delta).sqrt();
670 let kt = 8.0 * g_eff * (r_eff_m * delta).sqrt();
671 (kn, kt)
672 }
673
674 pub fn coordination_number(&self, void_ratio: f64) -> f64 {
678 (10.726 - 11.469 * void_ratio).max(3.0)
679 }
680
681 pub fn pack_stiffness(&self, g: f64, nu: f64, e: f64, sigma: f64) -> f64 {
686 let z = self.coordination_number(e);
687 let factor = (5.0 - 4.0 * nu) / (5.0 * (2.0 - nu));
688 let inner =
689 3.0 * z * z * (1.0 - e) * (1.0 - e) * g * g / (2.0 * PI * PI * (1.0 + e) * (1.0 + e));
690 factor * inner.powf(1.0 / 3.0) * sigma.abs().powf(1.0 / 3.0)
691 }
692
693 pub fn shear_wave_velocity(&self, rho_bulk: f64, sigma: f64) -> f64 {
695 let g_pack = self.pack_stiffness(
696 self.grain_shear_modulus,
697 self.grain_poisson_ratio,
698 self.void_ratio,
699 sigma,
700 );
701 (g_pack / rho_bulk).sqrt()
702 }
703}
704
705#[derive(Debug, Clone)]
711pub struct LoessModel {
712 pub natural_void_ratio: f64,
714 pub liquid_limit: f64,
716 pub plastic_limit: f64,
718 pub water_content: f64,
720 pub k_sat: f64,
722}
723
724impl LoessModel {
725 pub fn new(
727 natural_void_ratio: f64,
728 liquid_limit: f64,
729 plastic_limit: f64,
730 water_content: f64,
731 k_sat: f64,
732 ) -> Self {
733 Self {
734 natural_void_ratio,
735 liquid_limit,
736 plastic_limit,
737 water_content,
738 k_sat,
739 }
740 }
741
742 pub fn chinese_loess() -> Self {
744 Self::new(1.05, 28.0, 16.0, 12.0, 1e-6)
745 }
746
747 pub fn plasticity_index(&self) -> f64 {
749 self.liquid_limit - self.plastic_limit
750 }
751
752 pub fn liquidity_index(&self) -> f64 {
754 let pi = self.plasticity_index();
755 if pi <= 0.0 {
756 return 0.0;
757 }
758 (self.water_content - self.plastic_limit) / pi
759 }
760
761 pub fn collapsibility_coefficient(&self, e_saturated: f64) -> f64 {
765 (self.natural_void_ratio - e_saturated) / (1.0 + self.natural_void_ratio)
766 }
767}
768
769#[derive(Debug, Clone)]
775pub struct RockfillModel {
776 pub k_modulus: f64,
778 pub n_exponent: f64,
780 pub rf: f64,
782 pub cohesion: f64,
784 pub friction_angle: f64,
786 pub pa: f64,
788}
789
790impl RockfillModel {
791 pub fn new(
793 k_modulus: f64,
794 n_exponent: f64,
795 rf: f64,
796 cohesion: f64,
797 friction_angle_deg: f64,
798 ) -> Self {
799 Self {
800 k_modulus,
801 n_exponent,
802 rf,
803 cohesion,
804 friction_angle: friction_angle_deg.to_radians(),
805 pa: 101325.0,
806 }
807 }
808
809 pub fn hard_quartzite_rockfill() -> Self {
811 Self::new(800.0, 0.4, 0.8, 0.0, 40.0)
812 }
813
814 pub fn initial_tangent_modulus(&self, sigma3: f64) -> f64 {
818 self.k_modulus * self.pa * (sigma3 / self.pa).abs().powf(self.n_exponent)
819 }
820
821 pub fn deviatoric_stress_at_failure(&self, sigma3: f64) -> f64 {
825 let phi = self.friction_angle;
826 2.0 * (self.cohesion * phi.cos() + sigma3 * phi.sin()) / (1.0 - phi.sin())
827 }
828
829 pub fn tangent_modulus(&self, sigma1: f64, sigma3: f64) -> f64 {
833 let ei = self.initial_tangent_modulus(sigma3);
834 let delta_f = self.deviatoric_stress_at_failure(sigma3);
835 if delta_f <= 0.0 {
836 return ei;
837 }
838 let delta = sigma1 - sigma3;
839 let ratio = self.rf * delta / delta_f;
840 let factor = (1.0 - ratio).max(0.0);
841 ei * factor * factor
842 }
843}
844
845#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
856 fn test_soil_yield_function_elastic() {
857 let soil = SoilModel::medium_sand();
858 let f = soil.yield_function(50e3, 100e3); assert!(f < 0.0, "Should be elastic when σ1 < σ3: {f}");
862 }
863
864 #[test]
865 fn test_soil_yield_function_at_failure() {
866 let soil = SoilModel::stiff_clay();
867 let sigma3 = 100e3;
869 let phi = soil.friction_angle;
870 let c = soil.cohesion;
871 let sigma1 = (2.0 * c * phi.cos() + sigma3 * (1.0 + phi.sin())) / (1.0 - phi.sin());
874 let f = soil.yield_function(sigma1, sigma3);
875 assert!(
876 f.abs() < 1e-6 * sigma1,
877 "Yield function should be ~0 at failure: {f}"
878 );
879 }
880
881 #[test]
882 fn test_soil_bulk_modulus() {
883 let soil = SoilModel::medium_sand();
884 let k = soil.bulk_modulus();
885 assert!(k > 0.0, "Bulk modulus must be positive");
886 }
887
888 #[test]
889 fn test_soil_shear_modulus() {
890 let soil = SoilModel::medium_sand();
891 let g = soil.shear_modulus();
892 assert!(g > 0.0);
893 }
894
895 #[test]
896 fn test_soil_ucs() {
897 let soil = SoilModel::stiff_clay();
898 let qu = soil.ucs();
899 assert!(qu > 100e3 && qu < 300e3, "UCS {qu} Pa out of range");
901 }
902
903 #[test]
904 fn test_soil_k0() {
905 let soil = SoilModel::medium_sand();
906 let k0 = soil.k0_jaky();
907 assert!(k0 > 0.0 && k0 < 1.0, "Ko must be in (0,1): {k0}");
908 }
909
910 #[test]
911 fn test_soil_critical_state_q() {
912 let soil = SoilModel::medium_sand();
913 let q = soil.critical_state_line_q(200e3);
914 assert!(q > 0.0);
915 }
916
917 #[test]
918 fn test_soil_plastic_multiplier() {
919 let soil = SoilModel::medium_sand();
920 let lambda = soil.plastic_multiplier(0.01);
921 assert!(lambda > 0.0 && lambda < 0.01);
922 }
923
924 #[test]
927 fn test_camclay_e_cs() {
928 let model = CamClayModel::kaolin_ncc();
929 let e = model.e_cs(100e3); assert!(e > 0.0, "Void ratio on CSL must be positive: {e}");
931 }
932
933 #[test]
934 fn test_camclay_yield_surface_inside() {
935 let model = CamClayModel::kaolin_ncc();
936 let f = model.yield_surface_p0(50e3, 0.0);
938 assert!(f < 0.0, "Should be inside yield surface at p=50kPa<p0: {f}");
939 }
940
941 #[test]
942 fn test_camclay_yield_surface_on() {
943 let model = CamClayModel::kaolin_ncc();
944 let f = model.yield_surface_p0(model.p_c0, 0.0);
946 assert!(
947 f.abs() < 1e-6 * model.p_c0 * model.p_c0,
948 "On yield surface: {f}"
949 );
950 }
951
952 #[test]
953 fn test_camclay_preconsolidation_pressure() {
954 let model = CamClayModel::kaolin_ncc();
955 let p0 = model.preconsolidation_pressure(model.e0);
956 assert!(p0 > 0.0, "Preconsolidation pressure must be positive: {p0}");
957 }
958
959 #[test]
960 fn test_camclay_elastic_volumetric_strain() {
961 let model = CamClayModel::kaolin_ncc();
962 let deps = model.volumetric_strain_elastic(10e3, 100e3);
963 assert!(deps > 0.0, "Elastic strain must be positive");
964 }
965
966 #[test]
967 fn test_camclay_plastic_strain() {
968 let model = CamClayModel::kaolin_ncc();
969 let deps = model.volumetric_strain_plastic(10e3, 100e3);
970 assert!(deps > 0.0);
971 }
972
973 #[test]
976 fn test_rock_mb_granite() {
977 let rock = RockModel::granite();
978 let mb = rock.mb_parameter();
979 assert!(mb > 0.0 && mb < rock.mi, "mb < mi: {mb}");
980 }
981
982 #[test]
983 fn test_rock_s_parameter() {
984 let rock = RockModel::granite();
985 let s = rock.s_parameter();
986 assert!(s > 0.0 && s <= 1.0, "s ∈ (0,1]: {s}");
987 }
988
989 #[test]
990 fn test_rock_a_parameter() {
991 let rock = RockModel::granite();
992 let a = rock.a_parameter();
993 assert!((0.5..0.65).contains(&a), "a ≈ 0.5 for intact rock: {a}");
994 }
995
996 #[test]
997 fn test_rock_ucs() {
998 let rock = RockModel::granite();
999 let ucs_mass = rock.uniaxial_compressive_strength_rock();
1000 assert!(
1001 ucs_mass > 0.0 && ucs_mass <= rock.ucs,
1002 "Rock mass UCS ≤ intact UCS"
1003 );
1004 }
1005
1006 #[test]
1007 fn test_rock_tensile_strength() {
1008 let rock = RockModel::granite();
1009 let t = rock.tensile_strength_rock();
1010 assert!(
1011 t < 0.0,
1012 "Tensile strength should be negative (tension): {t}"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_rock_hoek_brown_yield() {
1018 let rock = RockModel::granite();
1019 let sigma3 = 0.0;
1021 let ucs_mass = rock.uniaxial_compressive_strength_rock();
1022 let sigma1 = ucs_mass * 2.0;
1023 let yielding = rock.yield_hoek_brown(sigma1, sigma3);
1024 assert!(
1025 yielding,
1026 "Should yield at sigma1=2*ucs_mass, sigma3=0: ucs_mass={ucs_mass:.3e}"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_rock_deformation_modulus() {
1032 let rock = RockModel::granite();
1033 let em = rock.deformation_modulus();
1034 assert!(em > 0.0, "Deformation modulus must be positive");
1035 }
1036
1037 #[test]
1038 fn test_rock_shale_params() {
1039 let rock = RockModel::shale();
1040 let mb = rock.mb_parameter();
1041 let mb_granite = RockModel::granite().mb_parameter();
1042 assert!(mb < mb_granite, "Shale has lower mb than granite");
1043 }
1044
1045 #[test]
1048 fn test_sand_peak_friction_angle() {
1049 let sand = SandModel::medium_dense_quartz();
1050 let phi_p = sand.peak_friction_angle(0.6);
1051 assert!(phi_p >= sand.critical_state_friction_angle, "Peak φ ≥ CS φ");
1052 }
1053
1054 #[test]
1055 fn test_sand_dilatancy_index_positive() {
1056 let sand = SandModel::medium_dense_quartz();
1057 let ir = sand.dilatancy_index(0.7, 100.0);
1058 assert!(ir >= 0.0, "Dilatancy index non-negative");
1059 }
1060
1061 #[test]
1062 fn test_sand_bolton_dilatancy() {
1063 let sand = SandModel::medium_dense_quartz();
1064 let ir = sand.bolton_dilatancy(0.6, 100.0);
1065 assert!(ir >= 0.0);
1066 }
1067
1068 #[test]
1069 fn test_sand_void_ratio() {
1070 let sand = SandModel::medium_dense_quartz();
1071 let e = sand.current_void_ratio();
1072 assert!(
1073 e >= sand.e_min() && e <= sand.e_max(),
1074 "Void ratio out of range: {e}"
1075 );
1076 }
1077
1078 #[test]
1079 fn test_sand_dry_unit_weight() {
1080 let sand = SandModel::medium_dense_quartz();
1081 let gd = sand.dry_unit_weight();
1082 assert!(
1083 gd > 0.0 && gd < 30.0,
1084 "Dry unit weight {gd} kN/m^3 out of range"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_sand_loose_has_lower_dr() {
1090 let loose = SandModel::loose_sand();
1091 let dense = SandModel::medium_dense_quartz();
1092 assert!(loose.relative_density < dense.relative_density);
1093 }
1094
1095 #[test]
1098 fn test_permafrost_unfrozen_content_above_zero() {
1099 let pf = PermafrostModel::siberian_silt();
1100 let theta = pf.unfrozen_water_content(5.0);
1101 assert_eq!(
1102 theta, pf.total_water_content,
1103 "Above 0°C: all water unfrozen"
1104 );
1105 }
1106
1107 #[test]
1108 fn test_permafrost_unfrozen_content_frozen() {
1109 let pf = PermafrostModel::siberian_silt();
1110 let theta = pf.unfrozen_water_content(-10.0);
1111 assert!(
1112 theta < pf.total_water_content,
1113 "Below 0°C: some water frozen"
1114 );
1115 assert!(theta >= 0.0);
1116 }
1117
1118 #[test]
1119 fn test_permafrost_ice_saturation_above_zero() {
1120 let pf = PermafrostModel::siberian_silt();
1121 let si = pf.ice_saturation(5.0);
1122 assert_eq!(si, 0.0, "No ice above 0°C");
1123 }
1124
1125 #[test]
1126 fn test_permafrost_ice_saturation_frozen() {
1127 let pf = PermafrostModel::siberian_silt();
1128 let si = pf.ice_saturation(-20.0);
1129 assert!(si > 0.0 && si <= 1.0, "Ice saturation in (0,1]: {si}");
1130 }
1131
1132 #[test]
1133 fn test_permafrost_bulk_modulus_increases_with_cooling() {
1134 let pf = PermafrostModel::siberian_silt();
1135 let k_warm = pf.bulk_modulus_frozen(-1.0);
1136 let k_cold = pf.bulk_modulus_frozen(-20.0);
1137 assert!(k_cold > k_warm, "Frozen soil stiffens with cooling");
1138 }
1139
1140 #[test]
1141 fn test_permafrost_thermal_conductivity() {
1142 let pf = PermafrostModel::siberian_silt();
1143 let lam = pf.thermal_conductivity(0.1);
1144 assert!(
1145 lam > 0.0 && lam < 10.0,
1146 "Conductivity {lam} W/(mK) out of range"
1147 );
1148 }
1149
1150 #[test]
1151 fn test_permafrost_heat_capacity() {
1152 let pf = PermafrostModel::siberian_silt();
1153 let c = pf.volumetric_heat_capacity(-10.0);
1154 assert!(
1155 c > 1e5 && c < 5e6,
1156 "Heat capacity {c} J/(m^3 K) out of range"
1157 );
1158 }
1159
1160 #[test]
1163 fn test_seabed_su_at_mudline() {
1164 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1165 let su = sed.undrained_shear_strength(0.0);
1166 assert_eq!(su, sed.su0, "At z=0, Su = su0");
1167 }
1168
1169 #[test]
1170 fn test_seabed_su_increases_with_depth() {
1171 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1172 let su1 = sed.undrained_shear_strength(5.0);
1173 let su2 = sed.undrained_shear_strength(10.0);
1174 assert!(su2 > su1, "Su increases with depth");
1175 }
1176
1177 #[test]
1178 fn test_seabed_settlement_positive() {
1179 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1180 let ds = sed.settlement_primary(50e3, 100e3, 10.0, sed.e0);
1181 assert!(ds > 0.0, "Settlement must be positive");
1182 }
1183
1184 #[test]
1185 fn test_seabed_settlement_zero_when_no_loading() {
1186 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1187 let ds = sed.settlement_primary(50e3, 50e3, 10.0, sed.e0);
1188 assert_eq!(ds, 0.0, "No settlement without additional load");
1189 }
1190
1191 #[test]
1192 fn test_seabed_time_consolidation() {
1193 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1194 let t = sed.time_to_consolidation(2e-8, 5.0, 0.5);
1195 assert!(t > 0.0, "Consolidation time must be positive");
1196 }
1197
1198 #[test]
1199 fn test_seabed_ocr() {
1200 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1201 let ocr = sed.ocr(50e3, 100e3);
1202 assert!((ocr - 2.0).abs() < 1e-10, "OCR = 2 for sigma_p = 2*sigma_v");
1203 }
1204
1205 #[test]
1208 fn test_granular_hertz_mindlin_stiffness() {
1209 let model = GranularPressureModel::quartz_sand();
1210 let (kn, kt) = model.hertz_mindlin_stiffness(44e9, 0.07, 0.15e-3, 100e3);
1211 assert!(kn > 0.0, "Normal stiffness must be positive: {kn}");
1212 assert!(kt > 0.0, "Tangential stiffness must be positive: {kt}");
1213 }
1214
1215 #[test]
1216 fn test_granular_hertz_mindlin_kn_gt_kt_ratio() {
1217 let model = GranularPressureModel::quartz_sand();
1218 let (kn, kt) = model.hertz_mindlin_stiffness(44e9, 0.07, 0.15e-3, 100e3);
1219 let ratio = kt / kn;
1221 assert!((ratio - 6.0).abs() < 1e-6, "kt/kn should be 6: {ratio}");
1222 }
1223
1224 #[test]
1225 fn test_granular_coordination_number() {
1226 let model = GranularPressureModel::quartz_sand();
1227 let z = model.coordination_number(0.65);
1228 assert!(z > 3.0 && z < 12.0, "Coordination number {z} out of range");
1229 }
1230
1231 #[test]
1232 fn test_granular_pack_stiffness_positive() {
1233 let model = GranularPressureModel::quartz_sand();
1234 let g_pack = model.pack_stiffness(44e9, 0.07, 0.65, 100e3);
1235 assert!(g_pack > 0.0, "Pack stiffness must be positive");
1236 }
1237
1238 #[test]
1239 fn test_granular_pack_stiffness_pressure_dependent() {
1240 let model = GranularPressureModel::quartz_sand();
1241 let g1 = model.pack_stiffness(44e9, 0.07, 0.65, 100e3);
1242 let g2 = model.pack_stiffness(44e9, 0.07, 0.65, 400e3);
1243 assert!(g2 > g1, "Pack stiffness increases with pressure");
1244 }
1245
1246 #[test]
1247 fn test_granular_shear_wave_velocity() {
1248 let model = GranularPressureModel::quartz_sand();
1249 let rho = 1600.0; let vs = model.shear_wave_velocity(rho, 100e3);
1251 assert!(vs > 0.0 && vs < 2000.0, "Vs {vs} m/s out of range");
1252 }
1253
1254 #[test]
1257 fn test_loess_plasticity_index() {
1258 let loess = LoessModel::chinese_loess();
1259 let pi = loess.plasticity_index();
1260 assert!((pi - 12.0).abs() < 1e-10, "PI = LL - PL = 12");
1261 }
1262
1263 #[test]
1264 fn test_loess_liquidity_index() {
1265 let loess = LoessModel::chinese_loess();
1266 let li = loess.liquidity_index();
1267 assert!(li < 0.0, "LI should be negative for stiff loess");
1269 }
1270
1271 #[test]
1272 fn test_loess_collapsibility() {
1273 let loess = LoessModel::chinese_loess();
1274 let delta_s = loess.collapsibility_coefficient(0.85);
1275 assert!(delta_s > 0.0, "Collapsibility must be positive");
1276 }
1277
1278 #[test]
1281 fn test_rockfill_initial_modulus() {
1282 let rf = RockfillModel::hard_quartzite_rockfill();
1283 let ei = rf.initial_tangent_modulus(200e3);
1284 assert!(ei > 0.0, "Initial modulus must be positive");
1285 }
1286
1287 #[test]
1288 fn test_rockfill_tangent_modulus_decreases() {
1289 let rf = RockfillModel::hard_quartzite_rockfill();
1290 let sigma3 = 200e3;
1291 let ei = rf.initial_tangent_modulus(sigma3);
1292 let delta_f = rf.deviatoric_stress_at_failure(sigma3);
1293 let sigma1_half = sigma3 + 0.5 * delta_f;
1295 let et = rf.tangent_modulus(sigma1_half, sigma3);
1296 assert!(et < ei, "Tangent modulus decreases with shear");
1297 }
1298
1299 #[test]
1300 fn test_rockfill_deviatoric_stress_at_failure_positive() {
1301 let rf = RockfillModel::hard_quartzite_rockfill();
1302 let dsf = rf.deviatoric_stress_at_failure(200e3);
1303 assert!(dsf > 0.0);
1304 }
1305
1306 #[test]
1309 fn test_camclay_e_cs_decreases_with_pressure() {
1310 let model = CamClayModel::kaolin_ncc();
1311 let e1 = model.e_cs(100e3);
1312 let e2 = model.e_cs(1000e3);
1313 assert!(e2 < e1, "Void ratio on CSL decreases with pressure");
1314 }
1315
1316 #[test]
1317 fn test_rock_granite_stronger_than_shale() {
1318 let granite = RockModel::granite();
1319 let shale = RockModel::shale();
1320 assert!(
1321 granite.uniaxial_compressive_strength_rock()
1322 > shale.uniaxial_compressive_strength_rock(),
1323 "Granite stronger than shale"
1324 );
1325 }
1326
1327 #[test]
1328 fn test_soil_clay_has_cohesion() {
1329 let clay = SoilModel::stiff_clay();
1330 assert!(clay.cohesion > 0.0, "Clay has cohesion");
1331 }
1332
1333 #[test]
1334 fn test_sand_has_no_cohesion() {
1335 let sand = SoilModel::medium_sand();
1336 assert_eq!(sand.cohesion, 0.0, "Sand has no cohesion");
1337 }
1338
1339 #[test]
1340 fn test_seabed_pore_pressure_ratio() {
1341 let sed = SeabedSedimentModel::gulf_of_mexico_clay();
1342 let ru = sed.pore_pressure_ratio(20e3, 100e3);
1343 assert!((ru - 0.2).abs() < 1e-10);
1344 }
1345}