DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_Elements.cpp
Go to the documentation of this file.
1/**
2 * @file test_Elements.cpp
3 * @brief Comprehensive doctest-based unit tests for element traits and shape functions.
4 *
5 * Tests cover:
6 * A. ElementTraits data integrity:
7 * 1. Basic identification fields (elemType, dim, order, etc.)
8 * 2. Standard coordinates consistency
9 * 3. Face definitions (GetFaceType, faceNodes)
10 * 4. Order elevation (elevatedType, elevSpans, elevNodeSpanTypes)
11 * 5. Bisection refinement (numBisect, bisectElements)
12 * 6. VTK compatibility (vtkCellType, vtkNodeOrder)
13 *
14 * B. Shape function correctness:
15 * 1. Partition of unity: sum_j N_j(p) == 1
16 * 2. Nodal interpolation: N_i(node_j) == delta_ij
17 * 3. First derivative consistency (FD vs analytic)
18 * 4. Second derivative consistency (FD of D1)
19 *
20 * This is a serial test (no MPI required).
21 */
22
23#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
24#include "doctest.h"
25
26#include "Geom/Elements.hpp"
27#include "Geom/Quadrature.hpp"
28
29#include <cmath>
30#include <random>
31#include <array>
32#include <iostream>
33#include <set>
34
35using namespace DNDS::Geom;
36using namespace DNDS::Geom::Elem;
37
38// Convenience: 3D point from 1–3 coords, rest zeroed.
39static tPoint MakePoint(t_real x, t_real y = 0, t_real z = 0)
40{
41 tPoint p;
42 p << x, y, z;
43 return p;
44}
45
46// Get reference node coordinates (columns of a 3×N matrix).
47static Eigen::Matrix<t_real, 3, Eigen::Dynamic> NodeCoords(ElemType t)
48{
49 return GetStandardCoord(t);
50}
51
52// Random interior point for each param-space type.
53// Uses a fixed seed for reproducibility.
54static tPoint RandomInteriorPoint(ElemType t, std::mt19937 &rng)
55{
56 std::uniform_real_distribution<t_real> u01(0.05, 0.95);
57 std::uniform_real_distribution<t_real> um11(-0.8, 0.8);
58
59 Element e{t};
60 auto ps = e.GetParamSpace();
61
62 switch (ps)
63 {
64 case LineSpace:
65 return MakePoint(um11(rng));
66 case TriSpace:
67 {
68 // Ensure xi + et < 1
69 t_real xi = u01(rng) * 0.5;
70 t_real et = u01(rng) * (1.0 - xi) * 0.8;
71 return MakePoint(xi, et);
72 }
73 case QuadSpace:
74 return MakePoint(um11(rng), um11(rng));
75 case TetSpace:
76 {
77 t_real xi = u01(rng) * 0.3;
78 t_real et = u01(rng) * (1.0 - xi) * 0.3;
79 t_real zt = u01(rng) * (1.0 - xi - et) * 0.8;
80 return MakePoint(xi, et, zt);
81 }
82 case HexSpace:
83 return MakePoint(um11(rng), um11(rng), um11(rng));
84 case PrismSpace:
85 {
86 t_real xi = u01(rng) * 0.5;
87 t_real et = u01(rng) * (1.0 - xi) * 0.8;
88 t_real zt = um11(rng);
89 return MakePoint(xi, et, zt);
90 }
91 case PyramidSpace:
92 {
93 // zt in (0, 1), base in (-1,1)^2 scaled by (1-zt)
94 t_real zt = u01(rng) * 0.8;
95 t_real scale = (1.0 - zt) * 0.8;
96 t_real xi = um11(rng) * scale;
97 t_real et = um11(rng) * scale;
98 return MakePoint(xi, et, zt);
99 }
100 default:
101 return MakePoint(0);
102 }
103}
104
105// All valid element types.
106static constexpr ElemType AllTypes[] = {
110
111// 2D element types that have faces
112static constexpr ElemType Elem2DWithFaces[] = {
113 Tri3, Tri6, Quad4, Quad9};
114
115// 3D element types that have faces
116static constexpr ElemType Elem3DWithFaces[] = {
118
119// O1 element types
120static constexpr ElemType O1Types[] = {
122
123// O2 element types
124static constexpr ElemType O2Types[] = {
126
127// ===================================================================
128// Part A: ElementTraits Data Integrity Tests
129// ===================================================================
130
131TEST_CASE("ElementTraits: basic identification fields are consistent")
132{
133 for (auto t : AllTypes)
134 {
135 Element e{t};
136 CAPTURE(t);
137
138 // Basic sanity checks
139 CHECK(e.GetDim() >= 1);
140 CHECK(e.GetDim() <= 3);
141 CHECK(e.GetNumNodes() >= e.GetNumVertices());
142 CHECK(e.GetNumVertices() >= 2); // At least a line
143
144 // Order should be 1 or 2 for supported elements
145 CHECK((e.GetOrder() == 1 || e.GetOrder() == 2));
146
147 // ParamSpace volume should be positive (accessed via helper)
148 CHECK(ParamSpaceVolume(e.GetParamSpace()) > 0);
149 }
150}
151
152TEST_CASE("ElementTraits: standard coordinates have correct dimensions")
153{
154 for (auto t : AllTypes)
155 {
156 auto coords = NodeCoords(t);
157 Element e{t};
158 CAPTURE(t);
159
160 // Should have 3 rows (x, y, z)
161 CHECK(coords.rows() == 3);
162
163 // Should have numNodes columns
164 CHECK(coords.cols() == e.GetNumNodes());
165
166 // For 1D elements, y and z should be zero
167 if (e.GetDim() == 1)
168 {
169 for (DNDS::index i = 0; i < e.GetNumNodes(); i++)
170 {
171 CHECK(coords(1, i) == doctest::Approx(0.0));
172 CHECK(coords(2, i) == doctest::Approx(0.0));
173 }
174 }
175
176 // For 2D elements, z should be zero
177 if (e.GetDim() == 2)
178 {
179 for (DNDS::index i = 0; i < e.GetNumNodes(); i++)
180 {
181 CHECK(coords(2, i) == doctest::Approx(0.0));
182 }
183 }
184 }
185}
186
187TEST_CASE("ElementTraits: face definitions are consistent for 2D elements")
188{
189 for (auto t : Elem2DWithFaces)
190 {
191 Element e{t};
192 CAPTURE(t);
193
194 int numFaces = e.GetNumFaces();
195 CHECK(numFaces > 0);
196
197 for (int iFace = 0; iFace < numFaces; iFace++)
198 {
199 Element faceElem = e.ObtainFace(iFace);
200
201 // Face should have consistent dimensionality
202 CHECK(faceElem.GetDim() == e.GetDim() - 1);
203
204 // Face should be a line element
205 CHECK(faceElem.GetDim() == 1);
206 }
207 }
208}
209
210TEST_CASE("ElementTraits: face definitions are consistent for 3D elements")
211{
212 for (auto t : Elem3DWithFaces)
213 {
214 Element e{t};
215 CAPTURE(t);
216
217 int numFaces = e.GetNumFaces();
218 CHECK(numFaces > 0);
219
220 for (int iFace = 0; iFace < numFaces; iFace++)
221 {
222 Element faceElem = e.ObtainFace(iFace);
223
224 // Face should have consistent dimensionality
225 CHECK(faceElem.GetDim() == e.GetDim() - 1);
226
227 // Face should be a 2D element
228 CHECK(faceElem.GetDim() == 2);
229 }
230 }
231}
232
233TEST_CASE("ElementTraits: ExtractFaceNodes works correctly")
234{
235 // Test Tri3 face extraction
236 {
237 Element e{Tri3};
238 std::vector<DNDS::index> nodes = {0, 1, 2}; // 3 nodes
239 std::array<DNDS::index, 2> faceNodes;
240
241 // Edge 0: should extract nodes 0 and 1
242 e.ExtractFaceNodes(0, nodes, faceNodes);
243 CHECK(faceNodes[0] == 0);
244 CHECK(faceNodes[1] == 1);
245
246 // Edge 1: should extract nodes 1 and 2
247 e.ExtractFaceNodes(1, nodes, faceNodes);
248 CHECK(faceNodes[0] == 1);
249 CHECK(faceNodes[1] == 2);
250
251 // Edge 2: should extract nodes 2 and 0
252 e.ExtractFaceNodes(2, nodes, faceNodes);
253 CHECK(faceNodes[0] == 2);
254 CHECK(faceNodes[1] == 0);
255 }
256
257 // Test Quad4 face extraction
258 {
259 Element e{Quad4};
260 std::vector<DNDS::index> nodes = {0, 1, 2, 3}; // 4 nodes
261 std::array<DNDS::index, 2> faceNodes;
262
263 // Each face should have 2 nodes
264 for (int i = 0; i < 4; i++)
265 {
266 e.ExtractFaceNodes(i, nodes, faceNodes);
267 // Just verify indices are valid
268 CHECK(faceNodes[0] < 4);
269 CHECK(faceNodes[1] < 4);
270 }
271 }
272}
273
274TEST_CASE("ElementTraits: order elevation data is consistent for O1 elements")
275{
276 // O1 elements should elevate to O2
277 for (auto t : O1Types)
278 {
279 Element e{t};
280 CAPTURE(t);
281
282 Element elevated = e.ObtainElevatedElem();
283
284 // Elevated element should exist
285 CHECK(elevated.type != UnknownElem);
286
287 // Elevated element should have more nodes
288 CHECK(elevated.GetNumNodes() > e.GetNumNodes());
289
290 // Elevated element should have same dimension
291 CHECK(elevated.GetDim() == e.GetDim());
292
293 // Should have elevation nodes defined
294 CHECK(e.GetNumElev_O1O2() > 0);
295 }
296}
297
298TEST_CASE("ElementTraits: order elevation data for specific elements")
299{
300 // Line2 -> Line3
301 {
302 Element e{Line2};
303 CHECK(e.ObtainElevatedElem().type == Line3);
304 CHECK(e.GetNumElev_O1O2() == 1); // One edge midpoint
305
306 // Check elevation span type
307 Element spanElem = e.ObtainElevNodeSpan(0);
308 CHECK(spanElem.type == Line2); // Edge span
309 }
310
311 // Tri3 -> Tri6
312 {
313 Element e{Tri3};
314 CHECK(e.ObtainElevatedElem().type == Tri6);
315 CHECK(e.GetNumElev_O1O2() == 3); // Three edge midpoints
316 }
317
318 // Quad4 -> Quad9
319 {
320 Element e{Quad4};
321 CHECK(e.ObtainElevatedElem().type == Quad9);
322 CHECK(e.GetNumElev_O1O2() == 5); // 4 edges + 1 face center
323 }
324
325 // Hex8 -> Hex27
326 {
327 Element e{Hex8};
328 CHECK(e.ObtainElevatedElem().type == Hex27);
329 CHECK(e.GetNumElev_O1O2() == 19); // 12 edges + 6 faces + 1 center
330 }
331}
332
333TEST_CASE("ElementTraits: O2 elements do not have further elevation")
334{
335 // O2 elements should not have elevation defined
336 for (auto t : O2Types)
337 {
338 Element e{t};
339 CAPTURE(t);
340
341 Element elevated = e.ObtainElevatedElem();
342 CHECK(elevated.type == UnknownElem);
343 CHECK(e.GetNumElev_O1O2() == 0);
344 }
345}
346
347TEST_CASE("ElementTraits: ExtractElevNodeSpanNodes works correctly")
348{
349 // Test Tri3 elevation spans
350 {
351 Element e{Tri3};
352 std::vector<DNDS::index> nodes = {0, 1, 2}; // Parent nodes
353 std::array<DNDS::index, 2> spanNodes;
354
355 // Each elevation span should connect 2 parent nodes
356 for (int i = 0; i < e.GetNumElev_O1O2(); i++)
357 {
358 e.ExtractElevNodeSpanNodes(i, nodes, spanNodes);
359 CHECK(spanNodes[0] < 3); // References valid parent node
360 CHECK(spanNodes[1] < 3);
361 }
362 }
363}
364
365TEST_CASE("ElementTraits: bisection data is valid for O2 elements")
366{
367 // Line3 bisection
368 {
369 Element e{Line3};
370 CHECK(e.GetO2NumBisect() == 2);
371
372 // Each sub-element should be Line2
373 for (int i = 0; i < e.GetO2NumBisect(); i++)
374 {
375 CHECK(e.ObtainO2BisectElem(i).type == Line2);
376 }
377 }
378
379 // Tri6 bisection
380 {
381 Element e{Tri6};
382 CHECK(e.GetO2NumBisect() == 4); // 4 sub-triangles
383 for (int i = 0; i < e.GetO2NumBisect(); i++)
384 {
385 CHECK(e.ObtainO2BisectElem(i).type == Tri3);
386 }
387 }
388
389 // Tet10 bisection
390 {
391 Element e{Tet10};
392 CHECK(e.GetO2NumBisect() == 8); // 8 sub-tets
393 for (int i = 0; i < e.GetO2NumBisect(); i++)
394 {
395 CHECK(e.ObtainO2BisectElem(i).type == Tet4);
396 }
397 }
398
399 // Hex27 bisection
400 {
401 Element e{Hex27};
402 CHECK(e.GetO2NumBisect() == 8); // 8 sub-hexes
403 for (int i = 0; i < e.GetO2NumBisect(); i++)
404 {
405 CHECK(e.ObtainO2BisectElem(i).type == Hex8);
406 }
407 }
408
409 // Prism18 bisection
410 {
411 Element e{Prism18};
412 CHECK(e.GetO2NumBisect() == 8); // 8 sub-prisms
413 for (int i = 0; i < e.GetO2NumBisect(); i++)
414 {
415 CHECK(e.ObtainO2BisectElem(i).type == Prism6);
416 }
417 }
418}
419
420TEST_CASE("ElementTraits: VTK conversion works correctly")
421{
422 // Test VTK conversion for each element type
423 for (auto t : AllTypes)
424 {
425 Element e{t};
426 CAPTURE(t);
427
428 // Create dummy node data
429 std::vector<t_real> nodes(e.GetNumNodes());
430 for (int i = 0; i < e.GetNumNodes(); i++)
431 nodes[i] = static_cast<t_real>(i);
432
433 // Convert to VTK
434 auto [vtkCellType, vtkNodes] = ToVTKVertsAndData(e, nodes);
435
436 // VTK cell type should be valid
437 CHECK(vtkCellType > 0);
438
439 // VTK nodes should have correct size
440 CHECK(vtkNodes.size() <= e.GetNumNodes());
441 CHECK(vtkNodes.size() > 0);
442 }
443}
444
445TEST_CASE("ElementTraits: VTK node order is a valid permutation for simple elements")
446{
447 // Test VTK node ordering produces valid permutations
448 DispatchElementType(Line2, [](auto traits) {
449 std::set<int> seen;
450 for (size_t i = 0; i < 2; i++)
451 seen.insert(traits.vtkNodeOrder[i]);
452 CHECK(seen.size() == 2); // All unique
453 CHECK(*seen.begin() == 0);
454 CHECK(*seen.rbegin() == 1);
455 });
456
457 DispatchElementType(Line3, [](auto traits) {
458 std::set<int> seen;
459 for (size_t i = 0; i < 3; i++)
460 seen.insert(traits.vtkNodeOrder[i]);
461 CHECK(seen.size() == 3); // All unique
462 // VTK uses different ordering: 0, 2, 1 (midpoint last)
463 CHECK(traits.vtkNodeOrder[0] == 0);
464 CHECK(traits.vtkNodeOrder[1] == 2);
465 CHECK(traits.vtkNodeOrder[2] == 1);
466 });
467
468 DispatchElementType(Tri6, [](auto traits) {
469 std::set<int> seen;
470 for (size_t i = 0; i < 6; i++)
471 seen.insert(traits.vtkNodeOrder[i]);
472 CHECK(seen.size() == 6); // All unique
473 });
474
475 DispatchElementType(Hex8, [](auto traits) {
476 std::set<int> seen;
477 for (size_t i = 0; i < 8; i++)
478 seen.insert(traits.vtkNodeOrder[i]);
479 CHECK(seen.size() == 8); // All unique
480 CHECK(traits.vtkCellType == 12); // VTK_HEXAHEDRON
481 });
482}
483
484// ===================================================================
485// Part B: Shape Function Tests
486// ===================================================================
487
488TEST_CASE("Shape functions: partition of unity")
489{
490 constexpr int nTrials = 10;
491 std::mt19937 rng(42);
492
493 for (auto t : AllTypes)
494 {
495 Element e{t};
496 CAPTURE(t);
497 for (int trial = 0; trial < nTrials; trial++)
498 {
499 auto p = RandomInteriorPoint(t, rng);
500 tNj Nj;
501 e.GetNj(p, Nj);
502
503 t_real sum = Nj.sum();
504 CHECK(sum == doctest::Approx(1.0).epsilon(1e-12));
505 }
506 }
507}
508
509TEST_CASE("Shape functions: nodal interpolation (Kronecker delta)")
510{
511 for (auto t : AllTypes)
512 {
513 Element e{t};
514 auto coords = NodeCoords(t);
515 DNDS::index nn = e.GetNumNodes();
516 CAPTURE(t);
517
518 for (DNDS::index j = 0; j < nn; j++)
519 {
520 tPoint p = coords.col(j);
521 tNj Nj;
522 e.GetNj(p, Nj);
523
524 for (DNDS::index i = 0; i < nn; i++)
525 {
526 t_real expected = (i == j) ? 1.0 : 0.0;
527 CAPTURE(i);
528 CAPTURE(j);
529 CHECK(Nj(0, i) == doctest::Approx(expected).epsilon(1e-10));
530 }
531 }
532 }
533}
534
535TEST_CASE("Shape functions: D1 derivatives vs finite difference")
536{
537 constexpr int nTrials = 5;
538 constexpr t_real h = 1e-7;
539 constexpr t_real tol = 1e-5;
540 std::mt19937 rng(123);
541
542 for (auto t : AllTypes)
543 {
544 Element e{t};
545 DNDS::index nn = e.GetNumNodes();
546 int dim = e.GetDim();
547 CAPTURE(t);
548
549 for (int trial = 0; trial < nTrials; trial++)
550 {
551 auto p = RandomInteriorPoint(t, rng);
552
553 tD1Nj D1Nj;
554 e.GetD1Nj(p, D1Nj);
555
556 // Finite difference for each parametric direction
557 for (int d = 0; d < dim; d++)
558 {
559 tPoint pp = p, pm = p;
560 pp[d] += h;
561 pm[d] -= h;
562
563 tNj Np, Nm;
564 e.GetNj(pp, Np);
565 e.GetNj(pm, Nm);
566
567 for (DNDS::index j = 0; j < nn; j++)
568 {
569 t_real fd = (Np(0, j) - Nm(0, j)) / (2 * h);
570 t_real an = D1Nj(d, j);
571 CAPTURE(d);
572 CAPTURE(j);
573 CHECK(an == doctest::Approx(fd).epsilon(tol));
574 }
575 }
576 }
577 }
578}
579
580TEST_CASE("Shape functions: D2 derivatives vs finite difference of D1")
581{
582 constexpr t_real h = 1e-6;
583 constexpr t_real tol = 1e-3;
584 std::mt19937 rng(456);
585
586 for (auto t : AllTypes)
587 {
588 Element e{t};
589 DNDS::index nn = e.GetNumNodes();
590 int dim = e.GetDim();
591 CAPTURE(t);
592
593 auto p = RandomInteriorPoint(t, rng);
594
595 tDiNj DiNj;
596 e.GetDiNj(p, DiNj, 2);
597
598 // Test pure second derivatives (d^2/dxi_d^2)
599 for (int d = 0; d < dim; d++)
600 {
601 tPoint pp = p, pm = p;
602 pp[d] += h;
603 pm[d] -= h;
604
605 tD1Nj D1p, D1m;
606 e.GetD1Nj(pp, D1p);
607 e.GetD1Nj(pm, D1m);
608
609 for (DNDS::index j = 0; j < nn; j++)
610 {
611 t_real fd_d2 = (D1p(d, j) - D1m(d, j)) / (2 * h);
612
613 // Row index for d^2/dxi_d^2 in the DiNj layout
614 DNDS::index row;
615 if (dim == 1)
616 row = 2; // d^2/dxi^2
617 else if (dim == 2)
618 row = 3 + (d == 0 ? 0 : 2); // row 3=d2/dxi2, row 5=d2/det2
619 else
620 row = 4 + d; // rows 4,5,6 for d2/dxi2, d2/det2, d2/dzt2
621
622 t_real an = DiNj(row, j);
623 CAPTURE(d);
624 CAPTURE(j);
625 CHECK(an == doctest::Approx(fd_d2).epsilon(tol));
626 }
627 }
628 }
629}
630
631TEST_CASE("Shape functions: all derivatives are finite")
632{
633 // Verify shape function derivatives don't produce NaN or Inf
634 std::mt19937 rng(789);
635
636 for (auto t : AllTypes)
637 {
638 Element e{t};
639 auto p = RandomInteriorPoint(t, rng);
640 CAPTURE(t);
641
642 tDiNj DiNj;
643 e.GetDiNj(p, DiNj, 3);
644
645 // Check all entries are finite
646 for (DNDS::index i = 0; i < DiNj.rows(); i++)
647 {
648 for (DNDS::index j = 0; j < DiNj.cols(); j++)
649 {
650 CHECK(std::isfinite(DiNj(i, j)));
651 }
652 }
653 }
654}
655
656// ===================================================================
657// Part C: Face Normal Orientation Tests
658// ===================================================================
659//
660// On the standard reference element, all faces are flat (even for O2
661// elements, since mid-edge nodes lie at exact geometric midpoints).
662// This means the face normal is constant across the face.
663//
664// For each face, we compute the expected unit normal analytically from
665// the face vertex positions, then verify that the Jacobian-derived
666// normal at every quadrature point matches it (direction and magnitude).
667//
668// 2D faces (edges in the xy-plane):
669// tangent = v1 - v0
670// expected outward normal direction = (tangent.y, -tangent.x, 0)
671//
672// 3D faces (triangles/quads):
673// expected outward normal direction = (v1 - v0) × (v2 - v0)
674
675/// Compute the centroid of the reference element by averaging all vertex
676/// coordinates (not all nodes — just the first numVertices).
677static tPoint CellCentroid(ElemType t)
678{
679 auto coords = NodeCoords(t);
680 Element e{t};
681 tPoint c = tPoint::Zero();
682 for (int i = 0; i < e.GetNumVertices(); i++)
683 c += coords.col(i);
684 c /= e.GetNumVertices();
685 return c;
686}
687
688/// Extract face node coordinates as a 3×N_face_nodes matrix.
689static Eigen::Matrix<t_real, 3, Eigen::Dynamic> FaceCoords(
690 ElemType cellType, int iFace)
691{
692 auto allCoords = NodeCoords(cellType);
693 Element cell{cellType};
694 Element face = cell.ObtainFace(iFace);
695 int nFaceNodes = face.GetNumNodes();
696
697 std::array<DNDS::index, 10> faceNodeIdx;
698 Eigen::Array<DNDS::index, Eigen::Dynamic, 1> localIdx(cell.GetNumNodes());
699 for (int i = 0; i < cell.GetNumNodes(); i++)
700 localIdx(i) = i;
701
702 cell.ExtractFaceNodes(iFace, localIdx, faceNodeIdx);
703
704 Eigen::Matrix<t_real, 3, Eigen::Dynamic> fc(3, nFaceNodes);
705 for (int i = 0; i < nFaceNodes; i++)
706 fc.col(i) = allCoords.col(faceNodeIdx[i]);
707 return fc;
708}
709
710/// Compute the expected unit outward normal for a 2D element's face (edge).
711/// The face is a line segment from vertex v0 to v1 in the xy-plane.
712/// Outward normal = (dy, -dx, 0) / length, verified against cell centroid.
713static tPoint ExpectedNormal2D(
714 const Eigen::Matrix<t_real, 3, Eigen::Dynamic> &fc,
715 const tPoint &centroid)
716{
717 tPoint v0 = fc.col(0);
718 tPoint v1 = fc.col(1);
719 tPoint tangent = v1 - v0;
720 tPoint n;
721 n << tangent(1), -tangent(0), 0.0;
722
723 // Ensure outward direction (away from centroid)
724 tPoint faceMid = (v0 + v1) * 0.5;
725 if (n.dot(faceMid - centroid) < 0)
726 n = -n;
727
728 return n.normalized();
729}
730
731/// Compute the expected unit outward normal for a 3D element's face.
732/// Uses the first 3 face vertices: normal = (v1-v0) × (v2-v0).
733/// Sign is verified against the cell centroid.
734static tPoint ExpectedNormal3D(
735 const Eigen::Matrix<t_real, 3, Eigen::Dynamic> &fc,
736 const tPoint &centroid)
737{
738 tPoint v0 = fc.col(0);
739 tPoint v1 = fc.col(1);
740 tPoint v2 = fc.col(2);
741 tPoint n = (v1 - v0).cross(v2 - v0);
742
743 // Ensure outward direction (away from centroid)
744 // Use the average of the first 3 vertices as face center
745 tPoint faceMid = (v0 + v1 + v2) / 3.0;
746 if (n.dot(faceMid - centroid) < 0)
747 n = -n;
748
749 return n.normalized();
750}
751
752TEST_CASE("Face normals: 2D elements match expected unit normal at quad points")
753{
754 const t_real tol = 1e-12;
755
756 for (auto t : Elem2DWithFaces)
757 {
758 Element cell{t};
759 tPoint centroid = CellCentroid(t);
760 CAPTURE(t);
761
762 for (int iFace = 0; iFace < cell.GetNumFaces(); iFace++)
763 {
764 Element face = cell.ObtainFace(iFace);
765 auto fc = FaceCoords(t, iFace);
766 tPoint expectedN = ExpectedNormal2D(fc, centroid);
767 CAPTURE(iFace);
768
769 Quadrature quad(face, 3);
770
771 for (int iG = 0; iG < quad.GetNumPoints(); iG++)
772 {
773 auto [pParam, w] = quad.GetQuadraturePointInfo(iG);
774
775 tD01Nj D01Nj;
776 face.GetD01Nj(pParam, D01Nj);
777
778 tJacobi J = ShapeJacobianCoordD01Nj(fc, D01Nj);
779
780 // Computed normal from tangent
781 tPoint tangent = J.col(0);
782 tPoint computedN;
783 computedN << tangent(1), -tangent(0), 0.0;
784 tPoint computedUnitN = computedN.normalized();
785
786 CAPTURE(iG);
787 // Direction should match expected
788 CHECK(computedUnitN(0) == doctest::Approx(expectedN(0)).epsilon(tol));
789 CHECK(computedUnitN(1) == doctest::Approx(expectedN(1)).epsilon(tol));
790 CHECK(computedUnitN(2) == doctest::Approx(expectedN(2)).epsilon(tol));
791
792 // Normal should point outward
793 tPoint pPhys = PPhysicsCoordD01Nj(fc, D01Nj);
794 t_real outwardDot = computedN.dot(pPhys - centroid);
795 CHECK(outwardDot > 0.0);
796
797 // Face Jacobian determinant = |tangent| should be positive
798 t_real jdet = JacobiDetFace<2>(J);
799 CHECK(jdet > 0.0);
800
801 // For flat faces on standard element, |tangent| should be
802 // half the edge length (parametric range is [-1,1], length 2)
803 tPoint edgeVec = fc.col(1) - fc.col(0);
804 t_real expectedJdet = edgeVec.norm() / 2.0; // dphys/dparam scaling
805 CHECK(jdet == doctest::Approx(expectedJdet).epsilon(tol));
806 }
807 }
808 }
809}
810
811TEST_CASE("Face normals: 3D elements match expected unit normal at quad points")
812{
813 const t_real tol = 1e-10;
814
815 for (auto t : Elem3DWithFaces)
816 {
817 Element cell{t};
818 tPoint centroid = CellCentroid(t);
819 CAPTURE(t);
820
821 for (int iFace = 0; iFace < cell.GetNumFaces(); iFace++)
822 {
823 Element face = cell.ObtainFace(iFace);
824 auto fc = FaceCoords(t, iFace);
825 tPoint expectedN = ExpectedNormal3D(fc, centroid);
826 CAPTURE(iFace);
827
828 Quadrature quad(face, 3);
829
830 for (int iG = 0; iG < quad.GetNumPoints(); iG++)
831 {
832 auto [pParam, w] = quad.GetQuadraturePointInfo(iG);
833
834 tD01Nj D01Nj;
835 face.GetD01Nj(pParam, D01Nj);
836
837 tJacobi J = ShapeJacobianCoordD01Nj(fc, D01Nj);
838 tPoint computedN = J.col(0).cross(J.col(1));
839 t_real computedMag = computedN.norm();
840 tPoint computedUnitN = computedN / computedMag;
841
842 CAPTURE(iG);
843
844 // Direction should match expected unit normal
845 CHECK(computedUnitN(0) == doctest::Approx(expectedN(0)).epsilon(tol));
846 CHECK(computedUnitN(1) == doctest::Approx(expectedN(1)).epsilon(tol));
847 CHECK(computedUnitN(2) == doctest::Approx(expectedN(2)).epsilon(tol));
848
849 // Normal should point outward (away from centroid)
850 tPoint pPhys = PPhysicsCoordD01Nj(fc, D01Nj);
851 t_real outwardDot = computedN.dot(pPhys - centroid);
852 CHECK(outwardDot > 0.0);
853
854 // Face Jacobian determinant should be positive
855 t_real jdet = JacobiDetFace<3>(J);
856 CHECK(jdet > 0.0);
857 CHECK(jdet == doctest::Approx(computedMag).epsilon(tol));
858 }
859 }
860 }
861}
862
863TEST_CASE("Face normals: CGNS outward convention (no centroid correction needed)")
864{
865 // Stronger test: the cross product / rotation normal should ALREADY
866 // point outward WITHOUT needing to flip sign. This verifies that
867 // the faceNodes ordering follows CGNS outward-normal convention
868 // directly (right-hand rule for 3D, counterclockwise for 2D).
869 //
870 // This test does NOT use ExpectedNormal (which auto-flips).
871 // Instead it directly checks: dot(raw_normal, face_center - centroid) > 0.
872
873 for (auto t : Elem2DWithFaces)
874 {
875 Element cell{t};
876 tPoint centroid = CellCentroid(t);
877 CAPTURE(t);
878
879 for (int iFace = 0; iFace < cell.GetNumFaces(); iFace++)
880 {
881 auto fc = FaceCoords(t, iFace);
882 tPoint v0 = fc.col(0), v1 = fc.col(1);
883 tPoint tangent = v1 - v0;
884 tPoint rawNormal;
885 rawNormal << tangent(1), -tangent(0), 0.0;
886
887 tPoint faceMid = (v0 + v1) * 0.5;
888 t_real dot = rawNormal.dot(faceMid - centroid);
889 CAPTURE(iFace);
890 CHECK(dot > 0.0);
891 }
892 }
893
894 for (auto t : Elem3DWithFaces)
895 {
896 Element cell{t};
897 tPoint centroid = CellCentroid(t);
898 CAPTURE(t);
899
900 for (int iFace = 0; iFace < cell.GetNumFaces(); iFace++)
901 {
902 auto fc = FaceCoords(t, iFace);
903 tPoint v0 = fc.col(0), v1 = fc.col(1), v2 = fc.col(2);
904 tPoint rawNormal = (v1 - v0).cross(v2 - v0);
905
906 // Face center from vertices (use first 3 for simplicity)
907 tPoint faceMid = (v0 + v1 + v2) / 3.0;
908 t_real dot = rawNormal.dot(faceMid - centroid);
909 CAPTURE(iFace);
910 CHECK(dot > 0.0);
911 }
912 }
913}
914
915TEST_CASE("Edge topology: 3D elements have correct edge count and types")
916{
917 for (auto t : Elem3DWithFaces)
918 {
919 Element cell{t};
920 CAPTURE(t);
921
922 int numEdges = cell.GetNumEdges();
923 CHECK(numEdges > 0);
924
925 for (int iEdge = 0; iEdge < numEdges; iEdge++)
926 {
927 Element edge = cell.ObtainEdge(iEdge);
928 CAPTURE(iEdge);
929
930 // Edge should be a 1D element (Line2 or Line3)
931 CHECK(edge.GetDim() == 1);
932 CHECK(edge.GetNumNodes() >= 2);
933 CHECK(edge.GetNumNodes() <= 3);
934 }
935 }
936}
937
938TEST_CASE("Edge topology: extracted edge nodes are valid cell nodes")
939{
940 for (auto t : Elem3DWithFaces)
941 {
942 Element cell{t};
943 auto allCoords = NodeCoords(t);
944 CAPTURE(t);
945
946 // Build identity local node index array
947 std::vector<DNDS::index> localIdx(cell.GetNumNodes());
948 for (int i = 0; i < cell.GetNumNodes(); i++)
949 localIdx[i] = i;
950
951 for (int iEdge = 0; iEdge < cell.GetNumEdges(); iEdge++)
952 {
953 Element edge = cell.ObtainEdge(iEdge);
954 std::array<DNDS::index, 3> edgeNodeIdx = {-1, -1, -1};
955 cell.ExtractEdgeNodes(iEdge, localIdx, edgeNodeIdx);
956 CAPTURE(iEdge);
957
958 for (int i = 0; i < edge.GetNumNodes(); i++)
959 {
960 CAPTURE(i);
961 CHECK(edgeNodeIdx[i] >= 0);
962 CHECK(edgeNodeIdx[i] < cell.GetNumNodes());
963 }
964
965 // First two nodes (vertices) should be distinct
966 CHECK(edgeNodeIdx[0] != edgeNodeIdx[1]);
967
968 // Edge endpoints should have positive distance
969 tPoint p0 = allCoords.col(edgeNodeIdx[0]);
970 tPoint p1 = allCoords.col(edgeNodeIdx[1]);
971 CHECK((p1 - p0).norm() > 0.0);
972
973 // For quadratic edges, the midpoint should be between the endpoints
974 if (edge.GetNumNodes() == 3)
975 {
976 tPoint pMid = allCoords.col(edgeNodeIdx[2]);
977 tPoint expected = (p0 + p1) * 0.5;
978 CHECK((pMid - expected).norm() < 1e-12);
979 }
980 }
981 }
982}
983
984TEST_CASE("Edge topology: edges are unique (no duplicate edges per cell)")
985{
986 for (auto t : Elem3DWithFaces)
987 {
988 Element cell{t};
989 CAPTURE(t);
990
991 std::vector<DNDS::index> localIdx(cell.GetNumNodes());
992 for (int i = 0; i < cell.GetNumNodes(); i++)
993 localIdx[i] = i;
994
995 // Collect edge vertex pairs as sorted pairs
996 std::set<std::pair<DNDS::index, DNDS::index>> edgeSet;
997 for (int iEdge = 0; iEdge < cell.GetNumEdges(); iEdge++)
998 {
999 std::array<DNDS::index, 3> edgeNodeIdx = {-1, -1, -1};
1000 cell.ExtractEdgeNodes(iEdge, localIdx, edgeNodeIdx);
1001
1002 auto v0 = std::min(edgeNodeIdx[0], edgeNodeIdx[1]);
1003 auto v1 = std::max(edgeNodeIdx[0], edgeNodeIdx[1]);
1004 auto result = edgeSet.insert({v0, v1});
1005 CAPTURE(iEdge);
1006 CHECK(result.second); // No duplicate
1007 }
1008 CHECK(static_cast<int>(edgeSet.size()) == cell.GetNumEdges());
1009 }
1010}
Eigen::Matrix< t_real, Eigen::Dynamic, Eigen::Dynamic > tDiNj
Definition Elements.hpp:204
Eigen::Matrix< t_real, 3, Eigen::Dynamic > tD1Nj
Definition Elements.hpp:202
tJacobi ShapeJacobianCoordD01Nj(const tCoordsIn &cs, Eigen::Ref< const tD01Nj > DiNj)
Definition Elements.hpp:537
DNDS_DEVICE_CALLABLE constexpr decltype(auto) DispatchElementType(ElemType t, Func &&func)
Static dispatch over element types at compile time.
DNDS_DEVICE_CALLABLE constexpr t_real ParamSpaceVolume(ParamSpace ps)
Get the volume of a parametric space.
Eigen::Matrix< t_real, 4, Eigen::Dynamic > tD01Nj
Definition Elements.hpp:203
std::pair< int, std::vector< index > > ToVTKVertsAndData(Element e, const TIn &vin)
Definition Elements.hpp:612
Eigen::Matrix< t_real, 3, Eigen::Dynamic > GetStandardCoord(ElemType t)
Definition Elements.cpp:6
Eigen::RowVector< t_real, Eigen::Dynamic > tNj
Definition Elements.hpp:201
tPoint PPhysicsCoordD01Nj(const tCoordsIn &cs, Eigen::Ref< const tD01Nj > DiNj)
Definition Elements.hpp:543
Eigen::Matrix3d tJacobi
Definition Geometric.hpp:10
Eigen::Vector3d tPoint
Definition Geometric.hpp:9
double t_real
Definition Geometric.hpp:8
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:112
DNDS_DEVICE_CALLABLE constexpr Element ObtainEdge(t_index iEdge) const
Edge sub-element type for edge iEdge (Line2 or Line3).
Definition Elements.hpp:270
DNDS_DEVICE_CALLABLE constexpr ParamSpace GetParamSpace() const
Definition Elements.hpp:212
constexpr Element ObtainElevNodeSpan(t_index iNodeElev) const
Definition Elements.hpp:309
void ExtractEdgeNodes(t_index iEdge, const TIn &nodes, TOut &edgeNodesOut)
Definition Elements.hpp:279
DNDS_DEVICE_CALLABLE constexpr Element ObtainFace(t_index iFace) const
Definition Elements.hpp:257
constexpr Element ObtainElevatedElem() const
Definition Elements.hpp:315
void GetD01Nj(const tPoint &pParam, tD01Nj &D01Nj) const
Definition Elements.hpp:384
void ExtractFaceNodes(t_index iFace, const TIn &nodes, TOut &faceNodesOut)
Definition Elements.hpp:296
DNDS_DEVICE_CALLABLE constexpr t_index GetDim() const
Definition Elements.hpp:217
void ExtractElevNodeSpanNodes(t_index iNodeElev, const TIn &nodes, TOut &spanNodes)
Definition Elements.hpp:332
DNDS_DEVICE_CALLABLE constexpr t_index GetNumNodes() const
Definition Elements.hpp:235
tVec z(NCells)
tVec x(NCells)
CHECK(result.size()==3)
real expected
auto result
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")
const tPoint const tPoint const tPoint & p
Eigen::Vector3d n(1.0, 0.0, 0.0)