core
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 Code parent = M::from_code(f.code(i)).ancestor(f.level(i) + 1).code();
297 Index p = c.find(parent);
298 c2p[static_cast<std::size_t>(i)] = p;
299 if (p >= 0)
300 ++cnt[static_cast<std::size_t>(p)];
301 }
302 std::vector<Index> start(static_cast<std::size_t>(nc) + 1, 0);
303 for (Index p = 0; p < nc; ++p)
304 start[static_cast<std::size_t>(p) + 1] =
305 start[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
306 std::vector<Index> idx(static_cast<std::size_t>(nf));
307 std::vector<Index> cur(start.begin(), start.end() - 1);
308 for (Index i = 0; i < nf; ++i) {
309 Index p = c2p[static_cast<std::size_t>(i)];
310 if (p >= 0)
311 idx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
312 }
313 levels_[L].c2p = toDevice(c2p, "mg_c2p");
314 levels_[L].childStart = toDevice(start, "mg_cstart");
315 levels_[L].childIdx = toDevice(idx, "mg_cidx");
316 }
317 // Quadratic coarse-fine correction CSR on the finest level (for solveQuad);
318 // openness (if any) flows in through hmg_->op(0)'s aperture + coarseStar gating.
319 buildQuadCsr(hmg_->op(0), hmg_->op(0).octree());
320 const Index n0 = levels_[0].n;
321 dq_ = View<double>("mg_dq", static_cast<std::size_t>(n0));
322 b0true_ = View<double>("mg_b0", static_cast<std::size_t>(n0));
323 }
324
325 // Build the consistent face CSR (+ invVol/bcDiag) for one level by assembling it ON THE DEVICE
326 // (D5/D2): upload this level's leaf set, then assembleFv reproduces AmrPoisson's exact
327 // forEachFaceNeighbor enumeration + openness·A_f/d_f weights — bit-for-bit identical to the host
328 // walk on OpenMP (validated in test_amr_assembly), but with no host CSR build / no
329 // re-upload. The Helmholtz c0/cD of this level are preserved (assembleFv returns the pure-L
330 // defaults; setHelmholtz / a prior value is re-applied), so a reassemble after the geometry moves
331 // keeps the momentum-preconditioner form.
332 void buildFaceCsr(const Poisson& ap, const Octree& t, Level& lv) {
333 const double c0 = lv.op.c0, cD = lv.op.cD;
334 BlockOctreeView<Dim, Bits> ov;
335 ov.upload(t);
336 lv.op = assembleFv<Dim, Bits>(ap, ov);
337 lv.op.c0 = c0;
338 lv.op.cD = cD;
339 }
340
341 public:
348 const std::size_t nl = hmg_->numLevels();
349 for (std::size_t L = 0; L < nl && L < levels_.size(); ++L)
350 buildFaceCsr(hmg_->op(L), hmg_->op(L).octree(), levels_[L]);
351 }
352
353 private:
354 // Build the quadratic coarse-fine correction CSR: dq_i = Σ coef·u[slot] equals
355 // (L_quad − L_std)u (AmrPoisson::coarseStar, expressed as a linear stencil).
356 void buildQuadCsr(const Poisson& ap, const Octree& t) {
357 const Index n = t.numLeaves();
358 std::vector<std::vector<std::pair<Index, double>>> per(static_cast<std::size_t>(n));
359 const double h0 = ap.h0();
360 for (Index i = 0; i < n; ++i) {
361 const unsigned Li = t.level(i);
362 const double invV = 1.0 / ap.cellVolume(i);
363 ap.forEachFaceNeighbor(i, [&](Index j, Real c, int axis, double a) {
364 const unsigned Lj = t.level(j);
365 if (Lj == Li)
366 return;
367 const Index coarse = (Lj > Li) ? j : i;
368 const Index fine = (Lj > Li) ? i : j;
369 const double scale = invV * (a * c) * ((Lj > Li) ? 1.0 : -1.0);
370 addCoarseStarStencil(ap, t, per[static_cast<std::size_t>(i)], coarse, fine, axis, scale,
371 h0);
372 });
373 }
374 std::vector<Index> qs(static_cast<std::size_t>(n) + 1, 0);
375 for (Index i = 0; i < n; ++i)
376 qs[static_cast<std::size_t>(i) + 1] =
377 qs[static_cast<std::size_t>(i)] +
378 static_cast<Index>(per[static_cast<std::size_t>(i)].size());
379 const Index nq = qs[static_cast<std::size_t>(n)];
380 std::vector<Index> slot(static_cast<std::size_t>(nq));
381 std::vector<double> coef(static_cast<std::size_t>(nq));
382 Index k = 0;
383 for (Index i = 0; i < n; ++i)
384 for (auto& e : per[static_cast<std::size_t>(i)]) {
385 slot[static_cast<std::size_t>(k)] = e.first;
386 coef[static_cast<std::size_t>(k)] = e.second;
387 ++k;
388 }
389 qStart_ = toDevice(qs, "mg_qstart");
390 qSlot_ = toDevice(slot, "mg_qslot");
391 qCoef_ = toDevice(coef, "mg_qcoef");
392 }
393
394 // coarseStar(coarse,fine,axis) − u[coarse] as a linear stencil over the coarse
395 // cell + its tangential coarse neighbours, scaled, appended to `out`. Mirrors
396 // AmrPoisson::coarseStar exactly (same gating, same coefficients).
397 static void addCoarseStarStencil(const Poisson& ap, const Octree& t,
398 std::vector<std::pair<Index, double>>& out, Index coarse,
399 Index fine, int axis, double scale, double h0) {
400 auto bc = t.bounds(coarse);
401 auto bf = t.bounds(fine);
402 const double H = ap.cellWidth(coarse);
403 const double sc = static_cast<double>(Index(1) << t.level(coarse));
404 const double sf = static_cast<double>(Index(1) << t.level(fine));
405 for (int tt = 0; tt < Dim; ++tt) {
406 if (tt == axis)
407 continue;
408 const double dt = ((static_cast<double>(bf[0][tt]) + 0.5 * sf) -
409 (static_cast<double>(bc[0][tt]) + 0.5 * sc)) *
410 h0;
411 Index cp = ap.periodicNeighbor(coarse, tt, +1);
412 Index cm = ap.periodicNeighbor(coarse, tt, -1);
413 if (cp < 0 || cm < 0)
414 continue;
415 if (t.level(cp) != t.level(coarse) || t.level(cm) != t.level(coarse))
416 continue;
417 if (ap.faceOpenness(coarse, tt, +1) < 0.5 || ap.faceOpenness(coarse, tt, -1) < 0.5)
418 continue;
419 const double cUp = dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
420 const double cUm = -dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
421 const double cUc = -dt * dt / (H * H);
422 out.emplace_back(coarse, scale * cUc);
423 out.emplace_back(cp, scale * cUp);
424 out.emplace_back(cm, scale * cUm);
425 }
426 }
427
428 // Owns the host hierarchy + per-level operators (with coarsened openness). The
429 // operators hold pointers into its octrees, so keep it stable (unique_ptr) for the
430 // lifetime of this object.
431 std::unique_ptr<AmrMultigrid<Dim, Bits>> hmg_;
432 std::vector<Level> levels_;
433 bool kappaRestrict_ = false; // default: plain volume-average restriction
434 bool removeMean_ = false; // default off; per-level nullspace projection for singular MG-PCG
435 View<Index> qStart_, qSlot_; // finest-level quadratic correction CSR
436 View<double> qCoef_;
437 View<double> dq_, b0true_; // finest-level deferred-correction scratch
438};
439
440} // namespace peclet::core::amr
441
442#endif // PECLET_CORE_HAVE_MORTON
443#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).
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