carapace_wasm/input_v1/sequence.rs
1//! Wire-format `AnalysisSequence` — mirrors the PySees
2//! `AnalysisStage` discriminated union: `Static` (pushover-style), `Modal`
3//! (eigenvalue), and `Transient` (Newmark time-history, optionally driven
4//! by `GroundMotionSpec`).
5
6use serde::{Deserialize, Serialize};
7
8use super::tables::{ElementKind, TimeSeriesSpec};
9
10// `ArcLength` is much larger than the other variants; these are decoded a
11// handful of times per input, so boxing would only complicate the wire type.
12#[allow(clippy::large_enum_variant)]
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
14#[serde(
15 tag = "kind",
16 rename_all = "camelCase",
17 rename_all_fields = "camelCase"
18)]
19pub enum IntegratorSpec {
20 LoadControl {
21 increment: f64,
22 },
23 /// `node`/`dof` index into `NodeTable`; the compiler is responsible for
24 /// having registered a reference load with nonzero sensitivity on that
25 /// DOF — decode does not re-derive that check.
26 DisplacementControl {
27 node: u32,
28 dof: u8,
29 increment: f64,
30 },
31 /// Arc-length continuation (`core::ArcLength`, docs/arclength.md):
32 /// traces the equilibrium path through load limits and snap-back. The
33 /// stage's `steps` is the step cap; `stop` can end the stage earlier.
34 /// Requires a Newton algorithm with the current tangent and no line
35 /// search, and a `normUnbalance` or `combined` convergence test (a
36 /// `combined` test is the default when `convergence` is omitted).
37 ArcLength(ArcLengthSpec),
38}
39
40/// Wire form of `core::ArcLength`. Omitted `minRadius`/`maxRadius` default
41/// to `initialRadius` (a fixed-radius run).
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct ArcLengthSpec {
45 pub initial_radius: f64,
46 #[tsify(optional)]
47 pub min_radius: Option<f64>,
48 #[tsify(optional)]
49 pub max_radius: Option<f64>,
50 #[tsify(optional)]
51 pub target_iterations: Option<u32>,
52 #[tsify(optional)]
53 pub max_retries: Option<u32>,
54 #[serde(default)]
55 #[tsify(optional)]
56 pub direction: ArcDirectionSpec,
57 pub scales: ArcScalesSpec,
58 #[serde(default)]
59 #[tsify(optional)]
60 pub predictor: ArcPredictorSpec,
61 #[tsify(optional)]
62 pub seed: Option<ArcSeedSpec>,
63 #[tsify(optional)]
64 pub arc_tolerance: Option<f64>,
65 #[tsify(optional)]
66 pub correction_tolerance: Option<f64>,
67 #[tsify(optional)]
68 pub backtracking: Option<BacktrackingSpec>,
69 #[tsify(optional)]
70 pub stop: Option<ArcStopSpec>,
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
74#[serde(rename_all = "camelCase")]
75pub enum ArcDirectionSpec {
76 #[default]
77 Increasing,
78 Decreasing,
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
82#[serde(rename_all = "camelCase")]
83pub enum ArcPredictorSpec {
84 #[default]
85 Secant,
86 Tangent,
87}
88
89/// The continuation metric's scales (`core::ArcScales`): explicit
90/// characteristic translation/rotation/load-factor sizes, or `auto`
91/// (translation/rotation scales derived from the first elastic tangent).
92#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
93#[serde(
94 tag = "kind",
95 rename_all = "camelCase",
96 rename_all_fields = "camelCase",
97 deny_unknown_fields
98)]
99pub enum ArcScalesSpec {
100 Explicit {
101 displacement: f64,
102 #[tsify(optional)]
103 rotation: Option<f64>,
104 load: f64,
105 },
106 Auto {
107 load: f64,
108 },
109}
110
111/// One component of a seed direction: `node`/`dof` index into `NodeTable`.
112#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
113#[serde(rename_all = "camelCase", deny_unknown_fields)]
114pub struct ArcSeedComponentSpec {
115 pub node: u32,
116 pub dof: u8,
117 pub value: f64,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122pub struct ArcSeedSpec {
123 pub components: Vec<ArcSeedComponentSpec>,
124 #[serde(default)]
125 #[tsify(optional)]
126 pub load: f64,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
130#[serde(rename_all = "camelCase", deny_unknown_fields)]
131pub struct BacktrackingSpec {
132 pub armijo: f64,
133 pub min_step: f64,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct DisplacementTargetSpec {
139 pub node: u32,
140 pub dof: u8,
141 pub value: f64,
142 #[serde(default)]
143 #[tsify(optional)]
144 pub exact: bool,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149pub struct LoadFactorTargetSpec {
150 pub value: f64,
151 #[serde(default)]
152 #[tsify(optional)]
153 pub exact: bool,
154}
155
156/// Optional criteria that end an arc-length stage before its step cap.
157#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, tsify::Tsify)]
158#[serde(rename_all = "camelCase", deny_unknown_fields)]
159pub struct ArcStopSpec {
160 #[tsify(optional)]
161 pub displacement: Option<DisplacementTargetSpec>,
162 #[tsify(optional)]
163 pub load_factor: Option<LoadFactorTargetSpec>,
164 #[serde(default)]
165 #[tsify(optional)]
166 pub load_factor_zero_crossing: bool,
167 #[tsify(optional)]
168 pub max_chord_length: Option<f64>,
169}
170
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
172#[serde(rename_all = "camelCase")]
173pub enum TangentStrategySpec {
174 #[default]
175 Current,
176 ReuseAtStepStart,
177 Initial,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
181#[serde(
182 tag = "kind",
183 rename_all = "camelCase",
184 rename_all_fields = "camelCase",
185 deny_unknown_fields
186)]
187pub enum LineSearchSpec {
188 Bisection {
189 tol: f64,
190 max_iter: u32,
191 max_eta: f64,
192 },
193 RegulaFalsi {
194 tol: f64,
195 max_iter: u32,
196 max_eta: f64,
197 },
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
201#[serde(
202 tag = "kind",
203 rename_all = "camelCase",
204 rename_all_fields = "camelCase",
205 deny_unknown_fields
206)]
207pub enum AlgorithmSpec {
208 Linear,
209 Newton {
210 #[serde(default)]
211 tangent: TangentStrategySpec,
212 #[tsify(optional)]
213 line_search: Option<LineSearchSpec>,
214 },
215 KrylovNewton {
216 tangent: TangentStrategySpec,
217 max_dimension: u32,
218 },
219}
220
221impl Default for AlgorithmSpec {
222 fn default() -> Self {
223 Self::Linear
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
228#[serde(
229 tag = "kind",
230 rename_all = "camelCase",
231 rename_all_fields = "camelCase"
232)]
233pub enum ConvergenceSpec {
234 NormUnbalance {
235 tol: f64,
236 max_iter: u32,
237 },
238 NormDispIncr {
239 tol: f64,
240 max_iter: u32,
241 },
242 EnergyIncr {
243 tol: f64,
244 max_iter: u32,
245 },
246 /// `core::ConvergenceTest::Combined`: per-equation force equilibrium
247 /// (`|r_i| <= forceTol|momentTol + relativeTol * F_ref_i`), plus an
248 /// optional displacement-correction bound; all must pass. `momentTol`
249 /// defaults to `forceTol`, `relativeTol` to `1e-6`.
250 Combined {
251 force_tol: f64,
252 #[tsify(optional)]
253 moment_tol: Option<f64>,
254 #[tsify(optional)]
255 relative_tol: Option<f64>,
256 #[tsify(optional)]
257 displacement_tol: Option<f64>,
258 max_iter: u32,
259 },
260}
261
262impl ConvergenceSpec {
263 /// The decoder's default when a stage omits `convergence` — the
264 /// TS type marks the field optional but does not name a
265 /// default, so this one is this decoder's own choice.
266 pub const DEFAULT: ConvergenceSpec = ConvergenceSpec::NormUnbalance {
267 tol: 1e-6,
268 max_iter: 20,
269 };
270
271 /// The default for an arc-length stage that omits `convergence`: force
272 /// equilibrium per equation (an arc-length step always also has to meet
273 /// its constraint tolerance).
274 pub const ARC_LENGTH_DEFAULT: ConvergenceSpec = ConvergenceSpec::Combined {
275 force_tol: 1e-6,
276 moment_tol: None,
277 relative_tol: None,
278 displacement_tol: None,
279 max_iter: 30,
280 };
281}
282
283/// Rayleigh damping (`C = alpha_m*M + beta_k*K`) — see
284/// `core::RayleighDamping`'s doc comment. Use `alpha_m: 0.0, beta_k: 0.0`
285/// for undamped (`core::RayleighDamping::NONE`).
286#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
287#[serde(rename_all = "camelCase")]
288pub struct DampingSpec {
289 pub alpha_m: f64,
290 pub beta_k: f64,
291 /// Resolve these modal anchors from the current domain at transient-stage entry.
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 #[tsify(optional)]
294 pub modal_anchors: Option<ModalDampingAnchors>,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
298#[serde(rename_all = "camelCase")]
299pub struct ModalDampingAnchors {
300 pub mode1: u32,
301 pub mode2: u32,
302 pub ratio: f64,
303}
304
305/// One `core::GroundMotion`: a uniform acceleration time history applied
306/// along one global translational axis (`direction`: `0`=x, `1`=y, and for
307/// the spatial profile `2`=z — never a rotational DOF). `series` is
308/// typically `TimeSeriesSpec::Path` (the accelerogram itself), reusing the
309/// same wire type a static `LoadPatternTable` entry uses — an accelerogram
310/// is exactly a piecewise-linear series, no separate representation needed
311/// (`core::GroundMotion`'s own doc comment).
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
313#[serde(rename_all = "camelCase")]
314pub struct GroundMotionSpec {
315 pub direction: u8,
316 pub series: TimeSeriesSpec,
317 pub scale_factor: f64,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, tsify::Tsify)]
321#[serde(
322 tag = "kind",
323 rename_all = "camelCase",
324 rename_all_fields = "camelCase"
325)]
326#[allow(clippy::large_enum_variant)] // see `IntegratorSpec`
327pub enum StageSpec {
328 Static {
329 id: String,
330 steps: u32,
331 integrator: IntegratorSpec,
332 algorithm: AlgorithmSpec,
333 #[tsify(optional)]
334 convergence: Option<ConvergenceSpec>,
335 /// Indices into `LoadPatternTable` to freeze (via
336 /// `Domain::hold_pattern_constant`) at this stage's final load
337 /// factor once it completes.
338 hold_patterns_after: Vec<u32>,
339 },
340 /// A single eigenvalue solve (`core::modal_analysis`) for the domain's
341 /// current state — not iterative like `Static`/`Transient`, so this
342 /// stage always takes exactly one `advance()` step regardless of the
343 /// caller's step budget (`decode.rs`'s `compile_stages` hardcodes
344 /// `steps: 1` for it). `modes` is the number of lowest-frequency modes
345 /// to compute (`1..=free_dof_count`).
346 Modal { id: String, modes: u32 },
347 /// Reverts the model to its initial state (OpenSees `reset`): displacements, velocities and
348 /// every element/material history return to their as-built values and time to `0`. Like
349 /// OpenSees, it does not undo `holdPatternsAfter` freezes (`loadConst`): a held pattern
350 /// stays applied at its frozen factor, so a later stage sees that load in full from its
351 /// first step; patterns that were not held ramp again from `0`. Load patterns and their
352 /// loads stay defined. Takes no steps and records nothing.
353 Reset { id: String },
354 /// Newmark-beta time-history analysis (`core::TransientAnalysis`,
355 /// fixed at the unconditionally-stable "average acceleration"
356 /// parameters — see its own doc comment). `steps` fixed-size `dt`
357 /// increments; `ground_motions` (zero or more) each add a uniform
358 /// inertial excitation along one axis, on top of whatever `LoadPattern`
359 /// reference loads are already active (e.g. a preceding `Static`
360 /// stage's frozen gravity).
361 Transient {
362 id: String,
363 steps: u32,
364 dt: f64,
365 damping: DampingSpec,
366 ground_motions: Vec<GroundMotionSpec>,
367 /// Omitted algorithms preserve the original linear Newmark solve.
368 #[serde(default)]
369 algorithm: AlgorithmSpec,
370 #[tsify(optional)]
371 convergence: Option<ConvergenceSpec>,
372 },
373}
374
375impl StageSpec {
376 pub fn id(&self) -> &str {
377 match self {
378 StageSpec::Static { id, .. }
379 | StageSpec::Modal { id, .. }
380 | StageSpec::Reset { id }
381 | StageSpec::Transient { id, .. } => id,
382 }
383 }
384}
385
386/// One recorder: a single scalar channel's history against its stage's load
387/// factor. `NodeDisp` was the first variant (node displacement/load-factor
388/// history for a static pushover); `ElementForce` (results-storage-indexeddb.md's
389/// "several more types of recorders" plan) is the second, sharing the exact
390/// same batching/storage machinery — see `session::ResolvedRecorder` and
391/// `StepOutcome::recorder_batches`, neither of which needed to change shape
392/// to add it. `ElementForce`'s `element_kind`/`element_index` pair mirrors
393/// `ElementLoadTable`'s existing disambiguation between per-kind element
394/// tables (`ElasticBeamColumn2dTable`, `FiberBeamColumn3dTable`, ...) — there is
395/// no single flat element table to index into directly, unlike `NodeTable`.
396/// Adding a future response kind (velocity, acceleration, ...) is one more
397/// variant here plus one more match arm in `ModelSession::record_sample`,
398/// not a new parallel type or a new `Session`/`StepOutcome` field.
399/// Which half of a fiber's `(strain, stress)` pair (`core::FiberSection::
400/// fiber_responses`'s doc comment) a `RecorderSpec::Fiber` reads — one scalar channel per recorder, same as every other
401/// kind here.
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
403#[serde(rename_all = "camelCase")]
404pub enum FiberResponseKind {
405 Strain,
406 Stress,
407}
408
409/// Whether a [`RecorderSpec::GaussPoint`] reads strain or stress.
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, tsify::Tsify)]
411#[serde(rename_all = "camelCase")]
412pub enum GaussQuantity {
413 Strain,
414 Stress,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, tsify::Tsify)]
418#[serde(
419 tag = "response",
420 rename_all = "camelCase",
421 rename_all_fields = "camelCase"
422)]
423pub enum RecorderSpec {
424 /// `node`/`dof` index into `NodeTable`.
425 NodeDisp { node: u32, dof: u8 },
426 /// Only meaningful during a `Transient` stage — `analysis.domain()`'s
427 /// node velocity is otherwise always zero (a `Static` stage never
428 /// touches it), so this recorder simply contributes no sample outside
429 /// one.
430 NodeVel { node: u32, dof: u8 },
431 /// See `NodeVel`'s doc comment — same restriction, for acceleration.
432 NodeAccel { node: u32, dof: u8 },
433 /// `element_index` indexes into whichever per-kind table `element_kind`
434 /// names (e.g. `ForceBeamColumnTable`), not a flat cross-kind element
435 /// list. `component` indexes the element's local nodal force vector
436 /// (`ElementOps::local_force` — axial/shear/moment at each end, in the
437 /// element's own axis frame), width `2 * dofsPerNode` (6 in 2D, 12 in
438 /// 3D); a `component` past `ElementOps::local_force_width` is
439 /// `DecodeError::InvalidRecorderComponent`.
440 ElementForce {
441 element_kind: ElementKind,
442 element_index: u32,
443 component: u8,
444 },
445 /// The uniform load an element carries at the sample's pseudo-time
446 /// (every pattern's load on it, scaled by its factor, summed — frozen
447 /// patterns included): `component` `0` = `wx`, `1` = `wy`, `2` = `wz`
448 /// (3D only), local axes; past the load's width it is
449 /// `DecodeError::InvalidRecorderComponent`.
450 /// What a consumer needs to recover internal-force diagrams from
451 /// `ElementForce`'s end forces. `0` for an unloaded element.
452 ElementLoad {
453 element_kind: ElementKind,
454 element_index: u32,
455 component: u8,
456 },
457 /// Only meaningful during a `Modal` stage: `mode` indexes that stage's
458 /// computed `Vec<Mode>` (ascending frequency, `0` = lowest), `node`/
459 /// `dof` locate one entry of that mode's free-DOF-indexed shape vector
460 /// (`core::Mode::shape`) via `Domain::equation_of` — a fixed DOF has no
461 /// equation number and reads as `0.0` (trivially true: a fixed DOF
462 /// can't participate in any mode shape). The recorded "time" component
463 /// is the mode's frequency (rad/time), not a pseudo-time or load
464 /// factor — there's no more natural scalar to pair a mode's shape
465 /// component with.
466 ModeShape { mode: u32, node: u32, dof: u8 },
467 /// Support/equilibrium reaction at `node`/`dof` (`core::Domain::
468 /// reaction`'s doc comment) — most useful at a fixed DOF, but not
469 /// restricted to one. Valid during `Static` and `Transient` stages
470 /// (paired with the stage's own load factor/time, same as `NodeDisp`);
471 /// records nothing during a `Modal` stage, which has no applied-load
472 /// state for `reaction` to evaluate against.
473 Reaction { node: u32, dof: u8 },
474 /// One fiber's strain or stress, at one integration point, of one
475 /// `DispBeamColumn`/`ForceBeamColumn` element (`core::Domain::
476 /// element_fiber_responses`'s doc comment) — `element_kind`/
477 /// `element_index` mirror `ElementForce`'s own disambiguation.
478 /// `point`/`fiber` index into that element's own integration-point/
479 /// fiber ordering, exactly as originally given to its constructor
480 /// (`point` therefore also matches `IntegrationSpec`'s point count,
481 /// and `fiber` matches `FiberTable`'s per-section fiber range).
482 /// Records nothing for any other element kind (no fiber section to
483 /// read) or an out-of-range `point`/`fiber` — the same "unresolved by
484 /// runtime state, not a decode-time error" handling `ModeShape` uses,
485 /// since neither can be checked before the analysis actually runs.
486 Fiber {
487 element_kind: ElementKind,
488 element_index: u32,
489 point: u32,
490 fiber: u32,
491 quantity: FiberResponseKind,
492 },
493 /// One component of the strain or stress at one Gauss point of a continuum
494 /// element (`tri3`: 1 point, `quad4`: 4, in `core`'s Gauss-point order).
495 /// `component` is `0..3` = `x, y, xy` (engineering shear for strain). Valid
496 /// during `Static` and `Transient` stages; records nothing during `Modal`.
497 /// `point` past the element's Gauss-point count (or any point of a
498 /// non-continuum kind) is `DecodeError::InvalidGaussPoint`; `component`
499 /// past 2 is `InvalidRecorderComponent`.
500 GaussPoint {
501 element_kind: ElementKind,
502 element_index: u32,
503 point: u32,
504 quantity: GaussQuantity,
505 component: u8,
506 },
507}
508
509#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, tsify::Tsify)]
510#[serde(rename_all = "camelCase")]
511pub struct SequenceSpec {
512 pub stages: Vec<StageSpec>,
513 pub recorders: Vec<RecorderSpec>,
514}