Skip to main content

carapace_core/analysis/
builder.rs

1use slotmap::Key;
2
3use crate::model::{Domain, ElementOps, ModelError, NodeId};
4
5use super::bordered::BorderedSolver;
6use super::{Algorithm, Analysis, ConstraintHandler, ConvergenceTest, Integrator, SparseSolver};
7
8/// Typestate analysis composition: each stage exposes only the next
9/// piece that must be wired, and only `AnalysisBuilder<Ready>` exposes
10/// `.build()`. Illegal sequencing (e.g. calling `.build()` before an
11/// algorithm is set) is a compile error, not a runtime `setLinks` ordering
12/// bug like Xara's `BasicAnalysisBuilder`.
13///
14/// `AnalysisBuilder<S>` itself isn't generic over the kinematic profile —
15/// only its state markers are (`WithIntegrator<NId>` etc., defaulted to the
16/// planar profile), since an `Integrator<NId>` only commits to which node ID
17/// type it controls. The rest of the profile (`NDIM`/`NDOF`/`ELEMENT_DOF`/
18/// `E::Id`/`E`) is inferred at `.build(domain)` time from `domain` itself.
19pub struct AnalysisBuilder<S = Unwired> {
20    state: S,
21}
22
23pub struct Unwired;
24
25pub struct WithConstraintHandler {
26    constraint_handler: ConstraintHandler,
27}
28
29pub struct WithIntegrator<NId = NodeId> {
30    constraint_handler: ConstraintHandler,
31    integrator: Integrator<NId>,
32}
33
34pub struct WithAlgorithm<NId = NodeId> {
35    constraint_handler: ConstraintHandler,
36    integrator: Integrator<NId>,
37    algorithm: Algorithm,
38}
39
40pub struct Ready<NId = NodeId> {
41    constraint_handler: ConstraintHandler,
42    integrator: Integrator<NId>,
43    algorithm: Algorithm,
44    test: ConvergenceTest,
45}
46
47impl Default for AnalysisBuilder<Unwired> {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl AnalysisBuilder<Unwired> {
54    pub fn new() -> Self {
55        AnalysisBuilder { state: Unwired }
56    }
57
58    pub fn constraint_handler(
59        self,
60        constraint_handler: ConstraintHandler,
61    ) -> AnalysisBuilder<WithConstraintHandler> {
62        AnalysisBuilder {
63            state: WithConstraintHandler { constraint_handler },
64        }
65    }
66}
67
68impl AnalysisBuilder<WithConstraintHandler> {
69    pub fn integrator<NId>(
70        self,
71        integrator: Integrator<NId>,
72    ) -> AnalysisBuilder<WithIntegrator<NId>> {
73        AnalysisBuilder {
74            state: WithIntegrator {
75                constraint_handler: self.state.constraint_handler,
76                integrator,
77            },
78        }
79    }
80}
81
82impl<NId> AnalysisBuilder<WithIntegrator<NId>> {
83    pub fn algorithm(self, algorithm: Algorithm) -> AnalysisBuilder<WithAlgorithm<NId>> {
84        AnalysisBuilder {
85            state: WithAlgorithm {
86                constraint_handler: self.state.constraint_handler,
87                integrator: self.state.integrator,
88                algorithm,
89            },
90        }
91    }
92}
93
94impl<NId> AnalysisBuilder<WithAlgorithm<NId>> {
95    pub fn test(self, test: ConvergenceTest) -> AnalysisBuilder<Ready<NId>> {
96        AnalysisBuilder {
97            state: Ready {
98                constraint_handler: self.state.constraint_handler,
99                integrator: self.state.integrator,
100                algorithm: self.state.algorithm,
101                test,
102            },
103        }
104    }
105}
106
107impl<NId: Copy> AnalysisBuilder<Ready<NId>> {
108    /// Numbers DOFs and builds the sparsity/solver setup once, here — not
109    /// re-checked every step. Generic
110    /// over the rest of the profile (`NDIM`/`NDOF`/`E::Id`/`E`), inferred
111    /// from `domain`'s type — this is what lets one `.build()` serve both
112    /// `Domain`/`Analysis` and `Domain3`/`Analysis3`.
113    ///
114    /// # Panics
115    /// If `domain` has any `equal_dof`/`rigid_diaphragm` constraint but
116    /// `ConstraintHandler::Plain` was selected — `Plain` can't resolve
117    /// multi-point constraints (see its doc comment); use `Transformation`.
118    /// Also if the model fails `Domain::validate` (use [`try_build`] to
119    /// receive that as an error instead). Both are model-construction
120    /// errors, not runtime conditions, so `build` reports them by panicking
121    /// (runtime failures are reported as errors; misuse of the builder is not).
122    ///
123    /// [`try_build`]: AnalysisBuilder::try_build
124    pub fn build<const NDIM: usize, const NDOF: usize, E>(
125        self,
126        domain: Domain<NDIM, NDOF, NId, E>,
127    ) -> Analysis<NDIM, NDOF, NId, E>
128    where
129        NId: Key,
130        E: ElementOps<NDIM, NDOF, NId>,
131    {
132        self.try_build(domain)
133            .unwrap_or_else(|error| panic!("invalid model: {error}"))
134    }
135
136    /// `build`, but a model that fails `Domain::validate` is returned as a
137    /// `ModelError` — what a caller that must not panic (the wasm session)
138    /// uses. Still panics on a `ConstraintHandler::Plain` / constraint
139    /// mismatch, which is a programming error rather than a model problem.
140    pub fn try_build<const NDIM: usize, const NDOF: usize, E>(
141        self,
142        mut domain: Domain<NDIM, NDOF, NId, E>,
143    ) -> Result<Analysis<NDIM, NDOF, NId, E>, ModelError>
144    where
145        NId: Key,
146        E: ElementOps<NDIM, NDOF, NId>,
147    {
148        assert!(
149            !(matches!(self.state.constraint_handler, ConstraintHandler::Plain) && domain.has_mp_constraints()),
150            "ConstraintHandler::Plain can't resolve multi-point constraints (equal_dof/rigid_diaphragm) — use ConstraintHandler::Transformation"
151        );
152        domain.validate()?;
153        Ok(Analysis {
154            domain,
155            constraint_handler: self.state.constraint_handler,
156            integrator: self.state.integrator,
157            algorithm: self.state.algorithm,
158            test: self.state.test,
159            solver: SparseSolver::new(),
160            step_count: 0,
161            load_factor: 0.0,
162            cached_factorization: None,
163            arc: None,
164            bordered: BorderedSolver::default(),
165        })
166    }
167}