flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_project.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_PROJECT_HPP
9#define PECLET_FLOW_FLOW_IBM_PROJECT_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
15 // ISSUES sweep item 1 (VOF_PLAN section 13 item 9): make `step()` ATOMIC across the two
16 // explicit two-phase stability throws. Both used to fire AFTER the momentum half had already
17 // advanced by the rejected dt -- the Weymouth-Yue boundedness cap from inside `advectVof`
18 // (which runs at the TAIL of the step without momentum consistency) and the Brackbill
19 // capillary cap from `updateVofCurvature` (which runs after `advectVofMomentum`). So the
20 // obvious driver pattern -- catch, halve dt, retry -- gave the momentum equation one extra
21 // over-long step the colour never saw, i.e. it DESYNCHRONISED the two fields instead of
22 // retrying. Both checks are now evaluated here, at the head of `step()` and before the first
23 // mutator, so a throw leaves every field bitwise as it was on entry. No-op unless VoF is on.
25 superficialVelocityPrecheck(); // its scope, before the first mutator (same reason)
26 // The configuration refusals of rung V8 (S0: all-fluid, no ghost projection, no harmonic rho_f,
27 // incremental pressure) and of the balanced-force projection (§4.6.4), HERE beside the VoF
28 // pre-check -- before advectVofMomentum, phaseChangeStep and updateProperties -- so a refused
29 // step also leaves every field bitwise as it was. Inert (no check at all) off those paths.
33 requireBalancedForceScope("step", true);
34
35 // The momentum-solver choice, for a configuration that never calls set_solid. set_solid is the
36 // other (and historically the only) place it is made; a domain-BC case with no immersed solid
37 // never reaches it, which is why enabling the velocity MG there used to build no hierarchy and
38 // segfault. Deciding at the head of the first step is if anything more correct than deciding in
39 // set_solid: every flag the eligibility test reads (advection scheme, variable properties,
40 // drag, porous) is final by now, whereas at set_solid time it need not be.
41 if (!vmgDecided_) {
44 vmgDecided_ = true;
45 }
46 const double ts0 = phaseTick();
47 tPredictor_ = tMomentum_ = tProjection_ = 0.0;
48 lastMomentumSweeps_ = 0;
49 lastMomentumResid_ = -1.0;
51 // Momentum-consistent geometric VoF (rung V2b, WO-K): the colour field and rho^c u_c are
52 // advanced TOGETHER, by the same fluxes, at the head of the step — the momentum advection has
53 // to precede the predictor that consumes it. The advecting field is u^n, the previous step's
54 // projected (discretely divergence-free) output. No-op unless enable_vof_momentum ran; when it
55 // has NOT, the colour advection keeps WO-J's slot at the bottom of the step and this path is
56 // byte-identical to V2a. See enableVofMomentum().
58 // Phase change (rungs P0/P1, WO-P01): mdot + the PLIC areas/normals from (C^n, T^n), the
59 // divergence source deposit read by project(), and the interface regression applied to the
60 // SAME C^n the planes came from. No-op (byte-identical) unless enable_phase_change ran.
61 {
62 const double _t0 = vofTick(); // WO-V9
64 vofAdd(vt_.phaseChange, _t0);
65 }
66 // Multiphysics: refresh material properties / body forces from the current fields (frozen over
67 // the step). No-op (byte-identical) when no closure is registered.
69 // WO-G: give the per-cell body force the ghost ring its consumer assumes. This is the ONE point
70 // in the step that is after BOTH writers (the closures just above; an external CFD-DEM
71 // field_view/exchange_field_add deposit, which happens before step()) and before every consumer
72 // (buildRhsVar in the Picard loop). Inert when no force field is registered.
74 // WO-I: and the same for the per-cell drag coefficient, for the same reason one phase earlier.
75 // Same call site, same justification: after BOTH writers, before the FIRST consumer (the
76 // momentum stencil builds just below). See fillDragBetaGhosts().
78 // Rung V4 (WO-P): the interface curvature of the colour field this step will run with, and the
79 // explicit capillary stability check. No-op (byte-identical) unless set_surface_tension ran.
80 {
81 const double _t0 = vofTick(); // WO-V9
83 vofAdd(vt_.curvature, _t0);
84 }
85 // eps-conservative porous momentum: the volume-averaged time term is (eps_f rho/dt) u, i.e.
86 // the variable-density machinery with the effective density rho_eff = eps*rho, refreshed from
87 // the just-deposited eps every step (eps ghosts are already filled by the coupling driver, so
88 // the whole-block product has valid ghosts). Without this weight the plain-u momentum lets the
89 // projection drag gas along with the moving porosity at zero inertia cost — a spurious energy
90 // source that pumps the particles through the drag (measured in the HCS benchmark).
91 if (porous_ && porousCons_)
93 // Variable properties / implicit drag: rebuild the diffusion stencil from the current mu/rho
94 // and drag_beta fields (the implicit-FOU path rebuilds it per Picard in buildAdvStencil*, so
95 // only the non-advective path needs this).
96 if (((varProps_ || varRho_ || hasDrag_ || (porous_ && porousCons_)) || dtDirty_) &&
97 !implicitAdv())
99 dtDirty_ = false; // implicit-FOU rebuilds per Picard below (reads dt_ live) — clear either way
100 // u^n time base, fixed for the whole step (Picard lags the advecting velocity at u^k, not the
101 // base).
102 for (int c = 0; c < 3; ++c)
103 Kokkos::deep_copy(old_[c], C[c].u);
104 if (cutcellPressure_ && incremental_) {
105 fillGhosts(P_);
106 if (hasBc_)
108 } // grad(P^n) for the incremental predictor (once)
109 // Rung V8 (doc/collocated_varrho_forces.md §4.2; its scope was checked at the step head).
110 if (colocatedFaceForce()) {
111 // The density ghosts under the PROPERTY ghost policy (Neumann copy at walls) — the same values
112 // the Poisson coefficient reads in projectBuildCoefficients (§4.9 L4: the operator's rho_f,
113 // the predictor's rho_f and Pi_rho's mass weights see the same cell rho). A bare halo fill
114 // wraps a wall's ghost periodically, and Pi_rho would then carry the near-wall velocity onto
115 // the closed wall face (a spurious transport flux; the constraint itself ignores that face).
116 if (effVarRho())
118 }
119 // (B) The balanced-force projection (doc/collocated_varrho_forces.md §4.6.2): once per step,
120 // before the Picard loop. Byte-identical (not even a flag read beyond this) when it is off.
121 coeffBuiltThisStep_ = false;
122 if (pWritten_ || pbWritten_) { // a restart restored the pressure: is Pb its split?
123 pbValid_ = pWritten_ && pbWritten_;
124 pWritten_ = pbWritten_ = false;
125 }
126 if (balancedForceActive()) {
128 } else {
129 pbValid_ = false; // this step moves P without Pb: the next ON step re-splits
130 if (!bfpSet_ && !bfpOpenNotice_ && colocatedFaceForce() && hasOpenDomainFace()) {
131 // The V8 default resolved OFF because of an open face: say so once, as the AUTO-scheme
132 // fallback does.
133 bfpOpenNotice_ = true;
134 int r = 0;
135#ifdef PECLET_FLOW_MPI
136 if (distributed_)
137 MPI_Comm_rank(comm_, &r);
138#endif
139 if (r == 0)
140 std::fprintf(stderr,
141 "peclet::flow SolverColocated: the balanced-force projection (default ON on "
142 "variable density / surface tension) is OFF: it does not support "
143 "inflow/outflow domain faces (v1). Call set_balanced_force_projection(False) "
144 "to silence this notice.\n");
145 }
146 }
147 lastOuterIters_ = 0;
148 for (int outer = 0; outer < outerIters_; ++outer) {
149 const double tp0 = phaseTick();
150 lastOuterIters_ = outer + 1;
151 if (outerTol_ > 0)
152 for (int c = 0; c < 3; ++c)
153 Kokkos::deep_copy(prev_[c], C[c].u);
154 if (advect_ || hasBc_ || (Grid::collocated && faceInterp_ >= 5 && faceInterp_ <= 7))
155 for (int c = 0; c < 3; ++c)
156 fillVelGhosts(c, 0); // explicit ghosts (periodic + BC) for advect / embed defect matvec
157 // A0 (advective cut-wall flux): build the advection's wall-aware velocity inputs. AFTER the
158 // ghost exchange, over the extended block, so ghost solid rows carry the wall velocity too.
159 // No-op -- and no allocation -- unless a scene instance is moving; static scenes keep reading
160 // C[*].u byte for byte. See buildAdvInputs().
162 // Porous advection-form compensation: the Koren/SOU/FOU advection operators are CONSERVATIVE
163 // (flux form, ∇·(u u)), which equals the true advective transport u·∇u only for a solenoidal
164 // advecting field. Under the volume-averaged continuity div(eps u)=0 the plain divergence
165 // div(u) = -(1/eps) u·grad(eps) != 0, and the flux form silently adds the spurious force
166 // +u(div u) — largest where grad(eps) is large (clusters), where it pumps particle kinetic
167 // energy through the drag with no physical source (measured: HCS variance rising ~x30 past
168 // the clustering plateau). Compensate by subtracting u_f·div(u)_f from the advection in the
169 // RHS (the exact identity u·∇u = ∇·(uu) − u∇·u; div(u) at the face = mean of the two cell
170 // divergences). Gated on porous_ so every other path is byte-identical.
171 if (porous_ && advect_)
173 // Rung V8: on the collocated grid with variable density and/or surface tension the pressure
174 // and every force go INSIDE this implicit predictor through the mass-adjoint face integral —
175 // WHY, and why never as a face acceleration after the viscous solve: see buildRhsColoVar.
176 // `colocatedFaceForce()` is false on the staggered path and on every constant-density
177 // collocated path without CSF, so this dispatch is inert there.
178 const bool coloFF = colocatedFaceForce();
179 for (int c = 0; c < 3; ++c) // RHS from u^n base + advection lagged at u^k
181 : (vofMomEnabled_ ? buildRhsVarMom(c)
182 : (effVarRho() ? buildRhsVar(c)
183 : (hasCellForce_ ? buildRhsForced(c) : buildRhs(c))));
184 // Rung V4 (WO-P): the balanced-force CSF, ADDED to whichever RHS the configuration built --
185 // at the same place, and with the same face difference, as the incremental -grad(P^n) just
186 // above it. Independent of the time term and the advection form, hence additive rather than a
187 // fifth RHS kernel. Gated: byte-identical when surface tension is off.
188 if (csfActive() && !coloFF) {
189 const double _t0 = vofTick(); // WO-V9
190 for (int c = 0; c < 3; ++c)
192 ? addCsfRhsBlocks(c)
193 : (csfMode_ == 0 ? addCsfRhs(c, C[c].b, true) : addCsfRhsCellInterp(c, C[c].b, true));
194 vofAdd(vt_.csf, _t0);
195 }
196 // Implicit-FOU: rebuild the IBM velocity stencil = backward-Euler diffusion + rho*FOU(u^k),
197 // then re-apply the cut-cell bake. Per Picard iteration (advecting velocity changes). Applies
198 // to the IBM (periodic/porous) path when the user opts in, AND ALWAYS to the domain-BC
199 // stencil path (inflow/outflow) -- implicitAdv() -> fully-implicit upwind advection (stable
200 // at large dt). The velocity-MG BC path keeps its own FOU coarse operator.
201 if (implicitAdv() && (!hasBc_ || !useVelocityMg_ || mixedVelocityMg()))
202 for (int c = 0; c < 3; ++c)
203 (varProps_ || effVarRho()) ? buildAdvStencilVar(c) : buildAdvStencil(c);
204 // Outflow backflow stabilization: dissipate reverse flow at the outlet in the momentum
205 // operator used by the domain-BC stencil smoother (prevents backflow divergence). Inert
206 // without reversal.
207 if (bcStencilPath() && backflowBeta_ > 0.0 && hasOutflow_)
208 for (int c = 0; c < 3; ++c)
210 // upwind-convective velocity-MG: restrict the (frozen u^k) advecting velocity to the coarse
211 // levels ONCE, before the per-component solves update it (shared across the 3 momentum
212 // components).
213 if (useVelocityMg_ && advect_ &&
214 ((implicitFou_ && !hasBc_) || (mixedVelocityMg() && implicitAdv())))
215 vmg_.restrictAdvVelocities( // A0: same inputs as the fine operator -- including the
216 // collocated projected face field, so the coarse FOU
217 // preconditioner stays consistent with the fine one
218 // (levels >= 1 only; level 0 IS buildAdvStencil's operator)
222 const double tp1 = phaseTick();
223 tPredictor_ += tp1 - tp0;
224 for (int c = 0; c < 3; ++c)
225 smoothComp(c); // per-component IBM implicit-diffusion solve
226 // Route (b): stash u* for the reaction-force budget. Every Picard iteration overwrites, so
227 // what survives is the LAST momentum solve -- the one whose viscous fluxes, together with
228 // the last projection, actually produced u^{n+1} (the time base is u^n for every iteration,
229 // so earlier iterations leave no trace in the final state except through P_). A pure
230 // deep_copy: no numerical effect on any solve.
231 if constexpr (!Grid::collocated) {
232 if (hasScene_) {
233 for (int c = 0; c < 3; ++c) {
234 if (uStar_[c].extent(0) != n_)
235 uStar_[c] = CCField("uStar", n_);
236 Kokkos::deep_copy(uStar_[c], C[c].u);
237 }
238 haveUStar_ = true;
239 }
240 }
241 const double tp2 = phaseTick();
242 tMomentum_ += tp2 - tp1;
243 // The porous (volume-averaged) projection lives entirely on the cut-cell operator rails
244 // (divergOpenEps + buildPorousCoeff* into CutcellMG). Without set_solid /
245 // set_pressure_geometry there is NO projection at all — the gas would never accelerate to
246 // the interstitial velocity in a bed and the drag comes out ~5x too weak (a fluidized bed
247 // quietly refuses to fluidize). Fail loudly instead of silently dropping the constraint.
248 if (porous_ && !cutcellPressure_)
249 throw std::runtime_error(
250 "set_porous_continuity(True) requires the cut-cell pressure operator: call "
251 "set_solid(...) or set_pressure_geometry(all-fluid SDF) before stepping (a "
252 "domain-BC-only box otherwise runs with NO continuity constraint at all)");
253 if (cutcellPressure_)
254 project(); // cut-cell projection -> incompressible
255 tProjection_ += phaseTick() - tp2;
256 if (hasBc_)
257 applyVelocityBcAll(0, false); // re-impose domain BCs (keep outflow), components 0, 1, 2
258 // WO-5b: a cut-cell scalar's open-face flux is the one this projection constrained; keep it
259 // before any ghost fill wraps the high-side plane (no-op without one; the last iteration wins).
260 if (cutcellPressure_)
262 if (outerTol_ > 0) { // outer convergence: max velocity change over this Picard iteration
263 double corr = 0.0;
264 for (int c = 0; c < 3; ++c)
265 corr = Kokkos::fmax(corr, maxAbsDiffInner(CCConst(C[c].u), CCConst(prev_[c])));
266 lastOuterCorr_ = corr;
267 if (corr < outerTol_)
268 break;
269 }
270 }
271 // Geometric VoF (rung V2a): advance the colour field with the just-projected, discretely
272 // divergence-free face velocities — the SAME slot, and the same justification, as
273 // advanceScalars() below (Weymouth-Yue's exact conservation is conditioned on the advecting
274 // field's discrete divergence; see advectVof()). No-op (byte-identical) when VoF is off.
275 // Under momentum consistency (V2b) the colour was already advanced at the head of the step,
276 // together with the momentum it shares its fluxes with — see advectVofMomentum().
277 if (!vofMomEnabled_)
278 advectVof();
279 // WO-P23: refresh k(C) / (rho c_p)(C) from the colour the energy solve is about to see. No-op
280 // unless the consistent-energy transport is on.
282 // WO-P01: refresh the energy scalar's per-cell Dirichlet set from the colour the energy solve
283 // is about to see. No-op unless the thermal mass flux is on.
285 // Segregated multiphysics: advance any transported scalars with the just-projected
286 // divergence-free velocity (properties frozen over the step). No-op (byte-identical) when no
287 // scalar is registered.
289 // set_superficial_velocity: the uniform shift of one velocity component, LAST, so the next step
290 // starts from the constrained field exactly as a driver shifting it between steps would. No-op
291 // when off.
293 // The UNSTABLE outflow regime, detected rather than guessed (peclet-examples ISSUES.md
294 // "Inflow/outflow diverges to NaN"): reversed flow on an outflow face with the backflow
295 // stabilization switched OFF. Checked only in that configuration (beta <= 0 is not the
296 // default), once per solver; see outflowBackflow() for the mechanism.
297 if (hasOutflow_ && backflowBeta_ <= 0.0 && !backflowWarned_) {
299 if (ob.reversed > 0) {
300 backflowWarned_ = true;
301 int r = 0;
302#ifdef PECLET_FLOW_MPI
303 if (distributed_)
304 MPI_Comm_rank(comm_, &r);
305#endif
306 if (r == 0)
307 std::fprintf(stderr,
308 "peclet::flow WARNING: reversed flow on an OUTFLOW face (max u.n = -%.3e, "
309 "%.1f%% of the outlet area, kinetic-energy influx %.3e) with the backflow "
310 "stabilization OFF. The zero-gradient outflow is only conditionally "
311 "energy-stable under reversal (the re-entering flux carries the boundary "
312 "cell's own velocity back in with no sink); this is the configuration in "
313 "which the literature's backflow divergence occurs. Call "
314 "set_backflow_stabilization(beta) (default 0.2; beta >= 0.5 is the "
315 "unconditional bound), or lengthen the domain so the recirculation closes "
316 "before the outlet. Monitor with outflow_backflow().\n",
317 ob.maxReverse, 100.0 * ob.fraction, ob.energyInflux);
318 }
319 }
320 tStep_ = phaseTick() - ts0;
321 if (vofTiming_) { // WO-V9: the coarse phases, summed over the profiled window
322 tStepSum_ += tStep_;
323 tPredSum_ += tPredictor_;
324 tMomSum_ += tMomentum_;
325 tProjSum_ += tProjection_;
326 ++vt_.steps;
327 }
328}
329
330template <class Grid>
332 if (superficialAxis_ < 0)
333 return;
334 const int a = superficialAxis_;
335 if (bc_[2 * a] != 0 || bc_[2 * a + 1] != 0)
336 throw std::runtime_error(
337 "set_superficial_velocity: the axis carries a domain boundary condition; a uniform "
338 "shift of the normal velocity is divergence-free only on a PERIODIC axis");
339 if (hasSolid_)
340 throw std::runtime_error(
341 "set_superficial_velocity: an immersed solid is present; a uniform shift would move fluid "
342 "through the cut faces (all-fluid domains only)");
343}
344
345template <class Grid>
347 if (superficialAxis_ < 0)
348 return;
349 const int a = superficialAxis_;
351 const C3 e = e_;
352 CCField f = C[a].u;
353 long ncell = static_cast<long>(nx_) * ny_ * nz_;
354#ifdef PECLET_FLOW_MPI
355 if (distributed_)
356 ncell = static_cast<long>(gnx_) * gny_ * gnz_;
357#endif
358 const double target = superficialVelPhys_ * u_.velToInt(a), dn = static_cast<double>(ncell);
359 Kokkos::View<double, CCMem> shift = superficialShift_;
360 const auto inner = MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G});
361 // The inner sum into the device scalar (asynchronous: the result never visits the host).
362 Kokkos::parallel_reduce(
363 "superficial_velocity_sum", inner,
364 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
365 acc += f((long)x + (long)y * e.x + (long)z * (long)e.x * e.y);
366 },
367 Kokkos::Sum<double, CCMem>(shift));
368#ifdef PECLET_FLOW_MPI
369 if (distributed_) {
370 // The global sum needs the host (MPI on host buffers): one scalar each way per step.
371 double loc = 0.0, glob = 0.0;
372 Kokkos::deep_copy(loc, shift);
373 MPI_Allreduce(&loc, &glob, 1, MPI_DOUBLE, MPI_SUM, comm_);
374 Kokkos::deep_copy(shift, glob);
375 }
376#endif
377 Kokkos::parallel_for(
378 "superficial_velocity_to_shift", Kokkos::RangePolicy<CCExec>(space, 0, 1),
379 KOKKOS_LAMBDA(int) { shift() = target - shift() / dn; });
380 Kokkos::parallel_for(
381 "superficial_velocity_shift", inner, KOKKOS_LAMBDA(int x, int y, int z) {
382 f((long)x + (long)y * e.x + (long)z * (long)e.x * e.y) += shift();
383 });
384 // The ghost ring follows its (shifted) owners: periodic images, and the domain BCs of the
385 // other axes re-imposed on the new tangential values.
386 fillVelGhosts(a, 0);
387}
388
389template <class Grid>
391 return Grid::collocated && (varRho_ || csfActive());
392}
393
394template <class Grid>
396 if constexpr (Grid::collocated) {
397 if (colSchemeAuto_ && ghostProjection_) {
398 ghostProjection_ = false;
399 gpNRows_ = -1;
400 faceInterp_ = 9;
402 "peclet::flow SolverColocated: AUTO scheme fell back to gauge-exact (%s is rung V8 "
403 "and the ghost projection v1 does not support it). Select explicitly with "
404 "set_collocated_scheme to silence this notice.\n",
405 why);
406 }
407 }
408}
409
410template <class Grid>
412 for (int c = 0; c < 3; ++c)
413 if (faceAcc_[c].extent(0) != n_)
414 faceAcc_[c] = CCField("faceAcc", n_);
415}
416
417template <class Grid>
419 if (!colocatedFaceForce())
420 return;
421 std::string m(who);
422 if (hasSolid_)
423 throw std::runtime_error(
424 m +
425 ": variable density / surface tension on SolverColocated is rung V8 and is ALL-FLUID "
426 "only (set_pressure_geometry). Immersed solids on this path are the next package "
427 "(doc/collocated_multiphase_solids_plan.md) — not this rung.");
428 if (ghostProjection_)
429 throw std::runtime_error(
430 m +
431 ": the ghost projection (v1) does not support variable density; select "
432 "set_collocated_scheme(\"gauge-exact\") (the AUTO fallback already does).");
433 if (rhoFaceHarmonic_)
434 throw std::runtime_error(
435 m +
436 ": set_rho_face_harmonic is not wired into the collocated variable-density pair (the "
437 "pressure force and the face coefficient would use different rho_f). Staggered only.");
438 if (!incremental_)
439 throw std::runtime_error(
440 m +
441 ": variable density / surface tension on SolverColocated needs the incremental pressure "
442 "(diagnostics.set_incremental_pressure(True)): the pressure force lives inside the "
443 "implicit predictor, and the non-incremental (Chorin) step has none, so its steady state "
444 "would depend on dt.");
445}
446
447template <class Grid>
449 if (enabled)
450 requireBalancedForceScope("set_balanced_force_projection", false);
451 bfpSet_ = true;
452 bfpOn_ = enabled;
454}
455
456template <class Grid>
458 if (balancedForceActive() && Pb_.extent(0) != n_)
459 Pb_ = addField("p_balanced"); // zero-initialised: with P = 0 that is a valid split
460}
461
462template <class Grid>
466
467template <class Grid>
469 return lastBalancedForceIters_;
470}
471
472template <class Grid>
474 return lastBalancedForceFailed_;
475}
476
477template <class Grid>
479 return balancedForceFailures_;
480}
481
482template <class Grid>
484 // U2 (doc/collocated_varrho_forces.md §0): an explicit setting wins; the DEFAULT is ON on the
485 // collocated variable-density / CSF path (V8), where the settled constant-kappa CSF balance
486 // needs it, and OFF everywhere else (staggered and constant-density collocated stay
487 // byte-identical). With an inflow/outflow domain face the V8 default resolves OFF (the option
488 // does not support open faces, §4.6.4 / Q7; step() says so once): only an EXPLICIT ON throws.
489 if (bfpSet_)
490 return bfpOn_;
492}
493
494template <class Grid>
496 for (int f = 0; f < 6; ++f)
497 if (bc_[f] == 2 || bc_[f] == 3)
498 return true;
499 return false;
500}
501
502template <class Grid>
504 const std::string m = std::string(who) + ": the balanced-force projection ";
505 if (porous_)
506 throw std::runtime_error(m +
507 "does not support the porous (volume-averaged) continuity: the "
508 "VANS operator carries eps and the drag relaxation (CFD-DEM is out "
509 "of scope). Call set_balanced_force_projection(False).");
510 bool autoGhost = false;
511 if constexpr (Grid::collocated)
512 autoGhost = colSchemeAuto_; // an AUTO 'ghost' may still fall back; final at the step
513 if (ghostProjection_ && (atStep || !autoGhost))
514 throw std::runtime_error(m +
515 "does not support the ghost projection (its overlay does not reach "
516 "the balanced face force yet). Call set_balanced_force_projection"
517 "(False).");
518 if (fluidOnlyMode_ == 2)
519 throw std::runtime_error(m + "does not support set_fluid_only_constraint(2).");
520 if (vofBlockCsf())
521 throw std::runtime_error(m + "does not support the block CSF (enable_vof_block_csf).");
522 for (int f = 0; f < 6; ++f)
523 if (bc_[f] == 2 || bc_[f] == 3)
524 throw std::runtime_error(
525 m +
526 "does not support inflow/outflow domain faces (v1: the operator-vs-flux openness rule "
527 "at inflow and the outflow planes are not wired). Call "
528 "set_balanced_force_projection(False).");
529 if (!incremental_)
530 throw std::runtime_error(m + "needs the incremental pressure (it re-splits P).");
531 if (atStep) {
532 if (!cutcellPressure_)
533 throw std::runtime_error(
534 m + "needs the cut-cell pressure operator (set_solid or set_pressure_geometry).");
535 if constexpr (Grid::collocated)
536 if (!colocatedFaceForce())
537 throw std::runtime_error(
538 m +
539 "has nothing to balance on the constant-density collocated path without surface "
540 "tension (its pressure force is the gauge-exact cell gradient, not the face "
541 "integral). It applies to variable density / surface tension on SolverColocated "
542 "and to every Solver configuration. Call set_balanced_force_projection(False).");
543 }
544}
545
546template <class Grid>
548 // beta = f_const + ½(f(j) + f(j-s)) (+ CSF below) on the face range [G, e-G]: the predictor's
549 // own face force. The face mean is the staggered Grid::atVelocity expression on BOTH grids (on
550 // the collocated grid the cell value is the predictor's placement, and the face mean is what the
551 // pressure must carry for rho g to balance: rho_c R(½(rho_L+rho_R) g/rho_f) = W rho_c g).
553 const double fc = f_[c];
554 const long s = strideOf(c);
555 const bool haveFb = hasCellForce_;
556 CCConst fb = CCConst(haveFb ? cellForce_[c] : C[c].rscale);
557 CCField af = faceAcc_[c];
558 C3 e = e_;
559 using MD = MDRange3<CCExec>;
560 Kokkos::parallel_for(
561 "peclet::flow::bfp_face_force", MD(space, {G, G, G}, {e.x - G + 1, e.y - G + 1, e.z - G + 1}),
562 KOKKOS_LAMBDA(int x, int y, int z) {
563 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
564 af(i) = fc + (haveFb ? 0.5 * ((double)fb(i) + (double)fb(i - s)) : 0.0);
565 });
566 // CSF from the SAME code the predictor uses (row scale off, face range): the V4 face form on the
567 // collocated V8 path (which always uses it), and the csf mode's kernel on the staggered grid.
568 if (csfActive()) {
569 if (Grid::collocated || csfMode_ == 0)
570 addCsfRhs(c, af, /*rowScaled=*/false, /*faceRange=*/true);
571 else
572 addCsfRhsCellInterp(c, af, /*rowScaled=*/false, /*faceRange=*/true);
573 }
574 // times c = rho0/rho_f^op, the operator's own face mean (buildRhoCoeff / buildRhoCoeffHarm).
575 if (varRho_) {
576 CCConst rho = CCConst(rhoField_);
577 const double rho0 = rho_;
578 const bool harm = rhoFaceHarmonic_;
579 Kokkos::parallel_for(
580 "peclet::flow::bfp_face_coeff",
581 MD(space, {G, G, G}, {e.x - G + 1, e.y - G + 1, e.z - G + 1}),
582 KOKKOS_LAMBDA(int x, int y, int z) {
583 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
584 const double cf = harm ? rho0 * (rho(i) + rho(i - s)) / (2.0 * rho(i) * rho(i - s))
585 : rho0 / (0.5 * (rho(i) + rho(i - s)));
586 af(i) *= cf;
587 });
588 }
589}
590
591template <class Grid>
593 // (scope: checked at the step head, beside vofStepPrecheck)
594 // (B1) the variable-rho operator, once for the step (project() then skips its rebuild).
595 if (varRho_) {
597 coeffBuiltThisStep_ = true;
598 }
599 registerBalancedForceState(); // normally already registered by the setter that enabled it
600 if (pb1_.extent(0) != n1_)
601 pb1_ = CCField("pb1", n1_);
602 // (B2) the face force c*beta, (B3) its constraint divergence (div_ is scratch here: project()
603 // recomputes it before anything reads it).
605 for (int c = 0; c < 3; ++c)
607 constraintDivergence(CCConst(faceAcc_[0]), CCConst(faceAcc_[1]), CCConst(faceAcc_[2]), div_);
608 // (B4) the INCREMENT solve (WO-P5): A dP_b = D(O c beta) - A P_b^{n-1}, stopped RELATIVE TO THE
609 // FULL right-hand side, |r| <= rtol * max|D(O c beta)| -- run as the projection's own driver
610 // started from x = P_b^{n-1} with a full-RHS stop reference (the driver's first residual
611 // b - A P_b^{n-1} IS the increment's right-hand side, and x - P_b^{n-1} the increment), and
612 // skipped outright (zero iterations) when that initial residual already meets the test: a
613 // static interface costs no iterations. A force with no divergence needs no pressure at all.
614 long iters = 0;
615 bool failed = false;
616 double bmax = maxAbsInner(CCConst(div_), e_, G);
617#ifdef PECLET_FLOW_MPI
618 if (distributed_) {
619 double g = 0.0;
621 bmax = g;
622 }
623#endif
624 if (bmax == 0.0) {
625 Kokkos::deep_copy(pb1_, 0.0);
626 } else {
627 copyInner(rhs1_, e1_, 1, CCConst(div_), e_, G);
628 {
630 CCField r = rhs1_;
631 Kokkos::parallel_for(
632 "peclet::flow::bfp_negdiv", Kokkos::RangePolicy<CCExec>(space, 0, n1_),
633 KOKKOS_LAMBDA(std::size_t i) { r(i) = -r(i); });
634 }
635 copyInner(pb1_, e1_, 1, CCConst(Pb_), e_, G);
636 const double rtol = useChebyshev_ ? chebRtol_ : pcgRtol_;
637 const double bref = std::max(mg_.rhsNorm(rhs1_, r_), 1e-300);
638 const double r0 = mg_.residualNorm(rhs1_, pb1_, r_, Ap_);
639 if (r0 > rtol * bref) {
640 iters = solveBalancedForceSystem(bref, r0);
641 // Captured right after the solve (ISSUES sweep item 6: a breakdown reports the cap and
642 // raises the flag); a non-finite X fails too. Agreed across ranks.
643 int bad = mg_.lastSolveFailed() ? 1 : 0;
644 {
646 CCConst x1 = CCConst(pb1_);
647 const C3 e1 = e1_;
648 long nf = 0; // a COUNT (sum reduction, identity 0) of the non-finite inner values
649 Kokkos::parallel_reduce(
650 "peclet::flow::bfp_nonfinite",
651 MDRange3<CCExec>(space, {1, 1, 1}, {e1.x - 1, e1.y - 1, e1.z - 1}),
652 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
653 const long i = (long)x + (long)y * e1.x + (long)z * (long)e1.x * e1.y;
654 if (!Kokkos::isfinite(x1(i)))
655 acc += 1;
656 },
657 nf);
658 bad = (bad || nf > 0) ? 1 : 0;
659 }
660#ifdef PECLET_FLOW_MPI
661 if (distributed_) {
662 int g = 0;
664 bad = g;
665 }
666#endif
667 failed = bad != 0;
668 }
669 }
670 lastBalancedForceIters_ = iters;
671 lastBalancedForceFailed_ = failed;
672 if (failed) {
673 // Keep the previous split: P and P_b unchanged (their ghosts were filled at the step head),
674 // so this step's predictor sees the forces exactly as with the option off. Counted, and the
675 // first one warns.
676 ++balancedForceFailures_;
677 if (!bfpFailWarned_) {
678 bfpFailWarned_ = true;
679 int r = 0;
680#ifdef PECLET_FLOW_MPI
681 if (distributed_)
682 MPI_Comm_rank(comm_, &r);
683#endif
684 if (r == 0)
685 std::fprintf(stderr,
686 "peclet::flow WARNING: the balanced-force pre-projection solve failed (driver "
687 "breakdown or a non-finite result, %ld iterations); P and p_balanced were "
688 "kept for this step. Further failures are counted silently in "
689 "diagnostics.balanced_force_failures().\n",
690 iters);
691 }
692 return;
693 }
694 // (B5) P += X - P_b; P_b = X (one fused kernel over the inner cells), then the P ghosts the
695 // predictor reads. When P_b is NOT a valid split of the current P (pbValid_: steps with the
696 // option off moved P, or a restart restored "p" without "p_balanced"), P already holds the
697 // balanced pressure, and P += X - P_b would add it a second time: RE-SPLIT instead, P_b = X
698 // with P unchanged (doc/collocated_varrho_forces.md §4.6.2).
699 {
701 CCField P = P_, pb = Pb_;
702 CCConst x1 = CCConst(pb1_);
703 const C3 e1 = e1_, e2 = e_;
704 const bool resplit = !pbValid_;
705 Kokkos::parallel_for(
706 "peclet::flow::bfp_split", MDRange3<CCExec>(space, {0, 0, 0}, {nx_, ny_, nz_}),
707 KOKKOS_LAMBDA(int x, int y, int z) {
708 const long i1 = (long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y;
709 const long i2 = (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
710 const double xv = x1(i1);
711 if (!resplit)
712 P(i2) += xv - pb(i2);
713 pb(i2) = xv;
714 });
715 }
716 pbValid_ = true;
717 fillGhosts(P_);
718 if (hasBc_)
720}
721
722template <class Grid>
724 // The balanced-force projection's own call path: the same operator and driver as the main
725 // projection, but the stop is relative to the full right-hand side (setStopReference, reset on
726 // every exit) and a Chebyshev solve uses bounds of its OWN (bfpChebA_/B_), never the main
727 // projection's (doc §11 Q9: the main bounds are estimated on the main right-hand side, and the
728 // per-step variable-rho refresh invalidates them every step). The own bounds are estimated once
729 // on this solve's right-hand side and kept across steps; they are re-estimated after a
730 // structural operator change (bfpChebSet_ cleared) or when the iteration DIVERGES: it ran to
731 // the cap and its residual ended above r0. Then the solve restarts from P_b^{n-1} with fresh
732 // bounds.
733 struct StopRefGuard {
734 CutcellMG& mg;
735 ~StopRefGuard() { mg.setStopReference(-1.0); }
736 } guard{mg_};
738 if (useChebyshev_) {
739 if (!bfpChebSet_) {
740 mg_.estimateEigenvalues(CCConst(rhs1_), bfpChebA_, bfpChebB_, 15, 2, 2, 12);
741 bfpChebSet_ = true;
742 }
743 long it =
744 mg_.solveChebyshev(rhs1_, pb1_, chebMaxit_, chebRtol_, 2, 2, 12, bfpChebA_, bfpChebB_);
745 if (it >= chebMaxit_) { // never met the stop: slow, or diverging on bounds gone stale?
746 const double rfin = mg_.residualNorm(rhs1_, pb1_, r_, Ap_);
747 if (!(rfin <= r0)) { // residual growth (or non-finite): re-estimate and restart once
748 mg_.estimateEigenvalues(CCConst(rhs1_), bfpChebA_, bfpChebB_, 15, 2, 2, 12);
749 copyInner(pb1_, e1_, 1, CCConst(Pb_), e_, G);
750 it +=
751 mg_.solveChebyshev(rhs1_, pb1_, chebMaxit_, chebRtol_, 2, 2, 12, bfpChebA_, bfpChebB_);
752 }
753 }
754 return it;
755 }
756 if (useFcg_) {
757 if (zp1_.extent(0) != n1_)
758 zp1_ = CCField("zp1", n1_);
759 return mg_.solveFCG(rhs1_, pb1_, r_, pp_, z_, zp1_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12, nullptr,
760 0, C3{nx_, ny_, nz_});
761 }
762 return mg_.solvePCG(rhs1_, pb1_, r_, pp_, z_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12, nullptr, 0,
763 C3{nx_, ny_, nz_});
764}
765
766template <class Grid>
769 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_, wc = u_.w[c];
770 C3 e = e_;
771 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
772 // A0: U/V/W (the advecting velocities) and aP (the advected field) come from the wall-aware
773 // advection inputs -- identical to C[*].u unless an instance is moving. `uu` stays the live
774 // field: its only other consumer is the porous advection-form compensation.
775 // COLLOCATED (`ufa`): the advecting velocities are instead the PROJECTED divergence-free MAC
776 // face field of the last projection (the advected field aP stays the CELL field) -- see
777 // ufAdvVelocity(). advWallInputs() is staggered-only, so the two never overlap.
778 const bool ufa = ufAdvVelocity();
779 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
780 W = ufa ? openFaceView(2) : advVelView(2), aP = advVelView(c), uu = CCConst(C[c].u),
781 un = CCConst(old_[c]);
782 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
783 // Pure implicit FOU (no deferred correction): 1st-order upwind carried entirely by the
784 // operator, no explicit high-order term in the RHS -- maximally dissipative/stable (diffuses
785 // sharp shear layers). Only meaningful on an implicit-advection path.
786 const bool pureFou = implicitAdv() && !deferredCorr_;
787 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
788 bc = hasBc_ && !bcStencilPath(); // fold RHS only on the const-coeff domain-BC path;
789 // on the stencil path (solid and/or implicit advection) the walls enter via reflection ghosts
790 // (smoothComp) and the RHS carries the IBM inhom (=0 for no-slip) + the deferred correction.
791 // incr predictor carries -grad(P^n).
792 const bool ifou = implicitAdv() &&
793 deferredCorr_; // deferred correction: keep (HO - FOU) explicit in the RHS
794 // (implicit on the domain-BC path by default, opt-in elsewhere)
795 const int sch = advScheme_; // 0 = SOU (default), 1 = Koren TVD
796 // R0: stash the explicit advective term for the reaction-force budget (staggered scenes only).
797 // The LAST Picard iteration overwrites, matching uStar_'s convention -- that is the RHS the
798 // final momentum solve actually saw.
799 const bool sa = ensureAdvStash(c, adv);
800 CCField ar = advRhs_[c];
801 // Embed rung 5: wall-aware pressure force (collocated): -grad(P) = the TRANSPOSE of the
802 // wall-aware cell->face constraint interpolation, precomputed per component (the plain path's
803 // central difference is the transpose of the plain 1/2-1/2 average, so this keeps the
804 // momentum/constraint operators an adjoint pair on both paths).
805 const bool tg = Grid::collocated && faceInterp_ == 5 && incr;
806 // embed 6/7: openness-weighted -grad(P^n) predictor, matching the fs-weighted correction
807 const bool wg = Grid::collocated && (faceInterp_ == 6 || faceInterp_ == 7) && incr;
808 // ghost mode and the gauge-exact scheme: directional gpCenterGrad predictor — the mode-0
809 // central difference reads the decoupled P=0 at solid-centered cells, a gauge-dependent O(1)
810 // gradient error at every cut cell (measured O(1/h) in physical units,
811 // ghost_collocated_apriori.py [C2]).
812 const bool gg = Grid::collocated && (ghostProjection_ || faceInterp_ == 9) && incr;
813 if constexpr (Grid::collocated) {
814 // Phase 2 C2: each gradient kernel carries the metric weight w_c of ITS OWN axis on its
815 // output (doc/anisotropic_metric.md §3), which is why the `gpw(i)` branch of the RHS below
816 // is not scaled again.
817 if (gg) {
818 gpCenterGrad(tgp_, CCConst(P_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), c, e_, G, u_.w[c]);
819 } else if (tg) {
820 CCField xcs[3] = {xcx_, xcy_, xcz_};
821 CCField oax[3] = {ox_, oy_, oz_};
822 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
823 true, c, e_, G, u_.w[c]);
824 } else if (wg) {
825 CCField oax[3] = {ox_, oy_, oz_};
826 centerGradOpen(tgp_, CCConst(P_), CCConst(oax[c]), c, e_, G, u_.w[c]);
827 }
828 }
829 CCConst gpw = CCConst(tgp_); // empty view on the staggered path (tg/wg/gg false there)
830 // Embed (5/6/7) FV momentum via DEFECT CORRECTION: solve M·u^{k+1} = M·u^k − rs·L_FV(u^k) +
831 // rs·b_FV so the fixed point satisfies the second-order finite-volume balance L_FV·u* = b_FV
832 // exactly, with the (stable, small-cell-safe) IBM matrix M only as preconditioner. fvM_ = M·u^k
833 // (stencilMatvec), fvL_ = L_FV(u^k) (embedViscousApply: o_f faces + cs time + true-normal
834 // wall drag). Interior cells: M = L_FV → the defect vanishes → byte-identical to mode 0.
835 // Stokes only (advection folds into the IBM matrix, not yet into L_FV).
836 // Porous advection-form compensation (+rho*u_f*div(u)_f): see the step() comment. Off (and the
837 // view untouched) on every non-porous path.
838 const bool pc = porous_ && advect_;
839 CCConst dv = CCConst(divAdv_);
840 const bool wd = Grid::collocated && faceInterp_ >= 5 && faceInterp_ <= 7;
841 if constexpr (Grid::collocated)
842 if (wd) {
843 stencilMatvec(fvM_, CCConst(C[c].u), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS),
844 FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT), e_, G);
845 embedViscousApply(fvL_, CCConst(C[c].u), CCConst(sdf_), CCConst(cs_), CCConst(ox_),
846 CCConst(oy_), CCConst(oz_), mu_, rho_ / dt_, e_, G, u_.w[0], u_.w[1],
847 u_.w[2], u_.hp[0], u_.hp[1], u_.hp[2]);
848 }
849 CCConst fvM = CCConst(fvM_), fvL = CCConst(fvL_), cs = CCConst(cs_);
850 // b = descale*(idiag*u^n - rho*Koren(u^k) + rho*FOU(u^k) + f - grad P^n) - inhom (+ BC fold
851 // brhs). The time base is u^n (Picard); the advecting velocity & advected field are the current
852 // iterate u^k.
853 ccFor3(
854 "rhs", C3{G, G, G}, C3{e.x - G, e.y - G, e.z - G}, KOKKOS_LAMBDA(int x, int y, int z) {
855 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
856 double aK = 0.0, aF = 0.0;
857 if (adv) {
858 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{aP, e.x, e.y};
859 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa, ufa)
860 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa, ufa);
861 if (ifou)
862 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa, ufa);
863 }
864 if (sa)
865 ar(i) = rho * (aF - aK);
866 // incremental predictor's -grad(P^n): central-difference cell gradient on the collocated
867 // grid (or the wall-aware transpose gradient, mode 2), one-sided face gradient (P at the
868 // high cell of the staggered face) on the staggered grid.
869 // The pressure gradient of component c carries the metric weight w_c
870 // (doc/anisotropic_metric.md §1.2/§2); wc == 1.0 exactly on the isotropic path. The
871 // gpw() kernels carry their own weight (§3), so they are not scaled again here.
872 const double gp =
873 !incr ? 0.0
874 : Grid::collocated
875 ? ((tg || wg || gg) ? gpw(i) : wc * (0.5 * (P((long)i + strd) - P((long)i - strd))))
876 : wc * (P(i) - P((long)i - strd));
877 if (wd) { // FV defect-correction RHS M·u − rs·(L_FV·u − b_FV), b_FV = idt·cs·u^n +
878 // cs·(f − grad P); the fixed point L_FV·u* = b_FV.
879 const double bfv = idiag * cs(i) * un(i) + cs(i) * (fc - gp);
880 bb(i) = fvM(i) - rs(i) * (fvL(i) - bfv);
881 } else {
882 const double comp = pc ? rho * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
883 bb(i) = rs(i) * (idiag * un(i) + fc - rho * aK + rho * aF + comp - gp) +
884 (bc ? brhs(i) : -inh(i));
885 }
886 }); // BC fold (brhs) on the domain-BC path; -inhom on the IBM path (=0 for no-slip)
887}
888
889template <class Grid>
892 const double idiag = rho_ / dt_, fc = f_[c], rho = rho_, wc = u_.w[c];
893 C3 e = e_;
894 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
895 CCConst fb = CCConst(cellForce_[c]);
896 // A0: U/V/W (the advecting velocities) and aP (the advected field) come from the wall-aware
897 // advection inputs -- identical to C[*].u unless an instance is moving. `uu` stays the live
898 // field: its only other consumer is the porous advection-form compensation.
899 // COLLOCATED (`ufa`): the advecting velocities are instead the PROJECTED divergence-free MAC
900 // face field of the last projection (the advected field aP stays the CELL field) -- see
901 // ufAdvVelocity(). advWallInputs() is staggered-only, so the two never overlap.
902 const bool ufa = ufAdvVelocity();
903 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
904 W = ufa ? openFaceView(2) : advVelView(2), aP = advVelView(c), uu = CCConst(C[c].u),
905 un = CCConst(old_[c]);
906 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
907 const bool pureFou = implicitAdv() && !deferredCorr_;
908 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
909 bc = hasBc_ && !bcStencilPath();
910 const bool ifou = implicitAdv() && deferredCorr_;
911 const int sch = advScheme_;
912 const bool sa = ensureAdvStash(c, adv); // R0: see buildRhs
913 CCField ar = advRhs_[c];
914 // Porous advection-form compensation (+rho*u_f*div(u)_f): see the step() comment.
915 const bool pc = porous_ && advect_;
916 CCConst dv = CCConst(divAdv_);
917 const bool tg = Grid::collocated && faceInterp_ == 5 && incr; // wall-aware -grad(P)
918 const bool gg = Grid::collocated && (ghostProjection_ || faceInterp_ == 9) &&
919 incr; // directional ghost -grad(P)
920 if constexpr (Grid::collocated) {
921 if (gg) {
922 gpCenterGrad(tgp_, CCConst(P_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), c, e_, G, u_.w[c]);
923 } else if (tg) {
924 CCField xcs[3] = {xcx_, xcy_, xcz_};
925 CCField oax[3] = {ox_, oy_, oz_};
926 transposeGradWallAware(tgp_, CCConst(P_), CCConst(sdf_), CCConst(oax[c]), CCConst(xcs[c]),
927 true, c, e_, G, u_.w[c]);
928 }
929 }
930 CCConst gpw = CCConst(tgp_);
931 using MD = MDRange3<CCExec>;
932 Kokkos::parallel_for(
933 "rhs_forced", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
934 KOKKOS_LAMBDA(int x, int y, int z) {
935 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
936 double aK = 0.0, aF = 0.0;
937 if (adv) {
938 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{aP, e.x, e.y};
939 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa, ufa)
940 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa, ufa);
941 if (ifou)
942 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa, ufa);
943 }
944 if (sa)
945 ar(i) = rho * (aF - aK);
946 const double gp =
947 !incr ? 0.0
948 : Grid::collocated
949 ? ((tg || gg) ? gpw(i) : wc * (0.5 * (P((long)i + strd) - P((long)i - strd))))
950 : wc * (P(i) - P((long)i - strd));
951 const double comp = pc ? rho * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
952 // The cell force is VOLUMETRIC: placed where the unknown lives (face mean on the MAC grid,
953 // cell value collocated) -- Grid::atVelocity, shared with buildRhsVar / addDragDiagonal.
954 const double fbU = Grid::atVelocity(fb, i, strd);
955 bb(i) = rs(i) * (idiag * un(i) + fc + fbU - rho * aK + rho * aF + comp - gp) +
956 (bc ? brhs(i) : -inh(i));
957 });
958}
959
960template <class Grid>
962 const double idt = 1.0 / dt_, fc = f_[c], wc = u_.w[c];
963 C3 e = e_;
964 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
965 CCConst fb = CCConst(cellForce_[c]);
967 // A0: U/V/W (the advecting velocities) and aP (the advected field) come from the wall-aware
968 // advection inputs -- identical to C[*].u unless an instance is moving. `uu` stays the live
969 // field: its only other consumer is the porous advection-form compensation.
970 // COLLOCATED (`ufa`): the advecting velocities are instead the PROJECTED divergence-free MAC
971 // face field of the last projection (the advected field aP stays the CELL field) -- see
972 // ufAdvVelocity(). advWallInputs() is staggered-only, so the two never overlap.
973 const bool ufa = ufAdvVelocity();
974 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
975 W = ufa ? openFaceView(2) : advVelView(2), aP = advVelView(c), uu = CCConst(C[c].u),
976 un = CCConst(old_[c]);
977 const long strd = strideOf(c);
978 const bool pureFou = implicitAdv() && !deferredCorr_;
979 const bool incr = cutcellPressure_ && incremental_, adv = advect_ && !pureFou,
980 bc = hasBc_ && !bcStencilPath();
981 const bool ifou = implicitAdv() && deferredCorr_;
982 const int sch = advScheme_;
983 // Porous advective-form compensation, weighted by the face density (rho_eff = eps*rho): the
984 // eps-weighted ADVECTIVE form eps*rho*(du/dt + u.grad u) IS the conservative volume-averaged
985 // momentum given the enforced continuity (the u*[d(eps)/dt + div(eps u)] bracket vanishes).
986 const bool pc = porous_ && advect_;
987 CCConst dv = CCConst(divAdv_);
988 // rule H (§4.5; §14 H-3c): host pencil, device MDRange; the body is verbatim. Each cell writes
989 // only bb(i), which nothing in the body reads (ccFor3's simd contract).
990 ccFor3(
991 "rhs_var", C3{G, G, G}, C3{e.x - G, e.y - G, e.z - G}, KOKKOS_LAMBDA(int x, int y, int z) {
992 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
993 const double rhoF = 0.5 * (rf(i) + rf(i - strd)); // face density of the velocity unknown
994 double aK = 0.0, aF = 0.0;
995 if (adv) {
996 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{aP, e.x, e.y};
997 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa, ufa)
998 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa, ufa);
999 if (ifou)
1000 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa, ufa);
1001 }
1002 const double gp = !incr ? 0.0
1003 : Grid::collocated ? wc * (0.5 * (P((long)i + strd) - P((long)i - strd)))
1004 : wc * (P(i) - P((long)i - strd));
1005 const double fbF = Grid::atVelocity(fb, i, strd);
1006 const double comp = pc ? rhoF * uu(i) * 0.5 * (dv(i) + dv((long)i - strd)) : 0.0;
1007 bb(i) = rs(i) * (rhoF * idt * un(i) + fc + fbF - rhoF * aK + rhoF * aF + comp - gp) +
1008 (bc ? brhs(i) : -inh(i));
1009 });
1010}
1011
1012template <class Grid>
1014 CCExec space;
1015 const double idt = 1.0 / dt_, fc = f_[c], wc = u_.w[c];
1016 C3 e = e_;
1017 CCField bb = C[c].b, rs = C[c].rscale, P = P_, brhs = bcBrhs_[c], inh = C[c].inhom;
1018 CCConst fb = CCConst(cellForce_[c]);
1019 CCConst rf = CCConst(effRhoField());
1020 CCConst ua = CCConst(uAdv_[c]);
1021 const long strd = strideOf(c);
1022 const bool incr = cutcellPressure_ && incremental_;
1023 const bool bc = hasBc_ && !bcStencilPath();
1024 using MD = MDRange3<CCExec>;
1025 Kokkos::parallel_for(
1026 "rhs_var_mom", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1027 KOKKOS_LAMBDA(int x, int y, int z) {
1028 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1029 const double rhoF = 0.5 * (rf(i) + rf(i - strd));
1030 const double gp = !incr ? 0.0
1031 : Grid::collocated ? wc * (0.5 * (P((long)i + strd) - P((long)i - strd)))
1032 : wc * (P(i) - P((long)i - strd));
1033 const double fbF = Grid::atVelocity(fb, i, strd);
1034 bb(i) = rs(i) * (rhoF * idt * ua(i) + fc + fbF - gp) + (bc ? brhs(i) : -inh(i));
1035 });
1036}
1037
1038// WHY the pressure and every force are HERE, inside the implicit predictor, and never a face
1039// acceleration added after the viscous solve (the Basilisk centered.h "kick" that rung V8/WO-T
1040// used until 2026-09-25): added after A^{-1}, the lagged pressure -dt*G P^n/rho_f lies exactly in
1041// the range the projection removes, so P^n never reaches u (the step is non-incremental Chorin:
1042// steady state scaled by (1 + dt*mu*Lambda)), and the rotational term -kappa*div then acts on it
1043// as an explicit diffusion, P^{n+1} = -4*kappa*dt*S*P^n/rho (-12.0000 measured at (pi,pi,pi)).
1044// Balance at a density jump comes from the operators, not the placement: the pressure force is
1045// rho_c * avg_faces(G_f P / rho_f) (FV face integral, acceleration-continuous face pressure), and
1046// the constraint reads the momentum-weighted face velocity, so M*Gamma = -C^T. Gates:
1047// tests/python/test_collocated_stability_guard.py. Design: doc/collocated_varrho_forces.md.
1048template <class Grid>
1050 // (S5) the face accelerations Phi_c = (f_const + CSF - w (P(j) - P(j-s))) / rho_f on the face
1051 // range [G, e-G], WITHOUT the openness (the reconstruction below applies it once). Reads the
1052 // live P_ (the incremental predictor's P^n; later Picard iterations see the updated P).
1053 ensureFaceAcc();
1054 const bool haveRho = effVarRho();
1055 const bool incr = cutcellPressure_ && incremental_;
1056 const long sc = strideOf(c);
1057 // Unread placeholder when the density is constant (a Kokkos View must still be a live handle).
1058 CCConst rf = CCConst(haveRho ? effRhoField() : C[c].rscale);
1059 buildFaceAccelVar(faceAcc_[c], CCConst(P_), rf, CCConst(C[c].rscale), /*haveFb=*/false, haveRho,
1060 rho_, f_[c], incr, /*scale=*/1.0, sc, e_, G, u_.w[c]);
1061 if (csfActive())
1062 addFaceAccelCsf(faceAcc_[c], CCConst(cField_), CCConst(kappaField_), CCConst(kappaBranch_), rf,
1063 haveRho, rho_, sigmaCsf_, 1.0 / u_.w[c], /*scale=*/1.0, sc, e_, G);
1064 // (S6) the RHS: b = rs [ (rho_c/dt) u^n - rho_c aK + rho_c aF + W f_c + rho_c R(Phi) ] + bc/inh,
1065 // R(Phi) = cellFromFaces(Phi(i), Phi(i+s), o(i), o(i+s)), W = weightSum(o(i), o(i+s)) — the SAME
1066 // two openness reads, so the per-cell force balances the reconstructed pressure force exactly.
1067 CCExec space;
1068 const double idt = 1.0 / dt_, rhoIdtC = rho_ / dt_, rhoK = rho_;
1069 C3 e = e_;
1070 CCField bb = C[c].b, rs = C[c].rscale, brhs = bcBrhs_[c], inh = C[c].inhom;
1071 CCField oax[3] = {ox_, oy_, oz_};
1072 CCConst o = CCConst(oax[c]), phiF = CCConst(faceAcc_[c]);
1073 const bool haveFb = hasCellForce_;
1074 CCConst fb = CCConst(haveFb ? cellForce_[c] : C[c].rscale);
1075 const bool ufa = ufAdvVelocity();
1076 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
1077 W = ufa ? openFaceView(2) : advVelView(2), aP = advVelView(c), un = CCConst(old_[c]);
1078 const bool pureFou = implicitAdv() && !deferredCorr_;
1079 const bool adv = advect_ && !pureFou, bc = hasBc_ && !bcStencilPath();
1080 const bool ifou = implicitAdv() && deferredCorr_;
1081 const int sch = advScheme_;
1082 using MD = MDRange3<CCExec>;
1083 Kokkos::parallel_for(
1084 "rhs_colo_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1085 KOKKOS_LAMBDA(int x, int y, int z) {
1086 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1087 const double rhoC = haveRho ? rf(i) : rhoK;
1088 const double dg = haveRho ? rhoC * idt : rhoIdtC;
1089 double aK = 0.0, aF = 0.0;
1090 if (adv) {
1091 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y}, Fa{aP, e.x, e.y};
1092 aK = (sch == 0) ? Grid::advect_sou(c, x, y, z, Ua, Va, Wa, Fa, ufa)
1093 : Grid::advect(c, x, y, z, Ua, Va, Wa, Fa, ufa);
1094 if (ifou)
1095 aF = Grid::advect_fou(c, x, y, z, Ua, Va, Wa, Fa, ufa);
1096 }
1097 const long j = i + (long)((c == 0) ? 1 : (c == 1) ? e.x : (long)e.x * e.y);
1098 const double oLo = o(i), oHi = o(j);
1099 const double wsum = varrho::weightSum(oLo, oHi);
1100 const double fcell = haveFb ? wsum * fb(i) : 0.0;
1101 const double pf = rhoC * varrho::cellFromFaces(phiF(i), phiF(j), oLo, oHi);
1102 bb(i) =
1103 rs(i) * (dg * un(i) - rhoC * aK + rhoC * aF + fcell + pf) + (bc ? brhs(i) : -inh(i));
1104 });
1105}
1106
1107template <class Grid>
1109 CCExec space;
1110 Kokkos::deep_copy(tgp_, f);
1111 fillGhosts(tgp_);
1112 CCConst src = CCConst(tgp_);
1113 CCConst sd = CCConst(sdf_);
1114 const double eps = rotFilterEps_;
1115 C3 e = e_;
1116 using MD = MDRange3<CCExec>;
1117 Kokkos::parallel_for(
1118 "peclet::flow::rot_filter", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1119 KOKKOS_LAMBDA(int x, int y, int z) {
1120 const long sy = e.x, sz = (long)e.x * e.y;
1121 const long i = (long)x + (long)y * sy + (long)z * sz;
1122 const long sa = (axis == 0) ? 1 : (axis == 1) ? sy : sz;
1123 if (sd(i) < 0.0)
1124 return;
1125 const bool am = sd(i - sa) >= 0.0, ap = sd(i + sa) >= 0.0;
1126 double sm;
1127 if (am && ap)
1128 sm = 0.25 * (src(i - sa) + 2.0 * src(i) + src(i + sa));
1129 else if (ap)
1130 sm = 0.5 * (src(i) + src(i + sa));
1131 else if (am)
1132 sm = 0.5 * (src(i) + src(i - sa));
1133 else
1134 sm = src(i);
1135 // eps-floor blend: S' = eps*I + (1-eps)*S. A pure S has an exact checkerboard null
1136 // space, which at dt -> infinity (where the (rho/dt)*phi term vanishes) degenerates the
1137 // fixed point into a frozen-checkerboard family; the floor keeps S' > 0 so
1138 // (rho/dt + mu S'A) phi = 0 forces phi = 0 at EVERY dt, while still cutting the
1139 // dangerous mode's feedback gain by ~1/eps.
1140 f(i) = eps * src(i) + (1.0 - eps) * sm;
1141 });
1142}
1143
1144template <class Grid>
1147 if (!coeffBuiltThisStep_) // the balanced-force stage (B1) already built them this step
1149 projectSolve();
1152}
1153
1154template <class Grid>
1156 // ghosts incl. domain BCs (outflow zero-gradient) BEFORE the divergence -- matches CUDA
1157 // apply_velocity_bc before diverg_open, so div(u*) counts the outflow flux (else the rotational
1158 // pressure pumps the mis-counted outflow divergence and blows up the outflow-wall corner).
1159 if constexpr (Grid::collocated) {
1160 // Approximate (MAC) projection: average the cell velocities onto a face field, then project
1161 // THAT. Use the BC-aware ghost fill (periodic / cross-rank + domain BCs) so the averaged
1162 // inflow/outflow faces carry the right value -- at open boundaries the flux is counted
1163 // (closed walls are openness 0).
1164 for (int c = 0; c < 3; ++c)
1165 fillVelGhosts(c, 0);
1166 if (colocatedFaceForce()) {
1167 // Rung V8 (S8.1): the constraint's face field is the MOMENTUM-weighted map Pi_rho u*, the
1168 // adjoint partner of the predictor's pressure force (collocated_varrho.hpp; §4.3). No face
1169 // acceleration: every force already went through the implicit predictor.
1170 if (effVarRho())
1171 centerToFaceMassWeighted(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u),
1172 CCConst(effRhoField()), e_, G);
1173 else
1174 centerToFace(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), e_, G);
1175 } else if (faceInterp_ == 5 || faceInterp_ == 7) // wall-aware flux map at solid (embed 5/7)
1176 centerToFaceWallAware(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u),
1177 CCConst(sdf_), CCConst(xcx_), CCConst(xcy_), CCConst(xcz_), true, e_,
1178 G);
1179 else
1180 centerToFace(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), e_, G);
1181 faceFieldValid_ = true; // ISSUES sweep item 5
1182 if (ghostProjection_) {
1183 // Collocated ghost divergence: the SAME binary-openness + closure-delta pair as the
1184 // staggered path, applied to the 1/2-1/2 face-averaged field (the closures only ever read
1185 // faces whose two adjacent centers are fluid, so the masked solid-cell zeros never enter
1186 // except at EXPLICIT slivers — faithfully modelled in the a-priori study).
1187 if (gpNRows_ < 0)
1188 throw std::runtime_error("ghost projection: call set_solid after set_ghost_projection");
1189 if (porous_ || varRho_ || useChebyshev_)
1190 throw std::runtime_error(
1191 "ghost projection: porous/variable-rho/Chebyshev unsupported (v1)");
1192 divergOpen(CCConst(uf_), CCConst(vf_), CCConst(wf_), CCConst(oxb_), CCConst(oyb_),
1193 CCConst(ozb_), div_, e_, G);
1194 gpDivergDelta(div_, CCConst(uf_), CCConst(vf_), CCConst(wf_), gpOv_, gpNRows_,
1195 C3{nx_, ny_, nz_}, e_, G, distributed_);
1196 } else
1197 constraintDivergence(CCConst(uf_), CCConst(vf_), CCConst(wf_), div_);
1198 } else {
1199 for (int c = 0; c < 3; ++c)
1200 fillVelGhosts(c, 0);
1201 if (porous_) { // volume-averaged continuity: div(open*eps*u*), constraint div(eps
1202 // u)=-d(eps)/dt
1203 fillPorousEpsGhosts(); // BEFORE the divergence — one eps ghost policy for RHS,
1204 // coefficients and residual (the deposit rewrites these ghosts
1205 // every step)
1206 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
1207 CCConst(oz_), CCConst(epsField_), div_, e_, G);
1208 } else if (ghostProjection_) {
1209 // Directional ghost-cell divergence: binary-openness face differences (COUPLED faces)
1210 // plus the wall-anchored closures at ghost faces, row-rescaled — the SAME kernel pair
1211 // serves the RHS here and the diagnostic in maxOpenDivergence (diagnostic == residual).
1212 if (gpNRows_ < 0)
1213 throw std::runtime_error("ghost projection: call set_solid after set_ghost_projection");
1214 if (porous_ || varRho_ || useChebyshev_)
1215 throw std::runtime_error(
1216 "ghost projection: porous/variable-rho/Chebyshev unsupported (v1)");
1217 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(oxb_), CCConst(oyb_),
1218 CCConst(ozb_), div_, e_, G);
1219 gpDivergDelta(div_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), gpOv_, gpNRows_,
1220 C3{nx_, ny_, nz_}, e_, G, distributed_);
1221 } else
1222 constraintDivergence(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), div_);
1223 }
1224 // MOVING GEOMETRY (rung 3): the wall's own volume flux. Inert without a moving instance.
1226 // WO-P01: the phase-change (and any prescribed) divergence source, so the deflated solve
1227 // delivers div(open u) = S instead of 0. Inert unless enable_phase_change /
1228 // set_divergence_source ran.
1230 // Porous continuity source: fold d(eps)/dt into the divergence so the Poisson solves for
1231 // div(eps u) = -d(eps)/dt (not 0). d(eps)/dt = (eps^{n+1}-eps^n)/dt from the deposited void
1232 // fraction; stored in depsdt_ (epsPrev_ is overwritten at step end, so the residual reuses
1233 // this).
1234 if (porous_) {
1235 CCExec space;
1236 C3 e = e_; // local copy — a KOKKOS_LAMBDA capturing e_ would read this-> on the device
1237 CCField d = div_, dd = depsdt_, ep = epsField_, epp = epsPrev_;
1238 const double idt = 1.0 / dt_;
1239 const bool useDt = porousDepsDt_;
1240 using MD = MDRange3<CCExec>;
1241 Kokkos::parallel_for(
1242 "peclet::flow::deps_dt", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1243 KOKKOS_LAMBDA(int x, int y, int z) {
1244 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
1245 dd(i) = (ep(i) - epp(i)) * idt;
1246 if (useDt)
1247 d(i) += dd(i); // off -> solve div(eps u)=0 (drop the noisy time-derivative source)
1248 });
1249 }
1250 // bridge -div(u*) (g=2 block) -> the MG rhs (g=1 block); keep div(u*) in div_ for the pressure
1251 // update
1252 copyInner(rhs1_, e1_, 1, CCConst(div_), e_, G);
1253 {
1254 CCExec space;
1255 CCField r = rhs1_;
1256 Kokkos::parallel_for(
1257 "negdiv", Kokkos::RangePolicy<CCExec>(space, 0, n1_),
1258 KOKKOS_LAMBDA(std::size_t i) { r(i) = -r(i); });
1259 }
1260 if (fluidOnlyMode_ == 2) {
1261 // Design B: solid rows carry no constraint -- mask their rhs (their operator rows are empty
1262 // in the filtered 7-point part; the star overlay never adds to them), so phi_s stays 0.
1263 if (useChebyshev_)
1264 throw std::runtime_error("set_fluid_only_constraint(2): Chebyshev unsupported (v1)");
1265 CCExec space;
1266 CCField r = rhs1_;
1267 CCConst sd = CCConst(sdf_);
1268 const C3 e1 = e1_, e2 = e_;
1269 Kokkos::parallel_for(
1270 "peclet::flow::star_mask_rhs", MDRange3<CCExec>(space, {0, 0, 0}, {nx_, ny_, nz_}),
1271 KOKKOS_LAMBDA(int x, int y, int z) {
1272 const long i2 = (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
1273 if (sd(i2) < 0.0)
1274 r((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) = 0.0;
1275 });
1276 }
1277}
1278
1279template <class Grid>
1281 // Variable density: rebuild the Poisson operator with the face coefficients
1282 // c_f = open_f * rho0/rho_f (rho0 = the scalar rho_, so uniform rho == rho_ reduces exactly to
1283 // the openness operator). The coefficient fields ride the openness rails: bridge rho to the g=1
1284 // block INCLUDING its ghost ring, form the coefficients on the inner cells, and hand them to
1285 // setOpenness, whose per-level ghost fill + boundary re-imposition + coarsening (rediscretized
1286 // averaging) treat them exactly like openness. Rebuilt every step (rho may be closure/transport
1287 // driven); Chebyshev bounds are invalidated (stale bounds under changing coefficients diverge
1288 // silently — PCG is the recommended/default driver here).
1289 if (varRho_) {
1290 fillPropGhosts(rhoField_);
1291 copyBlockShifted(rho1_, e1_, CCConst(rhoField_), e_, G - 1);
1292 // Face mean of rho: ARITHMETIC by default (the hydrostatic-exactness + series-mobility
1293 // choice); the harmonic sibling is the opt-in WO-J knob and must be paired with the matching
1294 // correction in projectVelocities or the projection stops being exact (mac_pressure.hpp).
1295 if (rhoFaceHarmonic_)
1296 buildRhoCoeffHarm(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1297 CCConst(rho1_), rho_, e1_, 1);
1298 else
1299 buildRhoCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), CCConst(rho1_),
1300 rho_, e1_, 1);
1301 // WO-R2 item 1: buildRhoCoeff covers the LOW domain face of each axis (an inner index) but
1302 // not the HIGH one (a ghost index). Fill the high outflow planes here, and tell the MG to
1303 // carry the caller's coefficient at every Dirichlet domain face instead of re-imposing the
1304 // literal openness 1.0 (which used to overwrite `rho0/rho_f` with 1 on BOTH sides and made
1305 // the low-side outlet inconsistent with projectCorrectVar by the full density ratio).
1306 {
1307 CCField cc[3] = {cx1_, cy1_, cz1_};
1308 CCField oo[3] = {ox1_, oy1_, oz1_};
1309 for (int a = 0; a < 3; ++a)
1310 if (bc_[2 * a + 1] == 3 && touchesGlobalFace(2 * a + 1))
1311 buildRhoCoeffOutflowFace(cc[a], CCConst(oo[a]), CCConst(rho1_), rho_, e1_, 1, a,
1312 rhoFaceHarmonic_);
1313 }
1314 mg_.setBoundaryConditions(bc_);
1315 mg_.setOutflowCoefficient(hasOutflow_ && outflowOpCoeff_);
1316 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), u_.w[0], u_.w[1], u_.w[2]);
1317 mg_.setOutflowCoefficient(false);
1318 // The spectrum changed with the coefficients: re-estimated by the solve, warm-started from
1319 // the previous estimate's iterates when the bounds being dropped were valid (D3, §5.13).
1320 chebWarmOk_ = chebWarmOk_ || chebBoundsSet_;
1321 chebBoundsSet_ = false;
1322 }
1323 // Porous continuity: the Poisson operator is eps-weighted (c_f = open_f * eps_f), same rails as
1324 // the density coefficient above. Rebuilt every step (eps moves with the particles). With
1325 // implicit CFD-DEM drag the coefficient AND the correction carry the drag-relaxation w_f =
1326 // idt/(idt+beta_f) (idt = rho/dt) so the pressure correction is consistent with the drag-loaded
1327 // momentum diagonal A_P = idt+beta (SIMPLE/PISO-with-implicit-drag; stiff drag -> w_f->0 -> the
1328 // drag holds the velocity, stable). beta==0 reduces exactly to the plain eps-weighted operator.
1329 if (porous_) {
1330 // eps ghosts were filled by fillPorousEpsGhosts() before the divergence above — the SAME
1331 // ghost values must feed the coefficient bridge (face eps == 1 at open domain faces), or the
1332 // operator and the RHS disagree at the boundary rows.
1333 copyBlockShifted(eps1_, e1_, CCConst(epsField_), e_, G - 1);
1334 if (hasDrag_) {
1335 fillPropGhosts(dragBeta_);
1336 copyBlockShifted(beta1_, e1_, CCConst(dragBeta_), e_, G - 1);
1337 } else if (porousCons_) {
1338 Kokkos::deep_copy(beta1_, 0.0); // conservative kernels read beta unconditionally when used
1339 }
1340 if (porousCons_) {
1341 // eps-CONSERVATIVE pair: c_f = open * (eps_f rho idt)/(eps_f rho idt + beta_f), matching
1342 // the eps-weighted momentum diagonal; the eps of the flux cancels the eps of the inertia
1343 // (see mac_pressure.hpp). Correction: projectCorrectPorousCons below.
1344 buildPorousCoeffCons(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1345 CCConst(eps1_), CCConst(beta1_), hasDrag_, rho_ / dt_, e1_, 1);
1346 } else if (hasDrag_) {
1347 buildPorousCoeffDrag(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1348 CCConst(eps1_), CCConst(beta1_), rho_ / dt_, e1_, 1);
1349 } else {
1350 buildPorousCoeff(cx1_, cy1_, cz1_, CCConst(ox1_), CCConst(oy1_), CCConst(oz1_),
1351 CCConst(eps1_), e1_, 1);
1352 }
1353 mg_.setBoundaryConditions(bc_);
1354 // SCALING_ISSUES #3: the widened porous builders above wrote the HIGH Dirichlet face plane
1355 // too, so the outlet row carries open_f*eps_f (and w_f) exactly like every inner face,
1356 // instead of the literal openness 1.0.
1357 mg_.setOutflowCoefficient(hasOutflow_ && outflowOpCoeff_);
1358 mg_.setOpenness(CCConst(cx1_), CCConst(cy1_), CCConst(cz1_), u_.w[0], u_.w[1], u_.w[2]);
1359 mg_.setOutflowCoefficient(false);
1360 chebWarmOk_ = chebWarmOk_ || chebBoundsSet_; // a coefficient rebuild, as above (D3)
1361 chebBoundsSet_ = false;
1362 }
1363}
1364
1365template <class Grid>
1367 // geometric multigrid solve of the cut-cell pressure Poisson A phi = -div(u*) (CUDA
1368 // mac_multigrid): MG-PCG by default, or the communication-light Chebyshev driver (bounds
1369 // estimated once, then reused). Warm start (CUDA pwarm_): keep the previous step's phi1_ as the
1370 // initial guess instead of zeroing.
1371 if (!pwarm_)
1372 Kokkos::deep_copy(phi1_, 0.0);
1373 lastPressureIters_ = solvePressureSystem(rhs1_, phi1_);
1374 // ISSUES sweep item 6: a solve that gave up on a non-finite recurrence scalar reports the
1375 // iteration CAP (see CutcellMG::solvePCG) and raises this flag, so `pressure_solve_failed()`
1376 // and the usual rule-3b "no capped solve" check both catch it. It used to print one line to
1377 // stdout, silently zero the correction and report 0 iterations.
1378 lastPressureFailed_ = mg_.lastSolveFailed();
1379 if (fluidOnlyMode_ == 2) {
1380 // Pin phi at solid-centered cells to 0 (their rows are unconstrained; the smoother must not
1381 // leave garbage there -- projectCorrect reads phi_s at fluid|solid faces and the
1382 // starCorrectFaces fix-up assumes the applied value was exactly 0).
1383 CCExec space;
1384 CCField ph = phi1_;
1385 CCConst sd = CCConst(sdf_);
1386 const C3 e1 = e1_, e2 = e_;
1387 Kokkos::parallel_for(
1388 "peclet::flow::star_pin_solid", MDRange3<CCExec>(space, {0, 0, 0}, {nx_, ny_, nz_}),
1389 KOKKOS_LAMBDA(int x, int y, int z) {
1390 const long i2 = (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
1391 if (sd(i2) < 0.0)
1392 ph((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) = 0.0;
1393 });
1394 }
1395 copyInner(phi_, e_, G, CCConst(phi1_), e1_, 1); // bridge phi back g=1 -> g=2
1396 fillGhosts(phi_);
1397 if (hasOutflow_) { // hold phi=0 at the outflow ghost so grad(phi) drives the outflow face
1398 // (Dirichlet p=0)
1399 B3 e{e_.x, e_.y, e_.z};
1400 for (int a = 0; a < 3; ++a)
1401 for (int s = 0; s < 2; ++s)
1402 if (bc_[2 * a + s] == 3 && touchesGlobalFace(2 * a + s))
1403 bcZeroPressureGhost(phi_, e, G, a, s);
1404 }
1405}
1406
1407template <class Grid>
1409 long iters = 0;
1410 if (useChebyshev_) {
1411 // D3 (doc/vof_step_performance_design.md §5.13): after a coefficient rebuild the bounds are
1412 // re-estimated from the previous estimate's last v_max / v_min iterates with 5 + 5 power
1413 // iterations instead of 15 + 15 from the right-hand side. Guard: a solve on such bounds that
1414 // reaches the cap, or whose residual after 3 iterations exceeds r0, is redone from its
1415 // starting iterate on cold bounds (counted in chebColdRestarts_). Every other estimate --
1416 // the first, and the one after a structural change (a driver/density/porous setter, a
1417 // hierarchy rebuild) -- is the cold one, unchanged.
1418 using ES = typename CutcellMG::EigenStart;
1419 bool warm = false;
1420 if (!chebBoundsSet_) {
1421 warm = chebWarmOk_ && mg_.eigenWarmReady();
1422 mg_.estimateEigenvalues(CCConst(rhs1), chebA_, chebB_, warm ? 5 : 15, 2, 2, 12,
1423 warm ? ES::Warm : ES::ColdKeep);
1424 if (warm && !mg_.eigenWarmReady()) { // the warm estimate came out degenerate: cold one
1425 warm = false;
1426 mg_.estimateEigenvalues(CCConst(rhs1), chebA_, chebB_, 15, 2, 2, 12, ES::ColdKeep);
1427 }
1428 chebBoundsSet_ = true;
1429 }
1430 if (!warm) {
1431 iters = mg_.solveChebyshev(rhs1, x1, chebMaxit_, chebRtol_, 2, 2, 12, chebA_, chebB_);
1432 } else {
1433 if (pwarm_) { // the starting iterate is the previous phi: keep it for a redo
1434 if (chebX0_.extent(0) != x1.extent(0))
1435 chebX0_ = CCField("chebX0", x1.extent(0));
1436 Kokkos::deep_copy(chebX0_, x1);
1437 }
1438 iters = mg_.solveChebyshev(rhs1, x1, chebMaxit_, chebRtol_, 2, 2, 12, chebA_, chebB_, true);
1439 if (iters >= chebMaxit_ || mg_.chebyshevGuardTripped()) {
1440 ++chebColdRestarts_;
1441 mg_.estimateEigenvalues(CCConst(rhs1), chebA_, chebB_, 15, 2, 2, 12, ES::ColdKeep);
1442 if (pwarm_)
1443 Kokkos::deep_copy(x1, chebX0_);
1444 else
1445 Kokkos::deep_copy(x1, 0.0);
1446 iters += mg_.solveChebyshev(rhs1, x1, chebMaxit_, chebRtol_, 2, 2, 12, chebA_, chebB_);
1447 }
1448 }
1449 } else if (ghostProjection_) {
1450 // Nonsymmetric ghost-projection operator (both grids — the phi matrix is identical):
1451 // BiCGStab, preconditioned by the symmetric binary-openness V-cycle (the hierarchy set up
1452 // in setSolid); the overlay delta enters the fine-level matvec only. Distributed, the
1453 // matvec stages the iterate on this solver's g=2 block (gpX2_) whose halo carries the
1454 // overlay's +/-2 reach.
1455#ifdef PECLET_FLOW_MPI
1456 if (distributed_)
1457 iters =
1458 mg_.solveBiCGStab(rhs1, x1, r_, gpRh_, pp_, Ap_, gpT_, z_, gpZ2_, pcgMaxit_, pcgRtol_, 2,
1459 2, 12, gpOv_, gpNRows_, C3{nx_, ny_, nz_}, gpX2_, velDev_.get(), e_);
1460 else
1461#endif
1462 iters = mg_.solveBiCGStab(rhs1, x1, r_, gpRh_, pp_, Ap_, gpT_, z_, gpZ2_, pcgMaxit_, pcgRtol_,
1463 2, 2, 12, gpOv_, gpNRows_, C3{nx_, ny_, nz_});
1464 // Pin the FREE variables of the binary-openness operator to their design value phi = 0:
1465 // solid-centered (sdfGp < 0) cells and fully-BC_ONLY overlay rows have a zero row AND zero
1466 // rhs, so the Krylov iteration leaves an arbitrary (V-cycle-prolongation, iteration-path,
1467 // and decomposition dependent) value there — invisible to the gp residual (the closures
1468 // never read those faces) but INJECTED into real near-wall fluid velocities by the plain
1469 // projectCorrect face gradient. The doc contract of ghost_projection.hpp is "decoupled
1470 // rows hold phi = 0"; enforce it (measured: without this, np=2 runs differ from the
1471 // reference by ~1e-2 relative u at sphere-surface faces while agreeing 1e-13 elsewhere).
1472 {
1473 CCExec space;
1474 CCField ph = x1;
1475 CCConst sg = CCConst(sdfGp_);
1476 auto idMap = gpIdMap_;
1477 auto ov = gpOv_;
1478 const C3 e1 = e1_, e2 = e_;
1479 const int lnx = nx_, lny = ny_;
1480 Kokkos::parallel_for(
1481 "peclet::flow::gp_pin_decoupled", MDRange3<CCExec>(space, {0, 0, 0}, {nx_, ny_, nz_}),
1482 KOKKOS_LAMBDA(int x, int y, int z) {
1483 const long i1 =
1484 (long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y;
1485 const long i2 =
1486 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
1487 bool dec = sg(i2) < 0.0;
1488 if (!dec) {
1489 const int s = idMap((long)x + (long)y * lnx + (long)z * (long)lnx * lny);
1490 if (s >= 0 && ov.coupled(s) == 0)
1491 dec = true;
1492 }
1493 if (dec)
1494 ph(i1) = 0.0;
1495 });
1496 }
1497 } else if (useFcg_) {
1498 // Flexible CG (set_pressure_fcg): identical to the MG-PCG branch below but for the
1499 // Polak-Ribiere beta, which tolerates a V-cycle preconditioner that is not symmetric w.r.t.
1500 // the fine operator. Its one extra vector is allocated on first use, so an unselected FCG
1501 // costs nothing (not even memory).
1502 if (zp1_.extent(0) != n1_)
1503 zp1_ = CCField("zp1", n1_);
1504 iters = mg_.solveFCG(rhs1, x1, r_, pp_, z_, zp1_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12,
1505 fluidOnlyMode_ == 2 ? &starOv_ : nullptr, nStar_, C3{nx_, ny_, nz_});
1506 } else {
1507 iters = mg_.solvePCG(rhs1, x1, r_, pp_, z_, Ap_, pcgMaxit_, pcgRtol_, 2, 2, 12,
1508 fluidOnlyMode_ == 2 ? &starOv_ : nullptr, nStar_, C3{nx_, ny_, nz_});
1509 }
1510 return iters;
1511}
1512
1513template <class Grid>
1515 divergOpen(fx, fy, fz, CCConst(ox_), CCConst(oy_), CCConst(oz_), out, e_, G);
1516}
1517
1518template <class Grid>
1520 if constexpr (Grid::collocated) {
1521 // phi: zero-gradient (Neumann) at non-periodic walls so the cell-centered central-difference
1522 // correction carries no spurious normal acceleration through the wall (the periodic fill
1523 // wrapped the opposite boundary's phi). Outflow (Dirichlet p=0) is handled by hasOutflow_
1524 // above (phase 5b).
1525 if (hasBc_) {
1526 B3 e{e_.x, e_.y, e_.z};
1527 for (int a = 0; a < 3; ++a)
1528 for (int s = 0; s < 2; ++s) {
1529 const int t = bc_[2 * a + s];
1530 if (t != 0 && t != 3 && touchesGlobalFace(2 * a + s))
1531 bcNeumannGhost(phi_, e, G, a, s);
1532 }
1533 }
1534 // Correct the face field (-> discretely divergence-free; transient this step) and the cell
1535 // field (central-difference cell gradient).
1536 // rung V8: per-face 1/rho_f on the gradient, matching the operator coefficient
1537 // c_f = o_f*rho0/rho_f — the SAME exact-adjoint face correction the staggered path uses.
1538 // Phase 2 C2 (doc/anisotropic_metric.md §3): the per-axis weight w_a multiplies the whole
1539 // existing correction, matching the w_a on the operator row of that axis. 1.0 isotropic.
1540 if (varRho_)
1541 projectCorrectVar(uf_, vf_, wf_, CCConst(phi_), CCConst(rhoField_), rho_, e_, G, u_.w[0],
1542 u_.w[1], u_.w[2]);
1543 else
1544 projectCorrect(uf_, vf_, wf_, CCConst(phi_), e_, G, u_.w[0], u_.w[1], u_.w[2]);
1545 if (fluidOnlyMode_ == 2) // Design B: replace the solid side's phi=0 by phibar_s at
1546 starCorrectFaces(uf_, vf_, wf_, CCConst(phi_), starOv_, nStar_, // fluid|solid faces
1547 C3{nx_, ny_, nz_}, e_, G, e_, G, exactResidual_, u_.w[0], u_.w[1], u_.w[2]);
1548 fillGhosts(uf_);
1549 fillGhosts(vf_);
1550 fillGhosts(wf_); // complete the divergence-free face field (boundary faces)
1551 if (hasOutflow_) { // correct the high-side outflow face on the face field so mass leaves
1552 // (phi=0 there)
1553 B3 e{e_.x, e_.y, e_.z};
1554 CCField fa[3] = {uf_, vf_, wf_};
1555 // WO-R2 item 1: same rule as the staggered path below — with the operator's outflow row
1556 // carrying rho0/rho_f (setOutflowCoefficient) the consistent correction carries it too.
1557 const bool var = varRho_ && !porous_ && outflowRhoCorr_;
1558 for (int a = 0; a < 3; ++a)
1559 if (bc_[2 * a + 1] == 3 && touchesGlobalFace(2 * a + 1)) {
1560 if (var)
1561 bcCorrectOutflowVar(fa[a], phi_, rhoField_, rho_, e, G, a, rhoFaceHarmonic_, u_.w[a]);
1562 else
1563 bcCorrectOutflow(fa[a], phi_, e, G, a, u_.w[a]);
1564 }
1565 }
1566 // The face field is now final for this step. Derive the OPEN-boundary view the next
1567 // predictor's advection and the divergence diagnostic read (buildOpenFaceField explains why
1568 // the outflow face cannot be the mass-balance closure there); a no-op without an outflow.
1570 if (colocatedFaceForce()) {
1571 // Rung V8 (S8.5): the cell correction is the predictor's face-to-cell reconstruction applied
1572 // to the face corrections k(j) = w (rho0/rho_f)(phi(j) - phi(j-s)) — the transpose of the
1573 // constraint, so the pair stays adjoint (collocated_varrho.hpp). Replaces the whole
1574 // constant-density cell-correction chain below.
1575 CCField oax[3] = {ox_, oy_, oz_};
1576 for (int cc = 0; cc < 3; ++cc)
1577 correctCellFaceAverageVar(C[cc].u, CCConst(phi_),
1578 CCConst(varRho_ ? rhoField_ : C[cc].rscale), CCConst(oax[cc]),
1579 varRho_, rho_, strideOf(cc), e_, G, u_.w[cc]);
1580 } else if (ghostProjection_ || faceInterp_ == 9) {
1581 // Ghost cell correction (also the gauge-exact scheme): the directional gpCenterGrad
1582 // gradient of phi — 2nd-order one-sided at cut cells, never reads a decoupled
1583 // (solid/pocket) phi. The same operator supplies the momentum's -grad(P^n) predictor
1584 // (buildRhs), so the pressure force the momentum feels and the correction stay one
1585 // operator family.
1586 for (int cc = 0; cc < 3; ++cc) {
1587 gpCenterGrad(tgp_, CCConst(phi_), CCConst(ghostProjection_ ? sdfGp_ : sdf_), cc, e_, G,
1588 u_.w[cc]);
1589 subtractField(C[cc].u, CCConst(tgp_), e_, G);
1590 }
1591 } else if (faceInterp_ == 5) { // embed 5: cell correction = the TRANSPOSE of the
1592 // wall-aware map, keeping (T, Tᵀ) an adjoint pair (transposeGradWallAware)
1593 CCField xcs[3] = {xcx_, xcy_, xcz_};
1594 CCField oax[3] = {ox_, oy_, oz_};
1595 for (int cc = 0; cc < 3; ++cc) {
1596 transposeGradWallAware(tgp_, CCConst(phi_), CCConst(sdf_), CCConst(oax[cc]),
1597 CCConst(xcs[cc]), true, cc, e_, G, u_.w[cc]);
1598 subtractField(C[cc].u, CCConst(tgp_), e_, G);
1599 }
1600 } else if (faceInterp_ == 6 || faceInterp_ == 7) { // embed: openness-WEIGHTED cell correction
1601 // (full open-face pressure force at cut cells) — Basilisk centered_grad
1602 projectCorrectCenterOpen(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
1603 CCConst(oz_), e_, G, u_.w[0], u_.w[1], u_.w[2]);
1604 } else {
1605 projectCorrectCenter(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(ox_), CCConst(oy_),
1606 CCConst(oz_), e_, G, u_.w[0], u_.w[1], u_.w[2]);
1607 }
1608 } else {
1609 if (porous_ && porousCons_) // eps-conservative gradient rho*idt/(eps_f rho idt + beta_f),
1610 // matching buildPorousCoeffCons (see mac_pressure.hpp)
1611 projectCorrectPorousCons(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(epsField_),
1612 CCConst(dragBeta_), hasDrag_, rho_ / dt_, e_, G, u_.w[0], u_.w[1],
1613 u_.w[2]);
1614 else if (porous_ &&
1615 hasDrag_) // drag-relaxed gradient w_f=idt/(idt+beta_f), matching buildPorousCoeffDrag
1616 projectCorrectPorousDrag(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(dragBeta_),
1617 rho_ / dt_, e_, G, u_.w[0], u_.w[1], u_.w[2]);
1618 else if (varRho_ && rhoFaceHarmonic_) // the WO-J harmonic knob: coefficient AND correction
1619 projectCorrectVarHarm(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(rhoField_), rho_, e_, G,
1620 u_.w[0], u_.w[1], u_.w[2]);
1621 else if (varRho_) // per-face 1/rho on the gradient, matching the operator coefficient
1622 projectCorrectVar(C[0].u, C[1].u, C[2].u, CCConst(phi_), CCConst(rhoField_), rho_, e_, G,
1623 u_.w[0], u_.w[1], u_.w[2]);
1624 else
1625 projectCorrect(C[0].u, C[1].u, C[2].u, CCConst(phi_), e_, G, u_.w[0], u_.w[1], u_.w[2]);
1626 if (hasOutflow_) { // correct the high-side outflow normal face that projectCorrect misses
1627 // (mass leaves)
1628 B3 e{e_.x, e_.y, e_.z};
1629 // WO-R item 4: with variable density every OTHER corrected face carries the mobility
1630 // factor rho0/rho_f (projectCorrectVar) and this one did not — a ratio-sized error on the
1631 // outflow face, invisible at constant density (rho_f == rho0) which is why the channel/BFS
1632 // validations never saw it. The porous branches keep the plain correction: their
1633 // coefficient is the drag/eps relaxation, not 1/rho, and a two-phase porous outlet is not
1634 // this rung. `!varRho_` is byte-identical.
1635 // `set_outflow_rho_correction(False)` drops the 1/rho_f
1636 // factor. DEFAULT ON since WO-R2 fixed the operator's outflow-face coefficient — see
1637 // setOutflowRhoCorrection for the before/after table and the mechanism.
1638 const bool var = varRho_ && !porous_ && outflowRhoCorr_;
1639 for (int a = 0; a < 3; ++a)
1640 if (bc_[2 * a + 1] == 3 && touchesGlobalFace(2 * a + 1)) {
1641 if (var)
1642 bcCorrectOutflowVar(C[a].u, phi_, rhoField_, rho_, e, G, a, rhoFaceHarmonic_, u_.w[a]);
1643 else
1644 bcCorrectOutflow(C[a].u, phi_, e, G, a, u_.w[a]);
1645 }
1646 outflowCorrValid_ = true; // the outflow face now carries the mass that leaves
1647 }
1648 }
1649 // the grad(phi) correction also touches solid faces; re-impose no-slip there so the decoupled
1650 // solid velocity cannot accumulate (matches the CUDA apply_mask/mask_k after correct_k ->
1651 // stability).
1652 for (int c = 0; c < 3; ++c)
1653 maskVelocity(c);
1654}
1655
1656template <class Grid>
1658 // Rotational incremental pressure (Timmermans), matching CUDA press_update_k: P += (rho/dt)*phi
1659 // - mu*div(u*). Classical non-incremental Chorin (!incremental_) skips the accumulation;
1660 // getPressure() derives p from phi.
1661 if (incremental_) {
1662 if (rotFilter_ && rotationalP_)
1663 for (int a = 0; a < 3; ++a)
1664 filterCellField(div_, a); // S(div u*): see setRotationalFilter
1665 CCExec space;
1666 CCField P = P_, ph = phi_, d = div_;
1667 // Pressure under-relaxation (MFIX §10.1): accumulate only omega_p of the increment into the
1668 // physical pressure P (the velocity correction still uses the full phi to satisfy
1669 // continuity), so the next step's incremental predictor -grad(P^n) can't overshoot for a
1670 // stiff drag diagonal. omega_p=1 (default) is the current behaviour; <1 only stabilizes the
1671 // porous+drag path.
1672 const double ct = pressUnderRelax_ * rho_ / dt_, mu = rotationalP_ ? rotWeight_ * mu_ : 0.0;
1673 if (varProps_) {
1674 // Variable viscosity: the pointwise Timmermans term -mu(i)*div(u*) is inconsistent for
1675 // heterogeneous mu (see setVariableRotational). Default = constant coefficient chi*mu_min
1676 // (stable by domination, exact fallback to the uniform-mu scheme); "full" = pointwise
1677 // (mild contrast only); "off" = plain incremental.
1678 if (varRotMode_ == 1) {
1679 CCConst mf = CCConst(muField_);
1680 const double chi = varRotChi_;
1681 Kokkos::parallel_for(
1682 "press_var_full", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1683 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - chi * mf(i) * d(i); });
1684 } else {
1685 const double muRot = (varRotMode_ == 2) ? 0.0 : varRotChi_ * minMuInner();
1686 Kokkos::parallel_for(
1687 "press_var_min", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1688 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - muRot * d(i); });
1689 }
1690 } else if (rotWallW_ > 0.0 && rotationalP_) {
1691 // Frank's wall-banded blend (setRotationalWallWeight): at fluid cells with a solid
1692 // axis-neighbour (the rows whose one-sided gpCenterGrad makes the cell-centered
1693 // rotational update marginally unstable) use
1694 // P += (rho/dt + w*mu/dx^2)*phi - (1-w)*mu*div(u*)
1695 // (dx = 1 in cell units): the (1-w) shrinks the destabilizing off-diagonal there and the
1696 // diagonal w*mu gain keeps those rows relaxing at dt -> infinity where rho/dt vanishes.
1697 // Bulk cells (w = 0) keep the full-speed rotational update. Outer-shell cells keep the
1698 // bulk formula (their P is ghost/overwritten).
1699 CCConst sd = CCConst(sdf_);
1700 const double w0 = rotWallW_, muF = rotWeight_ * mu_;
1701 C3 e = e_;
1702 Kokkos::parallel_for(
1703 "press_wallblend", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1704 KOKKOS_LAMBDA(std::size_t i) {
1705 const long sy = e.x, sz = (long)e.x * e.y;
1706 const int x = (int)(i % e.x), y = (int)((i / e.x) % e.y), z = (int)(i / sz);
1707 double w = 0.0;
1708 if (x > 0 && y > 0 && z > 0 && x < e.x - 1 && y < e.y - 1 && z < e.z - 1 &&
1709 sd(i) >= 0.0 &&
1710 (sd(i - 1) < 0.0 || sd(i + 1) < 0.0 || sd(i - sy) < 0.0 || sd(i + sy) < 0.0 ||
1711 sd(i - sz) < 0.0 || sd(i + sz) < 0.0))
1712 w = w0;
1713 P(i) += (ct + w * muF) * ph(i) - (1.0 - w) * muF * d(i);
1714 });
1715 } else {
1716 Kokkos::parallel_for(
1717 "press", Kokkos::RangePolicy<CCExec>(space, 0, n_),
1718 KOKKOS_LAMBDA(std::size_t i) { P(i) += ct * ph(i) - mu * d(i); });
1719 }
1720 }
1721 // Snapshot eps^{n+1} -> epsPrev_ for the next step's d(eps)/dt (this projection consumed it).
1722 if (porous_)
1723 Kokkos::deep_copy(epsPrev_, epsField_);
1724}
1725
1726} // namespace peclet::flow
1727
1728#endif // PECLET_FLOW_FLOW_IBM_PROJECT_HPP
int solveBiCGStab(CCField b, CCField x, CCField r, CCField rh, CCField p, CCField v, CCField t, CCField z, CCField z2, int maxit, double rtol, int pre, int post, int bottom, const GpOverlayReal< MReal > &ov, int nOv, C3 nn)
double residualNorm(CCField b, CCField x, CCField r, CCField Ap)
int solvePCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField Ap, int maxit, double rtol, int pre, int post, int bottom, const StarOverlay *star=nullptr, int nStar=0, C3 nnStar=C3{0, 0, 0})
void setOutflowCoefficient(bool on)
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
void setBoundaryConditions(const int bc[6])
int solveChebyshev(CCField b, CCField x, int maxit, double rtol, int pre, int post, int bottom, double a, double bnd, bool guard3=false)
void setStopReference(double ref)
void estimateEigenvalues(CCConst seed, double &lmin, double &lmax, int iters, int pre, int post, int bottom, EigenStart start=EigenStart::Cold)
int solveFCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField zp, CCField Ap, int maxit, double rtol, int pre, int post, int bottom, const StarOverlay *star=nullptr, int nStar=0, C3 nnStar=C3{0, 0, 0})
double rhsNorm(CCField b, CCField scratch)
void applyVelocityBcAll(int fold, bool doOutflow)
void fillVelGhosts(int comp, int fold)
void addCsfRhs(int c, CCField dst, bool rowScaled, bool faceRange=false)
void phaseChangeStep()
The in-step driver: everything applyPhaseChange does, at the head of step().
bool balancedForceProjection() const
void buildOpenFaceField()
Build ufOpen_ = the face field with each open (outflow) domain face overwritten by its zero-gradient ...
long solveBalancedForceSystem(double bref, double r0)
void fillGhosts(CCField f)
void fillPropGhosts(CCField f, bool edges=false)
void requireCollocatedFaceForceScope(const char *who)
void applyBackflowStab(int c)
void pcApplyDivergenceSource(CCField div)
Subtract the phase-change (and any prescribed) divergence source from div_ so the deflated pressure s...
void addCsfRhsBlocks(int c)
CCField addField(const std::string &name)
OutflowBackflow outflowBackflow()
void pcUpdateEnergyProps()
Refresh k(C) and (rho c_p)(C) on the G=2 block from the current colour.
CCConst advVelView(int c) const
The velocity view the advection operators must read for component c: the wall-corrected scratch while...
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
double maxAbsDiffInner(CCConst a, CCConst b)
bool ufAdvVelocity() const
Are the advection operators reading the projected face field this step? Collocated grid,...
bool lastBalancedForceFailed() const
CCConst openFaceView(int c) const
Component c of the face field AS THE ADVECTION AND THE DIVERGENCE DIAGNOSTIC MUST READ IT: advFaceVie...
void addWallFluxDivergence(CCField d)
long lastBalancedForceIterations() const
void constraintDivergence(CCConst fx, CCConst fy, CCConst fz, CCField out)
long balancedForceFailures() const
static constexpr int G
Definition flow_ibm.hpp:80
long strideOf(int c) const
void collocatedV8AutoFallback(const char *why)
long solvePressureSystem(CCField rhs1, CCField x1)
void superficialVelocityPrecheck() const
void requireBalancedForceScope(const char *who, bool atStep) const
void filterCellField(CCField f, int axis)
bool ensureAdvStash(int c, bool adv)
bool mixedVelocityMg() const
void addCsfRhsCellInterp(int c, CCField dst, bool rowScaled, bool faceRange=false)
void setBalancedForceProjection(bool enabled)
void copyBlockShifted(CCField dst, C3 de, CCConst src, C3 se, int off)
void restrictAdvVelocities(CCConst u0, CCConst v0, CCConst w0)
double weightSum(double oLo, double oHi)
double cellFromFaces(double aLo, double aHi, double oLo, double oHi)
void stencilMatvec(CCField y, CCConst u, MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, C3 e, int g)
void bcCorrectOutflow(BField f, BField phi, B3 ext, int g, int a, double wa=1.0)
Definition mac_bc.hpp:341
void buildRhoCoeffOutflowFace(CCField ca, CCConst oa, CCConst rho, double rho0, C3 e, int g, int a, bool harmonic)
void gpDivergDelta(CCField d, CCConst u, CCConst v, CCConst w, const GpOverlayReal< Real > &ov, int nOv, C3 nn, C3 extb, int gb, bool useGhost=false)
Overlay divergence correction: d(r) = rho_r * (d(r) + closure/BC/explicit face values),...
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 centerGradOpen(CCField out, CCConst p, CCConst o, int axis, C3 e, int g, double wa=1.0)
void projectCorrectPorousDrag(CCField u, CCField v, CCField w, CCConst phi, CCConst beta, double idt, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
Kokkos::View< const MReal *, CCMem > FPC
void buildRhoCoeff(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst rho, double rho0, C3 e, int g)
void buildPorousCoeffCons(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCConst beta, bool useBeta, double rhoidt, C3 e, int g)
void bcZeroPressureGhost(BField phi, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:317
void subtractField(CCField u, CCConst d, C3 e, int g)
void transposeGradWallAware(CCField out, CCConst p, CCConst sdf, CCConst o, CCConst xc, bool useCen, int axis, C3 e, int g, double wa=1.0)
void divergOpen(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCField d, C3 e, int g)
void projectCorrect(CCField u, CCField v, CCField w, CCConst phi, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void projectCorrectCenterOpen(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void correctCellFaceAverageVar(CCField u, CCConst phi, CCConst rho, CCConst o, bool haveRho, double rho0, long s, C3 e, int g, double wa=1.0)
void projectCorrectPorousCons(CCField u, CCField v, CCField w, CCConst phi, CCConst eps, CCConst beta, bool useBeta, double rhoidt, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void buildFaceAccelVar(CCField af, CCConst P, CCConst rho, CCConst fb, bool haveFb, bool haveRho, double rhoC, double fc, bool incr, double scale, long s, C3 e, int g, double wa=1.0)
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)
double maxAbsInner(CCConst a, C3 e, int g)
void starCorrectFaces(CCField uf, CCField vf, CCField wf, CCConst phi, const StarOverlay &ov, int nOv, C3 nn, C3 ext, int g, C3 extP, int gP, bool exact, double wx=1.0, double wy=1.0, double wz=1.0)
Fix the face correction at fluid|solid faces: projectCorrect applied -(phi_hi - phi_lo) with the soli...
void projectCorrectVar(CCField u, CCField v, CCField w, CCConst phi, CCConst rho, double rho0, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void ccFor3(const char *name, C3 lo, C3 hi, F f)
void centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e, int g)
void embedViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy, CCConst oz, double mu, double idt, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0, double hpx=1.0, double hpy=1.0, double hpz=1.0)
Kokkos::View< double *, CCMem > CCField
void buildPorousCoeff(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst eps, C3 e, int g)
Kokkos::DefaultExecutionSpace CCExec
void buildRhoCoeffHarm(CCField cx, CCField cy, CCField cz, CCConst ox, CCConst oy, CCConst oz, CCConst rho, double rho0, C3 e, int g)
void projectCorrectCenter(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void centerToFaceMassWeighted(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, CCConst rho, C3 e, int g)
void divergOpenEps(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCField d, C3 e, int g)
void projectCorrectVarHarm(CCField u, CCField v, CCField w, CCConst phi, CCConst rho, double rho0, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void addFaceAccelCsf(CCField af, CCConst cv, CCConst kp, CCConst kb, CCConst rho, bool haveRho, double rhoC, double sigma, double hGrad, double scale, long s, C3 e, int g)
void gpCenterGrad(CCField out, CCConst p, CCConst sdf, int axis, C3 e, int g, double wa=1.0)
Directional cell-center gradient (collocated ghost path) of a cell field p whose solid-centered rows ...
void bcCorrectOutflowVar(BField f, BField phi, BField rho, double rho0, B3 ext, int g, int a, bool harmonic, double wa=1.0)
Definition mac_bc.hpp:371
void bcNeumannGhost(BField f, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:246
Kokkos::View< const double *, CCMem > CCConst
OUTFLOW REVERSAL CENSUS – the regime in which the zero-gradient (do-nothing) outflow is only conditio...
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
double hp[3]
h_a' = h_a / hRef (>= 1, exactly 1 on the finest axis)
Definition flow_ibm.hpp:178
double velToInt(int a) const
Definition flow_ibm.hpp:150
double curvature
the V3 height-function cascade + the capillary dt check
double csf
the V4 balanced-force CSF added to the three momentum RHSs
double phaseChange
the Part II phase-change stage (mdot, deposit, regression)
static constexpr double AC
static constexpr double U