core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
flow.hpp
Go to the documentation of this file.
1// core — device (Kokkos) collocated incompressible Stokes step on a BlockOctree.
2//
3// The device counterpart of oracle::AmrFlow (flow.hpp): the whole cut-cell IBM projection step
4// runs in Kokkos kernels instead of the host-serial gaussSeidel + host projection the drag
5// study found to be the bottleneck. It reuses the host AmrCutCell / AmrPoisson to *build*
6// the operators (geometry, openness, cut stencils — done once), then drives the time step
7// entirely on the device:
8// * momentum predictor — MomentumOp (assembled cut-cell operator) solved with the
9// parallel BiCGStab of momentum.hpp;
10// * pressure projection — the openness Poisson on Multigrid / PCG;
11// * divergence, ABC gradient correction, rotational pressure update — face-CSR kernels
12// (FaceGeom) that mirror AmrPoisson::forEachFaceFull (same 2:1 sub-faces +
13// openness), so D / G / L stay consistent exactly as in the host collocated coupling.
14//
15// Stokes (advection off) and Navier–Stokes (setAdvection): implicit-FOU + explicit SOU/Koren
16// deferred correction, advected by the divergence-free face field uf (built each projection;
17// falls back to ½(u_i+u_j) until the first projection) — conservative (∇·uf = 0).
18//
19// Requires a Kokkos build + the morton checkout (PECLET_CORE_HAVE_MORTON).
20#ifndef PECLET_CORE_AMR_FLOW_HPP
21#define PECLET_CORE_AMR_FLOW_HPP
22
23#ifdef PECLET_CORE_HAVE_MORTON
24
25#include <array>
26#include <cmath>
27#include <cstdio>
28#include <cstdlib>
29#include <memory>
30#include <vector>
31
32#include "peclet/core/amr/adapt.hpp" // transferField (conservative remap for finishAdapt)
33
34#include "peclet/core/amr/advect_recon.hpp" // shared high-order face reconstruction (host+device)
37#include "peclet/core/amr/face_geom.hpp" // FaceGeom (shared with the device assembler)
38#include "peclet/core/amr/cf_scheme.hpp" // pluggable 2:1 C/F schemes (setCfScheme)
39#include "peclet/core/amr/facegeom_assembly.hpp" // assembleFaceGeom (D4/D6)
40#include "peclet/core/amr/ghost_projection.hpp" // directional ghost overlay (setGhostProjection)
42#include "peclet/core/amr/momentum_assembly.hpp" // assembleMomentum (D3/D6)
49
50#include "peclet/core/amr/distributed_adapt.hpp" // transferGradients (distributed finishAdapt)
51#include "peclet/core/amr/distributed_flow_mg.hpp" // distributed pressure MG (initMpi mode)
54
55namespace peclet::core::amr {
56
59inline bool amrEnvFlag(const char* name) {
60 const char* v = std::getenv(name);
61 return v && v[0] && !(v[0] == '0' && v[1] == '\0');
62}
63
64// FaceGeom (the collocated projection's static face-geometry CSR) now lives in face_geom.hpp so the
65// device assembler and this driver share the type without a circular include.
66
68template <int Dim, unsigned Bits, class FluidFn>
70 const Index n = ap.octree().numLeaves();
71 std::vector<Index> start(static_cast<std::size_t>(n) + 1, 0);
72 for (Index i = 0; i < n; ++i) {
73 Index cnt = 0;
74 ap.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++cnt; });
75 start[static_cast<std::size_t>(i) + 1] = start[static_cast<std::size_t>(i)] + cnt;
76 }
77 const Index nf = start[static_cast<std::size_t>(n)];
78 std::vector<Index> nbr(static_cast<std::size_t>(nf));
79 std::vector<int> axis(static_cast<std::size_t>(nf)), dir(static_cast<std::size_t>(nf));
80 std::vector<double> aArea(static_cast<std::size_t>(nf)), rArea(static_cast<std::size_t>(nf)),
81 dist(static_cast<std::size_t>(nf)), alpha(static_cast<std::size_t>(nf));
82 std::vector<Index> upupI(static_cast<std::size_t>(nf)), upupJ(static_cast<std::size_t>(nf));
83 std::vector<double> invVol(static_cast<std::size_t>(n));
84 std::vector<char> fluid(static_cast<std::size_t>(n));
85 for (Index i = 0; i < n; ++i) {
86 invVol[static_cast<std::size_t>(i)] = 1.0 / ap.cellVolume(i);
87 fluid[static_cast<std::size_t>(i)] = isFluid(i) ? 1 : 0;
88 Index k = start[static_cast<std::size_t>(i)];
89 ap.forEachFaceFull(i, [&](Index j, int ax, int dr, double area, double d, double al) {
90 nbr[static_cast<std::size_t>(k)] = j;
91 axis[static_cast<std::size_t>(k)] = ax;
92 dir[static_cast<std::size_t>(k)] = dr;
93 aArea[static_cast<std::size_t>(k)] = al * area;
94 rArea[static_cast<std::size_t>(k)] = area;
95 dist[static_cast<std::size_t>(k)] = d;
96 alpha[static_cast<std::size_t>(k)] = al;
97 // SOU upstream-of-upwind probes (point neighbour one cell further upstream): if i is the
98 // upwind cell the upstream is across i's −dir face; if j is upwind, across j's +dir face.
99 upupI[static_cast<std::size_t>(k)] = ap.periodicNeighbor(i, ax, -dr);
100 upupJ[static_cast<std::size_t>(k)] = ap.periodicNeighbor(j, ax, dr);
101 ++k;
102 });
103 }
104 FaceGeom g;
105 g.n = n;
106 g.start = toDevice(start, "fg_start");
107 g.nbr = toDevice(nbr, "fg_nbr");
108 g.axis = toDevice(axis, "fg_axis");
109 g.dir = toDevice(dir, "fg_dir");
110 g.alphaArea = toDevice(aArea, "fg_aarea");
111 g.rawArea = toDevice(rArea, "fg_rarea");
112 g.dist = toDevice(dist, "fg_dist");
113 g.alpha = toDevice(alpha, "fg_alpha");
114 g.upupI = toDevice(upupI, "fg_upupi");
115 g.upupJ = toDevice(upupJ, "fg_upupj");
116 g.invVol = toDevice(invVol, "fg_invvol");
117 g.fluid = toDevice(fluid, "fg_fluid");
118 return g;
119}
120
126 auto st = g.start;
127 auto nb = g.nbr;
128 auto ax = g.axis;
129 auto dr = g.dir;
130 auto aA = g.alphaArea;
131 auto iv = g.invVol;
132 auto fl = g.fluid;
133 Kokkos::parallel_for(
134 "amr::flow_div", g.n, KOKKOS_LAMBDA(const Index i) {
135 if (!fl(i)) {
136 div(i) = 0.0;
137 return;
138 }
139 double d = 0.0;
140 for (Index k = st(i); k < st(i + 1); ++k) {
141 const int a = ax(k);
142 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
143 const Index j = nb(k);
144 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
145 d += aA(k) * dr(k) * 0.5 * (ui + uj);
146 }
147 div(i) = d * iv(i);
148 });
149}
150
159 auto st = g.start;
160 auto nb = g.nbr;
161 auto ax = g.axis;
162 auto dr = g.dir;
163 auto di = g.dist;
164 Kokkos::parallel_for(
165 "amr::flow_buildface", g.n, KOKKOS_LAMBDA(const Index i) {
166 for (Index k = st(i); k < st(i + 1); ++k) {
167 const int a = ax(k);
168 const Index j = nb(k);
169 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
170 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
171 const double gphi = (dr(k) > 0) ? (phi(j) - phi(i)) / di(k) : (phi(i) - phi(j)) / di(k);
172 uf(k) = 0.5 * (ui + uj) - gphi;
173 }
174 });
175}
176
179inline double divFaceNorm(const FaceGeom& g, View<const double> uf) {
180 auto st = g.start;
181 auto dr = g.dir;
182 auto aA = g.alphaArea;
183 auto iv = g.invVol;
184 auto fl = g.fluid;
185 double s = 0.0;
186 Kokkos::parallel_reduce(
187 "amr::flow_divface", g.n,
188 KOKKOS_LAMBDA(const Index i, double& acc) {
189 if (!fl(i))
190 return;
191 double d = 0.0;
192 for (Index k = st(i); k < st(i + 1); ++k)
193 d += aA(k) * dr(k) * uf(k);
194 d *= iv(i);
195 acc += d * d;
196 },
197 s);
198 return std::sqrt(s);
199}
200
206 auto st = g.start;
207 auto nb = g.nbr;
208 auto ax = g.axis;
209 auto dr = g.dir;
210 auto di = g.dist;
211 auto al = g.alpha;
212 auto fl = g.fluid;
213 Kokkos::parallel_for(
214 "amr::flow_grad3", g.n, KOKKOS_LAMBDA(const Index i) {
215 if (!fl(i)) {
216 gx(i) = gy(i) = gz(i) = 0.0;
217 return;
218 }
219 const double fi = f(i);
220 double gp[3] = {0, 0, 0}, gm[3] = {0, 0, 0};
221 int np[3] = {0, 0, 0}, nm[3] = {0, 0, 0};
222 for (Index k = st(i); k < st(i + 1); ++k) {
223 if (al(k) <= 1e-12)
224 continue;
225 const int a = ax(k);
226 const double gg = (dr(k) > 0) ? (f(nb(k)) - fi) / di(k) : (fi - f(nb(k))) / di(k);
227 if (dr(k) > 0) {
228 gp[a] += gg;
229 ++np[a];
230 } else {
231 gm[a] += gg;
232 ++nm[a];
233 }
234 }
235 double out[3];
236 for (int a = 0; a < 3; ++a) {
237 const double a1 = np[a] ? gp[a] / np[a] : 0.0;
238 const double a2 = nm[a] ? gm[a] / nm[a] : 0.0;
239 out[a] = 0.5 * (a1 + a2);
240 }
241 gx(i) = out[0];
242 gy(i) = out[1];
243 gz(i) = out[2];
244 });
245}
246
259
263 if (ov.n == 0)
264 return;
265 auto cell = ov.cell;
266 auto idx = ov.idx;
267 auto w = ov.w;
268 Kokkos::parallel_for(
269 "amr::flow_ghostgrad", ov.n, KOKKOS_LAMBDA(const Index s) {
270 const Index i = cell(s);
271 double g[3];
272 for (int a = 0; a < 3; ++a) {
273 double acc = 0.0;
274 for (int k = 0; k < 3; ++k)
275 acc += w(s * 9 + a * 3 + k) * f(idx(s * 9 + a * 3 + k));
276 g[a] = acc;
277 }
278 gx(i) = g[0];
279 gy(i) = g[1];
280 gz(i) = g[2];
281 });
282}
283
289 View<const double> rscale, View<const char> fluid, double idiag, double fc,
290 View<double> b, Index n) {
291 Kokkos::parallel_for(
292 "amr::flow_momrhs", n, KOKKOS_LAMBDA(const Index i) {
293 b(i) = fluid(i) ? (idiag * uc(i) + fc - gradP(i) - adv(i)) * rscale(i) : 0.0;
294 });
295}
296
308 bool useFace) {
309 auto st = g.start;
310 auto nb = g.nbr;
311 auto ax = g.axis;
312 auto dr = g.dir;
313 auto ra = g.rawArea;
314 auto iv = g.invVol;
315 auto fl = g.fluid;
316 Kokkos::parallel_for(
317 "amr::flow_buildfou", g.n, KOKKOS_LAMBDA(const Index i) {
318 if (!fl(i)) {
319 advDiag(i) = 0.0;
320 for (Index k = st(i); k < st(i + 1); ++k)
321 advCoef(k) = 0.0;
322 return;
323 }
324 const double rs = rscale(i);
325 double dsum = 0.0;
326 for (Index k = st(i); k < st(i + 1); ++k) {
327 const Index j = nb(k);
328 if (!fl(j)) {
329 advCoef(k) = 0.0;
330 continue;
331 }
332 const int a = ax(k);
333 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
334 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
335 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (ui + uj);
336 const double w = rs * rho * ra(k) * velOut * iv(i);
337 if (velOut < 0.0) {
338 advCoef(k) = w; // inflow → off-diagonal toward upstream neighbour j
339 } else {
340 advCoef(k) = 0.0;
341 dsum += w; // outflow → diagonal
342 }
343 }
344 advDiag(i) = dsum;
345 });
346}
347
356 View<const double> u2, int comp, double rho, int advScheme,
358 auto st = g.start;
359 auto nb = g.nbr;
360 auto ax = g.axis;
361 auto dr = g.dir;
362 auto ra = g.rawArea;
363 auto iv = g.invVol;
364 auto fl = g.fluid;
365 auto uiP = g.upupI;
366 auto ujP = g.upupJ;
367 Kokkos::parallel_for(
368 "amr::flow_defsou", g.n, KOKKOS_LAMBDA(const Index i) {
369 if (!fl(i)) {
370 defc(i) = 0.0;
371 return;
372 }
373 auto fld = [&](Index c) { return (comp == 0) ? u0(c) : (comp == 1) ? u1(c) : u2(c); };
374 double sou = 0.0, fou = 0.0;
375 for (Index k = st(i); k < st(i + 1); ++k) {
376 const Index j = nb(k);
377 if (!fl(j))
378 continue;
379 const int a = ax(k);
380 const double uai = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
381 const double uaj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
382 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (uai + uaj);
383 const Index up = (velOut > 0.0) ? i : j;
384 const Index down = (velOut > 0.0) ? j : i;
385 const Index upup = (velOut > 0.0) ? uiP(k) : ujP(k);
386 const double phiUp = fld(up);
387 const double phiUpUp = (upup >= 0 && fl(upup)) ? fld(upup) : phiUp;
388 const double phiDown = fld(down);
389 const double phiFace = hoFaceValue(phiUpUp, phiUp, phiDown, advScheme); // shared recon
390 sou += ra(k) * velOut * phiFace;
391 fou += ra(k) * velOut * fld(up); // FOU flux = velOut · upwind value
392 }
393 defc(i) = rho * iv(i) * (sou - fou); // ρ·(SOU − FOU), unscaled
394 });
395}
396
400 View<const double> u2, int comp, double rho, int advScheme,
402 auto st = g.start;
403 auto nb = g.nbr;
404 auto ax = g.axis;
405 auto dr = g.dir;
406 auto ra = g.rawArea;
407 auto iv = g.invVol;
408 auto fl = g.fluid;
409 auto uiP = g.upupI;
410 auto ujP = g.upupJ;
411 Kokkos::parallel_for(
412 "amr::flow_advexpl", g.n, KOKKOS_LAMBDA(const Index i) {
413 if (!fl(i)) {
414 defc(i) = 0.0;
415 return;
416 }
417 auto fld = [&](Index c) { return (comp == 0) ? u0(c) : (comp == 1) ? u1(c) : u2(c); };
418 double sou = 0.0;
419 for (Index k = st(i); k < st(i + 1); ++k) {
420 const Index j = nb(k);
421 if (!fl(j))
422 continue;
423 const int a = ax(k);
424 const double uai = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
425 const double uaj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
426 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (uai + uaj);
427 const Index up = (velOut > 0.0) ? i : j;
428 const Index down = (velOut > 0.0) ? j : i;
429 const Index upup = (velOut > 0.0) ? uiP(k) : ujP(k);
430 const double phiUp = fld(up);
431 const double phiUpUp = (upup >= 0 && fl(upup)) ? fld(upup) : phiUp;
432 const double phiDown = fld(down);
433 const double phiFace = hoFaceValue(phiUpUp, phiUp, phiDown, advScheme); // shared recon
434 sou += ra(k) * velOut * phiFace;
435 }
436 defc(i) = rho * sou * iv(i); // ρ·SOU (fully explicit)
437 });
438}
439
442 Kokkos::parallel_for(
443 "amr::flow_correct", n, KOKKOS_LAMBDA(const Index i) {
444 if (fluid(i))
445 uc(i) -= gphi(i);
446 });
447}
448
451 View<const char> fluid, double rho_dt, double mu, Index n) {
452 Kokkos::parallel_for(
453 "amr::flow_presupd", n, KOKKOS_LAMBDA(const Index i) {
454 if (fluid(i))
455 p(i) += rho_dt * phi(i) - mu * div(i);
456 });
457}
458
459// ===========================================================================
460// AmrFlow — collocated Stokes projection step, fully on device.
461// ===========================================================================
462template <unsigned Bits = 21u>
463class AmrFlow {
464 public:
466
467 void init(const Octree& t, Real h0, Vec<3> origin = Vec<3>{}) {
468 t_ = &t;
469 h0_ = h0;
470 origin_ = origin;
471 dist_ = nullptr; // single-rank unless initMpi is used
472 }
473
483 init(d.local(), d.h0(), d.globalGeometry().origin);
484 dist_ = &d;
485 }
486 void setDensity(double rho) { rho_ = rho; }
487 void setViscosity(double mu) { mu_ = mu; }
488 void setDt(double dt) { dt_ = dt; }
489 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
491 void setPressurePCG(bool on) { presPCG_ = on; }
506 void setGhostGradient(bool on) { ghostGrad_ = on; }
534 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2) {
535 ghostProjReq_ = on ? 1 : 0; // explicit selection disables the AUTO default
536 gpMatrixOrder_ = matrixOrder;
537 gpRhsOrder_ = rhsOrder;
538 }
548 void setCfScheme(int scheme) { cfScheme_ = static_cast<CfScheme>(scheme); }
555 void setAdvection(bool on) { advect_ = on; }
558 void setImplicitAdvection(bool on) { implicitFou_ = on; }
560 void setAdvectionScheme(int s) { advScheme_ = s; }
570 void setMomentumTol(double tol) { momTol_ = tol; }
578 void setMomentumMG(bool on) { momMGon_ = on; }
579
584 void setVelocityMGStaircase(bool on) { useStaircaseMG_ = on; }
589 void setVelocityMGMinCoarse(Index m) { mgMinCoarse_ = m; }
593 void setMomentumGS(bool on) { momGS_ = on; }
594
607 void setMomentumMGSolver(bool on) { momMGSolver_ = on; }
608
614 void setOuterIterations(int n, double tol = 1e-6) {
615 outerIters_ = (n < 1) ? 1 : n;
616 outerTol_ = tol;
617 }
618
621 template <class SdfFn>
623 const Index n = t_->numLeaves();
624 if (dist_ && cfScheme_ != CfScheme::standard)
625 throw std::runtime_error(
626 "amr::AmrFlow: the C/F quadratic scheme is not distributed yet (rung-4 follow-up)");
627 // Host operator build (geometry, openness, cut stencils) — same as oracle::AmrFlow::setSolid.
628 mom_.init(*t_, h0_, origin_);
629 pres_.init(*t_, h0_);
630 pres_.setOrigin(origin_);
631 // Resolve the projection mode. DEFAULT SWITCH (2026-08-25, user decision): AUTO = the
632 // GHOST (fluid-only) projection — family-free/stable/unique (see the setter doc) — falling
633 // back to the aperture projection with a stderr notice when the finest band is too thin for
634 // the overlay (probed below; the July AUTO arm restored, now unconditional). Explicit
635 // setGhostProjection(true/false) pins the scheme (band violations then THROW as before).
636 // The overlay is built below (it needs only mom_'s sdf samples + pres_'s topology walk,
637 // no openness).
638 const bool wantGhost = (ghostProjReq_ != 0); // explicit on (1) or AUTO (-1)
639 ghostProj_ = wantGhost; // provisional; AUTO may fall back below
640 if (dist_) {
641 // Distributed: install the resolver seams and run every prober to the miss-collect
642 // fixpoint (docs/amr_distributed_flow.md). Freezes the ±2 halo, leaves mom_ FULLY built
643 // (its final round ran with every ghost resolved) and the solver hooks installed.
645 } else {
646 nExt_ = n;
647 allred_ = {};
648 momSolver_.setDistributed({}, {}, 0);
649 pcg_.setDistributed({}, {}, 0);
650 mom_.build(sdfFn, /*idiag=*/rho_ / dt_, /*beta=*/mu_ / (h0_ * h0_));
651 }
653 if (ghostProj_) { // probe the band margin; explicit request throws on violation, AUTO falls
654 bool viol = false;
655 hov = buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
656 if (dist_) {
657 // COLLECTIVE band-margin decision: the flag is agreed across ranks before anyone
658 // commits (one rank throwing/falling back alone would deadlock the MG collectives).
659 int lv = viol ? 1 : 0, gv = 0;
660 MPI_Allreduce(&lv, &gv, 1, MPI_INT, MPI_LOR, dist_->comm());
661 viol = gv != 0;
662 }
663 if (viol) {
664 if (ghostProjReq_ == 1)
665 throw std::runtime_error(
666 "amr ghost projection: an overlay row's ±2 closure reach crosses a 2:1 level "
667 "boundary — widen the refineToSdf band margin");
668 ghostProj_ = false; // AUTO: fall back to the aperture projection
670 "peclet::core AmrFlow: AUTO scheme fell back to the aperture projection (the "
671 "finest band is too thin for the ghost overlay). Select explicitly with "
672 "setGhostProjection to silence this notice.\n");
673 }
674 }
675 if (ghostProj_) {
676 // Ghost projection: the pressure geometry is the BINARY openness on the unchanged MG
677 // rails; the closure physics lives in the overlay (built above).
678 auto binFn = makeBinaryOpenFn([&sdfFn](const Vec<3>& p) { return sdfFn(p); }, h0_);
679 pres_.buildOpenness(binFn);
680 if (dist_)
681 presMGD_.build(*dist_, h0_, binFn, &dhalo_);
682 else
683 presMG_.build(*t_, h0_, binFn, /*periodic=*/true);
684 ghostGrad_ = true; // the directional gradient is part of the scheme
685 // Fragmentation guard: pockets outside the main binary component are decoupled (see
686 // findPocketCells) — folded into maskC_ below and hidden from the directional gradients.
687 // Single-rank host BFS only: the DISTRIBUTED label-propagation guard is a rung-6 item
688 // (a rank-local BFS would mislabel components that span ranks), so multi-rank runs are
689 // unprotected on fragmenting geometries until it lands.
690 gpPocket_ = dist_ ? std::vector<char>{} : findPocketCells(*t_, pres_, mom_.sdfCRaw());
691 } else {
692 gpPocket_.clear();
693 auto openFn = [&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); };
694 pres_.buildOpenness(openFn);
695 if (dist_)
696 presMGD_.build(*dist_, h0_, openFn, &dhalo_);
697 else
698 presMG_.build(*t_, h0_, openFn, /*periodic=*/true);
699 }
700 // Singular periodic pressure: per-level nullspace projection.
701 if (dist_) {
702 presMGD_.setRemoveMean(true);
703 pcg_.setDistributed([this](View<double> v) { presMGD_.sync(0, v); }, allred_,
704 presMGD_.extendedSize(0));
705 } else {
706 presMG_.setRemoveMean(true);
707 }
708
709 // Device assembly (D6): the static cut-cell momentum operator CSR and the collocated face
710 // geometry are assembled ON THE DEVICE (assembleMomentum / assembleFaceGeom), and the
711 // pressure MG operators are device-assembled per level (Multigrid D5) — so no host CSR walk and
712 // no operator round-trip. Each is bit-for-bit identical to the host assembler on OpenMP (locked
713 // in test_amr_momentum / test_amr_facegeom), so the flow result is unchanged. A shared device
714 // octree view backs both assemblers.
715 // DISTRIBUTED: assemble on the HOST through the resolver seam instead (the device walkers
716 // cannot resolve cross-block probes) and upload — same parity-locked builders, ghost
717 // columns included.
719 if (!dist_)
720 ov.upload(*t_);
721 if (dist_) {
722 auto A = mom_.assembleOperator();
723 momOp_ = MomentumOp{};
724 momOp_.n = n;
725 momOp_.diag = toDevice(A.diag, "mo_diag");
726 momOp_.faceStart = toDevice(A.start, "mo_start");
727 momOp_.faceNbr = toDevice(A.nbr, "mo_nbr");
728 momOp_.faceCoef = toDevice(A.coef, "mo_coef");
729 } else {
730 momOp_ = assembleMomentum<Bits>(mom_, ov); // static Stokes operator (hasAdv stays false)
731 }
732 // Velocity multigrid (momentum preconditioner): the Galerkin hierarchy A_c = R·A·P built
733 // directly from the exact assembled momentum CSR. Consistent with the fine cut-cell
734 // operator by construction (inherits the ξ-overlay + D_rescale row scaling; a coarse cell
735 // of all-solid children stays an identity row). It only changes the preconditioner (the
736 // BiCGStab matvec is the exact operator) ⇒ same converged solution, but the iteration
737 // count stays ~flat with N instead of growing like the Jacobi-preconditioned BiCGStab.
738 // Build the chosen momentum-MG: Galerkin (MomentumMG) by default, or the rediscretized
739 // staircase (VelocityMG) — both from the static Stokes operator, once.
740 // DISTRIBUTED: the Galerkin MG becomes RANK-LOCAL (additive-Schwarz preconditioner): the
741 // local rows of the exact operator with the ghost COLUMNS dropped. At np=1 nothing is
742 // dropped, so the preconditioner — and hence the whole step — stays bit-identical to the
743 // single-rank path; at np>1 it is a block preconditioner (iterations may grow with np,
744 // the converged step is unchanged — the preconditioner never moves the solution). The
745 // EXACT cross-rank Galerkin RAP is the documented follow-up
746 // (docs/amr_distributed_flow.md §4c); the staircase variant is single-rank-only for now
747 // (distributed falls back to Jacobi-with-halo preconditioning).
748 if (momMGon_ && dist_ && !useStaircaseMG_) {
749 auto A = mom_.assembleOperator();
750 std::vector<Index> lstart(1, 0);
751 std::vector<Index> lnbr;
752 std::vector<double> lcoef;
753 for (Index i = 0; i < n; ++i) {
754 for (Index k = A.start[static_cast<std::size_t>(i)];
755 k < A.start[static_cast<std::size_t>(i) + 1]; ++k)
756 if (A.nbr[static_cast<std::size_t>(k)] < n) { // drop ghost columns (Schwarz cut)
757 lnbr.push_back(A.nbr[static_cast<std::size_t>(k)]);
758 lcoef.push_back(A.coef[static_cast<std::size_t>(k)]);
759 }
760 lstart.push_back(static_cast<Index>(lnbr.size()));
761 }
762 momMG_.build(*t_, A.diag, lstart, lnbr, lcoef);
763 momMG_.setGaussSeidel(momGS_);
764 } else if (momMGon_ && !dist_) {
765 if (useStaircaseMG_) {
766 std::vector<double> kap(static_cast<std::size_t>(n));
767 std::vector<char> fl(static_cast<std::size_t>(n)), cu(static_cast<std::size_t>(n));
768 for (Index i = 0; i < n; ++i) {
769 kap[static_cast<std::size_t>(i)] = mom_.kappa(i);
770 fl[static_cast<std::size_t>(i)] = mom_.isFluid(i) ? 1 : 0;
771 cu[static_cast<std::size_t>(i)] = mom_.isCut(i) ? 1 : 0;
772 }
773 velMG_.build(*t_, h0_, rho_ / dt_, mu_, momOp_, kap, fl, cu, mgMinCoarse_);
774 velMG_.setGaussSeidel(momGS_);
775 } else {
776 // The Galerkin RAP hierarchy is a host triple-product over the fine CSR, so it needs the
777 // operator on the host; assemble it there for the MG build only (bit-identical to momOp_).
778 auto A = mom_.assembleOperator();
779 momMG_.build(*t_, A.diag, A.start, A.nbr, A.coef);
780 momMG_.setGaussSeidel(momGS_);
781 }
782 }
783 std::vector<char> fluidVec(static_cast<std::size_t>(n));
784 for (Index i = 0; i < n; ++i)
785 fluidVec[static_cast<std::size_t>(i)] = mom_.isFluid(i) ? 1 : 0;
786 if (dist_) {
787 // Host walker through the resolver seam (nbr/upup may be ghost slots), then EXTEND the
788 // fluid flags over the ghost tail — the advection kernels read fl(j)/fl(upup) at ghost
789 // slots (mom_.fluidRaw() carries the ghost metadata filled from the world SdfFn).
790 geom_ = buildFaceGeom(pres_, [&](Index i) { return mom_.isFluid(i); });
791 geom_.fluid = toDevice(mom_.fluidRaw(), "fg_fluid_ext");
792 } else {
793 geom_ = assembleFaceGeom<Bits>(pres_, fluidVec, ov);
794 }
795 std::vector<double> rs(static_cast<std::size_t>(n));
796 for (Index i = 0; i < n; ++i)
797 rs[static_cast<std::size_t>(i)] = mom_.rhsScale(i);
798 rscale_ = toDevice(rs, "df_rscale");
799 fluid_ = geom_.fluid;
800 if (ghostGrad_)
801 buildGhostGradOverlay();
802 else
803 gc_ = GhostGradOverlay{};
804 // C/F interface scheme overlays (cf_scheme.hpp): the same host builders the oracle uses
805 // (parity by construction), uploaded once. Momentum delta = ×μ on the α=1 velocity geometry
806 // (regular fluid rows; cut rows are finest-band: no C/F faces).
807 if (cfScheme_ != CfScheme::standard) {
808 auto fluidOk = [&](Index i) { return mom_.isFluid(i); };
809 auto rowFluid = [&](Index i) { return mom_.isFluid(i); };
810 auto rowRegular = [&](Index i) { return mom_.isFluid(i) && !mom_.isCut(i); };
811 cfMom_ = uploadCfCsr(buildCfLapDelta(mom_.lap(), *t_, mu_, rowRegular, fluidOk, cfScheme_),
812 "cf_mom");
813 cfDiv_ = uploadCfCompCsr(buildCfDivDelta(pres_, *t_, rowFluid, fluidOk, cfScheme_),
814 "cf_div");
815 auto gd = buildCfGradDelta(pres_, *t_, rowFluid, fluidOk, cfScheme_);
816 for (int a = 0; a < 3; ++a)
817 cfGrad_[static_cast<std::size_t>(a)] =
818 uploadCfCsr(gd[static_cast<std::size_t>(a)], "cf_grad");
819 auto ufd = buildCfUfDelta(pres_, *t_, fluidOk, cfScheme_);
820 cfUfVel_ = uploadCfCompCsr(ufd.vel, "cf_ufvel");
821 cfUfPhi_ = uploadCfCsr(ufd.phi, "cf_ufphi");
822 } else {
823 cfMom_ = CfCsrDev{};
824 cfDiv_ = CfCompCsrDev{};
825 for (int a = 0; a < 3; ++a)
826 cfGrad_[static_cast<std::size_t>(a)] = CfCsrDev{};
827 cfUfVel_ = CfCompCsrDev{};
828 cfUfPhi_ = CfCsrDev{};
829 }
830 if (ghostProj_) {
831 // Closure overlay (pre-built in the mode-resolve step above) + the coupled-subspace mask
832 // for the BiCGStab projection.
833 gpOv_ = uploadGhostOverlay(hov);
834 std::vector<double> mc(static_cast<std::size_t>(n), 0.0);
835 for (Index i = 0; i < n; ++i)
836 mc[static_cast<std::size_t>(i)] =
837 (mom_.isFluid(i) && !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(i)]))
838 ? 1.0
839 : 0.0;
840 for (Index r = 0; r < hov.n; ++r)
841 if (!hov.coupled[static_cast<std::size_t>(r)])
842 mc[static_cast<std::size_t>(hov.cell[static_cast<std::size_t>(r)])] = 0.0;
843 maskC_ = toDevice(mc, "gp_maskc");
844 // Krylov scratch carries the ghost tail (matvec inputs) in distributed mode.
845 auto mk = [&](const char* l) { return View<double>(l, static_cast<std::size_t>(nExt_)); };
846 gpr_ = mk("gp_r");
847 gprh_ = mk("gp_rhat");
848 gpp_ = mk("gp_p");
849 gpph_ = mk("gp_phat");
850 gpv_ = mk("gp_v");
851 gps_ = mk("gp_s");
852 gpsh_ = mk("gp_shat");
853 gpt_ = mk("gp_t");
854 }
855
856 // Device state. Fields whose ghost entries are READ through the CSRs / overlays (u, p, φ)
857 // — plus div (its View is deep_copied into the nExt-sized distributed MG rhs) and the u
858 // snapshots (full-extent deep_copies of u) — carry the ghost tail [n, nExt); nExt == n
859 // single-rank, so those allocations are unchanged there.
860 for (int c = 0; c < 3; ++c) {
861 u_[c] = View<double>("df_u", static_cast<std::size_t>(nExt_));
862 gx_[c] = View<double>("df_g", static_cast<std::size_t>(n));
863 Kokkos::deep_copy(u_[c], 0.0);
864 }
865 p_ = View<double>("df_p", static_cast<std::size_t>(nExt_));
866 phi_ = View<double>("df_phi", static_cast<std::size_t>(nExt_));
867 div_ = View<double>("df_div", static_cast<std::size_t>(nExt_));
868 uf_ = View<double>("df_uf",
869 geom_.nbr.extent(0)); // ABC divergence-free face field (per CSR face)
870 faceFieldBuilt_ = false;
871 bmom_ = View<double>("df_bmom", static_cast<std::size_t>(n));
872 Kokkos::deep_copy(p_, 0.0);
873 // Implicit-FOU advection state. The momentum operator + its velocity-MG are rebuilt each
874 // step from the *full* operator (viscous + FOU) so the MG is advection-aware (the viscous-
875 // only MG diverges on the advection operator at cut cells); the FOU is baked into the CSR
876 // (momOp_.hasAdv stays false). defc holds the device-computed explicit ρ(SOU−FOU)
877 // deferred correction. uadvHost_ caches u^n on the host for the per-step operator rebuild.
878 for (int c = 0; c < 3; ++c) {
879 defc_[c] = View<double>("df_defc", static_cast<std::size_t>(n));
880 Kokkos::deep_copy(defc_[c], 0.0);
881 // uⁿ snapshot for the backward-Euler mass term (frozen across Picard outer iters) + the
882 // previous outer iterate for the outer-loop convergence test.
883 u0_[c] = View<double>("df_u0", static_cast<std::size_t>(nExt_));
884 uprev_[c] = View<double>("df_uprev", static_cast<std::size_t>(nExt_));
885 }
886 // Device-resident implicit-FOU advection: the FOU operator (advDiag + per-face advCoef over the
887 // face-geometry CSR) is rebuilt on device each step from uⁿ and added to the static Stokes
888 // operator in the matvec (no host round-trip). The static operator + Galerkin velocity-MG are
889 // built once (above); the advection is a perturbation the static MG still preconditions.
890 advDiag_ = View<double>("df_advdiag", static_cast<std::size_t>(n));
891 advCoef_ = View<double>("df_advcoef", geom_.nbr.extent(0));
892 momOp_.advStart = geom_.start;
893 momOp_.advNbr = geom_.nbr;
894 momOp_.advDiag = advDiag_;
895 momOp_.advCoef = advCoef_;
896 momOp_.hasAdv = false; // set per-step when advection is on
897 momSolver_.setJacobi(2, 0.7);
898 // Generic MG preconditioner: dispatch the chosen hierarchy's V-cycle (z = M⁻¹ r), decoupling
899 // the solver from the MG type so Galerkin and staircase are interchangeable. Distributed:
900 // the rank-local Galerkin hierarchy built above (staircase unsupported there ⇒ Jacobi).
901 if (momMGon_ && !(dist_ && useStaircaseMG_)) {
902 if (useStaircaseMG_)
903 momSolver_.setPreconditioner(
904 [this](View<const double> r, View<double> z) { runMgVcycle(velMG_, r, z); });
905 else
906 momSolver_.setPreconditioner(
907 [this](View<const double> r, View<double> z) { runMgVcycle(momMG_, r, z); });
908 }
909 pcg_.setVcycle(2, 2, 60, 0.8);
910 pcg_.setSingular(true);
911 n_ = n;
912 }
913
917 void step(int momIters = 100, int presIters = 60) {
918 const Index n = n_;
919 const double idiag = rho_ / dt_;
920 lastMomIters_ = 0;
921 lastOuterIters_ = 1;
922 // Freeze the time-level uⁿ for the backward-Euler mass term + warm start; it stays anchored
923 // across the Picard outer iterations (only the advecting velocity re-lags). For outerIters_==1
924 // this is just a copy of uⁿ ⇒ bit-identical to the single lagged step.
925 for (int c = 0; c < 3; ++c)
926 Kokkos::deep_copy(u0_[c], View<const double>(u_[c]));
927 // −∇p^n is constant across the outer iterations (pressure is projected once, after the loop,
928 // like flow's single per-step projection) ⇒ hoist it out.
929 syncScalar(p_); // ghost tail of pⁿ before the gradient reads (no-op single-rank)
930 grad3(geom_, View<const double>(p_), gx_[0], gx_[1], gx_[2]);
931 for (int a = 0; a < 3; ++a) // 2nd-order C/F face gradients (level-boundary rows)
932 cfApply(cfGrad_[static_cast<std::size_t>(a)], View<const double>(p_), gx_[a]);
933 applyGhostGrad(gc_, View<const double>(p_), gx_[0], gx_[1], gx_[2]);
934 // Picard outer loop over the lagged advection only (the momentum nonlinearity); for
935 // outerIters_==1 this is the single lagged predictor, then one projection — bit-identical to
936 // before.
937 for (int outer = 0; outer < outerIters_; ++outer) {
938 // --- advection lagged to the *current* predictor iterate (uⁿ on the first pass):
939 // implicit-FOU operator + explicit ρ(SOU−FOU) deferred correction. The matvec stays linear
940 // during each solve (the advecting velocity is frozen in advDiag_/advCoef_). With advection
941 // OFF the operator and RHS are identical every pass, so a second pass reproduces the first ⇒
942 // instant early-stop. ---
943 if (advect_) {
944 momOp_.hasAdv = implicitFou_;
945 syncVel(); // ghost tails of the (lagged) advecting velocity for buildFou/deferredSou
946 // Advect with the divergence-free face field uf (from the previous projection); fall back
947 // to ½(u_i+u_j) before the first projection has built it. The implicit FOU and the explicit
948 // SOU/FOU deferred correction use the SAME velocity ⇒ the FOU cancels at steady state
949 // (host-parity).
950 const View<const double> ufv(uf_);
951 if (implicitFou_)
952 buildFou(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
953 View<const double>(u_[2]), rho_, View<const double>(rscale_), advDiag_, advCoef_,
954 ufv, faceFieldBuilt_);
955 for (int c = 0; c < 3; ++c) {
956 if (implicitFou_)
958 View<const double>(u_[2]), c, rho_, advScheme_, defc_[c], ufv,
959 faceFieldBuilt_);
960 else
962 View<const double>(u_[2]), c, rho_, advScheme_, defc_[c], ufv,
963 faceFieldBuilt_);
964 }
965 // The staircase MG's fine level mirrors the sharp operator; refresh it so it picks up the
966 // current advection state (hasAdv). (The Galerkin MG is the static viscous operator.)
967 if (momMGon_ && useStaircaseMG_)
968 velMG_.setFineOp(momOp_);
969 }
970 // --- predictor: incremental BE viscous (+ implicit-FOU) solve per component, RHS carries
971 // −∇p^n and −ρ(SOU−FOU); the mass term is anchored at uⁿ (u0_), the solve warm-starts at the
972 // current iterate. ---
973 for (int c = 0; c < 3; ++c) {
975 View<const double>(rscale_), View<const char>(fluid_), idiag, f_[c], bmom_, n);
976 // C/F-scheme deferred correction on the velocity diffusion: b += μ(∇²_scheme − ∇²_std)
977 // of the lagged component (regular rows only, rscale = 1 there).
978 cfApply(cfMom_, View<const double>(u_[c]), bmom_);
979 // P4 (opt-in): the velocity-MG used as the *solver* — MG-preconditioned defect correction,
980 // no Krylov (the flow RB-GS/velocity-MG mirror; cannot break down on the non-symmetric
981 // operator) — vs the default MG-preconditioned BiCGStab. Both reach the same solution (the
982 // matvec is the exact operator); the choice only trades robustness for convergence rate.
983 lastMomIters_ +=
984 (momMGSolver_ ? momSolver_.solveDefectCorrection(
985 momOp_, u_[c], View<const double>(bmom_), momIters, momTol_)
986 : momSolver_.solveBiCGStab(momOp_, u_[c], View<const double>(bmom_),
987 momIters, momTol_))
988 .iters;
989 }
990 // Outer-loop convergence on the predictor velocity (skipped for the default outerIters_==1,
991 // so that path is untouched). With advection off the second iterate equals the first ⇒ stops
992 // at 2.
993 if (outerIters_ > 1) {
994 lastOuterIters_ = outer + 1;
995 if (outer > 0) {
996 double dmax = 0.0;
997 for (int c = 0; c < 3; ++c)
998 dmax = std::max(
1000 if (dmax < outerTol_)
1001 break;
1002 }
1003 for (int c = 0; c < 3; ++c)
1004 Kokkos::deep_copy(uprev_[c], View<const double>(u_[c]));
1005 }
1006 }
1007 project(presIters); // single pressure projection per step (flow structure)
1008 }
1009
1011 void project(int presIters = 60) {
1012 const Index n = n_;
1013 syncVel(); // ghost tails of u* for the divergence (+ the ghost-closed overlay delta)
1014 divergence(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
1015 View<const double>(u_[2]), div_);
1016 cfApplyComp(cfDiv_, View<const double>(u_[0]), View<const double>(u_[1]),
1017 View<const double>(u_[2]), div_); // 2nd-order C/F face averages (setCfScheme)
1018 if (ghostProj_) // ghost-closed constraint: binary div (geom_ carries binary α) + overlay
1020 View<const double>(u_[2]), div_);
1021 Kokkos::deep_copy(phi_, 0.0);
1022 if (ghostProj_) {
1023 lastPresIters_ = solveGhostBiCGStab(phi_, View<const double>(div_), presIters);
1025 return;
1026 }
1027 // Two selectable pressure drivers, like flow's CutcellMG: MG-PCG (default, presPCG_) and the
1028 // bounded stationary V-cycle (setPressurePCG(false)). MG-PCG covers ADVECTION too — the
1029 // historic exclusion ("transient near-nullspace issue") was characterised 2026-08-19 and was
1030 // NOT a property of the operator (which is geometry-only, advection-independent, SPD in the
1031 // volume-weighted inner product): the aperture RHS is INCOMPATIBLE by a fluid-mean component
1032 // (div_ is zeroed at solid-centered cells whose faces are partially open, breaking the
1033 // telescoping that would make Σ V·div = 0 over the operator's DOF set; the defect grows with
1034 // the developed flow, ~3e-3 relative at steady state on the Z&H sphere). The un-deflated
1035 // V-cycle's residual therefore STALLS at exactly |mean|·sqrt(V_fluid) — the old "60 bounded
1036 // cycles" was a stagnation cap, not a convergence count. The PCG's per-iteration fluid-range
1037 // projection (maskSolid + volume-weighted fluid-mean removal) deflates exactly that component,
1038 // so CG is valid and healthy: measured flat 15–17 iters/step (tol 1e-10) across the whole
1039 // impulsive N=32 transient, steady K identical to the V-cycle path to 4+ digits.
1040 // Debug knob (env, default-off): PECLET_CORE_AMR_PRES_DEBUG=1 — per-cycle/-solve residual +
1041 // RHS-compatibility trace to stderr (the characterisation instrumentation, kept).
1042 const bool dbg = amrEnvFlag("PECLET_CORE_AMR_PRES_DEBUG");
1043 if (dbg && !dist_) {
1044 // RHS compatibility: the operator's left null vector is the constant over fluid cells in the
1045 // volume-weighted inner product, so a solvable RHS needs Σ V_i·div_i ≈ 0 over fluid cells.
1046 const FvOp& op0 = presMG_.op(0);
1047 auto st = op0.faceStart;
1048 auto w = op0.faceW;
1049 auto bcD = op0.bcDiag;
1050 auto iv = op0.invVol;
1051 auto dv = div_;
1052 double su = 0.0, sv = 0.0, sn = 0.0;
1053 Kokkos::parallel_reduce(
1054 "amr::dbg_compat", n,
1055 KOKKOS_LAMBDA(const Index i, double& a, double& b, double& c) {
1056 double d = bcD(i);
1057 for (Index k = st(i); k < st(i + 1); ++k)
1058 d += w(k);
1059 const double m = (d > 1e-30) ? 1.0 : 0.0;
1060 a += m * dv(i) / iv(i);
1061 b += m / iv(i);
1062 c += m * dv(i) * dv(i) / iv(i);
1063 },
1064 su, sv, sn);
1065 std::fprintf(stderr, "[amr pres] rhs fluid-mean=%.3e |rhs|_D=%.3e (rel mean %.3e)\n",
1066 su / sv, std::sqrt(sn), (su / sv) / (std::sqrt(sn) + 1e-300));
1067 if (!presDbgSpdDone_) {
1068 // One-shot direct SPD probe of the assembled pressure operator (advection cannot enter
1069 // the assembly — this measures it): symmetry <y,Lx>_D vs <x,Ly>_D on deterministic
1070 // pseudo-random vectors, and the Rayleigh quotient sign (L negative-semidefinite).
1071 presDbgSpdDone_ = true;
1072 View<double> xr("dbg_x", static_cast<std::size_t>(n)),
1073 yr("dbg_y", static_cast<std::size_t>(n)), Ax("dbg_Ax", static_cast<std::size_t>(n)),
1074 Ay("dbg_Ay", static_cast<std::size_t>(n));
1075 Kokkos::parallel_for(
1076 "amr::dbg_fill", n, KOKKOS_LAMBDA(const Index i) {
1077 xr(i) = std::sin(0.7 * static_cast<double>(i) + 0.3);
1078 yr(i) = std::cos(1.3 * static_cast<double>(i) + 1.1);
1079 });
1083 double s = 0.0;
1084 Kokkos::parallel_reduce(
1085 "amr::dbg_dot", n,
1086 KOKKOS_LAMBDA(const Index i, double& acc) { acc += a(i) * b(i) / iv(i); }, s);
1087 return s;
1088 };
1093 std::fprintf(stderr,
1094 "[amr pres] SPD probe: <y,Lx>_D=%.15e <x,Ly>_D=%.15e rel asym=%.2e; "
1095 "Rayleigh <x,Lx>_D=%.3e <y,Ly>_D=%.3e (must be <=0)\n",
1096 yLx, xLy, std::fabs(yLx - xLy) / (std::fabs(yLx) + 1e-300), xLx, yLy);
1097 }
1098 }
1099 if (presPCG_) {
1100 const auto R =
1101 dist_ ? pcg_.solve(presMGD_, phi_, View<const double>(div_), presIters, 1e-10)
1102 : pcg_.solve(presMG_, phi_, View<const double>(div_), presIters, 1e-10);
1103 lastPresIters_ = R.iters;
1104 if (dbg)
1105 std::fprintf(stderr, "[amr pres] pcg iters=%d res0=%.3e res=%.3e rel=%.3e\n", R.iters,
1106 R.res0, R.res, R.res0 > 0 ? R.res / R.res0 : 0.0);
1107 } else if (dist_) {
1108 Kokkos::deep_copy(presMGD_.b(0), div_);
1109 Kokkos::deep_copy(presMGD_.x(0), 0.0);
1110 for (int it = 0; it < presIters; ++it)
1111 presMGD_.vcycle(2, 2, 60, 0.8);
1112 Kokkos::deep_copy(phi_, presMGD_.x(0));
1113 lastPresIters_ = presIters;
1114 } else {
1115 Kokkos::deep_copy(presMG_.b(0), div_);
1116 Kokkos::deep_copy(presMG_.x(0), 0.0);
1118 if (dbg)
1119 rdbg = View<double>("pres_dbg_res", static_cast<std::size_t>(n));
1120 for (int it = 0; it < presIters; ++it) {
1121 presMG_.vcycle(2, 2, 60, 0.8);
1122 if (dbg) {
1123 residualFv(presMG_.op(0), View<const double>(presMG_.x(0)),
1124 View<const double>(presMG_.b(0)), rdbg);
1125 const double rn =
1127 std::fprintf(stderr, "[amr pres] vcycle %2d |r|=%.6e\n", it + 1, rn);
1128 }
1129 }
1130 Kokkos::deep_copy(phi_, presMG_.x(0));
1131 lastPresIters_ = presIters;
1132 }
1134 }
1135
1140 // u*'s ghost tail is current (project() synced it and u is untouched since); φ's is not.
1141 syncScalar(phi_);
1143 View<const double>(u_[2]), View<const double>(phi_), uf_);
1144 // 2nd-order C/F face values (setCfScheme): distance-weighted average + coarse* substitution
1145 // on the 2:1 sub-faces — the advecting flux matches the (quad) divergence constraint.
1146 cfApplyComp(cfUfVel_, View<const double>(u_[0]), View<const double>(u_[1]),
1147 View<const double>(u_[2]), uf_);
1148 cfApply(cfUfPhi_, View<const double>(phi_), uf_);
1149 faceFieldBuilt_ = true;
1150 grad3(geom_, View<const double>(phi_), gx_[0], gx_[1], gx_[2]);
1151 for (int a = 0; a < 3; ++a) // 2nd-order C/F face gradients (level-boundary rows)
1152 cfApply(cfGrad_[static_cast<std::size_t>(a)], View<const double>(phi_), gx_[a]);
1153 applyGhostGrad(gc_, View<const double>(phi_), gx_[0], gx_[1], gx_[2]);
1154 for (int c = 0; c < 3; ++c)
1155 correct(u_[c], View<const double>(gx_[c]), View<const char>(fluid_), n);
1157 rho_ / dt_, mu_, n);
1158 }
1159
1160 // ---- adaptivity during a run (ladder step 5) -------------------------------------------------
1161 // The octree is borrowed by pointer and mutated EXTERNALLY (refine/coarsen/balance/adapt on the
1162 // same object). beginAdapt snapshots the current topology + fields; after the mutation,
1163 // finishAdapt conservatively remaps u and the accumulated rotational p onto the new mesh
1164 // (minmod-limited linear transferField) and rebuilds every solver structure via setSolid. The
1165 // caller must keep the cut band at the finest level on the new mesh (refineToSdf the geometry
1166 // band again after a solution-driven adapt): the ghost overlay build throws / auto-falls-back
1167 // exactly as in setSolid. uf restarts from the ½-average fallback for one step.
1168
1175 void beginAdapt() {
1176 adaptOldT_ = std::make_unique<Octree>(*t_);
1177 for (int c = 0; c < 3; ++c)
1178 adaptU_[static_cast<std::size_t>(c)] = velocity(c);
1179 adaptP_ = pressure();
1180 if (dist_) {
1181 // Halo-completed prolongation gradients on the OLD mesh (while dist_ still holds it):
1182 // the block-local transfer stencil zeroes gradients at interior block boundaries,
1183 // which would make the remap np-dependent there (measured ~5% field divergence).
1184 for (int c = 0; c < 3; ++c)
1185 adaptGradU_[static_cast<std::size_t>(c)] =
1186 transferGradients(*dist_, adaptU_[static_cast<std::size_t>(c)]);
1187 adaptGradP_ = transferGradients(*dist_, adaptP_);
1188 }
1189 }
1190
1192 template <class SdfFn>
1194 if (!adaptOldT_)
1195 throw std::runtime_error("amr::AmrFlow::finishAdapt called without beginAdapt");
1196 std::array<std::vector<double>, 3> nu;
1197 for (int c = 0; c < 3; ++c)
1198 nu[static_cast<std::size_t>(c)] =
1199 transferField(*adaptOldT_, adaptU_[static_cast<std::size_t>(c)], *t_, /*linear=*/true,
1200 dist_ ? &adaptGradU_[static_cast<std::size_t>(c)] : nullptr);
1201 std::vector<double> np = transferField(*adaptOldT_, adaptP_, *t_, /*linear=*/true,
1202 dist_ ? &adaptGradP_ : nullptr);
1203 setSolid(sdfFn); // full operator/overlay rebuild on the new topology (zeroes the fields)
1204 for (int c = 0; c < 3; ++c) {
1205 setVelocity(c, nu[static_cast<std::size_t>(c)]);
1206 zeroSolid(u_[c]); // cells that became solid on the new mesh hold 0 (no-slip state)
1207 }
1208 setPressure(np);
1209 zeroSolid(p_); // solid p is pinned/decoupled
1210 adaptOldT_.reset();
1211 for (int c = 0; c < 3; ++c)
1212 adaptU_[static_cast<std::size_t>(c)].clear();
1213 adaptP_.clear();
1214 }
1215
1222 template <class SdfFn>
1224 if (!dist_)
1225 throw std::runtime_error("amr::AmrFlow::rebalanceMpi requires initMpi");
1226 std::vector<std::vector<double>> cols(4);
1227 for (int c = 0; c < 3; ++c)
1228 cols[static_cast<std::size_t>(c)] = velocity(c);
1229 cols[3] = pressure();
1230 dist_->rebalance(cols); // t_ still points at dist_->local(), now the new block's octree
1231 setSolid(sdfFn); // full rebuild (registry, halos, operators) on the new block
1232 for (int c = 0; c < 3; ++c) {
1233 setVelocity(c, cols[static_cast<std::size_t>(c)]);
1234 zeroSolid(u_[c]);
1235 }
1236 setPressure(cols[3]);
1237 zeroSolid(p_);
1238 }
1239
1241 void setPressure(const std::vector<double>& h) {
1242 auto m = Kokkos::create_mirror_view(p_);
1243 for (Index i = 0; i < n_; ++i)
1244 m(i) = h[static_cast<std::size_t>(i)];
1245 Kokkos::deep_copy(p_, m);
1246 }
1247
1250 auto fl = fluid_;
1251 Kokkos::parallel_for(
1252 "amr::flow_zerosolid", n_, KOKKOS_LAMBDA(const Index i) {
1253 if (!fl(i))
1254 v(i) = 0.0;
1255 });
1256 }
1257
1260 std::vector<double> debugSou(int comp) {
1261 syncVel();
1262 View<double> s("dbg_sou", static_cast<std::size_t>(n_));
1264 View<const double>(u_[2]), comp, 1.0, advScheme_, s, View<const double>(uf_),
1265 false);
1266 std::vector<double> h(static_cast<std::size_t>(n_));
1267 auto m = Kokkos::create_mirror_view(s);
1268 Kokkos::deep_copy(m, s);
1269 for (Index i = 0; i < n_; ++i)
1270 h[static_cast<std::size_t>(i)] = m(i);
1271 return h;
1272 }
1276 template <class MG>
1278 // Element copies over the LOCAL rows (not deep_copy): in distributed mode the Krylov
1279 // scratch carries a ghost tail while the (rank-local) MG levels are local-sized; the
1280 // solver refreshes z's ghost tail before the next matvec. Same values single-rank.
1281 const Index nl = mg.numLeaves(0);
1282 {
1283 auto b0 = mg.b(0);
1284 Kokkos::parallel_for(
1285 "amr::mgv_b", nl, KOKKOS_LAMBDA(const Index i) { b0(i) = r(i); });
1286 }
1287 Kokkos::deep_copy(mg.x(0), 0.0);
1288 mg.vcycle(mgVcPre_, mgVcPre_, mgVcBottom_, 0.7);
1289 {
1290 auto x0 = mg.x(0);
1291 Kokkos::parallel_for(
1292 "amr::mgv_z", nl, KOKKOS_LAMBDA(const Index i) { z(i) = x0(i); });
1293 }
1294 }
1297 double m = 0.0;
1298 Kokkos::parallel_reduce(
1299 "amr::flow_maxdiff", n,
1300 KOKKOS_LAMBDA(const Index i, double& lm) {
1301 double d = a(i) - b(i);
1302 if (d < 0.0)
1303 d = -d;
1304 if (d > lm)
1305 lm = d;
1306 },
1307 Kokkos::Max<double>(m));
1308 return m;
1309 }
1311 void copyToHost(const View<double>& d, std::vector<double>& h) const {
1312 auto m = Kokkos::create_mirror_view(d);
1313 Kokkos::deep_copy(m, d);
1314 for (Index i = 0; i < n_; ++i)
1315 h[static_cast<std::size_t>(i)] = m(i);
1316 }
1318 void setVelocity(int c, const std::vector<double>& h) {
1319 auto m = Kokkos::create_mirror_view(u_[c]);
1320 for (Index i = 0; i < n_; ++i)
1321 m(i) = h[static_cast<std::size_t>(i)];
1322 Kokkos::deep_copy(u_[c], m);
1323 }
1326 std::vector<double> velocity(int c) const { return localVector(u_[c]); }
1329 std::vector<double> pressure() const { return localVector(p_); }
1330
1332 std::vector<double> localVector(const View<double>& v) const {
1333 if (v.extent(0) == static_cast<std::size_t>(n_))
1334 return peclet::core::toVector(v);
1335 View<double> packed(Kokkos::view_alloc("amr::local_packed", Kokkos::WithoutInitializing),
1336 static_cast<std::size_t>(n_));
1337 Kokkos::parallel_for(
1338 "amr::pack_local", n_, KOKKOS_LAMBDA(const Index i) { packed(i) = v(i); });
1340 }
1341
1346 std::vector<double> velocities() const {
1347 View<double> packed(Kokkos::view_alloc("amr::vel_packed", Kokkos::WithoutInitializing),
1348 static_cast<std::size_t>(n_) * 3);
1349 for (int c = 0; c < 3; ++c) {
1350 auto uc = u_[c];
1351 auto p = packed;
1352 const int cc = c;
1353 Kokkos::parallel_for(
1354 "amr::pack_vel", n_, KOKKOS_LAMBDA(const Index i) { p(i * 3 + cc) = uc(i); });
1355 }
1357 }
1360 double divNormL2() {
1361 syncVel();
1362 divergence(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
1363 View<const double>(u_[2]), div_);
1364 cfApplyComp(cfDiv_, View<const double>(u_[0]), View<const double>(u_[1]),
1365 View<const double>(u_[2]), div_);
1366 if (ghostProj_)
1368 View<const double>(u_[2]), div_);
1369 return std::sqrt(allSum(dotPlain(View<const double>(div_), View<const double>(div_), n_)));
1370 }
1373 double divNormFace() {
1374 const double l = divFaceNorm(geom_, View<const double>(uf_));
1375 return std::sqrt(allSum(l * l));
1376 }
1379 std::vector<double> faceField() const { return peclet::core::toVector(uf_); }
1380 Index numLeaves() const { return n_; }
1382 Index numGhostCells() const { return nExt_ - n_; }
1384 bool isFluid(Index i) const { return mom_.isFluid(i); }
1386 int lastMomIters() const { return lastMomIters_; }
1388 int lastPresIters() const { return lastPresIters_; }
1390 int lastOuterIters() const { return lastOuterIters_; }
1391
1392 // ---- distributed plumbing (docs/amr_distributed_flow.md, rung 4) ----------------------------
1393
1395 void syncVel() {
1396 if (dist_)
1397 dhex_.exchange3(u_[0], u_[1], u_[2]);
1398 }
1401 if (dist_)
1402 dhex_.exchange(v);
1403 }
1405 double allSum(double s) const {
1406 if (!dist_)
1407 return s;
1408 double g = 0.0;
1409 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, dist_->comm());
1410 return g;
1411 }
1413 const FvOp& gpOp0() { return dist_ ? presMGD_.op(0) : presMG_.op(0); }
1414
1418 template <class SdfFn>
1420 dhalo_.init(*dist_);
1421 for (int a = 0; a < 3; ++a)
1422 shiftD_[a] = dist_->blockFineOrigin()[a];
1423 auto resv = [hp = &dhalo_, sh = shiftD_](const std::array<long, 3>& p) -> Index {
1424 std::array<long, 3> g = p;
1425 for (int a = 0; a < 3; ++a)
1426 g[a] += sh[a];
1427 return hp->resolveGlobal(g);
1428 };
1429 mom_.setFrameShift(shiftD_);
1430 mom_.setResolver(resv);
1431 pres_.setFrameShift(shiftD_);
1432 pres_.setResolver(resv);
1433 const Index n = t_->numLeaves();
1434 const double beta = mu_ / (h0_ * h0_);
1435 for (;;) {
1436 installGhostMeta(); // metadata for every ghost known so far (same-round hits read it)
1437 mom_.build(sdfFn, rho_ / dt_, beta); // ±1 probes (also fills the extended sdfC/fluid)
1438 // FaceGeom probes: the full face enumeration + the SOU upstream-of-upwind ±2 reach.
1439 for (Index i = 0; i < n; ++i)
1440 pres_.forEachFaceFull(i, [&](Index j, int ax, int dr, double, double, double) {
1441 (void)pres_.periodicNeighbor(i, ax, -dr);
1442 if (j >= 0)
1443 (void)pres_.periodicNeighbor(j, ax, dr);
1444 });
1445 // Overlay / directional-gradient ±2 chains (every cut cell is a non-clean overlay row,
1446 // so this covers buildGhostGradOverlay's probes too). Discovery only — result discarded.
1447 if (ghostProj_ || ghostGrad_) {
1448 bool viol = false;
1449 (void)buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
1450 }
1451 if (dhalo_.resolveMisses() == 0)
1452 break;
1453 }
1455 dhalo_.finalize();
1456 nExt_ = dhalo_.extendedSize();
1457 dhex_.init(dhalo_);
1458 allred_ = [this](double s) { return allSum(s); };
1459 momSolver_.setDistributed([this](View<double> v) { dhex_.exchange(v); }, allred_, nExt_);
1460 }
1461
1464 const Index ng = dhalo_.numGhosts();
1465 std::vector<std::array<long, 3>> glo(static_cast<std::size_t>(ng));
1466 std::vector<unsigned> glv(static_cast<std::size_t>(ng));
1467 for (Index g = 0; g < ng; ++g) {
1468 for (int a = 0; a < 3; ++a)
1469 glo[static_cast<std::size_t>(g)][a] =
1470 static_cast<long>(dhalo_.ghostCoord(g)[a]) - shiftD_[a];
1471 glv[static_cast<std::size_t>(g)] =
1472 static_cast<unsigned>(dhalo_.level(dhalo_.numLocal() + g));
1473 }
1474 mom_.setGhosts(glo, glv); // copies — pres_ takes the originals
1475 pres_.setGhosts(std::move(glo), std::move(glv));
1476 }
1477
1478 // (public like runMgVcycle: nvcc rejects extended device lambdas in private member functions)
1479 // Project onto the coupled subspace: pin decoupled rows (solid-centered + no-phi-coupling
1480 // overlay rows) to 0 and remove the volume-weighted mean over the coupled cells (the constant
1481 // null mode of the connected fluid region) — removeMeanVol with the coupled mask (the mean
1482 // reduced globally in distributed mode; allred_ is empty single-rank ⇒ bit-identical).
1484 removeMeanVolReduced(v, gpOp0().invVol, maskC_, n_, allred_);
1485 }
1486
1487 // Nonsymmetric ghost pressure matvec: y = P[rho·(L_bin x + Delta x)]. The caller keeps x's
1488 // ghost tail current (syncScalar before every call in distributed mode).
1490 applyFv(gpOp0(), x, y);
1491 ghostApplyDelta(gpOv_, x, y);
1492 gpProject(y);
1493 }
1494
1495 // Preconditioner: two binary-openness V-cycles (the unchanged MG hierarchy) + projection.
1497 if (dist_) {
1498 Kokkos::deep_copy(presMGD_.b(0), r);
1499 Kokkos::deep_copy(presMGD_.x(0), 0.0);
1500 presMGD_.vcycle(2, 2, 60, 0.8);
1501 presMGD_.vcycle(2, 2, 60, 0.8);
1502 Kokkos::deep_copy(z, presMGD_.x(0));
1503 } else {
1504 Kokkos::deep_copy(presMG_.b(0), r);
1505 Kokkos::deep_copy(presMG_.x(0), 0.0);
1506 presMG_.vcycle(2, 2, 60, 0.8);
1507 presMG_.vcycle(2, 2, 60, 0.8);
1508 Kokkos::deep_copy(z, presMG_.x(0));
1509 }
1510 gpProject(z);
1511 }
1512
1513 // MG-preconditioned BiCGStab on the ghost pressure operator (device mirror of the oracle's
1514 // solveGhostBiCGStab: same projection, same stagnation guard against the small
1515 // attainable-residual floor of the slightly incompatible ghost system). Returns iterations.
1517 const Index n = n_;
1518 // Dots are locally summed then globally reduced (allSum = identity single-rank); every
1519 // matvec input's ghost tail is refreshed first. Stagnation/early-break branches depend on
1520 // reduced scalars only ⇒ every rank takes the same branch.
1521 syncScalar(x);
1523 {
1524 auto r = gpr_;
1525 auto bb = b;
1526 Kokkos::parallel_for(
1527 "amr::gp_r0", n, KOKKOS_LAMBDA(const Index i) { r(i) = bb(i) - r(i); });
1528 }
1529 gpProject(gpr_);
1530 Kokkos::deep_copy(gprh_, gpr_);
1531 const double res0 =
1532 std::sqrt(allSum(dotPlain(View<const double>(gpr_), View<const double>(gpr_), n)));
1533 if (res0 == 0.0)
1534 return 0;
1535 double rho = 1, alpha = 1, omega = 1, best = res0;
1536 int noImprove = 0;
1537 Kokkos::deep_copy(gpv_, 0.0);
1538 Kokkos::deep_copy(gpp_, 0.0);
1539 int it = 0;
1540 for (; it < maxIters; ++it) {
1541 const double rhoNew =
1543 if (rhoNew == 0.0)
1544 break;
1545 const double beta = (rhoNew / rho) * (alpha / omega);
1546 bicgPUpdate(gpp_, View<const double>(gpr_), View<const double>(gpv_), beta, omega, n);
1547 ghostPrec(View<const double>(gpp_), gpph_);
1548 syncScalar(gpph_);
1549 ghostMatvec(View<const double>(gpph_), gpv_);
1550 const double rhatV =
1552 if (rhatV == 0.0)
1553 break;
1554 alpha = rhoNew / rhatV;
1555 Kokkos::deep_copy(gps_, gpr_);
1556 axpy(gps_, -alpha, View<const double>(gpv_), n);
1557 const double snorm =
1558 std::sqrt(allSum(dotPlain(View<const double>(gps_), View<const double>(gps_), n)));
1559 if (snorm <= tol * res0) {
1560 axpy(x, alpha, View<const double>(gpph_), n);
1561 ++it;
1562 break;
1563 }
1564 ghostPrec(View<const double>(gps_), gpsh_);
1565 syncScalar(gpsh_);
1566 ghostMatvec(View<const double>(gpsh_), gpt_);
1567 const double tt = allSum(dotPlain(View<const double>(gpt_), View<const double>(gpt_), n));
1568 omega = (tt != 0.0)
1570 : 0.0;
1571 axpy(x, alpha, View<const double>(gpph_), n);
1572 axpy(x, omega, View<const double>(gpsh_), n);
1573 Kokkos::deep_copy(gpr_, gps_);
1574 axpy(gpr_, -omega, View<const double>(gpt_), n);
1575 const double rnorm =
1576 std::sqrt(allSum(dotPlain(View<const double>(gpr_), View<const double>(gpr_), n)));
1577 if (rnorm <= tol * res0) {
1578 ++it;
1579 break;
1580 }
1581 if (rnorm < 0.999 * best) {
1582 best = rnorm;
1583 noImprove = 0;
1584 } else if (++noImprove >= 6) {
1585 ++it;
1586 break; // attainable-residual floor (compatibility gap) — stagnation guard
1587 }
1588 rho = rhoNew;
1589 if (omega == 0.0)
1590 break;
1591 }
1592 gpProject(x);
1593 return it;
1594 }
1595
1596 private:
1597 // Host build of the ghost-gradient overlay (setGhostGradient): one row per cut cell (fluid
1598 // with a solid face neighbour — the cells where the ABC grad3 is gauge-dependent O(1/h)),
1599 // holding a 3-point directional FD stencil per axis. Mirrors oracle::AmrFlow::gradOfDir: cut
1600 // cells have same-level face neighbours by the finest-band contract; the ±2 probe falls back
1601 // to the 2-point one-sided closure when that cell is solid or not same-level.
1602 void buildGhostGradOverlay() {
1603 const Index n = t_->numLeaves();
1604 std::vector<Index> cells;
1605 for (Index i = 0; i < n; ++i)
1606 if (mom_.isCut(i))
1607 cells.push_back(i);
1608 const Index m = static_cast<Index>(cells.size());
1609 std::vector<Index> idx(static_cast<std::size_t>(m) * 9, 0);
1610 std::vector<double> w(static_cast<std::size_t>(m) * 9, 0.0);
1611 // Pocket cells (fragmentation guard) count as solid for the directional gradient: their φ is
1612 // pinned/decoupled, and reading the pinned 0 is the gauge-dependent defect the ghost
1613 // gradient exists to avoid. Levels via pres_.levelOf: ghost-slot-safe in distributed
1614 // builds (identical to t_->level for local leaves; the pocket guard is single-rank-only —
1615 // gpPocket_ stays empty distributed until the label-propagation guard lands).
1616 auto ok = [&](Index j, Index i) {
1617 return j >= 0 && mom_.isFluid(j) && pres_.levelOf(j) == pres_.levelOf(i) &&
1618 !(!gpPocket_.empty() && j < n && gpPocket_[static_cast<std::size_t>(j)]);
1619 };
1620 for (Index s = 0; s < m; ++s) {
1621 const Index i = cells[static_cast<std::size_t>(s)];
1622 const double h = pres_.cellWidth(i);
1623 for (int a = 0; a < 3; ++a) {
1624 const std::size_t o = static_cast<std::size_t>(s) * 9 + static_cast<std::size_t>(a) * 3;
1625 for (int k = 0; k < 3; ++k)
1626 idx[o + static_cast<std::size_t>(k)] = i; // safe defaults (w = 0)
1627 const Index jp = pres_.periodicNeighbor(i, a, +1);
1628 const Index jm = pres_.periodicNeighbor(i, a, -1);
1629 const bool ap = ok(jp, i), am = ok(jm, i);
1630 if (am && ap) {
1631 idx[o] = jp;
1632 w[o] = 0.5 / h;
1633 idx[o + 1] = jm;
1634 w[o + 1] = -0.5 / h;
1635 } else if (ap) {
1636 const Index jpp = pres_.periodicNeighbor(jp, a, +1);
1637 if (ok(jpp, i)) {
1638 idx[o] = i;
1639 w[o] = -1.5 / h;
1640 idx[o + 1] = jp;
1641 w[o + 1] = 2.0 / h;
1642 idx[o + 2] = jpp;
1643 w[o + 2] = -0.5 / h;
1644 } else {
1645 idx[o] = jp;
1646 w[o] = 1.0 / h;
1647 idx[o + 1] = i;
1648 w[o + 1] = -1.0 / h;
1649 }
1650 } else if (am) {
1651 const Index jmm = pres_.periodicNeighbor(jm, a, -1);
1652 if (ok(jmm, i)) {
1653 idx[o] = i;
1654 w[o] = 1.5 / h;
1655 idx[o + 1] = jm;
1656 w[o + 1] = -2.0 / h;
1657 idx[o + 2] = jmm;
1658 w[o + 2] = 0.5 / h;
1659 } else {
1660 idx[o] = i;
1661 w[o] = 1.0 / h;
1662 idx[o + 1] = jm;
1663 w[o + 1] = -1.0 / h;
1664 }
1665 } // sandwiched: all weights stay 0
1666 }
1667 }
1668 gc_.n = m;
1669 gc_.cell = toDevice(cells, "gc_cell");
1670 gc_.idx = toDevice(idx, "gc_idx");
1671 gc_.w = toDevice(w, "gc_w");
1672 }
1673
1674 // flow ccFractionCore aperture (verbatim from oracle::AmrFlow::faceFrac).
1675 template <class SdfFn>
1676 double faceFrac(SdfFn&& sdfFn, const Vec<3>& fc, int axis) const {
1677 double sd = sdfFn(fc);
1678 if (sd <= 0.0)
1679 return 0.0;
1680 Vec<3> g{};
1681 for (int d = 0; d < 3; ++d) {
1682 Vec<3> pp = fc, pm = fc;
1683 pp[d] += h0_;
1684 pm[d] -= h0_;
1685 g[d] = (sdfFn(pp) - sdfFn(pm)) / (2.0 * h0_);
1686 }
1687 double gmag = std::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
1688 if (gmag < 1e-6)
1689 gmag = 1e-6;
1690 int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
1691 double denom = (std::fabs(g[t1]) + std::fabs(g[t2])) / gmag * h0_;
1692 if (denom < 1e-9)
1693 denom = 1e-9;
1694 double frac = 0.5 + sd / denom;
1695 return frac < 0.0 ? 0.0 : (frac > 1.0 ? 1.0 : frac);
1696 }
1697
1698 const Octree* t_ = nullptr;
1699 Real h0_ = 1.0;
1700 Vec<3> origin_{};
1701 double rho_ = 1.0, mu_ = 1.0, dt_ = 1e6;
1702 Vec<3> f_{};
1703 bool presPCG_ = true;
1704 bool presDbgSpdDone_ = false; // one-shot debug SPD probe (PECLET_CORE_AMR_PRES_DEBUG)
1705 bool momMGon_ = true; // velocity-MG momentum preconditioner (scalable; see setMomentumMG)
1706 bool useStaircaseMG_ = false; // false = Galerkin (MomentumMG), true = staircase (VelocityMG)
1707 int mgVcPre_ = 2, mgVcBottom_ = 30; // momentum-MG V-cycle pre/post sweeps + bottom sweeps
1708 Index mgMinCoarse_ = 256; // staircase velocity-MG pore-scale cap (coarsest cell count)
1709 bool momGS_ = false; // opt-in: multicolour Gauss–Seidel smoother in the momentum MG
1710 bool momMGSolver_ =
1711 false; // opt-in (P4): velocity-MG as the solver (defect correction), not BiCGStab
1712 bool ghostGrad_ = true; // gauge-exact directional cell gradient (setGhostGradient) — the
1713 // DEFAULT since 2026-08-18, mirroring flow's collocated
1714 // set_collocated_scheme("gauge-exact")
1715 bool ghostProj_ = false; // RESOLVED projection mode (set by setSolid from the request)
1716 int8_t ghostProjReq_ = -1; // -1 = AUTO (DEFAULT since 2026-08-25: ghost, aperture fallback
1717 // on thin band), 0 = explicit aperture, 1 = explicit ghost
1718 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure orders (2,2 = the production pair; the
1719 // (1,2) mixed form is march-unstable at scale)
1720 CfScheme cfScheme_ = CfScheme::standard; // 2:1 C/F interface scheme (setCfScheme)
1721 int outerIters_ = 1; // Picard outer iterations over the lagged advection (default 1)
1722 double outerTol_ = 1e-6; // outer-loop early-stop tolerance on max|Δu|
1723 double momTol_ = 1e-8; // per-step momentum BiCGStab relative tolerance (Phase-0 knob)
1724 bool advect_ = false; // momentum advection ∇·(u u) (off ⇒ Stokes)
1725 bool implicitFou_ = true; // implicit-FOU deferred-correction (stable) vs fully-explicit
1726 int advScheme_ = 0; // high-order flux: 0 = SOU (default), 1 = Koren TVD
1727 Index n_ = 0;
1728 int lastMomIters_ = 0, lastPresIters_ = 0, lastOuterIters_ = 1;
1729
1730 AmrCutCell<Bits> mom_;
1731 AmrPoisson<3, Bits> pres_;
1732 Multigrid<3, Bits> presMG_;
1733 MomentumMG<Bits> momMG_; // Galerkin velocity multigrid (momentum preconditioner)
1734 VelocityMG<Bits> velMG_; // rediscretized staircase velocity multigrid (alternative)
1735 MomentumOp momOp_;
1736 MomentumSolver<Bits> momSolver_;
1737 PCG<3, Bits> pcg_;
1738 std::array<View<double>, 3> defc_; // explicit ρ(SOU−FOU) deferred correction per component
1739 View<double> advDiag_, advCoef_; // device-resident implicit-FOU operator (rebuilt each step)
1740 FaceGeom geom_;
1741 GhostGradOverlay gc_; // directional ghost-gradient overlay (empty unless setGhostGradient)
1742 GhostOverlayDev gpOv_; // closure overlay (empty unless setGhostProjection)
1743 std::vector<char> gpPocket_; // fragmentation guard: 1 = decoupled pocket cell (ghost mode)
1744 CfCsrDev cfMom_; // +μ(∇²_scheme − ∇²_std) momentum RHS overlay
1745 CfCompCsrDev cfDiv_; // (D_scheme − D_std) divergence overlay
1746 std::array<CfCsrDev, 3> cfGrad_; // (G_scheme − G_std) per gradient axis
1747 CfCompCsrDev cfUfVel_; // (uf_scheme − uf_std) face-field overlay: velocity part
1748 CfCsrDev cfUfPhi_; // φ part
1749 View<double> maskC_; // 1 = coupled row (Krylov subspace), 0 = pinned
1750 View<double> gpr_, gprh_, gpp_, gpph_, gpv_, gps_, gpsh_, gpt_; // ghost BiCGStab scratch
1751 View<double> rscale_;
1752 View<char> fluid_;
1753 std::array<View<double>, 3> u_, gx_;
1754 std::array<View<double>, 3> u0_,
1755 uprev_; // frozen uⁿ (BE mass term) + previous Picard outer iterate
1756 View<double> p_, phi_, div_, bmom_;
1757 View<double> uf_; // ABC/Basilisk divergence-free face field (one per CSR (sub)face)
1758 bool faceFieldBuilt_ =
1759 false; // uf_ populated by a projection (else advection falls back to ½(u_i+u_j))
1760 std::unique_ptr<Octree> adaptOldT_; // beginAdapt topology snapshot
1761 std::array<std::vector<double>, 3> adaptU_; // beginAdapt field snapshots
1762 std::vector<double> adaptP_;
1763 std::array<std::vector<std::array<double, 3>>, 3> adaptGradU_; // distributed transfer grads
1764 std::vector<std::array<double, 3>> adaptGradP_;
1765
1766 // ---- distributed context (initMpi; all null/empty single-rank) -----------------------------
1767 DistributedOctree<3, Bits>* dist_ = nullptr; // the ORB block + communicator
1768 LeafHalo<3, Bits> dhalo_; // the flow's ±2 ghost registry (frozen in setSolid)
1769 LeafHaloExchange dhex_; // device value refresh over dhalo_
1770 DistributedFlowMultigrid<3, Bits> presMGD_; // distributed pressure MG (level 0 on dhalo_)
1771 std::array<long, 3> shiftD_{}; // block global fine origin
1772 Index nExt_ = 0; // n_ + ghosts (== n_ single-rank)
1773 std::function<double(double)> allred_; // Allreduce hook (empty single-rank)
1774};
1775
1776} // namespace peclet::core::amr
1777
1778#endif // PECLET_CORE_HAVE_MORTON
1779#endif // PECLET_CORE_AMR_FLOW_HPP
double allSum(double s) const
Global sum (identity single-rank).
Definition flow.hpp:1405
void setMomentumGS(bool on)
Opt-in: use the multicolour Gauss–Seidel smoother in the momentum MG (Galerkin or staircase) instead ...
Definition flow.hpp:593
void setViscosity(double mu)
Definition flow.hpp:487
void setPressure(const std::vector< double > &h)
Write the accumulated rotational pressure from host (restart / finishAdapt).
Definition flow.hpp:1241
void runMgVcycle(MG &mg, View< const double > r, View< double > z)
Run one V-cycle of a momentum MG as a preconditioner: z = M⁻¹ r.
Definition flow.hpp:1277
void setCfScheme(int scheme)
Coarse/fine (2:1) interface scheme (cf_scheme.hpp): 0 = standard two-point flux (default,...
Definition flow.hpp:548
void syncScalar(View< double > v)
Refresh the ghost tail of one cell scalar (p, φ, Krylov scratch).
Definition flow.hpp:1400
std::vector< double > faceField() const
Copy the divergence-free face field to host (one value per CSR (sub)face, forEachFaceFull order).
Definition flow.hpp:1379
void ghostMatvec(View< const double > x, View< double > y)
Definition flow.hpp:1489
void setVelocityMGStaircase(bool on)
Choose the momentum-MG coarse-operator strategy: false (default) = Galerkin (MomentumMG,...
Definition flow.hpp:584
void syncVel()
Refresh the ghost tails of the three velocity components (one batched message round).
Definition flow.hpp:1395
void setAdvectionScheme(int s)
High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.
Definition flow.hpp:560
std::vector< double > pressure() const
Copy the pressure field back to host (single D2H), (num_leaves,) — the incremental-rotational p.
Definition flow.hpp:1329
void copyToHost(const View< double > &d, std::vector< double > &h) const
Copy a device View into a host vector (sized n_).
Definition flow.hpp:1311
void initMpi(DistributedOctree< 3, Bits > &d)
Distributed mode (docs/amr_distributed_flow.md, rung 4): run this solver on one ORB block of a Distri...
Definition flow.hpp:482
void finishProjection(Index n)
Shared projection tail: build the div-free face field from u* + φ, correct the cell velocities (ABC /...
Definition flow.hpp:1139
void setMomentumMG(bool on)
Use the Galerkin velocity multigrid (MomentumMG) as the momentum BiCGStab preconditioner.
Definition flow.hpp:578
std::vector< double > velocity(int c) const
Copy a velocity component back to host (single D2H, no host loop — S2a).
Definition flow.hpp:1326
BlockOctree< 3, Bits > Octree
Definition flow.hpp:465
void setVelocity(int c, const std::vector< double > &h)
Set a velocity component from host (testing / initial conditions).
Definition flow.hpp:1318
std::vector< double > velocities() const
All three velocity components interleaved as a flat (n,3) row-major host buffer (out[i*3+c]) with a s...
Definition flow.hpp:1346
void setBodyForce(double fx, double fy, double fz)
Definition flow.hpp:489
double divNormFace()
L2 norm of the divergence of the ABC face field uf_ (built each project()): the φ-solve residual,...
Definition flow.hpp:1373
void setMomentumTol(double tol)
Relative tolerance for the per-step momentum BiCGStab solve (default 1e-8).
Definition flow.hpp:570
void zeroSolid(View< double > v)
Zero a per-leaf field on non-fluid cells (the transferred fields' solid cleanup).
Definition flow.hpp:1249
bool isFluid(Index i) const
Per-leaf fluid mask (false inside the solid) — for host-side post-processing / bindings.
Definition flow.hpp:1384
Index numGhostCells() const
Distributed: number of ghost slots in the ±2 registry (0 single-rank).
Definition flow.hpp:1382
std::vector< double > debugSou(int comp)
DEBUG: the raw high-order advection ∇·(u u_comp) per cell from the current velocity (== host oracle::...
Definition flow.hpp:1260
Index numLeaves() const
Definition flow.hpp:1380
void setVelocityMGMinCoarse(Index m)
Pore-scale cap for the staircase velocity-MG: the coarsest level keeps ≥ this many cells,...
Definition flow.hpp:589
void finishAdapt(SdfFn &&sdfFn)
Rebuild on the mutated octree and transfer the snapshotted fields onto it.
Definition flow.hpp:1193
static double maxAbsDiff(View< const double > a, View< const double > b, Index n)
Max |a − b| over all cells (the Picard outer-loop convergence measure).
Definition flow.hpp:1296
void ghostPrec(View< const double > r, View< double > z)
Definition flow.hpp:1496
int lastMomIters() const
Total momentum BiCGStab iterations (summed over the 3 components) of the last step.
Definition flow.hpp:1386
double divNormL2()
L2 norm of the (openness-weighted) divergence of the current velocity — the ghost-closed divergence w...
Definition flow.hpp:1360
void setMomentumMGSolver(bool on)
Opt-in (P4): solve the momentum predictor with the velocity multigrid used as the solver — MG-precond...
Definition flow.hpp:607
void setDensity(double rho)
Definition flow.hpp:486
void project(int presIters=60)
Pressure projection of the current velocity in place.
Definition flow.hpp:1011
void setOuterIterations(int n, double tol=1e-6)
Optional Picard outer loop over the lagged advection (mirror of flow's outerIters_): each outer itera...
Definition flow.hpp:614
void setImplicitAdvection(bool on)
Implicit-FOU deferred correction (default ON).
Definition flow.hpp:558
const FvOp & gpOp0()
The pressure operator the ghost solver runs on (distributed MG level 0 or presMG_'s).
Definition flow.hpp:1413
void rebalanceMpi(SdfFn &&sdfFn)
Distributed load rebalance (docs/amr_distributed_flow.md, rung 6): re-decompose the octree by leaf co...
Definition flow.hpp:1223
void installGhostMeta()
Mirror the halo registry's ghost metadata (block-local lo + level) into mom_ and pres_.
Definition flow.hpp:1463
int solveGhostBiCGStab(View< double > x, View< const double > b, int maxIters, double tol=1e-10)
Definition flow.hpp:1516
void prepareDistributed(SdfFn &&sdfFn)
Install the resolver seams, run every prober to the miss-collect fixpoint, freeze the ±2 halo,...
Definition flow.hpp:1419
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
Definition flow.hpp:467
int lastPresIters() const
Pressure PCG iterations of the last step.
Definition flow.hpp:1388
void setSolid(SdfFn &&sdfFn)
Build the cut-cell operators (host) + upload all device structures.
Definition flow.hpp:622
void setGhostGradient(bool on)
Directional ghost cell-gradient for the −∇pⁿ predictor and the projection's cell correction (the AMR ...
Definition flow.hpp:506
void step(int momIters=100, int presIters=60)
One incompressible step on device (Stokes, or Navier–Stokes with setAdvection).
Definition flow.hpp:917
void setPressurePCG(bool on)
Use MG-preconditioned CG for the pressure solve (default) vs plain V-cycles.
Definition flow.hpp:491
void setDt(double dt)
Definition flow.hpp:488
void gpProject(View< double > v)
Definition flow.hpp:1483
int lastOuterIters() const
Picard outer iterations actually run in the last step (1 unless setOuterIterations(>1)).
Definition flow.hpp:1390
void beginAdapt()
Snapshot the octree topology + (u, p) ahead of an external mesh mutation.
Definition flow.hpp:1175
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
FULL directional ghost-cell projection (the AMR port of flow's collocated set_ghost_projection): the ...
Definition flow.hpp:534
void setAdvection(bool on)
Enable momentum advection ∇·(u u) (default OFF ⇒ Stokes).
Definition flow.hpp:555
std::vector< double > localVector(const View< double > &v) const
Host copy of the LOCAL rows of a (possibly ghost-extended) per-cell field.
Definition flow.hpp:1332
Cell-centered FV Poisson operator on one (periodic) block octree.
Definition poisson.hpp:44
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:82
const Octree & octree() const
Definition poisson.hpp:567
Real cellWidth(Index i) const
Definition poisson.hpp:245
void setFrameShift(const std::array< long, Dim > &s)
Distributed frame shift: this block's global fine origin.
Definition poisson.hpp:92
void init(const Octree &t, Real h0)
Definition poisson.hpp:54
void setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:67
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:358
unsigned levelOf(Index slot) const
Octree level of an extended slot (local leaf or declared ghost).
Definition poisson.hpp:94
void setResolver(ExtResolver r)
Definition poisson.hpp:77
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:313
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:164
Real cellVolume(Index i) const
Definition poisson.hpp:246
Per-block adaptive octree over block-local Morton codes.
const AmrGeometry< Dim > & globalGeometry() const
void init(const LeafHalo< Dim, Bits > &h)
void exchange3(View< double > x0, View< double > x1, View< double > x2, int tag=46) const
Batched 3-component refresh (one message round for u0,u1,u2 — stride-3 packing).
void exchange(View< double > x, int tag=45) const
Refresh x[nLocal, nLocal+nGhost) (x is the extended device field, size >= extendedSize()).
int MPI_Allreduce(const void *sbuf, void *rbuf, int count, MPI_Datatype dt, MPI_Op, MPI_Comm)
Definition mpi_stub.hpp:80
#define MPI_INT
Definition mpi_stub.hpp:38
#define MPI_SUM
Definition mpi_stub.hpp:42
#define MPI_DOUBLE
Definition mpi_stub.hpp:40
void presUpdate(View< double > p, View< const double > phi, View< const double > div, View< const char > fluid, double rho_dt, double mu, Index n)
Rotational incremental pressure update: p += (ρ/dt)φ − μ·div, on fluid cells.
Definition flow.hpp:450
double divFaceNorm(const FaceGeom &g, View< const double > uf)
L2 norm of the divergence of the FACE field uf (the div-free flux diagnostic / host-parity check).
Definition flow.hpp:179
void axpy(View< double > y, double a, View< const double > x, Index n)
y += a·x
Definition pcg.hpp:107
GhostOverlay buildGhostOverlay(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const std::vector< double > &sdfC, int matrixOrder, int rhsOrder, bool *bandViolation=nullptr)
Build the overlay from the octree + the cell-centered SDF samples (AmrCutCell::sdfCRaw — EXTENDED ove...
CfUfDelta buildCfUfDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, FluidFn &&fluidOk, CfScheme scheme)
void bicgPUpdate(View< double > p, View< const double > r, View< const double > v, double beta, double omega, Index n)
BiCGStab direction update: p = r + β(p − ω v).
Definition momentum.hpp:118
void deferredSou(const FaceGeom &g, View< const double > u0, View< const double > u1, View< const double > u2, int comp, double rho, int advScheme, View< double > defc, View< const double > uf, bool useFace)
Deferred-correction advection term for component comp: defc = ρ·SOU − ρ·FOU (UNSCALED; the predictor ...
Definition flow.hpp:355
void advectExplicit(const FaceGeom &g, View< const double > u0, View< const double > u1, View< const double > u2, int comp, double rho, int advScheme, View< double > defc, View< const double > uf, bool useFace)
Fully-explicit high-order advection for component comp: defc = ρ·SOU (no implicit FOU; the setImplici...
Definition flow.hpp:399
void cfApply(const CfCsrDev &c, View< const double > f, View< double > out)
out(i) += Σ coef·f(slot).
void correct(View< double > uc, View< const double > gphi, View< const char > fluid, Index n)
u_c -= gradPhi_c on fluid cells (the projection velocity correction).
Definition flow.hpp:441
GhostOverlayDev uploadGhostOverlay(const GhostOverlay &h)
CfScheme
Coarse/fine interface scheme for the collocated flow operators.
Definition cf_scheme.hpp:55
@ standard
raw coarse value (two-point flux; 1st-order at 2:1 faces)
CfCompCsrDev uploadCfCompCsr(const CfCompCsr &h, const char *name)
void applyFv(const FvOp &op, View< const double > u, View< double > Lu)
Hu = (c0·I + cD·L) u (consistent conservative FV Laplacian, c0=0/cD=1 ⇒ pure L).
Definition fv_op.hpp:114
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
bool amrEnvFlag(const char *name)
Truthy environment flag (unset / "" / "0" ⇒ false).
Definition flow.hpp:59
void buildFaceField(const FaceGeom &g, View< const double > u0, View< const double > u1, View< const double > u2, View< const double > phi, View< double > uf)
Build the ABC/Basilisk divergence-free FACE field: uf(k) = ½(u^axis_i+u^axis_j) − (φ₊−φ₋)/dist for fa...
Definition flow.hpp:157
std::array< CfCsr, 3 > buildCfGradDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(G_scheme − G_std) for the ABC cell gradient gradOf/grad3.
void ghostApplyDelta(const GhostOverlayDev &ov, View< const double > x, View< double > y)
Device matrix overlay (== ghostApplyDeltaHost). Distinct rows per thread: no atomics.
void momRhs(View< const double > uc, View< const double > gradP, View< const double > adv, View< const double > rscale, View< const char > fluid, double idiag, double fc, View< double > b, Index n)
Momentum RHS for one component: b_i = fluid ? (idiag·u_i + f_c − gradP_i − adv_i)·rscale_i : 0 (== Am...
Definition flow.hpp:288
void buildFou(const FaceGeom &g, View< const double > u0, View< const double > u1, View< const double > u2, double rho, View< const double > rscale, View< double > advDiag, View< double > advCoef, View< const double > uf, bool useFace)
Build the implicit-FOU advection operator from the lagged velocity u0..2 (uⁿ) entirely on device,...
Definition flow.hpp:305
void applyGhostGrad(const GhostGradOverlay &ov, View< const double > f, View< double > gx, View< double > gy, View< double > gz)
Overwrite gx/gy/gz on the overlay cells with the directional stencil applied to f.
Definition flow.hpp:261
void grad3(const FaceGeom &g, View< const double > f, View< double > gx, View< double > gy, View< double > gz)
ABC cell-gradient of a scalar field f: gx/gy/gz = ½(g⁻+g⁺) of the adjacent face gradients along each ...
Definition flow.hpp:204
std::vector< char > findPocketCells(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const std::vector< double > &sdfC)
Fragmentation guard (the AMR port of flow's host-BFS pocket guard): the BINARY coupled-face graph (a ...
CfCsr buildCfLapDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, double factor, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(∇²_scheme − ∇²_std) as a scalar CSR, ×factor (pass μ for the momentum deferred-correction RHS,...
CfCompCsr buildCfDivDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(D_scheme − D_std) for the face-average divergence div_i = invV·Σ α·A·dir·(face value).
double dotPlain(View< const double > a, View< const double > b, Index n)
Plain (unweighted) dot product.
Definition momentum.hpp:109
FaceGeom buildFaceGeom(const AmrPoisson< Dim, Bits > &ap, FluidFn &&isFluid)
Build FaceGeom from a built AmrPoisson (openness set) + a fluid predicate.
Definition flow.hpp:69
void ghostDivergDelta(const GhostOverlayDev &ov, View< const double > u0, View< const double > u1, View< const double > u2, View< double > d)
Device divergence overlay (== ghostDivergDeltaHost).
void cfApplyComp(const CfCompCsrDev &c, View< const double > u0, View< const double > u1, View< const double > u2, View< double > out)
out(i) += Σ coef·u[comp](slot).
std::vector< double > transferField(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF, const BlockOctree< Dim, Bits > &newT, bool linear=true, const std::type_identity_t< std::vector< std::array< double, Dim > > > *gradIn=nullptr)
Conservative remap of a leaf field from oldT to newT (same domain).
Definition adapt.hpp:93
void divergence(const FaceGeom &g, View< const double > u0, View< const double > u1, View< const double > u2, View< double > div)
Openness-weighted FV divergence: div_i = invVol_i Σ_faces α·area·dir·½(u^axis_i+u^axis_j),...
Definition flow.hpp:124
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
std::vector< std::array< double, Dim > > transferGradients(const DistributedOctree< Dim, Bits > &d, const std::vector< double > &f)
transferField's minmod prolongation gradients on a DistributedOctree: bit-identical to the block-loca...
void removeMeanVolReduced(View< double > u, View< const double > invVol, View< const double > mask, Index n, const std::function< double(double)> &reduce)
Project u onto the FLUID range: zero solid cells, then subtract the volume-weighted mean over the flu...
Definition pcg.hpp:81
MORTON_HD double hoFaceValue(double upup, double up, double down, int scheme)
High-order advected face value from the two upwind cells (upup, up) and the downwind cell (down).
auto makeBinaryOpenFn(SdfFn sdfFn, double h0)
Binary openness callable factory for the MG surrogate: a face is open iff both adjacent centers (prob...
CfCsrDev uploadCfCsr(const CfCsr &h, const char *name)
std::array< Real, Dim > Vec
Multi-dimensional real vector.
Definition types.hpp:26
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::vector< std::remove_const_t< typename V::value_type > > toVector(const V &view)
Copy a (host- or device-resident) Kokkos View of any rank into a contiguous host std::vector,...
Definition view.hpp:63
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
Device mirror of a BlockOctree's leaf arrays + device-callable queries.
void upload(const Host &t)
(Re)upload the host octree's current leaf set to the device.
Device mirror of a CfCsr (empty when the scheme is standard / no C/F rows).
View< Index > upupI
upstream-of-i probe (periodicNeighbor(i,axis,−dir)) — SOU, size nFaces
Definition face_geom.hpp:29
View< double > dist
face-normal distance (physical) per face, size nFaces
Definition face_geom.hpp:27
View< Index > upupJ
upstream-of-j probe (periodicNeighbor(j,axis,+dir)) — SOU, size nFaces
Definition face_geom.hpp:30
View< double > rawArea
raw face area (physical, no openness) per face — advection flux
Definition face_geom.hpp:26
View< int > axis
face axis 0/1/2, size nFaces
Definition face_geom.hpp:23
View< char > fluid
per-cell fluid flag, size n
Definition face_geom.hpp:32
View< double > alphaArea
α·area (physical) per face, size nFaces
Definition face_geom.hpp:25
View< Index > start
CSR row offsets, size n+1.
Definition face_geom.hpp:21
View< double > invVol
1/V_i per cell, size n
Definition face_geom.hpp:31
View< double > alpha
openness per face (gradient gate), size nFaces
Definition face_geom.hpp:28
View< Index > nbr
neighbour leaf per face, size nFaces
Definition face_geom.hpp:22
View< int > dir
face direction +1/-1, size nFaces
Definition face_geom.hpp:24
Ghost-gradient overlay (setGhostGradient): per cut cell, a precomputed 3-point directional FD stencil...
Definition flow.hpp:253
View< Index > cell
[n] leaf index
Definition flow.hpp:255
Index n
number of overlay (cut) cells
Definition flow.hpp:254
View< Index > idx
[n*9] stencil cell, slot s*9 + axis*3 + k
Definition flow.hpp:256
View< double > w
[n*9] stencil weight (0 = unused)
Definition flow.hpp:257
Host ghost-projection overlay: one row per non-clean fluid leaf (== cut cell: some ±1 center sample s...
Assembled momentum operator on the device: (A u)_i = diag_i u_i + Σ coef·u[nbr], with an optional imp...
Definition momentum.hpp:43
View< double > diag
size n
Definition momentum.hpp:44
View< Index > advStart
face-geom CSR row offsets, size n+1
Definition momentum.hpp:52
View< Index > faceNbr
neighbour leaf per off-diagonal, size nnz
Definition momentum.hpp:46
View< Index > advNbr
face-geom neighbour per face, size nFaces
Definition momentum.hpp:53
View< double > advDiag
per-cell outflow (diagonal) advection weight, size n
Definition momentum.hpp:51
View< double > faceCoef
off-diagonal coefficient, size nnz
Definition momentum.hpp:47
View< Index > faceStart
CSR row offsets, size n+1.
Definition momentum.hpp:45
View< double > advCoef
per-face inflow advection coefficient (0 on outflow/solid faces)
Definition momentum.hpp:54