flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_geometry.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_GEOMETRY_HPP
9#define PECLET_FLOW_FLOW_IBM_GEOMETRY_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
14void Solver<Grid>::setPressureGeometry(const std::vector<double>& sdfInner) {
15 setSolid(sdfInner, true);
16}
17
18template <class Grid>
19void Solver<Grid>::setSolid(const std::vector<double>& sdfInner, bool cutcellPressure) {
20 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
21 if (sdfInner.size() != n)
22 throw std::runtime_error("set_solid: expected nx*ny*nz values");
23 CCField din("peclet::flow::sdfInner_d", n);
24 using HostConst =
25 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>;
26 // The caller's SDF is a PHYSICAL signed distance sampled at cellCentres(); the solver's
27 // geometry lives on the unit lattice, where a distance is measured in cells. (Isotropic in
28 // Phase 1: one hRef divides all three axes. Anisotropic cells need the metric of Phase 2,
29 // because the level set's gradient then stops being the index-space normal.)
30 const double k = u_.lenToInt(); // exactly 1.0 in cell units
31 if (k != 1.0) {
32 std::vector<double> scaled(n);
33 for (std::size_t i = 0; i < n; ++i)
34 scaled[i] = sdfInner[i] * k;
35 Kokkos::deep_copy(din, HostConst(scaled.data(), n));
36 } else {
37 Kokkos::deep_copy(din, HostConst(sdfInner.data(), n));
38 }
40}
41
42template <class Grid>
44 const bool useEx = hasExactCross_ && !Grid::collocated; // exact-theta arrays are for the
45 // staggered point placement
46 const mreal lam = (mreal)(wallSlip_ ? slipLambda_ : 0.0);
47 if (lam > 0.0f && slipSkipDev_.data() == nullptr)
48 slipSkipDev_ = Kokkos::View<int, CCMem>("peclet::flow::slipSkip");
49 for (int c = 0; c < 3; ++c) {
50 const Off3 off =
51 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
52 C[c].nCut = buildIbmOverlay<0>(
53 CCConst(sdf_), e_, G, off, /*Dirichlet*/ 0, C[c].ov, C[c].idMap, C[c].counter,
54 useEx ? CCConst(tEx_[c][0]) : CCConst(), useEx ? CCConst(tEx_[c][1]) : CCConst(),
55 useEx ? CCConst(tEx_[c][2]) : CCConst(), C3{nx_, ny_, nz_}, lam, lam > 0.0f ? c : -1,
56 lam > 0.0f ? slipSkipDev_ : Kokkos::View<int, CCMem>(), // SCHEME 0 = point-value (matches
57 // CUDA ibm_geometry_ext_k<0>)
58 (mreal)u_.hp[0], (mreal)u_.hp[1], (mreal)u_.hp[2]); // §6.4 slip metric (trap 9)
59 if (lam > 0.0f) {
60 int sk = 0;
61 Kokkos::deep_copy(sk, slipSkipDev_);
62 slipSandwich_[(std::size_t)c] = sk;
63 } else {
64 slipSandwich_[(std::size_t)c] = 0;
65 }
66 ibmSolidMask(C[c].mask, CCConst(sdf_), e_, off);
67 // Gate 7: ibmSolidMask samples the sdf at the staggered offset through the CLAMPING
68 // sampler, so on the outermost ghost plane the mask can disagree with the neighbour's
69 // interior value. The moving-geometry advection fill reads the ghost mask (it decides
70 // which ghost rows carry the wall velocity), so take the owner's mask there. Static
71 // scenes never consume ghost masks: keep them byte-identical by gating on motion.
72 if (hasMotion_)
73 exchangeExtRaw(C[c].mask);
74 if (resetU)
75 Kokkos::deep_copy(C[c].u, 0.0);
76 }
77 if constexpr (Grid::collocated)
78 if (resetU) {
79 // The collocated MAC face field is NOT a registry field and nothing above rebuilds it, so
80 // zeroing the cell velocity here (setSolid is the initial-geometry setup) would otherwise
81 // leave `uf_` holding the PREVIOUS geometry's projected field while `u` is 0 -- a stale
82 // advecting velocity for `cadv` (ufAdvVelocity()) and a stale flux for the colour /
83 // scalar transport that also ride it. Drop it and say so: `faceFieldValid_ = false`
84 // sends momentum advection back to the cell->face average (of a zero field: the same
85 // zero) for one step, and makes `advect_vof`'s guard refuse instead of accepting a field
86 // that is only "solenoidal" because it is zero.
87 Kokkos::deep_copy(uf_, 0.0);
88 Kokkos::deep_copy(vf_, 0.0);
89 Kokkos::deep_copy(wf_, 0.0);
90 faceFieldValid_ = false;
91 }
92}
93
94template <class Grid>
96 if (!hasBc_)
97 return;
98 B3 e{e_.x, e_.y, e_.z};
99 for (int face = 0; face < 6; ++face) {
100 if (!touchesGlobalFace(face))
101 continue;
102 const int a = face / 2, s = face % 2;
103 if (bc_[face] == 4)
104 bcMirrorGhost(f, e, G, a, s); // symmetry plane: the mirror IS what the BC asserts
105 else if (bc_[face] != 0)
106 bcNeumannGhost(f, e, G, a, s); // wall / inflow / outflow: constant normal extension
107 }
108}
109
110template <class Grid>
112 // Every geometry path (set_solid, set_pressure_geometry, set_solid_from_scene, redistribute)
113 // lands here: the cut-cell scalar record is stale from now on (rebuilt lazily, §2.5).
115 cutcellPressure_ = cutcellPressure;
119 // set_solid changes eligibility (hasSolid_), so re-decide at the next step(),
120 // where dt/mu/rho are final. Deciding HERE would read a dt the caller has not set yet.
121 vmgDecided_ = false;
122 if (cutcellPressure_) {
127 }
128 // Rung V5a (WO-Q): the colour block's cut-cell geometry is derived from the openness and the
129 // SDF that were just rebuilt, so set_solid AFTER enable_vof must rebuild it — the same reason
130 // initMpi rebuilds the block. Inert (and byte-identical) when VoF is off.
131 if (vofEnabled_)
133 geometryBuilt_ = true;
134}
135
136template <class Grid>
138 if constexpr (Grid::collocated) {
139 // DEFAULT SWITCH (2026-08-25, user decision after the attractor campaign): the collocated
140 // scheme default is AUTO = the GHOST (fluid-only) projection — family-free, unconditionally
141 // stable, protocol-independent (doc/collocated_invisible_subspace.md; clean ladders both
142 // beds) — falling back to gauge-exact with a stderr notice on the configurations the ghost
143 // v1 does not support (porous / variable-rho / domain-BC / Chebyshev / analytic overrides).
144 // Any explicit scheme selection (set_collocated_scheme / set_face_interp /
145 // set_ghost_projection / set_fluid_only_constraint) disables AUTO.
146 if (colSchemeAuto_) {
147 const bool ok = !(porous_ || varRho_ || hasBc_ || useChebyshev_ || hasExactCross_ ||
148 hasOpenOverride_ || fluidOnlyMode_ != 0);
149 if (ok) {
150 ghostProjection_ = true;
151 gpMatrixOrder_ = 2;
152 gpRhsOrder_ = 2;
153 faceInterp_ = 0; // the ghost owns the operators the face-interp modes replace
154 gpNRows_ = -1;
155 } else {
156 if (ghostProjection_)
157 gpNRows_ = -1;
158 ghostProjection_ = false;
159 faceInterp_ = 9;
161 "peclet::flow SolverColocated: AUTO scheme fell back to gauge-exact "
162 "(configuration unsupported by the ghost projection v1). Select explicitly "
163 "with set_collocated_scheme to silence this notice.\n");
164 }
165 }
166 }
167}
168
169template <class Grid>
171#ifdef PECLET_FLOW_MPI
172 for (bool& d : momStencilDirty_) // stencil ring re-exchange for the CA momentum sweeps
173 d = true;
174#endif
175 // Does the geometry actually contain solid? (all-fluid set_pressure_geometry passes sd>0
176 // everywhere -> stays false, keeping the channel/BFS path.) Device reduction: din lives on
177 // device now, and pulling it back just to scan it would defeat the point.
178 {
179 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
180 int anySolid = 0;
182 Kokkos::parallel_reduce(
183 "peclet::flow::has_solid", Kokkos::RangePolicy<CCExec>(0, nInner),
184 KOKKOS_LAMBDA(const std::size_t i, int& acc) { acc = acc || (dinC(i) < 0.0); }, anySolid);
185 hasSolid_ = anySolid != 0;
186 }
187#ifdef PECLET_FLOW_MPI
188 if (distributed_) { // a solid anywhere in the global domain enables the IBM momentum path
189 int local = hasSolid_ ? 1 : 0, global = 0;
191 hasSolid_ = global != 0;
192 }
193#endif
194#ifdef PECLET_FLOW_MPI
195 if (distributed_) {
196 // Multi-rank: din is THIS rank's LOCAL inner block; fill the inner cells ON DEVICE, then
197 // halo-exchange the ghosts (cross-rank + periodic) so the overlay/openness read the
198 // neighbour's SDF at the block boundary. (Was a host mirror + triple loop + full H2D.)
200 const int ex = e_.x, ey = e_.y, nx = nx_, ny = ny_, nz = nz_, g = G;
201 CCField sdf = sdf_;
203 Kokkos::parallel_for(
204 "peclet::flow::sdf_fill_inner", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
205 KOKKOS_LAMBDA(int x, int y, int z) {
206 sdf((long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey) =
207 dinC((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
208 });
209 space.fence();
210 velDev_->exchange(sdf_);
211 } else
212#endif
213 {
214 // Single-rank: periodic-wrap gather on device (G4) — fills the whole extended block
215 // (inner + periodic ghosts) in one kernel. `din` is already device-resident.
217 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
218 CCField sdf = sdf_;
219 Kokkos::parallel_for(
220 "peclet::flow::sdf_periodic_wrap", MDRange3<CCExec>(space, {0, 0, 0}, {ex, ey, ez}),
221 KOKKOS_LAMBDA(int x, int y, int z) {
222 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
223 iz = (((z - g) % nz) + nz) % nz;
224 sdf((long)x + (long)y * ex + (long)z * (long)ex * ey) =
225 din((std::size_t)ix + (std::size_t)iy * nx + (std::size_t)iz * (std::size_t)nx * ny);
226 });
227 space.fence();
228 }
229}
230
231template <class Grid>
233 // NON-PERIODIC domain faces: the periodic/halo fill above wrapped the OPPOSITE side of the
234 // domain into the ghost band, so the geometry a boundary face sees is teleported from the far
235 // side. On a FREE-SLIP face (type 4) that made a far wall a phantom solid ON the symmetry plane
236 // (measured: a half Poiseuille channel closed by a type-4 face read u ~ 0 there) and the mirror
237 // is what the BC asserts. On a WALL / INFLOW / OUTFLOW face (types 1/2/3) the same wrap decides
238 // the BOUNDARY-FACE APERTURE, because `ccFaceOpen` samples the SDF AT the face, i.e. halfway
239 // between the last inner cell and that ghost: a solid cell against an inflow plane was handed
240 // the far side's fluid and came out as a fully OPEN face, so the prescribed inflow velocity was
241 // pushed into a cell whose pressure row is entirely closed -- an inconsistent row, and the
242 // iteration cap and max|div| = U of SCALING_ISSUES #3 (doc/cutcell_openbc_convergence.md).
243 // The constant normal extension is the geometric reading of "the domain plane cuts the solid":
244 // the solid continues straight out, so the boundary face's aperture is the fluid fraction of the
245 // boundary plane itself. Rank-owned faces only (touchesGlobalFace); a bed clear of the open
246 // faces is byte-identical either way (the aperture is 1 whichever value the ghost carries).
248 buildVelocityOverlays(/*resetU=*/true);
249 // MOVING GEOMETRY (rung 2): the wall-velocity fields must exist BEFORE the momentum operator
250 // is assembled -- rebuildStencils folds them into the inhomogeneous term. sdf_ and its ghosts
251 // are final at this point, which is what the central-difference normals read.
254 // Staggered domain BCs bake an implicit-diffusion wall fold; the collocated grid instead uses
255 // explicit reflection ghosts (refreshed each smoother sweep), so it needs no fold.
256 if (hasBc_ && !Grid::collocated)
258}
259
260template <class Grid>
262 // WHICH MOMENTUM SOLVER, and the criterion is the PHYSICS, not the configuration. Decided
263 // 2026-09-15; it replaces two earlier rules, both of which keyed on the wrong thing:
264 // * 1.0.0: red-black by default, V-cycle only BELOW 65536 cells/rank and only at np > 1 --
265 // the sign of the block-size effect was backwards (the V-cycle's margin is LARGEST on big
266 // blocks: 2.23x at 147k cells/rank, 2.19x on one H100 at 56.6 M).
267 // * the same day, briefly: V-cycle always, for any configuration carrying a solid -- which
268 // made the solver depend on whether an immersed body happened to be present, even though
269 // IBM is also just a way of sculpting geometry and the momentum equation is the same one
270 // either way.
271 //
272 // The momentum operator is a screened Helmholtz whose condition number is a property of the
273 // TIMESTEP, not of the mesh or the geometry:
274 // kappa = lambda_max / lambda_min = 1 + 4 dt mu (w_x + w_y + w_z) / rho,
275 // which is 1 + 12 D on an isotropic grid in the diffusion number D = mu dt / (rho h^2), and
276 // stays correct on an anisotropic one because it reads the metric. A smoother removes error at
277 // a rate set by kappa, so it needs O(kappa) sweeps; the V-cycle does not.
278 //
279 // Measured crossover (96^3 and 64^3, channel with domain BCs and no solid, and the 384^3
280 // cut-cell bed): below kappa ~ 13 the smoother converges in a few tens of sweeps and the
281 // V-cycle's hierarchy is pure overhead (0.86x at D = 0.25); above it the V-cycle pulls away
282 // (1.88x at D = 4) and -- the reason this is a correctness threshold and not a tuning one --
283 // red-black stops MEETING ITS TOLERANCE, hitting velIters_ and returning a residual of 6.8e-08
284 // at D = 6 and 7.7e-06 at D = 12 against a 1e-10 target.
285 //
286 // The rule therefore names no geometry at all. It declines only for cause: an explicit
287 // set_velocity_multigrid() always wins; an operator mode the velocity MG is not validated for;
288 // and a per-rank block too short to coarsen, where the V-cycle degenerates to its bottom
289 // smoother and only adds setup.
290 if (vmgExplicit_ || vmgAutoCells_ == 0)
291 return;
292 const bool eligible = !varProps_ && !varRho_ && !hasDrag_ && !porous_ && !Grid::collocated &&
293 (!hasBc_ || hasSolid_ || !implicitFou_);
294 // Gershgorin condition number of the implicit-diffusion operator, from the metric the solver
295 // will actually use. dt, mu and rho are final here: the decision is taken at the head of the
296 // first step(), never at set_solid time, precisely so that a set_dt() after set_solid cannot
297 // leave the choice stale.
298 const double kappa =
299 1.0 + 4.0 * dt_ * mu_ * (u_.w[0] + u_.w[1] + u_.w[2]) / (rho_ > 0.0 ? rho_ : 1.0);
300 int minExt = std::min({e_.x - 2 * G, e_.y - 2 * G, e_.z - 2 * G});
301 // Serial (or a non-MPI build): the local block IS the global grid.
302 double global = (double)(e_.x - 2 * G) * (e_.y - 2 * G) * (e_.z - 2 * G);
303 double perRank = global;
304#ifdef PECLET_FLOW_MPI
305 if (distributed_) {
306 int np = 1, g = minExt;
309 minExt = g;
310 global = (double)gnx_ * gny_ * gnz_;
311 perRank = global / (double)np;
312 }
313#endif
314 useVelocityMg_ = eligible && kappa >= vmgAutoMinCond_ && minExt >= vmgAutoMinExtent_ &&
315 global >= (double)vmgAutoMinGlobal_ && perRank < (double)vmgAutoCells_;
316 if (useVelocityMg_) {
317 vmgLevels_ = 3;
318 vmgVcycles_ = 40;
319 }
320}
321
322template <class Grid>
324 // Nothing here needs the solid: the hierarchy is a property of the grid, the metric and the
325 // boundary conditions. It lived under set_solid only because that was the one caller, which is
326 // why enabling the velocity MG on a configuration WITHOUT an immersed solid used to build no
327 // levels and segfault on the first solve (pre-existing; fixed 2026-09-15).
328 if (useVelocityMg_ &&
329 vmg_.levels() == 0) { // velocity-MG hierarchy: IBM (staircase/upwind), domain-BC
330 // (const-coeff) or mixed (staircase + folds) mode
331 // The per-axis metric BEFORE the hierarchy is built (doc/anisotropic_metric.md trap 5):
332 // every level's b_a^L = mu' * w_a / cfac_a^2, and C3's aspect-ratio level rule reads it too.
333 vmg_.setMetric(u_.w, u_.hp);
334#ifdef PECLET_FLOW_MPI
335 // Distributed: level 0 on the solver's own decomposition (the g=2 velocity block), coarse
336 // levels coarsened in place with the even-block gate (no telescoping here yet -- measured
337 // first, see docs/SCALING_ISSUES.md issue 5).
338 if (distributed_)
339 vmg_.initMpi(*dec_, vmgLevels_, comm_);
340 else
341#endif
342 vmg_.init(nx_, ny_, nz_, vmgLevels_);
343 if (hasBc_)
344 vmg_.setBC(bc_);
345 if (!hasBc_ || hasSolid_) { // the staircase (IBM / mixed) paths classify by volume fraction
346 vmgTheta_ = CCField("vmgTheta", n_);
347 vmgClean_ = CCField("vmgClean", n_);
348 }
349 }
350}
351
352template <class Grid>
354 // Phase 2 C2 (doc/anisotropic_metric.md §4.5, trap 7): buildOpenness has taken dx,dy,dz
355 // all along and was handed 1.0 — `ccFractionCore` (the order-1 aperture model) forms the
356 // gradient (s+ - s-)/(2 dx) and the face extent |n_b| dy + |n_c| dz on the index lattice, so
357 // the right metric is h_a' (the marching-squares path, order 2, ignores them).
358 buildOpenness(ox_, oy_, oz_, CCConst(sdf_), e_, u_.hp[0], u_.hp[1], u_.hp[2],
359 apertureOrder_); // on the g=2 velocity block
360 if (hasOpenOverride_) {
361 // Analytic-SDF exact apertures (setOpennessOverride): overwrite the sampled-SDF openness
362 // with the user-provided inner fields + periodic wrap into the ghost ring (single-rank).
363 const std::vector<double>* src[3] = {&oxOverride_, &oyOverride_, &ozOverride_};
364 CCField dst[3] = {ox_, oy_, oz_};
365 for (int f = 0; f < 3; ++f) {
366 CCField din("peclet::flow::openOv_d", (std::size_t)nx_ * ny_ * nz_);
367 Kokkos::deep_copy(
368 din,
369 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
370 src[f]->data(), src[f]->size()));
372 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
373 CCField o = dst[f];
374 Kokkos::parallel_for(
375 "peclet::flow::open_override_wrap", MDRange3<CCExec>(space, {0, 0, 0}, {ex, ey, ez}),
376 KOKKOS_LAMBDA(int x, int y, int z) {
377 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
378 iz = (((z - g) % nz) + nz) % nz;
379 o((long)x + (long)y * ex + (long)z * (long)ex * ey) = din(
380 (std::size_t)ix + (std::size_t)iy * nx + (std::size_t)iz * (std::size_t)nx * ny);
381 });
382 space.fence();
383 }
384 }
385 if constexpr (Grid::collocated) { // static open-centroid wall distances (wall-aware map)
386 buildFaceCentroidDist(xcx_, xcy_, xcz_, CCConst(sdf_), e_);
387 buildCellFraction(cs_, CCConst(sdf_), e_, G); // cell fluid fraction (embed)
388 if (faceInterp_ >= 5 && faceInterp_ <= 7) { // EMBED: a solid-CENTRED cut cell (cs>0) is
389 // partially fluid and holds
390 // its reconstructed near-wall velocity — masking it to 0 (the sdf<0 IBM mask) drops the
391 // near-wall closure and shifts the whole channel. Re-mask from cs: pin ONLY fully-solid
392 // cells (cs≈0), keeping every partial-fluid cut cell live in the embed solve +
393 // projection.
394 CCConst cs = CCConst(cs_);
395 const std::size_t nn = n_;
396 for (int c = 0; c < 3; ++c) {
397 CCField m = C[c].mask;
398 Kokkos::parallel_for(
399 "peclet::flow::embed_solid_mask", Kokkos::RangePolicy<CCExec>(0, nn),
400 KOKKOS_LAMBDA(std::size_t i) { m(i) = cs(i) < 1e-6 ? 1.0 : 0.0; });
401 }
402 }
403 }
404 if (fluidOnlyMode_ == 1) {
405 // Mode-14a FLUID-ONLY constraint (setFluidOnlyConstraint): close every face with a
406 // solid-CENTERED side in the openness the pressure stack consumes. The aperture operator,
407 // the divergence, the face correction and the MG rediscretization all read these fields,
408 // so one filter makes constraint/operator/correction consistent by construction: pressure
409 // DOFs decouple at solid-centered cells (their rows go empty like solid cells), the
410 // invisible multiplier subspace of collocated_invisible_subspace.md S4 ceases to exist,
411 // and the operator stays SPD + 7-point (CG + CutcellMG untouched). Closure quality is
412 // Neumann-zero at the closed faces (the crude end of the fluid-only family -- measured,
413 // not assumed); the consistent-closure variants ride on the gp row machinery instead.
414 CCConst sd = CCConst(sdf_);
415 CCField oa[3] = {ox_, oy_, oz_};
416 C3 e = e_;
417 for (int a = 0; a < 3; ++a) {
418 CCField o = oa[a];
419 const long sa = (a == 0) ? 1 : (a == 1) ? (long)e.x : (long)e.x * e.y;
420 Kokkos::parallel_for(
421 "peclet::flow::fluid_only_openness",
422 MDRange3<CCExec>(CCExec(), {1, 1, 1}, {e.x, e.y, e.z}),
423 KOKKOS_LAMBDA(int x, int y, int z) {
424 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
425 if (sd(i) < 0.0 || sd(i - sa) < 0.0)
426 o(i) = 0.0;
427 });
428 }
429 }
430#ifdef PECLET_FLOW_MPI
431 // openness ghosts (the operator + divergence read the +neighbour face) -> exchange across
432 // ranks
433 if (distributed_) {
434 velDev_->exchange(ox_);
435 velDev_->exchange(oy_);
436 velDev_->exchange(oz_);
437 // SCALING_ISSUES #3: the HIGH domain face of each axis sits at a GHOST index, so the exchange
438 // just overwrote it with the periodic wrap of the opposite boundary -- on a rank owning that
439 // global face, another boundary's aperture. Re-derive it from the SDF. (The LOW face is an
440 // inner index and survives.) Invisible until this fix, because the Dirichlet row overwrote the
441 // value with the literal 1.0, and a bed clear of the open faces wraps 1.0 onto 1.0 anyway.
442 CCField oa[3] = {ox_, oy_, oz_};
443 for (int a = 0; a < 3; ++a)
444 if (bc_[2 * a + 1] != 0 && touchesGlobalFace(2 * a + 1))
445 buildOpennessHighFace(oa[a], CCConst(sdf_), e_, G, a, u_.hp[0], u_.hp[1], u_.hp[2],
446 apertureOrder_);
447 }
448#endif
449 if (hasBc_) { // FLUX openness (beta): a face is OPEN only where it carries normal flux --
450 // outflow, or
451 B3 e2{e_.x, e_.y, e_.z};
452 CCField oa[3] = {ox_, oy_, oz_}; // an inflow with nonzero normal velocity. Walls
453 for (int a = 0; a < 3; ++a)
454 for (int s = 0; s < 2; ++s) { // and tangential-only Dirichlet faces (e.g. a
455 const int t = bc_[2 * a + s]; // lid: type 2 with zero normal vel) are CLOSED.
456 const bool open = (t == 3) || (t == 2 && (bcProf_[2 * a + s].extent(0) > 0 ||
457 std::fabs(bcVel_[2 * a + s][a]) > 1e-12));
458 if (t != 0 && !open && touchesGlobalFace(2 * a + s))
459 bcZeroOpenness(oa[a], e2, G, a, s); // rank-owned global face only
460 }
461 } // the MG re-derives the OPERATOR openness alpha (inflow Neumann -> closed) per level via
462 // setBC.
463 if (hasBc_)
465 copyInner(ox1_, e1_, 1, CCConst(ox_), e_, G); // bridge openness g=2 -> g=1 for the MG
466 copyInner(oy1_, e1_, 1, CCConst(oy_), e_, G);
467 copyInner(oz1_, e1_, 1, CCConst(oz_), e_, G);
468}
469
470// The two sides of an axis are ASYMMETRIC in the face-index convention (mac_cutcell_mg.hpp): the
471// LOW domain face is the INNER index that `copyInner` above already wrote, the HIGH one is the
472// first GHOST index, which no kernel writes and the MG's periodic fill wraps over. At a Dirichlet
473// OUTFLOW face the MG now carries the true cut-cell aperture instead of the literal 1.0 (the WO-R2
474// item 1 save/restore/coarsen machinery, generalized from the variable-density coefficient to the
475// openness itself), so the g=1 bridge has to carry that plane across too.
476template <class Grid>
478 if (!hasOutflow_)
479 return;
480 const bool gp = ghostProjection_ && oxb_.extent(0) > 0; // the MG rails carry the BINARY
481 CCField dst[3] = {ox1_, oy1_, oz1_}; // openness in ghost mode
482 CCField src[3] = {gp ? oxb_ : ox_, gp ? oyb_ : oy_, gp ? ozb_ : oz_};
483 const int de[3] = {e1_.x, e1_.y, e1_.z}, se[3] = {e_.x, e_.y, e_.z};
484 const long dst3[3] = {1, (long)e1_.x, (long)e1_.x * e1_.y};
485 const long sst3[3] = {1, (long)e_.x, (long)e_.x * e_.y};
486 for (int a = 0; a < 3; ++a) {
487 if (bc_[2 * a + 1] != 3 || !touchesGlobalFace(2 * a + 1))
488 continue;
489 const int b = (a + 1) % 3, c = (a + 2) % 3;
490 const long dsa = dst3[a], dsb = dst3[b], dsc = dst3[c];
491 const long ssa = sst3[a], ssb = sst3[b], ssc = sst3[c];
492 const long dbf = de[a] - 1, sbf = se[a] - G; // the high boundary face on each block
493 const int nb = de[b] - 2, nc = de[c] - 2; // inner extent of the g=1 block
494 CCField d = dst[a];
495 CCConst sv = CCConst(src[a]);
496 Kokkos::parallel_for(
497 "peclet::flow::bridge_outflow_plane", MDRange2<CCExec>(CCExec(), {0, 0}, {nb, nc}),
498 KOKKOS_LAMBDA(int p0, int p1) {
499 d((long)(p0 + 1) * dsb + (long)(p1 + 1) * dsc + dbf * dsa) =
500 sv((long)(p0 + G) * ssb + (long)(p1 + G) * ssc + sbf * ssa);
501 });
502 }
503}
504
505// SCALING_ISSUES #3, the configuration the fix does NOT make solvable. A cell whose pressure row
506// is entirely closed (every one of its six OPERATOR apertures is 0) but which a prescribed inflow
507// face still feeds is an INCONSISTENT row: the row says "nothing may leave this cell", the
508// constraint says "this much enters", and no pressure satisfies both. MG-PCG cannot converge — it
509// runs to its iteration cap with max|div| stuck at exactly the prescribed influx — so the
510// geometry, not the solver, has to be rejected. It means a pocket of fluid that the solid seals
511// off from the rest of the domain and that opens only onto the inlet, which is a physically
512// impossible thing to ask of an incompressible fluid whatever the discretization.
513//
514// Before the ghost-extension fix above this fired on ordinary geometry, because a SOLID cell
515// against the inlet was handed a fully open inflow face by the periodic wrap. It now takes a
516// genuinely sealed pocket.
517template <class Grid>
519 const int dims[3] = {e_.x, e_.y, e_.z};
520 const long st[3] = {1, (long)e_.x, (long)e_.x * e_.y};
521 int bct[6];
522 int own[6];
523 for (int f = 0; f < 6; ++f) {
524 bct[f] = bc_[f];
525 own[f] = touchesGlobalFace(f) ? 1 : 0;
526 }
527 long sealedOn[6] = {0, 0, 0, 0, 0, 0};
528 for (int a = 0; a < 3; ++a)
529 for (int s = 0; s < 2; ++s) {
530 if (bct[2 * a + s] != 2 || !own[2 * a + s])
531 continue;
532 const int b = (a + 1) % 3, c = (a + 2) % 3;
533 const long sa = st[a], sb = st[b], sc = st[c];
534 const long base = (long)((s == 0) ? G : dims[a] - G - 1) * sa;
535 const long inflowFace = (s == 0) ? 0 : sa;
536 const int nb = dims[b] - 2 * G, nc = dims[c] - 2 * G;
537 CCConst oc[3] = {CCConst(ox_), CCConst(oy_), CCConst(oz_)};
538 const int dd[3] = {dims[0], dims[1], dims[2]};
539 const long ss[3] = {st[0], st[1], st[2]};
540 const int cA = a, cB = b, cC = c, cS = s;
541 long sealed = 0;
542 Kokkos::parallel_reduce(
543 "peclet::flow::sealed_inflow", MDRange2<CCExec>(CCExec(), {0, 0}, {nb, nc}),
544 KOKKOS_LAMBDA(int pb, int pc, long& acc) {
545 const long i = base + (long)(pb + G) * sb + (long)(pc + G) * sc;
546 if (oc[cA](i + inflowFace) <= 0.0)
547 return; // the inflow face carries no flux into this cell
548 int idx[3]; // the cell's index along each axis: a global face is an extreme one
549 idx[cA] = (cS == 0) ? G : (dd[cA] - G - 1);
550 idx[cB] = pb + G;
551 idx[cC] = pc + G;
552 double alpha = 0.0;
553 for (int a2 = 0; a2 < 3; ++a2)
554 for (int s2 = 0; s2 < 2; ++s2) {
555 const bool global = own[2 * a2 + s2] != 0 &&
556 ((s2 == 0) ? (idx[a2] == G) : (idx[a2] == dd[a2] - G - 1));
557 const int t = bct[2 * a2 + s2];
558 if (global && (t == 1 || t == 2 || t == 4))
559 continue; // Neumann domain face: the operator closes it whatever the aperture
560 alpha += oc[a2](i + ((s2 == 0) ? 0 : ss[a2]));
561 }
562 if (alpha <= 0.0)
563 ++acc;
564 },
565 sealed);
566 sealedOn[2 * a + s] = sealed;
567 }
568#ifdef PECLET_FLOW_MPI
569 if (distributed_) { // ONE reduction for all six faces: set_solid is per-step on moving geometry
570 long g[6];
572 for (int f = 0; f < 6; ++f)
573 sealedOn[f] = g[f];
574 }
575#endif
576 for (int f = 0; f < 6; ++f) {
577 if (sealedOn[f] == 0)
578 continue;
579 static const char* kFace[6] = {"-x", "+x", "-y", "+y", "-z", "+z"};
580 throw std::runtime_error(
581 std::string("set_solid: the immersed solid seals ") + std::to_string(sealedOn[f]) +
582 " fluid cell(s) against the " + kFace[f] +
583 " INFLOW face off from the rest of the domain: their pressure row is entirely closed "
584 "yet the inflow still feeds them, which no pressure field can satisfy (the pressure "
585 "solve would run to its iteration cap with max|div| stuck at the inflow velocity). "
586 "Either pull the solid clear of that face, refine the grid so the pocket connects, or "
587 "make the face a wall. See doc/cutcell_openbc_convergence.md.");
588 }
589}
590
591template <class Grid>
593 if (fluidOnlyMode_ == 2) {
594 // Design B: the MG hierarchy is built from the FILTERED openness (Design A's operator,
595 // the symmetric surrogate preconditioner + the 7-point part of the true operator); the
596 // geometric ox_/oy_/oz_ stay ORIGINAL for the divergence and the face correction. Filter
597 // the g=1 bridge in place, then build the star overlay from the original apertures.
598 if (porous_ || varRho_ || hasBc_ || ghostProjection_ || distributed_ || !Grid::collocated)
599 throw std::runtime_error(
600 "set_fluid_only_constraint(2): v1 is single-rank periodic collocated only");
602 CCConst sd = CCConst(sdf_);
603 CCField oa1[3] = {ox1_, oy1_, oz1_};
604 const C3 e1 = e1_, e2 = e_;
605 for (int a = 0; a < 3; ++a) {
606 CCField o1 = oa1[a];
607 const long sa2 = (a == 0) ? 1 : (a == 1) ? (long)e2.x : (long)e2.x * e2.y;
608 Kokkos::parallel_for(
609 "peclet::flow::star_filter_bridge", MDRange3<CCExec>(space, {0, 0, 0}, {nx_, ny_, nz_}),
610 KOKKOS_LAMBDA(int x, int y, int z) {
611 const long i2 =
612 (long)(x + G) + (long)(y + G) * e2.x + (long)(z + G) * (long)e2.x * e2.y;
613 if (sd(i2) < 0.0 || sd(i2 - sa2) < 0.0)
614 o1((long)(x + 1) + (long)(y + 1) * e1.x + (long)(z + 1) * (long)e1.x * e1.y) = 0.0;
615 });
616 }
617 space.fence();
618 starCounter_ = Kokkos::View<int, CCMem>("star_counter");
619 const C3 nn{nx_, ny_, nz_};
620 nStar_ = buildStarOverlay(CCConst(sdf_), CCConst(ox_), CCConst(oy_), CCConst(oz_), e_, G, nn,
621 StarOverlay{}, starCounter_);
622 starOv_ = starMakeOverlay(std::max(nStar_, 1));
623 buildStarOverlay(CCConst(sdf_), CCConst(ox_), CCConst(oy_), CCConst(oz_), e_, G, nn, starOv_,
624 starCounter_);
625 }
626}
627
628template <class Grid>
630 // A few setup paths below (the ghost-projection pocket decoupling) are host-side
631 // connected-component analyses and genuinely need the inner SDF on the host. Materialise it
632 // ONCE, lazily, so the common path keeps the device-resident benefit.
633 std::vector<double> sdfInnerHost_;
634 auto sdfInner_ = [&]() -> const std::vector<double>& {
635 if (sdfInnerHost_.empty()) {
636 const std::size_t nI = (std::size_t)nx_ * ny_ * nz_;
637 sdfInnerHost_.resize(nI);
638 Kokkos::deep_copy(
639 Kokkos::View<double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
640 sdfInnerHost_.data(), nI),
641 din);
642 }
643 return sdfInnerHost_;
644 };
645 if (ghostProjection_) {
646 // Directional ghost-cell projection: build the closure overlay + the binary (COUPLED)
647 // openness. The binary field replaces the geometric openness on the MG rails (the MG
648 // hierarchy becomes the symmetric surrogate preconditioner; the overlay delta enters only
649 // the fine-level BiCGStab matvec). The geometric ox_/oy_/oz_ above stay for diagnostics.
650 if (porous_ || varRho_ || hasBc_)
651 throw std::runtime_error(
652 "ghost projection: incompatible with porous/variable-rho/domain-BC (v1)");
653 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
654 gpOv_ = gpMakeOverlay<MReal>((long)nInner); // worst-case sizing, like the momentum overlay
655 gpIdMap_ = Kokkos::View<int*, CCMem>("gp_idmap", nInner);
656 gpCounter_ = Kokkos::View<int, CCMem>("gp_counter");
657 oxb_ = CCField("oxb", n_);
658 oyb_ = CCField("oyb", n_);
659 ozb_ = CCField("ozb", n_);
660 gpRh_ = CCField("gpRh", n1_);
661 gpT_ = CCField("gpT", n1_);
662 gpZ2_ = CCField("gpZ2", n1_);
663 if (distributed_)
664 gpX2_ = CCField("gpXg2", n_); // g=2 staging block for the distributed BiCGStab matvec
665 // Fragmentation guard: the binary COUPLED-face condition is stricter than aperture
666 // connectivity, so tight-throat geometries (e.g. a random close packing with touching
667 // spheres) fragment the fluid graph into a main component + tiny pockets at the
668 // contacts. Each pocket adds its own null vector that the single global mean-removal
669 // cannot handle, and BiCGStab breaks down (measured: fields to ~1e152 on the RCP
670 // example). Host BFS over the coupled graph of the INNER sdf; fluid cells outside the
671 // largest component are treated as SOLID for the PROJECTION ONLY (sdfGp), decoupling
672 // their rows; the momentum step keeps the true sdf.
673 // Distributed: connectivity is a GLOBAL property (a pocket can span rank boundaries) and
674 // every rank must agree on the main component, so allgather the inner sdf, run the
675 // deterministic guard on the global grid identically on every rank, and keep this
676 // rank's block of the result.
677 std::vector<double> sdfGpHost;
678 {
679 std::vector<double> work;
680 int fx = nx_, fy = ny_, fz = nz_;
681 bool verbose = true;
682#ifdef PECLET_FLOW_MPI
683 int myRank = 0;
684 if (distributed_) {
686 int nRanks = 1;
688 fx = gnx_;
689 fy = gny_;
690 fz = gnz_;
691 verbose = myRank == 0;
692 std::vector<int> cnts(nRanks), disp(nRanks);
693 long acc = 0;
694 for (int r = 0; r < nRanks; ++r) {
695 const auto b = dec_->block(r);
696 cnts[r] = (int)(b.size[0] * b.size[1] * b.size[2]);
697 disp[r] = (int)acc;
698 acc += cnts[r];
699 }
700 std::vector<double> flat((std::size_t)acc);
701 MPI_Allgatherv(sdfInner_().data(), (int)sdfInner_().size(), MPI_DOUBLE, flat.data(),
702 cnts.data(), disp.data(), MPI_DOUBLE, comm_);
703 work.assign((std::size_t)fx * fy * fz, 0.0);
704 for (int r = 0; r < nRanks; ++r) {
705 const auto b = dec_->block(r);
706 const double* src = flat.data() + disp[r];
707 for (int z = 0; z < (int)b.size[2]; ++z)
708 for (int y = 0; y < (int)b.size[1]; ++y)
709 for (int x = 0; x < (int)b.size[0]; ++x)
710 work[(std::size_t)(x + b.origin[0]) + (std::size_t)(y + b.origin[1]) * fx +
711 (std::size_t)(z + b.origin[2]) * (std::size_t)fx * fy] =
712 src[(std::size_t)x + (std::size_t)y * b.size[0] +
713 (std::size_t)z * (std::size_t)b.size[0] * b.size[1]];
714 }
715 } else
716#endif
717 work = sdfInner_();
718 const std::size_t nTot = work.size();
719 const int nx = fx, ny = fy, nz = fz;
720 auto id = [&](int x, int y, int z) {
721 return (std::size_t)((x + nx) % nx) + (std::size_t)((y + ny) % ny) * nx +
722 (std::size_t)((z + nz) % nz) * (std::size_t)nx * ny;
723 };
724 std::vector<int> comp(nTot, -1);
725 std::vector<std::size_t> stack;
726 int ncomp = 0, mainComp = -1;
727 std::size_t mainSize = 0, nActive = 0;
728 for (std::size_t seed = 0; seed < nTot; ++seed) {
729 if (comp[seed] >= 0 || work[seed] < 0.0)
730 continue;
731 std::size_t size = 0;
732 comp[seed] = ncomp;
733 stack.assign(1, seed);
734 while (!stack.empty()) {
735 const std::size_t c = stack.back();
736 stack.pop_back();
737 ++size;
738 const int x = (int)(c % nx), y = (int)((c / nx) % ny),
739 z = (int)(c / ((std::size_t)nx * ny));
740 const int nb[6][3] = {{x - 1, y, z}, {x + 1, y, z}, {x, y - 1, z},
741 {x, y + 1, z}, {x, y, z - 1}, {x, y, z + 1}};
742 for (auto& q : nb) {
743 const std::size_t j = id(q[0], q[1], q[2]);
744 if (comp[j] >= 0 || work[j] < 0.0)
745 continue;
746 // COUPLED face: mean-of-centers face sdf fluid AND both centers fluid
747 if (0.5 * (work[c] + work[j]) < 0.0)
748 continue;
749 comp[j] = ncomp;
750 stack.push_back(j);
751 }
752 }
753 if (size > mainSize) {
754 mainSize = size;
755 mainComp = ncomp;
756 }
757 nActive += size;
758 ++ncomp;
759 }
760 if (ncomp > 1) {
761 std::size_t pockets = 0;
762 for (std::size_t i = 0; i < nTot; ++i)
763 if (work[i] >= 0.0 && comp[i] != mainComp) {
764 work[i] = -(std::abs(work[i]) * 1.001 + 1e-30);
765 ++pockets;
766 }
767 if (verbose)
768 printf(
769 "peclet::flow ghost projection: %d fluid components; decoupled %zu pocket "
770 "cells outside the main component (%zu of %zu fluid cells)\n",
772 }
773#ifdef PECLET_FLOW_MPI
774 if (distributed_) {
775 const auto b = dec_->block(myRank);
776 sdfGpHost.resize(sdfInner_().size());
777 for (int z = 0; z < nz_; ++z)
778 for (int y = 0; y < ny_; ++y)
779 for (int x = 0; x < nx_; ++x)
780 sdfGpHost[(std::size_t)x + (std::size_t)y * nx_ +
781 (std::size_t)z * (std::size_t)nx_ * ny_] =
782 work[(std::size_t)(x + b.origin[0]) + (std::size_t)(y + b.origin[1]) * fx +
783 (std::size_t)(z + b.origin[2]) * (std::size_t)fx * fy];
784 } else
785#endif
786 sdfGpHost = std::move(work);
787 }
788 sdfGp_ = CCField("peclet::flow::sdfGp", n_);
789 CCField sdfGp = sdfGp_; // the projection's sdf view (pockets decoupled); persisted for
790 // the collocated gpCenterGrad predictor/correction
791#ifdef PECLET_FLOW_MPI
792 if (distributed_) {
793 // local inner block + halo exchange (cross-rank + periodic), same as the sdf_ upload:
794 // the overlay build reads sdfGp ghosts up to +/-2 = G across block boundaries.
795 auto h = Kokkos::create_mirror_view(sdfGp_);
796 Kokkos::deep_copy(h, sdfGp_);
797 for (int z = 0; z < nz_; ++z)
798 for (int y = 0; y < ny_; ++y)
799 for (int x = 0; x < nx_; ++x)
800 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
801 sdfGpHost[(std::size_t)x + (std::size_t)y * nx_ +
802 (std::size_t)z * (std::size_t)nx_ * ny_];
803 Kokkos::deep_copy(sdfGp_, h);
804 velDev_->exchange(sdfGp_);
805 } else
806#endif
807 { // upload + periodic wrap (same pattern as the sdf upload above)
808 CCField din("peclet::flow::sdfGpInner_d", nInner);
809 Kokkos::deep_copy(
810 din,
811 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
812 sdfGpHost.data(), sdfGpHost.size()));
814 const int ex = e_.x, ey = e_.y, ez = e_.z, nx = nx_, ny = ny_, nz = nz_, g = G;
815 Kokkos::parallel_for(
816 "peclet::flow::sdfgp_wrap", MDRange3<CCExec>(space, {0, 0, 0}, {ex, ey, ez}),
817 KOKKOS_LAMBDA(int x, int y, int z) {
818 const int ix = (((x - g) % nx) + nx) % nx, iy = (((y - g) % ny) + ny) % ny,
819 iz = (((z - g) % nz) + nz) % nz;
820 sdfGp((long)x + (long)y * ex + (long)z * (long)ex * ey) = din(
821 (std::size_t)ix + (std::size_t)iy * nx + (std::size_t)iz * (std::size_t)nx * ny);
822 });
823 space.fence();
824 }
825 gpBinaryOpenness(oxb_, oyb_, ozb_, CCConst(sdfGp), e_);
826 gpNRows_ = buildGpOverlay(CCConst(sdfGp), e_, G, C3{nx_, ny_, nz_}, gpOv_, gpIdMap_, gpCounter_,
827 gpMatrixOrder_, gpRhsOrder_,
828 hasExactCross_ ? CCConst(tEx_[0][0]) : CCConst(),
829 hasExactCross_ ? CCConst(tEx_[1][1]) : CCConst(),
830 hasExactCross_ ? CCConst(tEx_[2][2]) : CCConst(),
831 /*useGhost=*/distributed_);
832 if (gpDebugLevel() > 0) { // PECLET_FLOW_GP_DEBUG row forensics (analysis only)
833 int gpDbgRank = 0;
834#ifdef PECLET_FLOW_MPI
835 if (distributed_)
837#endif
838 gpDebugReport(gpOv_, gpNRows_, C3{nx_, ny_, nz_}, gpIdMap_, gpDbgRank);
839 }
840 copyInner(ox1_, e1_, 1, CCConst(oxb_), e_, G); // MG surrogate = binary openness
841 copyInner(oy1_, e1_, 1, CCConst(oyb_), e_, G);
842 copyInner(oz1_, e1_, 1, CCConst(ozb_), e_, G);
843 }
844}
845
846template <class Grid>
848 bridgeOutflowFacePlanes(); // after every writer of the g=1 openness rails (ghost-mode surrogate
849 // included), before the hierarchy reads them
850 mg_.setBoundaryConditions(bc_); // per-level wall openness + null-space gating (no-op if
851 // periodic); BEFORE initMpi — the per-level ghost width
852 // (CA smoothing) is chosen for the periodic operator only
853 // Phase 2 C3 (doc/anisotropic_metric.md §5, trap 5): the per-axis spacings BEFORE the level
854 // table is built — the aspect-ratio rule defers an axis that is already theta times coarser
855 // than the finest coarsenable one. Exactly (1,1,1) on the isotropic path, where the rule
856 // returns today's decision verbatim.
857 mg_.setMetric(u_.hp);
858#ifdef PECLET_FLOW_MPI
859 if (distributed_) { // share the level-0 decomposition so the MG block matches this rank's
860 // block; a weighted one (rebalanceByWeights) gets Repartition stages
861 mg_.setRepartition(weightedDec_);
862 mg_.initMpi(gnx_, gny_, gnz_, nLevels_, comm_, dec_.get());
863 } else
864#endif
865 mg_.init(nx_, ny_, nz_,
866 nLevels_); // geometric multigrid on the cut-cell openness (MG-PCG pressure)
867 bfpChebSet_ =
868 false; // a new hierarchy: the balanced-force pre-projection re-estimates its bounds
869 // Phase 2 C2 (doc/anisotropic_metric.md §1.3/§3): the per-axis pressure weight
870 // w_a = 1/h_a'^2 IS `setOpenness`'s idx2/idy2/idz2 (the coarse levels already form
871 // w_a/cfac_a^2). Exactly 1.0 on the isotropic path.
872 // SCALING_ISSUES #3: a Dirichlet (outflow) domain face row carries the face's own cut-cell
873 // APERTURE, not the literal openness 1.0. With a solid cutting the outlet the two differ, and
874 // the operator then solved a different constraint from the one the divergence imposes: the
875 // projection pushed flux through a face that carries none (measured max|div| 1.5e-2 against
876 // 4.7e-4 for the same bed pulled clear). `set_outflow_operator_coefficient(False)` is the
877 // ablation back to the literal 1.0; a bed clear of the open faces is byte-identical either way.
878 mg_.setOutflowCoefficient(hasOutflow_ && outflowOpCoeff_);
879 mg_.setOpenness(CCConst(ox1_), CCConst(oy1_), CCConst(oz1_), u_.w[0], u_.w[1], u_.w[2]);
880 // Coarse-solve policy: an explicit set_pressure_graph_amg(True) forces agglomeration,
881 // otherwise the mode set by set_pressure_bottom (default auto) decides.
882 mg_.setAgglomerationMode(pressGraphAmg_ ? 1 : pressAgglomMode_);
883 Kokkos::deep_copy(phi_, 0.0);
884 Kokkos::deep_copy(P_, 0.0);
885}
886
887template <class Grid>
889 return geometryBuilt_;
890}
891
892template <class Grid>
893void Solver<Grid>::requireNoGeometry(const char* who) const {
894 if (geometryBuilt_)
895 throw std::runtime_error(
896 std::string(who) +
897 ": call BEFORE the geometry (set_solid / set_pressure_geometry / set_solid_from_scene) "
898 "-- this setting is folded into the operators when the geometry is built, so a later "
899 "call would be silently ignored");
900}
901
902} // namespace peclet::flow
903
904#endif // PECLET_FLOW_FLOW_IBM_GEOMETRY_HPP
void setOutflowCoefficient(bool on)
void setMetric(const double hp[3])
Per-axis cell spacings h_a' = h_a / hRef of the physical domain (doc/anisotropic_metric....
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
void setBoundaryConditions(const int bc[6])
void setRepartition(bool on)
Repartition stages for a weighted level-0 decomposition (see repartition_); before initMpi.
void setAgglomerationMode(int mode)
void init(int nx, int ny, int nz, int nLevels)
void setSolidUploadSdf(CCField din)
void extendSdfDomainGhosts(CCField f)
Repair a cell-centred geometry field's ghost band on every rank-owned NON-PERIODIC domain face,...
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
void bridgeOutflowFacePlanes()
Bridge the HIGH-side boundary-face openness plane of every rank-owned outflow axis from the g=2 block...
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
void requireNoGeometry(const char *who) const
void exchangeExtRaw(CCField f)
Raw ghost fill of an extended-block field: the rank halo exchange under MPI, the periodic wrap single...
void setSolidGhostProjectionOverlay(CCField din)
void buildVelocityOverlays(bool resetU)
Build the three per-component Robust-Scaled cut-cell overlays + solid masks from the CURRENT sdf_ (ex...
void setPressureGeometry(const std::vector< double > &sdfInner)
static constexpr int G
Definition flow_ibm.hpp:80
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 checkSealedInflowCells()
Reject geometry that seals fluid cells against a prescribed INFLOW face off from the rest of the doma...
void setBC(const int bc[6])
void init(int nx, int ny, int nz, int nLevels)
void setMetric(const double w[3], const double hp[3])
Per-axis metric of the physical domain (doc/anisotropic_metric.md §1.1): the weights w_a = 1/h_a'^2,...
void buildFaceCentroidDist(CCField xcx, CCField xcy, CCField xcz, CCConst sdf, C3 e)
void ibmSolidMask(CCField mask, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:249
void gpBinaryOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext)
Binary openness for the symmetric MG surrogate, on the extended-block layout of buildOpenness: o(face...
StarOverlay starMakeOverlay(long n)
void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g)
void bcZeroOpenness(BField oa, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:454
int buildStarOverlay(CCConst sdf, CCConst ox, CCConst oy, CCConst oz, C3 ext, int g, C3 nn, const StarOverlay &ov, Kokkos::View< int, CCMem > counter)
Count + fill the star overlay from the cell-centered sdf and the ORIGINAL (unfiltered) apertures on t...
int gpDebugLevel()
0 = off (default). Read once per call; cheap enough, and keeps the flag hot-swappable in tests.
MReal mreal
Definition mac_ibm.hpp:24
void buildOpennessHighFace(CCField oa, CCConst sdf, C3 ext, int g, int a, double dx, double dy, double dz, int order=1)
int buildGpOverlay(CCConst sdf, C3 ext, int g, C3 nn, const GpOverlayReal< Real > &ov, Kokkos::View< int *, CCMem > idMap, Kokkos::View< int, CCMem > counter, int matrixOrder=2, int rhsOrder=2, CCConst tx=CCConst(), CCConst ty=CCConst(), CCConst tz=CCConst(), bool useGhost=false)
Build the overlay over the inner grid nn from the cell-centered sdf on the extended block (ext,...
void bcMirrorGhost(BField f, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:169
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
void gpDebugReport(const GpOverlayReal< Real > &ov, int nRows, C3 nn, Kokkos::View< int *, CCMem > idMap, int rank=0)
Census + optional per-row dump of the built overlay.
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
void bcNeumannGhost(BField f, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:246
Kokkos::View< const double *, CCMem > CCConst
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
double hp[3]
h_a' = h_a / hRef (>= 1, exactly 1 on the finest axis)
Definition flow_ibm.hpp:178
One entry per eliminated solid-centered cell: packed INNER flat index + the apertures of its (up to 6...