1use nalgebra::DVector;
20use slotmap::Key;
21
22use crate::model::{Domain, ElementOps, NodeId, SparseMatrix};
23
24use super::bordered::{Border, BorderedFactorization, BorderedSolver};
25use super::convergence::ForceTolerance;
26use super::{
27 Algorithm, Analysis, AnalysisError, ArcFailure, ConvergenceTest, StepResult, TangentStrategy,
28};
29
30#[derive(Debug, Clone, PartialEq)]
34pub struct ArcLength<NId = NodeId> {
35 pub initial_radius: f64,
37 pub min_radius: f64,
39 pub max_radius: f64,
42 pub target_iterations: usize,
45 pub max_retries: usize,
47 pub direction: ArcDirection,
49 pub scales: ArcScales,
51 pub predictor: ArcPredictor,
53 pub seed: Option<ArcSeed<NId>>,
56 pub arc_tolerance: f64,
58 pub correction_tolerance: Option<f64>,
61 pub backtracking: Backtracking,
62 pub stop: ArcStopCriteria<NId>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum ArcDirection {
68 #[default]
69 Increasing,
70 Decreasing,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum ArcScales {
77 Explicit {
81 displacement: f64,
82 rotation: Option<f64>,
83 load: f64,
84 },
85 Auto { load: f64 },
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95pub enum ArcPredictor {
96 #[default]
100 Secant,
101 Tangent,
103}
104
105#[derive(Debug, Clone, PartialEq)]
109pub struct ArcSeed<NId = NodeId> {
110 pub components: Vec<(NId, usize, f64)>,
111 pub load: f64,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq)]
116pub struct Backtracking {
117 pub armijo: f64,
119 pub min_step: f64,
121}
122
123impl Default for Backtracking {
124 fn default() -> Self {
125 Self {
126 armijo: 1e-4,
127 min_step: 1.0 / 128.0,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Default)]
136pub struct ArcStopCriteria<NId = NodeId> {
137 pub displacement: Option<DisplacementTarget<NId>>,
138 pub load_factor: Option<LoadFactorTarget>,
139 pub load_factor_zero_crossing: bool,
141 pub max_chord_length: Option<f64>,
142 pub max_steps: Option<usize>,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq)]
149pub struct DisplacementTarget<NId = NodeId> {
150 pub node: NId,
151 pub dof: usize,
152 pub value: f64,
153 pub exact: bool,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct LoadFactorTarget {
160 pub value: f64,
161 pub exact: bool,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq)]
166pub struct ArcStepInfo {
167 pub radius: f64,
169 pub next_radius: f64,
171 pub retries: usize,
173 pub corrector_solves: usize,
175 pub factorizations: usize,
177 pub force_measure: f64,
180 pub arc_error: f64,
182 pub chord_length: f64,
184 pub det_sign: Option<i8>,
187 pub bifurcation_suspected: bool,
190 pub displacement_scale: f64,
192 pub rotation_scale: Option<f64>,
194 pub stop: Option<ArcStop>,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq)]
200pub struct ArcStop {
201 pub reason: StopReason,
202 pub landed_exactly: bool,
204 pub overshoot: f64,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum StopReason {
210 DisplacementTarget,
211 LoadFactorTarget,
212 LoadFactorZeroCrossing,
213 ChordLength,
214 StepCount,
215}
216
217impl<NId> ArcLength<NId> {
218 pub fn fixed(radius: f64, scales: ArcScales) -> Self {
220 Self::adaptive(radius, radius, radius, scales)
221 }
222
223 pub fn adaptive(
226 initial_radius: f64,
227 min_radius: f64,
228 max_radius: f64,
229 scales: ArcScales,
230 ) -> Self {
231 Self {
232 initial_radius,
233 min_radius,
234 max_radius,
235 target_iterations: 6,
236 max_retries: 8,
237 direction: ArcDirection::Increasing,
238 scales,
239 predictor: ArcPredictor::Secant,
240 seed: None,
241 arc_tolerance: 1e-8,
242 correction_tolerance: None,
243 backtracking: Backtracking::default(),
244 stop: ArcStopCriteria {
245 displacement: None,
246 load_factor: None,
247 load_factor_zero_crossing: false,
248 max_chord_length: None,
249 max_steps: None,
250 },
251 }
252 }
253
254 pub fn validate_with(
262 &self,
263 algorithm: &Algorithm,
264 test: &ConvergenceTest,
265 ) -> Result<(), AnalysisError> {
266 self.validate()?;
267 test.validate()?;
268 match algorithm {
269 Algorithm::Newton {
270 tangent: TangentStrategy::Current,
271 line_search: None,
272 } => {}
273 _ => return Err(AnalysisError::InvalidOption { field: "algorithm" }),
274 }
275 if !matches!(
276 test,
277 ConvergenceTest::NormUnbalance { .. } | ConvergenceTest::Combined { .. }
278 ) {
279 return Err(AnalysisError::InvalidOption {
280 field: "convergence",
281 });
282 }
283 Ok(())
284 }
285
286 pub fn validate(&self) -> Result<(), AnalysisError> {
288 let positive = |v: f64| v.is_finite() && v > 0.0;
289 let invalid = |field| Err(AnalysisError::InvalidOption { field });
290 if !positive(self.min_radius) {
291 return invalid("integrator.minRadius");
292 }
293 if !positive(self.initial_radius) || self.initial_radius < self.min_radius {
294 return invalid("integrator.initialRadius");
295 }
296 if !positive(self.max_radius) || self.max_radius < self.initial_radius {
297 return invalid("integrator.maxRadius");
298 }
299 if self.target_iterations == 0 {
300 return invalid("integrator.targetIterations");
301 }
302 if self.max_retries > 64 {
303 return invalid("integrator.maxRetries");
304 }
305 match self.scales {
306 ArcScales::Explicit {
307 displacement,
308 rotation,
309 load,
310 } => {
311 if !positive(displacement) {
312 return invalid("integrator.displacementScale");
313 }
314 if rotation.is_some_and(|r| !positive(r)) {
315 return invalid("integrator.rotationScale");
316 }
317 if !positive(load) {
318 return invalid("integrator.loadScale");
319 }
320 }
321 ArcScales::Auto { load } => {
322 if !positive(load) {
323 return invalid("integrator.loadScale");
324 }
325 }
326 }
327 if !positive(self.arc_tolerance) {
328 return invalid("integrator.arcTolerance");
329 }
330 if self.correction_tolerance.is_some_and(|t| !positive(t)) {
331 return invalid("integrator.correctionTolerance");
332 }
333 let Backtracking { armijo, min_step } = self.backtracking;
334 if !(armijo.is_finite() && armijo > 0.0 && armijo < 0.5) {
335 return invalid("integrator.backtracking.armijo");
336 }
337 if !(min_step.is_finite() && min_step > 0.0 && min_step <= 1.0) {
338 return invalid("integrator.backtracking.minStep");
339 }
340 if let Some(seed) = &self.seed {
341 if !seed.load.is_finite() || seed.components.iter().any(|c| !c.2.is_finite()) {
342 return invalid("integrator.seed");
343 }
344 }
345 let stop = &self.stop;
346 if stop
347 .displacement
348 .as_ref()
349 .is_some_and(|t| !t.value.is_finite())
350 {
351 return invalid("integrator.stop.displacement");
352 }
353 if stop.load_factor.is_some_and(|t| !t.value.is_finite()) {
354 return invalid("integrator.stop.loadFactor");
355 }
356 if stop.max_chord_length.is_some_and(|c| !positive(c)) {
357 return invalid("integrator.stop.maxChordLength");
358 }
359 if stop.max_steps == Some(0) {
360 return invalid("integrator.stop.maxSteps");
361 }
362 Ok(())
363 }
364
365 pub(crate) fn same_path_definition(&self, other: &Self) -> bool
370 where
371 NId: PartialEq,
372 {
373 self.scales == other.scales
374 && self.direction == other.direction
375 && self.seed == other.seed
376 && self.predictor == other.predictor
377 }
378}
379
380#[derive(Debug, Clone)]
383pub(crate) struct ArcState {
384 weights: DVector<f64>,
385 beta: f64,
386 unknown_scale: DVector<f64>,
389 displacement_scale: f64,
390 rotation_scale: Option<f64>,
391 rotational: Vec<bool>,
392 direction: Option<DVector<f64>>,
394 seed: Option<DVector<f64>>,
396 last_delta_lambda: f64,
397 next_radius: f64,
398 chord_length: f64,
399 accepted_steps: usize,
400 force_tangent_next: bool,
401 det_sign: Option<i8>,
402 stopped: bool,
403}
404
405struct Accepted {
407 k: SparseMatrix,
409 delta_lambda: f64,
410 corrector_solves: usize,
411 damped: bool,
412 force_measure: f64,
413 arc_error: f64,
414 det_sign: Option<i8>,
415 increment: DVector<f64>,
416}
417
418struct StepContext<'a> {
420 q_n: &'a DVector<f64>,
421 lambda_n: f64,
422 p: &'a DVector<f64>,
423 external_n: &'a DVector<f64>,
424 internal_n: &'a DVector<f64>,
425 predictor: &'a DVector<f64>,
426}
427
428impl ArcState {
429 pub(crate) fn reopen(&mut self) {
434 self.stopped = false;
435 self.accepted_steps = 0;
436 }
437
438 fn metric_dot(&self, a: &DVector<f64>, b: &DVector<f64>) -> f64 {
439 let n = self.weights.len();
440 let mut sum = self.beta * self.beta * a[n] * b[n];
441 for i in 0..n {
442 sum += self.weights[i] * a[i] * b[i];
443 }
444 sum
445 }
446
447 fn metric_norm(&self, a: &DVector<f64>) -> f64 {
448 self.metric_dot(a, a).sqrt()
449 }
450}
451
452pub(crate) fn integrator_change_invalidates<NId: PartialEq>(
455 old: &super::Integrator<NId>,
456 new: &super::Integrator<NId>,
457) -> bool {
458 match (old, new) {
459 (super::Integrator::ArcLength(a), super::Integrator::ArcLength(b)) => {
460 !a.same_path_definition(b)
461 }
462 _ => true,
463 }
464}
465
466fn join(q: &DVector<f64>, lambda: f64) -> DVector<f64> {
468 let n = q.len();
469 DVector::from_fn(n + 1, |i, _| if i < n { q[i] } else { lambda })
470}
471
472fn unit_last(n: usize) -> DVector<f64> {
473 DVector::from_fn(n + 1, |i, _| if i == n { 1.0 } else { 0.0 })
474}
475
476fn all_finite(v: &DVector<f64>) -> bool {
477 v.iter().all(|x| x.is_finite())
478}
479
480impl<const NDIM: usize, const NDOF: usize, NId, E> Analysis<NDIM, NDOF, NId, E>
481where
482 NId: Key,
483 E: ElementOps<NDIM, NDOF, NId> + Clone,
484 E::Load: Clone,
485{
486 pub(crate) fn arc_step(
490 &mut self,
491 config: &ArcLength<NId>,
492 ) -> Result<StepResult, AnalysisError> {
493 if self.arc.as_ref().is_some_and(|state| state.stopped) {
494 return Err(AnalysisError::ContinuationComplete);
495 }
496 config.validate_with(&self.algorithm, &self.test)?;
497
498 let snapshot = self.domain.clone();
499 let snapshot_arc = self.arc.clone();
500 let (snapshot_step_count, snapshot_load_factor) = (self.step_count, self.load_factor);
501
502 let result = self.try_arc_step(config);
503 match result {
504 Ok(step) => {
505 self.domain.commit(self.load_factor);
506 Ok(step)
507 }
508 Err(error) => {
509 self.domain = snapshot;
510 self.arc = snapshot_arc;
511 self.step_count = snapshot_step_count;
512 self.load_factor = snapshot_load_factor;
513 Err(error)
514 }
515 }
516 }
517
518 fn try_arc_step(&mut self, config: &ArcLength<NId>) -> Result<StepResult, AnalysisError> {
519 let lambda_n = self.load_factor;
520 let p = self.domain.assemble_reference_load_sensitivity(lambda_n);
521 let (k_n, internal_n) = self.domain.assemble_tangent_and_resistance(lambda_n);
522 let external_n = self.domain.assemble_reference_load(lambda_n);
523 let q_n = self.domain.gather_displacement();
524 let mut factorizations = 0;
525
526 if self.arc.is_none() {
527 let state = self.initialize_arc(
528 config,
529 &p,
530 &k_n,
531 &internal_n,
532 &external_n,
533 &mut factorizations,
534 )?;
535 self.arc = Some(Box::new(state));
536 }
537 self.step_count += 1;
538 let step = self.step_count;
539
540 let state = self.arc.as_ref().expect("initialized above").clone();
541 let n = q_n.len();
542
543 let tangent_wanted = state.direction.is_none()
546 || config.predictor == ArcPredictor::Tangent
547 || state.force_tangent_next;
548 let predictor = if tangent_wanted {
549 let t_prev = match (&state.direction, &state.seed) {
550 (Some(direction), _) => direction.clone(),
551 (None, Some(seed)) => seed.clone(),
552 (None, None) => {
553 let sign = match config.direction {
554 ArcDirection::Increasing => 1.0,
555 ArcDirection::Decreasing => -1.0,
556 };
557 unit_last(n) * (sign / state.beta)
558 }
559 };
560 factorizations += 1;
561 match bordered_tangent(&mut self.bordered, &state, &k_n, &p, &t_prev) {
562 Ok((t, _det_at_committed_state)) => t,
563 Err(failure) => match &state.direction {
564 Some(direction) => direction.clone(),
565 None if state.seed.is_none() && failure == ArcFailure::SingularSystem => {
566 return Err(AnalysisError::MissingSeedDirection)
567 }
568 None => {
569 return Err(AnalysisError::CutbacksExhausted {
570 step,
571 attempts: 0,
572 radius: state.next_radius,
573 last_failure: failure,
574 })
575 }
576 },
577 }
578 } else {
579 state
580 .direction
581 .clone()
582 .expect("secant needs a previous step")
583 };
584
585 let context = StepContext {
586 q_n: &q_n,
587 lambda_n,
588 p: &p,
589 external_n: &external_n,
590 internal_n: &internal_n,
591 predictor: &predictor,
592 };
593
594 let start = self.domain.clone();
595 let mut radius = state
596 .next_radius
597 .clamp(config.min_radius, config.max_radius);
598 let mut retries = 0;
599 let mut attempts = 0;
600 let accepted = loop {
601 attempts += 1;
602 let outcome = attempt(
603 &mut self.domain,
604 &mut self.bordered,
605 &state,
606 config,
607 &self.test,
608 &context,
609 radius,
610 &mut factorizations,
611 );
612 match outcome {
613 Ok(accepted) => break accepted,
614 Err(failure) => {
615 self.domain = start.clone();
616 let exhausted = retries >= config.max_retries || radius <= config.min_radius;
617 if exhausted {
618 return Err(AnalysisError::CutbacksExhausted {
619 step,
620 attempts,
621 radius,
622 last_failure: failure,
623 });
624 }
625 radius = (radius * 0.5).max(config.min_radius);
626 retries += 1;
627 }
628 }
629 };
630
631 let mut accepted = accepted;
632 let mut radius_used = radius;
633 self.load_factor = lambda_n + accepted.delta_lambda;
634
635 let mut stop = None;
637 let stop_criteria = &config.stop;
638 if let Some(target) = stop_criteria.displacement {
639 let eq_value = |domain: &Domain<NDIM, NDOF, NId, E>| {
640 domain.node(target.node).displacement[target.dof]
641 };
642 let before = start.node(target.node).displacement[target.dof];
643 let after = eq_value(&self.domain);
644 if brackets(before, after, target.value) {
645 let landed = target.exact
646 && self.land_exactly(
647 &start,
648 &state,
649 config,
650 &context,
651 &mut accepted,
652 &mut radius_used,
653 &mut factorizations,
654 before,
655 after,
656 target.value,
657 &eq_value,
658 );
659 stop = Some(ArcStop {
660 reason: StopReason::DisplacementTarget,
661 landed_exactly: landed,
662 overshoot: eq_value(&self.domain) - target.value,
663 });
664 }
665 }
666 if stop.is_none() {
667 if let Some(target) = stop_criteria.load_factor {
668 let after = self.load_factor;
669 if brackets(lambda_n, after, target.value) {
670 let landed = target.exact
671 && self.land_exactly_on_load(
672 &start,
673 &state,
674 config,
675 &context,
676 &mut accepted,
677 &mut radius_used,
678 &mut factorizations,
679 after,
680 target.value,
681 );
682 stop = Some(ArcStop {
683 reason: StopReason::LoadFactorTarget,
684 landed_exactly: landed,
685 overshoot: self.load_factor - target.value,
686 });
687 }
688 }
689 }
690 if stop.is_none()
691 && stop_criteria.load_factor_zero_crossing
692 && lambda_n != 0.0
693 && (self.load_factor == 0.0 || self.load_factor.signum() != lambda_n.signum())
694 {
695 stop = Some(ArcStop {
696 reason: StopReason::LoadFactorZeroCrossing,
697 landed_exactly: false,
698 overshoot: self.load_factor,
699 });
700 }
701
702 let norm = state.metric_norm(&accepted.increment);
704 let new_direction = &accepted.increment / norm;
705 let sharp_turn = state
706 .direction
707 .as_ref()
708 .is_some_and(|old| state.metric_dot(old, &new_direction) < SHARP_TURN_COS);
709 let det_sign = accepted.det_sign.or_else(|| {
713 factorizations += 1;
714 accepted_det_sign(&mut self.bordered, &accepted.k, &p)
715 });
716 let state = self.arc.as_mut().expect("initialized above");
717 let bifurcation_suspected = matches!(
718 (state.det_sign, det_sign),
719 (Some(old), Some(new)) if old != 0 && new != 0 && old != new
720 ) && state.last_delta_lambda != 0.0
721 && accepted.delta_lambda.signum() == state.last_delta_lambda.signum();
722
723 let grow = retries == 0 && !accepted.damped && !sharp_turn;
724 let factor = (config.target_iterations as f64 / accepted.corrector_solves.max(1) as f64)
725 .sqrt()
726 .clamp(0.5, 1.5);
727 let factor = if grow { factor } else { factor.min(1.0) };
728 state.next_radius = (radius_used * factor).clamp(config.min_radius, config.max_radius);
729 state.direction = Some(new_direction);
730 state.seed = None;
731 state.last_delta_lambda = accepted.delta_lambda;
732 state.chord_length += norm;
733 state.accepted_steps += 1;
734 state.force_tangent_next = retries > 0 || sharp_turn;
735 state.det_sign = det_sign;
736
737 if stop.is_none()
738 && stop_criteria
739 .max_chord_length
740 .is_some_and(|c| state.chord_length >= c)
741 {
742 stop = Some(ArcStop {
743 reason: StopReason::ChordLength,
744 landed_exactly: false,
745 overshoot: 0.0,
746 });
747 }
748 if stop.is_none()
749 && stop_criteria
750 .max_steps
751 .is_some_and(|m| state.accepted_steps >= m)
752 {
753 stop = Some(ArcStop {
754 reason: StopReason::StepCount,
755 landed_exactly: false,
756 overshoot: 0.0,
757 });
758 }
759 state.stopped = stop.is_some();
760
761 let info = super::ArcStepInfo {
762 radius: radius_used,
763 next_radius: state.next_radius,
764 retries,
765 corrector_solves: accepted.corrector_solves,
766 factorizations,
767 force_measure: accepted.force_measure,
768 arc_error: accepted.arc_error,
769 chord_length: state.chord_length,
770 det_sign,
771 bifurcation_suspected,
772 displacement_scale: state.displacement_scale,
773 rotation_scale: state.rotation_scale,
774 stop,
775 };
776 Ok(StepResult {
777 step,
778 load_factor: self.load_factor,
779 iterations: accepted.corrector_solves,
780 factorizations,
781 arc: Some(info),
782 })
783 }
784
785 fn initialize_arc(
788 &mut self,
789 config: &ArcLength<NId>,
790 p: &DVector<f64>,
791 k_n: &SparseMatrix,
792 internal_n: &DVector<f64>,
793 external_n: &DVector<f64>,
794 factorizations: &mut usize,
795 ) -> Result<ArcState, AnalysisError> {
796 if self.domain.has_unfrozen_path_series() {
797 return Err(AnalysisError::UnsupportedLoadSeries);
798 }
799 if p.amax() == 0.0 {
800 return Err(AnalysisError::ZeroLoadSensitivity);
801 }
802 let n = p.len();
803 let rotational = self.domain.rotational_equations();
804 let has_rotation = rotational.iter().any(|&r| r);
805 let has_translation = rotational.iter().any(|&r| !r);
806
807 let residual = external_n - internal_n;
808 let (balanced, measure) = force_check(
809 &self.test,
810 &rotational,
811 &residual,
812 &[external_n, internal_n],
813 );
814 if !balanced {
815 return Err(AnalysisError::InitialStateNotInEquilibrium { measure });
816 }
817
818 let (displacement_scale, rotation_scale, load_scale) = match config.scales {
819 ArcScales::Explicit {
820 displacement,
821 rotation,
822 load,
823 } => {
824 if has_rotation && rotation.is_none() {
825 return Err(AnalysisError::InvalidOption {
826 field: "integrator.rotationScale",
827 });
828 }
829 (displacement, rotation.filter(|_| has_rotation), load)
830 }
831 ArcScales::Auto { load } => {
832 let zeros = DVector::zeros(n);
835 let minus_p = -p;
836 *factorizations += 1;
837 let factor = self
838 .bordered
839 .factor(
840 k_n,
841 Border {
842 column: &minus_p,
843 row: &zeros,
844 corner: 1.0,
845 },
846 &DVector::from_element(n + 1, 1.0),
847 )
848 .map_err(|_| AnalysisError::SingularSystem)?;
849 let v = factor
850 .solve(&unit_last(n))
851 .map_err(|_| AnalysisError::SingularSystem)?;
852 let largest = |want_rotation: bool| {
853 (0..n)
854 .filter(|&i| rotational[i] == want_rotation)
855 .map(|i| v[i].abs())
856 .fold(0.0, f64::max)
857 };
858 let displacement = load * largest(false);
859 let rotation = has_rotation.then(|| load * largest(true));
860 let usable = |s: f64| s.is_finite() && s > 0.0;
861 if has_translation && !usable(displacement) {
862 return Err(AnalysisError::InvalidOption {
863 field: "integrator.displacementScale",
864 });
865 }
866 if rotation.is_some_and(|r| !usable(r)) {
867 return Err(AnalysisError::InvalidOption {
868 field: "integrator.rotationScale",
869 });
870 }
871 (
872 if has_translation { displacement } else { 1.0 },
873 rotation,
874 load,
875 )
876 }
877 };
878
879 let unknown_scale = DVector::from_fn(n + 1, |i, _| {
880 if i == n {
881 load_scale
882 } else if rotational[i] {
883 rotation_scale.expect("rotation scale required above")
884 } else {
885 displacement_scale
886 }
887 });
888 let weights = DVector::from_fn(n, |i, _| unknown_scale[i].powi(-2));
889 let beta = 1.0 / load_scale;
890
891 let mut state = ArcState {
892 weights,
893 beta,
894 unknown_scale,
895 displacement_scale,
896 rotation_scale,
897 rotational,
898 direction: None,
899 seed: None,
900 last_delta_lambda: 0.0,
901 next_radius: config.initial_radius,
902 chord_length: 0.0,
903 accepted_steps: 0,
904 force_tangent_next: false,
905 det_sign: None,
906 stopped: false,
907 };
908
909 if let Some(seed) = &config.seed {
910 let mut q = DVector::zeros(n);
911 for &(node, dof, value) in &seed.components {
912 if dof >= NDOF {
913 return Err(AnalysisError::InvalidOption {
914 field: "integrator.seed",
915 });
916 }
917 let eq =
918 self.domain
919 .equation_of(node, dof)
920 .ok_or(AnalysisError::InvalidOption {
921 field: "integrator.seed",
922 })?;
923 q[eq] += value;
924 }
925 let direction = join(&q, seed.load);
926 let norm = state.metric_norm(&direction);
927 if !(norm.is_finite() && norm > 0.0) {
928 return Err(AnalysisError::InvalidOption {
929 field: "integrator.seed",
930 });
931 }
932 state.seed = Some(direction / norm);
933 }
934 Ok(state)
935 }
936
937 #[allow(clippy::too_many_arguments)]
941 fn land_exactly(
942 &mut self,
943 start: &Domain<NDIM, NDOF, NId, E>,
944 state: &ArcState,
945 config: &ArcLength<NId>,
946 context: &StepContext<'_>,
947 accepted: &mut Accepted,
948 radius_used: &mut f64,
949 factorizations: &mut usize,
950 before: f64,
951 after: f64,
952 target: f64,
953 value_of: &impl Fn(&Domain<NDIM, NDOF, NId, E>) -> f64,
954 ) -> bool {
955 self.retake_until(
956 start,
957 state,
958 config,
959 context,
960 accepted,
961 radius_used,
962 factorizations,
963 before,
964 after,
965 target,
966 &|domain, _lambda| value_of(domain),
967 )
968 }
969
970 #[allow(clippy::too_many_arguments)]
971 fn land_exactly_on_load(
972 &mut self,
973 start: &Domain<NDIM, NDOF, NId, E>,
974 state: &ArcState,
975 config: &ArcLength<NId>,
976 context: &StepContext<'_>,
977 accepted: &mut Accepted,
978 radius_used: &mut f64,
979 factorizations: &mut usize,
980 after: f64,
981 target: f64,
982 ) -> bool {
983 let before = context.lambda_n;
984 self.retake_until(
985 start,
986 state,
987 config,
988 context,
989 accepted,
990 radius_used,
991 factorizations,
992 before,
993 after,
994 target,
995 &|_domain, lambda| lambda,
996 )
997 }
998
999 #[allow(clippy::too_many_arguments)]
1000 fn retake_until(
1001 &mut self,
1002 start: &Domain<NDIM, NDOF, NId, E>,
1003 state: &ArcState,
1004 config: &ArcLength<NId>,
1005 context: &StepContext<'_>,
1006 accepted: &mut Accepted,
1007 radius_used: &mut f64,
1008 factorizations: &mut usize,
1009 before: f64,
1010 after: f64,
1011 target: f64,
1012 value_of: &impl Fn(&Domain<NDIM, NDOF, NId, E>, f64) -> f64,
1013 ) -> bool {
1014 let tolerance = LANDING_TOLERANCE * (after - before).abs().max(f64::MIN_POSITIVE);
1015 if (after - target).abs() <= tolerance {
1016 return true;
1017 }
1018 let bracket_domain = self.domain.clone();
1019 let bracket_lambda = self.load_factor;
1020
1021 let (mut s_lo, mut f_lo) = (0.0, before - target);
1023 let (mut s_hi, mut f_hi) = (*radius_used, after - target);
1024 for _ in 0..MAX_LANDING_RETAKES {
1025 let s = s_lo + (s_hi - s_lo) * (-f_lo) / (f_hi - f_lo);
1026 if !(s.is_finite() && s > 0.0) {
1027 break;
1028 }
1029 self.domain = start.clone();
1030 let mut retake_factorizations = 0;
1031 let Ok(retaken) = attempt(
1032 &mut self.domain,
1033 &mut self.bordered,
1034 state,
1035 config,
1036 &self.test,
1037 context,
1038 s,
1039 &mut retake_factorizations,
1040 ) else {
1041 *factorizations += retake_factorizations;
1042 break;
1043 };
1044 *factorizations += retake_factorizations;
1045 let lambda = context.lambda_n + retaken.delta_lambda;
1046 let f = value_of(&self.domain, lambda) - target;
1047 if f.abs() <= tolerance {
1048 self.load_factor = lambda;
1049 *accepted = retaken;
1050 *radius_used = s;
1051 return true;
1052 }
1053 if f.signum() == f_lo.signum() {
1054 s_lo = s;
1055 f_lo = f;
1056 } else {
1057 s_hi = s;
1058 f_hi = f;
1059 }
1060 }
1061 self.domain = bracket_domain;
1062 self.load_factor = bracket_lambda;
1063 false
1064 }
1065}
1066
1067const SHARP_TURN_COS: f64 = 0.866;
1070const ORIENTATION_EPS: f64 = 1e-6;
1073const LANDING_TOLERANCE: f64 = 1e-8;
1076const MAX_LANDING_RETAKES: usize = 6;
1077const STAGNATION_WINDOW: usize = 5;
1079
1080fn brackets(before: f64, after: f64, target: f64) -> bool {
1081 before != target && (before - target) * (after - target) <= 0.0
1082}
1083
1084fn force_check(
1086 test: &ConvergenceTest,
1087 rotational: &[bool],
1088 residual: &DVector<f64>,
1089 magnitudes: &[&DVector<f64>],
1090) -> (bool, f64) {
1091 match *test {
1092 ConvergenceTest::NormUnbalance { tol, .. } => {
1093 let norm = residual.norm();
1094 (norm < tol, norm)
1095 }
1096 _ => {
1097 let tolerance = ForceTolerance::for_test(test, rotational, magnitudes)
1098 .expect("arc length only accepts force tests");
1099 let measure = tolerance.scaled_max(residual);
1100 (measure <= 1.0, measure)
1101 }
1102 }
1103}
1104
1105fn bordered_tangent(
1109 bordered: &mut BorderedSolver,
1110 state: &ArcState,
1111 k: &SparseMatrix,
1112 p: &DVector<f64>,
1113 t_prev: &DVector<f64>,
1114) -> Result<(DVector<f64>, Option<i8>), ArcFailure> {
1115 let n = p.len();
1116 let row = DVector::from_fn(n, |i, _| state.weights[i] * t_prev[i]);
1117 let minus_p = -p;
1118 let factor = bordered.factor(
1119 k,
1120 Border {
1121 column: &minus_p,
1122 row: &row,
1123 corner: state.beta * state.beta * t_prev[n],
1124 },
1125 &state.unknown_scale,
1126 )?;
1127 let v = factor.solve(&unit_last(n))?;
1128 let norm = state.metric_norm(&v);
1129 if !(norm.is_finite() && norm > 0.0) {
1130 return Err(ArcFailure::NonFinite);
1131 }
1132 let t = v / norm;
1133 if state.metric_dot(&t, t_prev) <= 0.0 {
1136 return Err(ArcFailure::InaccurateSolve);
1137 }
1138 let det_k = if t[n] == 0.0 {
1139 0
1140 } else {
1141 factor.det_sign() * t[n].signum() as i8
1142 };
1143 Ok((t, Some(det_k)))
1144}
1145
1146fn accepted_det_sign(
1149 bordered: &mut BorderedSolver,
1150 k: &SparseMatrix,
1151 p: &DVector<f64>,
1152) -> Option<i8> {
1153 let n = p.len();
1154 let zeros = DVector::zeros(n);
1155 let minus_p = -p;
1156 match bordered.factor(
1157 k,
1158 Border {
1159 column: &minus_p,
1160 row: &zeros,
1161 corner: 1.0,
1162 },
1163 &DVector::from_element(n + 1, 1.0),
1164 ) {
1165 Ok(factor) => Some(factor.det_sign()),
1166 Err(ArcFailure::SingularSystem) => Some(0),
1167 Err(_) => None,
1168 }
1169}
1170
1171fn det_sign_from(factor: &BorderedFactorization, n: usize) -> Option<i8> {
1174 let x = factor.solve(&unit_last(n)).ok()?;
1175 let last = x[n];
1176 if last == 0.0 {
1177 Some(0)
1178 } else {
1179 Some(factor.det_sign() * last.signum() as i8)
1180 }
1181}
1182
1183#[allow(clippy::too_many_arguments)]
1188fn attempt<const NDIM: usize, const NDOF: usize, NId, E>(
1189 domain: &mut Domain<NDIM, NDOF, NId, E>,
1190 bordered: &mut BorderedSolver,
1191 state: &ArcState,
1192 config: &ArcLength<NId>,
1193 test: &ConvergenceTest,
1194 context: &StepContext<'_>,
1195 s: f64,
1196 factorizations: &mut usize,
1197) -> Result<Accepted, ArcFailure>
1198where
1199 NId: Key,
1200 E: ElementOps<NDIM, NDOF, NId>,
1201{
1202 let n = context.q_n.len();
1203 let t = context.predictor;
1204 let s2 = s * s;
1205
1206 let predictor_q = DVector::from_fn(n, |i, _| s * t[i]);
1207 domain.apply_displacement_increment(&predictor_q);
1208 let mut lambda = context.lambda_n + s * t[n];
1209
1210 let external_predicted = domain.assemble_reference_load(lambda);
1213 let force_tolerance = match *test {
1214 ConvergenceTest::NormUnbalance { tol, .. } => ForceTolerance::uniform(n, tol),
1215 _ => ForceTolerance::for_test(
1216 test,
1217 &state.rotational,
1218 &[context.external_n, &external_predicted, context.internal_n],
1219 )
1220 .expect("arc length only accepts force tests"),
1221 };
1222 let force_ok = |residual: &DVector<f64>| -> (bool, f64) {
1223 match *test {
1224 ConvergenceTest::NormUnbalance { tol, .. } => {
1225 let norm = residual.norm();
1226 (norm < tol, norm)
1227 }
1228 _ => {
1229 let measure = force_tolerance.scaled_max(residual);
1230 (measure <= 1.0, measure)
1231 }
1232 }
1233 };
1234 let displacement_tol = match *test {
1235 ConvergenceTest::Combined {
1236 displacement_tol, ..
1237 } => displacement_tol,
1238 _ => None,
1239 };
1240 let merit = |residual: &DVector<f64>, g: f64| {
1241 0.5 * (force_tolerance.scaled_norm_squared(residual)
1242 + (g / (s2 * config.arc_tolerance)).powi(2))
1243 };
1244 let constraint = |dq: &DVector<f64>, dl: f64| {
1245 let mut sum = state.beta * state.beta * dl * dl - s2;
1246 for i in 0..n {
1247 sum += state.weights[i] * dq[i] * dq[i];
1248 }
1249 sum
1250 };
1251 let evaluate = |domain: &Domain<NDIM, NDOF, NId, E>, lambda: f64| {
1252 let (k, internal) = domain.assemble_tangent_and_resistance(lambda);
1253 let residual = domain.assemble_reference_load(lambda) - internal;
1254 (k, residual)
1255 };
1256
1257 let (mut k, mut residual) = evaluate(domain, lambda);
1258 let mut corrector_solves = 0;
1259 let mut damped = false;
1260 let mut last_undamped: Option<DVector<f64>> = None;
1261 let mut last_factor: Option<BorderedFactorization> = None;
1262 let mut merit_history: Vec<f64> = Vec::new();
1263 let max_iter = test.max_iter();
1264
1265 loop {
1266 let dq = domain.gather_displacement() - context.q_n;
1267 let dl = lambda - context.lambda_n;
1268 if !all_finite(&residual) || !all_finite(&dq) || !dl.is_finite() {
1269 return Err(ArcFailure::NonFinite);
1270 }
1271 let g = constraint(&dq, dl);
1272 let (force_pass, force_measure) = force_ok(&residual);
1273 let arc_error = g.abs() / s2;
1274 let correction_pass = match (&last_undamped, config.correction_tolerance) {
1275 (Some(delta), Some(tol)) => state.metric_norm(delta) <= tol * s,
1276 _ => true,
1277 };
1278 let displacement_pass = match (&last_undamped, displacement_tol) {
1279 (Some(delta), Some(tol)) => delta.rows(0, n).amax() <= tol,
1280 _ => true,
1281 };
1282
1283 if force_pass && arc_error <= config.arc_tolerance && correction_pass && displacement_pass {
1284 let increment = join(&dq, dl);
1285 if state.metric_dot(&increment, t) < ORIENTATION_EPS * s2 {
1286 return Err(ArcFailure::BranchOrientation);
1287 }
1288 let det_sign = last_factor.as_ref().and_then(|f| det_sign_from(f, n));
1289 return Ok(Accepted {
1290 k,
1291 delta_lambda: dl,
1292 corrector_solves,
1293 damped,
1294 force_measure,
1295 arc_error,
1296 det_sign,
1297 increment,
1298 });
1299 }
1300 if corrector_solves >= max_iter {
1301 return Err(ArcFailure::NotConverged);
1302 }
1303 if dq.amax() == 0.0 && dl == 0.0 {
1304 return Err(ArcFailure::DegenerateConstraint);
1305 }
1306
1307 let minus_p = -context.p;
1308 let row = DVector::from_fn(n, |i, _| 2.0 * state.weights[i] * dq[i]);
1309 *factorizations += 1;
1310 let factor = bordered.factor(
1311 &k,
1312 Border {
1313 column: &minus_p,
1314 row: &row,
1315 corner: 2.0 * state.beta * state.beta * dl,
1316 },
1317 &state.unknown_scale,
1318 )?;
1319 let delta = factor.solve(&join(&residual, -g))?;
1320 corrector_solves += 1;
1321
1322 let phi0 = merit(&residual, g);
1330 let base = join(&dq, dl);
1331 let mut eta = 1.0;
1332 let accepted_candidate = loop {
1333 let candidate = &base + eta * δ
1334 let norm = state.metric_norm(&candidate);
1335 if norm.is_finite() && norm > 0.0 {
1336 let target = candidate * (s / norm);
1337 let current = domain.gather_displacement() - context.q_n;
1338 let shift = DVector::from_fn(n, |i, _| target[i] - current[i]);
1339 domain.apply_displacement_increment(&shift);
1340 let candidate_lambda = context.lambda_n + target[n];
1341 let (k_c, r_c) = evaluate(domain, candidate_lambda);
1342 let dq_c = domain.gather_displacement() - context.q_n;
1343 let g_c = constraint(&dq_c, candidate_lambda - context.lambda_n);
1344 let phi = merit(&r_c, g_c);
1345 let converged_here = force_ok(&r_c).0 && g_c.abs() / s2 <= config.arc_tolerance;
1346 if phi.is_finite()
1347 && (phi <= (1.0 - 2.0 * config.backtracking.armijo * eta) * phi0
1348 || converged_here)
1349 {
1350 break Some((k_c, r_c, candidate_lambda, phi));
1351 }
1352 }
1353 eta *= 0.5;
1354 if eta < config.backtracking.min_step {
1355 break None;
1356 }
1357 };
1358 let Some((k_c, r_c, candidate_lambda, phi)) = accepted_candidate else {
1359 return Err(ArcFailure::NoDescent);
1360 };
1361 damped |= eta < 1.0;
1362 k = k_c;
1363 residual = r_c;
1364 lambda = candidate_lambda;
1365 last_undamped = Some(delta);
1366 last_factor = Some(factor);
1367
1368 merit_history.push(phi);
1369 let len = merit_history.len();
1370 if len > STAGNATION_WINDOW && phi > 0.5 * merit_history[len - 1 - STAGNATION_WINDOW] {
1371 return Err(ArcFailure::Stagnated);
1372 }
1373 }
1374}