1#![allow(dead_code)]
15#![allow(clippy::too_many_arguments)]
16
17use std::f64::consts::PI;
18
19#[derive(Debug, Clone, Copy)]
27pub struct PlaneStress {
28 pub sigma1: f64,
30 pub sigma2: f64,
32 pub tau12: f64,
34}
35
36impl PlaneStress {
37 pub fn new(sigma1: f64, sigma2: f64, tau12: f64) -> Self {
39 Self {
40 sigma1,
41 sigma2,
42 tau12,
43 }
44 }
45
46 pub fn zero() -> Self {
48 Self {
49 sigma1: 0.0,
50 sigma2: 0.0,
51 tau12: 0.0,
52 }
53 }
54
55 pub fn scaled(self, factor: f64) -> Self {
57 Self {
58 sigma1: self.sigma1 * factor,
59 sigma2: self.sigma2 * factor,
60 tau12: self.tau12 * factor,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct InterlaminaStress {
68 pub sigma_z: f64,
70 pub tau_xz: f64,
72 pub tau_yz: f64,
74}
75
76impl InterlaminaStress {
77 pub fn new(sigma_z: f64, tau_xz: f64, tau_yz: f64) -> Self {
79 Self {
80 sigma_z,
81 tau_xz,
82 tau_yz,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
93pub struct PlyStrength {
94 pub f1t: f64,
96 pub f1c: f64,
98 pub f2t: f64,
100 pub f2c: f64,
102 pub f12: f64,
104 pub fzt: f64,
106 pub fzs: f64,
108}
109
110impl PlyStrength {
111 pub fn carbon_epoxy_t300() -> Self {
113 Self {
114 f1t: 1500.0e6,
115 f1c: 900.0e6,
116 f2t: 50.0e6,
117 f2c: 200.0e6,
118 f12: 70.0e6,
119 fzt: 50.0e6,
120 fzs: 100.0e6,
121 }
122 }
123
124 pub fn glass_epoxy() -> Self {
126 Self {
127 f1t: 780.0e6,
128 f1c: 500.0e6,
129 f2t: 28.0e6,
130 f2c: 130.0e6,
131 f12: 50.0e6,
132 fzt: 28.0e6,
133 fzs: 80.0e6,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
148pub struct MaxStressCriterion {
149 pub strength: PlyStrength,
151}
152
153impl MaxStressCriterion {
154 pub fn new(strength: PlyStrength) -> Self {
156 Self { strength }
157 }
158
159 pub fn fi_longitudinal(&self, stress: PlaneStress) -> f64 {
161 if stress.sigma1 >= 0.0 {
162 stress.sigma1 / self.strength.f1t
163 } else {
164 stress.sigma1.abs() / self.strength.f1c
165 }
166 }
167
168 pub fn fi_transverse(&self, stress: PlaneStress) -> f64 {
170 if stress.sigma2 >= 0.0 {
171 stress.sigma2 / self.strength.f2t
172 } else {
173 stress.sigma2.abs() / self.strength.f2c
174 }
175 }
176
177 pub fn fi_shear(&self, stress: PlaneStress) -> f64 {
179 stress.tau12.abs() / self.strength.f12
180 }
181
182 pub fn failure_indices(&self, stress: PlaneStress) -> [f64; 3] {
184 [
185 self.fi_longitudinal(stress),
186 self.fi_transverse(stress),
187 self.fi_shear(stress),
188 ]
189 }
190
191 pub fn max_fi(&self, stress: PlaneStress) -> f64 {
193 let fis = self.failure_indices(stress);
194 fis.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
195 }
196
197 pub fn has_failed(&self, stress: PlaneStress) -> bool {
199 self.max_fi(stress) >= 1.0
200 }
201
202 pub fn load_factor(&self, stress: PlaneStress) -> f64 {
205 let max = self.max_fi(stress);
206 if max <= 0.0 { f64::INFINITY } else { 1.0 / max }
207 }
208}
209
210#[derive(Debug, Clone)]
221pub struct TsaiHillCriterion {
222 pub strength: PlyStrength,
224}
225
226impl TsaiHillCriterion {
227 pub fn new(strength: PlyStrength) -> Self {
229 Self { strength }
230 }
231
232 pub fn failure_index_sq(&self, stress: PlaneStress) -> f64 {
236 let f1 = if stress.sigma1 >= 0.0 {
237 self.strength.f1t
238 } else {
239 self.strength.f1c
240 };
241 let f2 = if stress.sigma2 >= 0.0 {
242 self.strength.f2t
243 } else {
244 self.strength.f2c
245 };
246 let s1 = stress.sigma1 / f1;
247 let s2 = stress.sigma2 / f2;
248 let s12 = stress.tau12 / self.strength.f12;
249 s1 * s1 - s1 * s2 + s2 * s2 + s12 * s12
250 }
251
252 pub fn failure_index(&self, stress: PlaneStress) -> f64 {
254 self.failure_index_sq(stress).sqrt()
255 }
256
257 pub fn has_failed(&self, stress: PlaneStress) -> bool {
259 self.failure_index_sq(stress) >= 1.0
260 }
261
262 pub fn safety_margin(&self, stress: PlaneStress) -> f64 {
264 let h = self.failure_index(stress);
265 if h <= 0.0 { f64::INFINITY } else { 1.0 / h }
266 }
267
268 pub fn load_factor(&self, stress: PlaneStress) -> f64 {
272 let h2 = self.failure_index_sq(stress);
273 if h2 <= 0.0 {
274 f64::INFINITY
275 } else {
276 1.0 / h2.sqrt()
277 }
278 }
279
280 pub fn biaxial_strength(&self, k: f64) -> f64 {
284 let f1 = self.strength.f1t.max(self.strength.f1c);
287 let f2 = self.strength.f2t.max(self.strength.f2c);
288 let a = 1.0 / (f1 * f1) - k / (f1 * f1) + k * k / (f2 * f2);
289 if a <= 0.0 {
290 f64::INFINITY
291 } else {
292 1.0 / a.sqrt()
293 }
294 }
295}
296
297#[derive(Debug, Clone)]
307pub struct TsaiWuCriterion {
308 pub strength: PlyStrength,
310 pub f12_star: f64,
312}
313
314impl TsaiWuCriterion {
315 pub fn new(strength: PlyStrength, f12_star: f64) -> Self {
317 Self { strength, f12_star }
318 }
319
320 pub fn linear_terms(&self) -> [f64; 2] {
322 let f1 = 1.0 / self.strength.f1t - 1.0 / self.strength.f1c;
323 let f2 = 1.0 / self.strength.f2t - 1.0 / self.strength.f2c;
324 [f1, f2]
325 }
326
327 pub fn quadratic_terms(&self) -> [f64; 4] {
329 let f11 = 1.0 / (self.strength.f1t * self.strength.f1c);
330 let f22 = 1.0 / (self.strength.f2t * self.strength.f2c);
331 let f66 = 1.0 / (self.strength.f12 * self.strength.f12);
332 let f12 = self.f12_star
333 / (self.strength.f1t * self.strength.f1c * self.strength.f2t * self.strength.f2c)
334 .sqrt();
335 [f11, f22, f66, f12]
336 }
337
338 pub fn failure_index(&self, stress: PlaneStress) -> f64 {
340 let [f1, f2] = self.linear_terms();
341 let [f11, f22, f66, f12] = self.quadratic_terms();
342 let s1 = stress.sigma1;
343 let s2 = stress.sigma2;
344 let s12 = stress.tau12;
345 f1 * s1 + f2 * s2 + f11 * s1 * s1 + f22 * s2 * s2 + f66 * s12 * s12 + 2.0 * f12 * s1 * s2
346 }
347
348 pub fn has_failed(&self, stress: PlaneStress) -> bool {
350 self.failure_index(stress) >= 1.0
351 }
352
353 pub fn load_factor(&self, stress: PlaneStress) -> f64 {
357 let [f1, f2] = self.linear_terms();
358 let [f11, f22, f66, f12] = self.quadratic_terms();
359 let s1 = stress.sigma1;
360 let s2 = stress.sigma2;
361 let s12 = stress.tau12;
362 let a = f11 * s1 * s1 + f22 * s2 * s2 + f66 * s12 * s12 + 2.0 * f12 * s1 * s2;
364 let b = f1 * s1 + f2 * s2;
365 if a.abs() < 1e-40 {
366 if b.abs() < 1e-40 {
367 return f64::INFINITY;
368 }
369 return 1.0 / b;
370 }
371 let disc = b * b + 4.0 * a;
372 if disc < 0.0 {
373 return f64::INFINITY;
374 }
375 (-b + disc.sqrt()) / (2.0 * a)
376 }
377
378 pub fn is_stable(&self) -> bool {
380 let [f11, f22, _, f12] = self.quadratic_terms();
381 f12.abs() < (f11 * f22).sqrt()
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq)]
391pub enum PuckFfMode {
392 None,
394 Tension,
396 Compression,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq)]
402pub enum PuckIffMode {
403 None,
405 ModeA,
407 ModeB,
409 ModeC,
411}
412
413#[derive(Debug, Clone)]
418pub struct PuckCriterion {
419 pub strength: PlyStrength,
421 pub p_plus_12: f64,
423 pub p_minus_12: f64,
425 pub e1: f64,
427 pub nu12: f64,
429}
430
431impl PuckCriterion {
432 pub fn new(strength: PlyStrength, p_plus_12: f64, p_minus_12: f64, e1: f64, nu12: f64) -> Self {
434 Self {
435 strength,
436 p_plus_12,
437 p_minus_12,
438 e1,
439 nu12,
440 }
441 }
442
443 pub fn carbon_epoxy_default() -> Self {
445 Self::new(PlyStrength::carbon_epoxy_t300(), 0.30, 0.25, 130.0e9, 0.28)
446 }
447
448 pub fn ff_exposure(&self, stress: PlaneStress) -> f64 {
452 if stress.sigma1 >= 0.0 {
453 (stress.sigma1 / self.strength.f1t).abs()
455 } else {
456 (stress.sigma1.abs() / self.strength.f1c).abs()
458 }
459 }
460
461 pub fn ff_mode(&self, stress: PlaneStress) -> PuckFfMode {
463 let fe = self.ff_exposure(stress);
464 if fe < 1.0 {
465 PuckFfMode::None
466 } else if stress.sigma1 >= 0.0 {
467 PuckFfMode::Tension
468 } else {
469 PuckFfMode::Compression
470 }
471 }
472
473 pub fn iff_exposure_mode_a(&self, stress: PlaneStress) -> f64 {
480 if stress.sigma2 < 0.0 {
481 return 0.0;
482 }
483 let s2 = stress.sigma2;
484 let s12 = stress.tau12;
485 let f2a = self.strength.f12; let f2t = self.strength.f2t;
487 let sq = ((s12 / f2a).powi(2) + (s2 / f2t).powi(2)).sqrt();
488 sq + self.p_plus_12 * s2 / f2a
489 }
490
491 pub fn iff_exposure_mode_bc(&self, stress: PlaneStress) -> f64 {
495 if stress.sigma2 >= 0.0 {
496 return 0.0;
497 }
498 let s2 = stress.sigma2; let s12 = stress.tau12;
500 let f2a = self.strength.f12;
501 let sq = (s12 * s12 + (self.p_minus_12 * s2).powi(2)).sqrt();
502 (sq + self.p_minus_12 * s2) / f2a
503 }
504
505 pub fn iff_exposure(&self, stress: PlaneStress) -> f64 {
507 let fa = self.iff_exposure_mode_a(stress);
508 let fbc = self.iff_exposure_mode_bc(stress);
509 fa.max(fbc)
510 }
511
512 pub fn iff_mode(&self, stress: PlaneStress) -> PuckIffMode {
514 if self.iff_exposure(stress) < 1.0 {
515 return PuckIffMode::None;
516 }
517 if stress.sigma2 >= 0.0 {
518 PuckIffMode::ModeA
519 } else {
520 if stress.tau12.abs() > self.strength.f12 * 0.5 {
522 PuckIffMode::ModeB
523 } else {
524 PuckIffMode::ModeC
525 }
526 }
527 }
528
529 pub fn has_failed(&self, stress: PlaneStress) -> bool {
531 self.ff_exposure(stress) >= 1.0 || self.iff_exposure(stress) >= 1.0
532 }
533
534 pub fn fracture_plane_angle(&self, stress: PlaneStress) -> f64 {
538 if stress.sigma2 >= 0.0 {
539 return 0.0; }
541 let denom = 2.0 * self.strength.f2c + self.p_minus_12 * stress.sigma2.abs();
542 if denom < 1e-20 {
543 return 0.0;
544 }
545 (self.strength.f2c / denom).acos().to_degrees()
546 }
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum DegradationRule {
556 Total,
558 Partial,
560 None,
562}
563
564#[derive(Debug, Clone)]
566pub struct PlyState {
567 pub angle_deg: f64,
569 pub thickness: f64,
571 pub q_matrix: [f64; 4],
573 pub ff_failed: bool,
575 pub iff_failed: bool,
577 pub degradation: f64,
579}
580
581impl PlyState {
582 pub fn new(angle_deg: f64, thickness: f64, e1: f64, e2: f64, nu12: f64, g12: f64) -> Self {
586 let nu21 = nu12 * e2 / e1;
587 let denom = 1.0 - nu12 * nu21;
588 let q11 = e1 / denom;
589 let q22 = e2 / denom;
590 let q12 = nu12 * e2 / denom;
591 let q66 = g12;
592 Self {
593 angle_deg,
594 thickness,
595 q_matrix: [q11, q22, q12, q66],
596 ff_failed: false,
597 iff_failed: false,
598 degradation: 1.0,
599 }
600 }
601
602 pub fn apply_degradation(&mut self, rule: DegradationRule, ff: bool, iff: bool) {
604 self.ff_failed |= ff;
605 self.iff_failed |= iff;
606 self.degradation = match rule {
607 DegradationRule::Total => {
608 if ff {
609 0.01
610 } else if iff {
611 0.1
612 } else {
613 1.0
614 }
615 }
616 DegradationRule::Partial => {
617 if ff {
618 0.5
619 } else if iff {
620 0.7
621 } else {
622 1.0
623 }
624 }
625 DegradationRule::None => 1.0,
626 };
627 for q in &mut self.q_matrix {
628 *q *= self.degradation;
629 }
630 }
631
632 pub fn q11(&self) -> f64 {
634 self.q_matrix[0]
635 }
636
637 pub fn q22(&self) -> f64 {
639 self.q_matrix[1]
640 }
641
642 pub fn q12(&self) -> f64 {
644 self.q_matrix[2]
645 }
646
647 pub fn q66(&self) -> f64 {
649 self.q_matrix[3]
650 }
651}
652
653#[derive(Debug, Clone)]
659pub struct ProgressiveDamage {
660 pub plies: Vec<PlyState>,
662 pub rule: DegradationRule,
664 pub strengths: Vec<PlyStrength>,
666 pub fpf_load: f64,
668 pub lpf_load: f64,
670}
671
672impl ProgressiveDamage {
673 pub fn new(plies: Vec<PlyState>, strengths: Vec<PlyStrength>, rule: DegradationRule) -> Self {
675 Self {
676 plies,
677 rule,
678 strengths,
679 fpf_load: 0.0,
680 lpf_load: 0.0,
681 }
682 }
683
684 pub fn n_intact(&self) -> usize {
686 self.plies
687 .iter()
688 .filter(|p| !p.ff_failed && !p.iff_failed)
689 .count()
690 }
691
692 pub fn all_failed(&self) -> bool {
694 self.plies.iter().all(|p| p.ff_failed || p.iff_failed)
695 }
696
697 pub fn a_matrix(&self) -> [f64; 6] {
701 let mut a = [0.0_f64; 6];
702 for ply in &self.plies {
703 let t = ply.thickness * ply.degradation;
704 let q = transformed_q(&ply.q_matrix, ply.angle_deg.to_radians());
705 a[0] += q[0] * t;
706 a[1] += q[1] * t;
707 a[2] += q[2] * t;
708 a[3] += q[3] * t;
709 a[4] += q[4] * t;
710 a[5] += q[5] * t;
711 }
712 a
713 }
714
715 pub fn progressive_failure_sweep(&mut self, applied_load: [f64; 3], n_steps: usize) -> f64 {
719 let load_max = 2.0; let d_lambda = load_max / n_steps as f64;
721 let mut first_failure_lambda = f64::INFINITY;
722
723 for step in 1..=n_steps {
724 let lambda = step as f64 * d_lambda;
725 let n = [
726 applied_load[0] * lambda,
727 applied_load[1] * lambda,
728 applied_load[2] * lambda,
729 ];
730 let a = self.a_matrix();
732 let total_t: f64 = self
733 .plies
734 .iter()
735 .map(|p| p.thickness)
736 .sum::<f64>()
737 .max(1e-12);
738 let eps1 = n[0] / (a[0].max(1e-12) * total_t);
739 let eps2 = n[1] / (a[1].max(1e-12) * total_t);
740 let gam12 = n[2] / (a[5].max(1e-12) * total_t);
741
742 let mut any_new_failure = false;
743 for (i, ply) in self.plies.iter_mut().enumerate() {
744 if ply.ff_failed && ply.iff_failed {
745 continue;
746 }
747 let theta = ply.angle_deg.to_radians();
749 let c = theta.cos();
750 let s = theta.sin();
751 let eps1_ply = c * c * eps1 + s * s * eps2 + c * s * gam12;
752 let eps2_ply = s * s * eps1 + c * c * eps2 - c * s * gam12;
753 let gam12_ply = -2.0 * c * s * eps1 + 2.0 * c * s * eps2 + (c * c - s * s) * gam12;
754 let q = &ply.q_matrix;
755 let stress = PlaneStress::new(
756 q[0] * eps1_ply + q[2] * eps2_ply,
757 q[2] * eps1_ply + q[1] * eps2_ply,
758 q[3] * gam12_ply,
759 );
760 if i < self.strengths.len() {
761 let crit = MaxStressCriterion::new(self.strengths[i].clone());
762 if crit.has_failed(stress) && !ply.ff_failed {
763 ply.ff_failed = true;
764 ply.degradation = match self.rule {
765 DegradationRule::Total => 0.01,
766 DegradationRule::Partial => 0.5,
767 DegradationRule::None => 1.0,
768 };
769 any_new_failure = true;
770 if first_failure_lambda > lambda {
771 first_failure_lambda = lambda;
772 }
773 }
774 }
775 }
776 let _ = any_new_failure;
777 if self.all_failed() {
778 self.lpf_load = lambda;
779 break;
780 }
781 }
782 if first_failure_lambda < f64::INFINITY {
783 self.fpf_load = first_failure_lambda;
784 }
785 first_failure_lambda
786 }
787}
788
789#[derive(Debug, Clone, Copy)]
795pub struct EnergyReleaseRate {
796 pub g1: f64,
798 pub g2: f64,
800 pub g3: f64,
802}
803
804impl EnergyReleaseRate {
805 pub fn new(g1: f64, g2: f64, g3: f64) -> Self {
807 Self { g1, g2, g3 }
808 }
809
810 pub fn total(&self) -> f64 {
812 self.g1 + self.g2 + self.g3
813 }
814
815 pub fn mode_i_ratio(&self) -> f64 {
817 let gt = self.total();
818 if gt < 1e-20 { 0.0 } else { self.g1 / gt }
819 }
820
821 pub fn mode_ii_ratio(&self) -> f64 {
823 let gt = self.total();
824 if gt < 1e-20 { 0.0 } else { self.g2 / gt }
825 }
826}
827
828#[derive(Debug, Clone)]
835pub struct DelamCriterion {
836 pub z_t: f64,
838 pub s_xz: f64,
840 pub s_yz: f64,
842 pub g1c: f64,
844 pub g2c: f64,
846 pub g3c: f64,
848 pub bk_eta: f64,
850}
851
852impl DelamCriterion {
853 pub fn new(z_t: f64, s_xz: f64, s_yz: f64, g1c: f64, g2c: f64, g3c: f64, bk_eta: f64) -> Self {
855 Self {
856 z_t,
857 s_xz,
858 s_yz,
859 g1c,
860 g2c,
861 g3c,
862 bk_eta,
863 }
864 }
865
866 pub fn carbon_epoxy_default() -> Self {
868 Self::new(50.0e6, 100.0e6, 100.0e6, 200.0, 400.0, 400.0, 1.75)
869 }
870
871 pub fn quadratic_stress_index(&self, stress: InterlaminaStress) -> f64 {
877 let sz = stress.sigma_z.max(0.0); (sz / self.z_t).powi(2)
879 + (stress.tau_xz / self.s_xz).powi(2)
880 + (stress.tau_yz / self.s_yz).powi(2)
881 }
882
883 pub fn stress_delamination(&self, stress: InterlaminaStress) -> bool {
885 self.quadratic_stress_index(stress) >= 1.0
886 }
887
888 pub fn linear_err_index(&self, err: EnergyReleaseRate) -> f64 {
892 err.g1 / self.g1c + err.g2 / self.g2c + err.g3 / self.g3c
893 }
894
895 pub fn err_delamination(&self, err: EnergyReleaseRate) -> bool {
897 self.linear_err_index(err) >= 1.0
898 }
899
900 pub fn bk_criterion(&self, err: EnergyReleaseRate) -> f64 {
905 let gt = err.total();
906 if gt < 1e-20 {
907 return 0.0;
908 }
909 let g_shear = err.g2 + err.g3;
910 let mix = g_shear / gt;
911 let gc_mm = self.g1c + (self.g2c - self.g1c) * mix.powf(self.bk_eta);
912 gt / gc_mm
913 }
914
915 pub fn bk_delamination(&self, err: EnergyReleaseRate) -> bool {
917 self.bk_criterion(err) >= 1.0
918 }
919
920 pub fn stress_intensity_mode1(&self, sigma_z: f64, half_crack: f64) -> f64 {
924 sigma_z * (PI * half_crack).sqrt()
925 }
926
927 pub fn g1c_from_k1c(k1c: f64, ez: f64) -> f64 {
932 k1c * k1c / ez
933 }
934
935 pub fn j_integral_proxy(&self, err: EnergyReleaseRate) -> f64 {
937 err.total()
938 }
939
940 pub fn paris_growth_rate(
946 &self,
947 delta_g1: f64,
948 delta_g2: f64,
949 paris_c: f64,
950 paris_m: f64,
951 ) -> f64 {
952 let alpha = 0.5;
953 let g_equiv = delta_g1 + delta_g2 * (self.g2c / self.g1c).powf(alpha);
954 paris_c * g_equiv.powf(paris_m)
955 }
956}
957
958fn transformed_q(q: &[f64; 4], theta: f64) -> [f64; 6] {
966 let c = theta.cos();
967 let s = theta.sin();
968 let c2 = c * c;
969 let s2 = s * s;
970 let c4 = c2 * c2;
971 let s4 = s2 * s2;
972 let cs = c * s;
973 let cs2 = cs * cs;
974 let [q11, q22, q12, q66] = *q;
975 let q_bar11 = q11 * c4 + 2.0 * (q12 + 2.0 * q66) * cs2 + q22 * s4;
976 let q_bar22 = q11 * s4 + 2.0 * (q12 + 2.0 * q66) * cs2 + q22 * c4;
977 let q_bar12 = (q11 + q22 - 4.0 * q66) * cs2 + q12 * (c4 + s4);
978 let q_bar16 = (q11 - q12 - 2.0 * q66) * c2 * cs - (q22 - q12 - 2.0 * q66) * s2 * cs;
979 let q_bar26 = (q11 - q12 - 2.0 * q66) * s2 * cs - (q22 - q12 - 2.0 * q66) * c2 * cs;
980 let q_bar66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * cs2 + q66 * (c4 + s4);
981 [q_bar11, q_bar22, q_bar12, q_bar16, q_bar26, q_bar66]
982}
983
984pub fn reserve_factor(criterion_value: f64) -> Option<f64> {
988 if criterion_value <= 0.0 {
989 None
990 } else {
991 Some(1.0 / criterion_value)
992 }
993}
994
995pub fn tsai_hill_envelope(strength: &PlyStrength, n_points: usize) -> Vec<(f64, f64)> {
999 let crit = TsaiHillCriterion::new(strength.clone());
1000 let mut pts = Vec::with_capacity(n_points);
1001 for i in 0..n_points {
1002 let sigma1 =
1003 -strength.f1c + (strength.f1t + strength.f1c) * i as f64 / (n_points - 1).max(1) as f64;
1004 let f1 = if sigma1 >= 0.0 {
1006 strength.f1t
1007 } else {
1008 strength.f1c
1009 };
1010 let f2 = strength.f2t;
1011 let a = 1.0 / (f2 * f2);
1012 let b = -sigma1 / (f1 * f1);
1013 let c_coeff = (sigma1 / f1).powi(2) - 1.0;
1014 let disc = b * b - 4.0 * a * c_coeff;
1015 if disc >= 0.0 {
1016 let s2 = (-b + disc.sqrt()) / (2.0 * a);
1017 let _ = crit.failure_index_sq(PlaneStress::new(sigma1, s2, 0.0)); pts.push((sigma1, s2));
1019 }
1020 }
1021 pts
1022}
1023
1024#[cfg(test)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1035 fn test_plane_stress_zero() {
1036 let s = PlaneStress::zero();
1037 assert_eq!(s.sigma1, 0.0);
1038 }
1039
1040 #[test]
1041 fn test_plane_stress_scaled() {
1042 let s = PlaneStress::new(100.0, 50.0, 20.0).scaled(2.0);
1043 assert!((s.sigma1 - 200.0).abs() < 1e-10);
1044 assert!((s.tau12 - 40.0).abs() < 1e-10);
1045 }
1046
1047 #[test]
1050 fn test_carbon_epoxy_strengths_positive() {
1051 let st = PlyStrength::carbon_epoxy_t300();
1052 assert!(st.f1t > 0.0 && st.f1c > 0.0 && st.f2t > 0.0 && st.f12 > 0.0);
1053 }
1054
1055 #[test]
1056 fn test_glass_epoxy_f1t_less_than_carbon() {
1057 let ce = PlyStrength::carbon_epoxy_t300();
1058 let ge = PlyStrength::glass_epoxy();
1059 assert!(ge.f1t < ce.f1t);
1060 }
1061
1062 #[test]
1065 fn test_max_stress_no_failure_under_threshold() {
1066 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1067 let s = PlaneStress::new(100.0e6, 10.0e6, 5.0e6); assert!(!crit.has_failed(s));
1069 }
1070
1071 #[test]
1072 fn test_max_stress_failure_at_f1t() {
1073 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1074 let s = PlaneStress::new(1500.0e6, 0.0, 0.0);
1075 assert!(crit.has_failed(s));
1076 }
1077
1078 #[test]
1079 fn test_max_stress_fi_longitudinal_tension() {
1080 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1081 let s = PlaneStress::new(750.0e6, 0.0, 0.0);
1082 assert!((crit.fi_longitudinal(s) - 0.5).abs() < 1e-6);
1083 }
1084
1085 #[test]
1086 fn test_max_stress_fi_longitudinal_compression() {
1087 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1088 let s = PlaneStress::new(-900.0e6, 0.0, 0.0);
1089 assert!((crit.fi_longitudinal(s) - 1.0).abs() < 1e-6);
1090 }
1091
1092 #[test]
1093 fn test_max_stress_fi_transverse_zero() {
1094 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1095 let s = PlaneStress::new(100.0e6, 0.0, 0.0);
1096 assert!((crit.fi_transverse(s)).abs() < 1e-10);
1097 }
1098
1099 #[test]
1100 fn test_max_stress_load_factor_gt_one_safe() {
1101 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1102 let s = PlaneStress::new(100.0e6, 10.0e6, 5.0e6);
1103 assert!(crit.load_factor(s) > 1.0);
1104 }
1105
1106 #[test]
1107 fn test_max_stress_load_factor_le_one_failed() {
1108 let crit = MaxStressCriterion::new(PlyStrength::carbon_epoxy_t300());
1109 let s = PlaneStress::new(1500.0e6, 0.0, 0.0);
1110 assert!(crit.load_factor(s) <= 1.0);
1111 }
1112
1113 #[test]
1116 fn test_tsai_hill_no_failure_low_stress() {
1117 let crit = TsaiHillCriterion::new(PlyStrength::carbon_epoxy_t300());
1118 let s = PlaneStress::new(100.0e6, 10.0e6, 5.0e6);
1119 assert!(!crit.has_failed(s));
1120 }
1121
1122 #[test]
1123 fn test_tsai_hill_failure_at_strength() {
1124 let crit = TsaiHillCriterion::new(PlyStrength::carbon_epoxy_t300());
1125 let s = PlaneStress::new(1500.0e6, 0.0, 0.0);
1126 assert!(crit.has_failed(s));
1127 }
1128
1129 #[test]
1130 fn test_tsai_hill_safety_margin_gt_one_safe() {
1131 let crit = TsaiHillCriterion::new(PlyStrength::carbon_epoxy_t300());
1132 let s = PlaneStress::new(100.0e6, 5.0e6, 5.0e6);
1133 assert!(crit.safety_margin(s) > 1.0);
1134 }
1135
1136 #[test]
1137 fn test_tsai_hill_load_factor_positive() {
1138 let crit = TsaiHillCriterion::new(PlyStrength::carbon_epoxy_t300());
1139 let s = PlaneStress::new(300.0e6, 20.0e6, 10.0e6);
1140 assert!(crit.load_factor(s) > 0.0);
1141 }
1142
1143 #[test]
1144 fn test_tsai_hill_biaxial_strength_positive() {
1145 let crit = TsaiHillCriterion::new(PlyStrength::carbon_epoxy_t300());
1146 let sigma1 = crit.biaxial_strength(0.0);
1147 assert!(sigma1 > 0.0);
1148 }
1149
1150 #[test]
1151 fn test_tsai_hill_failure_index_at_uniaxial_f1t() {
1152 let st = PlyStrength::carbon_epoxy_t300();
1153 let crit = TsaiHillCriterion::new(st.clone());
1154 let s = PlaneStress::new(st.f1t, 0.0, 0.0);
1155 let fi = crit.failure_index_sq(s);
1156 assert!((fi - 1.0).abs() < 1e-6, "FI at F1t should be 1: {fi}");
1157 }
1158
1159 #[test]
1162 fn test_tsai_wu_stable_interaction_coeff() {
1163 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1164 assert!(crit.is_stable());
1165 }
1166
1167 #[test]
1168 fn test_tsai_wu_no_failure_low_stress() {
1169 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1170 let s = PlaneStress::new(100.0e6, 5.0e6, 5.0e6);
1171 assert!(!crit.has_failed(s));
1172 }
1173
1174 #[test]
1175 fn test_tsai_wu_failure_at_high_load() {
1176 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1177 let s = PlaneStress::new(1600.0e6, 60.0e6, 80.0e6);
1178 assert!(crit.has_failed(s));
1179 }
1180
1181 #[test]
1182 fn test_tsai_wu_load_factor_positive() {
1183 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1184 let s = PlaneStress::new(300.0e6, 20.0e6, 10.0e6);
1185 assert!(crit.load_factor(s) > 0.0);
1186 }
1187
1188 #[test]
1189 fn test_tsai_wu_linear_terms_sign() {
1190 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1191 let [f1, _f2] = crit.linear_terms();
1192 assert!(f1.is_finite());
1194 }
1195
1196 #[test]
1197 fn test_tsai_wu_quadratic_terms_f11_positive() {
1198 let crit = TsaiWuCriterion::new(PlyStrength::carbon_epoxy_t300(), -0.5);
1199 let [f11, f22, f66, _f12] = crit.quadratic_terms();
1200 assert!(f11 > 0.0 && f22 > 0.0 && f66 > 0.0);
1201 }
1202
1203 #[test]
1206 fn test_puck_ff_none_at_low_stress() {
1207 let puck = PuckCriterion::carbon_epoxy_default();
1208 let s = PlaneStress::new(100.0e6, 10.0e6, 5.0e6);
1209 assert_eq!(puck.ff_mode(s), PuckFfMode::None);
1210 }
1211
1212 #[test]
1213 fn test_puck_ff_tension_at_f1t() {
1214 let puck = PuckCriterion::carbon_epoxy_default();
1215 let s = PlaneStress::new(1600.0e6, 0.0, 0.0);
1216 assert_eq!(puck.ff_mode(s), PuckFfMode::Tension);
1217 }
1218
1219 #[test]
1220 fn test_puck_ff_compression_at_minus_f1c() {
1221 let puck = PuckCriterion::carbon_epoxy_default();
1222 let s = PlaneStress::new(-1000.0e6, 0.0, 0.0);
1223 assert_eq!(puck.ff_mode(s), PuckFfMode::Compression);
1224 }
1225
1226 #[test]
1227 fn test_puck_iff_mode_a_tension_transverse() {
1228 let puck = PuckCriterion::carbon_epoxy_default();
1229 let s = PlaneStress::new(0.0, 60.0e6, 0.0);
1231 assert!(puck.iff_exposure_mode_a(s) > 1.0);
1232 }
1233
1234 #[test]
1235 fn test_puck_iff_mode_bc_compression() {
1236 let puck = PuckCriterion::carbon_epoxy_default();
1237 let s = PlaneStress::new(0.0, -250.0e6, 80.0e6);
1238 let exp = puck.iff_exposure_mode_bc(s);
1240 assert!(exp >= 0.0);
1241 }
1242
1243 #[test]
1244 fn test_puck_fracture_angle_zero_for_tension() {
1245 let puck = PuckCriterion::carbon_epoxy_default();
1246 let s = PlaneStress::new(0.0, 30.0e6, 0.0);
1247 assert!((puck.fracture_plane_angle(s)).abs() < 1e-10);
1248 }
1249
1250 #[test]
1251 fn test_puck_fracture_angle_positive_for_compression() {
1252 let puck = PuckCriterion::carbon_epoxy_default();
1253 let s = PlaneStress::new(0.0, -100.0e6, 0.0);
1254 let angle = puck.fracture_plane_angle(s);
1255 assert!(angle >= 0.0);
1256 }
1257
1258 #[test]
1259 fn test_puck_no_failure_at_zero_stress() {
1260 let puck = PuckCriterion::carbon_epoxy_default();
1261 let s = PlaneStress::zero();
1262 assert!(!puck.has_failed(s));
1263 }
1264
1265 #[test]
1268 fn test_ply_state_construction() {
1269 let ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1270 assert!(ply.q11() > ply.q22());
1271 assert!(!ply.ff_failed);
1272 }
1273
1274 #[test]
1275 fn test_ply_state_q11_q22_ordering() {
1276 let ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1277 assert!(
1278 ply.q11() > ply.q22(),
1279 "Q11 should be > Q22 for UD ply at 0°"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_ply_degradation_total() {
1285 let mut ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1286 let q11_before = ply.q11();
1287 ply.apply_degradation(DegradationRule::Total, true, false);
1288 assert!(ply.q11() < q11_before);
1289 assert!(ply.ff_failed);
1290 }
1291
1292 #[test]
1293 fn test_ply_degradation_none_keeps_stiffness() {
1294 let mut ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1295 let q11_before = ply.q11();
1296 ply.apply_degradation(DegradationRule::None, false, true);
1297 assert!((ply.q11() - q11_before).abs() < 1e-3);
1298 }
1299
1300 #[test]
1301 fn test_progressive_damage_n_intact_all() {
1302 let ply1 = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1303 let ply2 = PlyState::new(90.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1304 let st1 = PlyStrength::carbon_epoxy_t300();
1305 let st2 = PlyStrength::carbon_epoxy_t300();
1306 let pd = ProgressiveDamage::new(vec![ply1, ply2], vec![st1, st2], DegradationRule::Total);
1307 assert_eq!(pd.n_intact(), 2);
1308 }
1309
1310 #[test]
1311 fn test_progressive_damage_not_all_failed_initially() {
1312 let ply1 = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1313 let pd = ProgressiveDamage::new(
1314 vec![ply1],
1315 vec![PlyStrength::carbon_epoxy_t300()],
1316 DegradationRule::Total,
1317 );
1318 assert!(!pd.all_failed());
1319 }
1320
1321 #[test]
1322 fn test_progressive_damage_a_matrix_nonzero() {
1323 let ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1324 let pd = ProgressiveDamage::new(
1325 vec![ply],
1326 vec![PlyStrength::carbon_epoxy_t300()],
1327 DegradationRule::Total,
1328 );
1329 let a = pd.a_matrix();
1330 assert!(a[0] > 0.0);
1331 }
1332
1333 #[test]
1334 fn test_progressive_failure_sweep_returns_finite() {
1335 let ply = PlyState::new(0.0, 0.125e-3, 130.0e9, 10.0e9, 0.28, 5.0e9);
1336 let mut pd = ProgressiveDamage::new(
1337 vec![ply],
1338 vec![PlyStrength::carbon_epoxy_t300()],
1339 DegradationRule::Total,
1340 );
1341 let lf = pd.progressive_failure_sweep([1.0e7, 0.0, 0.0], 100);
1342 assert!(lf.is_finite() || lf == f64::INFINITY);
1343 }
1344
1345 #[test]
1348 fn test_delam_no_failure_low_stress() {
1349 let dc = DelamCriterion::carbon_epoxy_default();
1350 let s = InterlaminaStress::new(10.0e6, 20.0e6, 20.0e6);
1351 assert!(!dc.stress_delamination(s));
1352 }
1353
1354 #[test]
1355 fn test_delam_failure_above_z_t() {
1356 let dc = DelamCriterion::carbon_epoxy_default();
1357 let s = InterlaminaStress::new(60.0e6, 0.0, 0.0); assert!(dc.stress_delamination(s));
1359 }
1360
1361 #[test]
1362 fn test_delam_compression_no_mode1_contribution() {
1363 let dc = DelamCriterion::carbon_epoxy_default();
1364 let s = InterlaminaStress::new(-100.0e6, 0.0, 0.0); assert!(!dc.stress_delamination(s));
1366 }
1367
1368 #[test]
1369 fn test_delam_linear_err_failure() {
1370 let dc = DelamCriterion::carbon_epoxy_default();
1371 let err = EnergyReleaseRate::new(200.0, 400.0, 0.0); assert!(dc.err_delamination(err));
1373 }
1374
1375 #[test]
1376 fn test_delam_linear_err_safe() {
1377 let dc = DelamCriterion::carbon_epoxy_default();
1378 let err = EnergyReleaseRate::new(50.0, 50.0, 0.0);
1379 assert!(!dc.err_delamination(err));
1380 }
1381
1382 #[test]
1383 fn test_bk_criterion_at_critical() {
1384 let dc = DelamCriterion::carbon_epoxy_default();
1385 let err = EnergyReleaseRate::new(200.0, 0.0, 0.0);
1387 let bk = dc.bk_criterion(err);
1388 assert!((bk - 1.0).abs() < 1e-6);
1389 }
1390
1391 #[test]
1392 fn test_bk_delamination_above_critical() {
1393 let dc = DelamCriterion::carbon_epoxy_default();
1394 let err = EnergyReleaseRate::new(300.0, 0.0, 0.0);
1395 assert!(dc.bk_delamination(err));
1396 }
1397
1398 #[test]
1399 fn test_bk_no_delamination_below_critical() {
1400 let dc = DelamCriterion::carbon_epoxy_default();
1401 let err = EnergyReleaseRate::new(50.0, 50.0, 0.0);
1402 assert!(!dc.bk_delamination(err));
1403 }
1404
1405 #[test]
1406 fn test_err_mode_i_ratio() {
1407 let err = EnergyReleaseRate::new(100.0, 50.0, 50.0);
1408 let r = err.mode_i_ratio();
1409 assert!((r - 0.5).abs() < 1e-10);
1410 }
1411
1412 #[test]
1413 fn test_err_total() {
1414 let err = EnergyReleaseRate::new(100.0, 200.0, 50.0);
1415 assert!((err.total() - 350.0).abs() < 1e-10);
1416 }
1417
1418 #[test]
1419 fn test_stress_intensity_mode1() {
1420 let dc = DelamCriterion::carbon_epoxy_default();
1421 let k1 = dc.stress_intensity_mode1(1.0e6, 1.0e-3 / PI);
1422 assert!((k1 - 1.0e6 / 1000.0_f64.sqrt()).abs() < 1.0);
1423 }
1424
1425 #[test]
1426 fn test_g1c_from_k1c() {
1427 let g = DelamCriterion::g1c_from_k1c(1000.0, 5.0e9);
1428 assert!((g - 1000.0 * 1000.0 / 5.0e9).abs() < 1e-10);
1429 }
1430
1431 #[test]
1432 fn test_paris_growth_rate_positive() {
1433 let dc = DelamCriterion::carbon_epoxy_default();
1434 let rate = dc.paris_growth_rate(100.0, 200.0, 1e-10, 3.0);
1435 assert!(rate > 0.0);
1436 }
1437
1438 #[test]
1441 fn test_transformed_q_identity_at_zero_angle() {
1442 let q = [130.0e9_f64, 10.0e9_f64, 3.0e9_f64, 5.0e9_f64];
1443 let qbar = transformed_q(&q, 0.0);
1444 assert!((qbar[0] - q[0]).abs() < 1.0, "Q11 not preserved at 0°");
1445 assert!((qbar[1] - q[1]).abs() < 1.0, "Q22 not preserved at 0°");
1446 }
1447
1448 #[test]
1449 fn test_transformed_q_90deg_swaps_11_22() {
1450 let q = [130.0e9_f64, 10.0e9_f64, 3.0e9_f64, 5.0e9_f64];
1451 let qbar0 = transformed_q(&q, 0.0);
1452 let qbar90 = transformed_q(&q, PI / 2.0);
1453 assert!(
1454 (qbar90[0] - qbar0[1]).abs() < 1.0,
1455 "Q̄11(90°) should equal Q̄22(0°)"
1456 );
1457 }
1458
1459 #[test]
1460 fn test_reserve_factor_positive() {
1461 assert!((reserve_factor(0.5).unwrap() - 2.0).abs() < 1e-10);
1462 }
1463
1464 #[test]
1465 fn test_reserve_factor_none_at_zero() {
1466 assert!(reserve_factor(0.0).is_none());
1467 }
1468
1469 #[test]
1470 fn test_tsai_hill_envelope_nonempty() {
1471 let st = PlyStrength::carbon_epoxy_t300();
1472 let pts = tsai_hill_envelope(&st, 10);
1473 assert!(!pts.is_empty());
1474 }
1475}