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
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#ifndef PECLET_AMR_FLOW_HPP
20#define PECLET_AMR_FLOW_HPP
21
22#include <array>
23#include <chrono>
24#include <cmath>
25#include <cstdio>
26#include <cstdlib>
27#include <memory>
28#include <stdexcept>
29#include <string>
30#include <vector>
31
32#include "peclet/amr/adapt.hpp" // transferField (conservative remap for finishAdapt)
33#include "peclet/amr/advect_recon.hpp" // shared high-order face reconstruction (host+device)
35#include "peclet/amr/cf_scheme.hpp" // pluggable 2:1 C/F schemes (setCfScheme)
36#include "peclet/amr/common.hpp"
38#include "peclet/amr/distributed_adapt.hpp" // transferGradients (distributed finishAdapt)
39#include "peclet/amr/distributed_flow_mg.hpp" // distributed pressure MG (initMpi mode)
41#include "peclet/amr/face_geom.hpp" // FaceGeom (shared with the device assembler)
42#include "peclet/amr/facegeom_assembly.hpp" // assembleFaceGeom (D4/D6)
43#include "peclet/amr/ghost_projection.hpp" // directional ghost overlay (setGhostProjection)
44#include "peclet/amr/ghost_projection_sampled.hpp" // mixed-level sampled overlay (setGhostSampled)
47#include "peclet/amr/momentum_assembly.hpp" // assembleMomentum (D3/D6)
49#include "peclet/amr/pcg.hpp"
51#include "peclet/amr/seam_recon.hpp" // seam-reconstruction tables (docs/amr_cf_convective.md)
53#include "peclet/core/common/host_parallel.hpp"
54#include "peclet/core/common/types.hpp"
55#include "peclet/core/common/view.hpp"
56
57namespace peclet::amr {
58
61inline bool amrEnvFlag(const char* name) {
62 const char* v = std::getenv(name);
63 return v && v[0] && !(v[0] == '0' && v[1] == '\0');
64}
65
66// FaceGeom (the collocated projection's static face-geometry CSR) now lives in face_geom.hpp so the
67// device assembler and this driver share the type without a circular include.
68
70template <int Dim, unsigned Bits, class FluidFn>
72 const Index n = ap.octree().numLeaves();
73 std::vector<Index> start(static_cast<std::size_t>(n) + 1, 0);
74 for (Index i = 0; i < n; ++i) {
75 Index cnt = 0;
76 ap.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++cnt; });
77 start[static_cast<std::size_t>(i) + 1] = start[static_cast<std::size_t>(i)] + cnt;
78 }
79 const Index nf = start[static_cast<std::size_t>(n)];
80 std::vector<Index> nbr(static_cast<std::size_t>(nf));
81 std::vector<int> axis(static_cast<std::size_t>(nf)), dir(static_cast<std::size_t>(nf));
82 std::vector<double> aArea(static_cast<std::size_t>(nf)), rArea(static_cast<std::size_t>(nf)),
83 dist(static_cast<std::size_t>(nf)), alpha(static_cast<std::size_t>(nf));
84 std::vector<Index> upupI(static_cast<std::size_t>(nf)), upupJ(static_cast<std::size_t>(nf));
85 std::vector<double> invVol(static_cast<std::size_t>(n));
86 std::vector<char> fluid(static_cast<std::size_t>(n));
87 for (Index i = 0; i < n; ++i) {
88 invVol[static_cast<std::size_t>(i)] = 1.0 / ap.cellVolume(i);
89 fluid[static_cast<std::size_t>(i)] = isFluid(i) ? 1 : 0;
90 Index k = start[static_cast<std::size_t>(i)];
91 ap.forEachFaceFull(i, [&](Index j, int ax, int dr, double area, double d, double al) {
92 nbr[static_cast<std::size_t>(k)] = j;
93 axis[static_cast<std::size_t>(k)] = ax;
94 dir[static_cast<std::size_t>(k)] = dr;
95 aArea[static_cast<std::size_t>(k)] = al * area;
96 rArea[static_cast<std::size_t>(k)] = area;
97 dist[static_cast<std::size_t>(k)] = d;
98 alpha[static_cast<std::size_t>(k)] = al;
99 // SOU upstream-of-upwind probes (point neighbour one cell further upstream): if i is the
100 // upwind cell the upstream is across i's −dir face; if j is upwind, across j's +dir face.
101 upupI[static_cast<std::size_t>(k)] = ap.periodicNeighbor(i, ax, -dr);
102 upupJ[static_cast<std::size_t>(k)] = ap.periodicNeighbor(j, ax, dr);
103 ++k;
104 });
105 }
106 FaceGeom g;
107 g.n = n;
108 g.start = toDevice(start, "fg_start");
109 g.nbr = toDevice(nbr, "fg_nbr");
110 g.axis = toDevice(axis, "fg_axis");
111 g.dir = toDevice(dir, "fg_dir");
112 g.alphaArea = toDevice(aArea, "fg_aarea");
113 g.rawArea = toDevice(rArea, "fg_rarea");
114 g.dist = toDevice(dist, "fg_dist");
115 g.alpha = toDevice(alpha, "fg_alpha");
116 g.upupI = toDevice(upupI, "fg_upupi");
117 g.upupJ = toDevice(upupJ, "fg_upupj");
118 g.invVol = toDevice(invVol, "fg_invvol");
119 g.fluid = toDevice(fluid, "fg_fluid");
120 return g;
121}
122
128 auto st = g.start;
129 auto nb = g.nbr;
130 auto ax = g.axis;
131 auto dr = g.dir;
132 auto aA = g.alphaArea;
133 auto iv = g.invVol;
134 auto fl = g.fluid;
135 Kokkos::parallel_for(
136 "amr::flow_div", g.n, KOKKOS_LAMBDA(const Index i) {
137 if (!fl(i)) {
138 div(i) = 0.0;
139 return;
140 }
141 double d = 0.0;
142 for (Index k = st(i); k < st(i + 1); ++k) {
143 const int a = ax(k);
144 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
145 const Index j = nb(k);
146 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
147 d += aA(k) * dr(k) * 0.5 * (ui + uj);
148 }
149 div(i) = d * iv(i);
150 });
151}
152
161 auto st = g.start;
162 auto nb = g.nbr;
163 auto ax = g.axis;
164 auto dr = g.dir;
165 auto di = g.dist;
166 Kokkos::parallel_for(
167 "amr::flow_buildface", g.n, KOKKOS_LAMBDA(const Index i) {
168 for (Index k = st(i); k < st(i + 1); ++k) {
169 const int a = ax(k);
170 const Index j = nb(k);
171 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
172 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
173 const double gphi = (dr(k) > 0) ? (phi(j) - phi(i)) / di(k) : (phi(i) - phi(j)) / di(k);
174 uf(k) = 0.5 * (ui + uj) - gphi;
175 }
176 });
177}
178
181inline double divFaceNorm(const FaceGeom& g, View<const double> uf) {
182 auto st = g.start;
183 auto dr = g.dir;
184 auto aA = g.alphaArea;
185 auto iv = g.invVol;
186 auto fl = g.fluid;
187 double s = 0.0;
188 Kokkos::parallel_reduce(
189 "amr::flow_divface", g.n,
190 KOKKOS_LAMBDA(const Index i, double& acc) {
191 if (!fl(i))
192 return;
193 double d = 0.0;
194 for (Index k = st(i); k < st(i + 1); ++k)
195 d += aA(k) * dr(k) * uf(k);
196 d *= iv(i);
197 acc += d * d;
198 },
199 s);
200 return std::sqrt(s);
201}
202
208 auto st = g.start;
209 auto nb = g.nbr;
210 auto ax = g.axis;
211 auto dr = g.dir;
212 auto di = g.dist;
213 auto al = g.alpha;
214 auto fl = g.fluid;
215 Kokkos::parallel_for(
216 "amr::flow_grad3", g.n, KOKKOS_LAMBDA(const Index i) {
217 if (!fl(i)) {
218 gx(i) = gy(i) = gz(i) = 0.0;
219 return;
220 }
221 const double fi = f(i);
222 double gp[3] = {0, 0, 0}, gm[3] = {0, 0, 0};
223 int np[3] = {0, 0, 0}, nm[3] = {0, 0, 0};
224 for (Index k = st(i); k < st(i + 1); ++k) {
225 if (al(k) <= 1e-12)
226 continue;
227 const int a = ax(k);
228 const double gg = (dr(k) > 0) ? (f(nb(k)) - fi) / di(k) : (fi - f(nb(k))) / di(k);
229 if (dr(k) > 0) {
230 gp[a] += gg;
231 ++np[a];
232 } else {
233 gm[a] += gg;
234 ++nm[a];
235 }
236 }
237 double out[3];
238 for (int a = 0; a < 3; ++a) {
239 const double a1 = np[a] ? gp[a] / np[a] : 0.0;
240 const double a2 = nm[a] ? gm[a] / nm[a] : 0.0;
241 out[a] = 0.5 * (a1 + a2);
242 }
243 gx(i) = out[0];
244 gy(i) = out[1];
245 gz(i) = out[2];
246 });
247}
248
261
265 if (ov.n == 0)
266 return;
267 auto cell = ov.cell;
268 auto idx = ov.idx;
269 auto w = ov.w;
270 Kokkos::parallel_for(
271 "amr::flow_ghostgrad", ov.n, KOKKOS_LAMBDA(const Index s) {
272 const Index i = cell(s);
273 double g[3];
274 for (int a = 0; a < 3; ++a) {
275 double acc = 0.0;
276 for (int k = 0; k < 3; ++k)
277 acc += w(s * 9 + a * 3 + k) * f(idx(s * 9 + a * 3 + k));
278 g[a] = acc;
279 }
280 gx(i) = g[0];
281 gy(i) = g[1];
282 gz(i) = g[2];
283 });
284}
285
291 View<const double> rscale, View<const char> fluid, double idiag, double fc,
292 View<double> b, Index n) {
293 Kokkos::parallel_for(
294 "amr::flow_momrhs", n, KOKKOS_LAMBDA(const Index i) {
295 b(i) = fluid(i) ? (idiag * uc(i) + fc - gradP(i) - adv(i)) * rscale(i) : 0.0;
296 });
297}
298
310 bool useFace) {
311 auto st = g.start;
312 auto nb = g.nbr;
313 auto ax = g.axis;
314 auto dr = g.dir;
315 auto ra = g.rawArea;
316 auto iv = g.invVol;
317 auto fl = g.fluid;
318 Kokkos::parallel_for(
319 "amr::flow_buildfou", g.n, KOKKOS_LAMBDA(const Index i) {
320 if (!fl(i)) {
321 advDiag(i) = 0.0;
322 for (Index k = st(i); k < st(i + 1); ++k)
323 advCoef(k) = 0.0;
324 return;
325 }
326 const double rs = rscale(i);
327 double dsum = 0.0;
328 for (Index k = st(i); k < st(i + 1); ++k) {
329 const Index j = nb(k);
330 if (!fl(j)) {
331 advCoef(k) = 0.0;
332 continue;
333 }
334 const int a = ax(k);
335 const double ui = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
336 const double uj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
337 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (ui + uj);
338 const double w = rs * rho * ra(k) * velOut * iv(i);
339 if (velOut < 0.0) {
340 advCoef(k) = w; // inflow → off-diagonal toward upstream neighbour j
341 } else {
342 advCoef(k) = 0.0;
343 dsum += w; // outflow → diagonal
344 }
345 }
346 advDiag(i) = dsum;
347 });
348}
349
364template <class FldFn>
366 double phiUpUp, double phiUp, double phiDown,
367 double distK, int advScheme, const FldFn& fld) {
368 const Index s = sv.on ? sv.seam(k) : Index(-1);
369 if (s < 0)
370 return hoFaceValue(phiUpUp, phiUp, phiDown, advScheme); // today's reconstruction, bit for bit
371 const Index rs = upIsI ? sv.sampI(s) : sv.sampJ(s); // φ_U* (case 1)
372 const Index ru = upIsI ? sv.uuRecI(s) : sv.uuRecJ(s); // φ_UU* (cases 2/3)
373 const Index rd = upIsI ? sv.sampJ(s) : sv.sampI(s); // φ_D*, Koren only
374 const double d1 = upIsI ? sv.d1I(s) : sv.d1J(s);
375 const double upStar = (rs >= 0) ? sv.gather(rs, fld) : phiUp;
376 // No upstream record: the probe is at the upwind cell's own level (or was withheld by a gate),
377 // which is the equal-spacing `dUU = 2·d1` today's formula already assumes.
378 double uu = phiUpUp, dUU = 2.0 * d1;
379 if (ru >= 0) {
380 uu = sv.gather(ru, fld);
381 dUU = sv.recDist(ru);
382 }
383 const double downStar = (advScheme != 0 && rd >= 0) ? sv.gather(rd, fld) : phiDown;
385}
386
395 View<const double> u2, int comp, double rho, int advScheme,
397 auto st = g.start;
398 auto nb = g.nbr;
399 auto ax = g.axis;
400 auto dr = g.dir;
401 auto ra = g.rawArea;
402 auto ds = g.dist;
403 auto iv = g.invVol;
404 auto fl = g.fluid;
405 auto uiP = g.upupI;
406 auto ujP = g.upupJ;
407 const SeamView sv = makeSeamView(g, seamOn);
408 Kokkos::parallel_for(
409 "amr::flow_defsou", g.n, KOKKOS_LAMBDA(const Index i) {
410 if (!fl(i)) {
411 defc(i) = 0.0;
412 return;
413 }
414 auto fld = [&](Index c) { return (comp == 0) ? u0(c) : (comp == 1) ? u1(c) : u2(c); };
415 double sou = 0.0, fou = 0.0;
416 for (Index k = st(i); k < st(i + 1); ++k) {
417 const Index j = nb(k);
418 if (!fl(j))
419 continue;
420 const int a = ax(k);
421 const double uai = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
422 const double uaj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
423 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (uai + uaj);
424 const bool upIsI = velOut > 0.0;
425 const Index up = upIsI ? i : j;
426 const Index down = upIsI ? j : i;
427 const Index upup = upIsI ? uiP(k) : ujP(k);
428 const double phiUp = fld(up);
429 const double phiUpUp = (upup >= 0 && fl(upup)) ? fld(upup) : phiUp;
430 const double phiDown = fld(down);
431 const double phiFace = advectedFaceValue(sv, k, upIsI, phiUpUp, phiUp, phiDown, ds(k),
432 advScheme, fld); // shared recon
433 sou += ra(k) * velOut * phiFace;
434 fou += ra(k) * velOut * fld(up); // FOU flux = velOut · upwind value
435 }
436 defc(i) = rho * iv(i) * (sou - fou); // ρ·(SOU − FOU), unscaled
437 });
438}
439
443 View<const double> u2, int comp, double rho, int advScheme,
445 auto st = g.start;
446 auto nb = g.nbr;
447 auto ax = g.axis;
448 auto dr = g.dir;
449 auto ra = g.rawArea;
450 auto ds = g.dist;
451 auto iv = g.invVol;
452 auto fl = g.fluid;
453 auto uiP = g.upupI;
454 auto ujP = g.upupJ;
455 const SeamView sv = makeSeamView(g, seamOn);
456 Kokkos::parallel_for(
457 "amr::flow_advexpl", g.n, KOKKOS_LAMBDA(const Index i) {
458 if (!fl(i)) {
459 defc(i) = 0.0;
460 return;
461 }
462 auto fld = [&](Index c) { return (comp == 0) ? u0(c) : (comp == 1) ? u1(c) : u2(c); };
463 double sou = 0.0;
464 for (Index k = st(i); k < st(i + 1); ++k) {
465 const Index j = nb(k);
466 if (!fl(j))
467 continue;
468 const int a = ax(k);
469 const double uai = (a == 0) ? u0(i) : (a == 1) ? u1(i) : u2(i);
470 const double uaj = (a == 0) ? u0(j) : (a == 1) ? u1(j) : u2(j);
471 const double velOut = useFace ? dr(k) * uf(k) : dr(k) * 0.5 * (uai + uaj);
472 const bool upIsI = velOut > 0.0;
473 const Index up = upIsI ? i : j;
474 const Index down = upIsI ? j : i;
475 const Index upup = upIsI ? uiP(k) : ujP(k);
476 const double phiUp = fld(up);
477 const double phiUpUp = (upup >= 0 && fl(upup)) ? fld(upup) : phiUp;
478 const double phiDown = fld(down);
479 const double phiFace = advectedFaceValue(sv, k, upIsI, phiUpUp, phiUp, phiDown, ds(k),
480 advScheme, fld); // shared recon
481 sou += ra(k) * velOut * phiFace;
482 }
483 defc(i) = rho * sou * iv(i); // ρ·SOU (fully explicit)
484 });
485}
486
489 Kokkos::parallel_for(
490 "amr::flow_correct", n, KOKKOS_LAMBDA(const Index i) {
491 if (fluid(i))
492 uc(i) -= gphi(i);
493 });
494}
495
498 View<const char> fluid, double rho_dt, double mu, Index n) {
499 Kokkos::parallel_for(
500 "amr::flow_presupd", n, KOKKOS_LAMBDA(const Index i) {
501 if (fluid(i))
502 p(i) += rho_dt * phi(i) - mu * div(i);
503 });
504}
505
506// ===========================================================================
507// AmrFlow — collocated Stokes projection step, fully on device.
508// ===========================================================================
509template <unsigned Bits = 21u>
510class AmrFlow {
511 public:
513
515 void init(const Octree& t, Real h0, Vec<3> origin = Vec<3>{}) {
516 init(t, detail::filledVec<3>(h0), origin);
517 }
518 void init(const Octree& t, const Vec<3>& h0, Vec<3> origin = Vec<3>{}) {
519 t_ = &t;
520 h0_ = h0;
521 origin_ = origin;
522 dist_ = nullptr; // single-rank unless initMpi is used
523 }
524
534 init(d.local(), d.h0(), d.globalGeometry().origin);
535 dist_ = &d;
536 }
541 Vec<3> b{};
542 for (int d = 0; d < 3; ++d)
543 b[d] = mu_ / (h0_[d] * h0_[d]);
544 return b;
545 }
546 void setDensity(double rho) { rho_ = rho; }
547 void setViscosity(double mu) { mu_ = mu; }
548 void setDt(double dt) { dt_ = dt; }
549 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
551 void setPressurePCG(bool on) { presPCG_ = on; }
566 void setGhostGradient(bool on) { ghostGrad_ = on; }
596 void setApertureOrder(int order) { apertureOrder_ = order; }
597 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2) {
598 ghostProjReq_ = on ? 1 : 0; // explicit selection disables the AUTO default
599 gpMatrixOrder_ = matrixOrder;
600 gpRhsOrder_ = rhsOrder;
601 }
613 void setGhostSampled(bool on, double rho = 2.2, long maxSamples = 0) {
614 ghostSampledReq_ = on ? 1 : 0;
615 gpsRho_ = rho;
616 gpsMaxN_ = maxSamples;
617 }
631 void setCfScheme(int scheme) { cfScheme_ = static_cast<CfScheme>(scheme); }
638 void setAdvection(bool on) { advect_ = on; }
641 void setImplicitAdvection(bool on) { implicitFou_ = on; }
643 void setAdvectionScheme(int s) { advScheme_ = s; }
653 void setMomentumTol(double tol) { momTol_ = tol; }
660 void setPressureTol(double tol) { presTol_ = tol; }
669 void setUfAdvection(bool on) { ufAdvect_ = on; }
678 void setSeamReconstruction(bool on) { seamRecon_ = on; }
686 void setMomentumMG(bool on) { momMGon_ = on; }
687
692 void setVelocityMGStaircase(bool on) { useStaircaseMG_ = on; }
697 void setVelocityMGMinCoarse(Index m) { mgMinCoarse_ = m; }
701 void setMomentumGS(bool on) { momGS_ = on; }
702
715 void setMomentumMGSolver(bool on) { momMGSolver_ = on; }
716
722 void setOuterIterations(int n, double tol = 1e-6) {
723 outerIters_ = (n < 1) ? 1 : n;
724 outerTol_ = tol;
725 }
726
729 template <class SdfFn>
731 const Index n = t_->numLeaves();
732 // Phase profiler (PECLET_AMR_PROFILE_SETUP=1): setSolid is the whole setup cost at bed
733 // scale (measured 127 us/leaf single-threaded, [[performance-sota-yardstick]]) — the
734 // per-phase breakdown is what any optimization must start from.
735 const bool prof_ = [] {
736 const char* e = std::getenv("PECLET_AMR_PROFILE_SETUP");
737 return e && e[0] == '1';
738 }();
739 auto profT0_ = std::chrono::steady_clock::now();
740 auto profPhase = [&](const char* name) {
741 if (!prof_)
742 return;
743 Kokkos::fence();
744 auto now = std::chrono::steady_clock::now();
745 double ms = std::chrono::duration<double, std::milli>(now - profT0_).count();
746 std::fprintf(stderr, "[setSolid] %-28s %9.1f ms (%6.1f us/leaf)\n", name, ms,
747 1e3 * ms / static_cast<double>(n));
748 profT0_ = std::chrono::steady_clock::now();
749 };
750 // Host operator build (geometry, openness, cut stencils) — same as oracle::AmrFlow::setSolid.
751 mom_.init(*t_, h0_, origin_);
752 pres_.init(*t_, h0_);
753 pres_.setOrigin(origin_);
754 profPhase("mom.init + pres.init");
755 // Resolve the projection mode. DEFAULT SWITCH (2026-08-25, user decision): AUTO = the
756 // GHOST (fluid-only) projection — family-free/stable/unique (see the setter doc) — falling
757 // back to the aperture projection with a stderr notice when the finest band is too thin for
758 // the overlay (probed below; the July AUTO arm restored, now unconditional). Explicit
759 // setGhostProjection(true/false) pins the scheme (band violations then THROW as before).
760 // The overlay is built below (it needs only mom_'s sdf samples + pres_'s topology walk,
761 // no openness).
762 const bool wantGhost = (ghostProjReq_ != 0); // explicit on (1) or AUTO (-1)
763 ghostProj_ = wantGhost; // provisional; AUTO may fall back below
764 ghostSampled_ = (ghostSampledReq_ == 1) && wantGhost;
765 // D1/D2 (docs/amr_march_perf_and_distributed_plan.md): the mixed-level cut band is
766 // DISTRIBUTED. The sampled builders probe through the LeafHalo seam inside
767 // prepareDistributed's fixpoint (D1) and their least-squares clouds read ghost slots like any
768 // other CSR the step consumes (D2), so there is no single-rank guard left. Acceptance:
769 // np=1 bitwise, np=2,4 within the established decomposition-independence class
770 // (tests/test_amr_distributed_seam_mpi.cpp).
771 if (dist_) {
772 // Distributed: install the resolver seams and run every prober to the miss-collect
773 // fixpoint (docs/amr_distributed_flow.md). Freezes the ±2 halo, leaves mom_ FULLY built
774 // (its final round ran with every ghost resolved) and the solver hooks installed.
776 } else {
777 nExt_ = n;
778 allred_ = {};
779 momSolver_.setDistributed({}, {}, 0);
780 pcg_.setDistributed({}, {}, 0);
781 mom_.build(sdfFn, /*idiag=*/rho_ / dt_, /*beta=*/betaPerAxis());
782 }
783 profPhase("mom.build (SDF+cut stencils)");
786 if (ghostSampled_) { // mixed-level cut band: sample-slot overlay, no band-margin probe
787 const auto gfine = globalFineExtent();
788 hovS = buildGhostOverlaySampled(*t_, pres_, sdfFn, gpMatrixOrder_, gpRhsOrder_, origin_,
789 &gfine, shiftD_, /*discovery=*/false, gpsRho_, gpsMaxN_);
790 profPhase("buildGhostOverlaySampled");
791 } else if (ghostProj_) { // probe the band margin; explicit request throws on violation, AUTO
792 // falls
793 bool viol = false;
794 hov = buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
795 if (dist_) {
796 // COLLECTIVE band-margin decision: the flag is agreed across ranks before anyone
797 // commits (one rank throwing/falling back alone would deadlock the MG collectives).
798 int lv = viol ? 1 : 0, gv = 0;
799 MPI_Allreduce(&lv, &gv, 1, MPI_INT, MPI_LOR, dist_->comm());
800 viol = gv != 0;
801 }
802 if (viol) {
803 if (ghostProjReq_ == 1)
804 throw std::runtime_error(
805 "amr ghost projection: an overlay row's ±2 closure reach crosses a 2:1 level "
806 "boundary — widen the refineToSdf band margin");
807 ghostProj_ = false; // AUTO: fall back to the aperture projection
809 "peclet::core AmrFlow: AUTO scheme fell back to the aperture projection (the "
810 "finest band is too thin for the ghost overlay). Select explicitly with "
811 "setGhostProjection to silence this notice.\n");
812 }
813 }
814 if (ghostProj_) {
815 // Ghost projection: the pressure geometry is the BINARY openness on the unchanged MG
816 // rails; the closure physics lives in the overlay (built above). Sampled mode: the
817 // level-aware canonical rule (the sampled overlay's face states are FORCED to it — the
818 // overlay-closed <=> binary-closed invariant).
819 if (ghostSampled_) {
821 *t_, pres_, [&sdfFn](const Vec<3>& p) { return sdfFn(p); }, h0_, origin_, shiftD_);
822 pres_.buildOpenness(binFn);
823 profPhase("buildOpenness (mixed)");
824 // D1: the pressure hierarchy has to be the DISTRIBUTED one when a seam is installed —
825 // gpOp0() reads presMGD_ whenever dist_ is set, so building presMG_ here left the ghost
826 // solver pointing at an empty operator.
827 if (dist_)
828 presMGD_.build(*dist_, h0_, binFn, &dhalo_, /*liftRoot=*/true, presBottomExtent_);
829 else
830 presMG_.build(*t_, h0_, binFn, /*periodic=*/true, /*immersedWall=*/false,
831 /*liftRoot=*/true, presBottomExtent_);
832 profPhase("presMG.build");
833 } else {
834 auto binFn = makeBinaryOpenFn([&sdfFn](const Vec<3>& p) { return sdfFn(p); }, h0_);
835 pres_.buildOpenness(binFn);
836 if (dist_)
837 presMGD_.build(*dist_, h0_, binFn, &dhalo_, /*liftRoot=*/true, presBottomExtent_);
838 else
839 presMG_.build(*t_, h0_, binFn, /*periodic=*/true, /*immersedWall=*/false,
840 /*liftRoot=*/true, presBottomExtent_);
841 }
842 ghostGrad_ = true; // the directional gradient is part of the scheme
843 // Fragmentation guard: pockets outside the main binary component are decoupled (see
844 // findPocketCells) — folded into maskC_ below and hidden from the directional gradients.
845 // Single-rank host BFS only: the DISTRIBUTED label-propagation guard is a rung-6 item
846 // (a rank-local BFS would mislabel components that span ranks), so multi-rank runs are
847 // unprotected on fragmenting geometries until it lands.
848 gpPocket_ = dist_ ? std::vector<char>{} : findPocketCells(*t_, pres_, mom_.sdfCRaw());
849 profPhase("findPocketCells");
850 } else {
851 gpPocket_.clear();
852 auto openFn = [&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); };
853 pres_.buildOpenness(openFn);
854 if (dist_)
855 presMGD_.build(*dist_, h0_, openFn, &dhalo_, /*liftRoot=*/true, presBottomExtent_);
856 else
857 presMG_.build(*t_, h0_, openFn, /*periodic=*/true, /*immersedWall=*/false,
858 /*liftRoot=*/true, presBottomExtent_);
859 }
860 // What solves the coarsest level (docs/amr_mg_depth.md §6.6): `auto` engages the agglomerated
861 // GraphAMG-PCG bottom only where the ladder ran out above `presBottomExtent_`.
862 if (dist_)
863 presMGD_.setBottom(presBottom_);
864 else
865 presMG_.setBottom(presBottom_);
866 // Singular periodic pressure: per-level nullspace projection.
867 if (dist_) {
868 presMGD_.setRemoveMean(true);
869 pcg_.setDistributed([this](View<double> v) { presMGD_.sync(0, v); }, allred_,
870 presMGD_.extendedSize(0));
871 } else {
872 presMG_.setRemoveMean(true);
873 }
874 profPhase("nullspace setup");
875
876 // Device assembly (D6): the static cut-cell momentum operator CSR and the collocated face
877 // geometry are assembled ON THE DEVICE (assembleMomentum / assembleFaceGeom), and the
878 // pressure MG operators are device-assembled per level (Multigrid D5) — so no host CSR walk and
879 // no operator round-trip. Each is bit-for-bit identical to the host assembler on OpenMP (locked
880 // in test_amr_momentum / test_amr_facegeom), so the flow result is unchanged. A shared device
881 // octree view backs both assemblers.
882 // DISTRIBUTED: assemble on the HOST through the resolver seam instead (the device walkers
883 // cannot resolve cross-block probes) and upload — same parity-locked builders, ghost
884 // columns included.
886 if (!dist_)
887 ov.upload(*t_);
888 if (dist_) {
889 auto A = mom_.assembleOperator();
890 momOp_ = MomentumOp{};
891 momOp_.n = n;
892 momOp_.diag = toDevice(A.diag, "mo_diag");
893 momOp_.faceStart = toDevice(A.start, "mo_start");
894 momOp_.faceNbr = toDevice(A.nbr, "mo_nbr");
895 momOp_.faceCoef = toDevice(A.coef, "mo_coef");
896 } else {
897 momOp_ = assembleMomentum<Bits>(mom_, ov); // static Stokes operator (hasAdv stays false)
898 profPhase("assembleMomentum (device)");
899 }
900 // Velocity multigrid (momentum preconditioner): the Galerkin hierarchy A_c = R·A·P built
901 // directly from the exact assembled momentum CSR. Consistent with the fine cut-cell
902 // operator by construction (inherits the ξ-overlay + D_rescale row scaling; a coarse cell
903 // of all-solid children stays an identity row). It only changes the preconditioner (the
904 // BiCGStab matvec is the exact operator) ⇒ same converged solution, but the iteration
905 // count stays ~flat with N instead of growing like the Jacobi-preconditioned BiCGStab.
906 // Build the chosen momentum-MG: Galerkin (MomentumMG) by default, or the rediscretized
907 // staircase (VelocityMG) — both from the static Stokes operator, once.
908 // DISTRIBUTED: the Galerkin MG becomes RANK-LOCAL (additive-Schwarz preconditioner): the
909 // local rows of the exact operator with the ghost COLUMNS dropped. At np=1 nothing is
910 // dropped, so the preconditioner — and hence the whole step — stays bit-identical to the
911 // single-rank path; at np>1 it is a block preconditioner (iterations may grow with np,
912 // the converged step is unchanged — the preconditioner never moves the solution). The
913 // EXACT cross-rank Galerkin RAP is the documented follow-up
914 // (docs/amr_distributed_flow.md §4c); the staircase variant is single-rank-only for now
915 // (distributed falls back to Jacobi-with-halo preconditioning).
916 if (momMGon_ && dist_ && !useStaircaseMG_) {
917 auto A = mom_.assembleOperator();
918 std::vector<Index> lstart(1, 0);
919 std::vector<Index> lnbr;
920 std::vector<double> lcoef;
921 for (Index i = 0; i < n; ++i) {
922 for (Index k = A.start[static_cast<std::size_t>(i)];
923 k < A.start[static_cast<std::size_t>(i) + 1]; ++k)
924 if (A.nbr[static_cast<std::size_t>(k)] < n) { // drop ghost columns (Schwarz cut)
925 lnbr.push_back(A.nbr[static_cast<std::size_t>(k)]);
926 lcoef.push_back(A.coef[static_cast<std::size_t>(k)]);
927 }
928 lstart.push_back(static_cast<Index>(lnbr.size()));
929 }
930 momMG_.build(*t_, A.diag, lstart, lnbr, lcoef);
931 momMG_.setGaussSeidel(momGS_);
932 } else if (momMGon_ && !dist_) {
933 if (useStaircaseMG_) {
934 std::vector<double> kap(static_cast<std::size_t>(n));
935 std::vector<char> fl(static_cast<std::size_t>(n)), cu(static_cast<std::size_t>(n));
936 for (Index i = 0; i < n; ++i) {
937 kap[static_cast<std::size_t>(i)] = mom_.kappa(i);
938 fl[static_cast<std::size_t>(i)] = mom_.isFluid(i) ? 1 : 0;
939 cu[static_cast<std::size_t>(i)] = mom_.isCut(i) ? 1 : 0;
940 }
941 velMG_.build(*t_, h0_, rho_ / dt_, mu_, momOp_, kap, fl, cu, mgMinCoarse_);
942 velMG_.setGaussSeidel(momGS_);
943 } else {
944 // The Galerkin RAP hierarchy is a host triple-product over the fine CSR, so it needs the
945 // operator on the host; assemble it there for the MG build only (bit-identical to momOp_).
946 auto A = mom_.assembleOperator();
947 momMG_.build(*t_, A.diag, A.start, A.nbr, A.coef);
948 momMG_.setGaussSeidel(momGS_);
949 }
950 }
951 profPhase("velocity MG build");
952 std::vector<char> fluidVec(static_cast<std::size_t>(n));
953 for (Index i = 0; i < n; ++i)
954 fluidVec[static_cast<std::size_t>(i)] = mom_.isFluid(i) ? 1 : 0;
955 if (dist_) {
956 // Host walker through the resolver seam (nbr/upup may be ghost slots), then EXTEND the
957 // fluid flags over the ghost tail — the advection kernels read fl(j)/fl(upup) at ghost
958 // slots (mom_.fluidRaw() carries the ghost metadata filled from the world SdfFn).
959 geom_ = buildFaceGeom(pres_, [&](Index i) { return mom_.isFluid(i); });
960 geom_.fluid = toDevice(mom_.fluidRaw(), "fg_fluid_ext");
961 } else {
962 geom_ = assembleFaceGeom<Bits>(pres_, fluidVec, ov);
963 }
964 profPhase("assembleFaceGeom");
965 std::vector<double> rs(static_cast<std::size_t>(n));
966 for (Index i = 0; i < n; ++i)
967 rs[static_cast<std::size_t>(i)] = mom_.rhsScale(i);
968 rscale_ = toDevice(rs, "df_rscale");
969 fluid_ = geom_.fluid;
970 if (ghostGrad_ && !ghostSampled_)
971 buildGhostGradOverlay();
972 else
973 gc_ = GhostGradOverlay{}; // sampled mode: the CSR overlay gcS_ owns all cut cells
974 // C/F interface scheme overlays (cf_scheme.hpp): the same host builders the oracle uses
975 // (parity by construction), uploaded once. Momentum delta = ×μ on the α=1 velocity geometry.
976 // The per-FACE C/F gate (docs/amr_cf_flux_gate.md §6.1, §6.4) needs `regular = fluid && !cut`
977 // at GHOST slots too: the gate is a property of the FACE, and a 2:1 sub-face at a block seam
978 // has one of its two cells on another rank. `cut(g)` cannot be recomputed here — it is the SDF
979 // sign at g's SIX face-neighbour centres, and the discovery fixpoint registers the C/F
980 // tangential reach and the face sweep's ±2 upstream reach, not those six — so the OWNER's flag
981 // is exchanged instead. One `syncScalar` on an nExt-sized View, exact by definition, and
982 // decomposition-independent because the owner's own SDF samples are.
983 std::vector<char> cfRegular(static_cast<std::size_t>(nExt_), 0);
984 for (Index i = 0; i < n; ++i)
985 cfRegular[static_cast<std::size_t>(i)] = (mom_.isFluid(i) && !mom_.isCut(i)) ? 1 : 0;
986 if (dist_) {
987 std::vector<double> rx(static_cast<std::size_t>(nExt_), 0.0);
988 for (Index i = 0; i < n; ++i)
989 rx[static_cast<std::size_t>(i)] = cfRegular[static_cast<std::size_t>(i)] ? 1.0 : 0.0;
990 View<double> rv = toDevice(rx, "cf_regular");
991 syncScalar(rv);
992 const std::vector<double> rh = toVector(rv);
993 for (Index s = n; s < nExt_; ++s)
994 cfRegular[static_cast<std::size_t>(s)] = (rh[static_cast<std::size_t>(s)] > 0.5) ? 1 : 0;
995 }
996 // Programming-error guard (§6.4): a builder that asks for a slot the flag was never filled for
997 // is a bug in the discovery, not a configuration the solver should tolerate silently.
998 auto regularOk = [&](Index s) {
999 if (s < 0 || s >= static_cast<Index>(cfRegular.size()))
1000 throw std::logic_error("peclet::amr: C/F regular flag queried outside [0, nExt)");
1001 return cfRegular[static_cast<std::size_t>(s)] != 0;
1002 };
1003 cfCutFaces_ = countCfCutFaces(pres_, *t_, regularOk, [&](Index s) { return mom_.isFluid(s); });
1004 if (cfScheme_ != CfScheme::standard) {
1005 auto fluidOk = [&](Index i) { return mom_.isFluid(i); };
1006 auto rowRegular = [&](Index i) { return mom_.isFluid(i) && !mom_.isCut(i); };
1007 auto lapD = buildCfLapDelta(mom_.lap(), *t_, mu_, rowRegular, fluidOk, cfScheme_);
1008 // The projection family (D, the ABC gradient substitution, and the advecting face field)
1009 // is gated PER FACE, never per row: `cfFace(i, j) = level mismatch && regular(i) &&
1010 // regular(j)`, one predicate through one emitter (docs/amr_cf_flux_gate.md). A face flux is
1011 // ONE number shared by two cells, so a gate that is a property of a cell cannot be
1012 // conservative — under the old row gate the regular cell booked the C/F correction in its
1013 // constraint, the cut cell did not, and uf carried it for both: a permanent mass source at
1014 // the cut cell, ‖D(Δvel) − Δ_cfDiv‖ = 2.018e-01 against ‖div(uf)‖ = 2.016e-01 on a mesh
1015 // whose cut band meets the level boundary. Cut rows still receive no delta (the 2026-08-27
1016 // stability fix — 2 of 12 throat-graded meshes marched to k~1e12 with cut rows in cfDiv —
1017 // is retained in full by the same predicate, since a cut row has no passing face); what
1018 // changed is that the REGULAR row across a cut-adjacent 2:1 face no longer receives it
1019 // either, and that face reverts to the standard two-point value. Inert by geometry on any
1020 // uniform or finest-band mesh (cut cells there have no C/F face); `numCfCutFaces()` counts
1021 // the sub-faces where it is not.
1022 auto divD = buildCfDivDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
1023 auto gd = buildCfGradDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
1024 auto ufd = buildCfUfDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
1025 // Distributed diagnostic (numCfGhostColumns): how many overlay entries read a GHOST slot.
1026 // It is the observable that says the seam is actually exercised — a build that silently
1027 // fell back to the raw coarse value at every block boundary reports zero.
1028 cfGhostCols_ = 0;
1029 auto countGhost = [&](const std::vector<Index>& col) {
1030 for (Index c : col)
1031 if (c >= n)
1032 ++cfGhostCols_;
1033 };
1034 countGhost(lapD.slot);
1035 countGhost(divD.slot);
1036 for (int a = 0; a < 3; ++a)
1037 countGhost(gd[static_cast<std::size_t>(a)].slot);
1038 countGhost(ufd.vel.slot);
1039 cfMom_ = uploadCfCsr(lapD, "cf_mom");
1040 cfDiv_ = uploadCfCompCsr(divD, "cf_div");
1041 for (int a = 0; a < 3; ++a)
1042 cfGrad_[static_cast<std::size_t>(a)] =
1043 uploadCfCsr(gd[static_cast<std::size_t>(a)], "cf_grad");
1044 cfUfVel_ = uploadCfCompCsr(ufd.vel, "cf_ufvel");
1045 // The convective flux at a 2:1 seam (ROADMAP B5, docs/amr_cf_convective.md §5.2): the
1046 // descriptors + records the two advective kernels reconstruct the ADVECTED value from.
1047 // It reuses the SAME per-face gate (`regularOk`) and the SAME tangential sample operator
1048 // (`cfAppendStencil`, quadratic — §5.1 Q-A: one such operator suite-wide) as the four
1049 // overlays above, which is why it is built here rather than beside the FaceGeom.
1050 //
1051 // It RIDES THE C/F SCHEME (the design note pins the stencil — "cfAppendStencil ...
1052 // scheme = quadratic" §5.1 — but not this gating): with CfScheme::standard the whole
1053 // projection family takes the raw coarse value at a 2:1 face, and a quadratic tangential
1054 // sample in the advection alone would be a closure the rest of the operator does not
1055 // share. It is also what keeps the distributed probe story exact — probeCfScheme() and
1056 // probeSeamLayer() run under the same condition, so every coordinate this builder asks
1057 // for is already in the halo registry.
1058 uploadSeamRecon(buildSeamRecon(pres_, regularOk, fluidOk, CfScheme::quadratic));
1059 } else {
1060 cfGhostCols_ = 0;
1061 cfMom_ = CfCsrDev{};
1062 cfDiv_ = CfCompCsrDev{};
1063 for (int a = 0; a < 3; ++a)
1064 cfGrad_[static_cast<std::size_t>(a)] = CfCsrDev{};
1065 cfUfVel_ = CfCompCsrDev{};
1066 clearSeamRecon();
1067 }
1068 profPhase("cf overlays");
1069 if (ghostProj_) {
1070 // Closure overlay (pre-built in the mode-resolve step above) + the coupled-subspace mask
1071 // for the BiCGStab projection. Sampled mode uploads the sample-slot overlay instead (the
1072 // classic gpOv_ stays empty — every delta site calls both, empties no-op).
1073 gpOv_ = ghostSampled_ ? GhostOverlayDev{} : uploadGhostOverlay(hov);
1074 gpOvS_ = ghostSampled_ ? uploadGhostOverlaySampled(hovS) : GhostOverlaySampledDev{};
1075 const GhostOverlay& rows = ghostSampled_ ? hovS.base : hov;
1076 std::vector<double> mc(static_cast<std::size_t>(n), 0.0);
1077 for (Index i = 0; i < n; ++i)
1078 mc[static_cast<std::size_t>(i)] =
1079 (mom_.isFluid(i) && !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(i)]))
1080 ? 1.0
1081 : 0.0;
1082 for (Index r = 0; r < rows.n; ++r)
1083 if (!rows.coupled[static_cast<std::size_t>(r)])
1084 mc[static_cast<std::size_t>(rows.cell[static_cast<std::size_t>(r)])] = 0.0;
1085 maskC_ = toDevice(mc, "gp_maskc");
1086 // Sampled mode: momentum ξ-row seam correction (host CSR folds 1/rscale for the oracle's
1087 // pre-rscale source; the device adds POST-rscale via cfApply, so re-fold rscale per row)
1088 // + the CSR directional-gradient overlay (cascade over sample functionals; classic
1089 // same-level fallback on cut cells without a row — the oracle's gradP split).
1090 if (ghostSampled_) {
1091 CfCsr msd = buildMomSeamDelta(hovS, *t_, pres_, mom_, sdfFn, origin_, rho_ / dt_, mu_,
1092 /*nSkipped=*/nullptr, shiftD_);
1093 for (Index i = 0; i < n; ++i) {
1094 const double rs = mom_.rhsScale(i);
1095 for (Index k = msd.start[static_cast<std::size_t>(i)];
1096 k < msd.start[static_cast<std::size_t>(i) + 1]; ++k)
1097 msd.coef[static_cast<std::size_t>(k)] *= rs;
1098 }
1099 gpsMomDelta_ = uploadCfCsr(msd, "gps_momdelta");
1100 auto classicOk = [&](Index j, Index i) {
1101 return j >= 0 && mom_.isFluid(j) && pres_.levelOf(j) == pres_.levelOf(i) &&
1102 !(!gpPocket_.empty() && j < n && gpPocket_[static_cast<std::size_t>(j)]);
1103 };
1105 hovS, *t_, pres_, [&](Index i) { return mom_.isCut(i); }, classicOk));
1106 } else {
1107 gpsMomDelta_ = CfCsrDev{};
1108 gcS_ = GhostGradCsrDev{};
1109 }
1110 // Krylov scratch carries the ghost tail (matvec inputs) in distributed mode.
1111 auto mk = [&](const char* l) { return View<double>(l, static_cast<std::size_t>(nExt_)); };
1112 gpr_ = mk("gp_r");
1113 gprh_ = mk("gp_rhat");
1114 gpp_ = mk("gp_p");
1115 gpph_ = mk("gp_phat");
1116 gpv_ = mk("gp_v");
1117 gps_ = mk("gp_s");
1118 gpsh_ = mk("gp_shat");
1119 gpt_ = mk("gp_t");
1120 }
1121
1122 // Device state. Fields whose ghost entries are READ through the CSRs / overlays (u, p, φ)
1123 // — plus div (its View is deep_copied into the nExt-sized distributed MG rhs) and the u
1124 // snapshots (full-extent deep_copies of u) — carry the ghost tail [n, nExt); nExt == n
1125 // single-rank, so those allocations are unchanged there.
1126 for (int c = 0; c < 3; ++c) {
1127 u_[c] = View<double>("df_u", static_cast<std::size_t>(nExt_));
1128 gx_[c] = View<double>("df_g", static_cast<std::size_t>(n));
1129 Kokkos::deep_copy(u_[c], 0.0);
1130 }
1131 p_ = View<double>("df_p", static_cast<std::size_t>(nExt_));
1132 phi_ = View<double>("df_phi", static_cast<std::size_t>(nExt_));
1133 div_ = View<double>("df_div", static_cast<std::size_t>(nExt_));
1134 uf_ = View<double>("df_uf",
1135 geom_.nbr.extent(0)); // ABC divergence-free face field (per CSR face)
1136 faceFieldBuilt_ = false;
1137 bmom_ = View<double>("df_bmom", static_cast<std::size_t>(n));
1138 Kokkos::deep_copy(p_, 0.0);
1139 // Implicit-FOU advection state. The momentum operator + its velocity-MG are rebuilt each
1140 // step from the *full* operator (viscous + FOU) so the MG is advection-aware (the viscous-
1141 // only MG diverges on the advection operator at cut cells); the FOU is baked into the CSR
1142 // (momOp_.hasAdv stays false). defc holds the device-computed explicit ρ(SOU−FOU)
1143 // deferred correction. uadvHost_ caches u^n on the host for the per-step operator rebuild.
1144 for (int c = 0; c < 3; ++c) {
1145 defc_[c] = View<double>("df_defc", static_cast<std::size_t>(n));
1146 Kokkos::deep_copy(defc_[c], 0.0);
1147 // uⁿ snapshot for the backward-Euler mass term (frozen across Picard outer iters) + the
1148 // previous outer iterate for the outer-loop convergence test.
1149 u0_[c] = View<double>("df_u0", static_cast<std::size_t>(nExt_));
1150 uprev_[c] = View<double>("df_uprev", static_cast<std::size_t>(nExt_));
1151 }
1152 // Device-resident implicit-FOU advection: the FOU operator (advDiag + per-face advCoef over the
1153 // face-geometry CSR) is rebuilt on device each step from uⁿ and added to the static Stokes
1154 // operator in the matvec (no host round-trip). The static operator + Galerkin velocity-MG are
1155 // built once (above); the advection is a perturbation the static MG still preconditions.
1156 advDiag_ = View<double>("df_advdiag", static_cast<std::size_t>(n));
1157 advCoef_ = View<double>("df_advcoef", geom_.nbr.extent(0));
1158 momOp_.advStart = geom_.start;
1159 momOp_.advNbr = geom_.nbr;
1160 momOp_.advDiag = advDiag_;
1161 momOp_.advCoef = advCoef_;
1162 momOp_.hasAdv = false; // set per-step when advection is on
1163 momSolver_.setJacobi(2, 0.7);
1164 // Generic MG preconditioner: dispatch the chosen hierarchy's V-cycle (z = M⁻¹ r), decoupling
1165 // the solver from the MG type so Galerkin and staircase are interchangeable. Distributed:
1166 // the rank-local Galerkin hierarchy built above (staircase unsupported there ⇒ Jacobi).
1167 if (momMGon_ && !(dist_ && useStaircaseMG_)) {
1168 if (useStaircaseMG_)
1169 momSolver_.setPreconditioner(
1170 [this](View<const double> r, View<double> z) { runMgVcycle(velMG_, r, z); });
1171 else
1172 momSolver_.setPreconditioner(
1173 [this](View<const double> r, View<double> z) { runMgVcycle(momMG_, r, z); });
1174 }
1175 pcg_.setVcycle(2, 2, 60, 0.8);
1176 pcg_.setSingular(true);
1177 n_ = n;
1178 ready_ = true;
1179 profPhase("overlays upload + state alloc");
1180 }
1181
1185 // ---- M0 (docs/amr_march_perf_and_distributed_plan.md): the per-phase STEP profiler -----------
1186 // `PECLET_AMR_PROFILE_STEP=1` accumulates FENCED per-phase timings over a window of steps
1187 // (`PECLET_AMR_PROFILE_STEP_WINDOW`, default 50) and prints ms/step and µs/leaf per phase next
1188 // to the iteration counters — the instrument Phase M's attribution matrix reads. The four
1189 // pressure-solve rows are NESTED inside `pressure solve` (the residual is Krylov vector work and
1190 // the reductions), so they are printed indented and must not be added to the total.
1191 //
1192 // OFF (the default) every hook is one bool test: no fence, no clock, no change to the kernel
1193 // sequence — so a profiler-off march is bitwise what it was before the profiler existed.
1211 using SpClock = std::chrono::steady_clock;
1212 SpClock::time_point spMark() const {
1213 if (!stepProf_)
1214 return SpClock::time_point{};
1215 Kokkos::fence();
1216 return SpClock::now();
1217 }
1218 void spAdd(int ph, SpClock::time_point t0) {
1219 if (!stepProf_)
1220 return;
1221 Kokkos::fence();
1222 spAcc_[ph] += std::chrono::duration<double, std::milli>(SpClock::now() - t0).count();
1223 }
1225 void spEndStep() {
1226 if (!stepProf_)
1227 return;
1228 ++spSteps_;
1229 spMom_ += lastMomIters_;
1230 spPres_ += lastPresIters_;
1231 spOuter_ += lastOuterIters_;
1232 if (!spHeader_) { // the static hierarchy shape, on the first profiled step (H-mg's
1233 // denominator, and the one line a contention-exposed box can still trust)
1234 const std::vector<Index> lad = pressureMgLevels();
1235 std::fprintf(stderr, "[step-prof] leaves %lld | pressure MG levels %zu:", (long long)n_,
1236 lad.size());
1237 for (Index n : lad)
1238 std::fprintf(stderr, " %lld", (long long)n);
1239 std::fprintf(stderr, " | bottom %s\n", pressureMgBottom().c_str());
1240 spHeader_ = true;
1241 }
1242 if (spSteps_ < spWindow_)
1243 return;
1244 static const char* kName[SP_N] = {
1245 "glue (copies + syncs)", "grad3 (binary)", "cf delta kernels", "overlay delta kernels",
1246 "advection build", "momentum rhs", "momentum solve", "divergence",
1247 "pressure solve", " . binary matvec", " . overlay matvec", " . projection",
1248 " . MG preconditioner", "finish projection"};
1249 const double w = static_cast<double>(spSteps_);
1250 const double leaves = static_cast<double>(n_);
1251 double tot = 0.0;
1252 for (int k = 0; k < SP_N; ++k)
1254 tot += spAcc_[k];
1255 std::fprintf(stderr,
1256 "[step-prof] %d steps | %.3f ms/step (%.3f us/leaf) | mom %.1f it, "
1257 "pres %.1f it, outer %.2f\n",
1258 spSteps_, tot / w, 1e3 * tot / w / leaves, spMom_ / w, spPres_ / w, spOuter_ / w);
1259 for (int k = 0; k < SP_N; ++k) {
1260 const bool nested = (k >= SP_PRES_MV && k <= SP_PRES_PC);
1261 std::fprintf(stderr, "[step-prof] %-22s %8.3f ms/step %8.4f us/leaf %5.1f%%%s\n",
1262 kName[k], spAcc_[k] / w, 1e3 * spAcc_[k] / w / leaves,
1263 100.0 * spAcc_[k] / (tot > 0 ? tot : 1.0), nested ? " (nested)" : "");
1264 }
1265 for (int k = 0; k < SP_N; ++k)
1266 spAcc_[k] = 0.0;
1267 spSteps_ = 0;
1268 spMom_ = spPres_ = spOuter_ = 0.0;
1269 }
1270
1274 void requireOperator(const char* what) const {
1275 if (!ready_)
1276 throw std::runtime_error(std::string("amr::AmrFlow::") + what +
1277 ": no cut-cell operator -- call setSolid() first (Python: "
1278 "Flow.set_solid / set_solid_spheres; after begin_adapt, "
1279 "finish_adapt)");
1280 }
1281
1282 void step(int momIters = 100, int presIters = 60) {
1283 requireOperator("step");
1284 const Index n = n_;
1285 const double idiag = rho_ / dt_;
1286 lastMomIters_ = 0;
1287 lastOuterIters_ = 1;
1288 const auto spT0 = spMark();
1289 // Freeze the time-level uⁿ for the backward-Euler mass term + warm start; it stays anchored
1290 // across the Picard outer iterations (only the advecting velocity re-lags). For outerIters_==1
1291 // this is just a copy of uⁿ ⇒ bit-identical to the single lagged step.
1292 for (int c = 0; c < 3; ++c)
1293 Kokkos::deep_copy(u0_[c], View<const double>(u_[c]));
1294 // −∇p^n is constant across the outer iterations (pressure is projected once, after the loop,
1295 // like flow's single per-step projection) ⇒ hoist it out.
1296 syncScalar(p_); // ghost tail of pⁿ before the gradient reads (no-op single-rank)
1297 spAdd(SP_GLUE, spT0);
1298 auto spT = spMark();
1299 grad3(geom_, View<const double>(p_), gx_[0], gx_[1], gx_[2]);
1300 spAdd(SP_GRAD, spT);
1301 spT = spMark();
1302 for (int a = 0; a < 3; ++a) // 2nd-order C/F face gradients (level-boundary rows)
1303 cfApply(cfGrad_[static_cast<std::size_t>(a)], View<const double>(p_), gx_[a]);
1304 spAdd(SP_CF, spT);
1305 spT = spMark();
1306 applyGhostGrad(gc_, View<const double>(p_), gx_[0], gx_[1], gx_[2]);
1307 applyGhostGradCsr(gcS_, View<const double>(p_), gx_[0], gx_[1], gx_[2]);
1309 // Picard outer loop over the lagged advection only (the momentum nonlinearity); for
1310 // outerIters_==1 this is the single lagged predictor, then one projection — bit-identical to
1311 // before.
1312 for (int outer = 0; outer < outerIters_; ++outer) {
1313 // --- advection lagged to the *current* predictor iterate (uⁿ on the first pass):
1314 // implicit-FOU operator + explicit ρ(SOU−FOU) deferred correction. The matvec stays linear
1315 // during each solve (the advecting velocity is frozen in advDiag_/advCoef_). With advection
1316 // OFF the operator and RHS are identical every pass, so a second pass reproduces the first ⇒
1317 // instant early-stop. ---
1318 spT = spMark();
1319 if (advect_) {
1320 momOp_.hasAdv = implicitFou_;
1321 syncVel(); // ghost tails of the (lagged) advecting velocity for buildFou/deferredSou
1322 // Advect with the divergence-free face field uf (from the previous projection); fall back
1323 // to ½(u_i+u_j) before the first projection has built it. The implicit FOU and the explicit
1324 // SOU/FOU deferred correction use the SAME velocity ⇒ the FOU cancels at steady state
1325 // (host-parity).
1326 const View<const double> ufv(uf_);
1327 const bool useUf = faceFieldBuilt_ && ufAdvect_;
1328 if (implicitFou_)
1329 buildFou(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
1330 View<const double>(u_[2]), rho_, View<const double>(rscale_), advDiag_, advCoef_,
1331 ufv, useUf);
1332 for (int c = 0; c < 3; ++c) {
1333 if (implicitFou_)
1335 View<const double>(u_[2]), c, rho_, advScheme_, defc_[c], ufv, useUf,
1336 seamRecon_);
1337 else
1339 View<const double>(u_[2]), c, rho_, advScheme_, defc_[c], ufv, useUf,
1340 seamRecon_);
1341 }
1342 // The staircase MG's fine level mirrors the sharp operator; refresh it so it picks up the
1343 // current advection state (hasAdv). (The Galerkin MG is the static viscous operator.)
1344 if (momMGon_ && useStaircaseMG_)
1345 velMG_.setFineOp(momOp_);
1346 }
1347 spAdd(SP_ADV, spT);
1348 // --- predictor: incremental BE viscous (+ implicit-FOU) solve per component, RHS carries
1349 // −∇p^n and −ρ(SOU−FOU); the mass term is anchored at uⁿ (u0_), the solve warm-starts at the
1350 // current iterate. ---
1351 // D2: the momentum ξ-seam delta is a CSR over the overlay's SAMPLE functionals, so on a
1352 // mixed-level band it reads ghost slots — and with advection off nothing else has refreshed
1353 // uⁿ's ghost tail since the previous step's projection. The C/F momentum delta (cfMom_) is
1354 // in exactly the same position: it reads u at the coarse cell's tangential neighbours,
1355 // which are ghost slots at a block seam. (Zero ghosts single-rank / np=1, so this is a
1356 // no-op exchange there and the path stays bit-identical.)
1357 if (dist_ && (ghostSampled_ || cfScheme_ != CfScheme::standard))
1358 syncVel();
1359 for (int c = 0; c < 3; ++c) {
1360 spT = spMark();
1362 View<const double>(rscale_), View<const char>(fluid_), idiag, f_[c], bmom_, n);
1364 spT = spMark();
1365 // C/F-scheme deferred correction on the velocity diffusion: b += μ(∇²_scheme − ∇²_std)
1366 // of the lagged component (regular rows only, rscale = 1 there).
1367 cfApply(cfMom_, View<const double>(u_[c]), bmom_);
1368 // Momentum ξ-row seam correction (sampled mode; rscale re-folded at upload).
1369 cfApply(gpsMomDelta_, View<const double>(u_[c]), bmom_);
1370 spAdd(SP_CF, spT);
1371 spT = spMark();
1372 // P4 (opt-in): the velocity-MG used as the *solver* — MG-preconditioned defect correction,
1373 // no Krylov (the flow RB-GS/velocity-MG mirror; cannot break down on the non-symmetric
1374 // operator) — vs the default MG-preconditioned BiCGStab. Both reach the same solution (the
1375 // matvec is the exact operator); the choice only trades robustness for convergence rate.
1376 lastMomIters_ +=
1377 (momMGSolver_ ? momSolver_.solveDefectCorrection(
1378 momOp_, u_[c], View<const double>(bmom_), momIters, momTol_)
1379 : momSolver_.solveBiCGStab(momOp_, u_[c], View<const double>(bmom_),
1380 momIters, momTol_))
1381 .iters;
1383 }
1384 // Outer-loop convergence on the predictor velocity (skipped for the default outerIters_==1,
1385 // so that path is untouched). With advection off the second iterate equals the first ⇒ stops
1386 // at 2.
1387 if (outerIters_ > 1) {
1388 lastOuterIters_ = outer + 1;
1389 if (outer > 0) {
1390 double dmax = 0.0;
1391 for (int c = 0; c < 3; ++c)
1392 dmax = std::max(
1394 if (dmax < outerTol_)
1395 break;
1396 }
1397 for (int c = 0; c < 3; ++c)
1398 Kokkos::deep_copy(uprev_[c], View<const double>(u_[c]));
1399 }
1400 }
1401 project(presIters); // single pressure projection per step (flow structure)
1402 spEndStep();
1403 }
1404
1406 void project(int presIters = 60) {
1407 requireOperator("project");
1408 const Index n = n_;
1409 auto spT = spMark();
1410 syncVel(); // ghost tails of u* for the divergence (+ the ghost-closed overlay delta)
1411 divergence(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
1412 View<const double>(u_[2]), div_);
1413 spAdd(SP_DIV, spT);
1414 spT = spMark();
1415 cfApplyComp(cfDiv_, View<const double>(u_[0]), View<const double>(u_[1]),
1416 View<const double>(u_[2]), div_); // 2nd-order C/F face averages (setCfScheme)
1417 spAdd(SP_CF, spT);
1418 spT = spMark();
1419 if (ghostProj_) { // ghost-closed constraint: binary div (geom_ carries binary α) + overlay
1421 View<const double>(u_[2]), div_);
1423 View<const double>(u_[2]), div_);
1424 }
1426 Kokkos::deep_copy(phi_, 0.0);
1427 if (ghostProj_) {
1428 spT = spMark();
1429 lastPresIters_ = solveGhostBiCGStab(phi_, View<const double>(div_), presIters, presTol_);
1430 spAdd(SP_PRES, spT);
1431 spT = spMark();
1434 return;
1435 }
1436 // Two selectable pressure drivers, like flow's CutcellMG: MG-PCG (default, presPCG_) and the
1437 // bounded stationary V-cycle (setPressurePCG(false)). MG-PCG covers ADVECTION too — the
1438 // historic exclusion ("transient near-nullspace issue") was characterised 2026-08-19 and was
1439 // NOT a property of the operator (which is geometry-only, advection-independent, SPD in the
1440 // volume-weighted inner product): the aperture RHS is INCOMPATIBLE by a fluid-mean component
1441 // (div_ is zeroed at solid-centered cells whose faces are partially open, breaking the
1442 // telescoping that would make Σ V·div = 0 over the operator's DOF set; the defect grows with
1443 // the developed flow, ~3e-3 relative at steady state on the Z&H sphere). The un-deflated
1444 // V-cycle's residual therefore STALLS at exactly |mean|·sqrt(V_fluid) — the old "60 bounded
1445 // cycles" was a stagnation cap, not a convergence count. The PCG's per-iteration fluid-range
1446 // projection (maskSolid + volume-weighted fluid-mean removal) deflates exactly that component,
1447 // so CG is valid and healthy: measured flat 15–17 iters/step (tol 1e-10) across the whole
1448 // impulsive N=32 transient, steady K identical to the V-cycle path to 4+ digits.
1449 // Debug knob (env, default-off): PECLET_AMR_PRES_DEBUG=1 — per-cycle/-solve residual +
1450 // RHS-compatibility trace to stderr (the characterisation instrumentation, kept).
1451 const bool dbg = amrEnvFlag("PECLET_AMR_PRES_DEBUG");
1452 if (dbg && !dist_) {
1453 // RHS compatibility: the operator's left null vector is the constant over fluid cells in the
1454 // volume-weighted inner product, so a solvable RHS needs Σ V_i·div_i ≈ 0 over fluid cells.
1455 const FvOp& op0 = presMG_.op(0);
1456 auto st = op0.faceStart;
1457 auto w = op0.faceW;
1458 auto bcD = op0.bcDiag;
1459 auto iv = op0.invVol;
1460 auto dv = div_;
1461 double su = 0.0, sv = 0.0, sn = 0.0;
1462 Kokkos::parallel_reduce(
1463 "amr::dbg_compat", n,
1464 KOKKOS_LAMBDA(const Index i, double& a, double& b, double& c) {
1465 double d = bcD(i);
1466 for (Index k = st(i); k < st(i + 1); ++k)
1467 d += w(k);
1468 const double m = (d > 1e-30) ? 1.0 : 0.0;
1469 a += m * dv(i) / iv(i);
1470 b += m / iv(i);
1471 c += m * dv(i) * dv(i) / iv(i);
1472 },
1473 su, sv, sn);
1474 std::fprintf(stderr, "[amr pres] rhs fluid-mean=%.3e |rhs|_D=%.3e (rel mean %.3e)\n", su / sv,
1475 std::sqrt(sn), (su / sv) / (std::sqrt(sn) + 1e-300));
1476 if (!presDbgSpdDone_) {
1477 // One-shot direct SPD probe of the assembled pressure operator (advection cannot enter
1478 // the assembly — this measures it): symmetry <y,Lx>_D vs <x,Ly>_D on deterministic
1479 // pseudo-random vectors, and the Rayleigh quotient sign (L negative-semidefinite).
1480 presDbgSpdDone_ = true;
1481 View<double> xr("dbg_x", static_cast<std::size_t>(n)),
1482 yr("dbg_y", static_cast<std::size_t>(n)), Ax("dbg_Ax", static_cast<std::size_t>(n)),
1483 Ay("dbg_Ay", static_cast<std::size_t>(n));
1484 Kokkos::parallel_for(
1485 "amr::dbg_fill", n, KOKKOS_LAMBDA(const Index i) {
1486 xr(i) = std::sin(0.7 * static_cast<double>(i) + 0.3);
1487 yr(i) = std::cos(1.3 * static_cast<double>(i) + 1.1);
1488 });
1492 double s = 0.0;
1493 Kokkos::parallel_reduce(
1494 "amr::dbg_dot", n,
1495 KOKKOS_LAMBDA(const Index i, double& acc) { acc += a(i) * b(i) / iv(i); }, s);
1496 return s;
1497 };
1502 std::fprintf(stderr,
1503 "[amr pres] SPD probe: <y,Lx>_D=%.15e <x,Ly>_D=%.15e rel asym=%.2e; "
1504 "Rayleigh <x,Lx>_D=%.3e <y,Ly>_D=%.3e (must be <=0)\n",
1505 yLx, xLy, std::fabs(yLx - xLy) / (std::fabs(yLx) + 1e-300), xLx, yLy);
1506 }
1507 }
1508 spT = spMark();
1509 if (presPCG_) {
1510 const auto R = dist_
1511 ? pcg_.solve(presMGD_, phi_, View<const double>(div_), presIters, presTol_)
1512 : pcg_.solve(presMG_, phi_, View<const double>(div_), presIters, presTol_);
1513 lastPresIters_ = R.iters;
1514 if (dbg)
1515 std::fprintf(stderr, "[amr pres] pcg iters=%d res0=%.3e res=%.3e rel=%.3e\n", R.iters,
1516 R.res0, R.res, R.res0 > 0 ? R.res / R.res0 : 0.0);
1517 } else if (dist_) {
1518 Kokkos::deep_copy(presMGD_.b(0), div_);
1519 Kokkos::deep_copy(presMGD_.x(0), 0.0);
1520 for (int it = 0; it < presIters; ++it)
1521 presMGD_.vcycle(2, 2, 60, 0.8);
1522 Kokkos::deep_copy(phi_, presMGD_.x(0));
1523 lastPresIters_ = presIters;
1524 } else {
1525 Kokkos::deep_copy(presMG_.b(0), div_);
1526 Kokkos::deep_copy(presMG_.x(0), 0.0);
1528 if (dbg)
1529 rdbg = View<double>("pres_dbg_res", static_cast<std::size_t>(n));
1530 for (int it = 0; it < presIters; ++it) {
1531 presMG_.vcycle(2, 2, 60, 0.8);
1532 if (dbg) {
1533 residualFv(presMG_.op(0), View<const double>(presMG_.x(0)),
1534 View<const double>(presMG_.b(0)), rdbg);
1535 const double rn =
1536 std::sqrt(dotPlain(View<const double>(rdbg), View<const double>(rdbg), n));
1537 std::fprintf(stderr, "[amr pres] vcycle %2d |r|=%.6e\n", it + 1, rn);
1538 }
1539 }
1540 Kokkos::deep_copy(phi_, presMG_.x(0));
1541 lastPresIters_ = presIters;
1542 }
1543 spAdd(SP_PRES, spT);
1544 spT = spMark();
1547 }
1548
1553 // u*'s ghost tail is current (project() synced it and u is untouched since); φ's is not.
1554 syncScalar(phi_);
1556 View<const double>(u_[2]), View<const double>(phi_), uf_);
1557 // 2nd-order C/F face values (setCfScheme), VELOCITY part only: the distance-weighted
1558 // {u_fine, coarse*} interpolation on the 2:1 sub-faces. Its divergence IS the constraint's
1559 // Δ_cfDiv — measured identical to 8.7e-17 — so the projection has already balanced it and uf
1560 // stays divergence-free with it in.
1561 cfApplyComp(cfUfVel_, View<const double>(u_[0]), View<const double>(u_[1]),
1562 View<const double>(u_[2]), uf_);
1563 // The φ part is NOT applied, and must not be. It substitutes the quadratic coarse* into uf's
1564 // FACE GRADIENT, but the pressure matrix is L = D_std·G_std (a recorded decision: at the fixed
1565 // point φ→0, so the matrix's C/F order cannot move the steady solution). A gradient the solve
1566 // never inverted cannot be balanced by it, so adding it breaks exactly the property uf exists
1567 // to have: Σ_faces α·A·dir·uf = 0, i.e. that advecting a scalar with uf conserves it and
1568 // leaves a uniform field uniform.
1569 //
1570 // Measured on a graded sphere at cf=1 (N=32, lmax=2, band=3, 25 steps), decomposing
1571 // d = (rhs − Lφ) − Δ_cfDiv + D(Δvel) + D(Δφ) [identity holds to 2.3e-15]
1572 // solve residual ‖rhs − Lφ‖ 1.34e-12
1573 // ‖D(Δvel) − Δ_cfDiv‖ 8.71e-17 ← velocity part: balanced, keep it
1574 // ‖D(Δφ)‖ 6.675e-03 ← THE ENTIRE flux imbalance
1575 // It is proportional to φ (0.240 at step 1, 6.7e-3 at step 25, 3.1e-4 at step 60) so it
1576 // vanishes at steady state and never showed up in a steady drag gate — but with advection on
1577 // it sat at 4.0e-2, the same order as the cell field's O(h²) divergence, i.e. uf had stopped
1578 // being a conservative flux for the whole of the transient.
1579 //
1580 // If a quadratic face gradient in uf is wanted, the pressure matrix has to invert the same
1581 // operator (L = D·G_quad). Mixing them is what this removes. The φ CSR is no longer built at
1582 // all (docs/amr_cf_flux_gate.md §6.7) — a CSR that must never be applied is a trap; §6.8
1583 // records the deferred-correction path that would buy a quadratic uf face gradient legally.
1584 faceFieldBuilt_ = true;
1585 grad3(geom_, View<const double>(phi_), gx_[0], gx_[1], gx_[2]);
1586 for (int a = 0; a < 3; ++a) // 2nd-order C/F face gradients (level-boundary rows)
1587 cfApply(cfGrad_[static_cast<std::size_t>(a)], View<const double>(phi_), gx_[a]);
1588 applyGhostGrad(gc_, View<const double>(phi_), gx_[0], gx_[1], gx_[2]);
1589 applyGhostGradCsr(gcS_, View<const double>(phi_), gx_[0], gx_[1], gx_[2]);
1590 for (int c = 0; c < 3; ++c)
1591 correct(u_[c], View<const double>(gx_[c]), View<const char>(fluid_), n);
1593 rho_ / dt_, mu_, n);
1594 }
1595
1596 // ---- adaptivity during a run (ladder step 5) -------------------------------------------------
1597 // The octree is borrowed by pointer and mutated EXTERNALLY (refine/coarsen/balance/adapt on the
1598 // same object). beginAdapt snapshots the current topology + fields; after the mutation,
1599 // finishAdapt conservatively remaps u and the accumulated rotational p onto the new mesh
1600 // (minmod-limited linear transferField) and rebuilds every solver structure via setSolid. The
1601 // caller must keep the cut band at the finest level on the new mesh (refineToSdf the geometry
1602 // band again after a solution-driven adapt): the ghost overlay build throws / auto-falls-back
1603 // exactly as in setSolid. uf restarts from the ½-average fallback for one step.
1604
1611 void beginAdapt() {
1612 requireOperator("beginAdapt");
1613 ready_ = false; // no operator until finishAdapt rebuilds it on the new topology
1614 adaptOldT_ = std::make_unique<Octree>(*t_);
1615 for (int c = 0; c < 3; ++c)
1616 adaptU_[static_cast<std::size_t>(c)] = velocity(c);
1617 adaptP_ = pressure();
1618 if (dist_) {
1619 // Halo-completed prolongation gradients on the OLD mesh (while dist_ still holds it):
1620 // the block-local transfer stencil zeroes gradients at interior block boundaries,
1621 // which would make the remap np-dependent there (measured ~5% field divergence).
1622 for (int c = 0; c < 3; ++c)
1623 adaptGradU_[static_cast<std::size_t>(c)] =
1624 transferGradients(*dist_, adaptU_[static_cast<std::size_t>(c)]);
1625 adaptGradP_ = transferGradients(*dist_, adaptP_);
1626 }
1627 }
1628
1630 template <class SdfFn>
1632 if (!adaptOldT_)
1633 throw std::runtime_error("amr::AmrFlow::finishAdapt called without beginAdapt");
1634 std::array<std::vector<double>, 3> nu;
1635 for (int c = 0; c < 3; ++c)
1636 nu[static_cast<std::size_t>(c)] =
1637 transferField(*adaptOldT_, adaptU_[static_cast<std::size_t>(c)], *t_, /*linear=*/true,
1638 dist_ ? &adaptGradU_[static_cast<std::size_t>(c)] : nullptr);
1639 std::vector<double> np =
1640 transferField(*adaptOldT_, adaptP_, *t_, /*linear=*/true, dist_ ? &adaptGradP_ : nullptr);
1641 setSolid(sdfFn); // full operator/overlay rebuild on the new topology (zeroes the fields)
1642 for (int c = 0; c < 3; ++c) {
1643 setVelocity(c, nu[static_cast<std::size_t>(c)]);
1644 zeroSolid(u_[c]); // cells that became solid on the new mesh hold 0 (no-slip state)
1645 }
1646 setPressure(np);
1647 zeroSolid(p_); // solid p is pinned/decoupled
1648 adaptOldT_.reset();
1649 for (int c = 0; c < 3; ++c)
1650 adaptU_[static_cast<std::size_t>(c)].clear();
1651 adaptP_.clear();
1652 }
1653
1660 template <class SdfFn>
1662 if (!dist_)
1663 throw std::runtime_error("amr::AmrFlow::rebalanceMpi requires initMpi");
1664 std::vector<std::vector<double>> cols(4);
1665 for (int c = 0; c < 3; ++c)
1666 cols[static_cast<std::size_t>(c)] = velocity(c);
1667 cols[3] = pressure();
1668 dist_->rebalance(cols); // t_ still points at dist_->local(), now the new block's octree
1669 setSolid(sdfFn); // full rebuild (registry, halos, operators) on the new block
1670 for (int c = 0; c < 3; ++c) {
1671 setVelocity(c, cols[static_cast<std::size_t>(c)]);
1672 zeroSolid(u_[c]);
1673 }
1674 setPressure(cols[3]);
1675 zeroSolid(p_);
1676 }
1677
1679 void setPressure(const std::vector<double>& h) {
1680 auto m = Kokkos::create_mirror_view(p_);
1681 for (Index i = 0; i < n_; ++i)
1682 m(i) = h[static_cast<std::size_t>(i)];
1683 Kokkos::deep_copy(p_, m);
1684 }
1685
1688 auto fl = fluid_;
1689 Kokkos::parallel_for(
1690 "amr::flow_zerosolid", n_, KOKKOS_LAMBDA(const Index i) {
1691 if (!fl(i))
1692 v(i) = 0.0;
1693 });
1694 }
1695
1698 std::vector<double> debugSou(int comp) {
1699 syncVel();
1700 View<double> s("dbg_sou", static_cast<std::size_t>(n_));
1702 View<const double>(u_[2]), comp, 1.0, advScheme_, s, View<const double>(uf_),
1703 false, seamRecon_);
1704 std::vector<double> h(static_cast<std::size_t>(n_));
1705 auto m = Kokkos::create_mirror_view(s);
1706 Kokkos::deep_copy(m, s);
1707 for (Index i = 0; i < n_; ++i)
1708 h[static_cast<std::size_t>(i)] = m(i);
1709 return h;
1710 }
1714 template <class MG>
1716 // Element copies over the LOCAL rows (not deep_copy): in distributed mode the Krylov
1717 // scratch carries a ghost tail while the (rank-local) MG levels are local-sized; the
1718 // solver refreshes z's ghost tail before the next matvec. Same values single-rank.
1719 const Index nl = mg.numLeaves(0);
1720 {
1721 auto b0 = mg.b(0);
1722 Kokkos::parallel_for("amr::mgv_b", nl, KOKKOS_LAMBDA(const Index i) { b0(i) = r(i); });
1723 }
1724 Kokkos::deep_copy(mg.x(0), 0.0);
1725 mg.vcycle(mgVcPre_, mgVcPre_, mgVcBottom_, 0.7);
1726 {
1727 auto x0 = mg.x(0);
1728 Kokkos::parallel_for("amr::mgv_z", nl, KOKKOS_LAMBDA(const Index i) { z(i) = x0(i); });
1729 }
1730 }
1733 double m = 0.0;
1734 Kokkos::parallel_reduce(
1735 "amr::flow_maxdiff", n,
1736 KOKKOS_LAMBDA(const Index i, double& lm) {
1737 double d = a(i) - b(i);
1738 if (d < 0.0)
1739 d = -d;
1740 if (d > lm)
1741 lm = d;
1742 },
1743 Kokkos::Max<double>(m));
1744 return m;
1745 }
1747 void copyToHost(const View<double>& d, std::vector<double>& h) const {
1748 auto m = Kokkos::create_mirror_view(d);
1749 Kokkos::deep_copy(m, d);
1750 for (Index i = 0; i < n_; ++i)
1751 h[static_cast<std::size_t>(i)] = m(i);
1752 }
1754 void setVelocity(int c, const std::vector<double>& h) {
1755 auto m = Kokkos::create_mirror_view(u_[c]);
1756 for (Index i = 0; i < n_; ++i)
1757 m(i) = h[static_cast<std::size_t>(i)];
1758 Kokkos::deep_copy(u_[c], m);
1759 }
1762 std::vector<double> velocity(int c) const { return localVector(u_[c]); }
1765 std::vector<double> pressure() const { return localVector(p_); }
1766
1768 std::vector<double> localVector(const View<double>& v) const {
1769 if (v.extent(0) == static_cast<std::size_t>(n_))
1770 return peclet::core::toVector(v);
1771 View<double> packed(Kokkos::view_alloc("amr::local_packed", Kokkos::WithoutInitializing),
1772 static_cast<std::size_t>(n_));
1773 Kokkos::parallel_for("amr::pack_local", n_, KOKKOS_LAMBDA(const Index i) { packed(i) = v(i); });
1774 return peclet::core::toVector(packed);
1775 }
1776
1781 std::vector<double> velocities() const {
1782 View<double> packed(Kokkos::view_alloc("amr::vel_packed", Kokkos::WithoutInitializing),
1783 static_cast<std::size_t>(n_) * 3);
1784 for (int c = 0; c < 3; ++c) {
1785 auto uc = u_[c];
1786 auto p = packed;
1787 const int cc = c;
1788 Kokkos::parallel_for(
1789 "amr::pack_vel", n_, KOKKOS_LAMBDA(const Index i) { p(i * 3 + cc) = uc(i); });
1790 }
1791 return peclet::core::toVector(packed);
1792 }
1795 double divNormL2() {
1796 syncVel();
1797 divergence(geom_, View<const double>(u_[0]), View<const double>(u_[1]),
1798 View<const double>(u_[2]), div_);
1799 cfApplyComp(cfDiv_, View<const double>(u_[0]), View<const double>(u_[1]),
1800 View<const double>(u_[2]), div_);
1801 if (ghostProj_) {
1803 View<const double>(u_[2]), div_);
1805 View<const double>(u_[2]), div_);
1806 }
1807 return std::sqrt(allSum(dotPlain(View<const double>(div_), View<const double>(div_), n_)));
1808 }
1824 double divNormFace() {
1825 const double l = divFaceNorm(geom_, View<const double>(uf_));
1826 return std::sqrt(allSum(l * l));
1827 }
1834 std::vector<Index> pressureMgLevels() const {
1835 std::vector<Index> out;
1836 if (!dist_) {
1837 for (std::size_t L = 0; L < presMG_.numLevels(); ++L)
1838 out.push_back(presMG_.numLeaves(L));
1839 return out;
1840 }
1841 for (std::size_t L = 0; L < presMGD_.numLevels(); ++L)
1842 out.push_back(presMGD_.numLeaves(L));
1843 for (std::size_t L = 0; L < presMGD_.numStageLevels(); ++L)
1844 out.push_back(presMGD_.stageLeaves(L)); // §6.5: the stage's level 0 IS the moved level
1845 return out;
1846 }
1852 std::string pressureMgBottom() const {
1853 return dist_ ? presMGD_.bottomName() : presMG_.bottomName();
1854 }
1855
1868 void setPressureBottom(const std::string& kind) {
1869 using B = typename Multigrid<3, Bits>::Bottom;
1870 if (kind == "auto")
1871 presBottom_ = B::Auto;
1872 else if (kind == "smoother")
1873 presBottom_ = B::Smoother;
1874 else if (kind == "agglomerated")
1875 presBottom_ = B::Agglomerated;
1876 else
1877 throw std::runtime_error(
1878 "amr::AmrFlow::setPressureBottom: expected 'auto', 'smoother' or 'agglomerated'");
1879 if (dist_)
1880 presMGD_.setBottom(presBottom_);
1881 else
1882 presMG_.setBottom(presBottom_);
1883 }
1884
1895 if (e < 1)
1896 throw std::runtime_error("amr::AmrFlow::setPressureBottomExtent: must be >= 1");
1897 presBottomExtent_ = e;
1898 }
1900 Index pressureBottomExtent() const { return presBottomExtent_; }
1901
1904 std::vector<double> faceField() const { return peclet::core::toVector(uf_); }
1915 std::vector<Index> start, nbr, upupI, upupJ;
1916 std::vector<int> axis, dir;
1917 std::vector<double> rawArea, dist, alpha;
1922 std::vector<Index> seam, sampI, sampJ, uuRecI, uuRecJ, recStart, recCell;
1923 std::vector<double> d1I, d1J, recDist, recW;
1928 std::vector<double> center;
1929 };
1932 t.start = peclet::core::toVector(geom_.start);
1933 t.nbr = peclet::core::toVector(geom_.nbr);
1934 t.upupI = peclet::core::toVector(geom_.upupI);
1935 t.upupJ = peclet::core::toVector(geom_.upupJ);
1936 t.axis = peclet::core::toVector(geom_.axis);
1937 t.dir = peclet::core::toVector(geom_.dir);
1938 t.rawArea = peclet::core::toVector(geom_.rawArea);
1939 t.dist = peclet::core::toVector(geom_.dist);
1940 t.alpha = peclet::core::toVector(geom_.alpha);
1941 t.seam = peclet::core::toVector(geom_.seam);
1942 t.sampI = peclet::core::toVector(geom_.sampI);
1943 t.sampJ = peclet::core::toVector(geom_.sampJ);
1944 t.uuRecI = peclet::core::toVector(geom_.uuRecI);
1945 t.uuRecJ = peclet::core::toVector(geom_.uuRecJ);
1946 t.recStart = peclet::core::toVector(geom_.recStart);
1947 t.recCell = peclet::core::toVector(geom_.recCell);
1948 t.d1I = peclet::core::toVector(geom_.d1I);
1949 t.d1J = peclet::core::toVector(geom_.d1J);
1950 t.recDist = peclet::core::toVector(geom_.recDist);
1951 t.recW = peclet::core::toVector(geom_.recW);
1952 t.center = slotCenters();
1953 return t;
1954 }
1959 std::vector<double> slotCenters() const {
1960 std::vector<double> c(static_cast<std::size_t>(nExt_) * 3, 0.0);
1961 for (Index s = 0; s < nExt_; ++s) {
1962 const std::array<long, 3> lo = pres_.loOf(s);
1963 const double w = static_cast<double>(1L << pres_.levelOf(s));
1964 for (int a = 0; a < 3; ++a)
1965 c[static_cast<std::size_t>(s) * 3 + static_cast<std::size_t>(a)] =
1966 origin_[a] + h0_[a] * (static_cast<double>(lo[a] + shiftD_[a]) + 0.5 * w);
1967 }
1968 return c;
1969 }
1973 Index numSeamSampleRecords() const { return seamSampleRecords_; }
1976 Index numSeamLayerRecords() const { return seamLayerRecords_; }
1977 Index numLeaves() const { return n_; }
1979 Index numGhostCells() const { return nExt_ - n_; }
1983 Index numCfGhostColumns() const { return cfGhostCols_; }
1990 Index numCfCutFaces() const { return cfCutFaces_; }
1992 bool isFluid(Index i) const { return mom_.isFluid(i); }
1994 int lastMomIters() const { return lastMomIters_; }
1996 int lastPresIters() const { return lastPresIters_; }
1998 int lastOuterIters() const { return lastOuterIters_; }
1999
2000 // ---- distributed plumbing (docs/amr_distributed_flow.md, rung 4) ----------------------------
2001
2003 void syncVel() {
2004 if (dist_)
2005 dhex_.exchange3(u_[0], u_[1], u_[2]);
2006 }
2009 if (dist_)
2010 dhex_.exchange(v);
2011 }
2013 double allSum(double s) const {
2014 if (!dist_)
2015 return s;
2016 double g = 0.0;
2017 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, dist_->comm());
2018 return g;
2019 }
2023 std::array<long, 3> globalFineExtent() const {
2024 std::array<long, 3> g{};
2025 for (int a = 0; a < 3; ++a)
2026 g[a] = dist_ ? static_cast<long>(dist_->globalFineSize()[a])
2027 : static_cast<long>(pres_.fineExt()[a]);
2028 return g;
2029 }
2030
2032 const FvOp& gpOp0() { return dist_ ? presMGD_.op(0) : presMG_.op(0); }
2033
2037 template <class SdfFn>
2039 dhalo_.init(*dist_);
2040 for (int a = 0; a < 3; ++a)
2041 shiftD_[a] = dist_->blockFineOrigin()[a];
2042 auto resv = [hp = &dhalo_, sh = shiftD_](const std::array<long, 3>& p) -> Index {
2043 std::array<long, 3> g = p;
2044 for (int a = 0; a < 3; ++a)
2045 g[a] += sh[a];
2046 return hp->resolveGlobal(g);
2047 };
2048 mom_.setFrameShift(shiftD_);
2049 mom_.setResolver(resv);
2050 pres_.setFrameShift(shiftD_);
2051 pres_.setResolver(resv);
2052 const Index n = t_->numLeaves();
2053 const Vec<3> beta = betaPerAxis();
2054 int rounds = 0;
2055 const bool profSetup = amrEnvFlag("PECLET_AMR_PROFILE_SETUP");
2056 for (;;) {
2057 ++rounds;
2058 const auto rT0 = std::chrono::steady_clock::now();
2059 installGhostMeta(); // metadata for every ghost known so far (same-round hits read it)
2060 // Classic path: mom_.build stays in the loop because its sdfC feeds the classic overlay
2061 // prober below. Sampled path (D3 setup-cost fix): its ±1 probes are a subset of the face
2062 // sweep's explicit periodicNeighbor probes, so the full build runs ONCE after the fixpoint
2063 // instead of once per round.
2064 if (!ghostSampled_)
2065 mom_.build(sdfFn, rho_ / dt_, beta); // ±1 probes (also fills the extended sdfC/fluid)
2066 // FaceGeom probes: the full face enumeration + the SOU upstream-of-upwind ±2 reach.
2067 // Host-parallel since the F1 resolution (miss registration is mutex-guarded and the miss
2068 // SET is order-canonical — see LeafHalo::resolve); pure discovery, results discarded.
2069 hostParFor(n, [&](Index i) {
2070 pres_.forEachFaceFull(i, [&](Index j, int ax, int dr, double, double, double) {
2071 (void)pres_.periodicNeighbor(i, ax, -dr);
2072 if (j >= 0)
2073 (void)pres_.periodicNeighbor(j, ax, dr);
2074 });
2075 });
2076 // Overlay / directional-gradient ±2 chains (every cut cell is a non-clean overlay row,
2077 // so this covers buildGhostGradOverlay's probes too). Discovery only — result discarded.
2078 if (ghostSampled_) {
2079 // D1 (DD2): the SAMPLED builders are probers too, and they reach further than the classic
2080 // ±2 chain — an LS cloud descends a whole box of radius 2.2·max(h,H) around each virtual
2081 // position, and the mixed openness rule probes ±¼h0 off every face centroid. The overlay
2082 // runs in PROBE-ONLY discovery mode (D3 setup-cost fix): identical control flow and probe
2083 // pattern, pending regions blind-descended at 4-cell granularity so their misses register
2084 // in one collective round instead of one octree level per round, and none of the
2085 // least-squares work a discovery round would discard is done. The REAL build (setSolid,
2086 // after this fixpoint) runs with every ghost resolved and never sets the flag.
2087 const auto gfine = globalFineExtent();
2088 (void)buildGhostOverlaySampled(*t_, pres_, sdfFn, gpMatrixOrder_, gpRhsOrder_, origin_,
2089 &gfine, shiftD_, /*discovery=*/true, gpsRho_, gpsMaxN_);
2091 *t_, pres_, [&sdfFn](const Vec<3>& p) { return sdfFn(p); }, h0_, origin_, shiftD_);
2092 pres_.buildOpenness(binFn); // discovery only; setSolid rebuilds it after the fixpoint
2093 } else if (ghostProj_ || ghostGrad_) {
2094 bool viol = false;
2095 (void)buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
2096 }
2097 if (cfScheme_ != CfScheme::standard) {
2098 probeCfScheme(); // the four buildCf*Delta builders' tangential reach
2099 probeSeamLayer(); // the seam builder's far-side reach across a GHOST neighbour
2100 }
2101 const double rBuildMs =
2102 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - rT0).count();
2103 const long pend = dhalo_.resolveMisses();
2104 if (profSetup)
2105 std::fprintf(stderr, "[setSolid] round %d: build %.1f ms, %lld ghosts, %ld pending\n",
2106 rounds, rBuildMs, (long long)dhalo_.numGhosts(), pend);
2107 if (pend == 0)
2108 break;
2109 }
2110 if (profSetup)
2111 // The discovery fixpoint's round count. The classic +-2 chain converges in <=3; the SAMPLED
2112 // builders reach a whole LS box, and D0's descent deliberately learns one octree level per
2113 // round rather than registering a miss per fine cell, so the sampled path needs more (it
2114 // grows with the level span the clouds cover). Each round is a full rebuild + a collective.
2115 std::fprintf(stderr, "[setSolid] distributed discovery fixpoint: %d rounds (%lld ghosts)\n",
2116 rounds, (long long)dhalo_.numGhosts());
2118 if (ghostSampled_)
2119 mom_.build(sdfFn, rho_ / dt_, beta); // once, every coord it probes already resolved
2120 dhalo_.finalize();
2121 nExt_ = dhalo_.extendedSize();
2122 dhex_.init(dhalo_);
2123 allred_ = [this](double s) { return allSum(s); };
2124 momSolver_.setDistributed([this](View<double> v) { dhex_.exchange(v); }, allred_, nExt_);
2125 }
2126
2152 const Index n = t_->numLeaves();
2153 hostParFor(n, [&](Index i) {
2154 const unsigned Li = pres_.levelOf(i);
2155 pres_.forEachFaceFull(i, [&](Index j, int axis, int, double, double, double) {
2156 const unsigned Lj = pres_.levelOf(j);
2157 if (Lj == Li)
2158 return;
2159 probeCfTangential((Lj > Li) ? j : i, axis);
2160 });
2161 });
2162 }
2163
2168 const unsigned Lc = pres_.levelOf(coarse);
2169 const std::array<long, 3> bc = pres_.loOf(coarse);
2170 const long sc = 1L << Lc, sh = sc >> 1;
2171 for (int tt = 0; tt < 3; ++tt) {
2172 if (tt == axis)
2173 continue;
2174 for (int dir = -1; dir <= 1; dir += 2) {
2175 (void)pres_.periodicNeighbor(coarse, tt, dir);
2176 std::array<long, 3> lo = bc;
2177 lo[tt] += (dir > 0) ? sc : -sc;
2178 for (int oct = 0; oct < (1 << 3); ++oct) {
2179 std::array<long, 3> q = lo;
2180 for (int d = 0; d < 3; ++d)
2181 if ((oct >> d) & 1)
2182 q[d] += sh;
2183 (void)pres_.probeSlot(q);
2184 }
2185 }
2186 }
2187 }
2188
2206 const Index n = t_->numLeaves();
2207 hostParFor(n, [&](Index i) {
2208 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double, double) {
2209 if (j < 0)
2210 return;
2211 const unsigned Lj = pres_.levelOf(j);
2212 if (Lj > 0) { // the four face-layer corners beyond j (a layer record with C == j)
2213 const std::array<long, 3> lo = pres_.loOf(j);
2214 const long sj = 1L << Lj, sh = sj >> 1;
2215 std::array<long, 3> p{};
2216 for (int d = 0; d < 3; ++d)
2217 p[d] = lo[d];
2218 p[axis] = (dir > 0) ? lo[axis] + sj : lo[axis] - 1;
2219 for (int k = 0; k < 4; ++k) {
2220 std::array<long, 3> q = p;
2221 int bit = 0;
2222 for (int t = 0; t < 3; ++t) {
2223 if (t == axis)
2224 continue;
2225 q[t] = lo[t] + (((k >> bit) & 1) ? sh : 0L);
2226 ++bit;
2227 }
2228 (void)pres_.probeSlot(q);
2229 }
2230 }
2231 const Index uJ = pres_.periodicNeighbor(j, axis, dir);
2232 if (uJ >= 0 && pres_.levelOf(uJ) > Lj)
2233 probeCfTangential(uJ, axis); // a sample record with C == uJ, F == j (case 2)
2234 });
2235 });
2236 }
2237
2240 const Index ng = dhalo_.numGhosts();
2241 std::vector<std::array<long, 3>> glo(static_cast<std::size_t>(ng));
2242 std::vector<unsigned> glv(static_cast<std::size_t>(ng));
2243 for (Index g = 0; g < ng; ++g) {
2244 for (int a = 0; a < 3; ++a)
2245 glo[static_cast<std::size_t>(g)][a] =
2246 static_cast<long>(dhalo_.ghostCoord(g)[a]) - shiftD_[a];
2247 glv[static_cast<std::size_t>(g)] = static_cast<unsigned>(dhalo_.level(dhalo_.numLocal() + g));
2248 }
2249 mom_.setGhosts(glo, glv); // copies — pres_ takes the originals
2250 pres_.setGhosts(std::move(glo), std::move(glv));
2251 }
2252
2253 // (public like runMgVcycle: nvcc rejects extended device lambdas in private member functions)
2254 // Project onto the coupled subspace: pin decoupled rows (solid-centered + no-phi-coupling
2255 // overlay rows) to 0 and remove the volume-weighted mean over the coupled cells (the constant
2256 // null mode of the connected fluid region) — removeMeanVol with the coupled mask (the mean
2257 // reduced globally in distributed mode; allred_ is empty single-rank ⇒ bit-identical).
2258 void gpProject(View<double> v) { removeMeanVolReduced(v, gpOp0().invVol, maskC_, n_, allred_); }
2259
2260 // Nonsymmetric ghost pressure matvec: y = P[rho·(L_bin x + Delta x)]. The caller keeps x's
2261 // ghost tail current (syncScalar before every call in distributed mode).
2263 auto spT = spMark();
2264 applyFv(gpOp0(), x, y);
2266 spT = spMark();
2267 ghostApplyDelta(gpOv_, x, y);
2268 ghostApplyDeltaSampled(gpOvS_, x, y);
2270 spT = spMark();
2271 gpProject(y);
2273 }
2274
2275 // Preconditioner: two binary-openness V-cycles (the unchanged MG hierarchy) + projection.
2277 const auto spT = spMark();
2278 if (dist_) {
2279 Kokkos::deep_copy(presMGD_.b(0), r);
2280 Kokkos::deep_copy(presMGD_.x(0), 0.0);
2281 presMGD_.vcycle(2, 2, 60, 0.8);
2282 presMGD_.vcycle(2, 2, 60, 0.8);
2283 Kokkos::deep_copy(z, presMGD_.x(0));
2284 } else {
2285 Kokkos::deep_copy(presMG_.b(0), r);
2286 Kokkos::deep_copy(presMG_.x(0), 0.0);
2287 presMG_.vcycle(2, 2, 60, 0.8);
2288 presMG_.vcycle(2, 2, 60, 0.8);
2289 Kokkos::deep_copy(z, presMG_.x(0));
2290 }
2291 gpProject(z);
2293 }
2294
2295 // MG-preconditioned BiCGStab on the ghost pressure operator (device mirror of the oracle's
2296 // solveGhostBiCGStab: same projection, same stagnation guard against the small
2297 // attainable-residual floor of the slightly incompatible ghost system). Returns iterations.
2299 const Index n = n_;
2300 // Dots are locally summed then globally reduced (allSum = identity single-rank); every
2301 // matvec input's ghost tail is refreshed first. Stagnation/early-break branches depend on
2302 // reduced scalars only ⇒ every rank takes the same branch.
2303 syncScalar(x);
2305 {
2306 auto r = gpr_;
2307 auto bb = b;
2308 Kokkos::parallel_for("amr::gp_r0", n, KOKKOS_LAMBDA(const Index i) { r(i) = bb(i) - r(i); });
2309 }
2310 gpProject(gpr_);
2311 Kokkos::deep_copy(gprh_, gpr_);
2312 const double res0 =
2313 std::sqrt(allSum(dotPlain(View<const double>(gpr_), View<const double>(gpr_), n)));
2314 if (res0 == 0.0)
2315 return 0;
2316 double rho = 1, alpha = 1, omega = 1, best = res0;
2317 int noImprove = 0;
2318 Kokkos::deep_copy(gpv_, 0.0);
2319 Kokkos::deep_copy(gpp_, 0.0);
2320 int it = 0;
2321 for (; it < maxIters; ++it) {
2322 const double rhoNew =
2323 allSum(dotPlain(View<const double>(gprh_), View<const double>(gpr_), n));
2324 if (rhoNew == 0.0)
2325 break;
2326 const double beta = (rhoNew / rho) * (alpha / omega);
2327 bicgPUpdate(gpp_, View<const double>(gpr_), View<const double>(gpv_), beta, omega, n);
2328 ghostPrec(View<const double>(gpp_), gpph_);
2329 syncScalar(gpph_);
2330 ghostMatvec(View<const double>(gpph_), gpv_);
2331 const double rhatV = allSum(dotPlain(View<const double>(gprh_), View<const double>(gpv_), n));
2332 if (rhatV == 0.0)
2333 break;
2334 alpha = rhoNew / rhatV;
2335 Kokkos::deep_copy(gps_, gpr_);
2336 axpy(gps_, -alpha, View<const double>(gpv_), n);
2337 const double snorm =
2338 std::sqrt(allSum(dotPlain(View<const double>(gps_), View<const double>(gps_), n)));
2339 if (snorm <= tol * res0) {
2340 axpy(x, alpha, View<const double>(gpph_), n);
2341 ++it;
2342 break;
2343 }
2344 ghostPrec(View<const double>(gps_), gpsh_);
2345 syncScalar(gpsh_);
2346 ghostMatvec(View<const double>(gpsh_), gpt_);
2347 const double tt = allSum(dotPlain(View<const double>(gpt_), View<const double>(gpt_), n));
2348 omega = (tt != 0.0)
2349 ? allSum(dotPlain(View<const double>(gpt_), View<const double>(gps_), n)) / tt
2350 : 0.0;
2351 axpy(x, alpha, View<const double>(gpph_), n);
2352 axpy(x, omega, View<const double>(gpsh_), n);
2353 Kokkos::deep_copy(gpr_, gps_);
2354 axpy(gpr_, -omega, View<const double>(gpt_), n);
2355 const double rnorm =
2356 std::sqrt(allSum(dotPlain(View<const double>(gpr_), View<const double>(gpr_), n)));
2357 if (rnorm <= tol * res0) {
2358 ++it;
2359 break;
2360 }
2361 if (rnorm < 0.999 * best) {
2362 best = rnorm;
2363 noImprove = 0;
2364 } else if (++noImprove >= 6) {
2365 ++it;
2366 break; // attainable-residual floor (compatibility gap) — stagnation guard
2367 }
2368 rho = rhoNew;
2369 if (omega == 0.0)
2370 break;
2371 }
2372 gpProject(x);
2373 return it;
2374 }
2375
2376 private:
2378 void uploadSeamRecon(const SeamReconHost& h) {
2379 geom_.seam = toDevice(h.seam, "seam_desc");
2380 geom_.sampI = toDevice(h.sampI, "seam_sampi");
2381 geom_.sampJ = toDevice(h.sampJ, "seam_sampj");
2382 geom_.uuRecI = toDevice(h.uuRecI, "seam_uureci");
2383 geom_.uuRecJ = toDevice(h.uuRecJ, "seam_uurecj");
2384 geom_.d1I = toDevice(h.d1I, "seam_d1i");
2385 geom_.d1J = toDevice(h.d1J, "seam_d1j");
2386 geom_.recStart = toDevice(h.recStart, "seam_recstart");
2387 geom_.recDist = toDevice(h.recDist, "seam_recdist");
2388 geom_.recCell = toDevice(h.recCell, "seam_reccell");
2389 geom_.recW = toDevice(h.recW, "seam_recw");
2390 seamSampleRecords_ = h.numSampleRecords;
2391 seamLayerRecords_ = h.numLayerRecords;
2392 }
2394 void clearSeamRecon() {
2395 geom_.seam = View<Index>{};
2396 geom_.sampI = View<Index>{};
2397 geom_.sampJ = View<Index>{};
2398 geom_.uuRecI = View<Index>{};
2399 geom_.uuRecJ = View<Index>{};
2400 geom_.d1I = View<double>{};
2401 geom_.d1J = View<double>{};
2402 geom_.recStart = View<Index>{};
2403 geom_.recDist = View<double>{};
2404 geom_.recCell = View<Index>{};
2405 geom_.recW = View<double>{};
2406 seamSampleRecords_ = 0;
2407 seamLayerRecords_ = 0;
2408 }
2409
2410 // Host build of the ghost-gradient overlay (setGhostGradient): one row per cut cell (fluid
2411 // with a solid face neighbour — the cells where the ABC grad3 is gauge-dependent O(1/h)),
2412 // holding a 3-point directional FD stencil per axis. Mirrors oracle::AmrFlow::gradOfDir: cut
2413 // cells have same-level face neighbours by the finest-band contract; the ±2 probe falls back
2414 // to the 2-point one-sided closure when that cell is solid or not same-level.
2415 void buildGhostGradOverlay() {
2416 const Index n = t_->numLeaves();
2417 std::vector<Index> cells;
2418 for (Index i = 0; i < n; ++i)
2419 if (mom_.isCut(i))
2420 cells.push_back(i);
2421 const Index m = static_cast<Index>(cells.size());
2422 std::vector<Index> idx(static_cast<std::size_t>(m) * 9, 0);
2423 std::vector<double> w(static_cast<std::size_t>(m) * 9, 0.0);
2424 // Pocket cells (fragmentation guard) count as solid for the directional gradient: their φ is
2425 // pinned/decoupled, and reading the pinned 0 is the gauge-dependent defect the ghost
2426 // gradient exists to avoid. Levels via pres_.levelOf: ghost-slot-safe in distributed
2427 // builds (identical to t_->level for local leaves; the pocket guard is single-rank-only —
2428 // gpPocket_ stays empty distributed until the label-propagation guard lands).
2429 auto ok = [&](Index j, Index i) {
2430 return j >= 0 && mom_.isFluid(j) && pres_.levelOf(j) == pres_.levelOf(i) &&
2431 !(!gpPocket_.empty() && j < n && gpPocket_[static_cast<std::size_t>(j)]);
2432 };
2433 for (Index s = 0; s < m; ++s) {
2434 const Index i = cells[static_cast<std::size_t>(s)];
2435 const double h = pres_.cellWidth(i);
2436 for (int a = 0; a < 3; ++a) {
2437 const std::size_t o = static_cast<std::size_t>(s) * 9 + static_cast<std::size_t>(a) * 3;
2438 for (int k = 0; k < 3; ++k)
2439 idx[o + static_cast<std::size_t>(k)] = i; // safe defaults (w = 0)
2440 const Index jp = pres_.periodicNeighbor(i, a, +1);
2441 const Index jm = pres_.periodicNeighbor(i, a, -1);
2442 const bool ap = ok(jp, i), am = ok(jm, i);
2443 if (am && ap) {
2444 idx[o] = jp;
2445 w[o] = 0.5 / h;
2446 idx[o + 1] = jm;
2447 w[o + 1] = -0.5 / h;
2448 } else if (ap) {
2449 const Index jpp = pres_.periodicNeighbor(jp, a, +1);
2450 if (ok(jpp, i)) {
2451 idx[o] = i;
2452 w[o] = -1.5 / h;
2453 idx[o + 1] = jp;
2454 w[o + 1] = 2.0 / h;
2455 idx[o + 2] = jpp;
2456 w[o + 2] = -0.5 / h;
2457 } else {
2458 idx[o] = jp;
2459 w[o] = 1.0 / h;
2460 idx[o + 1] = i;
2461 w[o + 1] = -1.0 / h;
2462 }
2463 } else if (am) {
2464 const Index jmm = pres_.periodicNeighbor(jm, a, -1);
2465 if (ok(jmm, i)) {
2466 idx[o] = i;
2467 w[o] = 1.5 / h;
2468 idx[o + 1] = jm;
2469 w[o + 1] = -2.0 / h;
2470 idx[o + 2] = jmm;
2471 w[o + 2] = 0.5 / h;
2472 } else {
2473 idx[o] = i;
2474 w[o] = 1.0 / h;
2475 idx[o + 1] = jm;
2476 w[o + 1] = -1.0 / h;
2477 }
2478 } // sandwiched: all weights stay 0
2479 }
2480 }
2481 gc_.n = m;
2482 gc_.cell = toDevice(cells, "gc_cell");
2483 gc_.idx = toDevice(idx, "gc_idx");
2484 gc_.w = toDevice(w, "gc_w");
2485 }
2486
2487 // Fluid area fraction of a face. DEFAULT (order 2, 2026-08-26 user decision): triangle-fan
2488 // marching squares on FIVE ANALYTIC samples (4 corners + centre) -- exact linear fraction per
2489 // triangle, O(h^2), no saddle ambiguity, and the sub-resolution floor 1e-6 (alpha ~ 1e-12 rows
2490 // destroy the pressure conditioning -- flow tracker rows 51/52). Because the AMR solver holds
2491 // the analytic sdfFn, this samples the TRUE geometry (no trilinear ceiling). order 1 = the
2492 // legacy one-sample gradient-normalised linear model (kept for A/B), which carries a SIGNED
2493 // convexity bias measured at +0.59%/+0.27% bed permeability at R=8/12 (decay ~h^2).
2494 static double triFrac(double a, double b, double c) {
2495 const bool pa = a >= 0.0, pb = b >= 0.0, pc = c >= 0.0;
2496 const int np = (pa ? 1 : 0) + (pb ? 1 : 0) + (pc ? 1 : 0);
2497 if (np == 3)
2498 return 1.0;
2499 if (np == 0)
2500 return 0.0;
2501 double x, y, z;
2502 if (np == 1) {
2503 if (pa) {
2504 x = a;
2505 y = b;
2506 z = c;
2507 } else if (pb) {
2508 x = b;
2509 y = c;
2510 z = a;
2511 } else {
2512 x = c;
2513 y = a;
2514 z = b;
2515 }
2516 const double den = (x - y) * (x - z);
2517 return den > 1e-300 ? (x * x) / den : 1.0;
2518 }
2519 if (!pa) {
2520 x = a;
2521 y = b;
2522 z = c;
2523 } else if (!pb) {
2524 x = b;
2525 y = c;
2526 z = a;
2527 } else {
2528 x = c;
2529 y = a;
2530 z = b;
2531 }
2532 const double den = (x - y) * (x - z);
2533 return 1.0 - (den > 1e-300 ? (x * x) / den : 1.0);
2534 }
2535 template <class SdfFn>
2536 double faceFrac(SdfFn&& sdfFn, const Vec<3>& fc, int axis) const {
2537 if (apertureOrder_ >= 2) {
2538 if (sdfFn(fc) <= 0.0)
2539 return 0.0; // center gate (kept from order 1; see flow ccFaceOpenMS)
2540 const int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
2541 // Phase 3: the four corner probes sit half a cell out on EACH tangential axis.
2542 const double e1 = 0.5 * h0_[t1], e2 = 0.5 * h0_[t2];
2543 auto at = [&](double d1, double d2) {
2544 Vec<3> p = fc;
2545 p[t1] += d1;
2546 p[t2] += d2;
2547 return sdfFn(p);
2548 };
2549 const double c00 = at(-e1, -e2), c10 = at(e1, -e2), c11 = at(e1, e2), c01 = at(-e1, e2);
2550 const double cc = sdfFn(fc);
2551 const double frac = 0.25 * (triFrac(c00, c10, cc) + triFrac(c10, c11, cc) +
2552 triFrac(c11, c01, cc) + triFrac(c01, c00, cc));
2553 return frac < 1e-3 ? 0.0 : (frac > 1.0 - 1e-12 ? 1.0 : frac); // floor: see flow ccFaceOpenMS
2554 }
2555 double sd = sdfFn(fc);
2556 if (sd <= 0.0)
2557 return 0.0;
2558 Vec<3> g{};
2559 for (int d = 0; d < 3; ++d) {
2560 Vec<3> pp = fc, pm = fc;
2561 pp[d] += h0_[d];
2562 pm[d] -= h0_[d];
2563 g[d] = (sdfFn(pp) - sdfFn(pm)) / (2.0 * h0_[d]);
2564 }
2565 double gmag = std::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
2566 if (gmag < 1e-6)
2567 gmag = 1e-6;
2568 int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
2569 // Phase 3: the projected in-plane extent of the face, per tangential axis.
2570 double denom = (std::fabs(g[t1]) * h0_[t1] + std::fabs(g[t2]) * h0_[t2]) / gmag;
2571 if (denom < 1e-9)
2572 denom = 1e-9;
2573 double frac = 0.5 + sd / denom;
2574 return frac < 0.0 ? 0.0 : (frac > 1.0 ? 1.0 : frac);
2575 }
2576
2577 const Octree* t_ = nullptr;
2578 Vec<3> h0_ = detail::filledVec<3>(1.0);
2579 Vec<3> origin_{};
2580 double rho_ = 1.0, mu_ = 1.0, dt_ = 1e6;
2581 Vec<3> f_{};
2582 bool presPCG_ = true;
2583 bool presDbgSpdDone_ = false; // one-shot debug SPD probe (PECLET_AMR_PRES_DEBUG)
2584 bool momMGon_ = true; // velocity-MG momentum preconditioner (scalable; see setMomentumMG)
2585 bool useStaircaseMG_ = false; // false = Galerkin (MomentumMG), true = staircase (VelocityMG)
2586 int mgVcPre_ = 2, mgVcBottom_ = 30; // momentum-MG V-cycle pre/post sweeps + bottom sweeps
2587 Index mgMinCoarse_ = 256; // staircase velocity-MG pore-scale cap (coarsest cell count)
2588 bool momGS_ = false; // opt-in: multicolour Gauss–Seidel smoother in the momentum MG
2589 bool momMGSolver_ =
2590 false; // opt-in (P4): velocity-MG as the solver (defect correction), not BiCGStab
2591 bool ghostGrad_ = true; // gauge-exact directional cell gradient (setGhostGradient) — the
2592 // DEFAULT since 2026-08-18, mirroring flow's collocated
2593 // set_collocated_scheme("gauge-exact")
2594 bool ghostProj_ = false; // RESOLVED projection mode (set by setSolid from the request)
2595 int8_t ghostProjReq_ = -1; // -1 = AUTO (DEFAULT since 2026-08-25: ghost, aperture fallback
2596 // on thin band), 0 = explicit aperture, 1 = explicit ghost
2597 int apertureOrder_ = 2; // aperture estimator order (setApertureOrder; default 2)
2598 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure orders (2,2 = the production pair; the
2599 // (1,2) mixed form is march-unstable at scale)
2600 // DEFAULT since 2026-09-21 (user decision): the Martin-Cartwright tangential quadratic, NOT the
2601 // standard two-point flux. The register had already settled that "cf=1 is not optional on graded
2602 // meshes" and rejected cf=0 there, but the shipped default stayed standard, so a user who built a
2603 // graded mesh silently got the configuration the project had decided against. Measured on a
2604 // self-similar ladder (docs/amr_graded_convergence.md §3), as orders:
2605 //
2606 // standard quadratic
2607 // interface NORMAL to the variation 2.00 2.00 (inert: no tangential variation to
2608 // interface TANGENTIAL 0.41 1.60 correct, so the delta is empty)
2609 //
2610 // with the absolute gap on the tangential arm widening 3.8x at n=32 to 8.8x at n=64. INERT BY
2611 // GEOMETRY on any uniform or finest-band mesh: no C/F faces means an empty delta CSR and the
2612 // identical code path, so only graded runs move.
2613 CfScheme cfScheme_ = CfScheme::quadratic; // 2:1 C/F interface scheme (setCfScheme)
2614 int outerIters_ = 1; // Picard outer iterations over the lagged advection (default 1)
2615 double outerTol_ = 1e-6; // outer-loop early-stop tolerance on max|Δu|
2616 double momTol_ = 1e-8; // per-step momentum BiCGStab relative tolerance (Phase-0 knob)
2617 double presTol_ = 1e-10; // pressure solve relative tolerance (MG-PCG and ghost BiCGStab)
2618 bool advect_ = false; // momentum advection ∇·(u u) (off ⇒ Stokes)
2619 bool implicitFou_ = true; // implicit-FOU deferred-correction (stable) vs fully-explicit
2620 int advScheme_ = 0; // high-order flux: 0 = SOU (default), 1 = Koren TVD
2621 Index n_ = 0;
2622 bool ready_ = false; // an operator exists (setSolid ran; cleared by beginAdapt)
2623 int lastMomIters_ = 0, lastPresIters_ = 0, lastOuterIters_ = 1;
2624 // M0 step profiler (PECLET_AMR_PROFILE_STEP): all inert unless stepProf_.
2625 bool stepProf_ = amrEnvFlag("PECLET_AMR_PROFILE_STEP");
2626 bool spHeader_ = false;
2627 int spWindow_ = [] {
2628 const char* e = std::getenv("PECLET_AMR_PROFILE_STEP_WINDOW");
2629 const int v = e ? std::atoi(e) : 0;
2630 return v > 0 ? v : 50;
2631 }();
2632 int spSteps_ = 0;
2633 double spAcc_[SP_N] = {};
2634 double spMom_ = 0.0, spPres_ = 0.0, spOuter_ = 0.0;
2635
2636 AmrCutCell<Bits> mom_;
2637 AmrPoisson<3, Bits> pres_;
2638 Multigrid<3, Bits> presMG_;
2642 Index presBottomExtent_ = 4;
2643 MomentumMG<Bits> momMG_; // Galerkin velocity multigrid (momentum preconditioner)
2644 VelocityMG<Bits> velMG_; // rediscretized staircase velocity multigrid (alternative)
2645 MomentumOp momOp_;
2646 MomentumSolver<Bits> momSolver_;
2647 PCG<3, Bits> pcg_;
2648 std::array<View<double>, 3> defc_; // explicit ρ(SOU−FOU) deferred correction per component
2649 View<double> advDiag_, advCoef_; // device-resident implicit-FOU operator (rebuilt each step)
2650 FaceGeom geom_;
2651 GhostGradOverlay gc_; // directional ghost-gradient overlay (empty unless setGhostGradient)
2652 GhostOverlayDev gpOv_; // closure overlay (empty unless setGhostProjection)
2653 bool ghostSampled_ = false; // RESOLVED sampled mode (set by setSolid)
2654 int8_t ghostSampledReq_ = 0; // setGhostSampled request (mixed-level cut band)
2655 double gpsRho_ = 2.2; // sampled band: LS cloud radius factor (setGhostSampled)
2656 long gpsMaxN_ = 0; // sampled band: nearest-N candidate cap, 0 = uncapped
2657 GhostOverlaySampledDev gpOvS_; // sample-slot overlay (empty unless sampled)
2658 GhostGradCsrDev gcS_; // sampled CSR directional-gradient overlay
2659 CfCsrDev gpsMomDelta_; // momentum ξ-row seam correction (rscale-folded)
2660 std::vector<char> gpPocket_; // fragmentation guard: 1 = decoupled pocket cell (ghost mode)
2661 CfCsrDev cfMom_; // +μ(∇²_scheme − ∇²_std) momentum RHS overlay
2662 CfCompCsrDev cfDiv_; // (D_scheme − D_std) divergence overlay
2663 std::array<CfCsrDev, 3> cfGrad_; // (G_scheme − G_std) per gradient axis
2664 CfCompCsrDev cfUfVel_; // (uf_scheme − uf_std) face-field overlay: velocity part
2665 Index cfGhostCols_ = 0; // overlay entries reading a ghost slot (diagnostic)
2666 Index cfCutFaces_ = 0; // C/F slots where the face gate withholds the quadratic
2667 bool seamRecon_ = true; // seam reconstruction of the advected value (§5.5, default on)
2668 Index seamSampleRecords_ = 0; // seam tables: sample records built (docs/amr_cf_convective.md)
2669 Index seamLayerRecords_ = 0; // seam tables: face-layer records built
2670 View<double> maskC_; // 1 = coupled row (Krylov subspace), 0 = pinned
2671 View<double> gpr_, gprh_, gpp_, gpph_, gpv_, gps_, gpsh_, gpt_; // ghost BiCGStab scratch
2672 View<double> rscale_;
2673 View<char> fluid_;
2674 std::array<View<double>, 3> u_, gx_;
2675 std::array<View<double>, 3> u0_,
2676 uprev_; // frozen uⁿ (BE mass term) + previous Picard outer iterate
2677 View<double> p_, phi_, div_, bmom_;
2678 View<double> uf_; // ABC/Basilisk divergence-free face field (one per CSR (sub)face)
2679 bool faceFieldBuilt_ =
2680 false; // uf_ populated by a projection (else advection falls back to ½(u_i+u_j))
2681 bool ufAdvect_ = true; // ABLATION (diagnostics): advect with uf_ (ON, the shipped scheme) or
2682 // with the un-projected ½(u_i+u_j) — see setUfAdvection
2683 std::unique_ptr<Octree> adaptOldT_; // beginAdapt topology snapshot
2684 std::array<std::vector<double>, 3> adaptU_; // beginAdapt field snapshots
2685 std::vector<double> adaptP_;
2686 std::array<std::vector<std::array<double, 3>>, 3> adaptGradU_; // distributed transfer grads
2687 std::vector<std::array<double, 3>> adaptGradP_;
2688
2689 // ---- distributed context (initMpi; all null/empty single-rank) -----------------------------
2690 DistributedOctree<3, Bits>* dist_ = nullptr; // the ORB block + communicator
2691 LeafHalo<3, Bits> dhalo_; // the flow's ±2 ghost registry (frozen in setSolid)
2692 LeafHaloExchange dhex_; // device value refresh over dhalo_
2693 DistributedFlowMultigrid<3, Bits> presMGD_; // distributed pressure MG (level 0 on dhalo_)
2694 std::array<long, 3> shiftD_{}; // block global fine origin
2695 Index nExt_ = 0; // n_ + ghosts (== n_ single-rank)
2696 std::function<double(double)> allred_; // Allreduce hook (empty single-rank)
2697};
2698
2699} // namespace peclet::amr
2700
2701#endif // PECLET_AMR_FLOW_HPP
void setImplicitAdvection(bool on)
Implicit-FOU deferred correction (default ON).
Definition flow.hpp:641
Index numSeamSampleRecords() const
Seam-reconstruction census (docs/amr_cf_convective.md §5.2): how many sample records (one per 2:1 sub...
Definition flow.hpp:1973
void setApertureOrder(int order)
FULL directional ghost-cell projection (the AMR port of flow's collocated set_ghost_projection): the ...
Definition flow.hpp:596
void setMomentumMG(bool on)
Use the Galerkin velocity multigrid (MomentumMG) as the momentum BiCGStab preconditioner.
Definition flow.hpp:686
std::vector< double > velocity(int c) const
Copy a velocity component back to host (single D2H, no host loop — S2a).
Definition flow.hpp:1762
void finishAdapt(SdfFn &&sdfFn)
Rebuild on the mutated octree and transfer the snapshotted fields onto it.
Definition flow.hpp:1631
void setGhostGradient(bool on)
Directional ghost cell-gradient for the −∇pⁿ predictor and the projection's cell correction (the AMR ...
Definition flow.hpp:566
void ghostPrec(View< const double > r, View< double > z)
Definition flow.hpp:2276
void step(int momIters=100, int presIters=60)
Definition flow.hpp:1282
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:533
void gpProject(View< double > v)
Definition flow.hpp:2258
void prepareDistributed(SdfFn &&sdfFn)
Install the resolver seams, run every prober to the miss-collect fixpoint, freeze the ±2 halo,...
Definition flow.hpp:2038
void setBodyForce(double fx, double fy, double fz)
Definition flow.hpp:549
void setPressurePCG(bool on)
Use MG-preconditioned CG for the pressure solve (default) vs plain V-cycles.
Definition flow.hpp:551
void syncVel()
Refresh the ghost tails of the three velocity components (one batched message round).
Definition flow.hpp:2003
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:1781
std::vector< double > pressure() const
Copy the pressure field back to host (single D2H), (num_leaves,) — the incremental-rotational p.
Definition flow.hpp:1765
void setPressureTol(double tol)
Relative tolerance of the PRESSURE solve, for both drivers: the MG-PCG (the default) and the ghost pr...
Definition flow.hpp:660
void setVelocityMGMinCoarse(Index m)
Pore-scale cap for the staircase velocity-MG: the coarsest level keeps ≥ this many cells,...
Definition flow.hpp:697
void setVelocity(int c, const std::vector< double > &h)
Set a velocity component from host (testing / initial conditions).
Definition flow.hpp:1754
void setDt(double dt)
Definition flow.hpp:548
bool isFluid(Index i) const
Per-leaf fluid mask (false inside the solid) — for host-side post-processing / bindings.
Definition flow.hpp:1992
double divNormL2()
L2 norm of the (openness-weighted) divergence of the current velocity — the ghost-closed divergence w...
Definition flow.hpp:1795
std::array< long, 3 > globalFineExtent() const
The DOMAIN's fine extent (D1): the block's own single-rank, where block == domain.
Definition flow.hpp:2023
void copyToHost(const View< double > &d, std::vector< double > &h) const
Copy a device View into a host vector (sized n_).
Definition flow.hpp:1747
void setAdvectionScheme(int s)
High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.
Definition flow.hpp:643
int solveGhostBiCGStab(View< double > x, View< const double > b, int maxIters, double tol=1e-10)
Definition flow.hpp:2298
void init(const Octree &t, const Vec< 3 > &h0, Vec< 3 > origin=Vec< 3 >{})
Definition flow.hpp:518
void installGhostMeta()
Mirror the halo registry's ghost metadata (block-local lo + level) into mom_ and pres_.
Definition flow.hpp:2239
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:1698
void setViscosity(double mu)
Definition flow.hpp:547
Index numGhostCells() const
Distributed: number of ghost slots in the ±2 registry (0 single-rank).
Definition flow.hpp:1979
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:1715
Index numCfGhostColumns() const
Developer diagnostic: entries of the four C/F-scheme overlays that read a GHOST slot (0 single-rank,...
Definition flow.hpp:1983
void finishProjection(Index n)
Shared projection tail: build the div-free face field from u* + φ, correct the cell velocities (ABC /...
Definition flow.hpp:1552
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
Definition flow.hpp:597
void probeCfScheme()
Discovery arm for the C/F quadratic scheme (cf_scheme.hpp), run inside prepareDistributed's miss-coll...
Definition flow.hpp:2151
Index pressureBottomExtent() const
The bottom extent the next setSolid builds the ladder with (default 4).
Definition flow.hpp:1900
Index numSeamLayerRecords() const
The face-layer half of the census: one record per (regular coarse cell, face) whose far side is refin...
Definition flow.hpp:1976
int lastOuterIters() const
Picard outer iterations actually run in the last step (1 unless setOuterIterations(>1)).
Definition flow.hpp:1998
void setDensity(double rho)
Definition flow.hpp:546
Index numCfCutFaces() const
The C/F census (docs/amr_cf_flux_gate.md §6.5): owned forEachFaceFull slots on a 2:1 sub-face between...
Definition flow.hpp:1990
double divNormFace()
L2 norm of the APERTURE-weighted divergence of the ABC face field uf_ (built each project()).
Definition flow.hpp:1824
void ghostMatvec(View< const double > x, View< double > y)
Definition flow.hpp:2262
void spAdd(int ph, SpClock::time_point t0)
Definition flow.hpp:1218
void setPressureBottom(const std::string &kind)
What solves the coarsest pressure level (docs/amr_mg_depth.md §6.6/§11.6): "auto" (the default — the ...
Definition flow.hpp:1868
std::string pressureMgBottom() const
What actually solves the coarsest pressure level (docs/amr_mg_depth.md §6.6/§6.7): "jacobi" (60 dampe...
Definition flow.hpp:1852
void project(int presIters=60)
Pressure projection of the current velocity in place.
Definition flow.hpp:1406
void syncScalar(View< double > v)
Refresh the ghost tail of one cell scalar (p, φ, Krylov scratch).
Definition flow.hpp:2008
std::vector< double > faceField() const
Copy the divergence-free face field to host (one value per CSR (sub)face, forEachFaceFull order).
Definition flow.hpp:1904
void setMomentumTol(double tol)
Relative tolerance for the per-step momentum BiCGStab solve (default 1e-8).
Definition flow.hpp:653
void setSeamReconstruction(bool on)
ABLATION (docs/amr_cf_convective.md §5.5, Q-C).
Definition flow.hpp:678
void setPressureBottomExtent(Index e)
Where the pressure ladder stops lifting the root and hands over to the bottom (docs/amr_mg_depth....
Definition flow.hpp:1894
Index numLeaves() const
Definition flow.hpp:1977
void setUfAdvection(bool on)
ABLATION (developer tier).
Definition flow.hpp:669
void setPressure(const std::vector< double > &h)
Write the accumulated rotational pressure from host (restart / finishAdapt).
Definition flow.hpp:1679
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:1768
const FvOp & gpOp0()
The pressure operator the ghost solver runs on (distributed MG level 0 or presMG_'s).
Definition flow.hpp:2032
std::chrono::steady_clock SpClock
Definition flow.hpp:1211
void probeCfTangential(Index coarse, int axis)
Every coordinate cfAppendStencil can probe from the coarse cell of one C/F face: the tangential ± nei...
Definition flow.hpp:2167
void setVelocityMGStaircase(bool on)
Choose the momentum-MG coarse-operator strategy: false (default) = Galerkin (MomentumMG,...
Definition flow.hpp:692
BlockOctree< 3, Bits > Octree
Definition flow.hpp:512
void zeroSolid(View< double > v)
Zero a per-leaf field on non-fluid cells (the transferred fields' solid cleanup).
Definition flow.hpp:1687
void spEndStep()
Close a profiled step; print + reset when the window fills.
Definition flow.hpp:1225
int lastPresIters() const
Pressure PCG iterations of the last step.
Definition flow.hpp:1996
void requireOperator(const char *what) const
Every solve entry point needs the cut-cell operators that setSolid builds; without them the state vie...
Definition flow.hpp:1274
void rebalanceMpi(SdfFn &&sdfFn)
Distributed load rebalance (docs/amr_distributed_flow.md, rung 6): re-decompose the octree by leaf co...
Definition flow.hpp:1661
void probeSeamLayer()
Discovery arm for the SEAM builder (seam_recon.hpp), run beside probeCfScheme() inside prepareDistrib...
Definition flow.hpp:2205
StepPhase
One incompressible step on device (Stokes, or Navier–Stokes with setAdvection).
Definition flow.hpp:1194
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:1732
std::vector< Index > pressureMgLevels() const
The pressure multigrid's ladder as BUILT on this rank: per-level leaf counts, level 0 first (docs/amr...
Definition flow.hpp:1834
int lastMomIters() const
Total momentum BiCGStab iterations (summed over the 3 components) of the last step.
Definition flow.hpp:1994
void beginAdapt()
Snapshot the octree topology + (u, p) ahead of an external mesh mutation.
Definition flow.hpp:1611
void setGhostSampled(bool on, double rho=2.2, long maxSamples=0)
SAMPLED ghost projection: the mixed-level cut-band mode (docs/amr_mixed_level_cut_band_plan....
Definition flow.hpp:613
Vec< 3 > betaPerAxis() const
The viscous coefficient of each derivative axis, mu/h0_a^2 — the octree twin of the structured solver...
Definition flow.hpp:540
void setMomentumGS(bool on)
Opt-in: use the multicolour Gauss–Seidel smoother in the momentum MG (Galerkin or staircase) instead ...
Definition flow.hpp:701
void setMomentumMGSolver(bool on)
Opt-in (P4): solve the momentum predictor with the velocity multigrid used as the solver — MG-precond...
Definition flow.hpp:715
std::vector< double > slotCenters() const
World centre of every slot of the extended (local + ghost) leaf array, row-major (nExt,...
Definition flow.hpp:1959
double allSum(double s) const
Global sum (identity single-rank).
Definition flow.hpp:2013
void setAdvection(bool on)
Enable momentum advection ∇·(u u) (default OFF ⇒ Stokes).
Definition flow.hpp:638
void setCfScheme(int scheme)
Coarse/fine (2:1) interface scheme (cf_scheme.hpp): 1 = Martin–Cartwright tangential quadratic,...
Definition flow.hpp:631
void setSolid(SdfFn &&sdfFn)
Build the cut-cell operators (host) + upload all device structures.
Definition flow.hpp:730
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:722
FaceTopologyHost faceTopology() const
Definition flow.hpp:1930
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
Cubic-cell overload — the pre-Phase-3 spelling (docs/amr_anisotropic.md §2).
Definition flow.hpp:515
SpClock::time_point spMark() const
Definition flow.hpp:1212
Cell-centered FV Poisson operator on one (periodic) block octree.
Definition poisson.hpp:43
unsigned levelOf(Index slot) const
Octree level of an extended slot (local leaf or declared ghost).
Definition poisson.hpp:98
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:167
Real cellWidth(Index i, int axis) const
Cell width along axis (Phase 3: per axis).
Definition poisson.hpp:255
void setFrameShift(const std::array< long, Dim > &s)
Distributed frame shift: this block's global fine origin.
Definition poisson.hpp:96
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:329
const std::array< Coord, Dim > & fineExt() const
Per-axis fine-grid extent (brick·2^lmax) — the periodic wrap modulus.
Definition poisson.hpp:592
std::pair< Index, unsigned > probeSlot(const std::array< long, Dim > &p) const
Covering slot + level of a block-local probe (longs, possibly outside the block).
Definition poisson.hpp:120
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:375
void setGhosts(std::vector< std::array< long, Dim > > lo, std::vector< unsigned > lv)
Declare the ghost slots [n, n+nGhost): block-local lo corner (longs — ghosts lie outside the block) a...
Definition poisson.hpp:86
const Octree & octree() const
Definition poisson.hpp:586
void setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:71
void init(const Octree &t, Real h0)
Cubic-cell overload — the pre-Phase-3 spelling, and exactly the per-axis one with all three spacings ...
Definition poisson.hpp:56
void setResolver(ExtResolver r)
Definition poisson.hpp:81
std::array< long, Dim > loOf(Index slot) const
Block-local lo corner of an extended slot as longs (ghosts may lie outside the block).
Definition poisson.hpp:103
Real cellVolume(Index i) const
Definition poisson.hpp:262
Per-block adaptive octree over block-local Morton codes.
const Vec< Dim > & h0() const
Phase 3: the FINEST spacing per axis (docs/amr_anisotropic.md §2).
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()).
Bottom
What solves the coarsest level (docs/amr_mg_depth.md §6.6).
FaceGeom buildFaceGeom(const AmrPoisson< Dim, Bits > &ap, FluidFn &&isFluid)
Build FaceGeom from a built AmrPoisson (openness set) + a fluid predicate.
Definition flow.hpp:71
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:126
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:488
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:80
Index countCfCutFaces(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk)
The C/F CENSUS (docs/amr_cf_flux_gate.md §6.5): how many OWNED forEachFaceFull slots (i,...
auto makeBinaryOpenFnMixed(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn sdfFn, const Vec< 3 > &h0, Vec< 3 > origin, std::array< long, 3 > frameShift={})
The canonical (sub)face openness for mixed-level cut bands: open iff both adjacent ACTUAL leaf center...
CfCsrDev uploadCfCsr(const CfCsr &h, const char *name)
GhostOverlaySampled buildGhostOverlaySampled(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn &&sdf, int matrixOrder, int rhsOrder, Vec< 3 > origin=Vec< 3 >{}, const std::array< long, 3 > *globalFine=nullptr, std::array< long, 3 > frameShift={}, bool discovery=false, double rhoFactor=2.2, long maxSamples=0)
Build the sampled overlay.
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:290
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:263
CfCompCsr buildCfDivDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(D_scheme − D_std) for the face-average divergence div_i = invV·Σ α·A·dir·(face value).
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, bool seamOn)
Fully-explicit high-order advection for component comp: defc = ρ·SOU (no implicit FOU; the setImplici...
Definition flow.hpp:442
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, bool seamOn)
Deferred-correction advection term for component comp: defc = ρ·SOU − ρ·FOU (UNSCALED; the predictor ...
Definition flow.hpp:394
void cfApply(const CfCsrDev &c, View< const double > f, View< double > out)
out(i) += Σ coef·f(slot).
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).
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:307
GhostOverlaySampledDev uploadGhostOverlaySampled(const GhostOverlaySampled &h)
CfCompCsrDev uploadCfCompCsr(const CfCompCsr &h, const char *name)
CfUfDelta buildCfUfDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
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:497
SeamReconHost buildSeamRecon(const AmrPoisson< 3, Bits > &ap, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
Build the seam-reconstruction tables over the LOCAL rows of ap (docs/amr_cf_convective....
double hoFaceValueSeam(double upup, double up, double upStar, double downStar, double d1, double dUU, double dD, int scheme)
The same reconstruction with every distance and every sample made explicit — the rule of docs/amr_cf_...
void ghostApplyDelta(const GhostOverlayDev &ov, View< const double > x, View< double > y)
Device matrix overlay (== ghostApplyDeltaHost). Distinct rows per thread: no atomics.
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...
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 ...
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:91
GhostGradCsrHost buildSampledGradCsr(const GhostOverlaySampled &ov, const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, IsCutFn &&isCut, OkFn &&classicOk)
void ghostApplyDeltaSampled(const GhostOverlaySampledDev &ov, View< const double > x, View< double > y)
Device matrix overlay (== ghostApplyDeltaSampledHost). Distinct rows per thread: no atomics.
void residualFv(const FvOp &op, View< const double > u, View< const double > rhs, View< double > res)
res = rhs − H u.
Definition fv_op.hpp:119
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:206
bool amrEnvFlag(const char *name)
Truthy environment flag (unset / "" / "0" ⇒ false).
Definition flow.hpp:61
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:159
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:181
auto makeBinaryOpenFn(SdfFn sdfFn, const Vec< 3 > &h0)
Binary openness callable factory for the MG surrogate: a face is open iff both adjacent centers (prob...
void applyGhostGradCsr(const GhostGradCsrDev &ov, View< const double > f, View< double > gx, View< double > gy, View< double > gz)
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,...
void ghostDivergDeltaSampled(const GhostOverlaySampledDev &ov, View< const double > u0, View< const double > u1, View< const double > u2, View< double > d)
Device divergence overlay (== ghostDivergDeltaSampledHost).
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:112
GhostGradCsrDev uploadGhostGradCsr(const GhostGradCsrHost &h)
GhostOverlayDev uploadGhostOverlay(const GhostOverlay &h)
double advectedFaceValue(const SeamView &sv, Index k, bool upIsI, double phiUpUp, double phiUp, double phiDown, double distK, int advScheme, const FldFn &fld)
THE advected face value at one (sub)face slot — written once so deferredSou and advectExplicit cannot...
Definition flow.hpp:365
std::array< CfCsr, 3 > buildCfGradDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(G_scheme − G_std) for the ABC cell gradient gradOf/grad3.
SeamView makeSeamView(const FaceGeom &g, bool on)
Bundle a FaceGeom's seam tables for the kernels.
Definition face_geom.hpp:76
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
void ghostDivergDelta(const GhostOverlayDev &ov, View< const double > u0, View< const double > u1, View< const double > u2, View< double > d)
Device divergence overlay (== ghostDivergDeltaHost).
CfCsr buildMomSeamDelta(const GhostOverlaySampled &ov, const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const Mom &mom, SdfFn &&sdf, Vec< 3 > origin, double idiag, double mu, long *nSkipped=nullptr, std::array< long, 3 > frameShift={})
Momentum ξ-overlay seam correction (plan §6.2, rung 1): at overlay rows with a different-level ±1 nei...
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).
CfScheme
Coarse/fine interface scheme for the collocated flow operators.
Definition cf_scheme.hpp:73
@ standard
raw coarse value (two-point flux; 1st-order at 2:1 faces)
@ quadratic
Martin–Cartwright tangential quadratic (2nd-order at 2:1 faces)
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...
The face CSR topology faceField() is indexed by, copied to host: start the per-cell row offsets (size...
Definition flow.hpp:1914
std::vector< double > center
World centre of EVERY slot (local leaves then ghosts, size nExt): the only way a caller can say what ...
Definition flow.hpp:1928
std::vector< double > recDist
Definition flow.hpp:1923
std::vector< double > rawArea
Definition flow.hpp:1917
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).
Scalar-input CSR overlay: out(i) += Σ_k coef·f(slot) over rows with C/F faces.
Definition cf_scheme.hpp:79
View< Index > uuRecJ
per descriptor: upstream-probe record for up == j, else −1
Definition face_geom.hpp:45
View< double > recDist
per record: dUU for UPSTREAM use (unused as a φ_U* sample)
Definition face_geom.hpp:49
View< Index > uuRecI
per descriptor: upstream-probe record for up == i, else −1
Definition face_geom.hpp:44
View< Index > recCell
record entries: cell slot
Definition face_geom.hpp:50
View< double > alphaArea
α·area (physical) per face, size nFaces
Definition face_geom.hpp:28
View< int > dir
face direction +1/-1, size nFaces
Definition face_geom.hpp:27
View< double > recW
record entries: weight
Definition face_geom.hpp:51
View< double > invVol
1/V_i per cell, size n
Definition face_geom.hpp:34
View< double > rawArea
raw face area (physical, no openness) per face — advection flux
Definition face_geom.hpp:29
View< int > axis
face axis 0/1/2, size nFaces
Definition face_geom.hpp:26
View< Index > upupJ
upstream-of-j probe (periodicNeighbor(j,axis,+dir)) — SOU, size nFaces
Definition face_geom.hpp:33
View< Index > nbr
neighbour leaf per face, size nFaces
Definition face_geom.hpp:25
View< double > alpha
openness per face (gradient gate), size nFaces
Definition face_geom.hpp:31
View< double > dist
face-normal distance (physical) per face, size nFaces
Definition face_geom.hpp:30
View< Index > start
CSR row offsets, size n+1.
Definition face_geom.hpp:24
View< Index > sampI
per descriptor: sample record when i is the COARSE cell, else −1
Definition face_geom.hpp:42
View< Index > seam
descriptor id per face slot, −1 = plain slot; size nFaces
Definition face_geom.hpp:41
View< Index > upupI
upstream-of-i probe (periodicNeighbor(i,axis,−dir)) — SOU, size nFaces
Definition face_geom.hpp:32
View< Index > recStart
record CSR offsets, size nRec+1
Definition face_geom.hpp:48
View< char > fluid
per-cell fluid flag, size n
Definition face_geom.hpp:35
View< double > d1J
per descriptor: half width of j along the face axis (physical)
Definition face_geom.hpp:47
View< double > d1I
per descriptor: half width of i along the face axis (physical)
Definition face_geom.hpp:46
View< Index > sampJ
per descriptor: sample record when j is the COARSE cell, else −1
Definition face_geom.hpp:43
Device CSR gradient overlay (sampled mode) + upload + apply (the variable-length analog of GhostGradO...
Ghost-gradient overlay (setGhostGradient): per cut cell, a precomputed 3-point directional FD stencil...
Definition flow.hpp:255
View< double > w
[n*9] stencil weight (0 = unused)
Definition flow.hpp:259
Index n
number of overlay (cut) cells
Definition flow.hpp:256
View< Index > idx
[n*9] stencil cell, slot s*9 + axis*3 + k
Definition flow.hpp:258
View< Index > cell
[n] leaf index
Definition flow.hpp:257
Device mirror of GhostOverlay (uploaded once per setSolid).
Device mirror of GhostOverlaySampled (uploaded once per setSolid).
Sampled overlay: the classic per-row fields (weights, states, rescale — base; base....
Host ghost-projection overlay: one row per non-clean fluid leaf (== cut cell: some ±1 center sample s...
Host-side seam-reconstruction tables (docs/amr_cf_convective.md §5.2), built once per AmrFlow::setSol...
The seam tables as the advective kernels capture them: const Views plus the ablation flag (AmrFlow::s...
Definition face_geom.hpp:58