carapace_core/model/materials/
composite.rs1use super::Material;
2
3impl Material {
4 pub fn parallel(materials: Vec<Material>) -> Self {
8 Material::Parallel(materials.into_iter().map(|m| (m, 1.0)).collect())
9 }
10
11 pub fn series(children: Vec<Material>) -> Self {
17 let n = children.len();
18 Material::Series {
19 children,
20 child_strains: vec![0.0; n],
21 stress: 0.0,
22 max_iter: 30,
23 tol: 1e-10,
24 }
25 }
26
27 pub fn min_max(inner: Material, min_strain: f64, max_strain: f64) -> Self {
28 Material::MinMax {
29 inner: Box::new(inner),
30 min_strain,
31 max_strain,
32 failed: false,
33 }
34 }
35}
36
37pub(super) fn evaluate_parallel(children: &[(Material, f64)], strain: f64) -> (f64, f64, Material) {
38 let mut stress = 0.0;
39 let mut tangent = 0.0;
40 let mut next_children = Vec::with_capacity(children.len());
41 for (child, factor) in children {
42 let (s, t, next) = child.evaluate(strain);
43 stress += factor * s;
44 tangent += factor * t;
45 next_children.push((next, *factor));
46 }
47 (stress, tangent, Material::Parallel(next_children))
48}
49
50pub(super) fn evaluate_min_max(
51 inner: &Material,
52 min_strain: f64,
53 max_strain: f64,
54 failed: bool,
55 strain: f64,
56) -> (f64, f64, Material) {
57 if failed || strain <= min_strain || strain >= max_strain {
58 let tangent = 1.0e-8 * inner.initial_tangent();
59 (
60 0.0,
61 tangent,
62 Material::MinMax {
63 inner: Box::new(inner.clone()),
64 min_strain,
65 max_strain,
66 failed: true,
67 },
68 )
69 } else {
70 let (s, t, next_inner) = inner.evaluate(strain);
71 (
72 s,
73 t,
74 Material::MinMax {
75 inner: Box::new(next_inner),
76 min_strain,
77 max_strain,
78 failed: false,
79 },
80 )
81 }
82}
83
84pub(super) fn evaluate_series(m: &Material, total_strain: f64) -> (f64, f64, Material) {
92 let Material::Series {
93 children,
94 child_strains,
95 stress: cstress,
96 max_iter,
97 tol,
98 } = m
99 else {
100 unreachable!()
101 };
102
103 let n = children.len();
104 let mut e = child_strains.clone();
105 let mut child_stress = vec![0.0; n];
106 let mut flex = vec![0.0; n];
107 let mut tstress = *cstress;
108 let mut series_tangent = 0.0;
109
110 let safe_inv = |t: f64| {
111 if t.abs() > 1.0e-12 {
112 1.0 / t
113 } else if t < 0.0 {
114 -1.0e12
115 } else {
116 1.0e12
117 }
118 };
119
120 for _ in 0..*max_iter {
121 let mut f = 0.0;
122 let mut vr = 0.0;
123 for i in 0..n {
124 let ds = tstress - child_stress[i];
125 e[i] += flex[i] * ds;
126 let (s, t) = children[i].trial_stress_tangent(e[i]);
127 child_stress[i] = s;
128 flex[i] = safe_inv(t);
129 let ds = tstress - child_stress[i];
130 let de = flex[i] * ds;
131 f += flex[i];
132 vr += e[i] + de;
133 }
134 series_tangent = safe_inv(f);
135 let dv = total_strain - vr;
136 let dq = series_tangent * dv;
137 tstress += dq;
138 if (dq * dv).abs() < *tol {
139 break;
140 }
141 }
142
143 let next_children = children
144 .iter()
145 .zip(e.iter())
146 .map(|(child, &strain)| child.evaluate(strain).2)
147 .collect();
148
149 (
150 tstress,
151 series_tangent,
152 Material::Series {
153 children: next_children,
154 child_strains: e,
155 stress: tstress,
156 max_iter: *max_iter,
157 tol: *tol,
158 },
159 )
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn parallel_of_two_elastics_equals_one_elastic_with_summed_stiffness() {
168 let combined = Material::parallel(vec![
169 Material::Elastic { e: 100.0 },
170 Material::Elastic { e: 50.0 },
171 ]);
172 let single = Material::Elastic { e: 150.0 };
173 assert_eq!(
174 combined.trial_stress_tangent(0.01),
175 single.trial_stress_tangent(0.01)
176 );
177 }
178
179 #[test]
180 fn parallel_history_is_independent_per_child() {
181 let combined = Material::parallel(vec![
184 Material::elastic_pp(100.0, 0.01),
185 Material::elastic_pp(50.0, 0.02),
186 ]);
187 let (stress, _tangent) = combined.trial_stress_tangent(0.05);
188 assert!((stress - 2.0).abs() < 1e-9, "got {stress}");
190 }
191
192 #[test]
193 fn series_of_two_equal_elastics_halves_the_stiffness() {
194 let series = Material::series(vec![
195 Material::Elastic { e: 100.0 },
196 Material::Elastic { e: 100.0 },
197 ]);
198 let (stress, tangent) = series.trial_stress_tangent(0.02);
199 assert!(
203 (tangent - 50.0).abs() < 1e-6,
204 "expected tangent 50, got {tangent}"
205 );
206 assert!((stress - 50.0 * 0.02).abs() < 1e-6, "got {stress}");
207 }
208
209 #[test]
210 fn series_splits_strain_so_each_child_sees_equal_stress() {
211 let series = Material::series(vec![
212 Material::Elastic { e: 100.0 },
213 Material::Elastic { e: 300.0 },
214 ]);
215 let Material::Series { child_strains, .. } = series.commit(0.04) else {
216 panic!()
217 };
218 assert!(
221 (child_strains[0] - 0.03).abs() < 1e-6,
222 "got {child_strains:?}"
223 );
224 assert!(
225 (child_strains[1] - 0.01).abs() < 1e-6,
226 "got {child_strains:?}"
227 );
228 }
229
230 #[test]
231 fn min_max_fails_permanently_once_strain_exits_bounds() {
232 let m = Material::min_max(Material::Elastic { e: 100.0 }, -0.01, 0.01);
233 let m = m.commit(0.02); let Material::MinMax { failed, .. } = m else {
235 panic!()
236 };
237 assert!(failed);
238
239 let (stress, tangent) = m.trial_stress_tangent(0.0); assert_eq!(
241 stress, 0.0,
242 "failed material must read zero stress even if strain re-enters bounds"
243 );
244 assert!((tangent - 1.0e-8 * 100.0).abs() < 1e-12, "got {tangent}");
245 }
246
247 #[test]
248 fn min_max_passes_through_inner_material_before_failure() {
249 let m = Material::min_max(Material::Elastic { e: 100.0 }, -0.01, 0.01);
250 assert_eq!(m.trial_stress_tangent(0.005), (0.5, 100.0));
251 }
252}