core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
multigrid.hpp
Go to the documentation of this file.
1// core — device (Kokkos) geometric-multigrid V-cycle on a BlockOctree,
2// with the *consistent* graded (cut-cell-ready) operator.
3//
4// The device compute path's capstone: a full MG V-cycle running entirely in device
5// kernels over the leaf Views, on a genuinely graded octree. The hierarchy is the
6// octree coarsened uniformly one level at a time (same as host AmrMultigrid); each
7// level's operator is the conservative two-point FV Laplacian
8// (L u)_i = invVol_i · Σ_f w_f (u[nbr_f] − u_i), w_f = openness·A_f/d_f,
9// captured as a precomputed face CSR (built on the host from AmrPoisson's exact
10// forEachFaceNeighbor enumeration, so the 2:1 sub-faces and coefficients match the
11// host operator bit-for-bit). This replaces the earlier *plain* (u_j−u_i)/h0²
12// operator, which ignored 2:1-interface geometry and so stalled the graded V-cycle.
13//
14// Every stage is deterministic / order-independent ⇒ bit-identical to the same
15// Jacobi V-cycle on the host (validated in test_amr_device_multigrid_kokkos):
16// * smoother — weighted Jacobi over the face CSR (reads only the prev iterate);
17// * restriction — each coarse cell sums its children in fixed CSR order (no atomics);
18// * prolongation— piecewise-constant, per fine cell.
19//
20// 2nd-order at 2:1 interfaces: solveQuad() wraps the V-cycle in deferred correction
21// with the quadratic coarse-fine flux (AmrPoisson::coarseStar), evaluated on device
22// as a second precomputed CSR (quadDelta). Openness (cut-cell) flows in for
23// free: build with an AmrPoisson that has openness set and w_f carries it.
24//
25// Requires a Kokkos build + the morton checkout (PECLET_CORE_HAVE_MORTON).
26#ifndef PECLET_CORE_AMR_MULTIGRID_HPP
27#define PECLET_CORE_AMR_MULTIGRID_HPP
28
29#ifdef PECLET_CORE_HAVE_MORTON
30
31#include <cmath>
32#include <memory>
33#include <utility>
34#include <vector>
35
36#include "peclet/core/amr/assembly.hpp" // assembleFv (device per-level operator rebuild, D5)
41
42namespace peclet::core::amr {
43
45inline void restrictField(View<const Index> childStart, View<const Index> childIdx,
47 Kokkos::parallel_for(
48 "amr::device_restrict", nCoarse, KOKKOS_LAMBDA(const Index p) {
49 const Index a = childStart(p), z = childStart(p + 1);
50 double s = 0.0;
51 for (Index k = a; k < z; ++k)
52 s += fine(childIdx(k));
53 coarse(p) = (z > a) ? s / static_cast<double>(z - a) : 0.0;
54 });
55}
56
63inline void restrictKappa(View<const Index> childStart, View<const Index> childIdx,
65 Index nCoarse) {
66 Kokkos::parallel_for(
67 "amr::device_restrict_kappa", nCoarse, KOKKOS_LAMBDA(const Index p) {
68 const Index a = childStart(p), z = childStart(p + 1);
69 double sw = 0.0, swv = 0.0;
70 for (Index k = a; k < z; ++k) {
71 const Index ch = childIdx(k);
72 const double w = kappa(ch);
73 sw += w;
74 swv += w * fine(ch);
75 }
76 coarse(p) = (sw > 0.0) ? swv / sw : 0.0;
77 });
78}
79
82 Index nFine) {
83 Kokkos::parallel_for(
84 "amr::device_prolong", nFine, KOKKOS_LAMBDA(const Index i) {
85 const Index p = c2p(i);
86 if (p >= 0)
87 fine(i) += coarse(p);
88 });
89}
90
95 Kokkos::parallel_for(
96 "amr::device_prolong_masked", nFine, KOKKOS_LAMBDA(const Index i) {
97 if (excl(i))
98 return;
99 const Index p = c2p(i);
100 if (p >= 0)
101 fine(i) += coarse(p);
102 });
103}
104
109 Kokkos::parallel_for(
110 "amr::device_zero_masked", n, KOKKOS_LAMBDA(const Index i) {
111 if (excl(i))
112 v(i) = 0.0;
113 });
114}
115
116template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
118 public:
120 using M = typename Octree::M;
121 using Code = typename Octree::Code;
123
127 void build(const Octree& finest, double h0) {
128 hmg_ = std::make_unique<AmrMultigrid<Dim, Bits>>();
129 hmg_->build(finest, h0);
130 buildFromHostMg();
131 }
132
137 template <class OpenFn>
138 void build(const Octree& finest, double h0, OpenFn&& openFn, bool periodic = true,
139 bool immersedWall = false) {
140 hmg_ = std::make_unique<AmrMultigrid<Dim, Bits>>();
141 hmg_->build(finest, h0);
142 hmg_->setOpenness(std::forward<OpenFn>(openFn));
143 hmg_->setPeriodic(periodic); // non-periodic ⇒ homogeneous Dirichlet domain walls
144 hmg_->setImmersedWall(immersedWall); // true ⇒ velocity operator (solid faces = no-slip walls)
145 buildFromHostMg();
146 }
147
150 void setKappaRestrict(bool on) { kappaRestrict_ = on; }
151
157 void setRemoveMean(bool on) { removeMean_ = on; }
158
164 void setHelmholtz(double c0, double cD) {
165 for (auto& lv : levels_) {
166 lv.op.c0 = c0;
167 lv.op.cD = cD;
168 }
169 }
170
171 std::size_t numLevels() const { return levels_.size(); }
172 Index numLeaves(std::size_t L = 0) const { return levels_[L].n; }
173 Code octreeCode(std::size_t L, Index i) const { return hmg_->op(L).octree().code(i); }
174 View<double> x(std::size_t L = 0) { return levels_[L].x; }
175 View<double> b(std::size_t L = 0) { return levels_[L].b; }
176 const FvOp& op(std::size_t L = 0) const { return levels_[L].op; }
177 View<Index> quadStart() const { return qStart_; }
178 View<Index> quadSlot() const { return qSlot_; }
179 View<double> quadCoef() const { return qCoef_; }
180
183 void vcycle(int pre = 2, int post = 2, int bottom = 40, double omega = 0.8, std::size_t L = 0) {
184 Level& lv = levels_[L];
186 if (L + 1 == levels_.size()) {
187 for (int s = 0; s < bottom; ++s)
188 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
189 if (removeMean_)
190 removeMeanFv(lv.op, lv.x);
191 return;
192 }
193 for (int s = 0; s < pre; ++s)
194 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
195 residualFv(lv.op, View<const double>(lv.x), bc, lv.res);
196 Level& cl = levels_[L + 1];
197 if (kappaRestrict_)
198 restrictKappa(lv.childStart, lv.childIdx, View<const double>(lv.res),
199 View<const double>(lv.kappa), cl.b, cl.n);
200 else
201 restrictField(lv.childStart, lv.childIdx, View<const double>(lv.res), cl.b, cl.n);
202 Kokkos::deep_copy(cl.x, 0.0);
203 vcycle(pre, post, bottom, omega, L + 1);
204 prolongAdd(lv.c2p, View<const double>(cl.x), lv.x, lv.n);
205 for (int s = 0; s < post; ++s)
206 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
207 if (removeMean_)
208 removeMeanFv(lv.op, lv.x);
209 }
210
216 double solveQuad(int outer = 20, int cyclesPerOuter = 1, int pre = 2, int post = 2,
217 int bottom = 40, double omega = 0.8) {
218 Level& lv = levels_[0];
219 Kokkos::deep_copy(b0true_, lv.b); // true rhs
220 auto dq = dq_;
221 auto b0 = b0true_;
222 auto bb = lv.b;
223 double r = 0.0;
224 for (int o = 0; o < outer; ++o) {
226 View<const double>(lv.x), dq_, lv.n);
227 Kokkos::parallel_for(
228 "amr::quad_rhsp", lv.n, KOKKOS_LAMBDA(const Index i) { bb(i) = b0(i) - dq(i); });
229 for (int c = 0; c < cyclesPerOuter; ++c)
230 vcycle(pre, post, bottom, omega, 0);
231 }
232 // r = || b0true − (L_std + quad) x ||
235 View<const double>(lv.x), dq_, lv.n);
236 auto res = lv.res;
237 double s = 0.0;
238 Kokkos::parallel_reduce(
239 "amr::quad_resnorm", lv.n,
240 KOKKOS_LAMBDA(const Index i, double& acc) {
241 double rr = res(i) - dq(i);
242 acc += rr * rr;
243 },
244 s);
245 r = std::sqrt(s);
246 Kokkos::deep_copy(lv.b, b0true_); // restore rhs
247 return r;
248 }
249
250 private:
251 struct Level {
252 FvOp op;
253 Index n = 0;
254 View<double> x, b, res, tmp;
255 View<double> kappa; // per-cell fluid-fraction weight (κ restriction)
256 View<Index> c2p, childStart, childIdx; // describe L → L+1 (unused on coarsest)
257 };
258
259 // Upload the device hierarchy from the host AmrMultigrid hmg_ (which already holds
260 // the uniform-coarsened octrees + per-level operators with coarsened openness). Each
261 // level's face CSR is built from hmg_->op(L), so the device operator == that host
262 // operator bit-for-bit on every level.
263 void buildFromHostMg() {
264 const std::size_t nl = hmg_->numLevels();
265 levels_.clear();
266 levels_.resize(nl);
267 for (std::size_t L = 0; L < nl; ++L) {
268 const Poisson& ap = hmg_->op(L);
269 const Octree& oct = ap.octree();
270 Level& lv = levels_[L];
271 lv.n = oct.numLeaves();
272 buildFaceCsr(ap, oct, lv);
273 // Per-cell κ weight for the optional κ-weighted restriction = mean face aperture
274 // (1 fully open ⇒ reduces to plain average; <1 near cuts).
275 std::vector<double> kap(static_cast<std::size_t>(lv.n));
276 for (Index i = 0; i < lv.n; ++i) {
277 double s = 0.0;
278 for (int axis = 0; axis < Dim; ++axis)
279 for (int dir = -1; dir <= 1; dir += 2)
280 s += ap.faceOpenness(i, axis, dir);
281 kap[static_cast<std::size_t>(i)] = s / static_cast<double>(2 * Dim);
282 }
283 lv.kappa = toDevice(kap, "mg_kappa");
284 lv.x = View<double>("mg_x", static_cast<std::size_t>(lv.n));
285 lv.b = View<double>("mg_b", static_cast<std::size_t>(lv.n));
286 lv.res = View<double>("mg_res", static_cast<std::size_t>(lv.n));
287 lv.tmp = View<double>("mg_tmp", static_cast<std::size_t>(lv.n));
288 }
289 for (std::size_t L = 0; L + 1 < nl; ++L) {
290 const Octree& f = hmg_->op(L).octree();
291 const Octree& c = hmg_->op(L + 1).octree();
292 const Index nf = f.numLeaves(), nc = c.numLeaves();
293 std::vector<Index> c2p(static_cast<std::size_t>(nf));
294 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
295 for (Index i = 0; i < nf; ++i) {
296 // Covering-leaf construction: identical to ancestor(level+1)+find for merged children,
297 // and CORRECT (identity) for cells already at root level in mixed-depth ladders — the
298 // ancestor form maps those to the 2^(lmax+1)-aligned corner root cell instead (measured:
299 // 49/56 root rows mis-parented on a graded 16^3 ladder), and its alignment depends on
300 // the block origin, which would break the distributed WORLD==SELF transfer contract.
301 Index p = c.find(f.code(i));
302 c2p[static_cast<std::size_t>(i)] = p;
303 if (p >= 0)
304 ++cnt[static_cast<std::size_t>(p)];
305 }
306 std::vector<Index> start(static_cast<std::size_t>(nc) + 1, 0);
307 for (Index p = 0; p < nc; ++p)
308 start[static_cast<std::size_t>(p) + 1] =
309 start[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
310 std::vector<Index> idx(static_cast<std::size_t>(nf));
311 std::vector<Index> cur(start.begin(), start.end() - 1);
312 for (Index i = 0; i < nf; ++i) {
313 Index p = c2p[static_cast<std::size_t>(i)];
314 if (p >= 0)
315 idx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
316 }
317 levels_[L].c2p = toDevice(c2p, "mg_c2p");
318 levels_[L].childStart = toDevice(start, "mg_cstart");
319 levels_[L].childIdx = toDevice(idx, "mg_cidx");
320 }
321 // Quadratic coarse-fine correction CSR on the finest level (for solveQuad);
322 // openness (if any) flows in through hmg_->op(0)'s aperture + coarseStar gating.
323 buildQuadCsr(hmg_->op(0), hmg_->op(0).octree());
324 const Index n0 = levels_[0].n;
325 dq_ = View<double>("mg_dq", static_cast<std::size_t>(n0));
326 b0true_ = View<double>("mg_b0", static_cast<std::size_t>(n0));
327 }
328
329 // Build the consistent face CSR (+ invVol/bcDiag) for one level by assembling it ON THE DEVICE
330 // (D5/D2): upload this level's leaf set, then assembleFv reproduces AmrPoisson's exact
331 // forEachFaceNeighbor enumeration + openness·A_f/d_f weights — bit-for-bit identical to the host
332 // walk on OpenMP (validated in test_amr_assembly), but with no host CSR build / no
333 // re-upload. The Helmholtz c0/cD of this level are preserved (assembleFv returns the pure-L
334 // defaults; setHelmholtz / a prior value is re-applied), so a reassemble after the geometry moves
335 // keeps the momentum-preconditioner form.
336 void buildFaceCsr(const Poisson& ap, const Octree& t, Level& lv) {
337 const double c0 = lv.op.c0, cD = lv.op.cD;
338 BlockOctreeView<Dim, Bits> ov;
339 ov.upload(t);
340 lv.op = assembleFv<Dim, Bits>(ap, ov);
341 lv.op.c0 = c0;
342 lv.op.cD = cD;
343 }
344
345 public:
352 const std::size_t nl = hmg_->numLevels();
353 for (std::size_t L = 0; L < nl && L < levels_.size(); ++L)
354 buildFaceCsr(hmg_->op(L), hmg_->op(L).octree(), levels_[L]);
355 }
356
357 private:
358 // Build the quadratic coarse-fine correction CSR: dq_i = Σ coef·u[slot] equals
359 // (L_quad − L_std)u (AmrPoisson::coarseStar, expressed as a linear stencil).
360 void buildQuadCsr(const Poisson& ap, const Octree& t) {
361 const Index n = t.numLeaves();
362 std::vector<std::vector<std::pair<Index, double>>> per(static_cast<std::size_t>(n));
363 const double h0 = ap.h0();
364 for (Index i = 0; i < n; ++i) {
365 const unsigned Li = t.level(i);
366 const double invV = 1.0 / ap.cellVolume(i);
367 ap.forEachFaceNeighbor(i, [&](Index j, Real c, int axis, double a) {
368 const unsigned Lj = t.level(j);
369 if (Lj == Li)
370 return;
371 const Index coarse = (Lj > Li) ? j : i;
372 const Index fine = (Lj > Li) ? i : j;
373 const double scale = invV * (a * c) * ((Lj > Li) ? 1.0 : -1.0);
374 addCoarseStarStencil(ap, t, per[static_cast<std::size_t>(i)], coarse, fine, axis, scale,
375 h0);
376 });
377 }
378 std::vector<Index> qs(static_cast<std::size_t>(n) + 1, 0);
379 for (Index i = 0; i < n; ++i)
380 qs[static_cast<std::size_t>(i) + 1] =
381 qs[static_cast<std::size_t>(i)] +
382 static_cast<Index>(per[static_cast<std::size_t>(i)].size());
383 const Index nq = qs[static_cast<std::size_t>(n)];
384 std::vector<Index> slot(static_cast<std::size_t>(nq));
385 std::vector<double> coef(static_cast<std::size_t>(nq));
386 Index k = 0;
387 for (Index i = 0; i < n; ++i)
388 for (auto& e : per[static_cast<std::size_t>(i)]) {
389 slot[static_cast<std::size_t>(k)] = e.first;
390 coef[static_cast<std::size_t>(k)] = e.second;
391 ++k;
392 }
393 qStart_ = toDevice(qs, "mg_qstart");
394 qSlot_ = toDevice(slot, "mg_qslot");
395 qCoef_ = toDevice(coef, "mg_qcoef");
396 }
397
398 // coarseStar(coarse,fine,axis) − u[coarse] as a linear stencil over the coarse
399 // cell + its tangential coarse neighbours, scaled, appended to `out`. Mirrors
400 // AmrPoisson::coarseStar exactly (same gating, same coefficients).
401 static void addCoarseStarStencil(const Poisson& ap, const Octree& t,
402 std::vector<std::pair<Index, double>>& out, Index coarse,
403 Index fine, int axis, double scale, double h0) {
404 auto bc = t.bounds(coarse);
405 auto bf = t.bounds(fine);
406 const double H = ap.cellWidth(coarse);
407 const double sc = static_cast<double>(Index(1) << t.level(coarse));
408 const double sf = static_cast<double>(Index(1) << t.level(fine));
409 for (int tt = 0; tt < Dim; ++tt) {
410 if (tt == axis)
411 continue;
412 const double dt = ((static_cast<double>(bf[0][tt]) + 0.5 * sf) -
413 (static_cast<double>(bc[0][tt]) + 0.5 * sc)) *
414 h0;
415 Index cp = ap.periodicNeighbor(coarse, tt, +1);
416 Index cm = ap.periodicNeighbor(coarse, tt, -1);
417 if (cp < 0 || cm < 0)
418 continue;
419 if (t.level(cp) != t.level(coarse) || t.level(cm) != t.level(coarse))
420 continue;
421 if (ap.faceOpenness(coarse, tt, +1) < 0.5 || ap.faceOpenness(coarse, tt, -1) < 0.5)
422 continue;
423 const double cUp = dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
424 const double cUm = -dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
425 const double cUc = -dt * dt / (H * H);
426 out.emplace_back(coarse, scale * cUc);
427 out.emplace_back(cp, scale * cUp);
428 out.emplace_back(cm, scale * cUm);
429 }
430 }
431
432 // Owns the host hierarchy + per-level operators (with coarsened openness). The
433 // operators hold pointers into its octrees, so keep it stable (unique_ptr) for the
434 // lifetime of this object.
435 std::unique_ptr<AmrMultigrid<Dim, Bits>> hmg_;
436 std::vector<Level> levels_;
437 bool kappaRestrict_ = false; // default: plain volume-average restriction
438 bool removeMean_ = false; // default off; per-level nullspace projection for singular MG-PCG
439 View<Index> qStart_, qSlot_; // finest-level quadratic correction CSR
440 View<double> qCoef_;
441 View<double> dq_, b0true_; // finest-level deferred-correction scratch
442};
443
444} // namespace peclet::core::amr
445
446#endif // PECLET_CORE_HAVE_MORTON
447#endif // PECLET_CORE_AMR_MULTIGRID_HPP
void setRemoveMean(bool on)
Opt-in (default off): project the correction to mean-zero over fluid cells at every V-cycle level — t...
View< Index > quadSlot() const
void vcycle(int pre=2, int post=2, int bottom=40, double omega=0.8, std::size_t L=0)
One V-cycle on level L (default finest) of the standard (consistent conservative) operator,...
void build(const Octree &finest, double h0, OpenFn &&openFn, bool periodic=true, bool immersedWall=false)
Build with cut-cell openness openFn(faceCentreWorld, axis) → [0,1], coarsened to every level by area-...
Code octreeCode(std::size_t L, Index i) const
BlockOctree< Dim, Bits > Octree
const FvOp & op(std::size_t L=0) const
View< double > x(std::size_t L=0)
void setKappaRestrict(bool on)
Opt-in: use κ-weighted (fluid-fraction) restriction instead of the default plain volume-average.
AmrPoisson< Dim, Bits > Poisson
double solveQuad(int outer=20, int cyclesPerOuter=1, int pre=2, int post=2, int bottom=40, double omega=0.8)
Solve L_quad u = rhs (the 2nd-order graded operator) by deferred correction: each outer step solves L...
Index numLeaves(std::size_t L=0) const
void reassembleOperators()
Re-assemble every level's operator ON THE DEVICE from the (host) hierarchy's current geometry — the d...
typename Octree::Code Code
void build(const Octree &finest, double h0)
Build + upload the hierarchy from a finest octree (uniform coarsening), openness- free.
View< Index > quadStart() const
void setHelmholtz(double c0, double cD)
Turn every level's operator into the Helmholtz form H = c0·I + cD·L (default c0=0,...
View< double > b(std::size_t L=0)
View< double > quadCoef() const
std::size_t numLevels() const
void prolongAdd(View< const Index > c2p, View< const double > coarse, View< double > fine, Index nFine)
Prolong (piecewise-constant) + correct: fine(i) += coarse(c2p(i)).
Definition multigrid.hpp:81
void zeroMasked(View< double > v, View< const char > excl, Index n)
Zero v at excluded cells (excl != 0).
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:53
void removeMeanFv(const FvOp &op, View< double > u)
Project u to volume-weighted-mean-zero over the ACTIVE (fluid) cells of the operator — the constant-n...
Definition fv_op.hpp:151
void quadDelta(View< const Index > qStart, View< const Index > qSlot, View< const double > qCoef, View< const double > u, View< double > dq, Index n)
dq = (L_quad − L_std) u, the quadratic coarse-fine correction as its own SpMV over a precomputed CSR ...
Definition fv_op.hpp:191
void jacobiFv(const FvOp &op, View< double > u, View< const double > rhs, View< double > tmp, double omega)
One weighted-Jacobi sweep of H u = rhs (in place).
Definition fv_op.hpp:135
void restrictKappa(View< const Index > childStart, View< const Index > childIdx, View< const double > fine, View< const double > kappa, View< double > coarse, Index nCoarse)
Restrict, κ-weighted: coarse(p) = Σ_child κ_c·fine_c / Σ_child κ_c, with κ the fine cell's fluid-frac...
Definition multigrid.hpp:63
void restrictField(View< const Index > childStart, View< const Index > childIdx, View< const double > fine, View< double > coarse, Index nCoarse)
Restrict: coarse(p) = mean over p's children (CSR fixed order ⇒ deterministic).
Definition multigrid.hpp:45
void residualFv(const FvOp &op, View< const double > u, View< const double > rhs, View< double > res)
res = rhs − H u.
Definition fv_op.hpp:121
void prolongAddMasked(View< const Index > c2p, View< const double > coarse, View< const char > excl, View< double > fine, Index nFine)
Masked piecewise-constant prolong + correct: fine(i) += coarse(c2p(i)) only on non-excluded fine cell...
Definition multigrid.hpp:93
View< T > toDevice(const std::vector< T > &h, const std::string &label)
Upload a host std::vector into a freshly-sized device View (empty vector => empty view).
Definition view.hpp:44
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15