1#[derive(Debug, Clone, Copy)]
25#[allow(dead_code)]
26pub struct IsotropicHardening {
27 pub sigma_y0: f64,
29 pub hardening_modulus: f64,
31}
32
33#[allow(dead_code)]
34impl IsotropicHardening {
35 pub fn new(sigma_y0: f64, hardening_modulus: f64) -> Self {
37 Self {
38 sigma_y0,
39 hardening_modulus,
40 }
41 }
42
43 pub fn yield_stress(&self, eps_p: f64) -> f64 {
45 self.sigma_y0 + self.hardening_modulus * eps_p
46 }
47
48 pub fn d_yield_stress(&self) -> f64 {
50 self.hardening_modulus
51 }
52
53 pub fn elastic_perfectly_plastic(sigma_y0: f64) -> Self {
55 Self::new(sigma_y0, 0.0)
56 }
57
58 pub fn mild_steel() -> Self {
60 Self::new(250.0e6, 2.0e9)
61 }
62
63 pub fn aluminium_alloy() -> Self {
65 Self::new(270.0e6, 1.5e9)
66 }
67}
68
69#[derive(Debug, Clone, Copy)]
77#[allow(dead_code)]
78pub struct VoceHardening {
79 pub sigma_y0: f64,
81 pub sigma_inf: f64,
83 pub b: f64,
85}
86
87#[allow(dead_code)]
88impl VoceHardening {
89 pub fn new(sigma_y0: f64, sigma_inf: f64, b: f64) -> Self {
91 Self {
92 sigma_y0,
93 sigma_inf,
94 b,
95 }
96 }
97
98 pub fn yield_stress(&self, eps_p: f64) -> f64 {
100 self.sigma_inf - (self.sigma_inf - self.sigma_y0) * (-self.b * eps_p).exp()
101 }
102
103 pub fn d_yield_stress(&self, eps_p: f64) -> f64 {
105 self.b * (self.sigma_inf - self.sigma_y0) * (-self.b * eps_p).exp()
106 }
107
108 pub fn copper_like() -> Self {
110 Self::new(50.0e6, 300.0e6, 10.0)
111 }
112}
113
114#[derive(Debug, Clone)]
123#[allow(dead_code)]
124pub struct PragerKinematic {
125 pub sigma_y0: f64,
127 pub c_kinematic: f64,
129}
130
131#[allow(dead_code)]
132impl PragerKinematic {
133 pub fn new(sigma_y0: f64, c_kinematic: f64) -> Self {
135 Self {
136 sigma_y0,
137 c_kinematic,
138 }
139 }
140
141 pub fn backstress_increment(&self, delta_ep: &[f64; 6]) -> [f64; 6] {
145 let c = self.c_kinematic;
146 [
147 c * delta_ep[0],
148 c * delta_ep[1],
149 c * delta_ep[2],
150 c * delta_ep[3],
151 c * delta_ep[4],
152 c * delta_ep[5],
153 ]
154 }
155
156 pub fn effective_stress(stress: &[f64; 6], backstress: &[f64; 6]) -> [f64; 6] {
158 [
159 stress[0] - backstress[0],
160 stress[1] - backstress[1],
161 stress[2] - backstress[2],
162 stress[3] - backstress[3],
163 stress[4] - backstress[4],
164 stress[5] - backstress[5],
165 ]
166 }
167
168 pub fn von_mises_norm(s: &[f64; 6]) -> f64 {
170 let mean = (s[0] + s[1] + s[2]) / 3.0;
171 let dev = [s[0] - mean, s[1] - mean, s[2] - mean];
172 let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
173 + s[3].powi(2)
174 + s[4].powi(2)
175 + s[5].powi(2);
176 (3.0 * j2).sqrt() }
178
179 pub fn yield_function(&self, stress: &[f64; 6], backstress: &[f64; 6]) -> f64 {
181 let s_eff = Self::effective_stress(stress, backstress);
182 Self::von_mises_norm(&s_eff) - self.sigma_y0
183 }
184}
185
186#[derive(Debug, Clone)]
195#[allow(dead_code)]
196pub struct Chaboche {
197 pub sigma_y0: f64,
199 pub q: f64,
201 pub b_iso: f64,
203 pub c_kin: f64,
205 pub gamma_kin: f64,
207}
208
209#[allow(dead_code)]
210impl Chaboche {
211 pub fn new(sigma_y0: f64, q: f64, b_iso: f64, c_kin: f64, gamma_kin: f64) -> Self {
213 Self {
214 sigma_y0,
215 q,
216 b_iso,
217 c_kin,
218 gamma_kin,
219 }
220 }
221
222 pub fn steel_default() -> Self {
224 Self::new(
225 250.0e6, 80.0e6, 15.0, 50.0e9, 500.0, )
231 }
232
233 pub fn isotropic_stress(&self, p: f64) -> f64 {
235 self.q * (1.0 - (-self.b_iso * p).exp())
236 }
237
238 pub fn yield_stress(&self, p: f64) -> f64 {
240 self.sigma_y0 + self.isotropic_stress(p)
241 }
242
243 pub fn backstress_increment(
248 &self,
249 flow_dir: &[f64; 6],
250 alpha: &[f64; 6],
251 delta_p: f64,
252 ) -> [f64; 6] {
253 let mut da = [0.0f64; 6];
254 for i in 0..6 {
255 da[i] = (self.c_kin * flow_dir[i] - self.gamma_kin * alpha[i]) * delta_p;
256 }
257 da
258 }
259
260 pub fn backstress_norm(alpha: &[f64; 6]) -> f64 {
262 let sum = alpha[0].powi(2)
263 + alpha[1].powi(2)
264 + alpha[2].powi(2)
265 + 2.0 * (alpha[3].powi(2) + alpha[4].powi(2) + alpha[5].powi(2));
266 (2.0 / 3.0 * sum).sqrt()
267 }
268}
269
270#[derive(Debug, Clone, Copy)]
277#[allow(dead_code)]
278pub struct J2ReturnMapping {
279 pub shear_modulus: f64,
281 pub bulk_modulus: f64,
283 pub sigma_y0: f64,
285 pub hardening_modulus: f64,
287}
288
289#[allow(dead_code)]
290impl J2ReturnMapping {
291 pub fn new(
293 shear_modulus: f64,
294 bulk_modulus: f64,
295 sigma_y0: f64,
296 hardening_modulus: f64,
297 ) -> Self {
298 Self {
299 shear_modulus,
300 bulk_modulus,
301 sigma_y0,
302 hardening_modulus,
303 }
304 }
305
306 #[allow(non_snake_case)]
308 pub fn from_young_poisson(E: f64, nu: f64, sigma_y0: f64, h: f64) -> Self {
309 let g = E / (2.0 * (1.0 + nu));
310 let k = E / (3.0 * (1.0 - 2.0 * nu));
311 Self::new(g, k, sigma_y0, h)
312 }
313
314 pub fn von_mises(stress: &[f64; 6]) -> f64 {
316 let mean = (stress[0] + stress[1] + stress[2]) / 3.0;
317 let dev = [stress[0] - mean, stress[1] - mean, stress[2] - mean];
318 let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
319 + stress[3].powi(2)
320 + stress[4].powi(2)
321 + stress[5].powi(2);
322 (3.0 * j2).sqrt()
323 }
324
325 pub fn return_map(&self, trial_stress: &[f64; 6], eps_p_bar: f64) -> ([f64; 6], [f64; 6], f64) {
329 let sigma_y = self.sigma_y0 + self.hardening_modulus * eps_p_bar;
330 let vm = Self::von_mises(trial_stress);
331
332 if vm <= sigma_y + f64::EPSILON * sigma_y {
333 return (*trial_stress, [0.0; 6], 0.0);
335 }
336
337 let delta_gamma = (vm - sigma_y) / (3.0 * self.shear_modulus + self.hardening_modulus);
339
340 let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
342 let dev_trial = [
343 trial_stress[0] - p,
344 trial_stress[1] - p,
345 trial_stress[2] - p,
346 trial_stress[3],
347 trial_stress[4],
348 trial_stress[5],
349 ];
350
351 let scale = 1.0 - 3.0 * self.shear_modulus * delta_gamma / vm;
353
354 let updated_stress = [
356 p + dev_trial[0] * scale,
357 p + dev_trial[1] * scale,
358 p + dev_trial[2] * scale,
359 dev_trial[3] * scale,
360 dev_trial[4] * scale,
361 dev_trial[5] * scale,
362 ];
363
364 let n_hat = [
366 dev_trial[0] / vm * 3.0 / 2.0,
367 dev_trial[1] / vm * 3.0 / 2.0,
368 dev_trial[2] / vm * 3.0 / 2.0,
369 dev_trial[3] / vm * 3.0 / 2.0,
370 dev_trial[4] / vm * 3.0 / 2.0,
371 dev_trial[5] / vm * 3.0 / 2.0,
372 ];
373 let delta_eps_p = [
374 delta_gamma * n_hat[0],
375 delta_gamma * n_hat[1],
376 delta_gamma * n_hat[2],
377 delta_gamma * n_hat[3],
378 delta_gamma * n_hat[4],
379 delta_gamma * n_hat[5],
380 ];
381
382 (updated_stress, delta_eps_p, delta_gamma)
383 }
384
385 pub fn consistent_tangent_uniaxial(&self) -> f64 {
390 let e = 9.0 * self.bulk_modulus * self.shear_modulus
391 / (3.0 * self.bulk_modulus + self.shear_modulus);
392 e * self.hardening_modulus / (e + self.hardening_modulus)
393 }
394}
395
396#[derive(Debug, Clone, Copy)]
407#[allow(dead_code)]
408pub struct DruckerPragerModel {
409 pub friction_angle: f64,
411 pub cohesion: f64,
413 pub shear_modulus: f64,
415 pub bulk_modulus: f64,
417}
418
419#[allow(dead_code)]
420impl DruckerPragerModel {
421 pub fn new(friction_angle: f64, cohesion: f64, shear_modulus: f64, bulk_modulus: f64) -> Self {
423 Self {
424 friction_angle,
425 cohesion,
426 shear_modulus,
427 bulk_modulus,
428 }
429 }
430
431 #[allow(non_snake_case, clippy::too_many_arguments)]
433 pub fn from_mohr_coulomb_degrees(phi_deg: f64, cohesion: f64, E: f64, nu: f64) -> Self {
434 let phi = phi_deg.to_radians();
435 let g = E / (2.0 * (1.0 + nu));
436 let k = E / (3.0 * (1.0 - 2.0 * nu));
437 Self::new(phi, cohesion, g, k)
438 }
439
440 pub fn alpha_dp(&self) -> f64 {
442 let sin_phi = self.friction_angle.sin();
443 2.0 * sin_phi / (3.0_f64.sqrt() * (3.0 - sin_phi))
444 }
445
446 pub fn k_dp(&self) -> f64 {
448 let sin_phi = self.friction_angle.sin();
449 let cos_phi = self.friction_angle.cos();
450 6.0 * self.cohesion * cos_phi / (3.0_f64.sqrt() * (3.0 - sin_phi))
451 }
452
453 fn j2(stress: &[f64; 6]) -> f64 {
455 let mean = (stress[0] + stress[1] + stress[2]) / 3.0;
456 let d = [stress[0] - mean, stress[1] - mean, stress[2] - mean];
457 0.5 * (d[0].powi(2) + d[1].powi(2) + d[2].powi(2))
458 + stress[3].powi(2)
459 + stress[4].powi(2)
460 + stress[5].powi(2)
461 }
462
463 pub fn yield_function(&self, stress: &[f64; 6]) -> f64 {
465 let i1 = stress[0] + stress[1] + stress[2];
466 let sqrt_j2 = Self::j2(stress).sqrt();
467 sqrt_j2 + self.alpha_dp() * i1 - self.k_dp()
468 }
469
470 pub fn is_elastic(&self, stress: &[f64; 6]) -> bool {
472 self.yield_function(stress) <= 0.0
473 }
474
475 pub fn uniaxial_tensile_strength(&self) -> f64 {
479 let sin_phi = self.friction_angle.sin();
480 let cos_phi = self.friction_angle.cos();
481 2.0 * self.cohesion * cos_phi / (1.0 + sin_phi)
482 }
483
484 pub fn uniaxial_compressive_strength(&self) -> f64 {
488 let sin_phi = self.friction_angle.sin();
489 let cos_phi = self.friction_angle.cos();
490 2.0 * self.cohesion * cos_phi / (1.0 - sin_phi)
491 }
492}
493
494#[derive(Debug, Clone, Copy)]
506#[allow(dead_code)]
507pub struct ModifiedCamClay {
508 pub m: f64,
510 pub kappa: f64,
512 pub lambda: f64,
514 pub e0: f64,
516 pub poisson_ratio: f64,
518 pub pc0: f64,
520}
521
522#[allow(dead_code)]
523impl ModifiedCamClay {
524 #[allow(clippy::too_many_arguments)]
526 pub fn new(m: f64, kappa: f64, lambda: f64, e0: f64, poisson_ratio: f64, pc0: f64) -> Self {
527 Self {
528 m,
529 kappa,
530 lambda,
531 e0,
532 poisson_ratio,
533 pc0,
534 }
535 }
536
537 pub fn soft_clay() -> Self {
539 Self::new(0.9, 0.05, 0.2, 1.5, 0.3, 100.0e3)
540 }
541
542 pub fn bulk_modulus(&self, p_prime: f64) -> f64 {
544 (1.0 + self.e0) * p_prime / self.kappa
545 }
546
547 pub fn shear_modulus(&self, p_prime: f64) -> f64 {
549 let k = self.bulk_modulus(p_prime);
550 3.0 * k * (1.0 - 2.0 * self.poisson_ratio) / (2.0 * (1.0 + self.poisson_ratio))
551 }
552
553 pub fn mean_stress(stress: &[f64; 6]) -> f64 {
555 (stress[0] + stress[1] + stress[2]) / 3.0
556 }
557
558 pub fn deviatoric_stress(stress: &[f64; 6]) -> f64 {
560 let p = Self::mean_stress(stress);
561 let dev = [stress[0] - p, stress[1] - p, stress[2] - p];
562 let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
563 + stress[3].powi(2)
564 + stress[4].powi(2)
565 + stress[5].powi(2);
566 (3.0 * j2).sqrt()
567 }
568
569 pub fn yield_function(&self, stress: &[f64; 6], pc: f64) -> f64 {
571 let p = Self::mean_stress(stress);
572 let q = Self::deviatoric_stress(stress);
573 q * q / (self.m * self.m) + p * (p - pc)
574 }
575
576 pub fn critical_state_pressure(&self, pc: f64) -> f64 {
580 pc / 2.0
581 }
582
583 pub fn update_preconsolidation_pressure(&self, pc: f64, delta_eps_v_p: f64) -> f64 {
587 pc * ((1.0 + self.e0) * delta_eps_v_p / (self.lambda - self.kappa)).exp()
588 }
589
590 pub fn compression_index(&self) -> f64 {
592 self.lambda * 10.0_f64.ln()
593 }
594}
595
596#[allow(dead_code)]
603pub fn plastic_dissipation(stress: &[f64; 6], delta_eps_p: &[f64; 6]) -> f64 {
604 let normal =
606 stress[0] * delta_eps_p[0] + stress[1] * delta_eps_p[1] + stress[2] * delta_eps_p[2];
607 let shear = 2.0
609 * (stress[3] * delta_eps_p[3] + stress[4] * delta_eps_p[4] + stress[5] * delta_eps_p[5]);
610 normal + shear
611}
612
613#[allow(dead_code)]
617pub fn cumulative_dissipation(
618 stresses: &[[f64; 6]],
619 plastic_strain_increments: &[[f64; 6]],
620) -> f64 {
621 stresses
622 .iter()
623 .zip(plastic_strain_increments.iter())
624 .map(|(s, dep)| plastic_dissipation(s, dep))
625 .sum()
626}
627
628#[allow(dead_code)]
639pub fn limit_load_factor_lower_bound(
640 elastic_stresses_voigt: &[[f64; 6]],
641 yield_stress: f64,
642) -> f64 {
643 if elastic_stresses_voigt.is_empty() || yield_stress <= 0.0 {
644 return 0.0;
645 }
646 let max_vm = elastic_stresses_voigt
647 .iter()
648 .map(von_mises_stress_voigt)
649 .fold(0.0_f64, f64::max);
650 if max_vm < f64::EPSILON {
651 return f64::INFINITY;
652 }
653 yield_stress / max_vm
654}
655
656#[allow(dead_code)]
658pub fn von_mises_stress_voigt(s: &[f64; 6]) -> f64 {
659 let (sxx, syy, szz, sxy, syz, sxz) = (s[0], s[1], s[2], s[3], s[4], s[5]);
660 let term1 = (sxx - syy).powi(2) + (syy - szz).powi(2) + (szz - sxx).powi(2);
661 let term2 = 6.0 * (sxy.powi(2) + syz.powi(2) + sxz.powi(2));
662 ((term1 + term2) / 2.0).sqrt()
663}
664
665#[derive(Debug, Clone, Copy)]
671#[allow(dead_code)]
672pub struct RambergOsgood {
673 pub young_modulus: f64,
675 pub sigma_ref: f64,
677 pub n: f64,
679 pub alpha: f64,
681}
682
683#[allow(dead_code)]
684impl RambergOsgood {
685 pub fn new(young_modulus: f64, sigma_ref: f64, n: f64, alpha: f64) -> Self {
687 Self {
688 young_modulus,
689 sigma_ref,
690 n,
691 alpha,
692 }
693 }
694
695 pub fn total_strain(&self, sigma: f64) -> f64 {
699 sigma / self.young_modulus
700 + self.alpha * (sigma / self.sigma_ref).powf(self.n) * self.sigma_ref
701 / self.young_modulus
702 }
703
704 pub fn plastic_strain(&self, sigma: f64) -> f64 {
706 self.total_strain(sigma) - sigma / self.young_modulus
707 }
708
709 pub fn secant_modulus(&self, sigma: f64) -> f64 {
711 let eps = self.total_strain(sigma);
712 if eps.abs() < f64::EPSILON {
713 return self.young_modulus;
714 }
715 sigma / eps
716 }
717
718 pub fn tangent_modulus(&self, sigma: f64) -> f64 {
720 let d_eps_d_sigma = 1.0 / self.young_modulus
721 + self.alpha * self.n / self.sigma_ref * (sigma / self.sigma_ref).powf(self.n - 1.0);
722 if d_eps_d_sigma.abs() < f64::EPSILON {
723 return self.young_modulus;
724 }
725 1.0 / d_eps_d_sigma
726 }
727}
728
729#[allow(dead_code)]
746pub struct J2ConsistentTangent {
747 pub shear_modulus: f64,
749 pub bulk_modulus: f64,
751 pub hardening_modulus: f64,
753}
754
755#[allow(dead_code)]
756impl J2ConsistentTangent {
757 pub fn new(shear_modulus: f64, bulk_modulus: f64, hardening_modulus: f64) -> Self {
759 Self {
760 shear_modulus,
761 bulk_modulus,
762 hardening_modulus,
763 }
764 }
765
766 #[allow(non_snake_case)]
768 pub fn from_young_poisson(E: f64, nu: f64, hardening_modulus: f64) -> Self {
769 let g = E / (2.0 * (1.0 + nu));
770 let k = E / (3.0 * (1.0 - 2.0 * nu));
771 Self::new(g, k, hardening_modulus)
772 }
773
774 pub fn elastic_stiffness(&self) -> [f64; 36] {
778 let g = self.shear_modulus;
779 let k = self.bulk_modulus;
780 let lam = k - 2.0 / 3.0 * g; let mut c = [0.0_f64; 36];
782 for i in 0..3 {
784 for j in 0..3 {
785 c[i * 6 + j] = lam;
786 }
787 c[i * 6 + i] += 2.0 * g;
788 }
789 c[3 * 6 + 3] = g;
791 c[4 * 6 + 4] = g;
792 c[5 * 6 + 5] = g;
793 c
794 }
795
796 pub fn compute_consistent_tangent(
809 &self,
810 trial_stress: &[f64; 6],
811 delta_gamma: f64,
812 ) -> [f64; 36] {
813 let g = self.shear_modulus;
814 let h = self.hardening_modulus;
815
816 let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
818 let dev = [
819 trial_stress[0] - p,
820 trial_stress[1] - p,
821 trial_stress[2] - p,
822 trial_stress[3],
823 trial_stress[4],
824 trial_stress[5],
825 ];
826 let q_tr = {
827 let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
828 + dev[3].powi(2)
829 + dev[4].powi(2)
830 + dev[5].powi(2);
831 (3.0 * j2).sqrt()
832 };
833
834 if delta_gamma < f64::EPSILON || q_tr < f64::EPSILON {
836 return self.elastic_stiffness();
837 }
838
839 let scale = if q_tr > 1e-30 { 1.0 / q_tr } else { 0.0 };
841 let n: [f64; 6] = [
842 dev[0] * scale,
843 dev[1] * scale,
844 dev[2] * scale,
845 dev[3] * scale,
846 dev[4] * scale,
847 dev[5] * scale,
848 ];
849
850 let mut c = self.elastic_stiffness();
852
853 let coeff1 = 6.0 * g * g / (3.0 * g + h);
855 for i in 0..6 {
856 for j in 0..6 {
857 c[i * 6 + j] -= coeff1 * n[i] * n[j];
858 }
859 }
860
861 let coeff2 = 2.0 * g * delta_gamma / q_tr;
864 let i_dev_diag = [2.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0, 1.0, 1.0, 1.0];
866 for i in 0..3 {
867 for j in 0..3 {
868 let i_dev_ij = if i == j { i_dev_diag[i] } else { -1.0 / 3.0 };
869 c[i * 6 + j] -= coeff2 * (i_dev_ij - n[i] * n[j]);
870 }
871 }
872 for k in 3..6 {
873 c[k * 6 + k] -= coeff2 * (i_dev_diag[k] - n[k] * n[k]);
874 }
875
876 c
877 }
878}
879
880#[allow(dead_code)]
892pub struct DruckerPragerApexReturn {
893 pub alpha: f64,
895 pub k_dp: f64,
897 pub bulk_modulus: f64,
899 pub shear_modulus: f64,
901}
902
903#[allow(dead_code)]
904impl DruckerPragerApexReturn {
905 pub fn new(alpha: f64, k_dp: f64, bulk_modulus: f64, shear_modulus: f64) -> Self {
907 Self {
908 alpha,
909 k_dp,
910 bulk_modulus,
911 shear_modulus,
912 }
913 }
914
915 pub fn from_dp_model(dp: &DruckerPragerModel) -> Self {
917 Self::new(dp.alpha_dp(), dp.k_dp(), dp.bulk_modulus, dp.shear_modulus)
918 }
919
920 pub fn apex_pressure(&self) -> f64 {
922 if self.alpha.abs() < f64::EPSILON {
923 return 0.0;
924 }
925 self.k_dp / (3.0 * self.alpha)
926 }
927
928 pub fn needs_apex_return(&self, trial_stress: &[f64; 6]) -> bool {
933 let p = (trial_stress[0] + trial_stress[1] + trial_stress[2]) / 3.0;
934 p < self.apex_pressure()
935 }
936
937 pub fn compute_apex_return(&self, _trial_stress: &[f64; 6]) -> [f64; 6] {
941 let p_apex = self.apex_pressure();
942 [p_apex, p_apex, p_apex, 0.0, 0.0, 0.0]
943 }
944
945 pub fn compute_apex_multiplier(&self, trial_stress: &[f64; 6]) -> f64 {
949 let i1 = trial_stress[0] + trial_stress[1] + trial_stress[2];
950 let p = i1 / 3.0;
951 let dev = [
952 trial_stress[0] - p,
953 trial_stress[1] - p,
954 trial_stress[2] - p,
955 ];
956 let j2 = 0.5 * (dev[0].powi(2) + dev[1].powi(2) + dev[2].powi(2))
957 + trial_stress[3].powi(2)
958 + trial_stress[4].powi(2)
959 + trial_stress[5].powi(2);
960 let sqrt_j2 = j2.sqrt();
961 let f_tr = sqrt_j2 + self.alpha * i1 - self.k_dp;
962 let denom = 3.0 * self.alpha * self.bulk_modulus + self.shear_modulus;
963 if denom.abs() < f64::EPSILON {
964 return 0.0;
965 }
966 f_tr / denom
967 }
968}
969
970#[allow(dead_code)]
983#[derive(Debug, Clone, Copy)]
984pub struct CamClayPreconsolidation {
985 pub cc: f64,
987 pub cs: f64,
989 pub e0: f64,
991 pub pc0: f64,
993}
994
995#[allow(dead_code)]
996impl CamClayPreconsolidation {
997 pub fn new(cc: f64, cs: f64, e0: f64, pc0: f64) -> Self {
999 Self { cc, cs, e0, pc0 }
1000 }
1001
1002 pub fn soft_clay() -> Self {
1004 Self::new(0.5, 0.05, 1.2, 100.0e3)
1005 }
1006
1007 pub fn lambda(&self) -> f64 {
1009 self.cc / 10.0_f64.ln()
1010 }
1011
1012 pub fn kappa(&self) -> f64 {
1014 self.cs / 10.0_f64.ln()
1015 }
1016
1017 pub fn compute_preconsolidation(&self, pc_old: f64, delta_eps_v_p: f64) -> f64 {
1021 let lambda = self.lambda();
1022 let kappa = self.kappa();
1023 if (lambda - kappa).abs() < f64::EPSILON {
1024 return pc_old;
1025 }
1026 pc_old * ((1.0 + self.e0) * delta_eps_v_p / (lambda - kappa)).exp()
1027 }
1028
1029 pub fn void_ratio_ncl(&self, p: f64) -> f64 {
1033 self.e0 - self.cc * (p / self.pc0).log10()
1034 }
1035
1036 pub fn void_ratio_swelling(&self, p: f64, p0: f64, e_curr: f64) -> f64 {
1040 e_curr - self.cs * (p / p0).log10()
1041 }
1042
1043 pub fn ocr(&self, p_prime: f64, pc: f64) -> f64 {
1045 if p_prime.abs() < f64::EPSILON {
1046 return 1.0;
1047 }
1048 pc / p_prime
1049 }
1050
1051 pub fn critical_state_pressure(&self, pc: f64) -> f64 {
1055 pc / 2.0
1056 }
1057}
1058
1059#[cfg(test)]
1062mod tests {
1063 use super::*;
1064
1065 #[test]
1068 fn test_isotropic_hardening_at_zero_strain() {
1069 let h = IsotropicHardening::mild_steel();
1070 let sy = h.yield_stress(0.0);
1071 assert!((sy - 250.0e6).abs() < 1.0, "σ_y(0) = {sy}");
1072 }
1073
1074 #[test]
1075 fn test_isotropic_hardening_increases_with_strain() {
1076 let h = IsotropicHardening::mild_steel();
1077 let sy0 = h.yield_stress(0.0);
1078 let sy1 = h.yield_stress(0.01);
1079 assert!(
1080 sy1 > sy0,
1081 "yield stress should increase with plastic strain"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_isotropic_hardening_linear_rate() {
1087 let h = IsotropicHardening::new(200.0e6, 1.0e9);
1088 let eps_p = 0.005;
1089 let sy = h.yield_stress(eps_p);
1090 let expected = 200.0e6 + 1.0e9 * eps_p;
1091 assert!(
1092 (sy - expected).abs() < 1.0,
1093 "linear hardening: {sy} vs {expected}"
1094 );
1095 }
1096
1097 #[test]
1098 fn test_isotropic_hardening_derivative_is_h() {
1099 let h = IsotropicHardening::new(200.0e6, 5.0e9);
1100 assert!((h.d_yield_stress() - 5.0e9).abs() < 1.0);
1101 }
1102
1103 #[test]
1104 fn test_elastic_perfectly_plastic_constant_yield() {
1105 let h = IsotropicHardening::elastic_perfectly_plastic(300.0e6);
1106 assert!((h.yield_stress(0.0) - h.yield_stress(1.0)).abs() < 1.0);
1107 }
1108
1109 #[test]
1112 fn test_voce_at_zero_strain() {
1113 let v = VoceHardening::copper_like();
1114 let sy = v.yield_stress(0.0);
1115 assert!((sy - 50.0e6).abs() < 1.0, "Voce σ_y(0) = {sy}");
1116 }
1117
1118 #[test]
1119 fn test_voce_saturates_at_large_strain() {
1120 let v = VoceHardening::copper_like();
1121 let sy = v.yield_stress(100.0);
1122 assert!(
1123 (sy - v.sigma_inf).abs() / v.sigma_inf < 0.001,
1124 "Voce should saturate: {sy}"
1125 );
1126 }
1127
1128 #[test]
1129 fn test_voce_derivative_positive_at_small_strain() {
1130 let v = VoceHardening::new(100.0e6, 400.0e6, 5.0);
1131 assert!(v.d_yield_stress(0.01) > 0.0, "dσ_y/dε_p should be positive");
1132 }
1133
1134 #[test]
1135 fn test_voce_derivative_decreases_with_strain() {
1136 let v = VoceHardening::new(100.0e6, 400.0e6, 5.0);
1137 let d1 = v.d_yield_stress(0.0);
1138 let d2 = v.d_yield_stress(0.1);
1139 assert!(d2 < d1, "hardening rate should decrease with strain");
1140 }
1141
1142 #[test]
1145 fn test_prager_backstress_increment_direction() {
1146 let pk = PragerKinematic::new(250.0e6, 10.0e9);
1147 let dep = [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0];
1148 let da = pk.backstress_increment(&dep);
1149 assert!((da[0] - 10.0e9 * 0.001).abs() < 1.0, "Δα_xx");
1150 }
1151
1152 #[test]
1153 fn test_prager_von_mises_norm_uniaxial() {
1154 let s = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1156 let vm = PragerKinematic::von_mises_norm(&s);
1157 assert!((vm - 300.0e6).abs() < 1.0, "VM norm for uniaxial: {vm}");
1158 }
1159
1160 #[test]
1161 fn test_prager_yield_function_below_yield() {
1162 let pk = PragerKinematic::new(300.0e6, 10.0e9);
1163 let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1164 let backstress = [0.0; 6];
1165 let f = pk.yield_function(&stress, &backstress);
1166 assert!(f < 0.0, "Below yield: f = {f}");
1167 }
1168
1169 #[test]
1170 fn test_prager_effective_stress_with_backstress() {
1171 let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1172 let backstress = [50.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1173 let s_eff = PragerKinematic::effective_stress(&stress, &backstress);
1174 assert!((s_eff[0] - 250.0e6).abs() < 1.0, "σ_eff_xx = {}", s_eff[0]);
1175 }
1176
1177 #[test]
1180 fn test_chaboche_isotropic_stress_zero_at_zero_strain() {
1181 let c = Chaboche::steel_default();
1182 assert!(c.isotropic_stress(0.0).abs() < 1.0);
1183 }
1184
1185 #[test]
1186 fn test_chaboche_isotropic_stress_saturates() {
1187 let c = Chaboche::steel_default();
1188 let r_large = c.isotropic_stress(1000.0);
1189 assert!(
1190 (r_large - c.q).abs() / c.q < 0.001,
1191 "Chaboche R should saturate: {r_large}"
1192 );
1193 }
1194
1195 #[test]
1196 fn test_chaboche_yield_stress_increases_with_p() {
1197 let c = Chaboche::steel_default();
1198 let sy0 = c.yield_stress(0.0);
1199 let sy1 = c.yield_stress(0.01);
1200 assert!(sy1 > sy0, "yield stress should increase with p");
1201 }
1202
1203 #[test]
1204 fn test_chaboche_backstress_norm_positive() {
1205 let alpha = [50.0e6, -20.0e6, -30.0e6, 5.0e6, 0.0, 0.0];
1206 let norm = Chaboche::backstress_norm(&alpha);
1207 assert!(norm > 0.0, "backstress norm should be positive: {norm}");
1208 }
1209
1210 #[test]
1211 fn test_chaboche_backstress_increment_sign() {
1212 let c = Chaboche::steel_default();
1213 let flow_dir = [1.0, -0.5, -0.5, 0.0, 0.0, 0.0];
1214 let alpha = [0.0; 6];
1215 let da = c.backstress_increment(&flow_dir, &alpha, 0.001);
1216 assert!(
1218 da[0] > 0.0,
1219 "backstress increment xx should be positive: {}",
1220 da[0]
1221 );
1222 }
1223
1224 #[test]
1227 fn test_j2_return_map_elastic() {
1228 let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
1229 let trial = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1230 let (stress, _dep, delta_gamma) = rm.return_map(&trial, 0.0);
1231 assert!(
1232 delta_gamma < f64::EPSILON,
1233 "Should be elastic: Δγ={delta_gamma}"
1234 );
1235 assert!(
1236 (stress[0] - 100.0e6).abs() < 1.0,
1237 "stress unchanged: {}",
1238 stress[0]
1239 );
1240 }
1241
1242 #[test]
1243 fn test_j2_return_map_plastic() {
1244 let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
1245 let trial = [500.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1246 let (stress, _dep, delta_gamma) = rm.return_map(&trial, 0.0);
1247 assert!(delta_gamma > 0.0, "Should yield: Δγ={delta_gamma}");
1248 let vm = J2ReturnMapping::von_mises(&stress);
1250 let sy_updated = 250.0e6 + 2.0e9 * delta_gamma;
1251 assert!(
1252 (vm - sy_updated).abs() / sy_updated < 0.001,
1253 "VM after return: {vm} vs σ_y: {sy_updated}"
1254 );
1255 }
1256
1257 #[test]
1258 fn test_j2_von_mises_uniaxial() {
1259 let s = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1260 let vm = J2ReturnMapping::von_mises(&s);
1261 assert!((vm - 300.0e6).abs() < 1.0, "VM uniaxial: {vm}");
1262 }
1263
1264 #[test]
1265 fn test_j2_consistent_tangent_less_than_elastic() {
1266 let rm = J2ReturnMapping::from_young_poisson(200.0e9, 0.3, 250.0e6, 2.0e9);
1267 let c_ep = rm.consistent_tangent_uniaxial();
1268 let e =
1269 9.0 * rm.bulk_modulus * rm.shear_modulus / (3.0 * rm.bulk_modulus + rm.shear_modulus);
1270 assert!(c_ep < e, "C_ep should be < E");
1271 assert!(c_ep > 0.0, "C_ep should be positive");
1272 }
1273
1274 #[test]
1277 fn test_drucker_prager_cohesion_only_yield_stress() {
1278 let dp = DruckerPragerModel::from_mohr_coulomb_degrees(0.0, 1.0e6, 30.0e9, 0.3);
1280 let k = dp.k_dp();
1281 let expected = 2.0 * 1.0e6 / 3.0_f64.sqrt();
1282 assert!(
1283 (k - expected).abs() / expected < 0.01,
1284 "k_dp = {k} vs {expected}"
1285 );
1286 }
1287
1288 #[test]
1289 fn test_drucker_prager_hydrostatic_elastic() {
1290 let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 1.0e6, 30.0e9, 0.3);
1291 let stress = [-10.0e6, -10.0e6, -10.0e6, 0.0, 0.0, 0.0];
1293 assert!(
1294 dp.is_elastic(&stress),
1295 "Deep hydrostatic compression should be elastic"
1296 );
1297 }
1298
1299 #[test]
1300 fn test_drucker_prager_tensile_strength_positive() {
1301 let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 500.0e3, 10.0e9, 0.3);
1302 let st = dp.uniaxial_tensile_strength();
1303 assert!(st > 0.0, "tensile strength should be positive: {st}");
1304 }
1305
1306 #[test]
1307 fn test_drucker_prager_compressive_strength_gt_tensile() {
1308 let dp = DruckerPragerModel::from_mohr_coulomb_degrees(30.0, 500.0e3, 10.0e9, 0.3);
1309 let st = dp.uniaxial_tensile_strength();
1310 let sc = dp.uniaxial_compressive_strength();
1311 assert!(sc > st, "compressive > tensile for φ>0: sc={sc}, st={st}");
1312 }
1313
1314 #[test]
1317 fn test_mcc_bulk_modulus_increases_with_pressure() {
1318 let mcc = ModifiedCamClay::soft_clay();
1319 let k1 = mcc.bulk_modulus(50.0e3);
1320 let k2 = mcc.bulk_modulus(100.0e3);
1321 assert!(k2 > k1, "K should increase with p'");
1322 }
1323
1324 #[test]
1325 fn test_mcc_yield_function_on_yield_surface() {
1326 let mcc = ModifiedCamClay::soft_clay();
1327 let pc = mcc.pc0;
1328 let stress_apex = [pc, pc, pc, 0.0, 0.0, 0.0];
1331 let f = mcc.yield_function(&stress_apex, pc);
1332 assert!(
1333 f.abs() < 1.0,
1334 "MCC yield f at right apex should be ~0, got {f}"
1335 );
1336 }
1337
1338 #[test]
1339 fn test_mcc_hardening_increases_pc_under_compression() {
1340 let mcc = ModifiedCamClay::soft_clay();
1341 let pc_new = mcc.update_preconsolidation_pressure(mcc.pc0, 0.01);
1342 assert!(
1343 pc_new > mcc.pc0,
1344 "pc should increase under plastic volumetric compression"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_mcc_compression_index_positive() {
1350 let mcc = ModifiedCamClay::soft_clay();
1351 assert!(mcc.compression_index() > 0.0);
1352 }
1353
1354 #[test]
1355 fn test_mcc_critical_state_pressure_half_pc() {
1356 let mcc = ModifiedCamClay::soft_clay();
1357 let p_cs = mcc.critical_state_pressure(mcc.pc0);
1358 assert!((p_cs - mcc.pc0 / 2.0).abs() < 1e-6);
1359 }
1360
1361 #[test]
1364 fn test_plastic_dissipation_positive() {
1365 let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1366 let dep = [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0];
1367 let d = plastic_dissipation(&stress, &dep);
1368 assert!(d > 0.0, "plastic dissipation should be positive: {d}");
1369 }
1370
1371 #[test]
1372 fn test_plastic_dissipation_zero_for_zero_strain() {
1373 let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1374 let dep = [0.0; 6];
1375 let d = plastic_dissipation(&stress, &dep);
1376 assert!(d.abs() < 1e-10, "zero strain → zero dissipation: {d}");
1377 }
1378
1379 #[test]
1380 fn test_cumulative_dissipation() {
1381 let stresses = vec![
1382 [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
1383 [310.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
1384 ];
1385 let deps = vec![
1386 [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0_f64],
1387 [0.001, -0.0005, -0.0005, 0.0, 0.0, 0.0_f64],
1388 ];
1389 let d_total = cumulative_dissipation(&stresses, &deps);
1390 assert!(d_total > 0.0, "cumulative dissipation should be positive");
1391 }
1392
1393 #[test]
1396 fn test_limit_load_factor_below_yield() {
1397 let stresses = vec![[100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64]];
1399 let f = limit_load_factor_lower_bound(&stresses, 250.0e6);
1400 assert!((f - 2.5).abs() < 0.01, "limit load factor: {f}");
1401 }
1402
1403 #[test]
1404 fn test_limit_load_factor_multiple_elements() {
1405 let stresses = vec![
1406 [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64],
1407 [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64], ];
1409 let f = limit_load_factor_lower_bound(&stresses, 250.0e6);
1410 assert!((f - 1.25).abs() < 0.01, "limit load factor: {f}");
1411 }
1412
1413 #[test]
1414 fn test_limit_load_empty_returns_zero() {
1415 let f = limit_load_factor_lower_bound(&[], 250.0e6);
1416 assert_eq!(f, 0.0);
1417 }
1418
1419 #[test]
1420 fn test_von_mises_voigt_uniaxial() {
1421 let s = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1422 let vm = von_mises_stress_voigt(&s);
1423 assert!((vm - 200.0e6).abs() < 1.0, "VM uniaxial: {vm}");
1424 }
1425
1426 #[test]
1427 fn test_von_mises_voigt_hydrostatic_zero() {
1428 let s = [100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0];
1430 let vm = von_mises_stress_voigt(&s);
1431 assert!(vm.abs() < 1.0, "VM hydrostatic should be ~0: {vm}");
1432 }
1433
1434 #[test]
1437 fn test_ramberg_osgood_elastic_at_small_stress() {
1438 let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
1439 let sigma = 1.0e6; let eps = ro.total_strain(sigma);
1441 let eps_elastic = sigma / ro.young_modulus;
1442 assert!(
1443 (eps - eps_elastic).abs() / eps_elastic < 0.01,
1444 "small stress should be nearly elastic"
1445 );
1446 }
1447
1448 #[test]
1449 fn test_ramberg_osgood_plastic_strain_zero_at_zero_stress() {
1450 let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
1451 assert!(ro.plastic_strain(0.0).abs() < f64::EPSILON);
1452 }
1453
1454 #[test]
1455 fn test_ramberg_osgood_tangent_modulus_less_than_elastic() {
1456 let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
1457 let et = ro.tangent_modulus(ro.sigma_ref);
1458 assert!(
1459 et < ro.young_modulus,
1460 "tangent should be < E at reference stress"
1461 );
1462 assert!(et > 0.0, "tangent should be positive");
1463 }
1464
1465 #[test]
1466 fn test_ramberg_osgood_secant_less_than_elastic() {
1467 let ro = RambergOsgood::new(200.0e9, 400.0e6, 5.0, 3.0 / 7.0);
1468 let es = ro.secant_modulus(ro.sigma_ref);
1469 assert!(
1470 es < ro.young_modulus,
1471 "secant modulus at ref stress should be < E"
1472 );
1473 }
1474
1475 #[test]
1479 fn test_j2_consistent_tangent_elastic_step() {
1480 let ct = J2ConsistentTangent::from_young_poisson(200.0e9, 0.3, 2.0e9);
1481 let trial = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64];
1482 let c = ct.compute_consistent_tangent(&trial, 0.0);
1483 let ce = ct.elastic_stiffness();
1484 for i in 0..36 {
1485 assert!(
1486 (c[i] - ce[i]).abs() < 1.0,
1487 "Elastic tangent mismatch at {i}: {} vs {}",
1488 c[i],
1489 ce[i]
1490 );
1491 }
1492 }
1493
1494 #[test]
1496 fn test_j2_consistent_tangent_c11() {
1497 let e = 200.0e9_f64;
1498 let nu = 0.3_f64;
1499 let ct = J2ConsistentTangent::from_young_poisson(e, nu, 2.0e9);
1500 let c = ct.elastic_stiffness();
1501 let k = e / (3.0 * (1.0 - 2.0 * nu));
1502 let g = e / (2.0 * (1.0 + nu));
1503 let c11 = k + 4.0 / 3.0 * g;
1504 assert!((c[0] - c11).abs() / c11 < 1e-8, "C11: {} vs {}", c[0], c11);
1505 }
1506
1507 #[test]
1509 fn test_j2_consistent_tangent_c12() {
1510 let e = 200.0e9_f64;
1511 let nu = 0.3_f64;
1512 let ct = J2ConsistentTangent::from_young_poisson(e, nu, 2.0e9);
1513 let c = ct.elastic_stiffness();
1514 let k = e / (3.0 * (1.0 - 2.0 * nu));
1515 let g = e / (2.0 * (1.0 + nu));
1516 let c12 = k - 2.0 / 3.0 * g;
1517 assert!(
1518 (c[1] - c12).abs() / c12.abs() < 1e-8,
1519 "C12: {} vs {}",
1520 c[1],
1521 c12
1522 );
1523 }
1524
1525 #[test]
1527 fn test_j2_consistent_tangent_plastic_step_differs() {
1528 let ct = J2ConsistentTangent::from_young_poisson(200.0e9, 0.3, 2.0e9);
1529 let trial = [500.0e6, 0.0, 0.0, 0.0, 0.0, 0.0_f64];
1531 let delta_gamma = 1.0e-4;
1532 let c_ep = ct.compute_consistent_tangent(&trial, delta_gamma);
1533 let c_e = ct.elastic_stiffness();
1534 let diff: f64 = c_ep
1535 .iter()
1536 .zip(c_e.iter())
1537 .map(|(a, b)| (a - b).abs())
1538 .sum();
1539 assert!(
1540 diff > 1.0,
1541 "Plastic tangent should differ from elastic: total diff = {diff}"
1542 );
1543 }
1544
1545 #[test]
1549 fn test_dp_apex_pressure() {
1550 let phi = 30.0_f64.to_radians();
1551 let cohesion = 50.0e3_f64;
1552 let e = 50.0e6_f64;
1553 let nu = 0.3_f64;
1554 let dp = DruckerPragerModel::new(
1555 phi,
1556 cohesion,
1557 e / (2.0 * (1.0 + nu)),
1558 e / (3.0 * (1.0 - 2.0 * nu)),
1559 );
1560 let apr = DruckerPragerApexReturn::from_dp_model(&dp);
1561 let p_apex = apr.apex_pressure();
1562 let expected = dp.k_dp() / (3.0 * dp.alpha_dp());
1563 assert!(
1564 (p_apex - expected).abs() / expected < 1e-10,
1565 "Apex pressure: {} vs {}",
1566 p_apex,
1567 expected
1568 );
1569 }
1570
1571 #[test]
1573 fn test_dp_apex_return_zero_deviatoric() {
1574 let apr = DruckerPragerApexReturn::new(0.2, 100.0e3, 30.0e6, 15.0e6);
1575 let trial = [-200.0e3, -100.0e3, -50.0e3, 10.0e3, 0.0, 0.0];
1576 let s_apex = apr.compute_apex_return(&trial);
1577 assert!(
1579 (s_apex[0] - s_apex[1]).abs() < f64::EPSILON,
1580 "Apex return should be hydrostatic"
1581 );
1582 assert!(
1583 (s_apex[1] - s_apex[2]).abs() < f64::EPSILON,
1584 "Apex return should be hydrostatic"
1585 );
1586 assert_eq!(s_apex[3], 0.0);
1588 }
1589
1590 #[test]
1594 fn test_cam_clay_preconsolidation_increases_under_compression() {
1595 let cc = CamClayPreconsolidation::soft_clay();
1596 let pc_new = cc.compute_preconsolidation(cc.pc0, 0.01);
1597 assert!(
1598 pc_new > cc.pc0,
1599 "Preconsolidation should increase under compression"
1600 );
1601 }
1602
1603 #[test]
1605 fn test_cam_clay_preconsolidation_zero_strain_unchanged() {
1606 let cc = CamClayPreconsolidation::soft_clay();
1607 let pc_new = cc.compute_preconsolidation(cc.pc0, 0.0);
1608 assert!(
1609 (pc_new - cc.pc0).abs() / cc.pc0 < 1e-12,
1610 "Zero plastic strain: pc should not change: {} vs {}",
1611 pc_new,
1612 cc.pc0
1613 );
1614 }
1615
1616 #[test]
1618 fn test_cam_clay_ocr_normally_consolidated() {
1619 let cc = CamClayPreconsolidation::soft_clay();
1620 let ocr = cc.ocr(cc.pc0, cc.pc0);
1621 assert!(
1622 (ocr - 1.0).abs() < f64::EPSILON,
1623 "OCR should be 1 at p' = pc: {ocr}"
1624 );
1625 }
1626
1627 #[test]
1629 fn test_cam_clay_critical_state_pressure() {
1630 let cc = CamClayPreconsolidation::soft_clay();
1631 let pcs = cc.critical_state_pressure(cc.pc0);
1632 assert!(
1633 (pcs - cc.pc0 / 2.0).abs() < f64::EPSILON,
1634 "p'_cs should be pc/2: {pcs}"
1635 );
1636 }
1637}