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}