flow
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 <array>
22#include <cmath>
23#include <cstring>
24#include <Kokkos_Core.hpp>
25#include <memory>
26#include <vector>
27
28#include "face_props.hpp"
29#include "grid_layout.hpp"
31#include "mac_cutcell_mg.hpp"
32#include "mac_ibm.hpp"
33#include "mac_pressure.hpp"
34#include "mac_stencils.hpp"
35#include "mac_velocity_mg.hpp"
36#include "peclet/core/field/field_set.hpp"
37#include "property_closures.hpp"
38#include "scalar_transport.hpp"
40
41namespace peclet::flow {
42
43// Templated on a GridLayout policy (grid_layout.hpp) that supplies the grid-position-dependent
44// pieces (currently: the per-component velocity sample offset). IbmSolver == Solver<Staggered> (the
45// alias below) is bit-identical to the pre-policy solver; the Colocated policy is added in a later
46// phase.
47template <class Grid>
48class Solver {
49 public:
50 using FV = Kokkos::View<float*, CCMem>;
51 static constexpr int G = 2; // velocity block: Koren advection reach (pressure/MG bridged to g=1)
52
53 Solver(int nx, int ny, int nz) { allocateBlock(nx, ny, nz); }
54
55 // (Re)allocate every per-block buffer for a local inner block of nx*ny*nz. Called by the
56 // constructor and by redistribute() after a re-decomposition changes this rank's block size.
57 void allocateBlock(int nx, int ny, int nz) {
58 nx_ = nx;
59 ny_ = ny;
60 nz_ = nz;
61 e_ = C3{nx + 2 * G, ny + 2 * G, nz + 2 * G};
62 n_ = (std::size_t)e_.x * e_.y * e_.z;
63 e1_ = C3{nx + 2, ny + 2, nz + 2}; // g=1 block for the cut-cell pressure MG
64 n1_ = (std::size_t)e1_.x * e1_.y * e1_.z;
65 sdf_ = CCField("sdf", n_);
66 ox_ = CCField("ox", n_);
67 oy_ = CCField("oy", n_);
68 oz_ = CCField("oz", n_);
69 phi_ = CCField("phi", n_);
70 div_ = CCField("div", n_);
71 P_ = CCField("P", n_);
72 // g=1 scratch for the MG bridge (openness + rhs/phi + PCG vectors)
73 ox1_ = CCField("ox1", n1_);
74 oy1_ = CCField("oy1", n1_);
75 oz1_ = CCField("oz1", n1_);
76 rhs1_ = CCField("rhs1", n1_);
77 phi1_ = CCField("phi1", n1_);
78 r_ = CCField("r", n1_);
79 z_ = CCField("z", n1_);
80 pp_ = CCField("pp", n1_);
81 Ap_ = CCField("Ap", n1_);
82 for (int c = 0; c < 3; ++c) {
83 C[c].u = CCField("u", n_);
84 C[c].b = CCField("b", n_);
85 C[c].AC = FV("AC", n_);
86 C[c].AW = FV("AW", n_);
87 C[c].AE = FV("AE", n_);
88 C[c].AS = FV("AS", n_);
89 C[c].AN = FV("AN", n_);
90 C[c].AB = FV("AB", n_);
91 C[c].AT = FV("AT", n_);
92 C[c].inhom = CCField("inhom", n_);
93 C[c].rscale = CCField("rscale", n_);
94 C[c].mask = CCField("mask", n_);
95 bcDcorr_[c] = CCField("dcorr", n_);
96 bcBrhs_[c] = CCField("brhs", n_);
97 const int maxCut = nx * ny * nz;
98 C[c].ov = IbmOverlay{Kokkos::View<int*, CCMem>("ci", maxCut),
99 Kokkos::View<int*, CCMem>("nb", maxCut),
100 FV("dr", maxCut),
101 Kokkos::View<int*, CCMem>("dc", (std::size_t)maxCut * 6),
102 FV("K", (std::size_t)maxCut * 6),
103 FV("M", (std::size_t)maxCut * 6),
104 FV("X", (std::size_t)maxCut * 6),
105 FV("Nbc", (std::size_t)maxCut * 6),
106 FV("R", (std::size_t)maxCut * 6)};
107 C[c].idMap = Kokkos::View<int*, CCMem>("idMap", n_);
108 C[c].counter = Kokkos::View<int, CCMem>("cnt");
109 old_[c] = CCField("uOld", n_); // u^n time base (fixed over the step's Picard sweeps)
110 prev_[c] = CCField("uPrev", n_); // previous Picard iterate (outer-tolerance check)
111 }
112 if constexpr (Grid::collocated) { // transient face (MAC) field for the approximate projection
113 uf_ = CCField("uf", n_);
114 vf_ = CCField("vf", n_);
115 wf_ = CCField("wf", n_);
116 tgp_ = CCField("tgp", n_); // scratch: wall-aware transpose gradient (setFaceInterp(2/3))
117 wdef_ = CCField("wdef", n_); // scratch: FV wall viscous-flux defect (setFaceInterp(4))
118 fvM_ = CCField("fvM", n_); // scratch: M·u^k (mode-4 defect matvec)
119 fvL_ = CCField("fvL", n_); // scratch: L_FV(u^k) (mode-4 FV operator apply)
120 cs_ = CCField("cs", n_); // static cell fluid fraction (setFaceInterp(4))
121 xcx_ = CCField("xcx", n_); // static per-face open-centroid wall distance (setFaceInterp(3))
122 xcy_ = CCField("xcy", n_);
123 xcz_ = CCField("xcz", n_);
124 }
125 // Register the pre-existing solver fields in the named directory so the multiphysics machinery
126 // (scalar transport, property closures) and load-balance redistribution can enumerate the whole
127 // set uniformly. adopt() aliases the members (no reallocation, no ownership); all live on the
128 // G=2 velocity block and share velHalo_ under MPI.
129 fields_.adopt("u", C[0].u, G, peclet::core::Centering::FaceX);
130 fields_.adopt("v", C[1].u, G, peclet::core::Centering::FaceY);
131 fields_.adopt("w", C[2].u, G, peclet::core::Centering::FaceZ);
132 fields_.adopt("p", P_, G, peclet::core::Centering::Cell);
133 fields_.adopt("sdf", sdf_, G, peclet::core::Centering::Cell);
134 }
135
136 void setRho(double r) { rho_ = r; }
137 void setMu(double m) { mu_ = m; }
138 void setDt(double d) { dt_ = d; }
139 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
140 void setVelocityIterations(int it) { velIters_ = it; }
141 void setPressureIterations(int it) { presIters_ = it; }
142 void setAdvection(bool on) { advect_ = on; } // explicit high-order advection (default SOU)
143 // High-order advection scheme for the (explicit, or deferred-correction) flux: 0 = second-order
144 // upwind (SOU, default — 2nd order at smooth extrema too); 1 = Koren TVD (monotone limiter, the
145 // legacy CUDA scheme). Only matters when advection is enabled; FOU stays the deferred-correction
146 // base.
147 void setAdvectionScheme(int s) { advScheme_ = s; }
148 // Implicit-FOU deferred-correction advection (CUDA set_implicit_advection): solve the
149 // first-order-upwind part of advection implicitly (in the velocity operator) + keep (Koren-FOU)
150 // explicit in the RHS -> unconditionally stable for advection (high Re / large dt). Requires the
151 // IBM stencil (rebuilt per Picard iteration with the FOU term); the domain-BC path needs
152 // velocity-MG (separate milestone).
153 void setImplicitAdvection(bool on) { implicitFou_ = on; }
154 // Picard outer iterations over the step (CUDA set_outer_iterations): the advecting velocity is
155 // lagged at the current iterate u^k while the time base stays u^n. iters>=1; tol>0 stops early on
156 // max|du| < tol.
157 void setOuterIterations(int iters) { outerIters_ = iters < 1 ? 1 : iters; }
158 void setOuterTolerance(double tol) { outerTol_ = tol; }
159 long lastOuterIterations() const { return lastOuterIters_; }
160 // Velocity (momentum) multigrid for the IBM diffusion solve (CUDA set_velocity_multigrid): the
161 // STAIRCASE coarse operator (exact == RB-GS, stiff-stable at large dt). Call before set_solid;
162 // built at geometry time.
163 void setVelocityMultigrid(bool on, int levels, int vcycles) {
164 useVelocityMg_ = on;
165 vmgLevels_ = levels < 1 ? 1 : levels;
166 vmgVcycles_ = vcycles < 1 ? 1 : vcycles;
167 }
168 // Enable the agglomerated GraphAMG bottom solve in the pressure MG: the coarsest level is solved
169 // by a mesh-agnostic algebraic multigrid on the operator gathered to rank 0 --
170 // decomposition-agnostic, so multilevel convergence works under a WEIGHTED ORB (where the
171 // geometric coarse levels can't cleanly coarsen). Applied at the next set_solid / geometry
172 // rebuild.
173 void setPressureGraphAmg(bool on) {
174 pressGraphAmg_ = on;
175 if (cutcellPressure_)
176 mg_.setGraphAmgBottom(on); // propagate live (previously only applied at the next set_solid,
177 // so toggling after geometry silently had no effect)
178 }
179 void setPressureLevels(int levels) {
180 nLevels_ = levels < 1 ? 1 : levels;
181 } // MG depth (CUDA default 4)
182 // Backflow stabilization at outflow faces (Bazilevs 2009 / Esmaily-Moghadam 2011): beta in [0,1]
183 // scales the dissipative outflow term that prevents backflow divergence (0 = off). Default 0.2.
184 void setBackflowStab(double beta) { backflowBeta_ = beta < 0.0 ? 0.0 : beta; }
185 // Deferred-correction advection: on (default) = implicit FOU operator + explicit (HO - FOU)
186 // high-order correction (2nd order; HO = SOU by default, or Koren TVD via set_advection_scheme).
187 // off = pure implicit FOU (1st order, more dissipative, unconditionally stable) -- useful for
188 // very sharp shear layers where the (unlimited SOU) explicit correction overshoots and
189 // destabilizes.
190 void setDeferredCorrection(bool on) { deferredCorr_ = on; }
191 // Chebyshev pressure driver (CUDA set_pressure_chebyshev): communication-light alternative to
192 // MG-PCG -- Chebyshev semi-iteration preconditioned by one symmetric V-cycle, no per-iteration
193 // global dot-products. Spectral bounds of M^{-1}A are estimated once (lazily) on the first solve
194 // and reused every step.
195 void setPressureChebyshev(bool on, int maxit, double rtol) {
196 useChebyshev_ = on;
197 chebMaxit_ = maxit;
198 chebRtol_ = rtol;
199 chebBoundsSet_ = false;
200 }
201 // MG-PCG pressure tolerance/iteration cap (CUDA set_pressure_pcg). The Kokkos cut-cell pressure
202 // solve is MG-PCG by default; this just sets its bounds (the `on` flag is accepted for API
203 // parity).
204 void setPressurePcg(bool /*on*/, int maxit, double rtol) {
205 pcgMaxit_ = maxit;
206 pcgRtol_ = rtol;
207 }
208 // Incremental-rotational pressure (CUDA set_incremental_pressure, default ON): the predictor
209 // carries -grad(P^n) and the physical pressure is accumulated rotationally P += (rho/dt)*phi -
210 // mu*div(u*). OFF => classical non-incremental Chorin (no -grad(P^n) predictor; P derived on
211 // demand as (rho/dt)*phi).
212 void setIncrementalPressure(bool on) { incremental_ = on; }
213 // Pressure warm-start (CUDA set_pressure_warmstart, default OFF): seed each cut-cell pressure
214 // solve from the previous step's projection potential (consecutive phi's are similar along a
215 // steady march -> a more converged phi per fixed solver budget) instead of zeroing the initial
216 // guess.
217 void setPressureWarmstart(bool on) { pwarm_ = on; }
218 // Collocated cut-cell treatment of the approximate projection (no effect on the staggered path):
219 // 0 = plain ½/½ cell->face averaging + central-difference -grad(P) (default; a consistent
220 // adjoint pair of the WRONG geometry — wall at the solid neighbour's center — first-order
221 // drag at curved walls);
222 // 1 = wall-aware cell->face map only (ablation: breaks the adjoint pairing — WORSE, don't use);
223 // 2 = wall-aware map + its TRANSPOSE as the predictor -grad(P) and the cell correction
224 // (consistent pair, but face-CENTER point values under-count the open-area flux —
225 // ablation);
226 // 3 = mode 2 evaluated at the OPEN-FACE-CENTROID wall distance (static geometry from
227 // buildFaceCentroidDist) — the flux-consistent constraint quadrature (stable, but the
228 // momentum row is still the O(h) axis-by-axis IBM: FV constraint vs FD momentum are
229 // inconsistent);
230 // 4 = FULLY-FV: mode-3 projection PLUS the second-order wall viscous-flux deferred correction
231 // on
232 // the momentum (fvViscousApply: μ Σ_a W_a·centroid wall drag via defect correction, W_a
233 // from the divergence-theorem fragment normal o_{a−}−o_{a+}, centroid gradient at the SDF
234 // foot point). Momentum and constraint now share the same finite-volume cut-cell geometry →
235 // targets O(h²).
236 // 5 = EMBED (Basilisk embed.h): like mode 4 but the momentum wall drag is the TRUE-NORMAL
237 // image-point gradient embedDirichletGradient (μ·area·d(U)/dn along n̂, O(h²) a-priori)
238 // rather than the axis-by-axis W_a g_a — the reconstruction the mode-4 arc found the O(h)
239 // ceiling in. Keeps the mode-3 (wall-aware, o-adjoint) projection.
240 // 6 = EMBED momentum + PLAIN (mode-0) projection: the Basilisk pairing — embed viscous no-slip
241 // with the ½/½ face average, fs-weighted cut-cell Poisson, and central-difference
242 // correction (the mode-1/2/3 wall-aware projection was measured WORSE than plain; embed
243 // drives momentum).
244 void setFaceInterp(int mode) { faceInterp_ = mode; }
245 // Under-relaxation of the mode-4 FV wall-flux defect correction (1 = full; <1 damps the stiff
246 // explicit-lagged wall term). The steady state is independent of this value.
247 void setFvRelax(double w) { fvRelax_ = w; }
248 // CUDA-only 3-stream concurrent velocity solve (set_velocity_streams): no Kokkos analogue in this
249 // port (the default-execution-space kernels are already stream-ordered). Accepted as a no-op for
250 // API parity.
251 void setVelocityStreams(bool /*on*/) {}
252 // Seed/restore the velocity state (CUDA set_state / upload_velocity): u/v/w are inner-cell fields
253 // (flat x-fastest, size nx*ny*nz); written into the velocity block + ghosts refreshed (periodic
254 // wrap).
255 void uploadVelocity(const std::vector<double>& uu, const std::vector<double>& vv,
256 const std::vector<double>& ww) {
257 const std::vector<double>* src[3] = {&uu, &vv, &ww};
258 CCExec space;
259 const int ex = e_.x, ey = e_.y, nx = nx_, ny = ny_, nz = nz_, g = G;
260 for (int c = 0; c < 3; ++c) {
261 // Upload the inner field once, write it into the inner cells on device, then refresh the
262 // periodic ghosts (G4) — the old path mirrored the field down, looped on host, and copied
263 // back up.
264 CCField din("peclet::flow::vel_in_d", static_cast<std::size_t>(nx_) * ny_ * nz_);
265 Kokkos::deep_copy(
266 din,
267 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
268 src[c]->data(), src[c]->size()));
269 CCField u = C[c].u;
270 Kokkos::parallel_for(
271 "peclet::flow::upload_velocity",
272 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {nx, ny, nz}),
273 KOKKOS_LAMBDA(int x, int y, int z) {
274 u((long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey) =
275 din((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
276 });
277 fillGhosts(C[c].u);
278 }
279 }
280#ifdef PECLET_FLOW_MPI
281 // Multi-rank: this rank's IbmSolver is constructed with its LOCAL block dims (= the
282 // BlockDecomposer of the GLOBAL grid for this rank); initMpi wires the g=2 velocity-block halo +
283 // the global-origin red-black parity, and switches fillGhosts/maxOpenDivergence + the pressure MG
284 // (CutcellMG::initMpi) onto their distributed paths. The caller decomposes first (deterministic
285 // ORB) to size the constructor; initMpi re-derives it.
286 void initMpi(int gnx, int gny, int gnz, MPI_Comm comm) {
287 int size = 1;
288 MPI_Comm_size(comm, &size);
289 peclet::core::decomp::BlockDecomposer<3> dec(static_cast<std::size_t>(size),
290 peclet::core::IVec<3>{gnx, gny, gnz});
291 initMpi(dec, comm);
292 }
293 // Shared-decomposition overload: wire the g=2 velocity-block halo from an EXTERNALLY-built ORB
294 // (so flow and dem share one BlockDecomposer for coupled runs, and redistribute() can re-init
295 // onto a re-decomposed partition). The local block size must already match dec.block(rank).size
296 // (set via the constructor / allocateBlock).
297 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec, MPI_Comm comm) {
298 distributed_ = true;
299 comm_ = comm;
300 const auto& gs = dec.globalSize();
301 gnx_ = (int)gs[0];
302 gny_ = (int)gs[1];
303 gnz_ = (int)gs[2];
304 int rank = 0;
305 MPI_Comm_rank(comm, &rank);
306 std::array<bool, 3> per{true, true, true};
307 velHalo_ = std::make_shared<GridHaloTopology<3>>();
308 velHalo_->buildTopology(dec, rank, G, per, comm);
309 velDev_ = std::make_shared<GridHalo<double>>();
310 velDev_->init(*velHalo_);
311 dec_ =
312 std::make_shared<peclet::core::decomp::BlockDecomposer<3>>(dec); // remember the partition
313 const auto oig = velHalo_->indexer().originInclGhost();
314 og_ = {(int)oig[0] + G, (int)oig[1] + G,
315 (int)oig[2] + G}; // block inner origin -> global parity
316 }
317 // Redistribute the solver's state onto a NEW decomposition (dynamic load balancing). Enumerates
318 // the registered fields, moves them from the current block layout to the new one (bit-exact via
319 // redistributeGridFields), reallocates every buffer to the new block, re-inits the halo +
320 // pressure MG on the new partition, and rebuilds all geometry-derived state (openness / IBM
321 // overlay / stencils) from the migrated SDF. Velocity + pressure + SDF (+ any registered
322 // scalar/property fields) survive; per-step scratch is rebuilt.
323 void redistribute(const peclet::core::decomp::BlockDecomposer<3>& newDec) {
324 if (!distributed_ || !dec_)
325 return;
326 int rank = 0;
327 MPI_Comm_rank(comm_, &rank);
328 const auto ob = dec_->block(rank), nb = newDec.block(rank);
329 const int oex = (int)ob.size[0] + 2 * G, oey = (int)ob.size[1] + 2 * G,
330 oez = (int)ob.size[2] + 2 * G;
331 const int nex = (int)nb.size[0] + 2 * G, ney = (int)nb.size[1] + 2 * G,
332 nez = (int)nb.size[2] + 2 * G;
333
334 // 1. gather the surviving registered fields to host padded buffers on the OLD block.
335 const auto names = fields_.names();
336 std::vector<std::vector<double>> oldHost(names.size()), newHost(names.size());
337 for (std::size_t k = 0; k < names.size(); ++k) {
338 CCField f = fields_.at(names[k]).data;
339 auto h = Kokkos::create_mirror_view(f);
340 Kokkos::deep_copy(h, f);
341 oldHost[k].assign(h.data(), h.data() + (std::size_t)oex * oey * oez);
342 newHost[k].assign((std::size_t)nex * ney * nez, 0.0);
343 }
344 // 2. redistribute each field OLD -> NEW (host, bit-exact pure data movement).
345 std::vector<const double*> op(names.size());
346 std::vector<double*> np(names.size());
347 for (std::size_t k = 0; k < names.size(); ++k) {
348 op[k] = oldHost[k].data();
349 np[k] = newHost[k].data();
350 }
351 peclet::core::decomp::redistributeGridFields<double>(*dec_, newDec, rank, G, op, np, comm_);
352
353 // 3. reallocate every buffer to the new block; re-init the halo + MG on the new partition.
354 allocateBlock((int)nb.size[0], (int)nb.size[1], (int)nb.size[2]);
355 initMpi(newDec, comm_);
356 // scatter a padded host buffer into a registered field's device buffer.
357 auto scatterPadded = [&](const std::string& name, const std::vector<double>& src) {
358 CCField f = fields_.at(name).data;
359 auto h = Kokkos::create_mirror_view(f);
360 std::memcpy(h.data(), src.data(), sizeof(double) * (std::size_t)nex * ney * nez);
361 Kokkos::deep_copy(f, h);
362 };
363 // 4. scatter all migrated fields into the fresh (new-block) buffers.
364 for (std::size_t k = 0; k < names.size(); ++k)
365 scatterPadded(names[k], newHost[k]);
366 // 5. rebuild geometry-derived state (openness/IBM/stencils/MG) from the migrated SDF. setSolid
367 // zeroes the velocity + pressure (it is the initial-geometry setup), so re-instate every
368 // non-SDF field afterward from the migrated data.
369 setSolid(gatherInner(sdf_), cutcellPressure_);
370 for (std::size_t k = 0; k < names.size(); ++k)
371 if (names[k] != "sdf")
372 scatterPadded(names[k], newHost[k]);
373 }
374 // Redistribute onto the weighted ORB of per-cell weights `w` (global x-fastest, gnx*gny*gnz). The
375 // ergonomic Python entry point for load balancing: the caller passes a weight field (e.g. fluid
376 // work + gamma*particle_count) and both flow and dem rebuild the SAME deterministic partition
377 // from it. No BlockDecomposer object crosses the language boundary.
378 void rebalanceByWeights(const std::vector<peclet::core::Real>& w) {
379 if (!distributed_)
380 return;
381 int size = 1;
382 MPI_Comm_size(comm_, &size);
383 peclet::core::decomp::BlockDecomposer<3> newDec((std::size_t)size,
384 peclet::core::IVec<3>{gnx_, gny_, gnz_}, w);
385 redistribute(newDec);
386 }
387#endif
388 // per-face domain BC {face 0..5 = -x,+x,-y,+y,-z,+z}: type 0=periodic,1=no-slip
389 // wall,2=Dirichlet/inflow,3=outflow.
390 void setDomainBc(int face, int type, double vx, double vy, double vz) {
391 bc_[face] = type;
392 bcVel_[face][0] = vx;
393 bcVel_[face][1] = vy;
394 bcVel_[face][2] = vz;
395 hasBc_ = false;
396 hasOutflow_ = false;
397 for (int i = 0; i < 6; ++i) {
398 if (bc_[i])
399 hasBc_ = true;
400 if (bc_[i] == 3)
401 hasOutflow_ = true;
402 }
403 }
404 // per-position inlet velocity profile on `face` (CUDA set_domain_bc_profile): prof is (nb,nc,3)
405 // on the inner grid of the face's two perpendicular axes; sets the face to inflow (type 2).
406 // Resampled (clamp) to the ghost-inclusive face grid so the BC kernel indexes it directly by face
407 // position.
408 void setDomainBcProfile(int face, const std::vector<double>& prof, int nb, int nc) {
409 const int a = face / 2;
410 const int dims[3] = {e_.x, e_.y, e_.z};
411 const int bax = (a + 1) % 3, cax = (a + 2) % 3;
412 const int Lb = dims[bax], Lc = dims[cax];
413 CCField pf("bcprof", (std::size_t)Lb * Lc * 3);
414 auto h = Kokkos::create_mirror_view(pf);
415 auto cl = [](int v, int n) { return v < 0 ? 0 : (v >= n ? n - 1 : v); };
416 for (int p0 = 0; p0 < Lb; ++p0)
417 for (int p1 = 0; p1 < Lc; ++p1) {
418 const int ib = cl(p0 - G, nb), ic = cl(p1 - G, nc);
419 for (int k = 0; k < 3; ++k)
420 h(((long)p0 * Lc + p1) * 3 + k) = prof[((std::size_t)ib * nc + ic) * 3 + k];
421 }
422 Kokkos::deep_copy(pf, h);
423 bcProf_[face] = pf;
424 bcProfNc_[face] = Lc;
425 bc_[face] = 2;
426 hasBc_ = true; // a profiled face is an inflow
427 }
428 // all-fluid + domain-BC pressure (CUDA set_pressure_geometry): same path as set_solid with an
429 // open SDF.
430 void setPressureGeometry(const std::vector<double>& sdfInner) { setSolid(sdfInner, true); }
431
432 // SDF on the inner cells (flat x-fastest, size nx*ny*nz; <0 solid). cutcellPressure enables the
433 // open-face-weighted cut-cell projection (off => velocity-only, e.g. unidirectional body-force
434 // flow).
435 void setSolid(const std::vector<double>& sdfInner, bool cutcellPressure) {
436 cutcellPressure_ = cutcellPressure;
437 hasSolid_ =
438 false; // does the geometry actually contain solid? (all-fluid set_pressure_geometry
439 for (double v :
440 sdfInner) // passes sd>0 everywhere -> stays false, keeping the channel/BFS path)
441 if (v < 0.0) {
442 hasSolid_ = true;
443 break;
444 }
445#ifdef PECLET_FLOW_MPI
446 if (distributed_) { // a solid anywhere in the global domain enables the IBM momentum path
447 int local = hasSolid_ ? 1 : 0, global = 0;
448 MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MAX, comm_);
449 hasSolid_ = global != 0;
450 }
451#endif
452#ifdef PECLET_FLOW_MPI
453 if (distributed_) {
454 // Multi-rank: sdfInner is THIS rank's LOCAL inner block; fill the inner cells, then
455 // halo-exchange the ghosts (cross-rank + periodic) so the overlay/openness read the
456 // neighbour's SDF at the block boundary.
457 auto h = Kokkos::create_mirror_view(sdf_);
458 Kokkos::deep_copy(h, sdf_);
459 for (int z = 0; z < nz_; ++z)
460 for (int y = 0; y < ny_; ++y)
461 for (int x = 0; x < nx_; ++x)
462 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
463 sdfInner[(std::size_t)x + (std::size_t)y * nx_ +
464 (std::size_t)z * (std::size_t)nx_ * ny_];
465 Kokkos::deep_copy(sdf_, h);
466 velDev_->exchange(sdf_);
467 } else
468#endif
469 {
470 // Single-rank: upload the inner SDF once and do the periodic-wrap gather on device (G4) —
471 // fills the whole extended block (inner + periodic ghosts) in one kernel instead of a host
472 // triple loop + a full extended-block H2D.
473 CCField din("peclet::flow::sdfInner_d", static_cast<std::size_t>(nx_) * ny_ * nz_);
474 Kokkos::deep_copy(
475 din,
476 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
477 sdfInner.data(), sdfInner.size()));
478 CCExec space;
479 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
480 CCField sdf = sdf_;
481 Kokkos::parallel_for(
482 "peclet::flow::sdf_periodic_wrap",
483 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {ex, ey, ez}),
484 KOKKOS_LAMBDA(int x, int y, int z) {
485 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
486 iz = (((z - g) % nz) + nz) % nz;
487 sdf((long)x + (long)y * ex + (long)z * (long)ex * ey) = din(
488 (std::size_t)ix + (std::size_t)iy * nx + (std::size_t)iz * (std::size_t)nx * ny);
489 });
490 space.fence();
491 }
492 for (int c = 0; c < 3; ++c) {
493 const Off3 off =
494 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
495 C[c].nCut = buildIbmOverlay<0>(
496 CCConst(sdf_), e_, G, off, /*Dirichlet*/ 0, C[c].ov, C[c].idMap,
497 C[c].counter); // SCHEME 0 = point-value (matches CUDA ibm_geometry_ext_k<0>)
498 ibmSolidMask(C[c].mask, CCConst(sdf_), e_, off);
499 Kokkos::deep_copy(C[c].u, 0.0);
500 }
502 // Staggered domain BCs bake an implicit-diffusion wall fold; the collocated grid instead uses
503 // explicit reflection ghosts (refreshed each smoother sweep), so it needs no fold.
504 if (hasBc_ && !Grid::collocated)
506 if (useVelocityMg_) { // velocity-MG hierarchy: IBM (staircase/upwind) or domain-BC
507 // (const-coeff) mode
508 vmg_.init(nx_, ny_, nz_, vmgLevels_);
509 if (hasBc_)
510 vmg_.setBC(bc_);
511 else {
512 vmgTheta_ = CCField("vmgTheta", n_);
513 vmgClean_ = CCField("vmgClean", n_);
514 }
515 }
516 if (cutcellPressure_) {
517 buildOpenness(ox_, oy_, oz_, CCConst(sdf_), e_, 1.0, 1.0, 1.0); // on the g=2 velocity block
518 if constexpr (Grid::collocated) { // static open-centroid wall distances (setFaceInterp(3))
519 buildFaceCentroidDist(xcx_, xcy_, xcz_, CCConst(sdf_), e_);
520 buildCellFraction(cs_, CCConst(sdf_), e_, G); // cell fluid fraction (setFaceInterp(4))
521 if (faceInterp_ >=
522 5) { // EMBED: a solid-CENTRED cut cell (cs>0) is partially fluid and holds
523 // its reconstructed near-wall velocity — masking it to 0 (the sdf<0 IBM mask) drops the
524 // near-wall closure and shifts the whole channel. Re-mask from cs: pin ONLY fully-solid
525 // cells (cs≈0), keeping every partial-fluid cut cell live in the embed solve +
526 // projection.
527 CCConst cs = CCConst(cs_);
528 const std::size_t nn = n_;
529 for (int c = 0; c < 3; ++c) {
530 CCField m = C[c].mask;
531 Kokkos::parallel_for(
532 "peclet::flow::embed_solid_mask", Kokkos::RangePolicy<CCExec>(0, nn),
533 KOKKOS_LAMBDA(std::size_t i) { m(i) = cs(i) < 1e-6 ? 1.0 : 0.0; });
534 }
535 }
536 }
537#ifdef PECLET_FLOW_MPI
538 // openness ghosts (the operator + divergence read the +neighbour face) -> exchange across
539 // ranks
540 if (distributed_) {
541 velDev_->exchange(ox_);
542 velDev_->exchange(oy_);
543 velDev_->exchange(oz_);
544 }
545#endif
546 if (hasBc_) { // FLUX openness (beta): a face is OPEN only where it carries normal flux --
547 // outflow, or
548 B3 e2{e_.x, e_.y, e_.z};
549 CCField oa[3] = {ox_, oy_, oz_}; // an inflow with nonzero normal velocity. Walls
550 for (int a = 0; a < 3; ++a)
551 for (int s = 0; s < 2; ++s) { // and tangential-only Dirichlet faces (e.g. a
552 const int t = bc_[2 * a + s]; // lid: type 2 with zero normal vel) are CLOSED.
553 const bool open = (t == 3) || (t == 2 && (bcProf_[2 * a + s].extent(0) > 0 ||
554 std::fabs(bcVel_[2 * a + s][a]) > 1e-12));
555 if (t != 0 && !open)
556 bcZeroOpenness(oa[a], e2, G, a, s);
557 }
558 } // the MG re-derives the OPERATOR openness alpha (inflow Neumann -> closed) per level via
559 // setBC.
560 copyInner(ox1_, e1_, 1, CCConst(ox_), e_, G); // bridge openness g=2 -> g=1 for the MG
561 copyInner(oy1_, e1_, 1, CCConst(oy_), e_, G);
562 copyInner(oz1_, e1_, 1, CCConst(oz_), e_, G);
563#ifdef PECLET_FLOW_MPI
564 if (distributed_) // share the level-0 decomposition so the MG block matches this rank's
565 // block
566 mg_.initMpi(gnx_, gny_, gnz_, nLevels_, comm_, dec_.get());
567 else
568#endif
569 mg_.init(nx_, ny_, nz_,
570 nLevels_); // geometric multigrid on the cut-cell openness (MG-PCG pressure)
572 bc_); // per-level wall openness + null-space gating (no-op if periodic)
573 mg_.setOpenness(CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), 1.0, 1.0, 1.0);
574 mg_.setGraphAmgBottom(pressGraphAmg_); // decomposition-agnostic algebraic coarse solve
575 Kokkos::deep_copy(phi_, 0.0);
576 Kokkos::deep_copy(P_, 0.0);
577 }
578 }
579
580 void step() {
581 // Multiphysics: refresh material properties / body forces from the current fields (frozen over
582 // the step). No-op (byte-identical) when no closure is registered.
584 // Variable properties / implicit drag: rebuild the diffusion stencil from the current mu/rho
585 // and drag_beta fields (the implicit-FOU path rebuilds it per Picard in buildAdvStencil*, so
586 // only the non-advective path needs this).
587 if ((varProps_ || varRho_ || hasDrag_) && !implicitAdv())
589 // u^n time base, fixed for the whole step (Picard lags the advecting velocity at u^k, not the
590 // base).
591 for (int c = 0; c < 3; ++c)
592 Kokkos::deep_copy(old_[c], C[c].u);
593 if (cutcellPressure_ && incremental_) {
594 fillGhosts(P_);
595 if (hasBc_)
597 } // grad(P^n) for the incremental predictor (once)
598 lastOuterIters_ = 0;
599 for (int outer = 0; outer < outerIters_; ++outer) {
600 lastOuterIters_ = outer + 1;
601 if (outerTol_ > 0)
602 for (int c = 0; c < 3; ++c)
603 Kokkos::deep_copy(prev_[c], C[c].u);
604 if (advect_ || hasBc_ || (Grid::collocated && faceInterp_ >= 4))
605 for (int c = 0; c < 3; ++c)
607 0); // explicit ghosts (periodic + BC) for advect / mode-4 FV defect matvec
608 for (int c = 0; c < 3; ++c) // RHS from u^n base + advection lagged at u^k
609 varRho_ ? buildRhsVar(c) : (hasCellForce_ ? buildRhsForced(c) : buildRhs(c));
610 // Implicit-FOU: rebuild the IBM velocity stencil = backward-Euler diffusion + rho*FOU(u^k),
611 // then re-apply the cut-cell bake. Per Picard iteration (advecting velocity changes). Applies
612 // to the IBM (periodic/porous) path when the user opts in, AND ALWAYS to the domain-BC
613 // stencil path (inflow/outflow) -- implicitAdv() -> fully-implicit upwind advection (stable
614 // at large dt). The velocity-MG BC path keeps its own FOU coarse operator.
615 if (implicitAdv() && (!hasBc_ || !useVelocityMg_))
616 for (int c = 0; c < 3; ++c)
617 (varProps_ || varRho_) ? buildAdvStencilVar(c) : buildAdvStencil(c);
618 // Outflow backflow stabilization: dissipate reverse flow at the outlet in the momentum
619 // operator used by the domain-BC stencil smoother (prevents backflow divergence). Inert
620 // without reversal.
621 if (bcStencilPath() && backflowBeta_ > 0.0 && hasOutflow_)
622 for (int c = 0; c < 3; ++c)
624 // upwind-convective velocity-MG: restrict the (frozen u^k) advecting velocity to the coarse
625 // levels ONCE, before the per-component solves update it (shared across the 3 momentum
626 // components).
627 if (useVelocityMg_ && implicitFou_ && advect_ && !hasBc_)
628 vmg_.restrictAdvVelocities(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u));
629 for (int c = 0; c < 3; ++c)
630 smoothComp(c); // per-component IBM implicit-diffusion solve
631 if (cutcellPressure_)
632 project(); // cut-cell projection -> incompressible
633 if (hasBc_)
634 for (int c = 0; c < 3; ++c)
635 applyVelocityBcComp(c, 0, false); // re-impose domain BCs (keep outflow)
636 if (outerTol_ > 0) { // outer convergence: max velocity change over this Picard iteration
637 double corr = 0.0;
638 for (int c = 0; c < 3; ++c)
639 corr = Kokkos::fmax(corr, maxAbsDiffInner(CCConst(C[c].u), CCConst(prev_[c])));
640 lastOuterCorr_ = corr;
641 if (corr < outerTol_)
642 break;
643 }
644 }
645 // Segregated multiphysics: advance any transported scalars with the just-projected
646 // divergence-free velocity (properties frozen over the step). No-op (byte-identical) when no
647 // scalar is registered.
649 }
650
651 // velocity component c (0=u,1=v,2=w) on the inner cells, flat x-fastest [nx*ny*nz].
652 std::vector<double> getVelocity(int c) { return gatherInner(C[c].u); }
653 // The divergence-free FACE velocity component (collocated: the projected MAC face field
654 // uf_/vf_/wf_, exactly div-free; staggered: C[c].u already lives on the faces). For a periodic
655 // bed its mean is the momentum-balance superficial velocity, unperturbed by the openness-aware
656 // cell gradient correction (projectCorrectCenter) that biases the cell-field mean at cut cells.
657 std::vector<double> getFaceVelocity(int c) {
658 if constexpr (Grid::collocated) {
659 CCField fa[3] = {uf_, vf_, wf_};
660 return gatherInner(fa[c]);
661 } else {
662 return gatherInner(C[c].u);
663 }
664 }
665 // TEMP DIAGNOSTIC: the face openness (fluid area fraction) used by the cut-cell projection.
666 // component c: 0 -> ox_ (low -x face of each inner cell), 1 -> oy_, 2 -> oz_. Grid-independent
667 // (built once from the SDF). Exposed to compare the open-weighted superficial flux against the
668 // raw velocity mean.
669 std::vector<double> getOpenness(int c) {
670 CCField o[3] = {ox_, oy_, oz_};
671 return gatherInner(o[c]);
672 }
673 std::vector<double> getPressure() {
674 // Incremental scheme: P_ accumulates the physical pressure. Classical Chorin (!incremental_):
675 // derive it on demand from the last projection potential, p = (rho/dt)*phi (CUDA
676 // press_from_phi_k).
677 if (incremental_)
678 return gatherInner(P_);
679 std::vector<double> out = gatherInner(phi_);
680 const double ct = rho_ / dt_;
681 for (double& x : out)
682 x *= ct;
683 return out;
684 }
686 if (!cutcellPressure_)
687 return 0.0;
688 if constexpr (Grid::collocated) {
689 // Report the residual of the PROJECTED face field uf_ (made divergence-free by project(),
690 // ghosts filled). Re-averaging the central-difference-corrected CELL field would instead show
691 // the inherent O(h^2) approximate-projection cell divergence -- a property of the scheme, not
692 // the solver residual. At an outflow, re-impose the zero-gradient face (matching the
693 // staggered diagnostic, whose fillVelGhosts overwrites the mass-conserving outflow
694 // correction): the operator zeroes the alpha-divergence, but the raw beta-divergence at the
695 // open-boundary corner is otherwise spurious.
696 if (hasOutflow_) {
697 B3 e{e_.x, e_.y, e_.z};
698 CCField fa[3] = {uf_, vf_, wf_};
699 for (int a = 0; a < 3; ++a)
700 if (bc_[2 * a + 1] == 3)
701 bcNeumannGhost(fa[a], e, G, a, 1);
702 }
703 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(ox_), CCConst(oy_), CCConst(oz_),
704 div_, e_, G);
705 } else {
706 for (int c = 0; c < 3; ++c)
707 fillVelGhosts(c, 0); // ghosts incl. outflow zero-gradient before the divergence
708 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
709 CCConst(oz_), div_, e_, G);
710 }
711 double m = reduceMaxAbsInner(CCConst(div_));
712#ifdef PECLET_FLOW_MPI
713 if (distributed_) {
714 double g = 0;
715 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
716 return g;
717 }
718#endif
719 return m;
720 }
721 // Residual of the volume-averaged continuity, max|div(open*eps*u) + d(eps)/dt| — the quantity the
722 // porous projection actually drives to zero (NOT the velocity divergence, which is -d(eps)/dt !=
723 // 0 in a fluidizing bed). Meaningful only with set_porous_continuity(True); returns 0 otherwise.
725 if (!porous_ || !cutcellPressure_)
726 return 0.0;
727 for (int c = 0; c < 3; ++c)
728 fillVelGhosts(c, 0);
729 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
730 CCConst(oz_), CCConst(epsField_), div_, e_, G);
731 { // add back the SAME d(eps)/dt source the projection used (depsdt_ from the last project())
732 CCExec space;
733 C3 e = e_; // local copy — capturing e_ in the KOKKOS_LAMBDA would read this-> on the device
734 CCField d = div_, dd = depsdt_;
735 const bool useDt = porousDepsDt_;
736 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
737 Kokkos::parallel_for(
738 "peclet::flow::porous_resid", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
739 KOKKOS_LAMBDA(int x, int y, int z) {
740 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
741 if (useDt)
742 d(i) += dd(i); // residual of the SAME constraint the projection solved
743 });
744 }
745 double m = reduceMaxAbsInner(CCConst(div_));
746#ifdef PECLET_FLOW_MPI
747 if (distributed_) {
748 double g = 0;
749 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
750 return g;
751 }
752#endif
753 return m;
754 }
755 long lastPressureIterations() const { return lastPressureIters_; }
756 int nx() const { return nx_; }
757 int ny() const { return ny_; }
758 int nz() const { return nz_; }
759
760 private:
761 struct Comp {
762 CCField u, b, inhom, rscale, mask;
763 FV AC, AW, AE, AS, AN, AB, AT;
764 IbmOverlay ov;
765 Kokkos::View<int*, CCMem> idMap;
766 Kokkos::View<int, CCMem> counter;
767 int nCut = 0;
768 };
769
770 public: // nvcc forbids extended __host__ __device__ lambdas inside private/protected members.
771 // Advection treated implicitly (implicit-FOU upwind + deferred correction): the user opt-in
772 // (set_implicit_advection) on any path, OR the DEFAULT on the domain-BC path (inflow/outflow),
773 // where explicit advection is unstable. The velocity-MG BC path carries its own FOU coarse
774 // operator, so the default does not apply there (it still honours the explicit opt-in).
775 bool implicitAdv() const { return advect_ && (implicitFou_ || (hasBc_ && !useVelocityMg_)); }
776 // Domain-BC momentum solved via the Robust-Scaled cut-cell / FOU stencil smoother
777 // (ibmRbgsStencilColor
778 // + reflection-ghost BCs), not the all-fluid const-coeff fold. Needed when (a) an immersed solid
779 // is present (cut-cell no-slip must be in the operator), or (b) advection is implicit (the FOU
780 // upwind lives in the stencil -> stable at large dt, the fully-implicit design), or (c) any
781 // per-cell coefficient lives in the stencil: variable properties, or the implicit CFD-DEM drag
782 // diagonal (hasDrag_). Without (c) an all-fluid domain-BC problem fell through to the
783 // CONST-COEFFICIENT fold smoother (Ac = rho/dt + 6mu computed inline), which never reads the
784 // assembled band -- the drag never entered the momentum operator while the porous projection's
785 // w_f=idt/(idt+beta_f) assumed it did, an inconsistency with pressure-loop gain beta*dt/rho (a
786 // fixed bed diverged whenever beta > rho/dt; measured gain 3.84 vs predicted 3.85 at beta=77,
787 // idt=20).
788 bool bcStencilPath() const {
789 return hasBc_ && !useVelocityMg_ &&
790 (hasSolid_ || implicitAdv() || varProps_ || varRho_ || hasDrag_);
791 }
792 // Fill a property field's ghosts for the face means: periodic/halo base, then zero-gradient
793 // (copy) on domain-BC (wall/inflow/outflow) faces — a periodic wrap there would bring the wrong
794 // layer's value to the wall face (destabilising, especially for the harmonic mean).
796 fillGhosts(f);
797 if (!distributed_)
798 for (int face = 0; face < 6; ++face)
799 if (bc_[face] != 0)
800 applyScalarBcFace(f, face / 2, face % 2, 1, 0.0); // type 1 = Neumann copy
801 }
802 void fillMuGhosts() { fillPropGhosts(muField_); }
803 // Staggered face stride of velocity component c (the -c face of cell i pairs cells i and i-s).
804 long strideOf(int c) const { return (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y; }
805 // The face-property accessor for the momentum stencil of component c: mu constant-or-field
806 // (arithmetic/harmonic mean), rho constant-or-field (arithmetic face mean for the time diagonal —
807 // the same face density the variable-density projection uses).
809 VarFaceProps fp;
810 fp.haveMu = varProps_;
811 if (varProps_)
812 fp.mu = CCConst(muField_);
813 else
814 fp.muC = mu_;
815 fp.harmMu = harmonicMu_;
816 fp.haveRho = varRho_;
817 if (varRho_) {
818 fp.rho = CCConst(rhoField_);
819 fp.idt = 1.0 / dt_;
820 fp.sc = strideOf(c);
821 } else
822 fp.rhoIdtC = rho_ / dt_;
823 return fp;
824 }
826 const double idiag = rho_ / dt_, beta = mu_;
827 if (varProps_)
828 fillMuGhosts(); // face means read mu at i +- stride (boundary inner cells -> ghosts)
829 if (varRho_)
830 fillPropGhosts(rhoField_);
831 for (int c = 0; c < 3; ++c) {
832 Kokkos::deep_copy(C[c].rscale, 1.0);
833 Kokkos::deep_copy(C[c].inhom, 0.0);
834 if (varProps_ || varRho_)
835 ibmBuildDiffusionVar(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e_.x,
836 e_.y, e_.z, G, makeFaceProps(c));
837 else
838 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,
839 e_.z, beta, idiag);
840 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,
841 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
842 if (hasDrag_)
844 }
845 }
846 // copy the nx*ny*nz inner cells between two extended blocks of different ghost width (g=2 <-> g=1
847 // MG).
848 void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg) {
849 CCExec space;
850 const int NX = nx_, NY = ny_;
851 Kokkos::parallel_for(
852 "peclet::flow::copyInner", Kokkos::RangePolicy<CCExec>(space, 0, (long)nx_ * ny_ * nz_),
853 KOKKOS_LAMBDA(long c) {
854 const int ix = (int)(c % NX), iy = (int)((c / NX) % NY), iz = (int)(c / ((long)NX * NY));
855 const long di =
856 (long)(ix + dg) + (long)(iy + dg) * de.x + (long)(iz + dg) * (long)de.x * de.y;
857 const long si =
858 (long)(ix + sg) + (long)(iy + sg) * se.x + (long)(iz + sg) * (long)se.x * se.y;
859 dst(di) = src(si);
860 });
861 }
862 // Copy the ENTIRE destination block (including its ghost ring) from the source block at per-axis
863 // cell offset `off`: dst(x,y,z) <- src(x+off, y+off, z+off). Bridges a G=2 field to the g=1 MG
864 // block INCLUDING the g=1 ghosts (off = G-1), so face means at the first inner cell read a valid
865 // neighbour. Requires the source ghosts filled (fillGhosts/fillPropGhosts) — under MPI those are
866 // the cross-rank values, so the bridge is decomposition-correct.
867 void copyBlockShifted(CCField dst, C3 de, CCConst src, C3 se, int off) {
868 CCExec space;
869 Kokkos::parallel_for(
870 "peclet::flow::copyBlockShifted",
871 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {0, 0, 0}, {de.x, de.y, de.z}),
872 KOKKOS_LAMBDA(int x, int y, int z) {
873 const long di = (long)x + (long)y * de.x + (long)z * (long)de.x * de.y;
874 const long si =
875 (long)(x + off) + (long)(y + off) * se.x + (long)(z + off) * (long)se.x * se.y;
876 dst(di) = src(si);
877 });
878 }
879 // Fill ghost width G periodically on all 3 axes (x then y then z, covering corners). Distributed:
880 // the velocity-block halo (cross-rank + periodic, all ghosts incl. corners).
882#ifdef PECLET_FLOW_MPI
883 if (distributed_) {
884 velDev_->exchange(f);
885 return;
886 }
887#endif
888 fillAxis(f, 0);
889 fillAxis(f, 1);
890 fillAxis(f, 2);
891 }
892 // Fused periodic FACE-ghost fill in ONE kernel (vs 3 fillAxis): each inner boundary cell scatters
893 // its periodic image to the opposite face ghost, all 3 axes at once. Valid only for
894 // FACE-neighbour (7-point) stencils -- it does NOT fill the corner/edge ghosts (which fillAxis's
895 // sequential x->y->z does). The IBM RB-GS smoother reads only the 7-point stencil, so this is
896 // exact there and cuts the velocity solve's dominant kernel-launch cost (~7200 -> ~2400 fill
897 // launches/step) at low resolution. NOT for the Koren advection RHS (reads diagonals) -- keep the
898 // full fillGhosts there.
900#ifdef PECLET_FLOW_MPI
901 if (distributed_) {
902 velDev_->exchange(f);
903 return;
904 } // halo gives all ghosts; the 7-pt smoother uses the faces
905#endif
906 CCExec space;
907 C3 e = e_;
908 const int Nx = nx_, Ny = ny_, Nz = nz_;
909 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
910 CCField ff = f;
911 Kokkos::parallel_for(
912 "peclet::flow::ibm_facefill", Kokkos::RangePolicy<CCExec>(space, 0, (long)nx_ * ny_ * nz_),
913 KOKKOS_LAMBDA(long n) {
914 const int ix = (int)(n % Nx), iy = (int)((n / Nx) % Ny), iz = (int)(n / ((long)Nx * Ny));
915 const long i = (long)(ix + G) * sx + (long)(iy + G) * sy + (long)(iz + G) * sz;
916 if (ix < G)
917 ff(i + (long)Nx * sx) = ff(i);
918 else if (ix >= Nx - G)
919 ff(i - (long)Nx * sx) = ff(i);
920 if (iy < G)
921 ff(i + (long)Ny * sy) = ff(i);
922 else if (iy >= Ny - G)
923 ff(i - (long)Ny * sy) = ff(i);
924 if (iz < G)
925 ff(i + (long)Nz * sz) = ff(i);
926 else if (iz >= Nz - G)
927 ff(i - (long)Nz * sz) = ff(i);
928 });
929 }
930 void fillAxis(CCField f, int axis) {
931 CCExec space;
932 C3 e = e_;
933 int N3[3] = {nx_, ny_, nz_};
934 int dims[3] = {e.x, e.y, e.z};
935 long st[3] = {1, e.x, (long)e.x * e.y};
936 const int a = axis, b = (axis + 1) % 3, c = (axis + 2) % 3;
937 const long sa = st[a], sb = st[b], sc = st[c];
938 const int N = N3[a];
939 CCField ff = f;
940 Kokkos::parallel_for(
941 "peclet::flow::ibm_pfill",
942 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {0, 0}, {dims[b], dims[c]}),
943 KOKKOS_LAMBDA(int p0, int p1) {
944 const long base = (long)p0 * sb + (long)p1 * sc;
945 for (int gl = 0; gl < G; ++gl) {
946 ff(base + (long)gl * sa) = ff(base + (long)(gl + N) * sa);
947 ff(base + (long)(G + N + gl) * sa) = ff(base + (long)(G + gl) * sa);
948 }
949 });
950 }
951 void buildRhs(int c) {
952 CCExec space;
953 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_;
954 C3 e = e_;
955 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
956 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
957 un = CCConst(old_[c]);
958 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
959 // Pure implicit FOU (no deferred correction): 1st-order upwind carried entirely by the
960 // operator, no explicit high-order term in the RHS -- maximally dissipative/stable (diffuses
961 // sharp shear layers). Only meaningful on an implicit-advection path.
962 const bool pureFou = implicitAdv() && !deferredCorr_;
963 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
964 bc = hasBc_ && !bcStencilPath(); // fold RHS only on the const-coeff domain-BC path;
965 // on the stencil path (solid and/or implicit advection) the walls enter via reflection ghosts
966 // (smoothComp) and the RHS carries the IBM inhom (=0 for no-slip) + the deferred correction.
967 // incr predictor carries -grad(P^n).
968 const bool ifou =
969 implicitAdv() &&
970 deferredCorr_; // deferred correction: keep (HO - FOU) explicit in the RHS
971 // (implicit on the domain-BC path by default, opt-in elsewhere)
972 const int sch = advScheme_; // 0 = SOU (default), 1 = Koren TVD
973 // Mode-2 wall-aware pressure force (collocated): -grad(P) = the TRANSPOSE of the wall-aware
974 // cell->face constraint interpolation, precomputed per component (the plain path's central
975 // difference is the transpose of the plain 1/2-1/2 average, so this keeps the momentum/
976 // constraint operators an adjoint pair on both paths).
977 const bool tg = Grid::collocated && faceInterp_ >= 2 && faceInterp_ <= 5 && incr;
978 // modes 6/7: openness-weighted -grad(P^n) predictor, matching the fs-weighted correction
979 const bool wg = Grid::collocated && (faceInterp_ == 6 || faceInterp_ == 7) && incr;
980 if constexpr (Grid::collocated) {
981 if (tg) {
982 CCField xcs[3] = {xcx_, xcy_, xcz_};
983 CCField oax[3] = {ox_, oy_, oz_};
984 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
985 faceInterp_ >= 3, c, e_, G);
986 } else if (wg) {
987 CCField oax[3] = {ox_, oy_, oz_};
988 centerGradOpen(tgp_, CCConst(P_), CCConst(oax[c]), c, e_, G);
989 }
990 }
991 CCConst gpw = CCConst(tgp_); // empty view on the staggered path (tg/wg false there)
992 // Mode-4 fully-FV momentum via DEFECT CORRECTION: solve M·u^{k+1} = M·u^k − rs·L_FV(u^k) +
993 // rs·b_FV so the fixed point satisfies the second-order finite-volume balance L_FV·u* = b_FV
994 // exactly, with the (stable, small-cell-safe) IBM matrix M only as preconditioner. fvM_ = M·u^k
995 // (stencilMatvec), fvL_ = L_FV(u^k) (fvViscousApply: o_f faces + cs time + centroid wall drag).
996 // Interior cells: M = L_FV → the defect vanishes → byte-identical to mode 0. Stokes only
997 // (advection folds into the IBM matrix, not yet into L_FV).
998 const bool wd = Grid::collocated && faceInterp_ >= 4;
999 if constexpr (Grid::collocated)
1000 if (wd) {
1001 stencilMatvec(fvM_, CCConst(C[c].u), MConst(C[c].AC), MConst(C[c].AW), MConst(C[c].AE),
1002 MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB), MConst(C[c].AT), e_, G);
1003 // modes 5/6: TRUE-NORMAL embed wall drag (embedDirichletGradient); mode 4: axis-by-axis W_a
1004 // g_a
1005 if (faceInterp_ >= 5)
1006 embedViscousApply(fvL_, CCConst(C[c].u), CCConst(sdf_), CCConst(cs_), CCConst(ox_),
1007 CCConst(oy_), CCConst(oz_), mu_, rho_ / dt_, e_, G);
1008 else
1009 fvViscousApply(fvL_, CCConst(C[c].u), CCConst(sdf_), CCConst(cs_), CCConst(ox_),
1010 CCConst(oy_), CCConst(oz_), mu_, rho_ / dt_, e_, G);
1011 }
1012 CCConst fvM = CCConst(fvM_), fvL = CCConst(fvL_), cs = CCConst(cs_);
1013 const double fvw = fvRelax_; // local copy — a KOKKOS_LAMBDA must not read a member (device
1014 // deref of the host `this` pointer = illegal memory access)
1015 // b = descale*(idiag*u^n - rho*Koren(u^k) + rho*FOU(u^k) + f - grad P^n) - inhom (+ BC fold
1016 // brhs). The time base is u^n (Picard); the advecting velocity & advected field are the current
1017 // iterate u^k.
1018 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1019 Kokkos::parallel_for(
1020 "rhs", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1021 KOKKOS_LAMBDA(int x, int y, int z) {
1022 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1023 double aK = 0.0, aF = 0.0;
1024 if (adv) {
1025 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};
1026 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1027 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1028 if (ifou)
1029 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1030 }
1031 // incremental predictor's -grad(P^n): central-difference cell gradient on the collocated
1032 // grid (or the wall-aware transpose gradient, mode 2), one-sided face gradient (P at the
1033 // high cell of the staggered face) on the staggered grid.
1034 const double gp =
1035 !incr ? 0.0
1036 : Grid::collocated
1037 ? ((tg || wg) ? gpw(i) : 0.5 * (P((long)i + strd) - P((long)i - strd)))
1038 : (P(i) - P((long)i - strd));
1039 if (wd) { // FV defect-correction RHS M·u − ω·rs·(L_FV·u − b_FV), b_FV = idt·cs·u^n +
1040 // cs·(f − grad P). ω<1 damps the (stiff, explicit-lagged) wall-flux
1041 // correction; the fixed point L_FV·u* = b_FV is independent of ω.
1042 const double bfv = idiag * cs(i) * un(i) + cs(i) * (fc - gp);
1043 bb(i) = fvM(i) - fvw * rs(i) * (fvL(i) - bfv);
1044 } else {
1045 bb(i) =
1046 rs(i) * (idiag * un(i) + fc - rho * aK + rho * aF - gp) + (bc ? brhs(i) : -inh(i));
1047 }
1048 }); // BC fold (brhs) on the domain-BC path; -inhom on the IBM path (=0 for no-slip)
1049 }
1050 // Sibling of buildRhs adding a per-cell body force fb(i) (Boussinesq buoyancy / CFD-DEM
1051 // feedback): the constant fc becomes fc + fb(i). Kept as a separate kernel so buildRhs stays
1052 // byte-identical (no codegen drift on the single-phase path). Selected in step() when
1053 // hasCellForce_.
1054 void buildRhsForced(int c) {
1055 CCExec space;
1056 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_;
1057 C3 e = e_;
1058 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1059 CCConst fb = CCConst(cellForce_[c]);
1060 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
1061 un = CCConst(old_[c]);
1062 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
1063 const bool pureFou = implicitAdv() && !deferredCorr_;
1064 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
1065 bc = hasBc_ && !bcStencilPath();
1066 const bool ifou = implicitAdv() && deferredCorr_;
1067 const int sch = advScheme_;
1068 const bool tg = Grid::collocated && faceInterp_ >= 2 && incr; // wall-aware -grad(P) (mode 2/3)
1069 if constexpr (Grid::collocated)
1070 if (tg) {
1071 CCField xcs[3] = {xcx_, xcy_, xcz_};
1072 CCField oax[3] = {ox_, oy_, oz_};
1073 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
1074 faceInterp_ >= 3, c, e_, G);
1075 }
1076 CCConst gpw = CCConst(tgp_);
1077 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1078 Kokkos::parallel_for(
1079 "rhs_forced", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1080 KOKKOS_LAMBDA(int x, int y, int z) {
1081 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1082 double aK = 0.0, aF = 0.0;
1083 if (adv) {
1084 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};
1085 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1086 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1087 if (ifou)
1088 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1089 }
1090 const double gp = !incr ? 0.0
1091 : Grid::collocated
1092 ? (tg ? gpw(i) : 0.5 * (P((long)i + strd) - P((long)i - strd)))
1093 : (P(i) - P((long)i - strd));
1094 bb(i) = rs(i) * (idiag * un(i) + fc + fb(i) - rho * aK + rho * aF - gp) +
1095 (bc ? brhs(i) : -inh(i));
1096 });
1097 }
1098 // Variable-density RHS (sibling of buildRhsForced): the time term, the advection weight, and the
1099 // per-cell body force all use the FACE density of component c (arithmetic mean over the staggered
1100 // face, matching VarFaceProps::idiag and the projection coefficient — this three-way consistency
1101 // is what makes discrete hydrostatic balance exact). The cell force fb is face-interpolated for
1102 // the same reason (a rho*g cell field becomes rho_face*g at the velocity location). Requires the
1103 // rho ghosts filled (rebuildStencils / buildAdvStencilVar did it this step).
1104 void buildRhsVar(int c) {
1105 CCExec space;
1106 const double idt = 1.0 / dt_, fc = f_[c];
1107 C3 e = e_;
1108 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1109 CCConst fb = CCConst(cellForce_[c]);
1110 CCConst rf = CCConst(rhoField_);
1111 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u), uu = CCConst(C[c].u),
1112 un = CCConst(old_[c]);
1113 const long strd = strideOf(c);
1114 const bool pureFou = implicitAdv() && !deferredCorr_;
1115 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
1116 bc = hasBc_ && !bcStencilPath();
1117 const bool ifou = implicitAdv() && deferredCorr_;
1118 const int sch = advScheme_;
1119 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1120 Kokkos::parallel_for(
1121 "rhs_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1122 KOKKOS_LAMBDA(int x, int y, int z) {
1123 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1124 const double rhoF = 0.5 * (rf(i) + rf(i - strd)); // face density of the velocity unknown
1125 double aK = 0.0, aF = 0.0;
1126 if (adv) {
1127 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};
1128 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa)
1129 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa);
1130 if (ifou)
1131 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa);
1132 }
1133 const double gp = !incr ? 0.0
1134 : Grid::collocated ? 0.5 * (P((long)i + strd) - P((long)i - strd))
1135 : (P(i) - P((long)i - strd));
1136 const double fbF = 0.5 * (fb(i) + fb(i - strd));
1137 bb(i) = rs(i) * (rhoF * idt * un(i) + fc + fbF - rhoF * aK + rhoF * aF - gp) +
1138 (bc ? brhs(i) : -inh(i));
1139 });
1140 }
1141 // Implicit-FOU velocity stencil (CUDA build_adv_stencil_k + ibm_modify_stencil): backward-Euler
1142 // diffusion (idiag+6beta diag, -beta off) + rho*FOU(u^k) upwind operator (diagonally dominant ->
1143 // stable at high Re), then the Robust-Scaled cut-cell bake. The advecting velocity u^k = the
1144 // current C[*].u (ghosts filled).
1145 void buildAdvStencil(int c) {
1146 const double idiag = rho_ / dt_, beta = mu_, fouw = rho_;
1147 C3 e = e_;
1148 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,
1149 beta, idiag);
1150 CCExec space;
1151 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,
1152 AT = C[c].AT;
1153 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
1154 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1155 Kokkos::parallel_for(
1156 "advstencil", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1157 KOKKOS_LAMBDA(int x, int y, int z) {
1158 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1159 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
1160 czp = AT(i);
1161 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
1162 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp);
1163 AC(i) = (float)cC;
1164 AW(i) = (float)cxm;
1165 AE(i) = (float)cxp;
1166 AS(i) = (float)cym;
1167 AN(i) = (float)cyp;
1168 AB(i) = (float)czm;
1169 AT(i) = (float)czp;
1170 });
1171
1172 Kokkos::deep_copy(C[c].rscale, 1.0);
1173 Kokkos::deep_copy(C[c].inhom, 0.0);
1174 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,
1175 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
1176 if (hasDrag_)
1177 addDragDiagonal(c);
1178 }
1179 // Variable-property sibling of buildAdvStencil: VarFaceProps diffusion build (per-face mu, face-
1180 // density time diagonal) + the FOU upwind weighted by the FACE density (constant path:
1181 // fouw=rho_). Separate kernel so the validated buildAdvStencil stays byte-identical.
1183 C3 e = e_;
1184 if (c == 0) {
1185 if (varProps_)
1186 fillMuGhosts();
1187 if (varRho_)
1188 fillPropGhosts(rhoField_);
1189 }
1190 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,
1191 e.z, G, makeFaceProps(c));
1192 CCExec space;
1193 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,
1194 AT = C[c].AT;
1195 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
1196 const bool vr = varRho_;
1197 const double rhoC = rho_;
1198 CCConst rf = vr ? CCConst(rhoField_) : CCConst();
1199 const long sc = strideOf(c);
1200 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1201 Kokkos::parallel_for(
1202 "advstencil_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1203 KOKKOS_LAMBDA(int x, int y, int z) {
1204 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1205 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
1206 czp = AT(i);
1207 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
1208 const double fouw = vr ? 0.5 * (rf(i) + rf(i - sc)) : rhoC;
1209 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp);
1210 AC(i) = (float)cC;
1211 AW(i) = (float)cxm;
1212 AE(i) = (float)cxp;
1213 AS(i) = (float)cym;
1214 AN(i) = (float)cyp;
1215 AB(i) = (float)czm;
1216 AT(i) = (float)czp;
1217 });
1218 Kokkos::deep_copy(C[c].rscale, 1.0);
1219 Kokkos::deep_copy(C[c].inhom, 0.0);
1220 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,
1221 C[c].rscale, C[c].ov, C[c].nCut, 0.0f);
1222 if (hasDrag_)
1223 addDragDiagonal(c);
1224 }
1225 // Backflow stabilization (Bazilevs 2009 / Esmaily-Moghadam 2011) for the NORMAL momentum at
1226 // outflow faces: add the dissipative diagonal term beta*rho*|min(u.n,0)| where the outflow
1227 // reverses (fluid re-entering, u.n<0). This removes the spurious kinetic-energy influx that the
1228 // do-nothing/zero- gradient outflow advects in -- the "backflow divergence" that blows up
1229 // separated flows (e.g. the BFS recirculation reaching the outlet), worse on finer grids. Purely
1230 // dissipative (u_ext=0), so it is implicit + unconditionally stable, and INERT where the outlet
1231 // is outgoing (u.n>=0) -> the channel and any non-reversing outflow stay byte-identical. Applied
1232 // to C[c].AC after buildAdvStencil (per Picard iteration, lagged at u^k); only the component
1233 // normal to each outflow face.
1234 void applyBackflowStab(int c) {
1235 if (backflowBeta_ <= 0.0 || !hasOutflow_)
1236 return;
1237 CCExec space;
1238 const double beta = backflowBeta_, rho = rho_;
1239 C3 e = e_;
1240 int dims[3] = {e.x, e.y, e.z};
1241 long st[3] = {1, e.x, (long)e.x * e.y};
1242 FV AC = C[c].AC;
1243 CCConst u = CCConst(C[c].u);
1244 const int a = c; // the normal component of a face on axis a is component a
1245 for (int s = 0; s < 2; ++s) {
1246 if (bc_[2 * a + s] != 3)
1247 continue; // outflow faces only
1248 const long sa = st[a];
1249 const int na = dims[a];
1250 const int bic = (s == 0) ? G : (na - G - 1); // outflow-adjacent inner normal-velocity cell
1251 const double sgn = (s == 0) ? 1.0 : -1.0; // reversal (u.n<0): u>0 at -a, u<0 at +a
1252 const int b = (a + 1) % 3, cc = (a + 2) % 3;
1253 const long sb = st[b], sc = st[cc];
1254 using MD2 = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>;
1255 Kokkos::parallel_for(
1256 "peclet::flow::backflow", MD2(space, {G, G}, {dims[b] - G, dims[cc] - G}),
1257 KOKKOS_LAMBDA(int p0, int p1) {
1258 const long i = (long)p0 * sb + (long)p1 * sc + (long)bic * sa;
1259 const double back =
1260 sgn * u(i); // > 0 exactly where the outflow reverses (|min(u.n,0)|)
1261 if (back > 0.0)
1262 AC(i) += (float)(beta * rho * back); // dissipative diagonal (u_ext = 0)
1263 });
1264 }
1265 }
1266 // max|a-b| over inner cells (Picard outer-tolerance check).
1268 CCExec space;
1269 C3 e = e_;
1270 double m = 0;
1271 Kokkos::parallel_reduce(
1272 "maxdiff",
1273 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
1274 {e.x - G, e.y - G, e.z - G}),
1275 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1276 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1277 const double d = Kokkos::fabs(a(i) - b(i));
1278 if (d > acc)
1279 acc = d;
1280 },
1281 Kokkos::Max<double>(m));
1282 return m;
1283 }
1284 void smoothComp(int c) {
1285 if constexpr (Grid::collocated) {
1286 if (hasBc_) { // collocated domain BC: the (all-fluid) IBM diffusion stencil + cell-centered
1287 // wall
1288 // reflection ghosts refreshed each colour (explicit no-slip; no fold). Converges to the
1289 // wall value.
1290 for (int it = 0; it < velIters_; ++it) {
1291 fillVelGhostsTo(C[c].u, c, 0);
1292 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1293 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1294 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 0);
1295 fillVelGhostsTo(C[c].u, c, 0);
1296 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1297 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1298 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 1);
1299 }
1300 return;
1301 }
1302 }
1303 if (bcStencilPath()) {
1304 // Domain BCs solved with the Robust-Scaled cut-cell / FOU stencil (built by setSolid /
1305 // buildAdvStencil) while refreshing the domain-BC ghosts each colour -- explicit walls/inflow
1306 // (reflection, fold=0) + outflow zero-gradient. Mirrors the collocated path above. Used for
1307 // an immersed solid (cut-cell no-slip in the operator) and/or implicit advection (FOU upwind
1308 // in the stencil -> stable at large dt). The const-coeff fold smoothers below are all-fluid,
1309 // diffusion-only: they ignore the solid AND run advection explicitly (CFL-limited).
1310 for (int it = 0; it < velIters_; ++it) {
1311 fillVelGhostsTo(C[c].u, c, 0);
1312 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1313 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1314 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 0);
1315 fillVelGhostsTo(C[c].u, c, 0);
1316 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1317 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1318 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 1);
1319 }
1320 return;
1321 }
1322 if (hasBc_ &&
1323 useVelocityMg_) { // domain-BC velocity multigrid: const-coeff aniso op + no-slip/inflow/
1324 // outflow boundary fold on every level (CUDA setDiffusionConstAllLevels +
1325 // setDiffusionBoundaryFold).
1326 vmg_.setDomainBcOp(c, mu_, rho_ / dt_); // per component (the fold is component-dependent)
1328 c, 1); // set the level-0 boundary ghosts (wall fold=0, inflow value, outflow zero-grad)
1329 // Re-impose the velocity BC on the vel-MG's level-0 iterate each colour/residual (the
1330 // const-coeff smoother updates the held Dirichlet faces) -> the vel-MG converges to the RB-GS
1331 // fixed point (not the ~2% drift CUDA's vmg leaves at the boundary corners).
1332 vmg_.setBcApplyL0([this, c](CCField x) { fillVelGhostsTo(x, c, 1); });
1333 vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8);
1334 return;
1335 }
1336 if (hasBc_) { // domain-BC (no immersed solid): CUDA's double const-coeff diff_k + dcorr fold
1337 const I3 e{e_.x, e_.y, e_.z}, og{0, 0, 0};
1338 const double beta = mu_, Ac = rho_ / dt_ + 6.0 * mu_;
1339 for (int it = 0; it < velIters_; ++it) {
1340 fillVelGhosts(c, 1); // re-impose wall faces (fold) before each color
1341 diffSmoothColor(C[c].u, CCConst(C[c].b), e, og, G, beta, Ac, 0, CCConst(bcDcorr_[c]));
1342 fillVelGhosts(c, 1);
1343 diffSmoothColor(C[c].u, CCConst(C[c].b), e, og, G, beta, Ac, 1, CCConst(bcDcorr_[c]));
1344 }
1345 return;
1346 }
1347 if (useVelocityMg_) { // IBM velocity multigrid: fine = sharp As_[c]; coarse op depends on the
1348 // regime.
1349 vmg_.setFineStencil(FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1350 FPC(C[c].AB), FPC(C[c].AT));
1351 if (implicitFou_ && advect_) {
1352 // UPWIND-CONVECTIVE coarse op (advection-dominated): aniso const-coeff diffusion + dt*FOU
1353 // from the restricted advecting velocity (restrictAdvVelocities ran once in step()). No pin
1354 // / no exclude mask.
1355 vmg_.buildUpwindCoarse(c, mu_, rho_ / dt_, rho_);
1356 } else {
1357 // STAIRCASE coarse op (diffusion-only): theta classification + clean-fluid exclude (exact
1358 // == RB-GS).
1359 const Off3 off =
1360 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
1361 ibmVolfrac(vmgTheta_, CCConst(sdf_), e_, off);
1362 ibmCleanFluidMask(vmgClean_, CCConst(sdf_), e_, off);
1363 vmg_.setStaircase(CCConst(vmgTheta_), CCConst(C[c].mask), CCConst(vmgClean_), mu_,
1364 rho_ / dt_, 0.5);
1365 }
1366 vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8);
1368 c); // re-impose no-slip at solid (the masked solve leaves them at the pin value)
1369 return;
1370 }
1371 for (int it = 0; it < velIters_;
1372 ++it) { // IBM / periodic: Robust-Scaled cut-cell stencil (float)
1374 C[c].u); // 7-point smoother reads faces only -> the fused 1-kernel face fill suffices
1375 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1376 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1377 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 0);
1378 fillGhostsFaces(C[c].u);
1379 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), MConst(C[c].AC), MConst(C[c].AW),
1380 MConst(C[c].AE), MConst(C[c].AS), MConst(C[c].AN), MConst(C[c].AB),
1381 MConst(C[c].AT), CCConst(C[c].mask), e_, og_, G, 1);
1382 }
1383 }
1384 // pressure ghost at domain faces for the incremental predictor's grad(P): zero-gradient (Neumann)
1385 // at every non-periodic face so grad(P) carries no spurious force there (the periodic fill
1386 // wrapped the opposite boundary's pressure). Outflow pressure (Dirichlet p=0) is enforced
1387 // separately in the MG solve.
1389 CCExec space;
1390 C3 e = e_;
1391 CCField P = P_;
1392 int dims[3] = {e.x, e.y, e.z};
1393 long st[3] = {1, e.x, (long)e.x * e.y};
1394 for (int a = 0; a < 3; ++a)
1395 for (int s = 0; s < 2; ++s) {
1396 if (bc_[2 * a + s] == 0)
1397 continue;
1398 const int b = (a + 1) % 3, c = (a + 2) % 3;
1399 const long sa = st[a], sb = st[b], sc = st[c];
1400 const int na = dims[a];
1401 const int bic = (s == 0) ? G : (na - G - 1);
1402 const int lo = (s == 0) ? 0 : (na - G), hi = (s == 0) ? (G - 1) : (na - 1);
1403 Kokkos::parallel_for(
1404 "pbcghost",
1405 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {0, 0}, {dims[b], dims[c]}),
1406 KOKKOS_LAMBDA(int p0, int p1) {
1407 const long base = (long)p0 * sb + (long)p1 * sc;
1408 const double pin = P(base + (long)bic * sa);
1409 for (int ia = lo; ia <= hi; ++ia)
1410 P(base + (long)ia * sa) = pin;
1411 });
1412 }
1413 }
1414 // domain-BC velocity ghosts: periodic-fill periodic axes, then apply per-face BCs (fold=0
1415 // explicit/1 implicit).
1416 void fillVelGhosts(int comp, int fold) { fillVelGhostsTo(C[comp].u, comp, fold); }
1417 void applyVelocityBcComp(int comp, int fold, bool doOutflow) {
1418 applyVelocityBcCompTo(C[comp].u, comp, fold, doOutflow);
1419 }
1420 // Field-parameterized variants (so the velocity-MG can re-impose the BC on its own level-0
1421 // iterate).
1422 void fillVelGhostsTo(CCField f, int comp, int fold) {
1423#ifdef PECLET_FLOW_MPI
1424 if (distributed_) {
1425 velDev_->exchange(f);
1426 applyVelocityBcCompTo(f, comp, fold, true);
1427 return;
1428 }
1429#endif
1430 for (int a = 0; a < 3; ++a)
1431 if (bc_[2 * a] == 0 && bc_[2 * a + 1] == 0)
1432 fillAxis(f, a);
1433 applyVelocityBcCompTo(f, comp, fold, true);
1434 }
1435 void applyVelocityBcCompTo(CCField f, int comp, int fold, bool doOutflow) {
1436 if (!hasBc_)
1437 return;
1438 B3 e{e_.x, e_.y, e_.z};
1439 if constexpr (Grid::collocated) {
1440 // Cell-centered velocity: reflect this component about each non-periodic boundary face. Walls
1441 // (type 1, vel 0) and Dirichlet/lid (type 2, prescribed vel) both use the same reflection;
1442 // outflow (type 3) and per-position inlet profiles are the inflow/outflow milestone (phase
1443 // 5b).
1444 for (int a = 0; a < 3; ++a)
1445 for (int s = 0; s < 2; ++s) {
1446 const int ff = 2 * a + s;
1447 const int t = bc_[ff];
1448 if (t == 0)
1449 continue;
1450 if (t == 3) {
1451 if (doOutflow)
1452 bcNeumannGhost(f, e, G, a, s);
1453 continue;
1454 } // outflow: zero-gradient ghost
1455 if (bcProf_[ff].extent(0) >
1456 0) // per-position inlet profile (e.g. the BFS partial parabola)
1457 bcVelocityColocated(f, e, G, a, s, 0.0, comp, bcProf_[ff], bcProfNc_[ff]);
1458 else
1459 bcVelocityColocated(f, e, G, a, s,
1460 bcVel_[ff][comp]); // wall / inflow / lid (Dirichlet)
1461 }
1462 return;
1463 }
1464 for (int a = 0; a < 3; ++a)
1465 for (int s = 0; s < 2; ++s) {
1466 const int ff = 2 * a + s;
1467 const int t = bc_[ff];
1468 if (t == 0)
1469 continue;
1470 if (t == 3) {
1471 if (doOutflow)
1472 bcOutflowComp(f, e, G, a, s, comp, fold);
1473 continue;
1474 }
1475 if (bcProf_[ff].extent(0) > 0)
1476 bcVelocityComp(f, e, G, a, s, comp, 0.0, fold, bcProf_[ff], bcProfNc_[ff]);
1477 else
1478 bcVelocityComp(f, e, G, a, s, comp, bcVel_[ff][comp], fold);
1479 }
1480 }
1481 // implicit-diffusion wall fold (CUDA setup_bc_diffusion): dcorr += (wall:+beta tangential /
1482 // outflow:-beta), brhs += 2*beta*wall (tangential Dirichlet); bake dcorr into the per-component
1483 // stencil diagonal.
1485 const double beta = mu_;
1486 B3 e{e_.x, e_.y, e_.z};
1487 for (int c = 0; c < 3; ++c) {
1488 Kokkos::deep_copy(bcDcorr_[c], 0.0);
1489 Kokkos::deep_copy(bcBrhs_[c], 0.0);
1490 for (int a = 0; a < 3; ++a)
1491 for (int s = 0; s < 2; ++s) {
1492 const int t = bc_[2 * a + s];
1493 double dval, bval;
1494 if (t == 3) {
1495 dval = -beta;
1496 bval = 0.0;
1497 } else if (t != 0 && c != a) {
1498 dval = beta;
1499 bval = 2.0 * beta * bcVel_[2 * a + s][c];
1500 } else
1501 continue; // periodic, or the normal component at a wall (held directly)
1502 bcDiffusionFold(bcDcorr_[c], bcBrhs_[c], e, G, a, s, dval, bval);
1503 }
1504 // dcorr is passed to the (double) const-coeff smoother diffSmoothColor each sweep -- matching
1505 // CUDA diff_k (Ac + dcorr in double), NOT baked into the float stencil.
1506 }
1507 }
1508 // Incremental (rotational) cut-cell projection: solve A phi = -div_open(u*) (RB-GS,
1509 // mean-removed), u -= grad phi, then accumulate the physical pressure P += (rho/dt)*phi -
1510 // mu*div(u*) (Timmermans).
1511 void project() {
1512 // ghosts incl. domain BCs (outflow zero-gradient) BEFORE the divergence -- matches CUDA
1513 // apply_velocity_bc before diverg_open, so div(u*) counts the outflow flux (else the rotational
1514 // pressure pumps the mis-counted outflow divergence and blows up the outflow-wall corner).
1515 if constexpr (Grid::collocated) {
1516 // Approximate (MAC) projection: average the cell velocities onto a face field, then project
1517 // THAT. Use the BC-aware ghost fill (periodic / cross-rank + domain BCs) so the averaged
1518 // inflow/outflow faces carry the right value -- at open boundaries the flux is counted
1519 // (closed walls are openness 0).
1520 for (int c = 0; c < 3; ++c)
1521 fillVelGhosts(c, 0);
1522 if ((faceInterp_ >= 1 && faceInterp_ <= 5) ||
1523 faceInterp_ == 7) // wall-aware flux map at solid
1524 centerToFaceWallAware(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u),
1525 CCConst(sdf_), CCConst(xcx_), CCConst(xcy_), CCConst(xcz_),
1526 faceInterp_ >= 3, e_, G); // faces (modes 1-5,7; mode 6 = plain)
1527 else
1528 centerToFace(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), e_, G);
1529 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(ox_), CCConst(oy_), CCConst(oz_),
1530 div_, e_, G);
1531 } else {
1532 for (int c = 0; c < 3; ++c)
1533 fillVelGhosts(c, 0);
1534 if (porous_) // volume-averaged continuity: div(open*eps*u*), constraint div(eps
1535 // u)=-d(eps)/dt
1536 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
1537 CCConst(oz_), CCConst(epsField_), div_, e_, G);
1538 else
1539 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
1540 CCConst(oz_), div_, e_, G);
1541 }
1542 // Porous continuity source: fold d(eps)/dt into the divergence so the Poisson solves for
1543 // div(eps u) = -d(eps)/dt (not 0). d(eps)/dt = (eps^{n+1}-eps^n)/dt from the deposited void
1544 // fraction; stored in depsdt_ (epsPrev_ is overwritten at step end, so the residual reuses
1545 // this).
1546 if (porous_) {
1547 CCExec space;
1548 C3 e = e_; // local copy — a KOKKOS_LAMBDA capturing e_ would read this-> on the device
1549 CCField d = div_, dd = depsdt_, ep = epsField_, epp = epsPrev_;
1550 const double idt = 1.0 / dt_;
1551 const bool useDt = porousDepsDt_;
1552 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
1553 Kokkos::parallel_for(
1554 "peclet::flow::deps_dt", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1555 KOKKOS_LAMBDA(int x, int y, int z) {
1556 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
1557 dd(i) = (ep(i) - epp(i)) * idt;
1558 if (useDt)
1559 d(i) += dd(i); // off -> solve div(eps u)=0 (drop the noisy time-derivative source)
1560 });
1561 }
1562 // bridge -div(u*) (g=2 block) -> the MG rhs (g=1 block); keep div(u*) in div_ for the pressure
1563 // update
1564 copyInner(rhs1_, e1_, 1, CCConst(div_), e_, G);
1565 {
1566 CCExec space;
1567 CCField r = rhs1_;
1568 Kokkos::parallel_for(
1569 "negdiv", Kokkos::RangePolicy<CCExec>(space, 0, n1_),
1570 KOKKOS_LAMBDA(std::size_t i) { r(i) = -r(i); });
1571 }
1572 // Variable density: rebuild the Poisson operator with the face coefficients
1573 // c_f = open_f * rho0/rho_f (rho0 = the scalar rho_, so uniform rho == rho_ reduces exactly to
1574 // the openness operator). The coefficient fields ride the openness rails: bridge rho to the g=1
1575 // block INCLUDING its ghost ring, form the coefficients on the inner cells, and hand them to
1576 // setOpenness, whose per-level ghost fill + boundary re-imposition + coarsening (rediscretized
1577 // averaging) treat them exactly like openness. Rebuilt every step (rho may be closure/transport
1578 // driven); Chebyshev bounds are invalidated (stale bounds under changing coefficients diverge
1579 // silently — PCG is the recommended/default driver here).
1580 if (varRho_) {
1581 fillPropGhosts(rhoField_);
1582 copyBlockShifted(rho1_, e1_, CCConst(rhoField_), e_, G - 1);
1583 buildRhoCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), CCConst(rho1_),
1584 rho_, e1_, 1);
1585 mg_.setBoundaryConditions(bc_);
1586 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), 1.0, 1.0, 1.0);
1587 chebBoundsSet_ = false; // spectrum changed with the coefficients (re-estimated by the solve)
1588 }
1589 // Porous continuity: the Poisson operator is eps-weighted (c_f = open_f * eps_f), same rails as
1590 // the density coefficient above. Rebuilt every step (eps moves with the particles). With
1591 // implicit CFD-DEM drag the coefficient AND the correction carry the drag-relaxation w_f =
1592 // idt/(idt+beta_f) (idt = rho/dt) so the pressure correction is consistent with the drag-loaded
1593 // momentum diagonal A_P = idt+beta (SIMPLE/PISO-with-implicit-drag; stiff drag -> w_f->0 -> the
1594 // drag holds the velocity, stable). beta==0 reduces exactly to the plain eps-weighted operator.
1595 if (porous_) {
1596 fillPropGhosts(epsField_);
1597 copyBlockShifted(eps1_, e1_, CCConst(epsField_), e_, G - 1);
1598 if (hasDrag_) {
1599 fillPropGhosts(dragBeta_);
1600 copyBlockShifted(beta1_, e1_, CCConst(dragBeta_), e_, G - 1);
1601 buildPorousCoeffDrag(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1602 CCConst(eps1_), CCConst(beta1_), rho_ / dt_, e1_, 1);
1603 } else {
1604 buildPorousCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1605 CCConst(eps1_), e1_, 1);
1606 }
1607 mg_.setBoundaryConditions(bc_);
1608 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), 1.0, 1.0, 1.0);
1609 chebBoundsSet_ = false;
1610 }
1611 // geometric multigrid solve of the cut-cell pressure Poisson A phi = -div(u*) (CUDA
1612 // mac_multigrid): MG-PCG by default, or the communication-light Chebyshev driver (bounds
1613 // estimated once, then reused). Warm start (CUDA pwarm_): keep the previous step's phi1_ as the
1614 // initial guess instead of zeroing.
1615 if (!pwarm_)
1616 Kokkos::deep_copy(phi1_, 0.0);
1617 if (useChebyshev_) {
1618 if (!chebBoundsSet_) {
1619 mg_.estimateEigenvalues(CCConst(rhs1_), chebA_, chebB_, 15, 2, 2, 12);
1620 chebBoundsSet_ = true;
1621 }
1622 lastPressureIters_ =
1623 mg_.solveChebyshev(rhs1_, phi1_, chebMaxit_, chebRtol_, 2, 2, 12, chebA_, chebB_);
1624 } else {
1625 lastPressureIters_ =
1626 mg_.solvePCG(rhs1_, phi1_, r_, pp_, z_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12);
1627 }
1628 copyInner(phi_, e_, G, CCConst(phi1_), e1_, 1); // bridge phi back g=1 -> g=2
1629 fillGhosts(phi_);
1630 if (hasOutflow_) { // hold phi=0 at the outflow ghost so grad(phi) drives the outflow face
1631 // (Dirichlet p=0)
1632 B3 e{e_.x, e_.y, e_.z};
1633 for (int a = 0; a < 3; ++a)
1634 for (int s = 0; s < 2; ++s)
1635 if (bc_[2 * a + s] == 3)
1636 bcZeroPressureGhost(phi_, e, G, a, s);
1637 }
1638 if constexpr (Grid::collocated) {
1639 // phi: zero-gradient (Neumann) at non-periodic walls so the cell-centered central-difference
1640 // correction carries no spurious normal acceleration through the wall (the periodic fill
1641 // wrapped the opposite boundary's phi). Outflow (Dirichlet p=0) is handled by hasOutflow_
1642 // above (phase 5b).
1643 if (hasBc_) {
1644 B3 e{e_.x, e_.y, e_.z};
1645 for (int a = 0; a < 3; ++a)
1646 for (int s = 0; s < 2; ++s) {
1647 const int t = bc_[2 * a + s];
1648 if (t != 0 && t != 3)
1649 bcNeumannGhost(phi_, e, G, a, s);
1650 }
1651 }
1652 // Correct the face field (-> discretely divergence-free; transient this step) and the cell
1653 // field (central-difference cell gradient).
1654 projectCorrect(uf_, vf_, wf_, CCConst(phi_), e_, G);
1655 fillGhosts(uf_);
1656 fillGhosts(vf_);
1657 fillGhosts(wf_); // complete the divergence-free face field (boundary faces)
1658 if (hasOutflow_) { // correct the high-side outflow face on the face field so mass leaves
1659 // (phi=0 there)
1660 B3 e{e_.x, e_.y, e_.z};
1661 CCField fa[3] = {uf_, vf_, wf_};
1662 for (int a = 0; a < 3; ++a)
1663 if (bc_[2 * a + 1] == 3)
1664 bcCorrectOutflow(fa[a], phi_, e, G, a);
1665 }
1666 if (faceInterp_ >= 2 &&
1667 faceInterp_ <= 5) { // modes 2-5: cell correction = the TRANSPOSE of the
1668 // wall-aware map, keeping (T, Tᵀ) an adjoint pair (transposeGradWallAware)
1669 CCField xcs[3] = {xcx_, xcy_, xcz_};
1670 CCField oax[3] = {ox_, oy_, oz_};
1671 for (int cc = 0; cc < 3; ++cc) {
1672 transposeGradWallAware(tgp_, CCConst(phi_), CCConst(sdf_), CCConst(oax[cc]),
1673 CCConst(xcs[cc]), faceInterp_ >= 3, cc, e_, G);
1674 subtractField(C[cc].u, CCConst(tgp_), e_, G);
1675 }
1676 } else if (faceInterp_ == 6 ||
1677 faceInterp_ == 7) { // embed: openness-WEIGHTED cell correction
1678 // (full open-face pressure force at cut cells) — Basilisk centered_grad
1679 projectCorrectCenterOpen(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
1680 CCConst(oz_), e_, G);
1681 } else {
1682 projectCorrectCenter(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
1683 CCConst(oz_), e_, G);
1684 }
1685 } else {
1686 if (porous_ &&
1687 hasDrag_) // drag-relaxed gradient w_f=idt/(idt+beta_f), matching buildPorousCoeffDrag
1688 projectCorrectPorousDrag(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(dragBeta_),
1689 rho_ / dt_, e_, G);
1690 else if (varRho_) // per-face 1/rho on the gradient, matching the operator coefficient
1691 projectCorrectVar(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(rhoField_), rho_, e_, G);
1692 else
1693 projectCorrect(C[0].u, C[1].u, C[2].u, CCConst(phi_), e_, G);
1694 if (hasOutflow_) { // correct the high-side outflow normal face that projectCorrect misses
1695 // (mass leaves)
1696 B3 e{e_.x, e_.y, e_.z};
1697 for (int a = 0; a < 3; ++a)
1698 if (bc_[2 * a + 1] == 3)
1699 bcCorrectOutflow(C[a].u, phi_, e, G, a);
1700 }
1701 }
1702 // the grad(phi) correction also touches solid faces; re-impose no-slip there so the decoupled
1703 // solid velocity cannot accumulate (matches the CUDA apply_mask/mask_k after correct_k ->
1704 // stability).
1705 for (int c = 0; c < 3; ++c)
1706 maskVelocity(c);
1707 // Rotational incremental pressure (Timmermans), matching CUDA press_update_k: P += (rho/dt)*phi
1708 // - mu*div(u*). Classical non-incremental Chorin (!incremental_) skips the accumulation;
1709 // getPressure() derives p from phi.
1710 if (incremental_) {
1711 CCExec space;
1712 CCField P = P_, ph = phi_, d = div_;
1713 // Pressure under-relaxation (MFIX §10.1): accumulate only omega_p of the increment into the
1714 // physical pressure P (the velocity correction still uses the full phi to satisfy
1715 // continuity), so the next step's incremental predictor -grad(P^n) can't overshoot for a
1716 // stiff drag diagonal. omega_p=1 (default) is the current behaviour; <1 only stabilizes the
1717 // porous+drag path.
1718 const double ct = pressUnderRelax_ * rho_ / dt_, mu = mu_;
1719 if (varProps_) {
1720 // Variable viscosity: the pointwise Timmermans term -mu(i)*div(u*) is inconsistent for
1721 // heterogeneous mu (see setVariableRotational). Default = constant coefficient chi*mu_min
1722 // (stable by domination, exact fallback to the uniform-mu scheme); "full" = pointwise
1723 // (mild contrast only); "off" = plain incremental.
1724 if (varRotMode_ == 1) {
1725 CCConst mf = CCConst(muField_);
1726 const double chi = varRotChi_;
1727 Kokkos::parallel_for(
1728 "press_var_full", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1729 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - chi * mf(i) * d(i); });
1730 } else {
1731 const double muRot = (varRotMode_ == 2) ? 0.0 : varRotChi_ * minMuInner();
1732 Kokkos::parallel_for(
1733 "press_var_min", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1734 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - muRot * d(i); });
1735 }
1736 } else {
1737 Kokkos::parallel_for(
1738 "press", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1739 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - mu * d(i); });
1740 }
1741 }
1742 // Snapshot eps^{n+1} -> epsPrev_ for the next step's d(eps)/dt (this projection consumed it).
1743 if (porous_)
1744 Kokkos::deep_copy(epsPrev_, epsField_);
1745 }
1746 void maskVelocity(int c) {
1747 CCExec space;
1748 CCField u = C[c].u, m = C[c].mask;
1749 Kokkos::parallel_for(
1750 "vmask", Kokkos::RangePolicy<CCExec>(space, 0, n_), KOKKOS_LAMBDA(std::size_t i) {
1751 if (m(i) > 0.5)
1752 u(i) = 0.0;
1753 });
1754 }
1755 // Minimum viscosity over the (global, under MPI) inner cells — the provably-stable rotational
1756 // coefficient for variable viscosity (chi*mu_min <= mu(x) everywhere).
1757 double minMuInner() {
1758 CCExec space;
1759 C3 e = e_;
1760 CCConst f = CCConst(muField_);
1761 double m = 1e300;
1762 Kokkos::parallel_reduce(
1763 "minmu",
1764 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
1765 {e.x - G, e.y - G, e.z - G}),
1766 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1767 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1768 if (f(i) < acc)
1769 acc = f(i);
1770 },
1771 Kokkos::Min<double>(m));
1772#ifdef PECLET_FLOW_MPI
1773 if (distributed_) {
1774 double g = m;
1775 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MIN, comm_);
1776 m = g;
1777 }
1778#endif
1779 return m;
1780 }
1782 CCExec space;
1783 C3 e = e_;
1784 double m = 0;
1785 Kokkos::parallel_reduce(
1786 "maxabs",
1787 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
1788 {e.x - G, e.y - G, e.z - G}),
1789 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1790 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1791 const double a = Kokkos::fabs(f(i));
1792 if (a > acc)
1793 acc = a;
1794 },
1795 Kokkos::Max<double>(m));
1796 return m;
1797 }
1798 std::vector<double> gatherInner(CCField fld) {
1799 auto h = Kokkos::create_mirror_view(fld);
1800 Kokkos::deep_copy(h, fld);
1801 std::vector<double> out((std::size_t)nx_ * ny_ * nz_);
1802 for (int z = 0; z < nz_; ++z)
1803 for (int y = 0; y < ny_; ++y)
1804 for (int x = 0; x < nx_; ++x)
1805 out[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_] =
1806 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y);
1807 return out;
1808 }
1809 // Inverse of gatherInner: scatter an x-fastest (nx,ny,nz) inner-region host buffer into the inner
1810 // cells of a ghosted G=2 field (ghost cells untouched — refill via exchangeField/fillGhosts).
1811 void scatterInner(CCField fld, const std::vector<double>& in) {
1812 if (in.size() != (std::size_t)nx_ * ny_ * nz_)
1813 throw std::runtime_error("flow::setField: array size does not match the inner grid");
1814 auto h = Kokkos::create_mirror_view(fld);
1815 Kokkos::deep_copy(h, fld); // preserve existing ghosts
1816 for (int z = 0; z < nz_; ++z)
1817 for (int y = 0; y < ny_; ++y)
1818 for (int x = 0; x < nx_; ++x)
1819 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
1820 in[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_];
1821 Kokkos::deep_copy(fld, h);
1822 }
1823
1824 // --- Named field registry (multiphysics field container) ------------------------------------
1825 // Register a new zero-initialised cell-centred field on the G=2 velocity block and return its
1826 // buffer. Idempotent: re-adding an existing name returns the existing buffer unchanged.
1827 CCField addField(const std::string& name) {
1828 if (fields_.has(name))
1829 return fields_.at(name).data;
1830 return fields_.add(name, n_, G, peclet::core::Centering::Cell).data;
1831 }
1832 bool hasField(const std::string& name) const { return fields_.has(name); }
1833 CCField fieldView(const std::string& name) { return fields_.at(name).data; }
1834 std::vector<std::string> fieldNames() const { return fields_.names(); }
1835 // Ghost-exchange a registered field (cross-rank + periodic under MPI; periodic-only single-rank).
1836 void exchangeField(const std::string& name) { fillGhosts(fields_.at(name).data); }
1837 // Add-reduce ("reverse") halo: fold ghost-layer deposits back onto their owner cell (both
1838 // cross-rank AND periodic self-wrap). This is the coupling primitive for particle->grid
1839 // deposition (e.g. void fraction / drag reaction) where a particle near a block boundary scatters
1840 // into ghost cells owned by a neighbour; after this the inner block holds the complete sum.
1841 // Single-rank non-periodic: a no-op.
1842 void exchangeFieldAdd(const std::string& name) {
1843#ifdef PECLET_FLOW_MPI
1844 if (distributed_ && velHalo_) {
1845 CCField f = fields_.at(name).data;
1846 auto h = Kokkos::create_mirror_view(f);
1847 Kokkos::deep_copy(h, f);
1848 peclet::core::halo::GridFieldView<double> view{h.data()};
1849 velHalo_->reverseAdd(view);
1850 Kokkos::deep_copy(f, h);
1851 }
1852#else
1853 (void)name;
1854#endif
1855 }
1856 // Host round-trip: read a registered field's inner region as an x-fastest (nx,ny,nz) buffer, or
1857 // write one (ghosts left stale until the next exchangeField).
1858 std::vector<double> getField(const std::string& name) {
1859 return gatherInner(fields_.at(name).data);
1860 }
1861 void setField(const std::string& name, const std::vector<double>& v) {
1862 scatterInner(fields_.at(name).data, v);
1863 }
1864 // Padded-block extents + ghost width, so a zero-copy field buffer (size ex*ey*ez, x-fastest) can
1865 // be reshaped in Python.
1866 std::array<int, 3> blockShape() const { return {e_.x, e_.y, e_.z}; }
1867 int ghostWidth() const { return G; }
1868 // Global grid dims (== local dims single-rank). For the CFD-DEM co-decomposition weight field.
1869 std::array<int, 3> globalResolution() const {
1870#ifdef PECLET_FLOW_MPI
1871 if (distributed_)
1872 return {gnx_, gny_, gnz_};
1873#endif
1874 return {nx_, ny_, nz_};
1875 }
1876 // This rank's inner-block origin in GLOBAL cells ({0,0,0} single-rank). The deposit-origin shift
1877 // so particles in global coords land in the local block (gm origin = blockOrigin * h).
1878 std::array<int, 3> blockOrigin() const { return {og_.x, og_.y, og_.z}; }
1879
1880 // --- Scalar transport (advection-diffusion) -------------------------------------------------
1881 // Register a transported scalar `name` with constant diffusivity D (grid units). scheme: 0 FOU,
1882 // 1 Koren TVD (default), 2 SOU. iters = RB-GS sweeps for the implicit diffusion solve. Its field
1883 // is registered in the directory (get_field/set_field/field_view). Openness (set_solid /
1884 // set_pressure_geometry) must be established for transport to occur.
1885 void addScalar(const std::string& name, double D, int scheme, int iters) {
1886 ScalarField sc;
1887 sc.name = name;
1888 sc.c = addField(name); // registered, zero-initialised, on the G=2 block
1889 sc.cOld = CCField(name + "_old", n_);
1890 sc.b = CCField(name + "_b", n_);
1891 sc.AC = CCField(name + "_AC", n_);
1892 sc.AW = CCField(name + "_AW", n_);
1893 sc.AE = CCField(name + "_AE", n_);
1894 sc.AS = CCField(name + "_AS", n_);
1895 sc.AN = CCField(name + "_AN", n_);
1896 sc.AB = CCField(name + "_AB", n_);
1897 sc.AT = CCField(name + "_AT", n_);
1898 sc.D = D;
1899 sc.scheme = scheme;
1900 sc.iters = iters < 1 ? 1 : iters;
1901 scalars_.push_back(sc);
1902 }
1903 bool hasScalar(const std::string& name) const {
1904 for (const auto& sc : scalars_)
1905 if (sc.name == name)
1906 return true;
1907 return false;
1908 }
1909 // Per-face scalar BC: face 0..5 = -x,+x,-y,+y,-z,+z; type 0 periodic, 1 Neumann zero-flux
1910 // (adiabatic), 2 Dirichlet value. Single-rank / non-decomposed domains (distributed BC deferred).
1911 void setScalarBc(const std::string& name, int face, int type, double value) {
1912 for (auto& sc : scalars_)
1913 if (sc.name == name) {
1914 sc.bc[face] = type;
1915 sc.bcVal[face] = value;
1916 return;
1917 }
1918 throw std::runtime_error("set_scalar_bc: no scalar named '" + name + "'");
1919 }
1920 // Advance all registered scalars one dt with the current divergence-free velocity (also called at
1921 // the end of step()). Exposed so a test can prescribe a velocity and transport a scalar in
1922 // isolation.
1924 if (scalars_.empty())
1925 return;
1926 const double idt = 1.0 / dt_;
1927 CCField Uf, Vf, Wf;
1928 if constexpr (Grid::collocated) {
1929 Uf = uf_;
1930 Vf = vf_;
1931 Wf = wf_;
1932 } else {
1933 Uf = C[0].u;
1934 Vf = C[1].u;
1935 Wf = C[2].u;
1936 }
1937 fillGhosts(Uf);
1938 fillGhosts(Vf);
1939 fillGhosts(Wf); // face velocities need the ±2 advection reach
1940 for (auto& sc : scalars_) {
1941 scalarBuildDiffusionOpen(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, CCConst(ox_),
1942 CCConst(oy_), CCConst(oz_), sc.D, idt, e_, G);
1943 applyScalarBcStencil(sc); // re-open Dirichlet domain faces (set_domain_bc closes openness)
1944 Kokkos::deep_copy(sc.cOld, sc.c);
1945 scalarFillGhosts(sc);
1946 scalarBuildRhs(sc.b, CCConst(sc.cOld), CCConst(Uf), CCConst(Vf), CCConst(Wf), CCConst(ox_),
1947 CCConst(oy_), CCConst(oz_), idt, sc.scheme, e_, G);
1948 // implicit diffusion: red-black Gauss-Seidel with a ghost fill before each color sweep.
1949 for (int it = 0; it < sc.iters; ++it) {
1950 scalarFillGhosts(sc);
1951 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
1952 og_, G, 0);
1953 scalarFillGhosts(sc);
1954 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
1955 og_, G, 1);
1956 }
1957 scalarFillGhosts(sc);
1958 }
1959 }
1960
1961 // --- Property closures + per-cell body force ------------------------------------------------
1962 // Register a property/force closure. target: a registered field name — a material property
1963 // ("mu"/"rho"/…) or a body-force component ("force_x"/"force_y"/"force_z"). kind: LinearMix /
1964 // BoussinesqForce / ArrheniusMu. in0/in1: input field names (in1 "" if unused). params: up to 4
1965 // doubles (meaning per kind — property_closures.hpp). Applied at the top of step() in
1966 // registration order. Targeting a force component turns on the per-cell body-force RHS path.
1967 void setPropertyModel(const std::string& target, ClosureKind kind, const std::string& in0,
1968 const std::string& in1, const std::vector<double>& params) {
1969 Closure cl;
1970 cl.kind = kind;
1971 cl.out = ensureTarget(target);
1972 cl.in0 = CCConst(fields_.at(in0).data);
1973 if (!in1.empty())
1974 cl.in1 = CCConst(fields_.at(in1).data);
1975 for (int k = 0; k < 4 && k < (int)params.size(); ++k)
1976 cl.p[k] = params[k];
1977 closures_.push_back(cl);
1978 if (target == "mu") // a closure driving mu turns on variable viscosity
1979 setPropertyMode(true, harmonicMu_);
1980 if (target == "rho") // a closure driving rho turns on the variable-density path
1981 setDensityMode(true);
1982 }
1983 // Enable/disable variable density: binds the "rho" field (creating it seeded with the scalar rho_
1984 // if absent) into the momentum time term, the advection weight, and the pressure projection
1985 // (face coefficient open/rho_f + 1/rho_f correction). rho_ (set_rho) becomes the REFERENCE
1986 // density rho0 of the projection scaling — a uniform rho field == rho_ reduces exactly to the
1987 // constant solver. Escape hatch: set_field("rho", arr) + set_density_mode(True); or a closure
1988 // targeting "rho" (e.g. rho = LinearMix of a transported phase fraction) enables it
1989 // automatically. Staggered grid only (v1); the velocity multigrid (scalar-coefficient) is
1990 // disabled.
1991 void setDensityMode(bool variable) {
1992 if constexpr (Grid::collocated) {
1993 if (variable)
1994 throw std::runtime_error("set_density_mode: variable density is staggered-only (v1)");
1995 }
1996 varRho_ = variable;
1997 if (variable) {
1998 if (fields_.has("rho"))
1999 rhoField_ = fields_.at("rho").data;
2000 else {
2001 rhoField_ = addField("rho");
2002 Kokkos::deep_copy(rhoField_, rho_);
2003 }
2004 if (rho1_.extent(0) == 0) { // g=1 MG-block scratch for the projection coefficients
2005 rho1_ = CCField("rho1", n1_);
2006 cx1_ = CCField("cx1", n1_);
2007 cy1_ = CCField("cy1", n1_);
2008 cz1_ = CCField("cz1", n1_);
2009 }
2010 ensureCellForceAll(); // buildRhsVar reads the per-cell force (zero until a closure sets it)
2011 useVelocityMg_ = false; // scalar-coefficient velocity MG (variable-coeff deferred)
2012 // Pressure driver: CHEBYSHEV by default under variable density. MG-PCG stalls on the
2013 // rho-scaled coefficient operator (the hierarchy's transfer pair was built/validated for
2014 // geometric openness; with scaled coefficients the V-cycle preconditioner loses the
2015 // SPD-preserving structure CG needs — observed: PCG 5000 iters stuck where Chebyshev
2016 // converges in ~20). Chebyshev only needs real spectrum bounds, which are re-estimated on
2017 // every coefficient rebuild (chebBoundsSet_ invalidation in project()). An explicit
2018 // set_pressure_pcg/set_pressure_chebyshev AFTER set_density_mode still wins (last set).
2019 useChebyshev_ = true;
2020 chebBoundsSet_ = false;
2021 }
2022 }
2023 // Enable/disable the volume-averaged (porous) continuity for unresolved CFD-DEM: the projection
2024 // enforces d(eps)/dt + div(eps u) = 0 instead of div(u)=0, so the velocity is NOT solenoidal
2025 // where the void fraction changes. Binds the "eps" field (void fraction from the particle
2026 // deposition; created seeded to 1 if absent). Staggered-only. The coupling deposits eps each step
2027 // BEFORE step().
2028 void setPorousContinuity(bool on) {
2029 if constexpr (Grid::collocated) {
2030 if (on)
2031 throw std::runtime_error("set_porous_continuity: staggered-only (v1)");
2032 }
2033 porous_ = on;
2034 if (on) {
2035 if (fields_.has("eps"))
2036 epsField_ = fields_.at("eps").data;
2037 else {
2038 epsField_ = addField("eps");
2039 Kokkos::deep_copy(epsField_, 1.0); // no particles -> eps=1 -> reduces to div(u)=0
2040 }
2041 if (epsPrev_.extent(0) == 0) {
2042 epsPrev_ = CCField("epsPrev", n_);
2043 depsdt_ = CCField("depsdt", n_);
2044 }
2045 Kokkos::deep_copy(epsPrev_, epsField_); // d(eps)/dt=0 on the first step
2046 if (eps1_.extent(0) == 0)
2047 eps1_ = CCField("eps1", n1_);
2048 if (beta1_.extent(0) == 0)
2049 beta1_ = CCField("beta1", n1_);
2050 if (rho1_.extent(0) == 0) { // share the g=1 coefficient scratch with the varRho path
2051 rho1_ = CCField("rho1", n1_);
2052 cx1_ = CCField("cx1", n1_);
2053 cy1_ = CCField("cy1", n1_);
2054 cz1_ = CCField("cz1", n1_);
2055 }
2056 // CHEBYSHEV by default (as for variable density): MG-PCG stalls on the eps-scaled coefficient
2057 // operator — its V-cycle preconditioner loses the SPD structure CG needs (observed: PCG 2000
2058 // iters stuck where Chebyshev converges in ~40). Bounds re-estimated on every coefficient
2059 // rebuild (chebBoundsSet_ invalidation in project()). An explicit driver set afterwards wins.
2060 useChebyshev_ = true;
2061 chebBoundsSet_ = false;
2062 configurePorousDragSolver(); // if drag already on, switch to GraphAMG+PCG (Chebyshev
2063 // diverges)
2064 }
2065 }
2066 // Reseed eps^n = eps^{n+1} so d(eps)/dt = 0 this step. Call after the FIRST void-fraction
2067 // deposition (the "eps" field starts empty, so without this step 0 sees a spurious d(eps)/dt from
2068 // 0 -> eps).
2070 if (porous_)
2071 Kokkos::deep_copy(epsPrev_, epsField_);
2072 }
2073 // Include (default) or drop the d(eps)/dt source in the porous projection RHS. Dropping it
2074 // enforces div(eps u)=0 — useful when eps is a bare per-cell particle deposit whose
2075 // time-derivative is too jagged and drives the eps-weighted pressure solve unstable.
2076 void setPorousDepsDt(bool on) { porousDepsDt_ = on; }
2077 // Pressure under-relaxation factor omega_p in (0,1] (MFIX-style); 1.0 = off (default).
2078 void setPressureUnderRelax(double w) { pressUnderRelax_ = w; }
2079 // Enable/disable variable-coefficient momentum (variable viscosity). variable=true binds the "mu"
2080 // field (creating it, seeded with the current scalar mu, if absent) and forces the stencil solve
2081 // path. harmonic selects the harmonic face mean (continuous shear stress across a viscosity jump)
2082 // vs arithmetic. Escape hatch: set_field("mu", arr) then set_property_mode(True).
2083 void setPropertyMode(bool variable, bool harmonic) {
2084 varProps_ = variable;
2085 harmonicMu_ = harmonic;
2086 if (variable) {
2087 if (fields_.has("mu"))
2088 muField_ = fields_.at("mu").data;
2089 else {
2090 muField_ = addField("mu");
2091 Kokkos::deep_copy(muField_,
2092 mu_); // default to the scalar mu until a closure/set_field sets it
2093 }
2094 useVelocityMg_ =
2095 false; // the velocity multigrid takes a scalar mu (variable-coeff vmg deferred)
2096 }
2097 }
2098 // Rotational-pressure treatment under variable viscosity. The Timmermans rotational term
2099 // P += (rho/dt)phi - mu*div(u*) is only valid for HOMOGENEOUS viscosity (Deteix & Yakoubi, Appl.
2100 // Math. Lett. 2018 / arXiv:1902.05643): with spatially varying mu the pointwise term is no longer
2101 // the gradient part of the viscous stress, and the accumulated inconsistency destabilises the
2102 // incremental scheme at strong contrast (observed: 10x jump + harmonic faces -> divergence).
2103 // Modes (the incremental predictor -grad(P^n) and P accumulation are kept in ALL of them — that
2104 // is what enables large-dt / steady-Stokes stepping):
2105 // 0 "min" (default): rotational coefficient chi*mu_min — a CONSTANT dominated by the true
2106 // local
2107 // dissipation everywhere (mu_min <= mu(x)), so the constant-viscosity stability theory
2108 // carries over; reduces EXACTLY to the validated scheme when mu is uniform.
2109 // 1 "full": chi*mu(i) pointwise — better pressure consistency at MILD contrast; not stable at
2110 // strong contrast (user's responsibility).
2111 // 2 "off" : plain incremental (no rotational term) — unconditionally stable, keeps the
2112 // artificial
2113 // pressure Neumann layer of the non-rotational scheme.
2114 // The fully consistent variable-viscosity correction (shear-rate projection: an extra Poisson
2115 // solve for psi with rhs div(div(2 nu D(u)))) is deferred.
2116 void setVariableRotational(int mode, double chi) {
2117 varRotMode_ = mode < 0 ? 0 : (mode > 2 ? 2 : mode);
2118 varRotChi_ = chi < 0.0 ? 0.0 : chi;
2119 }
2120 // Tabulated property: out = piecewise-linear interp of (xs, ys) at the input field (xs
2121 // ascending).
2122 void setPropertyTable(const std::string& target, const std::string& in0,
2123 const std::vector<double>& xs, const std::vector<double>& ys) {
2124 Closure cl;
2126 cl.out = ensureTarget(target);
2127 cl.in0 = CCConst(fields_.at(in0).data);
2128 cl.nTab = (int)std::min(xs.size(), ys.size());
2129 cl.tabX = CCField(target + "_tabx", cl.nTab);
2130 cl.tabY = CCField(target + "_taby", cl.nTab);
2131 auto hx = Kokkos::create_mirror_view(cl.tabX);
2132 auto hy = Kokkos::create_mirror_view(cl.tabY);
2133 for (int k = 0; k < cl.nTab; ++k) {
2134 hx(k) = xs[k];
2135 hy(k) = ys[k];
2136 }
2137 Kokkos::deep_copy(cl.tabX, hx);
2138 Kokkos::deep_copy(cl.tabY, hy);
2139 closures_.push_back(cl);
2140 }
2141 // Apply all closures (also called at the top of step()). Exposed for testing.
2143 for (auto& cl : closures_)
2144 applyClosure(cl, e_, G);
2145 }
2146 // Allocate + register the per-cell body-force fields ("force_x/y/z") and route them into the
2147 // momentum RHS, for an EXTERNAL writer (CFD-DEM feedback) to fill directly via field_view — no
2148 // closure needed. buildRhsForced then adds them each step (they persist; the writer overwrites).
2149 void enableCellForce() { ensureCellForceAll(); }
2150 // Implicit (semi-implicit) linear drag: a per-cell coefficient field "drag_beta" is added to the
2151 // momentum diagonal each step, so a drag source −β(u − u_p) is treated implicitly (the fluid
2152 // solve becomes (ρ/dt + β)u = … + β u_p). The drag TARGET β·u_p goes into the force_x/y/z fields
2153 // (the RHS). Unconditionally stable for any β (unlike an explicit −β u force, which diverges for
2154 // the stiff β of a dense particle bed). The external writer (CFD-DEM) fills "drag_beta" +
2155 // "force_*" via field_view; enableDrag() allocates them and turns the diagonal path on.
2156 void enableDrag() {
2157 if (!fields_.has("drag_beta"))
2158 dragBeta_ = addField("drag_beta");
2159 else
2160 dragBeta_ = fields_.at("drag_beta").data;
2161 ensureCellForceAll(); // force_* carries beta*u_p (the implicit-drag RHS target)
2162 hasDrag_ = true;
2164 }
2165 // Porous + implicit drag: the drag-relaxation w_f=idt/(idt+beta) makes the pressure coefficient
2166 // high-ratio (~1 in the freeboard, ->0 in the dense bed). Chebyshev diverges on it; the algebraic
2167 // GraphAMG coarse solve + PCG is robust. Applied whenever BOTH porous_ and hasDrag_ are on
2168 // (either set second). An explicit set_pressure_* afterwards still wins.
2170 if (!(porous_ && hasDrag_))
2171 return;
2172 pressGraphAmg_ = true; // GraphAMG bottom (domain-BC operators: buildAmg skips
2173 // the wrap across non-periodic faces and pcgAmg keeps the
2174 // mean only when the operator is singular)
2175 if (cutcellPressure_) // MG already built (set_solid ran) -> apply now
2176 mg_.setGraphAmgBottom(pressGraphAmg_);
2177 useChebyshev_ = false; // PCG, not Chebyshev (diverges on the high w_f ratio)
2178 chebBoundsSet_ = false;
2179 }
2180 // Add the drag coefficient beta(i) to the (float) momentum diagonal of component c. Called after
2181 // each stencil (re)build when hasDrag_. All-fluid (rscale==1) is exact; the drag×cut-cell-IBM
2182 // interaction (rscale≠1) is untested (documented).
2183 void addDragDiagonal(int c) {
2184 CCExec space;
2185 C3 e = e_;
2186 FV AC = C[c].AC;
2187 CCConst beta = CCConst(dragBeta_);
2188 const long sc = strideOf(c);
2189 // Porous continuity: the projection's operator/correction carry the FACE drag relaxation
2190 // w_f = idt/(idt + beta_f), beta_f = 1/2(beta(i)+beta(i-sc)) (buildPorousCoeffDrag /
2191 // projectCorrectPorousDrag). The staggered momentum diagonal of u_c(i) — the face between cells
2192 // i-sc and i — must carry the SAME beta_f: then a pressure perturbation deltaP produces
2193 // du* = -grad(deltaP)/(idt+beta_f) and the projection returns phi = -deltaP/idt exactly (same
2194 // operator), so the incremental predictor cancels pressure errors in one step. With the cell
2195 // value beta(i) the loop has gain (idt+beta_f)/(idt+beta_cell) at a beta jump (bed top: ~3) and
2196 // the accumulated pressure diverges exponentially. Non-porous (incompressible drag, w==1 path)
2197 // keeps the validated cell-beta form.
2198 const bool faceAvg = porous_;
2199 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
2200 Kokkos::parallel_for(
2201 "peclet::flow::add_drag_diag", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
2202 KOKKOS_LAMBDA(int x, int y, int z) {
2203 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2204 const double bd =
2205 faceAvg ? 0.5 * ((double)beta(i) + (double)beta(i - sc)) : (double)beta(i);
2206 AC(i) = (float)((double)AC(i) + bd);
2207 });
2208 }
2209
2210 private:
2211 // Resolve a closure target to a registered buffer. A force component allocates ALL three
2212 // cellForce_ slots (buildRhsForced reads every component) and enables the body-force RHS path.
2213 CCField ensureTarget(const std::string& name) {
2214 if (name == "force_x" || name == "force_y" || name == "force_z")
2215 ensureCellForceAll();
2216 return addField(name); // idempotent; returns the (now-existing) buffer
2217 }
2218 void ensureCellForceAll() {
2219 static const char* fn[3] = {"force_x", "force_y", "force_z"};
2220 for (int c = 0; c < 3; ++c)
2221 cellForce_[c] = addField(fn[c]); // zero-initialised, registered
2222 hasCellForce_ = true;
2223 }
2224 // Ghost fill for a scalar: periodic (single-rank) / MPI halo base, then override any domain
2225 // Dirichlet/Neumann faces.
2226 void scalarFillGhosts(ScalarField& sc) {
2227 fillGhosts(sc.c);
2228 applyScalarBc(sc);
2229 }
2230 // Overwrite the ghost band on each Dirichlet/Neumann domain face (both layers, for the ±2
2231 // advection reach). Single-rank only — distributed domain-BC scalars are a later phase.
2232 void applyScalarBc(ScalarField& sc) {
2233 bool any = false;
2234 for (int f = 0; f < 6; ++f)
2235 any = any || (sc.bc[f] != 0);
2236 if (!any || distributed_)
2237 return;
2238 for (int f = 0; f < 6; ++f)
2239 if (sc.bc[f] != 0)
2240 applyScalarBcFace(sc.c, f / 2, f % 2, sc.bc[f], sc.bcVal[f]);
2241 }
2242 // Re-open the diffusion face at a Dirichlet domain boundary: set_domain_bc closes the boundary
2243 // openness (ox_=0), which correctly makes Neumann/adiabatic walls zero-flux but would also cut a
2244 // Dirichlet wall's heat path. For each Dirichlet face, restore the face coefficient (band = -D,
2245 // A_C += D); the ghost carries 2*value - inner so the row is the standard Dirichlet operator.
2246 void applyScalarBcStencil(ScalarField& sc) {
2247 if (distributed_)
2248 return;
2249 for (int f = 0; f < 6; ++f) {
2250 if (sc.bc[f] != 2)
2251 continue; // only Dirichlet reopens; Neumann/periodic leave the (closed/interior) band
2252 const int a = f / 2, side = f % 2;
2253 CCField band = (a == 0) ? (side == 0 ? sc.AW : sc.AE)
2254 : (a == 1) ? (side == 0 ? sc.AS : sc.AN)
2255 : (side == 0 ? sc.AB : sc.AT);
2256 patchScalarDirichletFace(sc.AC, band, sc.D, a, side);
2257 }
2258 }
2259 // nvcc requires member functions that contain extended (device) lambdas to be PUBLIC — the
2260 // OpenMP/host build accepts them private, so the breakage only shows on the CUDA backend.
2261 public:
2262 void patchScalarDirichletFace(CCField AC, CCField band, double D, int a, int side) {
2263 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
2264 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
2265 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
2266 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
2267 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
2268 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
2269 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
2270 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
2271 const int aInner = (side == 0) ? G : (G + na - 1);
2272 CCExec space;
2273 Kokkos::parallel_for(
2274 "peclet::flow::scalar_bc_stencil",
2275 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {G, G}, {G + nt1, G + nt2}),
2276 KOKKOS_LAMBDA(int j1, int j2) {
2277 const long i = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
2278 // base build put band(i) = -D*open_face and A_C += D*open_face; force the face fully open
2279 // (band -> -D, A_C gains D*(1-open)) without double-counting when it was already open.
2280 AC(i) += D + band(i);
2281 band(i) = -D;
2282 });
2283 }
2284 void applyScalarBcFace(CCField c, int a, int side, int type, double val) {
2285 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
2286 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
2287 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
2288 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
2289 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
2290 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
2291 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
2292 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
2293 const int aInner = (side == 0) ? G : (G + na - 1); // inner boundary cell a-index
2294 const int dir = (side == 0) ? -1 : +1; // toward the ghost
2295 CCExec space;
2296 Kokkos::parallel_for(
2297 "peclet::flow::scalar_bc_face",
2298 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {G, G}, {G + nt1, G + nt2}),
2299 KOKKOS_LAMBDA(int j1, int j2) {
2300 const long base = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
2301 for (int L = 1; L <= 2; ++L) {
2302 const long gcell = base + (long)dir * L * sa;
2303 const long icell = base - (long)dir * (L - 1) * sa;
2304 c(gcell) = (type == 2) ? (2.0 * val - c(icell)) : c(icell);
2305 }
2306 });
2307 }
2308
2309 private:
2310 int nx_, ny_, nz_;
2311 C3 e_, e1_;
2312 std::size_t n_, n1_;
2313 double rho_ = 1.0, mu_ = 0.1, dt_ = 50.0;
2314 std::array<double, 3> f_{{0, 0, 0}};
2315 int velIters_ = 200, presIters_ = 20;
2316 int pcgMaxit_ = 500;
2317 double pcgRtol_ = 1e-10; // cut-cell pressure MG-PCG
2318 bool useChebyshev_ = false,
2319 chebBoundsSet_ = false; // Chebyshev pressure driver (set_pressure_chebyshev)
2320 int chebMaxit_ = 120;
2321 double chebRtol_ = 1e-9, chebA_ = 0.0, chebB_ = 0.0;
2322 int nLevels_ = 4; // multigrid depth (CUDA default; set_pressure_multigrid)
2323 bool pressGraphAmg_ = false; // agglomerated GraphAMG bottom solve (decomposition-agnostic)
2324 long lastPressureIters_ = 0;
2325 CutcellMG mg_;
2326 // --- multi-rank (MPI) state, gated (single-GPU module never links MPI -> byte-identical when
2327 // off) ---
2328 bool distributed_ = false;
2329 C3 og_{0, 0, 0}; // velocity-block inner origin (global red-black parity); {0,0,0} single-rank
2330#ifdef PECLET_FLOW_MPI
2331 std::shared_ptr<GridHaloTopology<3>> velHalo_; // g=2 velocity-block topology
2332 std::shared_ptr<GridHalo<double>> velDev_; // g=2 velocity-block ghost exchange
2333 std::shared_ptr<peclet::core::decomp::BlockDecomposer<3>>
2334 dec_; // current partition (redistribute)
2335 MPI_Comm comm_ = MPI_COMM_NULL;
2336 int gnx_ = 0, gny_ = 0, gnz_ = 0; // communicator + GLOBAL dims
2337#endif
2338 int bc_[6] = {0, 0, 0, 0, 0, 0};
2339 double bcVel_[6][3] = {};
2340 bool hasBc_ = false, hasOutflow_ = false; // domain BCs
2341 bool hasSolid_ =
2342 false; // an immersed solid is present (any inner SDF < 0) -- with domain BCs, the
2343 // momentum solve must use the cut-cell IBM stencil, not the all-fluid fold
2344 double backflowBeta_ =
2345 0.2; // outflow backflow-stabilization coefficient (0 = off; inert unless the
2346 // outflow reverses, so purely-outgoing outlets stay byte-identical)
2347 CCField bcProf_[6];
2348 int bcProfNc_[6] = {0, 0, 0, 0, 0, 0}; // per-position inlet profiles (face grid [Lb*Lc*3])
2349 CCField bcDcorr_[3], bcBrhs_[3]; // implicit-diffusion face fold (per component)
2350 bool advect_ = false, cutcellPressure_ = false, implicitFou_ = false;
2351 bool deferredCorr_ = true; // deferred-correction advection (off = pure implicit FOU, 1st order)
2352 int advScheme_ = 0; // high-order advection: 0 = SOU (default), 1 = Koren TVD
2353 bool incremental_ = true,
2354 pwarm_ = false; // incremental-rotational pressure (CUDA default on) + warm-start
2355 int faceInterp_ = 0; // collocated cell->face map: 0 = plain average, 1 = wall-aware (opt-in)
2356 double fvRelax_ = 1.0; // mode-4 FV defect-correction under-relaxation (setFvRelax)
2357 bool useVelocityMg_ = false;
2358 int vmgLevels_ = 4, vmgVcycles_ = 8; // IBM velocity multigrid (staircase)
2359 VelocityMG vmg_;
2360 CCField vmgTheta_, vmgClean_;
2361 int outerIters_ = 1;
2362 double outerTol_ = 0.0; // Picard outer iteration (CUDA set_outer_iterations)
2363 long lastOuterIters_ = 0;
2364 double lastOuterCorr_ = 0.0;
2365 CCField sdf_, ox_, oy_, oz_, phi_, div_, P_, ox1_, oy1_, oz1_, rhs1_, phi1_, r_, z_, pp_, Ap_;
2366 CCField uf_, vf_, wf_; // collocated: transient face (MAC) field (approx projection)
2367 CCField tgp_; // collocated: transpose-gradient scratch (setFaceInterp(2/3))
2368 CCField wdef_; // collocated: FV wall viscous-flux defect scratch (setFaceInterp(4))
2369 CCField fvM_, fvL_, cs_; // collocated: mode-4 defect scratch (M·u, L_FV·u) + cell fluid fraction
2370 CCField xcx_, xcy_, xcz_; // collocated: open-centroid wall distance per face (setFaceInterp(3))
2371 CCField old_[3], prev_[3]; // u^n time base + previous Picard iterate
2372 Comp C[3];
2373 peclet::core::FieldSet fields_; // named directory of all cell fields (velocity/p/sdf + user)
2374 std::vector<ScalarField> scalars_; // transported scalars (advection-diffusion)
2375 std::vector<Closure> closures_; // property/body-force closures (applied at top of step())
2376 CCField cellForce_[3]; // per-cell momentum body force (Boussinesq / CFD-DEM feedback)
2377 bool hasCellForce_ = false;
2378 bool varProps_ = false; // variable-coefficient momentum (variable viscosity)
2379 bool harmonicMu_ = false; // harmonic vs arithmetic face-viscosity mean
2380 CCField muField_; // per-cell dynamic viscosity (when varProps_)
2381 int varRotMode_ = 0; // rotational term under varProps: 0 chi*mu_min, 1 chi*mu(i), 2 off
2382 double varRotChi_ = 1.0; // rotational coefficient scale chi
2383 bool varRho_ = false; // variable density (momentum + projection); staggered only
2384 CCField rhoField_; // per-cell density (when varRho_); rho_ is the reference rho0
2385 CCField rho1_, cx1_, cy1_, cz1_; // g=1 MG-block density bridge + projection face coefficients
2386 bool porous_ = false; // volume-averaged continuity d(eps)/dt+div(eps u)=0 (CFD-DEM)
2387 double pressUnderRelax_ = 1.0; // omega_p for the incremental pressure accumulation (1.0 = off)
2388 bool porousDepsDt_ = true; // include the d(eps)/dt source in the projection RHS. Off ->
2389 // enforce div(eps u)=0 (drop the term, which is jagged/noisy
2390 // because eps is a bare per-cell particle deposit; the noisy
2391 // source can drive the eps-weighted pressure solve unstable).
2392 CCField epsField_, epsPrev_, eps1_, depsdt_; // eps^{n+1}, eps^n, g=1 bridge, stored d(eps)/dt
2393 CCField beta1_; // g=1 bridge of the drag coeff (semi-implicit-drag pressure)
2394 bool hasDrag_ = false; // implicit linear drag (CFD-DEM): beta on the momentum diagonal
2395 CCField dragBeta_; // per-cell drag coefficient (added to AC; target beta*u_p rides
2396 // the force_* cellForce fields)
2397};
2398
2399// The staggered MAC solver — THE flow solver, bit-identical to the pre-policy class. Bindings + the
2400// kokkos_mpi tests reference this name unchanged.
2402
2403} // namespace peclet::flow
2404
2405#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)
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
int solvePCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField Ap, int maxit, double rtol, int pre, int post, int bottom)
void setBoundaryConditions(const int bc[6])
void init(int nx, int ny, int nz, int nLevels)
void estimateEigenvalues(CCConst seed, double &lmin, double &lmax, int iters, int pre, int post, int bottom)
std::vector< double > getFaceVelocity(int c)
Definition flow_ibm.hpp:657
std::array< int, 3 > globalResolution() const
void fillVelGhosts(int comp, int fold)
void setIncrementalPressure(bool on)
Definition flow_ibm.hpp:212
bool implicitAdv() const
Definition flow_ibm.hpp:775
void buildRhsVar(int c)
void scatterInner(CCField fld, const std::vector< double > &in)
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Definition flow_ibm.hpp:435
void setPressurePcg(bool, int maxit, double rtol)
Definition flow_ibm.hpp:204
void setFvRelax(double w)
Definition flow_ibm.hpp:247
void fillGhosts(CCField f)
Definition flow_ibm.hpp:881
long lastOuterIterations() const
Definition flow_ibm.hpp:159
void setDomainBc(int face, int type, double vx, double vy, double vz)
Definition flow_ibm.hpp:390
void setFaceInterp(int mode)
Definition flow_ibm.hpp:244
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)
void applyVelocityBcComp(int comp, int fold, bool doOutflow)
void setOuterTolerance(double tol)
Definition flow_ibm.hpp:158
CCField addField(const std::string &name)
double maxOpenDivergence()
Definition flow_ibm.hpp:685
void setPressureGraphAmg(bool on)
Definition flow_ibm.hpp:173
void fillGhostsFaces(CCField f)
Definition flow_ibm.hpp:899
void exchangeFieldAdd(const std::string &name)
void setVelocityStreams(bool)
Definition flow_ibm.hpp:251
void exchangeField(const std::string &name)
void setAdvection(bool on)
Definition flow_ibm.hpp:142
double maxPorousResidual()
Definition flow_ibm.hpp:724
void uploadVelocity(const std::vector< double > &uu, const std::vector< double > &vv, const std::vector< double > &ww)
Definition flow_ibm.hpp:255
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
Definition flow_ibm.hpp:848
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:184
void fillPropGhosts(CCField f)
Definition flow_ibm.hpp:795
void setVelocityIterations(int it)
Definition flow_ibm.hpp:140
void setPressureChebyshev(bool on, int maxit, double rtol)
Definition flow_ibm.hpp:195
void smoothComp(int c)
void setScalarBc(const std::string &name, int face, int type, double value)
std::vector< double > getOpenness(int c)
Definition flow_ibm.hpp:669
Solver(int nx, int ny, int nz)
Definition flow_ibm.hpp:53
void addDragDiagonal(int c)
void patchScalarDirichletFace(CCField AC, CCField band, double D, int a, int side)
void setVariableRotational(int mode, double chi)
void setPressureUnderRelax(double w)
void setPressureLevels(int levels)
Definition flow_ibm.hpp:179
std::array< int, 3 > blockOrigin() const
void setDeferredCorrection(bool on)
Definition flow_ibm.hpp:190
void setPressureGeometry(const std::vector< double > &sdfInner)
Definition flow_ibm.hpp:430
static constexpr int G
Definition flow_ibm.hpp:51
void fillAxis(CCField f, int axis)
Definition flow_ibm.hpp:930
void setDt(double d)
Definition flow_ibm.hpp:138
void setPorousDepsDt(bool on)
void setField(const std::string &name, const std::vector< double > &v)
void setBodyForce(double fx, double fy, double fz)
Definition flow_ibm.hpp:139
void applyScalarBcFace(CCField c, int a, int side, int type, double val)
std::vector< double > getVelocity(int c)
Definition flow_ibm.hpp:652
std::array< int, 3 > blockShape() const
long strideOf(int c) const
Definition flow_ibm.hpp:804
bool hasField(const std::string &name) const
void setDomainBcProfile(int face, const std::vector< double > &prof, int nb, int nc)
Definition flow_ibm.hpp:408
void setAdvectionScheme(int s)
Definition flow_ibm.hpp:147
long lastPressureIterations() const
Definition flow_ibm.hpp:755
void allocateBlock(int nx, int ny, int nz)
Definition flow_ibm.hpp:57
void setVelocityMultigrid(bool on, int levels, int vcycles)
Definition flow_ibm.hpp:163
void setDensityMode(bool variable)
bool hasScalar(const std::string &name) const
void setMu(double m)
Definition flow_ibm.hpp:137
void setPressureWarmstart(bool on)
Definition flow_ibm.hpp:217
void setPressureIterations(int it)
Definition flow_ibm.hpp:141
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:50
CCField fieldView(const std::string &name)
void addScalar(const std::string &name, double D, int scheme, int iters)
void buildAdvStencilVar(int c)
bool bcStencilPath() const
Definition flow_ibm.hpp:788
void setImplicitAdvection(bool on)
Definition flow_ibm.hpp:153
VarFaceProps makeFaceProps(int c)
Definition flow_ibm.hpp:808
void buildRhs(int c)
Definition flow_ibm.hpp:951
double reduceMaxAbsInner(CCConst f)
void setPorousContinuity(bool on)
int ghostWidth() const
void setRho(double r)
Definition flow_ibm.hpp:136
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)
Definition flow_ibm.hpp:867
std::vector< double > getPressure()
Definition flow_ibm.hpp:673
void setOuterIterations(int iters)
Definition flow_ibm.hpp:157
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 — 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 buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz)
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 ibmSolidMask(CCField mask, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:87
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:100
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)
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
void diffSmoothColor(SField c, SConst b, I3 e, I3 og, int g, double beta, double Ac, int color, SConst dcorr)
void bcDiffusionFold(BField dcorr, BField brhs, B3 ext, int g, int a, int s, double dval, double bval)
Definition mac_bc.hpp:172
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:120
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
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 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)
void bcCorrectOutflow(BField f, BField phi, B3 ext, int g, int a)
Definition mac_bc.hpp:215
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 centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e, int g)
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)
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)
Kokkos::View< const float *, CCMem > MConst
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:73
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).
std::array< double, 4 > p
static constexpr double AC
static constexpr int N
Kokkos::View< float *, IMem > FV