1use nalgebra::DVector;
2
3use super::AnalysisError;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum ConvergenceTest {
15 NormUnbalance { tol: f64, max_iter: usize },
18 NormDispIncr { tol: f64, max_iter: usize },
22 EnergyIncr { tol: f64, max_iter: usize },
29 Combined {
46 force_tol: f64,
47 moment_tol: f64,
48 relative_tol: f64,
49 displacement_tol: Option<f64>,
50 max_iter: usize,
51 },
52}
53
54pub(crate) struct IterationContext<'a> {
59 pub(crate) residual: &'a DVector<f64>,
60 pub(crate) du: &'a DVector<f64>,
61 pub(crate) force_tolerance: Option<&'a ForceTolerance>,
62}
63
64impl ConvergenceTest {
65 pub(crate) fn max_iter(&self) -> usize {
66 match self {
67 ConvergenceTest::NormUnbalance { max_iter, .. }
68 | ConvergenceTest::NormDispIncr { max_iter, .. }
69 | ConvergenceTest::EnergyIncr { max_iter, .. }
70 | ConvergenceTest::Combined { max_iter, .. } => *max_iter,
71 }
72 }
73
74 pub fn validate(&self) -> Result<(), AnalysisError> {
79 let positive = |value: f64| value.is_finite() && value > 0.0;
80 let invalid = |field| Err(AnalysisError::InvalidOption { field });
81 match *self {
82 ConvergenceTest::NormUnbalance { tol, max_iter }
83 | ConvergenceTest::NormDispIncr { tol, max_iter }
84 | ConvergenceTest::EnergyIncr { tol, max_iter } => {
85 if !positive(tol) {
86 return invalid("convergence.tol");
87 }
88 if max_iter == 0 {
89 return invalid("convergence.maxIter");
90 }
91 }
92 ConvergenceTest::Combined {
93 force_tol,
94 moment_tol,
95 relative_tol,
96 displacement_tol,
97 max_iter,
98 } => {
99 if !positive(force_tol) {
100 return invalid("convergence.forceTol");
101 }
102 if !positive(moment_tol) {
103 return invalid("convergence.momentTol");
104 }
105 if !relative_tol.is_finite() || relative_tol < 0.0 {
106 return invalid("convergence.relativeTol");
107 }
108 if displacement_tol.is_some_and(|tol| !positive(tol)) {
109 return invalid("convergence.displacementTol");
110 }
111 if max_iter == 0 {
112 return invalid("convergence.maxIter");
113 }
114 }
115 }
116 Ok(())
117 }
118
119 pub(crate) fn accepts_predictor(&self) -> bool {
124 matches!(
125 self,
126 ConvergenceTest::NormUnbalance { .. } | ConvergenceTest::Combined { .. }
127 )
128 }
129
130 pub(crate) fn check(&self, ctx: &IterationContext<'_>) -> bool {
132 if !ctx.residual.iter().all(|value| value.is_finite())
133 || !ctx.du.iter().all(|value| value.is_finite())
134 {
135 return false;
136 }
137 match self {
138 ConvergenceTest::NormUnbalance { tol, .. } => ctx.residual.norm() < *tol,
139 ConvergenceTest::NormDispIncr { tol, .. } => ctx.du.norm() < *tol,
140 ConvergenceTest::EnergyIncr { tol, .. } => ctx.du.dot(ctx.residual).abs() < *tol,
141 ConvergenceTest::Combined {
142 displacement_tol, ..
143 } => {
144 let force_ok = ctx
145 .force_tolerance
146 .expect("Combined convergence always receives a force tolerance")
147 .scaled_max(ctx.residual)
148 <= 1.0;
149 force_ok && displacement_tol.is_none_or(|tol| ctx.du.amax() <= tol)
150 }
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
161pub(crate) struct ForceTolerance {
162 thresholds: DVector<f64>,
163}
164
165impl ForceTolerance {
166 pub(crate) fn combined(
170 force_tol: f64,
171 moment_tol: f64,
172 relative_tol: f64,
173 rotational: &[bool],
174 magnitudes: &[&DVector<f64>],
175 ) -> Self {
176 let thresholds = DVector::from_fn(rotational.len(), |i, _| {
177 let reference = magnitudes
178 .iter()
179 .map(|vector| vector[i].abs())
180 .fold(0.0, f64::max);
181 let absolute = if rotational[i] { moment_tol } else { force_tol };
182 absolute + relative_tol * reference
183 });
184 Self { thresholds }
185 }
186
187 pub(crate) fn uniform(n: usize, tol: f64) -> Self {
191 Self {
192 thresholds: DVector::from_element(n, tol),
193 }
194 }
195
196 pub(crate) fn for_test(
199 test: &ConvergenceTest,
200 rotational: &[bool],
201 magnitudes: &[&DVector<f64>],
202 ) -> Option<Self> {
203 match *test {
204 ConvergenceTest::Combined {
205 force_tol,
206 moment_tol,
207 relative_tol,
208 ..
209 } => Some(Self::combined(
210 force_tol,
211 moment_tol,
212 relative_tol,
213 rotational,
214 magnitudes,
215 )),
216 _ => None,
217 }
218 }
219
220 pub(crate) fn scaled_max(&self, residual: &DVector<f64>) -> f64 {
222 residual
223 .iter()
224 .zip(self.thresholds.iter())
225 .map(|(r, t)| r.abs() / t)
226 .fold(0.0, f64::max)
227 }
228
229 pub(crate) fn scaled_norm_squared(&self, residual: &DVector<f64>) -> f64 {
231 residual
232 .iter()
233 .zip(self.thresholds.iter())
234 .map(|(r, t)| (r / t).powi(2))
235 .sum()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn combined(displacement_tol: Option<f64>) -> ConvergenceTest {
244 ConvergenceTest::Combined {
245 force_tol: 1e-3,
246 moment_tol: 1e-1,
247 relative_tol: 1e-6,
248 displacement_tol,
249 max_iter: 10,
250 }
251 }
252
253 #[test]
254 fn combined_uses_moment_tolerance_on_rotational_equations_only() {
255 let load = DVector::from_column_slice(&[1000.0, 0.0]);
256 let tol = ForceTolerance::combined(1e-3, 1e-1, 1e-6, &[false, true], &[&load]);
257 let test = combined(None);
258 let du = DVector::zeros(2);
259 let pass = DVector::from_column_slice(&[1.9e-3, 0.09]);
261 let fail_force = DVector::from_column_slice(&[2.1e-3, 0.0]);
262 let fail_moment = DVector::from_column_slice(&[0.0, 0.11]);
263 let ctx = |r| IterationContext {
264 residual: r,
265 du: &du,
266 force_tolerance: Some(&tol),
267 };
268 assert!(test.check(&ctx(&pass)));
269 assert!(!test.check(&ctx(&fail_force)));
270 assert!(!test.check(&ctx(&fail_moment)));
271 }
272
273 #[test]
274 fn small_correction_never_hides_large_residual_under_combined() {
275 let tol = ForceTolerance::combined(1e-6, 1e-6, 0.0, &[false], &[]);
276 let test = combined(Some(1.0));
277 let residual = DVector::from_column_slice(&[10.0]);
278 let du = DVector::from_column_slice(&[1e-12]);
279 assert!(!test.check(&IterationContext {
280 residual: &residual,
281 du: &du,
282 force_tolerance: Some(&tol),
283 }));
284 }
285
286 #[test]
287 fn nonfinite_state_never_converges() {
288 let test = ConvergenceTest::NormDispIncr {
289 tol: 1.0,
290 max_iter: 1,
291 };
292 let residual = DVector::from_column_slice(&[f64::NAN]);
293 let du = DVector::from_column_slice(&[0.0]);
294 assert!(!test.check(&IterationContext {
295 residual: &residual,
296 du: &du,
297 force_tolerance: None,
298 }));
299 }
300
301 #[test]
302 fn validation_rejects_bad_combined_options() {
303 assert!(combined(None).validate().is_ok());
304 let bad = ConvergenceTest::Combined {
305 force_tol: 0.0,
306 moment_tol: 1.0,
307 relative_tol: 0.0,
308 displacement_tol: None,
309 max_iter: 1,
310 };
311 assert_eq!(
312 bad.validate(),
313 Err(AnalysisError::InvalidOption {
314 field: "convergence.forceTol"
315 })
316 );
317 }
318}