DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_MeshConnectivity.cpp
Go to the documentation of this file.
1/**
2 * @file test_MeshConnectivity.cpp
3 * @brief Unit tests for MeshConnectivity DSL operations: Inverse, Compose, ComposeFiltered.
4 * Also tests MeshConnectivity struct management (cone/support).
5 */
6
7#define DOCTEST_CONFIG_IMPLEMENT
8#include "doctest.h"
9
12#include "Geom/Mesh/Mesh.hpp"
13#include <string>
14#include <vector>
15#include <set>
16#include <algorithm>
17#include <unordered_set>
18#include <numeric>
19
20using namespace DNDS;
21using namespace DNDS::Geom;
22
23static MPIInfo g_mpi;
24
25// ===========================================================================
26// Inverse tests
27// ===========================================================================
28
29// ===========================================================================
30// Inverse Tests
31// ===========================================================================
32
33TEST_CASE("Inverse: 4-quad serial correctness")
34{
35 auto c2n = make4QuadCell2Node(g_mpi);
36 c2n.father->createGlobalMapping();
37 auto nodeGM = makeNodeGlobalMapping4Quad(g_mpi);
38
39 DNDS::index nNodeLocal = nodeLocalCount4Quad(g_mpi);
41 c2n, nNodeLocal, g_mpi,
42 [&](DNDS::index i)
43 { return cellLocal2Global4Quad(g_mpi, i); },
44 [&](DNDS::index i)
45 { return nodeLocal2Global4Quad(g_mpi, i); },
46 nodeGM);
47
48 // Gather global result for verification
49 auto globalN2C = gatherInverseGlobal(
51 [&](DNDS::index i)
52 { return nodeLocal2Global4Quad(g_mpi, i); },
53 9, g_mpi);
54
55 // Expected node2cell (from the 4-quad layout):
56 // node 0: {0}
57 // node 1: {0, 1}
58 // node 2: {1}
59 // node 3: {0, 2}
60 // node 4: {0, 1, 2, 3}
61 // node 5: {1, 3}
62 // node 6: {2}
63 // node 7: {2, 3}
64 // node 8: {3}
65 std::vector<std::set<DNDS::index>> expected = {
66 {0}, // node 0
67 {0, 1}, // node 1
68 {1}, // node 2
69 {0, 2}, // node 3
70 {0, 1, 2, 3}, // node 4
71 {1, 3}, // node 5
72 {2}, // node 6
73 {2, 3}, // node 7
74 {3}, // node 8
75 };
76
77 for (DNDS::index n = 0; n < 9; n++)
78 {
79 CAPTURE(n);
80 CHECK(globalN2C[n] == expected[n]);
81 }
82}
83
84TEST_CASE("Inverse: round-trip covers original cell2node")
85{
86 // inverse(inverse(cell2node)) should give cell2cell-like structure where
87 // each cell's row contains at least itself and all cells sharing any node.
88 // Specifically: for each cell c, for each node n in c2n[c], all cells in
89 // n2c[n] should appear in the re-inverted result for c.
90
91 auto c2n = make4QuadCell2Node(g_mpi);
92 c2n.father->createGlobalMapping();
93 auto nodeGM = makeNodeGlobalMapping4Quad(g_mpi);
94
95 DNDS::index nNodeLocal = nodeLocalCount4Quad(g_mpi);
96 DNDS::index nCellLocal = c2n.father->Size();
97
99 c2n, nNodeLocal, g_mpi,
100 [&](DNDS::index i)
101 { return cellLocal2Global4Quad(g_mpi, i); },
102 [&](DNDS::index i)
103 { return nodeLocal2Global4Quad(g_mpi, i); },
104 nodeGM);
105
106 // Now create a cell global mapping for the second inverse
107 auto cellGM = std::make_shared<GlobalOffsetsMapping>();
108 cellGM->setMPIAlignBcast(g_mpi, nCellLocal);
109
110 // inverse(n2c) -> cell2node_roundtrip
111 // n2c maps node->cells, so inverse gives cell->nodes
112 auto c2n_rt = MeshConnectivity::Inverse(
113 n2c, nCellLocal, g_mpi,
114 [&](DNDS::index i)
115 { return nodeLocal2Global4Quad(g_mpi, i); },
116 [&](DNDS::index i)
117 { return cellLocal2Global4Quad(g_mpi, i); },
118 cellGM);
119
120 // For each local cell, verify that the original c2n nodes are a subset
121 // of the round-tripped nodes
122 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
123 {
124 std::set<DNDS::index> originalNodes;
125 for (auto n : c2n.father->operator[](iCell))
126 originalNodes.insert(n);
127
128 std::set<DNDS::index> roundTripNodes;
129 for (auto n : c2n_rt.father->operator[](iCell))
130 roundTripNodes.insert(n);
131
132 for (auto n : originalNodes)
133 {
134 CAPTURE(iCell);
135 CAPTURE(n);
136 CHECK(roundTripNodes.count(n) == 1);
137 }
138 }
139}
140
141
142// ===========================================================================
143// ComposeFiltered tests
144// ===========================================================================
145
146// ===========================================================================
147// Compose / ComposeFiltered Tests
148// ===========================================================================
149
150TEST_CASE("ComposeFiltered: 4-quad cell2cell via node-neighbor")
151{
152 auto c2n = make4QuadCell2Node(g_mpi);
153 c2n.father->createGlobalMapping();
154 auto nodeGM = makeNodeGlobalMapping4Quad(g_mpi);
155
156 DNDS::index nNodeLocal = nodeLocalCount4Quad(g_mpi);
157 DNDS::index nCellLocal = c2n.father->Size();
158
159 // Step 1: Inverse to get node2cell
161 c2n, nNodeLocal, g_mpi,
162 [&](DNDS::index i)
163 { return cellLocal2Global4Quad(g_mpi, i); },
164 [&](DNDS::index i)
165 { return nodeLocal2Global4Quad(g_mpi, i); },
166 nodeGM);
167
168 // Step 2: Ghost-pull n2c for off-rank nodes referenced by c2n
169 // Build ghost list: all node globals in c2n that are off-rank
170 std::unordered_set<DNDS::index> ghostNodeSet;
171 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
172 for (auto iNode : c2n.father->operator[](iCell))
173 {
174 auto [ret, rank, val] = nodeGM->search(iNode);
175 if (rank != g_mpi.rank)
176 ghostNodeSet.insert(iNode);
177 }
178 std::vector<DNDS::index> ghostNodes(ghostNodeSet.begin(), ghostNodeSet.end());
179
180 n2c.son = make_ssp<decltype(n2c.son)::element_type>(ObjName{"n2c.son"}, g_mpi);
181 n2c.TransAttach();
182 n2c.trans.createFatherGlobalMapping();
183 n2c.trans.createGhostMapping(ghostNodes);
184 n2c.trans.createMPITypes();
185 n2c.trans.pullOnce();
186
187 // Build bGlobal2Local map for n2c
188 std::unordered_map<DNDS::index, DNDS::index> nodeG2L;
189 for (DNDS::index i = 0; i < n2c.Size(); i++)
190 nodeG2L[n2c.trans.pLGhostMapping->operator()(-1, i)] = i;
191
192 // Step 3: ComposeFiltered with SharedCountPredicate{1, removeSelf=true}
194 c2n, n2c, nCellLocal, nodeG2L,
195 [&](DNDS::index i)
196 { return cellLocal2Global4Quad(g_mpi, i); },
197 SharedCountPredicate{.minShared = 1, .removeSelf = true});
198
199 // Gather and verify globally
200 // Expected cell2cell (node-neighbor, self removed):
201 // cell 0: {1, 2, 3} (shares node 1 with cell 1, node 3 with cell 2, node 4 with all)
202 // cell 1: {0, 2, 3}
203 // cell 2: {0, 1, 3}
204 // cell 3: {0, 1, 2}
205 auto cellGM = std::make_shared<GlobalOffsetsMapping>();
206 cellGM->setMPIAlignBcast(g_mpi, nCellLocal);
207
208 // Gather local c2c results
209 std::vector<std::set<DNDS::index>> globalC2C(4);
210 for (DNDS::index i = 0; i < nCellLocal; i++)
211 {
212 DNDS::index cG = cellLocal2Global4Quad(g_mpi, i);
213 for (auto v : c2c.father->operator[](i))
214 globalC2C[cG].insert(v);
215 }
216
217 // Allgather to merge
218 for (DNDS::index c = 0; c < 4; c++)
219 {
220 std::vector<DNDS::index> localVec(globalC2C[c].begin(), globalC2C[c].end());
221 int localSize = static_cast<int>(localVec.size());
222 std::vector<int> sizes(g_mpi.size);
223 MPI_Allgather(&localSize, 1, MPI_INT, sizes.data(), 1, MPI_INT, g_mpi.comm);
224 std::vector<int> disps(g_mpi.size + 1, 0);
225 for (int r = 0; r < g_mpi.size; r++)
226 disps[r + 1] = disps[r] + sizes[r];
227 std::vector<DNDS::index> allVec(disps[g_mpi.size]);
228 MPI_Allgatherv(localVec.data(), localSize, DNDS_MPI_INDEX,
229 allVec.data(), sizes.data(), disps.data(), DNDS_MPI_INDEX,
230 g_mpi.comm);
231 globalC2C[c].clear();
232 for (auto v : allVec)
233 globalC2C[c].insert(v);
234 }
235
236 // For 4-quad: all cells share at least node 4, so every cell is neighbor of every other
237 for (DNDS::index c = 0; c < 4; c++)
238 {
239 CAPTURE(c);
240 CHECK(globalC2C[c].size() == 3);
241 for (DNDS::index other = 0; other < 4; other++)
242 if (other != c)
243 CHECK(globalC2C[c].count(other) == 1);
244 }
245}
246
247TEST_CASE("ComposeFiltered: face-share filter (minShared=dim)")
248{
249 // For 2D quads, face-share means >= 2 shared nodes.
250 // In the 4-quad grid:
251 // cell 0 shares edge with cell 1 (nodes 1,4) and cell 2 (nodes 3,4)
252 // cell 0 shares only node 4 with cell 3 -> NOT face-neighbor
253 // cell 3 shares edge with cell 1 (nodes 4,5) and cell 2 (nodes 4,7)
254 // cell 3 shares only node 4 with cell 0 -> NOT face-neighbor
255
256 auto c2n = make4QuadCell2Node(g_mpi);
257 c2n.father->createGlobalMapping();
258 auto nodeGM = makeNodeGlobalMapping4Quad(g_mpi);
259
260 DNDS::index nNodeLocal = nodeLocalCount4Quad(g_mpi);
261 DNDS::index nCellLocal = c2n.father->Size();
262
264 c2n, nNodeLocal, g_mpi,
265 [&](DNDS::index i)
266 { return cellLocal2Global4Quad(g_mpi, i); },
267 [&](DNDS::index i)
268 { return nodeLocal2Global4Quad(g_mpi, i); },
269 nodeGM);
270
271 // Ghost-pull n2c
272 std::unordered_set<DNDS::index> ghostNodeSet;
273 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
274 for (auto iNode : c2n.father->operator[](iCell))
275 {
276 auto [ret, rank, val] = nodeGM->search(iNode);
277 if (rank != g_mpi.rank)
278 ghostNodeSet.insert(iNode);
279 }
280 std::vector<DNDS::index> ghostNodes(ghostNodeSet.begin(), ghostNodeSet.end());
281
282 n2c.son = make_ssp<decltype(n2c.son)::element_type>(ObjName{"n2c.son"}, g_mpi);
283 n2c.TransAttach();
284 n2c.trans.createFatherGlobalMapping();
285 n2c.trans.createGhostMapping(ghostNodes);
286 n2c.trans.createMPITypes();
287 n2c.trans.pullOnce();
288
289 std::unordered_map<DNDS::index, DNDS::index> nodeG2L;
290 for (DNDS::index i = 0; i < n2c.Size(); i++)
291 nodeG2L[n2c.trans.pLGhostMapping->operator()(-1, i)] = i;
292
293 // ComposeFiltered with minShared=2 (face-share for 2D)
295 c2n, n2c, nCellLocal, nodeG2L,
296 [&](DNDS::index i)
297 { return cellLocal2Global4Quad(g_mpi, i); },
298 SharedCountPredicate{.minShared = 2, .removeSelf = true});
299
300 // Gather globally
301 std::vector<std::set<DNDS::index>> globalC2CFace(4);
302 for (DNDS::index i = 0; i < nCellLocal; i++)
303 {
304 DNDS::index cG = cellLocal2Global4Quad(g_mpi, i);
305 for (auto v : c2c_face.father->operator[](i))
306 globalC2CFace[cG].insert(v);
307 }
308 for (DNDS::index c = 0; c < 4; c++)
309 {
310 std::vector<DNDS::index> localVec(globalC2CFace[c].begin(), globalC2CFace[c].end());
311 int localSize = static_cast<int>(localVec.size());
312 std::vector<int> sizes(g_mpi.size);
313 MPI_Allgather(&localSize, 1, MPI_INT, sizes.data(), 1, MPI_INT, g_mpi.comm);
314 std::vector<int> disps(g_mpi.size + 1, 0);
315 for (int r = 0; r < g_mpi.size; r++)
316 disps[r + 1] = disps[r] + sizes[r];
317 std::vector<DNDS::index> allVec(disps[g_mpi.size]);
318 MPI_Allgatherv(localVec.data(), localSize, DNDS_MPI_INDEX,
319 allVec.data(), sizes.data(), disps.data(), DNDS_MPI_INDEX,
320 g_mpi.comm);
321 globalC2CFace[c].clear();
322 for (auto v : allVec)
323 globalC2CFace[c].insert(v);
324 }
325
326 // Expected face-neighbors:
327 // cell 0: {1, 2} (not 3, only vertex-share)
328 // cell 1: {0, 3} (not 2, only vertex-share)
329 // cell 2: {0, 3} (not 1, only vertex-share)
330 // cell 3: {1, 2} (not 0, only vertex-share)
331 std::vector<std::set<DNDS::index>> expected = {
332 {1, 2},
333 {0, 3},
334 {0, 3},
335 {1, 2},
336 };
337
338 for (DNDS::index c = 0; c < 4; c++)
339 {
340 CAPTURE(c);
341 CHECK(globalC2CFace[c] == expected[c]);
342 }
343}
344
345
346// ===========================================================================
347// Regression tests
348// ===========================================================================
349
350// ===========================================================================
351// Regression: DSL vs legacy pipeline on real mesh
352// ===========================================================================
353
354/// Build a mesh through the legacy pipeline up to node2cell state,
355/// then compare with DSL Inverse.
356TEST_CASE("Regression: Inverse matches RecoverNode2CellAndNode2Bnd on UniformSquare_10")
357{
358 // Build mesh via legacy pipeline
359 auto mesh = std::make_shared<UnstructuredMesh>(g_mpi, 2);
361 reader.ReadFromCGNSSerial(meshPath("UniformSquare_10.cgns"));
362 reader.Deduplicate1to1Periodic(1e-8);
363 reader.BuildCell2Cell();
364
366 pOpt.metisType = "KWAY";
367 pOpt.metisUfactor = 30;
368 pOpt.metisSeed = 42;
369 pOpt.metisNcuts = 1;
370 reader.MeshPartitionCell2Cell(pOpt);
372
373 // Legacy: build node2cell
374 mesh->RecoverNode2CellAndNode2Bnd();
375
376 // Snapshot the legacy node2cell
377 DNDS::index nNodeLocal = mesh->coords.father->Size();
378 std::vector<std::set<DNDS::index>> legacyN2C(nNodeLocal);
379 for (DNDS::index i = 0; i < nNodeLocal; i++)
380 for (auto v : mesh->node2cell.father->operator[](i))
381 legacyN2C[i].insert(v);
382
383 // DSL: Inverse
384 if (!mesh->coords.father->pLGlobalMapping)
385 mesh->coords.father->createGlobalMapping();
386 if (!mesh->cell2node.father->pLGlobalMapping)
387 mesh->cell2node.father->createGlobalMapping();
388
389 auto dslN2C = MeshConnectivity::Inverse(
390 mesh->cell2node, nNodeLocal, g_mpi,
391 [&](DNDS::index i)
392 { return mesh->CellIndexLocal2Global_NoSon(i); },
393 [&](DNDS::index i)
394 { return mesh->NodeIndexLocal2Global_NoSon(i); },
395 mesh->coords.father->pLGlobalMapping);
396
397 // Compare: for each local node, the set of global cells must match
398 for (DNDS::index i = 0; i < nNodeLocal; i++)
399 {
400 std::set<DNDS::index> dslSet;
401 for (auto v : dslN2C.father->operator[](i))
402 dslSet.insert(v);
403 CAPTURE(i);
404 CHECK(dslSet == legacyN2C[i]);
405 }
406}
407
408TEST_CASE("Regression: ComposeFiltered matches RecoverCell2CellAndBnd2Cell on UniformSquare_10")
409{
410 // Build mesh via legacy pipeline
411 auto mesh = std::make_shared<UnstructuredMesh>(g_mpi, 2);
413 reader.ReadFromCGNSSerial(meshPath("UniformSquare_10.cgns"));
414 reader.Deduplicate1to1Periodic(1e-8);
415 reader.BuildCell2Cell();
416
418 pOpt.metisType = "KWAY";
419 pOpt.metisUfactor = 30;
420 pOpt.metisSeed = 42;
421 pOpt.metisNcuts = 1;
422 reader.MeshPartitionCell2Cell(pOpt);
424
425 mesh->RecoverNode2CellAndNode2Bnd();
426 mesh->RecoverCell2CellAndBnd2Cell();
427
428 // Snapshot legacy cell2cell (global indices, in Adj_PointToGlobal state)
430 std::vector<std::set<DNDS::index>> legacyC2C(nCellLocal);
431 for (DNDS::index i = 0; i < nCellLocal; i++)
432 for (auto v : mesh->cell2cell.father->operator[](i))
433 legacyC2C[i].insert(v);
434
435 // DSL approach: use Inverse to get node2cell, ghost-pull, then ComposeFiltered
436
437 if (!mesh->coords.father->pLGlobalMapping)
438 mesh->coords.father->createGlobalMapping();
439 if (!mesh->cell2node.father->pLGlobalMapping)
440 mesh->cell2node.father->createGlobalMapping();
441
442 DNDS::index nNodeLocal = mesh->coords.father->Size();
443
444 auto dslN2C = MeshConnectivity::Inverse(
445 mesh->cell2node, nNodeLocal, g_mpi,
446 [&](DNDS::index i)
447 { return mesh->CellIndexLocal2Global_NoSon(i); },
448 [&](DNDS::index i)
449 { return mesh->NodeIndexLocal2Global_NoSon(i); },
450 mesh->coords.father->pLGlobalMapping);
451
452 // Ghost-pull dslN2C for off-rank nodes
453 std::unordered_set<DNDS::index> ghostNodeSet;
454 for (DNDS::index i = 0; i < nCellLocal; i++)
455 for (auto iNode : mesh->cell2node.father->operator[](i))
456 {
457 auto [ret, rank, val] = mesh->coords.father->pLGlobalMapping->search(iNode);
458 if (rank != g_mpi.rank)
459 ghostNodeSet.insert(iNode);
460 }
461 std::vector<DNDS::index> ghostNodes(ghostNodeSet.begin(), ghostNodeSet.end());
462
463 dslN2C.son = make_ssp<decltype(dslN2C.son)::element_type>(ObjName{"dslN2C.son"}, g_mpi);
464 dslN2C.TransAttach();
465 dslN2C.trans.createFatherGlobalMapping();
466 dslN2C.trans.createGhostMapping(ghostNodes);
467 dslN2C.trans.createMPITypes();
468 dslN2C.trans.pullOnce();
469
470 // Build bGlobal2Local map
471 std::unordered_map<DNDS::index, DNDS::index> nodeG2L;
472 for (DNDS::index i = 0; i < dslN2C.Size(); i++)
473 nodeG2L[dslN2C.trans.pLGhostMapping->operator()(-1, i)] = i;
474
475 // Verify all nodes in cell2node are in the map
476 for (DNDS::index i = 0; i < nCellLocal; i++)
477 for (auto iNode : mesh->cell2node.father->operator[](i))
478 REQUIRE(nodeG2L.count(iNode));
479
480 // ComposeFiltered with SharedCountPredicate{1, removeSelf=true} -> cell2cell (node-neighbor)
482 mesh->cell2node, dslN2C, nCellLocal, nodeG2L,
483 [&](DNDS::index i)
484 { return mesh->CellIndexLocal2Global_NoSon(i); },
485 SharedCountPredicate{.minShared = 1, .removeSelf = true});
486
487 // Compare
488 for (DNDS::index i = 0; i < nCellLocal; i++)
489 {
490 std::set<DNDS::index> dslSet;
491 for (auto v : dslC2C.father->operator[](i))
492 dslSet.insert(v);
493 CAPTURE(i);
494 CHECK(dslSet == legacyC2C[i]);
495 }
496}
497
498// ===========================================================================
499// MeshConnectivity struct management
500// ===========================================================================
501
502// ===========================================================================
503// Cone / Support management tests
504// ===========================================================================
505
506TEST_CASE("MeshConnectivity: cone management")
507{
509 dag.meshDim = 2;
510
511 CHECK(!dag.hasCone(2, 0));
512 auto &c = dag.addCone(2, 0);
513 CHECK(dag.hasCone(2, 0));
514 CHECK(!dag.hasCone(0, 2));
515 CHECK(c.fromDepth == 2);
516 CHECK(c.toDepth == 0);
517
518 auto *found = dag.findCone(2, 0);
519 CHECK(found == &c);
520 CHECK(dag.findCone(0, 2) == nullptr);
521
522 auto &c2 = dag.addCone(1, 0);
523 CHECK(dag.hasCone(1, 0));
524 CHECK(dag.cones.size() == 2);
525}
526
527TEST_CASE("MeshConnectivity: support management")
528{
530 dag.meshDim = 2;
531
532 CHECK(!dag.hasSupport(0, 2));
533 auto &s = dag.addSupport(0, 2);
534 CHECK(dag.hasSupport(0, 2));
535 CHECK(!dag.hasSupport(2, 0));
536 CHECK(s.fromDepth == 0);
537 CHECK(s.toDepth == 2);
538
539 CHECK(dag.findSupport(0, 2) == &s);
540 CHECK(dag.findSupport(2, 0) == nullptr);
541 CHECK(dag.supports.size() == 1);
542}
543
544TEST_CASE("ConeAdj: AdjVariant typed access")
545{
546 ConeAdj cone;
547 cone.fromDepth = 2;
548 cone.toDepth = 0;
549 // Default-constructed ConeAdj has adj == nullptr (ssp<AdjVariant>)
550 CHECK(!cone.adj);
551 CHECK(!cone.initialized());
552 CHECK(!cone.hasPbi());
553
554 // Initialize with a tAdjPair via makeAdjVariant
555 cone.adj = makeAdjVariant<tAdjPair>();
556 CHECK(cone.adj);
557 CHECK(std::holds_alternative<tAdjPair>(*cone.adj));
558 CHECK(!cone.initialized()); // father is still null inside the pair
559
560 // Initialize with a tAdj2Pair variant
561 ConeAdj cone2;
562 cone2.fromDepth = 1;
563 cone2.toDepth = 2;
564 cone2.adj = makeAdjVariant<tAdj2Pair>();
565 CHECK(std::holds_alternative<tAdj2Pair>(*cone2.adj));
566}
567
568TEST_CASE("SupportAdj: no pbi member")
569{
570 SupportAdj sup;
571 sup.fromDepth = 0;
572 sup.toDepth = 2;
573 CHECK(!sup.initialized());
574 // SupportAdj has no pbi member -- compile-time check that it's absent
575 // (this test just verifies the struct compiles correctly)
576 CHECK(sup.fatherSize() == 0);
577}
578
579// ===========================================================================
580// Fixed-width template tests
581// ===========================================================================
582
583TEST_CASE("Inverse<2>: fixed-width face2cell input")
584{
585 // Build a tiny mesh: 2 triangles sharing an edge.
586 // face2cell: face0→{0}, face1→{0,1}, face2→{0}, face3→{1}, face4→{1}
587 // (face1 is the shared internal face with 2 parents.)
588 //
589 // We construct face2cell as a tAdj2Pair (fixed-2) and call Inverse<2>
590 // to produce cell2face (variable-width).
591
592 DNDS::index nFaces = (g_mpi.rank == 0) ? 5 : 0;
593 DNDS::index nCells = (g_mpi.rank == 0) ? 2 : 0;
594
595 tAdj2Pair f2c;
596 f2c.InitPair("f2c", g_mpi);
597 f2c.father->Resize(nFaces);
598 if (g_mpi.rank == 0)
599 {
600 // boundary face 0: cell 0 only
601 f2c.father->operator()(0, 0) = 0;
602 f2c.father->operator()(0, 1) = UnInitIndex;
603 // internal face 1: cells 0 and 1
604 f2c.father->operator()(1, 0) = 0;
605 f2c.father->operator()(1, 1) = 1;
606 // boundary faces 2, 3, 4
607 f2c.father->operator()(2, 0) = 0;
608 f2c.father->operator()(2, 1) = UnInitIndex;
609 f2c.father->operator()(3, 0) = 1;
610 f2c.father->operator()(3, 1) = UnInitIndex;
611 f2c.father->operator()(4, 0) = 1;
612 f2c.father->operator()(4, 1) = UnInitIndex;
613 }
614
615 auto cellGM = std::make_shared<GlobalOffsetsMapping>();
616 cellGM->setMPIAlignBcast(g_mpi, nCells);
617
618 // Call Inverse with fixed-width template param: Inverse<2>
619 auto c2f = MeshConnectivity::Inverse<2>(
620 f2c, nCells, g_mpi,
622 {
623 // face local2global: identity (rank 0 has all faces)
624 if (g_mpi.rank == 0) return i;
625 return -1;
626 },
628 {
629 // cell local2global: identity
630 if (g_mpi.rank == 0) return i;
631 return -1;
632 },
633 cellGM);
634
635 if (g_mpi.rank == 0)
636 {
637 // Verify: cell 0 should have faces {0, 1, 2}, cell 1 should have faces {1, 3, 4}
638 CHECK(c2f.father->Size() == 2);
639
640 std::set<DNDS::index> c0faces, c1faces;
641 for (auto f : c2f.father->operator[](0))
642 c0faces.insert(f);
643 for (auto f : c2f.father->operator[](1))
644 c1faces.insert(f);
645
646 CHECK(c0faces == std::set<DNDS::index>{0, 1, 2});
647 CHECK(c1faces == std::set<DNDS::index>{1, 3, 4});
648 }
649}
650
651TEST_CASE("narrowAdjToFixed: variable-to-fixed-2 conversion")
652{
653 // Build a variable-width adjacency with rows of sizes 1 and 2.
654 tAdjPair source;
655 source.InitPair("source", g_mpi);
656 source.father->Resize(3);
657 source.father->ResizeRow(0, 2);
658 source.father->operator()(0, 0) = 10;
659 source.father->operator()(0, 1) = 20;
660 source.father->ResizeRow(1, 1);
661 source.father->operator()(1, 0) = 30;
662 source.father->ResizeRow(2, 2);
663 source.father->operator()(2, 0) = 40;
664 source.father->operator()(2, 1) = 50;
665
666 tAdj2Pair target;
667 target.InitPair("target", g_mpi);
668 target.father->Resize(3);
669
670 narrowAdjToFixed<2>(source, target, 3);
671
672 CHECK(target.father->operator()(0, 0) == 10);
673 CHECK(target.father->operator()(0, 1) == 20);
674 CHECK(target.father->operator()(1, 0) == 30);
675 CHECK(target.father->operator()(1, 1) == UnInitIndex);
676 CHECK(target.father->operator()(2, 0) == 40);
677 CHECK(target.father->operator()(2, 1) == 50);
678}
679
680// ===========================================================================
681// Periodic ComposeFiltered test
682// ===========================================================================
683
684TEST_CASE("Periodic 2x2x2: ComposeFiltered cell2cellFace is WRONG without pbi filter")
685{
686 // This test uses MPI-collective operations (Inverse, ghost comm),
687 // so all ranks must participate. Only rank 0 has data.
688 DNDS::index nCells = (g_mpi.rank == 0) ? 8 : 0;
689 DNDS::index nNodes = (g_mpi.rank == 0) ? 8 : 0;
690
691 tAdjPair c2n;
692 c2n.InitPair("p2x2x2_c2n_compose", g_mpi);
693 c2n.father->Resize(nCells);
694
695 if (g_mpi.rank == 0)
696 {
697 auto pm = makePeriodic2x2x2Mesh(g_mpi);
698 // Copy data from the full mesh
699 for (DNDS::index i = 0; i < 8; i++)
700 {
701 c2n.father->ResizeRow(i, 8);
702 for (DNDS::rowsize j = 0; j < 8; j++)
703 c2n.father->operator()(i, j) = pm.cell2node.father->operator()(i, j);
704 }
705 }
706
707 c2n.father->createGlobalMapping();
708 auto nodeGM = std::make_shared<GlobalOffsetsMapping>();
709 nodeGM->setMPIAlignBcast(g_mpi, nNodes);
710
712 c2n, nNodes, g_mpi,
713 [](DNDS::index i) { return i; },
714 [](DNDS::index i) { return i; },
715 nodeGM);
716
717 n2c.son = make_ssp<decltype(n2c.son)::element_type>(ObjName{"n2c.son"}, g_mpi);
718 n2c.TransAttach();
719 n2c.trans.createFatherGlobalMapping();
720 std::vector<DNDS::index> emptyGhost;
721 n2c.trans.createGhostMapping(emptyGhost);
722 n2c.trans.createMPITypes();
723 n2c.trans.pullOnce();
724
725 std::unordered_map<DNDS::index, DNDS::index> nodeG2L;
726 for (DNDS::index i = 0; i < n2c.Size(); i++)
727 nodeG2L[n2c.trans.pLGhostMapping->operator()(-1, i)] = i;
728
730 c2n, n2c, nCells, nodeG2L,
731 [](DNDS::index i) { return i; },
732 SharedCountPredicate{.minShared = 3, .removeSelf = true});
733
734 // Only rank 0 has cells to verify
735 if (g_mpi.rank == 0)
736 {
737 // Without pbi filter: each cell has 7 neighbors (WRONG, should be 3)
738 for (DNDS::index i = 0; i < 8; i++)
739 {
740 CAPTURE(i);
741 CHECK(c2cFace.father->RowSize(i) == 7);
742 }
743 }
744}
745
746// ===========================================================================
747// ComposeFiltered with matchExtra: pbi containment check for bnd2cell
748// ===========================================================================
749
750/// Build a pbi containment matchExtra for ComposeFiltered.
751/// For bnd2cell: checks that every (node, pbi) pair in the A-entity (bnd)
752/// appears in the C-entity (cell). This is stricter than uniform XOR —
753/// it requires exact pbi matching per node, not just consistent shift.
754///
755/// @param a2nodePbi A-entity → node pbi (bnd2nodePbi).
756/// @param c2node C-entity → nodes (cell2node, with ghost for lookup).
757/// @param c2nodePbi C-entity → node pbi (cell2nodePbi, with ghost).
758/// @param cGlobal2Local Maps global C-index to local-appended index.
759static std::function<bool(DNDS::index, DNDS::index, const std::vector<DNDS::index> &)>
760makePbiContainmentMatchExtra(
761 const tAdjPair &a2node,
762 const tPbiPair &a2nodePbi,
763 const tAdjPair &c2node,
764 const tPbiPair &c2nodePbi,
765 const std::unordered_map<DNDS::index, DNDS::index> &cGlobal2Local)
766{
767 return [&](DNDS::index aLocal, DNDS::index cGlobal,
768 const std::vector<DNDS::index> & /*sharedNodes*/) -> bool
769 {
770 auto itC = cGlobal2Local.find(cGlobal);
771 if (itC == cGlobal2Local.end())
772 return false;
773 DNDS::index cLocal = itC->second;
774
775 // For every node in A's row, check that the same (node, pbi) pair
776 // exists in C's row.
777 for (DNDS::rowsize ia = 0; ia < a2node.father->RowSize(aLocal); ia++)
778 {
779 DNDS::index nodeA = a2node.father->operator()(aLocal, ia);
780 uint8_t pbiA = uint8_t(a2nodePbi.father->operator()(aLocal, ia));
781 bool found = false;
782 auto cRow = c2node[cLocal];
783 for (DNDS::rowsize ic = 0; ic < cRow.size(); ic++)
784 {
785 if (cRow[ic] == nodeA && uint8_t(c2nodePbi(cLocal, ic)) == pbiA)
786 {
787 found = true;
788 break;
789 }
790 }
791 if (!found)
792 return false;
793 }
794 return true;
795 };
796}
797
798/// Verify that cell2cellFace on periodic meshes should be derived from
799/// Interpolate (face→cell), not from ComposeFiltered. The compose-based
800/// approach cannot correctly distinguish face-neighbors from non-face-neighbors
801/// when all cells share all nodes (as on 2×2 doubly-periodic or 2×2×2
802/// triply-periodic meshes).
803///
804/// The correct cell2cellFace derivation is tested in
805/// "Periodic 2x2: cell2face from Interpolate" and
806/// "Periodic 2x2x2: face interpolation" tests above.
807
808
809// ---------------------------------------------------------------------------
810int main(int argc, char **argv)
811{
812 MPI_Init(&argc, &argv);
813 g_mpi.setWorld();
814
815 doctest::Context ctx;
816 ctx.applyCommandLine(argc, argv);
817 int res = ctx.run();
818
819 MPI_Finalize();
820 return res;
821}
Layered DAG of mesh adjacency relations with composable DSL operations.
DNDS::index nFaces
Shared synthetic mesh builders for MeshConnectivity unit tests.
int main()
Definition dummy.cpp:3
the host side operators are provided as implemented
const MPI_Datatype DNDS_MPI_INDEX
MPI datatype matching index (= MPI_INT64_T).
Definition MPI.hpp:106
DNDS_CONSTANT const index UnInitIndex
Sentinel "not initialised" index value (= INT64_MIN).
Definition Defines.hpp:181
int32_t rowsize
Row-width / per-row element-count type (signed 32-bit).
Definition Defines.hpp:114
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:112
ssp< T > make_ssp(Args &&...args)
Type-safe replacement for DNDS_MAKE_SSP. Creates ssp<T> with forwarded args.
Definition Defines.hpp:260
DNDS_DEVICE_CALLABLE auto RowSize() const
Uniform row width (delegates to father; father/son share it).
Definition ArrayPair.hpp:40
DNDS_DEVICE_CALLABLE index Size() const
Combined father + son row count.
Definition ArrayPair.hpp:33
t_arrayDeviceView father
Convenience bundle of a father, son, and attached ArrayTransformer.
ssp< TArray > father
Owned-side array (must be resized before ghost setup).
void InitPair(const std::string &name, Args &&...args)
Allocate both father and son arrays, forwarding all args to TArray constructor.
ssp< TArray > son
Ghost-side array (sized automatically by createMPITypes / BorrowAndPull).
bool initialized() const
Check if the adjacency pair is initialized (father is non-null).
int toDepth
Target stratum (e.g., 0 for nodes, dim-1 for faces)
int fromDepth
Source stratum (e.g., dim for cells, dim-1 for faces)
ssp< AdjVariant > adj
Shared adjacency pair (typed by row width)
bool hasPbi() const
Check if pbi is attached (only valid for toDepth==0).
ConeAdj & addCone(int fromDepth, int toDepth)
std::vector< SupportAdj > supports
ConeAdj * findCone(int fromDepth, int toDepth)
Find a cone by (fromDepth, toDepth). Returns nullptr if not found.
static tAdjPair Inverse(const ArrayAdjacencyPair< cone_rs > &cone, index nToLocal, const MPIInfo &mpi, const std::function< index(index)> &fromLocal2Global, const std::function< index(index)> &toLocal2Global, const ssp< GlobalOffsetsMapping > &toGlobalMapping)
bool hasSupport(int fromDepth, int toDepth) const
SupportAdj & addSupport(int fromDepth, int toDepth)
static ArrayAdjacencyPair< out_rs > ComposeFiltered(const ArrayAdjacencyPair< rs_AB > &AB, const ArrayAdjacencyPair< rs_BC > &BC, index nALocal, const std::unordered_map< index, index > &bGlobal2Local, const std::function< index(index)> &aLocal2Global, Predicate &&pred, const std::function< bool(index aLocal, index cGlobal, const std::vector< index > &sharedBGlobals)> &matchExtra=nullptr)
bool hasCone(int fromDepth, int toDepth) const
SupportAdj * findSupport(int fromDepth, int toDepth)
Find a support by (fromDepth, toDepth). Returns nullptr if not found.
int toDepth
Target stratum (e.g., dim for cells)
int fromDepth
Source stratum (e.g., 0 for nodes)
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
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:231
int size
Number of ranks in comm (-1 until initialised).
Definition MPI.hpp:237
int rank
This rank's 0-based index within comm (-1 until initialised).
Definition MPI.hpp:235
MPI_Comm comm
The underlying MPI communicator handle.
Definition MPI.hpp:233
void setWorld()
Initialise the object to MPI_COMM_WORLD. Requires MPI_Init to have run.
Definition MPI.hpp:258
Tag type for naming objects created via make_ssp.
Definition Defines.hpp:254
Eigen::Matrix< real, 5, 1 > v
tVec r(NCells)
CHECK(result.size()==3)
std::vector< std::set< DNDS::index > > expected
DNDS::index nNodeLocal
std::vector< DNDS::index > ghostNodes(ghostNodeSet.begin(), ghostNodeSet.end())
DNDS::index nCellLocal
std::vector< std::set< DNDS::index > > globalC2CFace(4)
std::unordered_map< DNDS::index, DNDS::index > nodeG2L
std::unordered_set< DNDS::index > ghostNodeSet
REQUIRE(bool(result.parent2entityPbi.father))
DistributedHex3D mesh
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")
Eigen::Vector3d n(1.0, 0.0, 0.0)