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}