Skip to main content

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}