1use nalgebra::{SMatrix, SVector};
2
3use super::super::transform::Corotational2d;
4use super::super::{GeomTransf, GeomTransf3, Node, Node3, Node3Id, NodeId};
5use super::truss::{SpatialElementMatrix, SpatialElementVector};
6
7#[derive(Debug, Clone)]
13pub struct ElasticBeamColumn {
14 pub node_i: NodeId,
15 pub node_j: NodeId,
16 pub e: f64,
17 pub a: f64,
18 pub iz: f64,
19 pub transform: GeomTransf,
20 pub density: f64,
23}
24
25impl ElasticBeamColumn {
26 pub fn new(
27 node_i: NodeId,
28 node_j: NodeId,
29 e: f64,
30 a: f64,
31 iz: f64,
32 transform: GeomTransf,
33 ) -> Self {
34 ElasticBeamColumn {
35 node_i,
36 node_j,
37 e,
38 a,
39 iz,
40 transform,
41 density: 0.0,
42 }
43 }
44
45 pub fn with_density(mut self, density: f64) -> Self {
46 self.density = density;
47 self
48 }
49
50 fn geometry(&self, node_i: &Node, node_j: &Node) -> (f64, f64, f64) {
51 let dx = node_j.coords[0] - node_i.coords[0];
52 let dy = node_j.coords[1] - node_i.coords[1];
53 let length = (dx * dx + dy * dy).sqrt();
54 (length, dx / length, dy / length)
55 }
56
57 fn local_elastic_stiffness(&self, length: f64) -> SMatrix<f64, 6, 6> {
60 let (e, a, i) = (self.e, self.a, self.iz);
61 let l = length;
62 let ea_l = e * a / l;
63 let ei = e * i;
64
65 #[rustfmt::skip]
66 let k = SMatrix::<f64, 6, 6>::from_row_slice(&[
67 ea_l, 0.0, 0.0, -ea_l, 0.0, 0.0,
68 0.0, 12.0*ei/l.powi(3), 6.0*ei/l.powi(2), 0.0, -12.0*ei/l.powi(3), 6.0*ei/l.powi(2),
69 0.0, 6.0*ei/l.powi(2), 4.0*ei/l, 0.0, -6.0*ei/l.powi(2), 2.0*ei/l,
70 -ea_l, 0.0, 0.0, ea_l, 0.0, 0.0,
71 0.0, -12.0*ei/l.powi(3), -6.0*ei/l.powi(2), 0.0, 12.0*ei/l.powi(3), -6.0*ei/l.powi(2),
72 0.0, 6.0*ei/l.powi(2), 2.0*ei/l, 0.0, -6.0*ei/l.powi(2), 4.0*ei/l,
73 ]);
74 k
75 }
76
77 fn geometric_stiffness(&self, p: f64, length: f64) -> SMatrix<f64, 6, 6> {
88 let l = length;
89 let mut kg = SMatrix::<f64, 6, 6>::zeros();
90 #[rustfmt::skip]
91 let block = [
92 [ 6.0/5.0, l/10.0, -6.0/5.0, l/10.0],
93 [ l/10.0, 2.0*l*l/15.0, -l/10.0, -l*l/30.0],
94 [-6.0/5.0, -l/10.0, 6.0/5.0, -l/10.0],
95 [ l/10.0, -l*l/30.0, -l/10.0, 2.0*l*l/15.0],
96 ];
97 let indices = [1, 2, 4, 5];
98 for (bi, &gi) in indices.iter().enumerate() {
99 for (bj, &gj) in indices.iter().enumerate() {
100 kg[(gi, gj)] = (p / l) * block[bi][bj];
101 }
102 }
103 kg
104 }
105
106 fn transformation(&self, cx: f64, cy: f64) -> SMatrix<f64, 6, 6> {
108 #[rustfmt::skip]
109 let block = SMatrix::<f64, 3, 3>::new(
110 cx, cy, 0.0,
111 -cy, cx, 0.0,
112 0.0, 0.0, 1.0,
113 );
114 let mut t = SMatrix::<f64, 6, 6>::zeros();
115 t.fixed_view_mut::<3, 3>(0, 0).copy_from(&block);
116 t.fixed_view_mut::<3, 3>(3, 3).copy_from(&block);
117 t
118 }
119
120 pub(super) fn form_tangent_and_resistance(
121 &self,
122 node_i: &Node,
123 node_j: &Node,
124 ) -> (SMatrix<f64, 6, 6>, SVector<f64, 6>) {
125 if self.transform == GeomTransf::Corotational {
126 let transform = Corotational2d::new(node_i, node_j);
127 let l = transform.initial_length();
128 let ea_l = self.e * self.a / l;
129 let eiz_l = self.e * self.iz / l;
130 let k_basic = SMatrix::<f64, 3, 3>::new(
131 ea_l,
132 0.0,
133 0.0,
134 0.0,
135 4.0 * eiz_l,
136 2.0 * eiz_l,
137 0.0,
138 2.0 * eiz_l,
139 4.0 * eiz_l,
140 );
141 let q = k_basic * transform.basic_deformation();
142 return (
143 transform.global_tangent(&k_basic, &q),
144 transform.global_resistance(&q),
145 );
146 }
147
148 let (length, cx, cy) = self.geometry(node_i, node_j);
149 let t = self.transformation(cx, cy);
150 let k_local = self.local_elastic_stiffness(length);
151
152 let d_global = SVector::<f64, 6>::from_row_slice(&[
153 node_i.displacement[0],
154 node_i.displacement[1],
155 node_i.displacement[2],
156 node_j.displacement[0],
157 node_j.displacement[1],
158 node_j.displacement[2],
159 ]);
160 let d_local = t * d_global;
161
162 let resistance_local = k_local * d_local;
163 let resistance = t.transpose() * resistance_local;
164
165 let k_total_local = match self.transform {
166 GeomTransf::Linear => k_local,
167 GeomTransf::PDelta => {
168 let axial_force = resistance_local[3]; k_local + self.geometric_stiffness(axial_force, length)
170 }
171 GeomTransf::Corotational => unreachable!("handled above"),
172 };
173 let k = t.transpose() * k_total_local * t;
174
175 (k, resistance)
176 }
177
178 pub(super) fn local_force(&self, node_i: &Node, node_j: &Node) -> SVector<f64, 6> {
185 if self.transform == GeomTransf::Corotational {
186 let transform = Corotational2d::new(node_i, node_j);
187 let l = transform.initial_length();
188 let ea_l = self.e * self.a / l;
189 let eiz_l = self.e * self.iz / l;
190 let k_basic = SMatrix::<f64, 3, 3>::new(
191 ea_l,
192 0.0,
193 0.0,
194 0.0,
195 4.0 * eiz_l,
196 2.0 * eiz_l,
197 0.0,
198 2.0 * eiz_l,
199 4.0 * eiz_l,
200 );
201 let q = k_basic * transform.basic_deformation();
202 return transform.local_resistance(&q);
203 }
204
205 let (length, cx, cy) = self.geometry(node_i, node_j);
206 let t = self.transformation(cx, cy);
207 let k_local = self.local_elastic_stiffness(length);
208 let d_global = SVector::<f64, 6>::from_row_slice(&[
209 node_i.displacement[0],
210 node_i.displacement[1],
211 node_i.displacement[2],
212 node_j.displacement[0],
213 node_j.displacement[1],
214 node_j.displacement[2],
215 ]);
216 k_local * (t * d_global)
217 }
218
219 pub(super) fn form_load_vector(
227 &self,
228 node_i: &Node,
229 node_j: &Node,
230 wx: f64,
231 wy: f64,
232 ) -> SVector<f64, 6> {
233 if wx == 0.0 && wy == 0.0 {
234 return SVector::<f64, 6>::zeros();
235 }
236 if self.transform == GeomTransf::Corotational {
237 let transform = Corotational2d::new(node_i, node_j);
238 return transform.global_to_local().transpose()
239 * Self::uniform_load_local(transform.initial_length(), wx, wy);
240 }
241 let (length, cx, cy) = self.geometry(node_i, node_j);
242 let t = self.transformation(cx, cy);
243 t.transpose() * Self::uniform_load_local(length, wx, wy)
244 }
245
246 pub(super) fn form_local_load_vector(
249 &self,
250 node_i: &Node,
251 node_j: &Node,
252 wx: f64,
253 wy: f64,
254 ) -> SVector<f64, 6> {
255 if self.transform == GeomTransf::Corotational {
256 let length = Corotational2d::new(node_i, node_j).initial_length();
257 return Self::uniform_load_local(length, wx, wy);
258 }
259 let (length, _cx, _cy) = self.geometry(node_i, node_j);
260 Self::uniform_load_local(length, wx, wy)
261 }
262
263 fn uniform_load_local(l: f64, wx: f64, wy: f64) -> SVector<f64, 6> {
264 super::uniform_load::planar_local(l, wx, wy)
265 }
266
267 pub(super) fn form_mass(&self, node_i: &Node, node_j: &Node) -> SVector<f64, 6> {
273 let (length, _cx, _cy) = self.geometry(node_i, node_j);
274 let half = self.density * self.a * length / 2.0;
275 SVector::<f64, 6>::from_column_slice(&[half, half, 0.0, half, half, 0.0])
276 }
277}
278
279#[derive(Debug, Clone)]
293pub struct ElasticBeamColumn3 {
294 pub node_i: Node3Id,
295 pub node_j: Node3Id,
296 pub e: f64,
297 pub g: f64,
298 pub a: f64,
299 pub j: f64,
302 pub iy: f64,
305 pub iz: f64,
309 pub transform: GeomTransf3,
310 pub density: f64,
312}
313
314impl ElasticBeamColumn3 {
315 #[allow(clippy::too_many_arguments)]
316 pub fn new(
317 node_i: Node3Id,
318 node_j: Node3Id,
319 e: f64,
320 g: f64,
321 a: f64,
322 j: f64,
323 iy: f64,
324 iz: f64,
325 transform: GeomTransf3,
326 ) -> Self {
327 ElasticBeamColumn3 {
328 node_i,
329 node_j,
330 e,
331 g,
332 a,
333 j,
334 iy,
335 iz,
336 transform,
337 density: 0.0,
338 }
339 }
340
341 pub fn with_density(mut self, density: f64) -> Self {
342 self.density = density;
343 self
344 }
345
346 fn local_elastic_stiffness(&self, length: f64) -> SpatialElementMatrix {
356 let (e, g, a, j, iy, iz) = (self.e, self.g, self.a, self.j, self.iy, self.iz);
357 let l = length;
358 let mut k = SpatialElementMatrix::zeros();
359
360 let ea_l = e * a / l;
361 k[(0, 0)] = ea_l;
362 k[(6, 6)] = ea_l;
363 k[(0, 6)] = -ea_l;
364 k[(6, 0)] = -ea_l;
365
366 let gj_l = g * j / l;
367 k[(3, 3)] = gj_l;
368 k[(9, 9)] = gj_l;
369 k[(3, 9)] = -gj_l;
370 k[(9, 3)] = -gj_l;
371
372 let eiz = e * iz;
374 let z_dof = [1, 5, 7, 11];
375 #[rustfmt::skip]
376 let z_block = [
377 [ 12.0*eiz/l.powi(3), 6.0*eiz/l.powi(2), -12.0*eiz/l.powi(3), 6.0*eiz/l.powi(2)],
378 [ 6.0*eiz/l.powi(2), 4.0*eiz/l, -6.0*eiz/l.powi(2), 2.0*eiz/l],
379 [-12.0*eiz/l.powi(3), -6.0*eiz/l.powi(2), 12.0*eiz/l.powi(3), -6.0*eiz/l.powi(2)],
380 [ 6.0*eiz/l.powi(2), 2.0*eiz/l, -6.0*eiz/l.powi(2), 4.0*eiz/l],
381 ];
382 for (bi, &gi) in z_dof.iter().enumerate() {
383 for (bj, &gj) in z_dof.iter().enumerate() {
384 k[(gi, gj)] = z_block[bi][bj];
385 }
386 }
387
388 let eiy = e * iy;
391 let y_dof = [2, 4, 8, 10];
392 #[rustfmt::skip]
393 let y_block = [
394 [ 12.0*eiy/l.powi(3), -6.0*eiy/l.powi(2), -12.0*eiy/l.powi(3), -6.0*eiy/l.powi(2)],
395 [ -6.0*eiy/l.powi(2), 4.0*eiy/l, 6.0*eiy/l.powi(2), 2.0*eiy/l],
396 [-12.0*eiy/l.powi(3), 6.0*eiy/l.powi(2), 12.0*eiy/l.powi(3), 6.0*eiy/l.powi(2)],
397 [ -6.0*eiy/l.powi(2), 2.0*eiy/l, 6.0*eiy/l.powi(2), 4.0*eiy/l],
398 ];
399 for (bi, &gi) in y_dof.iter().enumerate() {
400 for (bj, &gj) in y_dof.iter().enumerate() {
401 k[(gi, gj)] = y_block[bi][bj];
402 }
403 }
404
405 k
406 }
407
408 fn geometric_stiffness(&self, p: f64, length: f64) -> SpatialElementMatrix {
425 let l = length;
426 let mut kg = SpatialElementMatrix::zeros();
427
428 let z_dof = [1, 5, 7, 11];
430 #[rustfmt::skip]
431 let z_block = [
432 [ 6.0/5.0, l/10.0, -6.0/5.0, l/10.0],
433 [ l/10.0, 2.0*l*l/15.0, -l/10.0, -l*l/30.0],
434 [-6.0/5.0, -l/10.0, 6.0/5.0, -l/10.0],
435 [ l/10.0, -l*l/30.0, -l/10.0, 2.0*l*l/15.0],
436 ];
437 for (bi, &gi) in z_dof.iter().enumerate() {
438 for (bj, &gj) in z_dof.iter().enumerate() {
439 kg[(gi, gj)] = (p / l) * z_block[bi][bj];
440 }
441 }
442
443 let y_dof = [2, 4, 8, 10];
447 #[rustfmt::skip]
448 let y_block = [
449 [ 6.0/5.0, -l/10.0, -6.0/5.0, -l/10.0],
450 [ -l/10.0, 2.0*l*l/15.0, l/10.0, -l*l/30.0],
451 [-6.0/5.0, l/10.0, 6.0/5.0, l/10.0],
452 [ -l/10.0, -l*l/30.0, l/10.0, 2.0*l*l/15.0],
453 ];
454 for (bi, &gi) in y_dof.iter().enumerate() {
455 for (bj, &gj) in y_dof.iter().enumerate() {
456 kg[(gi, gj)] = (p / l) * y_block[bi][bj];
457 }
458 }
459
460 kg
461 }
462
463 pub(super) fn form_tangent_and_resistance(
464 &self,
465 node_i: &Node3,
466 node_j: &Node3,
467 ) -> (SpatialElementMatrix, SpatialElementVector) {
468 let (length, r) = self.transform.local_axes(node_i, node_j);
469 let t = GeomTransf3::rotation_matrix(&r);
470 let k_local = self.local_elastic_stiffness(length);
471
472 let d_global = SpatialElementVector::from_iterator(
473 node_i
474 .displacement
475 .iter()
476 .chain(node_j.displacement.iter())
477 .copied(),
478 );
479 let d_local = t * d_global;
480
481 let resistance_local = k_local * d_local;
482 let resistance = t.transpose() * resistance_local;
483
484 let k_total_local = match self.transform {
485 GeomTransf3::Linear3 { .. } => k_local,
486 GeomTransf3::PDelta3 { .. } => {
487 let axial_force = resistance_local[6]; k_local + self.geometric_stiffness(axial_force, length)
489 }
490 };
491 let k = t.transpose() * k_total_local * t;
492
493 (k, resistance)
494 }
495
496 pub(super) fn local_force(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
500 let (length, r) = self.transform.local_axes(node_i, node_j);
501 let t = GeomTransf3::rotation_matrix(&r);
502 let k_local = self.local_elastic_stiffness(length);
503 let d_global = SpatialElementVector::from_iterator(
504 node_i
505 .displacement
506 .iter()
507 .chain(node_j.displacement.iter())
508 .copied(),
509 );
510 k_local * (t * d_global)
511 }
512
513 pub(super) fn form_load_vector(
531 &self,
532 node_i: &Node3,
533 node_j: &Node3,
534 wx: f64,
535 wy: f64,
536 wz: f64,
537 ) -> SpatialElementVector {
538 if wx == 0.0 && wy == 0.0 && wz == 0.0 {
539 return SpatialElementVector::zeros();
540 }
541 let (length, r) = self.transform.local_axes(node_i, node_j);
542 let t = GeomTransf3::rotation_matrix(&r);
543 t.transpose() * Self::uniform_load_local(length, wx, wy, wz)
544 }
545
546 pub(super) fn form_local_load_vector(
549 &self,
550 node_i: &Node3,
551 node_j: &Node3,
552 wx: f64,
553 wy: f64,
554 wz: f64,
555 ) -> SpatialElementVector {
556 let (length, _r) = self.transform.local_axes(node_i, node_j);
557 Self::uniform_load_local(length, wx, wy, wz)
558 }
559
560 fn uniform_load_local(l: f64, wx: f64, wy: f64, wz: f64) -> SpatialElementVector {
561 super::uniform_load::spatial_local(l, wx, wy, wz)
562 }
563
564 pub(super) fn form_mass(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
568 let (length, _r) = self.transform.local_axes(node_i, node_j);
569 let half = self.density * self.a * length / 2.0;
570 let mut mass = SpatialElementVector::zeros();
571 for dof in [0, 1, 2, 6, 7, 8] {
572 mass[dof] = half;
573 }
574 mass
575 }
576}
577
578#[cfg(test)]
579mod spatial_tests {
580 use nalgebra::Matrix3;
581
582 use super::*;
583
584 #[test]
595 fn axis_aligned_cantilever_matches_closed_form_axial_biaxial_bending_and_torsion() {
596 let (e, g, a, j, iy, iz) = (30_000.0, 12_000.0, 20.0, 5.0, 400.0, 800.0);
597 let length = 100.0;
598 let transform = GeomTransf3::linear([0.0, 0.0, 1.0]);
599 let beam = ElasticBeamColumn3::new(
600 Node3Id::default(),
601 Node3Id::default(),
602 e,
603 g,
604 a,
605 j,
606 iy,
607 iz,
608 transform,
609 );
610
611 let node_i = Node3::new([0.0, 0.0, 0.0]);
612 let node_j = Node3::new([length, 0.0, 0.0]);
613 let (geom_length, r) = beam.transform.local_axes(&node_i, &node_j);
614 assert_eq!(geom_length, length);
615 assert!(
616 (r - Matrix3::identity()).norm() < 1e-12,
617 "vec_xz=[0,0,1] with a member along x must give R = identity"
618 );
619
620 let k_local = beam.local_elastic_stiffness(length);
621
622 let k_free = k_local.fixed_view::<6, 6>(6, 6).into_owned();
625 let (axial_force, fy, fz, torque) = (100.0, 50.0, 30.0, 20.0);
626 let f = SVector::<f64, 6>::from_row_slice(&[axial_force, fy, fz, torque, 0.0, 0.0]);
628 let d = k_free
629 .lu()
630 .solve(&f)
631 .expect("k_free is nonsingular for a real prismatic section");
632
633 let expected_ux = axial_force * length / (e * a);
634 let expected_uy = fy * length.powi(3) / (3.0 * e * iz);
635 let expected_rz = fy * length.powi(2) / (2.0 * e * iz);
636 let expected_uz = fz * length.powi(3) / (3.0 * e * iy);
637 let expected_ry = -fz * length.powi(2) / (2.0 * e * iy);
638 let expected_rx = torque * length / (g * j);
639
640 let tol = 1e-9;
641 assert!(
642 (d[0] - expected_ux).abs() < tol,
643 "axial: {} vs {}",
644 d[0],
645 expected_ux
646 );
647 assert!(
648 (d[1] - expected_uy).abs() < tol,
649 "z-bending deflection: {} vs {}",
650 d[1],
651 expected_uy
652 );
653 assert!(
654 (d[5] - expected_rz).abs() < tol,
655 "z-bending rotation: {} vs {}",
656 d[5],
657 expected_rz
658 );
659 assert!(
660 (d[2] - expected_uz).abs() < tol,
661 "y-bending deflection: {} vs {}",
662 d[2],
663 expected_uz
664 );
665 assert!(
666 (d[4] - expected_ry).abs() < tol,
667 "y-bending rotation: {} vs {}",
668 d[4],
669 expected_ry
670 );
671 assert!(
672 (d[3] - expected_rx).abs() < tol,
673 "torsion: {} vs {}",
674 d[3],
675 expected_rx
676 );
677 }
678
679 #[test]
687 fn skew_cantilever_transform_round_trips_a_known_local_solution() {
688 let (e, g, a, j, iy, iz) = (30_000.0, 12_000.0, 20.0, 5.0, 400.0, 800.0);
689 let node_i = Node3::new([0.0, 0.0, 0.0]);
690 let node_j = Node3::new([3.0, 4.0, 12.0]); let transform = GeomTransf3::linear([1.0, 0.0, 0.0]);
692 let beam = ElasticBeamColumn3::new(
693 Node3Id::default(),
694 Node3Id::default(),
695 e,
696 g,
697 a,
698 j,
699 iy,
700 iz,
701 transform,
702 );
703
704 let (length, r) = beam.transform.local_axes(&node_i, &node_j);
705 assert!((length - 13.0).abs() < 1e-12);
706
707 let delta = 0.005;
709 let mut local_d = SVector::<f64, 6>::zeros();
710 local_d[0] = delta;
711 let global_d_j = r.transpose() * local_d.fixed_rows::<3>(0).into_owned();
712 let global_rot_j = r.transpose() * local_d.fixed_rows::<3>(3).into_owned();
713
714 let mut node_j_displaced = node_j.clone();
715 node_j_displaced.displacement[0] = global_d_j[0];
716 node_j_displaced.displacement[1] = global_d_j[1];
717 node_j_displaced.displacement[2] = global_d_j[2];
718 node_j_displaced.displacement[3] = global_rot_j[0];
719 node_j_displaced.displacement[4] = global_rot_j[1];
720 node_j_displaced.displacement[5] = global_rot_j[2];
721
722 let (_, resistance) = beam.form_tangent_and_resistance(&node_i, &node_j_displaced);
723
724 let expected_axial_force = e * a * delta / length;
727 let axial_disp_of_force = resistance
728 .fixed_rows::<3>(6)
729 .into_owned()
730 .dot(&r.row(0).transpose());
731 assert!((axial_disp_of_force - expected_axial_force).abs() < 1e-9);
732 }
733}