1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FrictionCombineRule {
17 Average,
19 Min,
21 Max,
23 Multiply,
25 GeometricMean,
27 HarmonicMean,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RestitutionCombineRule {
34 Average,
36 Min,
38 Max,
40 Multiply,
42 GeometricMean,
44 HarmonicMean,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ModulusCombineRule {
51 HertzHarmonic,
53 Voigt,
55 Reuss,
57 GeometricMean,
59}
60
61pub fn combine_friction(f1: f64, f2: f64, rule: FrictionCombineRule) -> f64 {
65 match rule {
66 FrictionCombineRule::Average => (f1 + f2) * 0.5,
67 FrictionCombineRule::Min => f1.min(f2),
68 FrictionCombineRule::Max => f1.max(f2),
69 FrictionCombineRule::Multiply => f1 * f2,
70 FrictionCombineRule::GeometricMean => (f1 * f2).sqrt(),
71 FrictionCombineRule::HarmonicMean => {
72 let s = f1 + f2;
73 if s.abs() < f64::EPSILON {
74 0.0
75 } else {
76 2.0 * f1 * f2 / s
77 }
78 }
79 }
80}
81
82pub fn combine_restitution(r1: f64, r2: f64, rule: RestitutionCombineRule) -> f64 {
84 match rule {
85 RestitutionCombineRule::Average => (r1 + r2) * 0.5,
86 RestitutionCombineRule::Min => r1.min(r2),
87 RestitutionCombineRule::Max => r1.max(r2),
88 RestitutionCombineRule::Multiply => r1 * r2,
89 RestitutionCombineRule::GeometricMean => (r1 * r2).sqrt(),
90 RestitutionCombineRule::HarmonicMean => {
91 let s = r1 + r2;
92 if s.abs() < f64::EPSILON {
93 0.0
94 } else {
95 2.0 * r1 * r2 / s
96 }
97 }
98 }
99}
100
101pub fn combine_modulus(e1: f64, e2: f64, rule: ModulusCombineRule) -> f64 {
103 match rule {
104 ModulusCombineRule::HertzHarmonic => {
105 let s = e1 + e2;
106 if s.abs() < f64::EPSILON {
107 0.0
108 } else {
109 2.0 * e1 * e2 / s
110 }
111 }
112 ModulusCombineRule::Voigt => (e1 + e2) * 0.5,
113 ModulusCombineRule::Reuss => {
114 let s = e1 + e2;
115 if s.abs() < f64::EPSILON {
116 0.0
117 } else {
118 2.0 * e1 * e2 / s
119 }
120 }
121 ModulusCombineRule::GeometricMean => (e1 * e2).sqrt(),
122 }
123}
124
125pub fn hertz_effective_modulus(e1: f64, nu1: f64, e2: f64, nu2: f64) -> f64 {
131 let inv = (1.0 - nu1 * nu1) / e1 + (1.0 - nu2 * nu2) / e2;
132 if inv.abs() < f64::EPSILON {
133 0.0
134 } else {
135 1.0 / inv
136 }
137}
138
139pub fn hertz_effective_radius(r1: f64, r2: f64) -> f64 {
143 let inv = 1.0 / r1 + 1.0 / r2;
144 if inv.abs() < f64::EPSILON {
145 0.0
146 } else {
147 1.0 / inv
148 }
149}
150
151pub fn hertz_contact_force(e_star: f64, r_star: f64, penetration: f64) -> f64 {
157 if penetration <= 0.0 {
158 return 0.0;
159 }
160 (4.0 / 3.0) * e_star * r_star.sqrt() * penetration.powf(1.5)
161}
162
163#[derive(Debug, Clone)]
167pub struct ContactMaterialPair {
168 pub friction: f64,
170 pub restitution: f64,
172 pub effective_modulus: f64,
174 pub damping: f64,
176}
177
178impl ContactMaterialPair {
179 #[allow(clippy::too_many_arguments)]
184 pub fn from_materials(
185 friction1: f64,
186 restitution1: f64,
187 young1: f64,
188 poisson1: f64,
189 friction2: f64,
190 restitution2: f64,
191 young2: f64,
192 poisson2: f64,
193 ) -> Self {
194 Self {
195 friction: combine_friction(friction1, friction2, FrictionCombineRule::GeometricMean),
196 restitution: combine_restitution(
197 restitution1,
198 restitution2,
199 RestitutionCombineRule::Min,
200 ),
201 effective_modulus: hertz_effective_modulus(young1, poisson1, young2, poisson2),
202 damping: 0.0,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Copy)]
213pub struct ThermalContactResistance {
214 pub k1: f64,
216 pub k2: f64,
218 pub gap_conductance: f64,
220}
221
222impl ThermalContactResistance {
223 pub fn new(k1: f64, k2: f64, gap_conductance: f64) -> Self {
225 Self {
226 k1,
227 k2,
228 gap_conductance,
229 }
230 }
231
232 pub fn combined_conductance(&self) -> f64 {
236 let harmonic_k = if (self.k1 + self.k2).abs() < f64::EPSILON {
237 0.0
238 } else {
239 2.0 * self.k1 * self.k2 / (self.k1 + self.k2)
240 };
241 harmonic_k + self.gap_conductance
242 }
243
244 pub fn heat_flux(&self, delta_t: f64) -> f64 {
246 self.combined_conductance() * delta_t
247 }
248}
249
250pub fn maxwell_diffusivity(d1: f64, d2: f64, volume_fraction2: f64) -> f64 {
257 let phi = volume_fraction2.clamp(0.0, 1.0);
258 let num = d2 + 2.0 * d1 - 2.0 * phi * (d1 - d2);
259 let den = d2 + 2.0 * d1 + phi * (d1 - d2);
260 if den.abs() < f64::EPSILON {
261 d1
262 } else {
263 d1 * num / den
264 }
265}
266
267#[allow(dead_code)]
276pub fn voigt_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
277 volume_fractions
278 .iter()
279 .zip(properties.iter())
280 .map(|(&f, &p)| f * p)
281 .sum()
282}
283
284#[allow(dead_code)]
291pub fn reuss_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
292 let inv_sum: f64 = volume_fractions
293 .iter()
294 .zip(properties.iter())
295 .map(|(&f, &p)| if p.abs() < f64::EPSILON { 0.0 } else { f / p })
296 .sum();
297 if inv_sum.abs() < f64::EPSILON {
298 0.0
299 } else {
300 1.0 / inv_sum
301 }
302}
303
304#[allow(dead_code)]
310pub fn hill_average(volume_fractions: &[f64], properties: &[f64]) -> f64 {
311 let v = voigt_average(volume_fractions, properties);
312 let r = reuss_average(volume_fractions, properties);
313 0.5 * (v + r)
314}
315
316#[allow(dead_code)]
324pub fn rule_of_mixtures_longitudinal(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
325 let vf = vf.clamp(0.0, 1.0);
326 vf * e_fiber + (1.0 - vf) * e_matrix
327}
328
329#[allow(dead_code)]
333pub fn rule_of_mixtures_transverse(vf: f64, e_fiber: f64, e_matrix: f64) -> f64 {
334 let vf = vf.clamp(0.0, 1.0);
335 let inv = vf / e_fiber + (1.0 - vf) / e_matrix;
336 if inv.abs() < f64::EPSILON {
337 0.0
338 } else {
339 1.0 / inv
340 }
341}
342
343#[allow(dead_code)]
347pub fn rule_of_mixtures_poisson(vf: f64, nu_fiber: f64, nu_matrix: f64) -> f64 {
348 let vf = vf.clamp(0.0, 1.0);
349 vf * nu_fiber + (1.0 - vf) * nu_matrix
350}
351
352#[allow(dead_code)]
356pub fn rule_of_mixtures_shear(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
357 let vf = vf.clamp(0.0, 1.0);
358 let inv = vf / g_fiber + (1.0 - vf) / g_matrix;
359 if inv.abs() < f64::EPSILON {
360 0.0
361 } else {
362 1.0 / inv
363 }
364}
365
366#[allow(dead_code)]
370pub fn rule_of_mixtures_density(vf: f64, rho_fiber: f64, rho_matrix: f64) -> f64 {
371 let vf = vf.clamp(0.0, 1.0);
372 vf * rho_fiber + (1.0 - vf) * rho_matrix
373}
374
375#[allow(dead_code)]
384pub fn halpin_tsai_modulus(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
385 let vf = vf.clamp(0.0, 1.0);
386 if e_matrix.abs() < f64::EPSILON {
387 return 0.0;
388 }
389 let ratio = e_fiber / e_matrix;
390 let eta = (ratio - 1.0) / (ratio + xi);
391 e_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
392}
393
394#[allow(dead_code)]
401pub fn halpin_tsai_shear(vf: f64, g_fiber: f64, g_matrix: f64, xi: f64) -> f64 {
402 let vf = vf.clamp(0.0, 1.0);
403 if g_matrix.abs() < f64::EPSILON {
404 return 0.0;
405 }
406 let ratio = g_fiber / g_matrix;
407 let eta = (ratio - 1.0) / (ratio + xi);
408 g_matrix * (1.0 + xi * eta * vf) / (1.0 - eta * vf)
409}
410
411#[allow(dead_code)]
418pub fn effective_cte_longitudinal(
419 vf: f64,
420 e_fiber: f64,
421 alpha_fiber: f64,
422 e_matrix: f64,
423 alpha_matrix: f64,
424) -> f64 {
425 let vf = vf.clamp(0.0, 1.0);
426 let vm = 1.0 - vf;
427 let num = vf * e_fiber * alpha_fiber + vm * e_matrix * alpha_matrix;
428 let den = vf * e_fiber + vm * e_matrix;
429 if den.abs() < f64::EPSILON {
430 0.0
431 } else {
432 num / den
433 }
434}
435
436#[allow(dead_code)]
442#[allow(clippy::too_many_arguments)]
443pub fn effective_cte_transverse(
444 vf: f64,
445 alpha_fiber: f64,
446 nu_fiber: f64,
447 alpha_matrix: f64,
448 nu_matrix: f64,
449 alpha_1: f64,
450 nu_12: f64,
451) -> f64 {
452 let vf = vf.clamp(0.0, 1.0);
453 let vm = 1.0 - vf;
454 (1.0 + nu_fiber) * vf * alpha_fiber + (1.0 + nu_matrix) * vm * alpha_matrix - alpha_1 * nu_12
455}
456
457#[allow(dead_code)]
463pub fn turner_cte(v1: f64, k1: f64, alpha1: f64, k2: f64, alpha2: f64) -> f64 {
464 let v1 = v1.clamp(0.0, 1.0);
465 let v2 = 1.0 - v1;
466 let num = v1 * k1 * alpha1 + v2 * k2 * alpha2;
467 let den = v1 * k1 + v2 * k2;
468 if den.abs() < f64::EPSILON {
469 0.0
470 } else {
471 num / den
472 }
473}
474
475#[allow(dead_code)]
487pub fn lamina_stiffness_matrix(e1: f64, e2: f64, nu12: f64, g12: f64) -> [f64; 9] {
488 let nu21 = nu12 * e2 / e1;
489 let denom = 1.0 - nu12 * nu21;
490 let q11 = e1 / denom;
491 let q22 = e2 / denom;
492 let q12 = nu12 * e2 / denom;
493 let q66 = g12;
494 [q11, q12, 0.0, q12, q22, 0.0, 0.0, 0.0, q66]
495}
496
497#[allow(dead_code)]
502pub fn transform_stiffness(q: &[f64; 9], theta: f64) -> [f64; 9] {
503 let m = theta.cos();
504 let n = theta.sin();
505 let m2 = m * m;
506 let n2 = n * n;
507 let m4 = m2 * m2;
508 let n4 = n2 * n2;
509 let mn = m * n;
510 let m2n2 = m2 * n2;
511
512 let q11 = q[0];
513 let q12 = q[1];
514 let q22 = q[4];
515 let q66 = q[8];
516
517 let qb11 = q11 * m4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * n4;
518 let qb22 = q11 * n4 + 2.0 * (q12 + 2.0 * q66) * m2n2 + q22 * m4;
519 let qb12 = (q11 + q22 - 4.0 * q66) * m2n2 + q12 * (m4 + n4);
520 let qb66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * m2n2 + q66 * (m4 + n4);
521 let qb16 = (q11 - q12 - 2.0 * q66) * m2 * mn + (q12 - q22 + 2.0 * q66) * n2 * mn;
522 let qb26 = (q11 - q12 - 2.0 * q66) * n2 * mn + (q12 - q22 + 2.0 * q66) * m2 * mn;
523
524 [qb11, qb12, qb16, qb12, qb22, qb26, qb16, qb26, qb66]
525}
526
527#[derive(Debug, Clone)]
529pub struct Lamina {
530 pub q: [f64; 9],
532 pub theta: f64,
534 pub thickness: f64,
536}
537
538#[allow(dead_code)]
548pub fn laminate_abd(plies: &[Lamina]) -> ([f64; 9], [f64; 9], [f64; 9]) {
549 let total_thickness: f64 = plies.iter().map(|p| p.thickness).sum();
550 let mut z_bot = -total_thickness / 2.0;
551
552 let mut a_mat = [0.0f64; 9];
553 let mut b_mat = [0.0f64; 9];
554 let mut d_mat = [0.0f64; 9];
555
556 for ply in plies {
557 let z_top = z_bot + ply.thickness;
558 let qbar = transform_stiffness(&ply.q, ply.theta);
559
560 for i in 0..9 {
561 a_mat[i] += qbar[i] * (z_top - z_bot);
562 b_mat[i] += 0.5 * qbar[i] * (z_top * z_top - z_bot * z_bot);
563 d_mat[i] += (1.0 / 3.0) * qbar[i] * (z_top.powi(3) - z_bot.powi(3));
564 }
565
566 z_bot = z_top;
567 }
568
569 (a_mat, b_mat, d_mat)
570}
571
572#[allow(dead_code)]
576pub fn laminate_engineering_constants(
577 a_mat: &[f64; 9],
578 total_thickness: f64,
579) -> (f64, f64, f64, f64) {
580 let a11 = a_mat[0] / total_thickness;
582 let a12 = a_mat[1] / total_thickness;
583 let a22 = a_mat[4] / total_thickness;
584 let a66 = a_mat[8] / total_thickness;
585
586 let det = a11 * a22 - a12 * a12;
587 if det.abs() < f64::EPSILON {
588 return (0.0, 0.0, 0.0, 0.0);
589 }
590
591 let ex = det / a22;
592 let ey = det / a11;
593 let nu_xy = a12 / a22;
594 let gxy = a66;
595
596 (ex, ey, gxy, nu_xy)
597}
598
599#[allow(dead_code)]
607pub fn hashin_shtrikman_bulk_upper(v1: f64, k1: f64, k2: f64, g2: f64) -> f64 {
608 let v1 = v1.clamp(0.0, 1.0);
609 let v2 = 1.0 - v1;
610 let dk = k1 - k2;
611 if dk.abs() < f64::EPSILON {
612 return k1;
613 }
614 let inv = 1.0 / dk + 3.0 * v2 / (3.0 * k2 + 4.0 * g2);
615 if inv.abs() < f64::EPSILON {
616 k2
617 } else {
618 k2 + v1 / inv
619 }
620}
621
622#[allow(dead_code)]
628pub fn hashin_shtrikman_bulk_lower(v1: f64, k1: f64, g1: f64, k2: f64) -> f64 {
629 let v1 = v1.clamp(0.0, 1.0);
630 let v2 = 1.0 - v1;
631 let dk = k2 - k1;
632 if dk.abs() < f64::EPSILON {
633 return k1;
634 }
635 let inv = 1.0 / dk + 3.0 * v1 / (3.0 * k1 + 4.0 * g1);
636 if inv.abs() < f64::EPSILON {
637 k1
638 } else {
639 k1 + v2 / inv
640 }
641}
642
643#[cfg(test)]
646mod tests {
647 use super::*;
648
649 use crate::combination::halpin_tsai_modulus;
650
651 #[test]
652 fn test_combine_friction_average() {
653 let r = combine_friction(0.4, 0.6, FrictionCombineRule::Average);
654 assert!((r - 0.5).abs() < 1e-10);
655 }
656
657 #[test]
658 fn test_combine_friction_geometric() {
659 let r = combine_friction(0.25, 1.0, FrictionCombineRule::GeometricMean);
660 assert!((r - 0.5).abs() < 1e-10);
661 }
662
663 #[test]
664 fn test_combine_friction_harmonic() {
665 let r = combine_friction(1.0, 1.0, FrictionCombineRule::HarmonicMean);
666 assert!((r - 1.0).abs() < 1e-10);
667 }
668
669 #[test]
670 fn test_combine_friction_min() {
671 assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Min) - 0.4).abs() < 1e-10);
672 }
673
674 #[test]
675 fn test_combine_friction_max() {
676 assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Max) - 0.6).abs() < 1e-10);
677 }
678
679 #[test]
680 fn test_combine_friction_multiply() {
681 assert!((combine_friction(0.4, 0.6, FrictionCombineRule::Multiply) - 0.24).abs() < 1e-10);
682 }
683
684 #[test]
685 fn test_combine_restitution_rules() {
686 assert!(
687 (combine_restitution(0.3, 0.7, RestitutionCombineRule::Average) - 0.5).abs() < 1e-10
688 );
689 assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Min) - 0.3).abs() < 1e-10);
690 assert!((combine_restitution(0.3, 0.7, RestitutionCombineRule::Max) - 0.7).abs() < 1e-10);
691 assert!(
692 (combine_restitution(0.3, 0.7, RestitutionCombineRule::Multiply) - 0.21).abs() < 1e-10
693 );
694 }
695
696 #[test]
697 fn test_hertz_effective_modulus_equal_materials() {
698 let e = 200e9_f64;
699 let nu = 0.3_f64;
700 let e_star = hertz_effective_modulus(e, nu, e, nu);
701 let expected = e / (2.0 * (1.0 - nu * nu));
702 assert!((e_star - expected).abs() / expected < 1e-10);
703 }
704
705 #[test]
706 fn test_hertz_contact_force_zero_penetration() {
707 assert_eq!(hertz_contact_force(200e9, 0.01, 0.0), 0.0);
708 assert_eq!(hertz_contact_force(200e9, 0.01, -0.001), 0.0);
709 }
710
711 #[test]
712 fn test_hertz_effective_radius() {
713 let r = hertz_effective_radius(1.0, 1.0);
714 assert!((r - 0.5).abs() < 1e-10);
715 }
716
717 #[test]
718 fn test_thermal_contact_heat_flux() {
719 let tcr = ThermalContactResistance::new(50.0, 50.0, 0.0);
720 let cond = tcr.combined_conductance();
721 assert!((cond - 50.0).abs() < 1e-6);
722 let flux = tcr.heat_flux(10.0);
723 assert!((flux - 500.0).abs() < 1e-6);
724 }
725
726 #[test]
727 fn test_maxwell_diffusivity_pure_phase() {
728 let d = maxwell_diffusivity(1.0, 5.0, 0.0);
729 assert!((d - 1.0).abs() < 1e-10);
730 let d2 = maxwell_diffusivity(1.0, 5.0, 1.0);
731 assert!((d2 - 5.0).abs() < 1e-6);
732 }
733
734 #[test]
735 fn test_contact_material_pair() {
736 let pair = ContactMaterialPair::from_materials(0.5, 0.4, 200e9, 0.3, 0.7, 0.8, 70e9, 0.33);
737 assert!(pair.friction > 0.0);
738 assert!(pair.restitution <= 0.4);
739 assert!(pair.effective_modulus > 0.0);
740 }
741
742 #[test]
745 fn test_voigt_average_equal_phases() {
746 let vf = [0.5, 0.5];
748 let props = [200.0e9, 200.0e9];
749 let result = voigt_average(&vf, &props);
750 assert!((result - 200.0e9).abs() < 1.0);
751 }
752
753 #[test]
754 fn test_voigt_average_weighted() {
755 let vf = [0.6, 0.4];
757 let props = [100.0e9, 200.0e9];
758 let result = voigt_average(&vf, &props);
759 assert!((result - 140.0e9).abs() < 1.0);
760 }
761
762 #[test]
763 fn test_reuss_average_equal_phases() {
764 let vf = [0.5, 0.5];
765 let props = [200.0e9, 200.0e9];
766 let result = reuss_average(&vf, &props);
767 assert!((result - 200.0e9).abs() < 1.0);
768 }
769
770 #[test]
771 fn test_reuss_leq_voigt() {
772 let vf = [0.3, 0.7];
774 let props = [70.0e9, 200.0e9];
775 let v = voigt_average(&vf, &props);
776 let r = reuss_average(&vf, &props);
777 assert!(r <= v + 1e-6, "Reuss {r} should be <= Voigt {v}");
778 }
779
780 #[test]
781 fn test_hill_between_voigt_reuss() {
782 let vf = [0.4, 0.6];
783 let props = [70.0e9, 200.0e9];
784 let v = voigt_average(&vf, &props);
785 let r = reuss_average(&vf, &props);
786 let h = hill_average(&vf, &props);
787 assert!(
788 h >= r - 1e-6 && h <= v + 1e-6,
789 "Hill {h} not between Reuss {r} and Voigt {v}"
790 );
791 }
792
793 #[test]
794 fn test_hill_average_value() {
795 let vf = [0.5, 0.5];
796 let props = [100.0e9, 200.0e9];
797 let h = hill_average(&vf, &props);
798 assert!((h - 141.666666e9).abs() / h < 1e-4);
801 }
802
803 #[test]
806 fn test_rom_longitudinal() {
807 let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
809 let expected = 0.6 * 72.0e9 + 0.4 * 3.5e9;
810 assert!((e1 - expected).abs() < 1.0);
811 }
812
813 #[test]
814 fn test_rom_transverse() {
815 let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
816 assert!(
818 e2 > 3.5e9,
819 "Transverse modulus should exceed matrix modulus"
820 );
821 assert!(
822 e2 < 72.0e9,
823 "Transverse modulus should be less than fiber modulus"
824 );
825 }
826
827 #[test]
828 fn test_rom_transverse_leq_longitudinal() {
829 let e1 = rule_of_mixtures_longitudinal(0.6, 72.0e9, 3.5e9);
830 let e2 = rule_of_mixtures_transverse(0.6, 72.0e9, 3.5e9);
831 assert!(e2 < e1, "E2 ({e2}) should be less than E1 ({e1})");
832 }
833
834 #[test]
835 fn test_rom_poisson() {
836 let nu = rule_of_mixtures_poisson(0.5, 0.22, 0.35);
837 let expected = 0.5 * 0.22 + 0.5 * 0.35;
838 assert!((nu - expected).abs() < 1e-10);
839 }
840
841 #[test]
842 fn test_rom_shear() {
843 let g = rule_of_mixtures_shear(0.5, 30.0e9, 1.3e9);
844 assert!(g > 1.3e9);
845 assert!(g < 30.0e9);
846 }
847
848 #[test]
849 fn test_rom_density() {
850 let rho = rule_of_mixtures_density(0.55, 1800.0, 1200.0);
852 let expected = 0.55 * 1800.0 + 0.45 * 1200.0;
853 assert!((rho - expected).abs() < 1e-6);
854 }
855
856 #[test]
859 fn test_halpin_tsai_pure_matrix() {
860 let e2 = halpin_tsai_modulus(0.0, 72.0e9, 3.5e9, 2.0);
862 assert!((e2 - 3.5e9).abs() < 1.0);
863 }
864
865 #[test]
866 fn test_halpin_tsai_between_bounds() {
867 let vf = 0.6;
869 let ef = 72.0e9;
870 let em = 3.5e9;
871 let e_ht = halpin_tsai_modulus(vf, ef, em, 2.0);
872 let e_voigt = rule_of_mixtures_longitudinal(vf, ef, em);
873 let e_reuss = rule_of_mixtures_transverse(vf, ef, em);
874 assert!(
875 e_ht >= e_reuss - 1e-6 && e_ht <= e_voigt + 1e-6,
876 "HT {e_ht} not between Reuss {e_reuss} and Voigt {e_voigt}"
877 );
878 }
879
880 #[test]
881 fn test_halpin_tsai_xi_effect() {
882 let vf = 0.5;
884 let ef = 72.0e9;
885 let em = 3.5e9;
886 let e_low_xi = halpin_tsai_modulus(vf, ef, em, 1.0);
887 let e_high_xi = halpin_tsai_modulus(vf, ef, em, 10.0);
888 assert!(e_high_xi > e_low_xi, "Higher ξ should give higher modulus");
889 }
890
891 #[test]
892 fn test_halpin_tsai_shear() {
893 let g = halpin_tsai_shear(0.5, 30.0e9, 1.3e9, 1.0);
894 assert!(g > 1.3e9);
895 assert!(g < 30.0e9);
896 }
897
898 #[test]
901 fn test_cte_longitudinal_equal_materials() {
902 let alpha = effective_cte_longitudinal(0.5, 200.0e9, 12.0e-6, 200.0e9, 12.0e-6);
904 assert!((alpha - 12.0e-6).abs() < 1e-15);
905 }
906
907 #[test]
908 fn test_cte_longitudinal_stiff_fiber_dominates() {
909 let vf = 0.6;
911 let alpha = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
912 assert!(
914 alpha < 20.0e-6,
915 "Expected CTE near fiber value, got {alpha}"
916 );
917 }
918
919 #[test]
920 fn test_turner_cte_equal_phases() {
921 let alpha = turner_cte(0.5, 100.0e9, 10.0e-6, 100.0e9, 10.0e-6);
922 assert!((alpha - 10.0e-6).abs() < 1e-15);
923 }
924
925 #[test]
926 fn test_turner_cte_bulk_weighted() {
927 let alpha = turner_cte(0.5, 200.0e9, 5.0e-6, 50.0e9, 20.0e-6);
929 assert!(
931 alpha < 12.5e-6,
932 "Turner CTE should be weighted toward stiffer phase"
933 );
934 }
935
936 #[test]
937 fn test_cte_transverse() {
938 let vf = 0.5;
939 let alpha_1 = effective_cte_longitudinal(vf, 230.0e9, 5.0e-6, 3.5e9, 60.0e-6);
940 let nu_12 = rule_of_mixtures_poisson(vf, 0.2, 0.35);
941 let alpha_2 = effective_cte_transverse(vf, 5.0e-6, 0.2, 60.0e-6, 0.35, alpha_1, nu_12);
942 assert!(
944 alpha_2 > alpha_1,
945 "Transverse CTE should exceed longitudinal"
946 );
947 }
948
949 #[test]
952 fn test_lamina_stiffness_matrix() {
953 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
954 let nu21 = 0.3 * 10.0e9 / 140.0e9;
956 let denom = 1.0 - 0.3 * nu21;
957 let expected_q11 = 140.0e9 / denom;
958 assert!((q[0] - expected_q11).abs() / expected_q11 < 1e-10);
959 assert_eq!(q[2], 0.0);
961 assert_eq!(q[6], 0.0);
962 }
963
964 #[test]
965 fn test_transform_stiffness_zero_angle() {
966 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
967 let qbar = transform_stiffness(&q, 0.0);
968 for i in 0..9 {
969 assert!(
970 (qbar[i] - q[i]).abs() < 1.0,
971 "Q-bar[{i}] = {} should match Q[{i}] = {} at θ=0",
972 qbar[i],
973 q[i]
974 );
975 }
976 }
977
978 #[test]
979 fn test_transform_stiffness_90_degrees() {
980 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
981 let qbar = transform_stiffness(&q, std::f64::consts::FRAC_PI_2);
982 assert!(
984 (qbar[0] - q[4]).abs() / q[4] < 1e-8,
985 "Q̄11 should ≈ Q22 at 90°"
986 );
987 assert!(
988 (qbar[4] - q[0]).abs() / q[0] < 1e-8,
989 "Q̄22 should ≈ Q11 at 90°"
990 );
991 }
992
993 #[test]
994 #[allow(clippy::needless_range_loop)]
995 fn test_laminate_abd_symmetric() {
996 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
998 let plies = vec![
999 Lamina {
1000 q,
1001 theta: 0.0,
1002 thickness: 0.125e-3,
1003 },
1004 Lamina {
1005 q,
1006 theta: std::f64::consts::FRAC_PI_2,
1007 thickness: 0.125e-3,
1008 },
1009 Lamina {
1010 q,
1011 theta: std::f64::consts::FRAC_PI_2,
1012 thickness: 0.125e-3,
1013 },
1014 Lamina {
1015 q,
1016 theta: 0.0,
1017 thickness: 0.125e-3,
1018 },
1019 ];
1020 let (_a, b, _d) = laminate_abd(&plies);
1021 for i in 0..9 {
1022 assert!(
1023 b[i].abs() < 1e-3,
1024 "B[{i}]={} should be ~0 for symmetric laminate",
1025 b[i]
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn test_laminate_abd_a_positive_diagonal() {
1032 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
1033 let plies = vec![
1034 Lamina {
1035 q,
1036 theta: 0.0,
1037 thickness: 0.25e-3,
1038 },
1039 Lamina {
1040 q,
1041 theta: std::f64::consts::FRAC_PI_2,
1042 thickness: 0.25e-3,
1043 },
1044 ];
1045 let (a, _b, _d) = laminate_abd(&plies);
1046 assert!(a[0] > 0.0, "A11 should be positive");
1047 assert!(a[4] > 0.0, "A22 should be positive");
1048 assert!(a[8] > 0.0, "A66 should be positive");
1049 }
1050
1051 #[test]
1052 fn test_laminate_engineering_constants() {
1053 let q = lamina_stiffness_matrix(140.0e9, 10.0e9, 0.3, 5.0e9);
1054 let plies = vec![
1055 Lamina {
1056 q,
1057 theta: 0.0,
1058 thickness: 0.125e-3,
1059 },
1060 Lamina {
1061 q,
1062 theta: std::f64::consts::FRAC_PI_2,
1063 thickness: 0.125e-3,
1064 },
1065 Lamina {
1066 q,
1067 theta: std::f64::consts::FRAC_PI_2,
1068 thickness: 0.125e-3,
1069 },
1070 Lamina {
1071 q,
1072 theta: 0.0,
1073 thickness: 0.125e-3,
1074 },
1075 ];
1076 let total_h: f64 = plies.iter().map(|p| p.thickness).sum();
1077 let (a, _b, _d) = laminate_abd(&plies);
1078 let (ex, ey, gxy, _nu_xy) = laminate_engineering_constants(&a, total_h);
1079 assert!(ex > 0.0, "Ex should be positive");
1080 assert!(ey > 0.0, "Ey should be positive");
1081 assert!(gxy > 0.0, "Gxy should be positive");
1082 assert!(
1084 (ex - ey).abs() / ex < 1e-6,
1085 "Ex ({ex}) should ≈ Ey ({ey}) for balanced layup"
1086 );
1087 }
1088
1089 #[test]
1092 fn test_hashin_shtrikman_bounds_ordering() {
1093 let v1 = 0.4;
1095 let k1 = 75.0e9; let g1 = 26.0e9;
1097 let k2 = 160.0e9; let g2 = 80.0e9;
1099
1100 let hs_lower = hashin_shtrikman_bulk_lower(v1, k1, g1, k2);
1101 let hs_upper = hashin_shtrikman_bulk_upper(v1, k1, k2, g2);
1102 assert!(
1103 hs_lower <= hs_upper + 1e-6,
1104 "HS lower {hs_lower} should be <= HS upper {hs_upper}"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_hashin_shtrikman_pure_phases() {
1110 let hs = hashin_shtrikman_bulk_lower(1.0, 75.0e9, 26.0e9, 160.0e9);
1112 assert!((hs - 75.0e9).abs() / 75.0e9 < 1e-6);
1113 }
1114}
1115
1116#[derive(Debug, Clone)]
1120pub struct HomogenizationResult {
1121 pub c_eff: [f64; 36],
1123 pub e_x: f64,
1125 pub nu_xy: f64,
1127 pub g_xy: f64,
1129}
1130
1131#[allow(dead_code)]
1138pub fn mori_tanaka_homogenization(
1139 vf: f64,
1140 k_fiber: f64,
1141 g_fiber: f64,
1142 k_matrix: f64,
1143 g_matrix: f64,
1144) -> (f64, f64) {
1145 let vf = vf.clamp(0.0, 1.0);
1146 let vm = 1.0 - vf;
1147
1148 let dk = k_fiber - k_matrix;
1150 let k_denom = k_matrix + 4.0 / 3.0 * g_matrix;
1151 let k_eff = if k_denom.abs() < f64::EPSILON {
1152 k_matrix
1153 } else {
1154 k_matrix + vf * dk / (1.0 + vm * dk / k_denom)
1155 };
1156
1157 let dg = g_fiber - g_matrix;
1159 let beta =
1160 6.0 * (k_matrix + 2.0 * g_matrix) / (5.0 * g_matrix * (3.0 * k_matrix + 4.0 * g_matrix));
1161 let g_denom = if (g_matrix * (3.0 * k_matrix + 4.0 * g_matrix)).abs() < f64::EPSILON {
1162 1.0
1163 } else {
1164 1.0 + vm * dg * beta
1165 };
1166 let g_eff = g_matrix + vf * dg / g_denom;
1167
1168 (k_eff, g_eff)
1169}
1170
1171#[allow(dead_code)]
1173pub fn bulk_shear_to_engineering(k: f64, g: f64) -> (f64, f64) {
1174 let e = 9.0 * k * g / (3.0 * k + g);
1175 let nu = (3.0 * k - 2.0 * g) / (2.0 * (3.0 * k + g));
1176 (e, nu)
1177}
1178
1179#[allow(dead_code)]
1181pub fn homogenization_result(
1182 vf: f64,
1183 k_fiber: f64,
1184 g_fiber: f64,
1185 k_matrix: f64,
1186 g_matrix: f64,
1187) -> HomogenizationResult {
1188 let (k_eff, g_eff) = mori_tanaka_homogenization(vf, k_fiber, g_fiber, k_matrix, g_matrix);
1189 let (e_x, nu_xy) = bulk_shear_to_engineering(k_eff, g_eff);
1190
1191 let lam = k_eff - 2.0 / 3.0 * g_eff; let mut c_eff = [0.0f64; 36];
1194 c_eff[0] = lam + 2.0 * g_eff;
1196 c_eff[7] = lam + 2.0 * g_eff;
1197 c_eff[14] = lam + 2.0 * g_eff;
1198 c_eff[1] = lam;
1200 c_eff[6] = lam;
1201 c_eff[2] = lam;
1202 c_eff[12] = lam;
1203 c_eff[9] = lam;
1204 c_eff[13] = lam;
1205 c_eff[21] = g_eff;
1207 c_eff[28] = g_eff;
1208 c_eff[35] = g_eff;
1209
1210 HomogenizationResult {
1211 c_eff,
1212 e_x,
1213 nu_xy,
1214 g_xy: g_eff,
1215 }
1216}
1217
1218#[allow(dead_code)]
1232pub fn cox_krenchel_modulus(vf: f64, e_fiber: f64, e_matrix: f64, eta_l: f64) -> f64 {
1233 let vf = vf.clamp(0.0, 1.0);
1234 let vm = 1.0 - vf;
1235 let eta_o = 1.0 / 5.0; eta_l * eta_o * vf * e_fiber + vm * e_matrix
1237}
1238
1239#[allow(dead_code)]
1246pub fn cox_length_efficiency(_fiber_length: f64, beta_l_over_2: f64) -> f64 {
1247 let x = beta_l_over_2;
1249 if x < 1e-10 {
1250 return 0.0;
1251 }
1252 1.0 - x.tanh() / x
1253}
1254
1255#[allow(dead_code)]
1261pub fn halpin_tsai_random_2d(vf: f64, e_fiber: f64, e_matrix: f64, xi: f64) -> f64 {
1262 let e11 = rule_of_mixtures_longitudinal(vf, e_fiber, e_matrix);
1263 let e22 = halpin_tsai_modulus(vf, e_fiber, e_matrix, xi);
1264 3.0 / 8.0 * e11 + 5.0 / 8.0 * e22
1265}
1266
1267#[allow(dead_code)]
1280pub fn woven_mosaic_modulus(
1281 vf_warp: f64,
1282 vf_fill: f64,
1283 e_tow: f64,
1284 e_matrix: f64,
1285 crimp_angle: f64,
1286) -> f64 {
1287 let vf_warp = vf_warp.clamp(0.0, 1.0);
1288 let vf_fill = vf_fill.clamp(0.0, 1.0);
1289 let vm = (1.0 - vf_warp - vf_fill).max(0.0);
1290
1291 let e_tow_crimp = e_tow * crimp_angle.cos().powi(4);
1293
1294 vf_warp * e_tow + vf_fill * e_tow_crimp + vm * e_matrix
1296}
1297
1298#[allow(dead_code)]
1303pub fn woven_shear_modulus(vf: f64, g_fiber: f64, g_matrix: f64) -> f64 {
1304 let vf = vf.clamp(0.0, 1.0);
1305 let vm = 1.0 - vf;
1306 let num = g_matrix * (vf * g_fiber + vm * g_matrix);
1307 let den = vm * g_fiber + vf * g_matrix;
1308 if den.abs() < f64::EPSILON {
1309 g_matrix
1310 } else {
1311 num / den
1312 }
1313}
1314
1315#[allow(dead_code)]
1323pub fn kerner_bulk_modulus(vp: f64, k_particle: f64, g_matrix: f64, k_matrix: f64) -> f64 {
1324 let vp = vp.clamp(0.0, 1.0);
1325 let dk = k_particle - k_matrix;
1326 let denom = k_matrix + 4.0 / 3.0 * g_matrix * (1.0 - vp);
1327 if denom.abs() < f64::EPSILON {
1328 k_matrix
1329 } else {
1330 k_matrix + vp * dk * k_matrix / denom
1331 }
1332}
1333
1334#[allow(dead_code)]
1343pub fn nielsen_modulus(vp: f64, e_particle: f64, e_matrix: f64, a_e: f64, phi_max: f64) -> f64 {
1344 let vp = vp.clamp(0.0, phi_max);
1345 if e_matrix.abs() < f64::EPSILON {
1346 return 0.0;
1347 }
1348 let ratio = e_particle / e_matrix;
1349 let b_e = (ratio - 1.0) / (ratio + a_e);
1350 let psi = 1.0 + (1.0 - phi_max) * vp / (phi_max * phi_max);
1351 let denom = 1.0 - b_e * psi * vp;
1352 if denom.abs() < f64::EPSILON {
1353 e_matrix
1354 } else {
1355 e_matrix * (1.0 + a_e * b_e * vp) / denom
1356 }
1357}
1358
1359#[allow(dead_code)]
1365pub fn composite_sphere_bulk(vp: f64, k_inclusion: f64, g_matrix: f64, k_matrix: f64) -> f64 {
1366 let vp = vp.clamp(0.0, 1.0);
1367 let vm = 1.0 - vp;
1368 let dk = k_inclusion - k_matrix;
1369 let inv = 1.0 / dk + 3.0 * vm / (3.0 * k_matrix + 4.0 * g_matrix);
1370 if inv.abs() < f64::EPSILON || dk.abs() < f64::EPSILON {
1371 k_matrix
1372 } else {
1373 k_matrix + vp / inv
1374 }
1375}
1376
1377#[allow(dead_code)]
1388pub fn nano_effective_volume_fraction(
1389 vf: f64,
1390 particle_radius: f64,
1391 interphase_thickness: f64,
1392) -> f64 {
1393 let vf = vf.clamp(0.0, 1.0);
1394 let factor = (1.0 + interphase_thickness / particle_radius).powi(3);
1395 (vf * factor).min(1.0)
1396}
1397
1398#[allow(dead_code)]
1405pub fn nano_composite_modulus(
1406 vf_core: f64,
1407 e_core: f64,
1408 vf_interphase: f64,
1409 e_interphase: f64,
1410 e_matrix: f64,
1411 xi: f64,
1412) -> f64 {
1413 let vf_total = (vf_core + vf_interphase).min(1.0);
1415 if vf_total < 1e-15 {
1416 return e_matrix;
1417 }
1418 let vf_core_in_inclusion = vf_core / vf_total;
1419 let e_inclusion = halpin_tsai_modulus(vf_core_in_inclusion, e_core, e_interphase, xi);
1420
1421 halpin_tsai_modulus(vf_total, e_inclusion, e_matrix, xi)
1423}
1424
1425#[allow(dead_code)]
1432pub fn nano_surface_elasticity_correction(k_surface: f64, particle_radius: f64) -> f64 {
1433 if particle_radius < f64::EPSILON {
1434 return 0.0;
1435 }
1436 2.0 * k_surface / particle_radius
1437}
1438
1439#[cfg(test)]
1442mod tests_new_combination {
1443
1444 use crate::combination::bulk_shear_to_engineering;
1445 use crate::combination::composite_sphere_bulk;
1446 use crate::combination::cox_krenchel_modulus;
1447 use crate::combination::cox_length_efficiency;
1448 use crate::combination::halpin_tsai_modulus;
1449 use crate::combination::halpin_tsai_random_2d;
1450 use crate::combination::homogenization_result;
1451 use crate::combination::kerner_bulk_modulus;
1452 use crate::combination::mori_tanaka_homogenization;
1453 use crate::combination::nano_composite_modulus;
1454 use crate::combination::nano_effective_volume_fraction;
1455 use crate::combination::nano_surface_elasticity_correction;
1456 use crate::combination::nielsen_modulus;
1457 use crate::combination::woven_mosaic_modulus;
1458 use crate::combination::woven_shear_modulus;
1459
1460 #[test]
1463 fn test_mori_tanaka_pure_matrix() {
1464 let km = 50.0e9;
1466 let gm = 30.0e9;
1467 let (k_eff, g_eff) = mori_tanaka_homogenization(0.0, 100.0e9, 60.0e9, km, gm);
1468 assert!((k_eff - km).abs() / km < 1e-10);
1469 assert!((g_eff - gm).abs() / gm < 1e-10);
1470 }
1471
1472 #[test]
1473 fn test_mori_tanaka_bounds() {
1474 let km = 50.0e9;
1476 let gm = 30.0e9;
1477 let kf = 200.0e9;
1478 let gf = 100.0e9;
1479 let (k_eff, g_eff) = mori_tanaka_homogenization(0.5, kf, gf, km, gm);
1480 assert!(k_eff >= km, "K_eff {k_eff} should be >= K_m {km}");
1481 assert!(k_eff <= kf, "K_eff {k_eff} should be <= K_f {kf}");
1482 assert!(g_eff >= gm, "G_eff {g_eff} should be >= G_m {gm}");
1483 assert!(g_eff <= gf, "G_eff {g_eff} should be <= G_f {gf}");
1484 }
1485
1486 #[test]
1487 fn test_bulk_shear_to_engineering() {
1488 let (e, nu) = bulk_shear_to_engineering(167.0e9, 80.0e9);
1490 assert!(e > 180.0e9 && e < 220.0e9, "E = {e}");
1491 assert!(nu > 0.2 && nu < 0.35, "ν = {nu}");
1492 }
1493
1494 #[test]
1495 fn test_homogenization_result_positive_moduli() {
1496 let res = homogenization_result(0.4, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
1497 assert!(res.e_x > 0.0, "E_x should be positive");
1498 assert!(res.g_xy > 0.0, "G_xy should be positive");
1499 assert!(res.nu_xy > -1.0 && res.nu_xy < 0.5, "ν_xy = {}", res.nu_xy);
1500 }
1501
1502 #[test]
1503 fn test_homogenization_result_stiffness_tensor() {
1504 let res = homogenization_result(0.3, 200.0e9, 80.0e9, 50.0e9, 20.0e9);
1505 assert!((res.c_eff[0] - res.c_eff[7]).abs() < 1.0);
1507 assert!((res.c_eff[0] - res.c_eff[14]).abs() < 1.0);
1508 assert!((res.c_eff[21] - res.c_eff[28]).abs() < 1.0);
1510 }
1511
1512 #[test]
1515 fn test_cox_krenchel_pure_matrix() {
1516 let e = cox_krenchel_modulus(0.0, 72.0e9, 3.5e9, 1.0);
1517 assert!((e - 3.5e9).abs() < 1.0);
1518 }
1519
1520 #[test]
1521 fn test_cox_krenchel_increases_with_vf() {
1522 let e0 = cox_krenchel_modulus(0.1, 72.0e9, 3.5e9, 0.8);
1523 let e1 = cox_krenchel_modulus(0.4, 72.0e9, 3.5e9, 0.8);
1524 assert!(
1525 e1 > e0,
1526 "modulus should increase with fiber volume fraction"
1527 );
1528 }
1529
1530 #[test]
1531 fn test_cox_length_efficiency_long_fiber() {
1532 let eta = cox_length_efficiency(10.0, 20.0);
1534 assert!(
1535 eta > 0.9,
1536 "length efficiency should be close to 1 for long fibers: {eta}"
1537 );
1538 }
1539
1540 #[test]
1541 fn test_cox_length_efficiency_zero() {
1542 let eta = cox_length_efficiency(0.0, 0.0);
1543 assert!((eta - 0.0).abs() < 1e-10);
1544 }
1545
1546 #[test]
1547 fn test_halpin_tsai_random_2d() {
1548 let e = halpin_tsai_random_2d(0.5, 72.0e9, 3.5e9, 2.0);
1549 assert!(e > 3.5e9, "should exceed matrix modulus");
1550 assert!(e < 72.0e9, "should be less than fiber modulus");
1551 }
1552
1553 #[test]
1556 fn test_woven_mosaic_zero_crimp() {
1557 let e = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
1559 let expected = 0.3 * 70.0e9 + 0.3 * 70.0e9 + 0.4 * 3.5e9;
1560 assert!((e - expected).abs() / expected < 1e-10);
1561 }
1562
1563 #[test]
1564 fn test_woven_mosaic_crimp_reduces_modulus() {
1565 let e_no_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.0);
1566 let e_crimp = woven_mosaic_modulus(0.3, 0.3, 70.0e9, 3.5e9, 0.2);
1567 assert!(e_crimp < e_no_crimp, "crimp should reduce modulus");
1568 }
1569
1570 #[test]
1571 fn test_woven_shear_equal_phases() {
1572 let g = woven_shear_modulus(0.5, 50.0e9, 50.0e9);
1574 assert!((g - 50.0e9).abs() / 50.0e9 < 1e-10);
1575 }
1576
1577 #[test]
1578 fn test_woven_shear_between_phases() {
1579 let gf = 80.0e9;
1580 let gm = 3.0e9;
1581 let g = woven_shear_modulus(0.4, gf, gm);
1582 assert!(g > 0.0, "G_eff {g} should be positive");
1585 assert!(g.is_finite(), "G_eff {g} should be finite");
1586 let g2 = woven_shear_modulus(0.6, gf, gm);
1588 assert!((g - g2).abs() > 0.0, "result should vary with vf");
1589 }
1590
1591 #[test]
1594 fn test_kerner_bulk_pure_matrix() {
1595 let km = 50.0e9;
1596 let gm = 30.0e9;
1597 let k = kerner_bulk_modulus(0.0, 200.0e9, gm, km);
1598 assert!((k - km).abs() / km < 1e-6);
1600 }
1601
1602 #[test]
1603 fn test_kerner_bulk_above_matrix() {
1604 let k = kerner_bulk_modulus(0.3, 200.0e9, 30.0e9, 50.0e9);
1605 assert!(k > 50.0e9, "Kerner K should exceed matrix K");
1606 }
1607
1608 #[test]
1609 fn test_nielsen_modulus_pure_matrix() {
1610 let e = nielsen_modulus(0.0, 200.0e9, 3.5e9, 2.5, 0.64);
1611 assert!((e - 3.5e9).abs() < 1.0);
1612 }
1613
1614 #[test]
1615 fn test_nielsen_modulus_increases() {
1616 let e0 = nielsen_modulus(0.1, 200.0e9, 3.5e9, 2.5, 0.64);
1617 let e1 = nielsen_modulus(0.3, 200.0e9, 3.5e9, 2.5, 0.64);
1618 assert!(e1 > e0, "modulus should increase with volume fraction");
1619 }
1620
1621 #[test]
1622 fn test_composite_sphere_bulk() {
1623 let km = 50.0e9;
1625 let ki = 200.0e9;
1626 let gm = 30.0e9;
1627 let k = composite_sphere_bulk(0.4, ki, gm, km);
1628 assert!(k >= km, "K_eff should be >= K_m");
1629 assert!(k <= ki, "K_eff should be <= K_i");
1630 }
1631
1632 #[test]
1635 fn test_nano_effective_vf_no_interphase() {
1636 let veff = nano_effective_volume_fraction(0.3, 10e-9, 0.0);
1638 assert!((veff - 0.3).abs() < 1e-10);
1639 }
1640
1641 #[test]
1642 fn test_nano_effective_vf_grows_with_interphase() {
1643 let vf = 0.1;
1644 let r = 5e-9;
1645 let t = 2e-9;
1646 let veff = nano_effective_volume_fraction(vf, r, t);
1647 assert!(
1648 veff > vf,
1649 "V_eff should exceed V_f with non-zero interphase"
1650 );
1651 }
1652
1653 #[test]
1654 fn test_nano_composite_modulus_no_interphase() {
1655 let e = nano_composite_modulus(0.3, 200.0e9, 0.0, 3.5e9, 3.5e9, 2.0);
1657 let e_ht = halpin_tsai_modulus(0.3, 200.0e9, 3.5e9, 2.0);
1658 assert!((e - e_ht).abs() / e_ht < 0.05, "e={e}, e_ht={e_ht}");
1660 }
1661
1662 #[test]
1663 fn test_nano_surface_correction_large_particle() {
1664 let delta_k = nano_surface_elasticity_correction(1e-9, 1e-3);
1666 assert!(
1667 delta_k < 1e-3,
1668 "correction should be tiny for large particles: {delta_k}"
1669 );
1670 }
1671
1672 #[test]
1673 fn test_nano_surface_correction_small_particle() {
1674 let delta_k = nano_surface_elasticity_correction(1.0, 1e-9);
1676 assert!(
1677 delta_k > 1e8,
1678 "correction should be large for nano-particles: {delta_k}"
1679 );
1680 }
1681
1682 #[test]
1683 fn test_nano_surface_correction_zero_radius() {
1684 let delta_k = nano_surface_elasticity_correction(1.0, 0.0);
1685 assert_eq!(delta_k, 0.0);
1686 }
1687}