1use nalgebra::{SMatrix, SVector};
2
3use super::super::{
4 Material, Node, Node3, Node3Id, NodeId, SpatialDof, ELEMENT_DOF, SPATIAL_ELEMENT_DOF,
5 SPATIAL_NDF, SPATIAL_NDIM,
6};
7
8pub type SpatialElementMatrix = SMatrix<f64, SPATIAL_ELEMENT_DOF, SPATIAL_ELEMENT_DOF>;
10pub type SpatialElementVector = SVector<f64, SPATIAL_ELEMENT_DOF>;
11
12#[derive(Debug, Clone)]
19pub struct Truss {
20 pub node_i: NodeId,
21 pub node_j: NodeId,
22 pub area: f64,
23 pub material: Material,
24 pub density: f64,
27}
28
29impl Truss {
30 pub fn new(node_i: NodeId, node_j: NodeId, area: f64, material: Material) -> Self {
31 Truss {
32 node_i,
33 node_j,
34 area,
35 material,
36 density: 0.0,
37 }
38 }
39
40 pub fn with_density(mut self, density: f64) -> Self {
41 self.density = density;
42 self
43 }
44
45 fn geometry(&self, node_i: &Node, node_j: &Node) -> (f64, f64, f64) {
46 let dx = node_j.coords[0] - node_i.coords[0];
47 let dy = node_j.coords[1] - node_i.coords[1];
48 let length = (dx * dx + dy * dy).sqrt();
49 (length, dx / length, dy / length)
50 }
51
52 fn strain(&self, node_i: &Node, node_j: &Node) -> f64 {
56 let (length, cx, cy) = self.geometry(node_i, node_j);
57 ((node_j.displacement[0] - node_i.displacement[0]) * cx
58 + (node_j.displacement[1] - node_i.displacement[1]) * cy)
59 / length
60 }
61
62 pub(super) fn form_tangent_and_resistance(
63 &self,
64 node_i: &Node,
65 node_j: &Node,
66 ) -> (
67 SMatrix<f64, ELEMENT_DOF, ELEMENT_DOF>,
68 SVector<f64, ELEMENT_DOF>,
69 ) {
70 let (length, cx, cy) = self.geometry(node_i, node_j);
71 let strain = self.strain(node_i, node_j);
72 let (stress, tangent_modulus) = self.material.trial_stress_tangent(strain);
73
74 let b = SVector::<f64, ELEMENT_DOF>::from_column_slice(&[-cx, -cy, 0.0, cx, cy, 0.0]);
77
78 let k = (tangent_modulus * self.area / length) * (b * b.transpose());
79 let resistance = (stress * self.area) * b;
80
81 (k, resistance)
82 }
83
84 pub(super) fn commit(&mut self, node_i: &Node, node_j: &Node) {
85 let strain = self.strain(node_i, node_j);
86 self.material = self.material.commit(strain);
87 }
88
89 pub(super) fn local_force(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
96 self.form_tangent_and_resistance(node_i, node_j).1
97 }
98
99 pub(super) fn form_mass(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
104 let (length, _cx, _cy) = self.geometry(node_i, node_j);
105 let half = self.density * self.area * length / 2.0;
106 SVector::<f64, ELEMENT_DOF>::from_column_slice(&[half, half, 0.0, half, half, 0.0])
107 }
108}
109
110#[derive(Debug, Clone)]
117pub struct Truss3 {
118 pub node_i: Node3Id,
119 pub node_j: Node3Id,
120 pub area: f64,
121 pub material: Material,
122 pub density: f64,
124}
125
126impl Truss3 {
127 pub fn new(node_i: Node3Id, node_j: Node3Id, area: f64, material: Material) -> Self {
128 Truss3 {
129 node_i,
130 node_j,
131 area,
132 material,
133 density: 0.0,
134 }
135 }
136
137 pub fn with_density(mut self, density: f64) -> Self {
138 self.density = density;
139 self
140 }
141
142 fn geometry(&self, node_i: &Node3, node_j: &Node3) -> (f64, [f64; SPATIAL_NDIM]) {
145 let dx = node_j.coords[0] - node_i.coords[0];
146 let dy = node_j.coords[1] - node_i.coords[1];
147 let dz = node_j.coords[2] - node_i.coords[2];
148 let length = (dx * dx + dy * dy + dz * dz).sqrt();
149 assert!(length > 0.0, "Truss3 endpoints must not coincide");
150 (length, [dx / length, dy / length, dz / length])
151 }
152
153 fn axial_strain(&self, node_i: &Node3, node_j: &Node3) -> f64 {
154 let (length, [cx, cy, cz]) = self.geometry(node_i, node_j);
155 let relative = (node_j.displacement[SpatialDof::Ux as usize]
156 - node_i.displacement[SpatialDof::Ux as usize])
157 * cx
158 + (node_j.displacement[SpatialDof::Uy as usize]
159 - node_i.displacement[SpatialDof::Uy as usize])
160 * cy
161 + (node_j.displacement[SpatialDof::Uz as usize]
162 - node_i.displacement[SpatialDof::Uz as usize])
163 * cz;
164 relative / length
165 }
166
167 pub fn form_tangent_and_resistance(
170 &self,
171 node_i: &Node3,
172 node_j: &Node3,
173 ) -> (SpatialElementMatrix, SpatialElementVector) {
174 let (length, [cx, cy, cz]) = self.geometry(node_i, node_j);
175 let strain = self.axial_strain(node_i, node_j);
176 let (stress, tangent_modulus) = self.material.trial_stress_tangent(strain);
177
178 let mut b = SpatialElementVector::zeros();
179 b[SpatialDof::Ux as usize] = -cx;
180 b[SpatialDof::Uy as usize] = -cy;
181 b[SpatialDof::Uz as usize] = -cz;
182 b[SPATIAL_NDF + SpatialDof::Ux as usize] = cx;
183 b[SPATIAL_NDF + SpatialDof::Uy as usize] = cy;
184 b[SPATIAL_NDF + SpatialDof::Uz as usize] = cz;
185
186 let tangent = (tangent_modulus * self.area / length) * (b.clone() * b.transpose());
187 let resistance = (stress * self.area) * b;
188 (tangent, resistance)
189 }
190
191 pub fn commit(&mut self, node_i: &Node3, node_j: &Node3) {
192 self.material = self.material.commit(self.axial_strain(node_i, node_j));
193 }
194
195 pub fn local_force(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
198 self.form_tangent_and_resistance(node_i, node_j).1
199 }
200
201 pub fn form_mass(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
205 let (length, _) = self.geometry(node_i, node_j);
206 let half = self.density * self.area * length / 2.0;
207 let mut mass = SpatialElementVector::zeros();
208 for dof in [SpatialDof::Ux, SpatialDof::Uy, SpatialDof::Uz] {
209 mass[dof as usize] = half;
210 mass[SPATIAL_NDF + dof as usize] = half;
211 }
212 mass
213 }
214}
215
216#[cfg(test)]
217mod spatial_tests {
218 use super::*;
219
220 #[test]
221 fn skew_truss_matches_direction_cosine_stiffness_and_resistance() {
222 let node_i = Node3::new([0.0, 0.0, 0.0]);
223 let mut node_j = Node3::new([2.0, 3.0, 6.0]);
224 node_j.displacement[SpatialDof::Ux as usize] = 0.004;
225 node_j.displacement[SpatialDof::Uy as usize] = 0.006;
226 node_j.displacement[SpatialDof::Uz as usize] = 0.012;
227
228 let truss = Truss3::new(
229 Node3Id::default(),
230 Node3Id::default(),
231 2.0,
232 Material::Elastic { e: 30_000.0 },
233 );
234 let (k, r) = truss.form_tangent_and_resistance(&node_i, &node_j);
235
236 assert!((r[6] - 240.0 / 7.0).abs() < 1e-12);
237 assert!((r[7] - 360.0 / 7.0).abs() < 1e-12);
238 assert!((r[8] - 720.0 / 7.0).abs() < 1e-12);
239 assert_eq!(r[3], 0.0);
240 assert_eq!(r[9], 0.0);
241
242 let mut u = SpatialElementVector::zeros();
243 u[6] = 0.004;
244 u[7] = 0.006;
245 u[8] = 0.012;
246 assert!((k * u - r).norm() < 1e-12);
247 }
248
249 #[test]
250 fn mass_is_lumped_over_three_translations_only() {
251 let node_i = Node3::new([0.0, 0.0, 0.0]);
252 let node_j = Node3::new([0.0, 0.0, 4.0]);
253 let truss = Truss3::new(
254 Node3Id::default(),
255 Node3Id::default(),
256 3.0,
257 Material::Elastic { e: 1.0 },
258 )
259 .with_density(2.0);
260 let mass = truss.form_mass(&node_i, &node_j);
261
262 for i in [0, 1, 2, 6, 7, 8] {
263 assert_eq!(mass[i], 12.0);
264 }
265 for i in [3, 4, 5, 9, 10, 11] {
266 assert_eq!(mass[i], 0.0);
267 }
268 }
269}