1use nalgebra::{SMatrix, SVector};
36
37use super::plane_common::{
38 b_matrix, check_section, load_vector, plane_coords, plane_displacement, plane_dofs, PlaneView,
39};
40use super::{DofMask, ElementForce, GaussResponse, TangentSink, VectorSink};
41use crate::model::continuum::{
42 physical_derivatives, quad4_dshape, quad4_shape, validate_quad, Jacobian, QUAD4_GAUSS_2X2,
43};
44use crate::model::{ElementLoad, NodeId, PlaneMaterial, PlaneMatrix, PlaneVector};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Quad4Formulation {
49 #[default]
51 Full,
52 Enhanced,
54}
55
56#[derive(Debug, Clone)]
57pub struct Quad4 {
58 pub nodes: [NodeId; 4],
59 pub thickness: f64,
60 pub density: f64,
61 formulation: Quad4Formulation,
62 materials: [PlaneMaterial; 4],
63 cache: Option<Cache>,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
68pub(super) struct Cache {
69 points: [PointGeometry; 4],
70 enhanced: Option<Enhanced>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
76struct Enhanced {
77 k: SMatrix<f64, 8, 8>,
78 recovery: SMatrix<f64, 4, 8>,
79 g: [SMatrix<f64, 3, 4>; 4],
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
85pub(super) struct PointGeometry {
86 pub b: SMatrix<f64, 3, 8>,
87 pub weight: f64,
89 pub n: [f64; 4],
91}
92
93pub(super) fn point_geometry(coords: &[[f64; 2]; 4]) -> [PointGeometry; 4] {
94 std::array::from_fn(|g| {
95 let (xi, eta, w) = QUAD4_GAUSS_2X2[g];
96 let dn = quad4_dshape(xi, eta);
97 let jacobian = Jacobian::new(coords, &dn);
98 PointGeometry {
99 b: b_matrix(&physical_derivatives(&jacobian, &dn)),
100 weight: jacobian.det * w,
101 n: quad4_shape(xi, eta),
102 }
103 })
104}
105
106fn enhanced_strain_matrices(coords: &[[f64; 2]; 4]) -> [SMatrix<f64, 3, 4>; 4] {
108 let j0 = Jacobian::new(coords, &quad4_dshape(0.0, 0.0));
109 std::array::from_fn(|g| {
110 let (xi, eta, _) = QUAD4_GAUSS_2X2[g];
111 let det = Jacobian::new(coords, &quad4_dshape(xi, eta)).det;
112 let dm = [[-2.0 * xi, 0.0], [0.0, -2.0 * eta]];
114 let gm = physical_derivatives(&j0, &dm);
115 let scale = j0.det / det;
116 let (g1, g2) = (gm[0], gm[1]);
117 scale
118 * SMatrix::<f64, 3, 4>::new(
119 g1[0], g2[0], 0.0, 0.0, 0.0, 0.0, g1[1], g2[1], g1[1], g2[1], g1[0], g2[0],
122 )
123 })
124}
125
126fn condense(
128 points: &[PointGeometry; 4],
129 d: &[PlaneMatrix; 4],
130 thickness: f64,
131 coords: &[[f64; 2]; 4],
132) -> Result<Enhanced, &'static str> {
133 let g = enhanced_strain_matrices(coords);
134 let mut k_uu = SMatrix::<f64, 8, 8>::zeros();
135 let mut k_ua = SMatrix::<f64, 8, 4>::zeros();
136 let mut k_aa = SMatrix::<f64, 4, 4>::zeros();
137 for i in 0..4 {
138 let scale = thickness * points[i].weight;
139 let (b, gi) = (&points[i].b, &g[i]);
140 k_uu += scale * b.transpose() * d[i] * b;
141 k_ua += scale * b.transpose() * d[i] * gi;
142 k_aa += scale * gi.transpose() * d[i] * gi;
143 }
144 let k_aa_inverse = k_aa
145 .try_inverse()
146 .filter(|inverse| inverse.iter().all(|v| v.is_finite()))
147 .ok_or("enhanced Quad4 mode matrix is singular")?;
148 let recovery = -k_aa_inverse * k_ua.transpose();
149 Ok(Enhanced {
150 k: k_uu + k_ua * recovery,
151 recovery,
152 g,
153 })
154}
155
156impl Quad4 {
157 pub fn new(nodes: [NodeId; 4], thickness: f64, material: PlaneMaterial) -> Self {
158 Quad4 {
159 nodes,
160 thickness,
161 density: 0.0,
162 formulation: Quad4Formulation::Full,
163 materials: std::array::from_fn(|_| material.clone()),
164 cache: None,
165 }
166 }
167
168 pub fn with_formulation(mut self, formulation: Quad4Formulation) -> Self {
171 self.formulation = formulation;
172 self
173 }
174
175 pub fn formulation(&self) -> Quad4Formulation {
176 self.formulation
177 }
178
179 pub fn with_density(mut self, density: f64) -> Self {
180 self.density = density;
181 self
182 }
183
184 pub fn materials(&self) -> &[PlaneMaterial; 4] {
186 &self.materials
187 }
188
189 fn compute_cache(&self, view: &PlaneView<'_>) -> Result<Cache, &'static str> {
190 let coords = plane_coords(&self.nodes, view);
191 let points = point_geometry(&coords);
192 let enhanced = match self.formulation {
193 Quad4Formulation::Full => None,
194 Quad4Formulation::Enhanced => {
195 let d: [PlaneMatrix; 4] = std::array::from_fn(|g| self.materials[g].d_matrix());
196 Some(condense(&points, &d, self.thickness, &coords)?)
197 }
198 };
199 Ok(Cache { points, enhanced })
200 }
201
202 fn cache_or_compute(&self, view: &PlaneView<'_>) -> Cache {
206 self.cache.unwrap_or_else(|| {
207 self.compute_cache(view).unwrap_or_else(|_| Cache {
208 points: point_geometry(&plane_coords(&self.nodes, view)),
209 enhanced: Some(Enhanced {
210 k: SMatrix::zeros(),
211 recovery: SMatrix::zeros(),
212 g: [SMatrix::zeros(); 4],
213 }),
214 })
215 })
216 }
217
218 pub(super) fn dof_mask(&self) -> DofMask {
219 DofMask::none().with(0).with(1)
220 }
221
222 pub(super) fn validate(&self, view: &PlaneView<'_>) -> Result<(), &'static str> {
223 check_section(self.thickness, self.density, &self.materials[0])?;
224 validate_quad(&plane_coords(&self.nodes, view))?;
225 if self.formulation == Quad4Formulation::Enhanced {
226 if !self.materials.iter().all(PlaneMaterial::is_linear) {
227 return Err("enhanced Quad4 requires a linear plane material");
228 }
229 self.compute_cache(view)?;
230 }
231 Ok(())
232 }
233
234 pub(super) fn prepare(&mut self, view: &PlaneView<'_>) {
235 self.cache = self.compute_cache(view).ok();
236 }
237
238 fn strains(&self, cache: &Cache, u: &SVector<f64, 8>) -> [PlaneVector; 4] {
240 let alpha = cache.enhanced.as_ref().map(|e| e.recovery * u);
241 std::array::from_fn(|g| {
242 let mut strain = cache.points[g].b * u;
243 if let (Some(e), Some(alpha)) = (&cache.enhanced, &alpha) {
244 strain += e.g[g] * alpha;
245 }
246 strain
247 })
248 }
249
250 pub(super) fn assemble_tangent<S: TangentSink<NodeId>>(
251 &self,
252 view: &PlaneView<'_>,
253 sink: &mut S,
254 ) {
255 let u = plane_displacement::<4, 8>(&self.nodes, view);
256 let cache = self.cache_or_compute(view);
257 if let Some(enhanced) = &cache.enhanced {
258 sink.add(
259 &plane_dofs::<4, 8>(&self.nodes),
260 &enhanced.k,
261 &(enhanced.k * u),
262 );
263 return;
264 }
265 let mut k = SMatrix::<f64, 8, 8>::zeros();
266 let mut r = SVector::<f64, 8>::zeros();
267 for (g, material) in cache.points.iter().zip(&self.materials) {
268 let (stress, d) = material.trial_stress_tangent(&(g.b * u));
269 let scale = self.thickness * g.weight;
270 k += scale * g.b.transpose() * d * g.b;
271 r += scale * g.b.transpose() * stress;
272 }
273 sink.add(&plane_dofs::<4, 8>(&self.nodes), &k, &r);
274 }
275
276 fn nodal_weights(&self, view: &PlaneView<'_>) -> [f64; 4] {
281 let mut weights = [0.0; 4];
282 for g in self.cache_or_compute(view).points {
283 for (i, w) in weights.iter_mut().enumerate() {
284 *w += self.thickness * g.n[i] * g.weight;
285 }
286 }
287 weights
288 }
289
290 pub(super) fn assemble_mass<S: VectorSink<NodeId>>(&self, view: &PlaneView<'_>, sink: &mut S) {
292 let weights = self.nodal_weights(view);
293 let m = SVector::<f64, 8>::from_fn(|a, _| self.density * weights[a / 2]);
294 sink.add(&plane_dofs::<4, 8>(&self.nodes), &m);
295 }
296
297 pub(super) fn load_vector(&self, view: &PlaneView<'_>, load: &ElementLoad) -> SVector<f64, 8> {
300 load_vector(
301 &plane_coords(&self.nodes, view),
302 &self.nodal_weights(view),
303 self.thickness,
304 load,
305 )
306 }
307
308 pub(super) fn assemble_load<S: VectorSink<NodeId>>(
309 &self,
310 view: &PlaneView<'_>,
311 load: &ElementLoad,
312 sink: &mut S,
313 ) {
314 sink.add(
315 &plane_dofs::<4, 8>(&self.nodes),
316 &self.load_vector(view, load),
317 );
318 }
319
320 pub(super) fn commit(&mut self, view: &PlaneView<'_>) {
321 let u = plane_displacement::<4, 8>(&self.nodes, view);
322 let strains = self.strains(&self.cache_or_compute(view), &u);
323 for (material, strain) in self.materials.iter_mut().zip(&strains) {
324 *material = material.commit(strain);
325 }
326 }
327
328 pub(super) fn local_force(&self, view: &PlaneView<'_>) -> ElementForce {
329 let u = plane_displacement::<4, 8>(&self.nodes, view);
330 let cache = self.cache_or_compute(view);
331 if let Some(enhanced) = &cache.enhanced {
332 return (enhanced.k * u).into();
333 }
334 let mut r = SVector::<f64, 8>::zeros();
335 for (g, material) in cache.points.iter().zip(&self.materials) {
336 let (stress, _) = material.trial_stress_tangent(&(g.b * u));
337 r += self.thickness * g.weight * g.b.transpose() * stress;
338 }
339 r.into()
340 }
341
342 pub(super) fn gauss_responses(&self, view: &PlaneView<'_>) -> Vec<GaussResponse> {
345 let u = plane_displacement::<4, 8>(&self.nodes, view);
346 let strains = self.strains(&self.cache_or_compute(view), &u);
347 strains
348 .iter()
349 .zip(&self.materials)
350 .map(|(strain, material)| {
351 let (stress, _) = material.trial_stress_tangent(strain);
352 GaussResponse {
353 strain: (*strain).into(),
354 stress: stress.into(),
355 }
356 })
357 .collect()
358 }
359
360 #[cfg(test)]
361 pub(super) fn cache(&self) -> Option<Cache> {
362 self.cache
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use nalgebra::SymmetricEigen;
369 use slotmap::SlotMap;
370
371 use super::*;
372 use crate::model::{DofRef, ElementLoad, Node};
373
374 struct Collect {
375 k: SMatrix<f64, 8, 8>,
376 r: SVector<f64, 8>,
377 }
378
379 impl TangentSink<NodeId> for Collect {
380 fn add<const N: usize>(
381 &mut self,
382 _dofs: &[DofRef<NodeId>; N],
383 k: &SMatrix<f64, N, N>,
384 r: &SVector<f64, N>,
385 ) {
386 self.k = SMatrix::from_fn(|i, j| k[(i, j)]);
387 self.r = SVector::from_fn(|i, _| r[i]);
388 }
389 }
390
391 struct Mass(SVector<f64, 8>);
392
393 impl VectorSink<NodeId> for Mass {
394 fn add<const N: usize>(&mut self, _dofs: &[DofRef<NodeId>; N], v: &SVector<f64, N>) {
395 self.0 = SVector::from_fn(|i, _| v[i]);
396 }
397 }
398
399 fn setup(coords: [[f64; 2]; 4]) -> (SlotMap<NodeId, Node>, [NodeId; 4]) {
400 let mut nodes = SlotMap::with_key();
401 let ids = coords.map(|c| nodes.insert(Node::new(c)));
402 (nodes, ids)
403 }
404
405 fn assemble(el: &Quad4, nodes: &SlotMap<NodeId, Node>) -> Collect {
406 let mut sink = Collect {
407 k: SMatrix::zeros(),
408 r: SVector::zeros(),
409 };
410 el.assemble_tangent(&PlaneView::new(nodes), &mut sink);
411 sink
412 }
413
414 fn masses(el: &Quad4, nodes: &SlotMap<NodeId, Node>) -> SVector<f64, 8> {
415 let mut sink = Mass(SVector::zeros());
416 el.assemble_mass(&PlaneView::new(nodes), &mut sink);
417 sink.0
418 }
419
420 const SKEW: [[f64; 2]; 4] = [[0.0, 0.0], [2.0, 0.2], [2.3, 1.7], [-0.1, 1.4]];
421
422 fn material() -> PlaneMaterial {
423 PlaneMaterial::plane_stress(30e3, 0.2).unwrap()
424 }
425
426 #[test]
427 fn stiffness_matches_the_closed_form_for_the_unit_square() {
428 let (nodes, ids) = setup([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
431 let el = Quad4::new(ids, 1.0, PlaneMaterial::plane_stress(1.0, 0.0).unwrap());
432 let k = assemble(&el, &nodes).k;
433 assert!((k[(0, 0)] - 0.5).abs() < 1e-14);
434 assert!((k[(0, 1)] - 0.125).abs() < 1e-14);
435 assert!((k[(0, 2)] + 0.25).abs() < 1e-14);
436 for row in 0..8 {
438 let sum_x: f64 = (0..4).map(|n| k[(row, 2 * n)]).sum();
439 let sum_y: f64 = (0..4).map(|n| k[(row, 2 * n + 1)]).sum();
440 assert!(sum_x.abs() < 1e-14 && sum_y.abs() < 1e-14);
441 }
442 }
443
444 #[test]
445 fn stiffness_is_symmetric_with_exactly_three_rigid_body_modes() {
446 let (nodes, ids) = setup(SKEW);
447 let el = Quad4::new(ids, 0.4, material());
448 let k = assemble(&el, &nodes).k;
449 assert!((k - k.transpose()).abs().max() < 1e-9 * k.abs().max());
450 let eigen = SymmetricEigen::new(k).eigenvalues;
451 let largest = eigen.max();
452 assert_eq!(
453 eigen.iter().filter(|&&l| l.abs() < 1e-9 * largest).count(),
454 3,
455 "{eigen}"
456 );
457 assert!(eigen.iter().all(|&l| l > -1e-9 * largest), "{eigen}");
458 }
459
460 #[test]
461 fn a_rigid_motion_is_stress_free() {
462 let (mut nodes, ids) = setup(SKEW);
463 let el = Quad4::new(ids, 0.4, material());
464 let (t, theta) = ([0.01, -0.02], 1e-5);
465 for id in ids {
466 let [x, y] = nodes[id].coords;
467 nodes[id].displacement = [t[0] - theta * y, t[1] + theta * x, 0.0];
468 }
469 assert!(assemble(&el, &nodes).r.abs().max() < 1e-9);
470 }
471
472 #[test]
473 fn cyclic_relabeling_of_the_nodes_gives_the_same_stiffness() {
474 let (nodes, ids) = setup(SKEW);
475 let base = assemble(&Quad4::new(ids, 0.4, material()), &nodes).k;
476 for shift in 1..4 {
477 let rotated: [NodeId; 4] = std::array::from_fn(|i| ids[(i + shift) % 4]);
478 let k = assemble(&Quad4::new(rotated, 0.4, material()), &nodes).k;
479 let perm = |a: usize| 2 * ((a / 2 + shift) % 4) + a % 2;
481 for a in 0..8 {
482 for b in 0..8 {
483 assert!(
484 (k[(a, b)] - base[(perm(a), perm(b))]).abs() < 1e-9 * base.abs().max(),
485 "shift {shift} ({a},{b})"
486 );
487 }
488 }
489 }
490 }
491
492 #[test]
493 fn lumped_masses_are_the_row_sums_of_the_consistent_mass() {
494 let (nodes, ids) = setup([[0.0, 0.0], [2.0, 0.0], [1.5, 1.0], [0.5, 1.0]]);
496 let m = masses(&Quad4::new(ids, 1.0, material()).with_density(1.0), &nodes);
497 for (node, expected) in [5.0 / 12.0, 5.0 / 12.0, 1.0 / 3.0, 1.0 / 3.0]
498 .into_iter()
499 .enumerate()
500 {
501 assert!(
502 (m[2 * node] - expected).abs() < 1e-14
503 && (m[2 * node + 1] - expected).abs() < 1e-14
504 );
505 }
506 let (nodes, ids) = setup([[0.0, 0.0], [2.0, 0.0], [3.0, 1.5], [1.0, 1.5]]);
508 let m = masses(&Quad4::new(ids, 0.5, material()).with_density(4.0), &nodes);
509 assert!(m.iter().all(|&v| (v - 4.0 * 0.5 * 3.0 / 4.0).abs() < 1e-14));
510 }
511
512 #[test]
513 fn mass_sums_to_rho_t_a_in_each_direction() {
514 let (nodes, ids) = setup(SKEW);
515 let area = 0.5
517 * (0..4)
518 .map(|i| SKEW[i][0] * SKEW[(i + 1) % 4][1] - SKEW[(i + 1) % 4][0] * SKEW[i][1])
519 .sum::<f64>();
520 let (rho, t) = (2.5, 0.2);
521 let m = masses(&Quad4::new(ids, t, material()).with_density(rho), &nodes);
522 let sum_x: f64 = (0..4).map(|i| m[2 * i]).sum();
523 let sum_y: f64 = (0..4).map(|i| m[2 * i + 1]).sum();
524 assert!((sum_x - rho * t * area).abs() < 1e-12 && (sum_y - rho * t * area).abs() < 1e-12);
525 }
526
527 #[test]
528 fn prepare_is_idempotent_and_the_cache_equals_recomputation() {
529 let (nodes, ids) = setup(SKEW);
530 let mut el = Quad4::new(ids, 0.4, material());
531 let before = assemble(&el, &nodes).k;
532 assert!(el.cache().is_none());
533 let view = PlaneView::new(&nodes);
534 el.prepare(&view);
535 el.prepare(&view);
536 assert_eq!(before, assemble(&el, &nodes).k);
537 assert_eq!(el.cache(), Some(el.compute_cache(&view).unwrap()));
538 }
539
540 #[test]
541 fn invalid_geometry_is_reported_with_the_offending_corner() {
542 let (nodes, ids) = setup([[0.0, 0.0], [1.0, 0.0], [0.4, 0.4], [0.0, 1.0]]);
543 let view = PlaneView::new(&nodes);
544 let error = Quad4::new(ids, 1.0, material())
545 .validate(&view)
546 .unwrap_err();
547 assert!(error.contains("corner 2"), "{error}");
548 let (nodes, ids) = setup(SKEW);
549 assert!(Quad4::new(ids, 1.0, material())
550 .validate(&PlaneView::new(&nodes))
551 .is_ok());
552 }
553
554 #[test]
555 fn each_gauss_point_commits_its_own_material_history() {
556 let (mut nodes, ids) = setup([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
557 let mut el = Quad4::new(ids, 1.0, PlaneMaterial::Probe { last: [0.0; 3] });
558 for id in ids {
560 let [x, y] = nodes[id].coords;
561 nodes[id].displacement = [0.01 * x * y, 0.0, 0.0];
562 }
563 el.commit(&PlaneView::new(&nodes));
564 let lasts: Vec<[f64; 3]> = el
565 .materials()
566 .iter()
567 .map(|m| match m {
568 PlaneMaterial::Probe { last } => *last,
569 _ => unreachable!(),
570 })
571 .collect();
572 assert!(lasts.windows(2).any(|w| w[0] != w[1]), "{lasts:?}");
573 let reported = el.gauss_responses(&PlaneView::new(&nodes));
575 for (last, point) in lasts.iter().zip(&reported) {
576 assert!((PlaneVector::from(*last) - PlaneVector::from(point.strain)).norm() < 1e-15);
577 }
578 }
579
580 fn enhanced_on_skew() -> (Quad4, SlotMap<NodeId, Node>) {
581 let (mut nodes, ids) = setup(SKEW);
582 let el = Quad4::new(ids, 0.4, material()).with_formulation(Quad4Formulation::Enhanced);
583 for (k, id) in ids.into_iter().enumerate() {
585 let [x, y] = nodes[id].coords;
586 nodes[id].displacement = [1e-3 * x * y + 1e-4 * k as f64, -5e-4 * x * x, 0.0];
587 }
588 (el, nodes)
589 }
590
591 #[test]
592 fn recovered_stress_includes_the_internal_modes_and_differs_from_d_b_u() {
593 let (el, nodes) = enhanced_on_skew();
594 let view = PlaneView::new(&nodes);
595 let cache = el.compute_cache(&view).unwrap();
596 let enhanced = cache.enhanced.unwrap();
597 let u = plane_displacement::<4, 8>(&el.nodes, &view);
598 let alpha = enhanced.recovery * u;
599 assert!(alpha.norm() > 1e-6, "internal modes should engage");
600 let d = material().d_matrix();
601 let responses = el.gauss_responses(&view);
602 for g in 0..4 {
603 let strain = cache.points[g].b * u + enhanced.g[g] * alpha;
604 let stress = d * strain;
605 assert!((PlaneVector::from(responses[g].stress) - stress).norm() < 1e-9);
606 assert!((PlaneVector::from(responses[g].strain) - strain).norm() < 1e-15);
607 let compatible_only = d * (cache.points[g].b * u);
608 assert!((stress - compatible_only).norm() > 1e-3 * stress.norm());
609 }
610 }
611
612 #[test]
613 fn condensation_reproduces_the_resistance_from_the_recovered_stresses() {
614 let (el, nodes) = enhanced_on_skew();
615 let view = PlaneView::new(&nodes);
616 let mut sink = Collect {
617 k: SMatrix::zeros(),
618 r: SVector::zeros(),
619 };
620 el.assemble_tangent(&view, &mut sink);
621 let cache = el.compute_cache(&view).unwrap();
622 let mut from_stress = SVector::<f64, 8>::zeros();
623 for (g, response) in cache.points.iter().zip(el.gauss_responses(&view)) {
624 from_stress +=
625 el.thickness * g.weight * g.b.transpose() * PlaneVector::from(response.stress);
626 }
627 assert!((from_stress - sink.r).norm() < 1e-9 * sink.r.norm());
628 assert_eq!(
629 sink.r,
630 sink.k * plane_displacement::<4, 8>(&el.nodes, &view)
631 );
632 assert!((sink.k - sink.k.transpose()).abs().max() < 1e-9 * sink.k.abs().max());
634 let eigen = SymmetricEigen::new(sink.k).eigenvalues;
635 let largest = eigen.max();
636 assert_eq!(
637 eigen.iter().filter(|&&l| l.abs() < 1e-9 * largest).count(),
638 3,
639 "{eigen}"
640 );
641 }
642
643 #[test]
644 fn a_nonlinear_material_is_rejected_by_the_enhanced_element_only() {
645 let (nodes, ids) = setup(SKEW);
646 let view = PlaneView::new(&nodes);
647 let probe = PlaneMaterial::Probe { last: [0.0; 3] };
648 let full = Quad4::new(ids, 1.0, probe.clone());
649 assert!(full.validate(&view).is_ok());
650 let enhanced = full.with_formulation(Quad4Formulation::Enhanced);
651 assert_eq!(
652 enhanced.validate(&view),
653 Err("enhanced Quad4 requires a linear plane material")
654 );
655 }
656
657 #[test]
658 fn the_enhanced_stiffness_is_softer_than_the_full_one_in_bending_only() {
659 let (nodes, ids) = setup([[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]]);
660 let full = Quad4::new(ids, 1.0, material());
661 let enhanced = full.clone().with_formulation(Quad4Formulation::Enhanced);
662 let (kf, ke) = (assemble(&full, &nodes).k, assemble(&enhanced, &nodes).k);
663 let bend = SVector::<f64, 8>::from_row_slice(&[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
665 + SVector::from_fn(|i, _| {
666 let node = i / 2;
667 let [x, y] = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]][node];
668 if i % 2 == 0 {
669 x * (y - 0.5)
670 } else {
671 -0.5 * x * x
672 }
673 });
674 assert!((bend.transpose() * ke * bend)[0] < 0.5 * (bend.transpose() * kf * bend)[0]);
675 let stretch = SVector::<f64, 8>::from_fn(|i, _| {
677 if i % 2 == 0 {
678 1e-3 * [0.0, 2.0, 2.0, 0.0][i / 2]
679 } else {
680 0.0
681 }
682 });
683 let (ef, ee) = (
684 (stretch.transpose() * kf * stretch)[0],
685 (stretch.transpose() * ke * stretch)[0],
686 );
687 assert!((ef - ee).abs() < 1e-12 * ef);
688 }
689
690 fn force_sums(v: &SVector<f64, 8>) -> [f64; 2] {
691 [
692 (0..4).map(|i| v[2 * i]).sum(),
693 (0..4).map(|i| v[2 * i + 1]).sum(),
694 ]
695 }
696
697 #[test]
698 fn body_force_totals_b_t_a_and_edge_loads_total_traction_times_length() {
699 let (nodes, ids) = setup([[0.0, 0.0], [2.0, 0.0], [1.5, 1.0], [0.5, 1.0]]);
700 let el = Quad4::new(ids, 0.4, material());
701 let view = PlaneView::new(&nodes);
702 let area = 1.5; let body = force_sums(&el.load_vector(&view, &ElementLoad::body(3.0, -2.0)));
704 assert!(
705 (body[0] - 3.0 * 0.4 * area).abs() < 1e-14
706 && (body[1] + 2.0 * 0.4 * area).abs() < 1e-14
707 );
708 let length = 1.25_f64.sqrt();
710 let traction =
711 force_sums(&el.load_vector(&view, &ElementLoad::edge_traction(1, 5.0, -7.0)));
712 assert!((traction[0] - 5.0 * 0.4 * length).abs() < 1e-13);
713 assert!((traction[1] + 7.0 * 0.4 * length).abs() < 1e-13);
714 let v = el.load_vector(&view, &ElementLoad::edge_traction(1, 5.0, -7.0));
716 assert!(v[0] == 0.0 && v[1] == 0.0 && v[6] == 0.0 && v[7] == 0.0);
717 }
718
719 #[test]
720 fn pressure_acts_along_the_inward_normal() {
721 let (nodes, ids) = setup([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
722 let el = Quad4::new(ids, 1.0, material());
723 let view = PlaneView::new(&nodes);
724 for (edge, expected) in [[0.0, 1.0], [-1.0, 0.0], [0.0, -1.0], [1.0, 0.0]]
726 .into_iter()
727 .enumerate()
728 {
729 let sum = force_sums(&el.load_vector(&view, &ElementLoad::edge_pressure(edge, 2.0)));
730 assert!(
731 (sum[0] - 2.0 * expected[0]).abs() < 1e-14,
732 "edge {edge}: {sum:?}"
733 );
734 assert!(
735 (sum[1] - 2.0 * expected[1]).abs() < 1e-14,
736 "edge {edge}: {sum:?}"
737 );
738 }
739 }
740
741 #[test]
742 fn gauss_responses_equal_d_b_u_for_the_plain_element() {
743 let (mut nodes, ids) = setup(SKEW);
744 let mat = material();
745 let el = Quad4::new(ids, 1.0, mat.clone());
746 for id in ids {
748 let [x, y] = nodes[id].coords;
749 nodes[id].displacement = [1e-3 * x * y, -2e-3 * x + 5e-4 * y, 0.0];
750 }
751 let view = PlaneView::new(&nodes);
752 let u = plane_displacement::<4, 8>(&el.nodes, &view);
753 let geometry = point_geometry(&plane_coords(&el.nodes, &view));
754 for (g, response) in el.gauss_responses(&view).iter().enumerate() {
755 let expected = mat.d_matrix() * (geometry[g].b * u);
756 assert!((PlaneVector::from(response.stress) - expected).norm() < 1e-12);
757 }
758 }
759}