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
velocity_mg.hpp
Go to the documentation of this file.
1// core — device (Kokkos) REDISCRETIZED velocity multigrid for the AMR momentum solve.
2//
3// The rediscretized counterpart of the Galerkin MomentumMG, mirroring flow's VelocityMG
4// (mac_velocity_mg.hpp) on the octree. The fine level is the *sharp* cut-cell operator (the
5// assembled FaceCsrOp = MomentumOp the BiCGStab matvec uses); the coarse levels are a
6// **staircase rediscretization** of the geometry rather than a Galerkin R·A·P of the fine
7// operator:
8// * coarsen the cell fluid-fraction κ to each level (average of children);
9// * classify κ < ½ → solid (identity row, pinned to 0 by the smoother), else fluid;
10// * a fluid cell is the per-axis const-coeff Helmholtz diag = ρ/dt + 6μ/H², off = −μ/H² to
11// each of its 6 (periodic) face neighbours (H = level cell width) — a face into a classified
12// solid cell is a no-slip wall implicitly (the neighbour is pinned to 0). This is
13// ε-solid-on-coarse for free, exactly as flow's buildVelocityStaircase.
14// Transfers: average restriction + masked piecewise-constant prolongation (no correction into a
15// solid fine cell). Smoother: weighted Jacobi (jacobiMom) — P5 swaps in multicolor-GS.
16//
17// Used as the momentum BiCGStab preconditioner exactly like MomentumMG; selectable so the
18// two coarse-operator strategies can be benchmarked head-to-head on the AMR. The implicit-FOU
19// advection on coarse levels (mirror buildUpwindCoarse) is a follow-up — the viscous staircase is
20// the diffusion preconditioner. Requires a Kokkos build + the morton checkout
21#ifndef PECLET_AMR_VELOCITY_MG_HPP
22#define PECLET_AMR_VELOCITY_MG_HPP
23
24#include <algorithm>
25#include <cmath>
26#include <memory>
27#include <utility>
28#include <vector>
29
30#include "peclet/amr/common.hpp"
34#include "peclet/core/common/view.hpp"
35
36namespace peclet::amr {
37
38template <unsigned Bits = 21u>
40 public:
42 using M = typename Octree::M;
43 using Code = typename Octree::Code;
45
56 void build(const Octree& finest, double h0, double idiag, double mu, const MomentumOp& fineOp,
57 const std::vector<double>& kappa, const std::vector<char>& fluid,
58 const std::vector<char>& cut, Index minCoarse = 256) {
59 build(finest, detail::filledVec<3>(h0), idiag, mu, fineOp, kappa, fluid, cut, minCoarse);
60 }
62 void build(const Octree& finest, const Vec<3>& h0, double idiag, double mu,
63 const MomentumOp& fineOp, const std::vector<double>& kappa,
64 const std::vector<char>& fluid, const std::vector<char>& cut, Index minCoarse = 256) {
65 hmg_ = std::make_unique<AmrMultigrid<3, Bits>>();
66 // liftRoot = false: the momentum hierarchy is NOT continued below the root brick here.
67 // docs/amr_mg_depth.md §12 / WO8 — the guard is `minCoarse` (a staircase-classified feature
68 // vanishes when coarsened below its scale), a property of the momentum operator, not of the
69 // root; until that work order lands this keeps the momentum path bit-identical.
70 hmg_->build(finest, h0, /*liftRoot=*/false); // octree hierarchy + per-level AmrPoisson
71 std::size_t nl = hmg_->numLevels();
72 while (nl > 1 && hmg_->op(nl - 1).octree().numLeaves() < minCoarse)
73 --nl; // pore-scale cap
74 levels_.clear();
75 levels_.resize(nl);
76
77 // Per-level κ (level 0 = fine; coarser = average of children) for the staircase classification.
78 std::vector<std::vector<double>> kap(nl);
79 kap[0] = kappa;
80 for (std::size_t L = 0; L + 1 < nl; ++L) {
81 const Octree& f = hmg_->op(L).octree();
82 const Octree& c = hmg_->op(L + 1).octree();
83 const Index nf = f.numLeaves(), nc = c.numLeaves();
84 std::vector<double> ks(static_cast<std::size_t>(nc), 0.0),
85 kn(static_cast<std::size_t>(nc), 0.0);
86 for (Index i = 0; i < nf; ++i) {
87 Code par = M::from_code(f.code(i)).ancestor(f.level(i) + 1).code();
88 Index p = c.find(par);
89 if (p >= 0) {
90 ks[static_cast<std::size_t>(p)] += kap[L][static_cast<std::size_t>(i)];
91 kn[static_cast<std::size_t>(p)] += 1.0;
92 }
93 }
94 for (Index p = 0; p < nc; ++p)
95 ks[static_cast<std::size_t>(p)] /=
96 (kn[static_cast<std::size_t>(p)] > 0 ? kn[static_cast<std::size_t>(p)] : 1.0);
97 kap[L + 1] = std::move(ks);
98 }
99
100 // Level 0: the sharp fine operator + the clean-fluid EXCLUDE mask (= cut OR solid). This is
101 // flow VelocityMG's Phase-3 fix (doc/velocity_mg_plan.md): the cut cells' D_rescale-scaled
102 // residuals and the solid cells are excluded from the coarse defect (zeroed before restriction
103 // + skipped in prolongation), so the inconsistent sharp-IBM rows never reach the coarse grid —
104 // the fine smoother owns the cut band, the coarse grid solves the clean interior. Without it
105 // the staircase V-cycle diverges at large dt.
106 levels_[0].op = fineOp;
107 {
108 std::vector<char> excl(fluid.size());
109 for (std::size_t i = 0; i < fluid.size(); ++i)
110 excl[i] = (!fluid[i] || cut[i]) ? 1 : 0;
111 levels_[0].solid = toDevice(excl, "vmg_excl0");
112 allocScratch(levels_[0], static_cast<Index>(fluid.size()));
113 // Colour the sharp fine operator (its face graph) for the optional GS smoother — copy the
114 // face CSR off the device once.
115 std::vector<Index> hs(static_cast<std::size_t>(fineOp.n) + 1), hn(fineOp.faceNbr.extent(0));
116 auto ms = Kokkos::create_mirror_view(fineOp.faceStart);
117 auto mn = Kokkos::create_mirror_view(fineOp.faceNbr);
118 Kokkos::deep_copy(ms, fineOp.faceStart);
119 Kokkos::deep_copy(mn, fineOp.faceNbr);
120 for (std::size_t i = 0; i < hs.size(); ++i)
121 hs[i] = ms(static_cast<Index>(i));
122 for (std::size_t i = 0; i < hn.size(); ++i)
123 hn[i] = mn(static_cast<Index>(i));
124 levels_[0].col = greedyColoring(hs, hn, fineOp.n);
125 }
126 // Coarse levels: rediscretized staircase operator + classified solid mask.
127 for (std::size_t L = 1; L < nl; ++L)
128 buildStaircase(L, idiag, mu, kap[L]);
129
130 // Transfer maps L → L+1 (child→parent + child CSR), as in MomentumMG.
131 for (std::size_t L = 0; L + 1 < nl; ++L)
132 buildTransfer(L);
133 }
134
135 std::size_t numLevels() const { return levels_.size(); }
136 Index numLeaves(std::size_t L = 0) const { return levels_[L].op.n; }
137 View<double> x(std::size_t L = 0) { return levels_[L].x; }
138 View<double> b(std::size_t L = 0) { return levels_[L].b; }
140 void setFineOp(const MomentumOp& fineOp) { levels_[0].op = fineOp; }
144 void setGaussSeidel(bool on) { useGS_ = on; }
145
149 void vcycle(int pre = 2, int post = 2, int bottom = 30, double omega = 0.7, std::size_t L = 0) {
150 Level& lv = levels_[L];
152 if (L + 1 == levels_.size()) {
153 smooth(lv, bottom, omega);
154 return;
155 }
156 smooth(lv, pre, omega);
157 residualMom(lv.op, View<const double>(lv.x), bc, lv.res);
158 // Clean-fluid exclude: zero the residual at cut/solid cells before restriction so the
159 // inconsistent sharp-IBM cut-cell residuals never pollute the coarse defect (the fix for the
160 // large-dt staircase divergence — flow VelocityMG mg_mul_mask).
161 zeroMasked(lv.res, View<const char>(lv.solid), lv.op.n);
162 Level& cl = levels_[L + 1];
163 restrictField(lv.childStart, lv.childIdx, View<const double>(lv.res), cl.b, cl.op.n);
164 Kokkos::deep_copy(cl.x, 0.0);
165 vcycle(pre, post, bottom, omega, L + 1);
167 smooth(lv, post, omega);
168 }
169
170 private:
171 struct Level {
172 MomentumOp op;
173 View<char> solid;
174 View<double> x, b, res, tmp;
175 View<Index> c2p, childStart, childIdx;
176 Coloring col;
177 };
178 void smooth(Level& lv, int sweeps, double omega) {
180 if (useGS_)
181 // Undamped GS (omega=1.0): stable on the staircase Helmholtz diagonal, and the passed omega
182 // is Jacobi's damping limit (~0.7) which would needlessly weaken the GS smoothing.
183 for (int s = 0; s < sweeps; ++s)
184 multicolorGSMom(lv.op, lv.x, bc, lv.col, 1.0);
185 else
186 for (int s = 0; s < sweeps; ++s)
187 jacobiMom(lv.op, lv.x, bc, lv.tmp, omega);
188 }
189
190 void allocScratch(Level& lv, Index n) {
191 lv.x = View<double>("vmg_x", static_cast<std::size_t>(n));
192 lv.b = View<double>("vmg_b", static_cast<std::size_t>(n));
193 lv.res = View<double>("vmg_res", static_cast<std::size_t>(n));
194 lv.tmp = View<double>("vmg_tmp", static_cast<std::size_t>(n));
195 }
196
197 // Rediscretized staircase operator on coarse level L from the coarsened κ.
198 void buildStaircase(std::size_t L, double idiag, double mu, const std::vector<double>& kap) {
199 const Poisson& ap = hmg_->op(L);
200 const Octree& oct = ap.octree();
201 const Index n = oct.numLeaves();
202 // Shift floor on the coarse reaction: at large dt (idiag→0) a small immersed object can vanish
203 // from the binary κ classification on the coarsest levels, leaving a singular periodic
204 // Laplacian whose bottom solve diverges. Flooring idiag to μ/L² (the slowest diffusion mode)
205 // keeps every coarse level non-singular (Galerkin avoids this by inheriting the sharp
206 // operator).
207 // Phase 3: a NON-SINGULARITY FLOOR on the coarse reaction, not a discretisation — it keeps
208 // the axis-0 form it has always had (`docs/amr_anisotropic.md` §3, recorded in §10).
209 const double Ldom = ap.h0()[0] * static_cast<double>(oct.brick()[0] * (Index(1) << oct.lmax()));
210 const double id = std::max(idiag, mu / (Ldom * Ldom));
211 std::vector<double> diag(static_cast<std::size_t>(n), 1.0);
212 std::vector<char> solid(static_cast<std::size_t>(n), 1);
213 std::vector<Index> start(static_cast<std::size_t>(n) + 1, 0);
214 std::vector<std::vector<std::pair<Index, double>>> rows(static_cast<std::size_t>(n));
215 for (Index i = 0; i < n; ++i) {
216 if (kap[static_cast<std::size_t>(i)] < 0.5)
217 continue; // classified solid: identity row
218 solid[static_cast<std::size_t>(i)] = 0;
219 // Phase 3: μ/H_a² per axis — the coarse-level twin of the momentum fold's `beta_b`.
220 // Every `coef[a]` is the same double on a cubic octree, so the diagonal sum below is the
221 // old `6*coef` term for term.
222 double mcoef[3]; // (`coef` is the CSR value array further down)
223 for (int a = 0; a < 3; ++a) {
224 const double Ha = ap.cellWidth(i, a); // h0[a]·2^L (uniform per level)
225 mcoef[a] = mu / (Ha * Ha);
226 }
227 double dsum = id;
228 for (int axis = 0; axis < 3; ++axis)
229 for (int dir = -1; dir <= 1; dir += 2) {
230 Index j = ap.periodicNeighbor(i, axis, dir); // periodic wrap; same-level neighbour
231 if (j < 0)
232 continue; // domain boundary (non-periodic): wall
233 rows[static_cast<std::size_t>(i)].emplace_back(j, -mcoef[axis]);
234 dsum += mcoef[axis]; // every face counts toward the diagonal (a wall to a solid nbr too)
235 }
236 diag[static_cast<std::size_t>(i)] = dsum;
237 }
238 for (Index i = 0; i < n; ++i)
239 start[static_cast<std::size_t>(i) + 1] =
240 start[static_cast<std::size_t>(i)] +
241 static_cast<Index>(rows[static_cast<std::size_t>(i)].size());
242 const Index nnz = start[static_cast<std::size_t>(n)];
243 std::vector<Index> nbr(static_cast<std::size_t>(nnz));
244 std::vector<double> coef(static_cast<std::size_t>(nnz));
245 for (Index i = 0; i < n; ++i) {
246 Index k = start[static_cast<std::size_t>(i)];
247 for (auto& e : rows[static_cast<std::size_t>(i)]) {
248 nbr[static_cast<std::size_t>(k)] = e.first;
249 coef[static_cast<std::size_t>(k)] = e.second;
250 ++k;
251 }
252 }
253 Level& lv = levels_[L];
254 lv.op.n = n;
255 lv.op.diag = toDevice(diag, "vmg_diag");
256 lv.op.faceStart = toDevice(start, "vmg_fstart");
257 lv.op.faceNbr = toDevice(nbr, "vmg_fnbr");
258 lv.op.faceCoef = toDevice(coef, "vmg_fcoef");
259 lv.solid = toDevice(solid, "vmg_solid");
260 lv.col = greedyColoring(start, nbr, n); // for the optional GS smoother
261 allocScratch(lv, n);
262 }
263
264 void buildTransfer(std::size_t L) {
265 const Octree& f = hmg_->op(L).octree();
266 const Octree& c = hmg_->op(L + 1).octree();
267 const Index nf = f.numLeaves(), nc = c.numLeaves();
268 std::vector<Index> c2p(static_cast<std::size_t>(nf));
269 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
270 for (Index i = 0; i < nf; ++i) {
271 Code par = M::from_code(f.code(i)).ancestor(f.level(i) + 1).code();
272 Index p = c.find(par);
273 c2p[static_cast<std::size_t>(i)] = p;
274 if (p >= 0)
275 ++cnt[static_cast<std::size_t>(p)];
276 }
277 std::vector<Index> cstart(static_cast<std::size_t>(nc) + 1, 0);
278 for (Index p = 0; p < nc; ++p)
279 cstart[static_cast<std::size_t>(p) + 1] =
280 cstart[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
281 std::vector<Index> cidx(static_cast<std::size_t>(nf));
282 std::vector<Index> cur(cstart.begin(), cstart.end() - 1);
283 for (Index i = 0; i < nf; ++i) {
284 Index p = c2p[static_cast<std::size_t>(i)];
285 if (p >= 0)
286 cidx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
287 }
288 levels_[L].c2p = toDevice(c2p, "vmg_c2p");
289 levels_[L].childStart = toDevice(cstart, "vmg_cstart");
290 levels_[L].childIdx = toDevice(cidx, "vmg_cidx");
291 }
292
293 std::unique_ptr<AmrMultigrid<3, Bits>> hmg_;
294 std::vector<Level> levels_;
295 bool useGS_ = false; // multicolour Gauss–Seidel smoother (opt-in; default weighted Jacobi)
296};
297
298} // namespace peclet::amr
299
300#endif // PECLET_AMR_VELOCITY_MG_HPP
Per-block adaptive octree over block-local Morton codes.
morton::Morton< Dim, Bits > M
typename M::code_type Code
View< double > x(std::size_t L=0)
void setGaussSeidel(bool on)
Opt-in: use multicolour Gauss–Seidel as the smoother (per-level colouring) instead of Jacobi — the st...
Index numLeaves(std::size_t L=0) const
void vcycle(int pre=2, int post=2, int bottom=30, double omega=0.7, std::size_t L=0)
One V-cycle (correction scheme) solving the fine operator.
void setFineOp(const MomentumOp &fineOp)
Re-point level 0 at the (possibly FOU-updated) fine operator before a solve.
AmrPoisson< 3, Bits > Poisson
BlockOctree< 3, Bits > Octree
void build(const Octree &finest, double h0, double idiag, double mu, const MomentumOp &fineOp, const std::vector< double > &kappa, const std::vector< char > &fluid, const std::vector< char > &cut, Index minCoarse=256)
Build the rediscretized hierarchy.
typename Octree::M M
std::size_t numLevels() const
View< double > b(std::size_t L=0)
typename Octree::Code Code
void build(const Octree &finest, const Vec< 3 > &h0, double idiag, double mu, const MomentumOp &fineOp, const std::vector< double > &kappa, const std::vector< char > &fluid, const std::vector< char > &cut, Index minCoarse=256)
Phase 3: the finest spacing per axis; every coarse level inherits the aspect ratio (AM1).
void zeroMasked(View< double > v, View< const char > excl, Index n)
Zero v at excluded cells (excl != 0).
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:94
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:46
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