DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_MeshReorder.cpp
Go to the documentation of this file.
1/**
2 * @file test_MeshReorder.cpp
3 * @brief Unit tests for ReorderPlan, ReorderRegistry, and classification.
4 *
5 * Tests:
6 * - AdjAction classification logic
7 * - ReorderRegistry registration and lookup
8 * - ReorderPlan::apply on synthetic data (no real mesh)
9 * - Full mesh ReorderEntities on real CGNS meshes (Phase 2b)
10 */
11
12#define DOCTEST_CONFIG_IMPLEMENT
13#include "doctest.h"
15#include "Geom/Mesh/Mesh.hpp"
16#include <numeric>
17#include <set>
18
19using namespace DNDS;
20using namespace DNDS::Geom;
21
22// NOTE: DNDS::index, DNDS::real, DNDS::rowsize clash with POSIX symbols.
23// Qualify in declarations to avoid ambiguity.
25
26int main(int argc, char **argv)
27{
28 MPI_Init(&argc, &argv);
29 doctest::Context ctx;
30 ctx.applyCommandLine(argc, argv);
31 int res = ctx.run();
32 MPI_Finalize();
33 return res;
34}
35
36static MPIInfo worldMPI()
37{
39 mpi.setWorld();
40 return mpi;
41}
42
43// =================================================================
44// Test: classifyAdj logic
45// =================================================================
46
47TEST_CASE("classifyAdj basic classification")
48{
49 std::unordered_set<EntityKind> reordered;
50
51 SUBCASE("empty reordered set")
52 {
53 CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::SKIP);
54 CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SKIP);
55 }
56
57 SUBCASE("cell only reordered")
58 {
59 reordered = {EntityKind::Cell};
60 CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE);
61 CHECK(classifyAdj(Adj::Cell2Face, reordered) == AdjAction::RELOCATE);
62 CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF);
63 CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::REMAP);
64 CHECK(classifyAdj(Adj::Face2Cell, reordered) == AdjAction::REMAP);
65 CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::REMAP);
66 CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::SKIP);
67 CHECK(classifyAdj(Adj::Face2Node, reordered) == AdjAction::SKIP);
68 }
69
70 SUBCASE("node only reordered")
71 {
72 reordered = {EntityKind::Node};
73 CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::REMAP);
74 CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::REMAP);
75 CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE);
76 CHECK(classifyAdj(Adj::Node2Bnd, reordered) == AdjAction::RELOCATE);
77 CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SKIP);
78 }
79
80 SUBCASE("cell + node reordered")
81 {
82 reordered = {EntityKind::Cell, EntityKind::Node};
83 CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE_REMAP);
84 CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE_REMAP);
85 CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF);
86 CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::REMAP);
87 CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::REMAP);
88 }
89
90 SUBCASE("cell + node + bnd reordered")
91 {
92 reordered = {EntityKind::Cell, EntityKind::Node, EntityKind::Bnd};
93 CHECK(classifyAdj(Adj::Cell2Node, reordered) == AdjAction::RELOCATE_REMAP);
94 CHECK(classifyAdj(Adj::Bnd2Node, reordered) == AdjAction::RELOCATE_REMAP);
95 CHECK(classifyAdj(Adj::Bnd2Cell, reordered) == AdjAction::RELOCATE_REMAP);
96 CHECK(classifyAdj(Adj::Node2Cell, reordered) == AdjAction::RELOCATE_REMAP);
97 CHECK(classifyAdj(Adj::Node2Bnd, reordered) == AdjAction::RELOCATE_REMAP);
98 CHECK(classifyAdj(Adj::Cell2Cell, reordered) == AdjAction::SELF);
99 }
100}
101
102// =================================================================
103// Test: ReorderRegistry basic operations
104// =================================================================
105
106TEST_CASE("ReorderRegistry register and query")
107{
108 auto mpi = worldMPI();
110
111 // Register a global mapping
112 auto gm = make_ssp<GlobalOffsetsMapping>();
113 gm->setMPIAlignBcast(mpi, 10);
114 reg.registerGlobalMapping(EntityKind::Cell, gm);
115
116 CHECK(reg.getGlobalMapping(EntityKind::Cell) == gm);
117 CHECK(reg.getGlobalMapping(EntityKind::Node) == nullptr);
118
119 // Register an adj
120 bool remapCalled = false;
121 bool relocateCalled = false;
122 reg.registerAdj(
125 { remapCalled = true; },
126 [&](const PermutationTransfer &, const MPIInfo &)
127 { relocateCalled = true; },
128 "cell2node");
129
130 CHECK(reg.adjs.size() == 1);
131 CHECK(reg.adjs[0].kind == Adj::Cell2Node);
132 CHECK(reg.adjs[0].name == "cell2node");
133
134 // Register a companion
135 bool compCalled = false;
136 reg.registerCompanion(
137 EntityKind::Cell,
138 [&](const PermutationTransfer &, const MPIInfo &)
139 { compCalled = true; },
140 "cellElemInfo");
141
142 CHECK(reg.companions.size() == 1);
143 CHECK(reg.companions[0].kind == EntityKind::Cell);
144}
145
146// =================================================================
147// Test: ReorderPlan::apply with synthetic data
148// =================================================================
149
150TEST_CASE("ReorderPlan::apply cell-only local permutation")
151{
152 auto mpi = worldMPI();
153 const DNDS::index nCell = 8;
154 const DNDS::index nNode = 4;
155
156 // Create synthetic cell2node: each cell references 2 nodes (global)
158 cell2node.InitPair("cell2node", mpi);
159 cell2node.father->Resize(nCell);
160 cell2node.father->createGlobalMapping();
161
162 DNDS::index cellOffset = (*cell2node.father->pLGlobalMapping)(mpi.rank, 0);
163
164 // Create synthetic node array
166 nodeArr.InitPair("nodeArr", mpi);
167 nodeArr.father->Resize(nNode);
168 nodeArr.father->createGlobalMapping();
169
170 DNDS::index nodeOffset = (*nodeArr.father->pLGlobalMapping)(mpi.rank, 0);
171
172 // Fill cell2node: cell i references nodes (i%nNode) and ((i+1)%nNode)
173 for (DNDS::index i = 0; i < nCell; i++)
174 {
175 cell2node(i, 0) = nodeOffset + (i % nNode);
176 cell2node(i, 1) = nodeOffset + ((i + 1) % nNode);
177 }
178
179 // Create a companion array (cellElemInfo analog)
180 ArrayAdjacencyPair<1> cellInfo;
181 cellInfo.InitPair("cellInfo", mpi);
182 cellInfo.father->Resize(nCell);
183 for (DNDS::index i = 0; i < nCell; i++)
184 cellInfo(i, 0) = 1000 + cellOffset + i; // tag = 1000 + global
185
186 // Build registry
188 reg.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping);
189 reg.registerGlobalMapping(EntityKind::Node, nodeArr.father->pLGlobalMapping);
190
191 reg.registerAdj(
193 nullptr, // no remap needed (only Cell reordered, not Node)
194 [&](const PermutationTransfer &t, const MPIInfo &m)
195 { t.transferRows(cell2node, m); },
196 "cell2node");
197
198 reg.registerCompanion(
199 EntityKind::Cell,
200 [&](const PermutationTransfer &t, const MPIInfo &m)
201 { t.transferRows(cellInfo, m); },
202 "cellInfo");
203
204 // Build plan: reverse cell permutation (all local)
205 std::vector<MPI_int> cellPartition(nCell, mpi.rank);
207 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
208
209 auto plan = ReorderPlan::build(input, reg, mpi);
210 CHECK(plan.isLocalOnly);
211 CHECK(plan.reorderedKinds.count(EntityKind::Cell));
212 CHECK_FALSE(plan.reorderedKinds.count(EntityKind::Node));
213
214 // Apply
215 plan.apply(reg, mpi);
216
217 // Since partition = all-self and ordering is preserved within rank,
218 // the data should be unchanged (identity permutation via fromPartition).
219 for (DNDS::index i = 0; i < nCell; i++)
220 {
221 CHECK(cell2node(i, 0) == nodeOffset + (i % nNode));
222 CHECK(cell2node(i, 1) == nodeOffset + ((i + 1) % nNode));
223 CHECK(cellInfo(i, 0) == 1000 + cellOffset + i);
224 }
225}
226
227// =================================================================
228// Test: ReorderPlan::apply with remap (node reorder, cells stay)
229// =================================================================
230
231TEST_CASE("ReorderPlan::apply node-only remap")
232{
233 auto mpi = worldMPI();
234 const DNDS::index nCell = 4;
235 const DNDS::index nNode = 6;
236
237 // cell2node: Cell->Node (cell rows fixed, node entries need remapping)
239 cell2node.InitPair("cell2node", mpi);
240 cell2node.father->Resize(nCell);
241 cell2node.father->createGlobalMapping();
242
244 nodeArr.InitPair("nodeArr", mpi);
245 nodeArr.father->Resize(nNode);
246 nodeArr.father->createGlobalMapping();
247
248 DNDS::index nodeOffset = (*nodeArr.father->pLGlobalMapping)(mpi.rank, 0);
249
250 // Fill: cell i refs nodes i and i+1
251 for (DNDS::index i = 0; i < nCell; i++)
252 {
253 cell2node(i, 0) = nodeOffset + i;
254 cell2node(i, 1) = nodeOffset + i + 1;
255 }
256
257 // Node companion: coords analog
259 coords.InitPair("coords", mpi);
260 coords.father->Resize(nNode);
261 for (DNDS::index i = 0; i < nNode; i++)
262 coords(i, 0) = 500 + nodeOffset + i; // value = 500 + globalNode
263
264 // Build registry
266 reg.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping);
267 reg.registerGlobalMapping(EntityKind::Node, nodeArr.father->pLGlobalMapping);
268
269 reg.registerAdj(
271 [&](const PermutationTransfer::LookupResult &lookup)
272 {
273 for (DNDS::index i = 0; i < nCell; i++)
274 for (rowsize j = 0; j < 2; j++)
275 {
277 if (v != UnInitIndex)
278 v = lookup.resolve(v);
279 }
280 },
281 nullptr, // no relocate (Cell not reordered)
282 "cell2node");
283
284 reg.registerCompanion(
285 EntityKind::Node,
286 [&](const PermutationTransfer &t, const MPIInfo &m)
287 { t.transferRows(coords, m); },
288 "coords");
289
290 // Reorder nodes: all stay local (identity partition)
291 std::vector<MPI_int> nodePartition(nNode, mpi.rank);
293 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, nodePartition});
294
295 auto plan = ReorderPlan::build(input, reg, mpi);
296 CHECK(plan.isLocalOnly);
297 CHECK(plan.reorderedKinds.count(EntityKind::Node));
298 CHECK_FALSE(plan.reorderedKinds.count(EntityKind::Cell));
299
300 // Apply
301 plan.apply(reg, mpi);
302
303 // With identity partition (all stay on same rank), new globals are
304 // contiguous starting at newGlobalOffsets[mpi.rank].
305 // For identity: newGlobalIndices[i] = nodeOffset + i (unchanged).
306 // So remap should be identity, coords should be unchanged.
307 for (DNDS::index i = 0; i < nCell; i++)
308 {
309 // Since it's identity partition, old globals map to same new globals
310 auto &transfer = plan.transfers.at(EntityKind::Node);
311 DNDS::index expectedNode0 = transfer.newGlobalIndices[i];
312 DNDS::index expectedNode1 = transfer.newGlobalIndices[i + 1];
313 CHECK(cell2node(i, 0) == expectedNode0);
314 CHECK(cell2node(i, 1) == expectedNode1);
315 }
316
317 for (DNDS::index i = 0; i < nNode; i++)
318 CHECK(coords(i, 0) == 500 + nodeOffset + i);
319}
320
321// =================================================================
322// Real mesh helpers
323// =================================================================
324
325static std::string meshPath(const std::string &name)
326{
327 std::string f(__FILE__);
328 for (int i = 0; i < 4; i++)
329 {
330 auto pos = f.rfind('/');
331 if (pos == std::string::npos)
332 pos = f.rfind('\\');
333 if (pos != std::string::npos)
334 f = f.substr(0, pos);
335 }
336 return f + "/data/mesh/" + name;
337}
338
339/// Build a mesh through the primary pipeline (up to ghost + local indices).
340/// Returns mesh in Adj_PointToLocal state with ghost layers.
341static ssp<UnstructuredMesh> buildMeshPrimary(
342 const MPIInfo &mpi, const std::string &file, int dim,
343 bool withFaces = false)
344{
345 auto mesh = make_ssp<UnstructuredMesh>(mpi, dim);
347 reader.ReadFromCGNSSerial(meshPath(file));
348 reader.BuildCell2Cell();
349
351 pOpt.metisType = "KWAY";
352 pOpt.metisUfactor = 30;
353 pOpt.metisSeed = 42;
354 pOpt.metisNcuts = 1;
355 reader.MeshPartitionCell2Cell(pOpt);
356 reader.PartitionReorderToMeshCell2Cell();
357
358 mesh->RecoverNode2CellAndNode2Bnd();
359 mesh->RecoverCell2CellAndBnd2Cell();
360 mesh->BuildGhostPrimary();
361 mesh->AdjGlobal2LocalPrimary();
362
363 if (withFaces)
364 {
365 mesh->InterpolateFace();
366 mesh->AdjLocal2GlobalN2CB();
367 mesh->BuildGhostN2CB();
368 mesh->AdjGlobal2LocalN2CB();
369 }
370
371 return mesh;
372}
373
374/// Collect all owned global indices for an entity kind (from globalMapping).
375static std::set<DNDS::index> collectOwnedGlobals(
376 const ssp<GlobalOffsetsMapping> &gm, const MPIInfo &mpi)
377{
378 std::set<DNDS::index> result;
379 DNDS::index offset = (*gm)(mpi.rank, 0);
380 DNDS::index count = gm->RLengths()[mpi.rank];
381 for (DNDS::index i = 0; i < count; i++)
382 result.insert(offset + i);
383 return result;
384}
385
386/// Gather all owned globals across all ranks (for total count check).
387static DNDS::index gatherGlobalCount(const ssp<GlobalOffsetsMapping> &gm, const MPIInfo &mpi)
388{
389 return gm->globalSize();
390}
391
392/// Check that an adj array's entries are all valid globals within
393/// [0, targetGlobalSize) or UnInitIndex.
394static bool checkAdjEntriesValid(
395 const auto &adj, DNDS::index nRows, DNDS::index targetGlobalSize)
396{
397 for (DNDS::index i = 0; i < nRows; i++)
398 for (rowsize j = 0; j < adj.RowSize(i); j++)
399 {
400 DNDS::index v = adj(i, j);
401 if (v == UnInitIndex)
402 continue;
403 if (v < 0 || v >= targetGlobalSize)
404 return false;
405 }
406 return true;
407}
408
409// =================================================================
410// Test: Cell-only local reorder on real mesh (no faces)
411// =================================================================
412
413TEST_CASE("ReorderEntities cell-only local on UniformSquare_10")
414{
415 auto mpi = worldMPI();
416 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
417
418 // Snapshot pre-reorder state
419 DNDS::index nCellBefore = mesh->NumCell();
420 DNDS::index nNodeBefore = mesh->NumNode();
421 DNDS::index nBndBefore = mesh->NumBnd();
422 DNDS::index nCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize();
423 DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize();
424
425 // Convert to global for reorder
426 mesh->AdjLocal2GlobalPrimary();
427
428 // Build cell reorder: all cells stay on same rank (identity partition)
429 std::vector<MPI_int> cellPartition(nCellBefore, mpi.rank);
431 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
432 // Default follows: Node and Bnd follow Cell
433
434 mesh->ReorderEntities(input);
435
436 // Post-condition checks
437 CHECK(mesh->adjPrimaryState == Adj_PointToGlobal);
438 CHECK(mesh->NumCell() == nCellBefore);
439 CHECK(mesh->NumNode() == nNodeBefore);
440 CHECK(mesh->NumBnd() == nBndBefore);
441
442 // Global counts preserved
443 CHECK(mesh->cell2node.father->pLGlobalMapping->globalSize() == nCellGlobal);
444 CHECK(mesh->coords.father->pLGlobalMapping->globalSize() == nNodeGlobal);
445
446 // Adj entries are valid globals
447 CHECK(checkAdjEntriesValid(mesh->cell2node, nCellBefore, nNodeGlobal));
448 CHECK(checkAdjEntriesValid(mesh->cell2cell, nCellBefore, nCellGlobal));
449 CHECK(checkAdjEntriesValid(mesh->bnd2cell, nBndBefore, nCellGlobal));
450
451 // Node2cell entries point to valid cell globals
452 CHECK(checkAdjEntriesValid(mesh->node2cell, nNodeBefore, nCellGlobal));
453
454 // Verify mesh can be rebuilt: ghost + local conversion
455 mesh->RecoverNode2CellAndNode2Bnd();
456 mesh->RecoverCell2CellAndBnd2Cell();
457 mesh->BuildGhostPrimary();
458 mesh->AdjGlobal2LocalPrimary();
459
460 // Sanity: cell2node entries should be valid local indices now
461 for (DNDS::index iC = 0; iC < mesh->NumCell(); iC++)
462 for (rowsize j = 0; j < mesh->cell2node.RowSize(iC); j++)
463 {
464 DNDS::index iN = mesh->cell2node(iC, j);
465 CHECK(iN >= 0);
466 CHECK(iN < mesh->NumNodeProc());
467 }
468}
469
470// =================================================================
471// Test: Cell-only local with faces (face destruction)
472// =================================================================
473
474TEST_CASE("ReorderEntities cell-only with face destruction on UniformSquare_10")
475{
476 auto mpi = worldMPI();
477 // Build without faces (simpler), then manually build faces to test destruction
478 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
479
480 // Build faces (from local state)
481 mesh->InterpolateFace();
482
483 CHECK(mesh->face2node.father); // faces exist before reorder
484
485 // Convert everything to global for reorder
486 mesh->AdjLocal2GlobalPrimary();
487 mesh->AdjLocal2GlobalFacial();
488 mesh->AdjLocal2GlobalC2F();
489
490 DNDS::index nCellBefore = mesh->NumCell();
491
492 // Reorder with face destruction
493 std::vector<MPI_int> cellPartition(nCellBefore, mpi.rank);
495 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
496 input.destroyKinds = {EntityKind::Face};
497
498 mesh->ReorderEntities(input);
499
500 // Faces should be destroyed
501 CHECK_FALSE(mesh->face2node.father);
502 CHECK_FALSE(mesh->face2cell.father);
503 CHECK_FALSE(mesh->cell2face.father);
504 CHECK(mesh->adjFacialState == Adj_Unknown);
505
506 // Primary adj still valid
507 CHECK(mesh->adjPrimaryState == Adj_PointToGlobal);
508 CHECK(mesh->NumCell() == nCellBefore);
509
510 // Can rebuild faces from scratch
511 mesh->RecoverNode2CellAndNode2Bnd();
512 mesh->RecoverCell2CellAndBnd2Cell();
513 mesh->BuildGhostPrimary();
514 mesh->AdjGlobal2LocalPrimary();
515 mesh->InterpolateFace();
516 mesh->AssertOnFaces();
517}
518
519// =================================================================
520// Test: Cell distributed reorder (round-robin) with node/bnd follow
521// =================================================================
522
523TEST_CASE("ReorderEntities cell distributed round-robin with follow")
524{
525 auto mpi = worldMPI();
526 if (mpi.size < 2)
527 return;
528
529 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
530
531 DNDS::index nCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize();
532 DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize();
533 DNDS::index nBndGlobal = mesh->bnd2node.father->pLGlobalMapping->globalSize();
534
535 // Convert to global
536 mesh->AdjLocal2GlobalPrimary();
537 // N2CB already global after buildMeshPrimary(withFaces=false)
538
539 DNDS::index nCellBefore = mesh->NumCell();
540
541 // Round-robin: cell i goes to rank (i % nRanks)
542 std::vector<MPI_int> cellPartition(nCellBefore);
543 for (DNDS::index i = 0; i < nCellBefore; i++)
544 cellPartition[i] = static_cast<MPI_int>(i % mpi.size);
545
547 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
548 // Node and Bnd follow automatically
549
550 mesh->ReorderEntities(input);
551
552 // Global counts preserved (collective check)
553 DNDS::index newCellGlobal = mesh->cell2node.father->pLGlobalMapping->globalSize();
554 DNDS::index newNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize();
555 DNDS::index newBndGlobal = mesh->bnd2node.father->pLGlobalMapping->globalSize();
556 CHECK(newCellGlobal == nCellGlobal);
557 CHECK(newNodeGlobal == nNodeGlobal);
558 CHECK(newBndGlobal == nBndGlobal);
559
560 // Adj entries valid
561 CHECK(checkAdjEntriesValid(mesh->cell2node, mesh->NumCell(), newNodeGlobal));
562 CHECK(checkAdjEntriesValid(mesh->cell2cell, mesh->NumCell(), newCellGlobal));
563 CHECK(checkAdjEntriesValid(mesh->bnd2node, mesh->NumBnd(), newNodeGlobal));
564 CHECK(checkAdjEntriesValid(mesh->bnd2cell, mesh->NumBnd(), newCellGlobal));
565
566 // Verify no duplicate globals: each rank's cell globals should be unique
567 // and contiguous within [offset, offset+nLocal).
568 DNDS::index myOffset = (*mesh->cell2node.father->pLGlobalMapping)(mpi.rank, 0);
569 DNDS::index myCount = mesh->NumCell();
570 for (DNDS::index i = 0; i < myCount; i++)
571 {
572 DNDS::index g = myOffset + i;
573 CHECK(g >= 0);
574 CHECK(g < newCellGlobal);
575 }
576
577 // Verify mesh can be fully rebuilt
578 mesh->RecoverNode2CellAndNode2Bnd();
579 mesh->RecoverCell2CellAndBnd2Cell();
580 mesh->BuildGhostPrimary();
581 mesh->AdjGlobal2LocalPrimary();
582
583 // Cell2node entries are valid local-appended indices
584 for (DNDS::index iC = 0; iC < mesh->NumCell(); iC++)
585 for (rowsize j = 0; j < mesh->cell2node.RowSize(iC); j++)
586 {
587 DNDS::index iN = mesh->cell2node(iC, j);
588 CHECK(iN >= 0);
589 CHECK(iN < mesh->NumNodeProc());
590 }
591}
592
593// =================================================================
594// Test: Node-only local reorder (cells stay, node entries remapped)
595// =================================================================
596
597TEST_CASE("ReorderEntities node-only local on UniformSquare_10")
598{
599 auto mpi = worldMPI();
600 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
601
602 DNDS::index nCellBefore = mesh->NumCell();
603 DNDS::index nNodeBefore = mesh->NumNode();
604 DNDS::index nNodeGlobal = mesh->coords.father->pLGlobalMapping->globalSize();
605
606 // Snapshot coords before reorder (to verify relocation)
607 std::vector<tPoint> coordsBefore(nNodeBefore);
608 for (DNDS::index i = 0; i < nNodeBefore; i++)
609 coordsBefore[i] = mesh->coords[i];
610
611 // Convert to global
612 mesh->AdjLocal2GlobalPrimary();
613 // N2CB already global (RecoverNode2CellAndNode2Bnd leaves it global
614 // when BuildGhostN2CB is not called)
615
616 // Node reorder: all stay local (identity)
617 std::vector<MPI_int> nodePartition(nNodeBefore, mpi.rank);
619 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, nodePartition});
620 input.destroyKinds = {EntityKind::Face}; // faces invalid after node reorder
621
622 mesh->ReorderEntities(input);
623
624 // Cells should not have moved (cell count same)
625 CHECK(mesh->NumCell() == nCellBefore);
626 CHECK(mesh->NumNode() == nNodeBefore);
627
628 // Node globals preserved
629 CHECK(mesh->coords.father->pLGlobalMapping->globalSize() == nNodeGlobal);
630
631 // Cell2node entries should point to valid node globals
632 CHECK(checkAdjEntriesValid(mesh->cell2node, nCellBefore, nNodeGlobal));
633
634 // Coords should be preserved (identity partition = no movement)
635 for (DNDS::index i = 0; i < nNodeBefore; i++)
636 CHECK(mesh->coords[i] == coordsBefore[i]);
637
638 // Verify rebuild works
639 mesh->RecoverNode2CellAndNode2Bnd();
640 mesh->RecoverCell2CellAndBnd2Cell();
641 mesh->BuildGhostPrimary();
642 mesh->AdjGlobal2LocalPrimary();
643 mesh->InterpolateFace();
644 mesh->AssertOnFaces();
645}
646
647// =================================================================
648// Test: Expected-value verification with non-identity local cell
649// reverse permutation (uses fromLocalPermutation path directly)
650// =================================================================
651
652TEST_CASE("PermutationTransfer + buildLookup: reverse permutation value tracking")
653{
654 auto mpi = worldMPI();
655 const DNDS::index nLocal = 6;
656
657 // Build a simple cell2node-like adj with known values
659 cell2node.InitPair("cell2node", mpi);
660 cell2node.father->Resize(nLocal);
661 cell2node.father->createGlobalMapping();
662
663 DNDS::index cellOffset = (*cell2node.father->pLGlobalMapping)(mpi.rank, 0);
664
665 // Fill with tracker values: cell i stores (100 + cellOffset + i, 200 + cellOffset + i)
666 for (DNDS::index i = 0; i < nLocal; i++)
667 {
668 cell2node(i, 0) = 100 + cellOffset + i;
669 cell2node(i, 1) = 200 + cellOffset + i;
670 }
671
672 // Snapshot original values for verification
673 std::vector<DNDS::index> origCol0(nLocal), origCol1(nLocal);
674 for (DNDS::index i = 0; i < nLocal; i++)
675 {
676 origCol0[i] = cell2node(i, 0);
677 origCol1[i] = cell2node(i, 1);
678 }
679
680 // Build a reverse permutation: old i -> new (nLocal-1-i)
681 std::vector<DNDS::index> old2new(nLocal);
682 for (DNDS::index i = 0; i < nLocal; i++)
683 old2new[i] = nLocal - 1 - i;
684
686 old2new, cell2node.father->pLGlobalMapping, mpi);
687 CHECK(pt.isLocalOnly);
688
689 // Verify newGlobalIndices[i] = myOffset + old2new[i] = cellOffset + (nLocal-1-i)
690 for (DNDS::index i = 0; i < nLocal; i++)
691 CHECK(pt.newGlobalIndices[i] == cellOffset + (nLocal - 1 - i));
692
693 // Transfer rows: after permutation, row (nLocal-1-i) should contain old row i's data
694 pt.transferRows(cell2node, mpi);
695
696 for (DNDS::index i = 0; i < nLocal; i++)
697 {
698 DNDS::index newSlot = old2new[i];
699 CHECK(cell2node(newSlot, 0) == origCol0[i]);
700 CHECK(cell2node(newSlot, 1) == origCol1[i]);
701 }
702
703 // Build lookup and verify resolve() produces the correct new globals
704 auto lookup = pt.buildLookup({}, mpi);
705
706 for (DNDS::index i = 0; i < nLocal; i++)
707 {
708 DNDS::index oldGlobal = cellOffset + i;
709 DNDS::index expectedNewGlobal = cellOffset + (nLocal - 1 - i);
710 CHECK(lookup.resolve(oldGlobal) == expectedNewGlobal);
711 }
712}
713
714// =================================================================
715// Test: Distributed fromPartition with cross-rank value tracking
716// =================================================================
717
718TEST_CASE("PermutationTransfer distributed value tracking cross-rank")
719{
720 auto mpi = worldMPI();
721 if (mpi.size < 2)
722 return;
723
724 const DNDS::index nLocal = 4;
725
727 arr.InitPair("arr", mpi);
728 arr.father->Resize(nLocal);
729 arr.father->createGlobalMapping();
730
731 DNDS::index myOffset = (*arr.father->pLGlobalMapping)(mpi.rank, 0);
732 DNDS::index globalSize = arr.father->pLGlobalMapping->globalSize();
733
734 // Tag each entry with a unique global-based sentinel
735 const DNDS::index TAG_BASE = 100000;
736 for (DNDS::index i = 0; i < nLocal; i++)
737 arr(i, 0) = TAG_BASE + myOffset + i;
738
739 // Round-robin partition: entry i goes to rank (i % mpi.size)
740 std::vector<MPI_int> partition(nLocal);
741 for (DNDS::index i = 0; i < nLocal; i++)
742 partition[i] = static_cast<MPI_int>(i % mpi.size);
743
745 partition, arr.father->pLGlobalMapping, mpi);
746 CHECK_FALSE(pt.isLocalOnly);
747
748 pt.transferRows(arr, mpi);
749
750 // After transfer: every entry on this rank carries a valid tag
751 DNDS::index nAfter = arr.father->Size();
752 std::set<DNDS::index> receivedTags;
753 for (DNDS::index i = 0; i < nAfter; i++)
754 {
755 DNDS::index v = arr(i, 0);
756 CHECK(v >= TAG_BASE);
757 CHECK(v < TAG_BASE + globalSize);
758 receivedTags.insert(v);
759 }
760 // All received tags are unique (no duplicates)
761 CHECK(receivedTags.size() == static_cast<size_t>(nAfter));
762
763 // Sum of nAfter across all ranks must equal original global size
764 DNDS::index totalAfter = 0;
765 MPI_Allreduce(&nAfter, &totalAfter, 1, DNDS_MPI_INDEX, MPI_SUM, mpi.comm);
766 CHECK(totalAfter == globalSize);
767}
768
769// =================================================================
770// Test: buildLookup with pullSet (cross-rank resolve verification)
771// =================================================================
772
773TEST_CASE("PermutationTransfer::buildLookup cross-rank resolve with pullSet")
774{
775 auto mpi = worldMPI();
776 if (mpi.size < 2)
777 return;
778
779 const DNDS::index nLocal = 4;
780
781 auto gm = make_ssp<GlobalOffsetsMapping>();
782 gm->setMPIAlignBcast(mpi, nLocal);
783
784 DNDS::index myOffset = (*gm)(mpi.rank, 0);
785
786 // Round-robin partition: entry i stays if i%size==myRank, else moves
787 std::vector<MPI_int> partition(nLocal);
788 for (DNDS::index i = 0; i < nLocal; i++)
789 partition[i] = static_cast<MPI_int>((mpi.rank + i) % mpi.size);
790
791 auto pt = PermutationTransfer::fromPartition(partition, gm, mpi);
792
793 // Build a pull set: globals on the next rank
794 int nextRank = (mpi.rank + 1) % mpi.size;
795 DNDS::index nextOffset = (*gm)(nextRank, 0);
796 std::vector<DNDS::index> pullSet;
797 for (DNDS::index i = 0; i < nLocal; i++)
798 pullSet.push_back(nextOffset + i);
799 std::sort(pullSet.begin(), pullSet.end());
800
801 auto lookup = pt.buildLookup(pullSet, mpi);
802
803 // For each old global in pullSet, resolve should give some valid new global
804 // within [0, globalSize).
805 DNDS::index globalSize = gm->globalSize();
806 for (auto oldG : pullSet)
807 {
808 DNDS::index newG = lookup.resolve(oldG);
809 CHECK(newG >= 0);
810 CHECK(newG < globalSize);
811 }
812
813 // Verify my own locals resolve too
814 for (DNDS::index i = 0; i < nLocal; i++)
815 {
816 DNDS::index oldG = myOffset + i;
817 DNDS::index newG = lookup.resolve(oldG);
818 CHECK(newG >= 0);
819 CHECK(newG < globalSize);
820 }
821
822 // UnInitIndex passthrough
823 CHECK(lookup.resolve(UnInitIndex) == UnInitIndex);
824}
825
826// =================================================================
827// Test: ReorderEntities with external companion array
828// (solver-like use case: external array extends the registry)
829// =================================================================
830
831TEST_CASE("ReorderEntities with external companion array (solver-like)")
832{
833 auto mpi = worldMPI();
834 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
835
837
838 // Create an "external" array (like a solver DOF array), parallel to cells
839 // Each entry is tagged with the cell's current global index
841 solverDOF.InitPair("solverDOF", mpi);
843 // Borrow global mapping from cell2node
844 solverDOF.father->pLGlobalMapping = mesh->cell2node.father->pLGlobalMapping;
845
846 mesh->AdjLocal2GlobalPrimary();
847 DNDS::index cellOffset = (*mesh->cell2node.father->pLGlobalMapping)(mpi.rank, 0);
848
849 // Tag each DOF with a pattern: (cellGlobal*10+0, cellGlobal*10+1, cellGlobal*10+2)
850 for (DNDS::index i = 0; i < nCellBefore; i++)
851 {
852 solverDOF(i, 0) = (cellOffset + i) * 10 + 0;
853 solverDOF(i, 1) = (cellOffset + i) * 10 + 1;
854 solverDOF(i, 2) = (cellOffset + i) * 10 + 2;
855 }
856
857 // Snapshot pre-reorder: map oldGlobal -> expected tag pattern
858 std::map<DNDS::index, std::array<DNDS::index, 3>> expectedByOldGlobal;
859 for (DNDS::index i = 0; i < nCellBefore; i++)
860 {
862 expectedByOldGlobal[g] = {g * 10, g * 10 + 1, g * 10 + 2};
863 }
864
865 // Plan + register external companion + apply
866 // Use round-robin partition to force distributed movement (when np>=2)
867 std::vector<MPI_int> cellPartition(nCellBefore);
868 for (DNDS::index i = 0; i < nCellBefore; i++)
869 cellPartition[i] = static_cast<MPI_int>((mpi.rank + i) % mpi.size);
870
872 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
873
874 // Build the plan (with default Node/Bnd follows)
875 auto plan = mesh->buildReorderPlan(input);
876
877 // Build registry and register the EXTERNAL companion BEFORE applying
878 auto reg = mesh->buildReorderRegistry(input.destroyKinds);
879 reg.registerCompanion(
880 EntityKind::Cell,
881 [&](const PermutationTransfer &t, const MPIInfo &m)
882 { t.transferRows(solverDOF, m); },
883 "solverDOF");
884
885 // Call the full mesh reorder (which uses its own registry internally;
886 // for external companion we must use buildReorderPlan + apply + manual rebuild).
887 //
888 // Simpler: apply plan to our extended registry directly, then rebuild mesh ghosts.
889 // However ReorderEntities does a lot of rebuild work. To test just the external
890 // companion path, we exercise it through the synthetic pattern:
891 plan.apply(reg, mpi);
892
893 // After apply, solverDOF has been relocated. Verify values preserved per old-global.
894 // New cells live at new globals — compute new-global -> old-global via plan transfer
895 const auto &transfer = plan.transfers.at(EntityKind::Cell);
896
897 // Build reverse map: new global -> old global (on this rank, post-reorder)
898 // After transferRows, solverDOF row i is at new local slot i with new global
899 // (myNewOffset + i). We need to know which old global was sent to new local slot i.
900 //
901 // The sender info isn't directly exposed; we use the tag pattern to verify.
902 // The received tag % 10 == 0,1,2 (col 0,1,2), and tag / 10 == oldGlobal.
903 DNDS::index nAfter = solverDOF.father->Size();
904 for (DNDS::index i = 0; i < nAfter; i++)
905 {
906 DNDS::index v0 = solverDOF(i, 0);
907 DNDS::index v1 = solverDOF(i, 1);
908 DNDS::index v2 = solverDOF(i, 2);
909 // All three entries must come from the same source cell
910 CHECK(v0 % 10 == 0);
911 CHECK(v1 % 10 == 1);
912 CHECK(v2 % 10 == 2);
913 CHECK(v0 / 10 == v1 / 10);
914 CHECK(v0 / 10 == v2 / 10);
915 }
916}
917
918// =================================================================
919// Test: RELOCATE_REMAP — both source and target reordered
920// (cell2node where BOTH cells and nodes move)
921// =================================================================
922
923TEST_CASE("ReorderPlan::apply RELOCATE_REMAP (both source and target reordered)")
924{
925 auto mpi = worldMPI();
928
929 // cell2node: Cell -> Node. Both will be reordered.
931 cell2node.InitPair("cell2node", mpi);
933 cell2node.father->createGlobalMapping();
934
936 nodeArr.InitPair("nodeArr", mpi);
938 nodeArr.father->createGlobalMapping();
939
940 DNDS::index cellOffset = (*cell2node.father->pLGlobalMapping)(mpi.rank, 0);
941 DNDS::index nodeOffset = (*nodeArr.father->pLGlobalMapping)(mpi.rank, 0);
942
943 // Fill: cell i references nodes i and i+1 (in local indexing converted to global)
944 for (DNDS::index i = 0; i < nCell; i++)
945 {
946 cell2node(i, 0) = nodeOffset + i;
947 cell2node(i, 1) = nodeOffset + i + 1;
948 }
949
950 // Snapshot pre-reorder cell2node for each cell by its old global
951 std::map<DNDS::index, std::array<DNDS::index, 2>> oldCell2NodeByGlobal;
952 for (DNDS::index i = 0; i < nCell; i++)
954
955 // Build registry
957 reg.registerGlobalMapping(EntityKind::Cell, cell2node.father->pLGlobalMapping);
958 reg.registerGlobalMapping(EntityKind::Node, nodeArr.father->pLGlobalMapping);
959
960 reg.registerAdj(
962 [&](const PermutationTransfer::LookupResult &lookup)
963 {
964 for (DNDS::index i = 0; i < cell2node.father->Size(); i++)
965 for (rowsize j = 0; j < 2; j++)
966 {
968 if (v != UnInitIndex)
969 v = lookup.resolve(v);
970 }
971 },
972 [&](const PermutationTransfer &t, const MPIInfo &m)
973 { t.transferRows(cell2node, m); },
974 "cell2node");
975
976 // Identity partitions (local-only) but with REMAP logic exercised
977 std::vector<MPI_int> cellPartition(nCell, mpi.rank);
978 std::vector<MPI_int> nodePartition(nNode, mpi.rank);
979
981 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Cell, cellPartition});
982 input.explicitMaps.push_back(EntityReorderMap{EntityKind::Node, nodePartition});
983
984 auto plan = ReorderPlan::build(input, reg, mpi);
985 CHECK(plan.reorderedKinds.count(EntityKind::Cell));
986 CHECK(plan.reorderedKinds.count(EntityKind::Node));
987 CHECK(classifyAdj(Adj::Cell2Node, plan.reorderedKinds) == AdjAction::RELOCATE_REMAP);
988
989 plan.apply(reg, mpi);
990
991 // With identity partitions, new globals == old globals, so values should be unchanged.
992 for (DNDS::index i = 0; i < nCell; i++)
993 {
994 CHECK(cell2node(i, 0) == nodeOffset + i);
995 CHECK(cell2node(i, 1) == nodeOffset + i + 1);
996 }
997}
998
999// =================================================================
1000// Test: pullSets population in buildReorderRegistry
1001// =================================================================
1002
1003TEST_CASE("buildReorderRegistry populates pullSets for off-rank references")
1004{
1005 auto mpi = worldMPI();
1006 if (mpi.size < 2)
1007 return;
1008
1009 auto mesh = buildMeshPrimary(mpi, "UniformSquare_10.cgns", 2, false);
1010 mesh->AdjLocal2GlobalPrimary();
1011
1012 auto reg = mesh->buildReorderRegistry({});
1013
1014 // Cell pullSet: should contain off-rank cell globals referenced by
1015 // cell2cell, bnd2cell, or node2cell ghost rows.
1016 auto cellGMIt = reg.globalMappings.find(EntityKind::Cell);
1017 REQUIRE(cellGMIt != reg.globalMappings.end());
1018 auto cellGM = cellGMIt->second;
1019 DNDS::index myCellOffset = (*cellGM)(mpi.rank, 0);
1020 DNDS::index myCellCount = cellGM->RLengths()[mpi.rank];
1021
1022 auto psIt = reg.pullSets.find(EntityKind::Cell);
1023 if (psIt != reg.pullSets.end())
1024 {
1025 const auto &ps = psIt->second;
1026 // All entries must be off-rank
1027 for (auto g : ps)
1028 {
1029 bool isOffRank = !(g >= myCellOffset && g < myCellOffset + myCellCount);
1030 CHECK(isOffRank);
1031 // Must be valid global
1032 CHECK(g >= 0);
1033 CHECK(g < cellGM->globalSize());
1034 }
1035 // Must be sorted and unique
1036 for (size_t i = 1; i < ps.size(); i++)
1037 {
1038 CHECK(ps[i] > ps[i - 1]);
1039 }
1040 }
1041
1042 // Node pullSet: off-rank node globals referenced by cell2node, bnd2node
1043 auto nodeGMIt = reg.globalMappings.find(EntityKind::Node);
1044 REQUIRE(nodeGMIt != reg.globalMappings.end());
1045 auto nodeGM = nodeGMIt->second;
1046 DNDS::index myNodeOffset = (*nodeGM)(mpi.rank, 0);
1047 DNDS::index myNodeCount = nodeGM->RLengths()[mpi.rank];
1048
1049 auto nodePsIt = reg.pullSets.find(EntityKind::Node);
1050 if (nodePsIt != reg.pullSets.end())
1051 {
1052 const auto &ps = nodePsIt->second;
1053 for (auto g : ps)
1054 {
1055 bool isOffRank = !(g >= myNodeOffset && g < myNodeOffset + myNodeCount);
1056 CHECK(isOffRank);
1057 CHECK(g >= 0);
1058 CHECK(g < nodeGM->globalSize());
1059 }
1060 for (size_t i = 1; i < ps.size(); i++)
1061 {
1062 CHECK(ps[i] > ps[i - 1]);
1063 }
1064 }
1065
1066 // Confirm all expected adj entries are registered
1067 auto hasAdj = [&](AdjKind k)
1068 {
1069 for (auto &adj : reg.adjs)
1070 if (adj.kind == k)
1071 return true;
1072 return false;
1073 };
1074 CHECK(hasAdj(Adj::Cell2Node));
1075 CHECK(hasAdj(Adj::Cell2Cell));
1076 CHECK(hasAdj(Adj::Bnd2Node));
1077 CHECK(hasAdj(Adj::Bnd2Cell));
1078
1079 // Confirm companions are registered (coords, cellElemInfo, bndElemInfo)
1080 std::set<std::string> compNames;
1081 for (auto &c : reg.companions)
1082 compNames.insert(c.name);
1083 CHECK(compNames.count("coords"));
1084 CHECK(compNames.count("cellElemInfo"));
1085 CHECK(compNames.count("bndElemInfo"));
1086}
Eigen::Matrix< real, 3, 3 > m
Distributed entity reordering framework: ReorderRegistry, ReorderPlan, ReorderInput.
int main()
Definition dummy.cpp:3
constexpr AdjKind Bnd2Node
constexpr AdjKind Cell2Node
constexpr AdjKind Node2Cell
constexpr AdjKind Face2Cell
constexpr AdjKind Cell2Face
constexpr AdjKind Node2Bnd
constexpr AdjKind Cell2Cell
constexpr AdjKind Face2Node
constexpr AdjKind Bnd2Cell
AdjAction classifyAdj(AdjKind adj, const std::unordered_set< EntityKind > &reorderedKinds)
Classify an adjacency given the set of reordered entity kinds.
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
std::shared_ptr< T > ssp
Shortened alias for std::shared_ptr used pervasively in DNDSR.
Definition Defines.hpp:143
int MPI_int
MPI counterpart type for MPI_int (= C int). Used for counts and ranks in MPI calls.
Definition MPI.hpp:54
t_arrayDeviceView father
Convenience bundle of a father, son, and attached ArrayTransformer.
ssp< TArray > father
Owned-side array (must be resized before ghost setup).
void InitPair(const std::string &name, Args &&...args)
Allocate both father and son arrays, forwarding all args to TArray constructor.
Per-entity reorder specification: where each owned entity goes.
Input to the reorder framework.
std::vector< EntityReorderMap > explicitMaps
Explicit reorder maps (caller-provided).
std::unordered_set< EntityKind > destroyKinds
static ReorderPlan build(const ReorderInput &input, const ReorderRegistry &registry, const MPIInfo &mpi)
void registerGlobalMapping(EntityKind kind, ssp< GlobalOffsetsMapping > gm)
Register a GlobalOffsetsMapping for an entity kind.
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:231
Result of buildLookup: ghost-pullable old-global -> new-global map.
static PermutationTransfer fromPartition(const std::vector< MPI_int > &partition, const ssp< GlobalOffsetsMapping > &oldGlobalMapping, const MPIInfo &mpi)
void transferRows(TPair &pair, const MPIInfo &mpi) const
static PermutationTransfer fromLocalPermutation(const std::vector< index > &old2new, const ssp< GlobalOffsetsMapping > &oldGlobalMapping, const MPIInfo &mpi)
Eigen::Matrix< real, 5, 1 > v
constexpr DNDS::index nLocal
for(DNDS::index i=0;i< nLocal;i++) for(DNDS j< dynCols;j++)(*father)(i, j)=(gOff+i) *0.1+j *0.001;DNDS::index gSize=father-> globalSize()
auto adj
auto result
REQUIRE(bool(result.parent2entityPbi.father))
auto reg
auto plan
DNDS::index nAfter
CHECK(plan.reorderedKinds.count(EntityKind::Cell))
std::vector< MPI_int > nodePartition(nNode, mpi.rank)
ArrayAdjacencyPair< 1 > nodeArr
ArrayAdjacencyPair< 3 > solverDOF
std::vector< MPI_int > cellPartition(nCellBefore)
std::map< DNDS::index, std::array< DNDS::index, 3 > > expectedByOldGlobal
std::map< DNDS::index, std::array< DNDS::index, 2 > > oldCell2NodeByGlobal
DNDS::index cellOffset
DNDS::index nodeOffset
DNDS::index idx
const DNDS::index nCell
const DNDS::index nNode
const auto & transfer
ReorderInput input
auto mesh
ArrayAdjacencyPair< 2 > cell2node
DNDS::index nCellBefore
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")