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
poisson.hpp
Go to the documentation of this file.
1// core — cell-centered finite-volume Poisson on a BlockOctree, with a
2// geometric multigrid built from the octree's own levels.
3//
4// This is the "grid + multigrid" use of the AMR octree (serial, host; the device
5// and distributed paths build on the same operator later). The operator is a
6// conservative two-point finite-volume Laplacian: for a leaf i,
7//
8// (L u)_i = (1/V_i) * sum_faces A_f / d_f * (u_j - u_i),
9//
10// with V_i the cell volume, A_f the shared face area, d_f the centre-to-centre
11// normal distance. At a 2:1 interface the coarse side sums the flux over all
12// 2^(Dim-1) fine neighbours (each with the fine face area), so the discretisation
13// is conservative (the global integral of L u is zero on a periodic domain). The
14// two-point gradient ignores the tangential centre offset, so the interface flux
15// is first-order there — a documented limitation; a tangential-gradient
16// correction (full 2nd-order AMR flux) is a follow-up.
17//
18// Multigrid: the hierarchy is the octree coarsened uniformly one level at a time
19// (coarsenIf over all sibling groups). Restriction averages children -> parent;
20// prolongation is piecewise-constant (correction scheme). Smoother is
21// lexicographic Gauss-Seidel over the Z-order leaf slots. Periodic BCs only (the
22// natural first target); the singular null space is fixed by mean removal.
23//
24#ifndef PECLET_AMR_POISSON_HPP
25#define PECLET_AMR_POISSON_HPP
26
27#include <array>
28#include <cmath>
29#include <functional>
30#include <vector>
31
33#include "peclet/amr/common.hpp"
34#include "peclet/amr/face_csr.hpp" // shared host+device FV (weight-CSR) row kernels
36#include "peclet/core/common/host_parallel.hpp"
37#include "peclet/core/common/types.hpp"
38
39namespace peclet::amr {
40
42template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
44 public:
46 using M = typename Octree::M;
47 using Code = typename Octree::Code;
48 using Coord = typename Octree::Coord;
49
50 AmrPoisson() = default;
51 AmrPoisson(const Octree& t, Real h0) { init(t, h0); }
52 AmrPoisson(const Octree& t, const Vec<Dim>& h0) { init(t, h0); }
53
56 void init(const Octree& t, Real h0) { init(t, detail::filledVec<Dim>(h0)); }
57
58 void init(const Octree& t, const Vec<Dim>& h0) {
59 t_ = &t;
60 h0_ = h0;
61 alpha_.clear();
62 hasOpen_ = false;
63 extResolve_ = nullptr; // distributed seam re-installed by the owner after every init
64 ghostLo_.clear();
65 ghostLv_.clear();
66 frameShift_ = {};
67 for (int d = 0; d < Dim; ++d)
68 fineExt_[d] = static_cast<Coord>(t.brick()[d] * (Index(1) << t.lmax()));
69 }
70
71 void setOrigin(const Vec<Dim>& o) { origin_ = o; }
72
73 // ---- distributed seam (docs/amr_distributed_flow.md) ---------------------------------------
80 using ExtResolver = std::function<Index(const std::array<long, Dim>&)>;
81 void setResolver(ExtResolver r) { extResolve_ = std::move(r); }
86 void setGhosts(std::vector<std::array<long, Dim>> lo, std::vector<unsigned> lv) {
87 ghostLo_ = std::move(lo);
88 ghostLv_ = std::move(lv);
89 }
90 Index numGhosts() const { return static_cast<Index>(ghostLv_.size()); }
96 void setFrameShift(const std::array<long, Dim>& s) { frameShift_ = s; }
98 unsigned levelOf(Index slot) const {
99 return slot < numLeaves() ? t_->level(slot)
100 : ghostLv_[static_cast<std::size_t>(slot - numLeaves())];
101 }
103 std::array<long, Dim> loOf(Index slot) const {
104 std::array<long, Dim> lo{};
105 if (slot < numLeaves()) {
106 auto b = t_->bounds(slot);
107 for (int d = 0; d < Dim; ++d)
108 lo[d] = static_cast<long>(b[0][d]);
109 } else {
110 lo = ghostLo_[static_cast<std::size_t>(slot - numLeaves())];
111 }
112 return lo;
113 }
114
120 std::pair<Index, unsigned> probeSlot(const std::array<long, Dim>& p) const {
121 bool in = true;
122 for (int d = 0; d < Dim; ++d)
123 if (p[d] < 0 || p[d] >= static_cast<long>(fineExt_[d])) {
124 in = false;
125 break;
126 }
127 std::array<Coord, Dim> q{};
128 if (!in) {
129 if (extResolve_) {
130 const Index s = extResolve_(p);
131 return {s, s >= 0 ? levelOf(s) : 0u};
132 }
133 for (int d = 0; d < Dim; ++d) {
134 const long e = static_cast<long>(fineExt_[d]);
135 q[d] = static_cast<Coord>(((p[d] % e) + e) % e);
136 }
137 } else {
138 for (int d = 0; d < Dim; ++d)
139 q[d] = static_cast<Coord>(p[d]);
140 }
141 const Index j = t_->find(M::encode(q).code());
142 return {j, j >= 0 ? t_->level(j) : 0u};
143 }
144
145 // ---- cut-cell openness (per-leaf per-face fluid fraction in [0,1]) -------
146 static constexpr int kFaces = 2 * Dim;
147 static int faceIndex(int axis, int dir) { return 2 * axis + (dir > 0 ? 0 : 1); }
148
150 double faceOpenness(Index i, int axis, int dir) const {
151 if (!hasOpen_)
152 return 1.0;
153 return alpha_[static_cast<std::size_t>(i) * kFaces + faceIndex(axis, dir)];
154 }
155
156 bool hasOpenness() const { return hasOpen_; }
157 const std::vector<double>& opennessRaw() const { return alpha_; }
158 void setOpennessRaw(std::vector<double> a) {
159 alpha_ = std::move(a);
160 hasOpen_ = true;
161 }
162
166 template <class OpenFn>
168 const Index n = numLeaves();
169 const Index ng = numGhosts();
170 alpha_.assign(static_cast<std::size_t>(n + ng) * kFaces, 1.0);
171 hasOpen_ = true;
172 // Face centroids in the GLOBAL frame (localCoord + frameShift_): every rank evaluates the
173 // world-coord openFn at bit-identical points, so ghost rows match the owner's exactly (the
174 // symmetric-openFn contract). Single-rank: frameShift_ = 0, unchanged.
175 auto fillRow = [&](Index row, const std::array<long, Dim>& lo, long s) {
176 for (int axis = 0; axis < Dim; ++axis)
177 for (int dir = -1; dir <= 1; dir += 2) {
178 const long plane =
179 (dir > 0) ? lo[axis] + frameShift_[axis] + s : lo[axis] + frameShift_[axis];
180 Vec<Dim> fc{};
181 for (int d = 0; d < Dim; ++d)
182 fc[d] = (d == axis) ? origin_[d] + static_cast<Real>(plane) * h0_[d]
183 : origin_[d] + (static_cast<Real>(lo[d] + frameShift_[d]) +
184 0.5 * static_cast<Real>(s)) *
185 h0_[d];
186 double a = static_cast<double>(openFn(fc, axis));
187 a = a < 0.0 ? 0.0 : (a > 1.0 ? 1.0 : a);
188 alpha_[static_cast<std::size_t>(row) * kFaces + faceIndex(axis, dir)] = a;
189 }
190 };
191 // Host-parallel (setup-parallel plan rung 2): leaf i writes only its own kFaces α slots and
192 // `openFn` is a pure function of the face centroid — disjoint writes, so α is bitwise
193 // identical under any thread count. Rank-local: the distributed contract is untouched.
194 hostParFor(n, [&](Index i) {
195 auto b = t_->bounds(i);
196 std::array<long, Dim> lo{};
197 for (int d = 0; d < Dim; ++d)
198 lo[d] = static_cast<long>(b[0][d]);
199 fillRow(i, lo, 1L << t_->level(i));
200 });
201 for (Index g = 0; g < ng; ++g) // ghost α rows (distributed builds; empty single-rank)
202 fillRow(n + g, ghostLo_[static_cast<std::size_t>(g)],
203 1L << ghostLv_[static_cast<std::size_t>(g)]);
204 }
205
206 Index numLeaves() const { return t_->numLeaves(); }
207
211 void setPeriodic(bool p) { periodic_ = p; }
212 bool periodic() const { return periodic_; }
213
221 void setImmersedWall(bool w) { immersedWall_ = w; }
222 bool immersedWall() const { return immersedWall_; }
223
229 double boundaryDiag(Index i) const {
230 if (periodic_ && !immersedWall_)
231 return 0.0;
232 auto b = t_->bounds(i);
233 const auto& lo = b[0];
234 const Coord si = Coord(Coord(1) << t_->level(i));
235 double s = 0.0;
236 for (int axis = 0; axis < Dim; ++axis) {
237 // Phase 3: A_f/d_f of a BOX cell — the face area normal to `axis` over the half width along
238 // `axis`. Identical to the old single `wall` when the three spacings are equal.
239 const double wall = areaOf(si, axis) / (0.5 * static_cast<Real>(si) * h0_[axis]);
240 for (int dir = -1; dir <= 1; dir += 2) {
241 const long pc = (dir > 0) ? static_cast<long>(lo[axis]) + static_cast<long>(si)
242 : static_cast<long>(lo[axis]) - 1;
243 const bool domainBoundary =
244 !periodic_ && (pc < 0 || pc >= static_cast<long>(fineExt_[axis]));
245 if (domainBoundary)
246 s += faceOpenness(i, axis, dir) * wall; // domain Dirichlet wall (open part)
247 else if (immersedWall_)
248 s += (1.0 - faceOpenness(i, axis, dir)) * wall; // immersed no-slip wall (solid part)
249 }
250 }
251 return s;
252 }
253
255 Real cellWidth(Index i, int axis) const {
256 return h0_[axis] * static_cast<Real>(Index(1) << levelOf(i));
257 }
261 Real cellWidth(Index i) const { return h0_[0] * static_cast<Real>(Index(1) << levelOf(i)); }
263 const Real f = static_cast<Real>(Index(1) << levelOf(i));
264 Real v = 1;
265 for (int d = 0; d < Dim; ++d)
266 v *= h0_[d] * f;
267 return v;
268 }
269
273 template <class Fn>
274 void forEachFaceNeighbor(Index i, Fn&& fn) const {
275 auto b = t_->bounds(i);
276 const auto& lo = b[0];
277 const unsigned Li = t_->level(i);
278 const Coord si = Coord(Coord(1) << Li);
279 for (int axis = 0; axis < Dim; ++axis)
280 for (int dir = -1; dir <= 1; dir += 2) {
281 const long pc = (dir > 0) ? static_cast<long>(lo[axis]) + static_cast<long>(si)
282 : static_cast<long>(lo[axis]) - 1;
283 // Non-periodic: a domain-boundary face has no neighbour cell (it is a Dirichlet
284 // wall handled by boundaryDiag) — skip it.
285 if (!periodic_ && (pc < 0 || pc >= static_cast<long>(fineExt_[axis])))
286 continue;
287 std::array<long, Dim> p{};
288 for (int d = 0; d < Dim; ++d)
289 p[d] = static_cast<long>(lo[d]);
290 p[axis] = pc;
291 const auto [j, Lj] = probeSlot(p);
292 if (j < 0)
293 continue; // unresolved during the distributed discovery fixpoint only
294 if (Lj >= Li) {
295 // same level or coarser: one neighbour, shared face = this cell's face.
296 // Openness lives on the finer side (here, this cell i).
297 fn(j, coeff(si, Coord(Coord(1) << Lj), axis), axis, faceOpenness(i, axis, dir));
298 } else {
299 // finer neighbour: 2^(Dim-1) sub-faces, each the fine face area.
300 const Coord sj = Coord(si >> 1);
301 const int nsub = 1 << (Dim - 1);
302 for (int k = 0; k < nsub; ++k) {
303 std::array<long, Dim> q = p;
304 int bit = 0;
305 for (int t = 0; t < Dim; ++t) {
306 if (t == axis)
307 continue;
308 q[t] = static_cast<long>(lo[t]) + (((k >> bit) & 1) ? static_cast<long>(sj) : 0L);
309 ++bit;
310 }
311 const Index jj = probeSlot(q).first;
312 if (jj < 0)
313 continue;
314 // Fine face area (min = sj) but the true centre-to-centre distance
315 // (si+sj)/2 — same value the fine side computes, so the operator is
316 // symmetric / conservative across the 2:1 interface. Openness lives on
317 // the finer side (the neighbour jj), its face toward i is -dir.
318 fn(jj, coeff(si, sj, axis), axis, faceOpenness(jj, axis, -dir));
319 }
320 }
321 }
322 }
323
328 template <class Fn>
329 void forEachFaceFull(Index i, Fn&& fn) const {
330 auto b = t_->bounds(i);
331 const auto& lo = b[0];
332 const unsigned Li = t_->level(i);
333 const Coord si = Coord(Coord(1) << Li);
334 for (int axis = 0; axis < Dim; ++axis)
335 for (int dir = -1; dir <= 1; dir += 2) {
336 const long pc = (dir > 0) ? static_cast<long>(lo[axis]) + static_cast<long>(si)
337 : static_cast<long>(lo[axis]) - 1;
338 std::array<long, Dim> p{};
339 for (int d = 0; d < Dim; ++d)
340 p[d] = static_cast<long>(lo[d]);
341 p[axis] = pc;
342 const auto [j, Lj] = probeSlot(p);
343 if (j < 0)
344 continue; // unresolved during the distributed discovery fixpoint only
345 if (Lj >= Li) {
346 const Coord sj = Coord(Coord(1) << Lj);
347 fn(j, axis, dir, areaOf(si, axis),
348 0.5 * (static_cast<Real>(si) + static_cast<Real>(sj)) * h0_[axis],
349 faceOpenness(i, axis, dir));
350 } else {
351 const Coord sj = Coord(si >> 1);
352 const int nsub = 1 << (Dim - 1);
353 for (int k = 0; k < nsub; ++k) {
354 std::array<long, Dim> q = p;
355 int bit = 0;
356 for (int t = 0; t < Dim; ++t) {
357 if (t == axis)
358 continue;
359 q[t] = static_cast<long>(lo[t]) + (((k >> bit) & 1) ? static_cast<long>(sj) : 0L);
360 ++bit;
361 }
362 const Index jj = probeSlot(q).first;
363 if (jj < 0)
364 continue;
365 fn(jj, axis, dir, areaOf(sj, axis),
366 0.5 * (static_cast<Real>(si) + static_cast<Real>(sj)) * h0_[axis],
367 faceOpenness(jj, axis, -dir));
368 }
369 }
370 }
371 }
372
375 Index periodicNeighbor(Index i, int axis, int dir) const {
376 std::array<long, Dim> p = loOf(i);
377 const long si = 1L << levelOf(i);
378 p[axis] = (dir > 0) ? p[axis] + si : p[axis] - 1;
379 return probeSlot(p).first;
380 }
381
389 double coarseStar(const std::vector<double>& u, Index coarse, Index fine, int axis) const {
390 const double uc = u[static_cast<std::size_t>(coarse)];
391 const std::array<long, Dim> bc = loOf(coarse); // ghost-safe (block-local longs)
392 const std::array<long, Dim> bf = loOf(fine);
393 const double sc = static_cast<double>(Index(1) << levelOf(coarse));
394 const double sf = static_cast<double>(Index(1) << levelOf(fine));
395 double val = uc;
396 for (int t = 0; t < Dim; ++t) {
397 if (t == axis)
398 continue;
399 // Phase 3: the tangential offset AND the differencing width are the spacings of the
400 // TANGENTIAL axis `t` (they were one `H` when the cells were cubes).
401 const double H = cellWidth(coarse, t);
402 const double dt =
403 ((static_cast<double>(bf[t]) + 0.5 * sf) - (static_cast<double>(bc[t]) + 0.5 * sc)) *
404 h0_[t];
407 if (cp < 0 || cm < 0)
408 continue;
409 if (levelOf(cp) != levelOf(coarse) || levelOf(cm) != levelOf(coarse))
410 continue;
411 // Skip the correction near a solid: a nearly-closed tangential face means
412 // the quadratic stencil would lean on a solid-side value. Drop to the raw
413 // coarse value on this axis (locally lower order, but robust).
414 if (faceOpenness(coarse, t, +1) < 0.5 || faceOpenness(coarse, t, -1) < 0.5)
415 continue;
416 const double up = u[static_cast<std::size_t>(cp)];
417 const double um = u[static_cast<std::size_t>(cm)];
418 const double Dt = (up - um) / (2.0 * H);
419 const double Dtt = (up - 2.0 * uc + um) / (H * H);
420 val += dt * Dt + 0.5 * dt * dt * Dtt;
421 }
422 return val;
423 }
424
426 void applyLaplacianQuad(const std::vector<double>& u, std::vector<double>& out) const {
427 const Index n = numLeaves();
428 out.assign(static_cast<std::size_t>(n), 0.0);
429 for (Index i = 0; i < n; ++i) {
430 const double ui = u[static_cast<std::size_t>(i)];
431 const unsigned Li = t_->level(i);
432 double acc = 0.0;
433 forEachFaceNeighbor(i, [&](Index j, Real c, int axis, double a) {
434 const unsigned Lj = levelOf(j); // ghost-safe
435 double uj = u[static_cast<std::size_t>(j)];
436 double uii = ui;
437 if (Lj > Li)
438 uj = coarseStar(u, j, i, axis); // j coarser: correct its value
439 else if (Lj < Li)
440 uii = coarseStar(u, i, j, axis); // i coarser: correct our value for this sub-face
441 acc += a * c * (uj - uii);
442 });
443 out[static_cast<std::size_t>(i)] = acc / cellVolume(i);
444 }
445 }
446
448 double residualQuad(const std::vector<double>& u, const std::vector<double>& rhs,
449 std::vector<double>& res) const {
450 std::vector<double> lu;
452 const Index n = numLeaves();
453 res.assign(static_cast<std::size_t>(n), 0.0);
454 double s = 0.0;
455 for (Index i = 0; i < n; ++i) {
456 double r = rhs[static_cast<std::size_t>(i)] - lu[static_cast<std::size_t>(i)];
457 res[static_cast<std::size_t>(i)] = r;
458 s += cellVolume(i) * r * r;
459 }
460 return std::sqrt(s);
461 }
462
467 struct FvAssembled {
468 std::vector<double> invVol;
469 std::vector<Index> start;
470 std::vector<Index> nbr;
471 std::vector<double> coef;
472 std::vector<double> bcDiag;
473 };
475 const Index n = numLeaves();
477 A.start.assign(static_cast<std::size_t>(n) + 1, 0);
478 for (Index i = 0; i < n; ++i) {
479 Index cnt = 0;
480 forEachFaceNeighbor(i, [&](Index, Real, int, double) { ++cnt; });
481 A.start[static_cast<std::size_t>(i) + 1] = A.start[static_cast<std::size_t>(i)] + cnt;
482 }
483 const Index nf = A.start[static_cast<std::size_t>(n)];
484 A.nbr.resize(static_cast<std::size_t>(nf));
485 A.coef.resize(static_cast<std::size_t>(nf));
486 A.invVol.resize(static_cast<std::size_t>(n));
487 A.bcDiag.resize(static_cast<std::size_t>(n));
488 for (Index i = 0; i < n; ++i) {
489 A.invVol[static_cast<std::size_t>(i)] = 1.0 / cellVolume(i);
490 A.bcDiag[static_cast<std::size_t>(i)] = boundaryDiag(i);
491 Index k = A.start[static_cast<std::size_t>(i)];
492 forEachFaceNeighbor(i, [&](Index j, Real c, int, double a) {
493 A.nbr[static_cast<std::size_t>(k)] = j;
494 A.coef[static_cast<std::size_t>(k)] = a * c;
495 ++k;
496 });
497 }
498 return A;
499 }
503 v.n = numLeaves();
504 v.invVol = HostArr<double>(A.invVol.data());
505 v.coef = HostArr<double>(A.coef.data());
506 v.start = HostArr<Index>(A.start.data());
507 v.nbr = HostArr<Index>(A.nbr.data());
508 v.bcDiag = HostArr<double>(A.bcDiag.data());
509 return v;
510 }
514 void applyFvShared(const std::vector<double>& u, std::vector<double>& out) const {
515 const FvAssembled A = assembleFv();
516 const auto op = hostFvOp(A);
517 const Index n = numLeaves();
518 out.assign(static_cast<std::size_t>(n), 0.0);
519 const HostArr<double> uacc(u.data());
520 for (Index i = 0; i < n; ++i)
521 out[static_cast<std::size_t>(i)] = fvApplyRow(op, i, uacc);
522 }
523
525 void applyLaplacian(const std::vector<double>& u, std::vector<double>& out) const {
526 const Index n = numLeaves();
527 out.assign(static_cast<std::size_t>(n), 0.0);
528 for (Index i = 0; i < n; ++i) {
529 const double ui = u[static_cast<std::size_t>(i)];
530 double acc = 0.0;
531 forEachFaceNeighbor(i, [&](Index j, Real c, int, double a) {
532 acc += a * c * (u[static_cast<std::size_t>(j)] - ui);
533 });
534 out[static_cast<std::size_t>(i)] = acc / cellVolume(i);
535 }
536 }
537
539 double residual(const std::vector<double>& u, const std::vector<double>& rhs,
540 std::vector<double>& res) const {
541 const Index n = numLeaves();
542 res.assign(static_cast<std::size_t>(n), 0.0);
543 double s = 0.0;
544 for (Index i = 0; i < n; ++i) {
545 const double ui = u[static_cast<std::size_t>(i)];
546 double acc = 0.0;
547 forEachFaceNeighbor(i, [&](Index j, Real c, int, double a) {
548 acc += a * c * (u[static_cast<std::size_t>(j)] - ui);
549 });
550 double r = rhs[static_cast<std::size_t>(i)] - acc / cellVolume(i);
551 res[static_cast<std::size_t>(i)] = r;
552 s += cellVolume(i) * r * r;
553 }
554 return std::sqrt(s);
555 }
556
558 void gaussSeidel(std::vector<double>& u, const std::vector<double>& rhs, int sweeps) const {
559 const Index n = numLeaves();
560 for (int s = 0; s < sweeps; ++s)
561 for (Index i = 0; i < n; ++i) {
562 double sumOff = 0.0, diag = 0.0;
563 forEachFaceNeighbor(i, [&](Index j, Real c, int, double a) {
564 sumOff += a * c * u[static_cast<std::size_t>(j)];
565 diag += a * c;
566 });
567 if (diag != 0.0)
568 u[static_cast<std::size_t>(i)] =
569 (sumOff - cellVolume(i) * rhs[static_cast<std::size_t>(i)]) / diag;
570 }
571 }
572
574 void removeMean(std::vector<double>& u) const {
575 const Index n = numLeaves();
576 double sum = 0.0, vol = 0.0;
577 for (Index i = 0; i < n; ++i) {
578 sum += cellVolume(i) * u[static_cast<std::size_t>(i)];
579 vol += cellVolume(i);
580 }
581 double m = sum / vol;
582 for (Index i = 0; i < n; ++i)
583 u[static_cast<std::size_t>(i)] -= m;
584 }
585
586 const Octree& octree() const { return *t_; }
587 const Vec<Dim>& h0() const { return h0_; }
589 Real h0Scalar() const { return h0_[0]; }
592 const std::array<Coord, Dim>& fineExt() const { return fineExt_; }
593 const Vec<Dim>& origin() const { return origin_; }
594
595 private:
596 Coord wrap(long c, int axis) const {
597 long e = static_cast<long>(fineExt_[axis]);
598 return static_cast<Coord>(((c % e) + e) % e);
599 }
600 // A_f / d_f (physical) for a cell of width-units `si` next to one of `sj`.
602 Real coeff(Coord si, Coord sj, int axis) const {
603 Real dist = 0.5 * (static_cast<Real>(si) + static_cast<Real>(sj)) * h0_[axis];
604 return areaOf(si < sj ? si : sj, axis) / dist;
605 }
606 Real coeff(Coord si, Coord sj) const { return coeff(si, sj, 0); }
607 // Physical area of a face NORMAL TO `axis` of a cell of width-units `s`: prod_{d != axis} s*h0_d.
608 Real areaOf(Coord s, int axis) const {
609 Real area = 1;
610 for (int d = 0; d < Dim; ++d)
611 if (d != axis)
612 area *= static_cast<Real>(s) * h0_[d];
613 return area;
614 }
616 Real areaOf(Coord s) const { return areaOf(s, 0); }
617
618 const Octree* t_ = nullptr;
619 Vec<Dim> h0_ = detail::filledVec<Dim>(1.0);
620 std::array<Coord, Dim> fineExt_{};
621 Vec<Dim> origin_{};
622 ExtResolver extResolve_; // distributed seam: out-of-block probe resolver
623 std::vector<std::array<long, Dim>> ghostLo_; // ghost slot → block-local lo (longs)
624 std::vector<unsigned> ghostLv_; // ghost slot → covering-leaf level
625 std::array<long, Dim> frameShift_{}; // block global fine origin (0 single-rank)
626 std::vector<double> alpha_; // per-(leaf+ghost) per-face openness (kFaces per row), or empty
627 bool hasOpen_ = false;
628 bool periodic_ = true; // false ⇒ homogeneous Dirichlet domain walls (boundaryDiag)
629 bool immersedWall_ = false; // true ⇒ velocity operator: (1−α) interior faces are no-slip walls
630};
631
633template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
635 public:
637 using M = typename Octree::M;
638 using Code = typename Octree::Code;
639
646 static bool canLiftLevel(const Octree& t, Index bottomExtent) {
647 const IVec<Dim>& G = t.brick();
648 Index mx = 0;
649 for (int d = 0; d < Dim; ++d)
650 mx = std::max(mx, G[d]);
651 if (mx <= bottomExtent)
652 return false;
653 for (int d = 0; d < Dim; ++d)
654 if ((G[d] % 2) != 0 || (G[d] / 2) < 2)
655 return false;
656 return t.canLiftRoot();
657 }
658
667 void build(const Octree& finest, Real h0, bool liftRoot = true, Index bottomExtent = 4) {
668 build(finest, detail::filledVec<Dim>(h0), liftRoot, bottomExtent);
669 }
673 void build(const Octree& finest, const Vec<Dim>& h0, bool liftRoot = true,
674 Index bottomExtent = 4) {
675 levels_.clear();
676 levels_.push_back(finest);
677 for (;;) {
678 Octree c = levels_.back();
679 Index merged = c.coarsenIf([](Code, unsigned) { return true; });
680 if (merged == 0 || c.numLeaves() == levels_.back().numLeaves())
681 break;
682 levels_.push_back(c);
683 if (c.numLeaves() == 1)
684 break;
685 }
686 // Below the root brick: lift and merge while §6.2 allows it. Every leaf is a root cell here
687 // (the octree loop ran to exhaustion), and an even brick with an even origin tiles complete
688 // octets, so the coarsenIf after a lift merges ALL of them.
689 if (liftRoot) {
690 while (canLiftLevel(levels_.back(), bottomExtent)) {
691 Octree c = levels_.back();
692 c.liftRoot();
693 const Index merged = c.coarsenIf([](Code, unsigned) { return true; });
694 if (merged == 0 || c.numLeaves() == levels_.back().numLeaves())
695 break; // defensive: a level that did not shrink would stall the ladder
696 levels_.push_back(std::move(c));
697 }
698 }
699 ops_.resize(levels_.size());
700 // All levels share the finest h0: a coarse octree's leaves carry a higher
701 // `level`, and cellWidth = h0 * 2^level already encodes the doubled width.
702 for (std::size_t L = 0; L < levels_.size(); ++L)
703 ops_[L].init(levels_[L], h0);
704 // child(fine slot) -> parent(coarse slot) for each fine/coarse pair.
705 c2p_.assign(levels_.size() ? levels_.size() - 1 : 0, {});
706 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
707 const Octree& f = levels_[L];
708 const Octree& c = levels_[L + 1];
709 c2p_[L].resize(static_cast<std::size_t>(f.numLeaves()));
710 // Host-parallel (rung 2): `find` is a pure binary search over the frozen coarse octree and
711 // each fine row owns one c2p slot — disjoint writes.
712 hostParFor(f.numLeaves(), [&](Index i) {
713 // Covering-leaf c2p (see multigrid.hpp): == ancestor+find for merged children, correct
714 // (identity) for root-level rows in mixed-depth ladders, block-alignment-independent.
715 c2p_[L][static_cast<std::size_t>(i)] = c.find(f.code(i));
716 });
717 }
718 }
719
720 std::size_t numLevels() const { return levels_.size(); }
721 const AmrPoisson<Dim, Bits>& op(std::size_t L = 0) const { return ops_[L]; }
722
725 void setPeriodic(bool p) {
726 for (auto& o : ops_)
727 o.setPeriodic(p);
728 }
729
733 void setImmersedWall(bool w) {
734 for (auto& o : ops_)
735 o.setImmersedWall(w);
736 }
737
739 void vcycle(std::size_t L, std::vector<double>& u, const std::vector<double>& rhs, int pre = 2,
740 int post = 2) {
741 if (L + 1 == levels_.size()) {
742 ops_[L].gaussSeidel(u, rhs, 40); // coarsest: solve hard
743 ops_[L].removeMean(u);
744 return;
745 }
746 ops_[L].gaussSeidel(u, rhs, pre);
747 std::vector<double> res;
748 ops_[L].residual(u, rhs, res);
749
750 // Restrict residual: volume-weighted average of children -> parent.
751 const Octree& f = levels_[L];
752 const Octree& c = levels_[L + 1];
753 std::vector<double> crhs(static_cast<std::size_t>(c.numLeaves()), 0.0);
754 std::vector<double> cvol(static_cast<std::size_t>(c.numLeaves()), 0.0);
755 for (Index i = 0; i < f.numLeaves(); ++i) {
756 Index p = c2p_[L][static_cast<std::size_t>(i)];
757 if (p < 0)
758 continue;
759 Real v = ops_[L].cellVolume(i);
760 crhs[static_cast<std::size_t>(p)] += v * res[static_cast<std::size_t>(i)];
761 cvol[static_cast<std::size_t>(p)] += v;
762 }
763 for (Index p = 0; p < c.numLeaves(); ++p)
764 if (cvol[static_cast<std::size_t>(p)] > 0)
765 crhs[static_cast<std::size_t>(p)] /= cvol[static_cast<std::size_t>(p)];
766
767 std::vector<double> ccorr(static_cast<std::size_t>(c.numLeaves()), 0.0);
768 vcycle(L + 1, ccorr, crhs, pre, post);
769
770 // Prolong correction (piecewise constant) and add.
771 for (Index i = 0; i < f.numLeaves(); ++i) {
772 Index p = c2p_[L][static_cast<std::size_t>(i)];
773 if (p >= 0)
774 u[static_cast<std::size_t>(i)] += ccorr[static_cast<std::size_t>(p)];
775 }
776 ops_[L].gaussSeidel(u, rhs, post);
777 ops_[L].removeMean(u);
778 }
779
784 double solveQuad(std::vector<double>& u, const std::vector<double>& rhs, int outer = 30,
785 int cyclesPerOuter = 1) {
786 AmrPoisson<Dim, Bits>& P = ops_[0];
787 const std::size_t n = u.size();
788 std::vector<double> lq, ls, rhsp(n), res;
789 double r = 0.0;
790 for (int o = 0; o < outer; ++o) {
791 P.applyLaplacianQuad(u, lq);
792 P.applyLaplacian(u, ls);
793 for (std::size_t i = 0; i < n; ++i)
794 rhsp[i] = rhs[i] - (lq[i] - ls[i]);
795 for (int c = 0; c < cyclesPerOuter; ++c)
796 vcycle(0, u, rhsp);
797 r = P.residualQuad(u, rhs, res);
798 }
799 return r;
800 }
801
808 template <class OpenFn>
810 if (levels_.empty())
811 return;
812 ops_[0].buildOpenness(openFn);
813 for (std::size_t L = 0; L + 1 < levels_.size(); ++L)
814 coarsenOpenness(L);
815 }
816
824 void setOpennessRaw(std::vector<double> alpha) {
825 if (levels_.empty())
826 return;
827 ops_[0].setOpennessRaw(std::move(alpha));
828 for (std::size_t L = 0; L + 1 < levels_.size(); ++L)
829 coarsenOpenness(L);
830 }
831
832 private:
833 static int faceIdx(int axis, int dir) { return 2 * axis + (dir > 0 ? 0 : 1); }
834
835 // Area-average the level-L face openness onto level L+1 (each coarse face is the
836 // mean of the 2^(Dim-1) fine sub-faces covering it; equal areas => plain mean).
837 void coarsenOpenness(std::size_t L) {
838 const Octree& f = levels_[L];
839 const Octree& c = levels_[L + 1];
840 const int F = 2 * Dim;
841 std::vector<double> ca(static_cast<std::size_t>(c.numLeaves()) * F, 0.0);
842 std::vector<int> cnt(static_cast<std::size_t>(c.numLeaves()) * F, 0);
843 for (Index i = 0; i < f.numLeaves(); ++i) {
844 Index p = c2p_[L][static_cast<std::size_t>(i)];
845 if (p < 0)
846 continue;
847 const std::size_t base = static_cast<std::size_t>(p) * F;
848 if (c.level(p) == f.level(i)) {
849 // identity (cell not coarsened this round): copy every face.
850 for (int axis = 0; axis < Dim; ++axis)
851 for (int dir = -1; dir <= 1; dir += 2) {
852 int fi = faceIdx(axis, dir);
853 ca[base + fi] += ops_[L].faceOpenness(i, axis, dir);
854 cnt[base + fi] += 1;
855 }
856 } else {
857 // merged child: each axis contributes its outward face to the parent face.
858 unsigned oct = M::from_code(f.code(i)).child_index(f.level(i));
859 for (int axis = 0; axis < Dim; ++axis) {
860 int dir = ((oct >> axis) & 1) ? +1 : -1;
861 int fi = faceIdx(axis, dir);
862 ca[base + fi] += ops_[L].faceOpenness(i, axis, dir);
863 cnt[base + fi] += 1;
864 }
865 }
866 }
867 for (std::size_t k = 0; k < ca.size(); ++k)
868 ca[k] = cnt[k] ? ca[k] / cnt[k] : 1.0;
869 ops_[L + 1].setOpennessRaw(std::move(ca));
870 }
871
872 std::vector<Octree> levels_;
873 std::vector<AmrPoisson<Dim, Bits>> ops_;
874 std::vector<std::vector<Index>> c2p_;
875};
876
877} // namespace peclet::amr
878
879#endif // PECLET_AMR_POISSON_HPP
Geometric multigrid for AmrPoisson over a uniformly-coarsened octree hierarchy.
Definition poisson.hpp:634
typename Octree::Code Code
Definition poisson.hpp:638
void setOpenness(OpenFn &&openFn)
Set cut-cell face openness on the finest level from a geometry callable openFn(faceCentreWorld,...
Definition poisson.hpp:809
void setOpennessRaw(std::vector< double > alpha)
Same, but the finest level's openness is handed over ALREADY EVALUATED (alpha is the AmrPoisson::open...
Definition poisson.hpp:824
void setPeriodic(bool p)
Apply a boundary condition to every level (periodic default, or non-periodic homogeneous Dirichlet).
Definition poisson.hpp:725
void vcycle(std::size_t L, std::vector< double > &u, const std::vector< double > &rhs, int pre=2, int post=2)
One V-cycle on level L solving L u = rhs (correction scheme).
Definition poisson.hpp:739
const AmrPoisson< Dim, Bits > & op(std::size_t L=0) const
Definition poisson.hpp:721
std::size_t numLevels() const
Definition poisson.hpp:720
typename Octree::M M
Definition poisson.hpp:637
void setImmersedWall(bool w)
Enable the immersed no-slip (Dirichlet) wall on every level — the velocity operator.
Definition poisson.hpp:733
static bool canLiftLevel(const Octree &t, Index bottomExtent)
The single-rank lift rule of docs/amr_mg_depth.md §6.2, evaluated on one level: may the hierarchy con...
Definition poisson.hpp:646
BlockOctree< Dim, Bits > Octree
Definition poisson.hpp:636
void build(const Octree &finest, Real h0, bool liftRoot=true, Index bottomExtent=4)
Build the hierarchy from a finest octree by uniform coarsening until a single leaf remains (or no ful...
Definition poisson.hpp:667
double solveQuad(std::vector< double > &u, const std::vector< double > &rhs, int outer=30, int cyclesPerOuter=1)
Solve the quadratic C/F operator L_quad u = rhs by deferred correction: the standard-operator V-cycle...
Definition poisson.hpp:784
void build(const Octree &finest, const Vec< Dim > &h0, bool liftRoot=true, Index bottomExtent=4)
Phase 3: every level shares the same PER-AXIS finest spacing — a coarse leaf carries a higher level,...
Definition poisson.hpp:673
Cell-centered FV Poisson operator on one (periodic) block octree.
Definition poisson.hpp:43
static constexpr int kFaces
Definition poisson.hpp:146
typename Octree::M M
Definition poisson.hpp:46
const Vec< Dim > & origin() const
Definition poisson.hpp:593
Index numGhosts() const
Definition poisson.hpp:90
unsigned levelOf(Index slot) const
Octree level of an extended slot (local leaf or declared ghost).
Definition poisson.hpp:98
void removeMean(std::vector< double > &u) const
Subtract the volume-weighted mean (fixes the periodic null space).
Definition poisson.hpp:574
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:167
Real cellWidth(Index i, int axis) const
Cell width along axis (Phase 3: per axis).
Definition poisson.hpp:255
void setFrameShift(const std::array< long, Dim > &s)
Distributed frame shift: this block's global fine origin.
Definition poisson.hpp:96
double residualQuad(const std::vector< double > &u, const std::vector< double > &rhs, std::vector< double > &res) const
L2 norm of rhs - L_quad u.
Definition poisson.hpp:448
double boundaryDiag(Index i) const
Σ over leaf i's Dirichlet-wall faces of the wall weight A_f/(½·cellWidth), folded into the operator d...
Definition poisson.hpp:229
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:329
void applyFvShared(const std::vector< double > &u, std::vector< double > &out) const
out = L u via the SHARED face_csr.hpp FV kernel over the assembled CSR — the same arithmetic the devi...
Definition poisson.hpp:514
const std::array< Coord, Dim > & fineExt() const
Per-axis fine-grid extent (brick·2^lmax) — the periodic wrap modulus.
Definition poisson.hpp:592
const Vec< Dim > & h0() const
Definition poisson.hpp:587
void forEachFaceNeighbor(Index i, Fn &&fn) const
Visit each face neighbour of leaf i: fn(neighbourSlot, coeff, axis, alpha) where coeff = A_f / d_f (p...
Definition poisson.hpp:274
static int faceIndex(int axis, int dir)
Definition poisson.hpp:147
std::pair< Index, unsigned > probeSlot(const std::array< long, Dim > &p) const
Covering slot + level of a block-local probe (longs, possibly outside the block).
Definition poisson.hpp:120
bool immersedWall() const
Definition poisson.hpp:222
double faceOpenness(Index i, int axis, int dir) const
Openness of leaf i's face on (axis,dir); 1 if no openness has been set.
Definition poisson.hpp:150
void applyLaplacianQuad(const std::vector< double > &u, std::vector< double > &out) const
out = L u with the quadratic coarse-fine flux (2nd-order at 2:1 interfaces).
Definition poisson.hpp:426
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:375
void setGhosts(std::vector< std::array< long, Dim > > lo, std::vector< unsigned > lv)
Declare the ghost slots [n, n+nGhost): block-local lo corner (longs — ghosts lie outside the block) a...
Definition poisson.hpp:86
void init(const Octree &t, const Vec< Dim > &h0)
Definition poisson.hpp:58
bool hasOpenness() const
Definition poisson.hpp:156
double residual(const std::vector< double > &u, const std::vector< double > &rhs, std::vector< double > &res) const
res = rhs - L u, returns its L2 norm (sqrt(sum V_i res_i^2)).
Definition poisson.hpp:539
FvCsrOpT< HostArr< double >, HostArr< Index > > hostFvOp(const FvAssembled &A) const
View an FvAssembled as the backend-agnostic FvCsrOpT (c0=0,cD=1 ⇒ pure FV Laplacian).
Definition poisson.hpp:501
typename Octree::Code Code
Definition poisson.hpp:47
const Octree & octree() const
Definition poisson.hpp:586
bool periodic() const
Definition poisson.hpp:212
void setImmersedWall(bool w)
Immersed no-slip (Dirichlet) wall mode.
Definition poisson.hpp:221
Index numLeaves() const
Definition poisson.hpp:206
typename Octree::Coord Coord
Definition poisson.hpp:48
double coarseStar(const std::vector< double > &u, Index coarse, Index fine, int axis) const
Quadratic coarse-fine value: the coarse leaf coarse's field, evaluated by tangential quadratic interp...
Definition poisson.hpp:389
void setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:71
std::function< Index(const std::array< long, Dim > &)> ExtResolver
Route probes that exit the block through an external resolver (the LeafHalo registry): fn(blockLocalP...
Definition poisson.hpp:80
Real cellWidth(Index i) const
The cell width when the octree is CUBIC.
Definition poisson.hpp:261
BlockOctree< Dim, Bits > Octree
Definition poisson.hpp:45
void init(const Octree &t, Real h0)
Cubic-cell overload — the pre-Phase-3 spelling, and exactly the per-axis one with all three spacings ...
Definition poisson.hpp:56
FvAssembled assembleFv() const
Definition poisson.hpp:474
void setOpennessRaw(std::vector< double > a)
Definition poisson.hpp:158
void applyLaplacian(const std::vector< double > &u, std::vector< double > &out) const
out = L u (periodic FV Laplacian).
Definition poisson.hpp:525
Real h0Scalar() const
The single spacing of a CUBIC octree (h0()[0]); every anisotropic path uses h0().
Definition poisson.hpp:589
void gaussSeidel(std::vector< double > &u, const std::vector< double > &rhs, int sweeps) const
sweeps lexicographic Gauss-Seidel relaxations of L u = rhs (in place).
Definition poisson.hpp:558
const std::vector< double > & opennessRaw() const
Definition poisson.hpp:157
AmrPoisson(const Octree &t, Real h0)
Definition poisson.hpp:51
AmrPoisson(const Octree &t, const Vec< Dim > &h0)
Definition poisson.hpp:52
void setResolver(ExtResolver r)
Definition poisson.hpp:81
std::array< long, Dim > loOf(Index slot) const
Block-local lo corner of an extended slot as longs (ghosts may lie outside the block).
Definition poisson.hpp:103
Real cellVolume(Index i) const
Definition poisson.hpp:262
void setPeriodic(bool p)
Boundary condition: periodic (default) wraps every face; non-periodic treats a domain-boundary face a...
Definition poisson.hpp:211
morton::Morton< Dim, Bits > M
unsigned level(Index i) const
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
std::array< std::array< Coord, Dim >, 2 > bounds(Index i) const
Inclusive integer bounds [lo, hi] of leaf i in fine units.
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
The assembled FV (weight-CSR) operator: per-face conductance w = A_f/d_f·openness,...
Definition poisson.hpp:467
std::vector< Index > nbr
neighbour leaf per face, size nFaces
Definition poisson.hpp:470
std::vector< Index > start
CSR row offsets, size n+1.
Definition poisson.hpp:469
std::vector< double > bcDiag
Dirichlet boundary diagonal per cell (0 if periodic)
Definition poisson.hpp:472
std::vector< double > coef
w = A_f/d_f·openness per face, size nFaces
Definition poisson.hpp:471
std::vector< double > invVol
1/V_i, size n
Definition poisson.hpp:468