core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
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 (see also morton/octree and ../../../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// Header-only, guarded by PECLET_CORE_HAVE_MORTON (set by CMake when the morton sibling
25// checkout is present). The query helpers carry morton's MORTON_HD, so they are
26// device-callable exactly when morton is (KOKKOS_FUNCTION under a Kokkos build);
27// topology mutation (refine/coarsen/balance) is host-side and rebuilds the leaf
28// arrays the device queries read — mirroring grid_halo.hpp (host) vs
29// grid_halo_kokkos.hpp (device).
30#ifndef PECLET_CORE_AMR_BLOCK_OCTREE_HPP
31#define PECLET_CORE_AMR_BLOCK_OCTREE_HPP
32
33#ifdef PECLET_CORE_HAVE_MORTON
34
35#include <algorithm>
36#include <array>
37#include <cstdint>
38#include <vector>
39
40#include "morton/morton.hpp"
42
43namespace peclet::core::amr {
44
51template <class M>
52MORTON_HD inline Index amrLocate(const typename M::code_type* codes, const std::uint8_t* levels,
53 Index n, typename M::code_type p) {
54 // First index with codes[idx] > p, then step back one.
55 Index lo = 0, hi = n;
56 while (lo < hi) {
57 Index mid = lo + ((hi - lo) >> 1);
58 if (codes[mid] <= p)
59 lo = mid + 1;
60 else
61 hi = mid;
62 }
63 Index idx = lo - 1;
64 if (idx < 0)
65 return -1;
66 // p is inside leaf idx iff clearing the leaf's low bits gives its origin.
67 if (M::from_code(p).ancestor(levels[idx]).code() == codes[idx])
68 return idx;
69 return -1;
70}
71
77template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
79 public:
80 using M = morton::Morton<Dim, Bits>;
81 using Code = typename M::code_type;
82 using Coord = typename M::coord_type;
83 static constexpr unsigned octants = M::octants; // children per node (2^Dim)
84
85 BlockOctree() = default;
86
94
96 brick_ = brick;
97 lmax_ = lmax;
98 globalOrigin_ = globalOrigin;
99 const Coord rootSpan = Coord(Coord(1) << lmax_); // fine units per root cell
100 codes_.clear();
101 levels_.clear();
102 IVec<Dim> bgn{};
103 IVec<Dim> end = brick_;
104 forEachInBox<Dim>(bgn, end, [&](const IVec<Dim>& r) {
105 std::array<Coord, Dim> o{};
106 for (int i = 0; i < Dim; ++i)
107 o[i] = static_cast<Coord>(r[i]) * rootSpan;
108 codes_.push_back(M::encode(o).code());
109 levels_.push_back(static_cast<std::uint8_t>(lmax_));
110 });
111 sortByCode();
112 }
113
119 void assign(IVec<Dim> brick, unsigned lmax, IVec<Dim> globalOrigin, std::vector<Code> codes,
120 std::vector<std::uint8_t> levels) {
121 brick_ = brick;
122 lmax_ = lmax;
123 globalOrigin_ = globalOrigin;
124 codes_ = std::move(codes);
125 levels_ = std::move(levels);
126 sortByCode();
127 }
128
129 // ---- introspection -----------------------------------------------------
130 Index numLeaves() const { return static_cast<Index>(codes_.size()); }
131 unsigned lmax() const { return lmax_; }
132 static constexpr unsigned bits() { return Bits; }
133 const IVec<Dim>& brick() const { return brick_; }
134 const IVec<Dim>& globalOrigin() const { return globalOrigin_; }
135 Code code(Index i) const { return codes_[static_cast<std::size_t>(i)]; }
136 unsigned level(Index i) const { return levels_[static_cast<std::size_t>(i)]; }
137 const std::vector<Code>& codes() const { return codes_; }
138 const std::vector<std::uint8_t>& levels() const { return levels_; }
139
140 // ---- queries -----------------------------------------------------------
141
143 Index find(Code p) const { return amrLocate<M>(codes_.data(), levels_.data(), numLeaves(), p); }
145 Index find(const std::array<Coord, Dim>& fine) const { return find(M::encode(fine).code()); }
146
148 std::array<std::array<Coord, Dim>, 2> bounds(Index i) const {
149 std::array<std::array<Coord, Dim>, 2> b{};
150 auto o = M::from_code(codes_[static_cast<std::size_t>(i)]).decode();
151 Coord extent = Coord((Coord(1) << levels_[static_cast<std::size_t>(i)]) - 1);
152 for (int d = 0; d < Dim; ++d) {
153 b[0][d] = o[d];
154 b[1][d] = Coord(o[d] + extent);
155 }
156 return b;
157 }
158
162 Index faceNeighbor(Index i, int axis, int dir) const {
163 M probe = M::from_code(codes_[static_cast<std::size_t>(i)]);
164 const Coord step = Coord(Coord(1) << levels_[static_cast<std::size_t>(i)]);
165 if (dir >= 0) {
166 if (!probe.try_add(static_cast<unsigned>(axis), step))
167 return -1; // block edge
168 } else {
169 if (!probe.try_sub(static_cast<unsigned>(axis), 1))
170 return -1; // one past lower face
171 }
172 return find(probe.code());
173 }
174
175 // ---- refinement / coarsening ------------------------------------------
176
182 template <class Pred>
184 std::vector<Code> nc;
185 std::vector<std::uint8_t> nl;
186 nc.reserve(codes_.size());
187 nl.reserve(levels_.size());
188 Index nsplit = 0;
189 for (std::size_t k = 0; k < codes_.size(); ++k) {
190 const unsigned L = levels_[k];
191 const Code c = codes_[k];
192 if (L > 0 && pred(c, L)) {
193 M parent = M::from_code(c);
194 for (unsigned oct = 0; oct < octants; ++oct) {
195 nc.push_back(parent.child(L, oct).code());
196 nl.push_back(static_cast<std::uint8_t>(L - 1));
197 }
198 ++nsplit;
199 } else {
200 nc.push_back(c);
201 nl.push_back(static_cast<std::uint8_t>(L));
202 }
203 }
204 codes_.swap(nc);
205 levels_.swap(nl);
206 return nsplit;
207 }
208
211 const Code target = codes_[static_cast<std::size_t>(i)];
212 return refineIf([&](Code c, unsigned) { return c == target; }) > 0;
213 }
214
218 template <class Pred>
220 std::vector<Code> nc;
221 std::vector<std::uint8_t> nl;
222 nc.reserve(codes_.size());
223 nl.reserve(levels_.size());
224 Index nmerged = 0;
225 std::size_t k = 0;
226 while (k < codes_.size()) {
227 const unsigned L = levels_[k];
228 bool merged = false;
229 if (L < lmax_ && k + octants <= codes_.size()) {
230 const Code parent = M::from_code(codes_[k]).ancestor(L + 1).code();
231 bool full = (codes_[k] == parent); // octant-0 child shares the parent origin
232 for (unsigned oct = 0; full && oct < octants; ++oct) {
233 full = levels_[k + oct] == L &&
234 M::from_code(codes_[k + oct]).ancestor(L + 1).code() == parent &&
235 M::from_code(parent).child(L + 1, oct).code() == codes_[k + oct];
236 }
237 if (full && pred(parent, L + 1)) {
238 nc.push_back(parent);
239 nl.push_back(static_cast<std::uint8_t>(L + 1));
240 k += octants;
241 ++nmerged;
242 merged = true;
243 }
244 }
245 if (!merged) {
246 nc.push_back(codes_[k]);
247 nl.push_back(static_cast<std::uint8_t>(L));
248 ++k;
249 }
250 }
251 codes_.swap(nc);
252 levels_.swap(nl);
253 return nmerged;
254 }
255
262 Index total = 0;
263 for (;;) {
264 std::vector<Code> toRefine; // origins of leaves to split this round
265 for (std::size_t k = 0; k < codes_.size(); ++k) {
266 const unsigned Lf = levels_[k];
267 for (int axis = 0; axis < Dim; ++axis) {
268 for (int dir = -1; dir <= 1; dir += 2) {
269 Index nb = faceNeighbor(static_cast<Index>(k), axis, dir);
270 if (nb >= 0 && levels_[static_cast<std::size_t>(nb)] >= Lf + 2)
271 toRefine.push_back(codes_[static_cast<std::size_t>(nb)]);
272 }
273 }
274 }
275 if (toRefine.empty())
276 break;
277 std::sort(toRefine.begin(), toRefine.end());
278 toRefine.erase(std::unique(toRefine.begin(), toRefine.end()), toRefine.end());
279 total += refineIf([&](Code c, unsigned) {
280 return std::binary_search(toRefine.begin(), toRefine.end(), c);
281 });
282 }
283 return total;
284 }
285
288 bool isBalanced() const {
289 for (std::size_t k = 0; k < codes_.size(); ++k) {
290 const unsigned Lf = levels_[k];
291 for (int axis = 0; axis < Dim; ++axis)
292 for (int dir = -1; dir <= 1; dir += 2) {
293 Index nb = faceNeighbor(static_cast<Index>(k), axis, dir);
294 if (nb >= 0) {
295 unsigned Ln = levels_[static_cast<std::size_t>(nb)];
296 unsigned hi = Lf > Ln ? Lf : Ln, lo = Lf > Ln ? Ln : Lf;
297 if (hi - lo > 1)
298 return false;
299 }
300 }
301 }
302 return true;
303 }
304
305 private:
306 void sortByCode() {
307 std::vector<Index> ord(codes_.size());
308 for (std::size_t i = 0; i < ord.size(); ++i)
309 ord[i] = static_cast<Index>(i);
310 std::sort(ord.begin(), ord.end(), [&](Index a, Index b) { return codes_[a] < codes_[b]; });
311 std::vector<Code> nc(codes_.size());
312 std::vector<std::uint8_t> nl(levels_.size());
313 for (std::size_t i = 0; i < ord.size(); ++i) {
314 nc[i] = codes_[static_cast<std::size_t>(ord[i])];
315 nl[i] = levels_[static_cast<std::size_t>(ord[i])];
316 }
317 codes_.swap(nc);
318 levels_.swap(nl);
319 }
320
321 IVec<Dim> brick_{};
322 IVec<Dim> globalOrigin_{};
323 unsigned lmax_ = 0;
324 std::vector<Code> codes_; // leaf origin codes, ascending (Z-order)
325 std::vector<std::uint8_t> levels_; // parallel to codes_
326};
327
328} // namespace peclet::core::amr
329
330#endif // PECLET_CORE_HAVE_MORTON
331#endif // PECLET_CORE_AMR_BLOCK_OCTREE_HPP
#define MORTON_HD
Per-block adaptive octree over block-local Morton codes.
const std::vector< Code > & codes() const
const IVec< Dim > & brick() const
Index coarsenIf(Pred &&pred)
Merge complete sibling groups (all 2^Dim children present, all at the same level) whose parent satisf...
morton::Morton< Dim, Bits > M
void init(IVec< Dim > brick, unsigned lmax, IVec< Dim > globalOrigin=IVec< Dim >{})
bool refineLeaf(Index i)
Refine a single leaf by index. Returns true if it was split.
Index balance2to1()
Enforce 2:1 (graded) balance within this block: no two face-adjacent leaves differ by more than one l...
Index find(const std::array< Coord, Dim > &fine) const
Leaf containing fine-unit coordinates, or -1.
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...
typename M::coord_type Coord
Index refineIf(Pred &&pred)
Split every leaf for which pred(code, level) is true (and level > 0) into its 2^Dim children one leve...
unsigned level(Index i) 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.
bool isBalanced() const
True iff the leaves form a valid 2:1-balanced partition: every face neighbour differs by at most one ...
const std::vector< std::uint8_t > & levels() const
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
static constexpr unsigned bits()
static constexpr unsigned octants
const IVec< Dim > & globalOrigin() const
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.
typename M::code_type Code
std::array< std::array< Coord, Dim >, 2 > bounds(Index i) const
Inclusive integer bounds [lo, hi] of leaf i in fine units.
MORTON_HD 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.
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15