core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
block_decomposer.hpp
Go to the documentation of this file.
1// core — orthogonal recursive bisection (ORB) domain decomposition.
2//
3// Ported from block_decomposer/src/BlockDecomposer.hpp (pbs::BlockDecomposer), modernized into the
4// tpx namespace. The global cell grid is split recursively along its largest axis into `numBlocks`
5// rank-owned blocks. Adds ownerOf() (a tree walk) for halo topology construction.
6#ifndef PECLET_CORE_DECOMP_BLOCK_DECOMPOSER_HPP
7#define PECLET_CORE_DECOMP_BLOCK_DECOMPOSER_HPP
8
9#include <cassert>
10#include <cmath>
11#include <cstddef>
12#include <limits>
13#include <stack>
14#include <vector>
15
17
19
21template <int Dim>
26
27template <int Dim>
29 public:
30 BlockDecomposer() = default;
32 BlockDecomposer(std::size_t numBlocks, IVec<Dim> globalSize, const std::vector<Real>& weights) {
33 init(numBlocks, globalSize, weights);
34 }
37 init(numBlocks, globalSize, align);
38 }
39
42 void init(std::size_t numBlocks, IVec<Dim> globalSize) {
43 align_ = IVec<Dim>{};
44 for (int i = 0; i < Dim; ++i)
45 align_[i] = 1;
46 initImpl(numBlocks, globalSize, nullptr);
47 }
48
54 void init(std::size_t numBlocks, IVec<Dim> globalSize, const IVec<Dim>& align) {
55 align_ = align;
56 for (int i = 0; i < Dim; ++i)
57 if (align_[i] < 1)
58 align_[i] = 1;
59 initImpl(numBlocks, globalSize, nullptr);
60 }
61
69 void init(std::size_t numBlocks, IVec<Dim> globalSize, const IVec<Dim>& align,
70 const IVec<Dim>& cellExtent) {
71 cellExtent_ = cellExtent;
72 init(numBlocks, globalSize, align);
73 }
74
80 void init(std::size_t numBlocks, IVec<Dim> globalSize, const std::vector<Real>& weights) {
81 assert(weights.size() == static_cast<std::size_t>([&] {
82 Index v = 1;
83 for (int i = 0; i < Dim; ++i)
84 v *= globalSize[i];
85 return v;
86 }()) &&
87 "weights array must cover the global grid (x-fastest)");
88 align_ = IVec<Dim>{};
89 for (int i = 0; i < Dim; ++i)
90 align_[i] = 1;
91 initImpl(numBlocks, globalSize, &weights);
92 }
93
94 std::size_t numBlocks() const { return origins_.size(); }
95 const IVec<Dim>& globalSize() const { return globalSize_; }
96 const std::vector<IVec<Dim>>& origins() const { return origins_; }
97 const std::vector<IVec<Dim>>& sizes() const { return sizes_; }
98
99 Block<Dim> block(std::size_t b) const { return {origins_[b], sizes_[b]}; }
100
102 int ownerOf(const IVec<Dim>& g) const {
103 Index node = 0;
104 while (tree_[node].splitDim != -1) {
105 node = (g[tree_[node].splitDim] < tree_[node].splitValue) ? 2 * node + 1 : 2 * node + 2;
106 }
107 return static_cast<int>(tree_[node].splitValue);
108 }
109
115 void flattenTree(std::vector<int>& splitDim, std::vector<Index>& splitVal) const {
116 splitDim.resize(tree_.size());
117 splitVal.resize(tree_.size());
118 for (std::size_t i = 0; i < tree_.size(); ++i) {
119 splitDim[i] = tree_[i].splitDim;
120 splitVal[i] = tree_[i].splitValue;
121 }
122 }
123
125 Index linearGlobal(const IVec<Dim>& g) const {
126 Index idx = g[Dim - 1];
127 for (int i = Dim - 2; i >= 0; --i) {
128 idx *= globalSize_[i];
129 idx += g[i];
130 }
131 return idx;
132 }
133
136 IVec<Dim> g{};
137 for (int i = 0; i < Dim; ++i) {
138 g[i] = lin % globalSize_[i];
139 lin /= globalSize_[i];
140 }
141 return g;
142 }
143
152 for (int k = 0; k < Dim; ++k) {
153 assert(ratio[k] >= 1 && globalSize_[k] % ratio[k] == 0 &&
154 "coarsened(): global size not divisible by ratio (build the decomposition aligned)");
155 c.globalSize_[k] = globalSize_[k] / ratio[k];
156 c.align_[k] = align_[k] / ratio[k] >= 1 ? align_[k] / ratio[k] : 1;
157 }
158 c.tree_ = tree_;
159 for (auto& nd : c.tree_)
160 if (nd.splitDim != -1) {
161 assert(nd.splitValue % ratio[nd.splitDim] == 0 &&
162 "coarsened(): split not divisible by ratio (build the decomposition aligned)");
163 nd.splitValue /= ratio[nd.splitDim];
164 }
165 c.origins_.resize(origins_.size());
166 c.sizes_.resize(sizes_.size());
167 for (std::size_t b = 0; b < origins_.size(); ++b)
168 for (int k = 0; k < Dim; ++k) {
169 assert(origins_[b][k] % ratio[k] == 0 && sizes_[b][k] % ratio[k] == 0 &&
170 "coarsened(): block origin/size not divisible by ratio (build aligned)");
171 c.origins_[b][k] = origins_[b][k] / ratio[k];
172 c.sizes_[b][k] = sizes_[b][k] / ratio[k];
173 }
174 return c;
175 }
176
193 for (int k = 0; k < Dim; ++k) {
194 assert(ratio[k] >= 1 && "refined(): ratio must be >= 1");
195 f.globalSize_[k] = globalSize_[k] * ratio[k];
196 f.align_[k] = align_[k] * ratio[k];
197 f.cellExtent_[k] = 1; // the refined grid's cells are the fine cells
198 }
199 f.tree_ = tree_;
200 for (auto& nd : f.tree_)
201 if (nd.splitDim != -1)
202 nd.splitValue *= ratio[nd.splitDim];
203 f.origins_.resize(origins_.size());
204 f.sizes_.resize(sizes_.size());
205 for (std::size_t b = 0; b < origins_.size(); ++b)
206 for (int k = 0; k < Dim; ++k) {
207 f.origins_[b][k] = origins_[b][k] * ratio[k];
208 f.sizes_[b][k] = sizes_[b][k] * ratio[k];
209 }
210 return f;
211 }
212
213 private:
214 struct TreeNode {
215 int splitDim = -1;
216 Index splitValue = 0;
217 };
218
221 void initImpl(std::size_t numBlocks, IVec<Dim> globalSize, const std::vector<Real>* weights);
222
227 Index splitPosition(const IVec<Dim>& origin, const IVec<Dim>& size, int kLargest,
228 std::size_t numSub, std::size_t numTotal,
229 const std::vector<Real>* weights) const;
230
231 IVec<Dim> globalSize_{};
232 IVec<Dim> align_{};
233 IVec<Dim> cellExtent_{};
234 std::vector<IVec<Dim>> origins_;
235 std::vector<IVec<Dim>> sizes_;
236 std::vector<TreeNode> tree_;
237};
238
239template <int Dim>
240void BlockDecomposer<Dim>::initImpl(std::size_t numBlocks, IVec<Dim> globalSize,
241 const std::vector<Real>* weights) {
242 globalSize_ = globalSize;
243 for (int k = 0; k < Dim; ++k)
244 if (cellExtent_[k] <= 0)
245 cellExtent_[k] = 1;
246 origins_.clear();
247 sizes_.clear();
248 tree_.clear();
249
250 struct StackBlock {
251 IVec<Dim> origin;
252 IVec<Dim> size;
253 std::size_t numSub;
254 Index treeIndx;
255 };
256 std::stack<StackBlock> stack;
257 stack.push(StackBlock{IVec<Dim>{}, globalSize, numBlocks, 0});
258
259 Index leafIndex = 0;
260 while (!stack.empty()) {
261 StackBlock cur = stack.top();
262 stack.pop();
263
264 if (static_cast<std::size_t>(cur.treeIndx) >= tree_.size()) {
265 tree_.resize(cur.treeIndx + 1);
266 }
267
268 if (cur.numSub > 1) {
269 // Split along the largest axis. The split position balances either the sub-block cell count
270 // (unweighted) or the cumulative weight (weighted) of the two children.
271 // Compare PHYSICAL extents: size*cellExtent. cellExtent is all-ones for an ordinary grid, so
272 // this is the plain largest-cell-count test unless an anisotropically coarsened grid says
273 // otherwise (see the cellExtent init).
274 int kLargest = 0;
275 for (int k = 1; k < Dim; ++k) {
276 if (cur.size[k] * cellExtent_[k] > cur.size[kLargest] * cellExtent_[kLargest])
277 kLargest = k;
278 }
279 std::size_t numSub = cur.numSub / 2;
280 Index szSub = splitPosition(cur.origin, cur.size, kLargest, numSub, cur.numSub, weights);
281
282 // Snap the split to a multiple of align_[kLargest] so the global split position
283 // (cur.origin[kLargest] + szSub, with cur.origin already aligned) is a multiple too — the
284 // precondition for coarsened() to divide cleanly. Skip if the box is too small to split aligned.
285 const Index a = align_[kLargest];
286 if (a > 1 && cur.size[kLargest] >= 2 * a) {
287 szSub = ((szSub + a / 2) / a) * a;
288 if (szSub < a)
289 szSub = a;
290 if (szSub > cur.size[kLargest] - a)
291 szSub = cur.size[kLargest] - a;
292 }
293
294 StackBlock left = cur;
295 left.numSub = numSub;
296 left.size[kLargest] = szSub;
297 left.treeIndx = 2 * cur.treeIndx + 1;
298
299 StackBlock right = cur;
300 right.origin[kLargest] += szSub;
301 right.size[kLargest] -= szSub;
302 right.numSub = cur.numSub - numSub;
303 right.treeIndx = 2 * cur.treeIndx + 2;
304
305 tree_[cur.treeIndx] = TreeNode{kLargest, cur.origin[kLargest] + szSub};
306 stack.push(right);
307 stack.push(left);
308 } else {
309 tree_[cur.treeIndx] = TreeNode{-1, leafIndex++};
310 origins_.push_back(cur.origin);
311 sizes_.push_back(cur.size);
312 }
313 }
314}
315
316template <int Dim>
317Index BlockDecomposer<Dim>::splitPosition(const IVec<Dim>& origin, const IVec<Dim>& size,
318 int kLargest, std::size_t numSub, std::size_t numTotal,
319 const std::vector<Real>* weights) const {
320 const Index n = size[kLargest];
321
322 // Unweighted (and the degenerate non-splittable axis): the classic proportional-to-count split.
323 // Kept as the exact same expression so the unweighted API is byte-for-byte unchanged.
324 if (weights == nullptr || n <= 1) {
325 return static_cast<Index>(std::round(static_cast<double>(n) * static_cast<double>(numSub) /
326 static_cast<double>(numTotal)));
327 }
328
329 // Weighted: accumulate the weight of each slab (fixed kLargest-coordinate) within the box, then
330 // pick the boundary whose cumulative weight is closest to the target fraction of the total.
331 std::vector<double> slab(static_cast<std::size_t>(n), 0.0);
332 IVec<Dim> bgn = origin, end{};
333 for (int i = 0; i < Dim; ++i)
334 end[i] = origin[i] + size[i];
335 forEachInBox<Dim>(bgn, end, [&](const IVec<Dim>& g) {
336 slab[static_cast<std::size_t>(g[kLargest] - origin[kLargest])] += (*weights)[linearGlobal(g)];
337 });
338
339 double total = 0.0;
340 for (double w : slab)
341 total += w;
342 const double target = total * static_cast<double>(numSub) / static_cast<double>(numTotal);
343
344 // Search boundaries in [1, n-1] (non-empty children). Ties resolve to the larger boundary, which
345 // mirrors std::round's half-away-from-zero rule so equal weights reproduce the unweighted split.
346 double cum = 0.0;
347 double bestDist = std::numeric_limits<double>::max();
348 Index best = 1;
349 for (Index s = 1; s < n; ++s) {
350 cum += slab[static_cast<std::size_t>(s - 1)];
351 const double dist = std::abs(cum - target);
352 if (dist <= bestDist) {
353 bestDist = dist;
354 best = s;
355 }
356 }
357 return best;
358}
359
360} // namespace peclet::core::decomp
361
362#endif // PECLET_CORE_DECOMP_BLOCK_DECOMPOSER_HPP
void flattenTree(std::vector< int > &splitDim, std::vector< Index > &splitVal) const
Flatten the implicit ORB tree into two parallel arrays for a device-callable ownerOf: for node i,...
Block< Dim > block(std::size_t b) const
const std::vector< IVec< Dim > > & sizes() const
void init(std::size_t numBlocks, IVec< Dim > globalSize, const IVec< Dim > &align, const IVec< Dim > &cellExtent)
Anisotropic-cell aligned ORB: cellExtent[k] is how many underlying FINE cells one cell of this grid s...
void init(std::size_t numBlocks, IVec< Dim > globalSize)
Build the decomposition of a globalSize cell grid into numBlocks blocks (equal cell count).
BlockDecomposer(std::size_t numBlocks, IVec< Dim > globalSize, const std::vector< Real > &weights)
const std::vector< IVec< Dim > > & origins() const
const IVec< Dim > & globalSize() const
void init(std::size_t numBlocks, IVec< Dim > globalSize, const IVec< Dim > &align)
Aligned ORB: force every split position (and hence every block origin/size) on axis k to be a multipl...
void init(std::size_t numBlocks, IVec< Dim > globalSize, const std::vector< Real > &weights)
Weighted ORB: balance the total weight per block instead of the cell count.
BlockDecomposer(std::size_t numBlocks, IVec< Dim > globalSize)
Index linearGlobal(const IVec< Dim > &g) const
Global multi-index -> global linear index (x-fastest: I = x + y*nx + z*nx*ny).
BlockDecomposer< Dim > refined(const IVec< Dim > &ratio) const
Derive the NESTED fine decomposition: the exact inverse of coarsened() — global size,...
BlockDecomposer(std::size_t numBlocks, IVec< Dim > globalSize, const IVec< Dim > &align)
Aligned ORB (coarsenable): see the aligned init() below.
int ownerOf(const IVec< Dim > &g) const
Owning block index of a global cell coordinate. Caller must wrap into [0, globalSize) first.
BlockDecomposer< Dim > coarsened(const IVec< Dim > &ratio) const
Derive the NESTED coarse decomposition: each block, split value and the global size divided by ratio ...
IVec< Dim > multiGlobal(Index lin) const
Global linear index -> global multi-index (inverse of linearGlobal).
std::array< Index, Dim > IVec
Multi-dimensional integer index / coordinate (x-fastest order by convention).
Definition types.hpp:22
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
A rank-owned axis-aligned block of the global cell grid.
IVec< Dim > size
extent in cells along each axis
IVec< Dim > origin
inclusive lower corner in global cell coordinates