DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_MeshConnectivity_Interpolate.cpp
Go to the documentation of this file.
1/**
2 * @file test_MeshConnectivity_Interpolate.cpp
3 * @brief Unit tests for MeshConnectivity Interpolate (local) and InterpolateGlobal (distributed).
4 *
5 * Tests cover face and edge extraction from 2D/3D meshes, periodic dedup with
6 * pbi collaborating check, and distributed interpolation with ownership resolution.
7 */
8
9#define DOCTEST_CONFIG_IMPLEMENT
10#include "doctest.h"
11
14#include "Geom/Mesh/Mesh.hpp"
15
16using namespace DNDS;
17using namespace DNDS::Geom;
18
19static MPIInfo g_mpi;
20
21// ===========================================================================
22// Local Interpolate tests
23// ===========================================================================
24
25// ---------------------------------------------------------------------------
26
27TEST_CASE("Interpolate: 4-quad faces (2D)")
28{
29 // Only run on rank 0 (serial test, other ranks have 0 cells)
30 if (g_mpi.rank != 0 && g_mpi.size > 1)
31 return;
32
33 // 6---7---8
34 // | | |
35 // | 2 | 3 |
36 // | | |
37 // 3---4---5
38 // | | |
39 // | 0 | 1 |
40 // | | |
41 // 0---1---2
42 //
43 // Quad4 face table: {0,1}, {1,2}, {2,3}, {3,0}
44
45 auto m = makeHandCraftedMesh(g_mpi,
46 {
47 {Elem::Quad4, {0, 1, 4, 3}},
48 {Elem::Quad4, {1, 2, 5, 4}},
49 {Elem::Quad4, {3, 4, 7, 6}},
50 {Elem::Quad4, {4, 5, 8, 7}},
51 }, 9);
52
53 auto query = makeFaceQuery(m.cellElemInfo);
54 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 4, 9, g_mpi);
55
56 // 4 quads × 4 edges = 16 half-edges, with 4 internal shared edges.
57 // Unique edges: 16 - 4 = 12
58 CHECK(res.nEntities == 12);
59
60 // Each cell should reference exactly 4 entities
61 for (DNDS::index i = 0; i < 4; i++)
62 {
63 CAPTURE(i);
64 CHECK(res.parent2entity.father->RowSize(i) == 4);
65 }
66
67 // All entities should be Line2
68 for (DNDS::index i = 0; i < res.nEntities; i++)
69 {
70 CAPTURE(i);
71 CHECK(res.entityElemInfo[i].getElemType() == Elem::Line2);
72 }
73
74 // Internal edges: count entities with 2+ parents (variable-width entity2parent)
75 int nInternal = 0;
76 for (DNDS::index i = 0; i < res.nEntities; i++)
77 if (res.entity2parent.father->RowSize(i) >= 2)
78 nInternal++;
80
81 // Boundary edges: 12 - 4 = 8
82 CHECK(res.nEntities - nInternal == 8);
83
84 // Verify no duplicate vertex sets
85 std::set<std::set<DNDS::index>> allVertSets;
86 for (DNDS::index i = 0; i < res.nEntities; i++)
87 {
88 auto vs = getEntityVertexSet(res.entity2node, res.entityElemInfo, i);
89 CHECK(allVertSets.count(vs) == 0);
90 allVertSets.insert(vs);
91 }
92
93 // Verify entity2node has 2 nodes per entity (Line2)
94 for (DNDS::index i = 0; i < res.nEntities; i++)
95 {
96 CAPTURE(i);
97 CHECK(res.entity2node.father->RowSize(i) == 2);
98 }
99
100 // Verify parent2entity → entity2parent consistency
101 for (DNDS::index iCell = 0; iCell < 4; iCell++)
102 {
103 for (DNDS::rowsize j = 0; j < res.parent2entity.father->RowSize(iCell); j++)
104 {
105 DNDS::index iEnt = res.parent2entity.father->operator()(iCell, j);
106 CAPTURE(iCell);
107 CAPTURE(j);
108 CAPTURE(iEnt);
109 bool found = false;
110 for (DNDS::rowsize p = 0; p < res.entity2parent.father->RowSize(iEnt); p++)
111 if (res.entity2parent.father->operator()(iEnt, p) == iCell)
112 found = true;
113 CHECK(found);
114 }
115 }
116}
117
118// ---------------------------------------------------------------------------
119// Interpolate: 2-tri mesh (2D) — face extraction
120// ---------------------------------------------------------------------------
121
122TEST_CASE("Interpolate: 2-tri faces (2D)")
123{
124 if (g_mpi.rank != 0 && g_mpi.size > 1)
125 return;
126
127 // 2---3
128 // |\ 1|
129 // | \ |
130 // |0 \|
131 // 0---1
132 //
133 // Tri3 face table: {0,1}, {1,2}, {2,0}
134 // Cell 0: nodes {0,1,2} → faces: {0,1}, {1,2}, {2,0}
135 // Cell 1: nodes {1,3,2} → faces: {1,3}, {3,2}, {2,1}
136 // Shared edge: {1,2}
137 // Total unique: 3 + 3 - 1 = 5
138
139 auto m = makeHandCraftedMesh(g_mpi,
140 {
141 {Elem::Tri3, {0, 1, 2}},
142 {Elem::Tri3, {1, 3, 2}},
143 }, 4);
144
145 auto query = makeFaceQuery(m.cellElemInfo);
146 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 2, 4, g_mpi);
147
148 CHECK(res.nEntities == 5);
149
150 // One internal edge (shared)
151 int nInternal = 0;
152 for (DNDS::index i = 0; i < res.nEntities; i++)
153 if (res.entity2parent.father->RowSize(i) >= 2)
154 nInternal++;
155 CHECK(nInternal == 1);
156
157 // The shared edge should have vertex set {1, 2}
158 for (DNDS::index i = 0; i < res.nEntities; i++)
159 {
160 if (res.entity2parent.father->RowSize(i) >= 2)
161 {
162 auto vs = getEntityVertexSet(res.entity2node, res.entityElemInfo, i);
163 CHECK(vs == std::set<DNDS::index>{1, 2});
164 }
165 }
166}
167
168// ---------------------------------------------------------------------------
169// Interpolate: single tet (3D) — face extraction
170// ---------------------------------------------------------------------------
171
172TEST_CASE("Interpolate: single tet faces (3D)")
173{
174 if (g_mpi.rank != 0 && g_mpi.size > 1)
175 return;
176
177 // Tet4: nodes {0,1,2,3}, 4 Tri3 faces
178 // face 0: {0,2,1}, face 1: {0,1,3}, face 2: {1,2,3}, face 3: {2,0,3}
179 auto m = makeHandCraftedMesh(g_mpi,
180 {
181 {Elem::Tet4, {0, 1, 2, 3}},
182 }, 4);
183
184 auto query = makeFaceQuery(m.cellElemInfo);
185 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 1, 4, g_mpi);
186
187 CHECK(res.nEntities == 4);
188
189 // All faces Tri3
190 for (DNDS::index i = 0; i < 4; i++)
191 {
192 CHECK(res.entityElemInfo[i].getElemType() == Elem::Tri3);
193 CHECK(res.entity2node.father->RowSize(i) == 3);
194 }
195
196 // All boundary (single parent)
197 for (DNDS::index i = 0; i < 4; i++)
198 CHECK(res.entity2parent.father->RowSize(i) < 2);
199
200 // Verify face vertex sets match Tet4 topology
201 std::set<std::set<DNDS::index>> expectedFaces = {
202 {0, 1, 2}, {0, 1, 3}, {1, 2, 3}, {0, 2, 3}};
203 std::set<std::set<DNDS::index>> actualFaces;
204 for (DNDS::index i = 0; i < 4; i++)
205 actualFaces.insert(getEntityVertexSet(res.entity2node, res.entityElemInfo, i));
207}
208
209// ---------------------------------------------------------------------------
210// Interpolate: two tets sharing a face (3D)
211// ---------------------------------------------------------------------------
212
213TEST_CASE("Interpolate: two tets sharing a face (3D)")
214{
215 if (g_mpi.rank != 0 && g_mpi.size > 1)
216 return;
217
218 // Tet A: {0,1,2,3} Tet B: {1,2,3,4}
219 // Shared face: vertices {1,2,3}
220 // Total unique faces: 4 + 4 - 1 = 7
221 auto m = makeHandCraftedMesh(g_mpi,
222 {
223 {Elem::Tet4, {0, 1, 2, 3}},
224 {Elem::Tet4, {1, 2, 3, 4}},
225 }, 5);
226
227 auto query = makeFaceQuery(m.cellElemInfo);
228 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 2, 5, g_mpi);
229
230 CHECK(res.nEntities == 7);
231
232 int nInternal = 0;
233 for (DNDS::index i = 0; i < res.nEntities; i++)
234 if (res.entity2parent.father->RowSize(i) >= 2)
235 nInternal++;
236 CHECK(nInternal == 1);
237
238 // Shared face is {1,2,3}
239 for (DNDS::index i = 0; i < res.nEntities; i++)
240 {
241 if (res.entity2parent.father->RowSize(i) >= 2)
242 {
243 auto vs = getEntityVertexSet(res.entity2node, res.entityElemInfo, i);
244 CHECK(vs == std::set<DNDS::index>{1, 2, 3});
245 }
246 }
247}
248
249// ---------------------------------------------------------------------------
250// Interpolate: single hex (3D) — face extraction
251// ---------------------------------------------------------------------------
252
253TEST_CASE("Interpolate: single hex faces (3D)")
254{
255 if (g_mpi.rank != 0 && g_mpi.size > 1)
256 return;
257
258 // Hex8: 6 Quad4 faces
259 auto m = makeHandCraftedMesh(g_mpi,
260 {
261 {Elem::Hex8, {0, 1, 2, 3, 4, 5, 6, 7}},
262 }, 8);
263
264 auto query = makeFaceQuery(m.cellElemInfo);
265 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 1, 8, g_mpi);
266
267 CHECK(res.nEntities == 6);
268
269 for (DNDS::index i = 0; i < 6; i++)
270 {
271 CHECK(res.entityElemInfo[i].getElemType() == Elem::Quad4);
272 CHECK(res.entity2node.father->RowSize(i) == 4);
273 CHECK(res.entity2parent.father->RowSize(i) < 2);
274 }
275
276 // Verify face vertex sets
277 std::set<std::set<DNDS::index>> expectedFaces = {
278 {0, 1, 2, 3}, {0, 1, 4, 5}, {1, 2, 5, 6},
279 {2, 3, 6, 7}, {0, 3, 4, 7}, {4, 5, 6, 7}};
280 std::set<std::set<DNDS::index>> actualFaces;
281 for (DNDS::index i = 0; i < 6; i++)
282 actualFaces.insert(getEntityVertexSet(res.entity2node, res.entityElemInfo, i));
284}
285
286// ---------------------------------------------------------------------------
287// Interpolate: edge extraction from single tet (3D)
288// ---------------------------------------------------------------------------
289
290TEST_CASE("Interpolate: single tet edges (3D)")
291{
292 if (g_mpi.rank != 0 && g_mpi.size > 1)
293 return;
294
295 // Tet4: 6 Line2 edges
296 auto m = makeHandCraftedMesh(g_mpi,
297 {
298 {Elem::Tet4, {0, 1, 2, 3}},
299 }, 4);
300
301 auto query = makeEdgeQuery(m.cellElemInfo);
302 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 1, 4, g_mpi);
303
304 CHECK(res.nEntities == 6);
305
306 for (DNDS::index i = 0; i < 6; i++)
307 {
308 CHECK(res.entityElemInfo[i].getElemType() == Elem::Line2);
309 CHECK(res.entity2node.father->RowSize(i) == 2);
310 }
311
312 // Tet4 edges: {0,1},{1,2},{2,0},{0,3},{1,3},{2,3}
313 std::set<std::set<DNDS::index>> expectedEdges = {
314 {0, 1}, {1, 2}, {0, 2}, {0, 3}, {1, 3}, {2, 3}};
315 std::set<std::set<DNDS::index>> actualEdges;
316 for (DNDS::index i = 0; i < 6; i++)
317 {
318 std::set<DNDS::index> vs;
319 for (DNDS::rowsize j = 0; j < 2; j++)
320 vs.insert(res.entity2node.father->operator()(i, j));
321 actualEdges.insert(vs);
322 }
324}
325
326// ---------------------------------------------------------------------------
327// Interpolate: edge deduplication across two tets (3D)
328// ---------------------------------------------------------------------------
329
330TEST_CASE("Interpolate: two-tet shared edges (3D)")
331{
332 if (g_mpi.rank != 0 && g_mpi.size > 1)
333 return;
334
335 // Tet A: {0,1,2,3}, Tet B: {1,2,3,4}
336 // Tet A edges: {0,1},{1,2},{2,0},{0,3},{1,3},{2,3}
337 // Tet B edges: {1,2},{2,3},{3,1},{1,4},{2,4},{3,4}
338 // Shared: {1,2},{1,3},{2,3} -> 3 shared
339 // Unique: 6 + 6 - 3 = 9
340 auto m = makeHandCraftedMesh(g_mpi,
341 {
342 {Elem::Tet4, {0, 1, 2, 3}},
343 {Elem::Tet4, {1, 2, 3, 4}},
344 }, 5);
345
346 auto query = makeEdgeQuery(m.cellElemInfo);
347 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 2, 5, g_mpi);
348
349 CHECK(res.nEntities == 9);
350
351 int nShared = 0;
352 for (DNDS::index i = 0; i < res.nEntities; i++)
353 if (res.entity2parent.father->RowSize(i) >= 2)
354 nShared++;
356
357 // Shared edges: {1,2}, {1,3}, {2,3}
358 std::set<std::set<DNDS::index>> expectedShared = {{1, 2}, {1, 3}, {2, 3}};
359 std::set<std::set<DNDS::index>> actualShared;
360 for (DNDS::index i = 0; i < res.nEntities; i++)
361 {
362 if (res.entity2parent.father->RowSize(i) >= 2)
363 {
364 std::set<DNDS::index> vs;
365 for (DNDS::rowsize j = 0; j < 2; j++)
366 vs.insert(res.entity2node.father->operator()(i, j));
367 actualShared.insert(vs);
368 }
369 }
371}
372
373// ---------------------------------------------------------------------------
374// Interpolate: mixed 2D mesh (tri + quad)
375// ---------------------------------------------------------------------------
376
377TEST_CASE("Interpolate: mixed tri+quad faces (2D)")
378{
379 if (g_mpi.rank != 0 && g_mpi.size > 1)
380 return;
381
382 // 3---4---5
383 // |\ 1| 2 |
384 // | \ | |
385 // |0 \| |
386 // 0---1---2
387 //
388 // Cell 0: Tri3 {0,1,3} → 3 Line2 faces
389 // Cell 1: Tri3 {1,4,3} → 3 Line2 faces
390 // Cell 2: Quad4 {1,2,5,4} → 4 Line2 faces
391 // Shared edges: {0,1}? No. {1,3} between cell 0 & 1, {1,4} between cell 1 & 2
392 // Cell 0 faces: {0,1}, {1,3}, {3,0}
393 // Cell 1 faces: {1,4}, {4,3}, {3,1}
394 // Cell 2 faces: {1,2}, {2,5}, {5,4}, {4,1}
395 // Shared: {1,3} (cells 0&1), {1,4} (cells 1&2)
396 // Total: 3 + 3 + 4 - 2 = 8
397
398 auto m = makeHandCraftedMesh(g_mpi,
399 {
400 {Elem::Tri3, {0, 1, 3}},
401 {Elem::Tri3, {1, 4, 3}},
402 {Elem::Quad4, {1, 2, 5, 4}},
403 }, 6);
404
405 auto query = makeFaceQuery(m.cellElemInfo);
406 auto res = MeshConnectivity::Interpolate(m.cell2node, query, 3, 6, g_mpi);
407
408 CHECK(res.nEntities == 8);
409
410 int nInternal = 0;
411 for (DNDS::index i = 0; i < res.nEntities; i++)
412 if (res.entity2parent.father->RowSize(i) >= 2)
413 nInternal++;
414 CHECK(nInternal == 2);
415
416 // All should be Line2
417 for (DNDS::index i = 0; i < res.nEntities; i++)
418 CHECK(res.entityElemInfo[i].getElemType() == Elem::Line2);
419}
420
421// ===========================================================================
422// Regression: Interpolate vs legacy InterpolateFace
423// ===========================================================================
424
425TEST_CASE("Regression: Interpolate matches InterpolateFace on UniformSquare_10")
426{
427 // Build mesh via legacy pipeline all the way through InterpolateFace
428 auto mesh = std::make_shared<UnstructuredMesh>(g_mpi, 2);
430 reader.ReadFromCGNSSerial(meshPath("UniformSquare_10.cgns"));
431 reader.Deduplicate1to1Periodic(1e-8);
432 reader.BuildCell2Cell();
433
435 pOpt.metisType = "KWAY";
436 pOpt.metisUfactor = 30;
437 pOpt.metisSeed = 42;
438 pOpt.metisNcuts = 1;
439 reader.MeshPartitionCell2Cell(pOpt);
441
442 mesh->RecoverNode2CellAndNode2Bnd();
443 mesh->RecoverCell2CellAndBnd2Cell();
444 mesh->BuildGhostPrimary();
445
446 mesh->AdjGlobal2LocalPrimary();
447 mesh->AdjGlobal2LocalN2CB();
448
449 // Now InterpolateFace requires Adj_PointToLocal state
450 mesh->InterpolateFace();
451
452 // Snapshot after InterpolateFace: indices are local
454 DNDS::index nCellGhost = mesh->cell2node.son ? mesh->cell2node.son->Size() : 0;
455 DNDS::index nCellAll = nCellLocal + nCellGhost;
456 DNDS::index nNodeAll = mesh->coords.father->Size() +
457 (mesh->coords.son ? mesh->coords.son->Size() : 0);
458
459 // DSL: run Interpolate on all cells (local + ghost), using local node indices
460 auto query = makeFaceQuery(mesh->cellElemInfo);
462 mesh->cell2node, query, nCellAll, nNodeAll, g_mpi);
463
464 // Compare: for each local cell, the set of face vertex sets from DSL should
465 // match the legacy cell2face → face2node vertex sets.
466 // Note: the DSL operates on ALL cells (local+ghost) without ownership filtering,
467 // so DSL may produce more faces than legacy (which discards ghost-only faces).
468 // But for each local cell, all its faces must exist in both and match.
469
470 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
471 {
472 CAPTURE(iCell);
473 auto eCell = Elem::Element{mesh->cellElemInfo[iCell]->getElemType()};
474 int nFaces = eCell.GetNumFaces();
475
476 // Legacy: collect face vertex sets from cell2face
477 std::set<std::set<DNDS::index>> legacyFaceSets;
478 for (int j = 0; j < nFaces; j++)
479 {
480 DNDS::index iFace = mesh->cell2face[iCell][j];
481 auto eFace = eCell.ObtainFace(j);
482 int nVerts = eFace.GetNumVertices();
483 std::set<DNDS::index> vs;
484 for (int k = 0; k < nVerts; k++)
485 vs.insert(mesh->face2node[iFace][k]);
486 legacyFaceSets.insert(vs);
487 }
488
489 // DSL: collect face vertex sets from parent2entity
490 std::set<std::set<DNDS::index>> dslFaceSets;
491 for (DNDS::rowsize j = 0; j < res.parent2entity.father->RowSize(iCell); j++)
492 {
493 DNDS::index iEnt = res.parent2entity.father->operator()(iCell, j);
494 auto eEnt = Elem::Element{res.entityElemInfo[iEnt].getElemType()};
495 int nVerts = eEnt.GetNumVertices();
496 std::set<DNDS::index> vs;
497 for (int k = 0; k < nVerts; k++)
498 vs.insert(res.entity2node.father->operator()(iEnt, k));
499 dslFaceSets.insert(vs);
500 }
501
502 CHECK(dslFaceSets == legacyFaceSets);
503 }
504}
505
506// ===========================================================================
507// Periodic Interpolate: 2×2 doubly-periodic quad mesh
508// ===========================================================================
509
510/// Build a 2×2 quad mesh on [0,2]×[0,2], doubly-periodic (X period 2, Y period 2).
511///
512/// After periodic deduplication, 9 original nodes collapse to 4:
513///
514/// Original layout: After dedup (4 nodes):
515/// 6---7---8 0=(0,0) 1=(1,0)
516/// | 2 | 3 | 2=(0,1) 3=(1,1)
517/// 3---4---5
518/// | 0 | 1 | Right col → left col + P1
519/// 0---1---2 Top row → bottom row + P2
520/// Corner 8 → 0 + P1|P2
521///
522/// cell2node (deduped):
523
524// ===========================================================================
525// Periodic Interpolate tests
526// ===========================================================================
527
528TEST_CASE("Interpolate: 2x2 periodic quad mesh — collaborating check required")
529{
530 if (g_mpi.rank != 0 && g_mpi.size > 1)
531 return;
532
533 auto pm = makePeriodic2x2Mesh(g_mpi);
534
535 // --- Without collaborating check: expect failure ---
536 {
537 auto faceQueryNoPbi = makeFaceQuery(pm.cellElemInfo);
538 auto resNoPbi = MeshConnectivity::Interpolate(
539 pm.cell2node, faceQueryNoPbi, 4, 4, g_mpi);
540
541 // Without the collaborating check, cells sharing all 4 nodes through
542 // different periodic images cause false face merges (3+ parents).
543 // With variable-width entity2parent, the extra parents are appended
544 // instead of overflowing. Check that at least one entity has >2 parents.
545 {
546 bool hasMultiParent = false;
547 for (DNDS::index i = 0; i < resNoPbi.nEntities; i++)
548 if (resNoPbi.entity2parent.father->RowSize(i) > 2)
549 hasMultiParent = true;
550 CHECK(hasMultiParent == true);
551 }
552 }
553
554 // --- With collaborating check: expect correct result ---
555 SubEntityQuery faceQueryPbi = makeFaceQuery(pm.cellElemInfo);
556 faceQueryPbi.matchExtra =
557 [&pm](DNDS::index iParent, int iSub,
558 DNDS::index /*iCandEntity*/,
559 DNDS::index candidateParent, int candidateSub) -> bool
560 {
561 auto eParentA = Elem::Element{pm.cellElemInfo[iParent]->getElemType()};
562 auto eFace = eParentA.ObtainFace(iSub);
563 int nFN = eFace.GetNumNodes();
564
565 std::vector<DNDS::index> nodesA(nFN), nodesB(nFN);
566 eParentA.ExtractFaceNodes(iSub, pm.cell2node[iParent], nodesA);
567 auto eParentB = Elem::Element{pm.cellElemInfo[candidateParent]->getElemType()};
568 eParentB.ExtractFaceNodes(candidateSub, pm.cell2node[candidateParent], nodesB);
569
570 std::vector<NodePeriodicBits> pbiA(nFN), pbiB(nFN);
571 eParentA.ExtractFaceNodes(iSub, pm.cell2nodePbi[iParent], pbiA);
572 eParentB.ExtractFaceNodes(candidateSub, pm.cell2nodePbi[candidateParent], pbiB);
573
574 using P = std::pair<DNDS::index, NodePeriodicBits>;
575 auto cmp = [](const P &L, const P &R)
576 { return L.first == R.first ? uint8_t(L.second) < uint8_t(R.second)
577 : L.first < R.first; };
578
579 std::vector<P> pA(nFN), pB(nFN);
580 for (int i = 0; i < nFN; i++)
581 {
582 pA[i] = {nodesA[i], pbiA[i]};
583 pB[i] = {nodesB[i], pbiB[i]};
584 }
585 std::sort(pA.begin(), pA.end(), cmp);
586 std::sort(pB.begin(), pB.end(), cmp);
587
588 auto v0 = pA[0].second ^ pB[0].second;
589 for (int i = 1; i < nFN; i++)
590 if ((pA[i].second ^ pB[i].second) != v0)
591 return false;
592 return true;
593 };
594
595 auto resPbi = MeshConnectivity::Interpolate(
596 pm.cell2node, faceQueryPbi, 4, 4, g_mpi);
597
598 // With collaborating check: expect exactly 8 unique faces
599 CHECK(resPbi.nEntities == 8);
600
601 // All faces should be internal (both parents set)
602 int nInternal = 0;
603 for (DNDS::index i = 0; i < resPbi.nEntities; i++)
604 if (resPbi.entity2parent.father->RowSize(i) >= 2)
605 nInternal++;
606 CHECK(nInternal == 8);
607
608 // Each cell should reference exactly 4 faces
609 for (DNDS::index i = 0; i < 4; i++)
610 {
611 CAPTURE(i);
612 CHECK(resPbi.parent2entity.father->RowSize(i) == 4);
613 }
614
615 // Verify parent2entity → entity2parent consistency
616 for (DNDS::index iCell = 0; iCell < 4; iCell++)
617 {
618 for (DNDS::rowsize j = 0; j < resPbi.parent2entity.father->RowSize(iCell); j++)
619 {
620 DNDS::index iEnt = resPbi.parent2entity.father->operator()(iCell, j);
621 CAPTURE(iCell); CAPTURE(j); CAPTURE(iEnt);
622 bool found = false;
623 for (DNDS::rowsize p = 0; p < resPbi.entity2parent.father->RowSize(iEnt); p++)
624 if (resPbi.entity2parent.father->operator()(iEnt, p) == iCell)
625 found = true;
626 CHECK(found);
627 }
628 }
629
630 // Verify no face connects a cell to itself
631 for (DNDS::index i = 0; i < resPbi.nEntities; i++)
632 {
633 CAPTURE(i);
634 for (DNDS::rowsize p = 0; p < resPbi.entity2parent.father->RowSize(i); p++)
635 for (DNDS::rowsize q = p + 1; q < resPbi.entity2parent.father->RowSize(i); q++)
636 CHECK(resPbi.entity2parent.father->operator()(i, p) !=
637 resPbi.entity2parent.father->operator()(i, q));
638 }
639
640 // Verify each cell-pair shares exactly the right number of faces:
641 // Cell 0-1: 2 faces, Cell 0-2: 2 faces, Cell 1-3: 2 faces, Cell 2-3: 2 faces
642 // Cell 0-3: 0 faces (diagonal, no shared face), Cell 1-2: 0 faces (diagonal)
643 std::map<std::set<DNDS::index>, int> pairCount;
644 for (DNDS::index i = 0; i < resPbi.nEntities; i++)
645 {
646 REQUIRE(resPbi.entity2parent.father->RowSize(i) >= 2);
647 auto p0 = resPbi.entity2parent.father->operator()(i, 0);
648 auto p1 = resPbi.entity2parent.father->operator()(i, 1);
649 pairCount[{p0, p1}]++;
650 }
651 CHECK(pairCount[{0, 1}] == 2);
652 CHECK(pairCount[{0, 2}] == 2);
653 CHECK(pairCount[{1, 3}] == 2);
654 CHECK(pairCount[{2, 3}] == 2);
655 CHECK(pairCount.count({0, 3}) == 0);
656 CHECK(pairCount.count({1, 2}) == 0);
657}
658
659// ===========================================================================
660// 2×2×2 triply-periodic hex mesh
661// ===========================================================================
662
663/// Build a 2×2×2 Hex8 mesh on [0,2]³, triply-periodic (P1=X, P2=Y, P3=Z).
664///
665/// After periodic dedup: 27 nodes → 8 nodes, 8 cells.
666/// All 8 cells reference all 8 nodes (with varying pbi).
667
668TEST_CASE("Periodic 2x2x2: face interpolation (3D, collaborating check)")
669{
670 if (g_mpi.rank != 0 && g_mpi.size > 1)
671 return;
672
673 auto pm = makePeriodic2x2x2Mesh(g_mpi);
674
675 // Without collaborating check: expect multi-parent entities from false merges
676 {
677 auto q = makeFaceQuery(pm.cellElemInfo);
678 auto res = MeshConnectivity::Interpolate(pm.cell2node, q, 8, 8, g_mpi);
679 // With variable-width entity2parent, extra parents are appended.
680 // Check that at least one entity has >2 parents.
681 bool hasMultiParent = false;
682 for (DNDS::index i = 0; i < res.nEntities; i++)
683 if (res.entity2parent.father->RowSize(i) > 2)
684 hasMultiParent = true;
686 }
687
688 // With collaborating check: expect 24 faces, all internal
689 {
690 auto q = makeFaceQuery(pm.cellElemInfo);
691 q.matchExtra = makePeriodicMatchExtra(pm.cellElemInfo, pm.cell2node, pm.cell2nodePbi, false);
692 auto res = MeshConnectivity::Interpolate(pm.cell2node, q, 8, 8, g_mpi);
693
694 CHECK(res.nEntities == 24);
695
696 // All faces internal (both parents set)
697 int nInternal = 0;
698 for (DNDS::index i = 0; i < res.nEntities; i++)
699 if (res.entity2parent.father->RowSize(i) >= 2)
700 nInternal++;
701 CHECK(nInternal == 24);
702
703 // No entity should have >2 parents (collaborating check prevents false merges)
704 for (DNDS::index i = 0; i < res.nEntities; i++)
705 CHECK(res.entity2parent.father->RowSize(i) <= 2);
706
707 // Each cell references 6 faces
708 for (DNDS::index i = 0; i < 8; i++)
709 {
710 CAPTURE(i);
711 CHECK(res.parent2entity.father->RowSize(i) == 6);
712 }
713
714 // All faces are Quad4
715 for (DNDS::index i = 0; i < res.nEntities; i++)
716 CHECK(res.entityElemInfo[i].getElemType() == Elem::Quad4);
717
718 // No face connects a cell to itself
719 for (DNDS::index i = 0; i < res.nEntities; i++)
720 {
721 REQUIRE(res.entity2parent.father->RowSize(i) >= 2);
722 auto p0 = res.entity2parent.father->operator()(i, 0);
723 auto p1 = res.entity2parent.father->operator()(i, 1);
724 CHECK(p0 != p1);
725 }
726
727 // Each cell has exactly 3 face-neighbors (X±, Y±, Z± — but periodic
728 // means X- wraps to X+, so each cell is face-adjacent to 6 cells?
729 // No: 2×2×2 periodic hex, each cell has 6 faces, each face is shared
730 // with exactly 1 other cell. Each cell has at most 6 neighbors, but
731 // some neighbors repeat (e.g., cell 0's +X neighbor is cell 1,
732 // cell 0's -X neighbor is also cell 1 via periodicity).
733 // Actually: cell 0 at (0,0,0), +X neighbor = cell 1 at (1,0,0),
734 // -X neighbor = cell 1 via X-periodic wrap. So cell 0 and cell 1
735 // share 2 faces (both X-faces). Similarly Y→cell2, Z→cell4.
736 // So each cell has 3 distinct face-neighbors, each sharing 2 faces.
737 std::map<DNDS::index, std::set<DNDS::index>> cellFN;
738 for (DNDS::index i = 0; i < res.nEntities; i++)
739 {
740 REQUIRE(res.entity2parent.father->RowSize(i) >= 2);
741 auto p0 = res.entity2parent.father->operator()(i, 0);
742 auto p1 = res.entity2parent.father->operator()(i, 1);
743 cellFN[p0].insert(p1);
744 cellFN[p1].insert(p0);
745 }
746 for (DNDS::index i = 0; i < 8; i++)
747 {
748 CAPTURE(i);
749 CHECK(cellFN[i].size() == 3);
750 }
751 }
752}
753
754TEST_CASE("Periodic 2x2x2: edge interpolation (3D, collaborating check)")
755{
756 if (g_mpi.rank != 0 && g_mpi.size > 1)
757 return;
758
759 auto pm = makePeriodic2x2x2Mesh(g_mpi);
760
761 // Without collaborating check: expect multi-parent entities from false merges
762 {
763 auto q = makeEdgeQuery(pm.cellElemInfo);
764 auto res = MeshConnectivity::Interpolate(pm.cell2node, q, 8, 8, g_mpi);
765 // With variable-width entity2parent, extra parents are appended.
766 // Check that at least one entity has >2 parents (false merges without pbi check).
767 bool hasMultiParent = false;
768 for (DNDS::index i = 0; i < res.nEntities; i++)
769 if (res.entity2parent.father->RowSize(i) > 2)
770 hasMultiParent = true;
771 CHECK(hasMultiParent == true);
772 }
773
774 // With collaborating check: expect 24 edges, all shared by 4 cells
775 {
776 auto q = makeEdgeQuery(pm.cellElemInfo);
777 q.matchExtra = makePeriodicMatchExtra(pm.cellElemInfo, pm.cell2node, pm.cell2nodePbi, true);
778 auto res = MeshConnectivity::Interpolate(pm.cell2node, q, 8, 8, g_mpi);
779
780 CHECK(res.nEntities == 24);
781
782 // All edges are Line2
783 for (DNDS::index i = 0; i < res.nEntities; i++)
784 CHECK(res.entityElemInfo[i].getElemType() == Elem::Line2);
785
786 // Each cell references 12 edges
787 for (DNDS::index i = 0; i < 8; i++)
788 {
789 CAPTURE(i);
790 CHECK(res.parent2entity.father->RowSize(i) == 12);
791 }
792
793 // With variable-width entity2parent, edges shared by 4 cells now store
794 // all 4 parents. Verify via entity2parent RowSize.
795 for (DNDS::index i = 0; i < res.nEntities; i++)
796 {
797 CAPTURE(i);
798 CHECK(res.entity2parent.father->RowSize(i) == 4);
799 }
800
801 // Also verify via parent2entity reference counting.
802 std::vector<int> edgeRefCount(res.nEntities, 0);
803 for (DNDS::index iCell = 0; iCell < 8; iCell++)
804 for (DNDS::rowsize j = 0; j < res.parent2entity.father->RowSize(iCell); j++)
805 edgeRefCount[res.parent2entity.father->operator()(iCell, j)]++;
806
807 // Each edge should be referenced by exactly 4 cells
808 for (DNDS::index i = 0; i < res.nEntities; i++)
809 {
810 CAPTURE(i);
811 CHECK(edgeRefCount[i] == 4);
812 }
813
814 // Euler characteristic: V - E + F = 8 - 24 + 24 = 8
815 // (For the cell complex: V - E + F - C = 8 - 24 + 24 - 8 = 0 for T³)
816 }
817}
818
819// ===========================================================================
820// InterpolateGlobal distributed tests
821// ===========================================================================
822
823TEST_CASE("InterpolateGlobal: 4x4x4 distributed hex faces (non-periodic)")
824{
826 mesh.build(4, false, g_mpi);
831
832 auto faceQuery = makeHex8FaceQueryPbi(mesh.cellElemInfo);
833
834 // Ownership: min parent rank wins.
836 [&](const std::vector<DNDS::index> &parents,
837 const std::vector<DNDS::MPI_int> &parentRanks,
839 {
840 DNDS::MPI_int minRank = *std::min_element(parentRanks.begin(), parentRanks.end());
841 bool anyLocal = false;
842 for (auto p : parents)
843 if (p < nLocal)
844 anyLocal = true;
845 if (!anyLocal)
846 return {false, {}};
847 if (minRank != g_mpi.rank)
848 return {false, {}};
849 std::vector<DNDS::MPI_int> peers;
850 for (size_t i = 0; i < parents.size(); i++)
851 if (parents[i] >= nLocal && parentRanks[i] != g_mpi.rank)
852 peers.push_back(parentRanks[i]);
853 std::sort(peers.begin(), peers.end());
854 peers.erase(std::unique(peers.begin(), peers.end()), peers.end());
855 return {true, std::move(peers)};
856 };
857
863 resolver, g_mpi);
864
865 // Check global face count: sum of owned faces across all ranks.
866 DNDS::index localOwned = result.nOwnedEntities;
868 MPI_Allreduce(&localOwned, &globalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
869
870 // Expected: 3 * (np*N) * N * N + 3 * N * N = ... let me compute:
871 // X-normal faces: (np*N + 1) * N * N
872 // Y-normal faces: np * N * (N + 1) * N
873 // Z-normal faces: np * N * N * (N + 1)
874 DNDS::index expectedFaces = (np * N + 1) * N * N +
875 np * N * (N + 1) * N +
876 np * N * N * (N + 1);
878
879 // Verify entity2node: all face nodes are Quad4 (4 nodes each).
880 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
881 {
882 CHECK(result.entity2node.father->RowSize(i) == 4);
883 CHECK(result.entityElemInfo.father->operator()(i, 0).getElemType() == Elem::Quad4);
884 }
885
886 // Verify entity2parent: boundary faces have 1 parent, internal have 2.
888 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
889 {
890 auto pRow = result.entity2parent.father->operator[](i);
891 if (pRow.size() == 1)
892 nBndFaces++;
893 else if (pRow.size() == 2)
894 nIntFaces++;
895 else
896 FAIL("face has " << pRow.size() << " parents");
897 }
898
900 MPI_Allreduce(&nBndFaces, &globalBnd, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
901 MPI_Allreduce(&nIntFaces, &globalInt, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
902 // Boundary faces: 2 * N * N (X-boundaries) + 2 * np*N * N (Y-boundaries) + 2 * np*N * N (Z-boundaries)
903 DNDS::index expectedBnd = 2 * N * N + 2 * np * N * N + 2 * np * N * N;
906
907 // Verify parent2entity: each local cell should have 6 faces.
908 for (DNDS::index iCell = 0; iCell < nLocalCells; iCell++)
909 {
910 CAPTURE(iCell);
911 CHECK(result.parent2entity.father->RowSize(iCell) == 6);
912 // All entries should be valid global face IDs (not UnInitIndex).
913 for (rowsize j = 0; j < 6; j++)
914 {
915 DNDS::index gFace = result.parent2entity.father->operator()(iCell, j);
916 CAPTURE(j);
917 CHECK(gFace != UnInitIndex);
918 CHECK(gFace >= 0);
919 CHECK(gFace < globalOwned);
920 }
921 }
922}
923
924TEST_CASE("InterpolateGlobal: 4x4x4 distributed hex faces (X-periodic)")
925{
926 if (g_mpi.size < 2)
927 return; // Periodic X needs at least 2 ranks to be meaningful.
928
930 mesh.build(4, true, g_mpi);
931 DNDS::index N = 4;
932 DNDS::index np = g_mpi.size;
935
936 auto faceQuery = makeHex8FaceQueryPbi(mesh.cellElemInfo);
937 // No pbi for now (X-periodic without pbi = just wrapping cell connectivity).
938 // For a proper periodic test we'd need cell2nodePbi, but the key test is
939 // that face dedup works across the periodic boundary.
940
942 [&](const std::vector<DNDS::index> &parents,
943 const std::vector<DNDS::MPI_int> &parentRanks,
945 {
946 DNDS::MPI_int minRank = *std::min_element(parentRanks.begin(), parentRanks.end());
947 bool anyLocal = false;
948 for (auto p : parents)
949 if (p < nLocal)
950 anyLocal = true;
951 if (!anyLocal)
952 return {false, {}};
953 if (minRank != g_mpi.rank)
954 return {false, {}};
955 std::vector<DNDS::MPI_int> peers;
956 for (size_t i = 0; i < parents.size(); i++)
957 if (parents[i] >= nLocal && parentRanks[i] != g_mpi.rank)
958 peers.push_back(parentRanks[i]);
959 std::sort(peers.begin(), peers.end());
960 peers.erase(std::unique(peers.begin(), peers.end()), peers.end());
961 return {true, std::move(peers)};
962 };
963
969 resolver, g_mpi);
970
971 DNDS::index localOwned = result.nOwnedEntities;
973 MPI_Allreduce(&localOwned, &globalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
974
975 // X-periodic: no X-boundary faces, all X-faces are internal.
976 // X-normal faces: np*N * N * N (no +1 boundary)
977 // Y-normal faces: np*N * (N+1) * N
978 // Z-normal faces: np*N * N * (N+1)
979 // Y and Z boundaries still exist.
980 DNDS::index expectedFaces = np * N * N * N +
981 np * N * (N + 1) * N +
982 np * N * N * (N + 1);
984
985 // All faces should have exactly 2 parents (no boundary in X, Y/Z still have boundary).
986 // Actually Y and Z boundaries have 1 parent.
988 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
989 {
990 auto pRow = result.entity2parent.father->operator[](i);
991 if (pRow.size() == 1)
992 nBndFaces++;
993 }
995 MPI_Allreduce(&nBndFaces, &globalBnd, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
996 // Only Y and Z boundaries remain: 2 * np*N * N (Y) + 2 * np*N * N (Z)
997 DNDS::index expectedBnd = 2 * np * N * N + 2 * np * N * N;
999
1000 // Each local cell still has 6 faces.
1001 for (DNDS::index iCell = 0; iCell < nLocalCells; iCell++)
1002 {
1003 CAPTURE(iCell);
1004 CHECK(result.parent2entity.father->RowSize(iCell) == 6);
1005 for (rowsize j = 0; j < 6; j++)
1006 {
1007 DNDS::index gFace = result.parent2entity.father->operator()(iCell, j);
1008 CHECK(gFace != UnInitIndex);
1009 }
1010 }
1011}
1012
1013TEST_CASE("InterpolateGlobal: 4x4x4 distributed hex edges (non-periodic)")
1014{
1016 mesh.build(4, false, g_mpi);
1017 DNDS::index N = 4;
1018 DNDS::index np = g_mpi.size;
1021
1022 auto edgeQuery = makeHex8EdgeQueryPbi(mesh.cellElemInfo);
1023
1025 [&](const std::vector<DNDS::index> &parents,
1026 const std::vector<DNDS::MPI_int> &parentRanks,
1028 {
1029 DNDS::MPI_int minRank = *std::min_element(parentRanks.begin(), parentRanks.end());
1030 bool anyLocal = false;
1031 for (auto p : parents)
1032 if (p < nLocal)
1033 anyLocal = true;
1034 if (!anyLocal)
1035 return {false, {}};
1036 if (minRank != g_mpi.rank)
1037 return {false, {}};
1038 std::vector<DNDS::MPI_int> peers;
1039 for (size_t i = 0; i < parents.size(); i++)
1040 if (parents[i] >= nLocal && parentRanks[i] != g_mpi.rank)
1041 peers.push_back(parentRanks[i]);
1042 std::sort(peers.begin(), peers.end());
1043 peers.erase(std::unique(peers.begin(), peers.end()), peers.end());
1044 return {true, std::move(peers)};
1045 };
1046
1052 resolver, g_mpi);
1053
1054 // Check global edge count.
1055 DNDS::index localOwned = result.nOwnedEntities;
1057 MPI_Allreduce(&localOwned, &globalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1058
1059 // Expected edges for non-periodic NxNxN stacked np times in X:
1060 // X-aligned: np*N * (N+1) * (N+1)
1061 // Y-aligned: (np*N+1) * N * (N+1)
1062 // Z-aligned: (np*N+1) * (N+1) * N
1063 DNDS::index expectedEdges = np * N * (N + 1) * (N + 1) +
1064 (np * N + 1) * N * (N + 1) +
1065 (np * N + 1) * (N + 1) * N;
1067
1068 // All edges should be Line2 (2 nodes each).
1069 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
1070 {
1071 CHECK(result.entity2node.father->RowSize(i) == 2);
1072 CHECK(result.entityElemInfo.father->operator()(i, 0).getElemType() == Elem::Line2);
1073 }
1074
1075 // Edge parent counts: internal edges have 4 parents, face-boundary 2, edge-boundary 1.
1076 // Just verify all edges have >= 1 parent and <= 4 parents.
1077 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
1078 {
1079 auto pRow = result.entity2parent.father->operator[](i);
1080 CAPTURE(i);
1081 CHECK(pRow.size() >= 1);
1082 CHECK(pRow.size() <= 4);
1083 }
1084
1085 // Each local cell should have 12 edges.
1086 for (DNDS::index iCell = 0; iCell < nLocalCells; iCell++)
1087 {
1088 CAPTURE(iCell);
1089 CHECK(result.parent2entity.father->RowSize(iCell) == 12);
1090 for (rowsize j = 0; j < 12; j++)
1091 {
1092 DNDS::index gEdge = result.parent2entity.father->operator()(iCell, j);
1093 CAPTURE(j);
1094 CHECK(gEdge != UnInitIndex);
1095 CHECK(gEdge >= 0);
1096 CHECK(gEdge < globalOwned);
1097 }
1098 }
1099}
1100
1101TEST_CASE("InterpolateGlobal: 4x4x4 distributed hex edges (X-periodic)")
1102{
1103 if (g_mpi.size < 2)
1104 return;
1105
1107 mesh.build(4, true, g_mpi);
1108 DNDS::index N = 4;
1109 DNDS::index np = g_mpi.size;
1112
1113 auto edgeQuery = makeHex8EdgeQueryPbi(mesh.cellElemInfo);
1114
1116 [&](const std::vector<DNDS::index> &parents,
1117 const std::vector<DNDS::MPI_int> &parentRanks,
1119 {
1120 DNDS::MPI_int minRank = *std::min_element(parentRanks.begin(), parentRanks.end());
1121 bool anyLocal = false;
1122 for (auto p : parents)
1123 if (p < nLocal)
1124 anyLocal = true;
1125 if (!anyLocal)
1126 return {false, {}};
1127 if (minRank != g_mpi.rank)
1128 return {false, {}};
1129 std::vector<DNDS::MPI_int> peers;
1130 for (size_t i = 0; i < parents.size(); i++)
1131 if (parents[i] >= nLocal && parentRanks[i] != g_mpi.rank)
1132 peers.push_back(parentRanks[i]);
1133 std::sort(peers.begin(), peers.end());
1134 peers.erase(std::unique(peers.begin(), peers.end()), peers.end());
1135 return {true, std::move(peers)};
1136 };
1137
1143 resolver, g_mpi);
1144
1145 DNDS::index localOwned = result.nOwnedEntities;
1147 MPI_Allreduce(&localOwned, &globalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1148
1149 // X-periodic: X-aligned edges wrap, no X-boundary.
1150 // X-aligned: np*N * (N+1) * (N+1) (same as non-periodic, but no +1 boundary -- wait)
1151 // Actually for non-periodic, X-aligned edges span from node X=0 to X=np*N,
1152 // so there are np*N edge segments per Y-Z grid line, times (N+1)^2 grid lines.
1153 // For X-periodic, same count: np*N edges per Y-Z grid line.
1154 // Y-aligned: np*N * N * (N+1) (no +1 in X dimension)
1155 // Z-aligned: np*N * (N+1) * N (no +1 in X dimension)
1156 DNDS::index expectedEdges = np * N * (N + 1) * (N + 1) +
1157 np * N * N * (N + 1) +
1158 np * N * (N + 1) * N;
1160
1161 // All edges should be Line2.
1162 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
1163 CHECK(result.entity2node.father->RowSize(i) == 2);
1164
1165 // X-periodic removes X-boundary edges. Internal edges in the bulk have 4 parents.
1166 // Y/Z boundary edges still have 1 or 2 parents.
1167 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
1168 {
1169 auto pRow = result.entity2parent.father->operator[](i);
1170 CHECK(pRow.size() >= 1);
1171 CHECK(pRow.size() <= 4);
1172 }
1173
1174 // Each local cell has 12 edges.
1175 for (DNDS::index iCell = 0; iCell < nLocalCells; iCell++)
1176 {
1177 CAPTURE(iCell);
1178 CHECK(result.parent2entity.father->RowSize(iCell) == 12);
1179 for (rowsize j = 0; j < 12; j++)
1180 {
1181 DNDS::index gEdge = result.parent2entity.father->operator()(iCell, j);
1182 CHECK(gEdge != UnInitIndex);
1183 }
1184 }
1185}
1186
1187TEST_CASE("InterpolateGlobal: 4x4x4 distributed hex faces (triply-periodic)")
1188{
1189 if (g_mpi.size < 2)
1190 return;
1191
1192 // Triply-periodic: X wraps across ranks, Y and Z wrap within each rank.
1193 // After periodic dedup: N^3 unique nodes per rank.
1194 // Node at (ix, iy, iz) on rank p: local index = ix*N*N + iy*N + iz.
1195 // Global index = rank_offset + local_index.
1196 // Cell (ix, iy, iz) has nodes at (ix+di, iy+dj, iz+dk) for di,dj,dk in {0,1},
1197 // with wrapping: Y mod N, Z mod N, X mod (np*N) across ranks.
1198 // Pbi: P1 set if X wraps, P2 if Y wraps, P3 if Z wraps.
1199
1200 const DNDS::index N = 4;
1201 const DNDS::index np = g_mpi.size;
1203 const DNDS::index nNodeLocal = N * N * N; // triply-periodic: N^3 unique nodes
1204
1205 auto cellGM = std::make_shared<GlobalOffsetsMapping>();
1206 cellGM->setMPIAlignBcast(g_mpi, nCellLocal);
1207 auto nodeGM = std::make_shared<GlobalOffsetsMapping>();
1208 nodeGM->setMPIAlignBcast(g_mpi, nNodeLocal);
1209
1210 DNDS::index cellOff = (*cellGM)(g_mpi.rank, 0);
1211 DNDS::index nodeOff = (*nodeGM)(g_mpi.rank, 0);
1212
1213 // Helper: global node index for physical (gx, gy, gz) with triply-periodic wrapping.
1215 {
1216 DNDS::index totalNx = np * N;
1217 gx = ((gx % totalNx) + totalNx) % totalNx;
1218 gy = ((gy % N) + N) % N;
1219 gz = ((gz % N) + N) % N;
1220 DNDS::MPI_int ownerRank = static_cast<DNDS::MPI_int>(gx / N);
1221 DNDS::index localIx = gx - ownerRank * N;
1222 return (*nodeGM)(ownerRank, 0) + localIx * N * N + gy * N + gz;
1223 };
1224
1225 // Helper: pbi for a cell-to-node reference at (gx+di, gy+dj, gz+dk).
1227 int di, int dj, int dk) -> NodePeriodicBits
1228 {
1229 NodePeriodicBits pbi{};
1230 DNDS::index totalNx = np * N;
1231 if ((gx + di) >= totalNx || (gx + di) < 0)
1232 pbi.setP1True();
1233 if ((gy + dj) >= N || (gy + dj) < 0)
1234 pbi.setP2True();
1235 if ((gz + dk) >= N || (gz + dk) < 0)
1236 pbi.setP3True();
1237 return pbi;
1238 };
1239
1240 // Build cell2node with pbi.
1242 cell2node.InitPair("c2n", g_mpi);
1245 cell2nodePbi.InitPair("c2nPbi", g_mpi);
1246 cell2nodePbi.father->Resize(nCellLocal, 8);
1248 cellElemInfo.InitPair("cInfo", g_mpi);
1250
1251 // Hex8 node order: (0,0,0),(1,0,0),(1,1,0),(0,1,0),(0,0,1),(1,0,1),(1,1,1),(0,1,1)
1252 const int di8[8] = {0, 1, 1, 0, 0, 1, 1, 0};
1253 const int dj8[8] = {0, 0, 1, 1, 0, 0, 1, 1};
1254 const int dk8[8] = {0, 0, 0, 0, 1, 1, 1, 1};
1255
1256 for (DNDS::index ix = 0; ix < N; ix++)
1257 for (DNDS::index iy = 0; iy < N; iy++)
1258 for (DNDS::index iz = 0; iz < N; iz++)
1259 {
1260 DNDS::index iCell = ix * N * N + iy * N + iz;
1261 DNDS::index gx = g_mpi.rank * N + ix;
1262 for (int k = 0; k < 8; k++)
1263 {
1264 cell2node.father->operator()(iCell, k) =
1265 nodeGlobal(gx + di8[k], iy + dj8[k], iz + dk8[k]);
1266 cell2nodePbi.father->operator()(iCell, k) =
1267 nodePbi(gx, iy, iz, di8[k], dj8[k], dk8[k]);
1268 }
1269 cellElemInfo.father->operator()(iCell, 0) = ElemInfo{
1270 static_cast<t_index>(Elem::Hex8), INTERNAL_ZONE};
1271 }
1272
1273 // Ghost cells: 1-layer from left and right neighbor ranks in X.
1274 DNDS::MPI_int leftRank = (g_mpi.rank - 1 + g_mpi.size) % g_mpi.size;
1275 DNDS::MPI_int rightRank = (g_mpi.rank + 1) % g_mpi.size;
1276
1277 struct GhostCell { DNDS::index global; std::array<DNDS::index, 8> nodes; std::array<NodePeriodicBits, 8> pbi; };
1278 std::vector<GhostCell> ghostCells;
1279
1280 auto addGhostLayer = [&](DNDS::MPI_int srcRank, DNDS::index srcIx, DNDS::index physX)
1281 {
1282 for (DNDS::index iy = 0; iy < N; iy++)
1283 for (DNDS::index iz = 0; iz < N; iz++)
1284 {
1285 GhostCell gc;
1286 gc.global = (*cellGM)(srcRank, 0) + srcIx * N * N + iy * N + iz;
1287 for (int k = 0; k < 8; k++)
1288 {
1289 gc.nodes[k] = nodeGlobal(physX + di8[k], iy + dj8[k], iz + dk8[k]);
1290 gc.pbi[k] = nodePbi(physX, iy, iz, di8[k], dj8[k], dk8[k]);
1291 }
1292 ghostCells.push_back(gc);
1293 }
1294 };
1295 addGhostLayer(leftRank, N - 1, g_mpi.rank * N - 1);
1296 addGhostLayer(rightRank, 0, (g_mpi.rank + 1) * N);
1297
1298 // Sort ghost cells by global index.
1299 std::sort(ghostCells.begin(), ghostCells.end(),
1300 [](const GhostCell &a, const GhostCell &b) { return a.global < b.global; });
1301
1302 // Collect all node globals.
1303 std::set<DNDS::index> allNodeGlobals;
1304 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1305 for (int k = 0; k < 8; k++)
1306 allNodeGlobals.insert(cell2node.father->operator()(iCell, k));
1307 for (auto &gc : ghostCells)
1308 for (auto ng : gc.nodes)
1309 allNodeGlobals.insert(ng);
1310
1311 // Ghost node mapping.
1312 std::vector<DNDS::index> ghostNodeGlobals;
1313 for (auto ng : allNodeGlobals)
1314 {
1316 if (nodeGM->search(ng, r, v) && r != g_mpi.rank)
1317 ghostNodeGlobals.push_back(ng);
1318 }
1319 tAdjPair dummyNode;
1320 dummyNode.InitPair("dn", g_mpi);
1321 dummyNode.father->Resize(nNodeLocal);
1322 dummyNode.TransAttach();
1323 dummyNode.trans.createFatherGlobalMapping();
1324 dummyNode.trans.createGhostMapping(ghostNodeGlobals);
1325 dummyNode.trans.createMPITypes();
1326 auto nodeGhostMapping = dummyNode.trans.pLGhostMapping;
1327 DNDS::index nNodeTotal = nNodeLocal + static_cast<DNDS::index>(ghostNodeGlobals.size());
1328
1329 // Build ghost cell son.
1330 std::vector<DNDS::index> ghostGlobals;
1331 for (auto &gc : ghostCells)
1332 ghostGlobals.push_back(gc.global);
1333
1334 cell2node.son = std::make_shared<tAdj::element_type>(ObjName{"c2n.son"}, g_mpi);
1335 cell2node.son->Resize(static_cast<DNDS::index>(ghostCells.size()), 8);
1336 cell2nodePbi.son = std::make_shared<decltype(cell2nodePbi.son)::element_type>(
1338 cell2nodePbi.son->Resize(static_cast<DNDS::index>(ghostCells.size()), 8);
1339 cellElemInfo.son = std::make_shared<tElemInfoArray::element_type>(ObjName{"cInfo.son"}, g_mpi);
1340 cellElemInfo.son->Resize(static_cast<DNDS::index>(ghostCells.size()));
1341
1343 cell2node.trans.createFatherGlobalMapping();
1344 cell2node.trans.createGhostMapping(ghostGlobals);
1345 cell2node.trans.createMPITypes();
1348 cell2nodePbi.trans.createMPITypes();
1350 cellElemInfo.trans.BorrowGGIndexing(cell2node.trans);
1351 cellElemInfo.trans.createMPITypes();
1352
1353 // Populate son AFTER createMPITypes.
1354 for (DNDS::index ig = 0; ig < static_cast<DNDS::index>(ghostCells.size()); ig++)
1355 {
1356 for (int k = 0; k < 8; k++)
1357 {
1358 cell2node.son->operator()(ig, k) = ghostCells[ig].nodes[k];
1359 cell2nodePbi.son->operator()(ig, k) = ghostCells[ig].pbi[k];
1360 }
1361 cellElemInfo.son->operator()(ig, 0) = ElemInfo{
1362 static_cast<t_index>(Elem::Hex8), INTERNAL_ZONE};
1363 }
1364
1365 auto cellGhostMapping = cell2node.trans.pLGhostMapping;
1366
1367 // Convert cell2node from global to local-appended node indices.
1369 for (DNDS::index iCell = 0; iCell < nCellAll; iCell++)
1370 for (rowsize k = 0; k < 8; k++)
1371 {
1372 DNDS::index &ng = cell2node(iCell, k);
1373 auto [found, r, la] = nodeGhostMapping->search_indexAppend(ng);
1375 ng = la;
1376 }
1377
1378 // Build face query with pbi collaborating check.
1379 auto faceQuery = makeHex8FaceQueryPbi(cellElemInfo);
1380 faceQuery.matchExtra = [&](DNDS::index iParent, int iSub,
1381 DNDS::index, DNDS::index candidateParent, int candidateSub) -> bool
1382 {
1383 auto eA = Elem::Element{cellElemInfo[iParent]->getElemType()};
1384 auto eB = Elem::Element{cellElemInfo[candidateParent]->getElemType()};
1385 auto fA = eA.ObtainFace(iSub);
1386 int nFN = fA.GetNumNodes();
1387 std::vector<DNDS::index> nodesA(nFN), nodesB(nFN);
1388 eA.ExtractFaceNodes(iSub, cell2node[iParent], nodesA);
1389 eB.ExtractFaceNodes(candidateSub, cell2node[candidateParent], nodesB);
1390 std::vector<NodePeriodicBits> pbiA(nFN), pbiB(nFN);
1391 eA.ExtractFaceNodes(iSub, cell2nodePbi[iParent], pbiA);
1392 eB.ExtractFaceNodes(candidateSub, cell2nodePbi[candidateParent], pbiB);
1393 using P = std::pair<DNDS::index, uint8_t>;
1394 auto cmp = [](const P &l, const P &r)
1395 { return l.first == r.first ? l.second < r.second : l.first < r.first; };
1396 std::vector<P> pa(nFN), pb(nFN);
1397 for (int i = 0; i < nFN; i++) { pa[i] = {nodesA[i], uint8_t(pbiA[i])}; pb[i] = {nodesB[i], uint8_t(pbiB[i])}; }
1398 std::sort(pa.begin(), pa.end(), cmp);
1399 std::sort(pb.begin(), pb.end(), cmp);
1400 uint8_t v0 = pa[0].second ^ pb[0].second;
1401 for (int i = 1; i < nFN; i++)
1402 if ((pa[i].second ^ pb[i].second) != v0)
1403 return false;
1404 return true;
1405 };
1406 faceQuery.extractPbi = [&](DNDS::index iP, int iSub,
1407 const std::function<NodePeriodicBits(int)> &pPbi,
1408 NodePeriodicBits *out)
1409 {
1410 auto e = Elem::Element{cellElemInfo[iP]->getElemType()};
1411 auto f = e.ObtainFace(iSub);
1412 std::vector<NodePeriodicBits> pp(e.GetNumNodes());
1413 for (int i = 0; i < e.GetNumNodes(); i++) pp[i] = pPbi(i);
1414 std::vector<NodePeriodicBits> fp(f.GetNumNodes());
1415 e.ExtractFaceNodes(iSub, pp, fp);
1416 for (int i = 0; i < f.GetNumNodes(); i++) out[i] = fp[i];
1417 };
1418
1420 [&](const std::vector<DNDS::index> &parents,
1421 const std::vector<DNDS::MPI_int> &parentRanks,
1423 {
1424 DNDS::MPI_int minRank = *std::min_element(parentRanks.begin(), parentRanks.end());
1425 bool anyLocal = false;
1426 for (auto p : parents)
1427 if (p < nLocal) anyLocal = true;
1428 if (!anyLocal) return {false, {}};
1429 if (minRank != g_mpi.rank) return {false, {}};
1430 std::vector<DNDS::MPI_int> peers;
1431 for (size_t i = 0; i < parents.size(); i++)
1432 if (parents[i] >= nLocal && parentRanks[i] != g_mpi.rank)
1433 peers.push_back(parentRanks[i]);
1434 std::sort(peers.begin(), peers.end());
1435 peers.erase(std::unique(peers.begin(), peers.end()), peers.end());
1436 return {true, std::move(peers)};
1437 };
1438
1444 resolver, g_mpi);
1445
1446 DNDS::index localOwned = result.nOwnedEntities;
1448 MPI_Allreduce(&localOwned, &globalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1449
1450 // Triply-periodic: 3 * np*N^3 faces (N^2 faces per direction per cell-layer,
1451 // np*N layers in X, N in Y, N in Z).
1452 DNDS::index expectedFaces = 3 * np * N * N * N;
1454
1455 // All faces are internal (2 parents) in triply-periodic.
1457 for (DNDS::index i = 0; i < result.nOwnedEntities; i++)
1458 if (result.entity2parent.father->RowSize(i) != 2)
1459 nBnd++;
1461 MPI_Allreduce(&nBnd, &globalNBnd, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1462 CHECK(globalNBnd == 0); // no boundary faces
1463
1464 // Each cell has 6 faces.
1465 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1466 {
1467 CAPTURE(iCell);
1468 CHECK(result.parent2entity.father->RowSize(iCell) == 6);
1469 }
1470
1471 // --- Verify face2nodePbi VALUES ---
1472 // For each owned face, the stored pbi should match the pbi extracted from
1473 // the first parent cell's perspective (entity2parent[iFace][0]).
1474 {
1475 DNDS::index nFail = 0;
1476 for (DNDS::index iFace = 0; iFace < result.nOwnedEntities; iFace++)
1477 {
1478 // Get the first parent (global A) and resolve to local-appended
1479 DNDS::index parentGlobal = result.entity2parent.father->operator()(iFace, 0);
1480 auto [pFound, pRank, parentLocal] = cellGhostMapping->search_indexAppend(parentGlobal);
1481 REQUIRE(pFound);
1482
1483 // Find which sub-entity slot of the parent maps to this face.
1484 // We need the local parent2entity, but we only have global B IDs.
1485 // Instead, recompute the face nodes and pbi from the parent, then
1486 // match by comparing global node sets.
1487
1488 // Get face's global node set
1489 auto faceNodeRow = result.entity2node.father->operator[](iFace);
1490 std::set<DNDS::index> faceNodeGlobals;
1491 for (auto gn : faceNodeRow)
1492 faceNodeGlobals.insert(gn);
1493
1494 auto eParent = Elem::Element{cellElemInfo[parentLocal]->getElemType()};
1495 int nFaces = eParent.GetNumFaces();
1496 bool matched = false;
1497 for (int iSub = 0; iSub < nFaces && !matched; iSub++)
1498 {
1499 auto eFace = eParent.ObtainFace(iSub);
1500 int nFN = eFace.GetNumNodes();
1501
1502 // Extract nodes (local-appended) and convert to global
1503 std::vector<DNDS::index> subNodes(nFN);
1504 std::vector<DNDS::index> parentNodes(8);
1505 for (int k = 0; k < 8; k++)
1506 parentNodes[k] = cell2node(parentLocal, k);
1507 eParent.ExtractFaceNodes(iSub, parentNodes, subNodes);
1508
1509 std::set<DNDS::index> subNodeGlobals;
1510 for (auto la : subNodes)
1511 subNodeGlobals.insert(nodeGhostMapping->operator()(-1, la));
1512
1513 if (subNodeGlobals != faceNodeGlobals)
1514 continue;
1515
1516 // Match found. Extract pbi from cell2nodePbi for this sub-entity.
1517 std::vector<NodePeriodicBits> parentPbiVec(8);
1518 for (int k = 0; k < 8; k++)
1519 parentPbiVec[k] = cell2nodePbi(parentLocal, k);
1520 std::vector<NodePeriodicBits> expectedPbi(nFN);
1521 eParent.ExtractFaceNodes(iSub, parentPbiVec, expectedPbi);
1522
1523 // Compare with stored entity2nodePbi
1524 REQUIRE(result.entity2nodePbi.father->RowSize(iFace) == nFN);
1525 for (int j = 0; j < nFN; j++)
1526 {
1527 auto stored = result.entity2nodePbi.father->operator()(iFace, j);
1528 if (!(stored == expectedPbi[j]))
1529 nFail++;
1530 }
1531 matched = true;
1532 }
1533 REQUIRE(matched);
1534 }
1536 MPI_Allreduce(&nFail, &globalNFail, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1538 }
1539
1540 // --- Verify parent2entityPbi VALUES (faces) ---
1541 // For each local cell and each face slot, the parent2entityPbi should be the
1542 // uniform XOR between the cell's face-node pbi and the face's stored pbi.
1543 // We verify: for each local cell iCell, sub j → face iFace:
1544 // cell's face-pbi (from cell2nodePbi) XOR face's stored pbi (entity2nodePbi)
1545 // should be uniform and equal to parent2entityPbi[iCell][j].
1546 REQUIRE(bool(result.parent2entityPbi.father));
1547 {
1548 DNDS::index nFail = 0;
1549 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1550 {
1551 auto eCell = Elem::Element{cellElemInfo[iCell]->getElemType()};
1552 for (rowsize j = 0; j < result.parent2entity.father->RowSize(iCell); j++)
1553 {
1554 DNDS::index gFace = result.parent2entity.father->operator()(iCell, j);
1555 if (gFace == UnInitIndex)
1556 continue;
1557 NodePeriodicBits storedRelPbi = result.parent2entityPbi.father->operator()(iCell, j);
1558
1559 // Find the face in owned entities to get its stored pbi.
1560 // gFace is a global face ID. We need to check if it's owned by this rank.
1561 auto faceFatherGM = result.entity2node.trans.pLGlobalMapping;
1562 DNDS::index myFaceStart = (*faceFatherGM)(g_mpi.rank, 0);
1563 DNDS::index myFaceEnd = myFaceStart + result.nOwnedEntities;
1564 if (gFace < myFaceStart || gFace >= myFaceEnd)
1565 continue; // face owned by another rank, skip (we don't have entity2nodePbi for it)
1566
1567 DNDS::index iFaceLocal = gFace - myFaceStart;
1568
1569 // Extract cell's face pbi
1570 auto eFace = eCell.ObtainFace(j);
1571 int nFN = eFace.GetNumNodes();
1572 std::vector<NodePeriodicBits> cellFacePbi(nFN);
1573 std::vector<DNDS::index> cellFaceNodes(nFN);
1574 {
1575 std::vector<NodePeriodicBits> parentPbiVec(8);
1576 std::vector<DNDS::index> parentNodes(8);
1577 for (int k = 0; k < 8; k++)
1578 {
1579 parentPbiVec[k] = cell2nodePbi(iCell, k);
1580 parentNodes[k] = cell2node(iCell, k);
1581 }
1582 eCell.ExtractFaceNodes(j, parentPbiVec, cellFacePbi);
1583 eCell.ExtractFaceNodes(j, parentNodes, cellFaceNodes);
1584 }
1585
1586 // Face stored pbi
1587 auto faceNodeRow = result.entity2node.father->operator[](iFaceLocal);
1588
1589 // Match nodes by identity (convert cell nodes to global)
1590 using NP = std::pair<DNDS::index, uint8_t>;
1591 auto cmp = [](const NP &a, const NP &b)
1592 { return a.first == b.first ? a.second < b.second : a.first < b.first; };
1593 std::vector<NP> cellNP(nFN), faceNP(nFN);
1594 for (int k = 0; k < nFN; k++)
1595 {
1596 cellNP[k] = {nodeGhostMapping->operator()(-1, cellFaceNodes[k]),
1597 uint8_t(cellFacePbi[k])};
1598 faceNP[k] = {faceNodeRow[k],
1599 uint8_t(result.entity2nodePbi.father->operator()(iFaceLocal, k))};
1600 }
1601 std::sort(cellNP.begin(), cellNP.end(), cmp);
1602 std::sort(faceNP.begin(), faceNP.end(), cmp);
1603
1604 // Compute expected uniform XOR
1605 uint8_t expectedXor = cellNP[0].second ^ faceNP[0].second;
1606 bool uniform = true;
1607 for (int k = 1; k < nFN; k++)
1608 if ((cellNP[k].second ^ faceNP[k].second) != expectedXor)
1609 uniform = false;
1610
1611 if (!uniform || !(NodePeriodicBits{expectedXor} == storedRelPbi))
1612 nFail++;
1613 }
1614 }
1616 MPI_Allreduce(&nFail, &globalNFail, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1617 CHECK(globalNFail == 0);
1618 }
1619
1620 // Similarly test edges.
1621 auto edgeQuery = makeHex8EdgeQueryPbi(cellElemInfo);
1622 edgeQuery.matchExtra = [&](DNDS::index iParent, int iSub,
1623 DNDS::index, DNDS::index candidateParent, int candidateSub) -> bool
1624 {
1625 auto eA = Elem::Element{cellElemInfo[iParent]->getElemType()};
1626 auto eB = Elem::Element{cellElemInfo[candidateParent]->getElemType()};
1627 auto edA = eA.ObtainEdge(iSub);
1628 int nEN = edA.GetNumNodes();
1629 std::vector<DNDS::index> nodesA(nEN), nodesB(nEN);
1630 eA.ExtractEdgeNodes(iSub, cell2node[iParent], nodesA);
1631 eB.ExtractEdgeNodes(candidateSub, cell2node[candidateParent], nodesB);
1632 std::vector<NodePeriodicBits> pbiA(nEN), pbiB(nEN);
1633 eA.ExtractEdgeNodes(iSub, cell2nodePbi[iParent], pbiA);
1634 eB.ExtractEdgeNodes(candidateSub, cell2nodePbi[candidateParent], pbiB);
1635 using P = std::pair<DNDS::index, uint8_t>;
1636 auto cmp = [](const P &l, const P &r)
1637 { return l.first == r.first ? l.second < r.second : l.first < r.first; };
1638 std::vector<P> pa(nEN), pb(nEN);
1639 for (int i = 0; i < nEN; i++) { pa[i] = {nodesA[i], uint8_t(pbiA[i])}; pb[i] = {nodesB[i], uint8_t(pbiB[i])}; }
1640 std::sort(pa.begin(), pa.end(), cmp);
1641 std::sort(pb.begin(), pb.end(), cmp);
1642 uint8_t v0 = pa[0].second ^ pb[0].second;
1643 for (int i = 1; i < nEN; i++)
1644 if ((pa[i].second ^ pb[i].second) != v0)
1645 return false;
1646 return true;
1647 };
1648 edgeQuery.extractPbi = [&](DNDS::index iP, int iSub,
1649 const std::function<NodePeriodicBits(int)> &pPbi,
1650 NodePeriodicBits *out)
1651 {
1652 auto e = Elem::Element{cellElemInfo[iP]->getElemType()};
1653 auto ed = e.ObtainEdge(iSub);
1654 std::vector<NodePeriodicBits> pp(e.GetNumNodes());
1655 for (int i = 0; i < e.GetNumNodes(); i++) pp[i] = pPbi(i);
1656 std::vector<NodePeriodicBits> ep(ed.GetNumNodes());
1657 e.ExtractEdgeNodes(iSub, pp, ep);
1658 for (int i = 0; i < ed.GetNumNodes(); i++) out[i] = ep[i];
1659 };
1660
1666 resolver, g_mpi);
1667
1670 MPI_Allreduce(&edgeLocalOwned, &edgeGlobalOwned, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1671
1672 // Triply-periodic: 3 * np*N^3 edges (same as faces for a hex mesh).
1673 DNDS::index expectedEdges = 3 * np * N * N * N;
1675
1676 // All edges are internal (4 parents) in triply-periodic hex.
1677 for (DNDS::index i = 0; i < edgeResult.nOwnedEntities; i++)
1678 {
1679 CAPTURE(i);
1680 CHECK(edgeResult.entity2parent.father->RowSize(i) == 4);
1681 }
1682
1683 // Each cell has 12 edges.
1684 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1685 {
1686 CAPTURE(iCell);
1687 CHECK(edgeResult.parent2entity.father->RowSize(iCell) == 12);
1688 }
1689
1690 // --- Verify edge2nodePbi VALUES ---
1691 // Same approach: for each owned edge, find the first parent cell, extract
1692 // the expected edge pbi, and compare with stored entity2nodePbi.
1693 {
1694 DNDS::index nFail = 0;
1695 for (DNDS::index iEdge = 0; iEdge < edgeResult.nOwnedEntities; iEdge++)
1696 {
1697 DNDS::index parentGlobal = edgeResult.entity2parent.father->operator()(iEdge, 0);
1698 auto [pFound, pRank, parentLocal] = cellGhostMapping->search_indexAppend(parentGlobal);
1699 REQUIRE(pFound);
1700
1701 auto edgeNodeRow = edgeResult.entity2node.father->operator[](iEdge);
1702 std::set<DNDS::index> edgeNodeGlobals;
1703 for (auto gn : edgeNodeRow)
1704 edgeNodeGlobals.insert(gn);
1705
1706 auto eParent = Elem::Element{cellElemInfo[parentLocal]->getElemType()};
1707 int nEdges = eParent.GetNumEdges();
1708 bool matched = false;
1709 for (int iSub = 0; iSub < nEdges && !matched; iSub++)
1710 {
1711 auto eEdge = eParent.ObtainEdge(iSub);
1712 int nEN = eEdge.GetNumNodes();
1713
1714 std::vector<DNDS::index> subNodes(nEN);
1715 std::vector<DNDS::index> parentNodes(8);
1716 for (int k = 0; k < 8; k++)
1717 parentNodes[k] = cell2node(parentLocal, k);
1718 eParent.ExtractEdgeNodes(iSub, parentNodes, subNodes);
1719
1720 std::set<DNDS::index> subNodeGlobals;
1721 for (auto la : subNodes)
1722 subNodeGlobals.insert(nodeGhostMapping->operator()(-1, la));
1723
1724 if (subNodeGlobals != edgeNodeGlobals)
1725 continue;
1726
1727 std::vector<NodePeriodicBits> parentPbiVec(8);
1728 for (int k = 0; k < 8; k++)
1729 parentPbiVec[k] = cell2nodePbi(parentLocal, k);
1730 std::vector<NodePeriodicBits> expectedPbi(nEN);
1731 eParent.ExtractEdgeNodes(iSub, parentPbiVec, expectedPbi);
1732
1733 REQUIRE(edgeResult.entity2nodePbi.father->RowSize(iEdge) == nEN);
1734 for (int j = 0; j < nEN; j++)
1735 {
1736 auto stored = edgeResult.entity2nodePbi.father->operator()(iEdge, j);
1737 if (!(stored == expectedPbi[j]))
1738 nFail++;
1739 }
1740 matched = true;
1741 }
1742 REQUIRE(matched);
1743 }
1745 MPI_Allreduce(&nFail, &globalNFail, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1746 CHECK(globalNFail == 0);
1747 }
1748
1749 // --- Verify parent2entityPbi for faces is at most 1-bit ---
1750 // A face can cross at most one periodic boundary, so the relative pbi
1751 // should have at most one bit set (P1, P2, P3, or 0).
1752 {
1754 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1755 for (rowsize j = 0; j < result.parent2entity.father->RowSize(iCell); j++)
1756 {
1757 uint8_t v = uint8_t(result.parent2entityPbi.father->operator()(iCell, j));
1758 int bits = __builtin_popcount(v);
1759 if (bits > 1)
1760 nMultiBit++;
1761 }
1763 MPI_Allreduce(&nMultiBit, &globalMultiBit, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1765 }
1766
1767 // --- Coordinate-based verification for faces and edges ---
1768 // Build synthetic node coordinates: node at local index ix*N*N+iy*N+iz
1769 // has physical coords (rank*N + ix, iy, iz).
1770 // Periodicity: P1 → +np*N in X, P2 → +N in Y, P3 → +N in Z.
1771 using tPoint = Eigen::Vector3d;
1772 auto makeCoord = [&](DNDS::index nodeGlobalIdx) -> tPoint
1773 {
1774 // Find owning rank and local index
1776 nodeGM->search(nodeGlobalIdx, r, v);
1777 DNDS::index localIdx = nodeGlobalIdx - (*nodeGM)(r, 0);
1778 DNDS::index ix = localIdx / (N * N);
1779 DNDS::index iy = (localIdx / N) % N;
1780 DNDS::index iz = localIdx % N;
1781 return tPoint(double(r * N + ix), double(iy), double(iz));
1782 };
1783 auto applyPbi = [&](const tPoint &c, NodePeriodicBits pbi) -> tPoint
1784 {
1785 tPoint ret = c;
1786 if (pbi.getP1()) ret(0) += double(np * N);
1787 if (pbi.getP2()) ret(1) += double(N);
1788 if (pbi.getP3()) ret(2) += double(N);
1789 return ret;
1790 };
1791
1792 // Face center verification
1793 {
1794 DNDS::index nFail = 0;
1795 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1796 {
1797 auto eCell = Elem::Element{cellElemInfo[iCell]->getElemType()};
1798 for (rowsize j = 0; j < result.parent2entity.father->RowSize(iCell); j++)
1799 {
1800 DNDS::index gFace = result.parent2entity.father->operator()(iCell, j);
1801 if (gFace == UnInitIndex) continue;
1802 auto faceFatherGM = result.entity2node.trans.pLGlobalMapping;
1803 DNDS::index myFaceStart = (*faceFatherGM)(g_mpi.rank, 0);
1804 DNDS::index myFaceEnd = myFaceStart + result.nOwnedEntities;
1805 if (gFace < myFaceStart || gFace >= myFaceEnd) continue;
1806 DNDS::index iFaceLocal = gFace - myFaceStart;
1807
1808 NodePeriodicBits relPbi = result.parent2entityPbi.father->operator()(iCell, j);
1809
1810 // Compute face center from CELL's perspective
1811 auto eFace = eCell.ObtainFace(j);
1812 int nFN = eFace.GetNumNodes();
1813 std::vector<DNDS::index> cellFaceNodes(nFN);
1814 std::vector<NodePeriodicBits> cellFacePbiVec(nFN);
1815 {
1816 std::vector<DNDS::index> pN(8);
1817 std::vector<NodePeriodicBits> pP(8);
1818 for (int k = 0; k < 8; k++)
1819 {
1820 pN[k] = cell2node(iCell, k);
1821 pP[k] = cell2nodePbi(iCell, k);
1822 }
1823 eCell.ExtractFaceNodes(j, pN, cellFaceNodes);
1824 eCell.ExtractFaceNodes(j, pP, cellFacePbiVec);
1825 }
1826 tPoint centerCell = tPoint::Zero();
1827 for (int k = 0; k < nFN; k++)
1828 {
1829 DNDS::index ng = nodeGhostMapping->operator()(-1, cellFaceNodes[k]);
1830 centerCell += applyPbi(makeCoord(ng), cellFacePbiVec[k]);
1831 }
1832 centerCell /= double(nFN);
1833
1834 // Compute face center from ENTITY's perspective, then transform via relPbi
1835 auto faceNodeRow = result.entity2node.father->operator[](iFaceLocal);
1836 tPoint centerEntity = tPoint::Zero();
1837 for (rowsize k = 0; k < static_cast<rowsize>(faceNodeRow.size()); k++)
1838 {
1839 NodePeriodicBits ePbi = result.entity2nodePbi.father->operator()(iFaceLocal, k);
1840 centerEntity += applyPbi(makeCoord(faceNodeRow[k]), ePbi);
1841 }
1842 centerEntity /= double(faceNodeRow.size());
1843 // Transform entity center to cell perspective
1844 tPoint centerConverted = applyPbi(centerEntity, relPbi);
1845
1846 if ((centerCell - centerConverted).norm() > 1e-12)
1847 nFail++;
1848 }
1849 }
1851 MPI_Allreduce(&nFail, &globalNFail, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1852 CHECK(globalNFail == 0);
1853 }
1854
1855 // Edge center verification
1856 {
1857 DNDS::index nFail = 0;
1858 for (DNDS::index iCell = 0; iCell < nCellLocal; iCell++)
1859 {
1860 auto eCell = Elem::Element{cellElemInfo[iCell]->getElemType()};
1861 for (rowsize j = 0; j < edgeResult.parent2entity.father->RowSize(iCell); j++)
1862 {
1863 DNDS::index gEdge = edgeResult.parent2entity.father->operator()(iCell, j);
1864 if (gEdge == UnInitIndex) continue;
1865 auto edgeFatherGM = edgeResult.entity2node.trans.pLGlobalMapping;
1866 DNDS::index myEdgeStart = (*edgeFatherGM)(g_mpi.rank, 0);
1867 DNDS::index myEdgeEnd = myEdgeStart + edgeResult.nOwnedEntities;
1868 if (gEdge < myEdgeStart || gEdge >= myEdgeEnd) continue;
1869 DNDS::index iEdgeLocal = gEdge - myEdgeStart;
1870
1871 NodePeriodicBits relPbi = edgeResult.parent2entityPbi.father->operator()(iCell, j);
1872
1873 // Compute edge center from CELL's perspective
1874 auto eEdge = eCell.ObtainEdge(j);
1875 int nEN = eEdge.GetNumNodes();
1876 std::vector<DNDS::index> cellEdgeNodes(nEN);
1877 std::vector<NodePeriodicBits> cellEdgePbiVec(nEN);
1878 {
1879 std::vector<DNDS::index> pN(8);
1880 std::vector<NodePeriodicBits> pP(8);
1881 for (int k = 0; k < 8; k++)
1882 {
1883 pN[k] = cell2node(iCell, k);
1884 pP[k] = cell2nodePbi(iCell, k);
1885 }
1886 eCell.ExtractEdgeNodes(j, pN, cellEdgeNodes);
1887 eCell.ExtractEdgeNodes(j, pP, cellEdgePbiVec);
1888 }
1889 tPoint centerCell = tPoint::Zero();
1890 for (int k = 0; k < nEN; k++)
1891 {
1892 DNDS::index ng = nodeGhostMapping->operator()(-1, cellEdgeNodes[k]);
1893 centerCell += applyPbi(makeCoord(ng), cellEdgePbiVec[k]);
1894 }
1895 centerCell /= double(nEN);
1896
1897 // Compute edge center from ENTITY's perspective, then transform via relPbi
1898 auto edgeNodeRow = edgeResult.entity2node.father->operator[](iEdgeLocal);
1899 tPoint centerEntity = tPoint::Zero();
1900 for (rowsize k = 0; k < static_cast<rowsize>(edgeNodeRow.size()); k++)
1901 {
1902 NodePeriodicBits ePbi = edgeResult.entity2nodePbi.father->operator()(iEdgeLocal, k);
1903 centerEntity += applyPbi(makeCoord(edgeNodeRow[k]), ePbi);
1904 }
1905 centerEntity /= double(edgeNodeRow.size());
1906 tPoint centerConverted = applyPbi(centerEntity, relPbi);
1907
1908 if ((centerCell - centerConverted).norm() > 1e-12)
1909 nFail++;
1910 }
1911 }
1913 MPI_Allreduce(&nFail, &globalNFail, 1, DNDS_MPI_INDEX, MPI_SUM, g_mpi.comm);
1914 CHECK(globalNFail == 0);
1915 }
1916}
1917
1918
1919// ---------------------------------------------------------------------------
1920int main(int argc, char **argv)
1921{
1922 MPI_Init(&argc, &argv);
1923 g_mpi.setWorld();
1924
1925 doctest::Context ctx;
1926 ctx.applyCommandLine(argc, argv);
1927 int res = ctx.run();
1928
1929 MPI_Finalize();
1930 return res;
1931}
#define DNDS_assert(expr)
Debug-only assertion (compiled out when DNDS_NDEBUG is defined). Prints the expression + file/line + ...
Definition Errors.hpp:112
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
int32_t t_index
Definition Geometric.hpp:6
Eigen::Vector3d tPoint
Definition Geometric.hpp:9
std::function< OwnershipDecision(const std::vector< index > &parents, const std::vector< MPI_int > &parentRanks, index nLocalParents)> OwnershipResolverMulti
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
int MPI_int
MPI counterpart type for MPI_int (= C int). Used for counts and ranks in MPI calls.
Definition MPI.hpp:54
DNDS_DEVICE_CALLABLE index Size() const
Combined father + son row count.
Definition ArrayPair.hpp:33
void TransAttach()
Bind the transformer to the current father / son pointers.
ssp< TArray > father
Owned-side array (must be resized before ghost setup).
index Size() const
Combined row count (father->Size() + son->Size()).
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).
TTrans trans
Ghost-communication engine bound to father and son.
DNDS_DEVICE_CALLABLE constexpr Element ObtainFace(t_index iFace) const
Definition Elements.hpp:257
DNDS_DEVICE_CALLABLE constexpr t_index GetNumVertices() const
Definition Elements.hpp:229
void ExtractFaceNodes(t_index iFace, const TIn &nodes, TOut &faceNodesOut)
Definition Elements.hpp:296
DNDS_DEVICE_CALLABLE constexpr t_index GetNumNodes() const
Definition Elements.hpp:235
static InterpolateResult Interpolate(const ArrayAdjacencyPair< p2n_rs > &parent2node, const SubEntityQuery &query, index nParent, index nNode, const MPIInfo &mpi)
static InterpolateGlobalResultT< e2p_rs > InterpolateGlobal(const ArrayAdjacencyPair< p2n_rs > &parent2node, const tPbiPair &parent2nodePbi, const OffsetAscendIndexMapping &parentGhostMapping, const GlobalOffsetsMapping &parentGlobalMapping, const OffsetAscendIndexMapping &nodeGhostMapping, const SubEntityQueryPbi &query, index nLocalParents, index nTotalParents, index nNode, const OwnershipResolverMulti &resolver, const MPIInfo &mpi)
static MPI_Datatype CommType()
std::function< bool(index iParent, int iSub, index iCandEntity, index candidateParent, int candidateSub)> matchExtra
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
void build(DNDS::index tileN, bool isPeriodic, const MPIInfo &mpi)
tElemInfoArrayPair cellElemInfo
ssp< OffsetAscendIndexMapping > nodeGhostMapping
ssp< GlobalOffsetsMapping > cellGM
ssp< OffsetAscendIndexMapping > cellGhostMapping
std::array< NodePeriodicBits, 8 > pbi
std::array< DNDS::index, 8 > nodes
Eigen::Matrix< real, 5, 1 > v
constexpr DNDS::index nLocal
tVec r(NCells)
tVec b(NCells)
if(g_mpi.rank==0) std auto[rhsDensity, rhsEnergy, incL2]
std::set< std::set< DNDS::index > > expectedEdges
std::vector< DNDS::index > ghostGlobals
std::set< std::set< DNDS::index > > actualShared
j edgeRefCount[res.parent2entity.father->operator()(iCell, j)]
DNDS::MPI_int rightRank
std::set< std::set< DNDS::index > > expectedShared
std::vector< GhostCell > ghostCells
MPI_Allreduce & nMultiBit
const DNDS::index nCellLocal
REQUIRE(bool(result.parent2entityPbi.father))
DNDS::index edgeGlobalOwned
std::set< std::set< DNDS::index > > actualFaces
std::set< DNDS::index > allNodeGlobals
CHECK(res.nEntities==12)
MPI_Allreduce & nFail
if(bits > 1) nMultiBit++
tElemInfoArrayPair cellElemInfo
std::map< DNDS::index, std::set< DNDS::index > > cellFN
for(DNDS::index i=0;i< 4;i++)
const DNDS::index nNodeLocal
std::set< std::set< DNDS::index > > allVertSets
std::set< std::set< DNDS::index > > actualEdges
std::set< std::set< DNDS::index > > expectedFaces
OwnershipResolverMulti resolver
DistributedHex3D mesh
input explicitMaps push_back(EntityReorderMap{EntityKind::Cell, cellPartition})
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")
const tPoint const tPoint const tPoint & p