DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_MeshPipeline.cpp
Go to the documentation of this file.
1/**
2 * @file test_MeshPipeline.cpp
3 * @brief Parameterized doctest tests for the full mesh pipeline.
4 *
5 * Each mesh configuration (file, dimension, periodic settings, expected counts)
6 * is tested through the complete pipeline:
7 * - Pipeline state tracking: every state variable checked
8 * - InterpolateFace: face topology, face2cell, bnd2face
9 * - ReorderLocalCells: cell permutation, partition starts
10 * - N2CB, C2F, Facial, C2CFace adjacency validation
11 * - ConstructBndMesh, BuildVTKConnectivity
12 * - Global count conservation
13 * - All 5 Adj round-trip inverses (on one mesh config)
14 *
15 * Mesh configurations:
16 * [0] UniformSquare_10 -- 2D, 100 quad cells, non-periodic
17 * [1] IV10_10 -- 2D, 100 quad cells, periodic (isentropic vortex)
18 * [2] NACA0012_H2 -- 2D, 20816 unstructured quad cells, non-periodic (airfoil)
19 * [3] IV10U_10 -- 2D, 322 unstructured tri cells, periodic (isentropic vortex)
20 */
21
22#define DOCTEST_CONFIG_IMPLEMENT
23#include "doctest.h"
24
25#include "Geom/Mesh.hpp"
26#include <string>
27#include <vector>
28#include <array>
29
30using namespace DNDS;
31using namespace DNDS::Geom;
32
33// NOTE: DNDS::index, DNDS::real, DNDS::rowsize clash with POSIX symbols.
34
35// ---------------------------------------------------------------------------
36// Mesh configuration
37// ---------------------------------------------------------------------------
38struct MeshConfig
39{
40 const char *name; ///< Human-readable label
41 const char *file; ///< Filename relative to data/mesh/
42 int dim; ///< Spatial dimension
43 bool periodic; ///< Has periodic boundaries
44 tPoint translation1; ///< Periodic translation axis 1
45 tPoint translation2; ///< Periodic translation axis 2
46 tPoint translation3; ///< Periodic translation axis 3
47 DNDS::index expectedCells; ///< Expected global cell count (-1 = skip check)
48 DNDS::index expectedBnds; ///< Expected global bnd count (-1 = skip check)
49};
50
51static const MeshConfig g_configs[] = {
52 {"UniformSquare_10", "UniformSquare_10.cgns", 2, false,
53 {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, 100, 40},
54 {"IV10_10", "IV10_10.cgns", 2, true,
55 {10, 0, 0}, {0, 10, 0}, {0, 0, 10}, 100, -1},
56 {"NACA0012_H2", "NACA0012_H2.cgns", 2, false,
57 {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, 20816, 484},
58 {"IV10U_10", "IV10U_10.cgns", 2, true,
59 {10, 0, 0}, {0, 10, 0}, {0, 0, 10}, 322, -1},
60 {"Ball2", "Ball2.cgns", 3, false,
61 {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, 958994, 16402}, // 3D mixed-element mesh (Ball2 is np=8 only)
62};
63static constexpr int N_CONFIGS = sizeof(g_configs) / sizeof(g_configs[0]);
64
65// ---------------------------------------------------------------------------
66// Globals
67// ---------------------------------------------------------------------------
68static MPIInfo g_mpi;
69
70/// One full-pipeline mesh per config.
71static ssp<UnstructuredMesh> g_full[N_CONFIGS];
72
73// ---------------------------------------------------------------------------
74// Helpers
75// ---------------------------------------------------------------------------
76
77static std::string meshPath(const std::string &name)
78{
79 std::string f(__FILE__);
80 for (int i = 0; i < 4; i++)
81 {
82 auto pos = f.rfind('/');
83 if (pos == std::string::npos)
84 pos = f.rfind('\\');
85 if (pos != std::string::npos)
86 f = f.substr(0, pos);
87 }
88 return f + "/data/mesh/" + name;
89}
90
91static ssp<UnstructuredMesh> buildFullMesh(
92 int meshId, const MeshConfig &cfg, int nReorderParts = 1)
93{
94 if (g_mpi.rank == 0)
95 std::cout << "[buildMesh " << meshId << " " << cfg.name << "] START" << std::endl;
96
97 auto mesh = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
98 UnstructuredMeshSerialRW reader(mesh, 0);
99
100 if (cfg.periodic)
101 {
102 tPoint zero{0, 0, 0};
103 mesh->SetPeriodicGeometry(
104 cfg.translation1, zero, zero,
105 cfg.translation2, zero, zero,
106 cfg.translation3, zero, zero);
107 }
108
109 reader.ReadFromCGNSSerial(meshPath(cfg.file));
110 reader.Deduplicate1to1Periodic(1e-8);
111 reader.BuildCell2Cell();
112
114 pOpt.metisType = "KWAY";
115 pOpt.metisUfactor = 30;
116 pOpt.metisSeed = 42;
117 pOpt.metisNcuts = 1;
118 reader.MeshPartitionCell2Cell(pOpt);
119 reader.PartitionReorderToMeshCell2Cell();
120
121 mesh->RecoverNode2CellAndNode2Bnd();
122 mesh->RecoverCell2CellAndBnd2Cell();
123 mesh->BuildGhostPrimary();
124
125 mesh->AdjGlobal2LocalPrimary();
126 mesh->AdjGlobal2LocalN2CB();
127
128 if (nReorderParts > 1)
129 mesh->ReorderLocalCells(nReorderParts);
130
131 mesh->InterpolateFace();
132 mesh->AssertOnFaces();
133
134 mesh->AdjLocal2GlobalN2CB();
135 mesh->BuildGhostN2CB();
136 mesh->AdjGlobal2LocalN2CB();
137 mesh->AssertOnN2CB();
138
139 mesh->BuildCell2CellFace();
140 mesh->AdjGlobal2LocalC2CFace();
141
142 if (cfg.periodic)
143 mesh->RecreatePeriodicNodes();
144 mesh->BuildVTKConnectivity();
145
146 if (g_mpi.rank == 0)
147 std::cout << "[buildMesh " << meshId << " " << cfg.name << "] DONE" << std::endl;
148
149 return mesh;
150}
151
152static std::vector<std::vector<DNDS::index>> snapshotAdj(
153 const tAdjPair &adj, DNDS::index nRows)
154{
155 std::vector<std::vector<DNDS::index>> out(nRows);
156 for (DNDS::index i = 0; i < nRows; i++)
157 {
158 out[i].resize(adj.RowSize(i));
159 for (DNDS::rowsize j = 0; j < adj.RowSize(i); j++)
160 out[i][j] = adj(i, j);
161 }
162 return out;
163}
164
165static std::vector<std::pair<DNDS::index, DNDS::index>> snapshotAdj2(
166 const tAdj2Pair &adj, DNDS::index nRows)
167{
168 std::vector<std::pair<DNDS::index, DNDS::index>> out(nRows);
169 for (DNDS::index i = 0; i < nRows; i++)
170 out[i] = {adj(i, 0), adj(i, 1)};
171 return out;
172}
173
174static std::vector<DNDS::index> snapshotAdj1(
175 const tAdj1Pair &adj, DNDS::index nRows)
176{
177 std::vector<DNDS::index> out(nRows);
178 for (DNDS::index i = 0; i < nRows; i++)
179 out[i] = adj(i, 0);
180 return out;
181}
182
183static void checkAdjMatchesSnapshot(
184 const tAdjPair &adj, DNDS::index nRows,
185 const std::vector<std::vector<DNDS::index>> &snap)
186{
187 for (DNDS::index i = 0; i < nRows; i++)
188 {
189 REQUIRE(adj.RowSize(i) == static_cast<DNDS::rowsize>(snap[i].size()));
190 for (DNDS::rowsize j = 0; j < adj.RowSize(i); j++)
191 CHECK(adj(i, j) == snap[i][j]);
192 }
193}
194
195// ---------------------------------------------------------------------------
196int main(int argc, char **argv)
197{
198 MPI_Init(&argc, &argv);
199 g_mpi.setWorld();
200
201 // Build one full-pipeline mesh per config.
202 for (int ic = 0; ic < N_CONFIGS; ic++)
203 g_full[ic] = buildFullMesh(ic, g_configs[ic]);
204
205 doctest::Context ctx;
206 ctx.applyCommandLine(argc, argv);
207 int res = ctx.run();
208
209 for (auto &m : g_full)
210 m.reset();
211 MPI_Finalize();
212 return res;
213}
214
215// ===========================================================================
216// Parameterized tests over all mesh configs (read-only)
217// ===========================================================================
218
219#define FOR_EACH_MESH_CONFIG(body) \
220 for (int _ci = 0; _ci < N_CONFIGS; _ci++) \
221 { \
222 /* Ball2 (index 4) is only tested with np=8 */ \
223 if (_ci == 4 && g_mpi.size != 8) continue; \
224 CAPTURE(_ci); \
225 const auto &cfg = g_configs[_ci]; \
226 auto m = g_full[_ci]; \
227 SUBCASE(cfg.name) { body } \
228 }
229
230// --- Pipeline state ---
231
232TEST_CASE("Pipeline: all five states are Local")
233{
235 CHECK(m->adjPrimaryState == Adj_PointToLocal);
236 CHECK(m->adjFacialState == Adj_PointToLocal);
237 CHECK(m->adjC2FState == Adj_PointToLocal);
238 CHECK(m->adjN2CBState == Adj_PointToLocal);
239 CHECK(m->adjC2CFaceState == Adj_PointToLocal);
240 })
241}
242
243// --- Global count conservation ---
244
245TEST_CASE("Pipeline: global cell count matches expected")
246{
248 DNDS::index localCells = m->NumCell();
249 DNDS::index globalCells = 0;
250 MPI_Allreduce(&localCells, &globalCells, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
251 CHECK(globalCells > 0);
252 if (cfg.expectedCells > 0)
253 CHECK(globalCells == cfg.expectedCells);
254 })
255}
256
257TEST_CASE("Pipeline: global bnd count matches expected")
258{
260 DNDS::index localBnds = m->NumBnd();
261 DNDS::index globalBnds = 0;
262 MPI_Allreduce(&localBnds, &globalBnds, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
263 if (cfg.expectedBnds > 0)
264 CHECK(globalBnds == cfg.expectedBnds);
265 else if (!cfg.periodic)
266 CHECK(globalBnds > 0); // non-periodic meshes must have boundaries
267 })
268}
269
270// --- InterpolateFace topology ---
271
272TEST_CASE("InterpolateFace: face count is positive")
273{
275 CHECK(m->NumFace() > 0);
276 })
277}
278
279TEST_CASE("InterpolateFace: face2cell has exactly 2 entries per face")
280{
282 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
283 CHECK(m->face2cell.RowSize(iF) == 2);
284 })
285}
286
287TEST_CASE("InterpolateFace: face2cell owner is a valid local cell")
288{
290 DNDS::index totalCells = m->NumCell() + m->NumCellGhost();
291 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
292 {
293 DNDS::index owner = m->face2cell(iF, 0);
294 CHECK(owner >= 0);
295 CHECK(owner < totalCells);
296 }
297 })
298}
299
300TEST_CASE("InterpolateFace: face2node indices in valid range")
301{
303 DNDS::index totalNodes = m->NumNode() + m->NumNodeGhost();
304 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
305 for (DNDS::rowsize j = 0; j < m->face2node.RowSize(iF); j++)
306 {
307 DNDS::index iN = m->face2node(iF, j);
308 CHECK(iN >= 0);
309 CHECK(iN < totalNodes);
310 }
311 })
312}
313
314TEST_CASE("InterpolateFace: cell2face row sizes match expected face count")
315{
317 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
318 {
319 auto elem = m->GetCellElement(iC);
320 int nFaceExpected = elem.GetNumFaces();
321 CHECK(m->cell2face.RowSize(iC) == nFaceExpected);
322 }
323 })
324}
325
326TEST_CASE("InterpolateFace: cell2face entries are valid face indices")
327{
329 DNDS::index totalFaces = m->NumFace() + m->NumFaceGhost();
330 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
331 for (DNDS::rowsize j = 0; j < m->cell2face.RowSize(iC); j++)
332 {
333 DNDS::index iF = m->cell2face(iC, j);
334 CHECK(iF >= 0);
335 CHECK(iF < totalFaces);
336 }
337 })
338}
339
340TEST_CASE("InterpolateFace: bnd2face maps to valid faces")
341{
343 for (DNDS::index ib = 0; ib < m->NumBnd(); ib++)
344 {
345 DNDS::index iFace = m->bnd2face(ib, 0);
346 // Periodic boundaries that became internal faces may have bnd2face == -1
347 if (iFace >= 0)
348 CHECK(iFace < m->NumFace() + m->NumFaceGhost());
349 }
350 })
351}
352
353TEST_CASE("InterpolateFace: face element types are valid")
354{
356 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
357 CHECK(m->GetFaceElement(iF).type != Elem::ElemType::UnknownElem);
358 })
359}
360
361// --- N2CB ---
362
363TEST_CASE("N2CB: every local node has at least one adjacent cell")
364{
366 for (DNDS::index iNode = 0; iNode < m->NumNode(); iNode++)
367 {
368 bool hasCell = false;
369 for (DNDS::rowsize j = 0; j < m->node2cell.RowSize(iNode); j++)
370 if (m->node2cell(iNode, j) >= 0)
371 hasCell = true;
372 CHECK(hasCell);
373 }
374 })
375}
376
377TEST_CASE("N2CB: node2cell entries are valid local indices")
378{
380 DNDS::index totalCells = m->NumCell() + m->NumCellGhost();
381 for (DNDS::index iNode = 0; iNode < m->NumNodeProc(); iNode++)
382 for (DNDS::rowsize j = 0; j < m->node2cell.RowSize(iNode); j++)
383 {
384 DNDS::index iC = m->node2cell(iNode, j);
385 if (iC >= 0)
386 CHECK(iC < totalCells);
387 }
388 })
389}
390
391// --- BuildCell2CellFace ---
392
393TEST_CASE("BuildCell2CellFace: row sizes match cell2face")
394{
396 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
397 CHECK(m->cell2cellFace.RowSize(iC) == m->cell2face.RowSize(iC));
398 })
399}
400
401TEST_CASE("BuildCell2CellFace: entries are valid cells or negative")
402{
404 DNDS::index totalCells = m->NumCell() + m->NumCellGhost();
405 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
406 for (DNDS::rowsize j = 0; j < m->cell2cellFace.RowSize(iC); j++)
407 {
408 DNDS::index nb = m->cell2cellFace(iC, j);
409 if (nb >= 0)
410 CHECK(nb < totalCells);
411 }
412 })
413}
414
415// --- ConstructBndMesh ---
416
417TEST_CASE("ConstructBndMesh: correct dimensions and state")
418{
420 if (cfg.periodic)
421 continue; // periodic meshes may have no real boundaries after deduplication
422 UnstructuredMesh bMesh(g_mpi, m->dim - 1);
423 m->ConstructBndMesh(bMesh);
424 CHECK(bMesh.dim == m->dim - 1);
425 CHECK(bMesh.NumCell() == m->NumBnd());
427 })
428}
429
430TEST_CASE("ConstructBndMesh: node2parentNode valid")
431{
433 if (cfg.periodic)
434 continue;
435 UnstructuredMesh bMesh(g_mpi, m->dim - 1);
436 m->ConstructBndMesh(bMesh);
437 CHECK(bMesh.node2parentNode.size() ==
438 static_cast<size_t>(bMesh.NumNode() + bMesh.NumNodeGhost()));
439 for (DNDS::index i = 0; i < bMesh.NumNode(); i++)
440 {
441 DNDS::index pn = bMesh.node2parentNode[i];
442 CHECK(pn >= 0);
443 CHECK(pn < m->NumNode() + m->NumNodeGhost());
444 }
445 })
446}
447
448// --- BuildVTKConnectivity ---
449
450TEST_CASE("BuildVTKConnectivity: arrays populated correctly")
451{
453 CHECK(m->vtkCell2node.size() > 0);
454 CHECK(m->vtkCell2nodeOffsets.size() == static_cast<size_t>(m->NumCell() + 1));
455 CHECK(m->vtkCellType.size() == static_cast<size_t>(m->NumCell()));
456 for (size_t i = 1; i < m->vtkCell2nodeOffsets.size(); i++)
457 CHECK(m->vtkCell2nodeOffsets[i] >= m->vtkCell2nodeOffsets[i - 1]);
458 for (auto ct : m->vtkCellType)
459 CHECK(ct != 0);
460 })
461}
462
463// ===========================================================================
464// ReorderLocalCells (config 0 only)
465// ===========================================================================
466
467TEST_CASE("ReorderLocalCells: all states preserved")
468{
470 m->ReorderLocalCells(2);
471 CHECK(m->adjPrimaryState == Adj_PointToLocal);
472 CHECK(m->adjFacialState == Adj_PointToLocal);
473 CHECK(m->adjC2FState == Adj_PointToLocal);
474 CHECK(m->adjN2CBState == Adj_PointToLocal);
475 CHECK(m->adjC2CFaceState == Adj_PointToLocal);
476 })
477}
478
479TEST_CASE("ReorderLocalCells: cell count preserved")
480{
482 DNDS::index g1 = 0;
483 DNDS::index g2 = 0;
484 DNDS::index loc1 = m->NumCell();
485 MPI_Allreduce(&loc1, &g1, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
486 m->ReorderLocalCells(2);
487 DNDS::index loc2 = m->NumCell();
488 MPI_Allreduce(&loc2, &g2, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
489 CHECK(g1 == g2);
490 )
491}
492
493TEST_CASE("ReorderLocalCells: partition starts valid")
494{
496 m->ReorderLocalCells(2);
497 int nParts = m->NLocalParts();
498 CHECK(nParts == 2);
499 CHECK(m->LocalPartStart(0) == 0);
500 CHECK(m->LocalPartEnd(nParts - 1) == m->NumCell());
501 for (int ip = 0; ip < nParts; ip++)
502 CHECK(m->LocalPartStart(ip) <= m->LocalPartEnd(ip));
503 })
504}
505
506TEST_CASE("ReorderLocalCells: cell2node still valid")
507{
509 m->ReorderLocalCells(2);
510 DNDS::index totalNodes = m->NumNode() + m->NumNodeGhost();
511 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
512 for (DNDS::rowsize j = 0; j < m->cell2node.RowSize(iC); j++)
513 {
514 DNDS::index iN = m->cell2node(iC, j);
515 CHECK(iN >= 0);
516 CHECK(iN < totalNodes);
517 }
518 })
519}
520
521TEST_CASE("ReorderLocalCells: face count preserved")
522{
524 DNDS::index g1 = 0;
525 DNDS::index g2 = 0;
526 DNDS::index loc1 = m->NumFace();
527 MPI_Allreduce(&loc1, &g1, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
528 m->ReorderLocalCells(2);
529 DNDS::index loc2 = m->NumFace();
530 MPI_Allreduce(&loc2, &g2, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
531 CHECK(g1 == g2);
532 )
533}
534
535// ===========================================================================
536// Adj round-trip tests (all mesh configs)
537// ===========================================================================
538
539TEST_CASE("AdjFacial round-trip: face2node and face2cell identity")
540{
542 REQUIRE(m->adjFacialState == Adj_PointToLocal);
543
544 auto f2nSnap = snapshotAdj(m->face2node, m->NumFace());
545 auto f2cSnap = snapshotAdj2(m->face2cell, m->NumFace());
546
547 m->AdjLocal2GlobalFacial();
548 CHECK(m->adjFacialState == Adj_PointToGlobal);
549
550 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
551 for (DNDS::rowsize j = 0; j < m->face2node.RowSize(iF); j++)
552 CHECK(m->face2node(iF, j) >= 0);
553
554 m->AdjGlobal2LocalFacial();
555 CHECK(m->adjFacialState == Adj_PointToLocal);
556
557 checkAdjMatchesSnapshot(m->face2node, m->NumFace(), f2nSnap);
558 for (DNDS::index iF = 0; iF < m->NumFace(); iF++)
559 {
560 CHECK(m->face2cell(iF, 0) == f2cSnap[iF].first);
561 CHECK(m->face2cell(iF, 1) == f2cSnap[iF].second);
562 }
563 // NOTE: face2bnd intentionally not round-tripped (asymmetric by design)
564 )
565}
566
567TEST_CASE("AdjC2F round-trip: cell2face and bnd2face identity")
568{
570 REQUIRE(m->adjC2FState == Adj_PointToLocal);
571
572 auto c2fSnap = snapshotAdj(m->cell2face, m->NumCell());
573 auto b2fSnap = snapshotAdj1(m->bnd2face, m->NumBnd());
574
575 m->AdjLocal2GlobalC2F();
576 CHECK(m->adjC2FState == Adj_PointToGlobal);
577
578 m->AdjGlobal2LocalC2F();
579 CHECK(m->adjC2FState == Adj_PointToLocal);
580
581 checkAdjMatchesSnapshot(m->cell2face, m->NumCell(), c2fSnap);
582 for (DNDS::index ib = 0; ib < m->NumBnd(); ib++)
583 CHECK(m->bnd2face(ib, 0) == b2fSnap[ib]);
584 )
585}
586
587TEST_CASE("AdjN2CB round-trip: node2cell and node2bnd identity")
588{
590 REQUIRE(m->adjN2CBState == Adj_PointToLocal);
591
592 auto n2cSnap = snapshotAdj(m->node2cell, m->NumNodeProc());
593 auto n2bSnap = snapshotAdj(m->node2bnd, m->NumNodeProc());
594
595 m->AdjLocal2GlobalN2CB();
596 CHECK(m->adjN2CBState == Adj_PointToGlobal);
597
598 m->AdjGlobal2LocalN2CB();
599 CHECK(m->adjN2CBState == Adj_PointToLocal);
600
601 checkAdjMatchesSnapshot(m->node2cell, m->NumNodeProc(), n2cSnap);
602 checkAdjMatchesSnapshot(m->node2bnd, m->NumNodeProc(), n2bSnap);
603 )
604}
605
606TEST_CASE("AdjC2CFace round-trip: cell2cellFace identity")
607{
609 REQUIRE(m->adjC2CFaceState == Adj_PointToLocal);
610
611 auto c2cfSnap = snapshotAdj(m->cell2cellFace, m->NumCell());
612
613 m->AdjLocal2GlobalC2CFace();
614 CHECK(m->adjC2CFaceState == Adj_PointToGlobal);
615
616 m->AdjGlobal2LocalC2CFace();
617 CHECK(m->adjC2CFaceState == Adj_PointToLocal);
618
619 checkAdjMatchesSnapshot(m->cell2cellFace, m->NumCell(), c2cfSnap);
620 )
621}
622
623TEST_CASE("AdjPrimary round-trip: serial-out pattern")
624{
626 REQUIRE(m->adjPrimaryState == Adj_PointToLocal);
627
628 auto c2nSnap = snapshotAdj(m->cell2node, m->NumCell());
629 auto c2cSnap = snapshotAdj(m->cell2cell, m->NumCell());
630 auto b2nSnap = snapshotAdj(m->bnd2node, m->NumBnd());
631 auto b2cSnap = snapshotAdj2(m->bnd2cell, m->NumBnd());
632
633 m->AdjLocal2GlobalPrimary();
634 CHECK(m->adjPrimaryState == Adj_PointToGlobal);
635
636 DNDS::index globalNodeSize = m->coords.father->globalSize();
637 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
638 for (DNDS::rowsize j = 0; j < m->cell2node.RowSize(iC); j++)
639 {
640 DNDS::index gN = m->cell2node(iC, j);
641 CHECK(gN >= 0);
642 CHECK(gN < globalNodeSize);
643 }
644
645 m->AdjGlobal2LocalPrimary();
646 CHECK(m->adjPrimaryState == Adj_PointToLocal);
647
648 checkAdjMatchesSnapshot(m->cell2node, m->NumCell(), c2nSnap);
649 checkAdjMatchesSnapshot(m->cell2cell, m->NumCell(), c2cSnap);
650 checkAdjMatchesSnapshot(m->bnd2node, m->NumBnd(), b2nSnap);
651 for (DNDS::index ib = 0; ib < m->NumBnd(); ib++)
652 {
653 CHECK(m->bnd2cell(ib, 0) == b2cSnap[ib].first);
654 CHECK(m->bnd2cell(ib, 1) == b2cSnap[ib].second);
655 }
656 )
657}
658
659TEST_CASE("AdjForBnd round-trip: ConstructBndMesh + ForBnd")
660{
662 UnstructuredMesh bMesh(g_mpi, m->dim - 1);
663 m->ConstructBndMesh(bMesh);
664 REQUIRE(bMesh.adjPrimaryState == Adj_PointToLocal);
665
666 auto c2nSnap = snapshotAdj(bMesh.cell2node, bMesh.NumCell());
667
668 bMesh.AdjLocal2GlobalPrimaryForBnd();
669 CHECK(bMesh.adjPrimaryState == Adj_PointToGlobal);
670
671 DNDS::index globalNodeSize = bMesh.coords.father->globalSize();
672 for (DNDS::index iC = 0; iC < bMesh.NumCell(); iC++)
673 for (DNDS::rowsize j = 0; j < bMesh.cell2node.RowSize(iC); j++)
674 {
675 DNDS::index gN = bMesh.cell2node(iC, j);
676 CHECK(gN >= 0);
677 CHECK(gN < globalNodeSize);
678 }
679
680 bMesh.AdjGlobal2LocalPrimaryForBnd();
681 CHECK(bMesh.adjPrimaryState == Adj_PointToLocal);
682
683 checkAdjMatchesSnapshot(bMesh.cell2node, bMesh.NumCell(), c2nSnap);
684 )
685}
686
687// ===========================================================================
688// Ball2: 3D mixed-element mesh (np=8 only)
689// ===========================================================================
690
691TEST_CASE("Ball2: mixed 3D element types")
692{
693 // Ball2 (config 4) is only tested with np=8
694 if (g_mpi.size != 8)
695 return;
696
697 auto m = g_full[4]; // Ball2 is index 4
698 DNDS_assert(m != nullptr);
699
700 // Count element types locally
701 std::map<Elem::ElemType, int> typeCounts;
702 for (DNDS::index iC = 0; iC < m->NumCell(); iC++)
703 {
704 auto elem = m->GetCellElement(iC);
705 typeCounts[elem.type]++;
706 }
707
708 // Gather all unique element types from all ranks
709 std::vector<int> localTypes;
710 for (const auto& [type, count] : typeCounts)
711 localTypes.push_back(static_cast<int>(type));
712
713 int nLocalTypes = localTypes.size();
714 std::vector<int> allRankSizes(g_mpi.size);
715 MPI_Gather(&nLocalTypes, 1, MPI_INT, allRankSizes.data(), 1, MPI_INT, 0, g_mpi.comm);
716
717 std::vector<int> allTypes;
718 std::vector<int> displs;
719 int totalTypes = 0;
720
721 if (g_mpi.rank == 0)
722 {
723 displs.resize(g_mpi.size);
724 for (int i = 0; i < g_mpi.size; i++)
725 {
726 displs[i] = totalTypes;
727 totalTypes += allRankSizes[i];
728 }
729 allTypes.resize(totalTypes);
730 }
731
732 MPI_Gatherv(localTypes.data(), nLocalTypes, MPI_INT,
733 allTypes.data(), allRankSizes.data(), displs.data(), MPI_INT, 0, g_mpi.comm);
734
735 // Build set of unique types (on rank 0)
736 std::set<Elem::ElemType> uniqueTypes;
737 if (g_mpi.rank == 0)
738 {
739 for (int typeInt : allTypes)
740 uniqueTypes.insert(static_cast<Elem::ElemType>(typeInt));
741 }
742
743 // Broadcast unique types to all ranks
744 int nUnique = uniqueTypes.size();
745 MPI_Bcast(&nUnique, 1, MPI_INT, 0, g_mpi.comm);
746 std::vector<int> uniqueTypesVec;
747 if (g_mpi.rank == 0)
748 {
749 for (auto type : uniqueTypes)
750 uniqueTypesVec.push_back(static_cast<int>(type));
751 }
752 uniqueTypesVec.resize(nUnique);
753 MPI_Bcast(uniqueTypesVec.data(), nUnique, MPI_INT, 0, g_mpi.comm);
754
755 // All ranks iterate over the same types in the same order
756 std::map<Elem::ElemType, int> globalCounts;
757 for (int typeInt : uniqueTypesVec)
758 {
759 Elem::ElemType type = static_cast<Elem::ElemType>(typeInt);
760 int localCount = typeCounts.count(type) ? typeCounts[type] : 0;
761 int globalCount;
762 MPI_Reduce(&localCount, &globalCount, 1, MPI_INT, MPI_SUM, 0, g_mpi.comm);
763 if (g_mpi.rank == 0)
764 globalCounts[type] = globalCount;
765 }
766
767 if (g_mpi.rank == 0)
768 {
769 std::cout << "\nBall2 element type counts:" << std::endl;
770 for (const auto& [type, count] : globalCounts)
771 {
772 std::cout << " Type " << static_cast<int>(type) << ": " << count << std::endl;
773 }
774
775 // Verify expected total
776 int total = 0;
777 for (const auto& [type, count] : globalCounts)
778 total += count;
779 CHECK(total == 958994);
780 }
781}
782
783// ===========================================================================
784// Elevation/Bisection tests
785// ===========================================================================
786
787TEST_CASE("Elevation: O2 mesh cell count equals O1 cell count")
788{
789 // Only test with config 0 (UniformSquare_10) for now
790 const auto& cfg = g_configs[0];
791
792 // Build fresh O1 mesh
793 auto mO1 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
794 UnstructuredMeshSerialRW reader(mO1, 0);
795
796 if (cfg.periodic)
797 {
798 tPoint zero{0, 0, 0};
799 mO1->SetPeriodicGeometry(cfg.translation1, zero, zero, cfg.translation2, zero, zero, cfg.translation3, zero, zero);
800 }
801
802 reader.ReadFromCGNSSerial(meshPath(cfg.file));
803 reader.Deduplicate1to1Periodic(1e-8);
804 reader.BuildCell2Cell();
805
807 pOpt.metisType = "KWAY";
808 pOpt.metisUfactor = 30;
809 pOpt.metisSeed = 42;
810 pOpt.metisNcuts = 1;
811 reader.MeshPartitionCell2Cell(pOpt);
813
814 mO1->RecoverNode2CellAndNode2Bnd();
815 mO1->RecoverCell2CellAndBnd2Cell();
816 mO1->BuildGhostPrimary();
817 mO1->AdjGlobal2LocalPrimary();
818
819 // Build O2 mesh via elevation
820 auto mO2 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
821 mO2->BuildO2FromO1Elevation(*mO1);
822
823 // Set up MPI state for mO2
824 mO2->RecoverNode2CellAndNode2Bnd();
825 mO2->RecoverCell2CellAndBnd2Cell();
826 mO2->BuildGhostPrimary();
827 mO2->AdjGlobal2LocalPrimary();
828
829 DNDS::index nCellO1 = mO1->NumCellGlobal();
830 DNDS::index nCellO2 = mO2->NumCellGlobal();
831
832 // Elevation preserves cell count
833 CHECK(nCellO2 == nCellO1);
834
835 // All cells should be O2 type
836 for (DNDS::index iC = 0; iC < mO2->NumCell(); iC++)
837 {
838 auto elem = mO2->GetCellElement(iC);
839 CHECK(elem.GetOrder() == 2);
840 }
841}
842
843TEST_CASE("Elevation: O2 mesh has more nodes than O1")
844{
845 const auto& cfg = g_configs[0];
846
847 // Build fresh O1 mesh
848 auto mO1 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
849 UnstructuredMeshSerialRW reader(mO1, 0);
850
851 if (cfg.periodic)
852 {
853 tPoint zero{0, 0, 0};
854 mO1->SetPeriodicGeometry(cfg.translation1, zero, zero, cfg.translation2, zero, zero, cfg.translation3, zero, zero);
855 }
856
857 reader.ReadFromCGNSSerial(meshPath(cfg.file));
858 reader.Deduplicate1to1Periodic(1e-8);
859 reader.BuildCell2Cell();
860
862 pOpt.metisType = "KWAY";
863 pOpt.metisUfactor = 30;
864 pOpt.metisSeed = 42;
865 pOpt.metisNcuts = 1;
866 reader.MeshPartitionCell2Cell(pOpt);
868
869 mO1->RecoverNode2CellAndNode2Bnd();
870 mO1->RecoverCell2CellAndBnd2Cell();
871 mO1->BuildGhostPrimary();
872 mO1->AdjGlobal2LocalPrimary();
873
874 // Build O2 mesh via elevation
875 auto mO2 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
876 mO2->BuildO2FromO1Elevation(*mO1);
877
878 // Set up MPI state for mO2
879 mO2->RecoverNode2CellAndNode2Bnd();
880 mO2->RecoverCell2CellAndBnd2Cell();
881 mO2->BuildGhostPrimary();
882 mO2->AdjGlobal2LocalPrimary();
883
884 DNDS::index nNodeO1 = mO1->coords.father->globalSize();
885 DNDS::index nNodeO2 = mO2->coords.father->globalSize();
886
887 // Elevation adds nodes
888 CHECK(nNodeO2 > nNodeO1);
889}
890
891TEST_CASE("Bisection: O1 mesh has more cells than O2")
892{
893 const auto& cfg = g_configs[0];
894
895 // Build fresh O1 mesh
896 auto mO1 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
897 UnstructuredMeshSerialRW reader(mO1, 0);
898
899 if (cfg.periodic)
900 {
901 tPoint zero{0, 0, 0};
902 mO1->SetPeriodicGeometry(cfg.translation1, zero, zero, cfg.translation2, zero, zero, cfg.translation3, zero, zero);
903 }
904
905 reader.ReadFromCGNSSerial(meshPath(cfg.file));
906 reader.Deduplicate1to1Periodic(1e-8);
907 reader.BuildCell2Cell();
908
910 pOpt.metisType = "KWAY";
911 pOpt.metisUfactor = 30;
912 pOpt.metisSeed = 42;
913 pOpt.metisNcuts = 1;
914 reader.MeshPartitionCell2Cell(pOpt);
916
917 mO1->RecoverNode2CellAndNode2Bnd();
918 mO1->RecoverCell2CellAndBnd2Cell();
919 mO1->BuildGhostPrimary();
920 mO1->AdjGlobal2LocalPrimary();
921
922 // Build O2 mesh
923 auto mO2 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
924 mO2->BuildO2FromO1Elevation(*mO1);
925 mO2->RecoverNode2CellAndNode2Bnd();
926 mO2->RecoverCell2CellAndBnd2Cell();
927 mO2->BuildGhostPrimary();
928 mO2->AdjGlobal2LocalPrimary();
929
930 // Build O1 mesh via bisection
931 mO2->AdjLocal2GlobalPrimary();
932 auto mO1B = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
933 mO1B->BuildBisectO1FormO2(*mO2);
934
935 DNDS::index nCellO2 = mO2->NumCellGlobal();
936 DNDS::index nCellO1B = mO1B->NumCellGlobal();
937
938 // Bisection increases cell count
939 CHECK(nCellO1B > nCellO2);
940
941 // All cells should be O1 (linear) element types
942 for (DNDS::index iC = 0; iC < mO1B->NumCell(); iC++)
943 {
944 auto elem = mO1B->GetCellElement(iC);
945 CHECK(elem.GetOrder() == 1);
946 }
947}
948
949TEST_CASE("Bisection: global node count is preserved from O2 mesh")
950{
951 const auto& cfg = g_configs[0];
952
953 // Build fresh O1 mesh
954 auto mO1 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
955 UnstructuredMeshSerialRW reader(mO1, 0);
956
957 if (cfg.periodic)
958 {
959 tPoint zero{0, 0, 0};
960 mO1->SetPeriodicGeometry(cfg.translation1, zero, zero, cfg.translation2, zero, zero, cfg.translation3, zero, zero);
961 }
962
963 reader.ReadFromCGNSSerial(meshPath(cfg.file));
964 reader.Deduplicate1to1Periodic(1e-8);
965 reader.BuildCell2Cell();
966
968 pOpt.metisType = "KWAY";
969 pOpt.metisUfactor = 30;
970 pOpt.metisSeed = 42;
971 pOpt.metisNcuts = 1;
972 reader.MeshPartitionCell2Cell(pOpt);
974
975 mO1->RecoverNode2CellAndNode2Bnd();
976 mO1->RecoverCell2CellAndBnd2Cell();
977 mO1->BuildGhostPrimary();
978 mO1->AdjGlobal2LocalPrimary();
979
980 // Build O2 mesh
981 auto mO2 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
982 mO2->BuildO2FromO1Elevation(*mO1);
983 mO2->RecoverNode2CellAndNode2Bnd();
984 mO2->RecoverCell2CellAndBnd2Cell();
985 mO2->BuildGhostPrimary();
986 mO2->AdjGlobal2LocalPrimary();
987
988 // Build O1 mesh via bisection
989 mO2->AdjLocal2GlobalPrimary();
990 auto mO1B = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
991 mO1B->BuildBisectO1FormO2(*mO2);
992
993 // No new nodes created during bisection
994 DNDS::index nNodeO2 = mO2->coords.father->globalSize();
995 DNDS::index nNodeO1B = mO1B->coords.father->globalSize();
996 CHECK(nNodeO1B == nNodeO2);
997}
998
999TEST_CASE("Elevation+Bisection: exact cell count progression")
1000{
1001 // UniformSquare_10: 100 Quad4 cells
1002 // After elevation: 100 Quad9 cells (same count, higher order)
1003 // After bisection: 400 Quad4 cells (4 sub-cells per Quad9)
1004 const auto& cfg = g_configs[0];
1005
1006 // Build fresh O1 mesh
1007 auto mO1 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
1008 UnstructuredMeshSerialRW reader(mO1, 0);
1009
1010 if (cfg.periodic)
1011 {
1012 tPoint zero{0, 0, 0};
1013 mO1->SetPeriodicGeometry(cfg.translation1, zero, zero, cfg.translation2, zero, zero, cfg.translation3, zero, zero);
1014 }
1015
1016 reader.ReadFromCGNSSerial(meshPath(cfg.file));
1017 reader.Deduplicate1to1Periodic(1e-8);
1018 reader.BuildCell2Cell();
1019
1021 pOpt.metisType = "KWAY";
1022 pOpt.metisUfactor = 30;
1023 pOpt.metisSeed = 42;
1024 pOpt.metisNcuts = 1;
1025 reader.MeshPartitionCell2Cell(pOpt);
1027
1028 mO1->RecoverNode2CellAndNode2Bnd();
1029 mO1->RecoverCell2CellAndBnd2Cell();
1030 mO1->BuildGhostPrimary();
1031 mO1->AdjGlobal2LocalPrimary();
1032
1033 // Build O2 mesh
1034 auto mO2 = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
1035 mO2->BuildO2FromO1Elevation(*mO1);
1036 mO2->RecoverNode2CellAndNode2Bnd();
1037 mO2->RecoverCell2CellAndBnd2Cell();
1038 mO2->BuildGhostPrimary();
1039 mO2->AdjGlobal2LocalPrimary();
1040
1041 // Build O1 mesh via bisection
1042 mO2->AdjLocal2GlobalPrimary();
1043 auto mO1B = std::make_shared<UnstructuredMesh>(g_mpi, cfg.dim);
1044 mO1B->BuildBisectO1FormO2(*mO2);
1045
1046 DNDS::index nCellOrig = mO1->NumCellGlobal();
1047 DNDS::index nCellElevated = mO2->NumCellGlobal();
1048 DNDS::index nCellBisected = mO1B->NumCellGlobal();
1049
1050 CHECK(nCellOrig == 100);
1051 CHECK(nCellElevated == 100);
1052 CHECK(nCellBisected == 400);
1053}
#define DNDS_assert(expr)
Debug-only assertion (compiled out when DNDS_NDEBUG is defined). Prints the expression + file/line + ...
Definition Errors.hpp:108
int main()
Definition dummy.cpp:3
Eigen::Vector3d tPoint
Definition Geometric.hpp:9
the host side operators are provided as implemented
const MPI_Datatype DNDS_MPI_INDEX
MPI datatype matching index (= MPI_INT64_T).
Definition MPI.hpp:90
int32_t rowsize
Row-width / per-row element-count type (signed 32-bit).
Definition Defines.hpp:109
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:107
std::shared_ptr< T > ssp
Shortened alias for std::shared_ptr used pervasively in DNDSR.
Definition Defines.hpp:138
Convenience bundle of a father, son, and attached ArrayTransformer.
void MeshPartitionCell2Cell(const PartitionOptions &options)
void ReadFromCGNSSerial(const std::string &fName, const t_FBCName_2_ID &FBCName_2_ID)
reads a cgns file as serial input
void BuildCell2Cell()
build cell2cell topology, with node-neighbors included
index NumNodeGhost() const
Definition Mesh.hpp:461
std::vector< index > node2parentNode
parent built
Definition Mesh.hpp:127
MeshAdjState adjPrimaryState
Definition Mesh.hpp:41
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:215
int size
Number of ranks in comm (-1 until initialised).
Definition MPI.hpp:221
int rank
This rank's 0-based index within comm (-1 until initialised).
Definition MPI.hpp:219
MPI_Comm comm
The underlying MPI communicator handle.
Definition MPI.hpp:217
void setWorld()
Initialise the object to MPI_COMM_WORLD. Requires MPI_Init to have run.
Definition MPI.hpp:242
DNDS::index expectedCells
Expected global cell count (-1 = skip check)
tPoint translation1
Periodic translation axis 1.
bool periodic
Has periodic boundaries.
int dim
Spatial dimension.
tPoint translation3
Periodic translation axis 3.
const char * file
Filename relative to data/mesh/.
tPoint translation2
Periodic translation axis 2.
const char * name
Human-readable label.
DNDS::index expectedBnds
Expected global bnd count (-1 = skip check)
auto adj
CHECK(result.size()==3)
#define FOR_EACH_MESH_CONFIG(body)
auto res
Definition test_ODE.cpp:196
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")