Skip to main content

carapace_wasm/input_v1/decode/
mod.rs

1//! Hand-written per-table decode: translates a [`CarapaceInputV1`] into a
2//! [`Session`] by calling the same `Domain`/`Element`/`Material`
3//! constructors native code calls — no generic deserialization into `core`
4//! types, no reflection.
5//! Panic-free: every lookup returns a [`DecodeError`] instead of indexing
6//! past the end of a table, since this is defense in depth against
7//! engine/schema version skew, not a re-check of what `pysees`'s compiler
8//! is expected to have already validated.
9//!
10//! The profile is chosen once from `header.ndm` and never branched on again
11//! downstream. `shared` holds the bookkeeping common to both (nodes,
12//! constraints, loads, recorders), `stages` the stage compilation, and
13//! `elements_2d`/`elements_3d` the two small per-profile element decoders.
14
15mod elements_2d;
16mod elements_3d;
17mod shared;
18mod stages;
19
20use super::error::DecodeError;
21use super::session::Session;
22use super::CarapaceInputV1;
23
24pub fn decode(input: CarapaceInputV1) -> Result<Session, DecodeError> {
25    // Validate all path data before it reaches interpolation, including inertial excitations.
26    let check = |series: &super::tables::TimeSeriesSpec| -> Result<(), DecodeError> {
27        use super::tables::TimeSeriesSpec;
28        if let TimeSeriesSpec::Path { times, factors }
29        | TimeSeriesSpec::BoundedPath { times, factors, .. } = series
30        {
31            if times.is_empty()
32                || times.len() != factors.len()
33                || times.iter().any(|t| !t.is_finite())
34                || factors.iter().any(|v| !v.is_finite())
35                || times.windows(2).any(|w| w[1] <= w[0])
36            {
37                return Err(DecodeError::InvalidAnalysisOption {
38                    stage: "timeSeries".into(),
39                    field: "path",
40                });
41            }
42        }
43        Ok(())
44    };
45    for series in &input.load_patterns.series {
46        check(series)?;
47    }
48    for stage in &input.sequence.stages {
49        if let super::sequence::StageSpec::Transient {
50            ground_motions,
51            damping,
52            steps,
53            ..
54        } = stage
55        {
56            if *steps == 0
57                || !damping.alpha_m.is_finite()
58                || !damping.beta_k.is_finite()
59                || damping.alpha_m < 0.0
60                || damping.beta_k < 0.0
61                || damping.modal_anchors.is_some_and(|a| {
62                    a.mode1 == 0
63                        || a.mode2 == 0
64                        || !a.ratio.is_finite()
65                        || !(0.0..=1.0).contains(&a.ratio)
66                })
67            {
68                return Err(DecodeError::InvalidAnalysisOption {
69                    stage: stage.id().into(),
70                    field: "damping/steps",
71                });
72            }
73            for motion in ground_motions {
74                check(&motion.series)?;
75                if !motion.scale_factor.is_finite() {
76                    return Err(DecodeError::InvalidAnalysisOption {
77                        stage: stage.id().into(),
78                        field: "groundMotion.scaleFactor",
79                    });
80                }
81            }
82        }
83    }
84    match input.header.ndm {
85        2 => elements_2d::decode(input).map(Session::D2),
86        3 => elements_3d::decode(input).map(Session::D3),
87        got => Err(DecodeError::UnsupportedNdm { got }),
88    }
89}