carapace_core/analysis/integrator.rs
1use nalgebra::DVector;
2use slotmap::Key;
3
4use crate::model::{Domain, ElementOps, NodeId, SparseMatrix};
5
6use super::{AnalysisError, ArcLength, SparseFactorization, SparseSolver};
7
8/// Static/pseudo-static integration strategy. Closed enum.
9/// `LoadControl`/`DisplacementControl` only change how this *step's* load
10/// factor is chosen (the "predictor") — the iteration that follows
11/// (see `Algorithm`) proceeds identically regardless of which one picked it.
12/// `ArcLength` is different in kind: it predicts displacement *and* load
13/// factor and corrects both together through a bordered system, so
14/// `Analysis::step` hands it to its own continuation driver instead of
15/// `predict`/`correct` (see `arclength.rs` and `docs/arclength.md`).
16///
17/// Generic over `NId` (the controlled node's ID type — `NodeId` or
18/// `Node3Id`), defaulted to the planar profile so bare `Integrator` keeps
19/// working, the same trick `Domain` uses. `predict`/`correct` take the rest
20/// of the profile (`NDIM`/`NDOF`/`ELEMENT_DOF`/`E`) as their own
21/// generic parameters instead of `Integrator`'s, since an integrator's
22/// identity only depends on which node/DOF it controls, not which `Domain`
23/// it's later used with.
24#[derive(Debug, Clone, PartialEq)]
25pub enum Integrator<NId = NodeId> {
26 LoadControl {
27 increment: f64,
28 },
29 /// Chooses this step's pseudo-time increment so that the controlled
30 /// node/DOF's displacement changes by exactly `increment`, via the
31 /// standard unit-load technique: solve the current tangent against the
32 /// *sensitivity* of the total applied load to a unit pseudo-time
33 /// increment (`Domain::assemble_reference_load_sensitivity` — every
34 /// non-frozen `LoadPattern`'s contribution, weighted by its series'
35 /// slope at the current pseudo-time; a `hold_pattern_constant`-frozen
36 /// or `LoadSeries::Constant` pattern contributes nothing, since it
37 /// doesn't respond to pseudo-time at all) once, then scale by whatever
38 /// factor makes the controlled DOF's response match `increment`.
39 DisplacementControl {
40 node: NId,
41 dof: usize,
42 increment: f64,
43 },
44 /// Arc-length continuation along the equilibrium path, through load
45 /// limits and snap-back — see `ArcLength`.
46 ArcLength(ArcLength<NId>),
47}
48
49impl<NId: Copy> Integrator<NId> {
50 /// Compute this step's new pseudo-time, given the domain's state as of
51 /// the end of the *previous* (converged) step, plus the displacement
52 /// predictor the caller must apply before iterating (`None` for
53 /// `LoadControl`). `DisplacementControl`'s predictor is the unit-load
54 /// response scaled by the pseudo-time increment, so the controlled DOF
55 /// starts the step exactly at its target and every Newton iteration
56 /// (the first included) measures a true correction. That is what lets
57 /// a linear step converge in one iteration instead of spending its
58 /// first on the predictor and a second on proving `du` is small.
59 pub(crate) fn predict<const NDIM: usize, const NDOF: usize, E>(
60 &self,
61 domain: &Domain<NDIM, NDOF, NId, E>,
62 solver: &SparseSolver,
63 current_pseudo_time: f64,
64 ) -> Result<(f64, Option<DVector<f64>>), AnalysisError>
65 where
66 NId: Key,
67 E: ElementOps<NDIM, NDOF, NId>,
68 {
69 match self {
70 Integrator::LoadControl { increment } => Ok((current_pseudo_time + increment, None)),
71 Integrator::DisplacementControl {
72 node,
73 dof,
74 increment,
75 } => {
76 let eq = domain
77 .equation_of(*node, *dof)
78 .ok_or(AnalysisError::InvalidConstraint)?;
79 let (k, _resistance) = domain.assemble_tangent_and_resistance(current_pseudo_time);
80 let sensitivity = domain.assemble_reference_load_sensitivity(current_pseudo_time);
81 let unit_response = solver.solve(&k, &sensitivity)?;
82 let delta_lambda = increment / unit_response[eq];
83 Ok((
84 current_pseudo_time + delta_lambda,
85 Some(unit_response * delta_lambda),
86 ))
87 }
88 Integrator::ArcLength(_) => {
89 unreachable!("Analysis::step routes ArcLength to its continuation driver")
90 }
91 }
92 }
93
94 /// Corrector for every Newton iteration of a step, applied after
95 /// `predict`'s displacement predictor.
96 /// `predict`'s pseudo-time only accounts for the tangent at the *start*
97 /// of the step — exact for `LoadControl` (whose pseudo-time never
98 /// changes mid-step, so `du_bar` unmodified is already the right
99 /// increment every iteration).
100 ///
101 /// For `DisplacementControl`, `predict` already moved the controlled DOF
102 /// to its target (OpenSees' `newStep` does the same), but the tangent it
103 /// used can go stale as the iteration proceeds (e.g. a step landing
104 /// exactly on a material's yield breakpoint sees the elastic tangent at
105 /// `predict` time, then the true, much softer, post-yield tangent) —
106 /// left uncorrected, ordinary fixed-load-factor Newton iteration would
107 /// then keep driving the controlled DOF *past* the target already
108 /// reached, chasing force equilibrium at the wrong load factor instead.
109 /// This is the standard Yang & Shieh consistent Displacement Control
110 /// corrector: solve the current tangent against the reference load
111 /// sensitivity again (`unit_response`, this iteration's version of
112 /// `predict`'s unit-load probe), then pick `delta_lambda` so that adding
113 /// `delta_lambda * unit_response` to `du_bar` cancels `du_bar`'s
114 /// contribution at the controlled DOF — i.e. this iteration leaves the
115 /// controlled DOF exactly where it already is, no matter how far the
116 /// tangent has drifted, while every other DOF still gets `du_bar`'s
117 /// residual-driven correction.
118 /// `current_factorization`, when supplied, must factor `k`. An older
119 /// Newton tangent must not be passed here: the load-sensitivity probe
120 /// continues to use the current tangent even under tangent reuse.
121 pub(crate) fn correct<const NDIM: usize, const NDOF: usize, E>(
122 &self,
123 domain: &Domain<NDIM, NDOF, NId, E>,
124 solver: &SparseSolver,
125 k: &SparseMatrix,
126 current_factorization: Option<&SparseFactorization>,
127 du_bar: DVector<f64>,
128 pseudo_time: f64,
129 ) -> Result<(f64, DVector<f64>), AnalysisError>
130 where
131 NId: Key,
132 E: ElementOps<NDIM, NDOF, NId>,
133 {
134 match self {
135 Integrator::LoadControl { .. } => Ok((0.0, du_bar)),
136 Integrator::DisplacementControl { node, dof, .. } => {
137 let eq = domain
138 .equation_of(*node, *dof)
139 .ok_or(AnalysisError::InvalidConstraint)?;
140 let sensitivity = domain.assemble_reference_load_sensitivity(pseudo_time);
141 let unit_response = match current_factorization {
142 Some(factorization) => factorization.solve(&sensitivity),
143 None => solver.solve(k, &sensitivity)?,
144 };
145 let delta_lambda = -du_bar[eq] / unit_response[eq];
146 let du = du_bar + unit_response * delta_lambda;
147 Ok((delta_lambda, du))
148 }
149 Integrator::ArcLength(_) => {
150 unreachable!("Analysis::step routes ArcLength to its continuation driver")
151 }
152 }
153 }
154}