peclet-amr 0.2.0
Block-local-Morton AMR octree + collocated cut-cell Navier–Stokes solver (header-only C++20, Kokkos + MPI)
Loading...
Searching...
No Matches
distributed_octree.hpp
Go to the documentation of this file.
1// core — distributed adaptive octree over the ORB block decomposition.
2//
3// The suite keeps the ORB *block* decomposition and gives each block its own
4// local octree (block_octree.hpp). DistributedOctree ties the per-rank blocks
5// together: ORB (peclet::core::decomp::BlockDecomposer) assigns a brick of global root
6// cells to each rank, each rank builds a BlockOctree with the matching world
7// geometry, and this class supplies the two pieces of distributed glue:
8//
9// * balance() — cross-block 2:1 (graded) balance to a *global*
10// fixpoint. Local balance + an owner-based NBX round
11// (a leaf at level Lf tells the owner of the cell just
12// across each block face its level; the owner refines
13// any covering leaf at level >= Lf+2), iterated until
14// no rank refines. Detection is from the fine side, so
15// a single probe per face is complete.
16// * faceNeighborGather() — owner-based ghost exchange: for every local leaf and
17// face it returns the neighbouring leaf's field value,
18// local or remote (a two-round NBX request/reply). For
19// a uniform octree this is exactly the structured-grid
20// face-neighbour halo; the same call works graded.
21//
22// Owner lookups go through BlockDecomposer::ownerOf on global root-cell
23// coordinates — the same owner-based, no-Cartesian-assumption pattern GridHaloTopology
24// uses. Self-addressed messages are handled locally (no MPI send-to-self).
25//
26// real MPI or the single-rank stub) and the NBX engine.
27#ifndef PECLET_AMR_DISTRIBUTED_OCTREE_HPP
28#define PECLET_AMR_DISTRIBUTED_OCTREE_HPP
29
30#include <algorithm>
31#include <array>
32#include <cstdint>
33#include <cstring>
34#include <map>
35#include <stdexcept>
36#include <utility>
37#include <vector>
38
39#include "morton/morton.hpp"
41#include "peclet/amr/common.hpp"
43#include "peclet/core/common/mpi.hpp"
44#include "peclet/core/common/types.hpp"
45#include "peclet/core/decomp/block_decomposer.hpp"
46#include "peclet/core/halo/nbx.hpp"
47
48namespace peclet::amr {
49
50template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
52 public:
54 using M = typename Octree::M;
55 using Code = typename Octree::Code;
56 using Coord = typename Octree::Coord;
57
58 static constexpr double kNoNeighbor = -1e300;
59
60 DistributedOctree() = default;
61
65 std::array<bool, Dim> periodic, MPI_Comm comm = MPI_COMM_WORLD) {
66 comm_ = comm;
67 MPI_Comm_rank(comm_, &rank_);
68 MPI_Comm_size(comm_, &size_);
69 globalRootSize_ = globalRootSize;
70 lmax_ = lmax;
71 globalGeo_ = globalGeo;
72 periodic_ = periodic;
73 rootSpan_ = Index(1) << lmax_;
74
75 dec_.init(static_cast<std::size_t>(size_), globalRootSize_);
76 auto blk = dec_.block(static_cast<std::size_t>(rank_));
77 blockOriginRoot_ = blk.origin;
78 blockBrick_ = blk.size;
79 for (int d = 0; d < Dim; ++d) {
80 blockFineOrigin_[d] = blockOriginRoot_[d] * rootSpan_;
81 blockFineSize_[d] = blockBrick_[d] * rootSpan_;
82 globalFineSize_[d] = globalRootSize_[d] * rootSpan_;
83 }
84 local_.init(blockBrick_, lmax_, blockOriginRoot_);
85 }
86
95 void initDecomposed(const decomp::BlockDecomposer<Dim>& dec, unsigned lmax,
96 AmrGeometry<Dim> globalGeo, std::array<bool, Dim> periodic, MPI_Comm comm) {
97 comm_ = comm;
98 MPI_Comm_rank(comm_, &rank_);
99 MPI_Comm_size(comm_, &size_);
100 if (dec.numBlocks() != static_cast<std::size_t>(size_))
101 throw std::invalid_argument(
102 "amr::DistributedOctree::initDecomposed: the decomposition needs one block per rank");
103 globalRootSize_ = dec.globalSize();
104 lmax_ = lmax;
105 globalGeo_ = globalGeo;
106 periodic_ = periodic;
107 rootSpan_ = Index(1) << lmax_;
108 dec_ = dec;
109 auto blk = dec_.block(static_cast<std::size_t>(rank_));
110 blockOriginRoot_ = blk.origin;
111 blockBrick_ = blk.size;
112 for (int d = 0; d < Dim; ++d) {
113 blockFineOrigin_[d] = blockOriginRoot_[d] * rootSpan_;
114 blockFineSize_[d] = blockBrick_[d] * rootSpan_;
115 globalFineSize_[d] = globalRootSize_[d] * rootSpan_;
116 }
117 local_.init(blockBrick_, lmax_, blockOriginRoot_);
118 }
119
120 // ---- accessors ---------------------------------------------------------
121 Octree& local() { return local_; }
122 const Octree& local() const { return local_; }
123 int rank() const { return rank_; }
124 int size() const { return size_; }
125 MPI_Comm comm() const { return comm_; }
126 unsigned lmax() const { return lmax_; }
127 const IVec<Dim>& blockOriginRoot() const { return blockOriginRoot_; }
128 const IVec<Dim>& blockBrick() const { return blockBrick_; }
129 const IVec<Dim>& globalRootSize() const { return globalRootSize_; }
130 const IVec<Dim>& blockFineOrigin() const { return blockFineOrigin_; }
131 const IVec<Dim>& globalFineSize() const { return globalFineSize_; }
132 const std::array<bool, Dim>& periodic() const { return periodic_; }
133 Index rootSpan() const { return rootSpan_; }
139 const decomp::BlockDecomposer<Dim>& decomposition() const { return dec_; }
141 const Vec<Dim>& h0() const { return globalGeo_.h0; }
143 double h0Scalar() const { return globalGeo_.h0[0]; }
144 const AmrGeometry<Dim>& globalGeometry() const { return globalGeo_; }
145
148 auto o = M::from_code(local_.code(i)).decode();
149 IVec<Dim> g{};
150 for (int d = 0; d < Dim; ++d)
151 g[d] = static_cast<Index>(o[d]) / rootSpan_ + blockOriginRoot_[d];
152 return g;
153 }
154
158 std::array<Coord, Dim> lc{};
159 for (int d = 0; d < Dim; ++d) {
160 long v = g[d] - blockOriginRoot_[d];
161 if (v < 0 || v >= blockBrick_[d])
162 return -1;
163 lc[d] = static_cast<Coord>(v * rootSpan_);
164 }
165 return local_.find(M::encode(lc).code());
166 }
167
168 // ---- root lift: a multigrid level below the root brick (docs/amr_mg_depth.md §6.4) ------
169
175 bool canLiftRoot() const {
176 for (int d = 0; d < Dim; ++d)
177 if ((globalRootSize_[d] % 2) != 0 || (blockOriginRoot_[d] % 2) != 0 ||
178 (blockBrick_[d] % 2) != 0)
179 return false;
180 return local_.canLiftRoot();
181 }
182
202 void liftRoot() {
204 for (int d = 0; d < Dim; ++d)
205 ratio[d] = 2;
206 dec_ = dec_.coarsened(ratio);
207 for (int d = 0; d < Dim; ++d) {
208 globalRootSize_[d] /= 2;
209 blockOriginRoot_[d] /= 2;
210 blockBrick_[d] /= 2;
211 }
212 rootSpan_ *= 2;
213 ++lmax_;
214 local_.liftRoot();
215 }
216
219 AmrGeometry<Dim> g = globalGeo_;
220 for (int d = 0; d < Dim; ++d)
221 g.origin[d] =
222 globalGeo_.origin[d] + static_cast<Real>(blockFineOrigin_[d]) * globalGeo_.h0[d];
223 return g;
224 }
225
228 auto b = local_.bounds(i);
229 std::array<Coord, Dim> g{};
230 for (int d = 0; d < Dim; ++d)
231 g[d] = static_cast<Coord>(b[0][d] + blockFineOrigin_[d]);
232 return M::encode(g).code();
233 }
234
235 // ---- cross-block 2:1 balance ------------------------------------------
236
240 Index grandTotal = 0;
241 for (;;) {
242 Index work = local_.balance2to1();
243
244 std::map<int, std::vector<char>> sendBufs;
245 std::vector<Code> toRefine;
246 auto consider = [&](const std::array<Coord, Dim>& gc, int Lf) {
247 Index leaf = locateGlobal(gc);
248 if (leaf >= 0 && static_cast<int>(local_.level(leaf)) >= Lf + 2)
249 toRefine.push_back(local_.code(leaf));
250 };
251
252 for (Index i = 0; i < local_.numLeaves(); ++i) {
253 const int Lf = static_cast<int>(local_.level(i));
254 for (int axis = 0; axis < Dim; ++axis)
255 for (int dir = -1; dir <= 1; dir += 2) {
256 std::array<Coord, Dim> gc{};
257 int owner = -1;
258 Index lnb = -1;
259 if (neighborInfo(i, axis, dir, gc, owner, lnb) != Remote)
260 continue;
261 if (owner == rank_)
262 consider(gc, Lf);
263 else
264 appendQuery(sendBufs[owner], gc, Lf);
265 }
266 }
267
268 halo::NbxEngine eng(comm_);
269 auto it = sendBufs.begin();
270 eng.exchange(
271 [&](std::vector<char>& out) -> int {
272 if (it == sendBufs.end())
273 return -1;
274 out = it->second;
275 int dest = it->first;
276 ++it;
277 return dest;
278 },
279 [&](int, std::vector<char>& msg) {
280 parseQueries(msg, [&](const std::array<Coord, Dim>& gc, int Lf) { consider(gc, Lf); });
281 });
282
283 std::sort(toRefine.begin(), toRefine.end());
284 toRefine.erase(std::unique(toRefine.begin(), toRefine.end()), toRefine.end());
285 Index nref = local_.refineIf([&](Code c, unsigned) {
286 return std::binary_search(toRefine.begin(), toRefine.end(), c);
287 });
288 work += nref;
289 grandTotal += nref;
290
291 long lw = static_cast<long>(work), gw = 0;
292 MPI_Allreduce(&lw, &gw, 1, MPI_LONG, MPI_SUM, comm_);
293 if (gw == 0)
294 break;
295 }
296 return grandTotal;
297 }
298
299 // ---- dynamic load re-balancing ----------------------------------------
300
312 Index rebalance(std::vector<std::vector<double>>& fields) {
313 const int K = static_cast<int>(fields.size());
314 const Index n = local_.numLeaves();
315
316 // 1. Weight grid: number of octree leaves under each *global root cell*,
317 // agreed across ranks (blocks are disjoint, so a SUM-Allreduce of the
318 // zero-padded local counts yields the full global grid).
319 std::size_t ncells = 1;
320 for (int d = 0; d < Dim; ++d)
321 ncells *= static_cast<std::size_t>(globalRootSize_[d]);
322 std::vector<double> localWeight(ncells, 0.0), weight(ncells, 0.0);
323 for (Index i = 0; i < n; ++i)
324 localWeight[static_cast<std::size_t>(dec_.linearGlobal(globalRootOf(i)))] += 1.0;
325 MPI_Allreduce(localWeight.data(), weight.data(), static_cast<int>(ncells), MPI_DOUBLE, MPI_SUM,
326 comm_);
327
328 // 2. Weighted re-decomposition over the same global root grid, COARSE-FIRST ALIGNED to the
329 // largest 2^a the 1.05 weight-imbalance budget allows (core's chooseAlignedWeighted,
330 // docs/amr_mg_core_boundary.md §11.4 / §11.5): every split, origin and size is then a
331 // multiple of 2^a, so the pressure ladder keeps a in-place lifts before a stage has to move
332 // the level. a = 0 is today's plain weighted ORB bit for bit. A pure function of the
333 // replicated weight grid, so every rank chooses the same partition without communicating.
334 auto choice =
335 decomp::chooseAlignedWeighted(static_cast<std::size_t>(size_), globalRootSize_, weight);
336 rebalanceAlign_ = choice.a;
337 rebalanceImbalance_ = choice.imbalance;
338 decomp::BlockDecomposer<Dim> newDec = std::move(choice.dec);
339 auto nblk = newDec.block(static_cast<std::size_t>(rank_));
340 const IVec<Dim> newOriginRoot = nblk.origin;
341 const IVec<Dim> newBrick = nblk.size;
343 for (int d = 0; d < Dim; ++d)
344 newFineOrigin[d] = newOriginRoot[d] * rootSpan_;
345
346 // 3. Classify every local leaf by its new owner; keep mine, pack the rest.
347 // Leaves are carried by *global* code so the receiver can rebase them.
348 std::vector<Code> codes;
349 std::vector<std::uint8_t> levels;
350 std::vector<std::vector<double>> cols(static_cast<std::size_t>(K));
351 std::map<int, std::vector<char>> sendBufs;
352 Index migratedOut = 0;
353 for (Index i = 0; i < n; ++i) {
354 const Code gc = globalCode(i);
355 const std::uint8_t lv = static_cast<std::uint8_t>(local_.level(i));
356 const int newOwner = newDec.ownerOf(globalRootOf(i));
357 if (newOwner == rank_) {
358 codes.push_back(gc);
359 levels.push_back(lv);
360 for (int c = 0; c < K; ++c)
361 cols[static_cast<std::size_t>(c)].push_back(fields[c][static_cast<std::size_t>(i)]);
362 } else {
363 appendLeaf(sendBufs[newOwner], gc, lv, fields, i, K);
364 ++migratedOut;
365 }
366 }
367
368 // 4. Deliver migrated leaves to their new owners (sparse NBX).
369 {
370 halo::NbxEngine eng(comm_);
371 auto it = sendBufs.begin();
372 eng.exchange(
373 [&](std::vector<char>& out) -> int {
374 if (it == sendBufs.end())
375 return -1;
376 out = it->second;
377 int dest = it->first;
378 ++it;
379 return dest;
380 },
381 [&](int, std::vector<char>& msg) {
382 parseLeaves(msg, K, [&](Code gc, std::uint8_t lv, const double* comps) {
383 codes.push_back(gc);
384 levels.push_back(lv);
385 for (int c = 0; c < K; ++c)
386 cols[static_cast<std::size_t>(c)].push_back(comps[c]);
387 });
388 });
389 }
390
391 // 5. Rebase global codes to the new block origin and sort into Z-order,
392 // carrying the field columns along with the permutation.
393 const std::size_t m = codes.size();
394 std::vector<Code> localCodes(m);
395 for (std::size_t j = 0; j < m; ++j) {
396 auto g = M::from_code(codes[j]).decode();
397 std::array<Coord, Dim> lc{};
398 for (int d = 0; d < Dim; ++d)
399 lc[d] = static_cast<Coord>(g[d] - newFineOrigin[d]);
400 localCodes[j] = M::encode(lc).code();
401 }
402 std::vector<std::size_t> ord(m);
403 for (std::size_t j = 0; j < m; ++j)
404 ord[j] = j;
405 std::sort(ord.begin(), ord.end(),
406 [&](std::size_t a, std::size_t b) { return localCodes[a] < localCodes[b]; });
407
408 std::vector<Code> sortedCodes(m);
409 std::vector<std::uint8_t> sortedLevels(m);
410 std::vector<std::vector<double>> sortedCols(static_cast<std::size_t>(K),
411 std::vector<double>(m));
412 for (std::size_t j = 0; j < m; ++j) {
414 sortedLevels[j] = levels[ord[j]];
415 for (int c = 0; c < K; ++c)
416 sortedCols[static_cast<std::size_t>(c)][j] = cols[static_cast<std::size_t>(c)][ord[j]];
417 }
418
419 // 6. Install the new decomposition, block geometry and local octree.
420 local_.assign(newBrick, lmax_, newOriginRoot, std::move(sortedCodes), std::move(sortedLevels));
421 dec_ = newDec;
422 blockOriginRoot_ = newOriginRoot;
423 blockBrick_ = newBrick;
424 blockFineOrigin_ = newFineOrigin;
425 for (int d = 0; d < Dim; ++d)
426 blockFineSize_[d] = newBrick[d] * rootSpan_;
427 // globalFineSize_ is decomposition-independent and unchanged.
428 for (int c = 0; c < K; ++c)
429 fields[c].swap(sortedCols[static_cast<std::size_t>(c)]);
430 return migratedOut;
431 }
432
437 int rebalanceAlignment() const { return rebalanceAlign_; }
438 double rebalanceImbalance() const { return rebalanceImbalance_; }
439
440 // ---- owner-based face-neighbour gather (the halo) ---------------------
441
445 std::vector<double> faceNeighborGather(const std::vector<double>& field,
446 double sentinel = kNoNeighbor) const {
447 const Index n = local_.numLeaves();
448 const int F = 2 * Dim;
449 std::vector<double> out(static_cast<std::size_t>(n) * F, sentinel);
450
451 // Phase 1: fill in-block / domain directly; build remote requests.
452 std::map<int, std::vector<char>> reqBufs;
453 for (Index i = 0; i < n; ++i)
454 for (int axis = 0; axis < Dim; ++axis)
455 for (int dir = -1; dir <= 1; dir += 2) {
456 const int slot = faceSlot(i, axis, dir, F);
457 std::array<Coord, Dim> gc{};
458 int owner = -1;
459 Index lnb = -1;
460 NbState st = neighborInfo(i, axis, dir, gc, owner, lnb);
461 if (st == InBlock) {
462 if (lnb >= 0)
463 out[static_cast<std::size_t>(slot)] = field[static_cast<std::size_t>(lnb)];
464 } else if (st == Remote) {
465 if (owner == rank_) {
466 Index leaf = locateGlobal(gc);
467 if (leaf >= 0)
468 out[static_cast<std::size_t>(slot)] = field[static_cast<std::size_t>(leaf)];
469 } else {
470 appendRequest(reqBufs[owner], gc, static_cast<std::int64_t>(slot));
471 }
472 } // DomainNone -> leave sentinel
473 }
474
475 // Phase 2: owners answer requests; requesters write replies into `out`.
476 std::map<int, std::vector<char>> replyBufs;
477 {
478 halo::NbxEngine eng(comm_);
479 auto it = reqBufs.begin();
480 eng.exchange(
481 [&](std::vector<char>& outb) -> int {
482 if (it == reqBufs.end())
483 return -1;
484 outb = it->second;
485 int dest = it->first;
486 ++it;
487 return dest;
488 },
489 [&](int src, std::vector<char>& msg) {
490 parseRequests(msg, [&](const std::array<Coord, Dim>& gc, std::int64_t reqId) {
491 Index leaf = locateGlobal(gc);
492 double v = (leaf >= 0) ? field[static_cast<std::size_t>(leaf)] : sentinel;
493 appendReply(replyBufs[src], reqId, v);
494 });
495 },
496 /*tag=*/11); // distinct tag: keep request/reply rounds from aliasing in NBX
497 }
498 {
499 halo::NbxEngine eng(comm_);
500 auto it = replyBufs.begin();
501 eng.exchange(
502 [&](std::vector<char>& outb) -> int {
503 if (it == replyBufs.end())
504 return -1;
505 outb = it->second;
506 int dest = it->first;
507 ++it;
508 return dest;
509 },
510 [&](int, std::vector<char>& msg) {
511 parseReplies(msg, [&](std::int64_t reqId, double v) {
512 out[static_cast<std::size_t>(reqId)] = v;
513 });
514 },
515 /*tag=*/12); // reply round: distinct tag from the request round (11)
516 }
517 return out;
518 }
519
528 std::vector<Index> directSlot, directLeaf;
529 std::vector<std::array<Coord, Dim>>
531 std::vector<Index> remoteSlot;
533 };
534
536 const Index n = local_.numLeaves();
537 const int F = 2 * Dim;
539 p.nFaces = n * F;
540 for (Index i = 0; i < n; ++i)
541 for (int axis = 0; axis < Dim; ++axis)
542 for (int dir = -1; dir <= 1; dir += 2) {
543 const int slot = faceSlot(i, axis, dir, F);
544 std::array<Coord, Dim> gc{};
545 int owner = -1;
546 Index lnb = -1;
547 NbState st = neighborInfo(i, axis, dir, gc, owner, lnb);
548 if (st == InBlock) {
549 if (lnb >= 0) {
550 p.directSlot.push_back(slot);
551 p.directLeaf.push_back(lnb);
552 }
553 } else if (st == Remote) {
554 // owner==rank resolves locally inside coverValues just as the un-planned path does, so
555 // both remote subcases route through remoteCoords — identical values, identical slots.
556 p.remoteCoords.push_back(gc);
557 p.remoteSlot.push_back(slot);
558 } // DomainNone -> leave sentinel
559 }
560 return p;
561 }
562
565 std::vector<double> faceNeighborGather(const FaceGatherPlan& plan,
566 const std::vector<double>& field,
567 double sentinel = kNoNeighbor) const {
568 std::vector<double> out(static_cast<std::size_t>(plan.nFaces), sentinel);
569 for (std::size_t k = 0; k < plan.directSlot.size(); ++k)
570 out[static_cast<std::size_t>(plan.directSlot[k])] =
571 field[static_cast<std::size_t>(plan.directLeaf[k])];
572 std::vector<double> vals = coverValues(plan.remoteCoords, field, sentinel);
573 for (std::size_t k = 0; k < plan.remoteSlot.size(); ++k)
574 out[static_cast<std::size_t>(plan.remoteSlot[k])] = vals[k];
575 return out;
576 }
577
578 // ---- device-resident gather halo topology (C2) -------------------------
579
585 // Locally-resolved faces (InBlock + Remote-but-owner==rank): out[localSlot[k]] =
586 // field[localLeaf[k]].
587 std::vector<Index> localSlot, localLeaf;
588 // Owner side of the value exchange: gather these local leaves (−1 ⇒ send sentinel) and send to
589 // the requester ranks (concatenated per sendRanks/sendCounts, in received reqId order).
590 std::vector<int> sendRanks, sendCounts;
591 std::vector<Index> sendLeaf;
592 // Requester side: scatter the received values into these out-slots (per recvRanks/recvCounts).
593 std::vector<int> recvRanks, recvCounts;
594 std::vector<Index> recvSlot;
596 };
597
604 t.nFaces = plan.nFaces;
605 t.localSlot = plan.directSlot; // InBlock direct fills
606 t.localLeaf = plan.directLeaf;
607 // Classify remote coords; group cross-rank requests by owner with a per-owner reqId = its
608 // position.
609 std::map<int, std::vector<char>> req; // owner -> serialized (coord, reqId) requests
610 std::map<int, std::vector<Index>> recvSlotByOwner; // owner -> out-slots in reqId order
611 for (std::size_t k = 0; k < plan.remoteCoords.size(); ++k) {
612 const std::array<Coord, Dim>& gc = plan.remoteCoords[k];
613 const Index slot = plan.remoteSlot[k];
614 const int owner = ownerOfFine(gc);
615 if (owner == rank_) {
616 const Index leaf = locateGlobal(gc);
617 if (leaf >= 0) {
618 t.localSlot.push_back(slot);
619 t.localLeaf.push_back(leaf);
620 } // leaf < 0 ⇒ leave sentinel
621 } else {
622 const std::int64_t reqId = static_cast<std::int64_t>(recvSlotByOwner[owner].size());
623 appendRequest(req[owner], gc, reqId);
624 recvSlotByOwner[owner].push_back(slot);
625 }
626 }
627 for (auto& kv : recvSlotByOwner) { // requester side (deterministic owner order from std::map)
628 t.recvRanks.push_back(kv.first);
629 t.recvCounts.push_back(static_cast<int>(kv.second.size()));
630 for (Index s : kv.second)
631 t.recvSlot.push_back(s);
632 }
633 // One NBX round: owners receive coord requests and resolve them to local leaves (in reqId
634 // order).
635 std::map<int, std::vector<Index>> sendLeafBySrc;
636 {
637 halo::NbxEngine eng(comm_);
638 auto it = req.begin();
639 eng.exchange(
640 [&](std::vector<char>& o) -> int {
641 if (it == req.end())
642 return -1;
643 o = it->second;
644 int d = it->first;
645 ++it;
646 return d;
647 },
648 [&](int src, std::vector<char>& msg) {
649 std::vector<Index>& leaves = sendLeafBySrc[src];
650 parseRequests(msg, [&](const std::array<Coord, Dim>& gc, std::int64_t reqId) {
651 const Index leaf = locateGlobal(gc);
652 if (static_cast<std::int64_t>(leaves.size()) <= reqId)
653 leaves.resize(static_cast<std::size_t>(reqId) + 1, -1);
654 leaves[static_cast<std::size_t>(reqId)] = leaf;
655 });
656 },
657 /*tag=*/31);
658 }
659 for (auto& kv : sendLeafBySrc) { // owner side
660 t.sendRanks.push_back(kv.first);
661 t.sendCounts.push_back(static_cast<int>(kv.second.size()));
662 for (Index l : kv.second)
663 t.sendLeaf.push_back(l);
664 }
665 return t;
666 }
667
668 // ---- by-coordinate owner gathers (graded consistent-operator halo) ----
669
672 struct FaceInfo {
673 int state = 2;
674 std::array<Coord, Dim> gc{};
675 int owner = -1;
677 };
678 FaceInfo faceAcross(Index i, int axis, int dir) const {
679 FaceInfo f;
680 f.state = static_cast<int>(neighborInfo(i, axis, dir, f.gc, f.owner, f.localNb));
681 return f;
682 }
683
686 std::vector<int> coverLevels(const std::vector<std::array<Coord, Dim>>& coords) const {
687 std::vector<double> out(coords.size(), -1.0);
688 std::map<int, std::vector<char>> req;
689 for (std::size_t idx = 0; idx < coords.size(); ++idx) {
690 int owner = ownerOfFine(coords[idx]);
691 if (owner == rank_) {
692 Index leaf = locateGlobal(coords[idx]);
693 out[idx] = (leaf >= 0) ? static_cast<double>(local_.level(leaf)) : -1.0;
694 } else {
695 appendRequest(req[owner], coords[idx], static_cast<std::int64_t>(idx));
696 }
697 }
698 requestReply(
699 req,
700 [&](const std::array<Coord, Dim>& gc) -> double {
701 Index leaf = locateGlobal(gc);
702 return (leaf >= 0) ? static_cast<double>(local_.level(leaf)) : -1.0;
703 },
704 out, /*tagA=*/21, /*tagB=*/22);
705 std::vector<int> lv(coords.size());
706 for (std::size_t i = 0; i < out.size(); ++i)
707 lv[i] = static_cast<int>(out[i]);
708 return lv;
709 }
710
713 std::vector<double> coverValues(const std::vector<std::array<Coord, Dim>>& coords,
714 const std::vector<double>& field,
715 double sentinel = kNoNeighbor) const {
716 std::vector<double> out(coords.size(), sentinel);
717 std::map<int, std::vector<char>> req;
718 for (std::size_t idx = 0; idx < coords.size(); ++idx) {
719 int owner = ownerOfFine(coords[idx]);
720 if (owner == rank_) {
721 Index leaf = locateGlobal(coords[idx]);
722 if (leaf >= 0)
723 out[idx] = field[static_cast<std::size_t>(leaf)];
724 } else {
725 appendRequest(req[owner], coords[idx], static_cast<std::int64_t>(idx));
726 }
727 }
728 requestReply(
729 req,
730 [&](const std::array<Coord, Dim>& gc) -> double {
731 Index leaf = locateGlobal(gc);
732 return (leaf >= 0) ? field[static_cast<std::size_t>(leaf)] : sentinel;
733 },
734 out, /*tagA=*/23, /*tagB=*/24);
735 return out;
736 }
737
738 private:
739 enum NbState { InBlock, Remote, DomainNone };
740
741 int ownerOfFine(const std::array<Coord, Dim>& gc) const {
743 for (int d = 0; d < Dim; ++d)
744 rootCell[d] = static_cast<Index>(gc[d]) / rootSpan_;
745 return dec_.ownerOf(rootCell);
746 }
747
751 template <class Responder>
752 void requestReply(std::map<int, std::vector<char>>& req, Responder&& respond,
753 std::vector<double>& out, int tagA, int tagB) const {
754 std::map<int, std::vector<char>> rep;
755 {
756 halo::NbxEngine eng(comm_);
757 auto it = req.begin();
758 eng.exchange(
759 [&](std::vector<char>& o) -> int {
760 if (it == req.end())
761 return -1;
762 o = it->second;
763 int d = it->first;
764 ++it;
765 return d;
766 },
767 [&](int src, std::vector<char>& msg) {
768 parseRequests(msg, [&](const std::array<Coord, Dim>& gc, std::int64_t reqId) {
769 appendReply(rep[src], reqId, respond(gc));
770 });
771 },
772 tagA);
773 }
774 {
775 halo::NbxEngine eng(comm_);
776 auto it = rep.begin();
777 eng.exchange(
778 [&](std::vector<char>& o) -> int {
779 if (it == rep.end())
780 return -1;
781 o = it->second;
782 int d = it->first;
783 ++it;
784 return d;
785 },
786 [&](int, std::vector<char>& msg) {
787 parseReplies(msg, [&](std::int64_t reqId, double v) {
788 out[static_cast<std::size_t>(reqId)] = v;
789 });
790 },
791 tagB);
792 }
793 }
794
795 static int faceSlot(Index i, int axis, int dir, int F) {
796 return static_cast<int>(i) * F + 2 * axis + (dir > 0 ? 0 : 1);
797 }
798
802 NbState neighborInfo(Index i, int axis, int dir, std::array<Coord, Dim>& gc, int& owner,
803 Index& localNb) const {
804 auto b = local_.bounds(i);
805 const auto& lo = b[0];
806 const Coord size = Coord(Coord(1) << local_.level(i));
807 const long pc = (dir > 0) ? static_cast<long>(lo[axis]) + static_cast<long>(size)
808 : static_cast<long>(lo[axis]) - 1;
809
810 if (pc >= 0 && pc < static_cast<long>(blockFineSize_[axis])) {
811 std::array<Coord, Dim> p = lo;
812 p[axis] = static_cast<Coord>(pc);
813 localNb = local_.find(M::encode(p).code());
814 return InBlock;
815 }
816
817 std::array<long, Dim> g{};
818 for (int d = 0; d < Dim; ++d)
819 g[d] = static_cast<long>(blockFineOrigin_[d]) + static_cast<long>(lo[d]);
820 g[axis] = static_cast<long>(blockFineOrigin_[axis]) + pc;
821 for (int d = 0; d < Dim; ++d) {
822 const long gf = static_cast<long>(globalFineSize_[d]);
823 if (g[d] < 0 || g[d] >= gf) {
824 if (!periodic_[d])
825 return DomainNone;
826 g[d] = ((g[d] % gf) + gf) % gf;
827 }
828 }
830 for (int d = 0; d < Dim; ++d) {
831 gc[d] = static_cast<Coord>(g[d]);
832 rootCell[d] = static_cast<Index>(g[d] / static_cast<long>(rootSpan_));
833 }
834 owner = dec_.ownerOf(rootCell);
835 return Remote;
836 }
837
840 Index locateGlobal(const std::array<Coord, Dim>& gc) const {
841 std::array<Coord, Dim> lc{};
842 for (int d = 0; d < Dim; ++d)
843 lc[d] = static_cast<Coord>(gc[d] - blockFineOrigin_[d]);
844 return local_.find(M::encode(lc).code());
845 }
846
847 // ---- message (de)serialization ----------------------------------------
848 static void appendBytes(std::vector<char>& buf, const void* p, std::size_t n) {
849 const char* c = static_cast<const char*>(p);
850 buf.insert(buf.end(), c, c + n);
851 }
852 static void appendQuery(std::vector<char>& buf, const std::array<Coord, Dim>& gc, int Lf) {
853 for (int d = 0; d < Dim; ++d) {
854 std::int64_t v = static_cast<std::int64_t>(gc[d]);
855 appendBytes(buf, &v, sizeof(v));
856 }
857 std::int32_t l = Lf;
858 appendBytes(buf, &l, sizeof(l));
859 }
860 template <class Fn>
861 static void parseQueries(const std::vector<char>& buf, Fn&& fn) {
862 std::size_t off = 0;
863 const std::size_t item = Dim * sizeof(std::int64_t) + sizeof(std::int32_t);
864 while (off + item <= buf.size()) {
865 std::array<Coord, Dim> gc{};
866 for (int d = 0; d < Dim; ++d) {
867 std::int64_t v;
868 std::memcpy(&v, buf.data() + off, sizeof(v));
869 off += sizeof(v);
870 gc[d] = static_cast<Coord>(v);
871 }
872 std::int32_t l;
873 std::memcpy(&l, buf.data() + off, sizeof(l));
874 off += sizeof(l);
875 fn(gc, static_cast<int>(l));
876 }
877 }
878 static void appendRequest(std::vector<char>& buf, const std::array<Coord, Dim>& gc,
879 std::int64_t reqId) {
880 for (int d = 0; d < Dim; ++d) {
881 std::int64_t v = static_cast<std::int64_t>(gc[d]);
882 appendBytes(buf, &v, sizeof(v));
883 }
884 appendBytes(buf, &reqId, sizeof(reqId));
885 }
886 template <class Fn>
887 static void parseRequests(const std::vector<char>& buf, Fn&& fn) {
888 std::size_t off = 0;
889 const std::size_t item = (Dim + 1) * sizeof(std::int64_t);
890 while (off + item <= buf.size()) {
891 std::array<Coord, Dim> gc{};
892 for (int d = 0; d < Dim; ++d) {
893 std::int64_t v;
894 std::memcpy(&v, buf.data() + off, sizeof(v));
895 off += sizeof(v);
896 gc[d] = static_cast<Coord>(v);
897 }
898 std::int64_t reqId;
899 std::memcpy(&reqId, buf.data() + off, sizeof(reqId));
900 off += sizeof(reqId);
901 fn(gc, reqId);
902 }
903 }
906 static void appendLeaf(std::vector<char>& buf, Code gc, std::uint8_t level,
907 const std::vector<std::vector<double>>& fields, Index i, int K) {
908 std::int64_t c = static_cast<std::int64_t>(gc);
909 appendBytes(buf, &c, sizeof(c));
910 std::int32_t l = static_cast<std::int32_t>(level);
911 appendBytes(buf, &l, sizeof(l));
912 for (int k = 0; k < K; ++k) {
913 double v = fields[static_cast<std::size_t>(k)][static_cast<std::size_t>(i)];
914 appendBytes(buf, &v, sizeof(v));
915 }
916 }
917 template <class Fn>
918 static void parseLeaves(const std::vector<char>& buf, int K, Fn&& fn) {
919 std::size_t off = 0;
920 const std::size_t item =
921 sizeof(std::int64_t) + sizeof(std::int32_t) + static_cast<std::size_t>(K) * sizeof(double);
922 std::vector<double> comps(static_cast<std::size_t>(K));
923 while (off + item <= buf.size()) {
924 std::int64_t c;
925 std::memcpy(&c, buf.data() + off, sizeof(c));
926 off += sizeof(c);
927 std::int32_t l;
928 std::memcpy(&l, buf.data() + off, sizeof(l));
929 off += sizeof(l);
930 for (int k = 0; k < K; ++k) {
931 std::memcpy(&comps[static_cast<std::size_t>(k)], buf.data() + off, sizeof(double));
932 off += sizeof(double);
933 }
934 fn(static_cast<Code>(c), static_cast<std::uint8_t>(l), comps.data());
935 }
936 }
937 static void appendReply(std::vector<char>& buf, std::int64_t reqId, double v) {
938 appendBytes(buf, &reqId, sizeof(reqId));
939 appendBytes(buf, &v, sizeof(v));
940 }
941 template <class Fn>
942 static void parseReplies(const std::vector<char>& buf, Fn&& fn) {
943 std::size_t off = 0;
944 const std::size_t item = sizeof(std::int64_t) + sizeof(double);
945 while (off + item <= buf.size()) {
946 std::int64_t reqId;
947 std::memcpy(&reqId, buf.data() + off, sizeof(reqId));
948 off += sizeof(reqId);
949 double v;
950 std::memcpy(&v, buf.data() + off, sizeof(v));
951 off += sizeof(v);
952 fn(reqId, v);
953 }
954 }
955
956 MPI_Comm comm_ = MPI_COMM_WORLD;
957 int rank_ = 0;
958 int size_ = 1;
959 decomp::BlockDecomposer<Dim> dec_;
960 Octree local_;
961 AmrGeometry<Dim> globalGeo_{};
962 IVec<Dim> globalRootSize_{};
963 IVec<Dim> blockOriginRoot_{};
964 IVec<Dim> blockBrick_{};
965 IVec<Dim> blockFineOrigin_{};
966 IVec<Dim> blockFineSize_{};
967 IVec<Dim> globalFineSize_{};
968 std::array<bool, Dim> periodic_{};
969 unsigned lmax_ = 0;
970 Index rootSpan_ = 1;
971 int rebalanceAlign_ = 0;
972 double rebalanceImbalance_ = 0.0;
973};
974
975} // namespace peclet::amr
976
977#endif // PECLET_AMR_DISTRIBUTED_OCTREE_HPP
Index balance2to1()
Enforce 2:1 (graded) balance within this block: no two face-adjacent leaves differ by more than one l...
Index refineIf(Pred &&pred)
Split every leaf for which pred(code, level) is true (and level > 0) into its 2^Dim children one leve...
void assign(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin, std::vector< Code > codes, std::vector< std::uint8_t > levels)
Replace the entire leaf set directly (used by load-balancing migration, which rebuilds a block from l...
Code code(Index i) const
morton::Morton< Dim, Bits > M
void liftRoot()
Lift the root one level: halve the root brick (and the block's global root origin) on every axis and ...
unsigned level(Index i) const
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
void init(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin=IVec< Dim >{})
std::array< std::array< Coord, Dim >, 2 > bounds(Index i) const
Inclusive integer bounds [lo, hi] of leaf i in fine units.
bool canLiftRoot() const
True iff the root brick can be halved on every axis: brick() and globalOrigin() both even everywhere.
std::vector< double > faceNeighborGather(const std::vector< double > &field, double sentinel=kNoNeighbor) const
For each local leaf and each of the 2*Dim faces, the neighbouring leaf's field value.
FaceGatherPlan buildFaceGatherPlan() const
const IVec< Dim > & globalRootSize() const
static constexpr double kNoNeighbor
gather sentinel for "no neighbour"
std::vector< int > coverLevels(const std::vector< std::array< Coord, Dim > > &coords) const
For each global fine coord (already wrapped into the domain), the level of the covering leaf on its o...
Index balance()
Bring the whole distributed octree to a 2:1-balanced state.
Index rebalance(std::vector< std::vector< double > > &fields)
Re-decompose by per-root-cell octree-leaf count and migrate leaves (with their field columns) to the ...
const Vec< Dim > & h0() const
Phase 3: the FINEST spacing per axis (docs/amr_anisotropic.md §2).
const AmrGeometry< Dim > & globalGeometry() const
AmrGeometry< Dim > localGeometry() const
World geometry of this rank's block (the global geometry shifted to its origin).
std::vector< double > coverValues(const std::vector< std::array< Coord, Dim > > &coords, const std::vector< double > &field, double sentinel=kNoNeighbor) const
For each global fine coord, the covering leaf's field value on its owner (sentinel if none).
const IVec< Dim > & blockFineOrigin() const
FaceInfo faceAcross(Index i, int axis, int dir) const
void init(IVec< Dim > globalRootSize, unsigned lmax, AmrGeometry< Dim > globalGeo, std::array< bool, Dim > periodic, MPI_Comm comm=MPI_COMM_WORLD)
Decompose a globalRootSize grid of root cells over the communicator and build this rank's local block...
const std::array< bool, Dim > & periodic() const
void initDecomposed(const decomp::BlockDecomposer< Dim > &dec, unsigned lmax, AmrGeometry< Dim > globalGeo, std::array< bool, Dim > periodic, MPI_Comm comm)
As init, but on a GIVEN decomposition of the global root grid dec.globalSize() instead of the proport...
void liftRoot()
Lift the root one level: the root CELL doubles on every axis, so the global root grid,...
const IVec< Dim > & globalFineSize() const
int rebalanceAlignment() const
The alignment exponent the last rebalance chose (align = 2^a on every axis; 0 = the plain weighted OR...
Index findGlobalRoot(const IVec< Dim > &g) const
Local leaf covering global root-cell g, or -1 if not owned by this rank (lmax==0).
BlockOctree< Dim, Bits > Octree
const decomp::BlockDecomposer< Dim > & decomposition() const
The ORB over the global ROOT grid in the CURRENT root units — rank r owns block r.
std::vector< double > faceNeighborGather(const FaceGatherPlan &plan, const std::vector< double > &field, double sentinel=kNoNeighbor) const
Face-neighbour gather using a precomputed FaceGatherPlan: only the field values move,...
double h0Scalar() const
The single spacing of a CUBIC global grid.
bool canLiftRoot() const
True iff THIS rank's block nests one level deeper: the global root grid, this block's origin and this...
Code globalCode(Index i) const
Global Morton code of local leaf i (origin in global fine coordinates).
GatherHaloTopology buildGatherHaloTopology(const FaceGatherPlan &plan) const
Build the value-only gather topology from a FaceGatherPlan: classify each remote coord by owner (owne...
const IVec< Dim > & blockBrick() const
IVec< Dim > globalRootOf(Index i) const
Global root-cell coordinate of local leaf i (lmax==0: the leaf is one root cell).
const IVec< Dim > & blockOriginRoot() const
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
Definition adapt.hpp:51
World-space placement of a block-local octree: fine coordinate (0,..,0) sits at origin,...
Static topology of the face-neighbour gather: which out-slots are filled from a local leaf (directSlo...
std::vector< Index > remoteSlot
out[remoteSlot[k]] = covered value
std::vector< Index > directLeaf
out[directSlot[k]] = field[directLeaf[k]]
std::vector< std::array< Coord, Dim > > remoteCoords
owner-gathered coords (incl. owner==rank)
Classification of leaf i's face on (axis,dir): {state, global fine probe gc, owner,...
Flattened, value-only topology for the face-neighbour gather, established ONCE: the per-matvec exchan...