flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm.hpp
Go to the documentation of this file.
1
18#ifndef PECLET_FLOW_SDFLOW_IBM_HPP
19#define PECLET_FLOW_SDFLOW_IBM_HPP
20
21#include <algorithm>
22#include <array>
23#include <chrono>
24#include <cmath>
25#include <cstring>
26#include <Kokkos_Core.hpp>
27#include <limits>
28#include <memory>
29#include <string>
30#include <vector>
31
32#include "face_props.hpp"
34#include "ghost_projection_debug.hpp" // opt-in gp row forensics (PECLET_FLOW_GP_DEBUG), no-op off
35#include "grid_layout.hpp"
37#include "mac_cutcell_mg.hpp"
38#include "mac_ibm.hpp"
39#include "mac_pressure.hpp"
40#include "mac_stencils.hpp"
41#include "mac_velocity_mg.hpp"
42#include "peclet/core/field/field_set.hpp"
43#include "property_closures.hpp"
44#include "scalar_transport.hpp"
46
47namespace peclet::flow {
48
49// Templated on a GridLayout policy (grid_layout.hpp) that supplies the grid-position-dependent
50// pieces (currently: the per-component velocity sample offset). IbmSolver == Solver<Staggered> (the
51// alias below) is bit-identical to the pre-policy solver; the Colocated policy is added in a later
52// phase.
53template <class Grid>
54class Solver {
55 public:
56 using FV = Kokkos::View<float*, CCMem>;
57 static constexpr int G = 2; // velocity block: Koren advection reach (pressure/MG bridged to g=1)
58
59 Solver(int nx, int ny, int nz) { allocateBlock(nx, ny, nz); }
60
61 // (Re)allocate every per-block buffer for a local inner block of nx*ny*nz. Called by the
62 // constructor and by redistribute() after a re-decomposition changes this rank's block size.
63 void allocateBlock(int nx, int ny, int nz) {
64 nx_ = nx;
65 ny_ = ny;
66 nz_ = nz;
67 e_ = C3{nx + 2 * G, ny + 2 * G, nz + 2 * G};
68 n_ = (std::size_t)e_.x * e_.y * e_.z;
69 e1_ = C3{nx + 2, ny + 2, nz + 2}; // g=1 block for the cut-cell pressure MG
70 n1_ = (std::size_t)e1_.x * e1_.y * e1_.z;
71 sdf_ = CCField("sdf", n_);
72 ox_ = CCField("ox", n_);
73 oy_ = CCField("oy", n_);
74 oz_ = CCField("oz", n_);
75 phi_ = CCField("phi", n_);
76 div_ = CCField("div", n_);
77 P_ = CCField("P", n_);
78 // g=1 scratch for the MG bridge (openness + rhs/phi + PCG vectors)
79 ox1_ = CCField("ox1", n1_);
80 oy1_ = CCField("oy1", n1_);
81 oz1_ = CCField("oz1", n1_);
82 rhs1_ = CCField("rhs1", n1_);
83 phi1_ = CCField("phi1", n1_);
84 r_ = CCField("r", n1_);
85 z_ = CCField("z", n1_);
86 pp_ = CCField("pp", n1_);
87 Ap_ = CCField("Ap", n1_);
88 for (int c = 0; c < 3; ++c) {
89 C[c].u = CCField("u", n_);
90 C[c].b = CCField("b", n_);
91 C[c].AC = FV("AC", n_);
92 C[c].AW = FV("AW", n_);
93 C[c].AE = FV("AE", n_);
94 C[c].AS = FV("AS", n_);
95 C[c].AN = FV("AN", n_);
96 C[c].AB = FV("AB", n_);
97 C[c].AT = FV("AT", n_);
98 C[c].inhom = CCField("inhom", n_);
99 C[c].rscale = CCField("rscale", n_);
100 C[c].mask = CCField("mask", n_);
101 bcDcorr_[c] = CCField("dcorr", n_);
102 bcBrhs_[c] = CCField("brhs", n_);
103 const int maxCut = nx * ny * nz;
104 C[c].ov = IbmOverlay{Kokkos::View<int*, CCMem>("ci", maxCut),
105 Kokkos::View<int*, CCMem>("nb", maxCut),
106 FV("dr", maxCut),
107 Kokkos::View<int*, CCMem>("dc", (std::size_t)maxCut * 6),
108 FV("K", (std::size_t)maxCut * 6),
109 FV("M", (std::size_t)maxCut * 6),
110 FV("X", (std::size_t)maxCut * 6),
111 FV("Nbc", (std::size_t)maxCut * 6),
112 FV("R", (std::size_t)maxCut * 6)};
113 C[c].idMap = Kokkos::View<int*, CCMem>("idMap", n_);
114 C[c].counter = Kokkos::View<int, CCMem>("cnt");
115 old_[c] = CCField("uOld", n_); // u^n time base (fixed over the step's Picard sweeps)
116 prev_[c] = CCField("uPrev", n_); // previous Picard iterate (outer-tolerance check)
117 }
118 if constexpr (Grid::collocated) { // transient face (MAC) field for the approximate projection
119 uf_ = CCField("uf", n_);
120 vf_ = CCField("vf", n_);
121 wf_ = CCField("wf", n_);
122 tgp_ = CCField("tgp", n_); // scratch: wall-aware transpose gradient (setFaceInterp(2/3))
123 wdef_ = CCField("wdef", n_); // scratch: FV wall viscous-flux defect (setFaceInterp(4))
124 fvM_ = CCField("fvM", n_); // scratch: M·u^k (mode-4 defect matvec)
125 fvL_ = CCField("fvL", n_); // scratch: L_FV(u^k) (mode-4 FV operator apply)
126 cs_ = CCField("cs", n_); // static cell fluid fraction (setFaceInterp(4))
127 xcx_ = CCField("xcx", n_); // static per-face open-centroid wall distance (setFaceInterp(3))
128 xcy_ = CCField("xcy", n_);
129 xcz_ = CCField("xcz", n_);
130 }
131 // Register the pre-existing solver fields in the named directory so the multiphysics machinery
132 // (scalar transport, property closures) and load-balance redistribution can enumerate the whole
133 // set uniformly. adopt() aliases the members (no reallocation, no ownership); all live on the
134 // G=2 velocity block and share velHalo_ under MPI.
135 fields_.adopt("u", C[0].u, G, peclet::core::Centering::FaceX);
136 fields_.adopt("v", C[1].u, G, peclet::core::Centering::FaceY);
137 fields_.adopt("w", C[2].u, G, peclet::core::Centering::FaceZ);
138 fields_.adopt("p", P_, G, peclet::core::Centering::Cell);
139 fields_.adopt("sdf", sdf_, G, peclet::core::Centering::Cell);
140 }
141
142 void setRho(double r) { rho_ = r; }
143 void setMu(double m) { mu_ = m; }
144 void setDt(double d) {
145 if (d != dt_) {
146 dt_ = d;
147 dtDirty_ = true; // the momentum stencil bakes rho/dt in its diagonal (rebuildStencils);
148 // a mid-run dt change must rebuild it or the operator and RHS disagree
149 }
150 }
151 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
152 void setVelocityIterations(int it) { velIters_ = it; }
153 // Momentum tolerance stop: end the RB-GS loop once the swept colour's max increment has dropped
154 // to rtol of the first sweep's (GS contracts geometrically, so the increment tracks the error).
155 // rtol = 0 (default) keeps the legacy fixed-count loop byte-identical. Easy regimes (small
156 // nu*dt/dx^2) exit after ~3-5 sweeps; stiff regimes run to the velIters_ cap unchanged. The
157 // check is fused into the sweep kernel (no extra memory pass) and the stop decision is
158 // rank-uniform (MPI max) so distributed halo exchanges stay in lockstep.
160 velTol_ = rtol > 0.0 ? rtol : 0.0;
161 velMinIters_ = minIters < 1 ? 1 : minIters;
162 }
163 long lastMomentumSweeps() const { return lastMomentumSweeps_; }
164 // Pressure-solve mean-removal scope: "fine" (default — drops the interior-level / post-matvec
165 // nullspace projections, ~3x fewer global-reduction latency hits per Krylov iteration; measured
166 // winner of the at-scale ablation, iteration counts identical) or "all" (legacy). See CutcellMG.
167 void setPressureMeanRemoval(bool all) { mg_.setMeanRemovalScope(all); }
168 void setPressureIterations(int it) { presIters_ = it; }
169 void setAdvection(bool on) { advect_ = on; } // explicit high-order advection (default SOU)
170 // High-order advection scheme for the (explicit, or deferred-correction) flux: 0 = second-order
171 // upwind (SOU, default — 2nd order at smooth extrema too); 1 = Koren TVD (monotone limiter, the
172 // legacy CUDA scheme). Only matters when advection is enabled; FOU stays the deferred-correction
173 // base.
174 void setAdvectionScheme(int s) { advScheme_ = s; }
175 // Implicit-FOU deferred-correction advection (CUDA set_implicit_advection): solve the
176 // first-order-upwind part of advection implicitly (in the velocity operator) + keep (Koren-FOU)
177 // explicit in the RHS -> unconditionally stable for advection (high Re / large dt). Requires the
178 // IBM stencil (rebuilt per Picard iteration with the FOU term); the domain-BC path needs
179 // velocity-MG (separate milestone).
180 void setImplicitAdvection(bool on) { implicitFou_ = on; }
181 // Picard outer iterations over the step (CUDA set_outer_iterations): the advecting velocity is
182 // lagged at the current iterate u^k while the time base stays u^n. iters>=1; tol>0 stops early on
183 // max|du| < tol.
184 void setOuterIterations(int iters) { outerIters_ = iters < 1 ? 1 : iters; }
185 void setOuterTolerance(double tol) { outerTol_ = tol; }
186 long lastOuterIterations() const { return lastOuterIters_; }
187 // Velocity (momentum) multigrid for the IBM diffusion solve (CUDA set_velocity_multigrid): the
188 // STAIRCASE coarse operator (exact == RB-GS, stiff-stable at large dt). Call before set_solid;
189 // built at geometry time.
190 void setVelocityMultigrid(bool on, int levels, int vcycles) {
191 useVelocityMg_ = on;
192 vmgLevels_ = levels < 1 ? 1 : levels;
193 vmgVcycles_ = vcycles < 1 ? 1 : vcycles;
194 }
195 // Enable the agglomerated GraphAMG bottom solve in the pressure MG: the coarsest level is solved
196 // by a mesh-agnostic algebraic multigrid on the operator gathered to rank 0 --
197 // decomposition-agnostic, so multilevel convergence works under a WEIGHTED ORB (where the
198 // geometric coarse levels can't cleanly coarsen). Applied at the next set_solid / geometry
199 // rebuild.
200 // Coarse-level (bottom) solve policy: 0 smoothed bottom (default), -1 auto (agglomerate exactly
201 // when the geometric hierarchy cannot reach a small enough coarsest grid), 1 always. See CutcellMG.
202 void setPressureBottomMode(int mode) {
203 pressAgglomMode_ = mode;
204 if (cutcellPressure_)
205 mg_.setAgglomerationMode(mode);
206 }
208 pressGraphAmg_ = on;
209 if (cutcellPressure_)
210 mg_.setGraphAmgBottom(on); // propagate live (previously only applied at the next set_solid,
211 // so toggling after geometry silently had no effect)
212 }
214 nLevels_ = levels < 1 ? 1 : levels;
215 } // MG depth (CUDA default 4)
216 // Backflow stabilization at outflow faces (Bazilevs 2009 / Esmaily-Moghadam 2011): beta in [0,1]
217 // scales the dissipative outflow term that prevents backflow divergence (0 = off). Default 0.2.
218 void setBackflowStab(double beta) { backflowBeta_ = beta < 0.0 ? 0.0 : beta; }
219 // Deferred-correction advection: on (default) = implicit FOU operator + explicit (HO - FOU)
220 // high-order correction (2nd order; HO = SOU by default, or Koren TVD via set_advection_scheme).
221 // off = pure implicit FOU (1st order, more dissipative, unconditionally stable) -- useful for
222 // very sharp shear layers where the (unlimited SOU) explicit correction overshoots and
223 // destabilizes.
224 void setDeferredCorrection(bool on) { deferredCorr_ = on; }
225 // Chebyshev pressure driver (CUDA set_pressure_chebyshev): communication-light alternative to
226 // MG-PCG -- Chebyshev semi-iteration preconditioned by one symmetric V-cycle, no per-iteration
227 // global dot-products. Spectral bounds of M^{-1}A are estimated once (lazily) on the first solve
228 // and reused every step.
229 void setPressureChebyshev(bool on, int maxit, double rtol) {
230 useChebyshev_ = on;
231 chebMaxit_ = maxit;
232 chebRtol_ = rtol;
233 chebBoundsSet_ = false;
234 }
235 // MG-PCG pressure tolerance/iteration cap (CUDA set_pressure_pcg). The Kokkos cut-cell pressure
236 // solve is MG-PCG by default; this just sets its bounds (the `on` flag is accepted for API
237 // parity).
238 void setPressurePcg(bool /*on*/, int maxit, double rtol) {
239 pcgMaxit_ = maxit;
240 pcgRtol_ = rtol;
241 }
242 // EXPERIMENTAL directional ghost-cell projection (second staggered IBM, ghost_projection.hpp):
243 // point-based FD divergence with wall-anchored directional closures instead of the
244 // openness-weighted cut-cell projection. Call BEFORE set_solid (the overlay is built there).
245 // v1: periodic + IBM only, stationary walls (both grids; the collocated variant closes the
246 // face-AVERAGED field and adds the gpCenterGrad predictor/correction, face_interp 0 only).
247 // Runs multi-rank (initMpi): gp-row ownership is by inner-block cell, the closures read the
248 // exchanged g=2 halo, and the fragmentation guard runs on the allgathered GLOBAL sdf (the
249 // exact-crossings / openness-override study inputs stay single-rank). The nonsymmetric extended
250 // stencil is solved by MG-preconditioned BiCGStab (binary-openness surrogate hierarchy).
251 // matrixOrder/rhsOrder select the closure order (1 = linear, 2 = wall-anchored quadratic) for
252 // the implicit phi couplings and the divergence RHS/diagnostic respectively:
253 // (2,2) full quadratic (13-point nonsymmetric matrix);
254 // (1,1) linear everywhere (7-point matrix, 1st-order closure);
255 // (1,2) MIXED/deferred: 2nd-order steady constraint with a 7-point near-symmetric matrix —
256 // the operator mismatch converges through the time stepping (measured rate ~0.4).
257 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2) {
258 if constexpr (Grid::collocated) {
259 // Collocated ghost mode: the SAME phi matrix/closures on the 1/2-1/2 face-averaged field
260 // (the face correction uf -= grad(phi) is the identical substitution), plus the directional
261 // gpCenterGrad cell gradient for the predictor -grad(P^n) and the cell correction. Only the
262 // plain (mode-0) face map applies — the wall-aware/FV/embed face-interp modes replace the
263 // very operators this scheme owns.
264 if (on && faceInterp_ != 0)
265 faceInterp_ = 0; // QUARANTINED verification path: it owns the operators mode 9 replaces,
266 // so it selects the plain face map itself rather than throwing on the
267 // (now default) gauge-exact scheme.
268 }
269 if (on && (porous_ || varRho_ || hasBc_ || useChebyshev_))
270 throw std::runtime_error(
271 "set_ghost_projection: incompatible with porous/variable-rho/domain-BC/Chebyshev (v1)");
273 throw std::runtime_error("set_ghost_projection: matrix_order/rhs_order must be 1 or 2");
274 if (on && distributed_ && (hasExactCross_ || hasOpenOverride_))
275 throw std::runtime_error(
276 "set_ghost_projection: exact-crossings/openness-override are single-rank only");
277 ghostProjection_ = on;
278 colSchemeAuto_ = false; // explicit selection disables the AUTO default
279 gpMatrixOrder_ = matrixOrder;
280 gpRhsOrder_ = rhsOrder;
281 gpNRows_ = -1; // takes effect at the next set_solid
282 }
283 // Analytic-SDF capability: EXACT wall-crossing fractions overriding the linear-interp theta in
284 // BOTH the momentum cut-cell overlay and the ghost-projection closures. t is a flat array of
285 // size 9*nx*ny*nz, blocks ordered [(c*3 + k)]: for velocity component c, t[(c*3+k)*n + i] is
286 // the exact crossing fraction in (0,1) from component c's staggered point at inner cell i
287 // toward its +k-axis neighbour point, NaN where the segment has no wall crossing. Computed in
288 // Python from the analytic geometry (e.g. line-sphere intersection). Call BEFORE set_solid;
289 // pass an empty array to clear. Single-rank only.
290 void setExactCrossings(const std::vector<double>& t) {
291 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
292 if (t.empty()) {
293 hasExactCross_ = false;
294 return;
295 }
296 if (t.size() != 9 * n)
297 throw std::runtime_error("set_exact_crossings: expected 9*nx*ny*nz values");
298#ifdef PECLET_FLOW_MPI
299 if (distributed_)
300 throw std::runtime_error("set_exact_crossings: single-rank only");
301#endif
302 for (int c = 0; c < 3; ++c)
303 for (int k = 0; k < 3; ++k) {
304 tEx_[c][k] = CCField("tEx", n);
305 Kokkos::deep_copy(
306 tEx_[c][k],
307 Kokkos::View<const double*, Kokkos::HostSpace,
308 Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
309 t.data() + ((std::size_t)c * 3 + k) * n, n));
310 }
311 hasExactCross_ = true;
312 }
313 // Analytic-SDF capability: EXACT face-openness (aperture) fields overriding the sampled-SDF
314 // ccFractionCore openness the cut-cell projection uses. Inner arrays (flat x-fastest,
315 // nx*ny*nz); ox[i] = fluid area fraction of the -x face of cell i, etc. Call BEFORE set_solid.
316 void setOpennessOverride(const std::vector<double>& ox, const std::vector<double>& oy,
317 const std::vector<double>& oz) {
318 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
319 if (ox.empty()) {
320 hasOpenOverride_ = false;
321 return;
322 }
323 if (ox.size() != n || oy.size() != n || oz.size() != n)
324 throw std::runtime_error("set_openness_override: expected nx*ny*nz values per field");
325#ifdef PECLET_FLOW_MPI
326 if (distributed_)
327 throw std::runtime_error("set_openness_override: single-rank only");
328#endif
329 oxOverride_ = ox;
330 oyOverride_ = oy;
331 ozOverride_ = oz;
332 hasOpenOverride_ = true;
333 }
334 // Incremental-rotational pressure (CUDA set_incremental_pressure, default ON): the predictor
335 // carries -grad(P^n) and the physical pressure is accumulated rotationally P += (rho/dt)*phi -
336 // mu*div(u*). OFF => classical non-incremental Chorin (no -grad(P^n) predictor; P derived on
337 // demand as (rho/dt)*phi).
338 void setIncrementalPressure(bool on) { incremental_ = on; }
339 // Pressure warm-start (CUDA set_pressure_warmstart, default OFF): seed each cut-cell pressure
340 // solve from the previous step's projection potential (consecutive phi's are similar along a
341 // steady march -> a more converged phi per fixed solver budget) instead of zeroing the initial
342 // guess.
343 void setPressureWarmstart(bool on) { pwarm_ = on; }
344 // Collocated cut-cell treatment of the approximate projection (no effect on the staggered path):
345 // 0 = plain ½/½ cell->face averaging + central-difference -grad(P) (default; a consistent
346 // adjoint pair of the WRONG geometry — wall at the solid neighbour's center — first-order
347 // drag at curved walls);
348 // 1 = wall-aware cell->face map only (ablation: breaks the adjoint pairing — WORSE, don't use);
349 // 2 = wall-aware map + its TRANSPOSE as the predictor -grad(P) and the cell correction
350 // (consistent pair, but face-CENTER point values under-count the open-area flux —
351 // ablation);
352 // 3 = mode 2 evaluated at the OPEN-FACE-CENTROID wall distance (static geometry from
353 // buildFaceCentroidDist) — the flux-consistent constraint quadrature (stable, but the
354 // momentum row is still the O(h) axis-by-axis IBM: FV constraint vs FD momentum are
355 // inconsistent);
356 // 4 = FULLY-FV: mode-3 projection PLUS the second-order wall viscous-flux deferred correction
357 // on
358 // the momentum (fvViscousApply: μ Σ_a W_a·centroid wall drag via defect correction, W_a
359 // from the divergence-theorem fragment normal o_{a−}−o_{a+}, centroid gradient at the SDF
360 // foot point). Momentum and constraint now share the same finite-volume cut-cell geometry →
361 // targets O(h²).
362 // 5 = EMBED (Basilisk embed.h): like mode 4 but the momentum wall drag is the TRUE-NORMAL
363 // image-point gradient embedDirichletGradient (μ·area·d(U)/dn along n̂, O(h²) a-priori)
364 // rather than the axis-by-axis W_a g_a — the reconstruction the mode-4 arc found the O(h)
365 // ceiling in. Keeps the mode-3 (wall-aware, o-adjoint) projection.
366 // 6 = EMBED momentum + PLAIN (mode-0) projection: the Basilisk pairing — embed viscous no-slip
367 // with the ½/½ face average, fs-weighted cut-cell Poisson, and central-difference
368 // correction (the mode-1/2/3 wall-aware projection was measured WORSE than plain; embed
369 // drives momentum).
370 // 9 = CUTCELL-GHOST HYBRID (the recommended collocated mode for tight-throat porous media):
371 // mode-0's aperture projection unchanged (plain ½/½ map, real openness divergence —
372 // throttles sub-cell throats, symmetric MG-PCG, no fragmentation concern) but the
373 // predictor -grad(P) and the cell correction use the directional gpCenterGrad gradient
374 // (2nd-order one-sided at cut cells, never reads a solid-centered cell's P — the measured
375 // O(1/h) mode-0 defect). Measured: Z&H drag in a −0.04..−0.10% band N=32..128 (NOT clean
376 // 2nd order — the pinned-face aperture-constraint truncation floors it — but 7–20× below
377 // mode 0); RCP permeability monotone toward the staggered-cutcell reference
378 // (−13.0/−8.6/−6.2% at Ng=32/44/56) where mode 0 is erratic (−20%..+14%, pathologically
379 // slow settling) and the ghost projection needs its fragmentation guard. See
380 // doc/collocated_second_order_open_problem.md §9.
381 // 10 = mode 9 with the OPEN-CENTROID wall-aware constraint quadrature (the mode-3
382 // centerToFaceWallAware map). DEAD ABLATION — kept for the record: O(h) with a worse
383 // constant than mode 9 on Z&H, and DIVERGES on RCP slivers (the mode-3a non-telescoping
384 // row-sum mechanism; the telescoping gpCenterGrad force does not cure the
385 // constraint-side injection). Do not use.
386 // Collocated cut-cell projection treatment. THE SUPPORTED VALUES ARE 0 AND 9 — prefer the
387 // string API setCollocatedScheme(). 9 ("gauge-exact") is the DEFAULT since 2026-08-18: the
388 // aperture constraint (unchanged, throat-safe, symmetric MG-PCG) with the directional
389 // gpCenterGrad replacing the two operators measured to be O(1) at cut cells — the -grad(P)
390 // predictor and the projection's cell correction. Measured on two periodic sphere beds
391 // (peclet-examples benchmarks/porous-scaling, colcmp*/colcmp060*): SECOND ORDER on both
392 // (2.36-2.89 over R=5..8, landing at +0.08 % of k_inf at R=16) where mode 0 is first order
393 // (0.94-1.20) and additionally fails to reach steady state within 800 steps on 3 of 5 rungs of
394 // the phi=0.60 bed; and cheapest of every variant tried, 4.6x faster than the STAGGERED
395 // cut-cell reference and 5-6x faster than the directional ghost projection.
396 //
397 // RETIRED 2026-08-18 (rejected here; the kernels remain but are unreachable, deletion is a
398 // follow-up): 1, 2 were pure ablations, and 10 was a documented dead ablation (O(h)
399 // with a worse constant on Z&H, divergent on RCP slivers — doc/
400 // collocated_second_order_open_problem.md §9.1). 3 and 4 (the FV-constraint variants, 4 with
401 // set_fv_relax) survive as ablations reachable only through this integer entry point.
402 //
403 // 5/6/7 were RE-INSTATED 2026-08-19: they are not ablations, they are the Basilisk embed.h port
404 // (true-normal dirichlet_gradient wall drag, openness-weighted cell correction, solid-cut-cell
405 // sliver mask — commits db5b4aa/f5fde8c/6d412ec/03a71c6, doc/collocated_embed_port_plan.md).
406 // That line is the live candidate for removing the collocated accuracy ceiling, so it must stay
407 // reachable. Retiring them was my error.
408 void setFaceInterp(int mode) {
409 static constexpr int kRetired[] = {1, 2, 10};
410 for (int r : kRetired)
411 if (mode == r)
412 throw std::runtime_error(
413 "set_face_interp(" + std::to_string(mode) +
414 "): retired 2026-08-18 (ablation / measured divergent). Use "
415 "set_collocated_scheme(\"gauge-exact\") — the default — or \"plain\" for the "
416 "legacy first-order aperture projection.");
417 if (mode != 0 && (mode < 3 || mode > 7) && mode != 9 && (mode < 11 || mode > 13))
418 throw std::runtime_error("set_face_interp: unknown mode " + std::to_string(mode));
419 if (ghostProjection_ && mode != 0)
420 throw std::runtime_error(
421 "set_face_interp: incompatible with the ghost projection (set_ghost_projection(False) "
422 "first, or use set_collocated_scheme which handles the transition)");
423 faceInterp_ = mode;
424 colSchemeAuto_ = false; // explicit selection disables the AUTO default
425 }
426 // Preferred API for the collocated projection scheme.
427 // "gauge-exact" (default) aperture constraint + directional (gauge-exact) pressure gradient
428 // "plain" the legacy plain-average / central-difference path (first order)
429 void setCollocatedScheme(const std::string& name) {
430 if (name != "ghost" && ghostProjection_) {
431 ghostProjection_ = false; // scheme transition: drop the ghost before selecting a face mode
432 gpNRows_ = -1;
433 }
434 if (name == "gauge-exact") {
435 setFaceInterp(9);
436 gauge2a_ = false;
437 } else if (name == "gauge-2a") { // EXPERIMENTAL: gauge-exact with the "gradient 2a"
438 setFaceInterp(9); // one-sided branch (see gauge_exact_gradient.hpp)
439 gauge2a_ = true;
440 } else if (name == "plain") {
441 setFaceInterp(0);
442 gauge2a_ = false;
443 } else if (name == "ghost") {
444 // The fluid-only constraint scheme (route 2b, 2026-08): binary-openness divergence +
445 // directional closures + gauge-exact gradient. Clean-protocol record: family-free
446 // (m1 -> 1e-5 monotone, |P| frozen), NO Layer-1 instability, C2 across dt = 60..1e20 with
447 // no stabilizer, both-bed ladders -1.4% -> +0.22% (R=8..24; its own small plateau), Z&H
448 // anchor -0.018% at N=128. Costs: BiCGStab (~2.3-2.7x pressure stage; star preconditioner
449 // planned), ~1.6 KB/cell overlay (caps single-GPU size), fragmentation guard. The (1,2)
450 // mixed mode stays quarantined (march-unstable on >2000-sphere beds).
451 setGhostProjection(true, 2, 2);
452 gauge2a_ = false;
453 } else
454 throw std::runtime_error(
455 "set_collocated_scheme: expected \"gauge-exact\", \"gauge-2a\", \"plain\" or "
456 "\"ghost\", got \"" +
457 name + "\"");
458 }
459 // PM I ablation (Guy-Fogelson): keep the incremental predictor -grad(P^n) but accumulate
460 // P += (rho/dt)*phi WITHOUT the rotational -mu*div(u*) term (constant-mu path only; the
461 // variable-mu branches keep their own treatment). Default true = shipped behaviour.
462 void setRotationalPressure(bool on) { rotationalP_ = on; }
463 // Rotational under-relaxation: P += ct*phi - w*mu*div(u*). w = 1 is the shipped Timmermans
464 // update; w = 0 is PM I. Shrinking w shrinks the O(1) velocity->pressure off-diagonal that
465 // makes the cell-centered approximate projection marginally unstable (Guy-Fogelson eq. 92-94:
466 // the destabilizing-perturbation threshold scales ~1/w), at the cost of ~1/w slower pressure
467 // relaxation of the smooth modes at large dt. phi = 0 stays the unique fixed point for ANY
468 // w > 0, at every dt including dt -> infinity.
469 void setRotationalWeight(double w) { rotWeight_ = w; }
470 // Wall-banded rotational blend (Frank, 2026-08-20): see the press_wallblend kernel. w0 = 0
471 // (default) disables; typical w0 ~ 0.3-0.5. Composes with setRotationalWeight (uniform factor).
472 void setRotationalWallWeight(double w0) { rotWallW_ = w0; }
473 // Fluid-only pressure constraint (route 2b). Call BEFORE set_solid. Collocated experiment;
474 // defaults byte-identical when 0. mode 1 = Design A (close every openness face with a
475 // solid-centered side, everywhere); mode 2 = Design B (Kron star elimination: filtered
476 // openness feeds the MG hierarchy only, the SPD star overlay restores the throat coupling in
477 // the PCG matvec, the divergence keeps the original apertures on fluid rows, and fluid|solid
478 // faces are corrected with phibar_s -- see star_elimination.hpp).
479 // Aperture estimation order (2026-08-26): 1 = the shipped one-sample linear model (default,
480 // byte-identical), 2 = marching-squares (5 trilinear samples/face, triangle-fan; O(h^2),
481 // removes the convexity bias measured at +0.59%/+0.27% bed-k at R=8/12 -- tracker row 51).
482 // In-solver ceiling is the trilinear field; for ANALYTIC geometry use exact/Saye apertures
483 // via set_openness_override (scripts/exact_apertures_spheres.py). Call before set_solid.
484 void setApertureOrder(int order) {
486 throw std::runtime_error("set_aperture_order: order must be 1 or 2");
487 apertureOrder_ = order;
488 }
489 void setFluidOnlyConstraint(int mode) {
490 if (mode < 0 || mode > 2)
491 throw std::runtime_error("set_fluid_only_constraint: mode must be 0, 1 or 2");
492 fluidOnlyMode_ = mode;
493 if (mode != 0)
494 colSchemeAuto_ = false; // mechanism instruments run on the aperture rails, not AUTO-ghost
495 }
496 // Filtered rotational update (experimental): P += ct*phi - mu*S(div u*), S = one mask-aware
497 // axis-wise (1,2,1)/4 smoothing pass per axis (one-sided 1/2(d_i+d_nbr) toward the open side at
498 // a solid-centered neighbour, identity when sandwiched). S annihilates the axis checkerboard
499 // including AT wall-adjacent cells; for smooth fields S = I + O(h^2). Steady state unchanged.
500 void setRotationalFilter(bool on, double eps = 0.05) {
501 rotFilter_ = on;
502 rotFilterEps_ = eps;
503 }
504 // Under-relaxation of the mode-4 FV wall-flux defect correction (1 = full; <1 damps the stiff
505 // explicit-lagged wall term). The steady state is independent of this value.
506 void setFvRelax(double w) { fvRelax_ = w; }
507 // CUDA-only 3-stream concurrent velocity solve (set_velocity_streams): no Kokkos analogue in this
508 // port (the default-execution-space kernels are already stream-ordered). Accepted as a no-op for
509 // API parity.
510 void setVelocityStreams(bool /*on*/) {}
511 // Seed/restore the velocity state (CUDA set_state / upload_velocity): u/v/w are inner-cell fields
512 // (flat x-fastest, size nx*ny*nz); written into the velocity block + ghosts refreshed (periodic
513 // wrap).
514 void uploadVelocity(const std::vector<double>& uu, const std::vector<double>& vv,
515 const std::vector<double>& ww) {
516 const std::vector<double>* src[3] = {&uu, &vv, &ww};
518 const int ex = e_.x, ey = e_.y, nx = nx_, ny = ny_, nz = nz_, g = G;
519 for (int c = 0; c < 3; ++c) {
520 // Upload the inner field once, write it into the inner cells on device, then refresh the
521 // periodic ghosts (G4) — the old path mirrored the field down, looped on host, and copied
522 // back up.
523 CCField din("peclet::flow::vel_in_d", static_cast<std::size_t>(nx_) * ny_ * nz_);
524 Kokkos::deep_copy(
525 din,
526 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
527 src[c]->data(), src[c]->size()));
528 CCField u = C[c].u;
529 Kokkos::parallel_for(
530 "peclet::flow::upload_velocity",
531 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx, ny, nz}),
532 KOKKOS_LAMBDA(int x, int y, int z) {
533 u((long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey) =
534 din((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
535 });
536 fillGhosts(C[c].u);
537 }
538 }
539#ifdef PECLET_FLOW_MPI
540 // Multi-rank: this rank's IbmSolver is constructed with its LOCAL block dims (= the
541 // BlockDecomposer of the GLOBAL grid for this rank); initMpi wires the g=2 velocity-block halo +
542 // the global-origin red-black parity, and switches fillGhosts/maxOpenDivergence + the pressure MG
543 // (CutcellMG::initMpi) onto their distributed paths. The caller decomposes first (deterministic
544 // ORB) to size the constructor; initMpi re-derives it.
545 void initMpi(int gnx, int gny, int gnz, MPI_Comm comm) {
546 int size = 1;
547 MPI_Comm_size(comm, &size);
548 // Build the shared decomposition so the pressure MG can derive nested coarse levels
549 // (CutcellMG::coarsened) — aligned ORB by default, or coarse-first when
550 // set_decomposition_levels/PECLET_FLOW_DECOMP_LEVELS asks for it. Depends only on the global
551 // grid and that setting, so it matches mpi_block()'s sizing exactly.
552 initMpi(CutcellMG::decomposition(static_cast<std::size_t>(size), gnx, gny, gnz), comm);
553 }
554 // Shared-decomposition overload: wire the g=2 velocity-block halo from an EXTERNALLY-built ORB
555 // (so flow and dem share one BlockDecomposer for coupled runs, and redistribute() can re-init
556 // onto a re-decomposed partition). The local block size must already match dec.block(rank).size
557 // (set via the constructor / allocateBlock).
558 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec, MPI_Comm comm) {
559 distributed_ = true;
560 comm_ = comm;
561 const auto& gs = dec.globalSize();
562 gnx_ = (int)gs[0];
563 gny_ = (int)gs[1];
564 gnz_ = (int)gs[2];
565 int rank = 0;
566 MPI_Comm_rank(comm, &rank);
567 std::array<bool, 3> per{true, true, true};
568 velHalo_ = std::make_shared<GridHaloTopology<3>>();
569 velHalo_->buildTopology(dec, rank, G, per, comm);
570 velDev_ = std::make_shared<GridHalo<double>>();
571 velDev_->init(*velHalo_);
572 // Communication-avoiding momentum sweeps (see smoothComp): the velocity block is g=2 already,
573 // so the CA pair needs no new topology — only this float exchange for the stencil ring
574 // (operator coefficients are float). Eligible when every rank's block is >= 4 on every axis
575 // (rank-uniform: the decomposition is replicated), gated by PECLET_FLOW_CA.
576 velDevF_ = std::make_shared<GridHalo<float>>();
577 velDevF_->init(*velHalo_);
578 long minExt = std::numeric_limits<long>::max();
579 for (const auto& s : dec.sizes())
580 for (int k = 0; k < 3; ++k)
581 minExt = std::min(minExt, (long)s[k]);
583 for (bool& d : momStencilDirty_)
584 d = true;
585 dec_ =
586 std::make_shared<peclet::core::decomp::BlockDecomposer<3>>(dec); // remember the partition
587 const auto oig = velHalo_->indexer().originInclGhost();
588 og_ = {(int)oig[0] + G, (int)oig[1] + G,
589 (int)oig[2] + G}; // block inner origin -> global parity
590 }
591 // Redistribute the solver's state onto a NEW decomposition (dynamic load balancing). Enumerates
592 // the registered fields, moves them from the current block layout to the new one (bit-exact via
593 // redistributeGridFields), reallocates every buffer to the new block, re-inits the halo +
594 // pressure MG on the new partition, and rebuilds all geometry-derived state (openness / IBM
595 // overlay / stencils) from the migrated SDF. Velocity + pressure + SDF (+ any registered
596 // scalar/property fields) survive; per-step scratch is rebuilt.
597 void redistribute(const peclet::core::decomp::BlockDecomposer<3>& newDec) {
598 if (!distributed_ || !dec_)
599 return;
600 int rank = 0;
601 MPI_Comm_rank(comm_, &rank);
602 const auto ob = dec_->block(rank), nb = newDec.block(rank);
603 const int oex = (int)ob.size[0] + 2 * G, oey = (int)ob.size[1] + 2 * G,
604 oez = (int)ob.size[2] + 2 * G;
605 const int nex = (int)nb.size[0] + 2 * G, ney = (int)nb.size[1] + 2 * G,
606 nez = (int)nb.size[2] + 2 * G;
607
608 // 1. gather the surviving registered fields to host padded buffers on the OLD block.
609 const auto names = fields_.names();
610 std::vector<std::vector<double>> oldHost(names.size()), newHost(names.size());
611 for (std::size_t k = 0; k < names.size(); ++k) {
612 CCField f = fields_.at(names[k]).data;
613 auto h = Kokkos::create_mirror_view(f);
614 Kokkos::deep_copy(h, f);
615 oldHost[k].assign(h.data(), h.data() + (std::size_t)oex * oey * oez);
616 newHost[k].assign((std::size_t)nex * ney * nez, 0.0);
617 }
618 // 2. redistribute each field OLD -> NEW (host, bit-exact pure data movement).
619 std::vector<const double*> op(names.size());
620 std::vector<double*> np(names.size());
621 for (std::size_t k = 0; k < names.size(); ++k) {
622 op[k] = oldHost[k].data();
623 np[k] = newHost[k].data();
624 }
625 peclet::core::decomp::redistributeGridFields<double>(*dec_, newDec, rank, G, op, np, comm_);
626
627 // 3. reallocate every buffer to the new block; re-init the halo + MG on the new partition.
628 allocateBlock((int)nb.size[0], (int)nb.size[1], (int)nb.size[2]);
630 // scatter a padded host buffer into a registered field's device buffer.
631 auto scatterPadded = [&](const std::string& name, const std::vector<double>& src) {
632 CCField f = fields_.at(name).data;
633 auto h = Kokkos::create_mirror_view(f);
634 std::memcpy(h.data(), src.data(), sizeof(double) * (std::size_t)nex * ney * nez);
635 Kokkos::deep_copy(f, h);
636 };
637 // 4. scatter all migrated fields into the fresh (new-block) buffers.
638 for (std::size_t k = 0; k < names.size(); ++k)
640 // 5. rebuild geometry-derived state (openness/IBM/stencils/MG) from the migrated SDF. setSolid
641 // zeroes the velocity + pressure (it is the initial-geometry setup), so re-instate every
642 // non-SDF field afterward from the migrated data.
643 setSolid(gatherInner(sdf_), cutcellPressure_);
644 for (std::size_t k = 0; k < names.size(); ++k)
645 if (names[k] != "sdf")
647 }
648 // Redistribute onto the weighted ORB of per-cell weights `w` (global x-fastest, gnx*gny*gnz). The
649 // ergonomic Python entry point for load balancing: the caller passes a weight field (e.g. fluid
650 // work + gamma*particle_count) and both flow and dem rebuild the SAME deterministic partition
651 // from it. No BlockDecomposer object crosses the language boundary.
652 void rebalanceByWeights(const std::vector<peclet::core::Real>& w) {
653 if (!distributed_)
654 return;
655 int size = 1;
656 MPI_Comm_size(comm_, &size);
657 peclet::core::decomp::BlockDecomposer<3> newDec((std::size_t)size,
658 peclet::core::IVec<3>{gnx_, gny_, gnz_}, w);
660 }
661#endif
662 // per-face domain BC {face 0..5 = -x,+x,-y,+y,-z,+z}: type 0=periodic,1=no-slip
663 // wall,2=Dirichlet/inflow,3=outflow.
664 void setDomainBc(int face, int type, double vx, double vy, double vz) {
665 bc_[face] = type;
666 bcVel_[face][0] = vx;
667 bcVel_[face][1] = vy;
668 bcVel_[face][2] = vz;
669 hasBc_ = false;
670 hasOutflow_ = false;
671 for (int i = 0; i < 6; ++i) {
672 if (bc_[i])
673 hasBc_ = true;
674 if (bc_[i] == 3)
675 hasOutflow_ = true;
676 }
677 }
678 // per-position inlet velocity profile on `face` (CUDA set_domain_bc_profile): prof is (nb,nc,3)
679 // on the inner grid of the face's two perpendicular axes; sets the face to inflow (type 2).
680 // Resampled (clamp) to the ghost-inclusive face grid so the BC kernel indexes it directly by face
681 // position.
682 void setDomainBcProfile(int face, const std::vector<double>& prof, int nb, int nc) {
683 const int a = face / 2;
684 const int dims[3] = {e_.x, e_.y, e_.z};
685 const int bax = (a + 1) % 3, cax = (a + 2) % 3;
686 const int Lb = dims[bax], Lc = dims[cax];
687 CCField pf("bcprof", (std::size_t)Lb * Lc * 3);
688 auto h = Kokkos::create_mirror_view(pf);
689 auto cl = [](int v, int n) { return v < 0 ? 0 : (v >= n ? n - 1 : v); };
690 for (int p0 = 0; p0 < Lb; ++p0)
691 for (int p1 = 0; p1 < Lc; ++p1) {
692 const int ib = cl(p0 - G, nb), ic = cl(p1 - G, nc);
693 for (int k = 0; k < 3; ++k)
694 h(((long)p0 * Lc + p1) * 3 + k) = prof[((std::size_t)ib * nc + ic) * 3 + k];
695 }
696 Kokkos::deep_copy(pf, h);
697 bcProf_[face] = pf;
698 bcProfNc_[face] = Lc;
699 bc_[face] = 2;
700 hasBc_ = true; // a profiled face is an inflow
701 }
702 // all-fluid + domain-BC pressure (CUDA set_pressure_geometry): same path as set_solid with an
703 // open SDF.
704 void setPressureGeometry(const std::vector<double>& sdfInner) { setSolid(sdfInner, true); }
705
706 // SDF on the inner cells (flat x-fastest, size nx*ny*nz; <0 solid). cutcellPressure enables the
707 // open-face-weighted cut-cell projection (off => velocity-only, e.g. unidirectional body-force
708 // flow).
709 void setSolid(const std::vector<double>& sdfInner, bool cutcellPressure) {
710 cutcellPressure_ = cutcellPressure;
711 if constexpr (Grid::collocated) {
712 // DEFAULT SWITCH (2026-08-25, user decision after the attractor campaign): the collocated
713 // scheme default is AUTO = the GHOST (fluid-only) projection — family-free, unconditionally
714 // stable, protocol-independent (doc/collocated_invisible_subspace.md; clean ladders both
715 // beds) — falling back to gauge-exact with a stderr notice on the configurations the ghost
716 // v1 does not support (porous / variable-rho / domain-BC / Chebyshev / analytic overrides).
717 // Any explicit scheme selection (set_collocated_scheme / set_face_interp /
718 // set_ghost_projection / set_fluid_only_constraint) disables AUTO.
719 if (colSchemeAuto_) {
720 const bool ok = !(porous_ || varRho_ || hasBc_ || useChebyshev_ || hasExactCross_ ||
721 hasOpenOverride_ || fluidOnlyMode_ != 0);
722 if (ok) {
723 ghostProjection_ = true;
724 gpMatrixOrder_ = 2;
725 gpRhsOrder_ = 2;
726 faceInterp_ = 0; // the ghost owns the operators the face-interp modes replace
727 gpNRows_ = -1;
728 } else {
729 if (ghostProjection_)
730 gpNRows_ = -1;
731 ghostProjection_ = false;
732 faceInterp_ = 9;
734 "peclet::flow SolverColocated: AUTO scheme fell back to gauge-exact "
735 "(configuration unsupported by the ghost projection v1). Select explicitly "
736 "with set_collocated_scheme to silence this notice.\n");
737 }
738 }
739 }
740#ifdef PECLET_FLOW_MPI
741 for (bool& d : momStencilDirty_) // stencil ring re-exchange for the CA momentum sweeps
742 d = true;
743#endif
744 hasSolid_ =
745 false; // does the geometry actually contain solid? (all-fluid set_pressure_geometry
746 for (double v :
747 sdfInner) // passes sd>0 everywhere -> stays false, keeping the channel/BFS path)
748 if (v < 0.0) {
749 hasSolid_ = true;
750 break;
751 }
752#ifdef PECLET_FLOW_MPI
753 if (distributed_) { // a solid anywhere in the global domain enables the IBM momentum path
754 int local = hasSolid_ ? 1 : 0, global = 0;
756 hasSolid_ = global != 0;
757 }
758#endif
759#ifdef PECLET_FLOW_MPI
760 if (distributed_) {
761 // Multi-rank: sdfInner is THIS rank's LOCAL inner block; fill the inner cells, then
762 // halo-exchange the ghosts (cross-rank + periodic) so the overlay/openness read the
763 // neighbour's SDF at the block boundary.
764 auto h = Kokkos::create_mirror_view(sdf_);
765 Kokkos::deep_copy(h, sdf_);
766 for (int z = 0; z < nz_; ++z)
767 for (int y = 0; y < ny_; ++y)
768 for (int x = 0; x < nx_; ++x)
769 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
770 sdfInner[(std::size_t)x + (std::size_t)y * nx_ +
771 (std::size_t)z * (std::size_t)nx_ * ny_];
772 Kokkos::deep_copy(sdf_, h);
773 velDev_->exchange(sdf_);
774 } else
775#endif
776 {
777 // Single-rank: upload the inner SDF once and do the periodic-wrap gather on device (G4) —
778 // fills the whole extended block (inner + periodic ghosts) in one kernel instead of a host
779 // triple loop + a full extended-block H2D.
780 CCField din("peclet::flow::sdfInner_d", static_cast<std::size_t>(nx_) * ny_ * nz_);
781 Kokkos::deep_copy(
782 din,
783 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
784 sdfInner.data(), sdfInner.size()));
786 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
787 CCField sdf = sdf_;
788 Kokkos::parallel_for(
789 "peclet::flow::sdf_periodic_wrap",
790 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {ex, ey, ez}),
791 KOKKOS_LAMBDA(int x, int y, int z) {
792 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
793 iz = (((z - g) % nz) + nz) % nz;
794 sdf((long)x + (long)y * ex + (long)z * (long)ex * ey) = din(
795 (std::size_t)ix + (std::size_t)iy * nx + (std::size_t)iz * (std::size_t)nx * ny);
796 });
797 space.fence();
798 }
799 const bool useEx = hasExactCross_ && !Grid::collocated; // exact-theta arrays are for the
800 // staggered point placement
801 for (int c = 0; c < 3; ++c) {
802 const Off3 off =
803 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
804 C[c].nCut = buildIbmOverlay<0>(
805 CCConst(sdf_), e_, G, off, /*Dirichlet*/ 0, C[c].ov, C[c].idMap, C[c].counter,
806 useEx ? CCConst(tEx_[c][0]) : CCConst(), useEx ? CCConst(tEx_[c][1]) : CCConst(),
807 useEx ? CCConst(tEx_[c][2]) : CCConst(),
808 C3{nx_, ny_, nz_}); // SCHEME 0 = point-value (matches CUDA ibm_geometry_ext_k<0>)
809 ibmSolidMask(C[c].mask, CCConst(sdf_), e_, off);
810 Kokkos::deep_copy(C[c].u, 0.0);
811 }
813 // Staggered domain BCs bake an implicit-diffusion wall fold; the collocated grid instead uses
814 // explicit reflection ghosts (refreshed each smoother sweep), so it needs no fold.
815 if (hasBc_ && !Grid::collocated)
817 if (useVelocityMg_) { // velocity-MG hierarchy: IBM (staircase/upwind) or domain-BC
818 // (const-coeff) mode
819 vmg_.init(nx_, ny_, nz_, vmgLevels_);
820 if (hasBc_)
821 vmg_.setBC(bc_);
822 else {
823 vmgTheta_ = CCField("vmgTheta", n_);
824 vmgClean_ = CCField("vmgClean", n_);
825 }
826 }
827 if (cutcellPressure_) {
828 buildOpenness(ox_, oy_, oz_, CCConst(sdf_), e_, 1.0, 1.0, 1.0,
829 apertureOrder_); // on the g=2 velocity block
830 if (hasOpenOverride_) {
831 // Analytic-SDF exact apertures (setOpennessOverride): overwrite the sampled-SDF openness
832 // with the user-provided inner fields + periodic wrap into the ghost ring (single-rank).
833 const std::vector<double>* src[3] = {&oxOverride_, &oyOverride_, &ozOverride_};
834 CCField dst[3] = {ox_, oy_, oz_};
835 for (int f = 0; f < 3; ++f) {
836 CCField din("peclet::flow::openOv_d", (std::size_t)nx_ * ny_ * nz_);
837 Kokkos::deep_copy(din, Kokkos::View<const double*, Kokkos::HostSpace,
838 Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
839 src[f]->data(), src[f]->size()));
841 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
842 CCField o = dst[f];
843 Kokkos::parallel_for(
844 "peclet::flow::open_override_wrap",
845 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {ex, ey, ez}),
846 KOKKOS_LAMBDA(int x, int y, int z) {
847 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
848 iz = (((z - g) % nz) + nz) % nz;
849 o((long)x + (long)y * ex + (long)z * (long)ex * ey) =
850 din((std::size_t)ix + (std::size_t)iy * nx +
851 (std::size_t)iz * (std::size_t)nx * ny);
852 });
853 space.fence();
854 }
855 }
856 if constexpr (Grid::collocated) { // static open-centroid wall distances (setFaceInterp(3))
857 buildFaceCentroidDist(xcx_, xcy_, xcz_, CCConst(sdf_), e_);
858 buildCellFraction(cs_, CCConst(sdf_), e_, G); // cell fluid fraction (setFaceInterp(4))
859 if (faceInterp_ >= 5 &&
860 faceInterp_ <= 7) { // EMBED: a solid-CENTRED cut cell (cs>0) is partially fluid and
861 // holds
862 // its reconstructed near-wall velocity — masking it to 0 (the sdf<0 IBM mask) drops the
863 // near-wall closure and shifts the whole channel. Re-mask from cs: pin ONLY fully-solid
864 // cells (cs≈0), keeping every partial-fluid cut cell live in the embed solve +
865 // projection.
866 CCConst cs = CCConst(cs_);
867 const std::size_t nn = n_;
868 for (int c = 0; c < 3; ++c) {
869 CCField m = C[c].mask;
870 Kokkos::parallel_for(
871 "peclet::flow::embed_solid_mask", Kokkos::RangePolicy<CCExec>(0, nn),
872 KOKKOS_LAMBDA(std::size_t i) { m(i) = cs(i) < 1e-6 ? 1.0 : 0.0; });
873 }
874 }
875 }
876 if (fluidOnlyMode_ == 1) {
877 // Mode-14a FLUID-ONLY constraint (setFluidOnlyConstraint): close every face with a
878 // solid-CENTERED side in the openness the pressure stack consumes. The aperture operator,
879 // the divergence, the face correction and the MG rediscretization all read these fields,
880 // so one filter makes constraint/operator/correction consistent by construction: pressure
881 // DOFs decouple at solid-centered cells (their rows go empty like solid cells), the
882 // invisible multiplier subspace of collocated_invisible_subspace.md S4 ceases to exist,
883 // and the operator stays SPD + 7-point (CG + CutcellMG untouched). Closure quality is
884 // Neumann-zero at the closed faces (the crude end of the fluid-only family -- measured,
885 // not assumed); the consistent-closure variants ride on the gp row machinery instead.
886 CCConst sd = CCConst(sdf_);
887 CCField oa[3] = {ox_, oy_, oz_};
888 C3 e = e_;
889 for (int a = 0; a < 3; ++a) {
890 CCField o = oa[a];
891 const long sa = (a == 0) ? 1 : (a == 1) ? (long)e.x : (long)e.x * e.y;
892 Kokkos::parallel_for(
893 "peclet::flow::fluid_only_openness",
894 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(CCExec(), {1, 1, 1},
895 {e.x, e.y, e.z}),
896 KOKKOS_LAMBDA(int x, int y, int z) {
897 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
898 if (sd(i) < 0.0 || sd(i - sa) < 0.0)
899 o(i) = 0.0;
900 });
901 }
902 }
903#ifdef PECLET_FLOW_MPI
904 // openness ghosts (the operator + divergence read the +neighbour face) -> exchange across
905 // ranks
906 if (distributed_) {
907 velDev_->exchange(ox_);
908 velDev_->exchange(oy_);
909 velDev_->exchange(oz_);
910 }
911#endif
912 if (hasBc_) { // FLUX openness (beta): a face is OPEN only where it carries normal flux --
913 // outflow, or
914 B3 e2{e_.x, e_.y, e_.z};
915 CCField oa[3] = {ox_, oy_, oz_}; // an inflow with nonzero normal velocity. Walls
916 for (int a = 0; a < 3; ++a)
917 for (int s = 0; s < 2; ++s) { // and tangential-only Dirichlet faces (e.g. a
918 const int t = bc_[2 * a + s]; // lid: type 2 with zero normal vel) are CLOSED.
919 const bool open = (t == 3) || (t == 2 && (bcProf_[2 * a + s].extent(0) > 0 ||
920 std::fabs(bcVel_[2 * a + s][a]) > 1e-12));
921 if (t != 0 && !open)
922 bcZeroOpenness(oa[a], e2, G, a, s);
923 }
924 } // the MG re-derives the OPERATOR openness alpha (inflow Neumann -> closed) per level via
925 // setBC.
926 copyInner(ox1_, e1_, 1, CCConst(ox_), e_, G); // bridge openness g=2 -> g=1 for the MG
927 copyInner(oy1_, e1_, 1, CCConst(oy_), e_, G);
928 copyInner(oz1_, e1_, 1, CCConst(oz_), e_, G);
929 if (fluidOnlyMode_ == 2) {
930 // Design B: the MG hierarchy is built from the FILTERED openness (Design A's operator,
931 // the symmetric surrogate preconditioner + the 7-point part of the true operator); the
932 // geometric ox_/oy_/oz_ stay ORIGINAL for the divergence and the face correction. Filter
933 // the g=1 bridge in place, then build the star overlay from the original apertures.
934 if (porous_ || varRho_ || hasBc_ || ghostProjection_ || distributed_ || !Grid::collocated)
935 throw std::runtime_error(
936 "set_fluid_only_constraint(2): v1 is single-rank periodic collocated only");
938 CCConst sd = CCConst(sdf_);
939 CCField oa1[3] = {ox1_, oy1_, oz1_};
940 const C3 e1 = e1_, e2 = e_;
941 for (int a = 0; a < 3; ++a) {
942 CCField o1 = oa1[a];
943 const long sa2 = (a == 0) ? 1 : (a == 1) ? (long)e2.x : (long)e2.x * e2.y;
944 Kokkos::parallel_for(
945 "peclet::flow::star_filter_bridge",
946 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx_, ny_, nz_}),
947 KOKKOS_LAMBDA(int x, int y, int z) {
948 const long i2 =
949 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
950 if (sd(i2) < 0.0 || sd(i2 - sa2) < 0.0)
951 o1((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) =
952 0.0;
953 });
954 }
955 space.fence();
956 starCounter_ = Kokkos::View<int, CCMem>("star_counter");
957 const C3 nn{nx_, ny_, nz_};
958 nStar_ = buildStarOverlay(CCConst(sdf_), CCConst(ox_), CCConst(oy_), CCConst(oz_), e_, G,
959 nn, StarOverlay{}, starCounter_);
960 starOv_ = starMakeOverlay(std::max(nStar_, 1));
961 buildStarOverlay(CCConst(sdf_), CCConst(ox_), CCConst(oy_), CCConst(oz_), e_, G, nn,
962 starOv_, starCounter_);
963 }
964 if (ghostProjection_) {
965 // Directional ghost-cell projection: build the closure overlay + the binary (COUPLED)
966 // openness. The binary field replaces the geometric openness on the MG rails (the MG
967 // hierarchy becomes the symmetric surrogate preconditioner; the overlay delta enters only
968 // the fine-level BiCGStab matvec). The geometric ox_/oy_/oz_ above stay for diagnostics.
969 if (porous_ || varRho_ || hasBc_)
970 throw std::runtime_error(
971 "ghost projection: incompatible with porous/variable-rho/domain-BC (v1)");
972 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
973 gpOv_ = gpMakeOverlay((long)nInner); // worst-case sizing, like the momentum overlay
974 gpIdMap_ = Kokkos::View<int*, CCMem>("gp_idmap", nInner);
975 gpCounter_ = Kokkos::View<int, CCMem>("gp_counter");
976 oxb_ = CCField("oxb", n_);
977 oyb_ = CCField("oyb", n_);
978 ozb_ = CCField("ozb", n_);
979 gpRh_ = CCField("gpRh", n1_);
980 gpT_ = CCField("gpT", n1_);
981 gpZ2_ = CCField("gpZ2", n1_);
982 if (distributed_)
983 gpX2_ = CCField("gpXg2", n_); // g=2 staging block for the distributed BiCGStab matvec
984 // Fragmentation guard: the binary COUPLED-face condition is stricter than aperture
985 // connectivity, so tight-throat geometries (e.g. a random close packing with touching
986 // spheres) fragment the fluid graph into a main component + tiny pockets at the
987 // contacts. Each pocket adds its own null vector that the single global mean-removal
988 // cannot handle, and BiCGStab breaks down (measured: fields to ~1e152 on the RCP
989 // example). Host BFS over the coupled graph of the INNER sdf; fluid cells outside the
990 // largest component are treated as SOLID for the PROJECTION ONLY (sdfGp), decoupling
991 // their rows; the momentum step keeps the true sdf.
992 // Distributed: connectivity is a GLOBAL property (a pocket can span rank boundaries) and
993 // every rank must agree on the main component, so allgather the inner sdf, run the
994 // deterministic guard on the global grid identically on every rank, and keep this
995 // rank's block of the result.
996 std::vector<double> sdfGpHost;
997 {
998 std::vector<double> work;
999 int fx = nx_, fy = ny_, fz = nz_;
1000 bool verbose = true;
1001#ifdef PECLET_FLOW_MPI
1002 int myRank = 0;
1003 if (distributed_) {
1005 int nRanks = 1;
1007 fx = gnx_;
1008 fy = gny_;
1009 fz = gnz_;
1010 verbose = myRank == 0;
1011 std::vector<int> cnts(nRanks), disp(nRanks);
1012 long acc = 0;
1013 for (int r = 0; r < nRanks; ++r) {
1014 const auto b = dec_->block(r);
1015 cnts[r] = (int)(b.size[0] * b.size[1] * b.size[2]);
1016 disp[r] = (int)acc;
1017 acc += cnts[r];
1018 }
1019 std::vector<double> flat((std::size_t)acc);
1020 MPI_Allgatherv(sdfInner.data(), (int)sdfInner.size(), MPI_DOUBLE, flat.data(),
1021 cnts.data(), disp.data(), MPI_DOUBLE, comm_);
1022 work.assign((std::size_t)fx * fy * fz, 0.0);
1023 for (int r = 0; r < nRanks; ++r) {
1024 const auto b = dec_->block(r);
1025 const double* src = flat.data() + disp[r];
1026 for (int z = 0; z < (int)b.size[2]; ++z)
1027 for (int y = 0; y < (int)b.size[1]; ++y)
1028 for (int x = 0; x < (int)b.size[0]; ++x)
1029 work[(std::size_t)(x + b.origin[0]) + (std::size_t)(y + b.origin[1]) * fx +
1030 (std::size_t)(z + b.origin[2]) * (std::size_t)fx * fy] =
1031 src[(std::size_t)x + (std::size_t)y * b.size[0] +
1032 (std::size_t)z * (std::size_t)b.size[0] * b.size[1]];
1033 }
1034 } else
1035#endif
1036 work = sdfInner;
1037 const std::size_t nTot = work.size();
1038 const int nx = fx, ny = fy, nz = fz;
1039 auto id = [&](int x, int y, int z) {
1040 return (std::size_t)((x + nx) % nx) + (std::size_t)((y + ny) % ny) * nx +
1041 (std::size_t)((z + nz) % nz) * (std::size_t)nx * ny;
1042 };
1043 std::vector<int> comp(nTot, -1);
1044 std::vector<std::size_t> stack;
1045 int ncomp = 0, mainComp = -1;
1046 std::size_t mainSize = 0, nActive = 0;
1047 for (std::size_t seed = 0; seed < nTot; ++seed) {
1048 if (comp[seed] >= 0 || work[seed] < 0.0)
1049 continue;
1050 std::size_t size = 0;
1051 comp[seed] = ncomp;
1052 stack.assign(1, seed);
1053 while (!stack.empty()) {
1054 const std::size_t c = stack.back();
1055 stack.pop_back();
1056 ++size;
1057 const int x = (int)(c % nx), y = (int)((c / nx) % ny),
1058 z = (int)(c / ((std::size_t)nx * ny));
1059 const int nb[6][3] = {{x - 1, y, z}, {x + 1, y, z}, {x, y - 1, z},
1060 {x, y + 1, z}, {x, y, z - 1}, {x, y, z + 1}};
1061 for (auto& q : nb) {
1062 const std::size_t j = id(q[0], q[1], q[2]);
1063 if (comp[j] >= 0 || work[j] < 0.0)
1064 continue;
1065 // COUPLED face: mean-of-centers face sdf fluid AND both centers fluid
1066 if (0.5 * (work[c] + work[j]) < 0.0)
1067 continue;
1068 comp[j] = ncomp;
1069 stack.push_back(j);
1070 }
1071 }
1072 if (size > mainSize) {
1073 mainSize = size;
1074 mainComp = ncomp;
1075 }
1076 nActive += size;
1077 ++ncomp;
1078 }
1079 if (ncomp > 1) {
1080 std::size_t pockets = 0;
1081 for (std::size_t i = 0; i < nTot; ++i)
1082 if (work[i] >= 0.0 && comp[i] != mainComp) {
1083 work[i] = -(std::abs(work[i]) * 1.001 + 1e-30);
1084 ++pockets;
1085 }
1086 if (verbose)
1087 printf("peclet::flow ghost projection: %d fluid components; decoupled %zu pocket "
1088 "cells outside the main component (%zu of %zu fluid cells)\n",
1090 }
1091#ifdef PECLET_FLOW_MPI
1092 if (distributed_) {
1093 const auto b = dec_->block(myRank);
1094 sdfGpHost.resize(sdfInner.size());
1095 for (int z = 0; z < nz_; ++z)
1096 for (int y = 0; y < ny_; ++y)
1097 for (int x = 0; x < nx_; ++x)
1098 sdfGpHost[(std::size_t)x + (std::size_t)y * nx_ +
1099 (std::size_t)z * (std::size_t)nx_ * ny_] =
1100 work[(std::size_t)(x + b.origin[0]) + (std::size_t)(y + b.origin[1]) * fx +
1101 (std::size_t)(z + b.origin[2]) * (std::size_t)fx * fy];
1102 } else
1103#endif
1104 sdfGpHost = std::move(work);
1105 }
1106 sdfGp_ = CCField("peclet::flow::sdfGp", n_);
1107 CCField sdfGp = sdfGp_; // the projection's sdf view (pockets decoupled); persisted for
1108 // the collocated gpCenterGrad predictor/correction
1109#ifdef PECLET_FLOW_MPI
1110 if (distributed_) {
1111 // local inner block + halo exchange (cross-rank + periodic), same as the sdf_ upload:
1112 // the overlay build reads sdfGp ghosts up to +/-2 = G across block boundaries.
1113 auto h = Kokkos::create_mirror_view(sdfGp_);
1114 Kokkos::deep_copy(h, sdfGp_);
1115 for (int z = 0; z < nz_; ++z)
1116 for (int y = 0; y < ny_; ++y)
1117 for (int x = 0; x < nx_; ++x)
1118 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
1119 sdfGpHost[(std::size_t)x + (std::size_t)y * nx_ +
1120 (std::size_t)z * (std::size_t)nx_ * ny_];
1121 Kokkos::deep_copy(sdfGp_, h);
1122 velDev_->exchange(sdfGp_);
1123 } else
1124#endif
1125 { // upload + periodic wrap (same pattern as the sdf upload above)
1126 CCField din("peclet::flow::sdfGpInner_d", nInner);
1127 Kokkos::deep_copy(din, Kokkos::View<const double*, Kokkos::HostSpace,
1128 Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
1129 sdfGpHost.data(), sdfGpHost.size()));
1130 CCExec space;
1131 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
1132 Kokkos::parallel_for(
1133 "peclet::flow::sdfgp_wrap",
1134 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {ex, ey, ez}),
1135 KOKKOS_LAMBDA(int x, int y, int z) {
1136 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
1137 iz = (((z - g) % nz) + nz) % nz;
1138 sdfGp((long)x + (long)y * ex + (long)z * (long)ex * ey) =
1139 din((std::size_t)ix + (std::size_t)iy * nx +
1140 (std::size_t)iz * (std::size_t)nx * ny);
1141 });
1142 space.fence();
1143 }
1144 gpBinaryOpenness(oxb_, oyb_, ozb_, CCConst(sdfGp), e_);
1145 gpNRows_ = buildGpOverlay(CCConst(sdfGp), e_, G, C3{nx_, ny_, nz_}, gpOv_, gpIdMap_,
1146 gpCounter_, gpMatrixOrder_, gpRhsOrder_,
1147 hasExactCross_ ? CCConst(tEx_[0][0]) : CCConst(),
1148 hasExactCross_ ? CCConst(tEx_[1][1]) : CCConst(),
1149 hasExactCross_ ? CCConst(tEx_[2][2]) : CCConst(),
1150 /*useGhost=*/distributed_);
1151 if (gpDebugLevel() > 0) { // PECLET_FLOW_GP_DEBUG row forensics (analysis only)
1152 int gpDbgRank = 0;
1153#ifdef PECLET_FLOW_MPI
1154 if (distributed_)
1156#endif
1157 gpDebugReport(gpOv_, gpNRows_, C3{nx_, ny_, nz_}, gpIdMap_, gpDbgRank);
1158 }
1159 copyInner(ox1_, e1_, 1, CCConst(oxb_), e_, G); // MG surrogate = binary openness
1160 copyInner(oy1_, e1_, 1, CCConst(oyb_), e_, G);
1161 copyInner(oz1_, e1_, 1, CCConst(ozb_), e_, G);
1162 }
1163 mg_.setBoundaryConditions(bc_); // per-level wall openness + null-space gating (no-op if
1164 // periodic); BEFORE initMpi — the per-level ghost width
1165 // (CA smoothing) is chosen for the periodic operator only
1166#ifdef PECLET_FLOW_MPI
1167 if (distributed_) // share the level-0 decomposition so the MG block matches this rank's
1168 // block
1169 mg_.initMpi(gnx_, gny_, gnz_, nLevels_, comm_, dec_.get());
1170 else
1171#endif
1172 mg_.init(nx_, ny_, nz_,
1173 nLevels_); // geometric multigrid on the cut-cell openness (MG-PCG pressure)
1174 mg_.setOpenness(CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), 1.0, 1.0, 1.0);
1175 // Coarse-solve policy: an explicit set_pressure_graph_amg(True) forces agglomeration,
1176 // otherwise the mode set by set_pressure_bottom (default auto) decides.
1177 mg_.setAgglomerationMode(pressGraphAmg_ ? 1 : pressAgglomMode_);
1178 Kokkos::deep_copy(phi_, 0.0);
1179 Kokkos::deep_copy(P_, 0.0);
1180 }
1181 }
1182
1183 void step() {
1184 const double ts0 = phaseTick();
1185 tPredictor_ = tMomentum_ = tProjection_ = 0.0;
1186 lastMomentumSweeps_ = 0;
1188 // Multiphysics: refresh material properties / body forces from the current fields (frozen over
1189 // the step). No-op (byte-identical) when no closure is registered.
1191 // eps-conservative porous momentum: the volume-averaged time term is (eps_f rho/dt) u, i.e.
1192 // the variable-density machinery with the effective density rho_eff = eps*rho, refreshed from
1193 // the just-deposited eps every step (eps ghosts are already filled by the coupling driver, so
1194 // the whole-block product has valid ghosts). Without this weight the plain-u momentum lets the
1195 // projection drag gas along with the moving porosity at zero inertia cost — a spurious energy
1196 // source that pumps the particles through the drag (measured in the HCS benchmark).
1197 if (porous_ && porousCons_)
1198 updateEpsRho();
1199 // Variable properties / implicit drag: rebuild the diffusion stencil from the current mu/rho
1200 // and drag_beta fields (the implicit-FOU path rebuilds it per Picard in buildAdvStencil*, so
1201 // only the non-advective path needs this).
1202 if (((varProps_ || varRho_ || hasDrag_ || (porous_ && porousCons_)) || dtDirty_) &&
1203 !implicitAdv())
1205 dtDirty_ = false; // implicit-FOU rebuilds per Picard below (reads dt_ live) — clear either way
1206 // u^n time base, fixed for the whole step (Picard lags the advecting velocity at u^k, not the
1207 // base).
1208 for (int c = 0; c < 3; ++c)
1209 Kokkos::deep_copy(old_[c], C[c].u);
1210 if (cutcellPressure_ && incremental_) {
1211 fillGhosts(P_);
1212 if (hasBc_)
1214 } // grad(P^n) for the incremental predictor (once)
1215 lastOuterIters_ = 0;
1216 for (int outer = 0; outer < outerIters_; ++outer) {
1217 const double tp0 = phaseTick();
1218 lastOuterIters_ = outer + 1;
1219 if (outerTol_ > 0)
1220 for (int c = 0; c < 3; ++c)
1221 Kokkos::deep_copy(prev_[c], C[c].u);
1222 if (advect_ || hasBc_ || (Grid::collocated && faceInterp_ >= 4 && faceInterp_ <= 7))
1223 for (int c = 0; c < 3; ++c)
1224 fillVelGhosts(c,
1225 0); // explicit ghosts (periodic + BC) for advect / mode-4 FV defect matvec
1226 // Porous advection-form compensation: the Koren/SOU/FOU advection operators are CONSERVATIVE
1227 // (flux form, ∇·(u u)), which equals the true advective transport u·∇u only for a solenoidal
1228 // advecting field. Under the volume-averaged continuity div(eps u)=0 the plain divergence
1229 // div(u) = -(1/eps) u·grad(eps) != 0, and the flux form silently adds the spurious force
1230 // +u(div u) — largest where grad(eps) is large (clusters), where it pumps particle kinetic
1231 // energy through the drag with no physical source (measured: HCS variance rising ~x30 past
1232 // the clustering plateau). Compensate by subtracting u_f·div(u)_f from the advection in the
1233 // RHS (the exact identity u·∇u = ∇·(uu) − u∇·u; div(u) at the face = mean of the two cell
1234 // divergences). Gated on porous_ so every other path is byte-identical.
1235 if (porous_ && advect_)
1236 computeDivAdv();
1237 for (int c = 0; c < 3; ++c) // RHS from u^n base + advection lagged at u^k
1238 effVarRho() ? buildRhsVar(c) : (hasCellForce_ ? buildRhsForced(c) : buildRhs(c));
1239 // Implicit-FOU: rebuild the IBM velocity stencil = backward-Euler diffusion + rho*FOU(u^k),
1240 // then re-apply the cut-cell bake. Per Picard iteration (advecting velocity changes). Applies
1241 // to the IBM (periodic/porous) path when the user opts in, AND ALWAYS to the domain-BC
1242 // stencil path (inflow/outflow) -- implicitAdv() -> fully-implicit upwind advection (stable
1243 // at large dt). The velocity-MG BC path keeps its own FOU coarse operator.
1244 if (implicitAdv() && (!hasBc_ || !useVelocityMg_))
1245 for (int c = 0; c < 3; ++c)
1246 (varProps_ || effVarRho()) ? buildAdvStencilVar(c) : buildAdvStencil(c);
1247 // Outflow backflow stabilization: dissipate reverse flow at the outlet in the momentum
1248 // operator used by the domain-BC stencil smoother (prevents backflow divergence). Inert
1249 // without reversal.
1250 if (bcStencilPath() && backflowBeta_ > 0.0 && hasOutflow_)
1251 for (int c = 0; c < 3; ++c)
1253 // upwind-convective velocity-MG: restrict the (frozen u^k) advecting velocity to the coarse
1254 // levels ONCE, before the per-component solves update it (shared across the 3 momentum
1255 // components).
1256 if (useVelocityMg_ && implicitFou_ && advect_ && !hasBc_)
1257 vmg_.restrictAdvVelocities(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u));
1258 const double tp1 = phaseTick();
1259 tPredictor_ += tp1 - tp0;
1260 for (int c = 0; c < 3; ++c)
1261 smoothComp(c); // per-component IBM implicit-diffusion solve
1262 const double tp2 = phaseTick();
1263 tMomentum_ += tp2 - tp1;
1264 // The porous (volume-averaged) projection lives entirely on the cut-cell operator rails
1265 // (divergOpenEps + buildPorousCoeff* into CutcellMG). Without set_solid /
1266 // set_pressure_geometry there is NO projection at all — the gas would never accelerate to
1267 // the interstitial velocity in a bed and the drag comes out ~5x too weak (a fluidized bed
1268 // quietly refuses to fluidize). Fail loudly instead of silently dropping the constraint.
1269 if (porous_ && !cutcellPressure_)
1270 throw std::runtime_error(
1271 "set_porous_continuity(True) requires the cut-cell pressure operator: call "
1272 "set_solid(...) or set_pressure_geometry(all-fluid SDF) before stepping (a "
1273 "domain-BC-only box otherwise runs with NO continuity constraint at all)");
1274 if (cutcellPressure_)
1275 project(); // cut-cell projection -> incompressible
1276 tProjection_ += phaseTick() - tp2;
1277 if (hasBc_)
1278 for (int c = 0; c < 3; ++c)
1279 applyVelocityBcComp(c, 0, false); // re-impose domain BCs (keep outflow)
1280 if (outerTol_ > 0) { // outer convergence: max velocity change over this Picard iteration
1281 double corr = 0.0;
1282 for (int c = 0; c < 3; ++c)
1283 corr = Kokkos::fmax(corr, maxAbsDiffInner(CCConst(C[c].u), CCConst(prev_[c])));
1284 lastOuterCorr_ = corr;
1285 if (corr < outerTol_)
1286 break;
1287 }
1288 }
1289 // Segregated multiphysics: advance any transported scalars with the just-projected
1290 // divergence-free velocity (properties frozen over the step). No-op (byte-identical) when no
1291 // scalar is registered.
1293 tStep_ = phaseTick() - ts0;
1294 }
1295
1296 // velocity component c (0=u,1=v,2=w) on the inner cells, flat x-fastest [nx*ny*nz].
1297 std::vector<double> getVelocity(int c) { return gatherInner(C[c].u); }
1298 // The divergence-free FACE velocity component (collocated: the projected MAC face field
1299 // uf_/vf_/wf_, exactly div-free; staggered: C[c].u already lives on the faces). For a periodic
1300 // bed its mean is the momentum-balance superficial velocity, unperturbed by the openness-aware
1301 // cell gradient correction (projectCorrectCenter) that biases the cell-field mean at cut cells.
1302 std::vector<double> getFaceVelocity(int c) {
1303 if constexpr (Grid::collocated) {
1304 CCField fa[3] = {uf_, vf_, wf_};
1305 return gatherInner(fa[c]);
1306 } else {
1307 return gatherInner(C[c].u);
1308 }
1309 }
1310 // TEMP DIAGNOSTIC: the face openness (fluid area fraction) used by the cut-cell projection.
1311 // component c: 0 -> ox_ (low -x face of each inner cell), 1 -> oy_, 2 -> oz_. Grid-independent
1312 // (built once from the SDF). Exposed to compare the open-weighted superficial flux against the
1313 // raw velocity mean.
1314 std::vector<double> getOpenness(int c) {
1315 CCField o[3] = {ox_, oy_, oz_};
1316 return gatherInner(o[c]);
1317 }
1318 // The openness whose face fluxes the PROJECTION conserves: the binary (COUPLED) openness in
1319 // ghost-projection mode (oxb_ — the geometric ox_ stays a diagnostic there), the geometric
1320 // cut-cell openness otherwise. This is what flux bookkeeping downstream of the solve must use
1321 // (e.g. peclet.pnm's extract_network_flow): sum(o_proj*u*A) over a cell's faces IS the
1322 // discrete divergence the projection drives to zero.
1323 std::vector<double> getOpennessProj(int c) {
1324 const bool gp = ghostProjection_ && oxb_.extent(0) > 0;
1325 CCField o[3] = {gp ? oxb_ : ox_, gp ? oyb_ : oy_, gp ? ozb_ : oz_};
1326 return gatherInner(o[c]);
1327 }
1328 std::vector<double> getPressure() {
1329 // Incremental scheme: P_ accumulates the physical pressure. Classical Chorin (!incremental_):
1330 // derive it on demand from the last projection potential, p = (rho/dt)*phi (CUDA
1331 // press_from_phi_k).
1332 if (incremental_)
1333 return gatherInner(P_);
1334 std::vector<double> out = gatherInner(phi_);
1335 const double ct = rho_ / dt_;
1336 for (double& x : out)
1337 x *= ct;
1338 return out;
1339 }
1341 if (!cutcellPressure_)
1342 return 0.0;
1343 if constexpr (Grid::collocated) {
1344 // Report the residual of the PROJECTED face field uf_ (made divergence-free by project(),
1345 // ghosts filled). Re-averaging the central-difference-corrected CELL field would instead show
1346 // the inherent O(h^2) approximate-projection cell divergence -- a property of the scheme, not
1347 // the solver residual. At an outflow, re-impose the zero-gradient face (matching the
1348 // staggered diagnostic, whose fillVelGhosts overwrites the mass-conserving outflow
1349 // correction): the operator zeroes the alpha-divergence, but the raw beta-divergence at the
1350 // open-boundary corner is otherwise spurious.
1351 if (hasOutflow_) {
1352 B3 e{e_.x, e_.y, e_.z};
1353 CCField fa[3] = {uf_, vf_, wf_};
1354 for (int a = 0; a < 3; ++a)
1355 if (bc_[2 * a + 1] == 3)
1356 bcNeumannGhost(fa[a], e, G, a, 1);
1357 }
1358 if (ghostProjection_ && gpNRows_ >= 0) {
1359 // Ghost mode: the closed point divergence of the projected face field (same kernel pair
1360 // as the RHS) — the mode's true residual.
1361 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(oxb_), CCConst(oyb_),
1362 CCConst(ozb_), div_, e_, G);
1363 gpDivergDelta(div_, CCConst(uf_), CCConst(vf_), CCConst(wf_), gpOv_, gpNRows_,
1364 C3{nx_, ny_, nz_}, e_, G, distributed_);
1365 } else
1366 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(ox_), CCConst(oy_),
1367 CCConst(oz_), div_, e_, G);
1368 } else {
1369 for (int c = 0; c < 3; ++c)
1370 fillVelGhosts(c, 0); // ghosts incl. outflow zero-gradient before the divergence
1371 if (ghostProjection_ && gpNRows_ >= 0) {
1372 // Ghost mode: the closed point divergence (same kernels as the RHS) IS the true residual
1373 // of the mode. (EXPLICIT sliver faces read the corrected stored value here vs u* in the
1374 // RHS — the only, and rare, departure from the exact identity.)
1375 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(oxb_), CCConst(oyb_),
1376 CCConst(ozb_), div_, e_, G);
1377 gpDivergDelta(div_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), gpOv_, gpNRows_,
1378 C3{nx_, ny_, nz_}, e_, G, distributed_);
1379 } else
1380 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
1381 CCConst(oz_), div_, e_, G);
1382 }
1383 double m = reduceMaxAbsInner(CCConst(div_));
1384#ifdef PECLET_FLOW_MPI
1385 if (distributed_) {
1386 double g = 0;
1387 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
1388 return g;
1389 }
1390#endif
1391 return m;
1392 }
1393 // Residual of the volume-averaged continuity, max|div(open*eps*u) + d(eps)/dt| — the quantity the
1394 // porous projection actually drives to zero (NOT the velocity divergence, which is -d(eps)/dt !=
1395 // 0 in a fluidizing bed). Meaningful only with set_porous_continuity(True); returns 0 otherwise.
1397 if (!porous_ || !cutcellPressure_)
1398 return 0.0;
1399 for (int c = 0; c < 3; ++c)
1400 fillVelGhosts(c, 0);
1401 fillPorousEpsGhosts(); // the SAME eps ghost policy the projection used (the coupling deposit
1402 // rewrites the ghosts between project() and this diagnostic)
1403 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
1404 CCConst(oz_), CCConst(epsField_), div_, e_, G);
1405 { // add back the SAME d(eps)/dt source the projection used (depsdt_ from the last project())
1406 CCExec space;
1407 C3 e = e_; // local copy — capturing e_ in the KOKKOS_LAMBDA would read this-> on the device
1408 CCField d = div_, dd = depsdt_;
1409 const bool useDt = porousDepsDt_;
1410 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1411 Kokkos::parallel_for(
1412 "peclet::flow::porous_resid", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1413 KOKKOS_LAMBDA(int x, int y, int z) {
1414 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
1415 if (useDt)
1416 d(i) += dd(i); // residual of the SAME constraint the projection solved
1417 });
1418 }
1419 double m = reduceMaxAbsInner(CCConst(div_));
1420#ifdef PECLET_FLOW_MPI
1421 if (distributed_) {
1422 double g = 0;
1423 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
1424 return g;
1425 }
1426#endif
1427 return m;
1428 }
1429 long lastPressureIterations() const { return lastPressureIters_; }
1430 // Per-phase wall times of the last step() in seconds, THIS RANK (device-fenced at each phase
1431 // boundary): predictor = ghost fills + RHS/advection/stencil builds, momentum = the per-component
1432 // implicit-diffusion solves, projection = the cut-cell pressure projection; step = the whole
1433 // step() (remainder = BC re-imposition, Picard bookkeeping, scalars). The allreduce pair is the
1434 // pressure solve's global-reduction tax (time in / count of MPI_Allreduce; 0 single-rank).
1435 double lastStepSeconds() const { return tStep_; }
1436 double lastPredictorSeconds() const { return tPredictor_; }
1437 double lastMomentumSeconds() const { return tMomentum_; }
1438 double lastProjectionSeconds() const { return tProjection_; }
1439 double lastPressureAllreduceSeconds() const { return mg_.allreduceSeconds(); }
1440 long lastPressureAllreduceCount() const { return mg_.allreduceCount(); }
1441 int nx() const { return nx_; }
1442 int ny() const { return ny_; }
1443 int nz() const { return nz_; }
1444
1445 private:
1446 struct Comp {
1447 CCField u, b, inhom, rscale, mask;
1448 FV AC, AW, AE, AS, AN, AB, AT;
1449 IbmOverlay ov;
1450 Kokkos::View<int*, CCMem> idMap;
1451 Kokkos::View<int, CCMem> counter;
1452 int nCut = 0;
1453 };
1454
1455 public: // nvcc forbids extended __host__ __device__ lambdas inside private/protected members.
1456 // Advection treated implicitly (implicit-FOU upwind + deferred correction): the user opt-in
1457 // (set_implicit_advection) on any path, OR the DEFAULT on the domain-BC path (inflow/outflow),
1458 // where explicit advection is unstable. The velocity-MG BC path carries its own FOU coarse
1459 // operator, so the default does not apply there (it still honours the explicit opt-in).
1460 bool implicitAdv() const { return advect_ && (implicitFou_ || (hasBc_ && !useVelocityMg_)); }
1461 // Domain-BC momentum solved via the Robust-Scaled cut-cell / FOU stencil smoother
1462 // (ibmRbgsStencilColor
1463 // + reflection-ghost BCs), not the all-fluid const-coeff fold. Needed when (a) an immersed solid
1464 // is present (cut-cell no-slip must be in the operator), or (b) advection is implicit (the FOU
1465 // upwind lives in the stencil -> stable at large dt, the fully-implicit design), or (c) any
1466 // per-cell coefficient lives in the stencil: variable properties, or the implicit CFD-DEM drag
1467 // diagonal (hasDrag_). Without (c) an all-fluid domain-BC problem fell through to the
1468 // CONST-COEFFICIENT fold smoother (Ac = rho/dt + 6mu computed inline), which never reads the
1469 // assembled band -- the drag never entered the momentum operator while the porous projection's
1470 // w_f=idt/(idt+beta_f) assumed it did, an inconsistency with pressure-loop gain beta*dt/rho (a
1471 // fixed bed diverged whenever beta > rho/dt; measured gain 3.84 vs predicted 3.85 at beta=77,
1472 // idt=20).
1473 bool bcStencilPath() const {
1474 return hasBc_ && !useVelocityMg_ &&
1475 (hasSolid_ || implicitAdv() || varProps_ || varRho_ || hasDrag_);
1476 }
1477 // Fill a property field's ghosts for the face means: periodic/halo base, then zero-gradient
1478 // (copy) on domain-BC (wall/inflow/outflow) faces — a periodic wrap there would bring the wrong
1479 // layer's value to the wall face (destabilising, especially for the harmonic mean).
1481 fillGhosts(f);
1482 if (!distributed_)
1483 for (int face = 0; face < 6; ++face)
1484 if (bc_[face] != 0)
1485 applyScalarBcFace(f, face / 2, face % 2, 1, 0.0); // type 1 = Neumann copy
1486 }
1487 void fillMuGhosts() { fillPropGhosts(muField_); }
1488 // Eps ghost policy for the porous (volume-averaged) machinery. Periodic/halo base fill, then at
1489 // non-periodic domain faces: wall -> zero-gradient; INFLOW/OUTFLOW -> mirror around 1 so the
1490 // arithmetic face mean is EXACTLY 1 (the boundary is pure gas: below the distributor and in the
1491 // freeboard eps = 1, so a prescribed inflow velocity is the SUPERFICIAL gas velocity and its face
1492 // flux is open_f*1*u — the Kuipers/MFIX distributor convention). Every consumer — the projection
1493 // RHS divergence, the Poisson coefficients, and maxPorousResidual — must use THIS fill: the
1494 // external deposit writes its own leakage into these ghosts each step, and any two consumers
1495 // reading different ghost values enforce two different constraints, which leaves an irreducible
1496 // residual (eps_f_rhs - eps_f_resid)*u_in pinned at the distributor row and feeds gas at
1497 // eps_f*U instead of U.
1499 fillGhosts(epsField_);
1500 if (!distributed_)
1501 for (int face = 0; face < 6; ++face) {
1502 const int t = bc_[face];
1503 if (t == 0)
1504 continue;
1505 if (t == 2 || t == 3)
1506 applyScalarBcFace(epsField_, face / 2, face % 2, 2, 1.0); // open face: face eps == 1
1507 else
1508 applyScalarBcFace(epsField_, face / 2, face % 2, 1, 0.0); // wall: zero-gradient
1509 }
1510 }
1511 // Staggered face stride of velocity component c (the -c face of cell i pairs cells i and i-s).
1512 long strideOf(int c) const { return (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y; }
1513 // The face-property accessor for the momentum stencil of component c: mu constant-or-field
1514 // (arithmetic/harmonic mean), rho constant-or-field (arithmetic face mean for the time diagonal —
1515 // the same face density the variable-density projection uses).
1516 // Effective variable density: true varRho, or the eps-conservative porous momentum (rho_eff =
1517 // eps*rho in epsRho_, refreshed per step by updateEpsRho).
1518 bool effVarRho() const { return varRho_ || (porous_ && porousCons_); }
1519 CCField effRhoField() { return varRho_ ? rhoField_ : epsRho_; }
1521 CCExec space;
1522 CCField er = epsRho_;
1523 CCConst ep = CCConst(epsField_);
1524 const double rho = rho_;
1525 Kokkos::parallel_for(
1526 "peclet::flow::eps_rho", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1527 KOKKOS_LAMBDA(std::size_t i) { er(i) = ep(i) * rho; });
1528 }
1531 fp.haveMu = varProps_;
1532 if (varProps_)
1533 fp.mu = CCConst(muField_);
1534 else
1535 fp.muC = mu_;
1536 fp.harmMu = harmonicMu_;
1537 fp.haveRho = effVarRho();
1538 if (effVarRho()) {
1539 fp.rho = CCConst(effRhoField());
1540 fp.idt = 1.0 / dt_;
1541 fp.sc = strideOf(c);
1542 } else
1543 fp.rhoIdtC = rho_ / dt_;
1544 return fp;
1545 }
1547 const double idiag = rho_ / dt_, beta = mu_;
1548 if (varProps_)
1549 fillMuGhosts(); // face means read mu at i +- stride (boundary inner cells -> ghosts)
1550 if (varRho_)
1551 fillPropGhosts(rhoField_);
1552 for (int c = 0; c < 3; ++c) {
1553 Kokkos::deep_copy(C[c].rscale, 1.0);
1554 Kokkos::deep_copy(C[c].inhom, 0.0);
1555 if (varProps_ || effVarRho())
1556 ibmBuildDiffusionVar(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e_.x,
1557 e_.y, e_.z, G, makeFaceProps(c));
1558 else
1559 ibmBuildDiffusion(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e_.x, e_.y,
1560 e_.z, beta, idiag);
1561 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
1562 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
1563 if (hasDrag_)
1564 addDragDiagonal(c);
1565 }
1566 }
1567 // copy the nx*ny*nz inner cells between two extended blocks of different ghost width (g=2 <-> g=1
1568 // MG).
1569 void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg) {
1570 CCExec space;
1571 const int NX = nx_, NY = ny_;
1572 Kokkos::parallel_for(
1573 "peclet::flow::copyInner", Kokkos::RangePolicy<CCExec>(space, 0, (long)nx_ * ny_ * nz_),
1574 KOKKOS_LAMBDA(long c) {
1575 const int ix = (int)(c % NX), iy = (int)((c / NX) % NY), iz = (int)(c / ((long)NX * NY));
1576 const long di =
1577 (long)(ix + dg) + (long)(iy + dg) * de.x + (long)(iz + dg) * (long)de.x * de.y;
1578 const long si =
1579 (long)(ix + sg) + (long)(iy + sg) * se.x + (long)(iz + sg) * (long)se.x * se.y;
1580 dst(di) = src(si);
1581 });
1582 }
1583 // Copy the ENTIRE destination block (including its ghost ring) from the source block at per-axis
1584 // cell offset `off`: dst(x,y,z) <- src(x+off, y+off, z+off). Bridges a G=2 field to the g=1 MG
1585 // block INCLUDING the g=1 ghosts (off = G-1), so face means at the first inner cell read a valid
1586 // neighbour. Requires the source ghosts filled (fillGhosts/fillPropGhosts) — under MPI those are
1587 // the cross-rank values, so the bridge is decomposition-correct.
1589 CCExec space;
1590 Kokkos::parallel_for(
1591 "peclet::flow::copyBlockShifted",
1592 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {de.x, de.y, de.z}),
1593 KOKKOS_LAMBDA(int x, int y, int z) {
1594 const long di = (long)x + (long)y * de.x + (long)z * (long)de.x * de.y;
1595 const long si =
1596 (long)(x + off) + (long)(y + off) * se.x + (long)(z + off) * (long)se.x * se.y;
1597 dst(di) = src(si);
1598 });
1599 }
1600 // Fill ghost width G periodically on all 3 axes (x then y then z, covering corners). Distributed:
1601 // the velocity-block halo (cross-rank + periodic, all ghosts incl. corners).
1603#ifdef PECLET_FLOW_MPI
1604 if (distributed_) {
1605 velDev_->exchange(f);
1606 return;
1607 }
1608#endif
1609 fillAxis(f, 0);
1610 fillAxis(f, 1);
1611 fillAxis(f, 2);
1612 }
1613 // Fused periodic FACE-ghost fill in ONE kernel (vs 3 fillAxis): each inner boundary cell scatters
1614 // its periodic image to the opposite face ghost, all 3 axes at once. Valid only for
1615 // FACE-neighbour (7-point) stencils -- it does NOT fill the corner/edge ghosts (which fillAxis's
1616 // sequential x->y->z does). The IBM RB-GS smoother reads only the 7-point stencil, so this is
1617 // exact there and cuts the velocity solve's dominant kernel-launch cost (~7200 -> ~2400 fill
1618 // launches/step) at low resolution. NOT for the Koren advection RHS (reads diagonals) -- keep the
1619 // full fillGhosts there.
1621#ifdef PECLET_FLOW_MPI
1622 if (distributed_) {
1623 velDev_->exchange(f);
1624 return;
1625 } // halo gives all ghosts; the 7-pt smoother uses the faces
1626#endif
1627 CCExec space;
1628 C3 e = e_;
1629 const int Nx = nx_, Ny = ny_, Nz = nz_;
1630 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
1631 CCField ff = f;
1632 Kokkos::parallel_for(
1633 "peclet::flow::ibm_facefill", Kokkos::RangePolicy<CCExec>(space, 0, (long)nx_ * ny_ * nz_),
1634 KOKKOS_LAMBDA(long n) {
1635 const int ix = (int)(n % Nx), iy = (int)((n / Nx) % Ny), iz = (int)(n / ((long)Nx * Ny));
1636 const long i = (long)(ix + G) * sx + (long)(iy + G) * sy + (long)(iz + G) * sz;
1637 if (ix < G)
1638 ff(i + (long)Nx * sx) = ff(i);
1639 else if (ix >= Nx - G)
1640 ff(i - (long)Nx * sx) = ff(i);
1641 if (iy < G)
1642 ff(i + (long)Ny * sy) = ff(i);
1643 else if (iy >= Ny - G)
1644 ff(i - (long)Ny * sy) = ff(i);
1645 if (iz < G)
1646 ff(i + (long)Nz * sz) = ff(i);
1647 else if (iz >= Nz - G)
1648 ff(i - (long)Nz * sz) = ff(i);
1649 });
1650 }
1651 void fillAxis(CCField f, int axis) {
1652 CCExec space;
1653 C3 e = e_;
1654 int N3[3] = {nx_, ny_, nz_};
1655 int dims[3] = {e.x, e.y, e.z};
1656 long st[3] = {1, e.x, (long)e.x * e.y};
1657 const int a = axis, b = (axis + 1) % 3, c = (axis + 2) % 3;
1658 const long sa = st[a], sb = st[b], sc = st[c];
1659 const int N = N3[a];
1660 CCField ff = f;
1661 Kokkos::parallel_for(
1662 "peclet::flow::ibm_pfill",
1663 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {0, 0}, {dims[b], dims[c]}),
1664 KOKKOS_LAMBDA(int p0, int p1) {
1665 const long base = (long)p0 * sb + (long)p1 * sc;
1666 for (int gl = 0; gl < G; ++gl) {
1667 ff(base + (long)gl * sa) = ff(base + (long)(gl + N) * sa);
1668 ff(base + (long)(G + N + gl) * sa) = ff(base + (long)(G + gl) * sa);
1669 }
1670 });
1671 }
1672 // Cell divergence of the current velocity iterate, on the inner cells + one ghost ring (the RHS
1673 // compensation reads div at i and i-strd, so faces at the low inner boundary need the ghost-cell
1674 // value; velocity ghosts were just filled). Porous-only scratch (divAdv_).
1676 CCExec space;
1677 C3 e = e_;
1678 CCField dv = divAdv_;
1679 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
1680 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1681 Kokkos::parallel_for(
1682 "peclet::flow::div_adv",
1683 MD(space, {G - 1, G - 1, G - 1}, {e.x - G + 1, e.y - G + 1, e.z - G + 1}),
1684 KOKKOS_LAMBDA(int x, int y, int z) {
1685 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
1686 const long i = (long)x + (long)y * sy + (long)z * sz;
1687 dv(i) = (U(i + sx) - U(i)) + (V(i + sy) - V(i)) + (W(i + sz) - W(i));
1688 });
1689 }
1690
1691 void buildRhs(int c) {
1692 CCExec space;
1693 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_;
1694 C3 e = e_;
1695 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1696 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
1697 un = CCConst(old_[c]);
1698 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
1699 // Pure implicit FOU (no deferred correction): 1st-order upwind carried entirely by the
1700 // operator, no explicit high-order term in the RHS -- maximally dissipative/stable (diffuses
1701 // sharp shear layers). Only meaningful on an implicit-advection path.
1702 const bool pureFou = implicitAdv() && !deferredCorr_;
1703 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
1704 bc = hasBc_ && !bcStencilPath(); // fold RHS only on the const-coeff domain-BC path;
1705 // on the stencil path (solid and/or implicit advection) the walls enter via reflection ghosts
1706 // (smoothComp) and the RHS carries the IBM inhom (=0 for no-slip) + the deferred correction.
1707 // incr predictor carries -grad(P^n).
1708 const bool ifou =
1709 implicitAdv() &&
1710 deferredCorr_; // deferred correction: keep (HO - FOU) explicit in the RHS
1711 // (implicit on the domain-BC path by default, opt-in elsewhere)
1712 const int sch = advScheme_; // 0 = SOU (default), 1 = Koren TVD
1713 // Mode-2 wall-aware pressure force (collocated): -grad(P) = the TRANSPOSE of the wall-aware
1714 // cell->face constraint interpolation, precomputed per component (the plain path's central
1715 // difference is the transpose of the plain 1/2-1/2 average, so this keeps the momentum/
1716 // constraint operators an adjoint pair on both paths).
1717 const bool tg = Grid::collocated && faceInterp_ >= 2 && faceInterp_ <= 5 && incr;
1718 // modes 6/7: openness-weighted -grad(P^n) predictor, matching the fs-weighted correction
1719 const bool wg = Grid::collocated && (faceInterp_ == 6 || faceInterp_ == 7) && incr;
1720 // mode 11: adjoint-aperture -grad(P^n) predictor G = -(D_a Pi)^T (centerGradAperture) --
1721 // support-consistent AND adjoint; matches the mode-11 correction so momentum and constraint
1722 // stay one operator family.
1723 const bool ag = Grid::collocated && (faceInterp_ >= 11 && faceInterp_ <= 13) && incr;
1724 // ghost mode (and the mode-9/10 cutcell-ghost hybrids): directional gpCenterGrad predictor —
1725 // the mode-0 central difference reads the decoupled P=0 at solid-centered cells, a
1726 // gauge-dependent O(1) gradient error at every cut cell (measured O(1/h) in physical units,
1727 // ghost_collocated_apriori.py [C2]).
1728 const bool gg =
1729 Grid::collocated && (ghostProjection_ || faceInterp_ == 9 || faceInterp_ == 10) && incr;
1730 if constexpr (Grid::collocated) {
1731 if (gg) {
1732 gpCenterGrad(tgp_, CCConst(P_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), c, e_, G, gauge2a_);
1733 } else if (tg) {
1734 CCField xcs[3] = {xcx_, xcy_, xcz_};
1735 CCField oax[3] = {ox_, oy_, oz_};
1736 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
1737 faceInterp_ >= 3, c, e_, G);
1738 } else if (wg) {
1739 CCField oax[3] = {ox_, oy_, oz_};
1740 centerGradOpen(tgp_, CCConst(P_), CCConst(oax[c]), c, e_, G);
1741 } else if (ag) {
1742 CCField oax[3] = {ox_, oy_, oz_};
1743 if (faceInterp_ == 12)
1744 centerGradApertureScaled(tgp_, CCConst(P_), CCConst(ox_), CCConst(oy_), CCConst(oz_), c,
1745 e_, G);
1746 else if (faceInterp_ == 13)
1747 centerGradOpenCapped(tgp_, CCConst(P_), CCConst(oax[c]), c, apertureFloor_, e_, G);
1748 else
1749 centerGradAperture(tgp_, CCConst(P_), CCConst(oax[c]), c, e_, G);
1750 }
1751 }
1752 CCConst gpw = CCConst(tgp_); // empty view on the staggered path (tg/wg/gg/ag false there)
1753 // Mode-4 fully-FV momentum via DEFECT CORRECTION: solve M·u^{k+1} = M·u^k − rs·L_FV(u^k) +
1754 // rs·b_FV so the fixed point satisfies the second-order finite-volume balance L_FV·u* = b_FV
1755 // exactly, with the (stable, small-cell-safe) IBM matrix M only as preconditioner. fvM_ = M·u^k
1756 // (stencilMatvec), fvL_ = L_FV(u^k) (fvViscousApply: o_f faces + cs time + centroid wall drag).
1757 // Interior cells: M = L_FV → the defect vanishes → byte-identical to mode 0. Stokes only
1758 // (advection folds into the IBM matrix, not yet into L_FV).
1759 // Porous advection-form compensation (+rho*u_f*div(u)_f): see the step() comment. Off (and the
1760 // view untouched) on every non-porous path.
1761 const bool pc = porous_ && advect_;
1762 CCConst dv = CCConst(divAdv_);
1763 const bool wd = Grid::collocated && faceInterp_ >= 4 && faceInterp_ <= 7;
1764 if constexpr (Grid::collocated)
1765 if (wd) {
1766 stencilMatvec(fvM_, CCConst(C[c].u), MConst(C[c].AC), MConst(C[c].AW), MConst(C[c].AE),
1767 MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB), MConst(C[c].AT), e_, G);
1768 // modes 5/6: TRUE-NORMAL embed wall drag (embedDirichletGradient); mode 4: axis-by-axis W_a
1769 // g_a
1770 if (faceInterp_ >= 5)
1771 embedViscousApply(fvL_, CCConst(C[c].u), CCConst(sdf_), CCConst(cs_), CCConst(ox_),
1772 CCConst(oy_), CCConst(oz_), mu_, rho_ / dt_, e_, G);
1773 else
1774 fvViscousApply(fvL_, CCConst(C[c].u), CCConst(sdf_), CCConst(cs_), CCConst(ox_),
1775 CCConst(oy_), CCConst(oz_), mu_, rho_ / dt_, e_, G);
1776 }
1777 CCConst fvM = CCConst(fvM_), fvL = CCConst(fvL_), cs = CCConst(cs_);
1778 const double fvw = fvRelax_; // local copy — a KOKKOS_LAMBDA must not read a member (device
1779 // deref of the host `this` pointer = illegal memory access)
1780 // b = descale*(idiag*u^n - rho*Koren(u^k) + rho*FOU(u^k) + f - grad P^n) - inhom (+ BC fold
1781 // brhs). The time base is u^n (Picard); the advecting velocity & advected field are the current
1782 // iterate u^k.
1783 ccFor3(
1784 "rhs", C3{G, G, G}, C3{e.x - G, e.y - G, e.z - G},
1785 KOKKOS_LAMBDA(int x, int y, int z) {
1786 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1787 double aK = 0.0, aF = 0.0;
1788 if (adv) {
1789 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{uu, e.x, e.y};
1790 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1791 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1792 if (ifou)
1793 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1794 }
1795 // incremental predictor's -grad(P^n): central-difference cell gradient on the collocated
1796 // grid (or the wall-aware transpose gradient, mode 2), one-sided face gradient (P at the
1797 // high cell of the staggered face) on the staggered grid.
1798 const double gp =
1799 !incr ? 0.0
1800 : Grid::collocated
1801 ? ((tg || wg || gg || ag) ? gpw(i) : 0.5 * (P((long)i + strd) - P((long)i - strd)))
1802 : (P(i) - P((long)i - strd));
1803 if (wd) { // FV defect-correction RHS M·u − ω·rs·(L_FV·u − b_FV), b_FV = idt·cs·u^n +
1804 // cs·(f − grad P). ω<1 damps the (stiff, explicit-lagged) wall-flux
1805 // correction; the fixed point L_FV·u* = b_FV is independent of ω.
1806 const double bfv = idiag * cs(i) * un(i) + cs(i) * (fc - gp);
1807 bb(i) = fvM(i) - fvw * rs(i) * (fvL(i) - bfv);
1808 } else {
1809 const double comp = pc ? rho * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
1810 bb(i) = rs(i) * (idiag * un(i) + fc - rho * aK + rho * aF + comp - gp) +
1811 (bc ? brhs(i) : -inh(i));
1812 }
1813 }); // BC fold (brhs) on the domain-BC path; -inhom on the IBM path (=0 for no-slip)
1814 }
1815 // Sibling of buildRhs adding a per-cell body force fb(i) (Boussinesq buoyancy / CFD-DEM
1816 // feedback): the constant fc becomes fc + fb(i). Kept as a separate kernel so buildRhs stays
1817 // byte-identical (no codegen drift on the single-phase path). Selected in step() when
1818 // hasCellForce_.
1819 void buildRhsForced(int c) {
1820 CCExec space;
1821 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_;
1822 C3 e = e_;
1823 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1824 CCConst fb = CCConst(cellForce_[c]);
1825 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
1826 un = CCConst(old_[c]);
1827 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
1828 const bool pureFou = implicitAdv() && !deferredCorr_;
1829 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
1830 bc = hasBc_ && !bcStencilPath();
1831 const bool ifou = implicitAdv() && deferredCorr_;
1832 const int sch = advScheme_;
1833 // Porous advection-form compensation (+rho*u_f*div(u)_f): see the step() comment.
1834 const bool pc = porous_ && advect_;
1835 CCConst dv = CCConst(divAdv_);
1836 const bool tg = Grid::collocated && faceInterp_ >= 2 && faceInterp_ <= 5 &&
1837 incr; // wall-aware -grad(P) (mode 2/3)
1838 const bool gg =
1839 Grid::collocated && (ghostProjection_ || faceInterp_ == 9 || faceInterp_ == 10) &&
1840 incr; // directional ghost -grad(P)
1841 const bool ag =
1842 Grid::collocated && (faceInterp_ >= 11 && faceInterp_ <= 13) && incr; // adjoint-aperture
1843 if constexpr (Grid::collocated) {
1844 if (gg) {
1845 gpCenterGrad(tgp_, CCConst(P_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), c, e_, G, gauge2a_);
1846 } else if (tg) {
1847 CCField xcs[3] = {xcx_, xcy_, xcz_};
1848 CCField oax[3] = {ox_, oy_, oz_};
1849 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
1850 faceInterp_ >= 3, c, e_, G);
1851 } else if (ag) {
1852 CCField oax[3] = {ox_, oy_, oz_};
1853 if (faceInterp_ == 12)
1854 centerGradApertureScaled(tgp_, CCConst(P_), CCConst(ox_), CCConst(oy_), CCConst(oz_), c,
1855 e_, G);
1856 else if (faceInterp_ == 13)
1857 centerGradOpenCapped(tgp_, CCConst(P_), CCConst(oax[c]), c, apertureFloor_, e_, G);
1858 else
1859 centerGradAperture(tgp_, CCConst(P_), CCConst(oax[c]), c, e_, G);
1860 }
1861 }
1862 CCConst gpw = CCConst(tgp_);
1863 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1864 Kokkos::parallel_for(
1865 "rhs_forced", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1866 KOKKOS_LAMBDA(int x, int y, int z) {
1867 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1868 double aK = 0.0, aF = 0.0;
1869 if (adv) {
1870 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{uu, e.x, e.y};
1871 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1872 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1873 if (ifou)
1874 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1875 }
1876 const double gp = !incr ? 0.0
1877 : Grid::collocated
1878 ? ((tg || gg || ag) ? gpw(i)
1879 : 0.5 * (P((long)i + strd) - P((long)i - strd)))
1880 : (P(i) - P((long)i - strd));
1881 const double comp = pc ? rho * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
1882 bb(i) = rs(i) * (idiag * un(i) + fc + fb(i) - rho * aK + rho * aF + comp - gp) +
1883 (bc ? brhs(i) : -inh(i));
1884 });
1885 }
1886 // Variable-density RHS (sibling of buildRhsForced): the time term, the advection weight, and the
1887 // per-cell body force all use the FACE density of component c (arithmetic mean over the staggered
1888 // face, matching VarFaceProps::idiag and the projection coefficient — this three-way consistency
1889 // is what makes discrete hydrostatic balance exact). The cell force fb is face-interpolated for
1890 // the same reason (a rho*g cell field becomes rho_face*g at the velocity location). Requires the
1891 // rho ghosts filled (rebuildStencils / buildAdvStencilVar did it this step).
1892 void buildRhsVar(int c) {
1893 CCExec space;
1894 const double idt = 1.0 / dt_, fc = f_[c];
1895 C3 e = e_;
1896 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1897 CCConst fb = CCConst(cellForce_[c]);
1899 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
1900 un = CCConst(old_[c]);
1901 const long strd = strideOf(c);
1902 const bool pureFou = implicitAdv() && !deferredCorr_;
1903 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
1904 bc = hasBc_ && !bcStencilPath();
1905 const bool ifou = implicitAdv() && deferredCorr_;
1906 const int sch = advScheme_;
1907 // Porous advective-form compensation, weighted by the face density (rho_eff = eps*rho): the
1908 // eps-weighted ADVECTIVE form eps*rho*(du/dt + u.grad u) IS the conservative volume-averaged
1909 // momentum given the enforced continuity (the u*[d(eps)/dt + div(eps u)] bracket vanishes).
1910 const bool pc = porous_ && advect_;
1911 CCConst dv = CCConst(divAdv_);
1912 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1913 Kokkos::parallel_for(
1914 "rhs_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1915 KOKKOS_LAMBDA(int x, int y, int z) {
1916 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1917 const double rhoF = 0.5 * (rf(i) + rf(i - strd)); // face density of the velocity unknown
1918 double aK = 0.0, aF = 0.0;
1919 if (adv) {
1920 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{uu, e.x, e.y};
1921 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1922 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1923 if (ifou)
1924 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1925 }
1926 const double gp = !incr ? 0.0
1927 : Grid::collocated ? 0.5 * (P((long)i + strd) - P((long)i - strd))
1928 : (P(i) - P((long)i - strd));
1929 const double fbF = 0.5 * (fb(i) + fb(i - strd));
1930 const double comp = pc ? rhoF * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
1931 bb(i) = rs(i) * (rhoF * idt * un(i) + fc + fbF - rhoF * aK + rhoF * aF + comp - gp) +
1932 (bc ? brhs(i) : -inh(i));
1933 });
1934 }
1935 // Implicit-FOU velocity stencil (CUDA build_adv_stencil_k + ibm_modify_stencil): backward-Euler
1936 // diffusion (idiag+6beta diag, -beta off) + rho*FOU(u^k) upwind operator (diagonally dominant ->
1937 // stable at high Re), then the Robust-Scaled cut-cell bake. The advecting velocity u^k = the
1938 // current C[*].u (ghosts filled).
1939 void buildAdvStencil(int c) {
1940 const double idiag = rho_ / dt_, beta = mu_, fouw = rho_;
1941 C3 e = e_;
1942 ibmBuildDiffusion(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e.x, e.y, e.z,
1943 beta, idiag);
1944 CCExec space;
1945 FV AC = C[c].AC, AW = C[c].AW, AE = C[c].AE, AS = C[c].AS, AN = C[c].AN, AB = C[c].AB,
1946 AT = C[c].AT;
1947 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
1948 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1949 Kokkos::parallel_for(
1950 "advstencil", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1951 KOKKOS_LAMBDA(int x, int y, int z) {
1952 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1953 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
1954 czp = AT(i);
1955 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
1956 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp);
1957 AC(i) = (float)cC;
1958 AW(i) = (float)cxm;
1959 AE(i) = (float)cxp;
1960 AS(i) = (float)cym;
1961 AN(i) = (float)cyp;
1962 AB(i) = (float)czm;
1963 AT(i) = (float)czp;
1964 });
1965
1966 Kokkos::deep_copy(C[c].rscale, 1.0);
1967 Kokkos::deep_copy(C[c].inhom, 0.0);
1968 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
1969 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
1970 if (hasDrag_)
1971 addDragDiagonal(c);
1972 }
1973 // Variable-property sibling of buildAdvStencil: VarFaceProps diffusion build (per-face mu, face-
1974 // density time diagonal) + the FOU upwind weighted by the FACE density (constant path:
1975 // fouw=rho_). Separate kernel so the validated buildAdvStencil stays byte-identical.
1977 C3 e = e_;
1978 if (c == 0) {
1979 if (varProps_)
1980 fillMuGhosts();
1981 if (varRho_)
1982 fillPropGhosts(rhoField_);
1983 if (!varRho_ && porous_ && porousCons_)
1984 updateEpsRho(); // eps ghosts are driver-filled; whole-block product has valid ghosts
1985 }
1986 ibmBuildDiffusionVar(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e.x, e.y,
1987 e.z, G, makeFaceProps(c));
1988 CCExec space;
1989 FV AC = C[c].AC, AW = C[c].AW, AE = C[c].AE, AS = C[c].AS, AN = C[c].AN, AB = C[c].AB,
1990 AT = C[c].AT;
1991 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
1992 const bool vr = effVarRho();
1993 const double rhoC = rho_;
1995 const long sc = strideOf(c);
1996 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1997 Kokkos::parallel_for(
1998 "advstencil_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1999 KOKKOS_LAMBDA(int x, int y, int z) {
2000 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2001 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
2002 czp = AT(i);
2003 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
2004 const double fouw = vr ? 0.5 * (rf(i) + rf(i - sc)) : rhoC;
2005 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp);
2006 AC(i) = (float)cC;
2007 AW(i) = (float)cxm;
2008 AE(i) = (float)cxp;
2009 AS(i) = (float)cym;
2010 AN(i) = (float)cyp;
2011 AB(i) = (float)czm;
2012 AT(i) = (float)czp;
2013 });
2014 Kokkos::deep_copy(C[c].rscale, 1.0);
2015 Kokkos::deep_copy(C[c].inhom, 0.0);
2016 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
2017 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
2018 if (hasDrag_)
2019 addDragDiagonal(c);
2020 }
2021 // Backflow stabilization (Bazilevs 2009 / Esmaily-Moghadam 2011) for the NORMAL momentum at
2022 // outflow faces: add the dissipative diagonal term beta*rho*|min(u.n,0)| where the outflow
2023 // reverses (fluid re-entering, u.n<0). This removes the spurious kinetic-energy influx that the
2024 // do-nothing/zero- gradient outflow advects in -- the "backflow divergence" that blows up
2025 // separated flows (e.g. the BFS recirculation reaching the outlet), worse on finer grids. Purely
2026 // dissipative (u_ext=0), so it is implicit + unconditionally stable, and INERT where the outlet
2027 // is outgoing (u.n>=0) -> the channel and any non-reversing outflow stay byte-identical. Applied
2028 // to C[c].AC after buildAdvStencil (per Picard iteration, lagged at u^k); only the component
2029 // normal to each outflow face.
2030 void applyBackflowStab(int c) {
2031 if (backflowBeta_ <= 0.0 || !hasOutflow_)
2032 return;
2033 CCExec space;
2034 const double beta = backflowBeta_, rho = rho_;
2035 C3 e = e_;
2036 int dims[3] = {e.x, e.y, e.z};
2037 long st[3] = {1, e.x, (long)e.x * e.y};
2038 FV AC = C[c].AC;
2039 CCConst u = CCConst(C[c].u);
2040 const int a = c; // the normal component of a face on axis a is component a
2041 for (int s = 0; s < 2; ++s) {
2042 if (bc_[2 * a + s] != 3)
2043 continue; // outflow faces only
2044 const long sa = st[a];
2045 const int na = dims[a];
2046 const int bic = (s == 0) ? G : (na - G - 1); // outflow-adjacent inner normal-velocity cell
2047 const double sgn = (s == 0) ? 1.0 : -1.0; // reversal (u.n<0): u>0 at -a, u<0 at +a
2048 const int b = (a + 1) % 3, cc = (a + 2) % 3;
2049 const long sb = st[b], sc = st[cc];
2050 using MD2 = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>;
2051 Kokkos::parallel_for(
2052 "peclet::flow::backflow", MD2(space, {G, G}, {dims[b] - G, dims[cc] - G}),
2053 KOKKOS_LAMBDA(int p0, int p1) {
2054 const long i = (long)p0 * sb + (long)p1 * sc + (long)bic * sa;
2055 const double back =
2056 sgn * u(i); // > 0 exactly where the outflow reverses (|min(u.n,0)|)
2057 if (back > 0.0)
2058 AC(i) += (float)(beta * rho * back); // dissipative diagonal (u_ext = 0)
2059 });
2060 }
2061 }
2062 // max|a-b| over inner cells (Picard outer-tolerance check).
2064 CCExec space;
2065 C3 e = e_;
2066 double m = 0;
2067 Kokkos::parallel_reduce(
2068 "maxdiff",
2069 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
2070 {e.x - G, e.y - G, e.z - G}),
2071 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
2072 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2073 const double d = Kokkos::fabs(a(i) - b(i));
2074 if (d > acc)
2075 acc = d;
2076 },
2077 Kokkos::Max<double>(m));
2078 return m;
2079 }
2080 // Shared momentum RB-GS loop: fixed velIters_ sweeps, or (velTol_ > 0) the tolerance stop —
2081 // colour 0 plain, colour 1 via the fused max-increment kernel, stop once the increment has
2082 // contracted to velTol_ of the first sweep's. The decision is rank-uniform under MPI (all ranks
2083 // see the same global max), so per-sweep halo exchanges stay in lockstep.
2084 template <class Fill, class Color, class ColorDu>
2086 double du0 = 0.0;
2087 int used = velIters_;
2088 for (int it = 0; it < velIters_; ++it) {
2089 fill();
2090 sweepColor(0);
2091 fill();
2092 if (velTol_ > 0.0) {
2093 double du = sweepColorDu(1);
2094#ifdef PECLET_FLOW_MPI
2095 if (distributed_) {
2096 double gd = 0.0;
2098 du = gd;
2099 }
2100#endif
2101 if (it == 0)
2102 du0 = du;
2103 if (it + 1 >= velMinIters_ && du <= velTol_ * du0) {
2104 used = it + 1;
2105 break;
2106 }
2107 } else {
2108 sweepColor(1);
2109 }
2110 }
2111 lastMomentumSweeps_ += used;
2112 }
2113
2114 void smoothComp(int c) {
2115 if constexpr (Grid::collocated) {
2116 if (hasBc_) { // collocated domain BC: the (all-fluid) IBM diffusion stencil + cell-centered
2117 // wall
2118 // reflection ghosts refreshed each colour (explicit no-slip; no fold). Converges to the
2119 // wall value.
2121 [&] { fillVelGhostsTo(C[c].u, c, 0); },
2122 [&](int col) {
2123 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2124 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2125 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G,
2126 col);
2127 },
2128 [&](int col) {
2129 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), MConst(C[c].AC),
2130 MConst(C[c].AW), MConst(C[c].AE), MConst(C[c].AS),
2131 MConst(C[c].AN), MConst(C[c].AB), MConst(C[c].AT),
2132 CCConst(C[c].mask), e_, og_, G, col);
2133 });
2134 return;
2135 }
2136 }
2137 if (bcStencilPath()) {
2138 // Domain BCs solved with the Robust-Scaled cut-cell / FOU stencil (built by setSolid /
2139 // buildAdvStencil) while refreshing the domain-BC ghosts each colour -- explicit walls/inflow
2140 // (reflection, fold=0) + outflow zero-gradient. Mirrors the collocated path above. Used for
2141 // an immersed solid (cut-cell no-slip in the operator) and/or implicit advection (FOU upwind
2142 // in the stencil -> stable at large dt). The const-coeff fold smoothers below are all-fluid,
2143 // diffusion-only: they ignore the solid AND run advection explicitly (CFL-limited).
2145 [&] { fillVelGhostsTo(C[c].u, c, 0); },
2146 [&](int col) {
2147 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2148 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
2149 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
2150 },
2151 [&](int col) {
2152 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2153 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2154 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_,
2155 og_, G, col);
2156 });
2157 return;
2158 }
2159 if (hasBc_ &&
2160 useVelocityMg_) { // domain-BC velocity multigrid: const-coeff aniso op + no-slip/inflow/
2161 // outflow boundary fold on every level (CUDA setDiffusionConstAllLevels +
2162 // setDiffusionBoundaryFold).
2163 vmg_.setDomainBcOp(c, mu_, rho_ / dt_); // per component (the fold is component-dependent)
2165 c, 1); // set the level-0 boundary ghosts (wall fold=0, inflow value, outflow zero-grad)
2166 // Re-impose the velocity BC on the vel-MG's level-0 iterate each colour/residual (the
2167 // const-coeff smoother updates the held Dirichlet faces) -> the vel-MG converges to the RB-GS
2168 // fixed point (not the ~2% drift CUDA's vmg leaves at the boundary corners).
2169 vmg_.setBcApplyL0([this, c](CCField x) { fillVelGhostsTo(x, c, 1); });
2170 vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8);
2171 return;
2172 }
2173 if (hasBc_) { // domain-BC (no immersed solid): CUDA's double const-coeff diff_k + dcorr fold
2174 const I3 e{e_.x, e_.y, e_.z}, og{0, 0, 0};
2175 const double beta = mu_, Ac = rho_ / dt_ + 6.0 * mu_;
2177 [&] { fillVelGhosts(c, 1); }, // re-impose wall faces (fold) before each color
2178 [&](int col) {
2179 diffSmoothColor(C[c].u, CCConst(C[c].b), e, og, G, beta, Ac, col, CCConst(bcDcorr_[c]));
2180 },
2181 [&](int col) {
2182 return diffSmoothColorDu(C[c].u, CCConst(C[c].b), e, og, G, beta, Ac, col,
2183 CCConst(bcDcorr_[c]));
2184 });
2185 return;
2186 }
2187 if (useVelocityMg_) { // IBM velocity multigrid: fine = sharp As_[c]; coarse op depends on the
2188 // regime.
2189 vmg_.setFineStencil(FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
2190 FPC(C[c].AB), FPC(C[c].AT));
2191 if (implicitFou_ && advect_) {
2192 // UPWIND-CONVECTIVE coarse op (advection-dominated): aniso const-coeff diffusion + dt*FOU
2193 // from the restricted advecting velocity (restrictAdvVelocities ran once in step()). No pin
2194 // / no exclude mask.
2195 vmg_.buildUpwindCoarse(c, mu_, rho_ / dt_, rho_);
2196 } else {
2197 // STAIRCASE coarse op (diffusion-only): theta classification + clean-fluid exclude (exact
2198 // == RB-GS).
2199 const Off3 off =
2200 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
2201 ibmVolfrac(vmgTheta_, CCConst(sdf_), e_, off);
2202 ibmCleanFluidMask(vmgClean_, CCConst(sdf_), e_, off);
2203 vmg_.setStaircase(CCConst(vmgTheta_), CCConst(C[c].mask), CCConst(vmgClean_), mu_,
2204 rho_ / dt_, 0.5);
2205 }
2206 vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8);
2208 c); // re-impose no-slip at solid (the masked solve leaves them at the pin value)
2209 return;
2210 }
2211 // IBM / periodic: Robust-Scaled cut-cell stencil (float). The 7-point smoother reads faces
2212 // only -> the fused 1-kernel face fill suffices.
2213#ifdef PECLET_FLOW_MPI
2214 if (distributed_ && caMomentum_) {
2215 // Communication-avoiding pair (the momentum counterpart of CutcellMG::smooth's CA path):
2216 // ONE 2-deep exchange per red-black pair instead of one per colour — the velocity block is
2217 // g=2 already. Colour 0 overlaps the exchange with the interior sweep, then sweeps the
2218 // boundary shell PLUS the 1-deep ghost ring, redundantly recomputing the neighbour's
2219 // boundary cells from the same operands the neighbour uses (2-deep u ghosts; the ring rows
2220 // of the stencil/mask/rhs are exchanged below, so they are the owner's bit-exact values).
2221 // Colour 1 then sweeps with NO exchange: its boundary cells read only colour-0 ring cells,
2222 // which equal what a fresh exchange would have delivered — bit-identical at half the halo
2223 // events. The tolerance stop's colour-1 kernel is the ORIGINAL full-inner fused reduction
2224 // (host pencil form intact), so du matches the blocking path exactly.
2225 // Stencil + mask ring exchange: once per (re)build. The per-step machinery (implicit-FOU
2226 // Picard rebuilds, variable properties, implicit drag, eps-conservative porous) rewrites the
2227 // stencil every solve, so those paths re-exchange every solve — mirrors the step() rebuild
2228 // gates; a false positive costs 8 extra exchanges, a false negative would break the np>1
2229 // bit-exactness (the ring rows would read a stale operator).
2230 const bool perStepStencil =
2231 implicitAdv() || varProps_ || varRho_ || effVarRho() || hasDrag_;
2232 if (momStencilDirty_[c] || perStepStencil) {
2233 for (FV* a : {&C[c].AC, &C[c].AW, &C[c].AE, &C[c].AS, &C[c].AN, &C[c].AB, &C[c].AT})
2234 velDevF_->exchange(*a);
2235 velDev_->exchange(C[c].mask);
2236 momStencilDirty_[c] = false;
2237 }
2238 velDev_->exchange(C[c].b); // rhs ring (owner's inner values); fixed over the sweeps
2239 const C3 lo{G + 1, G + 1, G + 1}, hi{e_.x - G - 1, e_.y - G - 1, e_.z - G - 1};
2240 const C3 rlo{G - 1, G - 1, G - 1}, rhi{e_.x - G + 1, e_.y - G + 1, e_.z - G + 1};
2241 const C3 z0{0, 0, 0};
2243 [] {},
2244 [&](int col) {
2245 if (col == 0) {
2246 velDev_->exchangeBegin(C[c].u);
2247 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2248 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2249 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_,
2250 col, lo, hi, z0, z0);
2251 velDev_->exchangeEnd(C[c].u);
2252 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2253 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2254 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_,
2255 col, rlo, rhi, lo, hi);
2256 } else {
2257 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2258 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2259 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G,
2260 col);
2261 }
2262 },
2263 [&](int col) {
2264 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2265 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2266 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_,
2267 og_, G, col);
2268 });
2269 return;
2270 }
2271 if (distributed_) {
2272 // Overlap the per-colour halo with the interior sweep (the momentum counterpart of the MG
2273 // smoothers' split, 3ace962): post the exchange, sweep the interior cells — whose 7-point
2274 // stencil reads no ghost — while the messages fly, complete it, then sweep the boundary
2275 // shell. A colour's cells never read same-colour cells, so interior-then-shell is
2276 // bit-identical to the blocking exchange + full sweep; the tolerance stop's max-increment
2277 // combines the two passes by max (order-independent). Only this periodic/IBM path overlaps:
2278 // the domain-BC paths re-impose ghost BCs each colour and keep the blocking order (the same
2279 // decision as VelocityMG's overlap). The exchange is posted INSIDE the colour lambda (fill
2280 // is a no-op) so the packed send values are exactly the blocking call's.
2281 const C3 ilo{G, G, G}, ihi{e_.x - G, e_.y - G, e_.z - G};
2282 const C3 lo{G + 1, G + 1, G + 1}, hi{e_.x - G - 1, e_.y - G - 1, e_.z - G - 1};
2283 const C3 z0{0, 0, 0};
2285 [] {},
2286 [&](int col) {
2287 velDev_->exchangeBegin(C[c].u);
2288 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2289 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2290 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_,
2291 col, lo, hi, z0, z0);
2292 velDev_->exchangeEnd(C[c].u);
2293 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2294 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2295 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_, og_,
2296 col, ilo, ihi, lo, hi);
2297 },
2298 [&](int col) {
2299 velDev_->exchangeBegin(C[c].u);
2300 const double di = ibmRbgsStencilColorDuBox(
2301 C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW), MConst(C[c].AE),
2302 MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB), MConst(C[c].AT),
2303 CCConst(C[c].mask), e_, og_, col, lo, hi, z0, z0);
2304 velDev_->exchangeEnd(C[c].u);
2305 const double ds = ibmRbgsStencilColorDuBox(
2306 C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW), MConst(C[c].AE),
2307 MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB), MConst(C[c].AT),
2308 CCConst(C[c].mask), e_, og_, col, ilo, ihi, lo, hi);
2309 return di > ds ? di : ds;
2310 });
2311 return;
2312 }
2313#endif
2315 [&] { fillGhostsFaces(C[c].u); },
2316 [&](int col) {
2317 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2318 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
2319 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
2320 },
2321 [&](int col) {
2322 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
2323 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN),
2324 MConst(C[c].AB), MConst(C[c].AT), CCConst(C[c].mask), e_,
2325 og_, G, col);
2326 });
2327 }
2328 // pressure ghost at domain faces for the incremental predictor's grad(P): zero-gradient (Neumann)
2329 // at every non-periodic face so grad(P) carries no spurious force there (the periodic fill
2330 // wrapped the opposite boundary's pressure). Outflow pressure (Dirichlet p=0) is enforced
2331 // separately in the MG solve.
2333 CCExec space;
2334 C3 e = e_;
2335 CCField P = P_;
2336 int dims[3] = {e.x, e.y, e.z};
2337 long st[3] = {1, e.x, (long)e.x * e.y};
2338 for (int a = 0; a < 3; ++a)
2339 for (int s = 0; s < 2; ++s) {
2340 if (bc_[2 * a + s] == 0)
2341 continue;
2342 const int b = (a + 1) % 3, c = (a + 2) % 3;
2343 const long sa = st[a], sb = st[b], sc = st[c];
2344 const int na = dims[a];
2345 const int bic = (s == 0) ? G : (na - G - 1);
2346 const int lo = (s == 0) ? 0 : (na - G), hi = (s == 0) ? (G - 1) : (na - 1);
2347 Kokkos::parallel_for(
2348 "pbcghost",
2349 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {0, 0}, {dims[b], dims[c]}),
2350 KOKKOS_LAMBDA(int p0, int p1) {
2351 const long base = (long)p0 * sb + (long)p1 * sc;
2352 const double pin = P(base + (long)bic * sa);
2353 for (int ia = lo; ia <= hi; ++ia)
2354 P(base + (long)ia * sa) = pin;
2355 });
2356 }
2357 }
2358 // domain-BC velocity ghosts: periodic-fill periodic axes, then apply per-face BCs (fold=0
2359 // explicit/1 implicit).
2364 // Field-parameterized variants (so the velocity-MG can re-impose the BC on its own level-0
2365 // iterate).
2366 void fillVelGhostsTo(CCField f, int comp, int fold) {
2367#ifdef PECLET_FLOW_MPI
2368 if (distributed_) {
2369 velDev_->exchange(f);
2370 applyVelocityBcCompTo(f, comp, fold, true);
2371 return;
2372 }
2373#endif
2374 for (int a = 0; a < 3; ++a)
2375 if (bc_[2 * a] == 0 && bc_[2 * a + 1] == 0)
2376 fillAxis(f, a);
2377 applyVelocityBcCompTo(f, comp, fold, true);
2378 }
2380 if (!hasBc_)
2381 return;
2382 B3 e{e_.x, e_.y, e_.z};
2383 if constexpr (Grid::collocated) {
2384 // Cell-centered velocity: reflect this component about each non-periodic boundary face. Walls
2385 // (type 1, vel 0) and Dirichlet/lid (type 2, prescribed vel) both use the same reflection;
2386 // outflow (type 3) and per-position inlet profiles are the inflow/outflow milestone (phase
2387 // 5b).
2388 for (int a = 0; a < 3; ++a)
2389 for (int s = 0; s < 2; ++s) {
2390 const int ff = 2 * a + s;
2391 const int t = bc_[ff];
2392 if (t == 0)
2393 continue;
2394 if (t == 3) {
2395 if (doOutflow)
2396 bcNeumannGhost(f, e, G, a, s);
2397 continue;
2398 } // outflow: zero-gradient ghost
2399 if (bcProf_[ff].extent(0) >
2400 0) // per-position inlet profile (e.g. the BFS partial parabola)
2401 bcVelocityColocated(f, e, G, a, s, 0.0, comp, bcProf_[ff], bcProfNc_[ff]);
2402 else
2403 bcVelocityColocated(f, e, G, a, s,
2404 bcVel_[ff][comp]); // wall / inflow / lid (Dirichlet)
2405 }
2406 return;
2407 }
2408 for (int a = 0; a < 3; ++a)
2409 for (int s = 0; s < 2; ++s) {
2410 const int ff = 2 * a + s;
2411 const int t = bc_[ff];
2412 if (t == 0)
2413 continue;
2414 if (t == 3) {
2415 if (doOutflow)
2416 bcOutflowComp(f, e, G, a, s, comp, fold);
2417 continue;
2418 }
2419 if (bcProf_[ff].extent(0) > 0)
2420 bcVelocityComp(f, e, G, a, s, comp, 0.0, fold, bcProf_[ff], bcProfNc_[ff]);
2421 else
2422 bcVelocityComp(f, e, G, a, s, comp, bcVel_[ff][comp], fold);
2423 }
2424 }
2425 // implicit-diffusion wall fold (CUDA setup_bc_diffusion): dcorr += (wall:+beta tangential /
2426 // outflow:-beta), brhs += 2*beta*wall (tangential Dirichlet); bake dcorr into the per-component
2427 // stencil diagonal.
2429 const double beta = mu_;
2430 B3 e{e_.x, e_.y, e_.z};
2431 for (int c = 0; c < 3; ++c) {
2432 Kokkos::deep_copy(bcDcorr_[c], 0.0);
2433 Kokkos::deep_copy(bcBrhs_[c], 0.0);
2434 for (int a = 0; a < 3; ++a)
2435 for (int s = 0; s < 2; ++s) {
2436 const int t = bc_[2 * a + s];
2437 double dval, bval;
2438 if (t == 3) {
2439 dval = -beta;
2440 bval = 0.0;
2441 } else if (t != 0 && c != a) {
2442 dval = beta;
2443 bval = 2.0 * beta * bcVel_[2 * a + s][c];
2444 } else
2445 continue; // periodic, or the normal component at a wall (held directly)
2446 bcDiffusionFold(bcDcorr_[c], bcBrhs_[c], e, G, a, s, dval, bval);
2447 }
2448 // dcorr is passed to the (double) const-coeff smoother diffSmoothColor each sweep -- matching
2449 // CUDA diff_k (Ac + dcorr in double), NOT baked into the float stencil.
2450 }
2451 }
2452 // Incremental (rotational) cut-cell projection: solve A phi = -div_open(u*) (RB-GS,
2453 // mean-removed), u -= grad phi, then accumulate the physical pressure P += (rho/dt)*phi -
2454 // mu*div(u*) (Timmermans).
2455 // one mask-aware axis-wise smoothing pass of a cell field (the filtered-rotational S; see
2456 // setRotationalFilter). Reads the +/-1 axis neighbours' sdf: fluid-fluid -> (1,2,1)/4;
2457 // one solid side -> 1/2(self + open-side neighbour); both solid -> identity.
2458 void filterCellField(CCField f, int axis) {
2459 CCExec space;
2460 Kokkos::deep_copy(tgp_, f);
2461 fillGhosts(tgp_);
2462 CCConst src = CCConst(tgp_);
2463 CCConst sd = CCConst(sdf_);
2464 const double eps = rotFilterEps_;
2465 C3 e = e_;
2466 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
2467 Kokkos::parallel_for(
2468 "peclet::flow::rot_filter", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
2469 KOKKOS_LAMBDA(int x, int y, int z) {
2470 const long sy = e.x, sz = (long)e.x * e.y;
2471 const long i = (long)x + (long)y * sy + (long)z * sz;
2472 const long sa = (axis == 0) ? 1 : (axis == 1) ? sy : sz;
2473 if (sd(i) < 0.0)
2474 return;
2475 const bool am = sd(i - sa) >= 0.0, ap = sd(i + sa) >= 0.0;
2476 double sm;
2477 if (am && ap)
2478 sm = 0.25 * (src(i - sa) + 2.0 * src(i) + src(i + sa));
2479 else if (ap)
2480 sm = 0.5 * (src(i) + src(i + sa));
2481 else if (am)
2482 sm = 0.5 * (src(i) + src(i - sa));
2483 else
2484 sm = src(i);
2485 // eps-floor blend: S' = eps*I + (1-eps)*S. A pure S has an exact checkerboard null
2486 // space, which at dt -> infinity (where the (rho/dt)*phi term vanishes) degenerates the
2487 // fixed point into a frozen-checkerboard family; the floor keeps S' > 0 so
2488 // (rho/dt + mu S'A) phi = 0 forces phi = 0 at EVERY dt, while still cutting the
2489 // dangerous mode's feedback gain by ~1/eps.
2490 f(i) = eps * src(i) + (1.0 - eps) * sm;
2491 });
2492 }
2493 void project() {
2494 // ghosts incl. domain BCs (outflow zero-gradient) BEFORE the divergence -- matches CUDA
2495 // apply_velocity_bc before diverg_open, so div(u*) counts the outflow flux (else the rotational
2496 // pressure pumps the mis-counted outflow divergence and blows up the outflow-wall corner).
2497 if constexpr (Grid::collocated) {
2498 // Approximate (MAC) projection: average the cell velocities onto a face field, then project
2499 // THAT. Use the BC-aware ghost fill (periodic / cross-rank + domain BCs) so the averaged
2500 // inflow/outflow faces carry the right value -- at open boundaries the flux is counted
2501 // (closed walls are openness 0).
2502 for (int c = 0; c < 3; ++c)
2503 fillVelGhosts(c, 0);
2504 if ((faceInterp_ >= 1 && faceInterp_ <= 5) || faceInterp_ == 7 ||
2505 faceInterp_ == 10) // wall-aware flux map at solid
2506 centerToFaceWallAware(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u),
2507 CCConst(sdf_), CCConst(xcx_), CCConst(xcy_), CCConst(xcz_),
2508 faceInterp_ >= 3, e_, G); // faces (modes 1-5,7,10; mode 6/9 = plain)
2509 else
2510 centerToFace(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), e_, G);
2511 if (ghostProjection_) {
2512 // Collocated ghost divergence: the SAME binary-openness + closure-delta pair as the
2513 // staggered path, applied to the 1/2-1/2 face-averaged field (the closures only ever read
2514 // faces whose two adjacent centers are fluid, so the masked solid-cell zeros never enter
2515 // except at EXPLICIT slivers — faithfully modelled in the a-priori study).
2516 if (gpNRows_ < 0)
2517 throw std::runtime_error("ghost projection: call set_solid after set_ghost_projection");
2518 if (porous_ || varRho_ || useChebyshev_)
2519 throw std::runtime_error(
2520 "ghost projection: porous/variable-rho/Chebyshev unsupported (v1)");
2521 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(oxb_), CCConst(oyb_),
2522 CCConst(ozb_), div_, e_, G);
2523 gpDivergDelta(div_, CCConst(uf_), CCConst(vf_), CCConst(wf_), gpOv_, gpNRows_,
2524 C3{nx_, ny_, nz_}, e_, G, distributed_);
2525 } else
2526 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(ox_), CCConst(oy_),
2527 CCConst(oz_), div_, e_, G);
2528 } else {
2529 for (int c = 0; c < 3; ++c)
2530 fillVelGhosts(c, 0);
2531 if (porous_) { // volume-averaged continuity: div(open*eps*u*), constraint div(eps
2532 // u)=-d(eps)/dt
2533 fillPorousEpsGhosts(); // BEFORE the divergence — one eps ghost policy for RHS,
2534 // coefficients and residual (the deposit rewrites these ghosts
2535 // every step)
2536 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
2537 CCConst(oz_), CCConst(epsField_), div_, e_, G);
2538 } else if (ghostProjection_) {
2539 // Directional ghost-cell divergence: binary-openness face differences (COUPLED faces)
2540 // plus the wall-anchored closures at ghost faces, row-rescaled — the SAME kernel pair
2541 // serves the RHS here and the diagnostic in maxOpenDivergence (diagnostic == residual).
2542 if (gpNRows_ < 0)
2543 throw std::runtime_error("ghost projection: call set_solid after set_ghost_projection");
2544 if (porous_ || varRho_ || useChebyshev_)
2545 throw std::runtime_error(
2546 "ghost projection: porous/variable-rho/Chebyshev unsupported (v1)");
2547 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(oxb_), CCConst(oyb_),
2548 CCConst(ozb_), div_, e_, G);
2549 gpDivergDelta(div_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), gpOv_, gpNRows_,
2550 C3{nx_, ny_, nz_}, e_, G, distributed_);
2551 } else
2552 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
2553 CCConst(oz_), div_, e_, G);
2554 }
2555 // Porous continuity source: fold d(eps)/dt into the divergence so the Poisson solves for
2556 // div(eps u) = -d(eps)/dt (not 0). d(eps)/dt = (eps^{n+1}-eps^n)/dt from the deposited void
2557 // fraction; stored in depsdt_ (epsPrev_ is overwritten at step end, so the residual reuses
2558 // this).
2559 if (porous_) {
2560 CCExec space;
2561 C3 e = e_; // local copy — a KOKKOS_LAMBDA capturing e_ would read this-> on the device
2562 CCField d = div_, dd = depsdt_, ep = epsField_, epp = epsPrev_;
2563 const double idt = 1.0 / dt_;
2564 const bool useDt = porousDepsDt_;
2565 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
2566 Kokkos::parallel_for(
2567 "peclet::flow::deps_dt", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
2568 KOKKOS_LAMBDA(int x, int y, int z) {
2569 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
2570 dd(i) = (ep(i) - epp(i)) * idt;
2571 if (useDt)
2572 d(i) += dd(i); // off -> solve div(eps u)=0 (drop the noisy time-derivative source)
2573 });
2574 }
2575 // bridge -div(u*) (g=2 block) -> the MG rhs (g=1 block); keep div(u*) in div_ for the pressure
2576 // update
2577 copyInner(rhs1_, e1_, 1, CCConst(div_), e_, G);
2578 {
2579 CCExec space;
2580 CCField r = rhs1_;
2581 Kokkos::parallel_for(
2582 "negdiv", Kokkos::RangePolicy<CCExec>(space, 0, n1_),
2583 KOKKOS_LAMBDA(std::size_t i) { r(i) = -r(i); });
2584 }
2585 if (fluidOnlyMode_ == 2) {
2586 // Design B: solid rows carry no constraint -- mask their rhs (their operator rows are empty
2587 // in the filtered 7-point part; the star overlay never adds to them), so phi_s stays 0.
2588 if (useChebyshev_)
2589 throw std::runtime_error("set_fluid_only_constraint(2): Chebyshev unsupported (v1)");
2590 CCExec space;
2591 CCField r = rhs1_;
2592 CCConst sd = CCConst(sdf_);
2593 const C3 e1 = e1_, e2 = e_;
2594 Kokkos::parallel_for(
2595 "peclet::flow::star_mask_rhs",
2596 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx_, ny_, nz_}),
2597 KOKKOS_LAMBDA(int x, int y, int z) {
2598 const long i2 =
2599 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
2600 if (sd(i2) < 0.0)
2601 r((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) = 0.0;
2602 });
2603 }
2604 // Variable density: rebuild the Poisson operator with the face coefficients
2605 // c_f = open_f * rho0/rho_f (rho0 = the scalar rho_, so uniform rho == rho_ reduces exactly to
2606 // the openness operator). The coefficient fields ride the openness rails: bridge rho to the g=1
2607 // block INCLUDING its ghost ring, form the coefficients on the inner cells, and hand them to
2608 // setOpenness, whose per-level ghost fill + boundary re-imposition + coarsening (rediscretized
2609 // averaging) treat them exactly like openness. Rebuilt every step (rho may be closure/transport
2610 // driven); Chebyshev bounds are invalidated (stale bounds under changing coefficients diverge
2611 // silently — PCG is the recommended/default driver here).
2612 if (varRho_) {
2613 fillPropGhosts(rhoField_);
2614 copyBlockShifted(rho1_, e1_, CCConst(rhoField_), e_, G - 1);
2615 buildRhoCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), CCConst(rho1_),
2616 rho_, e1_, 1);
2617 mg_.setBoundaryConditions(bc_);
2618 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), 1.0, 1.0, 1.0);
2619 chebBoundsSet_ = false; // spectrum changed with the coefficients (re-estimated by the solve)
2620 }
2621 // Porous continuity: the Poisson operator is eps-weighted (c_f = open_f * eps_f), same rails as
2622 // the density coefficient above. Rebuilt every step (eps moves with the particles). With
2623 // implicit CFD-DEM drag the coefficient AND the correction carry the drag-relaxation w_f =
2624 // idt/(idt+beta_f) (idt = rho/dt) so the pressure correction is consistent with the drag-loaded
2625 // momentum diagonal A_P = idt+beta (SIMPLE/PISO-with-implicit-drag; stiff drag -> w_f->0 -> the
2626 // drag holds the velocity, stable). beta==0 reduces exactly to the plain eps-weighted operator.
2627 if (porous_) {
2628 // eps ghosts were filled by fillPorousEpsGhosts() before the divergence above — the SAME
2629 // ghost values must feed the coefficient bridge (face eps == 1 at open domain faces), or the
2630 // operator and the RHS disagree at the boundary rows.
2631 copyBlockShifted(eps1_, e1_, CCConst(epsField_), e_, G - 1);
2632 if (hasDrag_) {
2633 fillPropGhosts(dragBeta_);
2634 copyBlockShifted(beta1_, e1_, CCConst(dragBeta_), e_, G - 1);
2635 } else if (porousCons_) {
2636 Kokkos::deep_copy(beta1_, 0.0); // conservative kernels read beta unconditionally when used
2637 }
2638 if (porousCons_) {
2639 // eps-CONSERVATIVE pair: c_f = open * (eps_f rho idt)/(eps_f rho idt + beta_f), matching
2640 // the eps-weighted momentum diagonal; the eps of the flux cancels the eps of the inertia
2641 // (see mac_pressure.hpp). Correction: projectCorrectPorousCons below.
2642 buildPorousCoeffCons(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
2643 CCConst(eps1_), CCConst(beta1_), hasDrag_, rho_ / dt_, e1_, 1);
2644 } else if (hasDrag_) {
2645 buildPorousCoeffDrag(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
2646 CCConst(eps1_), CCConst(beta1_), rho_ / dt_, e1_, 1);
2647 } else {
2648 buildPorousCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
2649 CCConst(eps1_), e1_, 1);
2650 }
2651 mg_.setBoundaryConditions(bc_);
2652 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), 1.0, 1.0, 1.0);
2653 chebBoundsSet_ = false;
2654 }
2655 // geometric multigrid solve of the cut-cell pressure Poisson A phi = -div(u*) (CUDA
2656 // mac_multigrid): MG-PCG by default, or the communication-light Chebyshev driver (bounds
2657 // estimated once, then reused). Warm start (CUDA pwarm_): keep the previous step's phi1_ as the
2658 // initial guess instead of zeroing.
2659 if (!pwarm_)
2660 Kokkos::deep_copy(phi1_, 0.0);
2661 if (useChebyshev_) {
2662 if (!chebBoundsSet_) {
2663 mg_.estimateEigenvalues(CCConst(rhs1_), chebA_, chebB_, 15, 2, 2, 12);
2664 chebBoundsSet_ = true;
2665 }
2666 lastPressureIters_ =
2667 mg_.solveChebyshev(rhs1_, phi1_, chebMaxit_, chebRtol_, 2, 2, 12, chebA_, chebB_);
2668 } else if (ghostProjection_) {
2669 // Nonsymmetric ghost-projection operator (both grids — the phi matrix is identical):
2670 // BiCGStab, preconditioned by the symmetric binary-openness V-cycle (the hierarchy set up
2671 // in setSolid); the overlay delta enters the fine-level matvec only. Distributed, the
2672 // matvec stages the iterate on this solver's g=2 block (gpX2_) whose halo carries the
2673 // overlay's +/-2 reach.
2674#ifdef PECLET_FLOW_MPI
2675 if (distributed_)
2676 lastPressureIters_ =
2677 mg_.solveBiCGStab(rhs1_, phi1_, r_, gpRh_, pp_, Ap_, gpT_, z_, gpZ2_, pcgMaxit_,
2678 pcgRtol_, 2, 2, 12, gpOv_, gpNRows_, C3{nx_, ny_, nz_}, gpX2_,
2679 velDev_.get(), e_);
2680 else
2681#endif
2682 lastPressureIters_ =
2683 mg_.solveBiCGStab(rhs1_, phi1_, r_, gpRh_, pp_, Ap_, gpT_, z_, gpZ2_, pcgMaxit_,
2684 pcgRtol_, 2, 2, 12, gpOv_, gpNRows_, C3{nx_, ny_, nz_});
2685 // Pin the FREE variables of the binary-openness operator to their design value phi = 0:
2686 // solid-centered (sdfGp < 0) cells and fully-BC_ONLY overlay rows have a zero row AND zero
2687 // rhs, so the Krylov iteration leaves an arbitrary (V-cycle-prolongation, iteration-path,
2688 // and decomposition dependent) value there — invisible to the gp residual (the closures
2689 // never read those faces) but INJECTED into real near-wall fluid velocities by the plain
2690 // projectCorrect face gradient. The doc contract of ghost_projection.hpp is "decoupled
2691 // rows hold phi = 0"; enforce it (measured: without this, np=2 runs differ from the
2692 // reference by ~1e-2 relative u at sphere-surface faces while agreeing 1e-13 elsewhere).
2693 {
2694 CCExec space;
2695 CCField ph = phi1_;
2696 CCConst sg = CCConst(sdfGp_);
2697 auto idMap = gpIdMap_;
2698 auto ov = gpOv_;
2699 const C3 e1 = e1_, e2 = e_;
2700 const int lnx = nx_, lny = ny_;
2701 Kokkos::parallel_for(
2702 "peclet::flow::gp_pin_decoupled",
2703 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx_, ny_, nz_}),
2704 KOKKOS_LAMBDA(int x, int y, int z) {
2705 const long i1 =
2706 (long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y;
2707 const long i2 =
2708 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
2709 bool dec = sg(i2) < 0.0;
2710 if (!dec) {
2711 const int s = idMap((long)x + (long)y * lnx + (long)z * (long)lnx * lny);
2712 if (s >= 0 && ov.coupled(s) == 0)
2713 dec = true;
2714 }
2715 if (dec)
2716 ph(i1) = 0.0;
2717 });
2718 }
2719 } else {
2720 lastPressureIters_ =
2721 mg_.solvePCG(rhs1_, phi1_, r_, pp_, z_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12,
2722 fluidOnlyMode_ == 2 ? &starOv_ : nullptr, nStar_, C3{nx_, ny_, nz_});
2723 }
2724 if (fluidOnlyMode_ == 2) {
2725 // Pin phi at solid-centered cells to 0 (their rows are unconstrained; the smoother must not
2726 // leave garbage there -- projectCorrect reads phi_s at fluid|solid faces and the
2727 // starCorrectFaces fix-up assumes the applied value was exactly 0).
2728 CCExec space;
2729 CCField ph = phi1_;
2730 CCConst sd = CCConst(sdf_);
2731 const C3 e1 = e1_, e2 = e_;
2732 Kokkos::parallel_for(
2733 "peclet::flow::star_pin_solid",
2734 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx_, ny_, nz_}),
2735 KOKKOS_LAMBDA(int x, int y, int z) {
2736 const long i2 =
2737 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
2738 if (sd(i2) < 0.0)
2739 ph((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) = 0.0;
2740 });
2741 }
2742 copyInner(phi_, e_, G, CCConst(phi1_), e1_, 1); // bridge phi back g=1 -> g=2
2743 fillGhosts(phi_);
2744 if (hasOutflow_) { // hold phi=0 at the outflow ghost so grad(phi) drives the outflow face
2745 // (Dirichlet p=0)
2746 B3 e{e_.x, e_.y, e_.z};
2747 for (int a = 0; a < 3; ++a)
2748 for (int s = 0; s < 2; ++s)
2749 if (bc_[2 * a + s] == 3)
2750 bcZeroPressureGhost(phi_, e, G, a, s);
2751 }
2752 if constexpr (Grid::collocated) {
2753 // phi: zero-gradient (Neumann) at non-periodic walls so the cell-centered central-difference
2754 // correction carries no spurious normal acceleration through the wall (the periodic fill
2755 // wrapped the opposite boundary's phi). Outflow (Dirichlet p=0) is handled by hasOutflow_
2756 // above (phase 5b).
2757 if (hasBc_) {
2758 B3 e{e_.x, e_.y, e_.z};
2759 for (int a = 0; a < 3; ++a)
2760 for (int s = 0; s < 2; ++s) {
2761 const int t = bc_[2 * a + s];
2762 if (t != 0 && t != 3)
2763 bcNeumannGhost(phi_, e, G, a, s);
2764 }
2765 }
2766 // Correct the face field (-> discretely divergence-free; transient this step) and the cell
2767 // field (central-difference cell gradient).
2768 projectCorrect(uf_, vf_, wf_, CCConst(phi_), e_, G);
2769 if (fluidOnlyMode_ == 2) // Design B: replace the solid side's phi=0 by phibar_s at
2770 starCorrectFaces(uf_, vf_, wf_, CCConst(phi_), starOv_, nStar_, // fluid|solid faces
2771 C3{nx_, ny_, nz_}, e_, G, e_, G);
2772 fillGhosts(uf_);
2773 fillGhosts(vf_);
2774 fillGhosts(wf_); // complete the divergence-free face field (boundary faces)
2775 if (hasOutflow_) { // correct the high-side outflow face on the face field so mass leaves
2776 // (phi=0 there)
2777 B3 e{e_.x, e_.y, e_.z};
2778 CCField fa[3] = {uf_, vf_, wf_};
2779 for (int a = 0; a < 3; ++a)
2780 if (bc_[2 * a + 1] == 3)
2781 bcCorrectOutflow(fa[a], phi_, e, G, a);
2782 }
2783 if (ghostProjection_ || faceInterp_ == 9 || faceInterp_ == 10) {
2784 // Ghost cell correction (also the mode-9/10 cutcell-ghost hybrids): the directional
2785 // gpCenterGrad gradient of phi — 2nd-order one-sided at cut cells, never reads a
2786 // decoupled (solid/pocket) phi. The same operator supplies the momentum's -grad(P^n)
2787 // predictor (buildRhs), so the pressure force the momentum feels and the correction stay
2788 // one operator family.
2789 for (int cc = 0; cc < 3; ++cc) {
2790 gpCenterGrad(tgp_, CCConst(phi_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), cc, e_, G, gauge2a_);
2791 subtractField(C[cc].u, CCConst(tgp_), e_, G);
2792 }
2793 } else if (faceInterp_ >= 2 &&
2794 faceInterp_ <= 5) { // modes 2-5: cell correction = the TRANSPOSE of the
2795 // wall-aware map, keeping (T, Tᵀ) an adjoint pair (transposeGradWallAware)
2796 CCField xcs[3] = {xcx_, xcy_, xcz_};
2797 CCField oax[3] = {ox_, oy_, oz_};
2798 for (int cc = 0; cc < 3; ++cc) {
2799 transposeGradWallAware(tgp_, CCConst(phi_), CCConst(sdf_), CCConst(oax[cc]),
2800 CCConst(xcs[cc]), faceInterp_ >= 3, cc, e_, G);
2801 subtractField(C[cc].u, CCConst(tgp_), e_, G);
2802 }
2803 } else if (faceInterp_ == 6 ||
2804 faceInterp_ == 7) { // embed: openness-WEIGHTED cell correction
2805 // (full open-face pressure force at cut cells) — Basilisk centered_grad
2806 projectCorrectCenterOpen(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
2807 CCConst(oz_), e_, G);
2808 } else if (faceInterp_ >= 11 && faceInterp_ <= 13) { // adjoint-aperture: cell correction
2809 // = the TRANSPOSE of the aperture divergence of the 1/2-1/2 average, G = -(D_a Pi)^T
2810 // (centerGradAperture) -- support-consistent (collapses the invisible subspace) and
2811 // adjoint (SPSD Uzawa map). Mode 12 = the same times the per-cell openness rescale S(i)
2812 // (centerGradApertureScaled), the accuracy repair for the 1/2*alpha under-weighting.
2813 CCField oax[3] = {ox_, oy_, oz_};
2814 for (int cc = 0; cc < 3; ++cc) {
2815 if (faceInterp_ == 12)
2816 centerGradApertureScaled(tgp_, CCConst(phi_), CCConst(ox_), CCConst(oy_), CCConst(oz_),
2817 cc, e_, G);
2818 else if (faceInterp_ == 13)
2819 centerGradOpenCapped(tgp_, CCConst(phi_), CCConst(oax[cc]), cc, apertureFloor_, e_, G);
2820 else
2821 centerGradAperture(tgp_, CCConst(phi_), CCConst(oax[cc]), cc, e_, G);
2822 subtractField(C[cc].u, CCConst(tgp_), e_, G);
2823 }
2824 } else {
2825 projectCorrectCenter(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
2826 CCConst(oz_), e_, G);
2827 }
2828 } else {
2829 if (porous_ && porousCons_) // eps-conservative gradient rho*idt/(eps_f rho idt + beta_f),
2830 // matching buildPorousCoeffCons (see mac_pressure.hpp)
2831 projectCorrectPorousCons(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(epsField_),
2832 CCConst(dragBeta_), hasDrag_, rho_ / dt_, e_, G);
2833 else if (porous_ &&
2834 hasDrag_) // drag-relaxed gradient w_f=idt/(idt+beta_f), matching buildPorousCoeffDrag
2835 projectCorrectPorousDrag(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(dragBeta_),
2836 rho_ / dt_, e_, G);
2837 else if (varRho_) // per-face 1/rho on the gradient, matching the operator coefficient
2838 projectCorrectVar(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(rhoField_), rho_, e_, G);
2839 else
2840 projectCorrect(C[0].u, C[1].u, C[2].u, CCConst(phi_), e_, G);
2841 if (hasOutflow_) { // correct the high-side outflow normal face that projectCorrect misses
2842 // (mass leaves)
2843 B3 e{e_.x, e_.y, e_.z};
2844 for (int a = 0; a < 3; ++a)
2845 if (bc_[2 * a + 1] == 3)
2846 bcCorrectOutflow(C[a].u, phi_, e, G, a);
2847 }
2848 }
2849 // the grad(phi) correction also touches solid faces; re-impose no-slip there so the decoupled
2850 // solid velocity cannot accumulate (matches the CUDA apply_mask/mask_k after correct_k ->
2851 // stability).
2852 for (int c = 0; c < 3; ++c)
2853 maskVelocity(c);
2854 // Rotational incremental pressure (Timmermans), matching CUDA press_update_k: P += (rho/dt)*phi
2855 // - mu*div(u*). Classical non-incremental Chorin (!incremental_) skips the accumulation;
2856 // getPressure() derives p from phi.
2857 if (incremental_) {
2858 if (rotFilter_ && rotationalP_)
2859 for (int a = 0; a < 3; ++a)
2860 filterCellField(div_, a); // S(div u*): see setRotationalFilter
2861 CCExec space;
2862 CCField P = P_, ph = phi_, d = div_;
2863 // Pressure under-relaxation (MFIX §10.1): accumulate only omega_p of the increment into the
2864 // physical pressure P (the velocity correction still uses the full phi to satisfy
2865 // continuity), so the next step's incremental predictor -grad(P^n) can't overshoot for a
2866 // stiff drag diagonal. omega_p=1 (default) is the current behaviour; <1 only stabilizes the
2867 // porous+drag path.
2868 const double ct = pressUnderRelax_ * rho_ / dt_,
2869 mu = rotationalP_ ? rotWeight_ * mu_ : 0.0;
2870 if (varProps_) {
2871 // Variable viscosity: the pointwise Timmermans term -mu(i)*div(u*) is inconsistent for
2872 // heterogeneous mu (see setVariableRotational). Default = constant coefficient chi*mu_min
2873 // (stable by domination, exact fallback to the uniform-mu scheme); "full" = pointwise
2874 // (mild contrast only); "off" = plain incremental.
2875 if (varRotMode_ == 1) {
2876 CCConst mf = CCConst(muField_);
2877 const double chi = varRotChi_;
2878 Kokkos::parallel_for(
2879 "press_var_full", Kokkos::RangePolicy<CCExec>(space, 0, n_),
2880 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - chi * mf(i) * d(i); });
2881 } else {
2882 const double muRot = (varRotMode_ == 2) ? 0.0 : varRotChi_ * minMuInner();
2883 Kokkos::parallel_for(
2884 "press_var_min", Kokkos::RangePolicy<CCExec>(space, 0, n_),
2885 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - muRot * d(i); });
2886 }
2887 } else if (rotWallW_ > 0.0 && rotationalP_) {
2888 // Frank's wall-banded blend (setRotationalWallWeight): at fluid cells with a solid
2889 // axis-neighbour (the rows whose one-sided gpCenterGrad makes the cell-centered
2890 // rotational update marginally unstable) use
2891 // P += (rho/dt + w*mu/dx^2)*phi - (1-w)*mu*div(u*)
2892 // (dx = 1 in cell units): the (1-w) shrinks the destabilizing off-diagonal there and the
2893 // diagonal w*mu gain keeps those rows relaxing at dt -> infinity where rho/dt vanishes.
2894 // Bulk cells (w = 0) keep the full-speed rotational update. Outer-shell cells keep the
2895 // bulk formula (their P is ghost/overwritten).
2896 CCConst sd = CCConst(sdf_);
2897 const double w0 = rotWallW_, muF = rotWeight_ * mu_;
2898 C3 e = e_;
2899 Kokkos::parallel_for(
2900 "press_wallblend", Kokkos::RangePolicy<CCExec>(space, 0, n_),
2901 KOKKOS_LAMBDA(std::size_t i) {
2902 const long sy = e.x, sz = (long)e.x * e.y;
2903 const int x = (int)(i % e.x), y = (int)((i / e.x) % e.y), z = (int)(i / sz);
2904 double w = 0.0;
2905 if (x > 0 && y > 0 && z > 0 && x < e.x - 1 && y < e.y - 1 && z < e.z - 1 &&
2906 sd(i) >= 0.0 &&
2907 (sd(i - 1) < 0.0 || sd(i + 1) < 0.0 || sd(i - sy) < 0.0 || sd(i + sy) < 0.0 ||
2908 sd(i - sz) < 0.0 || sd(i + sz) < 0.0))
2909 w = w0;
2910 P(i) += (ct + w * muF) * ph(i) - (1.0 - w) * muF * d(i);
2911 });
2912 } else {
2913 Kokkos::parallel_for(
2914 "press", Kokkos::RangePolicy<CCExec>(space, 0, n_),
2915 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - mu * d(i); });
2916 }
2917 }
2918 // Snapshot eps^{n+1} -> epsPrev_ for the next step's d(eps)/dt (this projection consumed it).
2919 if (porous_)
2920 Kokkos::deep_copy(epsPrev_, epsField_);
2921 }
2922 void maskVelocity(int c) {
2923 CCExec space;
2924 CCField u = C[c].u, m = C[c].mask;
2925 Kokkos::parallel_for(
2926 "vmask", Kokkos::RangePolicy<CCExec>(space, 0, n_), KOKKOS_LAMBDA(std::size_t i) {
2927 if (m(i) > 0.5)
2928 u(i) = 0.0;
2929 });
2930 }
2931 // Minimum viscosity over the (global, under MPI) inner cells — the provably-stable rotational
2932 // coefficient for variable viscosity (chi*mu_min <= mu(x) everywhere).
2933 double minMuInner() {
2934 CCExec space;
2935 C3 e = e_;
2936 CCConst f = CCConst(muField_);
2937 double m = 1e300;
2938 Kokkos::parallel_reduce(
2939 "minmu",
2940 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
2941 {e.x - G, e.y - G, e.z - G}),
2942 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
2943 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2944 if (f(i) < acc)
2945 acc = f(i);
2946 },
2947 Kokkos::Min<double>(m));
2948#ifdef PECLET_FLOW_MPI
2949 if (distributed_) {
2950 double g = m;
2951 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MIN, comm_);
2952 m = g;
2953 }
2954#endif
2955 return m;
2956 }
2958 CCExec space;
2959 C3 e = e_;
2960 double m = 0;
2961 Kokkos::parallel_reduce(
2962 "maxabs",
2963 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
2964 {e.x - G, e.y - G, e.z - G}),
2965 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
2966 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2967 const double a = Kokkos::fabs(f(i));
2968 if (a > acc)
2969 acc = a;
2970 },
2971 Kokkos::Max<double>(m));
2972 return m;
2973 }
2974 std::vector<double> gatherInner(CCField fld) {
2975 auto h = Kokkos::create_mirror_view(fld);
2976 Kokkos::deep_copy(h, fld);
2977 std::vector<double> out((std::size_t)nx_ * ny_ * nz_);
2978 for (int z = 0; z < nz_; ++z)
2979 for (int y = 0; y < ny_; ++y)
2980 for (int x = 0; x < nx_; ++x)
2981 out[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_] =
2982 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y);
2983 return out;
2984 }
2985 // Inverse of gatherInner: scatter an x-fastest (nx,ny,nz) inner-region host buffer into the inner
2986 // cells of a ghosted G=2 field (ghost cells untouched — refill via exchangeField/fillGhosts).
2987 void scatterInner(CCField fld, const std::vector<double>& in) {
2988 if (in.size() != (std::size_t)nx_ * ny_ * nz_)
2989 throw std::runtime_error("flow::setField: array size does not match the inner grid");
2990 auto h = Kokkos::create_mirror_view(fld);
2991 Kokkos::deep_copy(h, fld); // preserve existing ghosts
2992 for (int z = 0; z < nz_; ++z)
2993 for (int y = 0; y < ny_; ++y)
2994 for (int x = 0; x < nx_; ++x)
2995 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
2996 in[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_];
2997 Kokkos::deep_copy(fld, h);
2998 }
2999
3000 // --- Named field registry (multiphysics field container) ------------------------------------
3001 // Register a new zero-initialised cell-centred field on the G=2 velocity block and return its
3002 // buffer. Idempotent: re-adding an existing name returns the existing buffer unchanged.
3003 CCField addField(const std::string& name) {
3004 if (fields_.has(name))
3005 return fields_.at(name).data;
3006 return fields_.add(name, n_, G, peclet::core::Centering::Cell).data;
3007 }
3008 bool hasField(const std::string& name) const { return fields_.has(name); }
3009 CCField fieldView(const std::string& name) { return fields_.at(name).data; }
3010 std::vector<std::string> fieldNames() const { return fields_.names(); }
3011 // Ghost-exchange a registered field (cross-rank + periodic under MPI; periodic-only single-rank).
3012 void exchangeField(const std::string& name) { fillGhosts(fields_.at(name).data); }
3013 // Add-reduce ("reverse") halo: fold ghost-layer deposits back onto their owner cell (both
3014 // cross-rank AND periodic self-wrap). This is the coupling primitive for particle->grid
3015 // deposition (e.g. void fraction / drag reaction) where a particle near a block boundary scatters
3016 // into ghost cells owned by a neighbour; after this the inner block holds the complete sum.
3017 // Single-rank non-periodic: a no-op.
3018 void exchangeFieldAdd(const std::string& name) {
3019#ifdef PECLET_FLOW_MPI
3020 if (distributed_ && velHalo_) {
3021 CCField f = fields_.at(name).data;
3022 auto h = Kokkos::create_mirror_view(f);
3023 Kokkos::deep_copy(h, f);
3024 peclet::core::halo::GridFieldView<double> view{h.data()};
3025 velHalo_->reverseAdd(view);
3026 Kokkos::deep_copy(f, h);
3027 }
3028#else
3029 (void)name;
3030#endif
3031 }
3032 // Host round-trip: read a registered field's inner region as an x-fastest (nx,ny,nz) buffer, or
3033 // write one (ghosts left stale until the next exchangeField).
3034 std::vector<double> getField(const std::string& name) {
3035 return gatherInner(fields_.at(name).data);
3036 }
3037 void setField(const std::string& name, const std::vector<double>& v) {
3038 scatterInner(fields_.at(name).data, v);
3039 }
3040 // Padded-block extents + ghost width, so a zero-copy field buffer (size ex*ey*ez, x-fastest) can
3041 // be reshaped in Python.
3042 std::array<int, 3> blockShape() const { return {e_.x, e_.y, e_.z}; }
3043 int ghostWidth() const { return G; }
3044 // Global grid dims (== local dims single-rank). For the CFD-DEM co-decomposition weight field.
3045 std::array<int, 3> globalResolution() const {
3046#ifdef PECLET_FLOW_MPI
3047 if (distributed_)
3048 return {gnx_, gny_, gnz_};
3049#endif
3050 return {nx_, ny_, nz_};
3051 }
3052 // This rank's inner-block origin in GLOBAL cells ({0,0,0} single-rank). The deposit-origin shift
3053 // so particles in global coords land in the local block (gm origin = blockOrigin * h).
3054 std::array<int, 3> blockOrigin() const { return {og_.x, og_.y, og_.z}; }
3055
3056 // --- Scalar transport (advection-diffusion) -------------------------------------------------
3057 // Register a transported scalar `name` with constant diffusivity D (grid units). scheme: 0 FOU,
3058 // 1 Koren TVD (default), 2 SOU. iters = RB-GS sweeps for the implicit diffusion solve. Its field
3059 // is registered in the directory (get_field/set_field/field_view). Openness (set_solid /
3060 // set_pressure_geometry) must be established for transport to occur.
3061 void addScalar(const std::string& name, double D, int scheme, int iters) {
3062 ScalarField sc;
3063 sc.name = name;
3064 sc.c = addField(name); // registered, zero-initialised, on the G=2 block
3065 sc.cOld = CCField(name + "_old", n_);
3066 sc.b = CCField(name + "_b", n_);
3067 sc.AC = CCField(name + "_AC", n_);
3068 sc.AW = CCField(name + "_AW", n_);
3069 sc.AE = CCField(name + "_AE", n_);
3070 sc.AS = CCField(name + "_AS", n_);
3071 sc.AN = CCField(name + "_AN", n_);
3072 sc.AB = CCField(name + "_AB", n_);
3073 sc.AT = CCField(name + "_AT", n_);
3074 sc.D = D;
3075 sc.scheme = scheme;
3076 sc.iters = iters < 1 ? 1 : iters;
3077 scalars_.push_back(sc);
3078 }
3079 bool hasScalar(const std::string& name) const {
3080 for (const auto& sc : scalars_)
3081 if (sc.name == name)
3082 return true;
3083 return false;
3084 }
3085 // Per-face scalar BC: face 0..5 = -x,+x,-y,+y,-z,+z; type 0 periodic, 1 Neumann zero-flux
3086 // (adiabatic), 2 Dirichlet value. Single-rank / non-decomposed domains (distributed BC deferred).
3087 void setScalarBc(const std::string& name, int face, int type, double value) {
3088 for (auto& sc : scalars_)
3089 if (sc.name == name) {
3090 sc.bc[face] = type;
3091 sc.bcVal[face] = value;
3092 return;
3093 }
3094 throw std::runtime_error("set_scalar_bc: no scalar named '" + name + "'");
3095 }
3096 // Advance all registered scalars one dt with the current divergence-free velocity (also called at
3097 // the end of step()). Exposed so a test can prescribe a velocity and transport a scalar in
3098 // isolation.
3100 if (scalars_.empty())
3101 return;
3102 const double idt = 1.0 / dt_;
3103 CCField Uf, Vf, Wf;
3104 if constexpr (Grid::collocated) {
3105 Uf = uf_;
3106 Vf = vf_;
3107 Wf = wf_;
3108 } else {
3109 Uf = C[0].u;
3110 Vf = C[1].u;
3111 Wf = C[2].u;
3112 }
3113 fillGhosts(Uf);
3114 fillGhosts(Vf);
3115 fillGhosts(Wf); // face velocities need the ±2 advection reach
3116 for (auto& sc : scalars_) {
3117 scalarBuildDiffusionOpen(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, CCConst(ox_),
3118 CCConst(oy_), CCConst(oz_), sc.D, idt, e_, G);
3119 applyScalarBcStencil(sc); // re-open Dirichlet domain faces (set_domain_bc closes openness)
3120 Kokkos::deep_copy(sc.cOld, sc.c);
3121 scalarFillGhosts(sc);
3122 scalarBuildRhs(sc.b, CCConst(sc.cOld), CCConst(Uf), CCConst(Vf), CCConst(Wf), CCConst(ox_),
3123 CCConst(oy_), CCConst(oz_), idt, sc.scheme, e_, G);
3124 // implicit diffusion: red-black Gauss-Seidel with a ghost fill before each color sweep.
3125 for (int it = 0; it < sc.iters; ++it) {
3126 scalarFillGhosts(sc);
3127 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
3128 og_, G, 0);
3129 scalarFillGhosts(sc);
3130 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
3131 og_, G, 1);
3132 }
3133 scalarFillGhosts(sc);
3134 }
3135 }
3136
3137 // --- Property closures + per-cell body force ------------------------------------------------
3138 // Register a property/force closure. target: a registered field name — a material property
3139 // ("mu"/"rho"/…) or a body-force component ("force_x"/"force_y"/"force_z"). kind: LinearMix /
3140 // BoussinesqForce / ArrheniusMu. in0/in1: input field names (in1 "" if unused). params: up to 4
3141 // doubles (meaning per kind — property_closures.hpp). Applied at the top of step() in
3142 // registration order. Targeting a force component turns on the per-cell body-force RHS path.
3143 void setPropertyModel(const std::string& target, ClosureKind kind, const std::string& in0,
3144 const std::string& in1, const std::vector<double>& params) {
3145 Closure cl;
3146 cl.kind = kind;
3147 cl.out = ensureTarget(target);
3148 cl.in0 = CCConst(fields_.at(in0).data);
3149 if (!in1.empty())
3150 cl.in1 = CCConst(fields_.at(in1).data);
3151 for (int k = 0; k < 4 && k < (int)params.size(); ++k)
3152 cl.p[k] = params[k];
3153 closures_.push_back(cl);
3154 if (target == "mu") // a closure driving mu turns on variable viscosity
3155 setPropertyMode(true, harmonicMu_);
3156 if (target == "rho") // a closure driving rho turns on the variable-density path
3157 setDensityMode(true);
3158 }
3159 // Enable/disable variable density: binds the "rho" field (creating it seeded with the scalar rho_
3160 // if absent) into the momentum time term, the advection weight, and the pressure projection
3161 // (face coefficient open/rho_f + 1/rho_f correction). rho_ (set_rho) becomes the REFERENCE
3162 // density rho0 of the projection scaling — a uniform rho field == rho_ reduces exactly to the
3163 // constant solver. Escape hatch: set_field("rho", arr) + set_density_mode(True); or a closure
3164 // targeting "rho" (e.g. rho = LinearMix of a transported phase fraction) enables it
3165 // automatically. Staggered grid only (v1); the velocity multigrid (scalar-coefficient) is
3166 // disabled.
3168 if constexpr (Grid::collocated) {
3169 if (variable)
3170 throw std::runtime_error("set_density_mode: variable density is staggered-only (v1)");
3171 }
3172 varRho_ = variable;
3173 if (variable) {
3174 if (fields_.has("rho"))
3175 rhoField_ = fields_.at("rho").data;
3176 else {
3177 rhoField_ = addField("rho");
3178 Kokkos::deep_copy(rhoField_, rho_);
3179 }
3180 if (rho1_.extent(0) == 0) { // g=1 MG-block scratch for the projection coefficients
3181 rho1_ = CCField("rho1", n1_);
3182 cx1_ = CCField("cx1", n1_);
3183 cy1_ = CCField("cy1", n1_);
3184 cz1_ = CCField("cz1", n1_);
3185 }
3186 ensureCellForceAll(); // buildRhsVar reads the per-cell force (zero until a closure sets it)
3187 useVelocityMg_ = false; // scalar-coefficient velocity MG (variable-coeff deferred)
3188 // Pressure driver: CHEBYSHEV by default under variable density. MG-PCG stalls on the
3189 // rho-scaled coefficient operator (the hierarchy's transfer pair was built/validated for
3190 // geometric openness; with scaled coefficients the V-cycle preconditioner loses the
3191 // SPD-preserving structure CG needs — observed: PCG 5000 iters stuck where Chebyshev
3192 // converges in ~20). Chebyshev only needs real spectrum bounds, which are re-estimated on
3193 // every coefficient rebuild (chebBoundsSet_ invalidation in project()). An explicit
3194 // set_pressure_pcg/set_pressure_chebyshev AFTER set_density_mode still wins (last set).
3195 useChebyshev_ = true;
3196 chebBoundsSet_ = false;
3197 }
3198 }
3199 // Enable/disable the volume-averaged (porous) continuity for unresolved CFD-DEM: the projection
3200 // enforces d(eps)/dt + div(eps u) = 0 instead of div(u)=0, so the velocity is NOT solenoidal
3201 // where the void fraction changes. Binds the "eps" field (void fraction from the particle
3202 // deposition; created seeded to 1 if absent). Staggered-only. The coupling deposits eps each step
3203 // BEFORE step().
3204 // Has the cut-cell pressure operator been built (set_solid / set_pressure_geometry)? The porous
3205 // projection requires it — project() throws otherwise; the coupling driver queries this to
3206 // auto-install an all-fluid geometry.
3207 bool hasCutcellPressure() const { return cutcellPressure_; }
3209 if constexpr (Grid::collocated) {
3210 if (on)
3211 throw std::runtime_error("set_porous_continuity: staggered-only (v1)");
3212 }
3213 porous_ = on;
3214 if (on) {
3215 if (fields_.has("eps"))
3216 epsField_ = fields_.at("eps").data;
3217 else {
3218 epsField_ = addField("eps");
3219 Kokkos::deep_copy(epsField_, 1.0); // no particles -> eps=1 -> reduces to div(u)=0
3220 }
3221 if (epsPrev_.extent(0) == 0) {
3222 epsPrev_ = CCField("epsPrev", n_);
3223 depsdt_ = CCField("depsdt", n_);
3224 }
3225 if (divAdv_.extent(0) == 0)
3226 divAdv_ = CCField("divAdv", n_); // cell div(u) for the porous advection-form compensation
3227 if (epsRho_.extent(0) == 0)
3228 epsRho_ = CCField("epsRho", n_); // rho_eff = eps*rho (eps-conservative momentum)
3229 // The eps-conservative momentum path (porousCons_) routes through buildRhsVar, which reads
3230 // the per-cell force unconditionally — allocate it (zero) like setDensityMode does. Without
3231 // this a porous run with NO drag/closure (never the coupled case, which enables drag and
3232 // thereby the force fields) dereferences an empty device View.
3233 ensureCellForceAll();
3234 Kokkos::deep_copy(epsPrev_, epsField_); // d(eps)/dt=0 on the first step
3235 if (eps1_.extent(0) == 0)
3236 eps1_ = CCField("eps1", n1_);
3237 if (beta1_.extent(0) == 0)
3238 beta1_ = CCField("beta1", n1_);
3239 if (rho1_.extent(0) == 0) { // share the g=1 coefficient scratch with the varRho path
3240 rho1_ = CCField("rho1", n1_);
3241 cx1_ = CCField("cx1", n1_);
3242 cy1_ = CCField("cy1", n1_);
3243 cz1_ = CCField("cz1", n1_);
3244 }
3245 // CHEBYSHEV by default (as for variable density): MG-PCG stalls on the eps-scaled coefficient
3246 // operator — its V-cycle preconditioner loses the SPD structure CG needs (observed: PCG 2000
3247 // iters stuck where Chebyshev converges in ~40). Bounds re-estimated on every coefficient
3248 // rebuild (chebBoundsSet_ invalidation in project()). An explicit driver set afterwards wins.
3249 useChebyshev_ = true;
3250 chebBoundsSet_ = false;
3251 configurePorousDragSolver(); // if drag already on, switch to GraphAMG+PCG (Chebyshev
3252 // diverges)
3253 }
3254 }
3255 // Reseed eps^n = eps^{n+1} so d(eps)/dt = 0 this step. Call after the FIRST void-fraction
3256 // deposition (the "eps" field starts empty, so without this step 0 sees a spurious d(eps)/dt from
3257 // 0 -> eps).
3259 if (porous_)
3260 Kokkos::deep_copy(epsPrev_, epsField_);
3261 }
3262 // Include (default) or drop the d(eps)/dt source in the porous projection RHS. Dropping it
3263 // enforces div(eps u)=0 — useful when eps is a bare per-cell particle deposit whose
3264 // time-derivative is too jagged and drives the eps-weighted pressure solve unstable.
3265 void setPorousDepsDt(bool on) { porousDepsDt_ = on; }
3266 void setPorousConservative(bool on) { porousCons_ = on; }
3267 // Pressure under-relaxation factor omega_p in (0,1] (MFIX-style); 1.0 = off (default).
3268 void setPressureUnderRelax(double w) { pressUnderRelax_ = w; }
3269 // Enable/disable variable-coefficient momentum (variable viscosity). variable=true binds the "mu"
3270 // field (creating it, seeded with the current scalar mu, if absent) and forces the stencil solve
3271 // path. harmonic selects the harmonic face mean (continuous shear stress across a viscosity jump)
3272 // vs arithmetic. Escape hatch: set_field("mu", arr) then set_property_mode(True).
3273 void setPropertyMode(bool variable, bool harmonic) {
3274 varProps_ = variable;
3275 harmonicMu_ = harmonic;
3276 if (variable) {
3277 if (fields_.has("mu"))
3278 muField_ = fields_.at("mu").data;
3279 else {
3280 muField_ = addField("mu");
3281 Kokkos::deep_copy(muField_,
3282 mu_); // default to the scalar mu until a closure/set_field sets it
3283 }
3284 useVelocityMg_ =
3285 false; // the velocity multigrid takes a scalar mu (variable-coeff vmg deferred)
3286 }
3287 }
3288 // Rotational-pressure treatment under variable viscosity. The Timmermans rotational term
3289 // P += (rho/dt)phi - mu*div(u*) is only valid for HOMOGENEOUS viscosity (Deteix & Yakoubi, Appl.
3290 // Math. Lett. 2018 / arXiv:1902.05643): with spatially varying mu the pointwise term is no longer
3291 // the gradient part of the viscous stress, and the accumulated inconsistency destabilises the
3292 // incremental scheme at strong contrast (observed: 10x jump + harmonic faces -> divergence).
3293 // Modes (the incremental predictor -grad(P^n) and P accumulation are kept in ALL of them — that
3294 // is what enables large-dt / steady-Stokes stepping):
3295 // 0 "min" (default): rotational coefficient chi*mu_min — a CONSTANT dominated by the true
3296 // local
3297 // dissipation everywhere (mu_min <= mu(x)), so the constant-viscosity stability theory
3298 // carries over; reduces EXACTLY to the validated scheme when mu is uniform.
3299 // 1 "full": chi*mu(i) pointwise — better pressure consistency at MILD contrast; not stable at
3300 // strong contrast (user's responsibility).
3301 // 2 "off" : plain incremental (no rotational term) — unconditionally stable, keeps the
3302 // artificial
3303 // pressure Neumann layer of the non-rotational scheme.
3304 // The fully consistent variable-viscosity correction (shear-rate projection: an extra Poisson
3305 // solve for psi with rhs div(div(2 nu D(u)))) is deferred.
3306 void setVariableRotational(int mode, double chi) {
3307 varRotMode_ = mode < 0 ? 0 : (mode > 2 ? 2 : mode);
3308 varRotChi_ = chi < 0.0 ? 0.0 : chi;
3309 }
3310 // Tabulated property: out = piecewise-linear interp of (xs, ys) at the input field (xs
3311 // ascending).
3312 void setPropertyTable(const std::string& target, const std::string& in0,
3313 const std::vector<double>& xs, const std::vector<double>& ys) {
3314 Closure cl;
3316 cl.out = ensureTarget(target);
3317 cl.in0 = CCConst(fields_.at(in0).data);
3318 cl.nTab = (int)std::min(xs.size(), ys.size());
3319 cl.tabX = CCField(target + "_tabx", cl.nTab);
3320 cl.tabY = CCField(target + "_taby", cl.nTab);
3321 auto hx = Kokkos::create_mirror_view(cl.tabX);
3322 auto hy = Kokkos::create_mirror_view(cl.tabY);
3323 for (int k = 0; k < cl.nTab; ++k) {
3324 hx(k) = xs[k];
3325 hy(k) = ys[k];
3326 }
3327 Kokkos::deep_copy(cl.tabX, hx);
3328 Kokkos::deep_copy(cl.tabY, hy);
3329 closures_.push_back(cl);
3330 }
3331 // Apply all closures (also called at the top of step()). Exposed for testing.
3333 for (auto& cl : closures_)
3334 applyClosure(cl, e_, G);
3335 }
3336 // Allocate + register the per-cell body-force fields ("force_x/y/z") and route them into the
3337 // momentum RHS, for an EXTERNAL writer (CFD-DEM feedback) to fill directly via field_view — no
3338 // closure needed. buildRhsForced then adds them each step (they persist; the writer overwrites).
3339 void enableCellForce() { ensureCellForceAll(); }
3340 // Implicit (semi-implicit) linear drag: a per-cell coefficient field "drag_beta" is added to the
3341 // momentum diagonal each step, so a drag source −β(u − u_p) is treated implicitly (the fluid
3342 // solve becomes (ρ/dt + β)u = … + β u_p). The drag TARGET β·u_p goes into the force_x/y/z fields
3343 // (the RHS). Unconditionally stable for any β (unlike an explicit −β u force, which diverges for
3344 // the stiff β of a dense particle bed). The external writer (CFD-DEM) fills "drag_beta" +
3345 // "force_*" via field_view; enableDrag() allocates them and turns the diagonal path on.
3346 void enableDrag() {
3347 if (!fields_.has("drag_beta"))
3348 dragBeta_ = addField("drag_beta");
3349 else
3350 dragBeta_ = fields_.at("drag_beta").data;
3351 ensureCellForceAll(); // force_* carries beta*u_p (the implicit-drag RHS target)
3352 hasDrag_ = true;
3354 }
3355 // Porous + implicit drag: the drag-relaxation w_f=idt/(idt+beta) makes the pressure coefficient
3356 // high-ratio (~1 in the freeboard, ->0 in the dense bed). Chebyshev diverges on it; the algebraic
3357 // GraphAMG coarse solve + PCG is robust. Applied whenever BOTH porous_ and hasDrag_ are on
3358 // (either set second). An explicit set_pressure_* afterwards still wins.
3360 if (!(porous_ && hasDrag_))
3361 return;
3362 pressGraphAmg_ = true; // GraphAMG bottom (domain-BC operators: buildAmg skips
3363 // the wrap across non-periodic faces and pcgAmg keeps the
3364 // mean only when the operator is singular)
3365 if (cutcellPressure_) // MG already built (set_solid ran) -> apply now
3366 mg_.setAgglomerationMode(1);
3367 useChebyshev_ = false; // PCG, not Chebyshev (diverges on the high w_f ratio)
3368 chebBoundsSet_ = false;
3369 }
3370 // Add the drag coefficient beta(i) to the (float) momentum diagonal of component c. Called after
3371 // each stencil (re)build when hasDrag_. All-fluid (rscale==1) is exact; the drag×cut-cell-IBM
3372 // interaction (rscale≠1) is untested (documented).
3373 void addDragDiagonal(int c) {
3374 CCExec space;
3375 C3 e = e_;
3376 FV AC = C[c].AC;
3377 CCConst beta = CCConst(dragBeta_);
3378 const long sc = strideOf(c);
3379 // Porous continuity: the projection's operator/correction carry the FACE drag relaxation
3380 // w_f = idt/(idt + beta_f), beta_f = 1/2(beta(i)+beta(i-sc)) (buildPorousCoeffDrag /
3381 // projectCorrectPorousDrag). The staggered momentum diagonal of u_c(i) — the face between cells
3382 // i-sc and i — must carry the SAME beta_f: then a pressure perturbation deltaP produces
3383 // du* = -grad(deltaP)/(idt+beta_f) and the projection returns phi = -deltaP/idt exactly (same
3384 // operator), so the incremental predictor cancels pressure errors in one step. With the cell
3385 // value beta(i) the loop has gain (idt+beta_f)/(idt+beta_cell) at a beta jump (bed top: ~3) and
3386 // the accumulated pressure diverges exponentially. Non-porous (incompressible drag, w==1 path)
3387 // keeps the validated cell-beta form.
3388 const bool faceAvg = porous_;
3389 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
3390 Kokkos::parallel_for(
3391 "peclet::flow::add_drag_diag", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
3392 KOKKOS_LAMBDA(int x, int y, int z) {
3393 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
3394 const double bd =
3395 faceAvg ? 0.5 * ((double)beta(i) + (double)beta(i - sc)) : (double)beta(i);
3396 AC(i) = (float)((double)AC(i) + bd);
3397 });
3398 }
3399
3400 private:
3401 // Resolve a closure target to a registered buffer. A force component allocates ALL three
3402 // cellForce_ slots (buildRhsForced reads every component) and enables the body-force RHS path.
3403 CCField ensureTarget(const std::string& name) {
3404 if (name == "force_x" || name == "force_y" || name == "force_z")
3405 ensureCellForceAll();
3406 return addField(name); // idempotent; returns the (now-existing) buffer
3407 }
3408 void ensureCellForceAll() {
3409 static const char* fn[3] = {"force_x", "force_y", "force_z"};
3410 for (int c = 0; c < 3; ++c)
3411 cellForce_[c] = addField(fn[c]); // zero-initialised, registered
3412 hasCellForce_ = true;
3413 }
3414 // Ghost fill for a scalar: periodic (single-rank) / MPI halo base, then override any domain
3415 // Dirichlet/Neumann faces.
3416 void scalarFillGhosts(ScalarField& sc) {
3417 fillGhosts(sc.c);
3418 applyScalarBc(sc);
3419 }
3420 // Overwrite the ghost band on each Dirichlet/Neumann domain face (both layers, for the ±2
3421 // advection reach). Distributed: a rank applies a face's BC iff its block TOUCHES that global
3422 // face. The halo fill runs first (and may periodic-wrap those ghosts); the BC overwrite wins,
3423 // exactly matching the single-rank fill-then-BC order. Cross-rank ghost CORNERS on a BC face
3424 // keep their exchanged (pre-BC) values, but the scalar stencils only read axis-aligned ghosts
3425 // (7-point diffusion + straight ±2 advection reach), so those corners are never consumed.
3426 void applyScalarBc(ScalarField& sc) {
3427 for (int f = 0; f < 6; ++f)
3428 if (sc.bc[f] != 0 && touchesGlobalFace(f))
3429 applyScalarBcFace(sc.c, f / 2, f % 2, sc.bc[f], sc.bcVal[f]);
3430 }
3431 // Does this rank's block touch global domain face f (always true single-rank)?
3432 bool touchesGlobalFace(int f) const {
3433#ifdef PECLET_FLOW_MPI
3434 if (distributed_) {
3435 const int a = f / 2;
3436 const int o = (a == 0) ? og_.x : (a == 1) ? og_.y : og_.z;
3437 const int n = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
3438 const int gn = (a == 0) ? gnx_ : (a == 1) ? gny_ : gnz_;
3439 return (f % 2 == 0) ? (o == 0) : (o + n == gn);
3440 }
3441#endif
3442 (void)f;
3443 return true;
3444 }
3445 // Re-open the diffusion face at a Dirichlet domain boundary: set_domain_bc closes the boundary
3446 // openness (ox_=0), which correctly makes Neumann/adiabatic walls zero-flux but would also cut a
3447 // Dirichlet wall's heat path. For each Dirichlet face, restore the face coefficient (band = -D,
3448 // A_C += D); the ghost carries 2*value - inner so the row is the standard Dirichlet operator.
3449 void applyScalarBcStencil(ScalarField& sc) {
3450 for (int f = 0; f < 6; ++f) {
3451 if (sc.bc[f] != 2 || !touchesGlobalFace(f))
3452 continue; // only Dirichlet reopens; Neumann/periodic leave the (closed/interior) band
3453 const int a = f / 2, side = f % 2;
3454 CCField band = (a == 0) ? (side == 0 ? sc.AW : sc.AE)
3455 : (a == 1) ? (side == 0 ? sc.AS : sc.AN)
3456 : (side == 0 ? sc.AB : sc.AT);
3457 patchScalarDirichletFace(sc.AC, band, sc.D, a, side);
3458 }
3459 }
3460 // nvcc requires member functions that contain extended (device) lambdas to be PUBLIC — the
3461 // OpenMP/host build accepts them private, so the breakage only shows on the CUDA backend.
3462 public:
3463 void patchScalarDirichletFace(CCField AC, CCField band, double D, int a, int side) {
3464 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
3465 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
3466 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
3467 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
3468 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
3469 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
3470 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
3471 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
3472 const int aInner = (side == 0) ? G : (G + na - 1);
3473 CCExec space;
3474 Kokkos::parallel_for(
3475 "peclet::flow::scalar_bc_stencil",
3476 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {G, G}, {G + nt1, G + nt2}),
3477 KOKKOS_LAMBDA(int j1, int j2) {
3478 const long i = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
3479 // base build put band(i) = -D*open_face and A_C += D*open_face; force the face fully open
3480 // (band -> -D, A_C gains D*(1-open)) without double-counting when it was already open.
3481 AC(i) += D + band(i);
3482 band(i) = -D;
3483 });
3484 }
3485 void applyScalarBcFace(CCField c, int a, int side, int type, double val) {
3486 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
3487 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
3488 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
3489 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
3490 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
3491 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
3492 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
3493 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
3494 const int aInner = (side == 0) ? G : (G + na - 1); // inner boundary cell a-index
3495 const int dir = (side == 0) ? -1 : +1; // toward the ghost
3496 CCExec space;
3497 Kokkos::parallel_for(
3498 "peclet::flow::scalar_bc_face",
3499 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {G, G}, {G + nt1, G + nt2}),
3500 KOKKOS_LAMBDA(int j1, int j2) {
3501 const long base = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
3502 for (int L = 1; L <= 2; ++L) {
3503 const long gcell = base + (long)dir * L * sa;
3504 const long icell = base - (long)dir * (L - 1) * sa;
3505 c(gcell) = (type == 2) ? (2.0 * val - c(icell)) : c(icell);
3506 }
3507 });
3508 }
3509
3510 private:
3511 int nx_, ny_, nz_;
3512 C3 e_, e1_;
3513 std::size_t n_, n1_;
3514 double rho_ = 1.0, mu_ = 0.1, dt_ = 50.0;
3515 std::array<double, 3> f_{{0, 0, 0}};
3516 int velIters_ = 200, presIters_ = 20;
3517 double velTol_ = 0.0; // momentum tolerance stop (0 = legacy fixed-count loop)
3518 int velMinIters_ = 2;
3519 long lastMomentumSweeps_ = 0; // sweeps actually run last step (summed over components/Picard)
3520 int pcgMaxit_ = 500;
3521 double pcgRtol_ = 1e-10; // cut-cell pressure MG-PCG
3522 bool useChebyshev_ = false,
3523 chebBoundsSet_ = false; // Chebyshev pressure driver (set_pressure_chebyshev)
3524 int chebMaxit_ = 120;
3525 double chebRtol_ = 1e-9, chebA_ = 0.0, chebB_ = 0.0;
3526 int nLevels_ = 4; // multigrid depth (CUDA default; set_pressure_multigrid)
3527 bool pressGraphAmg_ = false;
3528 // Coarse-solve policy: -1 auto (DEFAULT — agglomerate when the coarsest grid exceeds
3529 // PECLET_FLOW_AGGLOM_EXTENT on any axis; identical to the smoothed bottom otherwise),
3530 // 0 smoothed, 1 always. Auto became the default 2026-08-13 after the IBM-path anomaly was
3531 // fixed (per-fluid-component null-space projection; see ../docs/DECOMPOSITION_AND_MULTIGRID.md).
3532 int pressAgglomMode_ = -1;
3533 long lastPressureIters_ = 0;
3534 CutcellMG mg_;
3535 // --- multi-rank (MPI) state, gated (single-GPU module never links MPI -> byte-identical when
3536 // off) ---
3537 bool distributed_ = false;
3538 C3 og_{0, 0, 0}; // velocity-block inner origin (global red-black parity); {0,0,0} single-rank
3539#ifdef PECLET_FLOW_MPI
3540 std::shared_ptr<GridHaloTopology<3>> velHalo_; // g=2 velocity-block topology
3541 std::shared_ptr<GridHalo<double>> velDev_; // g=2 velocity-block ghost exchange
3542 std::shared_ptr<GridHalo<float>> velDevF_; // float twin (momentum-stencil ring, CA sweeps)
3543 bool caMomentum_ = false; // communication-avoiding momentum sweeps (PECLET_FLOW_CA + extent>=4)
3544 bool momStencilDirty_[3] = {true, true, true}; // per-component: stencil ring needs an exchange
3545 std::shared_ptr<peclet::core::decomp::BlockDecomposer<3>>
3546 dec_; // current partition (redistribute)
3548 int gnx_ = 0, gny_ = 0, gnz_ = 0; // communicator + GLOBAL dims
3549#endif
3550 int bc_[6] = {0, 0, 0, 0, 0, 0};
3551 double bcVel_[6][3] = {};
3552 bool hasBc_ = false, hasOutflow_ = false; // domain BCs
3553 bool hasSolid_ =
3554 false; // an immersed solid is present (any inner SDF < 0) -- with domain BCs, the
3555 // momentum solve must use the cut-cell IBM stencil, not the all-fluid fold
3556 double backflowBeta_ =
3557 0.2; // outflow backflow-stabilization coefficient (0 = off; inert unless the
3558 // outflow reverses, so purely-outgoing outlets stay byte-identical)
3559 CCField bcProf_[6];
3560 int bcProfNc_[6] = {0, 0, 0, 0, 0, 0}; // per-position inlet profiles (face grid [Lb*Lc*3])
3561 CCField bcDcorr_[3], bcBrhs_[3]; // implicit-diffusion face fold (per component)
3562 bool advect_ = false, cutcellPressure_ = false, implicitFou_ = false;
3563 bool deferredCorr_ = true; // deferred-correction advection (off = pure implicit FOU, 1st order)
3564 int advScheme_ = 0; // high-order advection: 0 = SOU (default), 1 = Koren TVD
3565 bool incremental_ = true,
3566 pwarm_ = false; // incremental-rotational pressure (CUDA default on) + warm-start
3567 bool dtDirty_ = false; // set_dt after set_solid: momentum stencil needs a rebuild
3568 int faceInterp_ = 9; // collocated scheme: 9 = gauge-exact (DEFAULT), 0 = plain (legacy)
3569 double apertureFloor_ = [] { // mode-13 denominator floor (PECLET_FLOW_APERTURE_FLOOR)
3570 const char* v = std::getenv("PECLET_FLOW_APERTURE_FLOOR");
3571 return v ? std::atof(v) : 0.25;
3572 }();
3573 bool gauge2a_ = false; // gauge-exact with the Guy-Fogelson "gradient 2a" one-sided branch
3574 // (set_collocated_scheme("gauge-2a"); experimental stall fix).
3575 // Single-rank exact; at rank seams the +/-3 stencil falls back to the
3576 // 2-point form (decomposition-dependent there until the halo is widened).
3577 bool rotationalP_ = true; // false = PM I ablation: drop the -mu*div(u*) Timmermans term from
3578 // the incremental pressure accumulation (constant-mu path only)
3579 bool rotFilter_ = false; // filtered rotational: smooth div(u*) (mask-aware axis-wise 1-2-1,
3580 // one-sided toward the fluid at solid neighbours) before accumulating
3581 // -mu*div into P. Kills the wall-normal checkerboard feedback the
3582 // cell-centered rotational update is unstable through, keeps the O(1)
3583 // pressure-relaxation gain and the phi=0 (dt-free) fixed point.
3584 double rotFilterEps_ = 0.05; // S' = eps I + (1-eps) S (see setRotationalFilter)
3585 double rotWeight_ = 1.0; // rotational under-relaxation w (setRotationalWeight)
3586 double rotWallW_ = 0.0; // wall-banded rotational blend w0 (setRotationalWallWeight)
3587 int apertureOrder_ = 1; // face-aperture estimator order (setApertureOrder)
3588 int fluidOnlyMode_ = 0; // fluid-only constraint (setFluidOnlyConstraint): 1=A filter, 2=B star
3589 StarOverlay starOv_; // mode-B Kron star overlay (built in setSolid)
3590 Kokkos::View<int, CCMem> starCounter_;
3591 int nStar_ = 0;
3592 double fvRelax_ = 1.0; // mode-4 FV defect-correction under-relaxation (setFvRelax)
3593 bool useVelocityMg_ = false;
3594 int vmgLevels_ = 4, vmgVcycles_ = 8; // IBM velocity multigrid (staircase)
3595 VelocityMG vmg_;
3596 CCField vmgTheta_, vmgClean_;
3597 int outerIters_ = 1;
3598 double outerTol_ = 0.0; // Picard outer iteration (CUDA set_outer_iterations)
3599 long lastOuterIters_ = 0;
3600 double lastOuterCorr_ = 0.0;
3601 // per-step phase timers (seconds, this rank; see lastStepSeconds)
3602 double tStep_ = 0.0, tPredictor_ = 0.0, tMomentum_ = 0.0, tProjection_ = 0.0;
3603 // fence-then-read wall clock: phase boundaries must not attribute queued device work to the
3604 // next phase
3605 static double phaseTick() {
3606 Kokkos::fence();
3607 return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch())
3608 .count();
3609 }
3610 CCField sdf_, ox_, oy_, oz_, phi_, div_, P_, ox1_, oy1_, oz1_, rhs1_, phi1_, r_, z_, pp_, Ap_;
3611 bool ghostProjection_ = false; // directional ghost-cell projection (the collocated AUTO default)
3612 bool colSchemeAuto_ = Grid::collocated; // AUTO scheme resolution at setSolid (cleared by any
3613 // explicit scheme selection)
3614 GpOverlay gpOv_; // its per-row overlay (built by setSolid)
3615 Kokkos::View<int*, CCMem> gpIdMap_;
3616 Kokkos::View<int, CCMem> gpCounter_;
3617 int gpNRows_ = -1; // -1 = overlay not built (set_solid must run with the mode on)
3618 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure order: implicit phi couplings / RHS
3619 CCField tEx_[3][3]; // exact crossings t[c][k] (inner grid; setExactCrossings)
3620 bool hasExactCross_ = false;
3621 std::vector<double> oxOverride_, oyOverride_, ozOverride_; // exact apertures (inner)
3622 bool hasOpenOverride_ = false;
3623 CCField oxb_, oyb_, ozb_; // binary (COUPLED) openness on the g=2 block (ghost divergence)
3624 CCField sdfGp_; // the projection's sdf (fragmentation pockets decoupled) — gpCenterGrad reads
3625 // it so the collocated predictor/correction never touch a decoupled cell
3626 CCField gpRh_, gpT_, gpZ2_; // extra BiCGStab scratch (g=1 block)
3627 CCField gpX2_; // distributed BiCGStab matvec staging (g=2 solver block; overlay +/-2 halo)
3628 CCField uf_, vf_, wf_; // collocated: transient face (MAC) field (approx projection)
3629 CCField tgp_; // collocated: transpose-gradient scratch (setFaceInterp(2/3))
3630 CCField wdef_; // collocated: FV wall viscous-flux defect scratch (setFaceInterp(4))
3631 CCField fvM_, fvL_, cs_; // collocated: mode-4 defect scratch (M·u, L_FV·u) + cell fluid fraction
3632 CCField xcx_, xcy_, xcz_; // collocated: open-centroid wall distance per face (setFaceInterp(3))
3633 CCField old_[3], prev_[3]; // u^n time base + previous Picard iterate
3634 Comp C[3];
3635 peclet::core::FieldSet fields_; // named directory of all cell fields (velocity/p/sdf + user)
3636 std::vector<ScalarField> scalars_; // transported scalars (advection-diffusion)
3637 std::vector<Closure> closures_; // property/body-force closures (applied at top of step())
3638 CCField cellForce_[3]; // per-cell momentum body force (Boussinesq / CFD-DEM feedback)
3639 bool hasCellForce_ = false;
3640 bool varProps_ = false; // variable-coefficient momentum (variable viscosity)
3641 bool harmonicMu_ = false; // harmonic vs arithmetic face-viscosity mean
3642 CCField muField_; // per-cell dynamic viscosity (when varProps_)
3643 int varRotMode_ = 0; // rotational term under varProps: 0 chi*mu_min, 1 chi*mu(i), 2 off
3644 double varRotChi_ = 1.0; // rotational coefficient scale chi
3645 bool varRho_ = false; // variable density (momentum + projection); staggered only
3646 CCField rhoField_; // per-cell density (when varRho_); rho_ is the reference rho0
3647 CCField rho1_, cx1_, cy1_, cz1_; // g=1 MG-block density bridge + projection face coefficients
3648 bool porous_ = false; // volume-averaged continuity d(eps)/dt+div(eps u)=0 (CFD-DEM)
3649 double pressUnderRelax_ = 1.0; // omega_p for the incremental pressure accumulation (1.0 = off)
3650 bool porousDepsDt_ = true; // include the d(eps)/dt source in the projection RHS. Off ->
3651 // enforce div(eps u)=0 (drop the term, which is jagged/noisy
3652 // because eps is a bare per-cell particle deposit; the noisy
3653 // source can drive the eps-weighted pressure solve unstable).
3654 CCField epsField_, epsPrev_, eps1_, depsdt_; // eps^{n+1}, eps^n, g=1 bridge, stored d(eps)/dt
3655 CCField divAdv_; // cell div(u) — porous advection-form compensation (see buildRhs*)
3656 CCField epsRho_; // rho_eff = eps*rho — eps-conservative porous momentum (updateEpsRho per step)
3657 // eps-CONSERVATIVE porous momentum + projection pair (default): time term (eps_f rho/dt) u,
3658 // eps_f rho-weighted advective form, projection c_f = open*(eps rho idt)/(eps rho idt + beta)
3659 // with correction rho idt/(eps rho idt + beta) grad(phi). False = the legacy plain-u pair
3660 // (for A/B only; it kinematically drags gas with the moving porosity — energy injection).
3661 bool porousCons_ = true;
3662 CCField beta1_; // g=1 bridge of the drag coeff (semi-implicit-drag pressure)
3663 bool hasDrag_ = false; // implicit linear drag (CFD-DEM): beta on the momentum diagonal
3664 CCField dragBeta_; // per-cell drag coefficient (added to AC; target beta*u_p rides
3665 // the force_* cellForce fields)
3666};
3667
3668// The staggered MAC solver — THE flow solver, bit-identical to the pre-policy class. Bindings + the
3669// kokkos_mpi tests reference this name unchanged.
3671
3672} // namespace peclet::flow
3673
3674#endif // PECLET_FLOW_SDFLOW_IBM_HPP
int solveChebyshev(CCField b, CCField x, int maxit, double rtol, int pre, int post, int bottom, double a, double bnd)
int solvePCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField Ap, int maxit, double rtol, int pre, int post, int bottom, const StarOverlay *star=nullptr, int nStar=0, C3 nnStar=C3{0, 0, 0})
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
void setBoundaryConditions(const int bc[6])
void setAgglomerationMode(int mode)
int solveBiCGStab(CCField b, CCField x, CCField r, CCField rh, CCField p, CCField v, CCField t, CCField z, CCField z2, int maxit, double rtol, int pre, int post, int bottom, const GpOverlay &ov, int nOv, C3 nn)
void init(int nx, int ny, int nz, int nLevels)
void setMeanRemovalScope(bool all)
void estimateEigenvalues(CCConst seed, double &lmin, double &lmax, int iters, int pre, int post, int bottom)
bool hasCutcellPressure() const
void setPressureMeanRemoval(bool all)
Definition flow_ibm.hpp:167
std::vector< double > getFaceVelocity(int c)
std::array< int, 3 > globalResolution() const
void fillVelGhosts(int comp, int fold)
void setIncrementalPressure(bool on)
Definition flow_ibm.hpp:338
bool implicitAdv() const
void buildRhsVar(int c)
void velSweepLoop(Fill &&fill, Color &&sweepColor, ColorDu &&sweepColorDu)
long lastPressureAllreduceCount() const
void setVelocityTolerance(double rtol, int minIters)
Definition flow_ibm.hpp:159
void scatterInner(CCField fld, const std::vector< double > &in)
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Definition flow_ibm.hpp:709
void setPressurePcg(bool, int maxit, double rtol)
Definition flow_ibm.hpp:238
void setFvRelax(double w)
Definition flow_ibm.hpp:506
void fillGhosts(CCField f)
long lastOuterIterations() const
Definition flow_ibm.hpp:186
void setDomainBc(int face, int type, double vx, double vy, double vz)
Definition flow_ibm.hpp:664
double lastStepSeconds() const
void setFaceInterp(int mode)
Definition flow_ibm.hpp:408
void applyBackflowStab(int c)
std::vector< double > getField(const std::string &name)
void setPropertyMode(bool variable, bool harmonic)
void fillVelGhostsTo(CCField f, int comp, int fold)
double lastPressureAllreduceSeconds() const
void applyVelocityBcComp(int comp, int fold, bool doOutflow)
void setOpennessOverride(const std::vector< double > &ox, const std::vector< double > &oy, const std::vector< double > &oz)
Definition flow_ibm.hpp:316
void setOuterTolerance(double tol)
Definition flow_ibm.hpp:185
CCField addField(const std::string &name)
void setPressureGraphAmg(bool on)
Definition flow_ibm.hpp:207
void fillGhostsFaces(CCField f)
void exchangeFieldAdd(const std::string &name)
void setVelocityStreams(bool)
Definition flow_ibm.hpp:510
void exchangeField(const std::string &name)
void setAdvection(bool on)
Definition flow_ibm.hpp:169
void uploadVelocity(const std::vector< double > &uu, const std::vector< double > &vv, const std::vector< double > &ww)
Definition flow_ibm.hpp:514
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
void applyVelocityBcCompTo(CCField f, int comp, int fold, bool doOutflow)
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
double maxAbsDiffInner(CCConst a, CCConst b)
void setBackflowStab(double beta)
Definition flow_ibm.hpp:218
void fillPropGhosts(CCField f)
void setVelocityIterations(int it)
Definition flow_ibm.hpp:152
void setPressureChebyshev(bool on, int maxit, double rtol)
Definition flow_ibm.hpp:229
void smoothComp(int c)
void setScalarBc(const std::string &name, int face, int type, double value)
std::vector< double > getOpenness(int c)
Solver(int nx, int ny, int nz)
Definition flow_ibm.hpp:59
void addDragDiagonal(int c)
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
Definition flow_ibm.hpp:257
void patchScalarDirichletFace(CCField AC, CCField band, double D, int a, int side)
void setVariableRotational(int mode, double chi)
void setPressureUnderRelax(double w)
long lastMomentumSweeps() const
Definition flow_ibm.hpp:163
void setPressureLevels(int levels)
Definition flow_ibm.hpp:213
std::array< int, 3 > blockOrigin() const
void setFluidOnlyConstraint(int mode)
Definition flow_ibm.hpp:489
void setRotationalPressure(bool on)
Definition flow_ibm.hpp:462
void setDeferredCorrection(bool on)
Definition flow_ibm.hpp:224
void setPressureGeometry(const std::vector< double > &sdfInner)
Definition flow_ibm.hpp:704
static constexpr int G
Definition flow_ibm.hpp:57
void fillAxis(CCField f, int axis)
void setDt(double d)
Definition flow_ibm.hpp:144
void setPorousDepsDt(bool on)
void setField(const std::string &name, const std::vector< double > &v)
void setApertureOrder(int order)
Definition flow_ibm.hpp:484
double lastProjectionSeconds() const
void setBodyForce(double fx, double fy, double fz)
Definition flow_ibm.hpp:151
std::vector< double > getOpennessProj(int c)
void applyScalarBcFace(CCField c, int a, int side, int type, double val)
std::vector< double > getVelocity(int c)
void setPorousConservative(bool on)
std::array< int, 3 > blockShape() const
long strideOf(int c) const
bool hasField(const std::string &name) const
void setDomainBcProfile(int face, const std::vector< double > &prof, int nb, int nc)
Definition flow_ibm.hpp:682
void setAdvectionScheme(int s)
Definition flow_ibm.hpp:174
long lastPressureIterations() const
void allocateBlock(int nx, int ny, int nz)
Definition flow_ibm.hpp:63
void setVelocityMultigrid(bool on, int levels, int vcycles)
Definition flow_ibm.hpp:190
void setRotationalWeight(double w)
Definition flow_ibm.hpp:469
void setDensityMode(bool variable)
double lastPredictorSeconds() const
bool hasScalar(const std::string &name) const
void setMu(double m)
Definition flow_ibm.hpp:143
void setExactCrossings(const std::vector< double > &t)
Definition flow_ibm.hpp:290
void setPressureWarmstart(bool on)
Definition flow_ibm.hpp:343
void setPressureIterations(int it)
Definition flow_ibm.hpp:168
void setPropertyTable(const std::string &target, const std::string &in0, const std::vector< double > &xs, const std::vector< double > &ys)
void configurePorousDragSolver()
void buildAdvStencil(int c)
Kokkos::View< float *, CCMem > FV
Definition flow_ibm.hpp:56
CCField fieldView(const std::string &name)
void setCollocatedScheme(const std::string &name)
Definition flow_ibm.hpp:429
double lastMomentumSeconds() const
void filterCellField(CCField f, int axis)
void addScalar(const std::string &name, double D, int scheme, int iters)
void buildAdvStencilVar(int c)
bool bcStencilPath() const
void setImplicitAdvection(bool on)
Definition flow_ibm.hpp:180
bool effVarRho() const
void setPressureBottomMode(int mode)
Definition flow_ibm.hpp:202
VarFaceProps makeFaceProps(int c)
void setRotationalWallWeight(double w0)
Definition flow_ibm.hpp:472
double reduceMaxAbsInner(CCConst f)
void setPorousContinuity(bool on)
int ghostWidth() const
void setRho(double r)
Definition flow_ibm.hpp:142
void setRotationalFilter(bool on, double eps=0.05)
Definition flow_ibm.hpp:500
void buildRhsForced(int c)
std::vector< std::string > fieldNames() const
std::vector< double > gatherInner(CCField fld)
void maskVelocity(int c)
void copyBlockShifted(CCField dst, C3 de, CCConst src, C3 se, int off)
std::vector< double > getPressure()
void setOuterIterations(int iters)
Definition flow_ibm.hpp:184
void setFineStencil(FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT)
void buildUpwindCoarse(int comp, double nu_dt, double idiag, double fouw)
void setBcApplyL0(std::function< void(CCField)> fn)
void setDomainBcOp(int comp, double nu_dt, double idiag)
void solve(CCConst b, CCField x, int nvc, int pre, int post, int bottom)
void restrictAdvVelocities(CCConst u0, CCConst v0, CCConst w0)
void setStaircase(CCConst theta0, CCConst solid0, CCConst resmask0, double nu_dt, double idiag, double thresh)
void setBC(const int bc[6])
void init(int nx, int ny, int nz, int nLevels)
flow — face/cell material-property accessors for the variable-coefficient momentum operator.
flow — the gauge-exact directional cell-centre pressure gradient.
flow — OPT-IN forensics for the ghost-projection overlay (PECLET_FLOW_GP_DEBUG).
flow — GridLayout policy traits (placement of the velocity unknowns).
flow — collocated approximate (MAC) projection helpers (Almgren–Bell–Colella style).
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
flow — portable (Kokkos) IBM geometric fields + variable-coefficient RB-GS smoother.
flow — portable (Kokkos) cut-cell pressure operator + Chorin projection.
flow — portable (Kokkos) MAC stencil operators: Red-Black Gauss-Seidel smoothers + divergence.
flow — portable (Kokkos) velocity (momentum) multigrid for the IBM diffusion solve: the STAIRCASE coa...
void transposeGradWallAware(CCField out, CCConst p, CCConst sdf, CCConst o, CCConst xc, bool useCen, int axis, C3 e, int g)
void buildPorousCoeffDrag(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCConst beta, double idt, C3 e, int g)
void projectCorrectPorousDrag(CCField u, CCField v, CCField w, CCConst phi, CCConst beta, double idt, C3 e, int g)
void projectCorrectCenterOpen(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g)
void buildFaceCentroidDist(CCField xcx, CCField xcy, CCField xcz, CCConst sdf, C3 e)
Kokkos::View< const MReal *, CCMem > FPC
void buildRhoCoeff(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst rho, double rho0, C3 e, int g)
void buildPorousCoeffCons(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCConst beta, bool useBeta, double rhoidt, C3 e, int g)
void ibmSolidMask(CCField mask, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:110
void bcZeroPressureGhost(BField phi, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:193
void ibmCleanFluidMask(CCField m, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:123
void gpBinaryOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext)
Binary openness for the symmetric MG surrogate, on the extended-block layout of buildOpenness: o(face...
void subtractField(CCField u, CCConst d, C3 e, int g)
void stencilMatvec(CCField y, CCConst u, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, C3 e, int g)
StarOverlay starMakeOverlay(long n)
void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g)
void divergOpen(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCField d, C3 e, int g)
void bcZeroOpenness(BField oa, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:250
double ibmRbgsStencilColorDuBox(CCField x, CCConst b, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, CCConst solidmask, C3 ext, C3 og, int color, C3 rlo, C3 rhi, C3 slo, C3 shi)
Definition mac_ibm.hpp:305
void diffSmoothColor(SField c, SConst b, I3 e, I3 og, int g, double beta, double Ac, int color, SConst dcorr)
double ibmRbgsStencilColorDu(CCField x, CCConst b, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, CCConst solidmask, C3 ext, C3 og, int g, int color)
Definition mac_ibm.hpp:202
void centerGradApertureScaled(CCField out, CCConst p, CCConst ox, CCConst oy, CCConst oz, int axis, C3 e, int g)
int buildStarOverlay(CCConst sdf, CCConst ox, CCConst oy, CCConst oz, C3 ext, int g, C3 nn, const StarOverlay &ov, Kokkos::View< int, CCMem > counter)
Count + fill the star overlay from the cell-centered sdf and the ORIGINAL (unfiltered) apertures on t...
int gpDebugLevel()
0 = off (default). Read once per call; cheap enough, and keeps the flag hot-swappable in tests.
void bcDiffusionFold(BField dcorr, BField brhs, B3 ext, int g, int a, int s, double dval, double bval)
Definition mac_bc.hpp:172
double diffSmoothColorDu(SField c, SConst b, I3 e, I3 og, int g, double beta, double Ac, int color, SConst dcorr)
void ibmBuildDiffusionVar(Kokkos::View< float *, IMem > AC, Kokkos::View< float *, IMem > AW, Kokkos::View< float *, IMem > AE, Kokkos::View< float *, IMem > AS, Kokkos::View< float *, IMem > AN, Kokkos::View< float *, IMem > AB, Kokkos::View< float *, IMem > AT, int ex, int ey, int ez, int g, FaceProps fp)
void ibmRbgsStencilColor(CCField x, CCConst b, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, CCConst solidmask, C3 ext, C3 og, int g, int color)
Definition mac_ibm.hpp:147
void bcVelocityColocated(BField f, B3 ext, int g, int a, int s, double wall, int comp=0, BField prof=BField(), int prof_nc=0)
Definition mac_bc.hpp:90
GpOverlayT< CCMem > GpOverlay
void projectCorrectVar(CCField u, CCField v, CCField w, CCConst phi, CCConst rho, double rho0, C3 e, int g)
void centerToFaceWallAware(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, CCConst sdf, CCConst xcx, CCConst xcy, CCConst xcz, bool useCen, C3 e, int g)
void fvViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy, CCConst oz, double mu, double idt, C3 e, int g)
void starCorrectFaces(CCField uf, CCField vf, CCField wf, CCConst phi, const StarOverlay &ov, int nOv, C3 nn, C3 ext, int g, C3 extP, int gP)
Fix the face correction at fluid|solid faces: projectCorrect applied -(phi_hi - phi_lo) with the soli...
void gpDebugReport(const GpOverlay &ov, int nRows, C3 nn, Kokkos::View< int *, CCMem > idMap, int rank=0)
Census + optional per-row dump of the built overlay.
void ibmBuildDiffusion(Kokkos::View< float *, IMem > AC, Kokkos::View< float *, IMem > AW, Kokkos::View< float *, IMem > AE, Kokkos::View< float *, IMem > AS, Kokkos::View< float *, IMem > AN, Kokkos::View< float *, IMem > AB, Kokkos::View< float *, IMem > AT, int ex, int ey, int ez, double beta, double idiag)
void centerGradOpen(CCField out, CCConst p, CCConst o, int axis, C3 e, int g)
GpOverlay gpMakeOverlay(long n)
void bcCorrectOutflow(BField f, BField phi, B3 ext, int g, int a)
Definition mac_bc.hpp:215
void gpCenterGrad(CCField out, CCConst p, CCConst sdf, int axis, C3 e, int g, bool grad2a=false)
Directional cell-center gradient (collocated ghost path) of a cell field p whose solid-centered rows ...
void centerGradAperture(CCField out, CCConst p, CCConst o, int axis, C3 e, int g)
void bcVelocityComp(BField f, B3 ext, int g, int a, int s, int comp, double wall, int fold, BField prof=BField(), int prof_nc=0)
Definition mac_bc.hpp:38
void embedViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy, CCConst oz, double mu, double idt, C3 e, int g)
void ccFor3(const char *name, C3 lo, C3 hi, F f)
void centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e, int g)
void ibmFillEntry(const OV &o, int list_idx, int c_idx, float sdf_c, const float sdf_n[6], int bc_type, const float *thEx)
Kokkos::View< double *, CCMem > CCField
void scalarBuildDiffusionOpen(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst ox, CCConst oy, CCConst oz, double D, double idt, C3 e, int g)
void cutcellSmoothColor(CCField phi, CCConst b, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, C3 og, int g, int color)
void ibmModifyStencil(Kokkos::View< float *, IMem > AC, Kokkos::View< float *, IMem > AW, Kokkos::View< float *, IMem > AE, Kokkos::View< float *, IMem > AS, Kokkos::View< float *, IMem > AN, Kokkos::View< float *, IMem > AB, Kokkos::View< float *, IMem > AT, Kokkos::View< double *, IMem > a_inhom, Kokkos::View< double *, IMem > rhs_scale, const IbmOverlay &ibm, int numActive, float u_bc_val)
void buildPorousCoeff(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst eps, C3 e, int g)
void gpDivergDelta(CCField d, CCConst u, CCConst v, CCConst w, const GpOverlay &ov, int nOv, C3 nn, C3 extb, int gb, bool useGhost=false)
Overlay divergence correction: d(r) = rho_r * (d(r) + closure/BC/explicit face values),...
Kokkos::DefaultExecutionSpace CCExec
void projectCorrectCenter(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g)
void divergOpenEps(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCField d, C3 e, int g)
void ibmRbgsStencilColorBox(CCField x, CCConst b, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, CCConst solidmask, C3 ext, C3 og, int color, C3 rlo, C3 rhi, C3 slo, C3 shi)
Definition mac_ibm.hpp:270
Kokkos::View< const float *, CCMem > MConst
void projectCorrectPorousCons(CCField u, CCField v, CCField w, CCConst phi, CCConst eps, CCConst beta, bool useBeta, double rhoidt, C3 e, int g)
void centerGradOpenCapped(CCField out, CCConst p, CCConst o, int axis, double omin, C3 e, int g)
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
void bcOutflowComp(BField f, B3 ext, int g, int a, int s, int comp, int fold)
Definition mac_bc.hpp:142
void applyClosure(const Closure &cl, C3 e, int g)
void bcNeumannGhost(BField f, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:120
Kokkos::View< const double *, CCMem > CCConst
void projectCorrect(CCField u, CCField v, CCField w, CCConst phi, C3 e, int g)
void scalarBuildRhs(CCField b, CCConst cOld, CCConst U, CCConst V, CCConst W, CCConst ox, CCConst oy, CCConst oz, double idt, int scheme, C3 e, int g)
void ibmVolfrac(CCField theta, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:96
int buildGpOverlay(CCConst sdf, C3 ext, int g, C3 nn, const GpOverlay &ov, Kokkos::View< int *, CCMem > idMap, Kokkos::View< int, CCMem > counter, int matrixOrder=2, int rhsOrder=2, CCConst tx=CCConst(), CCConst ty=CCConst(), CCConst tz=CCConst(), bool useGhost=false)
Build the overlay over the inner grid nn from the cell-centered sdf on the extended block (ext,...
flow — device property closures: material properties / body forces as functions of fields.
flow — cell-centred scalar transport (advection–diffusion) on the cut-cell grid.
flow — portable (Kokkos) staggered MAC momentum advection (Koren TVD + FOU).
One entry per eliminated solid-centered cell: packed INNER flat index + the apertures of its (up to 6...
static constexpr double AC
static constexpr int N
Kokkos::View< float *, IMem > FV