1use nalgebra::{Matrix3, SMatrix, SVector, Vector3};
2
3use super::super::{
4 DofMask, FiberSection, FiberSection3, Material, Node, Node3, Node3Id, NodeId, ELEMENT_DOF, NDF,
5 SPATIAL_NDF,
6};
7use super::truss::{SpatialElementMatrix, SpatialElementVector};
8
9#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Orientation {
21 rotation: Matrix3<f64>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum OrientationError {
27 Degenerate,
29 OutOfPlane,
32}
33
34impl std::fmt::Display for OrientationError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 OrientationError::Degenerate => {
38 write!(
39 f,
40 "orient vectors are zero or parallel, so the local frame is undefined"
41 )
42 }
43 OrientationError::OutOfPlane => {
44 write!(
45 f,
46 "orient vectors must lie in the xy plane for a 2D element"
47 )
48 }
49 }
50 }
51}
52
53impl std::error::Error for OrientationError {}
54
55impl Orientation {
56 pub fn global() -> Self {
57 Orientation {
58 rotation: Matrix3::identity(),
59 }
60 }
61
62 pub fn new(x: [f64; 3], yp: [f64; 3]) -> Result<Self, OrientationError> {
63 let x = Vector3::from(x);
64 let yp = Vector3::from(yp);
65 let z = x.cross(&yp);
66 if x.norm() == 0.0 || z.norm() <= 1e-12 * x.norm() * yp.norm() {
69 return Err(OrientationError::Degenerate);
70 }
71 let ex = x.normalize();
72 let ez = z.normalize();
73 let ey = ez.cross(&ex);
74 Ok(Orientation {
75 rotation: Matrix3::from_rows(&[ex.transpose(), ey.transpose(), ez.transpose()]),
76 })
77 }
78
79 pub fn in_plane(x1: f64, x2: f64) -> Result<Self, OrientationError> {
83 Self::new([x1, x2, 0.0], [-x2, x1, 0.0])
84 }
85
86 fn frame3(&self) -> SMatrix<f64, SPATIAL_NDF, SPATIAL_NDF> {
89 let mut t = SMatrix::<f64, SPATIAL_NDF, SPATIAL_NDF>::zeros();
90 t.fixed_view_mut::<3, 3>(0, 0).copy_from(&self.rotation);
91 t.fixed_view_mut::<3, 3>(3, 3).copy_from(&self.rotation);
92 t
93 }
94
95 fn frame2(&self) -> Result<SMatrix<f64, NDF, NDF>, OrientationError> {
100 const TOL: f64 = 1e-9;
101 let r = &self.rotation;
102 if r[(0, 2)].abs() > TOL || r[(1, 2)].abs() > TOL {
103 return Err(OrientationError::OutOfPlane);
104 }
105 Ok(SMatrix::<f64, NDF, NDF>::new(
106 r[(0, 0)],
107 r[(0, 1)],
108 0.0,
109 r[(1, 0)],
110 r[(1, 1)],
111 0.0,
112 0.0,
113 0.0,
114 r[(2, 2)],
115 ))
116 }
117}
118
119fn local_relative<const N: usize>(
122 frame: &SMatrix<f64, N, N>,
123 dof: usize,
124 ui: &[f64; N],
125 uj: &[f64; N],
126) -> f64 {
127 (0..N).map(|c| frame[(dof, c)] * (uj[c] - ui[c])).sum()
128}
129
130fn frame_row_mask<const N: usize>(frame: &SMatrix<f64, N, N>, dof: usize) -> DofMask {
133 (0..N)
134 .filter(|&slot| frame[(dof, slot)].abs() > 1e-12)
135 .fold(DofMask::none(), |mask, slot| mask.with(slot))
136}
137
138fn frame_mask<const N: usize>(
139 frame: &SMatrix<f64, N, N>,
140 dofs: impl IntoIterator<Item = usize>,
141) -> DofMask {
142 dofs.into_iter().fold(DofMask::none(), |mask, dof| {
143 mask.union(frame_row_mask(frame, dof))
144 })
145}
146
147fn material_dofs<const N: usize>(
148 materials: &[Option<Material>; N],
149) -> impl Iterator<Item = usize> + '_ {
150 materials
151 .iter()
152 .enumerate()
153 .filter_map(|(dof, m)| m.is_some().then_some(dof))
154}
155
156fn dof_row_2d(frame: &SMatrix<f64, NDF, NDF>, dof: usize) -> SVector<f64, ELEMENT_DOF> {
159 let mut b = SVector::<f64, ELEMENT_DOF>::zeros();
160 for c in 0..NDF {
161 b[c] = -frame[(dof, c)];
162 b[NDF + c] = frame[(dof, c)];
163 }
164 b
165}
166
167fn dof_row_3d(
168 frame: &SMatrix<f64, SPATIAL_NDF, SPATIAL_NDF>,
169 dof: usize,
170) -> SVector<f64, { 2 * SPATIAL_NDF }> {
171 let mut b = SVector::<f64, { 2 * SPATIAL_NDF }>::zeros();
172 for c in 0..SPATIAL_NDF {
173 b[c] = -frame[(dof, c)];
174 b[SPATIAL_NDF + c] = frame[(dof, c)];
175 }
176 b
177}
178
179#[derive(Debug, Clone, Copy)]
209pub struct Friction {
210 pub normal_dof: usize,
211 pub shear_dof: usize,
212 pub mu: f64,
213 pub k0: f64,
214 pub b: f64,
227 slip: f64,
228}
229
230impl Friction {
231 pub fn new(normal_dof: usize, shear_dof: usize, mu: f64, k0: f64, b: f64) -> Self {
232 Friction {
233 normal_dof,
234 shear_dof,
235 mu,
236 k0,
237 b,
238 slip: 0.0,
239 }
240 }
241
242 fn evaluate(&self, normal_force: f64, shear_relative: f64) -> (f64, f64, f64, f64) {
249 let yield_force = self.mu * (-normal_force).max(0.0);
250 let trial = self.k0 * (shear_relative - self.slip);
251 if trial.abs() <= yield_force {
252 (trial, self.k0, 0.0, self.slip)
253 } else {
254 let force = (self.b * trial.abs() + (1.0 - self.b) * yield_force).copysign(trial);
255 let d_yield_d_normal = if normal_force < 0.0 { -self.mu } else { 0.0 };
260 let d_force_d_normal = (1.0 - self.b) * trial.signum() * d_yield_d_normal;
261 let next_slip = shear_relative - force / self.k0;
262 (force, self.b * self.k0, d_force_d_normal, next_slip)
263 }
264 }
265}
266
267#[derive(Debug, Clone, Copy)]
274pub struct Friction3 {
275 pub normal_dof: usize,
276 pub shear_dofs: [usize; 2],
277 pub mu: f64,
278 pub k0: f64,
279 pub b: f64,
281 slip: [f64; 2],
282}
283
284impl Friction3 {
285 pub fn new(normal_dof: usize, shear_dofs: [usize; 2], mu: f64, k0: f64, b: f64) -> Self {
286 Friction3 {
287 normal_dof,
288 shear_dofs,
289 mu,
290 k0,
291 b,
292 slip: [0.0, 0.0],
293 }
294 }
295
296 fn evaluate(
299 &self,
300 axis: usize,
301 normal_force: f64,
302 shear_relative: f64,
303 ) -> (f64, f64, f64, f64) {
304 let leg = Friction {
305 normal_dof: self.normal_dof,
306 shear_dof: self.shear_dofs[axis],
307 mu: self.mu,
308 k0: self.k0,
309 b: self.b,
310 slip: self.slip[axis],
311 };
312 leg.evaluate(normal_force, shear_relative)
313 }
314}
315
316#[derive(Debug, Clone)]
328pub struct ZeroLength {
329 pub node_i: NodeId,
330 pub node_j: NodeId,
331 materials: [Option<Material>; NDF],
332 friction: Option<Friction>,
333 frame: SMatrix<f64, NDF, NDF>,
334}
335
336impl ZeroLength {
337 pub fn new(node_i: NodeId, node_j: NodeId) -> Self {
338 ZeroLength {
339 node_i,
340 node_j,
341 materials: std::array::from_fn(|_| None),
342 friction: None,
343 frame: SMatrix::identity(),
344 }
345 }
346
347 pub fn with_material(mut self, dof: usize, material: Material) -> Self {
348 self.materials[dof] = Some(material);
349 self
350 }
351
352 pub fn with_orientation(mut self, orientation: Orientation) -> Result<Self, OrientationError> {
356 self.frame = orientation.frame2()?;
357 Ok(self)
358 }
359
360 pub fn with_friction(mut self, friction: Friction) -> Self {
365 debug_assert!(
366 self.materials[friction.shear_dof].is_none(),
367 "friction owns its shear DOF's resistance; don't also assign it a material"
368 );
369 debug_assert!(
370 self.materials[friction.normal_dof].is_some(),
371 "friction reads its normal DOF's force from that DOF's own material"
372 );
373 self.friction = Some(friction);
374 self
375 }
376
377 pub(super) fn dof_mask(&self) -> DofMask {
380 let friction = self
381 .friction
382 .iter()
383 .flat_map(|f| [f.normal_dof, f.shear_dof]);
384 frame_mask(&self.frame, material_dofs(&self.materials).chain(friction))
385 }
386
387 pub(super) fn form_tangent_and_resistance(
388 &self,
389 node_i: &Node,
390 node_j: &Node,
391 ) -> (
392 SMatrix<f64, ELEMENT_DOF, ELEMENT_DOF>,
393 SVector<f64, ELEMENT_DOF>,
394 ) {
395 let mut k = SMatrix::<f64, ELEMENT_DOF, ELEMENT_DOF>::zeros();
396 let mut resistance = SVector::<f64, ELEMENT_DOF>::zeros();
397
398 for (dof, material) in self.materials.iter().enumerate() {
399 let Some(material) = material else { continue };
400
401 let relative =
402 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
403 let (force, tangent_modulus) = material.trial_stress_tangent(relative);
404
405 let b = dof_row_2d(&self.frame, dof);
409
410 k += tangent_modulus * (b * b.transpose());
411 resistance += force * b;
412 }
413
414 if let Some(fr) = &self.friction {
415 let normal_material = self.materials[fr.normal_dof]
416 .as_ref()
417 .expect("friction needs a normal-direction material");
418 let normal_rel = local_relative(
419 &self.frame,
420 fr.normal_dof,
421 &node_i.displacement,
422 &node_j.displacement,
423 );
424 let (normal_force, normal_tangent) = normal_material.trial_stress_tangent(normal_rel);
425 let shear_rel = local_relative(
426 &self.frame,
427 fr.shear_dof,
428 &node_i.displacement,
429 &node_j.displacement,
430 );
431 let (force, dv_dshear, dv_dnormal, _) = fr.evaluate(normal_force, shear_rel);
432
433 let b_v = dof_row_2d(&self.frame, fr.shear_dof);
434 let b_n = dof_row_2d(&self.frame, fr.normal_dof);
435
436 k += dv_dshear * (b_v * b_v.transpose())
440 + (dv_dnormal * normal_tangent) * (b_v * b_n.transpose());
441 resistance += force * b_v;
442 }
443
444 (k, resistance)
445 }
446
447 pub(super) fn commit(&mut self, node_i: &Node, node_j: &Node) {
448 for (dof, material) in self.materials.iter_mut().enumerate() {
449 let Some(material) = material else { continue };
450 let relative =
451 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
452 *material = material.commit(relative);
453 }
454
455 if let Some(fr) = &mut self.friction {
456 let normal_rel = local_relative(
463 &self.frame,
464 fr.normal_dof,
465 &node_i.displacement,
466 &node_j.displacement,
467 );
468 let normal_force = self.materials[fr.normal_dof]
469 .as_ref()
470 .unwrap()
471 .trial_stress_tangent(normal_rel)
472 .0;
473 let shear_rel = local_relative(
474 &self.frame,
475 fr.shear_dof,
476 &node_i.displacement,
477 &node_j.displacement,
478 );
479 let (.., next_slip) = fr.evaluate(normal_force, shear_rel);
480 fr.slip = next_slip;
481 }
482 }
483
484 pub(super) fn local_force(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
489 self.form_tangent_and_resistance(node_i, node_j).1
490 }
491}
492
493#[derive(Debug, Clone)]
500pub struct ZeroLength3 {
501 pub node_i: Node3Id,
502 pub node_j: Node3Id,
503 materials: [Option<Material>; SPATIAL_NDF],
504 friction: Option<Friction3>,
505 frame: SMatrix<f64, SPATIAL_NDF, SPATIAL_NDF>,
506}
507
508impl ZeroLength3 {
509 pub fn new(node_i: Node3Id, node_j: Node3Id) -> Self {
510 ZeroLength3 {
511 node_i,
512 node_j,
513 materials: std::array::from_fn(|_| None),
514 friction: None,
515 frame: SMatrix::identity(),
516 }
517 }
518
519 pub fn with_material(mut self, dof: usize, material: Material) -> Self {
520 self.materials[dof] = Some(material);
521 self
522 }
523
524 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
527 self.frame = orientation.frame3();
528 self
529 }
530
531 pub fn with_friction(mut self, friction: Friction3) -> Self {
534 for shear_dof in friction.shear_dofs {
535 debug_assert!(
536 self.materials[shear_dof].is_none(),
537 "friction owns its shear DOFs' resistance; don't also assign them a material"
538 );
539 }
540 debug_assert!(
541 self.materials[friction.normal_dof].is_some(),
542 "friction reads its normal DOF's force from that DOF's own material"
543 );
544 self.friction = Some(friction);
545 self
546 }
547
548 pub(super) fn dof_mask(&self) -> DofMask {
551 let friction = self
552 .friction
553 .iter()
554 .flat_map(|f| [f.normal_dof, f.shear_dofs[0], f.shear_dofs[1]]);
555 frame_mask(&self.frame, material_dofs(&self.materials).chain(friction))
556 }
557
558 pub(super) fn form_tangent_and_resistance(
559 &self,
560 node_i: &Node3,
561 node_j: &Node3,
562 ) -> (SpatialElementMatrix, SpatialElementVector) {
563 let mut k = SpatialElementMatrix::zeros();
564 let mut resistance = SpatialElementVector::zeros();
565
566 for (dof, material) in self.materials.iter().enumerate() {
567 let Some(material) = material else { continue };
568
569 let relative =
570 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
571 let (force, tangent_modulus) = material.trial_stress_tangent(relative);
572
573 let b = dof_row_3d(&self.frame, dof);
574
575 k += tangent_modulus * (b * b.transpose());
576 resistance += force * b;
577 }
578
579 if let Some(fr) = &self.friction {
580 let normal_material = self.materials[fr.normal_dof]
581 .as_ref()
582 .expect("friction needs a normal-direction material");
583 let normal_rel = local_relative(
584 &self.frame,
585 fr.normal_dof,
586 &node_i.displacement,
587 &node_j.displacement,
588 );
589 let (normal_force, normal_tangent) = normal_material.trial_stress_tangent(normal_rel);
590
591 let b_n = dof_row_3d(&self.frame, fr.normal_dof);
592
593 for (axis, &shear_dof) in fr.shear_dofs.iter().enumerate() {
594 let shear_rel = local_relative(
595 &self.frame,
596 shear_dof,
597 &node_i.displacement,
598 &node_j.displacement,
599 );
600 let (force, dv_dshear, dv_dnormal, _) = fr.evaluate(axis, normal_force, shear_rel);
601
602 let b_v = dof_row_3d(&self.frame, shear_dof);
603
604 k += dv_dshear * (b_v * b_v.transpose())
605 + (dv_dnormal * normal_tangent) * (b_v * b_n.transpose());
606 resistance += force * b_v;
607 }
608 }
609
610 (k, resistance)
611 }
612
613 pub(super) fn commit(&mut self, node_i: &Node3, node_j: &Node3) {
614 for (dof, material) in self.materials.iter_mut().enumerate() {
615 let Some(material) = material else { continue };
616 let relative =
617 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
618 *material = material.commit(relative);
619 }
620
621 if let Some(fr) = &mut self.friction {
622 let normal_rel = local_relative(
625 &self.frame,
626 fr.normal_dof,
627 &node_i.displacement,
628 &node_j.displacement,
629 );
630 let normal_force = self.materials[fr.normal_dof]
631 .as_ref()
632 .unwrap()
633 .trial_stress_tangent(normal_rel)
634 .0;
635
636 let mut next_slip = fr.slip;
637 for (axis, &shear_dof) in fr.shear_dofs.iter().enumerate() {
638 let shear_rel = local_relative(
639 &self.frame,
640 shear_dof,
641 &node_i.displacement,
642 &node_j.displacement,
643 );
644 let (.., slip) = fr.evaluate(axis, normal_force, shear_rel);
645 next_slip[axis] = slip;
646 }
647 fr.slip = next_slip;
648 }
649 }
650
651 pub(super) fn local_force(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
653 self.form_tangent_and_resistance(node_i, node_j).1
654 }
655}
656
657#[derive(Debug, Clone)]
676pub struct ZeroLengthSection {
677 pub node_i: NodeId,
678 pub node_j: NodeId,
679 section: FiberSection,
680 materials: [Option<Material>; NDF],
681 frame: SMatrix<f64, NDF, NDF>,
682}
683
684impl ZeroLengthSection {
685 pub fn new(node_i: NodeId, node_j: NodeId, section: FiberSection) -> Self {
686 ZeroLengthSection {
687 node_i,
688 node_j,
689 section,
690 materials: std::array::from_fn(|_| None),
691 frame: SMatrix::identity(),
692 }
693 }
694
695 pub fn with_material(mut self, dof: usize, material: Material) -> Self {
699 self.materials[dof] = Some(material);
700 self
701 }
702
703 pub fn with_orientation(mut self, orientation: Orientation) -> Result<Self, OrientationError> {
706 self.frame = orientation.frame2()?;
707 Ok(self)
708 }
709
710 pub(super) fn dof_mask(&self) -> DofMask {
713 frame_mask(
714 &self.frame,
715 [0, 2].into_iter().chain(material_dofs(&self.materials)),
716 )
717 }
718
719 pub(super) fn form_tangent_and_resistance(
720 &self,
721 node_i: &Node,
722 node_j: &Node,
723 ) -> (
724 SMatrix<f64, ELEMENT_DOF, ELEMENT_DOF>,
725 SVector<f64, ELEMENT_DOF>,
726 ) {
727 let eps0 = local_relative(&self.frame, 0, &node_i.displacement, &node_j.displacement);
728 let kappa = local_relative(&self.frame, 2, &node_i.displacement, &node_j.displacement);
729 let (n, m, k_section) = self.section.trial(eps0, kappa);
730
731 let b_eps0 = dof_row_2d(&self.frame, 0);
732 let b_kappa = dof_row_2d(&self.frame, 2);
733
734 let mut k = k_section[0][0] * (b_eps0 * b_eps0.transpose())
735 + k_section[0][1] * (b_eps0 * b_kappa.transpose())
736 + k_section[1][0] * (b_kappa * b_eps0.transpose())
737 + k_section[1][1] * (b_kappa * b_kappa.transpose());
738 let mut resistance = b_eps0 * n + b_kappa * m;
739
740 for (dof, material) in self.materials.iter().enumerate() {
741 let Some(material) = material else { continue };
742
743 let relative =
744 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
745 let (force, tangent_modulus) = material.trial_stress_tangent(relative);
746
747 let b = dof_row_2d(&self.frame, dof);
748
749 k += tangent_modulus * (b * b.transpose());
750 resistance += force * b;
751 }
752
753 (k, resistance)
754 }
755
756 pub(super) fn commit(&mut self, node_i: &Node, node_j: &Node) {
757 let eps0 = local_relative(&self.frame, 0, &node_i.displacement, &node_j.displacement);
758 let kappa = local_relative(&self.frame, 2, &node_i.displacement, &node_j.displacement);
759 self.section.commit(eps0, kappa);
760
761 for (dof, material) in self.materials.iter_mut().enumerate() {
762 let Some(material) = material else { continue };
763 let relative =
764 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
765 *material = material.commit(relative);
766 }
767 }
768
769 pub(super) fn local_force(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
771 self.form_tangent_and_resistance(node_i, node_j).1
772 }
773}
774
775#[derive(Debug, Clone)]
784pub struct ZeroLengthSection3 {
785 pub node_i: Node3Id,
786 pub node_j: Node3Id,
787 section: FiberSection3,
788 materials: [Option<Material>; SPATIAL_NDF],
789 frame: SMatrix<f64, SPATIAL_NDF, SPATIAL_NDF>,
790}
791
792impl ZeroLengthSection3 {
793 pub fn new(node_i: Node3Id, node_j: Node3Id, section: FiberSection3) -> Self {
794 ZeroLengthSection3 {
795 node_i,
796 node_j,
797 section,
798 materials: std::array::from_fn(|_| None),
799 frame: SMatrix::identity(),
800 }
801 }
802
803 pub fn with_material(mut self, dof: usize, material: Material) -> Self {
807 self.materials[dof] = Some(material);
808 self
809 }
810
811 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
814 self.frame = orientation.frame3();
815 self
816 }
817
818 pub(super) fn dof_mask(&self) -> DofMask {
821 frame_mask(
822 &self.frame,
823 [0, 4, 5].into_iter().chain(material_dofs(&self.materials)),
824 )
825 }
826
827 pub(super) fn form_tangent_and_resistance(
828 &self,
829 node_i: &Node3,
830 node_j: &Node3,
831 ) -> (SpatialElementMatrix, SpatialElementVector) {
832 let eps0 = local_relative(&self.frame, 0, &node_i.displacement, &node_j.displacement);
833 let kappa_z = local_relative(&self.frame, 5, &node_i.displacement, &node_j.displacement);
834 let kappa_y = local_relative(&self.frame, 4, &node_i.displacement, &node_j.displacement);
835 let (n, mz, my, k_section) = self.section.trial(eps0, kappa_z, kappa_y);
836
837 let b_eps0 = dof_row_3d(&self.frame, 0);
838 let b_kappa_z = dof_row_3d(&self.frame, 5);
839 let b_kappa_y = dof_row_3d(&self.frame, 4);
840
841 let mut k = SpatialElementMatrix::zeros();
842 let mut resistance = b_eps0 * n + b_kappa_z * mz + b_kappa_y * my;
843
844 let b = [&b_eps0, &b_kappa_z, &b_kappa_y];
845 for (bi, row_i) in b.iter().enumerate() {
846 for (bj, row_j) in b.iter().enumerate() {
847 k += k_section[bi][bj] * (*row_i * row_j.transpose());
848 }
849 }
850
851 for (dof, material) in self.materials.iter().enumerate() {
852 let Some(material) = material else { continue };
853
854 let relative =
855 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
856 let (force, tangent_modulus) = material.trial_stress_tangent(relative);
857
858 let bm = dof_row_3d(&self.frame, dof);
859
860 k += tangent_modulus * (bm * bm.transpose());
861 resistance += force * bm;
862 }
863
864 (k, resistance)
865 }
866
867 pub(super) fn commit(&mut self, node_i: &Node3, node_j: &Node3) {
868 let eps0 = local_relative(&self.frame, 0, &node_i.displacement, &node_j.displacement);
869 let kappa_z = local_relative(&self.frame, 5, &node_i.displacement, &node_j.displacement);
870 let kappa_y = local_relative(&self.frame, 4, &node_i.displacement, &node_j.displacement);
871 self.section.commit(eps0, kappa_z, kappa_y);
872
873 for (dof, material) in self.materials.iter_mut().enumerate() {
874 let Some(material) = material else { continue };
875 let relative =
876 local_relative(&self.frame, dof, &node_i.displacement, &node_j.displacement);
877 *material = material.commit(relative);
878 }
879 }
880
881 pub(super) fn local_force(&self, node_i: &Node3, node_j: &Node3) -> SpatialElementVector {
883 self.form_tangent_and_resistance(node_i, node_j).1
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890 use crate::model::{Fiber, Fiber3, Material, Node};
891
892 #[test]
900 fn zero_length_dispatches_across_material_regimes() {
901 let node_i = Node::new([0.0, 0.0]);
902
903 let mut node_j = Node::new([0.0, 0.0]);
904 node_j.displacement[0] = -0.02; let epp = ZeroLength::new(NodeId::default(), NodeId::default())
909 .with_material(0, Material::elastic_pp(100.0, 0.01));
910 let (_, r) = epp.form_tangent_and_resistance(&node_i, &node_j);
911 assert_eq!(r[0], 100.0 * 0.01, "should be clamped to yield force");
912
913 let gap = ZeroLength::new(NodeId::default(), NodeId::default()).with_material(
914 0,
915 Material::Gap {
916 e: 100.0,
917 gap: 0.01,
918 },
919 );
920 let (_, r) = gap.form_tangent_and_resistance(&node_i, &node_j);
921 assert_eq!(r[0], 100.0 * (0.02 - 0.01), "gap engaged past closure");
922
923 let ent = ZeroLength::new(NodeId::default(), NodeId::default())
924 .with_material(0, Material::Ent { e: 100.0 });
925 let (_, r) = ent.form_tangent_and_resistance(&node_i, &node_j);
926 assert_eq!(r[0], 100.0 * 0.02, "ENT carries full compression");
927
928 let mut node_tension = Node::new([0.0, 0.0]);
929 node_tension.displacement[0] = 0.02; let (k, r) = ent.form_tangent_and_resistance(&node_i, &node_tension);
931 assert_eq!(r[0], 0.0, "ENT carries no tension");
932 assert_eq!(k[(0, 0)], 0.0, "no tangent stiffness while open");
933 }
934
935 #[test]
939 fn zero_length3_dispatches_independently_per_global_direction() {
940 let node_i = Node3::new([0.0, 0.0, 0.0]);
941 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
942 node_j.displacement[0] = 0.01; node_j.displacement[3] = -0.02; let zl = ZeroLength3::new(Node3Id::default(), Node3Id::default())
946 .with_material(0, Material::Elastic { e: 100.0 })
947 .with_material(3, Material::elastic_pp(50.0, 0.01));
948
949 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
950 assert_eq!(r[0], -(100.0 * 0.01), "Ux: linear elastic");
954 assert_eq!(r[SPATIAL_NDF], 100.0 * 0.01);
955 assert_eq!(
956 r[3],
957 50.0 * 0.01,
958 "Rx: clamped to yield force (sign follows relative disp)"
959 );
960 assert_eq!(r[SPATIAL_NDF + 3], -(50.0 * 0.01));
961 assert_eq!(k[(0, 0)], 100.0, "Ux tangent unaffected by Rx material");
962 assert_eq!(k[(3, 3)], 0.0, "Rx tangent is zero past yield");
963 for dof in [1, 2, 4, 5] {
964 assert_eq!(r[dof], 0.0, "unassigned directions carry no force");
965 }
966 }
967
968 #[test]
974 fn zero_length_section_reproduces_symmetric_ea_ei() {
975 let (e, area, iz): (f64, f64, f64) = (30000.0, 2.0, 1000.0);
976 let h = (iz / area).sqrt();
977 let section = FiberSection::new(vec![
978 Fiber::new(h, area / 2.0, Material::Elastic { e }),
979 Fiber::new(-h, area / 2.0, Material::Elastic { e }),
980 ]);
981
982 let node_i = Node::new([0.0, 0.0]);
983 let mut node_j = Node::new([0.0, 0.0]);
984 node_j.displacement[0] = 0.001; node_j.displacement[2] = 0.0002; let zl = ZeroLengthSection::new(NodeId::default(), NodeId::default(), section);
988 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
989
990 assert!(
991 (k[(0, 0)] - e * area).abs() < 1e-6,
992 "EA mismatch: {}",
993 k[(0, 0)]
994 );
995 assert!(
996 (k[(2, 2)] - e * iz).abs() < 1e-6,
997 "EI mismatch: {}",
998 k[(2, 2)]
999 );
1000 assert!(
1001 k[(0, 2)].abs() < 1e-9,
1002 "no axial-moment coupling for a symmetric section"
1003 );
1004
1005 let n = e * area * 0.001;
1006 let m = e * iz * 0.0002;
1007 assert!((r[3] - n).abs() < 1e-6, "node_j axial force");
1008 assert!((r[0] + n).abs() < 1e-6, "node_i axial force");
1009 assert!((r[5] - m).abs() < 1e-6, "node_j moment");
1010 assert!((r[2] + m).abs() < 1e-6, "node_i moment");
1011 assert_eq!(r[1], 0.0, "no material assigned to uy");
1012 }
1013
1014 #[test]
1019 fn zero_length_section_couples_axial_and_moment_for_an_asymmetric_section() {
1020 let (e, area, y): (f64, f64, f64) = (1000.0, 2.0, 3.0);
1021 let section = FiberSection::new(vec![Fiber::new(y, area, Material::Elastic { e })]);
1022
1023 let (_n, _m, k_section) = section.trial(0.001, 0.0002);
1024 assert!(
1025 k_section[0][1].abs() > 1e-9,
1026 "test setup: section must actually couple"
1027 );
1028
1029 let node_i = Node::new([0.0, 0.0]);
1030 let mut node_j = Node::new([0.0, 0.0]);
1031 node_j.displacement[0] = 0.001;
1032 node_j.displacement[2] = 0.0002;
1033
1034 let zl = ZeroLengthSection::new(NodeId::default(), NodeId::default(), section);
1035 let (k, _r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1036
1037 assert!(
1039 (k[(0, 2)] - k_section[0][1]).abs() < 1e-9,
1040 "axial-moment coupling should carry through to the element tangent"
1041 );
1042 }
1043
1044 #[test]
1050 fn zero_length_section3_reproduces_biaxial_ea_eiy_eiz() {
1051 let (e, area, iy, iz): (f64, f64, f64, f64) = (30000.0, 4.0, 500.0, 2000.0);
1052 let hz = (iy / area).sqrt();
1053 let hy = (iz / area).sqrt();
1054 let a4 = area / 4.0;
1055 let section = FiberSection3::new(vec![
1056 Fiber3::new(hy, hz, a4, Material::Elastic { e }),
1057 Fiber3::new(hy, -hz, a4, Material::Elastic { e }),
1058 Fiber3::new(-hy, hz, a4, Material::Elastic { e }),
1059 Fiber3::new(-hy, -hz, a4, Material::Elastic { e }),
1060 ]);
1061
1062 let node_i = Node3::new([0.0, 0.0, 0.0]);
1063 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
1064 node_j.displacement[0] = 0.001; node_j.displacement[5] = 0.0002; node_j.displacement[4] = 0.0003; let zl = ZeroLengthSection3::new(Node3Id::default(), Node3Id::default(), section);
1069 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1070
1071 assert!((k[(0, 0)] - e * area).abs() < 1e-6, "EA mismatch");
1072 assert!((k[(5, 5)] - e * iz).abs() < 1e-6, "EIz mismatch");
1073 assert!((k[(4, 4)] - e * iy).abs() < 1e-6, "EIy mismatch");
1074
1075 for dof in [1usize, 2, 3] {
1076 for local in [dof, SPATIAL_NDF + dof] {
1077 assert_eq!(r[local], 0.0, "unmodeled DOF carries no force");
1078 for col in 0..2 * SPATIAL_NDF {
1079 assert_eq!(k[(local, col)], 0.0, "unmodeled DOF has no stiffness");
1080 }
1081 }
1082 }
1083 }
1084
1085 #[test]
1091 fn zero_length_section_commit_propagates_fiber_history() {
1092 let mat = || Material::steel01(60.0, 29000.0, 0.01, 0.9, 5.0, 0.9, 5.0);
1093
1094 let section = FiberSection::new(vec![Fiber::new(0.0, 1.0, mat())]);
1097 let mut zl = ZeroLengthSection::new(NodeId::default(), NodeId::default(), section);
1098
1099 let mut hand = mat();
1103
1104 let node_i = Node::new([0.0, 0.0]);
1105 let mut node_j = Node::new([0.0, 0.0]);
1106 node_j.displacement[0] = 0.01; zl.commit(&node_i, &node_j);
1109 hand = hand.commit(0.01);
1110
1111 node_j.displacement[0] = 0.0;
1114 let (_k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1115
1116 let (expected_n, _tangent) = hand.trial_stress_tangent(0.0);
1117 assert!(
1118 expected_n.abs() > 1e-6,
1119 "test setup: material should have residual stress after yield"
1120 );
1121 assert!(
1122 (r[3] - expected_n).abs() < 1e-9,
1123 "committed plastic history should carry into subsequent trial"
1124 );
1125 }
1126
1127 #[test]
1131 fn zero_length_section_combines_with_an_independent_shear_spring() {
1132 let (e, area, iz): (f64, f64, f64) = (30000.0, 2.0, 1000.0);
1133 let h = (iz / area).sqrt();
1134 let section = FiberSection::new(vec![
1135 Fiber::new(h, area / 2.0, Material::Elastic { e }),
1136 Fiber::new(-h, area / 2.0, Material::Elastic { e }),
1137 ]);
1138 let shear_e = 500.0;
1139
1140 let zl = ZeroLengthSection::new(NodeId::default(), NodeId::default(), section)
1141 .with_material(1, Material::Elastic { e: shear_e });
1142
1143 let node_i = Node::new([0.0, 0.0]);
1144 let mut node_j = Node::new([0.0, 0.0]);
1145 node_j.displacement[0] = 0.001;
1146 node_j.displacement[1] = 0.002; node_j.displacement[2] = 0.0002;
1148
1149 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1150
1151 assert!((k[(1, 1)] - shear_e).abs() < 1e-9, "shear spring stiffness");
1152 assert_eq!(k[(1, 0)], 0.0, "shear spring uncoupled from axial");
1153 assert_eq!(k[(1, 2)], 0.0, "shear spring uncoupled from moment");
1154 assert!((r[4] - shear_e * 0.002).abs() < 1e-9, "node_j shear force");
1155
1156 assert!(
1158 (k[(0, 0)] - e * area).abs() < 1e-6,
1159 "EA unaffected by shear spring"
1160 );
1161 assert!(
1162 (k[(2, 2)] - e * iz).abs() < 1e-6,
1163 "EI unaffected by shear spring"
1164 );
1165 }
1166
1167 #[test]
1171 fn zero_length_section3_combines_with_an_independent_torsion_spring() {
1172 let (e, area, iy, iz): (f64, f64, f64, f64) = (30000.0, 4.0, 500.0, 2000.0);
1173 let hz = (iy / area).sqrt();
1174 let hy = (iz / area).sqrt();
1175 let a4 = area / 4.0;
1176 let section = FiberSection3::new(vec![
1177 Fiber3::new(hy, hz, a4, Material::Elastic { e }),
1178 Fiber3::new(hy, -hz, a4, Material::Elastic { e }),
1179 Fiber3::new(-hy, hz, a4, Material::Elastic { e }),
1180 Fiber3::new(-hy, -hz, a4, Material::Elastic { e }),
1181 ]);
1182 let torsion_e = 700.0;
1183
1184 let zl = ZeroLengthSection3::new(Node3Id::default(), Node3Id::default(), section)
1185 .with_material(3, Material::Elastic { e: torsion_e });
1186
1187 let node_i = Node3::new([0.0, 0.0, 0.0]);
1188 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
1189 node_j.displacement[0] = 0.001;
1190 node_j.displacement[3] = 0.004; node_j.displacement[5] = 0.0002;
1192 node_j.displacement[4] = 0.0003;
1193
1194 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1195
1196 assert!(
1197 (k[(3, 3)] - torsion_e).abs() < 1e-9,
1198 "torsion spring stiffness"
1199 );
1200 assert!((r[9] - torsion_e * 0.004).abs() < 1e-9, "node_j torque");
1201 assert!(
1202 (k[(0, 0)] - e * area).abs() < 1e-6,
1203 "EA unaffected by torsion spring"
1204 );
1205 assert!(
1206 (k[(5, 5)] - e * iz).abs() < 1e-6,
1207 "EIz unaffected by torsion spring"
1208 );
1209 assert!(
1210 (k[(4, 4)] - e * iy).abs() < 1e-6,
1211 "EIy unaffected by torsion spring"
1212 );
1213 }
1214
1215 fn friction_fixture() -> (Node, ZeroLength) {
1219 let node_i = Node::new([0.0, 0.0]);
1220 let zl = ZeroLength::new(NodeId::default(), NodeId::default())
1221 .with_material(0, Material::Elastic { e: 1000.0 })
1222 .with_friction(Friction::new(0, 1, 0.3, 500.0, 0.01));
1223 (node_i, zl)
1224 }
1225
1226 #[test]
1229 fn friction_sticks_below_yield_surface() {
1230 let (node_i, zl) = friction_fixture();
1231 let mut node_j = Node::new([0.0, 0.0]);
1232 node_j.displacement[0] = -1.0; node_j.displacement[1] = 0.1; let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1236 assert!(
1237 (r[NDF + 1] - 50.0).abs() < 1e-9,
1238 "V == k0*shear_rel while sticking"
1239 );
1240 assert!(
1241 (k[(1, 1)] - 500.0).abs() < 1e-9,
1242 "tangent == k0 while sticking"
1243 );
1244 }
1245
1246 #[test]
1250 fn friction_slides_past_yield_surface() {
1251 let (node_i, zl) = friction_fixture();
1252 let mut node_j = Node::new([0.0, 0.0]);
1253 node_j.displacement[0] = -1.0; node_j.displacement[1] = 5.0; let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1257
1258 let (mu, k0, b) = (0.3, 500.0, 0.01);
1259 let yield_force = mu * 1000.0;
1260 let trial = k0 * 5.0;
1261 let expected_v = b * trial + (1.0 - b) * yield_force;
1262
1263 assert!(
1264 (r[NDF + 1] - expected_v).abs() < 1e-9,
1265 "V matches the hardening-branch return map"
1266 );
1267 assert!(
1268 (k[(1, 1)] - b * k0).abs() < 1e-9,
1269 "tangent == b*k0 while sliding"
1270 );
1271 }
1272
1273 #[test]
1278 fn friction_yield_surface_scales_with_normal_force() {
1279 let (node_i, zl) = friction_fixture();
1280 let shear_rel = 1.0; let mut node_j_light = Node::new([0.0, 0.0]);
1283 node_j_light.displacement[0] = -0.5; node_j_light.displacement[1] = shear_rel;
1285
1286 let mut node_j_heavy = Node::new([0.0, 0.0]);
1287 node_j_heavy.displacement[0] = -2.0; node_j_heavy.displacement[1] = shear_rel;
1289
1290 let (_, r_light) = zl.form_tangent_and_resistance(&node_i, &node_j_light);
1291 let (_, r_heavy) = zl.form_tangent_and_resistance(&node_i, &node_j_heavy);
1292
1293 let (mu, k0, b) = (0.3, 500.0, 0.01);
1294 let expected_v_light = b * (k0 * shear_rel) + (1.0 - b) * (mu * 500.0);
1295 assert!(
1296 (r_light[NDF + 1] - expected_v_light).abs() < 1e-9,
1297 "lighter normal force: sliding"
1298 );
1299 assert!(
1300 (r_heavy[NDF + 1] - k0 * shear_rel).abs() < 1e-9,
1301 "heavier normal force: still sticking, V == trial"
1302 );
1303 }
1304
1305 #[test]
1310 fn friction_remembers_permanent_slip_after_commit() {
1311 let (node_i, mut zl) = friction_fixture();
1312 let (mu, k0, b) = (0.3, 500.0, 0.01);
1313
1314 let mut node_j = Node::new([0.0, 0.0]);
1315 node_j.displacement[0] = -1.0; node_j.displacement[1] = 5.0; zl.commit(&node_i, &node_j);
1318
1319 let yield_force = mu * 1000.0;
1320 let trial = k0 * 5.0;
1321 let force = b * trial + (1.0 - b) * yield_force;
1322 let committed_slip = 5.0 - force / k0;
1323
1324 node_j.displacement[1] = committed_slip + 0.05;
1326 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1327
1328 assert!(
1329 (r[NDF + 1] - k0 * 0.05).abs() < 1e-9,
1330 "elastic unload from committed slip, not from zero"
1331 );
1332 assert!(
1333 (k[(1, 1)] - k0).abs() < 1e-9,
1334 "tangent back to k0 (elastic) after unload"
1335 );
1336 }
1337
1338 #[test]
1342 fn friction_drops_capacity_when_normal_dof_is_in_tension() {
1343 let (node_i, zl) = friction_fixture();
1344 let mut node_j = Node::new([0.0, 0.0]);
1345 node_j.displacement[0] = 1.0; node_j.displacement[1] = 0.001;
1347
1348 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1349
1350 let (k0, b) = (500.0, 0.01);
1351 let expected_v = b * (k0 * 0.001); assert!(
1353 (r[NDF + 1] - expected_v).abs() < 1e-9,
1354 "near-zero force: no normal-direction capacity"
1355 );
1356 assert!(
1357 (k[(1, 1)] - b * k0).abs() < 1e-9,
1358 "post-slip tangent even at zero capacity"
1359 );
1360 }
1361
1362 #[test]
1371 fn friction_off_diagonal_tangent_matches_finite_difference() {
1372 let (node_i, zl) = friction_fixture();
1373 let mut node_j = Node::new([0.0, 0.0]);
1374 node_j.displacement[0] = -1.0; node_j.displacement[1] = 5.0; let (k, r0) = zl.form_tangent_and_resistance(&node_i, &node_j);
1378 let analytical = k[(1, 0)];
1379
1380 let h = 1e-6;
1381 let mut node_j_plus = node_j.clone();
1382 node_j_plus.displacement[0] += h;
1383 let (_, r_plus) = zl.form_tangent_and_resistance(&node_i, &node_j_plus);
1384
1385 let finite_diff = (r_plus[NDF + 1] - r0[NDF + 1]) / h;
1386
1387 assert!(
1388 (analytical - finite_diff).abs() < 1e-6,
1389 "analytical dV/d(normal_rel) = {analytical}, finite difference = {finite_diff}"
1390 );
1391 }
1392
1393 #[test]
1399 fn friction3_shear_axes_slide_independently_against_shared_normal_force() {
1400 let node_i = Node3::new([0.0, 0.0, 0.0]);
1401 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
1402 node_j.displacement[0] = -1.0; node_j.displacement[1] = 5.0; node_j.displacement[2] = 0.1; let (mu, k0, b) = (0.3, 500.0, 0.01);
1407 let zl = ZeroLength3::new(Node3Id::default(), Node3Id::default())
1408 .with_material(0, Material::Elastic { e: 1000.0 })
1409 .with_friction(Friction3::new(0, [1, 2], mu, k0, b));
1410
1411 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1412
1413 let yield_force = mu * 1000.0;
1414 let expected_v_uy = b * (k0 * 5.0) + (1.0 - b) * yield_force;
1415 let expected_v_uz = k0 * 0.1;
1416
1417 assert!(
1418 (r[SPATIAL_NDF + 1] - expected_v_uy).abs() < 1e-9,
1419 "Uy slides independently"
1420 );
1421 assert!(
1422 (r[SPATIAL_NDF + 2] - expected_v_uz).abs() < 1e-9,
1423 "Uz sticks independently"
1424 );
1425 assert!(
1426 (k[(1, 1)] - b * k0).abs() < 1e-9,
1427 "Uy tangent == b*k0 (sliding)"
1428 );
1429 assert!((k[(2, 2)] - k0).abs() < 1e-9, "Uz tangent == k0 (sticking)");
1430 assert_eq!(k[(1, 2)], 0.0, "no cross-talk between the two shear axes");
1431 assert_eq!(k[(2, 1)], 0.0, "no cross-talk between the two shear axes");
1432 }
1433
1434 fn approx(a: f64, b: f64) -> bool {
1435 (a - b).abs() < 1e-9
1436 }
1437
1438 #[test]
1441 fn oriented_zero_length_spring_follows_the_local_x_axis() {
1442 let orient = Orientation::in_plane(0.0, 1.0).unwrap();
1443 let zl = ZeroLength::new(NodeId::default(), NodeId::default())
1444 .with_material(0, Material::Elastic { e: 100.0 })
1445 .with_orientation(orient)
1446 .unwrap();
1447 let node_i = Node::new([0.0, 0.0]);
1448 let mut node_j = Node::new([0.0, 0.0]);
1449 node_j.displacement[1] = 0.01;
1450 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1451 assert!(approx(r[4], 1.0) && approx(r[1], -1.0), "uy force");
1452 assert!(approx(k[(4, 4)], 100.0), "uy stiffness");
1453 assert!(approx(k[(3, 3)], 0.0), "no ux stiffness");
1454 }
1455
1456 #[test]
1460 fn oriented_zero_length_at_45_degrees_splits_force_and_stays_consistent() {
1461 let orient = Orientation::in_plane(1.0, 1.0).unwrap();
1462 let zl = ZeroLength::new(NodeId::default(), NodeId::default())
1463 .with_material(0, Material::Elastic { e: 100.0 })
1464 .with_orientation(orient)
1465 .unwrap();
1466 let node_i = Node::new([0.0, 0.0]);
1467 let mut node_j = Node::new([0.0, 0.0]);
1468 node_j.displacement[0] = 0.02;
1469 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1470 assert!(approx(r[3], 100.0 * 0.02 * 0.5));
1471 assert!(approx(r[4], 100.0 * 0.02 * 0.5));
1472 let u = SVector::<f64, ELEMENT_DOF>::from_column_slice(&[0.0, 0.0, 0.0, 0.02, 0.0, 0.0]);
1473 assert!((k * u - r).norm() < 1e-9);
1474 }
1475
1476 #[test]
1477 fn oriented_zero_length_section_axis_follows_local_x() {
1478 let (e, area, iz): (f64, f64, f64) = (30000.0, 2.0, 1000.0);
1479 let h = (iz / area).sqrt();
1480 let section = FiberSection::new(vec![
1481 Fiber::new(h, area / 2.0, Material::Elastic { e }),
1482 Fiber::new(-h, area / 2.0, Material::Elastic { e }),
1483 ]);
1484 let orient = Orientation::in_plane(0.0, 1.0).unwrap();
1485 let zl = ZeroLengthSection::new(NodeId::default(), NodeId::default(), section)
1486 .with_orientation(orient)
1487 .unwrap();
1488 let node_i = Node::new([0.0, 0.0]);
1489 let mut node_j = Node::new([0.0, 0.0]);
1490 node_j.displacement[1] = 0.001;
1491 let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1492 assert!((k[(4, 4)] - e * area).abs() < 1e-6, "axial stiffness on uy");
1493 assert!(k[(3, 3)].abs() < 1e-9, "no axial stiffness on ux");
1494 assert!((r[4] - e * area * 0.001).abs() < 1e-6);
1495 }
1496
1497 #[test]
1498 fn oriented_zero_length_2d_rejects_out_of_plane_and_degenerate_frames() {
1499 assert_eq!(
1500 Orientation::in_plane(0.0, 0.0),
1501 Err(OrientationError::Degenerate)
1502 );
1503 assert_eq!(
1505 Orientation::in_plane(1.0, 2.0),
1506 Orientation::new([1.0, 2.0, 0.0], [-2.0, 1.0, 0.0])
1507 );
1508 assert_eq!(
1509 Orientation::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
1510 Err(OrientationError::Degenerate)
1511 );
1512 assert_eq!(
1513 Orientation::new([1.0, 0.0, 0.0], [2.0, 0.0, 0.0]),
1514 Err(OrientationError::Degenerate)
1515 );
1516 let tilted = Orientation::new([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]).unwrap();
1517 assert!(matches!(
1518 ZeroLength::new(NodeId::default(), NodeId::default()).with_orientation(tilted),
1519 Err(OrientationError::OutOfPlane)
1520 ));
1521 }
1522
1523 #[test]
1526 fn oriented_zero_length3_rotates_translations_and_rotations_together() {
1527 let orient = Orientation::new([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]).unwrap();
1528 let zl = ZeroLength3::new(Node3Id::default(), Node3Id::default())
1529 .with_material(0, Material::Elastic { e: 10.0 })
1530 .with_material(3, Material::Elastic { e: 7.0 })
1531 .with_orientation(orient);
1532 let node_i = Node3::new([0.0, 0.0, 0.0]);
1533 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
1534 node_j.displacement[2] = 0.1; node_j.displacement[5] = 0.2; let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1537 assert!(
1538 approx(r[SPATIAL_NDF + 2], 1.0),
1539 "uz force from the local x spring"
1540 );
1541 assert!(
1542 approx(r[SPATIAL_NDF + 5], 1.4),
1543 "rz moment from the local rx spring"
1544 );
1545 assert!(approx(r[SPATIAL_NDF], 0.0) && approx(r[SPATIAL_NDF + 3], 0.0));
1546 let mut u = SpatialElementVector::zeros();
1547 u[SPATIAL_NDF + 2] = 0.1;
1548 u[SPATIAL_NDF + 5] = 0.2;
1549 assert!((k * u - r).norm() < 1e-9);
1550 }
1551
1552 #[test]
1553 fn oriented_zero_length_section3_axis_follows_local_x() {
1554 let (e, area, iy, iz): (f64, f64, f64, f64) = (30000.0, 2.0, 800.0, 1000.0);
1555 let (hy, hz) = ((iz / area).sqrt(), (iy / area).sqrt());
1556 let section = FiberSection3::new(vec![
1557 Fiber3::new(hy, hz, area / 4.0, Material::Elastic { e }),
1558 Fiber3::new(-hy, hz, area / 4.0, Material::Elastic { e }),
1559 Fiber3::new(hy, -hz, area / 4.0, Material::Elastic { e }),
1560 Fiber3::new(-hy, -hz, area / 4.0, Material::Elastic { e }),
1561 ]);
1562 let orient = Orientation::new([0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]).unwrap();
1563 let zl = ZeroLengthSection3::new(Node3Id::default(), Node3Id::default(), section)
1564 .with_orientation(orient);
1565 let node_i = Node3::new([0.0, 0.0, 0.0]);
1566 let mut node_j = Node3::new([0.0, 0.0, 0.0]);
1567 node_j.displacement[1] = 0.001; let (k, r) = zl.form_tangent_and_resistance(&node_i, &node_j);
1569 assert!((k[(SPATIAL_NDF + 1, SPATIAL_NDF + 1)] - e * area).abs() < 1e-6);
1570 assert!(k[(SPATIAL_NDF, SPATIAL_NDF)].abs() < 1e-9);
1571 assert!((r[SPATIAL_NDF + 1] - e * area * 0.001).abs() < 1e-6);
1572 }
1573}