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
block_octree.hpp
Go to the documentation of this file.
1// core — per-block adaptive octree with block-local Morton codes.
2//
3// The suite's AMR primitive. Unlike the textbook linear octree (one global
4// space-filling curve, partitioned by index ranges à la p4est/Dendro), the suite
5// keeps the ORB *block* decomposition (peclet::core::decomp::BlockDecomposer) and gives
6// *each block its own local Morton coordinate system*. BlockOctree is that
7// per-block octree: leaves are addressed by `morton::Morton<Dim,Bits>` codes
8// relative to the block origin, so codes stay narrow and the block is
9// self-contained. The distributed glue (cross-block 2:1 balance + an owner-based
10// ghost halo) is layered on top later (DistributedOctree / AmrHalo); a uniform,
11// unrefined BlockOctree is bit-identical to the existing structured block grid.
12//
13// Conventions (morton's level convention; see also ../../../docs)
14// -----------------------------------------------------------------------------
15// * Coordinates are in *fine units*: 1 fine unit = a leaf at level 0. A leaf at
16// `level` L covers a 2^L block per axis; its origin Morton code has the low
17// L*Dim bits zero (morton's level convention).
18// * Root cells sit at `level = lmax`; refinement decreases level toward 0
19// (finest). A block is a "root brick" of brick[i] root cells per axis, so it
20// spans brick[i]*2^lmax fine units; require 2^Bits >= brick[i]*2^lmax.
21// * Leaves tile the block without overlap and are stored sorted by code, i.e. in
22// Z-order — the same order morton's curve and the device leaf arrays use.
23//
24// checkout is present). The query helpers carry morton's MORTON_HD, so they are
25// device-callable exactly when morton is (KOKKOS_FUNCTION under a Kokkos build);
26// topology mutation (refine/coarsen/balance) is host-side and rebuilds the leaf
27// arrays the device queries read — mirroring grid_halo.hpp (host) vs
28// grid_halo_kokkos.hpp (device).
29#ifndef PECLET_AMR_BLOCK_OCTREE_HPP
30#define PECLET_AMR_BLOCK_OCTREE_HPP
31
32#include <algorithm>
33#include <array>
34#include <cassert>
35#include <cstdint>
36#include <vector>
37
38#include "morton/morton.hpp"
39#include "peclet/amr/common.hpp"
40#include "peclet/core/common/types.hpp"
41
42namespace peclet::amr {
43
50template <class M>
51MORTON_HD inline Index amrLocate(const typename M::code_type* codes, const std::uint8_t* levels,
52 Index n, typename M::code_type p) {
53 // First index with codes[idx] > p, then step back one.
54 Index lo = 0, hi = n;
55 while (lo < hi) {
56 Index mid = lo + ((hi - lo) >> 1);
57 if (codes[mid] <= p)
58 lo = mid + 1;
59 else
60 hi = mid;
61 }
62 Index idx = lo - 1;
63 if (idx < 0)
64 return -1;
65 // p is inside leaf idx iff clearing the leaf's low bits gives its origin.
66 if (M::from_code(p).ancestor(levels[idx]).code() == codes[idx])
67 return idx;
68 return -1;
69}
70
76template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
78 public:
79 using M = morton::Morton<Dim, Bits>;
80 using Code = typename M::code_type;
81 using Coord = typename M::coord_type;
82 static constexpr unsigned octants = M::octants; // children per node (2^Dim)
83
84 BlockOctree() = default;
85
93
95 brick_ = brick;
96 lmax_ = lmax;
97 globalOrigin_ = globalOrigin;
98 const Coord rootSpan = Coord(Coord(1) << lmax_); // fine units per root cell
99 codes_.clear();
100 levels_.clear();
101 IVec<Dim> bgn{};
102 IVec<Dim> end = brick_;
103 forEachInBox<Dim>(bgn, end, [&](const IVec<Dim>& r) {
104 std::array<Coord, Dim> o{};
105 for (int i = 0; i < Dim; ++i)
106 o[i] = static_cast<Coord>(r[i]) * rootSpan;
107 codes_.push_back(M::encode(o).code());
108 levels_.push_back(static_cast<std::uint8_t>(lmax_));
109 });
110 sortByCode();
111 }
112
118 void assign(IVec<Dim> brick, unsigned lmax, IVec<Dim> globalOrigin, std::vector<Code> codes,
119 std::vector<std::uint8_t> levels) {
120 brick_ = brick;
121 lmax_ = lmax;
122 globalOrigin_ = globalOrigin;
123 codes_ = std::move(codes);
124 levels_ = std::move(levels);
125 sortByCode();
126 }
127
128 // ---- introspection -----------------------------------------------------
129 Index numLeaves() const { return static_cast<Index>(codes_.size()); }
130 unsigned lmax() const { return lmax_; }
131 static constexpr unsigned bits() { return Bits; }
132 const IVec<Dim>& brick() const { return brick_; }
133 const IVec<Dim>& globalOrigin() const { return globalOrigin_; }
134 Code code(Index i) const { return codes_[static_cast<std::size_t>(i)]; }
135 unsigned level(Index i) const { return levels_[static_cast<std::size_t>(i)]; }
136 const std::vector<Code>& codes() const { return codes_; }
137 const std::vector<std::uint8_t>& levels() const { return levels_; }
138
139 // ---- queries -----------------------------------------------------------
140
142 Index find(Code p) const { return amrLocate<M>(codes_.data(), levels_.data(), numLeaves(), p); }
144 Index find(const std::array<Coord, Dim>& fine) const { return find(M::encode(fine).code()); }
145
147 std::array<std::array<Coord, Dim>, 2> bounds(Index i) const {
148 std::array<std::array<Coord, Dim>, 2> b{};
149 auto o = M::from_code(codes_[static_cast<std::size_t>(i)]).decode();
150 Coord extent = Coord((Coord(1) << levels_[static_cast<std::size_t>(i)]) - 1);
151 for (int d = 0; d < Dim; ++d) {
152 b[0][d] = o[d];
153 b[1][d] = Coord(o[d] + extent);
154 }
155 return b;
156 }
157
161 Index faceNeighbor(Index i, int axis, int dir) const {
162 M probe = M::from_code(codes_[static_cast<std::size_t>(i)]);
163 const Coord step = Coord(Coord(1) << levels_[static_cast<std::size_t>(i)]);
164 if (dir >= 0) {
165 if (!probe.try_add(static_cast<unsigned>(axis), step))
166 return -1; // block edge
167 } else {
168 if (!probe.try_sub(static_cast<unsigned>(axis), 1))
169 return -1; // one past lower face
170 }
171 return find(probe.code());
172 }
173
174 // ---- refinement / coarsening ------------------------------------------
175
181 template <class Pred>
183 std::vector<Code> nc;
184 std::vector<std::uint8_t> nl;
185 nc.reserve(codes_.size());
186 nl.reserve(levels_.size());
187 Index nsplit = 0;
188 for (std::size_t k = 0; k < codes_.size(); ++k) {
189 const unsigned L = levels_[k];
190 const Code c = codes_[k];
191 if (L > 0 && pred(c, L)) {
192 M parent = M::from_code(c);
193 for (unsigned oct = 0; oct < octants; ++oct) {
194 nc.push_back(parent.child(L, oct).code());
195 nl.push_back(static_cast<std::uint8_t>(L - 1));
196 }
197 ++nsplit;
198 } else {
199 nc.push_back(c);
200 nl.push_back(static_cast<std::uint8_t>(L));
201 }
202 }
203 codes_.swap(nc);
204 levels_.swap(nl);
205 return nsplit;
206 }
207
210 const Code target = codes_[static_cast<std::size_t>(i)];
211 return refineIf([&](Code c, unsigned) { return c == target; }) > 0;
212 }
213
217 template <class Pred>
219 std::vector<Code> nc;
220 std::vector<std::uint8_t> nl;
221 nc.reserve(codes_.size());
222 nl.reserve(levels_.size());
223 Index nmerged = 0;
224 std::size_t k = 0;
225 while (k < codes_.size()) {
226 const unsigned L = levels_[k];
227 bool merged = false;
228 if (L < lmax_ && k + octants <= codes_.size()) {
229 const Code parent = M::from_code(codes_[k]).ancestor(L + 1).code();
230 bool full = (codes_[k] == parent); // octant-0 child shares the parent origin
231 for (unsigned oct = 0; full && oct < octants; ++oct) {
232 full = levels_[k + oct] == L &&
233 M::from_code(codes_[k + oct]).ancestor(L + 1).code() == parent &&
234 M::from_code(parent).child(L + 1, oct).code() == codes_[k + oct];
235 }
236 if (full && pred(parent, L + 1)) {
237 nc.push_back(parent);
238 nl.push_back(static_cast<std::uint8_t>(L + 1));
239 k += octants;
240 ++nmerged;
241 merged = true;
242 }
243 }
244 if (!merged) {
245 nc.push_back(codes_[k]);
246 nl.push_back(static_cast<std::uint8_t>(L));
247 ++k;
248 }
249 }
250 codes_.swap(nc);
251 levels_.swap(nl);
252 return nmerged;
253 }
254
255 // ---- root lift (multigrid below the root brick) -------------------------
256
264 bool canLiftRoot() const {
265 for (int d = 0; d < Dim; ++d)
266 if ((brick_[d] % 2) != 0 || (globalOrigin_[d] % 2) != 0)
267 return false;
268 return true;
269 }
270
282 void liftRoot() {
284 for (int d = 0; d < Dim; ++d) {
285 brick_[d] /= 2;
286 globalOrigin_[d] /= 2;
287 }
288 ++lmax_;
289 }
290
297 Index total = 0;
298 for (;;) {
299 std::vector<Code> toRefine; // origins of leaves to split this round
300 for (std::size_t k = 0; k < codes_.size(); ++k) {
301 const unsigned Lf = levels_[k];
302 for (int axis = 0; axis < Dim; ++axis) {
303 for (int dir = -1; dir <= 1; dir += 2) {
304 Index nb = faceNeighbor(static_cast<Index>(k), axis, dir);
305 if (nb >= 0 && levels_[static_cast<std::size_t>(nb)] >= Lf + 2)
306 toRefine.push_back(codes_[static_cast<std::size_t>(nb)]);
307 }
308 }
309 }
310 if (toRefine.empty())
311 break;
312 std::sort(toRefine.begin(), toRefine.end());
313 toRefine.erase(std::unique(toRefine.begin(), toRefine.end()), toRefine.end());
314 total += refineIf([&](Code c, unsigned) {
315 return std::binary_search(toRefine.begin(), toRefine.end(), c);
316 });
317 }
318 return total;
319 }
320
323 bool isBalanced() const {
324 for (std::size_t k = 0; k < codes_.size(); ++k) {
325 const unsigned Lf = levels_[k];
326 for (int axis = 0; axis < Dim; ++axis)
327 for (int dir = -1; dir <= 1; dir += 2) {
328 Index nb = faceNeighbor(static_cast<Index>(k), axis, dir);
329 if (nb >= 0) {
330 unsigned Ln = levels_[static_cast<std::size_t>(nb)];
331 unsigned hi = Lf > Ln ? Lf : Ln, lo = Lf > Ln ? Ln : Lf;
332 if (hi - lo > 1)
333 return false;
334 }
335 }
336 }
337 return true;
338 }
339
340 private:
341 void sortByCode() {
342 std::vector<Index> ord(codes_.size());
343 for (std::size_t i = 0; i < ord.size(); ++i)
344 ord[i] = static_cast<Index>(i);
345 std::sort(ord.begin(), ord.end(), [&](Index a, Index b) { return codes_[a] < codes_[b]; });
346 std::vector<Code> nc(codes_.size());
347 std::vector<std::uint8_t> nl(levels_.size());
348 for (std::size_t i = 0; i < ord.size(); ++i) {
349 nc[i] = codes_[static_cast<std::size_t>(ord[i])];
350 nl[i] = levels_[static_cast<std::size_t>(ord[i])];
351 }
352 codes_.swap(nc);
353 levels_.swap(nl);
354 }
355
356 IVec<Dim> brick_{};
357 IVec<Dim> globalOrigin_{};
358 unsigned lmax_ = 0;
359 std::vector<Code> codes_; // leaf origin codes, ascending (Z-order)
360 std::vector<std::uint8_t> levels_; // parallel to codes_
361};
362
363} // namespace peclet::amr
364
365#endif // PECLET_AMR_BLOCK_OCTREE_HPP
Per-block adaptive octree over block-local Morton codes.
static constexpr unsigned bits()
Index find(const std::array< Coord, Dim > &fine) const
Leaf containing fine-unit coordinates, or -1.
Index balance2to1()
Enforce 2:1 (graded) balance within this block: no two face-adjacent leaves differ by more than one l...
bool isBalanced() const
True iff the leaves form a valid 2:1-balanced partition: every face neighbour differs by at most one ...
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...
bool refineLeaf(Index i)
Refine a single leaf by index. Returns true if it was split.
Code code(Index i) const
const IVec< Dim > & brick() 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
const IVec< Dim > & globalOrigin() const
BlockOctree(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin=IVec< Dim >{})
Build a uniform block: a brick (in root cells) of leaves at level lmax.
Index coarsenIf(Pred &&pred)
Merge complete sibling groups (all 2^Dim children present, all at the same level) whose parent satisf...
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
const std::vector< std::uint8_t > & levels() const
void init(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin=IVec< Dim >{})
const std::vector< Code > & codes() const
std::array< std::array< Coord, Dim >, 2 > bounds(Index i) const
Inclusive integer bounds [lo, hi] of leaf i in fine units.
typename M::coord_type Coord
typename M::code_type Code
static constexpr unsigned octants
bool canLiftRoot() const
True iff the root brick can be halved on every axis: brick() and globalOrigin() both even everywhere.
Index faceNeighbor(Index i, int axis, int dir) const
The leaf across leaf i's face on axis in direction dir (±1), or -1 if it lies outside the block.
Index amrLocate(const typename M::code_type *codes, const std::uint8_t *levels, Index n, typename M::code_type p)
Locate the leaf containing Morton code p in sorted leaf arrays.
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