Skip to main content

carapace_core/model/elements/
ops.rs

1//! The interface between `Domain` and an element catalog.
2//!
3//! An element reports which nodes it connects and which node DOF slots it
4//! stiffens, and *pushes* its own contributions (tangent and resistance,
5//! equivalent loads, lumped mass) through a sink that is generic over the
6//! element's matrix size. `Domain` therefore never assumes a node count or
7//! an element DOF count: a 6x6 truss and an 8x8 quadrilateral can live in
8//! one catalog with no padding, and every element keeps fixed-size
9//! (`SMatrix`) math and a closed-enum dispatch (no `dyn`, no heap in the
10//! assembly loop).
11
12use arrayvec::ArrayVec;
13use nalgebra::{SMatrix, SVector};
14use slotmap::{Key, SlotMap};
15
16use crate::model::Node;
17
18/// The most nodes any element in this crate connects: two-node frame
19/// elements, three- and four-node planar elements (2D membranes, 3D shell
20/// triangles and quadrilaterals). 3D solid elements are not planned.
21pub const MAX_ELEMENT_NODES: usize = 4;
22
23/// The nodes an element connects, in the element's own local order.
24pub type NodeList<NId> = ArrayVec<NId, MAX_ELEMENT_NODES>;
25
26/// One element DOF: `(node, slot)`, where `slot` indexes the node's
27/// profile DOFs (`ux, uy, rz` in 2D; `ux..rz` in 3D).
28pub type DofRef<NId> = (NId, u8);
29
30/// Which node DOF slots an element touches, one bit per slot. One mask per
31/// element, applied to every node the element connects.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct DofMask(u8);
34
35impl DofMask {
36    /// Every one of a node's first `ndof` slots.
37    pub const fn all(ndof: usize) -> Self {
38        assert!(ndof <= 8);
39        DofMask(((1u16 << ndof) - 1) as u8)
40    }
41
42    /// No slots.
43    pub const fn none() -> Self {
44        DofMask(0)
45    }
46
47    /// This mask plus `slot`.
48    pub const fn with(self, slot: usize) -> Self {
49        assert!(slot < 8);
50        DofMask(self.0 | (1 << slot))
51    }
52
53    /// Every slot in either mask.
54    pub const fn union(self, other: DofMask) -> Self {
55        DofMask(self.0 | other.0)
56    }
57
58    pub const fn contains(self, slot: usize) -> bool {
59        slot < 8 && (self.0 >> slot) & 1 == 1
60    }
61}
62
63/// Read access to the domain's nodes, handed to every element method that
64/// needs node coordinates or state. A thin wrapper so the element interface
65/// does not expose how `Domain` stores nodes.
66pub struct NodeView<'a, const NDIM: usize, const NDOF: usize, NId: Key> {
67    nodes: &'a SlotMap<NId, Node<NDIM, NDOF>>,
68}
69
70impl<'a, const NDIM: usize, const NDOF: usize, NId: Key> NodeView<'a, NDIM, NDOF, NId> {
71    pub(crate) fn new(nodes: &'a SlotMap<NId, Node<NDIM, NDOF>>) -> Self {
72        NodeView { nodes }
73    }
74
75    pub fn get(&self, id: NId) -> &'a Node<NDIM, NDOF> {
76        &self.nodes[id]
77    }
78}
79
80/// Receives an element's tangent stiffness `k` and internal resisting force
81/// `r`, indexed by the element's DOFs `dofs` (row `a` of `k` and entry `a`
82/// of `r` belong to `dofs[a]`).
83pub trait TangentSink<NId> {
84    fn add<const N: usize>(
85        &mut self,
86        dofs: &[DofRef<NId>; N],
87        k: &SMatrix<f64, N, N>,
88        r: &SVector<f64, N>,
89    );
90}
91
92/// Receives a vector indexed by an element's DOFs: an equivalent nodal load
93/// or a lumped mass.
94pub trait VectorSink<NId> {
95    fn add<const N: usize>(&mut self, dofs: &[DofRef<NId>; N], v: &SVector<f64, N>);
96}
97
98/// DOFs of a two-node element laid out as `[all of node i's slots, all of
99/// node j's slots]`; `N` must equal `2 * ndof`.
100pub fn two_node_dofs<NId: Copy, const N: usize>(i: NId, j: NId, ndof: usize) -> [DofRef<NId>; N] {
101    debug_assert_eq!(N, 2 * ndof);
102    std::array::from_fn(|a| {
103        if a < ndof {
104            (i, a as u8)
105        } else {
106            (j, (a - ndof) as u8)
107        }
108    })
109}
110
111/// An element's local nodal force (or equivalent-load) vector, for results
112/// recording. Its length is `ElementOps::local_force_width`. Heap-backed:
113/// this is only ever used on recorder paths, never in assembly.
114#[derive(Debug, Clone, PartialEq)]
115pub struct ElementForce(Vec<f64>);
116
117impl ElementForce {
118    pub fn from_svector<const N: usize>(v: &SVector<f64, N>) -> Self {
119        ElementForce(v.iter().copied().collect())
120    }
121
122    pub fn len(&self) -> usize {
123        self.0.len()
124    }
125
126    pub fn is_empty(&self) -> bool {
127        self.0.is_empty()
128    }
129
130    pub fn iter(&self) -> std::slice::Iter<'_, f64> {
131        self.0.iter()
132    }
133
134    pub fn as_slice(&self) -> &[f64] {
135        &self.0
136    }
137
138    /// `self -= factor * other`, component by component.
139    pub fn subtract_scaled(&mut self, factor: f64, other: &ElementForce) {
140        assert_eq!(self.0.len(), other.0.len());
141        for (a, b) in self.0.iter_mut().zip(&other.0) {
142            *a -= factor * b;
143        }
144    }
145}
146
147impl<const N: usize> From<SVector<f64, N>> for ElementForce {
148    fn from(v: SVector<f64, N>) -> Self {
149        ElementForce::from_svector(&v)
150    }
151}
152
153impl std::ops::Index<usize> for ElementForce {
154    type Output = f64;
155    fn index(&self, index: usize) -> &f64 {
156        &self.0[index]
157    }
158}
159
160/// One integration point's committed strain and stress, for continuum
161/// elements. Plane elements report `[x, y, xy]` with engineering shear
162/// (`PlaneMaterial`'s convention).
163#[derive(Debug, Clone, Copy, PartialEq)]
164pub struct GaussResponse {
165    pub strain: [f64; 3],
166    pub stress: [f64; 3],
167}
168
169/// One shell integration point's committed generalized strain and stress resultants in the
170/// element's local axes, in `ShellSection`'s ordering and sign convention:
171/// `[eps_x, eps_y, gamma_xy, kappa_x, kappa_y, 2 kappa_xy, gamma_xz, gamma_yz]` and
172/// `[Nx, Ny, Nxy, Mx, My, Mxy, Qx, Qy]`.
173#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct ShellResponse {
175    pub strain: [f64; 8],
176    pub resultant: [f64; 8],
177}
178
179/// What `Domain`'s generic assembly/state plumbing needs from an element
180/// catalog. A trait rather than a shared enum because `Element` (2D) and
181/// `Element3` (3D) hold different variant sets, and a single enum would
182/// either duplicate every variant or need `Box<dyn Trait>` in the assembly
183/// loop. `NDIM`/`NDOF` are separate const generics (not computed from each
184/// other) because stable Rust cannot evaluate `2 * NDOF` in a generic item.
185pub trait ElementOps<const NDIM: usize, const NDOF: usize, NId: Key> {
186    /// An element-load kind this catalog supports. `Add` so several loads on
187    /// one element in one pattern accumulate.
188    type Load: Copy
189        + crate::model::ElementLoadComponents
190        + std::ops::Add<Output = Self::Load>
191        + std::ops::Mul<f64, Output = Self::Load>;
192
193    /// This catalog's own `Domain` element-store key. An associated type
194    /// rather than a further generic parameter of `Domain`, so it is always
195    /// determined by `E` and never an independently unconstrained type
196    /// variable (which broke inference at every `Domain::new()`).
197    type Id: Key;
198
199    /// The nodes this element connects, in local order.
200    fn nodes(&self) -> NodeList<NId>;
201
202    /// The node DOF slots this element stiffens (the same for each node).
203    /// Must cover every slot on which `assemble_tangent` can produce a
204    /// nonzero stiffness or resistance: a slot outside the mask that no
205    /// other element, constraint or boundary condition uses is not an
206    /// equation at all, so such a contribution would be silently dropped
207    /// (debug builds assert against it).
208    fn dof_mask(&self) -> DofMask;
209
210    /// Rejects an element whose geometry or parameters make it unusable
211    /// (coincident nodes, inverted orientation, ...); called by
212    /// `Domain::validate`. The default accepts everything.
213    fn validate(&self, _nodes: &NodeView<'_, NDIM, NDOF, NId>) -> Result<(), &'static str> {
214        Ok(())
215    }
216
217    /// Whether this element can carry every kind of load present in `load`.
218    /// Checked by `Domain::validate` on each pattern's load, before anything is
219    /// accumulated; the default accepts everything.
220    fn accepts_load(&self, _load: &Self::Load) -> bool {
221        true
222    }
223
224    /// Caches geometry-only quantities (shape-function derivatives, areas)
225    /// once the model is final; called by `Domain::validate` after `validate`
226    /// succeeds. Must be idempotent, and every method must give the same
227    /// answer whether or not it has been called (the default does nothing).
228    fn prepare(&mut self, _nodes: &NodeView<'_, NDIM, NDOF, NId>) {}
229
230    /// Tangent and internal resisting force at the current nodal state.
231    /// `load` is the effective element load at the pseudo-time being
232    /// assembled (every pattern's load on this element, scaled by its
233    /// factor). Only elements whose internal state depends on the load
234    /// itself (`ForceBeamColumn`) read it; for every other element loads
235    /// enter only through `assemble_load`.
236    fn assemble_tangent<S: TangentSink<NId>>(
237        &self,
238        nodes: &NodeView<'_, NDIM, NDOF, NId>,
239        load: Option<&Self::Load>,
240        sink: &mut S,
241    );
242
243    /// The equivalent nodal load vector of `load` (zero if `load` is
244    /// `None` or of a kind this element does not carry).
245    fn assemble_load<S: VectorSink<NId>>(
246        &self,
247        nodes: &NodeView<'_, NDIM, NDOF, NId>,
248        load: Option<&Self::Load>,
249        sink: &mut S,
250    );
251
252    /// Lumped (diagonal) mass, per element DOF.
253    fn assemble_mass<S: VectorSink<NId>>(
254        &self,
255        nodes: &NodeView<'_, NDIM, NDOF, NId>,
256        sink: &mut S,
257    );
258
259    /// Commit this element's state at the converged nodal state. `load` is
260    /// the effective element load at the committed pseudo-time.
261    fn commit(&mut self, nodes: &NodeView<'_, NDIM, NDOF, NId>, load: Option<&Self::Load>);
262
263    /// Length of `local_force`/`local_load_force`; the bound a recorder's
264    /// `component` index is checked against.
265    fn local_force_width(&self) -> usize;
266
267    /// This element's local nodal force at its current committed state,
268    /// in the element's own axis frame (the original fixed orientation for
269    /// `Linear`/`PDelta`, the current chord for `Corotational`). Never
270    /// mutates state.
271    fn local_force(&self, nodes: &NodeView<'_, NDIM, NDOF, NId>) -> ElementForce;
272
273    /// `assemble_load`'s equivalent nodal load in the same local frame as
274    /// `local_force`; what `Domain::element_end_force` subtracts so a
275    /// loaded member's end forces include the load's fixed-end effect.
276    fn local_load_force(
277        &self,
278        nodes: &NodeView<'_, NDIM, NDOF, NId>,
279        load: Option<&Self::Load>,
280    ) -> ElementForce;
281
282    /// How many Gauss points `gauss_responses` reports (zero for elements that
283    /// are not continuum elements); the bound a Gauss-point recorder's `point`
284    /// is checked against.
285    fn gauss_point_count(&self) -> usize {
286        0
287    }
288
289    /// Components per Gauss-point strain or stress vector (3 for plane elements, 8 for shells);
290    /// the bound a Gauss-point recorder's `component` is checked against.
291    fn gauss_component_count(&self) -> usize {
292        3
293    }
294
295    /// Every Gauss point's committed strain and stress; `None` for every
296    /// element that is not a continuum element.
297    fn gauss_responses(
298        &self,
299        _nodes: &NodeView<'_, NDIM, NDOF, NId>,
300    ) -> Option<Vec<GaussResponse>> {
301        None
302    }
303
304    /// Every Gauss point's committed generalized strain and resultants; `None` for every
305    /// element that is not a shell.
306    fn shell_responses(
307        &self,
308        _nodes: &NodeView<'_, NDIM, NDOF, NId>,
309    ) -> Option<Vec<ShellResponse>> {
310        None
311    }
312
313    /// Every integration point's per-fiber `(strain, stress)`; `None` for
314    /// every element that is not fiber-discretized.
315    fn fiber_responses(
316        &self,
317        nodes: &NodeView<'_, NDIM, NDOF, NId>,
318    ) -> Option<Vec<Vec<(f64, f64)>>>;
319}