flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm.hpp
Go to the documentation of this file.
1
18#ifndef PECLET_FLOW_SDFLOW_IBM_HPP
19#define PECLET_FLOW_SDFLOW_IBM_HPP
20
21#include <algorithm>
22#include <array>
23#include <chrono>
24#include <cmath>
25#include <cstdlib>
26#include <cstring>
27#include <Kokkos_Core.hpp>
28#include <limits>
29#include <memory>
30#include <string>
31#include <vector>
32
33#include "peclet/core/geom/device_scene.hpp"
34
35#include "collocated_varrho.hpp" // rung V8: the collocated variable-density (mass-adjoint) kernels
36#include "face_props.hpp"
38#include "ghost_projection_debug.hpp" // opt-in gp row forensics (PECLET_FLOW_GP_DEBUG), no-op off
39#include "grid_layout.hpp"
41#include "mac_cheb_momentum.hpp"
42#include "mac_cutcell_mg.hpp"
43#include "mac_ibm.hpp"
44#include "mac_pressure.hpp"
45#include "mac_stencils.hpp"
46#include "mac_velocity_mg.hpp"
47#include "peclet/core/field/field_set.hpp"
48#include "peclet/core/solver/anderson.hpp" // AndersonRole of the march state (MarchState)
49#include "property_closures.hpp"
50#include "scalar_cutcell_geometry.hpp" // cut-cell scalars: the geometry record (WO-2)
51#include "scalar_cutcell_operator.hpp" // cut-cell scalars: operator + per-scalar state (WO-3)
52#include "scalar_krylov.hpp" // cut-cell scalars: BiCGStab (WO-3)
53#include "scalar_mg.hpp" // cut-cell scalars: ScalarMG preconditioner (WO-4)
54#include "scalar_transport.hpp"
56#include "vof/advect_wy.hpp" // VoF rung V1: the Weymouth-Yue colour advector (its own g=3 block)
57#include "vof/block_container.hpp" // VoF Part III rung W0: the per-bubble block container
58#include "vof/block_exchange.hpp" // VoF Part III rung W0: the block gather / scatter
59#include "vof/colour_field.hpp" // VoF rung V2a: the G=2 <-> g=3 bridge + the colour ghost policy
60#include "vof/colour_bc.hpp" // VoF rung V-BC: inflow/outflow/backflow colour + the outside mask
61#include "vof/curvature_field.hpp" // VoF rung V3: the HF curvature cascade + PV fallback
62#include "vof/momentum_advect.hpp" // VoF rung V2b: momentum-consistent rho^c u_c transport
63#include "vof/surface_tension.hpp" // VoF rung V4: balanced-force CSF + the capillary dt
64#include "vof/phase_change.hpp" // VoF Part II rungs P0/P1: mass flux, plane-shift regression
65#include "vof/wetting_dynamic.hpp" // VoF rung V6: dynamic contact angle + hysteresis
66#include "vof/energy_advect.hpp" // VoF Part II rung P2/P3: consistent rho c_p T transport
67#include "vof/interface_area_field.hpp" // VoF Part II rung P3c: the cascade-consistent A_Gamma
68#include "vof/marching_cubes_field.hpp" // VoF Part II rung P3d: the JOINED (marching-tet) A_Gamma
69
70namespace peclet::flow {
71
72// Templated on a GridLayout policy (grid_layout.hpp) that supplies the grid-position-dependent
73// pieces (currently: the per-component velocity sample offset). IbmSolver == Solver<Staggered> (the
74// alias below) is bit-identical to the pre-policy solver; the Colocated policy is added in a later
75// phase.
76template <class Grid>
77class Solver {
78 public:
79 using FV = Kokkos::View<MReal*, CCMem>; // velocity operator storage tracks MReal
80 static constexpr int G = 2; // velocity block: Koren advection reach (pressure/MG bridged to g=1)
81
82 Solver(int nx, int ny, int nz);
83
84
85
86 // ==========================================================================================
87 // PHYSICAL UNITS — the solver takes the world in the caller's own units
88 // (suite/docs/PHYSICAL_UNITS_PLAN.md; Phase 1 = isotropic cells)
89 //
90 // DERIVATION. The discrete algorithms in this file are index-native: red-black sweeps,
91 // cut-cell apertures, multigrid transfers, the halo, PLIC. Rather than divide every finite
92 // difference by a cell size, the solver keeps computing on the UNIT LATTICE and folds the
93 // metric into constants at the API boundary (plan §3.2, option B). Write
94 //
95 // x_a = origin_a + h_a * xi_a (physical position of the index coordinate xi)
96 // t = tRef * t' (tRef = the first dt the caller sets)
97 // u_a = (h_a / tRef) * v_a (v = index velocity: cells per tRef)
98 // rho = rhoRef * rho'
99 //
100 // and substitute into rho (du/dt + u.grad u) = -grad p + mu lap u + F. Every term then
101 // carries the same factor rhoRef*hRef/tRef^2 when the cells are isotropic (h_a == hRef,
102 // Phase 1), and dividing it out leaves EXACTLY the equation this file already solves,
103 //
104 // rho' (dv/dt' + v.grad_xi v) = -grad_xi p' + mu' lap_xi v + F'
105 //
106 // with the boundary conversions
107 //
108 // rho' = rho / rhoRef dt' = dt / tRef
109 // mu' = mu * tRef / (rhoRef*hRef^2) (the cell diffusion number)
110 // p' = p * tRef^2 / (rhoRef*hRef^2) F' = F * tRef^2 / (rhoRef*h_a)
111 // v_a = u_a * tRef / h_a (the Courant number)
112 // sigma' = sigma * tRef^2 / (rhoRef*hRef^3) so p' = sigma'*kappa' with kappa' = kappa*hRef
113 // kappa' = kappa * hRef div' = div * tRef
114 // d' = d / hRef (any length: SDF value, slip length)
115 // xi_a = (x_a - origin_a) / h_a (any position)
116 //
117 // Advection is untouched in index space, so the Koren/TVD and implicit-FOU paths do not change;
118 // cut-cell apertures and wall crossings are ratios along an axis and are unit-free.
119 //
120 // WHY THE REFERENCE SCALES (plan decision D3). They exist so the STORED float operator
121 // coefficients stay O(1) whatever unit system the caller uses: mu' is a diffusion number and
122 // rho'/dt' is 1 at the first dt, where dividing mu by a physical h^2 would put 1e7 next to 1e8
123 // in float storage (docs/SCALING_ISSUES.md #1). hRef is fixed at construction, rhoRef at the
124 // first set_rho and tRef at the first set_dt, and never move again — no stored state is ever
125 // rescaled mid-run. Because the order of those calls must not matter, the PHYSICAL inputs are
126 // kept verbatim (rhoPhys_ / muPhys_ / ...) and `refreshUnitDerived()` re-derives the internal
127 // ones whenever a scale is first pinned.
128 //
129 // IDENTITY. With no extent at all (`extent=None`, the historical cell-unit API) the solver is
130 // never armed: hRef = rhoRef = tRef = 1 and every factor below evaluates to exactly 1.0,
131 // whatever rho and dt are. Multiplying a double by 1.0 is the identity in IEEE-754, so every
132 // pre-existing script, test and gallery page is bit-identical (plan §5 gate 1). With
133 // extent == cells, dt = 1 and rho = 1 the armed path reproduces the same 1.0 factors — that is
134 // the `units_identity` gate.
135 // ==========================================================================================
136 struct UnitScales {
137 bool physical = false;
138 double h[3] = {1.0, 1.0, 1.0};
139 double org[3] = {0.0, 0.0, 0.0};
140 double ext[3] = {0.0, 0.0, 0.0};
141 long cells[3] = {0, 0, 0};
142 double hRef = 1.0;
143 double rhoRef = 1.0;
144 double tRef = 1.0;
145 bool rhoRefSet = false, tRefSet = false;
146
147 // Every factor below is exactly 1.0 while `physical` is false.
148 double lenToInt() const { return 1.0 / hRef; }
149 double lenToPhys() const { return hRef; }
150 double velToInt(int a) const { return tRef / h[a]; }
151 double velToPhys(int a) const { return h[a] / tRef; }
152 double timeToInt() const { return 1.0 / tRef; }
153 double rhoToInt() const { return 1.0 / rhoRef; }
154 double muToInt() const { return tRef / (rhoRef * hRef * hRef); }
155 double pToInt() const { return tRef * tRef / (rhoRef * hRef * hRef); }
156 double pToPhys() const { return rhoRef * hRef * hRef / (tRef * tRef); }
157 double forceToInt(int a) const { return tRef * tRef / (rhoRef * h[a]); }
158 double sigmaToInt() const { return tRef * tRef / (rhoRef * hRef * hRef * hRef); }
159 double curvToPhys() const { return 1.0 / hRef; }
160 double divToPhys() const { return 1.0 / tRef; }
161 double timeToPhys() const { return tRef; }
162 double angVelToInt() const { return tRef; } // omega' = omega*tRef (v = omega x r on both sides)
163 // A TOTAL force F = int f dV: f' = f*tRef^2/(rhoRef*hRef) and dV' = dV/hRef^3, so
164 // F' = F*tRef^2/(rhoRef*hRef^4); a torque carries one more length.
165 double forceTotalToPhys() const {
166 const double h2 = hRef * hRef;
167 return rhoRef * h2 * h2 / (tRef * tRef);
168 }
169 double torqueToPhys() const { return forceTotalToPhys() * hRef; }
170
171 // ---- Phase 2 (anisotropic cells): the per-axis metric of the discrete operators ----------
172 // doc/anisotropic_metric.md §1.1. h_a' = h_a/hRef >= 1 (exactly 1.0 on the finest axis),
173 // w_a = 1/h_a'^2 <= 1 the pressure-gradient / Laplacian weight of axis a, V' = h_x'h_y'h_z'
174 // the cell volume in hRef^3, and `aniso` true iff some h_a' != 1. Every operator fold below
175 // is "today's expression times one of these", so on the isotropic path — where they are all
176 // EXACTLY 1.0 — the arithmetic is bit-identical (multiplying an IEEE-754 double by 1.0 is the
177 // identity). APPEND new members here; never reorder the struct.
178 double hp[3] = {1.0, 1.0, 1.0};
179 double w[3] = {1.0, 1.0, 1.0};
180 double vol = 1.0;
181 bool aniso = false;
182
183 // ---- Phase 3 (the VoF half; flow/doc/anisotropic_vof.md §9) appends TWO more, and reuses
184 // the four above verbatim — the two sessions derived the same metric and the rebase kept one
185 // copy. `hpMax` is the Wendland support scale of the curvature/area fits; `vofMetric()` is
186 // the per-axis cell size the container-free VoF kernels take (`core/vof/plic.hpp`), which is
187 // `{1,1,1}` — hence the pre-Phase-3 arithmetic exactly — whenever the cells are cubes.
188 double hpMax = 1.0;
189 vof::VofMetric vofMetric() const { return vof::VofMetric{{hp[0], hp[1], hp[2]}}; }
190
191 // ---- Scalar transport and the phase-change energy path (2026-10-02) -----------------------
192 // The internal system is the one above: length hRef, time tRef, mass rhoRef*hRef^3. A
193 // transported scalar (temperature, concentration) is NOT scaled — it enters every equation
194 // linearly or through an absolute value (T_sat, a Boussinesq T0, an Arrhenius 1/T) — so each
195 // factor below is the M-L-T dimension of its quantity alone, with temperature as its own unit.
196 // Every one is a product of 1.0s, i.e. EXACTLY 1.0, while `physical` is false.
197 // diffusivity D L^2/T D' = D * tRef/hRef^2 (the cell Fourier number)
198 // mass flux mdot M/(L^2 T) mdot' = mdot * tRef/(rhoRef*hRef)
199 // latent heat h_lv L^2/T^2 h' = h_lv * tRef^2/hRef^2
200 // conductivity k M L/(T^3 K) k' = k * tRef^3/(rhoRef*hRef^4)
201 // heat capacity rho c_p M/(L T^2 K) rcp' = rcp * tRef^2/(rhoRef*hRef^2)
202 // IHTR resistance R_int K L^2 T/M R' = R_int * rhoRef*hRef/tRef (T = mdot R)
203 // divergence source S 1/T S' = S * tRef
204 // so k'/rcp' = D', mdot' = k' grad'T / h' and mdot' R' = mdot R_int (a temperature): every
205 // phase-change formula holds unchanged on the internal values. Out, per CELL SUM (the
206 // diagnostics sum per cell without the cell volume, which the caller multiplies by `vol`):
207 double diffToInt() const { return tRef / (hRef * hRef); }
208 double mdotToInt() const { return tRef / (rhoRef * hRef); }
209 double mdotToPhys() const { return rhoRef * hRef / tRef; }
210 double latentToInt() const { return (tRef * tRef) / (hRef * hRef); }
211 double condToInt() const {
212 const double h2 = hRef * hRef;
213 return tRef * tRef * tRef / (rhoRef * h2 * h2);
214 }
215 double rcpToInt() const { return tRef * tRef / (rhoRef * hRef * hRef); }
216 double rintToInt() const { return rhoRef * hRef / tRef; }
217 double divToInt() const { return tRef; }
218 double areaToPhys() const { return hRef * hRef; }
219 double volToPhys() const { return hRef * hRef * hRef; }
222 double energyToPhys() const { return rhoRef * volToPhys() * hRef * hRef / (tRef * tRef); }
223 double powerToPhys() const { return energyToPhys() / tRef; }
224
225 // ---- Cut-cell scalars (doc/scalar_ibm_design.md §1.2, WO-3) -------------------------------
226 // A speed L/T — the Robin transfer coefficient k and the Neumann wall flux q (c L/T) — on the
227 // internal lattice: k' = k tRef/hRef. ISOTROPIC by construction (the wall closure divides by a
228 // probe distance in hRef units), unlike the per-axis velToInt(a). Exactly 1.0 unarmed.
229 double speedToInt() const { return tRef / hRef; }
230 // A contact resistance R_c, T/L (doc/scalar_ibm_design.md §1.2, WO-7): the conjugate
231 // interface's q = (psi_s - psi_f)/R_c sits in series with s/Lam (T/L), so R_c' = R_c
232 // hRef/tRef = 1/speedToInt(). Exactly 1.0 unarmed.
233 double resistToInt() const { return hRef / tRef; }
234 };
235
241 struct SceneMap {
242 double a[3] = {0.0, 0.0, 0.0};
243 double b[3] = {1.0, 1.0, 1.0};
244 double dToInt = 1.0;
245 double velToInt[3] = {1.0, 1.0, 1.0};
246 double angToInt = 1.0;
247 };
248 SceneMap sceneMap() const;
249
250
251
256 Solver(int nx, int ny, int nz, const std::array<double, 3>& extent,
257 const std::array<double, 3>& origin, const std::array<long, 3>& globalCells);
258
259
260
263 void setPhysicalDomain(const std::array<double, 3>& extent, const std::array<double, 3>& origin,
264 const std::array<long, 3>& globalCells);
265
266
267
281 void requireIsotropic(const char* what) const;
282
283
284
285 bool hasPhysicalDomain() const;
286
287
288 const UnitScales& unitScales() const;
289
290
293 std::array<double, 3> spacing() const;
294
295
297 std::array<double, 3> domainOrigin() const;
298
299
301 std::array<double, 3> domainExtent() const;
302
303
305 std::array<long, 3> globalCells() const;
306
307
310 std::vector<double> cellCentres(int axis) const;
311
312
313
317 void refreshUnitDerived();
318
319
320
327 void pushVofMetric();
328
329
330
331 // (Re)allocate every per-block buffer for a local inner block of nx*ny*nz. Called by the
332 // constructor and by redistribute() after a re-decomposition changes this rank's block size.
333 void allocateBlock(int nx, int ny, int nz);
334
335
336
339 void setRho(double r);
340
341
344 void setMu(double m);
345
346
347 void setDt(double d);
348
349
351 void setBodyForce(double fx, double fy, double fz);
352
363 void setSuperficialVelocity(bool on, int axis, double velocity);
366 double lastSuperficialVelocityShift() const;
367
368
369 void setVelocityIterations(int it);
370
371
372 // Momentum tolerance stop: end the RB-GS loop once the swept colour's max increment has dropped
373 // to rtol of the first sweep's (GS contracts geometrically, so the increment tracks the error).
374 // rtol = 0 (default) keeps the legacy fixed-count loop byte-identical. Easy regimes (small
375 // nu*dt/dx^2) exit after ~3-5 sweeps; stiff regimes run to the velIters_ cap unchanged. The
376 // check is fused into the sweep kernel (no extra memory pass) and the stop decision is
377 // rank-uniform (MPI max) so distributed halo exchanges stay in lockstep.
378 void setVelocityTolerance(double rtol, int minIters);
379
380
381 long lastMomentumSweeps() const;
382
383
384 // Residual-based momentum stop (opt-in, 0 = off): a component's implicit solve ends once
385 // max|b - A u| <= rtol * max|b| over the fluid unknowns (global under MPI). Unlike the update
386 // criterion (relative to the FIRST sweep's update, which on a warm-started near-steady step is
387 // already at noise level and then costs hundreds of sweeps to shrink by 1e-3) this measures the
388 // equation's own convergence, and a converged warm start stops at sweep 1. Available on the
389 // stencil paths (IBM / cut-cell domain-BC RB-GS, every velocity-MG mode); the const-coefficient
390 // domain-BC smoother keeps the update criterion.
391 // rtol > 0: fixed; rtol == 0: the legacy update criterion; rtol < 0 (DEFAULT): FOLLOW THE
392 // PRESSURE SOLVER'S TOLERANCE -- the projection is what consumes u*, and it resolves the
393 // divergence the momentum residual leaves to its own rtol, so "solve momentum no less
394 // accurately than pressure" is the self-consistent choice with no free constant.
395 void setVelocityResidualTolerance(double rtol);
396
397
398 // The momentum solver's default selection rule. Since 2026-09-15 the DEFAULT is the velocity
399 // V-cycle on every validated operator mode and at every rank count, replacing red-black
400 // Gauss-Seidel. Measured on the 1.0.0 scaling benchmark (384^3 cut-cell bed): the V-cycle is
401 // faster at every rung of both the Genoa and the H100 ladder -- 2.23x at 147k cells/rank,
402 // 2.19x on a single H100 -- and red-black was running at its sweep cap (velIters_), i.e. not
403 // converging, on every rung of both.
404 //
405 // This setter narrows that default rather than enabling it:
406 // cellsPerRank upper bound in cells per rank (default: none). 0 = never use the V-cycle.
407 // set_velocity_multigrid_auto(65536, 1 << 23) restores the 1.0.0 rule.
408 // minGlobalCells global-size floor (default: none).
409 // minBlockExtent shortest per-rank inner extent worth building a hierarchy on (default 16);
410 // below it the V-cycle degenerates to its bottom smoother and only adds setup.
411 // An explicit setVelocityMultigrid() overrides all of this.
413 int minBlockExtent = -1);
414
415
416 // Chebyshev semi-iteration as the momentum solver, in place of the red-black Gauss-Seidel
417 // smoother, on the SAME sharp cut-cell stencil and to the same tolerance -- so it has the same
418 // fixed point and the projection consumes the same u*. The momentum operator's condition number
419 // is kappa = 1 + 12 D in D = mu dt/(rho h^2) and does NOT grow with the mesh, so Chebyshev's
420 // O(sqrt kappa) beats Gauss-Seidel's O(kappa) without a coarse grid; and one Chebyshev step is
421 // ONE residual and ONE halo exchange against a red-black sweep's two of each. The spectral
422 // interval is Gershgorin arithmetic on the stored stencil (ibmStencilJacobiBounds), not a power
423 // iteration. IBM/periodic path only: a domain-BC configuration keeps RB-GS.
424 void setVelocityChebyshev(bool on, int maxit = 400);
425
426 // Return the momentum-solver choice to the automatic rule (the default), undoing an
427 // explicit setVelocityMultigrid/setVelocityChebyshev. The choice is re-taken at the next
428 // step(), where dt and mu are final.
430 bool velocityChebyshevActive() const;
431
432 // Chebyshev as the velocity MULTIGRID's smoother, in place of red-black Gauss-Seidel on every
433 // level. Independent of setVelocityChebyshev (which replaces the V-cycle outright): this keeps
434 // the V-cycle -- whose convergence is what actually wins on this operator -- and spends half the
435 // halo exchanges inside it. `degree` 0 follows the V-cycle's own pre/post/bottom sweep counts,
436 // so the two smoothers are compared at equal nominal work.
437 void setVelocityMgChebyshev(bool on, int degree = 0, double eig_ratio = 10.0);
438 bool velocityMgChebyshev() const;
439
440
441 // The tolerance actually in force (resolves the follow-the-pressure default).
442 double velocityResidualTolerance() const;
443
444
445 // max over components of max|r|/max|b| at exit of the last step's momentum solves (residual
446 // mode only; -1 otherwise).
447 double lastMomentumResidual() const;
448
449
450 // Pressure-solve mean-removal scope: "fine" (default — drops the interior-level / post-matvec
451 // nullspace projections, ~3x fewer global-reduction latency hits per Krylov iteration; measured
452 // winner of the at-scale ablation, iteration counts identical) or "all" (legacy). See CutcellMG.
453 void setPressureMeanRemoval(bool all);
454
455
456 void setPressureIterations(int it);
457
458
459 void setAdvection(bool on);
460
461 // explicit high-order advection (default SOU)
462 // High-order advection scheme for the (explicit, or deferred-correction) flux: 0 = second-order
463 // upwind (SOU, default — 2nd order at smooth extrema too); 1 = Koren TVD (monotone limiter, the
464 // legacy CUDA scheme). Only matters when advection is enabled; FOU stays the deferred-correction
465 // base.
466 void setAdvectionScheme(int s);
467
468
469 // Implicit-FOU deferred-correction advection (CUDA set_implicit_advection): solve the
470 // first-order-upwind part of advection implicitly (in the velocity operator) + keep (Koren-FOU)
471 // explicit in the RHS -> unconditionally stable for advection (high Re / large dt). Requires the
472 // IBM stencil (rebuilt per Picard iteration with the FOU term); the domain-BC path needs
473 // velocity-MG (separate milestone).
474 void setImplicitAdvection(bool on);
475
476
477 // Picard outer iterations over the step (CUDA set_outer_iterations): the advecting velocity is
478 // lagged at the current iterate u^k while the time base stays u^n. iters>=1; tol>0 stops early on
479 // max|du| < tol.
480 void setOuterIterations(int iters);
481
482
483 void setOuterTolerance(double tol);
484
485
486 long lastOuterIterations() const;
487
488
489 // Velocity (momentum) multigrid for the IBM diffusion solve (CUDA set_velocity_multigrid): the
490 // STAIRCASE coarse operator (exact == RB-GS, stiff-stable at large dt). Call before set_solid;
491 // built at geometry time.
492 // Refused (std::invalid_argument) with a variable viscosity: the V-cycle takes a scalar mu.
493 void setVelocityMultigrid(bool on, int levels, int vcycles);
494
495 // The one message for an explicit velocity multigrid combined with a variable viscosity.
496 void throwVelocityMgVariableMu(const char* who) const;
497
498
499 bool velocityMultigridActive() const;
500
501
502 // Enable the agglomerated GraphAMG bottom solve in the pressure MG: the coarsest level is solved
503 // by a mesh-agnostic algebraic multigrid on the operator gathered to rank 0 --
504 // decomposition-agnostic, so multilevel convergence works under a WEIGHTED ORB (where the
505 // geometric coarse levels can't cleanly coarsen). Applied at the next set_solid / geometry
506 // rebuild.
507 // Coarse-level (bottom) solve policy: 0 smoothed bottom (default), -1 auto (agglomerate exactly
508 // when the geometric hierarchy cannot reach a small enough coarsest grid), 1 always. See CutcellMG.
509 void setPressureBottomMode(int mode);
510 // Engine of an agglomerated bottom (doc/vof_step_performance_design.md §13): 0 "auto" (the
511 // single-team device bottom -- FP64 FCG preconditioned by the FP32 block-tridiagonal direct
512 // factor -- where eligible on a device backend, GraphAMG otherwise), 1 "direct" (raises at the
513 // bottom solve, naming the failed condition, where ineligible), 2 "algebraic" (GraphAMG always).
514 // Live; independent of the geometry.
516
517 // Coarse-level telescoping of the pressure multigrid (mac_cutcell_mg.hpp Telescope): when a
518 // per-rank block turns odd, merge ORB siblings onto fewer ranks and keep coarsening instead of
519 // stopping. Multi-rank only; a no-op single-rank. Takes effect at the next init_mpi/set_solid.
520 void setPressureTelescope(bool on);
521
522
523 bool pressureTelescope() const;
524
525
526 // Force a telescope at that level even where in-place coarsening is legal (tests: compare the
527 // two hierarchies on one problem); -1 = the trigger decides. Set before geometry.
528 void setPressureTelescopeForceLevel(int level);
529
530
531 int pressureTelescopeCount() const;
532
533
534
535 void setPressureGraphAmg(bool on);
536
537
538 void setPressureLevels(int levels);
539
540 // MG depth (CUDA default 4)
541 // Backflow stabilization at outflow faces (Bazilevs 2009 / Esmaily-Moghadam 2011): beta in [0,1]
542 // scales the dissipative outflow term that prevents backflow divergence (0 = off). Default 0.2.
543 void setBackflowStab(double beta);
544
545
546 // Deferred-correction advection: on (default) = implicit FOU operator + explicit (HO - FOU)
547 // high-order correction (2nd order; HO = SOU by default, or Koren TVD via set_advection_scheme).
548 // off = pure implicit FOU (1st order, more dissipative, unconditionally stable) -- useful for
549 // very sharp shear layers where the (unlimited SOU) explicit correction overshoots and
550 // destabilizes.
551 void setDeferredCorrection(bool on);
552
553
554 // Chebyshev pressure driver (CUDA set_pressure_chebyshev): communication-light alternative to
555 // MG-PCG -- Chebyshev semi-iteration preconditioned by one symmetric V-cycle, no per-iteration
556 // global dot-products. Spectral bounds of M^{-1}A are estimated once (lazily) on the first solve
557 // and reused every step.
558 // Selecting it clears the competing FCG selection (the three Krylov drivers are mutually
559 // exclusive, last set wins); `on = false` only deselects Chebyshev, so the solve falls back to
560 // whatever else is selected — FCG if set, otherwise MG-PCG.
561 void setPressureChebyshev(bool on, int maxit, double rtol);
562
563
564 // MG-PCG pressure driver (CUDA set_pressure_pcg) + its iteration cap / relative tolerance.
565 // `on = true` GENUINELY SELECTS MG-PCG, clearing both competing selections (Chebyshev and FCG),
566 // so it works after set_density_mode / set_porous — "last set wins", as CLAUDE.md and the
567 // docstring have always claimed. Until 2026-08-30 the flag was silently discarded (WO-H defect 1;
568 // the working spelling was set_pressure_chebyshev(False, ...)), which is why every "PCG under
569 // varRho/porous" measurement before WO-B actually measured Chebyshev.
570 //
571 // `on = false` cannot be honoured and therefore THROWS rather than being silently ignored (the
572 // failure mode this repair exists to remove): MG-PCG is the terminal fallback of the dispatch in
573 // project() — with neither Chebyshev nor FCG selected the solve IS MG-PCG — so "not PCG" is only
574 // expressible by selecting another driver. Say which one: set_pressure_chebyshev(True, ...) or
575 // set_pressure_fcg(True, ...).
576 //
577 // Under set_ghost_projection the operator is nonsymmetric and is solved by BiCGStab; this call
578 // stays legal there (it is how that path's cap/tolerance is set — pcgMaxit_/pcgRtol_ are shared)
579 // and simply does not change which Krylov method the gp branch runs.
580 void setPressurePcg(bool on, int maxit, double rtol);
581
582
583 // FLEXIBLE MG-CG (set_pressure_fcg): the same Krylov driver as MG-PCG with the same V-cycle
584 // preconditioner, the same stopping estimate, the same mean removal and the same cap/tolerance
585 // (`pcgMaxit_`/`pcgRtol_`, shared deliberately — it is the same solve), differing ONLY in the
586 // beta recurrence: Polak-Ribiere `r^T(z_{k+1} - z_k) / r^T z_k` instead of Fletcher-Reeves.
587 // Costs one extra level-0 vector and one extra global dot per iteration; buys tolerance of a
588 // preconditioner that is not symmetric w.r.t. the fine operator (CutcellMG::solveFCG).
589 //
590 // All three Krylov drivers are mutually exclusive in both directions and the last set wins:
591 // `on` clears `useChebyshev_` here, `setPressureChebyshev(true, ...)` clears `useFcg_`, and
592 // `setPressurePcg(true, ...)` clears both. `set_pressure_fcg(false)` returns the solve to MG-PCG.
593 // (Before WO-H, setPressurePcg's `on` flag was silently discarded — see its comment.)
594 void setPressureFcg(bool on, int maxit, double rtol);
595
596
597 // EXPERIMENTAL directional ghost-cell projection (second staggered IBM, ghost_projection.hpp):
598 // point-based FD divergence with wall-anchored directional closures instead of the
599 // openness-weighted cut-cell projection. Call BEFORE set_solid (the overlay is built there).
600 // v1: periodic + IBM only, stationary walls (both grids; the collocated variant closes the
601 // face-AVERAGED field and adds the gpCenterGrad predictor/correction, face_interp 0 only).
602 // Runs multi-rank (initMpi): gp-row ownership is by inner-block cell, the closures read the
603 // exchanged g=2 halo, and the fragmentation guard runs on the allgathered GLOBAL sdf (the
604 // exact-crossings / openness-override study inputs stay single-rank). The nonsymmetric extended
605 // stencil is solved by MG-preconditioned BiCGStab (binary-openness surrogate hierarchy).
606 // matrixOrder/rhsOrder select the closure order (1 = linear, 2 = wall-anchored quadratic) for
607 // the implicit phi couplings and the divergence RHS/diagnostic respectively:
608 // (2,2) full quadratic (13-point nonsymmetric matrix);
609 // (1,1) linear everywhere (7-point matrix, 1st-order closure);
610 // (1,2) MIXED/deferred: 2nd-order steady constraint with a 7-point near-symmetric matrix —
611 // the operator mismatch converges through the time stepping (measured rate ~0.4).
612 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2);
613
614
615 // Analytic-SDF capability: EXACT wall-crossing fractions overriding the linear-interp theta in
616 // BOTH the momentum cut-cell overlay and the ghost-projection closures. t is a flat array of
617 // size 9*nx*ny*nz, blocks ordered [(c*3 + k)]: for velocity component c, t[(c*3+k)*n + i] is
618 // the exact crossing fraction in (0,1) from component c's staggered point at inner cell i
619 // toward its +k-axis neighbour point, NaN where the segment has no wall crossing. Computed in
620 // Python from the analytic geometry (e.g. line-sphere intersection). Call BEFORE set_solid;
621 // pass an empty array to clear. Single-rank only.
622 void setExactCrossings(const std::vector<double>& t);
623
624
625 // Analytic-SDF capability: EXACT face-openness (aperture) fields overriding the sampled-SDF
626 // ccFractionCore openness the cut-cell projection uses. Inner arrays (flat x-fastest,
627 // nx*ny*nz); ox[i] = fluid area fraction of the -x face of cell i, etc. Call BEFORE set_solid.
628 void setOpennessOverride(const std::vector<double>& ox, const std::vector<double>& oy,
629 const std::vector<double>& oz);
630
631
632 // Incremental-rotational pressure (CUDA set_incremental_pressure, default ON): the predictor
633 // carries -grad(P^n) and the physical pressure is accumulated rotationally P += (rho/dt)*phi -
634 // mu*div(u*). OFF => classical non-incremental Chorin (no -grad(P^n) predictor; P derived on
635 // demand as (rho/dt)*phi).
636 void setIncrementalPressure(bool on);
637
638
639 // Balanced-force projection (doc/collocated_varrho_forces.md §4.6; PUBLIC tier, both grids).
640 // Once per step, after the property/curvature refresh and before the momentum predictor, solve
641 // D(O c w G X) = D(O c beta), c = rho0/rho_f^op (1 at constant rho),
642 // beta = f_const + ½(f(j) + f(j-s)) + CSF(j) (the predictor's own face force, without rs),
643 // with the projection's OWN operator, constraint divergence and driver, as an INCREMENT on the
644 // previous split, X = P_b + dP_b, stopped relative to the full right-hand side (and skipped when
645 // P_b already meets it), then P += X - P_b, P_b = X -- or, when P_b is not a valid split of the
646 // current P (the first step after steps with the option off, or a restart that restored "p"
647 // but not "p_balanced"), P_b = X with P unchanged (a RE-SPLIT: never a second copy of the
648 // balanced pressure). "p_balanced" is RESTART STATE: it is registered as soon as the option is
649 // active; restoring it together with "p" (set_field) keeps the split, omitting it re-splits.
650 // P stays the total physical pressure; the predictor then sees only
651 // the part of the forces that drives flow, so gradient forces (hydrostatics, constant-kappa CSF)
652 // are balanced exactly from step 1 at every mu, dt, density ratio and openness. The solve is
653 // state-independent: stability and the converged steady state are identical ON and OFF. Cost:
654 // one extra pressure solve per step while the forces change; none for a static interface.
655 // DEFAULT (U2): ON on the collocated variable-density / CSF path (V8) -- except with an
656 // inflow/outflow domain face, where the default resolves OFF with a one-time stderr notice --
657 // and OFF elsewhere; an explicit setting always wins. OFF is byte-identical to a solver
658 // without the option. Refused, with a named error at this setter when the configuration is
659 // already known and otherwise at the next step: porous continuity, the ghost projection,
660 // set_fluid_only_constraint(2), the block CSF, any inflow/outflow face, the non-incremental
661 // pressure, no cut-cell pressure operator, and the constant-density collocated path (its
662 // pressure force is gpCenterGrad; there is nothing to balance).
664 bool balancedForceProjection() const;
665 // Pressure-driver iterations of the last step's balanced-force solve (0 when the force
666 // divergence was exactly zero, or the option is off).
667 long lastBalancedForceIterations() const;
668 // Did the last step's balanced-force solve FAIL (driver breakdown, or a non-finite result)? A
669 // failed solve leaves P and P_b as they were (the step runs as if the option were off for that
670 // step's split) and is counted in balancedForceFailures(); the first one warns on stderr.
671 bool lastBalancedForceFailed() const;
672 long balancedForceFailures() const;
673
674
675 // Pressure warm-start (CUDA set_pressure_warmstart, default OFF): seed each cut-cell pressure
676 // solve from the previous step's projection potential (consecutive phi's are similar along a
677 // steady march -> a more converged phi per fixed solver budget) instead of zeroing the initial
678 // guess.
679 void setPressureWarmstart(bool on);
680
681
682 // Collocated cut-cell treatment of the approximate projection (no effect on the staggered path).
683 // The public API is the string form setCollocatedScheme(); the integer mode is the C++ switch
684 // behind it and the Python developer tier's `diagnostics.set_face_interp(5|6)`:
685 // 0 "plain" plain ½/½ cell->face averaging + central-difference -grad(P): a consistent
686 // adjoint pair of the WRONG geometry (wall at the solid neighbour's centre),
687 // first-order drag at curved walls. Legacy, kept for reproducing results.
688 // 9 "gauge-exact" the aperture constraint (unchanged, throat-safe, symmetric MG-PCG) with the
689 // directional gpCenterGrad replacing the two operators measured to be O(1)
690 // at cut cells -- the -grad(P) predictor and the projection's cell
691 // correction. SECOND ORDER on two periodic sphere beds (2.36-2.89 over
692 // R=5..8, +0.08 % of k_inf at R=16) where mode 0 is first order; the
693 // cheapest scheme measured (doc/collocated_paper_plan.md).
694 // 5/6/7 "embed" the Basilisk embed.h port (commits db5b4aa/f5fde8c/6d412ec/03a71c6,
695 // doc/history/collocated_embed_port_plan.md): the FV momentum operator with
696 // the true-normal dirichlet_gradient wall drag applied as a defect
697 // correction on the IBM matrix (5 = that alone, on the wall-aware face map
698 // with the transpose-gradient pressure force; 6 = plus the openness-weighted
699 // -grad(P) predictor and cell correction, on the plain face map; 7 = 6 on
700 // the wall-aware face map and the solid-cut-cell sliver mask -- the
701 // COMPLETE port, the public "embed"). The live candidate for removing the
702 // collocated accuracy ceiling; 5 and 6 are its two intermediate rungs.
703 // The "ghost" scheme (setGhostProjection) is not a face-interp mode: it owns the operators the
704 // modes replace and forces mode 0 underneath itself.
705 // Modes 1, 2, 3, 4, 10, 11, 12 and 13 (pure ablations, the FV-constraint variants and the
706 // adjoint-aperture family) and the "gauge-2a" one-sided gradient branch were DELETED with their
707 // kernels at 1.0.0 (suite/docs/QUALITY_PLAN.md F); the record of what they measured is in
708 // doc/history/collocated_*.md.
709 void setFaceInterp(int mode);
710
711
712 int faceInterp() const;
713
714
715 // Preferred API for the collocated projection scheme (the strings the Python API takes):
716 // "ghost" the fluid-only constraint scheme (the AUTO default where supported)
717 // "gauge-exact" aperture constraint + directional (gauge-exact) pressure gradient (mode 9)
718 // "plain" the legacy plain-average / central-difference path (mode 0, first order)
719 // "embed" the complete Basilisk embed.h port (mode 7)
720 void setCollocatedScheme(const std::string& name);
721
722
723 // PM I ablation (Guy-Fogelson): keep the incremental predictor -grad(P^n) but accumulate
724 // P += (rho/dt)*phi WITHOUT the rotational -mu*div(u*) term (constant-mu path only; the
725 // variable-mu branches keep their own treatment). Default true = shipped behaviour.
726 void setRotationalPressure(bool on);
727
728
729 // Rotational under-relaxation: P += ct*phi - w*mu*div(u*). w = 1 is the shipped Timmermans
730 // update; w = 0 is PM I. Shrinking w shrinks the O(1) velocity->pressure off-diagonal that
731 // makes the cell-centered approximate projection marginally unstable (Guy-Fogelson eq. 92-94:
732 // the destabilizing-perturbation threshold scales ~1/w), at the cost of ~1/w slower pressure
733 // relaxation of the smooth modes at large dt. phi = 0 stays the unique fixed point for ANY
734 // w > 0, at every dt including dt -> infinity.
735 void setRotationalWeight(double w);
736
737
738 // Wall-banded rotational blend (Frank, 2026-08-20): see the press_wallblend kernel. w0 = 0
739 // (default) disables; typical w0 ~ 0.3-0.5. Composes with setRotationalWeight (uniform factor).
740 void setRotationalWallWeight(double w0);
741
742
743 // Fluid-only pressure constraint (route 2b). Call BEFORE set_solid. Collocated experiment;
744 // defaults byte-identical when 0. mode 1 = Design A (close every openness face with a
745 // solid-centered side, everywhere); mode 2 = Design B (Kron star elimination: filtered
746 // openness feeds the MG hierarchy only, the SPD star overlay restores the throat coupling in
747 // the PCG matvec, the divergence keeps the original apertures on fluid rows, and fluid|solid
748 // faces are corrected with phibar_s -- see star_elimination.hpp).
749 // Aperture estimation order (2026-08-26): 1 = the shipped one-sample linear model (default,
750 // byte-identical), 2 = marching-squares (5 trilinear samples/face, triangle-fan; O(h^2),
751 // removes the convexity bias measured at +0.59%/+0.27% bed-k at R=8/12 -- tracker row 51).
752 // In-solver ceiling is the trilinear field; for ANALYTIC geometry use exact/Saye apertures
753 // via set_openness_override (scripts/exact_apertures_spheres.py). Call before set_solid.
754 void setApertureOrder(int order);
755
756
757 int apertureOrder() const;
758
759
761 void setAdvectionWallVelocity(bool on);
762
763
764 bool advectionWallVelocity() const;
765
766
777 void setUfAdvection(bool on);
778
779
780 bool ufAdvection() const;
781
782
785 void setCommAvoiding(int mask);
786
787
788 int commAvoiding() const;
789
790
793 void setMultigridAspectThreshold(double theta);
794
795
796 double multigridAspectThreshold() const;
797
798
801 void setPressureStrict(bool on);
802
803
804 bool pressureStrict() const;
805
806
809 void setPressureCoarseGhost(bool on);
810
811
812 bool pressureCoarseGhost() const;
813
814
817 void setPressureBottomExtent(int cells);
818
819
820 int pressureBottomExtent() const;
821
822
825 void setDecomposition(int levels, double maxImbalance = 1.05);
826
827
828 int decompositionLevels() const;
829
830
831 double decompositionMaxImbalance() const;
832
833
834 void setFluidOnlyConstraint(int mode);
835
836
837 // Filtered rotational update (experimental): P += ct*phi - mu*S(div u*), S = one mask-aware
838 // axis-wise (1,2,1)/4 smoothing pass per axis (one-sided 1/2(d_i+d_nbr) toward the open side at
839 // a solid-centered neighbour, identity when sandwiched). S annihilates the axis checkerboard
840 // including AT wall-adjacent cells; for smooth fields S = I + O(h^2). Steady state unchanged.
841 void setRotationalFilter(bool on, double eps = 0.05);
842
843
844 // Seed/restore the velocity state (CUDA set_state / upload_velocity): u/v/w are inner-cell fields
845 // (flat x-fastest, size nx*ny*nz); written into the velocity block + ghosts refreshed (periodic
846 // wrap).
847 // ISSUES sweep item 5. On the COLLOCATED grid the colour transport rides the MAC face field
848 // `uf_/vf_/wf_` (the only discretely divergence-free field on that grid), which only exists
849 // after a projection. `set_state` / `set_velocity` write the CELL field and used to leave the
850 // face field untouched — all zeros on a fresh solver — and `advect_vof` then ran happily:
851 // its guard measures the FACE field's divergence and a zero field is perfectly solenoidal. So a
852 // benchmark loop written the staggered way produced a "conservative" run in which nothing ever
853 // moved (measured on LeVeque 32^3: max|u| = 63.3, max|uf| = 0.0, max|C - C0| = 0.0 after
854 // `advect_vof`, no throw and no warning).
855 //
856 // Seeding the face field from the cell field with the SAME `centerToFace` map `project()` uses
857 // makes the kinematic entry point mean the same thing on both grids — and it makes the existing
858 // divergence guard MEASURE something: an analytic cell field that is not discretely solenoidal
859 // on the faces is now rejected instead of silently accepted. It does not make the face field
860 // divergence-free (only a projection does), and it is overwritten by the next `project()`, so
861 // nothing downstream changes. Staggered: a no-op (the cell field IS the face field).
863
864
867 void uploadVelocity(const std::vector<double>& uu, const std::vector<double>& vv,
868 const std::vector<double>& ww);
869
870#ifdef PECLET_FLOW_MPI
871
872 // Multi-rank: this rank's IbmSolver is constructed with its LOCAL block dims (= the
873 // BlockDecomposer of the GLOBAL grid for this rank); initMpi wires the g=2 velocity-block halo +
874 // the global-origin red-black parity, and switches fillGhosts/maxOpenDivergence + the pressure MG
875 // (CutcellMG::initMpi) onto their distributed paths. The caller decomposes first (deterministic
876 // ORB) to size the constructor; initMpi re-derives it.
877 void initMpi(int gnx, int gny, int gnz, MPI_Comm comm);
878
879
880 // Shared-decomposition overload: wire the g=2 velocity-block halo from an EXTERNALLY-built ORB
881 // (so flow and dem share one BlockDecomposer for coupled runs, and redistribute() can re-init
882 // onto a re-decomposed partition). The local block size must already match dec.block(rank).size
883 // (set via the constructor / allocateBlock).
884 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec, MPI_Comm comm);
885
886
887 // Redistribute the solver's state onto a NEW decomposition (dynamic load balancing). Enumerates
888 // the registered fields, moves them from the current block layout to the new one (bit-exact via
889 // redistributeGridFields), reallocates every buffer to the new block, re-inits the halo +
890 // pressure MG on the new partition, and rebuilds all geometry-derived state (openness / IBM
891 // overlay / stencils) from the migrated SDF. Velocity + pressure + SDF (+ any registered
892 // scalar/property fields) survive; per-step scratch is rebuilt.
893 void redistribute(const peclet::core::decomp::BlockDecomposer<3>& newDec);
894
895
896 // Redistribute onto the ALIGNED weighted ORB of per-cell weights `w` (global x-fastest,
897 // gnx*gny*gnz). The ergonomic Python entry point for load balancing: the caller passes a weight
898 // field (e.g. fluid work + gamma*particle_count) and both flow and dem rebuild the SAME
899 // deterministic partition from it. No BlockDecomposer object crosses the language boundary.
900 // Returns the alignment 2^a the imbalance budget chose (1 = the plain weighted ORB; 1 also when
901 // not distributed): a co-decomposing code must build its partition from the same `w` AND this
902 // alignment (dem: `migrate_to_weights(w, align=...)`), or the two own different blocks.
903 int rebalanceByWeights(const std::vector<peclet::core::Real>& w);
904
905 // The pressure multigrid hierarchy as built, one row per level in the format of the pure
906 // pre-flight CutcellMG::predict (flow.predict_hierarchy): global dims, ranks holding the level,
907 // this rank's block, ratio to the next level, and whether (and how) the transition out of it
908 // telescopes. Rank 0 holds every level. The test oracle for the forecast.
909 std::vector<CutcellMG::PlanRow> pressureMgPlan() const;
910
911#endif
912
913 // per-face domain BC {face 0..5 = -x,+x,-y,+y,-z,+z}: type 0=periodic,1=no-slip
914 // wall,2=Dirichlet/inflow,3=outflow,4=free-slip/symmetry (zero normal velocity, zero normal
915 // derivative of the tangential components, pressure Neumann like a wall; vx/vy/vz ignored).
916 // Call order (QUALITY_PLAN F milestone iv, refined 2026-09-10): the TYPE decides the operator
917 // openness (wall/inflow closed, outflow open, periodic) and the stencil path, so only a TYPE
918 // CHANGE has to precede the geometry; a VALUE update on a face whose type is unchanged (ramping
919 // an inflow jet, a lid's tangential speed) is allowed at any time and takes effect the next
920 // step -- see the refresh below.
921 void setDomainBc(int face, int type, double vx, double vy, double vz);
922
923
924 // per-position inlet velocity profile on `face` (CUDA set_domain_bc_profile): prof is (nb,nc,3)
925 // on the inner grid of the face's two perpendicular axes; sets the face to inflow (type 2).
926 // Resampled (clamp) to the ghost-inclusive face grid so the BC kernel indexes it directly by face
927 // position. Same call-order rule as set_domain_bc: allowed after the geometry only when the
928 // face is ALREADY inflow (a profile VALUE update), since that is the one case that does not
929 // change the type the operators were built with.
930 void setDomainBcProfile(int face, const std::vector<double>& prof, int nb, int nc);
931
932
933 // Resample the stored raw inlet profile of `face` onto THIS block's ghost-inclusive face grid.
934 void resampleBcProfile(int face);
935
936
937 // all-fluid + domain-BC pressure (CUDA set_pressure_geometry): same path as set_solid with an
938 // open SDF.
939 void setPressureGeometry(const std::vector<double>& sdfInner);
940
941
942
943 // SDF on the inner cells (flat x-fastest, size nx*ny*nz; <0 solid). cutcellPressure enables the
944 // open-face-weighted cut-cell projection (off => velocity-only, e.g. unidirectional body-force
945 // flow).
946 // ---------------------------------------------------------------------------------------
947 // ANALYTIC SCENE (Layer 2 of suite/docs/ANALYTIC_SDF_GEOMETRY.md)
948 //
949 // Geometry has only ever reached flow as an already-sampled field, with "analytic accuracy"
950 // supplied as override arrays computed in Python (set_exact_crossings / set_openness_override) --
951 // spheres only, and SINGLE-RANK only. A scene set here is device-resident and REPLICATED on every
952 // rank, so a rank derives its own block's geometry from it with no communication at all; that is
953 // what lifts the single-rank restriction rather than any new exchange.
954 //
955 // Geometry is expressed in CELL UNITS on the GLOBAL inner grid (cell centre (i,j,k) sits at
956 // (i,j,k)), matching scripts/exact_apertures_spheres.py's centers_cells / radii_cells.
957 //
958 // PERIODICITY is the caller's: the scene is evaluated at global cell coordinates and has no
959 // wrap of its own, so a periodic packing must instantiate its images (or use a node whose eval
960 // is periodic). Nothing here min-images for you.
961 // ---------------------------------------------------------------------------------------
962
973 void setScene(const std::vector<int>& nodeInts, const std::vector<double>& nodeReals,
974 const std::vector<int>& instInts, const std::vector<double>& instReals,
975 bool periodic = false);
976
977
978
981 std::array<double, 3> instanceCenter(int i) const;
982
983
984 bool instanceCenterPinned(int i) const;
985
986
987
996 void setInstanceMotion(int i, const std::array<double, 3>& lin,
997 const std::array<double, 3>& ang, const double* center = nullptr);
998
999
1000
1008 void setInstanceTransform(int i, const std::array<double, 3>& translation,
1009 const std::array<double, 4>& quat);
1010
1011
1012
1027 void rebuildGeometry();
1028
1029
1030
1046 void seedFreshCells(CCField mOld[3]);
1047
1048
1049
1053 void setFreshCellSeed(bool on);
1054
1055
1056 bool freshCellSeed() const;
1057
1058
1059
1072 void refreshWallVelocity();
1073
1074
1075
1078 bool hasMovingInstance() const;
1079
1080
1081 int sceneInstanceCount() const;
1082
1083
1084
1089 void setWallFluxDivergence(bool on);
1090
1091
1092 bool wallFluxDivergence() const;
1093
1094
1095
1096 bool hasScene() const;
1097
1098
1099
1102 std::vector<int> getCutOwner() const;
1103
1104
1105
1109
1110
1111
1119
1120
1125 std::vector<long> movingInstanceCutCells() const;
1126
1127
1129 std::vector<long> movingInstanceDegeneratePoints() const;
1130
1131
1132
1135 long periodicImageOverlapCells() const;
1136
1137
1138
1149
1150
1151
1154 void setSolid(const std::vector<double>& sdfInner, bool cutcellPressure);
1155
1156
1157
1162 void buildVelocityOverlays(bool resetU);
1163
1164
1165
1172
1173
1177
1178
1179 // ---- setSolidDevice stages (QUALITY_PLAN G.1): pure cut-and-paste, each a contiguous
1180 // block of the original function sharing only member fields and a CCExec. ----
1181 void setSolidSelectScheme();
1182
1183
1185
1186
1188
1189
1191
1192
1193 void initVelocityMg();
1194
1195
1196 void setSolidBuildOpenness();
1197
1201
1206
1207
1208 void setSolidStarOverlay();
1209
1210
1212
1213
1215
1216
1217 bool geometryBuilt() const;
1218
1219
1220 void requireNoGeometry(const char* who) const;
1221
1222
1223
1224 void step();
1225
1226
1227
1246 double maxReverse = 0.0, fraction = 0.0, energyInflux = 0.0;
1247 long reversed = 0, total = 0;
1248 };
1250
1251
1252 // velocity component c (0=u,1=v,2=w) on the inner cells, flat x-fastest [nx*ny*nz].
1253 std::vector<double> getVelocity(int c);
1254
1255
1259 void setVelocity(int c, const std::vector<double>& v);
1260
1261
1262 // The divergence-free FACE velocity component (collocated: the projected MAC face field
1263 // uf_/vf_/wf_, exactly div-free; staggered: C[c].u already lives on the faces). For a periodic
1264 // bed its mean is the momentum-balance superficial velocity, unperturbed by the openness-aware
1265 // cell gradient correction (projectCorrectCenter) that biases the cell-field mean at cut cells.
1266 std::vector<double> getFaceVelocity(int c);
1267
1268
1269 // TEMP DIAGNOSTIC: the face openness (fluid area fraction) used by the cut-cell projection.
1270 // component c: 0 -> ox_ (low -x face of each inner cell), 1 -> oy_, 2 -> oz_. Grid-independent
1271 // (built once from the SDF). Exposed to compare the open-weighted superficial flux against the
1272 // raw velocity mean.
1273 std::vector<double> getOpenness(int c);
1274
1275
1276 // Diagnostic read-out of the ASSEMBLED momentum-operator diagonal of component c — the float
1277 // stencil `AC` after the diffusion build, the Robust-Scaled cut-cell bake and, under implicit
1278 // drag, `addDragDiagonal`'s face drag beta_f — as an x-fastest (nx,ny,nz) inner-region host
1279 // buffer. Read-only; no solver state is touched. Added for WO-I's
1280 // `tests/kokkos_mpi/test_dragbeta_ghost_mpi.cpp`, which must gate the FACE drag mean where it is
1281 // formed: on a periodic box the projection homogenizes a single bad plane into a uniform mean
1282 // shift of the velocity, so a velocity-only gate sees THAT the drag was wrong but not WHERE.
1283 std::vector<double> getMomentumDiagonal(int c);
1284
1285
1286 // The openness whose face fluxes the PROJECTION conserves: the binary (COUPLED) openness in
1287 // ghost-projection mode (oxb_ — the geometric ox_ stays a diagnostic there), the geometric
1288 // cut-cell openness otherwise. This is what flux bookkeeping downstream of the solve must use
1289 // (e.g. peclet.pnm's extract_network_flow): sum(o_proj*u*A) over a cell's faces IS the
1290 // discrete divergence the projection drives to zero.
1291 std::vector<double> getOpennessProj(int c);
1292
1293
1294 std::vector<double> getPressure();
1295
1296
1297 // WO-R: the divergence of the field the projection ACTUALLY produced, outflow correction
1298 // included. `maxOpenDivergence()` below re-imposes the zero-gradient outflow face before
1299 // measuring (its own comment says so) — which both destroys `bcCorrectOutflow`'s correction as a
1300 // side effect and reports the divergence of a field the solver never used. On an open-boundary
1301 // two-phase box that artefact is the dominant number: measured 5e-3, flat in the iteration
1302 // count, flat in the density ratio and bit-identical in a `-DPECLET_FLOW_OPERATOR_DOUBLE` build —
1303 // i.e. not a solver residual at all. This sibling fills the ghosts with `doOutflow = false`,
1304 // exactly as `step()` does after `project()`, and leaves the velocity field alone.
1305 //
1306 // Kept as a SIBLING rather than a change of default: every recorded open-boundary number in the
1307 // repo was taken with the mutating one, and re-baselining them is not this work order's call.
1309
1310
1313
1314
1315 double maxOpenDivergence();
1316
1317
1320
1321
1322 // Residual of the volume-averaged continuity, max|div(open*eps*u) + d(eps)/dt| — the quantity the
1323 // porous projection actually drives to zero (NOT the velocity divergence, which is -d(eps)/dt !=
1324 // 0 in a fluidizing bed). Meaningful only with set_porous_continuity(True); returns 0 otherwise.
1325 double maxPorousResidual();
1326
1327
1328 long lastPressureIterations() const;
1329
1330
1331 // D3 (doc/vof_step_performance_design.md §5.13): how many pressure solves on warm-started
1332 // Chebyshev bounds tripped the guard (the cap, or a residual above r0 after 3 iterations) and
1333 // were redone on cold bounds, since construction.
1334 long numPressureChebyshevRestarts() const { return chebColdRestarts_; }
1335
1336
1337 // The pressure multigrid's per-level coarsening ratio, one {rx, ry, rz} per level
1338 // (doc/anisotropic_metric.md §5). On an isotropic domain this is today's table; on a stretched
1339 // one the aspect rule defers an axis while it is at least the aspect threshold (2) times
1340 // coarser than the finest coarsenable one. Empty until the cut-cell operator exists.
1341 std::vector<std::array<int, 3>> pressureMgLevelRatios() const;
1342
1343
1344 // ISSUES sweep item 6: did the last pressure solve break down (non-finite
1345 // preconditioner output / recurrence scalar)? A failing solve also reports the
1346 // iteration cap through `lastPressureIterations()`.
1347 bool pressureSolveFailed() const;
1348
1349
1350 // The state of the steady march (doc/steady_acceleration.md §3.1, §5.1, rev 1): exactly what
1351 // step() reads across steps, as the full padded G = 2 buffers, with their roles for the Anderson
1352 // accelerator's metric. Velocity = the three velocity components (measured); Carried = the
1353 // accumulated pressure P and, on the collocated grid with projected-face advection, the face
1354 // field uf/vf/wf (mixed and differenced, never measured). `signature` is (dt, rho, mu, Fx, Fy,
1355 // Fz, advection on, advection scheme, implicit advection), all internal: a change of any of them
1356 // changes the map (the last three: review R5).
1357 struct MarchState {
1358 std::vector<CCField> fields;
1359 std::vector<peclet::core::solver::AndersonRole> roles;
1360 C3 e{0, 0, 0};
1361 int G = 2;
1362 std::array<double, 9> signature{};
1363#ifdef PECLET_FLOW_MPI
1364 MPI_Comm comm = MPI_COMM_NULL;
1365 bool distributed = false;
1366#endif
1367 };
1368
1369
1370 // The march state of this solver (above), for the Anderson accelerator
1371 // (src/anderson_accelerator.hpp). Public C++, not bound. Throws std::runtime_error naming the
1372 // FIRST configuration the steady acceleration refuses (doc/steady_acceleration.md §5.3, D10): a
1373 // collocated scheme other than the fluid-only 'ghost' projection; phase change; VoF; transported
1374 // scalars; porous continuity; variable density or viscosity; a drag_beta field; cell force
1375 // fields; moving scene instances; a superficial-velocity target; the pressure warm start; the
1376 // balanced-force projection. A re-evaluation of host flags only, so the accelerator repeats it
1377 // at every step.
1378 MarchState marchState();
1379
1380
1381 // Per-phase wall times of the last step() in seconds, THIS RANK (device-fenced at each phase
1382 // boundary): predictor = ghost fills + RHS/advection/stencil builds, momentum = the per-component
1383 // implicit-diffusion solves, projection = the cut-cell pressure projection; step = the whole
1384 // step() (remainder = BC re-imposition, Picard bookkeeping, scalars). The allreduce pair is the
1385 // pressure solve's global-reduction tax (time in / count of MPI_Allreduce; 0 single-rank).
1386 double lastStepSeconds() const;
1387
1388
1389 double lastPredictorSeconds() const;
1390
1391
1392 double lastMomentumSeconds() const;
1393
1394
1395 double lastProjectionSeconds() const;
1396
1397
1398 // ---- WO-V9: the VoF pipeline's own per-stage timers ----------------------------------------
1399 //
1400 // `set_vof_timing(True)` arms them; they are OFF by default and, when off, cost one predictable
1401 // branch per stage and no fence. A phase boundary on a device backend has to fence or the
1402 // queued work of one stage is billed to the next — the same rule `phaseTick()` already applies
1403 // to the step's three coarse phases — so the boundaries fence WHEN ARMED and only then. A fence
1404 // reorders nothing and computes nothing: the gate is that the same run with timing on and off
1405 // produces bit-identical fields (`tests/kokkos/test_vof_timing.cpp`).
1406 //
1407 // The stages are cumulative over steps since the last reset; `steps` counts step() calls with
1408 // the instrument armed, so every number divides down to a per-step cost.
1409 struct VofTiming {
1410 double advect = 0.0;
1411 double bridge = 0.0;
1412 double momAdvect = 0.0;
1413 double momBridge = 0.0;
1414 double curvature = 0.0;
1415 double csf = 0.0;
1416 double phaseChange = 0.0;
1417 double blockAdvect = 0.0;
1418 double blockDebris = 0.0;
1419 long steps = 0;
1420 };
1421 void setVofTiming(bool on);
1422
1423
1424 bool vofTiming() const;
1425
1426
1427 void resetVofTiming();
1428
1429
1430 const VofTiming& vofTimingReport() const;
1431
1432
1434
1435
1437
1438
1442 void setVofCurvatureWorklist(bool on);
1443
1444
1445 bool vofCurvatureWorklist() const;
1446
1447
1448 double vofTimingStepSeconds() const;
1449
1450
1451 double vofTimingPredictorSeconds() const;
1452
1453
1454 double vofTimingMomentumSeconds() const;
1455
1456
1457 double vofTimingProjectionSeconds() const;
1458
1459
1462 void setVofWorklist(bool on);
1463
1464
1465 bool vofWorklist() const;
1466
1467
1468 double lastPressureAllreduceSeconds() const;
1469
1470
1471 long lastPressureAllreduceCount() const;
1472
1473
1474 int nx() const;
1475
1476
1477 int ny() const;
1478
1479
1480 int nz() const;
1481
1482
1483
1484 private:
1485 struct Comp {
1486 CCField u, b, inhom, rscale, mask;
1487 FV AC, AW, AE, AS, AN, AB, AT;
1488 IbmOverlay ov;
1489 Kokkos::View<int*, CCMem> idMap;
1490 Kokkos::View<int, CCMem> counter;
1491 int nCut = 0;
1492 };
1493
1494 public: // nvcc forbids extended __host__ __device__ lambdas inside private/protected members.
1495 // Advection treated implicitly (implicit-FOU upwind + deferred correction): the user opt-in
1496 // (set_implicit_advection) on any path, OR the DEFAULT on the domain-BC path (inflow/outflow),
1497 // where explicit advection is unstable. The velocity-MG BC path carries its own FOU coarse
1498 // operator, so the default does not apply there (it still honours the explicit opt-in).
1499 // Advection is implicit (first-order upwind in the stencil, stable at large dt) when the user
1500 // asks for it, and ALWAYS on the domain-BC stencil path -- which includes the mixed velocity MG
1501 // (solid + domain BCs), so switching that solver on does not silently change the momentum
1502 // discretization. The all-fluid domain-BC velocity MG (folded const-coefficient operator) keeps
1503 // explicit advection, as before.
1504 bool implicitAdv() const;
1505
1506
1507 // Domain-BC momentum solved via the Robust-Scaled cut-cell / FOU stencil smoother
1508 // (ibmRbgsStencilColor
1509 // + reflection-ghost BCs), not the all-fluid const-coeff fold. Needed when (a) an immersed solid
1510 // is present (cut-cell no-slip must be in the operator), or (b) advection is implicit (the FOU
1511 // upwind lives in the stencil -> stable at large dt, the fully-implicit design), or (c) any
1512 // per-cell coefficient lives in the stencil: variable properties, or the implicit CFD-DEM drag
1513 // diagonal (hasDrag_). Without (c) an all-fluid domain-BC problem fell through to the
1514 // CONST-COEFFICIENT fold smoother (Ac = rho/dt + 6mu computed inline), which never reads the
1515 // assembled band -- the drag never entered the momentum operator while the porous projection's
1516 // w_f=idt/(idt+beta_f) assumed it did, an inconsistency with pressure-loop gain beta*dt/rho (a
1517 // fixed bed diverged whenever beta > rho/dt; measured gain 3.84 vs predicted 3.85 at beta=77,
1518 // idt=20).
1519 // Domain BCs solved with the (unfolded) cut-cell / FOU stencil and reflection ghosts, as
1520 // opposed to the folded const-coefficient smoother. Decides the RHS treatment too (the fold's
1521 // RHS correction applies only off this path), so it must agree with the solver actually used:
1522 // the MIXED velocity MG (solid + domain BCs) runs on this stencil and is therefore ON this path,
1523 // while the all-fluid domain-BC velocity MG is the folded operator and is not.
1524 bool bcStencilPath() const;
1525
1526
1527 // The mixed velocity MG: solid + domain BCs, diffusion-dominated constant-property momentum
1528 // (implicit advection / variable properties / drag stay on RB-GS: their fine stencils are not
1529 // approximated by the staircase Helmholtz).
1530 bool mixedVelocityMg() const;
1531
1532
1533 // Fill a property field's ghosts for the face means: periodic/halo base, then zero-gradient
1534 // (copy) on domain-BC (wall/inflow/outflow) faces — a periodic wrap there would bring the wrong
1535 // layer's value to the wall face (destabilising, especially for the harmonic mean).
1536 // Distributed: the override is per-face rank-OWNED (`touchesGlobalFace`), exactly as
1537 // `applyScalarBc` does — the halo fill runs first (and periodic-wraps the global boundary ghost),
1538 // the BC overwrite wins on the rank that owns the face. The former `if (!distributed_)` guard
1539 // keyed on the wrong predicate: it dropped the override at EVERY np including 1.
1540 // `edges` extends the BC copy over the tangential ghost rows (the edge/corner ghosts), for a
1541 // consumer that reads diagonal neighbours (the staggered cross-face viscosity).
1542 void fillPropGhosts(CCField f, bool edges = false);
1543
1544
1545 void fillMuGhosts();
1546
1547
1548 // Ghost ring of the per-cell body-force fields ("force_x/y/z") — WO-G.
1549 //
1550 // Neither writer of these fields fills their ghosts: `applyClosure` writes the INNER cells only
1551 // ("ghosts untouched — refilled by the field's own exchange", `property_closures.hpp`), and the
1552 // external CFD-DEM writer (`field_view` + `exchangeFieldAdd`) folds its ghost-band deposit onto
1553 // the owners but leaves the ghost band holding that deposit residue. Nothing else exchanged them,
1554 // so `buildRhsVar`'s face interpolation `0.5*(fb(i) + fb(i - s_c))` read the registration zero (or
1555 // the residue) on the first inner plane of every block — the face body force came out exactly
1556 // HALVED at every rank boundary, and single-rank at the periodic wrap plane. Net effect on a
1557 // periodic axis: a body-force deficit of 1/(2*N_axis) on the whole domain, because the projection
1558 // removes the non-uniform part and what survives is the (deficient) mean.
1559 //
1560 // WHY THE PROPERTY POLICY IS THE RIGHT ONE, even though a body force is not a transported
1561 // property. `buildRhsVar` face-interpolates the force with the SAME arithmetic mean it uses for
1562 // the momentum time term and the projection coefficient face-interpolate rho, and the physical
1563 // content of that pair is the acceleration f_f/rho_f (this three-way consistency is what makes
1564 // the discrete hydrostatic balance exact — `doc/variable_density_projection.md` §1/§3). Whatever
1565 // ghost policy rho has, the force must have the SAME one or the ratio breaks at a boundary. rho
1566 // uses `fillPropGhosts` (halo/periodic base, then Neumann copy on a rank-OWNED domain-BC face),
1567 // so the force does too, per BC type:
1568 // * WALL / inflow (Dirichlet). The ghost feeds only the face force of the wall-NORMAL component
1569 // ON the boundary plane — the one unknown the Dirichlet BC pins (`bcVelocityComp`, comp == a:
1570 // `at(bf) = wall`) and whose flux openness is 0. So the value is unobservable there today,
1571 // which is exactly why the hydrostatic acid test passed at 2.75e-17 with the defect present.
1572 // Neumann copy is still the right answer: it is the only choice that keeps f_f/rho_f equal to
1573 // the intended acceleration if that pin is relaxed (free-slip / stress BC), and "zero" would
1574 // assert that the volumetric source stops at the wall. A body force is a SOURCE, not a flux —
1575 // there is no reflection or odd-extension principle to invoke, only extrapolation, and the
1576 // piecewise-constant (Neumann) extrapolation is O(h), the same order as rho's own ghost.
1577 // * OUTFLOW. Zero-gradient is what every other quantity gets there, and a zero ghost would
1578 // halve the body force on the outlet face — this same defect, relocated to the outlet.
1579 // So the policy does NOT differ per BC type; `fillPropGhosts` is used verbatim.
1580 //
1581 // Consumer note: on the MAC grid EVERY RHS kernel reads the ghost -- `buildRhsForced`,
1582 // `buildRhsVar` and `buildRhsVarMom` all place the force at the face by the same
1583 // `Grid::atVelocity` mean (the constant-density kernel used to read the cell value alone, half a
1584 // cell off the face; fixed 2026-09-25). On the collocated grid the cell value is the placement and
1585 // the fill is inert -- applied unconditionally anyway, so the field's ghost contract does not
1586 // depend on which RHS kernel happens to consume it.
1587 void fillCellForceGhosts();
1588
1589
1590 // Ghost ring of the per-cell drag coefficient "drag_beta" — WO-I.
1591 //
1592 // Same defect class as the body force above, one phase earlier in the step. Under `porous_`,
1593 // `addDragDiagonal` builds the staggered momentum diagonal from the FACE drag
1594 //
1595 // beta_f(i) = 0.5*(beta(i) + beta(i - s_c))
1596 //
1597 // and all three of its call sites (`rebuildStencils`, `buildAdvStencil`, `buildAdvStencilVar`)
1598 // run at/after the TOP of `step()`. But no writer of `drag_beta` fills its ghosts: `setField` and
1599 // `applyClosure` write the inner cells only, and the external CFD-DEM writer's driver FOLDS its
1600 // ghost-band deposit onto the owners and then ZEROES that band (single rank) or leaves the
1601 // reverse-halo residue in it (MPI). The only `fillPropGhosts(dragBeta_)` used to be inside
1602 // `project()` — i.e. AFTER the momentum build. So on the first inner plane of every block the
1603 // momentum diagonal was assembled from a stale/zero ghost while the projection's coefficient
1604 // (`buildPorousCoeffDrag`/`Cons`) and its correction (`projectCorrectPorous*`) used the freshly
1605 // exchanged value on that SAME face.
1606 //
1607 // That mismatch is exactly what `addDragDiagonal`'s own comment warns about: the incremental
1608 // pressure loop then has gain (idt + beta_f)/(idt + beta_f^momentum) instead of 1, and "the
1609 // accumulated pressure diverges exponentially" — here localized to block/wrap boundaries rather
1610 // than to the bed top. With the CFD-DEM writer's zeroed ghost the momentum diagonal on that plane
1611 // carried beta/2 against the projection's beta, a factor-2 error in the drag, not round-off.
1612 //
1613 // WHY THIS CALL SITE. It is the only point that is after BOTH writers — a closure targeting
1614 // "drag_beta" (applied by `updateProperties()` immediately above) and the external deposit (which
1615 // happens before `step()` is entered) — and before the FIRST consumer, the momentum stencil build
1616 // a few lines below. Nothing inside `step()` writes `drag_beta`.
1617 //
1618 // WHY `fillPropGhosts` AND NOT SOME OTHER POLICY. There is no freedom here: `project()` already
1619 // fills this very field with `fillPropGhosts` (halo/periodic base + Neumann copy on a rank-OWNED
1620 // domain-BC face), and the whole point is that the momentum diagonal and the projection
1621 // coefficient must agree on beta_f face by face — the three-way consistency
1622 // `doc/variable_density_projection.md` §1/§3 states for (time term, body force, projection
1623 // coefficient) and `doc/porous_drag_scheme.md` §2 states for (diagonal, operator, correction).
1624 // Any policy other than the one `project()` uses would re-create the mismatch it is fixing.
1625 //
1626 // `project()`'s fill is therefore REDUNDANT after this one (nothing writes `drag_beta` in
1627 // between). It is kept deliberately: removing it is a separate change, `project()` must keep its
1628 // own ghost contract for any future mid-step writer, and it costs one exchange on the porous path
1629 // only.
1630 //
1631 // Gated on `hasDrag_` (the field exists iff `enableDrag()` ran), not on `porous_`: the field's
1632 // ghost contract should not depend on which consumer happens to read it. Since 2026-09-25
1633 // `addDragDiagonal` takes the face mean on the MAC grid on the non-porous path too, so the fill
1634 // is live there as well; it is inert only on the collocated grid (cell value).
1635 void fillDragBetaGhosts();
1636
1637
1638 // Eps ghost policy for the porous (volume-averaged) machinery. Periodic/halo base fill, then at
1639 // non-periodic domain faces: wall -> zero-gradient; INFLOW/OUTFLOW -> mirror around 1 so the
1640 // arithmetic face mean is EXACTLY 1 (the boundary is pure gas: below the distributor and in the
1641 // freeboard eps = 1, so a prescribed inflow velocity is the SUPERFICIAL gas velocity and its face
1642 // flux is open_f*1*u — the Kuipers/MFIX distributor convention). Every consumer — the projection
1643 // RHS divergence, the Poisson coefficients, and maxPorousResidual — must use THIS fill: the
1644 // external deposit writes its own leakage into these ghosts each step, and any two consumers
1645 // reading different ghost values enforce two different constraints, which leaves an irreducible
1646 // residual (eps_f_rhs - eps_f_resid)*u_in pinned at the distributor row and feeds gas at
1647 // eps_f*U instead of U.
1648 void fillPorousEpsGhosts();
1649
1650
1651 // --- VoF internals (rung V2a, WO-J) ---------------------------------------------------------
1652 // Allocate the colour field's own g=3 working block and wire its ghost/all-reduce hooks. Called
1653 // by enableVof() and again by any path that re-sizes the block (redistribute -> initMpi), since
1654 // the advector's block must track the solver's.
1655 void buildVofBlock();
1656
1657
1658 // --- rung V5a (WO-Q): the cut-cell geometry of the colour block ------------------------------
1659 //
1660 // The advector needs, on ITS g=3 block and in ITS high-face index convention, the face openness
1661 // `o_d` and the cell fluid fraction `eps`. Both are built here and both are then run through the
1662 // colour field's OWN ghost policy (`vofFillGhosts`), which is what makes the classification at
1663 // the outermost ghost layer the owner's classification rather than a locally-guessed one — the
1664 // solid-band fill of `cutcell.hpp` reads fluid neighbours at ghost depth 3, so a wrong
1665 // classification there would be a decomposition dependence in the INNER result.
1666 //
1667 // `eps` comes from `buildCellFraction` (mac_approx_projection.hpp: 4^3-subsampled trilinear SDF),
1668 // which the collocated path already uses for `cs_`. `cs_` is allocated only on the collocated
1669 // grid, so the staggered path gets its own `vofCs_` here.
1670 //
1671 // The openness embed is `vof::copyFaceVelocity`, i.e. THE SAME shifted embed the face velocity
1672 // uses — because it is the same face. `ox_(i)` is the openness of the `-x` face of cell `i` and
1673 // the advector wants the `+x` face of cell `i`, exactly the low-face -> high-face shift of
1674 // `colour_field.hpp`. Using a concentric embed here instead is the openness twin of WO-J's 35 %
1675 // conservation defect and is what gate G3 exists to catch.
1676 // ISSUES sweep item 3. Which domain faces are WETTING WALLS for the colour band fill: a
1677 // type-1 no-slip or type-4 free-slip face, i.e. an impermeable one, and only while a contact
1678 // angle is actually set. Returned as a per-face bitmask (bit 2a+s), 0 when the feature is unused
1679 // -- which is what keeps every existing path byte-identical.
1680 //
1681 // A domain wall IS a flat SDF wall sitting exactly on the boundary face; the only reason
1682 // `set_contact_angle` used to be silently ignored there is that the band fill classifies cells
1683 // from the colour block's cut-cell GEOMETRY, and an all-fluid solver has none. So the repair is
1684 // not a new fill rule -- it is a SYNTHESISED geometry for those faces (`applyDomainWallGeometry`)
1685 // plus the matching wall SDF (`applyContactAngle`), after which WO-S's theta pass, WO-Q's
1686 // passes 2-3, the branch census and the V6 dynamic angle all run unchanged, with `n_w` coming
1687 // out of the same central difference and equalling the inward face normal by construction.
1688 int vofWetWallMask() const;
1689
1690
1691 // Signed distance (cell units, POSITIVE inside the domain) from a cell centre at GLOBAL index
1692 // (gx,gy,gz) to the nearest wetting domain wall plane. Container-free so a device lambda can
1693 // call it. `+inf` when no face is a wetting wall.
1694 KOKKOS_INLINE_FUNCTION static double vofWallPlaneSdf(int gx, int gy, int gz, I3 gs, int mask);
1695
1696
1697 void buildVofGeometry();
1698
1699
1700 // ISSUES sweep item 3: close the colour block's out-of-domain band across every wetting domain
1701 // wall, so `classifyGeometry` calls those ghost cells SOLID and WO-S's theta pass owns them.
1702 //
1703 // The rule is the one the SDF path uses, evaluated on an exact plane: a cell whose centre lies
1704 // outside the wall has fluid fraction 0, and a face is open only if BOTH of its cells are
1705 // inside. That closes the boundary face itself (which the flux openness already closes, so
1706 // nothing in the projection moves) AND all six faces of every band cell, which is what
1707 // `vofIsSolidCell` requires. Composes with an SDF solid: the solid's eps/openness are simply
1708 // masked to zero outside the wall.
1709 void applyDomainWallGeometry(int mask);
1710
1711
1712 // ISSUES sweep item 3: mark the out-of-domain band SOLID after the classification exchange
1713 // (see the call site for why the exchange undoes it).
1714 void imposeDomainWallKind(int mask);
1715
1716
1717 // ISSUES sweep item 3: the outermost (depth-3) band layer of a wetting DOMAIN wall is the one
1718 // cell no fill pass can write -- the passes stop at ghost depth 3-k and their 6-point stencil
1719 // would index outside the g=3 block there -- and the zero-gradient clamp that used to supply it
1720 // is now skipped (it would wipe the theta band). Continue the band outward instead: depth 3
1721 // takes depth 2's value, the zero-slope continuation of the theta plane rather than of the first
1722 // INNER cell's colour. For an SDF wall this cell is an ordinary solid cell and nothing here
1723 // applies.
1724 void vofExtendWallBand(CCField f, int mask);
1725
1726
1727 // Zero the canonical G=2 colour field inside solid cells (see buildVofGeometry).
1728 void zeroSolidColour();
1729
1730
1731 // Is axis `a` periodic for the colour field? flow's per-face bc_ is 0 (periodic) on BOTH ends of
1732 // a periodic axis, so an axis is periodic iff neither of its faces carries a domain BC.
1733 bool vofAxisPeriodic(int a) const;
1734
1735
1736 // The colour field's ghost policy on its own g=3 block: halo/periodic base, then zero-gradient
1737 // (globally clamped) on every non-periodic axis. Zero-gradient is the same policy the material
1738 // properties get (`fillPropGhosts`) — a wall neither creates nor destroys colour, and the MYC
1739 // stencil of an inner boundary cell must see a plausible continuation rather than a wrap from the
1740 // far side of the domain. Prescribing C at an inflow face is a V5+ concern (it needs a flux BC,
1741 // not a ghost value) and is not offered here.
1742 void vofFillGhosts(CCField f);
1743
1744
1745 void vofExchangeRaw(CCField f);
1746
1747
1748 I3 vofGlobalSize() const;
1749
1750
1751 // Global index of this block's inner cell (0,0,0). og_ is exactly that (originInclGhost + G).
1752 I3 vofOrigin() const;
1753
1754
1755 // Face velocities -> the advector's g=3 block. The advecting field must be the PROJECTED one
1756 // (see advectVof), and its ghost ring must be valid because the advector reads the `-d` face of
1757 // the first inner cell, which is a ghost cell's `+d` face. fillVelGhosts is the solver's own
1758 // halo+domain-BC fill and is exactly what the Picard loop does at the top of every iteration, so
1759 // calling it here leaves the velocity ghosts in the state the next consumer would have produced.
1760 void bridgeVelocityToVof();
1761
1762
1763 // --- rung W0: the block container's view of this rank's patch --------------------------------
1764 // The owned inner box of every rank, in global cells — the table the block gather/scatter pieces
1765 // are cut against. It is a function of the CURRENT decomposition, so it is built here rather
1766 // than inlined at `enable_vof_blocks`: a redistribute has to push the new one through
1767 // `bindVofBlockPatch` or every piece keeps addressing the previous partition.
1768 std::vector<vof::VofBox> vofBlockRankBoxes(int size) const;
1769
1770
1771 // The gather reads the face velocity in the ADVECTOR's high-face convention on the g=3 block,
1772 // i.e. exactly what `bridgeVelocityToVof` writes, and the scatter writes the union into that
1773 // same block's colour. `buildVofBlock` reallocates those Views, so the binding is refreshed
1774 // there — a stale View here would silently gather from freed memory.
1775 void bindVofBlockPatch();
1776
1777
1778 // The union colour on the g=3 block -> the canonical registered "C" (+ its ghost policy, the
1779 // same `fillPropGhosts` rho and mu are derived through).
1780 void harvestVofBlockUnion();
1781
1782
1783
1784 // Colour: G=2 registry mirror -> the g=3 working block, then the colour field's own ghost policy.
1785 // Inner cells only in the copy — the two blocks have different ghost extents and each fills its
1786 // own (the one bridge; see the enableVof note).
1787 void bridgeColourToVof();
1788
1789
1790
1791 // Staggered face stride of velocity component c (the -c face of cell i pairs cells i and i-s).
1792 long strideOf(int c) const;
1793
1794
1795 // The face-property accessor for the momentum stencil of component c: mu constant-or-field
1796 // (arithmetic/harmonic mean), rho constant-or-field (arithmetic face mean for the time diagonal —
1797 // the same face density the variable-density projection uses).
1798 // Effective variable density: true varRho, or the eps-conservative porous momentum (rho_eff =
1799 // eps*rho in epsRho_, refreshed per step by updateEpsRho).
1800 bool effVarRho() const;
1801
1802
1804
1805
1806 // --- rung V8: the collocated variable-density / CSF path ---------------------------------------
1807 //
1808 // TRUE exactly on the configurations that used to throw outright on this grid — variable density
1809 // (`set_density_mode`) and surface tension (which needs `enable_vof`) on `SolverColocated` — so
1810 // every validated collocated path (constant density, no VoF: `benchmarks/staggered-vs-collocated`,
1811 // the colocated regression baselines) and the whole staggered solver take the same branches they
1812 // always did, byte for byte.
1813 //
1814 // When it is on, the step is the mass-adjoint ABC pair of doc/collocated_varrho_forces.md: the
1815 // pressure and every force inside the implicit predictor (`buildRhsColoVar`), the momentum-weighted
1816 // centre-to-face map in the constraint, and the face-to-cell reconstruction of the face correction
1817 // as the cell correction — see `collocated_varrho.hpp`.
1818 bool colocatedFaceForce() const;
1819
1820
1821 // The AUTO collocated scheme (set in setSolid/setPressureGeometry) picks the GHOST projection when
1822 // the configuration allows it, and the ghost v1 supports neither variable density nor the V8 face
1823 // force. `set_density_mode` / `enable_vof` can be called AFTER the geometry, so re-run the same
1824 // fallback here rather than failing later inside project(). An explicit scheme selection has
1825 // already cleared colSchemeAuto_ and is left alone (it will hit the loud throw instead).
1826 void collocatedV8AutoFallback(const char* why);
1827
1828
1829 void ensureFaceAcc();
1830
1831
1832 // Guard rail for rung V8's scope. The collocated variable-density / face-force path is validated
1833 // ALL-FLUID (`set_pressure_geometry`); an immersed solid on it would need the cut-cell face
1834 // acceleration AND the one-sided (gauge-exact / ghost) closures to agree with the face averaging
1835 // operator, which is a separate derivation. Fail loudly instead of half-supporting it.
1836 void requireCollocatedFaceForceScope(const char* who);
1837
1838
1839 // --- the balanced-force projection (doc/collocated_varrho_forces.md §4.6) --------------------
1840 // Is it on for this step? (the explicit setting, else ON exactly on the V8 path without an
1841 // inflow/outflow face)
1842 bool balancedForceActive() const;
1843 bool hasOpenDomainFace() const; // any inflow (2) / outflow (3) domain face
1844 // The §4.6.4 refusals. `atStep` adds the checks that are only final once the solver is set up
1845 // (the cut-cell operator, the collocated density path, an AUTO-selected ghost scheme).
1846 void requireBalancedForceScope(const char* who, bool atStep) const;
1847 // (B2) faceAcc_[c] = c * beta on the face range [G, e-G] (no openness: the divergence applies it).
1848 void buildBalancedFaceForce(int c);
1849 // Register "p_balanced" (zero) as soon as the option is active, so a restart can restore it
1850 // with set_field before the first step. Called by every setter that can switch it on.
1852 // B1-B5, once per step before the Picard loop.
1854 // (B4) the increment solve with the full-RHS stop (WO-P5); pb1_ in/out, rhs1_ the RHS; r0 its
1855 // initial residual (the divergence test of the kept Chebyshev bounds).
1856 long solveBalancedForceSystem(double bref, double r0);
1857
1858
1859 void updateEpsRho();
1860
1861
1863
1864
1865 // Mirror the host motion arrays onto the device (KBs; rebuilt only when a driver changes a
1866 // body's velocity, not per step).
1867 void buildSceneQuery();
1868
1869
1870
1871 void refreshMotionFlag();
1872
1873
1874
1875 void uploadMotion();
1876
1877
1878
1879 peclet::core::geom::InstanceMotionView<double> motionView() const;
1880
1881
1882
1883 // MOVING GEOMETRY (Layer 3 rungs 2-3): sample the scene's KINEMATIC WALL VELOCITY onto the grid.
1884 //
1885 // At each probe p (component c's staggered point for rung 2; the cell centre for rung 3):
1886 // n_hat = central difference of the SAMPLED sdf_, normalised -- O(h), the v1 fidelity
1887 // w = p - sdf(p) * n_hat -- core's geom::wallPoint
1888 // u_w = geom::instanceVelocity(owner(p), w)
1889 //
1890 // THE OWNER IS QUERIED AT p, not read from the cell-centred cutOwner_ field. At a contact
1891 // between two bodies the staggered point and the cell centre can belong to different ones, and
1892 // a wall velocity taken from the wrong body is precisely the error this rung exists to avoid.
1893 //
1894 // Per-direction crossing-point placement (via tEx_) would put the wall point on the exact
1895 // crossing instead of along the gradient; that is the documented refinement, deliberately not
1896 // taken in v1 -- see the design note.
1897 void buildWallVelocity();
1898
1899
1900
1905 void exchangeExtRaw(CCField f);
1906
1907
1908
1909 // MOVING GEOMETRY rung 3: the wall's own volume flux, folded into the cell divergence.
1910 //
1911 // A rigid body sweeping through a cut cell injects a net flux through the WALL part of the
1912 // cell's fluid boundary; it is zero only integrally over a closed body, never cell by cell.
1913 // The wall area VECTOR is exact from the aperture identity -- apply the divergence theorem to
1914 // the constant field e_a over the cell's fluid region and the open-face terms telescope:
1915 // A_wall = -(oE - oW, oN - oS, oT - oB) (in h=1 cell units, matching divergOpen)
1916 // so continuity over the fluid region reads div_open(u) + u_w . A_wall = 0. divergOpen has
1917 // already written the first term into `d`; this adds the second, leaving rhs = -(d) untouched
1918 // in form. Inert unless a moving instance exists.
1920
1921
1922
1949 void scaleForceTorque(std::vector<double>& out, std::size_t m) const;
1950
1951
1952
1965 std::vector<double> hydroForceTorque();
1966
1967
1968
2063 std::vector<double> hydroForceTorqueReaction();
2064
2065
2066
2069 std::array<long, 3> fluidMomentumCells();
2070
2071
2072
2086 std::vector<double> reactionBudgetTerms();
2087
2088
2089
2098 std::array<double, 3> wallAreaProbe();
2099
2100
2101
2106 double wallFluxImbalance();
2107
2108
2109
2110 // Empty when the geometry is static -> ibmModifyStencil takes its scalar u_bc path, unchanged.
2111 CCConst wallVelView(int c) const;
2112
2113
2114
2115 void rebuildStencils();
2116
2117
2118 // copy the nx*ny*nz inner cells between two extended blocks of different ghost width (g=2 <-> g=1
2119 // MG).
2120 void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg);
2121
2122
2123 // Copy the ENTIRE destination block (including its ghost ring) from the source block at per-axis
2124 // cell offset `off`: dst(x,y,z) <- src(x+off, y+off, z+off). Bridges a G=2 field to the g=1 MG
2125 // block INCLUDING the g=1 ghosts (off = G-1), so face means at the first inner cell read a valid
2126 // neighbour. Requires the source ghosts filled (fillGhosts/fillPropGhosts) — under MPI those are
2127 // the cross-rank values, so the bridge is decomposition-correct.
2128 void copyBlockShifted(CCField dst, C3 de, CCConst src, C3 se, int off);
2129
2130
2131 // Fill ghost width G periodically on all 3 axes (x then y then z, covering corners). Distributed:
2132 // the velocity-block halo (cross-rank + periodic, all ghosts incl. corners).
2133 void fillGhosts(CCField f);
2134
2135
2136 // Fused periodic FACE-ghost fill in ONE kernel (vs 3 fillAxis): each inner boundary cell scatters
2137 // its periodic image to the opposite face ghost, all 3 axes at once. Valid only for
2138 // FACE-neighbour (7-point) stencils -- it does NOT fill the corner/edge ghosts (which fillAxis's
2139 // sequential x->y->z does). The IBM RB-GS smoother reads only the 7-point stencil, so this is
2140 // exact there and cuts the velocity solve's dominant kernel-launch cost (~7200 -> ~2400 fill
2141 // launches/step) at low resolution. NOT for the Koren advection RHS (reads diagonals) -- keep the
2142 // full fillGhosts there.
2143 void fillGhostsFaces(CCField f);
2144
2145
2146 void fillAxis(CCField f, int axis);
2147 // §14 H-3(d): the single-rank periodic fill of the axes in `axes` (bit a = axis a) in ONE launch
2148 // over their ghost slabs -- bit-identical to fillAxis on those axes in ascending order (see the
2149 // definition). Falls back to the sequential passes when a periodic axis is shorter than G.
2150 void fillPeriodicAxes(CCField f, int axes);
2151 // The single-rank fill of the domain's periodic axes (both faces periodic): fillPeriodicAxes when
2152 // two or three are periodic, fillAxis when one is -- bit-identical to fillAxis per periodic axis in
2153 // ascending order.
2155
2156
2157 // Cell divergence of the current velocity iterate, on the inner cells + one ghost ring (the RHS
2158 // compensation reads div at i and i-strd, so faces at the low inner boundary need the ghost-cell
2159 // value; velocity ghosts were just filled). Porous-only scratch (divAdv_).
2160 void computeDivAdv();
2161
2162
2163
2168 // --- A0: wall-aware advection inputs (advective cut-wall flux) -------------------------------
2169 //
2170 // The momentum advection kernels (`sadv::advect / advect_sou / advect_fou / fou_operator`) are
2171 // geometry-blind: `adv_vel` averages the solid-masked rows into the advecting face velocity and
2172 // the SOU/Koren `PHI` stencils read the advected field up to 2 cells INSIDE the solid.
2173 // `maskVelocity` pins those rows to 0.0, which IS the wall velocity for a STATIC wall -- so a
2174 // static scene only carries the O(h) aperture defect (D2, docs/ANALYTIC_SDF_GEOMETRY.md §7 item
2175 // 8, measured -0.4..-1% on the 4-sphere bed). For a body moving at u_wall the advective term
2176 // near the body is wrong by O(u_wall): an O(1) local error in exactly the term that produces the
2177 // finite-Re screening of the confined wall correction (D1). This is the same defect family the
2178 // fresh-cell seed closed for the TIME term (seedFreshCells); the advective term never got it.
2179 //
2180 // The fix does NOT touch the global mask convention -- the masked zeros are load-bearing for the
2181 // viscous/pressure operators and for the reaction budget's telescoping. Instead the advection
2182 // gets its own input: a copy of u whose masked rows hold `uBc_`, the local rigid-body wall
2183 // velocity `v_inst + omega_inst x r` that `buildWallVelocity` already evaluates at component c's
2184 // staggered points over the WHOLE extended block (ghosts included -- the scene is analytic, so
2185 // ghost solid rows are computable pointwise and no extra exchange is needed).
2186 //
2187 // Both the explicit path (buildRhs / buildRhsForced / buildRhsVar) and the implicit-FOU stencil
2188 // path (buildAdvStencil / buildAdvStencilVar) read these views, or the deferred correction
2189 // rho*(aF - aK) would be assembled from two different velocity fields. The advRhs_ stash (the
2190 // reaction budget's R0 term) therefore carries the corrected term automatically.
2191 //
2192 // The fluid rows are the current Picard iterate u^k, so the COPY is per Picard iteration; the
2193 // WALL rows come from uBc_, which depends only on instance motion and is built once per
2194 // geometry/motion update.
2195 //
2196 // ABLATION: `set_advection_wall_velocity(False)` restores the pre-A0 behaviour (masked zeros in
2197 // the advection inputs) -- the instrument that differences "zeros vs wall velocity in the
2198 // advective term" directly. Everything else on the moving path is untouched by it.
2199 bool advWallInputs() const;
2200
2201
2202 void buildAdvInputs();
2203
2204
2207 CCConst advVelView(int c) const;
2208
2209
2213 CCConst advFaceView(int c) const;
2214
2215
2220 CCConst openFaceView(int c) const;
2221
2222
2242 void buildOpenFaceField();
2243
2244
2250 bool ufAdvVelocity() const;
2251
2252
2253
2254 bool ensureAdvStash(int c, bool adv);
2255
2256
2257
2258 void buildRhs(int c);
2259
2260
2261 // Sibling of buildRhs adding a per-cell body force (Boussinesq buoyancy / CFD-DEM feedback): the
2262 // constant fc becomes fc + Grid::atVelocity(fb) -- the force at the unknown's location, the face
2263 // mean 1/2(fb(i)+fb(i-s_c)) on the MAC grid and fb(i) collocated (never chosen by the density
2264 // model: buildRhsVar uses the same rule). Kept as a separate kernel so buildRhs stays
2265 // byte-identical (no codegen drift on the single-phase path). Selected in step() when
2266 // hasCellForce_.
2267 void buildRhsForced(int c);
2268
2269
2270 // Variable-density RHS (sibling of buildRhsForced): the time term, the advection weight, and the
2271 // per-cell body force all use the FACE density of component c (arithmetic mean over the staggered
2272 // face, matching VarFaceProps::idiag and the projection coefficient — this three-way consistency
2273 // is what makes discrete hydrostatic balance exact). The cell force fb is face-interpolated for
2274 // the same reason (a rho*g cell field becomes rho_face*g at the velocity location). Requires the
2275 // rho ghosts filled (rebuildStencils / buildAdvStencilVar did it this step).
2276 void buildRhsVar(int c);
2277
2278
2279 // Momentum-consistent sibling of buildRhsVar (rung V2b, WO-K). The validated `buildRhsVar` is not
2280 // touched; this one differs in exactly one term and drops one.
2281 //
2282 // buildRhsVar : rho_f/dt * u^n - rho_f * adv(u^k) (+ implicit-FOU deferred correction)
2283 // this : rho_f/dt * u^adv
2284 //
2285 // `u^adv` is `(rho^c u_c)/rho^c` after the geometric advection that shared its fluxes with the
2286 // colour field, so it already contains BOTH the time base and the advection — the Koren/SOU term
2287 // and the deferred correction are not merely unnecessary here, adding them would advect the
2288 // momentum twice. Everything else (the incremental -grad(P^n), the face body force, the domain-BC
2289 // inhomogeneity, the cut-cell rescale) is verbatim, and `rho_f` is the SAME arithmetic face mean
2290 // the projection coefficient and the face body force use — the three-way consistency that makes
2291 // hydrostatic balance exact is untouched (see the enableVofMomentum note).
2292 void buildRhsVarMom(int c);
2293
2294
2295
2296 // --- rung V8: the collocated variable-density / CSF predictor ----------------------------------
2297 //
2298 // SIBLING of buildRhsVar, reached only when `colocatedFaceForce()` — i.e. only on `SolverColocated`
2299 // with variable density and/or surface tension (doc/collocated_varrho_forces.md §4.2, S5-S6):
2300 //
2301 // b = rs [ (rho_c/dt) u^n - rho_c aK + rho_c aF + W f_c + rho_c R(Phi) ] + (bc fold | -inhom)
2302 //
2303 // * the density weight of the time term and of the advection is the CELL density `rho(i)`: the
2304 // collocated velocity unknown IS the cell (`Grid::offset(c) == 0`), and this is the same
2305 // placement `VarFaceProps::idiag` uses for the operator diagonal;
2306 // * the pressure, the CSF and the uniform drive `set_body_force` (a mean pressure gradient) enter
2307 // as the face accelerations Phi = (f_const + CSF - w G_f P)/rho_f reconstructed at the cell,
2308 // R(Phi) = ½(o_lo Phi_lo + o_hi Phi_hi) (`varrho::cellFromFaces`);
2309 // * a per-cell force enters at the cell value times the reconstruction's weight sum W
2310 // (`varrho::weightSum`, the same two openness reads).
2311 //
2312 // What survives verbatim from the other builders: the cut-cell rescale `rs`, the domain-BC fold /
2313 // IBM inhomogeneity, the Koren/SOU advection and its implicit-FOU deferred correction. The WHY
2314 // (and the retired face-acceleration form it replaces) is the comment at the definition.
2315 void buildRhsColoVar(int c);
2316
2317
2318
2319 // --- balanced-force CSF (rung V4, WO-P) ------------------------------------------------------
2320 //
2321 // ADDITIVE to whichever RHS builder just ran (`buildRhs` / `buildRhsForced` / `buildRhsVar` /
2322 // `buildRhsVarMom`), because the force is independent of which time term and which advection form
2323 // the configuration selected — and because those four are validated kernel bodies (hard rule 1).
2324 // It is applied at the same point in the RHS as the incremental `-(P(i) - P(i - s_c))` and
2325 // carries the same `rs(i)` cut-cell rescale every other RHS term carries.
2326 //
2327 // WHY NOT THROUGH THE PER-CELL FORCE FIELD. `cellForce_` is the natural conduit for a body force
2328 // and its ghosts are sound since WO-G, but its face rule is the ARITHMETIC INTERPOLATION
2329 // `½(f(i) + f(i - s_c))` of a cell-centred force. That rule is exactly right for `ρg` (the pair
2330 // `f_f/ρ_f` is then the intended acceleration) and exactly wrong for `σκ∇C`: an interpolated
2331 // cell-centred `σκ∇C` is not in the range of the discrete gradient operator the projection
2332 // inverts, so the projection cannot annihilate it and the residue is the classical spurious
2333 // current. The face value here is instead formed BY the projection's own operator — the same
2334 // difference `C(i) - C(i - s_c)` that `projectCorrectVar` applies to φ and `buildRhsVar` applies
2335 // to P. See `vof/surface_tension.hpp` for the full argument; the stationary-droplet gate is what
2336 // measures it, and it fails loudly on any other choice.
2337 //
2338 // `sigmaCsf_ == 0` (the default) never reaches here: `csfActive()` gates the call site, so every
2339 // non-VoF path is byte-identical.
2340 //
2341 // `dst` receives the force (`C[c].b` in the predictor); `rowScaled` multiplies it by the cut-cell
2342 // row rescale `rs(i)` (true in the predictor, false for the balanced-force projection's face
2343 // force, doc/collocated_varrho_forces.md §4.6.2 B2, where `rs` multiplies the whole row and is not
2344 // part of the force). `faceRange` widens the kernel from the inner cells `[G, e-G)` to the face
2345 // range `[G, e-G]` the constraint divergence reads.
2346 void addCsfRhs(int c, CCField dst, bool rowScaled, bool faceRange = false);
2347
2348
2349 // ABLATION (`set_csf_mode(1)`): the same physics discretized the OTHER plausible way — a
2350 // cell-centred force `f(j) = sigma*kappa(j)*(C(j+s) - C(j-s))/2h` interpolated to the face with
2351 // the arithmetic mean `1/2 (f(i) + f(i-s))`, exactly as the per-cell body-force machinery would
2352 // carry a `rho*g` field. It is consistent, it converges, and it is WRONG for surface tension: the
2353 // face value is no longer in the range of the projection's discrete gradient, so the projection
2354 // cannot annihilate it. This kernel exists so the difference is a measured number in the ctest
2355 // rather than an argument — the same role the harmonic-rho_f ablation plays for WO-J's
2356 // hydrostatic gate. NEVER a production path. Arguments as `addCsfRhs`.
2357 void addCsfRhsCellInterp(int c, CCField dst, bool rowScaled, bool faceRange = false);
2358
2359
2360 // --- rung W2 (WO-W12): the BLOCK CSF, a sibling of `addCsfRhs` ------------------------------
2361 //
2362 // Same force, same place in the RHS, same `rs(i)` cut-cell rescale — but the face value was
2363 // formed ON THE BLOCKS (each marker's own curvature cascade on its own dense box, the same
2364 // `csfFaceCurvature` + `csfFaceForce` pair this file's `addCsfRhs` applies to the global field)
2365 // and scattered into `csfBlkF_` with UNPACK_SUM, so two markers whose bands overlap ADD their
2366 // forces instead of one of them being lost to the union's `max`. This is TBFsolver's
2367 // `VOF.f90::computeSurfaceTension` structure (block `stx/sty/stz` -> `boxes_2_grid_vf(...,
2368 // UNPACK_SUM)`), on the suite's own kernels.
2369 //
2370 // WHY THE FORCE AND NOT THE CURVATURE IS SCATTERED. kappa is not additive and the union colour
2371 // is a `max`, so a face between two overlapping markers has no single (kappa, dC) pair to build
2372 // a force from; the force is the additive quantity, and forming it where the marker's own colour
2373 // still exists is the only place the balanced-force pairing (the SAME face difference the
2374 // projection's gradient uses) is available per marker.
2375 //
2376 // Gated on `vofBlockCsf()`, which is false whenever the block container is absent.
2377 void addCsfRhsBlocks(int c);
2378
2379
2380
2381 // Census of the CSF face force over this rank's inner region, on the CURRENT colour + curvature
2382 // fields: the max |F| per component, and the number of ORPHAN faces — faces across which the
2383 // colour jumps by more than the wisp threshold but neither cell carries a curvature estimate, so
2384 // the force was silently dropped. An orphan is a defect (Basilisk's "this should not happen"); it
2385 // is counted rather than hidden.
2386 //
2387 // The threshold matters: the FORCE is applied at every face with `dC != 0` exactly (dropping the
2388 // round-off jumps would itself break the discrete-gradient identity by O(sigma*kappa*eps), which
2389 // is 1e-9 and would be visible in the machine-zero gate), but a face whose colour jump is 1e-30
2390 // is not a missing interface and counting it as one would bury the real thing.
2392 double maxForce[3] = {0.0, 0.0, 0.0};
2393 long orphanFaces[3] = {0, 0, 0};
2394 long forcedFaces[3] = {0, 0, 0};
2395 };
2396 CsfDiagnostics csfDiagnostics();
2397
2398
2399
2400 // Implicit-FOU velocity stencil (CUDA build_adv_stencil_k + ibm_modify_stencil): backward-Euler
2401 // diffusion (idiag+6beta diag, -beta off) + rho*FOU(u^k) upwind operator (diagonally dominant ->
2402 // stable at high Re), then the Robust-Scaled cut-cell bake. The advecting velocity u^k = the
2403 // current C[*].u (ghosts filled).
2404 void buildAdvStencil(int c);
2405
2406
2407 // Variable-property sibling of buildAdvStencil: VarFaceProps diffusion build (per-face mu, face-
2408 // density time diagonal) + the FOU upwind weighted by the FACE density (constant path:
2409 // fouw=rho_). Separate kernel so the validated buildAdvStencil stays byte-identical.
2410 void buildAdvStencilVar(int c);
2411
2412
2413 // Backflow stabilization (Bazilevs 2009 / Esmaily-Moghadam 2011) for the NORMAL momentum at
2414 // outflow faces: add the dissipative diagonal term beta*rho*|min(u.n,0)| where the outflow
2415 // reverses (fluid re-entering, u.n<0). This removes the spurious kinetic-energy influx that the
2416 // do-nothing/zero- gradient outflow advects in -- the "backflow divergence" that blows up
2417 // separated flows (e.g. the BFS recirculation reaching the outlet), worse on finer grids. Purely
2418 // dissipative (u_ext=0), so it is implicit + unconditionally stable, and INERT where the outlet
2419 // is outgoing (u.n>=0) -> the channel and any non-reversing outflow stay byte-identical. Applied
2420 // to C[c].AC after buildAdvStencil (per Picard iteration, lagged at u^k); only the component
2421 // normal to each outflow face.
2422 void applyBackflowStab(int c);
2423
2424
2425 // max|a-b| over inner cells (Picard outer-tolerance check).
2426 double maxAbsDiffInner(CCConst a, CCConst b);
2427
2428
2429 // Shared momentum RB-GS loop: fixed velIters_ sweeps, or (velTol_ > 0) the tolerance stop —
2430 // colour 0 plain, colour 1 via the fused max-increment kernel, stop once the increment has
2431 // contracted to velTol_ of the first sweep's. The decision is rank-uniform under MPI (all ranks
2432 // see the same global max), so per-sweep halo exchanges stay in lockstep.
2433 // Stencil paths supply `resid` (returns max|b - A u| over this rank's inner fluid cells after
2434 // a fresh ghost fill) and `bnorm` (max|b|), enabling the residual stop when velResTol_ > 0.
2435 template <class Fill, class Color, class ColorDu>
2436 void velSweepLoop(Fill&& fill, Color&& sweepColor, ColorDu&& sweepColorDu,
2437 std::function<double()> resid = nullptr, double bnorm = 0.0);
2438
2439
2440
2441 VelocityMG::Comm vmgComm() const;
2442
2443
2444 // residual functor + max|b| for the stencil paths of component c (see velSweepLoop)
2445 // Common tail of a residual evaluation: the held normal-Dirichlet face is imposed, not solved
2446 // (excluded), remember max|A u| for the convergence scale, return max|r|.
2447 double finishResidual(int c);
2448
2449
2450 std::function<double()> stencilResidual(int c, bool exchange = false);
2451
2452
2453 // The all-fluid domain-BC smoother's operator (per-axis constant coefficients + the boundary
2454 // fold). `aniso` selects the per-axis body; the isotropic path runs the legacy kernel literally
2455 // (doc/anisotropic_metric.md §2, trap 4).
2456 std::function<double()> constCoeffResidual(int c, double bx, double by, double bz, double Ac);
2457
2458
2459 double stencilBnorm(int c);
2460
2461
2462
2463 void smoothComp(int c);
2464 // Chebyshev momentum solve for one component (IBM/periodic path). Same stop criterion,
2465 // scale and counters as velSweepLoop, so the two solvers are interchangeable.
2466 void chebSolveComp(int c);
2467
2468
2469 // pressure ghost at domain faces for the incremental predictor's grad(P): zero-gradient (Neumann)
2470 // at every non-periodic face so grad(P) carries no spurious force there (the periodic fill
2471 // wrapped the opposite boundary's pressure). Outflow pressure (Dirichlet p=0) is enforced
2472 // separately in the MG solve.
2473 void pressureBcGhost();
2474
2475
2476 // domain-BC velocity ghosts: periodic-fill periodic axes, then apply per-face BCs (fold=0
2477 // explicit/1 implicit).
2478 void fillVelGhosts(int comp, int fold);
2479
2480
2481 void applyVelocityBcComp(int comp, int fold, bool doOutflow);
2482
2483
2484 // Field-parameterized variants (so the velocity-MG can re-impose the BC on its own level-0
2485 // iterate). `doOutflow = false` is the SIBLING behaviour merged in here (was
2486 // `fillVelGhostsKeepOutflow`, used ONLY by `bridgeVelocityToVof` and the two call sites below,
2487 // always with `fold = 0`): it does NOT re-impose the zero-gradient OUTFLOW face — exactly what
2488 // `step()` already passes after `project()` ("keep outflow").
2489 //
2490 // WHY THE `doOutflow = false` PATH EXISTS (WO-R; a defect found by gate F2). The projection
2491 // corrects the high-side OUTFLOW normal face separately from every other face
2492 // (`bcCorrectOutflow`) — that correction IS how mass leaves the domain, and `step()` deliberately
2493 // re-imposes the domain BCs afterwards with `doOutflow = false` so it survives. `bridgeVelocityToVof`
2494 // used to call the FULL fill (`doOutflow = true`), whose `bcOutflowComp` overwrites the boundary
2495 // face with the zero-gradient copy of the last inner cell — erasing the correction immediately
2496 // after the projection made it, on every step, whenever VoF is enabled.
2497 //
2498 // Two measured consequences, both of which vanish with `doOutflow = false`
2499 // (`tests/kokkos/test_vof_bc.cpp` gate F2):
2500 // * the field the colour advector is handed is NOT discretely divergence-free at the outflow,
2501 // which is precisely the hypothesis Weymouth-Yue's exact conservation rests on;
2502 // * `max_open_divergence()`, evaluated after `step()` returns, reports the ERASED field —
2503 // measured 4.0 on a stratified outflow box that the projection had actually solved.
2504 // The outer ghost layers beyond the boundary face are left as the exchange/periodic fill wrote
2505 // them, and the advector never reads them: its flux sweep along axis d reaches exactly the
2506 // domain boundary face and no further.
2507 //
2508 // Inert for everything that existed: `bridgeVelocityToVof` runs only under `enable_vof`, and no
2509 // VoF configuration before this rung combined VoF with an outflow face.
2510 void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow = true);
2511
2512
2513 // Distributed: a rank applies a face's BC iff its block TOUCHES that global face
2514 // (`touchesGlobalFace`, the same rule the scalar path uses in `applyScalarBc`). Without the test
2515 // every rank imposed the wall on its OWN block faces, so a partition cutting a walled axis split
2516 // the domain into independent sub-domains — invisible in the velocity (each sub-domain is
2517 // separately consistent) and only visible in the pressure. Single-rank the test is always true,
2518 // so this is byte-identical there.
2519 void applyVelocityBcCompTo(CCField f, int comp, int fold, bool doOutflow);
2520 // §14 H-3(e) (staggered): one face's velocity BC (the per-face body of applyVelocityBcCompTo);
2521 // whether both faces of axis a take bcVelocityComp on this rank with disjoint ghost planes (one
2522 // launch then covers both); and the three components back to back (applyVelocityBcComp for
2523 // c = 0, 1, 2), one launch per such axis.
2524 void applyVelocityBcFace(CCField f, int comp, int a, int s, int fold, bool doOutflow);
2525 bool bcVelFacePair(int a) const;
2526 void applyVelocityBcAll(int fold, bool doOutflow);
2527
2528
2529 // implicit-diffusion wall fold (CUDA setup_bc_diffusion): dcorr += (wall:+beta tangential /
2530 // outflow:-beta), brhs += 2*beta*wall (tangential Dirichlet); bake dcorr into the per-component
2531 // stencil diagonal.
2532 void setupBcDiffusion();
2533
2534
2535 // Incremental (rotational) cut-cell projection: solve A phi = -div_open(u*) (RB-GS,
2536 // mean-removed), u -= grad phi, then accumulate the physical pressure P += (rho/dt)*phi -
2537 // mu*div(u*) (Timmermans).
2538 // one mask-aware axis-wise smoothing pass of a cell field (the filtered-rotational S; see
2539 // setRotationalFilter). Reads the +/-1 axis neighbours' sdf: fluid-fluid -> (1,2,1)/4;
2540 // one solid side -> 1/2(self + open-side neighbour); both solid -> identity.
2541 void filterCellField(CCField f, int axis);
2542
2543
2544 void project();
2545
2546
2547 // ---- project() stages (QUALITY_PLAN G.1): pure cut-and-paste, each a contiguous
2548 // block of the original function sharing only member fields and a CCExec. ----
2550
2551
2553
2554
2555 void projectSolve();
2556
2557
2558 // The pressure-driver dispatch of projectSolve (Chebyshev with its bound estimate / ghost
2559 // BiCGStab / flexible CG / MG-PCG): solve the projection's own operator for `x1` (g=1 block,
2560 // used as the initial guess) with the right-hand side `rhs1`, at the driver's own rtol. Returns
2561 // the iteration count; `mg_.lastSolveFailed()` reports a non-finite recurrence. Shared by the
2562 // main projection and the balanced-force projection (doc/collocated_varrho_forces.md §4.6.2 B4).
2564
2565
2566 // The projection's constraint divergence of a face field (`out` on the g=2 block, inner cells):
2567 // the flux-openness divergence `divergOpen(f, ox_, oy_, oz_)` of the non-porous, non-ghost
2568 // branches of both grids. The balanced-force projection applies it to its face force (§4.9 L2:
2569 // always through this routine, never divergOpen directly, so a scheme's overlay reaches both).
2571
2572
2574
2575
2576 void projectPressureUpdate();
2577
2578
2579 void maskVelocity(int c);
2580
2581
2582 // Minimum viscosity over the (global, under MPI) inner cells — the provably-stable rotational
2583 // coefficient for variable viscosity (chi*mu_min <= mu(x) everywhere).
2584 double minMuInner();
2585
2586
2587 double reduceMaxAbsInner(CCConst f);
2588
2589
2590 std::vector<double> gatherInner(CCField fld);
2591
2592
2593 // Inverse of gatherInner: scatter an x-fastest (nx,ny,nz) inner-region host buffer into the inner
2594 // cells of a ghosted G=2 field (ghost cells untouched — refill via exchangeField/fillGhosts).
2595 void scatterInner(CCField fld, const std::vector<double>& in);
2596
2597
2598
2599 // --- Named field registry (multiphysics field container) ------------------------------------
2600 // Register a new zero-initialised cell-centred field on the G=2 velocity block and return its
2601 // buffer. Idempotent: re-adding an existing name returns the existing buffer unchanged.
2602 CCField addField(const std::string& name);
2603
2604
2605 bool hasField(const std::string& name) const;
2606
2607
2608 CCField fieldView(const std::string& name);
2609
2610
2611 std::vector<std::string> fieldNames() const;
2612
2613
2614 // Ghost-exchange a registered field (cross-rank + periodic under MPI; periodic-only single-rank).
2615 void exchangeField(const std::string& name);
2616
2617
2618 // Add-reduce ("reverse") halo: fold ghost-layer deposits back onto their owner cell (both
2619 // cross-rank AND periodic self-wrap). This is the coupling primitive for particle->grid
2620 // deposition (e.g. void fraction / drag reaction) where a particle near a block boundary scatters
2621 // into ghost cells owned by a neighbour; after this the inner block holds the complete sum.
2622 // Single-rank non-periodic: a no-op.
2623 void exchangeFieldAdd(const std::string& name);
2624
2625
2626 // Host round-trip: read a registered field's inner region as an x-fastest (nx,ny,nz) buffer, or
2627 // write one (ghosts left stale until the next exchangeField).
2628 std::vector<double> getField(const std::string& name);
2629
2630
2631 void setField(const std::string& name, const std::vector<double>& v);
2632
2633
2634 // Padded-block extents + ghost width, so a zero-copy field buffer (size ex*ey*ez, x-fastest) can
2635 // be reshaped in Python.
2636 std::array<int, 3> blockShape() const;
2637
2638
2639 int ghostWidth() const;
2640
2641
2642 // Global grid dims (== local dims single-rank). For the CFD-DEM co-decomposition weight field.
2643 std::array<int, 3> globalResolution() const;
2644
2645
2646 // This rank's inner-block origin in GLOBAL cells ({0,0,0} single-rank). The deposit-origin shift
2647 // so particles in global coords land in the local block (gm origin = blockOrigin * h).
2648 std::array<int, 3> blockOrigin() const;
2649
2650
2651
2652 // --- Scalar transport (advection-diffusion) -------------------------------------------------
2653 // Register a transported scalar `name` with constant diffusivity D (length^2/time in the
2654 // caller's units; under no extent, cells^2 per time unit). scheme: 0 FOU,
2655 // 1 Koren TVD (default), 2 SOU. iters = RB-GS sweeps for the implicit diffusion solve. Its field
2656 // is registered in the directory (get_field/set_field/field_view). Openness (set_solid /
2657 // set_pressure_geometry) must be established for transport to occur.
2658 // cutcell = true selects the cut-cell discretization (doc/scalar_ibm_design.md, §8.1): `iters`
2659 // is then unused (the solve is BiCGStab to a residual tolerance) and every other cut-cell setter
2660 // below applies.
2661 void addScalar(const std::string& name, double D, int scheme, int iters, bool cutcell = false);
2662
2663
2664 bool hasScalar(const std::string& name) const;
2665
2666
2667 // Per-face scalar BC: face 0..5 = -x,+x,-y,+y,-z,+z; type 0 periodic, 1 Neumann zero-flux
2668 // (adiabatic), 2 Dirichlet value. Single-rank / non-decomposed domains (distributed BC deferred).
2669 void setScalarBc(const std::string& name, int face, int type, double value);
2670
2671
2672 // Advance all registered scalars one dt with the current divergence-free velocity (also called at
2673 // the end of step()). Exposed so a test can prescribe a velocity and transport a scalar in
2674 // isolation.
2675 void advanceScalars();
2676
2677
2678 // --- Cut-cell scalar transport: the geometry record (doc/scalar_ibm_design.md §2, §3; WO-2) ---
2679 // One record per geometry, shared by every cut-cell scalar: kappa, the snapped UNGATED scalar
2680 // apertures, the fluid-unknown flags, the facet overlay with its fluid probes, and the census.
2681 // Built lazily (`ensureScalarCutGeometry`) on first use after the geometry exists; invalidated by
2682 // every geometry build (set_solid, set_solid_from_scene, set_pressure_geometry, redistribute --
2683 // all of which reach setSolidDevice). It never reads or writes ox_/oy_/oz_, cs_ or cutOwner_.
2686 // The record itself (builds it if stale). For tests and the operator; the views are the block's.
2688 // Inner (nx*ny*nz, x-fastest) copy of one record field: 0 kappa, 1/2/3 the snapped aperture of
2689 // the -x/-y/-z face of each cell, 4 the fluid-unknown flag (1/0).
2690 std::vector<double> scalarGeometryField(int which);
2691 // The geometry census, summed over ranks (collective under MPI); sealedVolume physical.
2693
2694 // --- Cut-cell scalar transport: operator, Krylov, steady + transient diffusion (WO-3) --------
2695 // doc/scalar_ibm_design.md §1.4, §4, §5.1, §8. A cut-cell scalar is advanced by
2696 // `advanceScalarCutCell` (the first statement of advanceScalars' per-scalar loop, §5.5); the
2697 // legacy kernels never see it. Every setter stores PHYSICAL values verbatim; they are converted
2698 // and folded into the operator at the next advance or steady solve, which rebuilds the operator
2699 // (ruling D-WO3-3). Advection (WO-5, §6): the explicit-implicit split of D12 on the projection's
2700 // face flux (`scalarFaceFlux`).
2701 bool isCutcellScalar(const std::string& name) const;
2702 // Wall condition of one scene instance (instance >= 0, needs a scene) or of every body
2703 // (instance = -1, which also clears the per-instance overrides): type 0 neumann (value = flux
2704 // into the fluid, c L/T), 1 dirichlet (value = wall c), 2 robin (coefficient = k, L/T; value = g).
2705 void setScalarWall(const std::string& name, int type, double value, double coefficient,
2706 int instance);
2707 // Conjugate transport (§1.1, §8.1, WO-7): make one scene instance (instance >= 0, needs a
2708 // scene) or every body (instance = -1, which also clears the per-instance settings) conjugate:
2709 // its solid interior carries the scalar too, in the registered field `<name>_solid` (psi_s =
2710 // c_s / K, allocated by the first call). D_s (L^2/T) the solid's own diffusivity, C_s the
2711 // capacity ratio, K the partition coefficient (c_s = K c at equilibrium), R_c the contact
2712 // resistance (T/L, on psi). A later setScalarWall for the same instance makes it a wall again.
2713 void setScalarSolid(const std::string& name, double D, double capacity, double partition,
2714 double contactResistance, int instance);
2715 // `get_scalar_solid` (§8.1): this rank's inner block (x-fastest) of c_s = K psi_s, NaN outside
2716 // the solid unknowns (all NaN before the first advance or steady solve with a conjugate body).
2717 std::vector<double> getScalarSolid(const std::string& name);
2718 // The solid geometry of a conjugate scalar (diagnostics.scalar_geometry): inner copies of
2719 // kappa_s = 1 - kappa (which = 0) and the solid-unknown flag (which = 1).
2720 std::vector<double> scalarSolidGeometryField(const std::string& name, int which);
2721 // Volumetric source S (c/T): uniform, or per cell (this rank's inner block, x-fastest).
2722 void setScalarSource(const std::string& name, double S);
2723 void setScalarSourceField(const std::string& name, const std::vector<double>& S);
2724 // Max-norm relative residual tolerance of the solve (default 1e-10, §5.1).
2725 void setScalarTolerance(const std::string& name, double rtol);
2726 // BiCGStab iteration cap (default 200, §5.1). Diagnostics tier.
2727 void setScalarMaxIterations(const std::string& name, int maxit);
2728 // Mean-gradient (closure) mode (§1.5, WO-6): G physical (c/L); the field then holds theta,
2729 // c = G.x + theta, in the frame moving with the fluid's mean velocity. (0, 0, 0) is off.
2730 void setScalarMeanGradient(const std::string& name, const std::array<double, 3>& G);
2731 // `scalar_mean_flux` (§8.1): the box-averaged total flux (3,), c L/T (physical), of the last
2732 // advance or steady solve: the face sum of -Lam a A (c_j - c_i), c = theta + G.x, plus
2733 // (1/V_box) sum_i (U_i - kappa_i V Ubar) theta_i. Collective under MPI.
2734 std::array<double, 3> scalarMeanFlux(const std::string& name);
2735 // G.x at an extended-block cell centre for the physical gradient G (§1.5; global, unwrapped).
2736 sco::MeanGradPosition scalarMeanGradPosition(const double Gphys[3]) const;
2737 // Dirichlet value of a domain face as a per-face profile: n1 x n2 values over this rank's face
2738 // cells in the face's tangential axes in x, y, z order, the first fastest (§8.1).
2739 void setScalarBcProfile(const std::string& name, int face, const std::vector<double>& prof,
2740 int n1, int n2);
2741 // Steady solve now (§1.4 steady mode): kappa/dt and c^n dropped; initial guess = the field.
2742 void solveScalarSteady(const std::string& name);
2743 // Bodies the wall table distinguishes: the scene's instances, or 1 for raw-SDF geometry.
2744 int scalarNumBodies() const;
2745 // Integrated flux INTO the fluid per body over the last advance/solve (c L^3/T, physical),
2746 // summed on the host in facet order, then over ranks (collective under MPI).
2747 std::vector<double> scalarWallFlux(const std::string& name);
2750 double mass = 0.0, massSolid = 0.0, dMass = 0.0, wallIn = 0.0, boundaryIn = 0.0,
2751 sourceIn = 0.0, defect = 0.0, identityError = 0.0;
2752 };
2753 ScalarCutBudget scalarBudget(const std::string& name);
2757 std::vector<double> centroid, normal, area, wallValue, flux;
2758 std::vector<int> instance, rung;
2759 };
2760 ScalarCutFacets scalarFacets(const std::string& name);
2761 // The cut-cell scalar of this name (throws std::invalid_argument when it is not one).
2762 ScalarField& cutcellScalar(const std::string& name, const char* who);
2763 // The transient advance of one cut-cell scalar (from advanceScalars).
2765 // The refusals of §8.1, checked at every advance / steady solve (std::runtime_error).
2766 void scalarCutRefusals(const ScalarField& sc) const;
2767 // Assemble (bands, overlay coefficients, surrogate diagonal, rhs) and solve; the field's
2768 // non-unknown cells are zeroed first and its ghosts filled after.
2769 void scalarCutAssembleSolve(ScalarField& sc, bool steady);
2770 // WO-8: the resolution warnings of §9 (thin solids, R2 > 1 % of the probes, sealed volume >
2771 // 1e-6 of the fluid volume), stderr on rank 0, once per scalar at its first build.
2773 bool scalarRootRank() const;
2774 // y = A x for the stored operator of `sc` (exchanges x's ghosts).
2776 // WO-7: the two-field y = A x of a conjugate scalar (exchanges both fields' ghosts).
2778 // WO-7: the per-scalar solid record (§2.7 materials, §1.3 solid unknowns, §3.4 solid probes),
2779 // rebuilt when the geometry or the set of conjugate bodies changed; `conj` the resolved
2780 // conjugate flag per body.
2781 void ensureScalarSolidRecord(ScalarField& sc, const std::vector<char>& conj);
2782 // WO-7: the per-body material table (internal units) of the current settings.
2783 sco::MaterialTable scalarMaterialTable(ScalarField& sc, std::vector<char>& conj);
2795 ScalarFaceFlux scalarFaceFlux(int a) const;
2796 // WO-5b (§6.5, ruling D-WO5-3): capture, on every flow inflow/outflow global face this rank
2797 // touches, the boundary-face flux open * vel the projection constrained (through
2798 // `scalarFaceFlux`), over the face plane's full transverse extent. Called by step() right after
2799 // the projection and the domain-BC re-imposition, before any ghost fill overwrites the high-side
2800 // plane (the first ghost index, which a fill wraps from the opposite face). No-op without an
2801 // open face; numerically inert for the flow (it only copies a plane).
2803 // WO-5 (§6): the face fluxes, the small-cell classification and the census of one advance or
2804 // steady solve, into `sc.cut` (st.advecting false: no face carries flux, nothing else is built).
2805 void scalarCutAdvection(ScalarField& sc, bool steady);
2806 // setSuperficialVelocity's per-step shift (the tail of step()); no-op when off.
2808 void superficialVelocityPrecheck() const;
2809
2810
2811
2812 // --- Geometric VoF: the colour field (rung V2a, WO-J) ---------------------------------------
2813 //
2814 // WHAT THIS RUNG IS. One phase, transported by geometric (PLIC + Weymouth-Yue) VoF, drives the
2815 // fluid properties through the ORDINARY property closures, and the existing variable-density
2816 // projection carries the density jump. No surface tension (V4), and — important — NO
2817 // MOMENTUM-CONSISTENT TRANSPORT (that is rung V2b / WO-K). Mass and momentum are therefore
2818 // advected by different fluxes, which multiplies the light phase's acceleration by the heavy
2819 // phase's density in a mixed cell: a spurious interfacial momentum source of order Δρ. The
2820 // literature is unambiguous that this breaks down around density ratio 1000 unless the
2821 // resolution is absurd (Rudman 1998; Arrufat et al., Computers & Fluids 215:104785, 2021 —
2822 // accurate raindrop at 15 cells/diameter WITH consistency versus ~200 without). SO: **V2a is
2823 // valid only at modest density ratios.** A ratio-1000 case that is at REST (the hydrostatic acid
2824 // test) is exact here, because there is no momentum to mis-advect; a ratio-1000 case with motion
2825 // is not this rung's business.
2826 //
2827 // USAGE
2828 // s.enable_vof() # registers "C" and the g=3 working block
2829 // s.set_vof(C0) # sharp initial colour, C in [0,1]
2830 // s.set_property_model("rho", "linear", "C", [rho_g, rho_l - rho_g]) # rho(C); enables varRho
2831 // s.set_property_model("mu", "linear", "C", [mu_g, mu_l - mu_g]) # mu(C) (optional)
2832 // s.set_property_model("force_z", "linear", "rho", [0.0, -g]) # gravity
2833 //
2834 // STRUCTURE (`suite/docs/VOF_PLAN.md` §3 rule 1: the colour field gets its own g=3 halo and the
2835 // solver's G = 2 is NOT widened). There are two blocks and the split is deliberate:
2836 // * `"C"` is a NORMAL registered G=2 cell field. That is what makes item 3 of the work order
2837 // ("ρ(C) and μ(C) through the EXISTING closures, no new closure machinery") possible at all:
2838 // `applyClosure` indexes its input and its output with the SAME linear index on the SAME
2839 // extent, so a closure input MUST live on the G=2 block. It is also what gives C
2840 // get_field/set_field/field_view/exchange_field/redistribute for free.
2841 // * the g=3 block is the advector's own working block (`vof::WyAdvector`), with its own
2842 // `GridHaloTopology` at width 3 under MPI. MYC needs 3^3 and the donor ring is one cell
2843 // outside the inner region (so advection alone needs 2); width 3 is the plan's choice for the
2844 // V3 height-function columns.
2845 // The two blocks exchange INNER REGIONS ONLY (`copyInner`, both ways) and each fills its own
2846 // ghosts with its own policy — that is the one bridge, and it carries no offset arithmetic
2847 // beyond `copyInner`'s. The face velocities go the other way, whole-block-embedded
2848 // (`vof::copyBlockEmbed`), because the advector reads them one cell outside its inner region.
2849 //
2850 // STAGGERED ONLY. The collocated path is rung V8 and needs the collocated variable-density
2851 // projection (which throws today) before it means anything; `enableVof` throws there rather than
2852 // half-supporting it.
2853 //
2854 // NO IMMERSED SOLIDS YET. `VOF_PLAN.md` §3 rule 2 makes C the liquid fraction of the FLUID volume
2855 // with openness-weighted geometric fluxes (Huang 2025/2026 solid-clipped flux polygons). That is
2856 // not this work order's scope, and a silently-unweighted flux would leak C into the solid, so
2857 // `advectVof` throws if an immersed solid is present. An ALL-FLUID `set_pressure_geometry` is
2858 // fine (and is what the acid test uses) — the check is on `hasSolid_`, i.e. on any inner SDF < 0.
2859 static constexpr int kVofG = 3; // the colour field's ghost width (VOF_PLAN §3 rule 1)
2860
2861 void enableVof();
2862
2863
2867 void setVofWispEps(double eps);
2868
2869
2870 double vofWispEps() const;
2871
2872
2875 static constexpr double defaultVofWispEps();
2876
2877
2878 bool vofEnabled() const;
2879
2880
2881 // Initial / prescribed colour field on the inner cells (flat x-fastest, nx*ny*nz), C in [0,1]:
2882 // the LIQUID fraction of the cell. Enables VoF if it is not on yet. Ghosts are refreshed here so
2883 // a closure applied before the first step already sees a consistent field.
2884 void setVof(const std::vector<double>& c);
2885
2886
2887 std::vector<double> getVof();
2888
2889
2890 // Local (this rank's) colour census: sum / min / max / mixed-cell count / wisp count.
2892
2893
2894 // sum of the canonical colour field "C" over SOLID cells of this rank (0 by construction).
2895 double vofSolidColourSum();
2896
2897
2898 // Interface-local Courant number max|uf|*dt/h over the faces of mixed cells and their face
2899 // neighbours, with the CURRENT velocity and dt (an all-reduce max under MPI). This is the number
2900 // the WY boundedness bound applies to — NOT the global max, which over-throttles badly (V1
2901 // measured 0.314 in a quiescent Zalesak corner against 0.157 at the interface). Use it to pick
2902 // dt: `dt_new = dt * cfl_target / vof_max_courant()`.
2903 double vofMaxCourant();
2904
2905
2906 // The interface-local Courant number of the step just taken (0 before the first step).
2907 double vofLastCourant() const;
2908
2909
2910 // Weymouth-Yue boundedness cap (default 0.25, the PROVEN 3D bound 1/(2(N-1)); 0.5 is the 2D
2911 // value). `step()` throws when the interface-local Courant number exceeds it.
2912 void setVofCflLimit(double v);
2913
2914
2915 double vofCflLimit() const;
2916
2917
2918
2919 // --- rung V5a (WO-Q): VoF transport through an immersed solid ---------------------------------
2920 //
2921 // The colour advection is openness-weighted (`vof/cutcell.hpp`): the geometric flux of every face
2922 // is multiplied by the face openness `o_f`, the update is done in FLUID-VOLUME units
2923 // (`eps_i C_i`), and the dilation term uses the same `o_f a_f` — so the conserved functional
2924 // `sum_i eps_eff_i C_i` telescopes against the projection's own openness-weighted divergence,
2925 // exactly as in the uncut case. Solid cells carry no colour (the canonical "C" reads 0 there) and
2926 // the working block's solid band carries the neutral 90-degree fill for the MYC / height-function
2927 // stencils.
2928 //
2929 // WHAT IT APPROXIMATES: the PLIC polyhedron is reconstructed on the WHOLE unit cell and its slab
2930 // volume is multiplied by the open area, instead of being clipped against the solid as well
2931 // (Huang, JCP 2025/2026). Conservative and exact where interface and wall are parallel or the
2932 // cell is whole; O(1) wrong in the distribution INSIDE a cell whose interface crosses its wall.
2933 // `vof_diagnostics().clipped_volume` is the tripwire.
2934 void requireVofGeometry(const char* who);
2935
2936
2937 // Does the colour advection run the cut-cell (openness-weighted) kernels?
2938 bool vofHasGeometry() const;
2939
2940
2941 // Ablation: drop Weymouth's admissible-interval clamp on the openness-weighted flux
2942 // (`vof/cutcell.hpp` vofCutFluxClamp). ON by default — the measurement that put it there is in
2943 // that header. With it off the [0,1] clip becomes the mechanism instead of a tripwire and the
2944 // conserved functional drifts.
2945 void setVofCutFluxClamp(bool on);
2946
2947
2948 bool vofCutFluxClamp() const;
2949
2950
2951 // What the CANONICAL "C" field carries in SOLID cells (the working block always carries the
2952 // neutral band fill, which is what the MYC / height-function stencils need):
2953 // true (default) 0 — "no colour in the solid", the WO-Q gate. The closures then see gas
2954 // density there and the CSF sees a full colour jump across a wall face.
2955 // false the band fill — a zero-slope continuation of the liquid into the wall.
2956 // Measured on the G5 cap (D/dx = 24, sigma = 1, mu = 0.05): see the WO-Q findings entry.
2957 void setVofSolidColourZero(bool on);
2958
2959
2960 bool vofSolidColourZero() const;
2961
2962
2963 // The colour field INCLUDING the neutral solid-band fill, on the inner region — i.e. what the
2964 // MYC / height-function stencils actually read, as opposed to the canonical "C" (0 in solid).
2965 // The fill is regenerated here, so this is also the direct gate on its decomposition
2966 // independence: it must be pointwise BITWISE across np (`tests/kokkos_mpi/test_vof_cutcell_mpi`).
2967 std::vector<double> getVofFilledColour();
2968
2969
2970 // The cut-cell geometry the colour block runs on, on the inner region: 0 = the cell fluid
2971 // fraction eps, 1/2/3 = the openness of the +x/+y/+z face of each cell (the ADVECTOR's high-face
2972 // convention), 4 = the cell classification (1 = solid). All must be bitwise across np.
2973 std::vector<double> getVofGeometry(int which);
2974
2975
2976 // --- rung V5b (WO-S): static contact angle on SDF solids --------------------------------------
2977 //
2978 // The band fill of rung V5a is a stencil device: it decides what the MYC 3^3 stencil and the V3
2979 // height-function columns of a near-wall cell see INSIDE the solid, and WO-Q's neutral (mean of
2980 // the fluid face neighbours) rule is the zero-slope continuation, i.e. the 90-degree
2981 // Afkhami-Bussmann limit. `set_contact_angle` replaces PASS 1 of that fill by the fractions of
2982 // the plane that continues the fluid-side interface into the solid at the prescribed angle
2983 // theta, measured THROUGH THE LIQUID (`m . n_w = cos theta`, `vof/wetting.hpp`). Nothing else
2984 // changes: no force is added at the wall, the V3 cascade and the V4 balanced force are the
2985 // unmodified ones, and passes 2-3 of the fill are WO-Q's.
2986 //
2987 // theta is a per-cell FIELD so the dynamic-angle rung (V6) changes only what fills it. Setting
2988 // it needs `set_solid(..., cutcell_pressure=True)` + `enable_vof` (there is no wall otherwise);
2989 // with no call the neutral fill runs and every V5a number is byte-identical.
2990 void setContactAngle(double thetaDeg);
2991
2992
2993 // ISSUES sweep item 3: `set_contact_angle` used to be a silent no-op whenever there was nothing
2994 // for it to bind to -- the theta field simply was never consulted, and
2995 // `contact_angle_diagnostics()['contact_cells']` reading 0 was the only tell. Say so instead.
2996 void requireWettingWall() const;
2997
2998
2999 // Per-cell contact angle in DEGREES on the inner region (flat x-fastest, nx*ny*nz). Only the
3000 // value at the SOLID band cell being filled is read, so cells away from a wall are irrelevant.
3001 void setContactAngleField(const std::vector<double>& thetaDeg);
3002
3003
3004 bool contactAngleSet() const;
3005
3006
3007 double contactAngle() const;
3008
3009
3010 // Which anchor the theta-plane uses (`vof::VofWettingPivot`): 0 volume-consistent (DEFAULT,
3011 // idempotent), 1 the PLIC centroid p_f (Afkhami-Bussmann), 2 the work order's
3012 // `c = p_f - sdf(p_f) n_w` (NOT idempotent — measured to be off by 0.26 in cell fraction at
3013 // theta = 60, gate G0), 3 the contact line on the wall. Ablation only.
3014 void setContactAnglePivot(int mode);
3015
3016
3017 int contactAnglePivot() const;
3018
3019
3020
3021 // --- rung V6 (WO-V6): the DYNAMIC contact angle and hysteresis --------------------------------
3022 //
3023 // Nothing in the V5b fill changes; only the VALUE of theta per contact cell does. See
3024 // `vof/wetting_dynamic.hpp` for the model (Afkhami, Zaleski & Bussmann, JCP 228:5370 (2009)):
3025 //
3026 // theta_Delta^3 = theta_e^3 + 9 Ca_cl ln(Delta/lambda), Ca_cl = mu_l U_cl / sigma
3027 //
3028 // with `Delta` the CELL SIZE and `lambda` an EXPLICIT slip length in cells. The explicit slip is
3029 // the whole point: a VoF contact line's numerical slip is proportional to `Delta`, so without it
3030 // the imposed angle is silently grid-dependent (VOF_PLAN §6). NEVER report a dynamic-wetting
3031 // result without stating lambda.
3032 //
3033 // @param thetaEDeg the equilibrium (static base) angle, degrees. Also becomes the static angle.
3034 // @param slipCells lambda / Delta, in CELLS. Must lie in (0, 1) — lambda >= Delta would make
3035 // ln(Delta/lambda) <= 0 and REVERSE the correction.
3036 // @param muLiquid the LIQUID dynamic viscosity entering Ca_cl (solver units).
3037 // @param sigma the surface tension entering Ca_cl; <= 0 means "use set_surface_tension".
3038 void setContactAngleDynamic(double thetaEDeg, double slipCells, double muLiquid,
3039 double sigma = 0.0);
3040
3041
3047 void setWallSlipLength(double lambdaCells);
3048
3049
3051 double wallSlipLength() const;
3052
3053
3055 std::array<int, 3> wallSlipSandwichCells() const;
3056
3057
3058 // theta_a / theta_r, degrees. Composes with the dynamic correction when that is also set: the
3059 // hysteresis selector picks the BASE angle and Cox-Voinov corrects it, except on the PINNED
3060 // branch (theta_r <= theta_app <= theta_a), where the apparent angle itself is imposed and the
3061 // idempotence of the V5b fill (WO-S finding 1) is what makes the contact line stand still.
3062 void setContactAngleHysteresis(double thetaADeg, double thetaRDeg);
3063
3064
3065 // Back to the static V5b angle, byte-identically (the driver's views are kept but never read).
3067
3068
3069 bool contactAngleDynamic() const;
3070
3071
3072 bool contactAngleHysteresis() const;
3073
3074
3075 double contactAngleSlip() const;
3076
3077
3078 // ABLATION: the 3-point in-wall mean of U_cl (default ON). Off = the raw per-cell MAC velocity.
3079 void setContactAngleSmoothing(bool on);
3080
3081
3082 // The angle clamp of the Cox-Voinov cube, degrees (default 1 / 179).
3083 void setContactAngleClamp(double loDeg, double hiDeg);
3084
3085
3086 // The sigma the dynamic correction uses: the explicit override if one was given, else the CSF's.
3087 double effectiveContactSigma() const;
3088
3089
3090 // The per-cell dynamic-wetting state on the inner region: 0 the IMPOSED angle (degrees),
3091 // 1 the measured APPARENT angle (degrees), 2 the smoothed U_cl, 3 Ca_cl, 4 the
3092 // `vof::VofDynamicState`. Non-contact cells read 0 in 1..4 and the static base in 0.
3093 std::vector<double> getVofDynamicField(int which);
3094
3095
3097 long contactCells = 0;
3099 long pureCells = 0;
3100 long parallelCells = 0;
3101 long neutralCells = 0;
3102 long unfilledCells = 0;
3103 double meanApparentAngle = 0.0;
3104 double setAngle = 0.0;
3105 // --- rung V6 (WO-V6), all zero unless a dynamic angle / hysteresis is configured -----------
3106 long dynamicCells = 0;
3107 long pinnedCells = 0;
3109 long recedingCells = 0;
3110 double meanImposedTheta = 0.0;
3111 double meanApparentTheta = 0.0;
3112 double maxCaCl = 0.0;
3113 double maxContactSpeed = 0.0;
3114 };
3115 // The band census of the CURRENT colour field: how many band cells each branch of the fill wrote
3116 // and the mean APPARENT angle the fluid-only normal reported at the contact cells (G1's
3117 // measurement, evaluated on the fill's own data rather than on a post-processed shape).
3119
3120
3121 // Wire the theta field + the wall SDF onto the colour block. Idempotent; called by the setters
3122 // and again by every geometry rebuild (`buildVofGeometry`), since the block can be re-sized.
3123 void applyContactAngle();
3124
3125
3126 // Cell-centre velocity on the colour block, for the V6 contact-line speed. Built from the
3127 // solver's own staggered faces (`0.5*(u(i) + u(i+s_c))`, both valid after `fillVelGhosts`) on
3128 // the INNER region and then run through the colour field's ghost policy, exactly as the wall SDF
3129 // and the fluid-only normals are — that is what keeps the imposed angle decomposition-
3130 // independent (WO-S finding 9 applied to a third field).
3131 void buildVofCellVelocity();
3132
3133
3134
3135 // The colour advector itself (its g=3 block, geometry views and planes). For TESTS: gate G3 of
3136 // `tests/kokkos/test_vof_cutcell.cpp` rebuilds the openness/fraction by an independent route and
3137 // compares against these.
3138 const vof::WyAdvector& vofAdvector() const;
3139
3140
3141 // The sweep permutation index of the NEXT colour advection (`kWySweepPerm[n % 6]`). Exposed so a
3142 // benchmark can hold the permutation fixed, or resume one, across a restart.
3143 // The sweep permutation is `kWySweepPerm[n % 6]`, so this counter is STATE: a run resumed with
3144 // it reset takes a different sweep order and its colour differs at the splitting error (measured
3145 // 6.2e-4 after ONE step of `channel_18`, off a bitwise-identical velocity). The BLOCK container
3146 // keeps its own counter — `VofBlockSet::step_`, which drives `WyAdvector::advect(dt, step_)` for
3147 // every marker — so a restart has to set both, and this is the one call that does it.
3148 void setVofStepParity(long n);
3149
3150
3151 long vofStepParity() const;
3152
3153
3154
3155 // KINEMATIC colour advection: advance C ONCE with the solver's CURRENT face velocity and the
3156 // given dt, with no Navier-Stokes step at all. This is the entry point the advection benchmarks
3157 // (Zalesak, LeVeque) and the cut-cell conservation gates use — a frozen Stokes field advecting a
3158 // colour slab is a pure statement about the advection scheme, with the momentum solve and the
3159 // pressure solve out of the picture.
3160 //
3161 // It REFUSES a velocity field that is not discretely divergence-free to 1e-10: Weymouth-Yue's
3162 // exact conservation is conditional on `sum_f o_f u_f = 0` per cell (the dilation term adds
3163 // `H(C-1/2)` times that residual to EVERY full cell's budget), so a run on a non-solenoidal field
3164 // would report a conservation "defect" that is really the caller's velocity. Use the solver's own
3165 // projected output (run `step()` to a steady state, or call `project()`), never an analytic
3166 // sample.
3168 void advectVofKinematic(double dtPhysArg);
3169
3170
3171 // --- Part III rung W0 (WO-W0): the per-bubble VoF BLOCK container -----------------------------
3172 //
3173 // A THIRD container over the same L1 kernels (`suite/docs/VOF_PLAN.md` §10, the TBFsolver
3174 // `vofBlock` pattern): one bubble = one `WyAdvector` on a small moving global index box with a
3175 // master rank of its own, and the registered `"C"` the closures see is the UNION
3176 // `C = max_blocks C_block`, never the source. Two bubbles that touch therefore CANNOT coalesce
3177 // numerically — coalescence becomes an explicit model decision (rung W4) instead of a numerical
3178 // accident. See `vof/block_container.hpp` for the three index boxes and the ghost policy, and
3179 // `vof/block_exchange.hpp` for why the gather is plain Isend/Irecv and not an NBX handshake.
3180 //
3181 // W0 stops at KINEMATIC transport: `advect_vof_blocks(dt)` is the block twin of `advect_vof(dt)`
3182 // and carries the same divergence-free precondition. NS coupling (union -> closures -> varRho
3183 // projection, per-block curvature + CSF scattered UNPACK_SUM) is rung W12.
3184 //
3185 // Scope at W0: all-fluid (no immersed solid — the cut-cell block is W12), and the seeds are
3186 // spheres given in CELL units. Not composable with `enableVofMomentum` (refused in either order:
3187 // that transport advects the structured colour and skips the slot that advects the markers).
3188 void enableVofBlocks(const std::vector<std::array<double, 4>>& seeds);
3189
3190
3191 // Everything `enable_vof_blocks` does EXCEPT the seeding, so the sphere seeds and the
3192 // general (`enable_vof_blocks_from_field`) seeds share one code path.
3193 void prepareVofBlocks();
3194
3195
3196 // The union the closures see, plus the rung-W1 master assignment on the seeded boxes.
3197 void finishVofBlocks();
3198
3199
3200 bool vofBlocksEnabled() const;
3201
3202
3203 void disableVofBlocks();
3204
3205
3206 // Kinematic block advection with the CURRENT (projected) face velocity — the block twin of
3207 // `advect_vof`. Same precondition: Weymouth-Yue conservation is conditional on the face field
3208 // being discretely divergence-free, so a field that is not is refused rather than silently
3209 // reported as a conservation defect.
3210 // `requireSolenoidal` is the KINEMATIC precondition: the Python entry point prescribes the
3211 // velocity itself, so a field that is not discretely divergence-free is a user error and is
3212 // refused rather than silently reported as a conservation defect. Inside `step()` the advecting
3213 // field is the projection's own output and its residual divergence IS the conservation floor --
3214 // exactly as for the structured `advectVof()`, which has never carried a check here -- so the
3215 // in-step call passes false. (Rung W2: with variable density the projected residual sits at
3216 // ~1e-7 without the exact level-0 apply, which would refuse every coupled step.)
3218 void advectVofBlocks(double dtPhysArg, bool requireSolenoidal = true);
3219
3220
3221 // Per-bubble Lagrangian census. Only this rank's MASTER blocks carry numbers (volume, centroid,
3222 // centroid velocity, the central second moments); the box and the master are replicated.
3223 std::vector<vof::VofBlockStats> vofBlockStats() const;
3224
3225
3226 // max/mean of the per-rank block-cell load under the CURRENT master assignment (round robin at
3227 // W0; the weighted-ORB assignment is W1 and these are the numbers to beat). 1.0 = perfect.
3228 double vofBlockImbalance() const;
3229
3230
3231 // Blocks mastered by each rank, and the inner cells those blocks carry.
3232 void vofBlockCensus(std::vector<long>& masters, std::vector<long>& cells) const;
3233
3234
3235
3236 // --- Part III rung W1 (WO-W12) ----------------------------------------------------------------
3237 //
3238 // (a) Master assignment. `mode` 0 = round robin (W0), 1 = LPT greedy on the block cell counts,
3239 // 2 = weighted ORB over a 1-D block space (core's `BlockDecomposer<1>`). `every` re-runs the
3240 // assignment every `every` block steps, MIGRATING the colour of any block that changed
3241 // master (nothing else in a block is state). Applied immediately.
3242 void setVofBlockAssign(int mode, long every);
3243
3244
3245 int vofBlockAssign() const;
3246
3247
3248 // The imbalance the CURRENT `assignMode` would give without applying it — so a study can put the
3249 // three modes side by side on one swarm without perturbing the run.
3250 double vofBlockImbalanceOf(int mode) const;
3251
3252
3253 // (b)/(c) instrumentation: device-resident packing on/off, and the block-pool hit census.
3254 void setVofBlockDeviceStaging(bool on);
3255
3256
3257 void setVofBlockPool(bool on);
3258
3259
3260 std::array<long, 2> vofBlockPoolStats() const;
3261
3262
3263
3264 // General seeding (rung W1): one block per given GLOBAL index box, with the colour taken from
3265 // the field `set_vof` installed. A sphere seed is a convenience over this; a Hysing bubble, a
3266 // quasi-2-D cylinder or any scanned marker enters here. The boxes are the BUBBLE extents (the
3267 // container grows them by the 3-cell margin itself) and must not overlap in a way that makes a
3268 // cell belong to two markers at seed time -- the gather is a copy, not a union, so a cell inside
3269 // two boxes would be given to both markers.
3270 void enableVofBlocksFromField(const std::vector<std::array<int, 6>>& boxes);
3271
3272
3273
3274 // --- rung W3: checkpoint / restart of the block container ------------------------------------
3275 //
3276 // The block's own inner colour is a block's ONLY state, so {box, colour} per block is a complete
3277 // checkpoint -- and unlike `enableVofBlocksFromField` it is exact when two markers touch (the
3278 // seeding gather out of the UNION would give each a slice of the other; WO-W12 open item 5).
3279 std::vector<double> vofBlockColour(long id);
3280 std::vector<double> vofBlockKappa(long id);
3282 const vof::VofBlockSet* vofBlockSet() const { return vofBlocks_.get(); }
3283
3284
3285 void enableVofBlocksFromColours(const std::vector<std::array<int, 6>>& boxes,
3286 const std::vector<std::vector<double>>& colours);
3287
3288
3289
3290 // --- rung W2: the block CSF ------------------------------------------------------------------
3291 //
3292 // Turn the surface tension of `set_surface_tension` into a PER-BLOCK force: each marker runs its
3293 // own curvature cascade on its own box and forms the V4 balanced-force face force there, and the
3294 // three face fields are summed into the local patch (UNPACK_SUM). Requires the block container.
3295 void enableVofBlockCsf();
3296
3297
3298 // Per-block curvature + CSF face force, scattered SUM into the registered face-force fields the
3299 // RHS reads. Called at the head of every step by `updateVofCurvature()`.
3300 void computeVofBlockCsf();
3301
3302
3303 // --- vof_overlap_design §5.6 (WO-7): phantom (gas-gas) interfaces of overlapping markers -------
3304 //
3305 // `S = sum_k C_k` is scattered beside the MAX union after every block advection (and when the
3306 // block CSF or the overlap density is switched on). While the block CSF is on and any cell has
3307 // `S > 1 + 1e-2` (a GLOBAL max; §11), the capillary limit --
3308 // `vof_step_limits()['capillary_dt']` and the `step()` check -- uses `2 rho_min` in place of `rho_l + rho_g`: the part of marker A's
3309 // surface inside B is an interface with rho_g on both sides. Detected at the end of step n, it
3310 // binds from step n+1. Not under the opt-in overlap density below, whose phantom is an ordinary
3311 // liquid-gas interface.
3313 double maxSum = 0.0;
3314 double excess = 0.0;
3315 long cells = 0;
3316 bool active = false;
3317 };
3318 VofOverlapCensus vofBlockOverlapCensus() const { return vofOverlap_; }
3319 // Recompute S and the census (collective under MPI). Public for nvcc's extended-lambda rule.
3320 void updateVofBlockOverlap();
3321 // OPT-IN MODEL, default off (MAX verbatim): the union colour that feeds rho(C), mu(C) (and the
3322 // registered "C") becomes `C_eff = S <= 1 + 1e-8 ? max_k C_k : max(0, 2 - S)` -- the overlap
3323 // volume as film liquid ("the tent"). See vof_overlap_design §5.6 for what it costs.
3325 // Per-step marker DEBRIS removal with exact volume return (vof_overlap_design §5.3, predicate
3326 // §11). Default: ON under enable_vof_block_csf, OFF otherwise (a kinematic block run is
3327 // unchanged). May be called before or after enable_vof_blocks / enable_vof_block_csf.
3328 void setVofBlockDebris(bool enabled);
3329 // vof_overlap_design §5.6: the phantom-aware capillary bound (2 rho_min while block markers
3330 // overlap). Default ON; `false` restores the pre-§5.6 bound (the ablation, review finding 5).
3332
3333
3334 // DIAGNOSTIC: the scattered block CSF face force on this rank's inner cells, component c (the
3335 // low face of each cell, the same convention `addCsfRhs` uses). This is the field the block mode
3336 // adds to the RHS; comparing it across decompositions is how a scatter defect is localised.
3337 std::vector<double> getVofBlockForce(int c);
3338
3339
3340 // The summed branch census of the last block CSF (LOCAL to this rank).
3342
3343
3344
3345 // Harmonic instead of arithmetic rho_f in the pressure projection (WO-J item 5). DEFAULT OFF and
3346 // it should stay off — read the long note in mac_pressure.hpp before turning it on: arithmetic
3347 // rho_f IS the harmonic mean of the mobility 1/rho (the series-correct choice for a normal flux)
3348 // and is what makes the discrete hydrostatic balance exact, because the momentum time term and
3349 // the face body force interpolate rho arithmetically and are NOT switched by this flag. Shipped
3350 // as a measured knob for the coefficient-coarsening question, not as an alternative scheme.
3351 void setRhoFaceHarmonic(bool on);
3352
3353
3357 void setPressureExactResidual(bool on);
3358
3359
3366
3367
3368 bool outflowOperatorCoefficient() const;
3369
3370
3371 bool pressureExactResidual() const;
3372
3373
3374 bool rhoFaceHarmonic() const;
3375
3376
3377 // WO-R item 4 asked for the `1/rho_f` factor on the high-side outflow correction;
3378 // `doc/variable_density_projection.md` §4 listed its absence as a defect. WO-R measured the
3379 // factor making the outflow divergence SEVEN ORDERS WORSE and recorded the item as refuted —
3380 // correctly, for the operator as it then was. WO-R2 item 1 fixed that operator
3381 // (`CutcellMG::setOutflowCoefficient`: the Dirichlet domain-face rows no longer overwrite
3382 // `buildRhoCoeff`'s `open_f*rho0/rho_f` with the literal 1.0) and the verdict INVERTED:
3383 //
3384 // stratified duct, ratio 10, 5 steps, max|div(open u)| of the PROJECTED field
3385 // operator = raw openness operator = rho0/rho_f (WO-R2)
3386 // without the factor 8.76e-10 9.97e-05
3387 // with the factor (this knob) 9.24e-03 8.31e-10
3388 // (tests/kokkos/test_vof_bc.cpp gate F2; at ratio 1 the two are bitwise equal, 1.40e-17.)
3389 //
3390 // The mechanism is one rule: a projection correction removes the discrete divergence only if it
3391 // uses the SAME face coefficient the operator row used. Both table columns obey it; the fixed
3392 // operator is the one whose coefficient is also the PHYSICALLY right mobility at the outlet
3393 // (the low-side outlet had no consistent pairing at all before the fix — see the Nusselt film).
3394 //
3395 // DEFAULT ON since WO-R2 (`set_outflow_rho_correction(False)` is the ablation). Bitwise inert at
3396 // constant density either way (rho_f == rho0 makes the factor exactly 1), and gated on varRho.
3397 void setOutflowRhoCorrection(bool on);
3398
3399
3400 bool outflowRhoCorrection() const;
3401
3402
3403
3404 // --- two-phase open boundaries (rung V-BC, WO-R) ---------------------------------------------
3405 //
3406 // Rung V2a gave the colour field one non-periodic ghost rule, `clampFill` (globally-clamped
3407 // zero-gradient). It is the right rule for a WALL and the wrong one for an INFLOW, where the
3408 // colour of the incoming fluid is a prescribed datum. Three API calls cover the three domain-BC
3409 // types; `src/vof/colour_bc.hpp` carries the rules and the reasoning, and this is the plumbing:
3410 //
3411 // set_vof_inflow(face, C) type-2 face: the colour of the incoming fluid
3412 // set_vof_inflow_profile(face, C2d) the same, per position on the face
3413 // set_vof_backflow(face, C) type-3 face: the colour of fluid that flows back IN
3414 // (a type-1 wall keeps `clampFill` = the 90 deg neutral continuation; WO-S replaces it)
3415 //
3416 // Setting any of them ARMS the rung: the advector gets the out-of-domain mask (so a boundary
3417 // donor is fluxed algebraically as `C_donor * a` rather than as a reconstructed PLIC slab — see
3418 // `wyFaceFluxBc`), the per-face boundary liquid volumes start being accumulated
3419 // (`vof_diagnostics()['inflow_volume'] / ['outflow_volume']`, and `vof_bc_volumes()` per face),
3420 // and the property ghosts of an inflow face follow the inflow colour through the closures
3421 // instead of copying the interior (item 5 below). With none of them set NOTHING changes.
3422 //
3423 // The colour of the incoming fluid may be FRACTIONAL and it then means "this fraction of the
3424 // incoming flux is liquid" — a flux statement, not a sub-cell interface position. That is
3425 // exactly what the algebraic boundary flux implements.
3426
3429 void setVofInflow(int f, double value);
3430
3431
3435 void setVofInflowProfile(int f, const std::vector<double>& prof, int nb, int nc);
3436
3437
3442 void setVofBackflow(int f, double value);
3443
3444
3445 bool vofBcActive() const;
3446
3447
3451 std::vector<double> vofBcVolumes() const;
3452
3453
3457 std::vector<double> vofBcVolumesTotal() const;
3458
3459
3460 void resetVofBcVolumes();
3461
3462
3463
3464 // nvcc requires the enclosing member of an extended device lambda to be public; these are
3465 // implementation detail (see the same note above `patchScalarDirichletFace`).
3466
3480 void vofApplyColourBc(CCField f);
3481
3482
3483
3500 void vofBcPropGhosts(CCField f);
3501
3502
3503
3507 void vofRebuildBcBlock();
3508
3509
3510
3514 CCField resampleFaceScalar(const std::vector<double>& prof, int nb, int nc, int face, C3 ext,
3515 int g, int& outNc);
3516
3517
3518
3522 void checkVofBcFace(int f, int wantType, const char* who);
3523
3524
3526 void vofBcArm();
3527
3528
3530 void vofHarvestBcVolumes();
3531
3532
3533
3534 // --- interface curvature (rung V3, WO-O) -----------------------------------------------------
3535 //
3536 // `compute_vof_curvature()` fills two registered G=2 cell fields from the CURRENT colour field:
3537 //
3538 // "kappa" kappa = 2H in units of 1/h (cell units). Multiply by 1/h for physical units.
3539 // POSITIVE for a convex blob of liquid: a sphere of liquid of radius R cells
3540 // reads +2/R. WO-P (balanced-force CSF) is the consumer.
3541 // "kappa_branch" which tier of the cascade produced it (`vof::CurvatureBranch`): 0 not
3542 // interfacial, 1 HF, 2 HF in a non-preferred direction, 3 mixed-HF fit (off by
3543 // default), 4/5 the PLIC-volumetric paraboloid fit, 6 NO estimate.
3544 //
3545 // Reading "kappa" without reading "kappa_branch" is a mistake: kappa is 0 both where there is no
3546 // interface (branch 0, correct) and where the cascade could not produce an estimate (branch 6,
3547 // which must never happen and is loud when it does). The branch field is the difference.
3548 //
3549 // The whole cascade is a pure local stencil on the colour field's g = 3 block — no reductions,
3550 // no new halo — so it is bitwise decomposition-independent by construction. See
3551 // `vof/curvature.hpp` for the cascade, its literature anchors and its measured branch shares.
3552 void computeVofCurvature();
3553
3554
3555 // The branch census of the last `computeVofCurvature()` — LOCAL to this rank (the driver is
3556 // MPI-free; a distributed caller sums them).
3558
3559
3562 std::vector<double> getVofCurvature();
3563
3564
3565 std::vector<double> getVofCurvatureBranch();
3566
3567
3568 // Wendland support width of the PV fallback fit, in cell units (Han et al.: 2.5 with a 5^3
3569 // stencil; 3.5 recovers first-order spurious-current convergence on a translating droplet and
3570 // 4.5 over-smooths and destroys it). Exposed for WO-P's sweep.
3571 void setVofCurvatureWeightWidth(double d);
3572
3573
3574 double vofCurvatureWeightWidth() const;
3575
3576
3577 // Tier 2b, the mixed height-position fit. OFF by default and it should stay off — see
3578 // `vof::VofCurvature::useMixedHeightFit` for the measurement that put it there.
3580
3581
3582 bool vofCurvatureMixedHeightFit() const;
3583
3584
3585
3586 // --- balanced-force surface tension (rung V4, WO-P) -------------------------------------------
3587 //
3588 // `set_surface_tension(sigma)` turns on the continuum surface force
3589 //
3590 // F_c(i) = sigma * kappa_f(i) * ( C(i) - C(i - s_c) ) / h (1)
3591 //
3592 // at every staggered velocity unknown, added to the momentum RHS at the same place, in the same
3593 // units and with the same cut-cell rescale as the incremental scheme's -(P(i) - P(i - s_c)).
3594 // `kappa_f` is the arithmetic mean of the two cells' curvatures where both carry one, the single
3595 // available one where only one does (`vof/surface_tension.hpp`).
3596 //
3597 // WHY (1) AND NOT AN INTERPOLATED CELL FORCE — the whole content of the rung. `C(i) - C(i - s_c)`
3598 // is the projection's OWN face difference. With a constant kappa the force is therefore exactly
3599 // the discrete gradient of `sigma*kappa*C`, i.e. it lies in the range of the operator the
3600 // projection inverts, so the projection removes it completely and a static drop stays at machine
3601 // zero. Face-interpolating a cell-centred `sigma*kappa*grad C` — the obvious way to reuse the
3602 // per-cell body-force machinery — produces a field that is NOT a discrete gradient of anything,
3603 // the projection cannot annihilate it, and what is left is the classical spurious current
3604 // (Francois et al. 2006; Popinet 2009). This is the momentum analogue of the three-way rho_f
3605 // consistency that makes the hydrostatic acid test exact.
3606 //
3607 // SIGN. `kappa` is positive for a convex blob of LIQUID (rung V3) and `C` is the liquid fraction,
3608 // so (1) with a plus sign gives the Young-Laplace overpressure INSIDE the drop: at equilibrium
3609 // `P = sigma*kappa*C + const`, the discrete solution of the projection, exactly.
3610 //
3611 // UNITS. `sigma` is in the solver's own units, in which the cell size is 1 (as are `rho`, `mu`
3612 // and `set_body_force`). `kappa` is in 1/h and `C(i) - C(i-s)` is `h * dC/dx`, so (1) is a force
3613 // per unit volume.
3614 //
3615 // REQUIREMENTS. VoF must be enabled (staggered only). The curvature cascade runs once per step,
3616 // at the head, from the SAME colour field the density closure sees.
3617 void setSurfaceTension(double sigma);
3618
3619
3621 double surfaceTension() const;
3622
3623
3624 // The wisp threshold above, exposed so it can be swept/ablated. Default 1e-8; 0 restores the
3625 // unguarded V3 predicate and, with surface tension on, reproduces the instability it exists for.
3626 void setVofInterfaceEps(double eps);
3627
3628
3629 double vofInterfaceEps() const;
3630
3631
3632 // Curvature admissibility clip (doc/vof_overlap_design.md §5.1, §11): |kappa| <= kappa_max on
3633 // every interfacial cell of every BLOCK marker's cascade (the single-field cascade never clips).
3634 // `kappaMaxPhys` is PHYSICAL (1/length); a negative value selects the default 1/Delta_min.
3635 // `enabled = false` restores the pre-clip block cascade verbatim. Default: ON at 1/Delta_min.
3636 void setVofBlockKappaClip(bool enabled, double kappaMaxPhys = -1.0);
3637
3638
3639 // ABLATION: 0 = the balanced-force face difference (default, the only production mode);
3640 // 1 = a cell-centred sigma*kappa*grad(C) face-interpolated like an ordinary body force. See
3641 // `addCsfRhsCellInterp`. Kept so the ctest can measure what the operator pairing is worth.
3642 void setCsfMode(int m);
3643
3644
3645 int csfMode() const;
3646
3647
3648 // Rung W2: the CSF may be formed on the BLOCKS instead of the global colour+kappa fields, in
3649 // which case there is no `kappaField_` at all. `vofBlockCsf()` is false whenever the block
3650 // container is absent (`vofBlocks_` is null unless `enable_vof_blocks` ran), so the expression
3651 // below reduces to W0's character for character on every non-block path.
3652 bool vofBlockCsf() const;
3653
3654
3655 bool csfActive() const;
3656
3657
3658
3659 // INSTRUMENT (not a configuration): stop recomputing the curvature at the head of each step and
3660 // use whatever is in the "kappa" / "kappa_branch" fields. Together with `set_vof_kappa_constant`
3661 // this isolates the BALANCED-FORCE identity from the curvature estimator — the exactness gate of
3662 // this rung, which must hold at machine zero for a curvature that is merely constant, whether or
3663 // not it is the right one.
3664 void setVofKappaFrozen(bool on);
3665
3666
3667 bool vofKappaFrozen() const;
3668
3669
3670 // INSTRUMENT: set kappa to a constant over the WHOLE block (inner + ghosts) and mark every cell's
3671 // branch as a valid estimate, then freeze it. The force (1) is then exactly the discrete gradient
3672 // of `sigma*kappa*C`, so the projection must annihilate it to round-off from ANY colour field.
3674 void setVofKappaConstant(double kappa);
3675
3676
3677
3678 // The Brackbill (1992) / Denner & van Wachem (2015) capillary time-step limit
3679 // `sqrt((rho_1 + rho_2) h^3 / (4 pi sigma))`. +inf when surface tension is off.
3680 //
3681 // The density SUM is taken from the declared phase pair when momentum consistency is on
3682 // (`enable_vof_momentum` validated it against the closure), and otherwise from `min(rho) +
3683 // max(rho)` over the current density field — an MPI_MIN/MPI_MAX pair, so it is exact and
3684 // decomposition-independent. It is the sum, not a mean: both phases oscillate.
3693 double capillaryDt();
3694
3695
3697 double capillaryDtInternal();
3698
3699
3700 // Safety factor on the capillary limit: `step()` throws when `dt > factor * capillaryDt()`.
3701 // Default 1.0 — Denner & van Wachem measured the Brackbill prefactor to BE the stability
3702 // boundary, so there is no margin built into the formula itself. Set it huge to disable the
3703 // check, exactly as `set_vof_cfl_limit` is the escape hatch for the Weymouth-Yue cap.
3704 void setCapillaryCfl(double f);
3705
3706
3707 double capillaryCfl() const;
3708
3709
3710
3711 // Both explicit two-phase step limits at the CURRENT state, and which one binds. This is the
3712 // number WO-P asks for: at pore-scale capillary numbers the capillary dt, not the Weymouth-Yue
3713 // CFL, is expected to be the binding constraint, and that decides whether implicit surface
3714 // tension is ever worth revisiting.
3716 double courant = 0.0;
3717 double cflDt = 0.0;
3718 double capillaryDt = 0.0;
3719 double binding = 0.0;
3720 bool capillaryBinds = false;
3721 };
3724
3725
3726 // rho_1 + rho_2 for the capillary limit. Public because nvcc refuses an extended
3727 // __host__ __device__ lambda inside a private member function (the WO-O build note).
3728 double phaseDensitySum();
3729 // The min and max of the density field behind `phaseDensitySum()` (lo + hi is that sum).
3730 void phaseDensityRange(double& lo, double& hi);
3731
3732
3733 // Head-of-step curvature refresh + the capillary dt check. No-op unless surface tension is on.
3734 //
3735 // The curvature is taken from the colour field the step is ABOUT to run with — the same field
3736 // `updateProperties()` just turned into rho(C) and mu(C), i.e. C^{n+1} under momentum consistency
3737 // (the VoF stage ran at the head) and C^n without it (it runs at the tail). Either way kappa,
3738 // rho and the colour in the force (1) are the same time level, which is what the balance needs.
3739 // ISSUES sweep item 2. One step at a dt re-picked from the CURRENT state, returning the dt used.
3740 //
3741 // Why this exists as an entry point rather than as a recipe in every driver: both limits
3742 // `vof_step_limits()` reports are INSTANTANEOUS. Re-picking every ten steps -- what the free
3743 // bubble and falling film studies do, and what the gallery's rising-bubble driver copied -- is
3744 // safe only where the face field changes slowly. In a packing it is not: a throat jet can grow
3745 // max|uf| by more than 50 % inside ten steps (measured, examples/bubble-through-packing), and
3746 // the Weymouth-Yue cap is a hard throw. And `capillary_dt` is itself STATE-dependent (it reads
3747 // the density field, so the first call on a domain whose closures have not run reports the base
3748 // `set_rho` value -- measured 7.1x too large on a gas-filled pore-scale domain).
3749 //
3750 // dt = min(cfl_target * cfl_dt, capillary_cfl * capillary_dt, dt_max), from the state as it
3751 // stands at the call, then `set_dt` + `step()`. This is EXACTLY what the three gallery drivers
3752 // (rising-bubble, bubble-through-packing, trickle-flow-packing) do in Python -- and bitwise so,
3753 // because IEEE multiplication is monotone, hence `f*min(a,b) == min(f*a,f*b)`.
3754 //
3755 // `set_dt` / `step()` semantics are untouched: this is a wrapper, not a mode.
3756 double stepAdaptive(double cflTarget, double capillaryCflTarget, double dtMax);
3757
3758
3759 // ISSUES sweep item 1: the two explicit two-phase stability caps, evaluated at the head of
3760 // `step()` on the state the call STARTS from, so a rejected dt costs nothing.
3761 //
3762 // What this can and cannot pre-empt, measured rather than assumed:
3763 // * the CAPILLARY cap is a function of dt, sigma, h and the density field only, so hoisting it
3764 // is exact -- it fires here for every dt that would have made `updateVofCurvature` throw
3765 // (the one difference is that rho is the PREVIOUS step's; the authoritative check inside
3766 // `updateVofCurvature`, on this step's rho, is deliberately left in place).
3767 // * the Weymouth-Yue cap is a function of the ADVECTING face field. Under
3768 // `enable_vof_momentum` the colour rides at the head of the step, so the field checked here
3769 // IS the field the advection will use and the pre-check is exact. Without it the advection
3770 // sits at the TAIL and rides THIS step's projected output, which does not exist yet -- so the
3771 // pre-check runs on u^n (the previous step's projected field, i.e. exactly what the previous
3772 // step's advection used) and pre-empts every case where the CAP or the DT moved, but not the
3773 // case where the projection itself accelerates the field inside the step. That residual is
3774 // what `step_adaptive` (item 2) removes, by re-picking dt from the same limits every step.
3775 void vofStepPrecheck();
3776
3777
3778 std::string capillaryThrowMessage(double cap) const;
3779
3780
3781 void updateVofCurvature();
3782
3783
3784
3785 // Advance the colour field one dt with the just-projected face velocities. Called by step()
3786 // immediately before advanceScalars(); exposed so a test can drive it in isolation.
3787 //
3788 // WHY HERE (the ordering, WO-J item 2). Weymouth-Yue conserves volume exactly only against a
3789 // DISCRETELY divergence-free face field: the dilation term adds H(C-1/2)*div*dt/h to every
3790 // cell's budget, interior full cells included, so the conservation floor is the advecting field's
3791 // own discrete divergence residual (WO-E finding 2 — with an analytically-but-not-discretely
3792 // solenoidal field the floor sits at O(h^2), ten orders above the gate). The only field in the
3793 // step with that property is the OUTPUT of the projection, which is exactly where
3794 // `advanceScalars` already sits and for exactly the same reason. u* is not divergence-free; u^n
3795 // is, but u^n at the top of step n+1 IS u^{n+1} at the bottom of step n, so the two placements
3796 // are the same point in the timeline and differ only in which side of `updateProperties()` they
3797 // fall on. Taking the `advanceScalars` slot therefore means the properties of step n are
3798 // rho(C^n), mu(C^n) — the same time level as the velocity base u^n, and the same segregated
3799 // contract every other multiphysics field in this solver obeys.
3800 //
3801 // HOW WO-K (rung V2b, momentum-consistent transport) DROPS IN. WO-K needs the momentum advection
3802 // to use the same geometric fluxes, the same sweep order and the SAME frozen dilation flag as
3803 // this colour advection, on half-shifted control volumes built by clipping the SAME PLIC planes
3804 // (`plicSlabVolume` is scale/offset invariant, so that is a rescale). Three structural facts make
3805 // that a local change here:
3806 // * everything WO-K must share lives inside `vofAdv_` and survives the call — the frozen flag
3807 // `cc_`, the per-sweep planes `mx_/my_/mz_/alpha_`, the face Courant numbers and the
3808 // permutation index. WO-K adds a sibling advector for rho^c u_c driven from those SAME
3809 // members rather than recomputing anything, so "sharing the fluxes" is a data-flow fact, not
3810 // a convention two call sites have to keep.
3811 // * the colour advection is ONE call (`advectVof()`), not a set of calls scattered through the
3812 // step, so moving it to the head of the predictor (where the momentum advection lives) is a
3813 // one-line move, and it moves to a velocity field — u^n — that is the same field it consumes
3814 // today, one step earlier in wall-clock and identical in content.
3815 // * the half-shifted colour field C^c is a new g=3 field on the SAME advector block, so it
3816 // needs no new bridge and no new halo: `vofFillGhosts` already carries any field on that
3817 // block, and the velocity is already embedded there (`bridgeVelocityToVof`).
3818 // What WO-K must NOT do, and what this structure keeps honest: interpolate C to build C^c. The
3819 // planes are here; clip them.
3820 void advectVof();
3821
3822
3823
3824 // --- momentum-consistent transport (rung V2b, WO-K) ------------------------------------------
3825 //
3826 // Turn on the transport of `rho^c u_c` on the half-shifted MAC control volumes by the SAME
3827 // geometric fluxes, the same sweep order and one frozen dilation flag as the colour advection of
3828 // the same step (`vof/momentum_advect.hpp` carries the construction and the consistency identity).
3829 //
3830 // The two phase densities are required EXPLICITLY rather than read off the closure: the momentum
3831 // flux is `rho_g (a - F) + rho_l F` with `F` the geometric LIQUID flux, so the scheme needs to
3832 // know which density each phase carries, and inferring that from a `rho` field would be a silent
3833 // dependence on the closure kind. `enableVofMomentum` validates the pair against the registered
3834 // density field on the spot (see `checkVofPhaseDensities`), so a mismatched call fails loudly.
3835 //
3836 // WHY THE ADVECTION MOVES TO THE HEAD OF THE STEP. WO-J placed `advectVof()` in the
3837 // `advanceScalars()` slot because Weymouth-Yue needs a discretely divergence-free advecting field
3838 // and the projection's output is the only one in the step. With momentum consistency the SAME
3839 // fluxes must also carry the momentum, and the momentum advection has to happen before the
3840 // predictor builds its RHS — so the whole VoF stage moves to the top of `step()`, where the
3841 // advecting field is `u^n`, i.e. the PREVIOUS step's projected output. That is the same field, one
3842 // step earlier in wall-clock: `u^{n+1}` at the bottom of step n IS `u^n` at the top of step n+1
3843 // (WO-J's own note). The one real consequence is at step 0, where `u^0` is whatever the user set
3844 // and is only divergence-free if the user made it so; every gate in this rung starts from rest or
3845 // from a uniform field, both exactly divergence-free.
3846 //
3847 // With this placement `updateProperties()` then sees `C^{n+1}`, so the step runs with
3848 // `rho(C^{n+1})`, `mu(C^{n+1})` — the density of the time level the momentum is being advanced TO,
3849 // which is what the conservative form `rho^{n+1} u^{n+1} = rho^n u^n - div(rho u u) dt + ...`
3850 // wants.
3851 //
3852 // WHAT IS *NOT* CHANGED, DELIBERATELY: the momentum time-term face density, the face body force
3853 // and the projection coefficient all keep the arithmetic face mean `1/2 (rho(i) + rho(i-s_c))`.
3854 // That three-way agreement is what makes discrete hydrostatic balance exact
3855 // (`doc/variable_density_projection.md` §1) and it is a validated kernel. Momentum consistency
3856 // enters as the ADVECTIVE base velocity `u^adv` replacing `u^n - dt*adv(u^n)`: the momentum
3857 // equation solved is `rho_f (u* - u^adv)/dt = -grad p + visc + f`, i.e. exactly the conservative
3858 // update divided through by the face density. Swapping the time term to the clipped `rho^c`
3859 // instead would break the hydrostatic acid test at O(d rho) — measured and recorded in the WO-K
3860 // findings.
3863 void enableVofMomentum(double rhoGasPhys, double rhoLiquidPhys);
3864
3865
3866 bool vofMomentumEnabled() const;
3867
3868
3869 // Floor on rho^c in the recovery divide u = (rho^c u)/rho^c, as a FRACTION of min(rho_g, rho_l)
3870 // (default 1e-6). rho^c leaves [rho_g, rho_l] only through a wisp in the half-shifted colour, and
3871 // driving it to zero would need C^c ~ -1/(ratio-1); the floor is a guard, not a model, and
3872 // `vof_momentum_diagnostics()` reports how many control volumes it actually touched.
3873 void setVofRhoFloorFrac(double f);
3874
3875
3876 double vofRhoFloorFrac() const;
3877
3878
3879 double vofRhoFloor() const;
3880
3881
3882 // MinMod-limited donor reconstruction in the momentum flux. OFF by default — on a control volume
3883 // a sweep empties, the slope's deviation from the volume's own velocity is amplified by
3884 // drho*F/rho^c, which is unbounded in the density ratio; measured, it grew the uniform-velocity
3885 // residual to 2.2e-10 at ratio 1e4 over 50 steps while plain donor-cell upwind stayed flat at
3886 // 6.7e-16. Harmless at ratio 1e3. See vof/momentum_advect.hpp.
3887 void setVofMomentumMuscl(bool on);
3888
3889
3890 // Ablation: the literal reading of "the same frozen dilation flag" (the PRESSURE-cell flag on the
3891 // shifted control volume instead of its structural analogue).
3892 void setVofMomentumCellFlag(bool on);
3893
3894
3895 // Ablation: drop the Weymouth flux clamp on the shifted control volume. With it off the
3896 // half-shifted colour leaves [0,1] by O(a^2) and rho^c goes NEGATIVE at high ratio — the
3897 // measurement that the clamp is a necessity, not a habit. See vof/momentum_advect.hpp point 3.
3898 void setVofFluxClamp(bool on);
3899
3900
3902
3903
3904 // The recovered advected velocity of component c on the inner cells (the momentum RHS's time
3905 // base). Exposed so a test can gate the uniform-velocity identity on the advection ALONE, with
3906 // the projection and the momentum solve out of the picture.
3907 std::vector<double> getVofAdvectedVelocity(int c);
3908
3909
3910
3911 // The coupled colour + momentum advection. Called from the head of step() when momentum
3912 // consistency is on; exposed so a test can drive it in isolation.
3913 void advectVofMomentum();
3914
3915
3916
3917
3918 // --- Phase change (Part II, rungs P0/P1 — WO-P01) --------------------------------------------
3919 //
3920 // The kernel set of `suite/docs/VOF_PLAN.md` §9 in its planar form, following Boyd & Ling (2023)
3921 // and Malan et al. (2021): a mass flux `mdot` on interfacial cells (prescribed at P0, from
3922 // one-sided pure-cell temperature gradients at P1), interface regression by a PLIC PLANE SHIFT
3923 // with exact clip-and-redistribute, and the volumetric divergence source shifted into the
3924 // compact pure-gas layer behind the interface so the interfacial cell's own face velocity stays
3925 // the LIQUID velocity and Weymouth-Yue advects the colour with a field it is entitled to.
3926 // Container-free geometry/physics lives in `vof/phase_change.hpp`; this is the block walk.
3927 //
3928 // ORDER WITHIN step() (and why). `phaseChangeStep()` runs at the HEAD of the step, before
3929 // `updateProperties()`:
3930 // 1. `mdot`, the PLIC area `A_G` and the unit normal `n` are built from (C^n, T^n);
3931 // 2. the divergence source is deposited into pure gas cells (read by `project()`);
3932 // 3. the regression is applied to the SAME C^n the planes were reconstructed from.
3933 // Doing the regression here rather than after the advection is what keeps the plane, the area
3934 // and the colour it is subtracted from at ONE time level; `updateProperties()` then sees the
3935 // post-regression colour, so rho(C) and the momentum/projection coefficients are consistent with
3936 // the interface the step actually runs with. The colour advection keeps its WO-J slot at the
3937 // bottom of the step (it needs the projected, discretely divergence-free face field).
3938 //
3939 // SCOPE at this rung, all enforced with a message: staggered grid, no immersed solid (the
3940 // solid-clipped flux polygons and the cut-cell source deposit are a later rung), and not
3941 // composable with `enable_vof_momentum` (both own the head of the step and the momentum flux
3942 // would have to carry the phase-change mass transfer as well).
3944 double mdotMin = 0.0, mdotMax = 0.0, mdotMean = 0.0;
3946 double removedVolume = 0.0;
3947 double redistributed = 0.0;
3948 long deficitCells = 0;
3949 long excessCells = 0;
3950 double sourceSum = 0.0;
3951 long sourceCells = 0;
3952 long fallbackCells = 0;
3953 double unresolved = 0.0;
3954 double minC = 0.0, maxC = 0.0;
3955 double area = 0.0;
3956 double bandDiv = 0.0;
3957 double Tmin = 0.0, Tmax = 0.0;
3958 long areaHf = 0;
3959 long areaPv = 0;
3960 long areaNone = 0;
3961 double areaOrphan = 0.0;
3963 double mdotFit = 0.0;
3965 double qOperator = 0.0;
3967 double qOrphan = 0.0;
3969 };
3970
3993 double hOpen = 0.0;
3994 double hOpenNew = 0.0;
3995 double hLiquid = 0.0;
3996 double hMasked = 0.0;
3997 double dEoverwrite = 0.0;
3999 double dEoverwriteNew = 0.0;
4000 double eEnter = 0.0;
4001 double eLeave = 0.0;
4002 double qGfm = 0.0;
4003 double qBehind = 0.0;
4006 long nEnterLiquid = 0;
4007 long nEnterGas = 0;
4008 long nLeaveLiquid = 0;
4009 long nLeaveGas = 0;
4010 long nMasked = 0;
4011 long calls = 0;
4012 };
4013
4021 void enablePhaseChange(double rhoG, double rhoL, double hlv);
4022
4023
4024 bool phaseChangeEnabled() const;
4025
4026
4027
4031 void setMassFluxUniform(double v);
4032
4033
4036 void setMassFlux(const std::vector<double>& v);
4037
4038
4045 void setPhaseChangeThermal(const std::string& tname, double Tsat, double kg, double kl,
4046 double Rint);
4047
4048
4050
4051
4052
4053 // --- WO-P23 (rungs P2/P3) --------------------------------------------------------------------
4054
4068
4069
4070 bool phaseChangePlaneDirichlet() const;
4071
4072
4073
4079 void setPhaseChangeQuadraticFit(bool on);
4080
4081
4082 bool phaseChangeQuadraticFit() const;
4083
4084
4085
4111 void setPhaseChangeArea(int mode);
4112
4113
4114 int phaseChangeArea() const;
4115
4116
4117
4122 double vofInterfaceArea();
4123
4124
4125
4129
4130
4131
4133 SField pcAreaCascadeField() const;
4134
4135
4136
4147 void setPhaseChangeEnergy(double rcpGas, double rcpLiquid);
4148
4149
4152 void setPhaseChangeEnergyMuscl(bool on);
4153
4154
4155 bool phaseChangeEnergyMuscl() const;
4156
4157
4159
4160
4161 bool phaseChangeEnergy() const;
4162
4163
4164
4168 void setPhaseChangeBudget(bool on);
4169
4170
4171 bool phaseChangeBudget() const;
4172
4173
4174
4178
4179
4180 bool phaseChangeCarryConserve() const;
4181
4182
4183
4190 void setPhaseChangeFitCurvature(double kappa);
4191
4192
4193 double phaseChangeFitCurvature() const;
4194
4195
4196
4216 void setPhaseChangeEnergyOrder(int order);
4217
4218
4219 int phaseChangeEnergyOrder() const;
4220
4221
4222
4225 void setPhaseChangeMdotOperator(bool on);
4226
4227
4228 bool phaseChangeMdotOperator() const;
4229
4230
4231
4234 void setPhaseChangeGfmOrder(int order);
4235
4236
4237 int phaseChangeGfmOrder() const;
4238
4239
4240
4246
4247
4248 bool phaseChangeCurvatureDistance() const;
4249
4250
4261
4262
4263 bool phaseChangeDepositFallback() const;
4264
4265
4267 double phaseChangeQOperator() const;
4268
4269
4270 double phaseChangeQOrphan() const;
4271
4272
4275 double phaseChangeCarryDeposited() const;
4276
4277
4278 double phaseChangeCarryLost() const;
4279
4280
4284
4285
4286
4297 double pcBandDivergence();
4298
4299
4300
4302 void pcUpdateEnergyProps();
4303
4304
4305
4310 void pcRefreshUnits();
4311
4312
4313
4318 void requireScalesPinned(const char* who, bool needRho) const;
4319
4320
4321
4324 double closureTargetToInt(const std::string& target) const;
4325
4326
4327
4331
4332
4335 void closureUnitsNotice(const std::string& target) const;
4336
4337
4338
4341 double pcLedgerEnergyToPhys() const;
4342
4343
4344
4348 void vofExchangeScalar(CCField f);
4349
4350
4351
4358 void setDivergenceSource(const std::vector<double>& v);
4359
4360
4361 void clearDivergenceSource();
4362
4363
4364
4373 void setDivergenceSink(const std::vector<double>& w);
4374
4375
4376 void clearDivergenceSink();
4377
4378
4379
4384 void applyPhaseChange(double dtPhysArg);
4385
4386
4387
4390 void phaseChangeStep();
4391
4392
4393
4395
4396
4397
4398 // nvcc requires members holding extended device lambdas to be public.
4399
4406 void pcBuildInterface();
4407
4408
4409
4415 void pcScatterSource();
4416
4417
4418
4426 void pcRegress(double dt);
4427
4428
4429
4439 void pcUpdateCurvature();
4440
4441
4442
4457 void pcBuildInDomain();
4458
4459
4460
4461 void pcUpdateThermalMask();
4462
4463
4464
4468
4469
4470
4502 void pcCarryDeposit(ScalarField& sc);
4503
4504
4505
4508 void pcCarryApply(ScalarField& sc);
4509
4510
4511
4516 void pcBudgetPre(ScalarField& sc);
4517
4518
4519
4528 void pcBudgetPost(ScalarField& sc);
4529
4530
4531
4535
4536
4537
4551 double pcEffInterfaceEps() const;
4552
4553
4558 double pcEffPureEps() const;
4559
4560
4561
4562 void requirePhaseChange(const char* who) const;
4563
4564
4565 ScalarField& scalarField(const std::string& name);
4566
4567
4570 void scalarDirichletMask(const std::string& name);
4571
4572
4573
4574 // --- Property closures + per-cell body force ------------------------------------------------
4575 // Register a property/force closure. target: a registered field name — a material property
4576 // ("mu"/"rho"/…) or a body-force component ("force_x"/"force_y"/"force_z"). kind: LinearMix /
4577 // BoussinesqForce / ArrheniusMu. in0/in1: input field names (in1 "" if unused). params: up to 4
4578 // doubles (meaning per kind — property_closures.hpp). Applied at the top of step() in
4579 // registration order. Targeting a force component turns on the per-cell body-force RHS path.
4580 // UNITS: the inputs are taken as UNSCALED fields (a phase fraction, a temperature, a
4581 // concentration — never rescaled), so the output's dimension sits in the parameters: on a
4582 // target "rho", "mu" or "force_a" the parameters carrying it (all of LinearMix's p0..p2,
4583 // Boussinesq's rho0, Arrhenius' mu_ref, a table's ys) are PHYSICAL and convert with that target's
4584 // factor (`closureTargetToInt`); the rest enter only as a product with that one (Boussinesq's
4585 // g*beta) or against the unscaled input (T0, B, Tref, xs) and stay as given. Any other target
4586 // is in that field's INTERNAL units (a stderr notice says so under a physical domain).
4587 void setPropertyModel(const std::string& target, ClosureKind kind, const std::string& in0,
4588 const std::string& in1, const std::vector<double>& params);
4589
4590
4591 // Enable/disable variable density: binds the "rho" field (creating it seeded with the scalar rho_
4592 // if absent) into the momentum time term, the advection weight, and the pressure projection
4593 // (face coefficient open/rho_f + 1/rho_f correction). rho_ (set_rho) becomes the REFERENCE
4594 // density rho0 of the projection scaling — a uniform rho field == rho_ reduces exactly to the
4595 // constant solver. Escape hatch: set_field("rho", arr) + set_density_mode(True); or a closure
4596 // targeting "rho" (e.g. rho = LinearMix of a transported phase fraction) enables it
4597 // automatically. Staggered grid only (v1); the velocity multigrid (scalar-coefficient) is
4598 // disabled.
4599 // Rung V8 lifted the collocated throw. On `SolverColocated` the variable-density path is the ABC
4600 // approximate projection with the face coefficient `c_f = o_f rho0/rho_f`, the MOMENTUM-weighted
4601 // centre-to-face map in the constraint, `projectCorrectVar` on the FACE field, and a cell
4602 // correction that is the face-to-cell reconstruction of the face corrections (never a
4603 // cell-centred grad(phi)/rho_c); the pressure and every force enter the implicit predictor as
4604 // the matching face integral (doc/collocated_varrho_forces.md). Scope: ALL-FLUID
4605 // (`set_pressure_geometry`) — an immersed solid still throws, at the first `step()`, and so do
4606 // the ghost projection and `set_rho_face_harmonic` (see requireCollocatedFaceForceScope).
4607 // Momentum consistency (`enable_vof_momentum`) is NOT in this rung: the collocated construction
4608 // needs Favre face states, so the collocated two-phase path is rated to density ratio ~10 for
4609 // cases WITH MOTION (WO-V3: a translating drop throws the VoF CFL cap at ratio 100 and 1000); a
4610 // high-ratio case at REST — hydrostatic, stationary droplet — is exact, measured at ratio 1000.
4611 void setDensityMode(bool variable);
4612
4613
4614 // Enable/disable the volume-averaged (porous) continuity for unresolved CFD-DEM: the projection
4615 // enforces d(eps)/dt + div(eps u) = 0 instead of div(u)=0, so the velocity is NOT solenoidal
4616 // where the void fraction changes. Binds the "eps" field (void fraction from the particle
4617 // deposition; created seeded to 1 if absent). Staggered-only. The coupling deposits eps each step
4618 // BEFORE step().
4619 // Has the cut-cell pressure operator been built (set_solid / set_pressure_geometry)? The porous
4620 // projection requires it — project() throws otherwise; the coupling driver queries this to
4621 // auto-install an all-fluid geometry.
4622 bool hasCutcellPressure() const;
4623
4624
4625 void setPorousContinuity(bool on);
4626
4627
4628 // Reseed eps^n = eps^{n+1} so d(eps)/dt = 0 this step. Call after the FIRST void-fraction
4629 // deposition (the "eps" field starts empty, so without this step 0 sees a spurious d(eps)/dt from
4630 // 0 -> eps).
4631 void syncPorousPrev();
4632
4633
4634 // Include (default) or drop the d(eps)/dt source in the porous projection RHS. Dropping it
4635 // enforces div(eps u)=0 — useful when eps is a bare per-cell particle deposit whose
4636 // time-derivative is too jagged and drives the eps-weighted pressure solve unstable.
4637 void setPorousDepsDt(bool on);
4638
4639
4640 void setPorousConservative(bool on);
4641
4642
4643 // Pressure under-relaxation factor omega_p in (0,1] (MFIX-style); 1.0 = off (default).
4644 void setPressureUnderRelax(double w);
4645
4646
4647 // Enable/disable variable-coefficient momentum (variable viscosity). variable=true binds the "mu"
4648 // field (creating it, seeded with the current scalar mu, if absent) and forces the stencil solve
4649 // path. harmonic selects the harmonic face mean (continuous shear stress across a viscosity jump)
4650 // vs arithmetic. Escape hatch: set_field("mu", arr) then set_property_mode(True).
4651 void setPropertyMode(bool variable, bool harmonic);
4652
4653
4654 // Rotational-pressure treatment under variable viscosity. The Timmermans rotational term
4655 // P += (rho/dt)phi - mu*div(u*) is only valid for HOMOGENEOUS viscosity (Deteix & Yakoubi, Appl.
4656 // Math. Lett. 2018 / arXiv:1902.05643): with spatially varying mu the pointwise term is no longer
4657 // the gradient part of the viscous stress, and the accumulated inconsistency destabilises the
4658 // incremental scheme at strong contrast (observed: 10x jump + harmonic faces -> divergence).
4659 // Modes (the incremental predictor -grad(P^n) and P accumulation are kept in ALL of them — that
4660 // is what enables large-dt / steady-Stokes stepping):
4661 // 0 "min" (default): rotational coefficient chi*mu_min — a CONSTANT dominated by the true
4662 // local
4663 // dissipation everywhere (mu_min <= mu(x)), so the constant-viscosity stability theory
4664 // carries over; reduces EXACTLY to the validated scheme when mu is uniform.
4665 // 1 "full": chi*mu(i) pointwise — better pressure consistency at MILD contrast; not stable at
4666 // strong contrast (user's responsibility).
4667 // 2 "off" : plain incremental (no rotational term) — unconditionally stable, keeps the
4668 // artificial
4669 // pressure Neumann layer of the non-rotational scheme.
4670 // The fully consistent variable-viscosity correction (shear-rate projection: an extra Poisson
4671 // solve for psi with rhs div(div(2 nu D(u)))) is deferred.
4672 void setVariableRotational(int mode, double chi);
4673
4674
4675 // Tabulated property: out = piecewise-linear interp of (xs, ys) at the input field (xs
4676 // ascending). Units as setPropertyModel: ys in the target's physical units, xs in the input's.
4677 void setPropertyTable(const std::string& target, const std::string& in0,
4678 const std::vector<double>& xs, const std::vector<double>& ys);
4679
4680
4681 // Apply all closures (also called at the top of step()). Exposed for testing.
4682 void updateProperties();
4683
4684
4685 // Allocate + register the per-cell body-force fields ("force_x/y/z") and route them into the
4686 // momentum RHS, for an EXTERNAL writer (CFD-DEM feedback) to fill directly via field_view — no
4687 // closure needed. buildRhsForced then adds them each step (they persist; the writer overwrites).
4688 void enableCellForce();
4689
4690
4691 // Implicit (semi-implicit) linear drag: a per-cell coefficient field "drag_beta" is added to the
4692 // momentum diagonal each step, so a drag source −β(u − u_p) is treated implicitly (the fluid
4693 // solve becomes (ρ/dt + β)u = … + β u_p). The drag TARGET β·u_p goes into the force_x/y/z fields
4694 // (the RHS). Unconditionally stable for any β (unlike an explicit −β u force, which diverges for
4695 // the stiff β of a dense particle bed). The external writer (CFD-DEM) fills "drag_beta" +
4696 // "force_*" via field_view; enableDrag() allocates them and turns the diagonal path on.
4697 void enableDrag();
4698
4699
4700 // Porous + implicit drag: the drag-relaxation w_f=idt/(idt+beta) makes the pressure coefficient
4701 // high-ratio (~1 in the freeboard, ->0 in the dense bed). Chebyshev diverges on it; the algebraic
4702 // GraphAMG coarse solve + PCG is robust. Applied whenever BOTH porous_ and hasDrag_ are on
4703 // (either set second). An explicit set_pressure_* afterwards still wins.
4705
4706
4707 // Add the drag coefficient at the unknown's location, Grid::atVelocity(beta) (face mean on the
4708 // MAC grid, cell value collocated), to the momentum diagonal of component c. Called after
4709 // each stencil (re)build when hasDrag_. All-fluid (rscale==1) is exact; the drag×cut-cell-IBM
4710 // interaction (rscale≠1) is untested (documented).
4711 void addDragDiagonal(int c);
4712
4713
4714
4715 private:
4716 // === dynamic load balancing: making EVERY per-block allocation follow the new block ===========
4717 //
4718 // `allocateBlock` re-creates the buffers it names explicitly and re-`adopt`s the five aliased
4719 // registry entries ("u"/"v"/"w"/"p"/"sdf"). Everything the solver has grown SINCE construction
4720 // does not follow it:
4721 //
4722 // 1. the FieldSet's OWN storage (`FieldSet::add` — every transported scalar, every property /
4723 // body-force closure target, VoF's "C" / "kappa" / "kappa_branch", the phase-change fields);
4724 // 2. the member handles that ALIAS one of those records (`cField_`, `rhoField_`, `muField_`,
4725 // `epsField_`, `cellForce_[]`, `ScalarField::c`, every `Closure`'s in/out views);
4726 // 3. the lazily-allocated per-block scratch (`vofCs_`, `uAdv_`, the phase-change and porous
4727 // work arrays, the g=1 MG-bridge coefficients, …), each allocated at the `n_` / `n1_` of
4728 // whichever block was current when its feature was first switched on.
4729 //
4730 // `redistribute` then memcpy'd the NEW block's padded extent into (1)'s OLD allocation: measured
4731 // by AddressSanitizer as a 184320-byte WRITE into a 115328-byte Kokkos HostSpace region at
4732 // `scatterPadded`, i.e. exactly the `free(): invalid pointer` /
4733 // `malloc(): unsorted double linked list corrupted` that made `rebalance_by_weights` unusable on
4734 // any run owning a registry field. (2) and (3) are the same defect one step later — a stale view
4735 // indexed with the NEW `nx_`/`ny_`/`nz_`.
4736 //
4737 // `resizeForBlock()` is the root fix and runs the three passes in this order, immediately after
4738 // `allocateBlock` and BEFORE `initMpi` (which rebuilds the VoF block and writes `cField_`).
4739 void resizeForBlock();
4740
4741
4742 // Pass 1: the FieldSet's own storage. Fresh, zero-initialised, same name/ghost/centering; the
4743 // migrated data is scattered back into it by `redistribute` step 4. Aliased records
4744 // (`ownStorage == false`) are left alone — `allocateBlock` has just re-adopted them.
4745 void reallocOwnedFields();
4746
4747
4748 // Pass 2: re-resolve every member handle that aliases a registry record. A handle whose record
4749 // does not exist is left as it is (the feature was never switched on, so the handle is empty).
4750 void rebindFieldAliases();
4751
4752
4753 // Pass 3: the lazily-allocated per-block scratch. A view that was never allocated (extent 0)
4754 // STAYS unallocated — "inert until its feature is enabled" is load-bearing all over this class
4755 // (every consumer branches on `extent(0)`), so resizing an empty view would switch a feature on.
4756 // Views that carry state across a step are re-derived below rather than left zeroed.
4757 static void resizeIfAllocated(CCField& f, const char* label, std::size_t n);
4758
4759
4760 void resizeBlockScratch();
4761
4762
4763 // Resolve a closure target to a registered buffer. A force component allocates ALL three
4764 // cellForce_ slots (buildRhsForced reads every component) and enables the body-force RHS path.
4765 CCField ensureTarget(const std::string& name);
4766
4767
4768 void ensureCellForceAll();
4769
4770
4771 // Ghost fill for a scalar: periodic (single-rank) / MPI halo base, then override any domain
4772 // Dirichlet/Neumann faces.
4773 void scalarFillGhosts(ScalarField& sc);
4774
4775
4776 // Overwrite the ghost band on each Dirichlet/Neumann domain face (both layers, for the ±2
4777 // advection reach). Distributed: a rank applies a face's BC iff its block TOUCHES that global
4778 // face. The halo fill runs first (and may periodic-wrap those ghosts); the BC overwrite wins,
4779 // exactly matching the single-rank fill-then-BC order. Cross-rank ghost CORNERS on a BC face
4780 // keep their exchanged (pre-BC) values, but the scalar stencils only read axis-aligned ghosts
4781 // (7-point diffusion + straight ±2 advection reach), so those corners are never consumed.
4782 void applyScalarBc(ScalarField& sc);
4783
4784
4785 // Does this rank's block touch global domain face f (always true single-rank)?
4786 bool touchesGlobalFace(int f) const;
4787
4788
4789 // Re-open the diffusion face at a Dirichlet domain boundary: set_domain_bc closes the boundary
4790 // openness (ox_=0), which correctly makes Neumann/adiabatic walls zero-flux but would also cut a
4791 // Dirichlet wall's heat path. For each Dirichlet face, restore the face coefficient (band = -D,
4792 // A_C += D); the ghost carries 2*value - inner so the row is the standard Dirichlet operator.
4795 void applyScalarBcStencilVar(ScalarField& sc);
4796
4797
4798 void applyScalarBcStencil(ScalarField& sc);
4799
4800
4801 // nvcc requires member functions that contain extended (device) lambdas to be PUBLIC — the
4802 // OpenMP/host build accepts them private, so the breakage only shows on the CUDA backend.
4803 public:
4804 void patchScalarDirichletFaceVar(CCField AC, CCField band, CCField kc, int a, int side);
4805
4806
4807 void patchScalarDirichletFace(CCField AC, CCField band, double Din, int a, int side);
4808
4809
4810 void applyScalarBcFace(CCField c, int a, int side, int type, double val, bool edges = false);
4811
4812
4813
4814 private:
4815 int nx_, ny_, nz_;
4816 C3 e_, e1_;
4817 std::size_t n_, n1_;
4818 double rho_ = 1.0, mu_ = 0.1, dt_ = 50.0; // INTERNAL (index-unit) values; see UnitScales
4819 UnitScales u_; // the metric + reference scales (all 1 in cell units)
4820 // The caller's own values, kept verbatim so refreshUnitDerived() can re-derive the internal
4821 // ones in any call order. Equal to the internal ones on the cell-unit path.
4822 double rhoPhys_ = 1.0, muPhys_ = 0.1, dtPhys_ = 50.0;
4823 std::array<double, 3> fPhys_{{0, 0, 0}};
4824 double sigmaPhys_ = 0.0;
4825 double slipPhys_ = 0.0;
4826 double bcVelPhys_[6][3] = {};
4827 std::array<double, 3> f_{{0, 0, 0}};
4828 // setSuperficialVelocity: the axis (-1 = off), the target in the CALLER's units (converted at
4829 // each application, so any set_rho/set_dt order works), and the device-resident shift of the
4830 // last application (internal units) -- a 0-d View so the single-rank path needs no host round
4831 // trip.
4832 int superficialAxis_ = -1;
4833 double superficialVelPhys_ = 0.0;
4834 Kokkos::View<double, CCMem> superficialShift_;
4835 int velIters_ = 200, presIters_ = 20;
4836 double velTol_ = 0.0; // momentum tolerance stop (0 = legacy fixed-count loop)
4837 int velMinIters_ = 2;
4838 long lastMomentumSweeps_ = 0; // sweeps actually run last step (summed over components/Picard)
4839 // Residual-based momentum stop (setVelocityResidualTolerance): < 0 follows the pressure rtol
4840 // (DEFAULT since 2026-09-02), 0 = the update criterion, > 0 = a fixed tolerance.
4841 double velResTol_ = -1.0;
4842 double lastMomentumResid_ = -1.0; // max_c max|r|/max|b| at exit (residual mode)
4843 CCField velRes_; // scratch for the stencil-path residual
4844 CCField chebD_; // scratch: the Chebyshev search direction (see chebSolveComp)
4845 double lastAxNorm_ = 0.0; // max|A u| of the last residual evaluation (scale)
4846 int pcgMaxit_ = 500;
4847 double pcgRtol_ = 1e-10; // cut-cell pressure MG-PCG
4848 bool useChebyshev_ = false,
4849 chebBoundsSet_ = false; // Chebyshev pressure driver (set_pressure_chebyshev)
4850 bool useFcg_ = false; // flexible-CG pressure driver (set_pressure_fcg); OFF by default,
4851 // so the shipped MG-PCG path is untouched (zp1_ is not allocated)
4852 CCField zp1_; // FCG's extra scratch: allocated lazily at the first FCG solve
4853 int chebMaxit_ = 120;
4854 double chebRtol_ = 1e-9, chebA_ = 0.0, chebB_ = 0.0;
4855 // D3 (§5.13): may the next main-bound re-estimate start warm? Set by a coefficient rebuild that
4856 // drops VALID bounds, cleared by every structural invalidation (driver, density, porous setters).
4857 bool chebWarmOk_ = false;
4858 long chebColdRestarts_ = 0; // guard firings (numPressureChebyshevRestarts)
4859 CCField chebX0_; // the solve's starting iterate under the pressure warm start
4860 int nLevels_ = 4; // multigrid depth (CUDA default; set_pressure_multigrid)
4861 bool pressGraphAmg_ = false;
4862 // Coarse-solve policy: -1 auto (DEFAULT — agglomerate when the coarsest grid exceeds
4863 // setPressureBottomExtent cells on any axis; identical to the smoothed bottom otherwise),
4864 // 0 smoothed, 1 always. Auto became the default 2026-08-13 after the IBM-path anomaly was
4865 // fixed (per-fluid-component null-space projection; see ../docs/DECOMPOSITION_AND_MULTIGRID.md).
4866 int pressAgglomMode_ = -1;
4867 long lastPressureIters_ = 0;
4868 bool lastPressureFailed_ = false; // ISSUES sweep item 6
4869 // Balanced-force projection (doc/collocated_varrho_forces.md §4.6). `Pb_` (the registered G=2
4870 // field "p_balanced", so redistribute carries it) and `pb1_` (g=1 scratch) exist only once the
4871 // option has run; `coeffBuiltThisStep_` lets project() skip the variable-rho coefficient rebuild
4872 // the balanced-force stage already did this step (rho is frozen within a step).
4873 bool bfpSet_ = false, bfpOn_ = false;
4874 bool bfpOpenNotice_ = false; // the "V8 default resolved OFF: open face" notice, once
4875 CCField Pb_, pb1_;
4876 // Is Pb_ the balanced part of the CURRENT P_ (a valid split)? True for a fresh solver (P = Pb =
4877 // 0); cleared by a step with the option off (P moved without Pb) and by a set_field of "p" or
4878 // "p_balanced" alone (a restart that restored one without the other); set again by every
4879 // balanced-force stage. An invalid split is RE-SPLIT (Pb := X, P unchanged), never P += X - Pb:
4880 // that would add the balanced pressure a second time (doc/collocated_varrho_forces.md §4.6.2).
4881 bool pbValid_ = true;
4882 bool pWritten_ = false, pbWritten_ = false; // set_field("p" / "p_balanced") since the last step
4883 // The pre-projection's OWN Chebyshev bounds (doc §4.8): estimated once, on its own right-hand
4884 // side, and KEPT across the per-step variable-rho coefficient refresh (which invalidates only
4885 // the MAIN bounds, chebBoundsSet_). Re-estimated after a structural operator change (the driver,
4886 // density, porous and rho-face setters, a pressure-MG rebuild) or when a pre-projection
4887 // Chebyshev solve diverges (it ran to the cap and its residual grew). Never shared with the
4888 // main solve, whose bound logic is unchanged.
4889 double bfpChebA_ = 0.0, bfpChebB_ = 0.0;
4890 bool bfpChebSet_ = false;
4891 // A failed pre-projection (a driver breakdown flag, or a non-finite X): P and P_b are kept.
4892 bool lastBalancedForceFailed_ = false;
4893 long balancedForceFailures_ = 0;
4894 bool bfpFailWarned_ = false;
4895 bool coeffBuiltThisStep_ = false;
4896 long lastBalancedForceIters_ = 0;
4897 // The collocated face field with every OPEN (outflow) domain face replaced by its
4898 // zero-gradient extrapolation -- see buildOpenFaceField(). Allocated on first use and only
4899 // when there IS an outflow; `openFaceView` falls through to uf_/vf_/wf_ otherwise.
4900 CCField ufOpen_[3];
4901 // ISSUES sweep item 5: has the collocated MAC face field ever been built (by a projection
4902 // or by `seedFaceFieldFromCells`)? Meaningless on the staggered grid.
4903 bool faceFieldValid_ = false;
4904 // ISSUES sweep items 1+2: `step_adaptive` has just evaluated the two limits on this state, so
4905 // `step()`'s head-of-step pre-check can skip its own (identical) evaluation. One-shot.
4906 bool vofPrecheckDone_ = false;
4907 CutcellMG mg_;
4908 // --- multi-rank (MPI) state, gated (single-GPU module never links MPI -> byte-identical when
4909 // off) ---
4910 // rung W0 (WO-W0): the per-bubble block container. Null unless enable_vof_blocks ran, so
4911 // every existing VoF path is byte-identical.
4912 std::shared_ptr<vof::VofBlockSet> vofBlocks_;
4913 std::shared_ptr<vof::VofBlockExchange> vofBlockExch_;
4914 // rung W2: the block CSF face force -- on the g=3 patch (the scatter's target) and mirrored onto
4915 // the G=2 registry block the RHS reads. Allocated only by `enable_vof_block_csf`.
4916 SField vofBlkF_[3];
4917 CCField csfBlkF_[3];
4918 // vof_overlap_design §5.6: S = sum_k C_k on the g=3 patch, the census, the opt-in tent and its
4919 // scratch patch. Allocated on first use; untouched unless the block CSF or the tent is on.
4920 SField vofBlkS_, vofBlkCeff_;
4921 VofOverlapCensus vofOverlap_{};
4922 bool vofOverlapDensity_ = false;
4923 // The block-path curvature clip in index units (VofCurvature::kappaMax: <0 = 1/Delta_min, 0 =
4924 // off), kept here so a setting made before enable_vof_blocks survives the container's creation.
4925 double vofBlockKappaMax_ = -1.0;
4926 // set_vof_block_debris: -1 = not set (the default: ON under the block CSF), 0 = off, 1 = on.
4927 int vofBlockDebris_ = -1;
4928 bool vofPhantomBound_ = true; // setVofPhantomCapillaryBound
4929 bool distributed_ = false;
4930 C3 og_{0, 0, 0}; // velocity-block inner origin (global red-black parity); {0,0,0} single-rank
4931#ifdef PECLET_FLOW_MPI
4932 std::shared_ptr<GridHaloTopology<3>> velHalo_; // g=2 velocity-block topology
4933 std::shared_ptr<GridHalo<double>> velDev_; // g=2 velocity-block ghost exchange
4934 std::shared_ptr<GridHalo<MReal>> velDevF_; // float twin (momentum-stencil ring, CA sweeps)
4935 bool caMomentum_ = false; // communication-avoiding momentum sweeps (setCommAvoiding, extent>=4)
4936 bool momStencilDirty_[3] = {true, true, true}; // per-component: stencil ring needs an exchange
4937 std::shared_ptr<peclet::core::decomp::BlockDecomposer<3>>
4938 dec_; // current partition (redistribute)
4939 MPI_Comm comm_ = MPI_COMM_NULL;
4940 int gnx_ = 0, gny_ = 0, gnz_ = 0; // communicator + GLOBAL dims
4941#endif
4942 int bc_[6] = {0, 0, 0, 0, 0, 0};
4943 double bcVel_[6][3] = {};
4944 bool hasBc_ = false, hasOutflow_ = false; // domain BCs
4945 bool hasSolid_ =
4946 false; // an immersed solid is present (any inner SDF < 0) -- with domain BCs, the
4947 // momentum solve must use the cut-cell IBM stencil, not the all-fluid fold
4948 bool backflowWarned_ = false; // the reversed-outflow-without-stabilization warning, once
4949 double backflowBeta_ =
4950 0.2; // outflow backflow-stabilization coefficient (0 = off; inert unless the
4951 // outflow reverses, so purely-outgoing outlets stay byte-identical)
4952 CCField bcProf_[6];
4953 int bcProfNc_[6] = {0, 0, 0, 0, 0, 0}; // per-position inlet profiles (face grid [Lb*Lc*3])
4954 std::vector<double> bcProfRaw_[6]; // the user's (nb, nc, 3) profile, kept so the resampled
4955 int bcProfNb_[6] = {0, 0, 0, 0, 0, 0}; // buffer above can be rebuilt when the block is resized
4956 int bcProfRawNc_[6] = {0, 0, 0, 0, 0, 0};
4957 CCField bcDcorr_[3], bcBrhs_[3]; // implicit-diffusion face fold (per component)
4958 bool advect_ = false, cutcellPressure_ = false, implicitFou_ = false;
4959 bool deferredCorr_ = true; // deferred-correction advection (off = pure implicit FOU, 1st order)
4960 int advScheme_ = 0; // high-order advection: 0 = SOU (default), 1 = Koren TVD
4961 bool incremental_ = true,
4962 pwarm_ = false; // incremental-rotational pressure (CUDA default on) + warm-start
4963 bool dtDirty_ = false; // set_dt/set_rho/set_mu after set_solid: momentum stencil needs a rebuild
4964 // Set once setSolidDevice has built the operators. The settings that are FOLDED INTO that build
4965 // (domain BCs, aperture order, exact crossings / openness overrides, the fluid-only constraint,
4966 // the ghost overlay) refuse a later call instead of being silently ignored (QUALITY_PLAN F).
4967 bool geometryBuilt_ = false;
4968 int faceInterp_ = 9; // collocated scheme: 9 = gauge-exact (DEFAULT), 0 = plain, 5/6/7 = embed
4969 // A0: fill the advection inputs' masked (solid) rows with the WALL velocity instead of zeros.
4970 // ON by default; setAdvectionWallVelocity(false) is the pre-A0 ablation. See advWallInputs.
4971 bool advWallVel_ = true;
4972 // Advect the collocated momentum with the projected divergence-free face field (the shipped
4973 // scheme) instead of the un-projected cell->face average. ON by default;
4974 // setUfAdvection(false) is the developer-tier ablation. See ufAdvVelocity / doc/uf_advection.md.
4975 bool ufAdvect_ = true;
4976 // WO-R2 item 1 — the variable-density coefficient on the operator's Dirichlet (outflow)
4977 // domain-face rows. ON by default; see setOutflowOperatorCoefficient.
4978 bool outflowOpCoeff_ = true;
4979 // P1 — the exact double flux-form level-0 apply (setPressureExactResidual; enableVof turns it
4980 // on). Mirrored into mg_, and read directly by the star overlay's additive delta.
4981 bool exactResidual_ = false;
4982 // Communication-avoiding smoothing mask (kCaMomentum | kCaMg), see setCommAvoiding. Both on by
4983 // default; the momentum half is additionally gated on the block extent (caMomentum_).
4984 int caMode_ = kCaBoth;
4985 // The shared level-0 MPI decomposition (setDecomposition): 0 = the aligned ORB, >= 2 =
4986 // coarse-first with that depth, taking the deepest candidate within maxImbalance.
4987 int decompLevels_ = 0;
4988 double decompMaxImbalance_ = 1.05;
4989 // True once rebalanceByWeights has put the solver on a WEIGHTED level-0 decomposition (reset by
4990 // initMpi(gnx, gny, gnz, comm)). It switches the pressure MG's Repartition stages on
4991 // (CutcellMG::setRepartition): a weighted ORB has odd splits, and without them a telescope stage
4992 // can collapse a level far larger than one rank's block onto one rank (SCALING_ISSUES #2).
4993 bool weightedDec_ = false;
4994 double aspectTheta_ = 2.0; // mirror of the two multigrids' threshold
4995 bool rotationalP_ = true; // false = PM I ablation: drop the -mu*div(u*) Timmermans term from
4996 // the incremental pressure accumulation (constant-mu path only)
4997 bool rotFilter_ = false; // filtered rotational: smooth div(u*) (mask-aware axis-wise 1-2-1,
4998 // one-sided toward the fluid at solid neighbours) before accumulating
4999 // -mu*div into P. Kills the wall-normal checkerboard feedback the
5000 // cell-centered rotational update is unstable through, keeps the O(1)
5001 // pressure-relaxation gain and the phi=0 (dt-free) fixed point.
5002 double rotFilterEps_ = 0.05; // S' = eps I + (1-eps) S (see setRotationalFilter)
5003 double rotWeight_ = 1.0; // rotational under-relaxation w (setRotationalWeight)
5004 double rotWallW_ = 0.0; // wall-banded rotational blend w0 (setRotationalWallWeight)
5005 // Face-aperture estimator order (setApertureOrder; DEFAULT 2 = marching-squares since
5006 // 2026-08-26 -- user decision, kills the convexity bias; 1 = the legacy one-sample model).
5007 int apertureOrder_ = 2;
5008 int fluidOnlyMode_ = 0; // fluid-only constraint (setFluidOnlyConstraint): 1=A filter, 2=B star
5009 StarOverlay starOv_; // mode-B Kron star overlay (built in setSolid)
5010 Kokkos::View<int, CCMem> starCounter_;
5011 int nStar_ = 0;
5012 bool useVelocityMg_ = false;
5013 bool vmgExplicit_ = false; // set_velocity_multigrid was called (AUTO rule off)
5014 // The V-cycle is the DEFAULT momentum solver (2026-09-15): no upper bound on the block size,
5015 // no global-size floor. 0 = never; a finite value restores an upper bound (1.0.0 used 65536
5016 // cells/rank with a 1<<23 global floor).
5017 long vmgAutoCells_ = std::numeric_limits<long>::max();
5018 long vmgAutoMinGlobal_ = 0L;
5019 int vmgAutoMinExtent_ = 16; // shortest per-rank inner extent the V-cycle is worth
5020 double vmgAutoMinCond_ = 13.0; // operator condition number above which the V-cycle wins
5021 // (1 + 12 D; measured crossover D ~ 1)
5022 bool vmgDecided_ = false; // the momentum-solver choice has been made (and the MG built)
5023 int vmgLevels_ = 4, vmgVcycles_ = 8; // IBM velocity multigrid (staircase)
5024 bool useVelocityCheb_ = false; // Chebyshev momentum solver (setVelocityChebyshev)
5025 bool vmgCheb_ = false; // Chebyshev smoother INSIDE the velocity MG
5026 int vmgChebDegree_ = 0;
5027 double vmgChebEigRatio_ = 10.0;
5028 int velChebMaxit_ = 400;
5029 VelocityMG vmg_;
5030 CCField vmgTheta_, vmgClean_;
5031 int outerIters_ = 1;
5032 double outerTol_ = 0.0; // Picard outer iteration (CUDA set_outer_iterations)
5033 long lastOuterIters_ = 0;
5034 double lastOuterCorr_ = 0.0;
5035 // per-step phase timers (seconds, this rank; see lastStepSeconds)
5036 double tStep_ = 0.0, tPredictor_ = 0.0, tMomentum_ = 0.0, tProjection_ = 0.0;
5037 // WO-V9: cumulative VoF-stage timers (armed by set_vof_timing; inert and unfenced when off)
5038 bool vofTiming_ = false;
5039 VofTiming vt_;
5040 double tStepSum_ = 0.0, tPredSum_ = 0.0, tMomSum_ = 0.0, tProjSum_ = 0.0;
5041 double vofTick() const;
5042
5043
5044 void vofAdd(double& acc, double t0);
5045
5046
5047 // fence-then-read wall clock: phase boundaries must not attribute queued device work to the
5048 // next phase
5049 static double phaseTick();
5050
5051
5052 CCField sdf_, ox_, oy_, oz_, phi_, div_, P_, ox1_, oy1_, oz1_, rhs1_, phi1_, r_, z_, pp_, Ap_;
5053 bool ghostProjection_ = false; // directional ghost-cell projection (the collocated AUTO default)
5054 bool colSchemeAuto_ = Grid::collocated; // AUTO scheme resolution at setSolid (cleared by any
5055 // explicit scheme selection)
5056 GpOverlayMReal gpOv_; // its per-row overlay (built by setSolid), follows MReal (G.6)
5057 Kokkos::View<int*, CCMem> gpIdMap_;
5058 Kokkos::View<int, CCMem> gpCounter_;
5059 int gpNRows_ = -1; // -1 = overlay not built (set_solid must run with the mode on)
5060 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure order: implicit phi couplings / RHS
5061 CCField tEx_[3][3]; // exact crossings t[c][k] (inner grid; setExactCrossings)
5062 bool hasExactCross_ = false;
5063 // WO-V6b: the Navier slip length (in cells) shared with the dynamic-wetting cut-off, and the
5064 // switch that makes the MOMENTUM wall closure use it.
5065 double slipLambda_ = 0.0;
5066 bool wallSlip_ = false;
5067 Kokkos::View<int, CCMem> slipSkipDev_;
5068 std::array<int, 3> slipSandwich_{0, 0, 0};
5069 bool sceneCrossings_ = false; // crossings came from the analytic scene (per-rank, no override)
5070 std::shared_ptr<peclet::core::geom::SceneQueryDevice<double, CCMem>> sceneQ_;
5071 bool hasScene_ = false;
5072 // CUT OWNERSHIP (Layer 3 rung 1). Which scene instance owns the nearest surface, per INNER cell
5073 // (x-fastest, nx*ny*nz), filled by set_solid_from_scene in the same traversal that samples the
5074 // SDF (core's evalOwner answers both at once, so the field is free). Meaningful everywhere; only
5075 // the cut cells consume it — moving geometry reads a wall velocity off the owner, and resolved
5076 // CFD-DEM posts the hydrodynamic force back to it. Empty until a scene is sampled.
5077 Kokkos::View<int*, CCMem> cutOwner_;
5078 // MOVING GEOMETRY (Layer 3 rungs 2-3). Per-scene-instance rigid-body motion: host copies (KBs,
5079 // rank-replicated like the scene itself) plus the device mirrors the kernels capture. All-zero
5080 // motion => hasMotion_ stays false and EVERY path below is skipped, so a static solver is
5081 // bit-identical to the build that predates this rung -- "moving with zero velocity" is not the
5082 // same code path and would not be.
5083 std::vector<double> instCen_, instLin_, instAng_; // 3*nInst_ each, world coordinates
5084 Kokkos::View<double*, CCMem> instCenD_, instLinD_, instAngD_;
5085 int nInst_ = 0;
5086 bool hasMotion_ = false;
5087 CCField uBc_[3]; // R2: wall velocity component c AT component c's staggered points
5088 CCField uwCell_[3]; // R3: the whole wall velocity at CELL CENTRES (the wall-flux divergence)
5089 // A0 (advective cut-wall flux): the momentum advection's OWN velocity inputs -- a copy of C[c].u
5090 // whose solid-masked rows carry uBc_ (the local rigid-body wall velocity) instead of the zeros
5091 // maskVelocity pins there. Allocated and filled only while an instance is moving; every other
5092 // configuration keeps reading C[c].u, byte for byte. See buildAdvInputs().
5093 CCField uwAdv_[3];
5094 // Route (b) instrumentation: u* = the LAST momentum solve's solution, stashed (ghosts included,
5095 // exactly as the smoother left them) before the projection overwrites it. The reaction force
5096 // needs the viscous fluxes AT u* -- the implicit solve acted on u*, not on the projected u.
5097 CCField uStar_[3];
5098 bool haveUStar_ = false;
5099 // R0: the explicit advective term EXACTLY as the last Picard RHS used it, per component and per
5100 // staggered cell, in the equation's own units (rho*(aF - aK), i.e. before the rscale descale).
5101 // Written by buildRhs / buildRhsForced when a scene is installed and advection is on; consumed
5102 // by hydroForceTorqueReaction, which must subtract every non-pressure, non-wall RHS term.
5103 CCField advRhs_[3];
5104 bool haveAdvRhs_ = false;
5105 bool wallFluxDiv_ = true; // rung 3 on (correct physics); off only to exhibit its absence
5106 // Fresh-cell seeding (see seedFreshCells). ON by default since 2026-08-30: measured on an
5107 // oscillating sphere that physically translates through the grid, it removes a
5108 // RESOLUTION-INDEPENDENT +2.6..2.9% drag bias, cuts the spurious force oscillation 20-50x to
5109 // within 17% of the non-moving floor, and improves the resolved CFD-DEM loop's total-momentum
5110 // conservation 95x. set_fresh_cell_seed(False) restores the old behaviour.
5111 bool freshSeed_ = true;
5112 // Rung 4: the scene is KEPT (not just its device query), so an instance transform can be updated
5113 // and the whole geometry re-derived without the caller re-encoding anything.
5114 std::shared_ptr<peclet::core::geom::SceneBuilder<double>> sceneB_;
5115 peclet::core::Vec3<double> sceneOrigin_{0, 0, 0}, sceneExtent_{0, 0, 0};
5116 bool scenePeriodic_ = false;
5117 long imageOverlapCells_ = 0; // set_solid_from_scene's periodic-image overlap count
5118 std::vector<long> movingCutCells_; // per-instance cut-cell counts (moving scenes only)
5119 std::vector<int> instCenPinned_; // 1 = centre of rotation pinned (explicit), 0 = follows the body
5120 std::vector<long> movingDegenerate_; // per-instance exactly-on-lattice staggered points
5121 bool sceneDirty_ = false; // a transform changed; the device query must be rebuilt
5122 std::vector<double> oxOverride_, oyOverride_, ozOverride_; // exact apertures (inner)
5123 bool hasOpenOverride_ = false;
5124 CCField oxb_, oyb_, ozb_; // binary (COUPLED) openness on the g=2 block (ghost divergence)
5125 CCField sdfGp_; // the projection's sdf (fragmentation pockets decoupled) — gpCenterGrad reads
5126 // it so the collocated predictor/correction never touch a decoupled cell
5127 CCField gpRh_, gpT_, gpZ2_; // extra BiCGStab scratch (g=1 block)
5128 CCField gpX2_; // distributed BiCGStab matvec staging (g=2 solver block; overlay +/-2 halo)
5129 CCField uf_, vf_, wf_; // collocated: transient face (MAC) field (approx projection)
5130 CCField faceAcc_[3]; // face-range scratch, allocated on first use by either consumer:
5131 // the V8 predictor's face accelerations Phi_c (buildRhsColoVar) and
5132 // the balanced-force projection's c*beta (buildBalancedFaceForce,
5133 // both grids).
5134 CCField tgp_; // collocated: cell pressure-gradient scratch
5135 CCField fvM_, fvL_, cs_; // collocated: embed defect scratch (M·u, L_FV·u) + cell fluid fraction
5136 CCField xcx_, xcy_, xcz_; // collocated: open-centroid wall distance per face (wall-aware map)
5137 CCField old_[3], prev_[3]; // u^n time base + previous Picard iterate
5138 Comp C[3];
5139 peclet::core::FieldSet fields_; // named directory of all cell fields (velocity/p/sdf + user)
5140 std::vector<ScalarField> scalars_; // transported scalars (advection-diffusion)
5141 // Cut-cell scalar geometry record (WO-2) and the geometry version it must match: every geometry
5142 // build bumps scgVersion_ (invalidateScalarCutGeometry), and ensureScalarCutGeometry rebuilds
5143 // when scg_.version differs. scg_.version starts at -1, so nothing is built until first use.
5145 long scgVersion_ = 0;
5146 // WO-5b: the captured open-face flux planes (scalarCaptureOpenFaceFlux), per global face, and
5147 // whether they belong to this block and geometry (cleared by every geometry build).
5148 CCField scalarOpenFlux_[6];
5149 bool scalarOpenValid_ = false;
5150 // --- phase change (WO-P01) -------------------------------------------------------------------
5151 bool pcEnabled_ = false, pcThermal_ = false, pcHasUser_ = false;
5152 double pcRhoG_ = 1.0, pcRhoL_ = 1.0, pcHlv_ = 1.0; // INTERNAL (pcRefreshUnits)
5153 double pcTsat_ = 0.0, pcKg_ = 0.0, pcKl_ = 0.0, pcRint_ = 0.0;
5154 // the caller's PHYSICAL values, kept verbatim (refreshUnitDerived re-derives the internal ones)
5155 double pcRhoGPhys_ = 1.0, pcRhoLPhys_ = 1.0, pcHlvPhys_ = 1.0;
5156 double pcKgPhys_ = 0.0, pcKlPhys_ = 0.0, pcRintPhys_ = 0.0;
5157 double pcRcpGPhys_ = 1.0, pcRcpLPhys_ = 1.0;
5158 double pcInterfaceEps_ = 1e-12, pcPureEps_ = 1e-12;
5159 std::string pcTName_;
5160 CCField pcMdot_, pcSrc_, pcUser_, pcArea_, pcNrm_[3], pcDep_, pcTgt_, pcCnew_, pcDefic_;
5161 PhaseChangeDiagnostics pcDiag_;
5162 // --- WO-P23 (rungs P2/P3) --------------------------------------------------------------------
5163 bool pcPlaneDir_ = true; // plane-anchored (GFM) Dirichlet rows instead of pinning the cell
5164 double pcGfmThMin_ = 0.1, pcGfmThMax_ = 1.9; // the GFM distance clamp (cells)
5165 bool pcQuadFit_ = true; // quadratic (Aslam) one-sided gradient fit (WO-P23 default)
5166 // WO-P3d (2026-09-03, coordinator's decision): the DEFAULT is the joined sheet (marching
5167 // tetrahedra on the PLIC signed-distance level set, centroid deposit). Gate re-derived: it is at
5168 // the floor on every a-priori geometry (sphere 1e-4, tilted planes 2e-6, cylinder order 2.04),
5169 // does not drift with wisps under advection, is byte-identical on every planar phase-change gate,
5170 // makes the P2 MPI gate bitwise, and removes the pure-gas deposit fallback (band_div 2e-3 ->
5171 // 6e-12). The P3 (Scriven) 1 % gate is still open at 1.0-1.5 % and is no longer explained by
5172 // the area (WO-P3e). `set_phase_change_area(0)` reproduces the P0-P3c numbers.
5173 int pcAreaMode_ = vof::kAreaMcPlic; // WO-P3c/P3d: which geometry A_Gamma comes from
5174 vof::VofInterfaceArea pcAreaC_; // the cascade area driver (g = 3 block; lazily initialised)
5175 vof::VofMcArea pcAreaMc_; // WO-P3d: the joined marching-tet area driver
5176 double pcMcOrphanArea_ = 0.0; // WO-P3d: area booked to non-interfacial cells
5177 CCField pcAreaCg2_; // its inner values on the G = 2 phase-change block
5178 // WO-P23 ablation (`set_phase_change_deposit_fallback`): give the interfacial
5179 // cells whose along-the-normal deposit candidates are BOTH still interfacial a target from the
5180 // 5^3 box instead of leaving the source in place. Default OFF — see the findings.
5181 bool pcDepositFallback_ = false;
5182 bool pcEnergy_ = false; // consistent rho c_p T transport + variable k(C)/rho c_p(C) operator
5183 double pcRcpG_ = 1.0, pcRcpL_ = 1.0;
5184 CCField pcKcell_, pcRcp_; // k(C) and (rho c_p)(C) on the G=2 block, refreshed per step
5185 vof::VofEnergyAdvector vofEnergy_;
5186 double pcBandDiv_ = 0.0; // max |div(open u)| over interfacial cells, last diagnostics call
5187 CCField pcGn_[3], pcGphi_, pcTgam_; // plane normal, centre distance, T_Gamma (WO-P23)
5188 CCField pcSink_; // auto-balanced sink weights (WO-P23)
5189 // WO-P3f: the energy-budget instrument. `pcClsPrev_` holds the PREVIOUS step's class of every
5190 // cell (0 gas, 1 interfacial/masked, 2 liquid) so the class CHANGES can be counted and their
5191 // enthalpy accounted; both are unallocated and every kernel is skipped unless the flag is on.
5192 bool pcBudgetOn_ = false;
5193 PhaseChangeBudget pcBudget_;
5194 CCField pcClsPrev_;
5195 // WO-P3f: conserve the enthalpy the per-cell Dirichlet OVERWRITE destroys (see
5196 // `pcCarryDeposit`). Off by default; `pcCarrySrc_` is unallocated until it is turned on.
5197 // WO-P3f: the PRESCRIBED interface curvature `div(n)` used to correct the one-sided fit's
5198 // sample distances (`vof::pcCurvedDistance`). 0 = the shipped tangent-plane distance, bitwise.
5199 double pcFitKappa_ = 0.0;
5200 bool pcCarryConserve_ = false;
5201 CCField pcCarrySrc_;
5202 double pcCarryDeposited_ = 0.0, pcCarryLost_ = 0.0;
5203 // --- WO-P3g: the second-order interfacial energy operator -----------------------------------
5204 // Three independent pieces, each an option so the ablation can be measured, and one package
5205 // switch (`set_phase_change_energy_order`) that turns all of them plus WO-P3f's `carry_conserve`
5206 // on together. Every one is bitwise inert at its default.
5207 bool pcMdotOperator_ = false; // item 1: mdot from the energy operator's OWN interfacial flux
5208 int pcGfmOrder_ = 1; // item 2: 2 = the Gibou-Fedkiw three-point row
5209 bool pcCurvDist_ = false; // item 3: curvature-consistent distances, kappa from the cascade
5210 CCField pcKappa_; // the V3 cascade's kappa on the G = 2 block (item 3)
5211 CCField pcMdotFit_; // the one-sided-fit mdot, kept as a DIAGNOSTIC under item 1
5212 CCField pcInDomain_; // 1 where a cell CARRIES A ROW (inside the global domain), else 0
5213 bool pcMaskFresh_ = false; // is the Dirichlet/plane geometry current for THIS colour field?
5214 double pcQOperator_ = 0.0; // sum of the operator's interfacial heat (W), last build
5215 double pcQOrphan_ = 0.0; // ... the part on cells with no interfacial AREA (dropped)
5216 double pcMdotFitMean_ = 0.0; // area-weighted mean of the diagnostic fit mdot
5217 bool pcHasSink_ = false;
5218 double pcSinkW_ = 0.0;
5219 std::vector<Closure> closures_; // property/body-force closures (applied at top of step())
5220 CCField cellForce_[3]; // per-cell momentum body force (Boussinesq / CFD-DEM feedback)
5221 bool hasCellForce_ = false;
5222 bool varProps_ = false; // variable-coefficient momentum (variable viscosity)
5223 bool harmonicMu_ = false; // harmonic vs arithmetic face-viscosity mean
5224 CCField muField_; // per-cell dynamic viscosity (when varProps_)
5225 int varRotMode_ = 0; // rotational term under varProps: 0 chi*mu_min, 1 chi*mu(i), 2 off
5226 double varRotChi_ = 1.0; // rotational coefficient scale chi
5227 bool varRho_ = false; // variable density (momentum + projection); staggered only
5228 CCField rhoField_; // per-cell density (when varRho_); rho_ is the reference rho0
5229 // --- geometric VoF (rung V2a, WO-J) ---
5230 bool vofEnabled_ = false;
5231 // WO-R2 item 4 — see setVofWispEps / defaultVofWispEps.
5232 double vofWispEps_ = defaultVofWispEps();
5233 bool rhoFaceHarmonic_ = false; // harmonic instead of arithmetic rho_f in the projection (OFF)
5234 bool outflowCorrValid_ = false; // the projection's outflow-face correction is still in u
5235 // WO-R item 4 measured NO against the DEFECTIVE operator; WO-R2 item 1 fixed the operator and
5236 // the answer flipped to YES (measured: with the fix, the projected outflow divergence at ratio
5237 // 10 is 8.31e-10 WITH the factor and 9.97e-05 without). DEFAULT ON;
5238 // `set_outflow_rho_correction(False)` restores the plain correction as the ablation.
5239 bool outflowRhoCorr_ = true;
5240 CCField cField_; // the G=2 registry mirror of the colour field ("C")
5241 CCField vofCs_; // rung V5a: cell fluid fraction on the G=2 block (staggered)
5242 CCField vofSolidG2_; // rung V5a: 1 where the cell is SOLID (G=2 mirror), else 0
5243 bool vofSolidZero_ = true; // rung V5a: canonical "C" is 0 in solid cells (see setter)
5244 // rung V5b (WO-S): the static contact angle. Unset => the neutral (90 deg) fill of WO-Q, and the
5245 // whole V5a battery is byte-identical.
5246 bool contactAngleSet_ = false;
5247 double contactAngleDeg_ = 90.0;
5248 int contactPivot_ = vof::kVofPivotVolume;
5249 std::vector<double> contactAngleField_;
5250 // rung V6 (WO-V6): the dynamic angle / hysteresis producer for the theta field. Inert unless
5251 // set_contact_angle_dynamic / _hysteresis is called (`vofDyn_.active()`), in which case the
5252 // whole V5b battery is byte-identical.
5253 vof::VofDynamicWetting vofDyn_;
5254 double contactSigmaOverride_ = 0.0;
5255 bool contactSigmaWarned_ = false;
5256 CCField vofDynVel_[3];
5257 vof::WyAdvector vofAdv_; // the g=3 working block: PLIC + Weymouth-Yue sweeps
5258 C3 e3_{0, 0, 0}; // extended extents of that block (n + 2*kVofG)
5259 double vofCflLimit_ = 0.25; // Weymouth's proven 3D boundedness bound 1/(2(N-1))
5260 double sigmaCsf_ = 0.0; // V4: surface-tension coefficient (0 = the force is off)
5261 double capillaryCfl_ = 1.0; // safety factor on the Brackbill capillary dt
5262 double csfInterfaceEps_ = 1e-8; // V4: wisp threshold on the curvature's interfacial predicate
5263 int csfMode_ = 0; // 0 = balanced-force (production), 1 = cell-interp ablation
5264 bool kappaFrozen_ = false; // V4 instrument: do not recompute kappa at the head of the step
5265 long vofStep_ = 0; // sweep-permutation counter (6-cycle)
5266 // --- two-phase open boundaries (rung V-BC, WO-R) ---
5267 bool vofBcActive_ = false; // any inflow colour / backflow colour set: arms the whole
5268 // rung (the mask, the ghost rules, the property ghosts)
5269 bool vofInflowSet_[6] = {false, false, false, false, false, false};
5270 bool vofBackflowSet_[6] = {false, false, false, false, false, false};
5271 double vofInflowC_[6] = {0, 0, 0, 0, 0, 0}; // uniform inflow colour per face
5272 double vofBackflowC_[6] = {0, 0, 0, 0, 0, 0}; // inletOutlet backflow colour per face
5273 std::vector<double> vofInflowProfRaw_[6]; // the user's (nb, nc) profile, kept so the device
5274 int vofInflowProfNb_[6] = {0, 0, 0, 0, 0, 0}; // views can be rebuilt when the block is rebuilt
5275 int vofInflowProfNc_[6] = {0, 0, 0, 0, 0, 0};
5276 CCField vofInflowProf3_[6], vofInflowProfG2_[6]; // resampled onto the g=3 / G=2 face planes
5277 int vofProf3Nc_[6] = {0, 0, 0, 0, 0, 0}, vofProfG2Nc_[6] = {0, 0, 0, 0, 0, 0};
5278 vof::UCField vofOutside_; // the out-of-domain mask on the g=3 block
5279 // SCALING_ISSUES #3/#8: scratch for fillVelGhostsTo's save/restore of the high-side outflow face
5280 // plane across the distributed halo exchange, one per axis, allocated on first use.
5281 CCField outflowPlane_[3];
5282 double vofBcVol_[6] = {0, 0, 0, 0, 0, 0}; // signed liquid volume of the LAST step, + = in
5283 double vofBcVolTotal_[6] = {0, 0, 0, 0, 0, 0}; // running total since enable_vof
5284 // --- curvature (rung V3, WO-O) ---
5285 vof::VofCurvature vofCurv_; // the cascade, on the SAME g=3 block as the colour field
5286 CCField kappaField_, kappaBranch_; // the G=2 registry mirrors ("kappa", "kappa_branch")
5287 vof::VofCurvature::Stats vofCurvStats_{};
5288 // --- momentum-consistent transport (rung V2b, WO-K) ---
5289 bool vofMomEnabled_ = false; // rho^c u_c advected by the SAME geometric fluxes as C
5290 vof::MomentumConsistentAdvector vofMom_; // the half-shifted CVs, on the SAME g=3 block
5291 CCField uAdv_[3]; // the recovered advected velocity on the solver's G=2 block (inner cells)
5292 double vofRhoG_ = 1.0, vofRhoL_ = 1.0; // the two phase densities (C = 0 / C = 1)
5293#ifdef PECLET_FLOW_MPI
5294 std::shared_ptr<GridHaloTopology<3>> vofHalo_; // the colour field's OWN g=3 topology
5295 std::shared_ptr<GridHalo<double>> vofDev_;
5296#endif
5297 CCField rho1_, cx1_, cy1_, cz1_; // g=1 MG-block density bridge + projection face coefficients
5298 bool porous_ = false; // volume-averaged continuity d(eps)/dt+div(eps u)=0 (CFD-DEM)
5299 double pressUnderRelax_ = 1.0; // omega_p for the incremental pressure accumulation (1.0 = off)
5300 bool porousDepsDt_ = true; // include the d(eps)/dt source in the projection RHS. Off ->
5301 // enforce div(eps u)=0 (drop the term, which is jagged/noisy
5302 // because eps is a bare per-cell particle deposit; the noisy
5303 // source can drive the eps-weighted pressure solve unstable).
5304 CCField epsField_, epsPrev_, eps1_, depsdt_; // eps^{n+1}, eps^n, g=1 bridge, stored d(eps)/dt
5305 CCField divAdv_; // cell div(u) — porous advection-form compensation (see buildRhs*)
5306 CCField epsRho_; // rho_eff = eps*rho — eps-conservative porous momentum (updateEpsRho per step)
5307 // eps-CONSERVATIVE porous momentum + projection pair (default): time term (eps_f rho/dt) u,
5308 // eps_f rho-weighted advective form, projection c_f = open*(eps rho idt)/(eps rho idt + beta)
5309 // with correction rho idt/(eps rho idt + beta) grad(phi). False = the legacy plain-u pair
5310 // (for A/B only; it kinematically drags gas with the moving porosity — energy injection).
5311 bool porousCons_ = true;
5312 CCField beta1_; // g=1 bridge of the drag coeff (semi-implicit-drag pressure)
5313 bool hasDrag_ = false; // implicit linear drag (CFD-DEM): beta on the momentum diagonal
5314 CCField dragBeta_; // per-cell drag coefficient (added to AC; target beta*u_p rides
5315 // the force_* cellForce fields)
5316};
5317
5318// The staggered MAC solver — THE flow solver, bit-identical to the pre-policy class. Bindings + the
5319// kokkos_mpi tests reference this name unchanged.
5321
5322} // namespace peclet::flow
5323
5324// The twelve domain headers hold the out-of-line member DEFINITIONS, and since G.8 only the two
5325// instantiation TUs need them: everything else linking `peclet_flow_solver` gets the declarations
5326// above and calls into the library. Narrowing the include to those TUs is what makes an edit to one
5327// domain header rebuild FOUR objects instead of all 49 (measured: 9m10s -> 4m35s of CPU at -j8).
5328//
5329// The one thing this forbids: a member TEMPLATE of `Solver` may not be defined in a domain header
5330// and called from a consumer -- an explicit instantiation of the class does not cover member
5331// templates, so the consumer would need the definition and would fail to link. There is exactly one
5332// member template on the class today (the VoF `template <class Fill, class Color, class ColorDu>`
5333// pair), it is used only from inside the class, and the whole battery links. If you add one that a
5334// test or the bindings must call, declare it in this file and define it HERE, above this point --
5335// not in a domain header.
5336#ifdef PECLET_FLOW_INSTANTIATING
5337#include "flow_ibm_core.hpp"
5338#include "flow_ibm_project.hpp"
5339#include "flow_ibm_scene.hpp"
5340#include "flow_ibm_geometry.hpp"
5341#include "flow_ibm_hydro.hpp"
5342#include "flow_ibm_vof.hpp"
5344#include "flow_ibm_closures.hpp"
5345#include "flow_ibm_scalars.hpp"
5347#include "flow_ibm_bc.hpp"
5348#include "flow_ibm_mpi.hpp"
5349#include "flow_ibm_diagnostics.hpp"
5350#include "policy.hpp"
5351#endif // PECLET_FLOW_INSTANTIATING
5352
5353
5354// The ONE compiled instantiation (suite/docs/QUALITY_PLAN.md §3.G.8). `Solver<Grid>` is a ~12 k-line
5355// class template whose 517 out-of-line members are defined in the twelve domain headers included just
5356// above (in the instantiation TUs only); every
5357// consumer used to instantiate all of it at -O3 for itself (45 test executables plus the bindings,
5358// for both grids), which is what made a full rebuild ~45-50 CPU-minutes and made an edit to any one
5359// domain header invalidate all 45. These declarations suppress that implicit instantiation; the
5360// definitions are compiled once, in src/flow_solver_staggered.cpp and src/flow_solver_colocated.cpp,
5361// into the static library every consumer links (cmake/PecletFlowSolver.cmake).
5362//
5363// Nothing is hidden by this. The class definition above is complete, so a consumer can still derive
5364// from it (the bindings' `BoundSolver final : Solver<Grid>` inherits the constructor) and the
5365// standard exempts inline functions -- everything defined inside the class body -- from an explicit
5366// instantiation declaration, so the small accessors are instantiated and inlined in the consumer
5367// exactly as before. What moves is the out-of-line bulk, compiled from the same source under the
5368// same flags (no LTO, no -march: there is no cross-TU optimization to lose).
5369//
5370// PECLET_FLOW_MPI must match between the instantiation TUs and their consumers -- it changes the
5371// class. The build makes that structural: the macro is a PUBLIC property of the MPI library.
5372#ifndef PECLET_FLOW_INSTANTIATING
5373namespace peclet::flow {
5374extern template class Solver<Staggered>;
5375extern template class Solver<Colocated>;
5376} // namespace peclet::flow
5377#endif
5378
5379#endif // PECLET_FLOW_SDFLOW_IBM_HPP
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
flow — VoF Part III rung W0: the per-bubble VoF BLOCK, a third container over the same L1 kernels (su...
flow — VoF Part III rung W0: the block gather/scatter (VOF_PLAN.md §10 design items 2 and 4).
void imposeDomainWallKind(int mask)
void vofExchangeScalar(CCField f)
The colour block's ghost policy WITHOUT the colour-specific rules (no solid-band fill,...
int pressureBottomExtent() const
void setVofMomentumMuscl(bool on)
void setScalarSource(const std::string &name, double S)
void setSolidUploadSdf(CCField din)
void setVofCurvatureMixedHeightFit(bool on)
double wallFluxImbalance()
Net wall flux this rank injects, sum over inner cells of u_w .
void scalarCutMatvecTwo(ScalarField &sc, CCField yf, CCField ys, CCField xf, CCField xs)
void setContactAngleClamp(double loDeg, double hiDeg)
const VofTiming & vofTimingReport() const
bool vofBlocksEnabled() const
void setPressureMeanRemoval(bool all)
std::vector< double > getScalarSolid(const std::string &name)
double multigridAspectThreshold() const
std::vector< double > getFaceVelocity(int c)
void setPhaseChangeGfmOrder(int order)
WO-P3g item 2: the order of the one-sided (ghost-fluid) Dirichlet row.
void setVofRhoFloorFrac(double f)
void setPhaseChangeEnergyMuscl(bool on)
MinMod-limited donor reconstruction of the face temperature in the consistent energy flux (vof/energy...
void enableVofBlocksFromField(const std::vector< std::array< int, 6 > > &boxes)
static constexpr int kVofG
void setScalarWall(const std::string &name, int type, double value, double coefficient, int instance)
void setInstanceTransform(int i, const std::array< double, 3 > &translation, const std::array< double, 4 > &quat)
Move one instance (Layer 3 rung 4).
std::array< int, 3 > globalResolution() const
long numPressureChebyshevRestarts() const
void extendSdfDomainGhosts(CCField f)
Repair a cell-centred geometry field's ghost band on every rank-owned NON-PERIODIC domain face,...
void setContactAngleSmoothing(bool on)
void setPhysicalDomain(const std::array< double, 3 > &extent, const std::array< double, 3 > &origin, const std::array< long, 3 > &globalCells)
Arm the physical domain.
void applyVelocityBcAll(int fold, bool doOutflow)
void vofApplyColourBc(CCField f)
The colour field's boundary rules, applied at the END of vofFillGhosts — i.e.
void fillVelGhosts(int comp, int fold)
void pcZeroDomainGhosts(CCField f)
Zero the two ghost layers on every NON-periodic domain face this rank owns.
std::vector< double > scalarGeometryField(int which)
void setIncrementalPressure(bool on)
void pcCarryDeposit(ScalarField &sc)
WO-P3f — the enthalpy the per-cell Dirichlet overwrite destroys, returned to the phase it came from.
std::function< double()> constCoeffResidual(int c, double bx, double by, double bz, double Ac)
void setVelocityTolerance(double rtol, int minIters)
double maxOpenDivergenceInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
void addCsfRhs(int c, CCField dst, bool rowScaled, bool faceRange=false)
double capillaryCfl() const
bool pressureCoarseGhost() const
void scatterInner(CCField fld, const std::vector< double > &in)
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
void phaseChangeStep()
The in-step driver: everything applyPhaseChange does, at the head of step().
void setContactAnglePivot(int mode)
void setVofBlockPool(bool on)
bool contactAngleSet() const
ScalarField & scalarField(const std::string &name)
PhaseChangeDiagnostics phaseChangeDiagnostics()
void setVofKappaConstant(double kappa)
kappa is a PHYSICAL curvature, 1/length (the internal field is 1/h; see vof_curvature()).
bool balancedForceProjection() const
void buildOpenFaceField()
Build ufOpen_ = the face field with each open (outflow) domain face overwritten by its zero-gradient ...
void setScalarSourceField(const std::string &name, const std::vector< double > &S)
std::array< double, 3 > scalarMeanFlux(const std::string &name)
bool hasMovingInstance() const
True when at least one instance carries a nonzero velocity – i.e.
long solveBalancedForceSystem(double bref, double r0)
void fillGhosts(CCField f)
std::vector< double > getVofCurvature()
The interface curvature kappa = 2H in the caller's units, i.e.
long lastOuterIterations() const
void setMassFluxUniform(double v)
Prescribe a UNIFORM mass flux (P0), in mass per area per time.
double decompositionMaxImbalance() const
void fillDomainPeriodic(CCField f)
void setPressureCoarseGhost(bool on)
Neumann (zero-gradient) coarse ghost on wall/inflow faces before the pressure MG's prolongation — the...
void setDomainBc(int face, int type, double vx, double vy, double vz)
bool advWallInputs() const
R0 helper: allocate (once) and arm the per-component advective-term stash the reaction-force budget c...
void refreshWallVelocity()
Re-derive ONLY the wall-velocity fields and the momentum operator that folds them in.
void setUfAdvection(bool on)
Advect the collocated momentum with the PROJECTED, divergence-free MAC face field uf_/vf_/wf_ (DEFAUL...
void setPhaseChangeFitCurvature(double kappa)
WO-P3f INSTRUMENT: prescribe the interface curvature kappa = div(n) the one-sided fits use to correct...
void fillPropGhosts(CCField f, bool edges=false)
void requireCollocatedFaceForceScope(const char *who)
void patchScalarDirichletFaceVar(CCField AC, CCField band, CCField kc, int a, int side)
void setFaceInterp(int mode)
void applyBackflowStab(int c)
void pcScatterSource()
(2) Deposit each interfacial cell's source into its chosen pure-gas cell, as a GATHER (each receiving...
void checkMovingInstancesAreCut()
A MOVING instance whose surface produces no fractional face aperture has no path for its wall velocit...
sco::MeanGradPosition scalarMeanGradPosition(const double Gphys[3]) const
std::vector< std::array< int, 3 > > pressureMgLevelRatios() const
void pcApplyDivergenceSource(CCField div)
Subtract the phase-change (and any prescribed) divergence source from div_ so the deflated pressure s...
void checkVofBcFace(int f, int wantType, const char *who)
A VoF boundary colour is only meaningful on a face that already carries the matching domain BC,...
void setPhaseChangeMdotOperator(bool on)
WO-P3g item 1: take mdot from the energy operator's own interfacial flux instead of the one-sided lea...
std::vector< double > scalarSolidGeometryField(const std::string &name, int which)
void setVofTiming(bool on)
std::vector< double > getField(const std::string &name)
std::vector< long > movingInstanceDegeneratePoints() const
Per instance: staggered points where the sampled sdf is exactly zero (see the warning).
void setOutflowRhoCorrection(bool on)
void setPropertyMode(bool variable, bool harmonic)
std::vector< double > vofBlockKappa(long id)
double lastPressureAllreduceSeconds() const
void addCsfRhsBlocks(int c)
double phaseChangeCarryDeposited() const
The carry ledger, physical: an energy with the consistent transport on, and a temperature times a vol...
void setVofMomentumCellFlag(bool on)
void setVelocityChebyshev(bool on, int maxit=400)
void applyVelocityBcComp(int comp, int fold, bool doOutflow)
void setOpennessOverride(const std::vector< double > &ox, const std::vector< double > &oy, const std::vector< double > &oz)
void setOuterTolerance(double tol)
CCField addField(const std::string &name)
void vofBcArm()
Arm the rung: install the mask, rebuild the profiles, and zero the boundary volume ledger.
double lastMomentumResidual() const
void scalarCutRefusals(const ScalarField &sc) const
std::array< double, 3 > domainOrigin() const
Physical lower corner of the GLOBAL inner grid.
void setPressureGraphAmg(bool on)
bool vofHasGeometry() const
void scalarCutMatvec(ScalarField &sc, CCField y, CCField x)
std::array< long, 3 > fluidMomentumCells()
The number of unmasked (fluid) staggered momentum cells per component – the exact discrete datum the ...
ScalarField & cutcellScalar(const std::string &name, const char *who)
void pcRegress(double dt)
(3) The regression: two Jacobi passes over the exchanged per-cell data, so the clip deficit is redist...
vof::VofInterfaceArea::Stats pcAreaCascadeCompute()
Run the cascade area driver on the (already bridged) g = 3 colour block.
VelocityMG::Comm vmgComm() const
double velocityResidualTolerance() const
ScalarCutFacets scalarFacets(const std::string &name)
SField pcAreaCascadeField() const
The area field the last pcAreaCascadeCompute filled (either driver).
double pcEffPureEps() const
…and the mirror statement: a cell the ADVECTOR treats as a pure phase is a pure phase here too,...
void setPressureTelescope(bool on)
std::vector< double > getVofFilledColour()
void setPressureStrict(bool on)
Throw instead of reporting when the pressure preconditioner returns a non-finite correction (ISSUES s...
void enableVofBlocks(const std::vector< std::array< double, 4 > > &seeds)
OutflowBackflow outflowBackflow()
void fillGhostsFaces(CCField f)
bool advectionWallVelocity() const
void applyVelocityBcFace(CCField f, int comp, int a, int s, int fold, bool doOutflow)
void scaleForceTorque(std::vector< double > &out, std::size_t m) const
Hydrodynamic force and torque on each scene instance (Layer 4 rung 2) – the resolved CFD-DEM feedback...
void scalarCutAssembleSolve(ScalarField &sc, bool steady)
std::array< double, 3 > wallAreaProbe()
A_wall EXACTNESS PROBE (diagnostic for the Layer-4 force integral).
void pcUpdateEnergyProps()
Refresh k(C) and (rho c_p)(C) on the G=2 block from the current colour.
void rebuildGeometry()
Re-derive ALL geometry from the current instance transforms (Layer 3 rung 4): rebuild the accelerated...
void exchangeFieldAdd(const std::string &name)
std::vector< double > getVofAdvectedVelocity(int c)
void advanceScalarCutCell(ScalarField &sc)
CCConst advVelView(int c) const
The velocity view the advection operators must read for component c: the wall-corrected scratch while...
void setContactAngleDynamic(double thetaEDeg, double slipCells, double muLiquid, double sigma=0.0)
void requireScalesPinned(const char *who, bool needRho) const
Under an armed extent, refuse a call that writes a dimensional value into a FIELD before the referenc...
void exchangeField(const std::string &name)
void setAdvection(bool on)
std::array< long, 3 > globalCells() const
The GLOBAL cell counts (this rank's block single-rank).
void bridgeOutflowFacePlanes()
Bridge the HIGH-side boundary-face openness plane of every rank-owned outflow axis from the g=2 block...
void setPressureExactResidual(bool on)
WO-R2 item 3 — the exact (matrix-free, double, flux-form) level-0 operator apply in the residual and ...
std::vector< double > getVofGeometry(int which)
double phaseChangeQOperator() const
The operator's interfacial heat (power), physical.
void setSolidFromScene(bool cutcellPressure)
Sample the scene onto this rank's inner grid and install it as the solid, entirely on device – no nx*...
void vofRebuildBcBlock()
(Re)build everything that lives on the g=3 block for this rung: the out-of-domain mask and the resamp...
void addScalar(const std::string &name, double D, int scheme, int iters, bool cutcell=false)
std::vector< double > vofBcVolumes() const
Signed liquid volume that crossed each of the six domain faces during the LAST colour advection,...
void setScalarBcProfile(const std::string &name, int face, const std::vector< double > &prof, int n1, int n2)
void uploadVelocity(const std::vector< double > &uu, const std::vector< double > &vv, const std::vector< double > &ww)
Upload an initial velocity field in the caller's PHYSICAL units (converted per component to the index...
double maxOpenDivergenceProjectedInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
std::vector< double > getVof()
double surfaceTension() const
The surface tension in the caller's units (what set_surface_tension was given).
bool outflowRhoCorrection() const
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
void requireNoGeometry(const char *who) const
void setDivergenceSink(const std::vector< double > &w)
WO-P23: an AUTO-BALANCED sink region for the phase-change divergence source.
void applyVelocityBcCompTo(CCField f, int comp, int fold, bool doOutflow)
double capillaryDtInternal()
The same limit in the solver's index time (per tRef) — what the step's own guards compare to.
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
double maxAbsDiffInner(CCConst a, CCConst b)
void fillPeriodicAxes(CCField f, int axes)
void scalarCutAdvection(ScalarField &sc, bool steady)
void requireIsotropic(const char *what) const
Phase 2 §7 — refuse an ANISOTROPIC domain in a consumer this phase does not carry.
double vofTimingProjectionSeconds() const
CsfDiagnostics csfDiagnostics()
const scg::ScalarCutGeometry & scalarCutGeometry()
void setBackflowStab(double beta)
std::array< double, 3 > spacing() const
Cell size per axis (equal on every axis unless the Phase 2 anisotropic path is armed – see unitScales...
void setSurfaceTension(double sigma)
void exchangeExtRaw(CCField f)
Raw ghost fill of an extended-block field: the rank halo exchange under MPI, the periodic wrap single...
bool velocityChebyshevActive() const
void setVelocityIterations(int it)
void vofHarvestBcVolumes()
Move the advector's per-face boundary volume ledger into the solver's, once per advection.
void enableVofMomentum(double rhoGasPhys, double rhoLiquidPhys)
rhoGas / rhoLiquid are PHYSICAL densities, the same numbers the rho closure is given.
ScalarFaceFlux scalarFaceFlux(int a) const
double finishResidual(int c)
ScalarCutBudget scalarBudget(const std::string &name)
bool ufAdvVelocity() const
Are the advection operators reading the projected face field this step? Collocated grid,...
std::vector< double > getVofCurvatureBranch()
bool bcVelFacePair(int a) const
std::vector< double > scalarWallFlux(const std::string &name)
void setPressureChebyshev(bool on, int maxit, double rtol)
void setVofInflowProfile(int f, const std::vector< double > &prof, int nb, int nc)
Per-position inflow colour on face f: prof is (nb, nc) on the INNER grid of the face's two perpendicu...
double vofBlockImbalanceOf(int mode) const
void scalarCutWarnings(ScalarField &sc)
const UnitScales & unitScales() const
bool lastBalancedForceFailed() const
std::vector< int > getCutOwner() const
Per-inner-cell owning instance (Layer 3 rung 1), x-fastest, -1 where no scene has been sampled yet.
CCConst openFaceView(int c) const
Component c of the face field AS THE ADVECTION AND THE DIVERGENCE DIAGNOSTIC MUST READ IT: advFaceVie...
void applyScalarBcFace(CCField c, int a, int side, int type, double val, bool edges=false)
void requireVofGeometry(const char *who)
void setSolidGhostProjectionOverlay(CCField din)
void addWallFluxDivergence(CCField d)
void ensureScalarSolidRecord(ScalarField &sc, const std::vector< char > &conj)
double capillaryDt()
The Brackbill capillary limit in the CALLER's time unit.
void setVof(const std::vector< double > &c)
void setContactAngleHysteresis(double thetaADeg, double thetaRDeg)
static constexpr double defaultVofWispEps()
The value enableVof starts from.
long lastBalancedForceIterations() const
void setScalarBc(const std::string &name, int face, int type, double value)
std::vector< double > getOpenness(int c)
long periodicImageOverlapCells() const
Cells on this rank whose solid/fluid sign was set by a periodic IMAGE of an instance wider than the b...
bool pressureStrict() const
void buildVelocityOverlays(bool resetU)
Build the three per-component Robust-Scaled cut-cell overlays + solid masks from the CURRENT sdf_ (ex...
vof::VofCurvature::Stats vofBlockCurvatureStats() const
void applyDomainWallGeometry(int mask)
void setPhaseChangeThermal(const std::string &tname, double Tsat, double kg, double kl, double Rint)
P1: compute mdot each step from the registered scalar tname by the one-sided pure-cell weighted least...
std::vector< double > getVofBlockForce(int c)
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
bool scalarRootRank() const
rank 0 (or not distributed): the one that prints a warning
bool vofKappaFrozen() const
void pcBudgetPost(ScalarField &sc)
WO-P3f, part 2: the state AFTER the energy solve, the heat the plane-anchored rows actually delivered...
std::array< long, 2 > vofBlockPoolStats() const
void constraintDivergence(CCConst fx, CCConst fy, CCConst fz, CCField out)
void setVofCutFluxClamp(bool on)
CCConst advFaceView(int c) const
Component c of the collocated MAC face field (uf_/vf_/wf_), flow's low-face convention: entry i is th...
void setPhaseChangePlaneDirichlet(bool on)
The PLANE-ANCHORED (ghost-fluid) Dirichlet condition, ON by default.
ContactAngleDiagnostics contactAngleDiagnostics()
void setVofCurvatureWeightWidth(double d)
void setVariableRotational(int mode, double chi)
void setWallFluxDivergence(bool on)
Rung 3 on/off.
void setPressureUnderRelax(double w)
long lastMomentumSweeps() const
void setScalarMeanGradient(const std::string &name, const std::array< double, 3 > &G)
bool vofCutFluxClamp() const
double vofRhoFloorFrac() const
double vofTimingPredictorSeconds() const
double vofInterfaceArea()
The summed interfacial area over the inner region, PHYSICAL (globally reduced under MPI) — the E7 gal...
void setPressureLevels(int levels)
long balancedForceFailures() const
void closureUnitsNotice(const std::string &target) const
Under a physical domain, say on stderr that a closure on a target of unknown dimension takes its para...
void vofExchangeRaw(CCField f)
std::array< int, 3 > blockOrigin() const
void setFluidOnlyConstraint(int mode)
double wallSlipLength() const
The Navier slip length in the caller's units (0 = no-slip).
void setRotationalPressure(bool on)
void setInstanceMotion(int i, const std::array< double, 3 > &lin, const std::array< double, 3 > &ang, const double *center=nullptr)
Rigid-body motion of one scene instance (Layer 3 rung 2).
void setPhaseChangeQuadraticFit(bool on)
The QUADRATIC one-sided gradient fit (T - T_G = G phi + Q phi^2) instead of the linear one.
void setDeferredCorrection(bool on)
void setScalarSolid(const std::string &name, double D, double capacity, double partition, double contactResistance, int instance)
double vofInterfaceEps() const
void setPressureGeometry(const std::vector< double > &sdfInner)
void setVofBlockAssign(int mode, long every)
void resampleBcProfile(int face)
double pcBandDivergence()
The BAND-EXTENDED LIQUID VELOCITY of VOF_PLAN §9 item 3, as a MEASUREMENT rather than a switch.
scg::ScalarCutCensus scalarCutCensus()
void pcBudgetPre(ScalarField &sc)
WO-P3f, the energy-budget instrument, part 1: the state BEFORE the energy solve, and the class-change...
double contactAngle() const
void setWallSlipLength(double lambdaCells)
WO-V6b – the VELOCITY half of the dynamic contact line.
std::array< double, 3 > instanceCenter(int i) const
The resolved centre of rotation of instance i and whether it is pinned (explicit) or follows the body...
void applyPhaseChange(double dtPhysArg)
Kinematic entry point (the P0a/P1 driver): build mdot/A_G/n from the current colour and temperature,...
void requireWettingWall() const
void vofBlockCensus(std::vector< long > &masters, std::vector< long > &cells) const
void setPhaseChangeCarryConserve(bool on)
WO-P3f: make the per-cell Dirichlet overwrite ENTHALPY-CONSERVING.
void setVofBackflow(int f, double value)
inletOutlet backflow colour on outflow face f (default 0 = gas): where the boundary face velocity poi...
vof::MomentumConsistentAdvector::Diagnostics vofMomentumDiagnostics()
bool wallFluxDivergence() const
std::vector< double > vofBlockColour(long id)
void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow=true)
std::vector< long > movingInstanceCutCells() const
Per instance: cut rows of the momentum operator (all three components) at inner points this rank owns...
std::vector< double > getVofDynamicField(int which)
std::vector< double > hydroForceTorque()
PHASE 2 (anisotropic cells), doc/anisotropic_metric.md §4.4 — ADMITTED since commit C4.
void setVofCurvatureWorklist(bool on)
VofCurvature::useWorklist — run the height-function cascade and the PV fallback over a COMPACTED list...
static constexpr int G
Definition flow_ibm.hpp:80
void setContactAngleField(const std::vector< double > &thetaDeg)
void seedFreshCells(CCField mOld[3])
FRESH CELLS: the points a moving body has just uncovered.
void setCapillaryCfl(double f)
SceneMap sceneMap() const
const vof::VofCurvature::Timing & vofCurvatureTiming() const
CCField resampleFaceScalar(const std::vector< double > &prof, int nb, int nc, int face, C3 ext, int g, int &outNc)
Clamp-resample a per-position face scalar from the user's (nb, nc) INNER grid onto the ghost-inclusiv...
bool vofMomentumEnabled() const
vof::VofCurvature::Stats vofCurvatureStats() const
void setCommAvoiding(int mask)
Communication-avoiding red-black smoothing: kCaMomentum | kCaMg.
void fillAxis(CCField f, int axis)
void setAdvectionWallVelocity(bool on)
A0 — wall velocity (not zeros) in the advection inputs' masked rows. DEFAULT true.
void pcBuildInterface()
(1) The interface build: for every inner interfacial cell reconstruct the PLIC plane from the canonic...
void setVofPhantomCapillaryBound(bool enabled)
std::vector< double > getMomentumDiagonal(int c)
std::vector< double > reactionBudgetTerms()
R0 DECOMPOSITION PROBE.
int contactAnglePivot() const
void setField(const std::string &name, const std::vector< double > &v)
void setSolidDevice(CCField din, bool cutcellPressure)
Device entry point (Layer 2): the inner SDF is ALREADY on device, so geometry never round-trips throu...
void setApertureOrder(int order)
double lastSuperficialVelocityShift() const
The shift the last step() applied (caller's velocity units; 0 when off).
void setVelocity(int c, const std::vector< double > &v)
Write a component's inner velocity from a host vector (x-fastest, inner region) and re-impose the sol...
void setBodyForce(double fx, double fy, double fz)
Body force per unit volume, in the caller's units (e.g. a mean pressure gradient, or rho*g).
void solveScalarSteady(const std::string &name)
std::vector< double > getOpennessProj(int c)
double pcLedgerEnergyToPhys() const
The factor from an internal phase-change ledger entry (a per-cell sum of rcp' dT) to the caller's uni...
std::vector< double > vofBcVolumesTotal() const
The same, accumulated since enable_vof() (or the last resetVofBcVolumes()).
void closureRefreshUnits(Closure &cl)
Re-derive a closure's internal p / tabY from its physical pPhys / tabYPhys with the current reference...
void setVofWorklist(bool on)
WyAdvector::useWorklist — the compaction of the PLIC reconstruction pass onto the mixed cells.
std::vector< double > getVelocity(int c)
void setPorousConservative(bool on)
std::array< int, 3 > blockShape() const
void vofExtendWallBand(CCField f, int mask)
VofOverlapCensus vofBlockOverlapCensus() const
void setRhoFaceHarmonic(bool on)
void setVofInflow(int f, double value)
Colour of the fluid entering through inflow face f (0..5 = -x,+x,-y,+y,-z,+z), in [0,...
bool pressureExactResidual() const
void setScalarTolerance(const std::string &name, double rtol)
bool contactAngleHysteresis() const
void setVofStepParity(long n)
long strideOf(int c) const
const vof::WyAdvector::Timing & vofKernelTiming() const
void collocatedV8AutoFallback(const char *why)
peclet::core::geom::InstanceMotionView< double > motionView() const
bool hasField(const std::string &name) const
void setDomainBcProfile(int face, const std::vector< double > &prof, int nb, int nc)
void setAdvectionScheme(int s)
void enablePhaseChange(double rhoG, double rhoL, double hlv)
Turn on phase change.
void throwVelocityMgVariableMu(const char *who) const
int pressureTelescopeCount() const
void setExactCrossingsFromScene()
EXACT wall crossings straight from the scene, on device, on every rank – the in-solver replacement fo...
std::array< int, 3 > wallSlipSandwichCells() const
Cut-cell axes at which a one-cell fluid gap kept the no-slip closure (per component).
void setPressurePcg(bool on, int maxit, double rtol)
void setVelocityMultigridAuto(long cellsPerRank, long minGlobalCells=-1, int minBlockExtent=-1)
long solvePressureSystem(CCField rhs1, CCField x1)
void allocateBlock(int nx, int ny, int nz)
void setVelocityMultigrid(bool on, int levels, int vcycles)
void setRotationalWeight(double w)
void setVofBlockDeviceStaging(bool on)
void setDensityMode(bool variable)
bool hasScalar(const std::string &name) const
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setVofKappaFrozen(bool on)
void pcBuildInDomain()
WO-P3g — 1 on every cell that CARRIES A ROW in the energy solve, 0 otherwise.
void setDecomposition(int levels, double maxImbalance=1.05)
How the shared level-0 MPI decomposition is built.
void setExactCrossings(const std::vector< double > &t)
double vofRhoFloor() const
void setPressureWarmstart(bool on)
void superficialVelocityPrecheck() const
void velSweepLoop(Fill &&fill, Color &&sweepColor, ColorDu &&sweepColorDu, std::function< double()> resid=nullptr, double bnorm=0.0)
void setPressureIterations(int it)
void pcCarryApply(ScalarField &sc)
WO-P3f: consume pcCarrySrc_ into the energy solve's time base.
Kokkos::View< MReal *, CCMem > FV
Definition flow_ibm.hpp:79
double stepAdaptive(double cflTarget, double capillaryCflTarget, double dtMax)
void pcUpdateCurvature()
The per-cell Dirichlet mask of the energy scalar: T = T_sat + mdot R_int in every interfacial cell,...
bool velocityMgChebyshev() const
void setVofBlockOverlapDensity(bool enabled)
void refreshUnitDerived()
Re-derive every internal (index-unit) quantity from the stored physical inputs.
void setScene(const std::vector< int > &nodeInts, const std::vector< double > &nodeReals, const std::vector< int > &instInts, const std::vector< double > &instReals, bool periodic=false)
Install an analytic scene from core's flat node/instance encoding (peclet/core/geom/scene_builder....
void setPropertyTable(const std::string &target, const std::string &in0, const std::vector< double > &xs, const std::vector< double > &ys)
void setPressureBottomSolver(int engine)
static double vofWallPlaneSdf(int gx, int gy, int gz, I3 gs, int mask)
void setScalarMaxIterations(const std::string &name, int maxit)
void enableVofBlocksFromColours(const std::vector< std::array< int, 6 > > &boxes, const std::vector< std::vector< double > > &colours)
double pcEffInterfaceEps() const
The colour tolerance that decides which cells are INTERFACIAL, and it has to be at least the colour a...
void setVofBlockDebris(bool enabled)
void setPressureTelescopeForceLevel(int level)
void requireBalancedForceScope(const char *who, bool atStep) const
void advectVofKinematic(double dtPhysArg)
dt is in the caller's time unit.
void setVofInterfaceEps(double eps)
void checkSealedInflowCells()
Reject geometry that seals fluid cells against a prescribed INFLOW face off from the rest of the doma...
void setVofWispEps(double eps)
WO-R2 item 4 — the wisp threshold on the advector's mixed-cell predicate and on the interface Courant...
double effectiveContactSigma() const
void setVofBlockKappaClip(bool enabled, double kappaMaxPhys=-1.0)
CCField fieldView(const std::string &name)
double vofLastCourant() const
void pushVofMetric()
Hand the anisotropic cell metric to every VoF driver that exists (Phase 3).
std::string capillaryThrowMessage(double cap) const
void setCollocatedScheme(const std::string &name)
void setVelocityMgChebyshev(bool on, int degree=0, double eig_ratio=10.0)
void setFreshCellSeed(bool on)
Fresh-cell policy for moving geometry.
void setPressureBottomExtent(int cells)
The set_pressure_bottom("auto") criterion: agglomerate once the coarsest GLOBAL grid exceeds this man...
void filterCellField(CCField f, int axis)
double closureTargetToInt(const std::string &target) const
The internal-per-physical factor of a closure TARGET: rho, mu, force_x/y/z; 0.0 for any other field (...
bool contactAngleDynamic() const
bool ensureAdvStash(int c, bool adv)
void setOutflowOperatorCoefficient(bool on)
WO-R2 item 1 ABLATION: set_outflow_operator_coefficient(False) restores the pre-WO-R2 operator,...
double vofTimingMomentumSeconds() const
void setImplicitAdvection(bool on)
double vofTimingStepSeconds() const
void setVofSolidColourZero(bool on)
void setPhaseChangeCurvatureDistance(bool on)
WO-P3g item 3: measure the GFM row's theta and the one-sided fits' sample distances to the CURVED int...
std::vector< vof::VofBox > vofBlockRankBoxes(int size) const
bool instanceCenterPinned(int i) const
double vofCurvatureWeightWidth() const
void setVelocityResidualTolerance(double rtol)
void setPhaseChangeEnergyOrder(int order)
WO-P3g — the SECOND-ORDER interfacial energy operator, as one package.
std::function< double()> stencilResidual(int c, bool exchange=false)
bool mixedVelocityMg() const
void setPressureBottomMode(int mode)
void requirePhaseChange(const char *who) const
void phaseDensityRange(double &lo, double &hi)
void setMassFlux(const std::vector< double > &v)
Prescribe a per-cell mass flux (P0), x-fastest over the inner region; units and call-order rule as se...
sco::MaterialTable scalarMaterialTable(ScalarField &sc, std::vector< char > &conj)
VarFaceProps makeFaceProps(int c)
void setPhaseChangeEnergy(double rcpGas, double rcpLiquid)
Turn on the CONSISTENT energy transport (VOF_PLAN §9 item 6) for the scalar set_phase_change_thermal ...
void setRotationalWallWeight(double w0)
void vofBcPropGhosts(CCField f)
WO-R item 5 — the Neumann property policy at an inflow face.
void vofFillGhosts(CCField f)
bool pressureTelescope() const
void addCsfRhsCellInterp(int c, CCField dst, bool rowScaled, bool faceRange=false)
void setBalancedForceProjection(bool enabled)
const vof::WyAdvector & vofAdvector() const
double reduceMaxAbsInner(CCConst f)
void setPorousContinuity(bool on)
void setRho(double r)
Fluid density, in the caller's units.
vof::WyAdvector::Diagnostics vofDiagnostics()
bool vofAxisPeriodic(int a) const
void patchScalarDirichletFace(CCField AC, CCField band, double Din, int a, int side)
void setVofCflLimit(double v)
void scalarDirichletMask(const std::string &name)
Allocate (idempotently) the per-cell Dirichlet mask + value of a registered scalar.
VofStepLimits vofStepLimits()
Both limits at the current state, in the CALLER's time unit (courant is dimensionless).
void setRotationalFilter(bool on, double eps=0.05)
bool velocityMultigridActive() const
std::array< double, 3 > domainExtent() const
Physical extent of the GLOBAL inner grid (cell counts without a physical domain).
std::vector< std::string > fieldNames() const
std::vector< double > gatherInner(CCField fld)
void setSuperficialVelocity(bool on, int axis, double velocity)
Hold the SUPERFICIAL velocity along a periodic axis: at the end of every step() one uniform shift is ...
double vofWispEps() const
bool vofSolidColourZero() const
bool vofCurvatureWorklist() const
double vofBlockImbalance() const
std::vector< double > cellCentres(int axis) const
Physical cell-centre coordinates of THIS rank's inner block along axis (nx/ny/nz values).
void copyBlockShifted(CCField dst, C3 de, CCConst src, C3 se, int off)
void pcRefreshUnits()
Re-derive the INTERNAL phase-change constants (pcRhoG_ ... pcRcpL_) from the caller's physical ones (...
int decompositionLevels() const
std::vector< vof::VofBlockStats > vofBlockStats() const
CCConst wallVelView(int c) const
void advectVofBlocks(double dtPhysArg, bool requireSolenoidal=true)
dt is in the caller's time unit.
void setContactAngle(double thetaDeg)
void setVofFluxClamp(bool on)
void setPhaseChangeBudget(bool on)
WO-P3f: turn the ENERGY BUDGET instrument on.
PhaseChangeBudget phaseChangeBudgetValues() const
The budget, physical: energies and powers with the consistent transport on, temperature x volume (and...
bool vofCurvatureMixedHeightFit() const
void setDivergenceSource(const std::vector< double > &v)
A PRESCRIBED extra divergence source (1/time, physical; set_dt must precede it under an extent,...
double contactAngleSlip() const
std::vector< double > hydroForceTorqueReaction()
Hydrodynamic force and torque per instance from the DISCRETE REACTION (route (b) of the design note's...
double vofCflLimit() const
bool outflowOperatorCoefficient() const
void setPressureFcg(bool on, int maxit, double rtol)
std::vector< double > getPressure()
const vof::VofBlockSet * vofBlockSet() const
The block container itself, read-only (tests: the metric and tunables its cascades carry).
void setMultigridAspectThreshold(double theta)
The anisotropic-coarsening aspect threshold theta shared by the pressure and velocity multigrids (doc...
void setOuterIterations(int iters)
void setPhaseChangeDepositFallback(bool on)
WO-P3f open item 6 / WO-P3g — the divergence source's 5^3 fallback target, as a setter (it was only r...
bool isCutcellScalar(const std::string &name) const
void setPhaseChangeArea(int mode)
WO-P3c — which geometry the interfacial AREA comes from.
bool hasPhysicalDomain() const
The container: a replicated table of blocks, the per-step orchestration (gather → advect → re-centre ...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
flow — collocated (ABC approximate-projection) variable-density kernels: the mass-adjoint pair of run...
flow — VoF rung V-BC (WO-R): the colour field's OPEN-BOUNDARY ghost rules and the out-of-domain mask ...
flow — VoF rung V2a (WO-J): the bridge between the solver's G = 2 block and the colour field's own g ...
flow — VoF rung V3 (WO-O): the view-level driver for the curvature cascade.
flow — VoF Part II (WO-P23): CONSISTENT rho c_p T transport, driven by the SAME geometric fluxes as t...
flow — face/cell material-property accessors for the variable-coefficient momentum operator.
flow — IbmSolver domain boundary conditions: setDomainBc/profiles, pressureBcGhost,...
flow — IbmSolver property closures: property/density modes, porous continuity, drag,...
flow — IbmSolver core: allocation, physical-domain scales, rho/mu/dt and pressure/velocity driver set...
flow — IbmSolver diagnostics: state getters, divergence probes, timers and the outflow/backflow censu...
flow — IbmSolver static solid geometry: setSolid / setSolidDevice and its stages, pressure geometry.
flow — IbmSolver hydrodynamic force and torque: the reaction-budget terms and wall probes.
flow — IbmSolver MPI: initMpi, redistribute, rebalanceByWeights and the post-repartition field-resize...
flow — IbmSolver phase change: interface area, mass flux, energy transport, budget accounting,...
flow — IbmSolver pressure projection: step(), the project() stages, the buildRhs family,...
flow — IbmSolver transported scalars: addScalar/setScalarBc/advanceScalars and their BC application.
flow — IbmSolver cut-cell scalar transport (doc/scalar_ibm_design.md): the geometry record of WO-2 — ...
flow — IbmSolver scene and moving geometry: scene instances and motion upload, wall velocity,...
flow — IbmSolver Volume-of-Fluid: the colour field, cut-cell VoF geometry, contact angle,...
flow — the gauge-exact directional cell-centre pressure gradient.
flow — OPT-IN forensics for the ghost-projection overlay (PECLET_FLOW_GP_DEBUG).
flow — GridLayout policy traits (placement of the velocity unknowns).
flow — WO-P3c: the view-level driver of the cascade-consistent interfacial area.
flow — collocated approximate (MAC) projection helpers (Almgren–Bell–Colella style).
flow — Chebyshev semi-iteration as a standalone momentum (implicit-diffusion) solver,...
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
flow — portable (Kokkos) IBM geometric fields + variable-coefficient RB-GS smoother.
flow — portable (Kokkos) cut-cell pressure operator + Chorin projection.
flow — portable (Kokkos) MAC stencil operators: Red-Black Gauss-Seidel smoothers + divergence.
flow — portable (Kokkos) velocity (momentum) multigrid for the IBM diffusion solve: the STAIRCASE coa...
flow — WO-P3d: the block driver of the marching-tetrahedra interfacial area.
flow — VoF rung V2b (WO-K): momentum-consistent transport of rho^c u_c on the half-shifted (MAC) mome...
Definition conj.py:1
@ kAreaMcPlic
the PLIC signed-distance level set, centroid deposit
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
GpOverlayReal< mreal > GpOverlayMReal
Definition mac_ibm.hpp:38
Kokkos::View< double *, CCMem > CCField
Kokkos::View< const double *, CCMem > CCConst
Kokkos::View< double *, SMem > SField
Definition prof.py:1
flow — VoF phase change (Part II, rungs P0/P1): the container-free kernel set.
flow — the multidimensional iteration policies: iteration order follows storage.
flow — device property closures: material properties / body forces as functions of fields.
flow — cell-centred scalar transport (advection–diffusion) on the cut-cell grid.
flow — portable (Kokkos) staggered MAC momentum advection (Koren TVD + FOU).
long unfilledCells
SOLID cells pass 1 left untouched (passes 2-3 then fill them)
double meanApparentAngle
mean measured apparent angle over contactCells, degrees
long dynamicCells
band cells the V6 pass produced an angle for
double setAngle
the prescribed angle, degrees (uniform case)
double maxContactSpeed
max |U_cl| (smoothed), solver velocity units
double meanApparentTheta
mean apparent angle over the same set, degrees
long neighbourCells
band cells written by the mean of the anchor's MIXED neighbours
double maxCaCl
max |Ca_cl| = |mu_l U_cl / sigma|
long parallelCells
band cells whose interface was parallel to the wall
long contactCells
band cells written by the theta plane of their own anchor
long pinnedCells
of those, cells whose contact line is PINNED
long pureCells
band cells that took the pure-phase continuation
double meanImposedTheta
mean IMPOSED angle over the V6 contact cells, degrees
long neutralCells
band cells that fell back to WO-Q's neutral mean
std::vector< peclet::core::solver::AndersonRole > roles
std::array< double, 9 > signature
std::vector< CCField > fields
OUTFLOW REVERSAL CENSUS – the regime in which the zero-gradient (do-nothing) outflow is only conditio...
WO-P3f — the ENERGY BUDGET of the phase-change energy solve.
long nLeaveGas
interfacial -> gas (the bubble swallowing a cell)
double hMasked
the same over MASKED (interfacial) cells
double hOpenNew
the same, after the solve
double qGfm
heat INTO the unmasked set across the plane-anchored rows (W)
double dEoverwrite
sum rcp (dval - T) over masked cells: what the Dirichlet rows inject when they overwrite the transpor...
long nEnterLiquid
liquid -> interfacial transitions this step
double qBehind
WO-P3g: the second-order row's one-sided rescaling of the band BEHIND each interfacial face,...
long calls
how many energy solves have been instrumented
double eLeave
... and of the cells that LEFT it (carrying pcCarriedValue)
double dEoverwriteNew
the part of it on cells that were NOT masked last step
double eEnter
sum rcp (T - T_sat) of the cells that JOINED the masked set
double hLiquid
sum rcp (T - T_sat) over PURE LIQUID cells, before the solve
long nMasked
size of the masked set
double hOpen
sum rcp (T - T_sat) over UNMASKED cells, before the solve
long nLeaveLiquid
interfacial -> liquid
long sourceCells
cells that RECEIVED a deposit
long deficitCells
cells that clipped at C = 0
double bandDiv
max |div(open u)| over interfacial cells (WO-P23; see pcBandDivergence)
double redistributed
|clip deficit| pushed into neighbours this step
long areaNone
WO-P3c: cells with NO cascade geometry (kept their MYC PLIC area)
double qOperator
WO-P3g: the heat (W) the operator's Dirichlet rows draw across the interface, i.e.
double mdotFit
WO-P3g: the AREA-WEIGHTED one-sided-fit mdot, kept as the diagnostic that item 1 replaced (0 unless t...
double areaOrphan
WO-P3d: area the JOINED sheet booked to cells that are not interfacial, i.e.
long areaPv
WO-P3c: cells whose area came from the PV paraboloid (tier 3)
long fallbackCells
interfacial cells whose +n walk found no pure gas cell
long areaHf
WO-P3c: cells whose area came from a height function (tiers 1/2a)
double sourceSum
sum of the deposited divergence source over inner cells (1/s)
double area
sum of the PLIC polygon areas over interfacial cells (h^2)
double removedVolume
sum of dV actually subtracted this step (+ = evaporated)
double Tmax
energy-scalar extrema (consistent transport only)
double unresolved
clip residue no neighbour could absorb (pushed anyway)
double qOrphan
... the part of it on interfacial cells with NO area, which the regression drops: the ONE non-conserv...
long excessCells
cells that clipped at C = 1
diagnostics.scalar_budget (§8.2), physical; collective under MPI.
diagnostics.scalar_facets (§8.2): this rank's facets, physical; per facet 3 values for the centroid a...
The ONE predicate for the cut-cell scalar's advective face flux (doc/scalar_ibm_design....
CCField vel
face-normal index velocity at the low a-face of cell i (internal)
CCField open
the projection's openness of that face
The map between the solver's INDEX coordinates and the coordinate system the analytic scene is writte...
Definition flow_ibm.hpp:241
double a[3]
scene_a = a[a] + b[a]*xi_a (xi = global index)
Definition flow_ibm.hpp:242
double velToInt[3]
scene velocity -> index velocity
Definition flow_ibm.hpp:245
double dToInt
scene signed distance -> index distance (1/hRef)
Definition flow_ibm.hpp:244
double angToInt
scene angular velocity -> index (omega*tRef)
Definition flow_ibm.hpp:246
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
double org[3]
physical lower corner of the GLOBAL inner grid
Definition flow_ibm.hpp:139
double rhoRef
reference density = the first set_rho
Definition flow_ibm.hpp:143
double forceToInt(int a) const
Definition flow_ibm.hpp:157
vof::VofMetric vofMetric() const
Definition flow_ibm.hpp:189
bool physical
an extent was given (the scales are armed)
Definition flow_ibm.hpp:137
double h[3]
cell size per axis = extent / cells
Definition flow_ibm.hpp:138
double hp[3]
h_a' = h_a / hRef (>= 1, exactly 1 on the finest axis)
Definition flow_ibm.hpp:178
double velToPhys(int a) const
Definition flow_ibm.hpp:151
double vol
V' = h_x' h_y' h_z' (cell volume in hRef^3)
Definition flow_ibm.hpp:180
long cells[3]
the GLOBAL cell counts the extent spans
Definition flow_ibm.hpp:141
bool aniso
some h_a' != 1 — kernels dispatch the per-axis body
Definition flow_ibm.hpp:181
double energyToPhys() const
An internal energy (rho c_p T summed over cells of volume hRef^3) to joules: the internal energy unit...
Definition flow_ibm.hpp:222
double tRef
reference time = the first set_dt
Definition flow_ibm.hpp:144
double hRef
reference length = min_a h_a (== h_a, Phase 1)
Definition flow_ibm.hpp:142
double ext[3]
physical extent of the GLOBAL inner grid
Definition flow_ibm.hpp:140
double velToInt(int a) const
Definition flow_ibm.hpp:150
long cells
cells with S > 1 + 1e-8 (global)
bool active
the phantom-aware capillary bound is in force
double maxSum
max_cells S (global)
double excess
sum over cells of (S - 1)^+ (global, cell volumes)
double courant
the interface-local Courant number at the current dt
double cflDt
the largest dt the WY boundedness cap admits
double capillaryDt
the largest dt the Brackbill capillary constraint admits
double binding
min(cflDt, capillaryCfl * capillaryDt)
double bridge
... of which: the G=2 <-> g=3 bridges + the C ghost policy
double curvature
the V3 height-function cascade + the capillary dt check
double momBridge
... of which: its bridges
double blockAdvect
the block-container colour stage as a whole (advectVofBlocks)
double csf
the V4 balanced-force CSF added to the three momentum RHSs
double momAdvect
the momentum-consistent colour+rho^c u stage (advectVofMomentum)
double blockDebris
... of which: the debris/residue pass (§5.3/§13 of the note)
double phaseChange
the Part II phase-change stage (mdot, deposit, regression)
The geometry part of diagnostics.scalar_census (§8.2).
The whole record: per-cell fields on the extended block (kappa and unknown haloed; the apertures comp...
The per-body material of a conjugate scalar, internal units (§4.2): conj 1 on a conjugate body; lam =...
G.x at the centre of extended-block cell j (physical; cellCentres' formula, unwrapped).
Per-call census of the cascade over this block's inner region (LOCAL; a distributed caller sums them ...
Per-call census over the inner region (LOCAL; a distributed caller sums it).
flow — VoF rung V4 (WO-P): balanced-force continuum surface force + the capillary Δt.
static constexpr double AC
flow — VoF rung V6 (WO-V6): the DYNAMIC contact angle (Cox-Voinov with an explicit slip length) and a...