1use nalgebra::{SMatrix, SVector};
2use slotmap::new_key_type;
3
4use super::{
5 ElementLoad, ElementLoad3, Node, Node3, Node3Id, NodeId, ELEMENT_DOF, NDF, PLANAR_NDIM,
6 SPATIAL_ELEMENT_DOF, SPATIAL_NDF, SPATIAL_NDIM,
7};
8
9mod ops;
10pub use ops::{
11 two_node_dofs, DofMask, DofRef, ElementForce, ElementOps, GaussResponse, NodeList, NodeView,
12 ShellResponse, TangentSink, VectorSink, MAX_ELEMENT_NODES,
13};
14
15mod disp_beam_column;
16mod elastic_beam_column;
17mod force_beam_column;
18mod plane_common;
19mod quad4;
20mod shell3;
21mod shell4;
22mod tri3;
23mod truss;
24mod uniform_load;
25mod zero_length;
26
27pub use disp_beam_column::{DispBeamColumn, DispBeamColumn3};
28pub use elastic_beam_column::{ElasticBeamColumn, ElasticBeamColumn3};
29pub use force_beam_column::{ForceBeamColumn, ForceBeamColumn3};
30pub use quad4::{Quad4, Quad4Formulation};
31pub use shell3::Shell3;
32pub use shell4::Shell4;
33pub use tri3::Tri3;
34pub use truss::{SpatialElementMatrix, SpatialElementVector, Truss, Truss3};
35pub use zero_length::{
36 Friction, Friction3, Orientation, OrientationError, ZeroLength, ZeroLength3, ZeroLengthSection,
37 ZeroLengthSection3,
38};
39
40new_key_type! {
41 pub struct ElementId;
43
44 pub struct Element3Id;
48}
49
50#[derive(Debug, Clone)]
53pub enum Element {
54 Truss(Truss),
55 ZeroLength(ZeroLength),
56 ZeroLengthSection(ZeroLengthSection),
57 ElasticBeamColumn(ElasticBeamColumn),
58 DispBeamColumn(DispBeamColumn),
59 ForceBeamColumn(ForceBeamColumn),
60 Tri3(Tri3),
61 Quad4(Quad4),
62}
63
64const CONTINUUM: &str = "continuum elements assemble through ElementOps, not the two-node path";
65
66impl Element {
67 pub fn nodes(&self) -> NodeList<NodeId> {
69 match self {
70 Element::Tri3(t) => t.nodes.into_iter().collect(),
71 Element::Quad4(q) => q.nodes.into_iter().collect(),
72 _ => self.pair().into_iter().collect(),
73 }
74 }
75
76 fn pair(&self) -> [NodeId; 2] {
78 match self {
79 Element::Tri3(_) | Element::Quad4(_) => unreachable!("{CONTINUUM}"),
80 Element::Truss(t) => [t.node_i, t.node_j],
81 Element::ZeroLength(z) => [z.node_i, z.node_j],
82 Element::ZeroLengthSection(z) => [z.node_i, z.node_j],
83 Element::ElasticBeamColumn(b) => [b.node_i, b.node_j],
84 Element::DispBeamColumn(b) => [b.node_i, b.node_j],
85 Element::ForceBeamColumn(b) => [b.node_i, b.node_j],
86 }
87 }
88
89 fn form_tangent_and_resistance(
96 &self,
97 node_i: &Node,
98 node_j: &Node,
99 load: Option<&ElementLoad>,
100 ) -> (
101 SMatrix<f64, ELEMENT_DOF, ELEMENT_DOF>,
102 SVector<f64, ELEMENT_DOF>,
103 ) {
104 match self {
105 Element::Truss(t) => t.form_tangent_and_resistance(node_i, node_j),
106 Element::ZeroLength(z) => z.form_tangent_and_resistance(node_i, node_j),
107 Element::ZeroLengthSection(z) => z.form_tangent_and_resistance(node_i, node_j),
108 Element::ElasticBeamColumn(b) => b.form_tangent_and_resistance(node_i, node_j),
109 Element::DispBeamColumn(b) => b.form_tangent_and_resistance(node_i, node_j),
110 Element::ForceBeamColumn(b) => b.form_tangent_and_resistance(node_i, node_j, load),
111 Element::Tri3(_) | Element::Quad4(_) => unreachable!("{CONTINUUM}"),
112 }
113 }
114
115 fn form_load_vector(
122 &self,
123 node_i: &Node,
124 node_j: &Node,
125 load: Option<&ElementLoad>,
126 ) -> SVector<f64, ELEMENT_DOF> {
127 match (self, load) {
128 (Element::ElasticBeamColumn(b), Some(load)) => {
129 b.form_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
130 }
131 (Element::DispBeamColumn(b), Some(load)) => {
132 b.form_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
133 }
134 (Element::ForceBeamColumn(b), Some(load)) => {
135 b.form_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
136 }
137 _ => SVector::<f64, ELEMENT_DOF>::zeros(),
138 }
139 }
140
141 fn form_local_load_vector(
143 &self,
144 node_i: &Node,
145 node_j: &Node,
146 load: Option<&ElementLoad>,
147 ) -> SVector<f64, ELEMENT_DOF> {
148 match (self, load) {
149 (Element::ElasticBeamColumn(b), Some(load)) => {
150 b.form_local_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
151 }
152 (Element::DispBeamColumn(b), Some(load)) => {
153 b.form_local_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
154 }
155 (Element::ForceBeamColumn(b), Some(load)) => {
156 b.form_local_load_vector(node_i, node_j, load.beam_uniform[0], load.beam_uniform[1])
157 }
158 _ => SVector::<f64, ELEMENT_DOF>::zeros(),
159 }
160 }
161
162 fn form_mass(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
167 match self {
168 Element::Truss(t) => t.form_mass(node_i, node_j),
169 Element::ZeroLength(_) => SVector::<f64, ELEMENT_DOF>::zeros(),
170 Element::ZeroLengthSection(_) => SVector::<f64, ELEMENT_DOF>::zeros(),
171 Element::ElasticBeamColumn(b) => b.form_mass(node_i, node_j),
172 Element::DispBeamColumn(b) => b.form_mass(node_i, node_j),
173 Element::ForceBeamColumn(b) => b.form_mass(node_i, node_j),
174 Element::Tri3(_) | Element::Quad4(_) => unreachable!("{CONTINUUM}"),
175 }
176 }
177
178 fn commit(&mut self, node_i: &Node, node_j: &Node, load: Option<&ElementLoad>) {
184 match self {
185 Element::Truss(t) => t.commit(node_i, node_j),
186 Element::ZeroLength(z) => z.commit(node_i, node_j),
187 Element::ZeroLengthSection(z) => z.commit(node_i, node_j),
188 Element::ElasticBeamColumn(_) => {}
189 Element::DispBeamColumn(b) => b.commit(node_i, node_j),
190 Element::ForceBeamColumn(b) => b.commit(node_i, node_j, load),
191 Element::Tri3(_) | Element::Quad4(_) => unreachable!("{CONTINUUM}"),
192 }
193 }
194
195 fn local_force(&self, node_i: &Node, node_j: &Node) -> SVector<f64, ELEMENT_DOF> {
200 match self {
201 Element::Truss(t) => t.local_force(node_i, node_j),
202 Element::ZeroLength(z) => z.local_force(node_i, node_j),
203 Element::ZeroLengthSection(z) => z.local_force(node_i, node_j),
204 Element::ElasticBeamColumn(b) => b.local_force(node_i, node_j),
205 Element::DispBeamColumn(b) => b.local_force(node_i, node_j),
206 Element::ForceBeamColumn(b) => b.local_force(),
207 Element::Tri3(_) | Element::Quad4(_) => unreachable!("{CONTINUUM}"),
208 }
209 }
210
211 fn fiber_responses(&self, node_i: &Node, node_j: &Node) -> Option<Vec<Vec<(f64, f64)>>> {
213 match self {
214 Element::DispBeamColumn(b) => Some(b.fiber_responses(node_i, node_j)),
215 Element::ForceBeamColumn(b) => Some(b.fiber_responses()),
216 Element::Truss(_)
217 | Element::ZeroLength(_)
218 | Element::ZeroLengthSection(_)
219 | Element::ElasticBeamColumn(_)
220 | Element::Tri3(_)
221 | Element::Quad4(_) => None,
222 }
223 }
224}
225
226impl ElementOps<PLANAR_NDIM, NDF, NodeId> for Element {
227 type Load = ElementLoad;
228 type Id = ElementId;
229
230 fn nodes(&self) -> NodeList<NodeId> {
231 Element::nodes(self)
232 }
233
234 fn dof_mask(&self) -> DofMask {
235 match self {
236 Element::Truss(_) => DofMask::none().with(0).with(1),
238 Element::ZeroLength(z) => z.dof_mask(),
239 Element::ZeroLengthSection(z) => z.dof_mask(),
240 Element::ElasticBeamColumn(_)
241 | Element::DispBeamColumn(_)
242 | Element::ForceBeamColumn(_) => DofMask::all(NDF),
243 Element::Tri3(t) => t.dof_mask(),
244 Element::Quad4(q) => q.dof_mask(),
245 }
246 }
247
248 fn validate(&self, nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>) -> Result<(), &'static str> {
249 match self {
250 Element::Tri3(t) => t.validate(nodes),
251 Element::Quad4(q) => q.validate(nodes),
252 _ => Ok(()),
253 }
254 }
255
256 fn accepts_load(&self, load: &ElementLoad) -> bool {
257 match self {
258 Element::ElasticBeamColumn(_)
259 | Element::DispBeamColumn(_)
260 | Element::ForceBeamColumn(_) => !load.has_body() && !load.has_edge(),
261 Element::Tri3(_) => !load.has_beam() && !load.has_edge_from(3),
262 Element::Quad4(_) => !load.has_beam(),
263 Element::Truss(_) | Element::ZeroLength(_) | Element::ZeroLengthSection(_) => {
264 !load.has_beam() && !load.has_body() && !load.has_edge()
265 }
266 }
267 }
268
269 fn prepare(&mut self, nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>) {
270 match self {
271 Element::Tri3(t) => t.prepare(nodes),
272 Element::Quad4(q) => q.prepare(nodes),
273 _ => {}
274 }
275 }
276
277 fn gauss_point_count(&self) -> usize {
278 match self {
279 Element::Tri3(_) => 1,
280 Element::Quad4(_) => 4,
281 _ => 0,
282 }
283 }
284
285 fn gauss_responses(
286 &self,
287 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
288 ) -> Option<Vec<GaussResponse>> {
289 match self {
290 Element::Tri3(t) => Some(t.gauss_responses(nodes)),
291 Element::Quad4(q) => Some(q.gauss_responses(nodes)),
292 _ => None,
293 }
294 }
295
296 fn assemble_tangent<S: TangentSink<NodeId>>(
297 &self,
298 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
299 load: Option<&ElementLoad>,
300 sink: &mut S,
301 ) {
302 match self {
303 Element::Tri3(t) => return t.assemble_tangent(nodes, sink),
304 Element::Quad4(q) => return q.assemble_tangent(nodes, sink),
305 _ => {}
306 }
307 let [i, j] = Element::pair(self);
308 let (k, r) = Element::form_tangent_and_resistance(self, nodes.get(i), nodes.get(j), load);
309 sink.add(&two_node_dofs::<_, ELEMENT_DOF>(i, j, NDF), &k, &r);
310 }
311
312 fn assemble_load<S: VectorSink<NodeId>>(
313 &self,
314 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
315 load: Option<&ElementLoad>,
316 sink: &mut S,
317 ) {
318 match (self, load) {
319 (Element::Tri3(t), load) => {
320 return load.map_or((), |load| t.assemble_load(nodes, load, sink))
321 }
322 (Element::Quad4(q), load) => {
323 return load.map_or((), |load| q.assemble_load(nodes, load, sink))
324 }
325 _ => {}
326 }
327 let [i, j] = Element::pair(self);
328 let v = Element::form_load_vector(self, nodes.get(i), nodes.get(j), load);
329 sink.add(&two_node_dofs::<_, ELEMENT_DOF>(i, j, NDF), &v);
330 }
331
332 fn assemble_mass<S: VectorSink<NodeId>>(
333 &self,
334 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
335 sink: &mut S,
336 ) {
337 match self {
338 Element::Tri3(t) => return t.assemble_mass(nodes, sink),
339 Element::Quad4(q) => return q.assemble_mass(nodes, sink),
340 _ => {}
341 }
342 let [i, j] = Element::pair(self);
343 let v = Element::form_mass(self, nodes.get(i), nodes.get(j));
344 sink.add(&two_node_dofs::<_, ELEMENT_DOF>(i, j, NDF), &v);
345 }
346
347 fn commit(
348 &mut self,
349 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
350 load: Option<&ElementLoad>,
351 ) {
352 match self {
353 Element::Tri3(t) => return t.commit(nodes),
354 Element::Quad4(q) => return q.commit(nodes),
355 _ => {}
356 }
357 let [i, j] = Element::pair(self);
358 Element::commit(self, nodes.get(i), nodes.get(j), load)
359 }
360
361 fn local_force_width(&self) -> usize {
362 match self {
363 Element::Tri3(_) => 6,
364 Element::Quad4(_) => 8,
365 _ => ELEMENT_DOF,
366 }
367 }
368
369 fn local_force(&self, nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>) -> ElementForce {
370 match self {
371 Element::Tri3(t) => return t.local_force(nodes),
372 Element::Quad4(q) => return q.local_force(nodes),
373 _ => {}
374 }
375 let [i, j] = Element::pair(self);
376 Element::local_force(self, nodes.get(i), nodes.get(j)).into()
377 }
378
379 fn local_load_force(
380 &self,
381 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
382 load: Option<&ElementLoad>,
383 ) -> ElementForce {
384 match self {
385 Element::Tri3(t) => {
386 return load
387 .map_or(SVector::<f64, 6>::zeros(), |l| t.load_vector(nodes, l))
388 .into()
389 }
390 Element::Quad4(q) => {
391 return load
392 .map_or(SVector::<f64, 8>::zeros(), |l| q.load_vector(nodes, l))
393 .into()
394 }
395 _ => {}
396 }
397 let [i, j] = Element::pair(self);
398 Element::form_local_load_vector(self, nodes.get(i), nodes.get(j), load).into()
399 }
400
401 fn fiber_responses(
402 &self,
403 nodes: &NodeView<'_, PLANAR_NDIM, NDF, NodeId>,
404 ) -> Option<Vec<Vec<(f64, f64)>>> {
405 if let Element::Tri3(_) | Element::Quad4(_) = self {
406 return None;
407 }
408 let [i, j] = Element::pair(self);
409 Element::fiber_responses(self, nodes.get(i), nodes.get(j))
410 }
411}
412#[derive(Debug, Clone)]
415pub enum Element3 {
416 Truss3(Truss3),
417 ZeroLength3(ZeroLength3),
418 ZeroLengthSection3(ZeroLengthSection3),
419 ElasticBeamColumn3(ElasticBeamColumn3),
420 DispBeamColumn3(DispBeamColumn3),
421 ForceBeamColumn3(ForceBeamColumn3),
422 Shell3(Shell3),
423 Shell4(Shell4),
424}
425
426const SHELL: &str = "shell elements assemble through ElementOps, not the two-node path";
427
428impl Element3 {
429 pub fn nodes(&self) -> NodeList<Node3Id> {
431 match self {
432 Element3::Shell3(s) => s.nodes.into_iter().collect(),
433 Element3::Shell4(s) => s.nodes.into_iter().collect(),
434 _ => self.pair().into_iter().collect(),
435 }
436 }
437
438 fn pair(&self) -> [Node3Id; 2] {
440 match self {
441 Element3::Shell3(_) | Element3::Shell4(_) => unreachable!("{SHELL}"),
442 Element3::Truss3(t) => [t.node_i, t.node_j],
443 Element3::ZeroLength3(z) => [z.node_i, z.node_j],
444 Element3::ZeroLengthSection3(z) => [z.node_i, z.node_j],
445 Element3::ElasticBeamColumn3(b) => [b.node_i, b.node_j],
446 Element3::DispBeamColumn3(b) => [b.node_i, b.node_j],
447 Element3::ForceBeamColumn3(b) => [b.node_i, b.node_j],
448 }
449 }
450
451 fn form_tangent_and_resistance(
452 &self,
453 node_i: &Node3,
454 node_j: &Node3,
455 load: Option<&ElementLoad3>,
456 ) -> (
457 SMatrix<f64, SPATIAL_ELEMENT_DOF, SPATIAL_ELEMENT_DOF>,
458 SVector<f64, SPATIAL_ELEMENT_DOF>,
459 ) {
460 match self {
461 Element3::Truss3(t) => t.form_tangent_and_resistance(node_i, node_j),
462 Element3::ZeroLength3(z) => z.form_tangent_and_resistance(node_i, node_j),
463 Element3::ZeroLengthSection3(z) => z.form_tangent_and_resistance(node_i, node_j),
464 Element3::ElasticBeamColumn3(b) => b.form_tangent_and_resistance(node_i, node_j),
465 Element3::DispBeamColumn3(b) => b.form_tangent_and_resistance(node_i, node_j),
466 Element3::ForceBeamColumn3(b) => b.form_tangent_and_resistance(node_i, node_j, load),
467 Element3::Shell3(_) | Element3::Shell4(_) => unreachable!("{SHELL}"),
468 }
469 }
470
471 fn form_load_vector(
476 &self,
477 node_i: &Node3,
478 node_j: &Node3,
479 load: Option<&ElementLoad3>,
480 ) -> SVector<f64, SPATIAL_ELEMENT_DOF> {
481 match (self, load) {
482 (Element3::ElasticBeamColumn3(b), Some(l)) => {
483 let [wx, wy, wz] = l.uniform;
484 b.form_load_vector(node_i, node_j, wx, wy, wz)
485 }
486 (Element3::DispBeamColumn3(b), Some(l)) => {
487 let [wx, wy, wz] = l.uniform;
488 b.form_load_vector(node_i, node_j, wx, wy, wz)
489 }
490 (Element3::ForceBeamColumn3(b), Some(l)) => {
491 let [wx, wy, wz] = l.uniform;
492 b.form_load_vector(node_i, node_j, wx, wy, wz)
493 }
494 _ => SVector::<f64, SPATIAL_ELEMENT_DOF>::zeros(),
495 }
496 }
497
498 fn form_local_load_vector(
500 &self,
501 node_i: &Node3,
502 node_j: &Node3,
503 load: Option<&ElementLoad3>,
504 ) -> SVector<f64, SPATIAL_ELEMENT_DOF> {
505 match (self, load) {
506 (Element3::ElasticBeamColumn3(b), Some(l)) => {
507 let [wx, wy, wz] = l.uniform;
508 b.form_local_load_vector(node_i, node_j, wx, wy, wz)
509 }
510 (Element3::DispBeamColumn3(b), Some(l)) => {
511 let [wx, wy, wz] = l.uniform;
512 b.form_local_load_vector(node_i, node_j, wx, wy, wz)
513 }
514 (Element3::ForceBeamColumn3(b), Some(l)) => {
515 let [wx, wy, wz] = l.uniform;
516 b.form_local_load_vector(node_i, node_j, wx, wy, wz)
517 }
518 _ => SVector::<f64, SPATIAL_ELEMENT_DOF>::zeros(),
519 }
520 }
521
522 fn form_mass(&self, node_i: &Node3, node_j: &Node3) -> SVector<f64, SPATIAL_ELEMENT_DOF> {
524 match self {
525 Element3::Truss3(t) => t.form_mass(node_i, node_j),
526 Element3::ZeroLength3(_) => SVector::<f64, SPATIAL_ELEMENT_DOF>::zeros(),
527 Element3::ZeroLengthSection3(_) => SVector::<f64, SPATIAL_ELEMENT_DOF>::zeros(),
528 Element3::ElasticBeamColumn3(b) => b.form_mass(node_i, node_j),
529 Element3::DispBeamColumn3(b) => b.form_mass(node_i, node_j),
530 Element3::ForceBeamColumn3(b) => b.form_mass(node_i, node_j),
531 Element3::Shell3(_) | Element3::Shell4(_) => unreachable!("{SHELL}"),
532 }
533 }
534
535 fn commit(&mut self, node_i: &Node3, node_j: &Node3, load: Option<&ElementLoad3>) {
536 match self {
537 Element3::Truss3(t) => t.commit(node_i, node_j),
538 Element3::ZeroLength3(z) => z.commit(node_i, node_j),
539 Element3::ZeroLengthSection3(z) => z.commit(node_i, node_j),
540 Element3::ElasticBeamColumn3(_) => {}
541 Element3::DispBeamColumn3(b) => b.commit(node_i, node_j),
542 Element3::ForceBeamColumn3(b) => b.commit(node_i, node_j, load),
543 Element3::Shell3(_) | Element3::Shell4(_) => unreachable!("{SHELL}"),
544 }
545 }
546
547 fn local_force(&self, node_i: &Node3, node_j: &Node3) -> SVector<f64, SPATIAL_ELEMENT_DOF> {
549 match self {
550 Element3::Truss3(t) => t.local_force(node_i, node_j),
551 Element3::ZeroLength3(z) => z.local_force(node_i, node_j),
552 Element3::ZeroLengthSection3(z) => z.local_force(node_i, node_j),
553 Element3::ElasticBeamColumn3(b) => b.local_force(node_i, node_j),
554 Element3::DispBeamColumn3(b) => b.local_force(node_i, node_j),
555 Element3::ForceBeamColumn3(b) => b.local_force(),
556 Element3::Shell3(_) | Element3::Shell4(_) => unreachable!("{SHELL}"),
557 }
558 }
559
560 fn fiber_responses(&self, node_i: &Node3, node_j: &Node3) -> Option<Vec<Vec<(f64, f64)>>> {
562 match self {
563 Element3::DispBeamColumn3(b) => Some(b.fiber_responses(node_i, node_j)),
564 Element3::ForceBeamColumn3(b) => Some(b.fiber_responses()),
565 Element3::Truss3(_)
566 | Element3::ZeroLength3(_)
567 | Element3::ZeroLengthSection3(_)
568 | Element3::ElasticBeamColumn3(_)
569 | Element3::Shell3(_)
570 | Element3::Shell4(_) => None,
571 }
572 }
573}
574
575impl ElementOps<SPATIAL_NDIM, SPATIAL_NDF, Node3Id> for Element3 {
576 type Load = ElementLoad3;
577 type Id = Element3Id;
578
579 fn nodes(&self) -> NodeList<Node3Id> {
580 Element3::nodes(self)
581 }
582
583 fn dof_mask(&self) -> DofMask {
584 match self {
585 Element3::Truss3(_) => DofMask::none().with(0).with(1).with(2),
586 Element3::ZeroLength3(z) => z.dof_mask(),
587 Element3::ZeroLengthSection3(z) => z.dof_mask(),
588 Element3::ElasticBeamColumn3(_)
589 | Element3::DispBeamColumn3(_)
590 | Element3::ForceBeamColumn3(_) => DofMask::all(SPATIAL_NDF),
591 Element3::Shell3(s) => s.dof_mask(),
592 Element3::Shell4(s) => s.dof_mask(),
593 }
594 }
595
596 fn validate(
597 &self,
598 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
599 ) -> Result<(), &'static str> {
600 match self {
601 Element3::Shell3(s) => s.validate(nodes),
602 Element3::Shell4(s) => s.validate(nodes),
603 _ => Ok(()),
604 }
605 }
606
607 fn accepts_load(&self, load: &ElementLoad3) -> bool {
608 match self {
609 Element3::Shell3(_) | Element3::Shell4(_) => !load.has_uniform(),
610 _ => !load.has_body() && !load.has_pressure(),
611 }
612 }
613
614 fn prepare(&mut self, nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>) {
615 match self {
616 Element3::Shell3(s) => s.prepare(nodes),
617 Element3::Shell4(s) => s.prepare(nodes),
618 _ => {}
619 }
620 }
621
622 fn gauss_point_count(&self) -> usize {
623 match self {
624 Element3::Shell3(_) | Element3::Shell4(_) => 4,
625 _ => 0,
626 }
627 }
628
629 fn gauss_component_count(&self) -> usize {
630 match self {
631 Element3::Shell3(_) | Element3::Shell4(_) => 8,
632 _ => 3,
633 }
634 }
635
636 fn shell_responses(
637 &self,
638 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
639 ) -> Option<Vec<ShellResponse>> {
640 match self {
641 Element3::Shell3(s) => Some(s.shell_responses(nodes)),
642 Element3::Shell4(s) => Some(s.shell_responses(nodes)),
643 _ => None,
644 }
645 }
646
647 fn assemble_tangent<S: TangentSink<Node3Id>>(
648 &self,
649 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
650 load: Option<&ElementLoad3>,
651 sink: &mut S,
652 ) {
653 match self {
654 Element3::Shell3(s) => return s.assemble_tangent(nodes, sink),
655 Element3::Shell4(s) => return s.assemble_tangent(nodes, sink),
656 _ => {}
657 }
658 let [i, j] = Element3::pair(self);
659 let (k, r) = Element3::form_tangent_and_resistance(self, nodes.get(i), nodes.get(j), load);
660 sink.add(
661 &two_node_dofs::<_, SPATIAL_ELEMENT_DOF>(i, j, SPATIAL_NDF),
662 &k,
663 &r,
664 );
665 }
666
667 fn assemble_load<S: VectorSink<Node3Id>>(
668 &self,
669 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
670 load: Option<&ElementLoad3>,
671 sink: &mut S,
672 ) {
673 match self {
674 Element3::Shell3(s) => {
675 return load.map_or((), |load| s.assemble_load(nodes, load, sink))
676 }
677 Element3::Shell4(s) => {
678 return load.map_or((), |load| s.assemble_load(nodes, load, sink))
679 }
680 _ => {}
681 }
682 let [i, j] = Element3::pair(self);
683 let v = Element3::form_load_vector(self, nodes.get(i), nodes.get(j), load);
684 sink.add(
685 &two_node_dofs::<_, SPATIAL_ELEMENT_DOF>(i, j, SPATIAL_NDF),
686 &v,
687 );
688 }
689
690 fn assemble_mass<S: VectorSink<Node3Id>>(
691 &self,
692 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
693 sink: &mut S,
694 ) {
695 match self {
696 Element3::Shell3(s) => return s.assemble_mass(nodes, sink),
697 Element3::Shell4(s) => return s.assemble_mass(nodes, sink),
698 _ => {}
699 }
700 let [i, j] = Element3::pair(self);
701 let v = Element3::form_mass(self, nodes.get(i), nodes.get(j));
702 sink.add(
703 &two_node_dofs::<_, SPATIAL_ELEMENT_DOF>(i, j, SPATIAL_NDF),
704 &v,
705 );
706 }
707
708 fn commit(
709 &mut self,
710 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
711 load: Option<&ElementLoad3>,
712 ) {
713 match self {
714 Element3::Shell3(s) => return s.commit(nodes),
715 Element3::Shell4(s) => return s.commit(nodes),
716 _ => {}
717 }
718 let [i, j] = Element3::pair(self);
719 Element3::commit(self, nodes.get(i), nodes.get(j), load)
720 }
721
722 fn local_force_width(&self) -> usize {
723 match self {
724 Element3::Shell3(_) => 18,
725 Element3::Shell4(_) => 24,
726 _ => SPATIAL_ELEMENT_DOF,
727 }
728 }
729
730 fn local_force(
731 &self,
732 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
733 ) -> ElementForce {
734 match self {
735 Element3::Shell3(s) => return s.local_force(nodes),
736 Element3::Shell4(s) => return s.local_force(nodes),
737 _ => {}
738 }
739 let [i, j] = Element3::pair(self);
740 Element3::local_force(self, nodes.get(i), nodes.get(j)).into()
741 }
742
743 fn local_load_force(
744 &self,
745 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
746 load: Option<&ElementLoad3>,
747 ) -> ElementForce {
748 match self {
749 Element3::Shell3(s) => {
750 return load
751 .map_or(SVector::<f64, 18>::zeros(), |l| s.load_vector(nodes, l))
752 .into()
753 }
754 Element3::Shell4(s) => {
755 return load
756 .map_or(SVector::<f64, 24>::zeros(), |l| s.load_vector(nodes, l))
757 .into()
758 }
759 _ => {}
760 }
761 let [i, j] = Element3::pair(self);
762 Element3::form_local_load_vector(self, nodes.get(i), nodes.get(j), load).into()
763 }
764
765 fn fiber_responses(
766 &self,
767 nodes: &NodeView<'_, SPATIAL_NDIM, SPATIAL_NDF, Node3Id>,
768 ) -> Option<Vec<Vec<(f64, f64)>>> {
769 if let Element3::Shell3(_) | Element3::Shell4(_) = self {
770 return None;
771 }
772 let [i, j] = Element3::pair(self);
773 Element3::fiber_responses(self, nodes.get(i), nodes.get(j))
774 }
775}
776
777#[cfg(test)]
784mod mask_conformance {
785 use std::collections::BTreeSet;
786
787 use slotmap::{Key, SlotMap};
788
789 use super::*;
790 use crate::model::{
791 BeamIntegration, ElasticBeamColumn, ElasticBeamColumn3, Fiber, Fiber3, FiberSection,
792 FiberSection3, ForceBeamColumn, ForceBeamColumn3, Friction, Friction3, GeomTransf,
793 GeomTransf3, Material, Orientation, Truss, Truss3, ZeroLength, ZeroLength3,
794 ZeroLengthSection, ZeroLengthSection3,
795 };
796
797 struct Touched<NId> {
800 slots: BTreeSet<u8>,
801 _marker: std::marker::PhantomData<NId>,
802 }
803
804 impl<NId: Key> TangentSink<NId> for Touched<NId> {
805 fn add<const N: usize>(
806 &mut self,
807 dofs: &[DofRef<NId>; N],
808 k: &SMatrix<f64, N, N>,
809 r: &SVector<f64, N>,
810 ) {
811 let k_scale = (0..N).map(|i| k[(i, i)].abs()).fold(0.0, f64::max);
812 let r_scale = r.iter().map(|v| v.abs()).fold(0.0, f64::max);
813 for (a, &(_, slot)) in dofs.iter().enumerate() {
814 if k[(a, a)].abs() > 1e-10 * k_scale || r[a].abs() > 1e-10 * r_scale {
815 self.slots.insert(slot);
816 }
817 }
818 }
819 }
820
821 fn touched<const NDIM: usize, const NDOF: usize, NId: Key, E: ElementOps<NDIM, NDOF, NId>>(
822 element: &E,
823 nodes: &SlotMap<NId, Node<NDIM, NDOF>>,
824 ) -> BTreeSet<u8> {
825 let mut sink = Touched::<NId> {
826 slots: BTreeSet::new(),
827 _marker: std::marker::PhantomData,
828 };
829 element.assemble_tangent(&NodeView::new(nodes), None, &mut sink);
830 sink.slots
831 }
832
833 fn mask_slots<const NDOF: usize>(mask: DofMask) -> BTreeSet<u8> {
834 (0..NDOF)
835 .filter(|&s| mask.contains(s))
836 .map(|s| s as u8)
837 .collect()
838 }
839
840 fn check<const NDIM: usize, const NDOF: usize, NId: Key, E: ElementOps<NDIM, NDOF, NId>>(
843 name: &str,
844 element: &E,
845 nodes: &SlotMap<NId, Node<NDIM, NDOF>>,
846 exact: bool,
847 ) {
848 let touched = touched::<NDIM, NDOF, NId, E>(element, nodes);
849 let mask = mask_slots::<NDOF>(element.dof_mask());
850 assert!(
851 touched.is_subset(&mask),
852 "{name}: stiffens slots {touched:?} but declares only {mask:?}"
853 );
854 if exact {
855 assert_eq!(
856 touched, mask,
857 "{name}: declared mask is wider than what it stiffens"
858 );
859 }
860 }
861
862 fn nodes_2d() -> (SlotMap<NodeId, Node>, NodeId, NodeId) {
865 let mut nodes = SlotMap::with_key();
866 let a = nodes.insert(Node::new([0.0, 0.0]));
867 let mut j = Node::new([300.0, 200.0]);
868 j.displacement = [0.4, -0.7, 0.003];
869 let b = nodes.insert(j);
870 (nodes, a, b)
871 }
872
873 fn nodes_3d() -> (SlotMap<Node3Id, Node3>, Node3Id, Node3Id) {
874 let mut nodes = SlotMap::with_key();
875 let a = nodes.insert(Node3::new([0.0, 0.0, 0.0]));
876 let mut j = Node3::new([300.0, 200.0, 150.0]);
877 j.displacement = [0.4, -0.7, 0.3, 0.002, -0.003, 0.004];
878 let b = nodes.insert(j);
879 (nodes, a, b)
880 }
881
882 fn steel() -> Material {
883 Material::Elastic { e: 200_000.0 }
884 }
885
886 fn nodes_plane() -> (SlotMap<NodeId, Node>, [NodeId; 4]) {
888 let mut nodes = SlotMap::with_key();
889 let coords = [[0.0, 0.0], [2.0, 0.2], [2.3, 1.7], [-0.1, 1.4]];
890 let ids = coords.map(|c| {
891 let mut node = Node::new(c);
892 node.displacement = [0.01 * (c[1] + 1.0), -0.02 * (c[0] + 0.5), 0.003];
893 nodes.insert(node)
894 });
895 (nodes, ids)
896 }
897
898 #[test]
899 fn continuum_elements_declare_exactly_the_translations_they_stiffen() {
900 let (nodes, [a, b, c, d]) = nodes_plane();
901 let material = || crate::model::PlaneMaterial::plane_stress(30e3, 0.2).unwrap();
902 check(
903 "Tri3",
904 &Element::Tri3(Tri3::new(a, b, c, 0.5, material())),
905 &nodes,
906 true,
907 );
908 check(
909 "Quad4",
910 &Element::Quad4(Quad4::new([a, b, c, d], 0.5, material())),
911 &nodes,
912 true,
913 );
914 check(
915 "Quad4 enhanced",
916 &Element::Quad4(
917 Quad4::new([a, b, c, d], 0.5, material())
918 .with_formulation(crate::model::Quad4Formulation::Enhanced),
919 ),
920 &nodes,
921 true,
922 );
923 }
924
925 #[test]
926 fn shell_declares_all_six_slots_it_stiffens() {
927 let mut nodes = SlotMap::with_key();
928 let coords = [
929 [0.0, 0.0, 0.0],
930 [2.0, 0.2, 0.3],
931 [2.3, 1.7, 0.9],
932 [-0.1, 1.4, 0.2],
933 ];
934 let ids = coords.map(|c| {
935 let mut node = Node3::new(c);
936 node.displacement = [0.01, -0.02, 0.03, 0.002, -0.003, 0.004];
937 nodes.insert(node)
938 });
939 let section =
940 crate::model::ShellSection::elastic_membrane_plate(3e4, 0.25, 0.4, 0.0).unwrap();
941 check(
942 "Shell4",
943 &Element3::Shell4(Shell4::new(ids, section.clone())),
944 &nodes,
945 true,
946 );
947 check(
948 "Shell3",
949 &Element3::Shell3(Shell3::new([ids[0], ids[1], ids[2]], section)),
950 &nodes,
951 true,
952 );
953 }
954
955 #[test]
956 fn planar_elements_declare_the_slots_they_stiffen() {
957 let (nodes, a, b) = nodes_2d();
958 let fibers = || {
959 vec![
960 Fiber::new(10.0, 100.0, steel()),
961 Fiber::new(-10.0, 100.0, steel()),
962 ]
963 };
964 let rot = 30.0_f64.to_radians();
965 let oriented =
966 || Orientation::new([rot.cos(), rot.sin(), 0.0], [-rot.sin(), rot.cos(), 0.0]).unwrap();
967
968 check(
969 "Truss",
970 &Element::Truss(Truss::new(a, b, 10.0, steel())),
971 &nodes,
972 true,
973 );
974 check(
975 "ZeroLength ux only",
976 &Element::ZeroLength(ZeroLength::new(a, b).with_material(0, steel())),
977 &nodes,
978 true,
979 );
980 check(
981 "ZeroLength rz only",
982 &Element::ZeroLength(ZeroLength::new(a, b).with_material(2, steel())),
983 &nodes,
984 true,
985 );
986 check(
987 "ZeroLength oriented",
988 &Element::ZeroLength(
989 ZeroLength::new(a, b)
990 .with_material(0, steel())
991 .with_material(1, steel())
992 .with_orientation(oriented())
993 .unwrap(),
994 ),
995 &nodes,
996 true,
997 );
998 check(
999 "ZeroLength friction",
1000 &Element::ZeroLength(
1001 ZeroLength::new(a, b)
1002 .with_material(0, steel())
1003 .with_friction(Friction::new(0, 1, 0.3, 500.0, 0.01)),
1004 ),
1005 &nodes,
1006 true,
1007 );
1008 check(
1009 "ZeroLengthSection",
1010 &Element::ZeroLengthSection(ZeroLengthSection::new(a, b, FiberSection::new(fibers()))),
1011 &nodes,
1012 true,
1013 );
1014 check(
1015 "ZeroLengthSection + shear spring",
1016 &Element::ZeroLengthSection(
1017 ZeroLengthSection::new(a, b, FiberSection::new(fibers())).with_material(1, steel()),
1018 ),
1019 &nodes,
1020 true,
1021 );
1022 check(
1023 "ElasticBeamColumn",
1024 &Element::ElasticBeamColumn(ElasticBeamColumn::new(
1025 a,
1026 b,
1027 200_000.0,
1028 1e4,
1029 1e8,
1030 GeomTransf::PDelta,
1031 )),
1032 &nodes,
1033 true,
1034 );
1035 check(
1036 "DispBeamColumn",
1037 &Element::DispBeamColumn(DispBeamColumn::new(
1038 a,
1039 b,
1040 fibers(),
1041 BeamIntegration::Lobatto { points: 3 },
1042 )),
1043 &nodes,
1044 true,
1045 );
1046 check(
1047 "ForceBeamColumn",
1048 &Element::ForceBeamColumn(ForceBeamColumn::new(
1049 a,
1050 b,
1051 fibers(),
1052 BeamIntegration::Lobatto { points: 3 },
1053 )),
1054 &nodes,
1055 true,
1056 );
1057 }
1058
1059 #[test]
1060 fn spatial_elements_declare_the_slots_they_stiffen() {
1061 let (nodes, a, b) = nodes_3d();
1062 let fibers = || {
1063 vec![
1064 Fiber3::new(10.0, 10.0, 50.0, steel()),
1065 Fiber3::new(10.0, -10.0, 50.0, steel()),
1066 Fiber3::new(-10.0, 10.0, 50.0, steel()),
1067 Fiber3::new(-10.0, -10.0, 50.0, steel()),
1068 ]
1069 };
1070 let tilt = Orientation::new([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]).unwrap();
1071
1072 check(
1073 "Truss3",
1074 &Element3::Truss3(Truss3::new(a, b, 10.0, steel())),
1075 &nodes,
1076 true,
1077 );
1078 check(
1079 "ZeroLength3 translations",
1080 &Element3::ZeroLength3(
1081 ZeroLength3::new(a, b)
1082 .with_material(0, steel())
1083 .with_material(1, steel())
1084 .with_material(2, steel()),
1085 ),
1086 &nodes,
1087 true,
1088 );
1089 check(
1090 "ZeroLength3 torsion only",
1091 &Element3::ZeroLength3(ZeroLength3::new(a, b).with_material(3, steel())),
1092 &nodes,
1093 true,
1094 );
1095 check(
1096 "ZeroLength3 oriented",
1097 &Element3::ZeroLength3(
1098 ZeroLength3::new(a, b)
1099 .with_material(0, steel())
1100 .with_orientation(tilt.clone()),
1101 ),
1102 &nodes,
1103 false,
1104 );
1105 check(
1106 "ZeroLength3 friction",
1107 &Element3::ZeroLength3(
1108 ZeroLength3::new(a, b)
1109 .with_material(0, steel())
1110 .with_friction(Friction3::new(0, [1, 2], 0.3, 500.0, 0.01)),
1111 ),
1112 &nodes,
1113 true,
1114 );
1115 check(
1116 "ZeroLengthSection3",
1117 &Element3::ZeroLengthSection3(ZeroLengthSection3::new(
1118 a,
1119 b,
1120 FiberSection3::new(fibers()),
1121 )),
1122 &nodes,
1123 true,
1124 );
1125 check(
1126 "ZeroLengthSection3 + torsion spring",
1127 &Element3::ZeroLengthSection3(
1128 ZeroLengthSection3::new(a, b, FiberSection3::new(fibers()))
1129 .with_material(3, steel()),
1130 ),
1131 &nodes,
1132 true,
1133 );
1134 check(
1135 "ElasticBeamColumn3",
1136 &Element3::ElasticBeamColumn3(ElasticBeamColumn3::new(
1137 a,
1138 b,
1139 200_000.0,
1140 80_000.0,
1141 1e4,
1142 1e7,
1143 5e7,
1144 1e8,
1145 GeomTransf3::Linear3 {
1146 vec_xz: [0.0, 0.0, 1.0],
1147 },
1148 )),
1149 &nodes,
1150 true,
1151 );
1152 check(
1153 "DispBeamColumn3",
1154 &Element3::DispBeamColumn3(DispBeamColumn3::new(
1155 a,
1156 b,
1157 80_000.0,
1158 1e7,
1159 [0.0, 0.0, 1.0],
1160 fibers(),
1161 BeamIntegration::Lobatto { points: 3 },
1162 )),
1163 &nodes,
1164 true,
1165 );
1166 check(
1167 "ForceBeamColumn3",
1168 &Element3::ForceBeamColumn3(ForceBeamColumn3::new(
1169 a,
1170 b,
1171 80_000.0,
1172 1e7,
1173 [0.0, 0.0, 1.0],
1174 fibers(),
1175 BeamIntegration::Lobatto { points: 3 },
1176 )),
1177 &nodes,
1178 true,
1179 );
1180 }
1181}