DNDSR 0.1.0.dev1+gcd065ad
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
28#include <cmath>
29#include <random>
30#include <array>
31#include <iostream>
32#include <set>
33
34using namespace DNDS::Geom;
35using namespace DNDS::Geom::Elem;
36
37// Convenience: 3D point from 1–3 coords, rest zeroed.
38static tPoint MakePoint(t_real x, t_real y = 0, t_real z = 0)
39{
40 tPoint p;
41 p << x, y, z;
42 return p;
43}
44
45// Get reference node coordinates (columns of a 3×N matrix).
46static Eigen::Matrix<t_real, 3, Eigen::Dynamic> NodeCoords(ElemType t)
47{
48 return GetStandardCoord(t);
49}
50
51// Random interior point for each param-space type.
52// Uses a fixed seed for reproducibility.
53static tPoint RandomInteriorPoint(ElemType t, std::mt19937 &rng)
54{
55 std::uniform_real_distribution<t_real> u01(0.05, 0.95);
56 std::uniform_real_distribution<t_real> um11(-0.8, 0.8);
57
58 Element e{t};
59 auto ps = e.GetParamSpace();
60
61 switch (ps)
62 {
63 case LineSpace:
64 return MakePoint(um11(rng));
65 case TriSpace:
66 {
67 // Ensure xi + et < 1
68 t_real xi = u01(rng) * 0.5;
69 t_real et = u01(rng) * (1.0 - xi) * 0.8;
70 return MakePoint(xi, et);
71 }
72 case QuadSpace:
73 return MakePoint(um11(rng), um11(rng));
74 case TetSpace:
75 {
76 t_real xi = u01(rng) * 0.3;
77 t_real et = u01(rng) * (1.0 - xi) * 0.3;
78 t_real zt = u01(rng) * (1.0 - xi - et) * 0.8;
79 return MakePoint(xi, et, zt);
80 }
81 case HexSpace:
82 return MakePoint(um11(rng), um11(rng), um11(rng));
83 case PrismSpace:
84 {
85 t_real xi = u01(rng) * 0.5;
86 t_real et = u01(rng) * (1.0 - xi) * 0.8;
87 t_real zt = um11(rng);
88 return MakePoint(xi, et, zt);
89 }
90 case PyramidSpace:
91 {
92 // zt in (0, 1), base in (-1,1)^2 scaled by (1-zt)
93 t_real zt = u01(rng) * 0.8;
94 t_real scale = (1.0 - zt) * 0.8;
95 t_real xi = um11(rng) * scale;
96 t_real et = um11(rng) * scale;
97 return MakePoint(xi, et, zt);
98 }
99 default:
100 return MakePoint(0);
101 }
102}
103
104// All valid element types.
105static constexpr ElemType AllTypes[] = {
109
110// 2D element types that have faces
111static constexpr ElemType Elem2DWithFaces[] = {
112 Tri3, Tri6, Quad4, Quad9};
113
114// 3D element types that have faces
115static constexpr ElemType Elem3DWithFaces[] = {
117
118// O1 element types
119static constexpr ElemType O1Types[] = {
121
122// O2 element types
123static constexpr ElemType O2Types[] = {
125
126// ===================================================================
127// Part A: ElementTraits Data Integrity Tests
128// ===================================================================
129
130TEST_CASE("ElementTraits: basic identification fields are consistent")
131{
132 for (auto t : AllTypes)
133 {
134 Element e{t};
135 CAPTURE(t);
136
137 // Basic sanity checks
138 CHECK(e.GetDim() >= 1);
139 CHECK(e.GetDim() <= 3);
140 CHECK(e.GetNumNodes() >= e.GetNumVertices());
141 CHECK(e.GetNumVertices() >= 2); // At least a line
142
143 // Order should be 1 or 2 for supported elements
144 CHECK((e.GetOrder() == 1 || e.GetOrder() == 2));
145
146 // ParamSpace volume should be positive (accessed via helper)
147 CHECK(ParamSpaceVolume(e.GetParamSpace()) > 0);
148 }
149}
150
151TEST_CASE("ElementTraits: standard coordinates have correct dimensions")
152{
153 for (auto t : AllTypes)
154 {
155 auto coords = NodeCoords(t);
156 Element e{t};
157 CAPTURE(t);
158
159 // Should have 3 rows (x, y, z)
160 CHECK(coords.rows() == 3);
161
162 // Should have numNodes columns
163 CHECK(coords.cols() == e.GetNumNodes());
164
165 // For 1D elements, y and z should be zero
166 if (e.GetDim() == 1)
167 {
168 for (DNDS::index i = 0; i < e.GetNumNodes(); i++)
169 {
170 CHECK(coords(1, i) == doctest::Approx(0.0));
171 CHECK(coords(2, i) == doctest::Approx(0.0));
172 }
173 }
174
175 // For 2D elements, z should be zero
176 if (e.GetDim() == 2)
177 {
178 for (DNDS::index i = 0; i < e.GetNumNodes(); i++)
179 {
180 CHECK(coords(2, i) == doctest::Approx(0.0));
181 }
182 }
183 }
184}
185
186TEST_CASE("ElementTraits: face definitions are consistent for 2D elements")
187{
188 for (auto t : Elem2DWithFaces)
189 {
190 Element e{t};
191 CAPTURE(t);
192
193 int numFaces = e.GetNumFaces();
194 CHECK(numFaces > 0);
195
196 for (int iFace = 0; iFace < numFaces; iFace++)
197 {
198 Element faceElem = e.ObtainFace(iFace);
199
200 // Face should have consistent dimensionality
201 CHECK(faceElem.GetDim() == e.GetDim() - 1);
202
203 // Face should be a line element
204 CHECK(faceElem.GetDim() == 1);
205 }
206 }
207}
208
209TEST_CASE("ElementTraits: face definitions are consistent for 3D elements")
210{
211 for (auto t : Elem3DWithFaces)
212 {
213 Element e{t};
214 CAPTURE(t);
215
216 int numFaces = e.GetNumFaces();
217 CHECK(numFaces > 0);
218
219 for (int iFace = 0; iFace < numFaces; iFace++)
220 {
221 Element faceElem = e.ObtainFace(iFace);
222
223 // Face should have consistent dimensionality
224 CHECK(faceElem.GetDim() == e.GetDim() - 1);
225
226 // Face should be a 2D element
227 CHECK(faceElem.GetDim() == 2);
228 }
229 }
230}
231
232TEST_CASE("ElementTraits: ExtractFaceNodes works correctly")
233{
234 // Test Tri3 face extraction
235 {
236 Element e{Tri3};
237 std::vector<DNDS::index> nodes = {0, 1, 2}; // 3 nodes
238 std::array<DNDS::index, 2> faceNodes;
239
240 // Edge 0: should extract nodes 0 and 1
241 e.ExtractFaceNodes(0, nodes, faceNodes);
242 CHECK(faceNodes[0] == 0);
243 CHECK(faceNodes[1] == 1);
244
245 // Edge 1: should extract nodes 1 and 2
246 e.ExtractFaceNodes(1, nodes, faceNodes);
247 CHECK(faceNodes[0] == 1);
248 CHECK(faceNodes[1] == 2);
249
250 // Edge 2: should extract nodes 2 and 0
251 e.ExtractFaceNodes(2, nodes, faceNodes);
252 CHECK(faceNodes[0] == 2);
253 CHECK(faceNodes[1] == 0);
254 }
255
256 // Test Quad4 face extraction
257 {
258 Element e{Quad4};
259 std::vector<DNDS::index> nodes = {0, 1, 2, 3}; // 4 nodes
260 std::array<DNDS::index, 2> faceNodes;
261
262 // Each face should have 2 nodes
263 for (int i = 0; i < 4; i++)
264 {
265 e.ExtractFaceNodes(i, nodes, faceNodes);
266 // Just verify indices are valid
267 CHECK(faceNodes[0] < 4);
268 CHECK(faceNodes[1] < 4);
269 }
270 }
271}
272
273TEST_CASE("ElementTraits: order elevation data is consistent for O1 elements")
274{
275 // O1 elements should elevate to O2
276 for (auto t : O1Types)
277 {
278 Element e{t};
279 CAPTURE(t);
280
281 Element elevated = e.ObtainElevatedElem();
282
283 // Elevated element should exist
284 CHECK(elevated.type != UnknownElem);
285
286 // Elevated element should have more nodes
287 CHECK(elevated.GetNumNodes() > e.GetNumNodes());
288
289 // Elevated element should have same dimension
290 CHECK(elevated.GetDim() == e.GetDim());
291
292 // Should have elevation nodes defined
293 CHECK(e.GetNumElev_O1O2() > 0);
294 }
295}
296
297TEST_CASE("ElementTraits: order elevation data for specific elements")
298{
299 // Line2 -> Line3
300 {
301 Element e{Line2};
302 CHECK(e.ObtainElevatedElem().type == Line3);
303 CHECK(e.GetNumElev_O1O2() == 1); // One edge midpoint
304
305 // Check elevation span type
306 Element spanElem = e.ObtainElevNodeSpan(0);
307 CHECK(spanElem.type == Line2); // Edge span
308 }
309
310 // Tri3 -> Tri6
311 {
312 Element e{Tri3};
313 CHECK(e.ObtainElevatedElem().type == Tri6);
314 CHECK(e.GetNumElev_O1O2() == 3); // Three edge midpoints
315 }
316
317 // Quad4 -> Quad9
318 {
319 Element e{Quad4};
320 CHECK(e.ObtainElevatedElem().type == Quad9);
321 CHECK(e.GetNumElev_O1O2() == 5); // 4 edges + 1 face center
322 }
323
324 // Hex8 -> Hex27
325 {
326 Element e{Hex8};
327 CHECK(e.ObtainElevatedElem().type == Hex27);
328 CHECK(e.GetNumElev_O1O2() == 19); // 12 edges + 6 faces + 1 center
329 }
330}
331
332TEST_CASE("ElementTraits: O2 elements do not have further elevation")
333{
334 // O2 elements should not have elevation defined
335 for (auto t : O2Types)
336 {
337 Element e{t};
338 CAPTURE(t);
339
340 Element elevated = e.ObtainElevatedElem();
341 CHECK(elevated.type == UnknownElem);
342 CHECK(e.GetNumElev_O1O2() == 0);
343 }
344}
345
346TEST_CASE("ElementTraits: ExtractElevNodeSpanNodes works correctly")
347{
348 // Test Tri3 elevation spans
349 {
350 Element e{Tri3};
351 std::vector<DNDS::index> nodes = {0, 1, 2}; // Parent nodes
352 std::array<DNDS::index, 2> spanNodes;
353
354 // Each elevation span should connect 2 parent nodes
355 for (int i = 0; i < e.GetNumElev_O1O2(); i++)
356 {
357 e.ExtractElevNodeSpanNodes(i, nodes, spanNodes);
358 CHECK(spanNodes[0] < 3); // References valid parent node
359 CHECK(spanNodes[1] < 3);
360 }
361 }
362}
363
364TEST_CASE("ElementTraits: bisection data is valid for O2 elements")
365{
366 // Line3 bisection
367 {
368 Element e{Line3};
369 CHECK(e.GetO2NumBisect() == 2);
370
371 // Each sub-element should be Line2
372 for (int i = 0; i < e.GetO2NumBisect(); i++)
373 {
374 CHECK(e.ObtainO2BisectElem(i).type == Line2);
375 }
376 }
377
378 // Tri6 bisection
379 {
380 Element e{Tri6};
381 CHECK(e.GetO2NumBisect() == 4); // 4 sub-triangles
382 for (int i = 0; i < e.GetO2NumBisect(); i++)
383 {
384 CHECK(e.ObtainO2BisectElem(i).type == Tri3);
385 }
386 }
387
388 // Tet10 bisection
389 {
390 Element e{Tet10};
391 CHECK(e.GetO2NumBisect() == 8); // 8 sub-tets
392 for (int i = 0; i < e.GetO2NumBisect(); i++)
393 {
394 CHECK(e.ObtainO2BisectElem(i).type == Tet4);
395 }
396 }
397
398 // Hex27 bisection
399 {
400 Element e{Hex27};
401 CHECK(e.GetO2NumBisect() == 8); // 8 sub-hexes
402 for (int i = 0; i < e.GetO2NumBisect(); i++)
403 {
404 CHECK(e.ObtainO2BisectElem(i).type == Hex8);
405 }
406 }
407
408 // Prism18 bisection
409 {
410 Element e{Prism18};
411 CHECK(e.GetO2NumBisect() == 8); // 8 sub-prisms
412 for (int i = 0; i < e.GetO2NumBisect(); i++)
413 {
414 CHECK(e.ObtainO2BisectElem(i).type == Prism6);
415 }
416 }
417}
418
419TEST_CASE("ElementTraits: VTK conversion works correctly")
420{
421 // Test VTK conversion for each element type
422 for (auto t : AllTypes)
423 {
424 Element e{t};
425 CAPTURE(t);
426
427 // Create dummy node data
428 std::vector<t_real> nodes(e.GetNumNodes());
429 for (int i = 0; i < e.GetNumNodes(); i++)
430 nodes[i] = static_cast<t_real>(i);
431
432 // Convert to VTK
433 auto [vtkCellType, vtkNodes] = ToVTKVertsAndData(e, nodes);
434
435 // VTK cell type should be valid
436 CHECK(vtkCellType > 0);
437
438 // VTK nodes should have correct size
439 CHECK(vtkNodes.size() <= e.GetNumNodes());
440 CHECK(vtkNodes.size() > 0);
441 }
442}
443
444TEST_CASE("ElementTraits: VTK node order is a valid permutation for simple elements")
445{
446 // Test VTK node ordering produces valid permutations
447 DispatchElementType(Line2, [](auto traits) {
448 std::set<int> seen;
449 for (size_t i = 0; i < 2; i++)
450 seen.insert(traits.vtkNodeOrder[i]);
451 CHECK(seen.size() == 2); // All unique
452 CHECK(*seen.begin() == 0);
453 CHECK(*seen.rbegin() == 1);
454 });
455
456 DispatchElementType(Line3, [](auto traits) {
457 std::set<int> seen;
458 for (size_t i = 0; i < 3; i++)
459 seen.insert(traits.vtkNodeOrder[i]);
460 CHECK(seen.size() == 3); // All unique
461 // VTK uses different ordering: 0, 2, 1 (midpoint last)
462 CHECK(traits.vtkNodeOrder[0] == 0);
463 CHECK(traits.vtkNodeOrder[1] == 2);
464 CHECK(traits.vtkNodeOrder[2] == 1);
465 });
466
467 DispatchElementType(Tri6, [](auto traits) {
468 std::set<int> seen;
469 for (size_t i = 0; i < 6; i++)
470 seen.insert(traits.vtkNodeOrder[i]);
471 CHECK(seen.size() == 6); // All unique
472 });
473
474 DispatchElementType(Hex8, [](auto traits) {
475 std::set<int> seen;
476 for (size_t i = 0; i < 8; i++)
477 seen.insert(traits.vtkNodeOrder[i]);
478 CHECK(seen.size() == 8); // All unique
479 CHECK(traits.vtkCellType == 12); // VTK_HEXAHEDRON
480 });
481}
482
483// ===================================================================
484// Part B: Shape Function Tests
485// ===================================================================
486
487TEST_CASE("Shape functions: partition of unity")
488{
489 constexpr int nTrials = 10;
490 std::mt19937 rng(42);
491
492 for (auto t : AllTypes)
493 {
494 Element e{t};
495 CAPTURE(t);
496 for (int trial = 0; trial < nTrials; trial++)
497 {
498 auto p = RandomInteriorPoint(t, rng);
499 tNj Nj;
500 e.GetNj(p, Nj);
501
502 t_real sum = Nj.sum();
503 CHECK(sum == doctest::Approx(1.0).epsilon(1e-12));
504 }
505 }
506}
507
508TEST_CASE("Shape functions: nodal interpolation (Kronecker delta)")
509{
510 for (auto t : AllTypes)
511 {
512 Element e{t};
513 auto coords = NodeCoords(t);
514 DNDS::index nn = e.GetNumNodes();
515 CAPTURE(t);
516
517 for (DNDS::index j = 0; j < nn; j++)
518 {
519 tPoint p = coords.col(j);
520 tNj Nj;
521 e.GetNj(p, Nj);
522
523 for (DNDS::index i = 0; i < nn; i++)
524 {
525 t_real expected = (i == j) ? 1.0 : 0.0;
526 CAPTURE(i);
527 CAPTURE(j);
528 CHECK(Nj(0, i) == doctest::Approx(expected).epsilon(1e-10));
529 }
530 }
531 }
532}
533
534TEST_CASE("Shape functions: D1 derivatives vs finite difference")
535{
536 constexpr int nTrials = 5;
537 constexpr t_real h = 1e-7;
538 constexpr t_real tol = 1e-5;
539 std::mt19937 rng(123);
540
541 for (auto t : AllTypes)
542 {
543 Element e{t};
544 DNDS::index nn = e.GetNumNodes();
545 int dim = e.GetDim();
546 CAPTURE(t);
547
548 for (int trial = 0; trial < nTrials; trial++)
549 {
550 auto p = RandomInteriorPoint(t, rng);
551
552 tD1Nj D1Nj;
553 e.GetD1Nj(p, D1Nj);
554
555 // Finite difference for each parametric direction
556 for (int d = 0; d < dim; d++)
557 {
558 tPoint pp = p, pm = p;
559 pp[d] += h;
560 pm[d] -= h;
561
562 tNj Np, Nm;
563 e.GetNj(pp, Np);
564 e.GetNj(pm, Nm);
565
566 for (DNDS::index j = 0; j < nn; j++)
567 {
568 t_real fd = (Np(0, j) - Nm(0, j)) / (2 * h);
569 t_real an = D1Nj(d, j);
570 CAPTURE(d);
571 CAPTURE(j);
572 CHECK(an == doctest::Approx(fd).epsilon(tol));
573 }
574 }
575 }
576 }
577}
578
579TEST_CASE("Shape functions: D2 derivatives vs finite difference of D1")
580{
581 constexpr t_real h = 1e-6;
582 constexpr t_real tol = 1e-3;
583 std::mt19937 rng(456);
584
585 for (auto t : AllTypes)
586 {
587 Element e{t};
588 DNDS::index nn = e.GetNumNodes();
589 int dim = e.GetDim();
590 CAPTURE(t);
591
592 auto p = RandomInteriorPoint(t, rng);
593
594 tDiNj DiNj;
595 e.GetDiNj(p, DiNj, 2);
596
597 // Test pure second derivatives (d^2/dxi_d^2)
598 for (int d = 0; d < dim; d++)
599 {
600 tPoint pp = p, pm = p;
601 pp[d] += h;
602 pm[d] -= h;
603
604 tD1Nj D1p, D1m;
605 e.GetD1Nj(pp, D1p);
606 e.GetD1Nj(pm, D1m);
607
608 for (DNDS::index j = 0; j < nn; j++)
609 {
610 t_real fd_d2 = (D1p(d, j) - D1m(d, j)) / (2 * h);
611
612 // Row index for d^2/dxi_d^2 in the DiNj layout
613 DNDS::index row;
614 if (dim == 1)
615 row = 2; // d^2/dxi^2
616 else if (dim == 2)
617 row = 3 + (d == 0 ? 0 : 2); // row 3=d2/dxi2, row 5=d2/det2
618 else
619 row = 4 + d; // rows 4,5,6 for d2/dxi2, d2/det2, d2/dzt2
620
621 t_real an = DiNj(row, j);
622 CAPTURE(d);
623 CAPTURE(j);
624 CHECK(an == doctest::Approx(fd_d2).epsilon(tol));
625 }
626 }
627 }
628}
629
630TEST_CASE("Shape functions: all derivatives are finite")
631{
632 // Verify shape function derivatives don't produce NaN or Inf
633 std::mt19937 rng(789);
634
635 for (auto t : AllTypes)
636 {
637 Element e{t};
638 auto p = RandomInteriorPoint(t, rng);
639 CAPTURE(t);
640
641 tDiNj DiNj;
642 e.GetDiNj(p, DiNj, 3);
643
644 // Check all entries are finite
645 for (DNDS::index i = 0; i < DiNj.rows(); i++)
646 {
647 for (DNDS::index j = 0; j < DiNj.cols(); j++)
648 {
649 CHECK(std::isfinite(DiNj(i, j)));
650 }
651 }
652 }
653}
Eigen::Matrix< t_real, Eigen::Dynamic, Eigen::Dynamic > tDiNj
Definition Elements.hpp:184
Eigen::Matrix< t_real, 3, Eigen::Dynamic > tD1Nj
Definition Elements.hpp:182
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.
std::pair< int, std::vector< index > > ToVTKVertsAndData(Element e, const TIn &vin)
Definition Elements.hpp:566
Eigen::Matrix< t_real, 3, Eigen::Dynamic > GetStandardCoord(ElemType t)
Definition Elements.cpp:6
Eigen::RowVector< t_real, Eigen::Dynamic > tNj
Definition Elements.hpp:181
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:107
DNDS_DEVICE_CALLABLE constexpr ParamSpace GetParamSpace() const
Definition Elements.hpp:192
constexpr Element ObtainElevNodeSpan(t_index iNodeElev) const
Definition Elements.hpp:261
DNDS_DEVICE_CALLABLE constexpr Element ObtainFace(t_index iFace) const
Definition Elements.hpp:237
constexpr Element ObtainElevatedElem() const
Definition Elements.hpp:267
void ExtractFaceNodes(t_index iFace, const TIn &nodes, TOut &faceNodesOut)
Definition Elements.hpp:247
DNDS_DEVICE_CALLABLE constexpr t_index GetDim() const
Definition Elements.hpp:197
void ExtractElevNodeSpanNodes(t_index iNodeElev, const TIn &nodes, TOut &spanNodes)
Definition Elements.hpp:284
DNDS_DEVICE_CALLABLE constexpr t_index GetNumNodes() const
Definition Elements.hpp:215
tVec z(NCells)
tVec x(NCells)
CHECK(result.size()==3)
real expected
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")