Skip to main content

carapace_core/model/
node.rs

1use slotmap::new_key_type;
2
3/// Planar coordinate components `[x, y]`.
4pub const PLANAR_NDIM: usize = 2;
5
6/// Spatial coordinate components `[x, y, z]`.
7pub const SPATIAL_NDIM: usize = 3;
8
9/// Spatial frame degrees of freedom `[ux, uy, uz, rx, ry, rz]`.
10pub const SPATIAL_NDF: usize = 6;
11
12/// Two-node spatial element DOFs: six at node i followed by six at node j.
13pub const SPATIAL_ELEMENT_DOF: usize = 2 * SPATIAL_NDF;
14
15/// Named spatial DOF indices, in the order used by every future spatial
16/// element/result/worker record.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(usize)]
19pub enum SpatialDof {
20    Ux = 0,
21    Uy = 1,
22    Uz = 2,
23    Rx = 3,
24    Ry = 4,
25    Rz = 5,
26}
27
28/// A global coordinate axis (`0..SPATIAL_NDIM`) — used where a spatial API
29/// needs to name one of `[x, y, z]` itself rather than a DOF (e.g.
30/// `Domain3::rigid_diaphragm_about`'s diaphragm-normal parameter), where a
31/// bare `usize` would silently also accept a rotational `SpatialDof` index.
32/// `as usize` gives the matching translational `SpatialDof`/coordinate
33/// index directly (`Axis3::Y as usize == SpatialDof::Uy as usize == 1`).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(usize)]
36pub enum Axis3 {
37    X = 0,
38    Y = 1,
39    Z = 2,
40}
41
42new_key_type! {
43    /// Generational index into the planar `Domain` node store. Stays valid
44    /// across removals of other nodes; using a stale key against a different
45    /// domain is a logic error the type cannot catch, but a use-after-free
46    /// within one domain's lifetime is.
47    pub struct NodeId;
48
49    /// Generational index for a future spatial `Domain3` node store. It is
50    /// distinct from `NodeId`, so planar elements cannot connect to spatial
51    /// nodes at compile time.
52    pub struct Node3Id;
53}
54
55/// A node in a concrete kinematic profile. Coordinates have `NDIM`
56/// components; state, boundary conditions, and mass have `NDOF` components.
57/// `Node` defaults to the existing planar `(2, 3)` profile, so its public API
58/// remains source-compatible; `Node3` is the spatial `(3, 6)` specialization.
59///
60/// `velocity`/`acceleration` are only meaningful for/written by
61/// `TransientAnalysis` — static `Analysis` never touches them. Applied load
62/// is not here; it belongs to `Domain` load patterns because one node can
63/// carry independently scaled loads across phases.
64#[derive(Debug, Clone)]
65pub struct Node<const NDIM: usize = PLANAR_NDIM, const NDOF: usize = 3> {
66    pub coords: [f64; NDIM],
67    pub fixed: [bool; NDOF],
68    pub displacement: [f64; NDOF],
69    pub velocity: [f64; NDOF],
70    pub acceleration: [f64; NDOF],
71    /// Lumped nodal mass per DOF — a user-assigned point mass, additive with
72    /// any element-consistent lumped mass.
73    pub mass: [f64; NDOF],
74}
75
76/// Explicit planar node specialization, useful where a profile must be named
77/// rather than inferred from `Node`'s default generic parameters.
78pub type Node2 = Node<PLANAR_NDIM, 3>;
79
80/// Explicit spatial node specialization. It shares the node implementation
81/// with planar `Node`; only fixed array dimensions differ.
82pub type Node3 = Node<SPATIAL_NDIM, SPATIAL_NDF>;
83
84impl<const NDIM: usize, const NDOF: usize> Node<NDIM, NDOF> {
85    pub fn new(coords: [f64; NDIM]) -> Self {
86        Node {
87            coords,
88            fixed: [false; NDOF],
89            displacement: [0.0; NDOF],
90            velocity: [0.0; NDOF],
91            acceleration: [0.0; NDOF],
92            mass: [0.0; NDOF],
93        }
94    }
95
96    pub fn fix(mut self, dof: usize) -> Self {
97        self.fixed[dof] = true;
98        self
99    }
100
101    pub fn with_mass(mut self, dof: usize, value: f64) -> Self {
102        self.mass[dof] = value;
103        self
104    }
105
106    /// Sets an initial displacement without solving for it.
107    pub fn with_initial_displacement(mut self, dof: usize, value: f64) -> Self {
108        self.displacement[dof] = value;
109        self
110    }
111
112    pub fn with_initial_velocity(mut self, dof: usize, value: f64) -> Self {
113        self.velocity[dof] = value;
114        self
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn planar_and_spatial_aliases_share_one_node_implementation() {
124        let planar: Node2 = Node::new([1.0, 2.0]).fix(2);
125        assert_eq!(planar.coords, [1.0, 2.0]);
126        assert!(planar.fixed[2]);
127        assert_eq!(planar.displacement.len(), 3);
128
129        let spatial = Node3::new([1.0, 2.0, 3.0])
130            .fix(SpatialDof::Rz as usize)
131            .with_mass(SpatialDof::Uz as usize, 4.0);
132        assert_eq!(spatial.coords, [1.0, 2.0, 3.0]);
133        assert!(spatial.fixed[SpatialDof::Rz as usize]);
134        assert_eq!(spatial.mass[SpatialDof::Uz as usize], 4.0);
135        assert_eq!(spatial.displacement.len(), 6);
136    }
137}