DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_MeshConnectivity_Ghost.cpp
Go to the documentation of this file.
1/**
2 * @file test_MeshConnectivity_Ghost.cpp
3 * @brief Unit tests for ghost chain types, compilation, and BFS evaluation.
4 *
5 * Tests:
6 * - GhostChain: defaultPrimary compiles into correct tree structure
7 * - GhostChain: prefix merging (shared trie paths)
8 * - GhostChain: invalid chain detection (empty, mismatch)
9 * - GhostChain: checkAvailable (missing adjacency detection)
10 * - GhostChain: dump (diagnostic output)
11 * - GhostChain: face ghost chain
12 * - AdjKind: equality and hash (direct vs intra-level)
13 * - entityDepth: 2D and 3D
14 * - adjKindName: formatted names
15 * - evaluateGhostTree: single hop cell2cell
16 * - evaluateGhostTree: two-hop cell2cell2node with manual ghost
17 * - evaluateGhostTree: 2-ring cell chain
18 * - evaluateGhostTree: union of multiple chains
19 * - evaluateGhostTree: np=1 produces no ghosts
20 * - evaluateGhostTree: performance benchmark (NxN grids with correctness)
21 *
22 * Running:
23 * cmake --build build -t geom_test_mesh_connectivity_ghost -j8
24 * ctest --test-dir build -R geom_mesh_connectivity_ghost --output-on-failure
25 *
26 * Running benchmark only:
27 * mpirun -np 2 ./build/test/cpp/geom_test_mesh_connectivity_ghost -tc="*performance*"
28 * mpirun -np 4 ./build/test/cpp/geom_test_mesh_connectivity_ghost -tc="*performance*"
29 *
30 * The benchmark generates synthetic NxN quad grids (N=100,500,1000),
31 * verifies exact ghost cell counts against analytical expectations,
32 * and prints timing for 1-hop (cell2cell) and 2-hop (cell2cell + cell2node)
33 * ghost evaluation.
34 */
35
36#define DOCTEST_CONFIG_IMPLEMENT
37#include "doctest.h"
38
41#include <string>
42#include <vector>
43#include <set>
44#include <algorithm>
45#include <unordered_set>
46
47using namespace DNDS;
48using namespace DNDS::Geom;
49
50static MPIInfo g_mpi;
51
52// ---------------------------------------------------------------------------
53// Helpers: 4-quad cell2cell and cell2node with mapping (ghost-test specific)
54// ---------------------------------------------------------------------------
55
56static tAdjPair make4QuadCell2Cell(const MPIInfo &mpi)
57{
58 tAdjPair c2c;
59 c2c.InitPair("test_c2c", mpi);
60
62 if (mpi.size == 1)
63 nLocal = 4;
64 else if (mpi.rank < 2)
65 nLocal = 2;
66
67 c2c.father->Resize(nLocal);
68
69 DNDS::index allNeighbors[4][3] = {
70 {1, 2, 3},
71 {0, 2, 3},
72 {0, 1, 3},
73 {0, 1, 2},
74 };
75
76 DNDS::index cellOffset = (mpi.size > 1) ? mpi.rank * 2 : 0;
77 for (DNDS::index i = 0; i < nLocal; i++)
78 {
79 DNDS::index globalCell = cellOffset + i;
80 c2c.father->ResizeRow(i, 3);
81 for (DNDS::rowsize j = 0; j < 3; j++)
82 c2c.father->operator()(i, j) = allNeighbors[globalCell][j];
83 }
84
85 c2c.father->pLGlobalMapping = std::make_shared<GlobalOffsetsMapping>();
86 c2c.father->pLGlobalMapping->setMPIAlignBcast(mpi, nLocal);
87
88 return c2c;
89}
90
91/// Build partitioned cell2node with pLGlobalMapping.
92static tAdjPair make4QuadCell2NodeWithMapping(const MPIInfo &mpi)
93{
94 tAdjPair c2n = make4QuadCell2Node(mpi);
95
97 if (mpi.size == 1)
98 nLocal = 4;
99 else if (mpi.rank < 2)
100 nLocal = 2;
101
102 c2n.father->pLGlobalMapping = std::make_shared<GlobalOffsetsMapping>();
103 c2n.father->pLGlobalMapping->setMPIAlignBcast(mpi, nLocal);
104
105 return c2n;
106}
107
108// ===========================================================================
109// Ghost chain compilation tests
110// ===========================================================================
111
112TEST_CASE("GhostChain: defaultPrimary compiles")
113{
114 auto spec = GhostSpec::defaultPrimary();
115 auto tree = CompiledGhostTree::compile(spec);
116
117 CHECK(tree.roots.size() == 2);
118
119 const GhostTreeNode *cellRoot = nullptr;
120 const GhostTreeNode *bndRoot = nullptr;
121 for (auto &r : tree.roots)
122 {
123 if (r.kind == EntityKind::Cell)
124 cellRoot = &r;
125 if (r.kind == EntityKind::Bnd)
126 bndRoot = &r;
127 }
128 REQUIRE(cellRoot);
129 REQUIRE(bndRoot);
130
131 CHECK(!cellRoot->collect);
132 CHECK(cellRoot->level == 0);
133
134 REQUIRE(cellRoot->children.size() == 1);
135 auto &c2cNode = cellRoot->children[0];
136 CHECK(c2cNode.kind == EntityKind::Cell);
137 CHECK(c2cNode.hop == Adj::Cell2Cell);
138 CHECK(c2cNode.collect);
139 CHECK(c2cNode.level == 1);
140
141 REQUIRE(c2cNode.children.size() == 1);
142 auto &c2nNode = c2cNode.children[0];
143 CHECK(c2nNode.kind == EntityKind::Node);
144 CHECK(c2nNode.hop == Adj::Cell2Node);
145 CHECK(c2nNode.collect);
146 CHECK(c2nNode.level == 2);
147 CHECK(c2nNode.children.empty());
148
149 CHECK(!bndRoot->collect);
150 CHECK(bndRoot->level == 0);
151
152 REQUIRE(bndRoot->children.size() == 1);
153 auto &b2nNode = bndRoot->children[0];
154 CHECK(b2nNode.kind == EntityKind::Node);
155 CHECK(b2nNode.hop == Adj::Bnd2Node);
156 CHECK(b2nNode.collect); // chain 4 marks intermediate Node as collect
157 CHECK(b2nNode.level == 1);
158
159 REQUIRE(b2nNode.children.size() == 1);
160 auto &n2bNode = b2nNode.children[0];
161 CHECK(n2bNode.kind == EntityKind::Bnd);
162 CHECK(n2bNode.hop == Adj::Node2Bnd);
163 CHECK(n2bNode.collect);
164 CHECK(n2bNode.level == 2);
165
166 REQUIRE(n2bNode.children.size() == 1);
167 auto &b2n2Node = n2bNode.children[0];
168 CHECK(b2n2Node.kind == EntityKind::Node);
169 CHECK(b2n2Node.hop == Adj::Bnd2Node);
170 CHECK(b2n2Node.collect);
171 CHECK(b2n2Node.level == 3);
172 CHECK(b2n2Node.children.empty());
173
174 CHECK(tree.maxLevel == 3);
175
176 auto kinds = tree.collectedKinds();
177 CHECK(kinds.count(EntityKind::Cell) == 1);
178 CHECK(kinds.count(EntityKind::Node) == 1);
179 CHECK(kinds.count(EntityKind::Bnd) == 1);
180 CHECK(kinds.count(EntityKind::Face) == 0);
181
182 auto adjs = tree.requiredAdjs();
183 CHECK(adjs.count(Adj::Cell2Cell) == 1);
184 CHECK(adjs.count(Adj::Cell2Node) == 1);
185 CHECK(adjs.count(Adj::Bnd2Node) == 1);
186 CHECK(adjs.count(Adj::Node2Bnd) == 1);
187 CHECK(adjs.size() == 4);
188}
189
190TEST_CASE("GhostChain: prefix merging")
191{
192 GhostSpec spec{{
193 {EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell},
194 {EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Cell}, EntityKind::Cell},
195 }};
196 auto tree = CompiledGhostTree::compile(spec);
197
198 REQUIRE(tree.roots.size() == 1);
199 auto &root = tree.roots[0];
200 CHECK(root.kind == EntityKind::Cell);
201 REQUIRE(root.children.size() == 1);
202
203 auto &ring1 = root.children[0];
204 CHECK(ring1.kind == EntityKind::Cell);
205 CHECK(ring1.collect);
206 CHECK(ring1.level == 1);
207 REQUIRE(ring1.children.size() == 1);
208
209 auto &ring2 = ring1.children[0];
210 CHECK(ring2.kind == EntityKind::Cell);
211 CHECK(ring2.collect);
212 CHECK(ring2.level == 2);
213 CHECK(ring2.children.empty());
214
215 CHECK(tree.maxLevel == 2);
216}
217
218TEST_CASE("GhostChain: invalid chain detection")
219{
220 CHECK_THROWS_AS(
222 {EntityKind::Cell, {}, EntityKind::Cell},
223 }}),
224 std::runtime_error);
225
226 CHECK_THROWS_AS(
228 {EntityKind::Node, {Adj::Cell2Node}, EntityKind::Node},
229 }}),
230 std::runtime_error);
231
232 CHECK_THROWS_AS(
234 {EntityKind::Cell, {Adj::Cell2Node}, EntityKind::Cell},
235 }}),
236 std::runtime_error);
237
238 CHECK_THROWS_AS(
240 {EntityKind::Bnd, {Adj::Bnd2Node, Adj::Cell2Node}, EntityKind::Node},
241 }}),
242 std::runtime_error);
243}
244
245TEST_CASE("GhostChain: checkAvailable")
246{
248 dag.meshDim = 2;
249
250 tAdjPair c2c, c2n;
251 dag.registerAdj(Adj::Cell2Cell, c2c);
252 dag.registerAdj(Adj::Cell2Node, c2n);
253
254 auto spec = GhostSpec::defaultPrimary();
255 auto tree = CompiledGhostTree::compile(spec);
256
257 auto missing = tree.checkAvailable(dag);
258 CHECK(missing.size() == 2);
259
260 std::unordered_set<AdjKind, AdjKindHash> missingSet(missing.begin(), missing.end());
261 CHECK(missingSet.count(Adj::Bnd2Node) == 1);
262 CHECK(missingSet.count(Adj::Node2Bnd) == 1);
263}
264
265TEST_CASE("GhostChain: dump")
266{
267 auto spec = GhostSpec::defaultPrimary();
268 auto tree = CompiledGhostTree::compile(spec);
269 std::string d = tree.dump();
270
271 CHECK(d.find("Cell") != std::string::npos);
272 CHECK(d.find("Bnd") != std::string::npos);
273 CHECK(d.find("Node") != std::string::npos);
274 CHECK(d.find("COLLECT") != std::string::npos);
275}
276
277TEST_CASE("GhostChain: face ghost chain")
278{
279 GhostSpec spec{{
280 {EntityKind::Cell, {Adj::Cell2Face}, EntityKind::Face},
281 }};
282 auto tree = CompiledGhostTree::compile(spec);
283
284 REQUIRE(tree.roots.size() == 1);
285 auto &root = tree.roots[0];
286 CHECK(root.kind == EntityKind::Cell);
287 REQUIRE(root.children.size() == 1);
288
289 auto &faceNode = root.children[0];
290 CHECK(faceNode.kind == EntityKind::Face);
291 CHECK(faceNode.hop == Adj::Cell2Face);
292 CHECK(faceNode.collect);
293 CHECK(faceNode.level == 1);
294
295 auto adjs = tree.requiredAdjs();
296 CHECK(adjs.count(Adj::Cell2Face) == 1);
297 CHECK(adjs.size() == 1);
298}
299
300TEST_CASE("GhostChain: AdjKind equality and hash")
301{
302 AdjKind a(EntityKind::Cell, EntityKind::Node);
303 AdjKind b(EntityKind::Cell, EntityKind::Node, EntityKind::Face);
304 CHECK(a == b); // direct: via ignored
305
306 AdjKind c(EntityKind::Cell, EntityKind::Cell, EntityKind::Node);
307 AdjKind d(EntityKind::Cell, EntityKind::Cell, EntityKind::Face);
308 CHECK(c != d); // intra-level: via matters
309
310 AdjKind e(EntityKind::Cell, EntityKind::Cell, EntityKind::Node);
311 CHECK(c == e);
312
313 AdjKindHash h;
314 CHECK(h(a) == h(b));
315 CHECK(h(c) == h(e));
316 (void)h(d);
317}
318
319TEST_CASE("GhostChain: entityDepth")
320{
321 CHECK(entityDepth(EntityKind::Cell, 3) == 3);
322 CHECK(entityDepth(EntityKind::Face, 3) == 2);
323 CHECK(entityDepth(EntityKind::Edge, 3) == 1);
324 CHECK(entityDepth(EntityKind::Node, 3) == 0);
325 CHECK(entityDepth(EntityKind::Bnd, 3) == 2);
326
327 CHECK(entityDepth(EntityKind::Cell, 2) == 2);
328 CHECK(entityDepth(EntityKind::Face, 2) == 1);
329 CHECK(entityDepth(EntityKind::Edge, 2) == 1);
330 CHECK(entityDepth(EntityKind::Node, 2) == 0);
331 CHECK(entityDepth(EntityKind::Bnd, 2) == 1);
332}
333
334TEST_CASE("GhostChain: adjKindName")
335{
336 CHECK(adjKindName(Adj::Cell2Node) == "Cell2Node");
337 CHECK(adjKindName(Adj::Node2Cell) == "Node2Cell");
338 CHECK(adjKindName(Adj::Cell2Cell) == "Cell2Cell(Node)");
339 CHECK(adjKindName(Adj::Cell2CellFace) == "Cell2Cell(Face)");
340 CHECK(adjKindName(Adj::Bnd2Node) == "Bnd2Node");
341}
342
343// ===========================================================================
344// evaluateGhostTree standalone tests
345// ===========================================================================
346
347// ---------------------------------------------------------------------------
348// Synthetic NxN-per-rank tiled quad grid for benchmarking
349// ---------------------------------------------------------------------------
350
351/// Generate a tiled NxN quad grid: each rank owns one NxN tile.
352/// The global mesh is N rows × (np*N) columns of quad cells.
353///
354/// Rank 0 tile Rank 1 tile Rank 2 tile ...
355/// [0,N)×[0,N) [0,N)×[N,2N) [0,N)×[2N,3N)
356///
357/// Cell (row r, col c) has global index r * totalCols + c,
358/// where totalCols = np * N.
359///
360/// Node (row r, col c) has global index r * (totalCols + 1) + c.
361///
362/// Each rank owns cells with col in [rank*N, (rank+1)*N).
363/// Ghost cells: 1-ring node-neighbors across tile boundaries.
364///
365/// NOTE: Node global indices are row-major across the full (N+1)×(np*N+1)
366/// grid, so they are NOT rank-contiguous. The node GlobalOffsetsMapping
367/// from setMPIAlignBcast assigns contiguous ownership ranges
368/// (rank 0 owns [0, nNodeLocal_0), rank 1 owns [nNodeLocal_0, ...)).
369/// This abstract partition does NOT align with geometric locality — a
370/// rank's "owned" range may include node indices physically located on
371/// other ranks' tiles. The test is self-consistent: both the evaluator
372/// and the analytical expected-value functions use the same nodeGM, so
373/// ghost counts are exact within this abstract partition.
375{
376 bool periodic{false};
381 std::vector<DNDS::index> ownerNodeOffsets; // pre-computed, no MPI
384
386 {
387 return rank * N * N + localRow * N + localCol;
388 }
390 {
391 DNDS::index rc = std::min(gc / N, ranksPerRow - 1);
392 DNDS::index rr = std::min(gr / N, ranksPerCol - 1);
393 return rr * ranksPerRow + rc;
394 }
395 /// Node global index via per-rank local matrix. Node at (r,c) belongs to
396 /// cellOwnerRank(r,c). Its local (row,col) within that rank and its
397 /// rank's offset give the 1-D global index. Periodic nodes wrap.
399 {
400 DNDS::index wr = r, wc = c;
401 if (periodic)
402 {
403 wr = ((r % totalRows) + totalRows) % totalRows;
404 wc = ((c % totalCols) + totalCols) % totalCols;
405 }
406 DNDS::index owner = cellOwnerRank(wr, wc);
407 DNDS::index ownerRC = (owner % ranksPerRow) * N;
408 DNDS::index ownerRR = (owner / ranksPerRow) * N;
409 DNDS::index ownerNCW = periodic ? N
410 : N + ((owner % ranksPerRow == ranksPerRow - 1) ? 1 : 0);
411 DNDS::index ownerNRH = periodic ? N
412 : N + ((owner / ranksPerRow == ranksPerCol - 1) ? 1 : 0);
413 DNDS::index localCol = wc - ownerRC;
414 DNDS::index localRow = wr - ownerRR;
415 return ownerNodeOffsets[owner] + localRow * ownerNCW + localCol;
416 }
417
418 void build(DNDS::index tileN, const MPIInfo &mpi)
419 {
420 N = tileN;
421
422 ranksPerRow = DNDS::index(std::sqrt(double(mpi.size)));
423 ranksPerCol = mpi.size / ranksPerRow;
424 while (ranksPerRow * ranksPerCol != mpi.size && ranksPerRow > 1)
425 ranksPerRow--, ranksPerCol = mpi.size / ranksPerRow;
426 if (ranksPerRow * ranksPerCol != mpi.size)
427 ranksPerRow = mpi.size, ranksPerCol = 1;
428
429 rankCol = mpi.rank % ranksPerRow;
430 rankRow = mpi.rank / ranksPerRow;
431
434 colStart = rankCol * N;
435 colEnd = (rankCol + 1) * N;
436 rowStart = rankRow * N;
437 rowEnd = (rankRow + 1) * N;
438 nCellLocal = N * N;
439
440 bool lastInRow = (rankCol == ranksPerRow - 1);
441 bool lastInCol = (rankRow == ranksPerCol - 1);
442
443 // Node partition: pre-compute offsets from known shapes.
444 ownerNodeOffsets.resize(mpi.size);
445 {
446 DNDS::index off = 0;
447 for (DNDS::index ri = 0; ri < mpi.size; ri++)
448 {
449 ownerNodeOffsets[ri] = off;
450 DNDS::index rc = ri % ranksPerRow;
451 DNDS::index rr = ri / ranksPerRow;
452 DNDS::index ncw = N + (periodic ? 0 : (rc == ranksPerRow - 1 ? 1 : 0));
453 DNDS::index nrh = N + (periodic ? 0 : (rr == ranksPerCol - 1 ? 1 : 0));
454 off += nrh * ncw;
455 }
456 }
457 nNodeLocal = (periodic ? N * N
458 : (N + (lastInRow ? 1 : 0)) * (N + (lastInCol ? 1 : 0)));
459
460 cellGM = std::make_shared<GlobalOffsetsMapping>();
461 cellGM->setMPIAlignBcast(mpi, nCellLocal);
462
463 nodeGM = std::make_shared<GlobalOffsetsMapping>();
464 nodeGM->setMPIAlignBcast(mpi, nNodeLocal);
465
466 cell2node.InitPair("tiled_c2n", mpi);
467 cell2node.father->Resize(nCellLocal);
468 for (DNDS::index iLocal = 0; iLocal < nCellLocal; iLocal++)
469 {
470 DNDS::index r = rowStart + iLocal / N;
471 DNDS::index c = colStart + iLocal % N;
472 cell2node.father->ResizeRow(iLocal, 4);
473 cell2node.father->operator()(iLocal, 0) = nodeGlobal(r, c);
474 cell2node.father->operator()(iLocal, 1) = nodeGlobal(r, c + 1);
475 cell2node.father->operator()(iLocal, 2) = nodeGlobal(r + 1, c + 1);
476 cell2node.father->operator()(iLocal, 3) = nodeGlobal(r + 1, c);
477 }
478 cell2node.father->pLGlobalMapping = cellGM;
479
480 cell2cell.InitPair("tiled_c2c", mpi);
481 cell2cell.father->Resize(nCellLocal);
482 for (DNDS::index iLocal = 0; iLocal < nCellLocal; iLocal++)
483 {
484 DNDS::index r = rowStart + iLocal / N;
485 DNDS::index c = colStart + iLocal % N;
486 DNDS::index myGlobal = cellGlobal(mpi.rank, iLocal / N, iLocal % N);
487
488 std::set<DNDS::index> neighbors;
489 for (DNDS::index dr = -1; dr <= 1; dr++)
490 {
491 for (DNDS::index dc = -1; dc <= 1; dc++)
492 {
493 if (dr == 0 && dc == 0)
494 continue;
495 DNDS::index nr = r + dr;
496 DNDS::index nc = c + dc;
497 if (periodic)
498 {
499 nr = ((nr % totalRows) + totalRows) % totalRows;
500 nc = ((nc % totalCols) + totalCols) % totalCols;
501 }
502 else if (nr < 0 || nr >= totalRows || nc < 0 || nc >= totalCols)
503 continue;
504 DNDS::index owner = cellOwnerRank(nr, nc);
505 DNDS::index nbr = nr - (owner / ranksPerRow) * N;
506 DNDS::index nbc = nc - (owner % ranksPerRow) * N;
507 DNDS::index nbGlobal = cellGlobal(owner, nbr, nbc);
508 if (nbGlobal != myGlobal)
509 neighbors.insert(nbGlobal);
510 }
511 }
512 std::vector<DNDS::index> nbVec(neighbors.begin(), neighbors.end());
513 cell2cell.father->ResizeRow(iLocal, nbVec.size());
514 for (DNDS::rowsize j = 0; j < static_cast<DNDS::rowsize>(nbVec.size()); j++)
515 cell2cell.father->operator()(iLocal, j) = nbVec[j];
516 }
517 cell2cell.father->pLGlobalMapping = cellGM;
518 }
519
521 {
522 DNDS::index dx = std::min(N + 2 * nLayers + 1, totalRows);
523 DNDS::index dy = std::min(N + 2 * nLayers + 1, totalCols);
524 DNDS::index total = (periodic ? N * N : nNodeLocal);
525 return dx * dy - total;
526 }
527
528 std::set<DNDS::index> expectedGhostCells(const MPIInfo &mpi) const
529 {
530 DNDS::index cellOffset = cellGM->operator()(mpi.rank, 0);
531 std::set<DNDS::index> ghosts;
532 for (DNDS::index iLocal = 0; iLocal < nCellLocal; iLocal++)
533 for (DNDS::rowsize j = 0; j < cell2cell.father->operator[](iLocal).size(); j++)
534 {
535 DNDS::index nb = cell2cell.father->operator()(iLocal, j);
536 if (nb < cellOffset || nb >= cellOffset + nCellLocal)
537 ghosts.insert(nb);
538 }
539 return ghosts;
540 }
541
542 std::set<DNDS::index> expectedGhostNodesFromOwnedCells(const MPIInfo &mpi) const
543 {
544 DNDS::index nodeOffset = nodeGM->operator()(mpi.rank, 0);
546 std::set<DNDS::index> ghosts;
547 for (DNDS::index iLocal = 0; iLocal < nCellLocal; iLocal++)
548 for (DNDS::rowsize j = 0; j < 4; j++)
549 {
550 DNDS::index n = cell2node.father->operator()(iLocal, j);
551 if (n < nodeOffset || n >= nodeEnd)
552 ghosts.insert(n);
553 }
554 return ghosts;
555 }
556
557 /// Analytical n-layer cell ghost count.
559 {
560 if (periodic)
561 {
562 DNDS::index dx = std::min(N + 2 * nLayers, totalCols);
563 DNDS::index dy = std::min(N + 2 * nLayers, totalRows);
564 return dx * dy - N * N;
565 }
566 DNDS::index rMin = std::max(DNDS::index(0), rowStart - nLayers * N);
567 DNDS::index rMax = std::min(totalRows, rowEnd + nLayers * N);
568 DNDS::index cMin = std::max(DNDS::index(0), colStart - nLayers * N);
569 DNDS::index cMax = std::min(totalCols, colEnd + nLayers * N);
570 return (rMax - rMin) * (cMax - cMin) - N * N;
571 }
572};
573TEST_CASE("evaluateGhostTree: performance benchmark")
574{
575 if (g_mpi.size < 2)
576 return;
577
578 std::vector<DNDS::index> sizes = {32, 100};
579 if (g_mpi.size <= 4)
580 sizes.push_back(500);
581
582 for (auto N : sizes)
583 {
585 grid.build(N, g_mpi);
586
588 dag.meshDim = 2;
591 dag.registerGlobalMapping(EntityKind::Cell, grid.cellGM);
592 dag.registerGlobalMapping(EntityKind::Node, grid.nodeGM);
593
594 // --- 1-hop: cell ghost ---
595 GhostSpec spec1{{{EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell}}};
596 auto tree1 = CompiledGhostTree::compile(spec1);
597
598 auto t0 = MPI_Wtime();
599 auto result1 = dag.evaluateGhostTree(tree1, g_mpi);
600 auto t1 = MPI_Wtime();
601
602 // Correctness: exact ghost cell count.
603 auto expectedCells = grid.expectedGhostCells(g_mpi);
604 auto &ghostCells = result1.ghostIndices[EntityKind::Cell];
605 std::set<DNDS::index> actualCells(ghostCells.begin(), ghostCells.end());
606 CHECK(actualCells.size() == expectedCells.size());
607 CHECK(actualCells == expectedCells);
608
609 // --- 2-hop: cell2cell + cell2node (owned cells only, no ghost c2n) ---
610 GhostSpec spec2{{
611 {EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell},
612 {EntityKind::Cell, {Adj::Cell2Node}, EntityKind::Node},
613 }};
614 auto tree2 = CompiledGhostTree::compile(spec2);
615
616 auto t2 = MPI_Wtime();
617 auto result2 = dag.evaluateGhostTree(tree2, g_mpi);
618 auto t3 = MPI_Wtime();
619
620 // Correctness: exact ghost node count (from owned cells only).
621 auto expectedNodes = grid.expectedGhostNodesFromOwnedCells(g_mpi);
622 auto &ghostNodes = result2.ghostIndices[EntityKind::Node];
623 std::set<DNDS::index> actualNodes(ghostNodes.begin(), ghostNodes.end());
624 CHECK(actualNodes.size() == expectedNodes.size());
625 CHECK(actualNodes == expectedNodes);
626
627 if (g_mpi.rank == 0)
628 {
629 fmt::print(" N={}(per rank): cells/rank={}, ghost_cells={} (expected={}), "
630 "ghost_nodes={} (expected={}), "
631 "1-hop={:.4f}s, 2-hop={:.4f}s\n",
632 N, grid.nCellLocal,
633 ghostCells.size(), expectedCells.size(),
634 ghostNodes.size(), expectedNodes.size(),
635 t1 - t0, t3 - t2);
636 }
637
638 CHECK(result1.hasGhosts(EntityKind::Cell));
639 }
640}
641
642TEST_CASE("evaluateGhostTree: single hop cell2cell")
643{
644 if (g_mpi.size < 2)
645 return;
646
647 auto c2c = make4QuadCell2Cell(g_mpi);
648
650 dag.meshDim = 2;
651 dag.registerAdj(Adj::Cell2Cell, c2c);
652 dag.registerGlobalMapping(EntityKind::Cell, c2c.father->pLGlobalMapping);
653
654 GhostSpec spec{{
655 {EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell},
656 }};
657 auto tree = CompiledGhostTree::compile(spec);
658 auto missing = tree.checkAvailable(dag);
659 REQUIRE(missing.empty());
660
661 auto result = dag.evaluateGhostTree(tree, g_mpi);
662
663 // hasGhosts is collective — true on all ranks if any rank has ghosts.
664 CHECK(result.hasGhosts(EntityKind::Cell));
665 CHECK(!result.hasGhosts(EntityKind::Node)); // no node chain in this spec
666 auto &ghostCells = result.ghostIndices[EntityKind::Cell];
667
668 if (g_mpi.rank == 0)
669 {
670 // Rank 0 owns cells {0,1}. All 4 cells share node 4, so neighbors = {2,3}.
671 // Exact: 2 ghost cells, no more, no less.
672 CHECK(ghostCells.size() == 2);
673 CHECK(result.totalGhosts() == 2);
674 CHECK(ghostCells[0] == 2);
675 CHECK(ghostCells[1] == 3);
676 }
677 else if (g_mpi.rank == 1)
678 {
679 CHECK(ghostCells.size() == 2);
680 CHECK(result.totalGhosts() == 2);
681 CHECK(ghostCells[0] == 0);
682 CHECK(ghostCells[1] == 1);
683 }
684 else
685 {
686 // Ranks 2+: no cells, no ghosts on this rank.
687 CHECK(ghostCells.empty());
688 CHECK(result.totalGhosts() == 0);
689 }
690}
691
692TEST_CASE("evaluateGhostTree: two-hop cell2cell2node with manual ghost")
693{
694 if (g_mpi.size < 2)
695 return;
696
697 auto c2c = make4QuadCell2Cell(g_mpi);
698 auto c2n = make4QuadCell2NodeWithMapping(g_mpi);
699
700 // Node global mapping: rank 0 owns 0-4, rank 1 owns 5-8.
701 DNDS::index nNodesLocal = 0;
702 if (g_mpi.size == 1)
703 nNodesLocal = 9;
704 else if (g_mpi.rank == 0)
705 nNodesLocal = 5;
706 else if (g_mpi.rank == 1)
707 nNodesLocal = 4;
708
709 auto nodeGM = std::make_shared<GlobalOffsetsMapping>();
710 nodeGM->setMPIAlignBcast(g_mpi, nNodesLocal);
711
712 // Manually ghost cell2node for ghost cells.
713 // All ranks must participate in MPI collectives.
714 {
715 c2n.TransAttach();
716 c2n.trans.createFatherGlobalMapping();
717
718 std::vector<DNDS::index> ghostIndices;
719 if (g_mpi.rank == 0)
720 ghostIndices = {2, 3};
721 else if (g_mpi.rank == 1)
722 ghostIndices = {0, 1};
723 // ranks >= 2: empty ghost set
724
725 c2n.trans.createGhostMapping(ghostIndices);
726 c2n.trans.createMPITypes();
727 c2n.trans.pullOnce();
728 }
729
731 dag.meshDim = 2;
732 dag.registerAdj(Adj::Cell2Cell, c2c);
733 dag.registerAdj(Adj::Cell2Node, c2n);
734 dag.registerGlobalMapping(EntityKind::Cell, c2c.father->pLGlobalMapping);
735 dag.registerGlobalMapping(EntityKind::Node, nodeGM);
736
737 GhostSpec spec{{
738 {EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell},
739 {EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Node}, EntityKind::Node},
740 }};
741 auto tree = CompiledGhostTree::compile(spec);
742 auto result = dag.evaluateGhostTree(tree, g_mpi);
743
744 // Collective: all ranks agree these kinds have ghosts.
745 CHECK(result.hasGhosts(EntityKind::Cell));
746 CHECK(result.hasGhosts(EntityKind::Node));
747
748 auto &ghostNodes = result.ghostIndices[EntityKind::Node];
749
750 if (g_mpi.rank == 0)
751 {
752 // Rank 0 owns nodes 0-4.
753 // Ghost cells 2,3 have nodes {3,4,7,6} and {4,5,8,7}.
754 // Owned cells 0,1 have nodes {0,1,4,3} and {1,2,5,4}.
755 // cell2cell2node = all nodes of owned+ghost cells = {0..8}.
756 // Non-owned: {5,6,7,8}. Exact count: 4 ghost nodes.
757 CHECK(ghostNodes.size() == 4);
758 std::set<DNDS::index> ghostNodeSet(ghostNodes.begin(), ghostNodes.end());
759 CHECK(ghostNodeSet == std::set<DNDS::index>{5, 6, 7, 8});
760 // Total: 2 ghost cells + 4 ghost nodes = 6.
761 CHECK(result.totalGhosts() == 6);
762 }
763 else if (g_mpi.rank == 1)
764 {
765 // Rank 1 owns nodes 5-8.
766 // cell2cell2node = all nodes of owned+ghost cells = {0..8}.
767 // Non-owned: {0,1,2,3,4}. Exact count: 5 ghost nodes.
768 CHECK(ghostNodes.size() == 5);
769 std::set<DNDS::index> ghostNodeSet(ghostNodes.begin(), ghostNodes.end());
770 CHECK(ghostNodeSet == std::set<DNDS::index>{0, 1, 2, 3, 4});
771 CHECK(result.totalGhosts() == 7); // 2 ghost cells + 5 ghost nodes
772 }
773 else
774 {
775 // Ranks 2+: no cells, no ghosts of any kind.
776 CHECK(result.ghostIndices[EntityKind::Node].empty());
777 CHECK(result.totalGhosts() == 0);
778 }
779}
780
781TEST_CASE("evaluateGhostTree: 2-ring cell chain")
782{
783 if (g_mpi.size < 2)
784 return;
785
786 auto c2c = make4QuadCell2Cell(g_mpi);
787
788 // Ghost cell2cell for 2-ring traversal. All ranks must participate.
789 {
790 c2c.TransAttach();
791 c2c.trans.createFatherGlobalMapping();
792 std::vector<DNDS::index> ghostIndices;
793 if (g_mpi.rank == 0)
794 ghostIndices = {2, 3};
795 else if (g_mpi.rank == 1)
796 ghostIndices = {0, 1};
797 c2c.trans.createGhostMapping(ghostIndices);
798 c2c.trans.createMPITypes();
799 c2c.trans.pullOnce();
800 }
801
803 dag.meshDim = 2;
804 dag.registerAdj(Adj::Cell2Cell, c2c);
805 dag.registerGlobalMapping(EntityKind::Cell, c2c.father->pLGlobalMapping);
806
807 GhostSpec spec1{{{EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell}}};
808 auto result1 = dag.evaluateGhostTree(CompiledGhostTree::compile(spec1), g_mpi);
809
810 GhostSpec spec2{{{EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Cell}, EntityKind::Cell}}};
811 auto result2 = dag.evaluateGhostTree(CompiledGhostTree::compile(spec2), g_mpi);
812
813 if (g_mpi.rank < 2)
814 {
815 // On this mesh, 1-ring already covers all cells. 2-ring is identical.
816 auto &ghost1 = result1.ghostIndices[EntityKind::Cell];
817 auto &ghost2 = result2.ghostIndices[EntityKind::Cell];
818 CHECK(ghost1.size() == 2);
819 CHECK(ghost2.size() == 2);
820 CHECK(ghost1 == ghost2);
821 }
822}
823
824TEST_CASE("evaluateGhostTree: union of multiple chains")
825{
826 if (g_mpi.size < 2)
827 return;
828
829 auto c2c = make4QuadCell2Cell(g_mpi);
830 auto c2n = make4QuadCell2NodeWithMapping(g_mpi);
831
832 DNDS::index nNodesLocal = 0;
833 if (g_mpi.size == 1)
834 nNodesLocal = 9;
835 else if (g_mpi.rank == 0)
836 nNodesLocal = 5;
837 else if (g_mpi.rank == 1)
838 nNodesLocal = 4;
839 auto nodeGM = std::make_shared<GlobalOffsetsMapping>();
840 nodeGM->setMPIAlignBcast(g_mpi, nNodesLocal);
841
842 // Ghost cell2node. All ranks participate.
843 {
844 c2n.TransAttach();
845 c2n.trans.createFatherGlobalMapping();
846 std::vector<DNDS::index> ghostIndices;
847 if (g_mpi.rank == 0)
848 ghostIndices = {2, 3};
849 else if (g_mpi.rank == 1)
850 ghostIndices = {0, 1};
851 c2n.trans.createGhostMapping(ghostIndices);
852 c2n.trans.createMPITypes();
853 c2n.trans.pullOnce();
854 }
855
857 dag.meshDim = 2;
858 dag.registerAdj(Adj::Cell2Cell, c2c);
859 dag.registerAdj(Adj::Cell2Node, c2n);
860 dag.registerGlobalMapping(EntityKind::Cell, c2c.father->pLGlobalMapping);
861 dag.registerGlobalMapping(EntityKind::Node, nodeGM);
862
863 // Chain 2 alone.
864 GhostSpec spec2{{{EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Node}, EntityKind::Node}}};
865 auto result2 = dag.evaluateGhostTree(CompiledGhostTree::compile(spec2), g_mpi);
866
867 // Union of chain 1 (cell2node) + chain 2 (cell2cell2node).
868 GhostSpec specUnion{{
869 {EntityKind::Cell, {Adj::Cell2Node}, EntityKind::Node},
870 {EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Node}, EntityKind::Node},
871 }};
872 auto resultUnion = dag.evaluateGhostTree(CompiledGhostTree::compile(specUnion), g_mpi);
873
874 if (g_mpi.rank == 0)
875 {
876 // Chain 2 alone: cell2cell→cell2node on all 4 cells → nodes {0..8},
877 // non-owned [0,5) → ghost = {5,6,7,8}.
878 auto &ghost2 = result2.ghostIndices[EntityKind::Node];
879 std::set<DNDS::index> set2(ghost2.begin(), ghost2.end());
880 CHECK(set2 == std::set<DNDS::index>{5, 6, 7, 8});
881
882 // Union of chain1 (owned cell2node → ghost {5}) + chain2 (→ ghost {5,6,7,8}).
883 // Exact: {5,6,7,8}.
884 auto &ghostU = resultUnion.ghostIndices[EntityKind::Node];
885 std::set<DNDS::index> setU(ghostU.begin(), ghostU.end());
886 CHECK(setU == std::set<DNDS::index>{5, 6, 7, 8});
887 }
888 else if (g_mpi.rank == 1)
889 {
890 // Chain 2 alone: rank 1 owns nodes [5,9). Ghost = {0,1,2,3,4}.
891 auto &ghost2 = result2.ghostIndices[EntityKind::Node];
892 std::set<DNDS::index> set2(ghost2.begin(), ghost2.end());
893 CHECK(set2 == std::set<DNDS::index>{0, 1, 2, 3, 4});
894
895 // Union: chain1 on rank 1 owned cells {2,3} → nodes {3,4,5,6,7,8},
896 // ghost from chain1 = {3,4} (outside [5,9)).
897 // Union = {3,4} ∪ {0,1,2,3,4} = {0,1,2,3,4}.
898 auto &ghostU = resultUnion.ghostIndices[EntityKind::Node];
899 std::set<DNDS::index> setU(ghostU.begin(), ghostU.end());
900 CHECK(setU == std::set<DNDS::index>{0, 1, 2, 3, 4});
901 }
902 else
903 {
904 // Ranks 2+: no cells, no ghost nodes.
905 CHECK(resultUnion.ghostIndices[EntityKind::Node].empty());
906 }
907}
908
909TEST_CASE("evaluateGhostTree: np=1 produces no ghosts")
910{
911 if (g_mpi.size != 1)
912 return;
913
914 auto c2c = make4QuadCell2Cell(g_mpi);
915
917 dag.meshDim = 2;
918 dag.registerAdj(Adj::Cell2Cell, c2c);
919 dag.registerGlobalMapping(EntityKind::Cell, c2c.father->pLGlobalMapping);
920
921 GhostSpec spec{{{EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell}}};
922 auto result = dag.evaluateGhostTree(CompiledGhostTree::compile(spec), g_mpi);
923
924 CHECK(!result.hasGhosts(EntityKind::Cell));
925 CHECK(result.totalGhosts() == 0);
926}
927
928// ===========================================================================
929// Doubly-periodic tiled grid test
930// ===========================================================================
931
932/// Generate a doubly-periodic NxN-per-rank tiled grid.
933///
934/// Tiled rectangular grid with optional periodic wrapping in both directions.
935/// 2D rank decomposition per sqrt(np); falls back to 1×np when not factorable.
936
937TEST_CASE("evaluateGhostTree: doubly-periodic tiled grid")
938{
939 if (g_mpi.size < 2)
940 return;
941
942 DNDS::index N = 32;
944 grid.periodic = true;
945 grid.build(N, g_mpi);
946
948 dag.meshDim = 2;
951 dag.registerGlobalMapping(EntityKind::Cell, grid.cellGM);
952 dag.registerGlobalMapping(EntityKind::Node, grid.nodeGM);
953
954 // --- Cell + node ghost via defaultPrimary-style unified spec ---
955 for (int nL = 1; nL <= 4; nL++)
956 {
957 std::vector<AdjKind> cellHops(nL, Adj::Cell2Cell);
958 std::vector<AdjKind> nodeHops(nL, Adj::Cell2Cell);
959 nodeHops.push_back(Adj::Cell2Node);
960
961 GhostSpec spec{{
962 {EntityKind::Cell, cellHops, EntityKind::Cell},
963 {EntityKind::Cell, nodeHops, EntityKind::Node},
964 }};
965 auto result = dag.evaluateGhostTree(
966 CompiledGhostTree::compile(spec), g_mpi);
967
968 auto &gcL = result.ghostIndices[EntityKind::Cell];
969 auto &gnL = result.ghostIndices[EntityKind::Node];
970 CHECK(gcL.size() == grid.expectedGhostCellsN(nL));
971 CHECK(gnL.size() == grid.expectedGhostNodesN(nL));
972
973 const char *pos = (grid.rankCol == 0 || grid.rankCol == grid.ranksPerRow - 1 ||
974 grid.rankRow == 0 || grid.rankRow == grid.ranksPerCol - 1)
975 ? "boundary"
976 : "inner";
977 fmt::print(" [{}] {} Periodic N={}: nLayers={}: cell_ghost={} (expected={}), "
978 "node_ghost={} (expected={})\n",
979 g_mpi.rank, pos, N, nL,
980 gcL.size(), grid.expectedGhostCellsN(nL),
981 gnL.size(), grid.expectedGhostNodesN(nL));
982 }
983
984 // --- n-layer cell ghost ---
985 for (int nL = 2; nL <= 4; nL++)
986 {
987 std::vector<AdjKind> hops(nL, Adj::Cell2Cell);
988 GhostSpec specML{{{EntityKind::Cell, hops, EntityKind::Cell}}};
989 auto resultML = dag.evaluateGhostTree(
990 CompiledGhostTree::compile(specML), g_mpi);
991 auto &gcL = resultML.ghostIndices[EntityKind::Cell];
992 CHECK(gcL.size() == grid.expectedGhostCellsN(nL));
993
994 fmt::print(" [{}] Periodic N={}: {}-layer cell ghost: size={}\n",
995 g_mpi.rank, N, nL, gcL.size());
996 }
997}
998
999TEST_CASE("evaluateGhostTree: small periodic tiled grid")
1000{
1001 if (g_mpi.size != 4 && g_mpi.size != 8)
1002 return;
1003
1004 DNDS::index N = 2;
1005 SyntheticTiledGrid grid;
1006 grid.periodic = true;
1007 grid.build(N, g_mpi);
1008
1009 MeshConnectivity dag;
1010 dag.meshDim = 2;
1013 dag.registerGlobalMapping(EntityKind::Cell, grid.cellGM);
1014 dag.registerGlobalMapping(EntityKind::Node, grid.nodeGM);
1015
1016 // 1-hop cell ghost.
1017 GhostSpec spec1{{{EntityKind::Cell, {Adj::Cell2Cell}, EntityKind::Cell}}};
1018 auto result1 = dag.evaluateGhostTree(CompiledGhostTree::compile(spec1), g_mpi);
1019 auto &ghostCells = result1.ghostIndices[EntityKind::Cell];
1020 DNDS::index expected1 = grid.expectedGhostCellsN(1);
1021 CHECK(ghostCells.size() == expected1);
1022
1023 auto expectedCells = grid.expectedGhostCells(g_mpi);
1024 CHECK(ghostCells.size() == expectedCells.size());
1025
1026 // 2-hop cell ghost: check via brute-force expected set.
1027 {
1028 GhostSpec spec2{{{EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Cell}, EntityKind::Cell}}};
1029 auto result2 = dag.evaluateGhostTree(CompiledGhostTree::compile(spec2), g_mpi);
1030 auto &gc2 = result2.ghostIndices[EntityKind::Cell];
1031 CHECK(gc2.size() == grid.expectedGhostCellsN(2));
1032 }
1033
1034 // Node ghost.
1035 GhostSpec specNodes{{
1036 {EntityKind::Cell, {Adj::Cell2Cell, Adj::Cell2Node}, EntityKind::Node},
1037 }};
1038 auto resultNodes = dag.evaluateGhostTree(CompiledGhostTree::compile(specNodes), g_mpi);
1039 auto &ghostNodes = resultNodes.ghostIndices[EntityKind::Node];
1040 CHECK(ghostNodes.size() == grid.expectedGhostNodesN(1));
1041
1042 fmt::print(" [{}] np={} Periodic N=2: 1-hop cells={} (expected={}), "
1043 "nodes={} (expected={})\n",
1044 g_mpi.rank, g_mpi.size,
1045 ghostCells.size(), expected1,
1046 ghostNodes.size(), grid.expectedGhostNodesN(1));
1047}
1048
1049// ---------------------------------------------------------------------------
1050int main(int argc, char **argv)
1051{
1052 MPI_Init(&argc, &argv);
1053 g_mpi.setWorld();
1054
1055 doctest::Context ctx;
1056 ctx.applyCommandLine(argc, argv);
1057 int res = ctx.run();
1058
1059 MPI_Finalize();
1060 return res;
1061}
Layered DAG of mesh adjacency relations with composable DSL operations.
Shared synthetic mesh builders for MeshConnectivity unit tests.
int main()
Definition dummy.cpp:3
constexpr AdjKind Bnd2Node
constexpr AdjKind Cell2Node
constexpr AdjKind Node2Cell
constexpr AdjKind Cell2Face
constexpr AdjKind Cell2CellFace
constexpr AdjKind Node2Bnd
constexpr AdjKind Cell2Cell
int entityDepth(EntityKind kind, int dim)
std::string adjKindName(const AdjKind &kind)
Format an AdjKind as a diagnostic string, e.g. "Cell2Node", "Cell2Cell(Node)".
the host side operators are provided as implemented
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
std::shared_ptr< T > ssp
Shortened alias for std::shared_ptr used pervasively in DNDSR.
Definition Defines.hpp:143
Convenience bundle of a father, son, and attached ArrayTransformer.
void TransAttach()
Bind the transformer to the current father / son pointers.
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.
TTrans trans
Ghost-communication engine bound to father and son.
Hash for AdjKind (for use in unordered containers).
static CompiledGhostTree compile(const GhostSpec &spec)
std::unordered_map< EntityKind, std::vector< index > > ghostIndices
Per EntityKind: sorted, deduplicated global indices to ghost.
static GhostSpec defaultPrimary()
One node in the compiled ghost tree.
int level
BFS depth (root = 0).
std::vector< GhostTreeNode > children
bool collect
If true, non-owned entities here become ghosts.
GhostResult evaluateGhostTree(const CompiledGhostTree &tree, const MPIInfo &mpi) const
void registerGlobalMapping(EntityKind kind, const ssp< GlobalOffsetsMapping > &gm)
Register a GlobalOffsetsMapping for an EntityKind.
void registerAdj(AdjKind kind, ssp< AdjVariant > adjPtr)
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
void setWorld()
Initialise the object to MPI_COMM_WORLD. Requires MPI_Init to have run.
Definition MPI.hpp:258
DNDS::index expectedGhostCellsN(DNDS::index nLayers) const
Analytical n-layer cell ghost count.
ssp< GlobalOffsetsMapping > cellGM
std::set< DNDS::index > expectedGhostCells(const MPIInfo &mpi) const
ssp< GlobalOffsetsMapping > nodeGM
DNDS::index nodeGlobal(DNDS::index r, DNDS::index c) const
std::vector< DNDS::index > ownerNodeOffsets
std::set< DNDS::index > expectedGhostNodesFromOwnedCells(const MPIInfo &mpi) const
void build(DNDS::index tileN, const MPIInfo &mpi)
DNDS::index cellGlobal(DNDS::index rank, DNDS::index localRow, DNDS::index localCol) const
DNDS::index cellOwnerRank(DNDS::index gr, DNDS::index gc) const
DNDS::index expectedGhostNodesN(DNDS::index nLayers) const
constexpr DNDS::index N
constexpr DNDS::index nLocal
tVec r(NCells)
tVec b(NCells)
CHECK(result.size()==3)
auto result
std::vector< DNDS::index > ghostNodes(ghostNodeSet.begin(), ghostNodeSet.end())
std::unordered_set< DNDS::index > ghostNodeSet
TEST_CASE("evaluateGhostTree: np=1 produces no ghosts")
std::vector< GhostCell > ghostCells
REQUIRE(bool(result.parent2entityPbi.father))
DNDS::index cellOffset
DNDS::index nodeOffset
Eigen::Vector3d n(1.0, 0.0, 0.0)