DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_IndexMapping.cpp
Go to the documentation of this file.
1/**
2 * @file test_IndexMapping.cpp
3 * @brief Doctest-based unit tests for DNDS::GlobalOffsetsMapping and
4 * DNDS::OffsetAscendIndexMapping.
5 *
6 * Run under mpirun with 1, 2, and 4 ranks. Tests offset computation,
7 * local-to-global and global-to-local mapping, and search operations
8 * for both uniform and non-uniform distributions.
9 *
10 * @see @ref dnds_unit_tests for the full test-suite overview.
11 * @test GlobalOffsetsMapping (uniform, non-uniform, search),
12 * OffsetAscendIndexMapping (pull-based, search_indexAppend,
13 * empty ghost set).
14 */
15
16#define DOCTEST_CONFIG_IMPLEMENT
17#include "doctest.h"
18#include "DNDS/IndexMapping.hpp"
19#include "DNDS/MPI.hpp"
20#include <numeric>
21
22using namespace DNDS;
23
24int main(int argc, char **argv)
25{
26 MPI_Init(&argc, &argv);
27 doctest::Context ctx;
28 ctx.applyCommandLine(argc, argv);
29 int res = ctx.run();
30 MPI_Finalize();
31 return res;
32}
33
34// ---------------------------------------------------------------------------
35// Helpers
36// ---------------------------------------------------------------------------
37
38static MPIInfo worldMPI()
39{
41 mpi.setWorld();
42 return mpi;
43}
44
45// ===================================================================
46// GlobalOffsetsMapping
47// ===================================================================
48
49TEST_CASE("GlobalOffsetsMapping uniform distribution")
50{
51 MPIInfo mpi = worldMPI();
52 const DNDS::index nLocal = 100;
53
55 gm.setMPIAlignBcast(mpi, nLocal);
56
57 SUBCASE("globalSize equals nLocal * size")
58 {
59 CHECK(gm.globalSize() == nLocal * mpi.size);
60 }
61
62 SUBCASE("RLengths all equal nLocal")
63 {
64 auto &lens = gm.RLengths();
65 REQUIRE(static_cast<int>(lens.size()) == mpi.size);
66 for (int r = 0; r < mpi.size; r++)
67 CHECK(lens[r] == nLocal);
68 }
69
70 SUBCASE("ROffsets are multiples of nLocal")
71 {
72 auto &offs = gm.ROffsets();
73 REQUIRE(static_cast<int>(offs.size()) == mpi.size + 1);
74 for (int r = 0; r <= mpi.size; r++)
75 CHECK(offs[r] == static_cast<DNDS::index>(r) * nLocal);
76 }
77
78 SUBCASE("operator() returns correct global index")
79 {
80 for (MPI_int r = 0; r < mpi.size; r++)
81 {
82 CHECK(gm(r, 0) == static_cast<DNDS::index>(r) * nLocal);
83 CHECK(gm(r, 99) == static_cast<DNDS::index>(r) * nLocal + 99);
84 }
85 }
86}
87
88// -------------------------------------------------------------------
89
90TEST_CASE("GlobalOffsetsMapping non-uniform distribution")
91{
92 MPIInfo mpi = worldMPI();
93
94 // Rank r owns (r+1)*10 elements
95 DNDS::index myLength = static_cast<DNDS::index>(mpi.rank + 1) * 10;
96
98 gm.setMPIAlignBcast(mpi, myLength);
99
100 // Expected total: sum_{r=0}^{size-1} (r+1)*10 = 10 * size*(size+1)/2
101 DNDS::index expectedTotal = 10 * static_cast<DNDS::index>(mpi.size) * (mpi.size + 1) / 2;
102
103 SUBCASE("globalSize matches expected sum")
104 {
105 CHECK(gm.globalSize() == expectedTotal);
106 }
107
108 SUBCASE("offsets are cumulative sums of lengths")
109 {
110 auto &offs = gm.ROffsets();
111 auto &lens = gm.RLengths();
112 CHECK(offs[0] == 0);
113 for (int r = 0; r < mpi.size; r++)
114 {
115 CHECK(lens[r] == static_cast<DNDS::index>(r + 1) * 10);
116 CHECK(offs[r + 1] == offs[r] + lens[r]);
117 }
118 }
119
120 SUBCASE("search recovers rank and local index for each rank's first element")
121 {
122 auto &offs = gm.ROffsets();
123 for (MPI_int r = 0; r < mpi.size; r++)
124 {
125 auto [found, rank, loc] = gm.search(offs[r]);
126 CHECK(found);
127 CHECK(rank == r);
128 CHECK(loc == 0);
129 }
130 }
131
132 SUBCASE("search recovers rank and local index for each rank's last element")
133 {
134 auto &offs = gm.ROffsets();
135 auto &lens = gm.RLengths();
136 for (MPI_int r = 0; r < mpi.size; r++)
137 {
138 DNDS::index lastGlobal = offs[r] + lens[r] - 1;
139 auto [found, rank, loc] = gm.search(lastGlobal);
140 CHECK(found);
141 CHECK(rank == r);
142 CHECK(loc == lens[r] - 1);
143 }
144 }
145}
146
147// -------------------------------------------------------------------
148
149TEST_CASE("GlobalOffsetsMapping search")
150{
151 MPIInfo mpi = worldMPI();
152 const DNDS::index nLocal = 25;
153
155 gm.setMPIAlignBcast(mpi, nLocal);
156
157 SUBCASE("search for first global index")
158 {
159 auto [found, rank, loc] = gm.search(0);
160 CHECK(found);
161 CHECK(rank == 0);
162 CHECK(loc == 0);
163 }
164
165 SUBCASE("search for last global index")
166 {
167 DNDS::index lastGlobal = gm.globalSize() - 1;
168 auto [found, rank, loc] = gm.search(lastGlobal);
169 CHECK(found);
170 CHECK(rank == mpi.size - 1);
171 CHECK(loc == nLocal - 1);
172 }
173
174 SUBCASE("search for a middle global index")
175 {
176 // Pick the middle element of rank 0's range
177 DNDS::index mid = nLocal / 2;
178 auto [found, rank, loc] = gm.search(mid);
179 CHECK(found);
180 CHECK(rank == 0);
181 CHECK(loc == mid);
182 }
183
184 SUBCASE("search across rank boundary")
185 {
186 if (mpi.size > 1)
187 {
188 // First element of rank 1
189 auto [found, rank, loc] = gm.search(nLocal);
190 CHECK(found);
191 CHECK(rank == 1);
192 CHECK(loc == 0);
193 }
194 }
195
196 SUBCASE("search out-of-range returns false")
197 {
198 auto [found, rank, loc] = gm.search(gm.globalSize());
199 CHECK_FALSE(found);
200 }
201
202 SUBCASE("search for negative index returns false")
203 {
204 auto [found, rank, loc] = gm.search(-1);
205 CHECK_FALSE(found);
206 }
207
208 SUBCASE("search with reference outputs matches tuple outputs")
209 {
210 for (DNDS::index g = 0; g < gm.globalSize(); g += nLocal)
211 {
212 auto [found1, rank1, loc1] = gm.search(g);
213 MPI_int rank2{-1};
214 DNDS::index loc2{-1};
215 bool found2 = gm.search(g, rank2, loc2);
216 CHECK(found1 == found2);
217 CHECK(rank1 == rank2);
218 CHECK(loc1 == loc2);
219 }
220 }
221}
222
223// ===================================================================
224// OffsetAscendIndexMapping — pull-based construction
225// ===================================================================
226
227TEST_CASE("OffsetAscendIndexMapping pull-based construction")
228{
229 MPIInfo mpi = worldMPI();
230 const DNDS::index nLocal = 50;
231
233 gm.setMPIAlignBcast(mpi, nLocal);
234
235 DNDS::index myOffset = gm(mpi.rank, 0);
236
237 // Build a pull set: request the first 3 elements from every other rank
238 std::vector<DNDS::index> pullIdx;
239 for (MPI_int r = 0; r < mpi.size; r++)
240 {
241 if (r == mpi.rank)
242 continue;
243 DNDS::index base = gm(r, 0);
244 for (DNDS::index i = 0; i < 3; i++)
245 pullIdx.push_back(base + i);
246 }
247
248 OffsetAscendIndexMapping mapping(myOffset, nLocal, std::move(pullIdx), gm, mpi);
249
250 SUBCASE("searchInMain finds local indices")
251 {
252 for (DNDS::index i = 0; i < nLocal; i++)
253 {
254 DNDS::index val = -1;
255 bool found = mapping.searchInMain(myOffset + i, val);
256 CHECK(found);
257 CHECK(val == i);
258 }
259 }
260
261 SUBCASE("searchInMain rejects out-of-range")
262 {
263 DNDS::index val = -1;
264 if (mpi.size > 1)
265 {
266 MPI_int other = (mpi.rank + 1) % mpi.size;
267 bool found = mapping.searchInMain(gm(other, 0), val);
268 CHECK_FALSE(found);
269 }
270 }
271
272 SUBCASE("searchInGhost finds pulled indices per rank")
273 {
274 for (MPI_int r = 0; r < mpi.size; r++)
275 {
276 if (r == mpi.rank)
277 continue;
278 DNDS::index base = gm(r, 0);
279 for (DNDS::index i = 0; i < 3; i++)
280 {
281 DNDS::index val = -1;
282 bool found = mapping.searchInGhost(base + i, r, val);
283 CHECK(found);
284 CHECK(val == i);
285 }
286 }
287 }
288
289 SUBCASE("searchInAllGhost finds pulled indices")
290 {
291 for (MPI_int r = 0; r < mpi.size; r++)
292 {
293 if (r == mpi.rank)
294 continue;
295 DNDS::index base = gm(r, 0);
296 for (DNDS::index i = 0; i < 3; i++)
297 {
298 MPI_int foundRank = -1;
299 DNDS::index val = -1;
300 bool found = mapping.searchInAllGhost(base + i, foundRank, val);
301 CHECK(found);
302 CHECK(foundRank == r);
303 CHECK(val >= 0);
304 }
305 }
306 }
307
308 SUBCASE("search dispatches to main (rank==-1) and ghost correctly")
309 {
310 // Search for a local element
311 {
312 auto [found, rank, val] = mapping.search(myOffset + 5);
313 CHECK(found);
314 CHECK(rank == -1);
315 CHECK(val == 5);
316 }
317
318 // Search for a ghost element
319 if (mpi.size > 1)
320 {
321 MPI_int other = (mpi.rank + 1) % mpi.size;
322 DNDS::index ghostGlobal = gm(other, 0);
323 auto [found, rank, val] = mapping.search(ghostGlobal);
324 CHECK(found);
325 CHECK(rank != -1); // ghost, not main
326 }
327 }
328
329 SUBCASE("operator() reverse mapping for main data")
330 {
331 for (DNDS::index i = 0; i < nLocal; i++)
332 {
333 DNDS::index global = mapping(-1, i);
334 CHECK(global == myOffset + i);
335 }
336 }
337
338 SUBCASE("operator() reverse mapping for ghost data")
339 {
340 // Recover ghost global indices via operator()(rank, val) where
341 // val is relative to ghostStart[rank]
342 for (MPI_int r = 0; r < mpi.size; r++)
343 {
344 if (r == mpi.rank)
345 continue;
346 DNDS::index base = gm(r, 0);
347 for (DNDS::index i = 0; i < 3; i++)
348 {
349 DNDS::index global = mapping(r, i);
350 CHECK(global == base + i);
351 }
352 }
353 }
354}
355
356// ===================================================================
357// OffsetAscendIndexMapping search_indexAppend
358// ===================================================================
359
360TEST_CASE("OffsetAscendIndexMapping search_indexAppend")
361{
362 MPIInfo mpi = worldMPI();
363 const DNDS::index nLocal = 50;
364
366 gm.setMPIAlignBcast(mpi, nLocal);
367
368 DNDS::index myOffset = gm(mpi.rank, 0);
369
370 // Pull first 4 elements from every other rank
371 std::vector<DNDS::index> pullIdx;
372 for (MPI_int r = 0; r < mpi.size; r++)
373 {
374 if (r == mpi.rank)
375 continue;
376 DNDS::index base = gm(r, 0);
377 for (DNDS::index i = 0; i < 4; i++)
378 pullIdx.push_back(base + i);
379 }
380
381 OffsetAscendIndexMapping mapping(myOffset, nLocal, std::move(pullIdx), gm, mpi);
382
383 SUBCASE("search_indexAppend returns local val for main data")
384 {
385 auto [found, rank, val] = mapping.search_indexAppend(myOffset + 10);
386 CHECK(found);
387 CHECK(rank == -1);
388 CHECK(val == 10);
389 }
390
391 SUBCASE("search_indexAppend offsets ghost val by mainSize")
392 {
393 if (mpi.size > 1)
394 {
395 MPI_int other = (mpi.rank + 1) % mpi.size;
396 DNDS::index ghostGlobal = gm(other, 0);
397
398 // First, get the raw ghost val from search()
399 auto [found1, rank1, rawVal] = mapping.search(ghostGlobal);
400 REQUIRE(found1);
401
402 // Now get the appended val
403 auto [found2, rank2, appendVal] = mapping.search_indexAppend(ghostGlobal);
404 REQUIRE(found2);
405 CHECK(rank2 == rank1);
406 CHECK(appendVal == rawVal + nLocal);
407 }
408 }
409
410 SUBCASE("search_indexAppend with reference outputs matches tuple outputs")
411 {
412 if (mpi.size > 1)
413 {
414 MPI_int other = (mpi.rank + 1) % mpi.size;
415 DNDS::index ghostGlobal = gm(other, 1);
416
417 auto [found1, rank1, val1] = mapping.search_indexAppend(ghostGlobal);
418
419 MPI_int rank2{-1};
420 DNDS::index val2{-1};
421 bool found2 = mapping.search_indexAppend(ghostGlobal, rank2, val2);
422
423 CHECK(found1 == found2);
424 CHECK(rank1 == rank2);
425 CHECK(val1 == val2);
426 }
427 }
428}
429
430// ===================================================================
431// OffsetAscendIndexMapping — empty ghost set
432// ===================================================================
433
434TEST_CASE("OffsetAscendIndexMapping empty ghost set")
435{
436 MPIInfo mpi = worldMPI();
437 const DNDS::index nLocal = 50;
438
440 gm.setMPIAlignBcast(mpi, nLocal);
441
442 DNDS::index myOffset = gm(mpi.rank, 0);
443
444 // Pull nothing
445 std::vector<DNDS::index> emptyPull;
446 OffsetAscendIndexMapping mapping(myOffset, nLocal, std::move(emptyPull), gm, mpi);
447
448 SUBCASE("searchInMain still works")
449 {
450 for (DNDS::index i = 0; i < nLocal; i++)
451 {
452 DNDS::index val = -1;
453 bool found = mapping.searchInMain(myOffset + i, val);
454 CHECK(found);
455 CHECK(val == i);
456 }
457 }
458
459 SUBCASE("searchInGhost returns false for any query")
460 {
461 for (MPI_int r = 0; r < mpi.size; r++)
462 {
463 DNDS::index val = -1;
464 bool found = mapping.searchInGhost(gm(r, 0), r, val);
465 CHECK_FALSE(found);
466 }
467 }
468
469 SUBCASE("searchInAllGhost returns false")
470 {
471 if (mpi.size > 1)
472 {
473 MPI_int other = (mpi.rank + 1) % mpi.size;
474 MPI_int foundRank = -1;
475 DNDS::index val = -1;
476 bool found = mapping.searchInAllGhost(gm(other, 0), foundRank, val);
477 CHECK_FALSE(found);
478 }
479 }
480
481 SUBCASE("search falls back to main only")
482 {
483 auto [found, rank, val] = mapping.search(myOffset);
484 CHECK(found);
485 CHECK(rank == -1);
486 CHECK(val == 0);
487
488 if (mpi.size > 1)
489 {
490 MPI_int other = (mpi.rank + 1) % mpi.size;
491 auto [found2, rank2, val2] = mapping.search(gm(other, 0));
492 CHECK_FALSE(found2);
493 }
494 }
495
496 SUBCASE("operator() still works for main data")
497 {
498 for (DNDS::index i = 0; i < nLocal; i++)
499 CHECK(mapping(-1, i) == myOffset + i);
500 }
501}
502
503// ===================================================================
504// Parametric tests over local sizes
505// ===================================================================
506
507template <DNDS::index N>
509{
510 static constexpr DNDS::index n = N;
511};
512
517
518TEST_CASE_TEMPLATE("Parametric GlobalOffsetsMapping", Tag,
520{
521 MPIInfo mpi = worldMPI();
522 constexpr DNDS::index nLocal = Tag::n;
523
525 gm.setMPIAlignBcast(mpi, nLocal);
526
527 SUBCASE("globalSize equals nLocal * size")
528 {
529 CHECK(gm.globalSize() == nLocal * mpi.size);
530 }
531
532 SUBCASE("search first element of each rank")
533 {
534 auto &offs = gm.ROffsets();
535 for (MPI_int r = 0; r < mpi.size; r++)
536 {
537 auto [found, rank, loc] = gm.search(offs[r]);
538 CHECK(found);
539 CHECK(rank == r);
540 CHECK(loc == 0);
541 }
542 }
543
544 SUBCASE("operator() returns correct global index")
545 {
546 for (MPI_int r = 0; r < mpi.size; r++)
547 {
548 CHECK(gm(r, 0) == static_cast<DNDS::index>(r) * nLocal);
549 CHECK(gm(r, nLocal - 1) == static_cast<DNDS::index>(r) * nLocal + nLocal - 1);
550 }
551 }
552}
553
554TEST_CASE_TEMPLATE("Parametric OffsetAscendIndexMapping", Tag,
556{
557 MPIInfo mpi = worldMPI();
558 constexpr DNDS::index nLocal = Tag::n;
559
561 gm.setMPIAlignBcast(mpi, nLocal);
562
563 DNDS::index myOffset = gm(mpi.rank, 0);
564
565 // Pull the first 3 elements from every other rank.
566 std::vector<DNDS::index> pullIdx;
567 for (MPI_int r = 0; r < mpi.size; r++)
568 {
569 if (r == mpi.rank)
570 continue;
571 DNDS::index base = gm(r, 0);
572 for (DNDS::index i = 0; i < 3; i++)
573 pullIdx.push_back(base + i);
574 }
575
576 OffsetAscendIndexMapping mapping(myOffset, nLocal, std::move(pullIdx), gm, mpi);
577
578 SUBCASE("searchInMain finds local indices")
579 {
580 for (DNDS::index i = 0; i < nLocal; i++)
581 {
582 DNDS::index val = -1;
583 bool found = mapping.searchInMain(myOffset + i, val);
584 CHECK(found);
585 CHECK(val == i);
586 }
587 }
588
589 SUBCASE("searchInGhost finds pulled ghost indices")
590 {
591 for (MPI_int r = 0; r < mpi.size; r++)
592 {
593 if (r == mpi.rank)
594 continue;
595 DNDS::index base = gm(r, 0);
596 for (DNDS::index i = 0; i < 3; i++)
597 {
598 DNDS::index val = -1;
599 bool found = mapping.searchInGhost(base + i, r, val);
600 CHECK(found);
601 CHECK(val == i);
602 }
603 }
604 }
605
606 SUBCASE("operator() reverse mapping for main data")
607 {
608 for (DNDS::index i = 0; i < nLocal; i++)
609 {
610 DNDS::index global = mapping(-1, i);
611 CHECK(global == myOffset + i);
612 }
613 }
614
615 SUBCASE("operator() reverse mapping for ghost data")
616 {
617 for (MPI_int r = 0; r < mpi.size; r++)
618 {
619 if (r == mpi.rank)
620 continue;
621 DNDS::index base = gm(r, 0);
622 for (DNDS::index i = 0; i < 3; i++)
623 {
624 DNDS::index global = mapping(r, i);
625 CHECK(global == base + i);
626 }
627 }
628 }
629}
Global-to-local index mapping for distributed arrays.
MPI wrappers: MPIInfo, collective operations, type mapping, CommStrategy.
Table mapping rank-local row indices to the global index space.
index globalSize() const
Total number of rows across all ranks.
t_IndexVec & ROffsets()
Prefix-sum offsets vector (size == nRanks + 1, last element == globalSize).
void setMPIAlignBcast(const MPIInfo &mpi, index myLength)
Broadcast each rank's length, then compute the global prefix sums.
bool search(index globalQuery, MPI_int &rank, index &val) const
Convert a global index to (rank, local). Returns false if out of range.
t_IndexVec & RLengths()
Per-rank lengths vector (size == nRanks).
Mapping between a rank's main data and its ghost copies.
bool search_indexAppend(index globalQuery, MPI_int &rank, index &val) const
Like search, but the returned val is in the concatenated father+son address space.
bool searchInGhost(index globalQuery, MPI_int rank, index &val) const
Search a single peer rank's ghost slab for a global index.
bool searchInMain(index globalQuery, index &val) const
Check whether a global index lies within the main block.
bool search(index globalQuery, MPI_int &rank, index &val) const
Search main + ghost in one call. rank == -1 signals "hit in main".
bool searchInAllGhost(index globalQuery, MPI_int &rank, index &val) const
Search the concatenated ghost array for a global index (O(log nGhost)).
int main()
Definition dummy.cpp:3
the host side operators are provided as implemented
int64_t index
Global row / DOF index type (signed 64-bit; handles multi-billion-cell meshes).
Definition Defines.hpp:107
int MPI_int
MPI counterpart type for MPI_int (= C int). Used for counts and ranks in MPI calls.
Definition MPI.hpp:43
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:215
static constexpr DNDS::index n
constexpr DNDS::index N
constexpr DNDS::index nLocal
tVec r(NCells)
TYPE_TO_STRING(SizeTag< 10 >)
TEST_CASE_TEMPLATE("Parametric GlobalOffsetsMapping", Tag, SizeTag< 10 >, SizeTag< 50 >, SizeTag< 200 >, SizeTag< 1000 >)
CHECK(result.size()==3)
auto res
Definition test_ODE.cpp:196
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")