flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_core.hpp
Go to the documentation of this file.
1
9#ifndef PECLET_FLOW_FLOW_IBM_CORE_HPP
10#define PECLET_FLOW_FLOW_IBM_CORE_HPP
11
12namespace peclet::flow {
13
14template <class Grid>
15Solver<Grid>::Solver(int nx, int ny, int nz) {
17}
18
19template <class Grid>
21 SceneMap m;
22 if (!u_.physical)
23 return m;
24 for (int a = 0; a < 3; ++a) {
25 m.a[a] = u_.org[a] + 0.5 * u_.h[a];
26 m.b[a] = u_.h[a];
27 m.velToInt[a] = u_.velToInt(a);
28 }
29 m.dToInt = u_.lenToInt();
30 m.angToInt = u_.angVelToInt();
31 return m;
32}
33
34template <class Grid>
35Solver<Grid>::Solver(int nx, int ny, int nz, const std::array<double, 3>& extent,
36 const std::array<double, 3>& origin, const std::array<long, 3>& globalCells) {
38 setPhysicalDomain(extent, origin, globalCells);
39}
40
41template <class Grid>
42void Solver<Grid>::setPhysicalDomain(const std::array<double, 3>& extent,
43 const std::array<double, 3>& origin,
44 const std::array<long, 3>& globalCells) {
45 const long c[3] = {globalCells[0] > 0 ? globalCells[0] : nx_,
46 globalCells[1] > 0 ? globalCells[1] : ny_,
47 globalCells[2] > 0 ? globalCells[2] : nz_};
48 for (int a = 0; a < 3; ++a) {
49 if (!(extent[a] > 0.0))
50 throw std::runtime_error("physical domain: every extent component must be > 0");
51 if (c[a] <= 0)
52 throw std::runtime_error("physical domain: every cell count must be > 0");
53 }
54 double hh[3];
55 for (int a = 0; a < 3; ++a)
56 hh[a] = extent[a] / (double)c[a];
57 // Phase 2 C2 — THE SNAP (doc/anisotropic_metric.md §1.4). Phase 1 refused any extent whose
58 // three spacings differed; this replaces the throw. If the three agree to 1e-12 relative the
59 // spacings are SNAPPED to h_0 on every axis, so the metric is EXACTLY the identity
60 // (hp = w = (1,1,1), vol = 1, aniso = false) — that exactness is what keeps every operator
61 // fold of C1/C2 bit-identical and what keeps `units_scale_invariance` on literally Phase 1's
62 // arithmetic, whose per-axis extents N_a*H can differ by an ulp across axes (trap 2).
63 // Otherwise the cells really are anisotropic: hRef = min_a h_a, and h_a' comes from the STORED
64 // doubles (trap 1 — never extent/(cells*hRef); h_a' is exactly 1.0 only on the finest axis).
65 // What that admits is §7 / `requireIsotropic` below; Phase 3 then admitted the VoF
66 // entry points too, so nothing in the solver refuses an anisotropic domain today.
67 bool iso = true;
68 for (int a = 1; a < 3; ++a)
69 if (std::fabs(hh[a] - hh[0]) > 1e-12 * std::fabs(hh[0]))
70 iso = false;
71 if (iso)
72 for (int a = 1; a < 3; ++a)
73 hh[a] = hh[0];
74 u_.physical = true;
75 for (int a = 0; a < 3; ++a) {
76 u_.h[a] = hh[a];
77 u_.ext[a] = extent[a];
78 u_.org[a] = origin[a];
79 u_.cells[a] = c[a];
80 }
81 u_.hRef = std::min(hh[0], std::min(hh[1], hh[2]));
82 for (int a = 0; a < 3; ++a) {
83 u_.hp[a] = iso ? 1.0 : u_.h[a] / u_.hRef;
84 u_.w[a] = iso ? 1.0 : 1.0 / (u_.hp[a] * u_.hp[a]);
85 }
86 u_.vol = iso ? 1.0 : u_.hp[0] * u_.hp[1] * u_.hp[2];
87 u_.aniso = !iso;
88 // rhoRef / tRef are pinned by the first set_rho / set_dt; whatever was set BEFORE the domain
89 // (nothing, in the documented order) is re-derived here.
91 // Phase 2 commit C4 LIFTED the collocated refusal that stood here: the embedded closures
92 // (`fvViscousApply`, `embedViscousApply` / `embedDirichletGradient`) now march along the
93 // anisotropic index-space normal `m` of doc/anisotropic_metric.md §6.1 and carry the per-axis
94 // `w_a` on their face and wall terms, `starCorrectFaces` carries the same `w_a` its
95 // `projectCorrect` fix-up needs, and the V8 face integral weights its pressure difference.
96 // `enable_vof` kept its own refusal here until PHASE 3 lifted it (see enableVof).
97}
98
99template <class Grid>
100void Solver<Grid>::requireIsotropic(const char* what) const {
101 if (!u_.aniso)
102 return;
103 char msg[520];
104 std::snprintf(msg, sizeof msg,
105 "%s: this entry point requires ISOTROPIC cells, but extent/cells gives "
106 "dx=%.17g dy=%.17g dz=%.17g (hRef=%.17g, h'=(%.17g, %.17g, %.17g)).",
107 what, u_.h[0], u_.h[1], u_.h[2], u_.hRef, u_.hp[0], u_.hp[1], u_.hp[2]);
108 throw std::runtime_error(msg);
109}
110
111template <class Grid>
113 return u_.physical;
114}
115
116template <class Grid>
118 return u_;
119}
120
121template <class Grid>
122std::array<double, 3> Solver<Grid>::spacing() const {
123 return {u_.h[0], u_.h[1], u_.h[2]};
124}
125
126template <class Grid>
127std::array<double, 3> Solver<Grid>::domainOrigin() const {
128 return {u_.org[0], u_.org[1], u_.org[2]};
129}
130
131template <class Grid>
132std::array<double, 3> Solver<Grid>::domainExtent() const {
133 if (u_.physical)
134 return {u_.ext[0], u_.ext[1], u_.ext[2]};
135 std::array<long, 3> gc = globalCells();
136 return {(double)gc[0], (double)gc[1], (double)gc[2]};
137}
138
139template <class Grid>
140std::array<long, 3> Solver<Grid>::globalCells() const {
141 if (u_.physical && u_.cells[0] > 0)
142 return {u_.cells[0], u_.cells[1], u_.cells[2]};
143#ifdef PECLET_FLOW_MPI
144 if (gnx_ > 0)
145 return {gnx_, gny_, gnz_};
146#endif
147 return {nx_, ny_, nz_};
148}
149
150template <class Grid>
151std::vector<double> Solver<Grid>::cellCentres(int axis) const {
152 if (axis < 0 || axis > 2)
153 throw std::runtime_error("cell_centers: axis must be 0, 1 or 2");
154 const int n[3] = {nx_, ny_, nz_};
155 const int og[3] = {og_.x, og_.y, og_.z};
156 std::vector<double> c((std::size_t)n[axis]);
157 for (int i = 0; i < n[axis]; ++i)
158 c[(std::size_t)i] = u_.org[axis] + ((double)(og[axis] + i) + 0.5) * u_.h[axis];
159 return c;
160}
161
162template <class Grid>
164 // Phase 3: `hp`, `w`, `vol` and `aniso` are set by `setPhysicalDomain` — including its SNAP,
165 // which forces them to the EXACT identity when the three spacings agree to 1e-12. Recomputing
166 // them from `h[a]/hRef` here would undo that snap the moment the extents differ by an ulp
167 // across axes, so this only derives the one member Phase 3 added and hands the metric to the
168 // VoF drivers.
169 u_.hpMax = std::max(u_.hp[0], std::max(u_.hp[1], u_.hp[2]));
171 rho_ = rhoPhys_ * u_.rhoToInt();
172 mu_ = muPhys_ * u_.muToInt();
173 const double dtInt = dtPhys_ * u_.timeToInt();
174 if (dtInt != dt_)
175 dt_ = dtInt;
176 for (int a = 0; a < 3; ++a)
177 f_[a] = fPhys_[a] * u_.forceToInt(a);
178 sigmaCsf_ = sigmaPhys_ * u_.sigmaToInt();
179 slipLambda_ = slipPhys_ * u_.lenToInt();
180 for (auto& sc : scalars_)
181 sc.D = sc.Dphys * u_.diffToInt(); // the scalar diffusivity follows tRef like mu does
182 pcRefreshUnits(); // the phase-change densities, latent heat, k, rho c_p, R_int
183 for (auto& cl : closures_)
184 closureRefreshUnits(cl); // rho/mu/force closure parameters
185 for (int face = 0; face < 6; ++face) {
186 for (int a = 0; a < 3; ++a)
187 bcVel_[face][a] = bcVelPhys_[face][a] * u_.velToInt(a);
188 if (!bcProfRaw_[face].empty())
189 resampleBcProfile(face);
190 }
191 // rho/mu/dt all feed the momentum diagonal; a scale that moved after the operator was built
192 // must rebuild it. (Only reachable on the physical path — the cell-unit path never calls
193 // this, so no existing run gains a rebuild.)
194 dtDirty_ = true;
195}
196
197template <class Grid>
199 const vof::VofMetric g = u_.vofMetric();
200 vofAdv_.metric = g;
201 vofCurv_.metric = g;
202 vofDyn_.metric = g;
203 pcAreaC_.metric = g;
204 pcAreaMc_.metric = g;
205 if (vofBlocks_)
206 vofBlocks_->setMetric(g);
207}
208
209template <class Grid>
210void Solver<Grid>::allocateBlock(int nx, int ny, int nz) {
211 nx_ = nx;
212 ny_ = ny;
213 nz_ = nz;
214 e_ = C3{nx + 2 * G, ny + 2 * G, nz + 2 * G};
215 n_ = (std::size_t)e_.x * e_.y * e_.z;
216 e1_ = C3{nx + 2, ny + 2, nz + 2}; // g=1 block for the cut-cell pressure MG
217 n1_ = (std::size_t)e1_.x * e1_.y * e1_.z;
218 sdf_ = CCField("sdf", n_);
219 ox_ = CCField("ox", n_);
220 oy_ = CCField("oy", n_);
221 oz_ = CCField("oz", n_);
222 phi_ = CCField("phi", n_);
223 div_ = CCField("div", n_);
224 P_ = CCField("P", n_);
225 // g=1 scratch for the MG bridge (openness + rhs/phi + PCG vectors)
226 ox1_ = CCField("ox1", n1_);
227 oy1_ = CCField("oy1", n1_);
228 oz1_ = CCField("oz1", n1_);
229 rhs1_ = CCField("rhs1", n1_);
230 phi1_ = CCField("phi1", n1_);
231 r_ = CCField("r", n1_);
232 z_ = CCField("z", n1_);
233 pp_ = CCField("pp", n1_);
234 Ap_ = CCField("Ap", n1_);
235 for (int c = 0; c < 3; ++c) {
236 C[c].u = CCField("u", n_);
237 C[c].b = CCField("b", n_);
238 C[c].AC = FV("AC", n_);
239 C[c].AW = FV("AW", n_);
240 C[c].AE = FV("AE", n_);
241 C[c].AS = FV("AS", n_);
242 C[c].AN = FV("AN", n_);
243 C[c].AB = FV("AB", n_);
244 C[c].AT = FV("AT", n_);
245 C[c].inhom = CCField("inhom", n_);
246 C[c].rscale = CCField("rscale", n_);
247 C[c].mask = CCField("mask", n_);
248 bcDcorr_[c] = CCField("dcorr", n_);
249 bcBrhs_[c] = CCField("brhs", n_);
250 const int maxCut = nx * ny * nz;
251 // G.6: IbmOverlay row data follows MReal (was hardcoded float -- FV32) so a
252 // -DPECLET_FLOW_OPERATOR_DOUBLE build carries the overlay in double end to end.
253 C[c].ov = IbmOverlay{Kokkos::View<int*, CCMem>("ci", maxCut),
254 Kokkos::View<int*, CCMem>("nb", maxCut),
255 FV("dr", maxCut),
256 Kokkos::View<int*, CCMem>("dc", (std::size_t)maxCut * 6),
257 FV("K", (std::size_t)maxCut * 6),
258 FV("M", (std::size_t)maxCut * 6),
259 FV("X", (std::size_t)maxCut * 6),
260 FV("Nbc", (std::size_t)maxCut * 6),
261 FV("R", (std::size_t)maxCut * 6)};
262 C[c].idMap = Kokkos::View<int*, CCMem>("idMap", n_);
263 C[c].counter = Kokkos::View<int, CCMem>("cnt");
264 old_[c] = CCField("uOld", n_); // u^n time base (fixed over the step's Picard sweeps)
265 prev_[c] = CCField("uPrev", n_); // previous Picard iterate (outer-tolerance check)
266 }
267 if constexpr (Grid::collocated) { // transient face (MAC) field for the approximate projection
268 uf_ = CCField("uf", n_);
269 vf_ = CCField("vf", n_);
270 wf_ = CCField("wf", n_);
271 tgp_ = CCField("tgp", n_); // scratch: the collocated cell pressure gradient
272 fvM_ = CCField("fvM", n_); // scratch: M·u^k (embed defect matvec)
273 fvL_ = CCField("fvL", n_); // scratch: L_FV(u^k) (embed operator apply)
274 cs_ = CCField("cs", n_); // static cell fluid fraction (embed)
275 xcx_ = CCField("xcx", n_); // static per-face open-centroid wall distance (wall-aware map)
276 xcy_ = CCField("xcy", n_);
277 xcz_ = CCField("xcz", n_);
278 }
279 // Register the pre-existing solver fields in the named directory so the multiphysics machinery
280 // (scalar transport, property closures) and load-balance redistribution can enumerate the whole
281 // set uniformly. adopt() aliases the members (no reallocation, no ownership); all live on the
282 // G=2 velocity block and share velHalo_ under MPI.
283 fields_.adopt("u", C[0].u, G, peclet::core::Centering::FaceX);
284 fields_.adopt("v", C[1].u, G, peclet::core::Centering::FaceY);
285 fields_.adopt("w", C[2].u, G, peclet::core::Centering::FaceZ);
286 fields_.adopt("p", P_, G, peclet::core::Centering::Cell);
287 fields_.adopt("sdf", sdf_, G, peclet::core::Centering::Cell);
288}
289
290template <class Grid>
291void Solver<Grid>::setRho(double r) {
292 rhoPhys_ = r;
293 if (u_.physical && !u_.rhoRefSet) {
294 u_.rhoRef = r;
295 u_.rhoRefSet = true;
297 return;
298 }
299 const double ri = r * u_.rhoToInt();
300 if (ri != rho_) {
301 rho_ = ri;
302 dtDirty_ = true; // the momentum stencil bakes rho/dt in its diagonal (rebuildStencils)
303 }
304}
305
306template <class Grid>
307void Solver<Grid>::setMu(double m) {
308 muPhys_ = m;
309 const double mi = m * u_.muToInt();
310 if (mi != mu_) {
311 mu_ = mi;
312 dtDirty_ = true; // the momentum stencil bakes mu in its off-diagonals (rebuildStencils)
313 }
314}
315
316template <class Grid>
317void Solver<Grid>::setDt(double d) {
318 dtPhys_ = d;
319 if (u_.physical && !u_.tRefSet) {
320 u_.tRef = d;
321 u_.tRefSet = true;
322 const double before = dt_;
323 refreshUnitDerived(); // pins tRef, so mu'/p'/F'/v all move with it
324 if (dt_ != before)
325 dtDirty_ = true;
326 return;
327 }
328 const double di = d * u_.timeToInt();
329 if (di != dt_) {
330 dt_ = di;
331 dtDirty_ = true; // the momentum stencil bakes rho/dt in its diagonal (rebuildStencils);
332 // a mid-run dt change must rebuild it or the operator and RHS disagree
333 }
334}
335
336template <class Grid>
337void Solver<Grid>::setBodyForce(double fx, double fy, double fz) {
338 fPhys_ = {fx, fy, fz};
339 for (int a = 0; a < 3; ++a)
340 f_[a] = fPhys_[a] * u_.forceToInt(a);
341}
342
343template <class Grid>
344void Solver<Grid>::setSuperficialVelocity(bool on, int axis, double velocity) {
345 if (!on) {
346 superficialAxis_ = -1;
347 return;
348 }
349 if (axis < 0 || axis > 2)
350 throw std::invalid_argument("set_superficial_velocity: axis must be 0, 1 or 2");
351 if constexpr (Grid::collocated)
352 throw std::runtime_error(
353 "set_superficial_velocity: staggered Solver only (the collocated grid would have to "
354 "shift the projected face field and the cell velocity consistently; not implemented)");
355 superficialAxis_ = axis;
356 superficialVelPhys_ = velocity;
357 if (superficialShift_.data() == nullptr)
358 superficialShift_ = Kokkos::View<double, CCMem>("superficial_shift");
359 Kokkos::deep_copy(superficialShift_, 0.0);
360}
361
362template <class Grid>
364 if (superficialAxis_ < 0 || superficialShift_.data() == nullptr)
365 return 0.0;
366 double h = 0.0;
367 Kokkos::deep_copy(h, superficialShift_);
368 return h * u_.velToPhys(superficialAxis_);
369}
370
371template <class Grid>
373 velIters_ = it;
374}
375
376template <class Grid>
378 velTol_ = rtol > 0.0 ? rtol : 0.0;
379 velMinIters_ = minIters < 1 ? 1 : minIters;
380}
381
382template <class Grid>
384 return lastMomentumSweeps_;
385}
386
387template <class Grid>
389 velResTol_ = rtol;
390}
391
392template <class Grid>
394 int minBlockExtent) {
395 vmgAutoCells_ = cellsPerRank;
396 if (minGlobalCells >= 0)
397 vmgAutoMinGlobal_ = minGlobalCells;
398 if (minBlockExtent >= 0)
399 vmgAutoMinExtent_ = minBlockExtent;
400}
401
402template <class Grid>
404 useVelocityCheb_ = on;
405 velChebMaxit_ = maxit < 1 ? 1 : maxit;
406}
407
408template <class Grid>
410 vmgExplicit_ = false;
411 useVelocityCheb_ = false;
412 useVelocityMg_ = false;
413 vmgDecided_ = false; // re-decide at the next step()
414}
415
416template <class Grid>
418 return useVelocityCheb_;
419}
420
421template <class Grid>
423 vmgCheb_ = on;
424 vmgChebDegree_ = degree;
425 vmgChebEigRatio_ = eig_ratio;
427}
428
429template <class Grid>
431 return vmgCheb_;
432}
433
434template <class Grid>
436 if (velResTol_ >= 0.0)
437 return velResTol_;
438 return useChebyshev_ ? chebRtol_ : pcgRtol_; // FCG shares pcgRtol_; the plain V-cycle driver
439 // has no tolerance and takes the PCG default
440}
441
442template <class Grid>
444 return lastMomentumResid_;
445}
446
447template <class Grid>
451
452template <class Grid>
454 presIters_ = it;
455}
456
457template <class Grid>
459 advect_ = on;
460}
461
462template <class Grid>
464 advScheme_ = s;
465}
466
467template <class Grid>
469 implicitFou_ = on;
470}
471
472template <class Grid>
474 outerIters_ = iters < 1 ? 1 : iters;
475}
476
477template <class Grid>
479 outerTol_ = tol;
480}
481
482template <class Grid>
484 return lastOuterIters_;
485}
486
487template <class Grid>
488void Solver<Grid>::setVelocityMultigrid(bool on, int levels, int vcycles) {
489 if (on && varProps_)
490 throwVelocityMgVariableMu("set_velocity_multigrid / set_velocity_solver('multigrid')");
491 useVelocityMg_ = on;
492 vmgExplicit_ = true; // an explicit choice disables the AUTO rule
493 vmgLevels_ = levels < 1 ? 1 : levels;
494 vmgVcycles_ = vcycles < 1 ? 1 : vcycles;
495}
496
497template <class Grid>
499 throw std::invalid_argument(
500 std::string(who) +
501 ": the velocity multigrid takes a SCALAR viscosity and would silently solve the momentum "
502 "equation with the constant set_mu value, ignoring the variable viscosity (a 'mu' "
503 "closure or set_property_mode('variable')). Use set_velocity_solver('gauss_seidel') or "
504 "'auto' (the AUTO rule already keeps variable viscosity on the red-black smoother).");
505}
506
507template <class Grid>
509 return useVelocityMg_;
510}
511
512template <class Grid>
514 pressAgglomMode_ = mode;
515 if (cutcellPressure_)
516 mg_.setAgglomerationMode(mode);
517}
518
519template <class Grid>
523
524template <class Grid>
528
529template <class Grid>
531 return mg_.telescope();
532}
533
534template <class Grid>
538
539template <class Grid>
541 return mg_.telescopeCount();
542}
543
544template <class Grid>
546 pressGraphAmg_ = on;
547 if (cutcellPressure_)
548 mg_.setGraphAmgBottom(on); // propagate live (previously only applied at the next set_solid,
549 // so toggling after geometry silently had no effect)
550}
551
552template <class Grid>
554 nLevels_ = levels < 1 ? 1 : levels;
555}
556
557template <class Grid>
559 backflowBeta_ = beta < 0.0 ? 0.0 : beta;
560}
561
562template <class Grid>
564 deferredCorr_ = on;
565}
566
567template <class Grid>
568void Solver<Grid>::setPressureChebyshev(bool on, int maxit, double rtol) {
569 useChebyshev_ = on;
570 if (on)
571 useFcg_ = false;
572 chebMaxit_ = maxit;
573 chebRtol_ = rtol;
574 chebBoundsSet_ = false;
575 chebWarmOk_ = false;
576 bfpChebSet_ = false;
577}
578
579template <class Grid>
580void Solver<Grid>::setPressurePcg(bool on, int maxit, double rtol) {
581 if (!on)
582 throw std::runtime_error(
583 "set_pressure_pcg(False): MG-PCG is the default/terminal pressure driver, so it cannot be "
584 "deselected on its own — select the driver you want instead "
585 "(set_pressure_chebyshev(True, ...) or set_pressure_fcg(True, ...)).");
586 useChebyshev_ = false; // genuine selection: the three drivers are mutually exclusive
587 useFcg_ = false;
588 chebBoundsSet_ = false;
589 chebWarmOk_ = false;
590 bfpChebSet_ = false;
591 pcgMaxit_ = maxit;
592 pcgRtol_ = rtol;
593}
594
595template <class Grid>
596void Solver<Grid>::setPressureFcg(bool on, int maxit, double rtol) {
597 if (on && ghostProjection_)
598 throw std::runtime_error(
599 "set_pressure_fcg: the ghost-projection operator is nonsymmetric and is solved by "
600 "BiCGStab; FCG does not apply");
601 useFcg_ = on;
602 if (on)
603 useChebyshev_ = false; // genuine selection (the pair is exclusive)
604 pcgMaxit_ = maxit;
605 pcgRtol_ = rtol;
606}
607
608template <class Grid>
610 if constexpr (Grid::collocated) {
611 // Collocated ghost mode: the SAME phi matrix/closures on the 1/2-1/2 face-averaged field
612 // (the face correction uf -= grad(phi) is the identical substitution), plus the directional
613 // gpCenterGrad cell gradient for the predictor -grad(P^n) and the cell correction. Only the
614 // plain (mode-0) face map applies — the wall-aware/FV/embed face-interp modes replace the
615 // very operators this scheme owns.
616 if (on && faceInterp_ != 0)
617 faceInterp_ = 0; // QUARANTINED verification path: it owns the operators the gauge-exact
618 // scheme replaces, so it selects the plain face map itself rather than
619 // throwing on the (now default) gauge-exact scheme.
620 }
621 if (on && geometryBuilt_)
622 throw std::runtime_error(
623 "set_ghost_projection / set_collocated_scheme('ghost'): call BEFORE set_solid -- the "
624 "ghost overlay is built with the geometry");
625 if (on && (porous_ || varRho_ || hasBc_ || useChebyshev_))
626 throw std::runtime_error(
627 "set_ghost_projection: incompatible with porous/variable-rho/domain-BC/Chebyshev (v1)");
629 throw std::runtime_error("set_ghost_projection: matrix_order/rhs_order must be 1 or 2");
630 if (on && distributed_ && (hasExactCross_ || hasOpenOverride_))
631 throw std::runtime_error(
632 "set_ghost_projection: exact-crossings/openness-override are single-rank only");
633 ghostProjection_ = on;
634 colSchemeAuto_ = false; // explicit selection disables the AUTO default
635 gpMatrixOrder_ = matrixOrder;
636 gpRhsOrder_ = rhsOrder;
637 gpNRows_ = -1; // takes effect at the next set_solid
638}
639
640template <class Grid>
641void Solver<Grid>::setExactCrossings(const std::vector<double>& t) {
642 requireNoGeometry("set_exact_crossings");
643 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
644 if (t.empty()) {
645 hasExactCross_ = false;
646 return;
647 }
648 if (t.size() != 9 * n)
649 throw std::runtime_error("set_exact_crossings: expected 9*nx*ny*nz values");
650#ifdef PECLET_FLOW_MPI
651 if (distributed_)
652 throw std::runtime_error("set_exact_crossings: single-rank only");
653#endif
654 for (int c = 0; c < 3; ++c)
655 for (int k = 0; k < 3; ++k) {
656 tEx_[c][k] = CCField("tEx", n);
657 Kokkos::deep_copy(
658 tEx_[c][k],
659 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
660 t.data() + ((std::size_t)c * 3 + k) * n, n));
661 }
662 hasExactCross_ = true;
663}
664
665template <class Grid>
666void Solver<Grid>::setOpennessOverride(const std::vector<double>& ox, const std::vector<double>& oy,
667 const std::vector<double>& oz) {
668 requireNoGeometry("set_openness_override");
669 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
670 if (ox.empty()) {
671 hasOpenOverride_ = false;
672 return;
673 }
674 if (ox.size() != n || oy.size() != n || oz.size() != n)
675 throw std::runtime_error("set_openness_override: expected nx*ny*nz values per field");
676#ifdef PECLET_FLOW_MPI
677 if (distributed_)
678 throw std::runtime_error("set_openness_override: single-rank only");
679#endif
680 oxOverride_ = ox;
681 oyOverride_ = oy;
682 ozOverride_ = oz;
683 hasOpenOverride_ = true;
684}
685
686template <class Grid>
688 incremental_ = on;
689}
690
691template <class Grid>
693 pwarm_ = on;
694}
695
696template <class Grid>
698 if (mode != 0 && mode != 9 && (mode < 5 || mode > 7))
699 throw std::runtime_error(
700 "set_face_interp: mode " + std::to_string(mode) +
701 " is not one of 0 (plain), 5/6/7 (embed rungs) or 9 (gauge-exact); use "
702 "set_collocated_scheme('plain' | 'gauge-exact' | 'embed' | 'ghost')");
703 if (ghostProjection_ && mode != 0)
704 throw std::runtime_error(
705 "set_face_interp: incompatible with the ghost projection (set_ghost_projection(False) "
706 "first, or use set_collocated_scheme which handles the transition)");
707 faceInterp_ = mode;
708 colSchemeAuto_ = false; // explicit selection disables the AUTO default
709}
710
711template <class Grid>
713 return faceInterp_;
714}
715
716template <class Grid>
717void Solver<Grid>::setCollocatedScheme(const std::string& name) {
718 if (name != "ghost" && ghostProjection_) {
719 ghostProjection_ = false; // scheme transition: drop the ghost before selecting a face mode
720 gpNRows_ = -1;
721 }
722 if (name == "gauge-exact") {
723 setFaceInterp(9);
724 } else if (name == "plain") {
725 setFaceInterp(0);
726 } else if (name == "embed") {
727 setFaceInterp(7);
728 } else if (name == "ghost") {
729 // The fluid-only constraint scheme (route 2b, 2026-08): binary-openness divergence +
730 // directional closures + gauge-exact gradient. Clean-protocol record: family-free
731 // (m1 -> 1e-5 monotone, |P| frozen), NO Layer-1 instability, C2 across dt = 60..1e20 with
732 // no stabilizer, both-bed ladders -1.4% -> +0.22% (R=8..24; its own small plateau), Z&H
733 // anchor -0.018% at N=128. Costs: BiCGStab (~2.3-2.7x pressure stage; star preconditioner
734 // planned), ~1.6 KB/cell overlay (caps single-GPU size), fragmentation guard. The (1,2)
735 // mixed mode stays quarantined (march-unstable on >2000-sphere beds).
736 setGhostProjection(true, 2, 2);
737 } else
738 throw std::runtime_error(
739 "set_collocated_scheme: expected 'ghost', 'gauge-exact', 'plain' or 'embed', got '" + name +
740 "'");
741}
742
743template <class Grid>
745 rotationalP_ = on;
746}
747
748template <class Grid>
750 rotWeight_ = w;
751}
752
753template <class Grid>
755 rotWallW_ = w0;
756}
757
758template <class Grid>
760 requireNoGeometry("set_aperture_order");
762 throw std::runtime_error("set_aperture_order: order must be 1 or 2");
763 apertureOrder_ = order;
764}
765
766template <class Grid>
768 return apertureOrder_;
769}
770
771template <class Grid>
773 advWallVel_ = on;
774}
775
776template <class Grid>
778 return advWallVel_;
779}
780
781template <class Grid>
783 if (distributed_)
784 throw std::runtime_error(
785 "set_comm_avoiding: call BEFORE init_mpi -- the momentum half is latched with the halo "
786 "topology");
787 caMode_ = mask;
788 mg_.setCommAvoiding(mask);
789}
790
791template <class Grid>
793 return caMode_;
794}
795
796template <class Grid>
798 if (!(theta > 1.0))
799 throw std::runtime_error("set_multigrid_aspect_threshold: theta must be > 1");
800 aspectTheta_ = theta;
801 mg_.setAspectThreshold(theta);
802 vmg_.setAspectThreshold(theta);
803}
804
805template <class Grid>
807 return aspectTheta_;
808}
809
810template <class Grid>
814
815template <class Grid>
817 return mg_.strictPressure();
818}
819
820template <class Grid>
824
825template <class Grid>
827 return mg_.coarseGhost();
828}
829
830template <class Grid>
834
835template <class Grid>
837 return mg_.agglomerationExtent();
838}
839
840template <class Grid>
842 if (distributed_)
843 throw std::runtime_error(
844 "set_decomposition: call BEFORE init_mpi -- the decomposition is built there");
845 decompLevels_ = levels;
846 decompMaxImbalance_ = maxImbalance;
847}
848
849template <class Grid>
851 return decompLevels_;
852}
853
854template <class Grid>
856 return decompMaxImbalance_;
857}
858
859template <class Grid>
861 requireNoGeometry("set_fluid_only_constraint");
862 if (mode < 0 || mode > 2)
863 throw std::runtime_error("set_fluid_only_constraint: mode must be 0, 1 or 2");
864 fluidOnlyMode_ = mode;
865 if (mode != 0)
866 colSchemeAuto_ = false; // mechanism instruments run on the aperture rails, not AUTO-ghost
867}
868
869template <class Grid>
870void Solver<Grid>::setRotationalFilter(bool on, double eps) {
871 rotFilter_ = on;
872 rotFilterEps_ = eps;
873}
874
875template <class Grid>
877 if constexpr (Grid::collocated) {
878 for (int c = 0; c < 3; ++c)
879 fillVelGhosts(c, 0);
880 if (faceInterp_ == 5 || faceInterp_ == 7)
881 centerToFaceWallAware(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u),
882 CCConst(sdf_), CCConst(xcx_), CCConst(xcy_), CCConst(xcz_), true, e_,
883 G);
884 else
885 centerToFace(uf_, vf_, wf_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), e_, G);
886 faceFieldValid_ = true;
887 buildOpenFaceField(); // keep the advecting/diagnostic view in step with the seeded field
888 }
889}
890
891template <class Grid>
892void Solver<Grid>::uploadVelocity(const std::vector<double>& uu, const std::vector<double>& vv,
893 const std::vector<double>& ww) {
894 const std::vector<double>* src[3] = {&uu, &vv, &ww};
895 std::vector<double> scaled[3];
896 for (int c = 0; c < 3; ++c) {
897 const double k = u_.velToInt(c);
898 if (k == 1.0)
899 continue;
900 scaled[c].resize(src[c]->size());
901 for (std::size_t i = 0; i < src[c]->size(); ++i)
902 scaled[c][i] = (*src[c])[i] * k;
903 src[c] = &scaled[c];
904 }
906 const int ex = e_.x, ey = e_.y, nx = nx_, ny = ny_, nz = nz_, g = G;
907 for (int c = 0; c < 3; ++c) {
908 // Upload the inner field once, write it into the inner cells on device, then refresh the
909 // periodic ghosts (G4) — the old path mirrored the field down, looped on host, and copied
910 // back up.
911 CCField din("peclet::flow::vel_in_d", static_cast<std::size_t>(nx_) * ny_ * nz_);
912 Kokkos::deep_copy(
913 din,
914 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>(
915 src[c]->data(), src[c]->size()));
916 CCField u = C[c].u;
917 Kokkos::parallel_for(
918 "peclet::flow::upload_velocity", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
919 KOKKOS_LAMBDA(int x, int y, int z) {
920 u((long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey) =
921 din((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
922 });
923 fillGhosts(C[c].u);
924 }
925 seedFaceFieldFromCells(); // ISSUES sweep item 5 (collocated only; a no-op staggered)
926}
927
928template <class Grid>
929int Solver<Grid>::nx() const {
930 return nx_;
931}
932
933template <class Grid>
934int Solver<Grid>::ny() const {
935 return ny_;
936}
937
938template <class Grid>
939int Solver<Grid>::nz() const {
940 return nz_;
941}
942
943template <class Grid>
945 return advect_ && (implicitFou_ || (hasBc_ && (!useVelocityMg_ || hasSolid_)));
946}
947
948template <class Grid>
950 return hasBc_ &&
951 (hasSolid_ || (!useVelocityMg_ && (implicitAdv() || varProps_ || varRho_ || hasDrag_)));
952}
953
954template <class Grid>
956 return hasBc_ && useVelocityMg_ && hasSolid_ && !varProps_ && !varRho_ && !hasDrag_;
957}
958
959template <class Grid>
960long Solver<Grid>::strideOf(int c) const {
961 return (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
962}
963
964template <class Grid>
966 // Per-axis viscous coefficient b_a = mu' * w_a (doc/anisotropic_metric.md §2); w == 1.0 on
967 // the isotropic path, so this is literally `beta = mu_` there.
968 const double idiag = rho_ / dt_, bx = mu_ * u_.w[0], by = mu_ * u_.w[1], bz = mu_ * u_.w[2];
969 if (varProps_)
970 fillMuGhosts(); // face means read mu at i +- stride (boundary inner cells -> ghosts)
971 if (varRho_)
972 fillPropGhosts(rhoField_);
973 for (int c = 0; c < 3; ++c) {
974 Kokkos::deep_copy(C[c].rscale, 1.0);
975 Kokkos::deep_copy(C[c].inhom, 0.0);
976 if (varProps_ || effVarRho())
977 ibmBuildDiffusionVar(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e_.x,
978 e_.y, e_.z, G, makeFaceProps(c), u_.w[0], u_.w[1], u_.w[2]);
979 else
980 ibmBuildDiffusion(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e_.x, e_.y,
981 e_.z, bx, by, bz, idiag);
982 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
983 C[c].rscale, C[c].ov, C[c].nCut, 0.0f, wallVelView(c));
984 if (hasDrag_)
986 }
987}
988
989template <class Grid>
990void Solver<Grid>::copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg) {
991 // rule H (§4.5; §14 H-3a): host pencil, device MDRange -- no per-cell 64-bit div/mod. dst and
992 // src are different fields (two blocks of different ghost width), so ccFor3's simd contract
993 // holds.
994 ccFor3(
995 "peclet::flow::copyInner", C3{0, 0, 0}, C3{nx_, ny_, nz_},
996 KOKKOS_LAMBDA(int ix, int iy, int iz) {
997 const long di =
998 (long)(ix + dg) + (long)(iy + dg) * de.x + (long)(iz + dg) * (long)de.x * de.y;
999 const long si =
1000 (long)(ix + sg) + (long)(iy + sg) * se.x + (long)(iz + sg) * (long)se.x * se.y;
1001 dst(di) = src(si);
1002 });
1003}
1004
1005template <class Grid>
1007 ccFor3( // rule H (doc/vof_step_performance_design.md §4.5): host pencil, device MDRange
1008 "peclet::flow::copyBlockShifted", C3{0, 0, 0}, C3{de.x, de.y, de.z},
1009 KOKKOS_LAMBDA(int x, int y, int z) {
1010 const long di = (long)x + (long)y * de.x + (long)z * (long)de.x * de.y;
1011 const long si =
1012 (long)(x + off) + (long)(y + off) * se.x + (long)(z + off) * (long)se.x * se.y;
1013 dst(di) = src(si);
1014 });
1015}
1016
1017template <class Grid>
1019#ifdef PECLET_FLOW_MPI
1020 if (distributed_) {
1021 velDev_->exchange(f);
1022 return;
1023 }
1024#endif
1025 fillPeriodicAxes(f, 7); // §14 H-3(d): x, y, z in one launch (was fillAxis 0, 1, 2)
1026}
1027
1028template <class Grid>
1030#ifdef PECLET_FLOW_MPI
1031 if (distributed_) {
1032 velDev_->exchange(f);
1033 return;
1034 } // halo gives all ghosts; the 7-pt smoother uses the faces
1035#endif
1036 CCExec space;
1037 C3 e = e_;
1038 const int Nx = nx_, Ny = ny_, Nz = nz_;
1039 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
1040 CCField ff = f;
1041 Kokkos::parallel_for(
1042 "peclet::flow::ibm_facefill", Kokkos::RangePolicy<CCExec>(space, 0, (long)nx_ * ny_ * nz_),
1043 KOKKOS_LAMBDA(long n) {
1044 const int ix = (int)(n % Nx), iy = (int)((n / Nx) % Ny), iz = (int)(n / ((long)Nx * Ny));
1045 const long i = (long)(ix + G) * sx + (long)(iy + G) * sy + (long)(iz + G) * sz;
1046 if (ix < G)
1047 ff(i + (long)Nx * sx) = ff(i);
1048 else if (ix >= Nx - G)
1049 ff(i - (long)Nx * sx) = ff(i);
1050 if (iy < G)
1051 ff(i + (long)Ny * sy) = ff(i);
1052 else if (iy >= Ny - G)
1053 ff(i - (long)Ny * sy) = ff(i);
1054 if (iz < G)
1055 ff(i + (long)Nz * sz) = ff(i);
1056 else if (iz >= Nz - G)
1057 ff(i - (long)Nz * sz) = ff(i);
1058 });
1059}
1060
1061template <class Grid>
1063 CCExec space;
1064 C3 e = e_;
1065 int N3[3] = {nx_, ny_, nz_};
1066 int dims[3] = {e.x, e.y, e.z};
1067 long st[3] = {1, e.x, (long)e.x * e.y};
1068 const int a = axis, b = (axis + 1) % 3, c = (axis + 2) % 3;
1069 const long sa = st[a], sb = st[b], sc = st[c];
1070 const int N = N3[a];
1071 CCField ff = f;
1072 Kokkos::parallel_for(
1073 "peclet::flow::ibm_pfill", MDRange2<CCExec>(space, {0, 0}, {dims[b], dims[c]}),
1074 KOKKOS_LAMBDA(int p0, int p1) {
1075 const long base = (long)p0 * sb + (long)p1 * sc;
1076 for (int gl = 0; gl < G; ++gl) {
1077 ff(base + (long)gl * sa) = ff(base + (long)(gl + N) * sa);
1078 ff(base + (long)(G + N + gl) * sa) = ff(base + (long)(G + gl) * sa);
1079 }
1080 });
1081}
1082
1083// §14 H-3(d): every ghost cell that has a ghost coordinate on at least one axis in `axes` takes the
1084// value of the cell its periodic coordinates (those in `axes`) wrap to; its other coordinates stay.
1085// That is exactly what the sequential fillAxis passes over those axes in ascending order (each over
1086// the full transverse extent, ghosts included) leave behind: a ghost an earlier pass wrote is
1087// overwritten by a later pass with the wrapped value of a cell the earlier pass had written from
1088// its own wrap, and the source of every write (inner on every axis in `axes`) is never written.
1089// Pure copies, so bit-identical (CutcellMG::fillWrap's argument, §5.4, with non-periodic axes
1090// kept). The shell as up to three disjoint slabs: z-ghost planes (all x, y) if z is in `axes`; then
1091// y-ghost rows (all x) over the z range the z slab did not cover; then x-ghost cells over the (y,
1092// z) range the first two did not cover.
1093template <class Grid>
1095 const bool px = axes & 1, py = axes & 2, pz = axes & 4;
1096 const int nx = nx_, ny = ny_, nz = nz_;
1097 if ((px && nx < G) || (py && ny < G) || (pz && nz < G)) {
1098 // a ghost band wider than the block: the sequential passes copy ghost from ghost, which the
1099 // one-shot wrap does not reproduce -- keep them
1100 for (int a = 0; a < 3; ++a)
1101 if (axes & (1 << a))
1102 fillAxis(f, a);
1103 return;
1104 }
1105 CCExec space;
1106 const C3 e = e_;
1107 const long exy = (long)e.x * e.y;
1108 // the z range of the y slab and the (y, z) range of the x slab: inner on a periodic axis whose
1109 // slab came first, the full extent otherwise
1110 const int z0 = pz ? G : 0, zn = pz ? nz : e.z;
1111 const int y0 = py ? G : 0, yn = py ? ny : e.y;
1112 const long nZ = pz ? 2L * G * exy : 0, nY = py ? (long)zn * 2 * G * e.x : 0,
1113 nX = px ? (long)zn * yn * 2 * G : 0;
1114 CCField ff = f;
1115 if constexpr (std::is_same_v<typename CCExec::memory_space, Kokkos::HostSpace>) {
1116 // host: the slabs ROW by row (one division per row, a contiguous copy along x), serially below
1117 // the cutoff (rule H). No simd mark: a row's source and destination can share the row.
1118 const long rZ = pz ? 2L * G * e.y : 0, rY = py ? (long)zn * 2 * G : 0,
1119 rX = px ? (long)zn * yn : 0;
1120 auto copyRow = [=](long dst, long src) { // one full x row, wrapped along x if x is periodic
1121 if (px) {
1122 for (int x = 0; x < G; ++x)
1123 ff(dst + x) = ff(src + x + nx);
1124 for (int x = G; x < G + nx; ++x)
1125 ff(dst + x) = ff(src + x);
1126 for (int x = G + nx; x < e.x; ++x)
1127 ff(dst + x) = ff(src + x - nx);
1128 } else {
1129 for (int x = 0; x < e.x; ++x)
1130 ff(dst + x) = ff(src + x);
1131 }
1132 };
1133 auto row = [=](long t) {
1134 if (t < rZ) { // z-ghost plane row (y, zg)
1135 const int l = (int)(t / e.y), y = (int)(t - (long)l * e.y);
1136 const int z = l < G ? l : nz + l;
1137 const int sy = !py ? y : (y < G ? y + ny : (y >= G + ny ? y - ny : y));
1138 const int sz = z < G ? z + nz : z - nz;
1139 copyRow((long)y * e.x + (long)z * exy, (long)sy * e.x + (long)sz * exy);
1140 } else if (t < rZ + rY) { // y-ghost row (yg, z) of the y slab's z range
1141 const long t2 = t - rZ;
1142 const int zi = (int)(t2 / (2 * G)), l = (int)(t2 - (long)zi * 2 * G);
1143 const int y = l < G ? l : ny + l, z = z0 + zi;
1144 const int sy = y < G ? y + ny : y - ny;
1145 copyRow((long)y * e.x + (long)z * exy, (long)sy * e.x + (long)z * exy);
1146 } else { // the x-ghost cells of row (y, z) of the x slab's range
1147 const long t3 = t - rZ - rY;
1148 const int zi = (int)(t3 / yn), yi = (int)(t3 - (long)zi * yn);
1149 const long base = (long)(y0 + yi) * e.x + (long)(z0 + zi) * exy;
1150 for (int x = 0; x < G; ++x)
1151 ff(base + x) = ff(base + x + nx);
1152 for (int x = G + nx; x < e.x; ++x)
1153 ff(base + x) = ff(base + x - nx);
1154 }
1155 };
1156 if (hostRunSerial(nZ + nY + nX)) {
1157 for (long t = 0; t < rZ + rY + rX; ++t)
1158 row(t);
1159 return;
1160 }
1161 Kokkos::parallel_for("peclet::flow::ibm_pfill3",
1162 Kokkos::RangePolicy<CCExec>(space, 0, rZ + rY + rX), row);
1163 return;
1164 }
1165 Kokkos::parallel_for(
1166 "peclet::flow::ibm_pfill3", Kokkos::RangePolicy<CCExec>(space, 0, nZ + nY + nX),
1167 KOKKOS_LAMBDA(long t) {
1168 int x, y, z;
1169 if (t < nZ) {
1170 const int l = (int)(t / exy);
1171 const long r = t - (long)l * exy;
1172 x = (int)(r % e.x);
1173 y = (int)(r / e.x);
1174 z = l < G ? l : nz + l;
1175 } else if (t < nZ + nY) {
1176 const long t2 = t - nZ, rw = 2L * G * e.x;
1177 const long zi = t2 / rw, r = t2 - zi * rw;
1178 const int l = (int)(r / e.x);
1179 x = (int)(r % e.x);
1180 y = l < G ? l : ny + l;
1181 z = z0 + (int)zi;
1182 } else {
1183 const long t3 = t - nZ - nY, rw = (long)yn * 2 * G;
1184 const long zi = t3 / rw, r = t3 - zi * rw;
1185 const int l = (int)(r % (2 * G));
1186 x = l < G ? l : nx + l;
1187 y = y0 + (int)(r / (2 * G));
1188 z = z0 + (int)zi;
1189 }
1190 const int sx = !px ? x : (x < G ? x + nx : (x >= G + nx ? x - nx : x));
1191 const int sy = !py ? y : (y < G ? y + ny : (y >= G + ny ? y - ny : y));
1192 const int sz = !pz ? z : (z < G ? z + nz : (z >= G + nz ? z - nz : z));
1193 ff((long)x + (long)y * e.x + (long)z * exy) =
1194 ff((long)sx + (long)sy * e.x + (long)sz * exy);
1195 });
1196}
1197
1198template <class Grid>
1200 int axes = 0, n = 0, last = -1;
1201 for (int a = 0; a < 3; ++a)
1202 if (bc_[2 * a] == 0 && bc_[2 * a + 1] == 0) {
1203 axes |= 1 << a;
1204 ++n;
1205 last = a;
1206 }
1207 if (n >= 2)
1209 else if (n == 1)
1210 fillAxis(f, last); // a single-axis fill keeps its own launch
1211}
1212
1213template <class Grid>
1215 return advWallVel_ && !Grid::collocated && hasScene_ && hasMotion_ && advect_ &&
1216 uBc_[0].extent(0) == n_ && C[0].mask.extent(0) == n_;
1217}
1218
1219template <class Grid>
1221 if (!advWallInputs())
1222 return;
1223 CCExec space;
1224 for (int c = 0; c < 3; ++c) {
1225 if (uwAdv_[c].extent(0) != n_)
1226 uwAdv_[c] = CCField("uwAdv", n_);
1227 Kokkos::deep_copy(uwAdv_[c], C[c].u);
1228 CCField a = uwAdv_[c];
1229 CCConst m = CCConst(C[c].mask), w = CCConst(uBc_[c]);
1230 Kokkos::parallel_for(
1231 "peclet::flow::adv_wall_inputs", Kokkos::RangePolicy<CCExec>(space, 0, (long)n_),
1232 KOKKOS_LAMBDA(long i) {
1233 if (m(i) <= 0.5)
1234 return;
1235 a(i) = w(i);
1236 });
1237 }
1238 space.fence();
1239}
1240
1241template <class Grid>
1243 return advWallInputs() ? CCConst(uwAdv_[c]) : CCConst(C[c].u);
1244}
1245
1246template <class Grid>
1248 return CCConst(c == 0 ? uf_ : (c == 1 ? vf_ : wf_));
1249}
1250
1251template <class Grid>
1253 return (hasOutflow_ && ufOpen_[c].extent(0) == n_) ? CCConst(ufOpen_[c]) : advFaceView(c);
1254}
1255
1256template <class Grid>
1258 if constexpr (Grid::collocated) {
1259 if (!hasOutflow_)
1260 return; // openFaceView then reads uf_ directly: nothing to replace
1261 CCField src[3] = {uf_, vf_, wf_};
1262 for (int a = 0; a < 3; ++a) {
1263 if (ufOpen_[a].extent(0) != n_)
1264 ufOpen_[a] = CCField("peclet::flow::ufOpen", n_);
1265 Kokkos::deep_copy(ufOpen_[a], src[a]);
1266 }
1267 B3 e{e_.x, e_.y, e_.z};
1268 for (int a = 0; a < 3; ++a)
1269 if (bc_[2 * a + 1] == 3 && touchesGlobalFace(2 * a + 1))
1270 bcNeumannGhost(ufOpen_[a], e, G, a, 1);
1271 }
1272}
1273
1274template <class Grid>
1276 return Grid::collocated && ufAdvect_ && faceFieldValid_;
1277}
1278
1279template <class Grid>
1281 ufAdvect_ = on;
1282}
1283
1284template <class Grid>
1286 return ufAdvect_;
1287}
1288
1289template <class Grid>
1290bool Solver<Grid>::ensureAdvStash(int c, bool adv) {
1291 const bool want = !Grid::collocated && hasScene_ && adv;
1292 if (want && advRhs_[c].extent(0) != n_)
1293 advRhs_[c] = CCField("advRhs", n_);
1294 haveAdvRhs_ = want;
1295 return want;
1296}
1297
1298template <class Grid>
1300 // b_a = mu' * w_a (doc/anisotropic_metric.md §2); the FOU part is index-native and unchanged.
1301 const double idiag = rho_ / dt_, fouw = rho_, bx = mu_ * u_.w[0], by = mu_ * u_.w[1],
1302 bz = mu_ * u_.w[2];
1303 C3 e = e_;
1304 ibmBuildDiffusion(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e.x, e.y, e.z,
1305 bx, by, bz, idiag);
1306 CCExec space;
1307 FV AC = C[c].AC, AW = C[c].AW, AE = C[c].AE, AS = C[c].AS, AN = C[c].AN, AB = C[c].AB,
1308 AT = C[c].AT;
1309 // A0: wall-aware inputs (staggered). COLLOCATED: the projected divergence-free face field --
1310 // the implicit FOU operator MUST see the same advecting velocity as the explicit deferred
1311 // (SOU - FOU) correction in buildRhs* or the FOU stops cancelling at steady state
1312 // (ufAdvVelocity()).
1313 const bool ufa = ufAdvVelocity();
1314 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
1315 W = ufa ? openFaceView(2) : advVelView(2);
1316 using MD = MDRange3<CCExec>;
1317 Kokkos::parallel_for(
1318 "advstencil", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1319 KOKKOS_LAMBDA(int x, int y, int z) {
1320 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1321 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
1322 czp = AT(i);
1323 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
1324 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp, ufa);
1325 AC(i) = (MReal)cC;
1326 AW(i) = (MReal)cxm;
1327 AE(i) = (MReal)cxp;
1328 AS(i) = (MReal)cym;
1329 AN(i) = (MReal)cyp;
1330 AB(i) = (MReal)czm;
1331 AT(i) = (MReal)czp;
1332 });
1333
1334 Kokkos::deep_copy(C[c].rscale, 1.0);
1335 Kokkos::deep_copy(C[c].inhom, 0.0);
1336 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
1337 C[c].rscale, C[c].ov, C[c].nCut, 0.0f, wallVelView(c));
1338 if (hasDrag_)
1339 addDragDiagonal(c);
1340}
1341
1342template <class Grid>
1344 C3 e = e_;
1345 if (c == 0) {
1346 if (varProps_)
1347 fillMuGhosts();
1348 if (varRho_)
1349 fillPropGhosts(rhoField_);
1350 if (!varRho_ && porous_ && porousCons_)
1351 updateEpsRho(); // eps ghosts are driver-filled; whole-block product has valid ghosts
1352 }
1353 ibmBuildDiffusionVar(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, e.x, e.y, e.z,
1354 G, makeFaceProps(c), u_.w[0], u_.w[1], u_.w[2]);
1355 CCExec space;
1356 FV AC = C[c].AC, AW = C[c].AW, AE = C[c].AE, AS = C[c].AS, AN = C[c].AN, AB = C[c].AB,
1357 AT = C[c].AT;
1358 // A0: wall-aware inputs (staggered). COLLOCATED: the projected divergence-free face field --
1359 // the implicit FOU operator MUST see the same advecting velocity as the explicit deferred
1360 // (SOU - FOU) correction in buildRhs* or the FOU stops cancelling at steady state
1361 // (ufAdvVelocity()).
1362 const bool ufa = ufAdvVelocity();
1363 CCConst U = ufa ? openFaceView(0) : advVelView(0), V = ufa ? openFaceView(1) : advVelView(1),
1364 W = ufa ? openFaceView(2) : advVelView(2);
1365 const bool vr = effVarRho();
1366 const double rhoC = rho_;
1367 CCConst rf = vr ? CCConst(effRhoField()) : CCConst();
1368 const long sc = strideOf(c);
1369 using MD = MDRange3<CCExec>;
1370 Kokkos::parallel_for(
1371 "advstencil_var", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1372 KOKKOS_LAMBDA(int x, int y, int z) {
1373 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1374 double cC = AC(i), cxm = AW(i), cxp = AE(i), cym = AS(i), cyp = AN(i), czm = AB(i),
1375 czp = AT(i);
1376 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
1377 const double fouw = vr ? 0.5 * (rf(i) + rf(i - sc)) : rhoC;
1378 Grid::fou_operator(c, x, y, z, Ua, Va, Wa, fouw, cC, cxm, cxp, cym, cyp, czm, czp, ufa);
1379 AC(i) = (MReal)cC;
1380 AW(i) = (MReal)cxm;
1381 AE(i) = (MReal)cxp;
1382 AS(i) = (MReal)cym;
1383 AN(i) = (MReal)cyp;
1384 AB(i) = (MReal)czm;
1385 AT(i) = (MReal)czp;
1386 });
1387 Kokkos::deep_copy(C[c].rscale, 1.0);
1388 Kokkos::deep_copy(C[c].inhom, 0.0);
1389 ibmModifyStencil(C[c].AC, C[c].AW, C[c].AE, C[c].AS, C[c].AN, C[c].AB, C[c].AT, C[c].inhom,
1390 C[c].rscale, C[c].ov, C[c].nCut, 0.0f, wallVelView(c));
1391 if (hasDrag_)
1392 addDragDiagonal(c);
1393}
1394
1395template <class Grid>
1397 CCExec space;
1398 C3 e = e_;
1399 double m = 0;
1400 Kokkos::parallel_reduce(
1401 "maxdiff", MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1402 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1403 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1404 const double d = Kokkos::fabs(a(i) - b(i));
1405 if (d > acc)
1406 acc = d;
1407 },
1408 Kokkos::Max<double>(m));
1409 return m;
1410}
1411
1412template <class Grid>
1413template <class Fill, class Color, class ColorDu>
1414void Solver<Grid>::velSweepLoop(Fill&& fill, Color&& sweepColor, ColorDu&& sweepColorDu,
1415 std::function<double()> resid, double bnorm) {
1416 double du0 = 0.0;
1417 int used = velIters_;
1418 const double vtol = velocityResidualTolerance();
1419 const bool useRes = vtol > 0.0 && resid;
1420 auto gmax = [&](double v) {
1421#ifdef PECLET_FLOW_MPI
1422 if (distributed_) {
1423 double g = 0.0;
1424 MPI_Allreduce(&v, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
1425 return g;
1426 }
1427#endif
1428 return v;
1429 };
1430 double scale = 0.0, rPrev = -1.0;
1431 if (useRes) {
1432 // The convergence scale from the initial residual: forcing may enter through a Dirichlet
1433 // ghost (inflow), not b, hence max(|b|, |A u|). NO early return on a converged warm start:
1434 // the projection needs u* to carry the O(rtol) response of the momentum equation (the
1435 // hydrostatic acid test drifts by 1e-8 in dP/dz if the solve is skipped), so at least one
1436 // sweep always runs -- a negligible cost against the 8-9 the solve typically takes.
1437 fill();
1438 (void)gmax(resid());
1439 scale = std::max(gmax(bnorm), gmax(lastAxNorm_));
1440 }
1441 for (int it = 0; it < velIters_; ++it) {
1442 fill();
1443 sweepColor(0);
1444 fill();
1445 if (useRes) {
1446 sweepColor(1);
1447 // check on the first sweep, then every 4th (a residual costs about half a sweep)
1448 if (it == 0 || (it + 1) % 4 == 0 || it + 1 == velIters_) {
1449 fill();
1450 const double r = gmax(resid());
1451 const double ratio = scale > 0 ? r / scale : 0.0;
1452 // Round-off floor / stagnation guard: a residual at ~1e-14 of the scale, or one that no
1453 // longer decreases between checks, cannot be improved by more sweeps -- stop rather than
1454 // chase noise to the cap (an exact case such as the hydrostatic column lands here).
1455 const bool floor = r <= 1e-14 * scale || (rPrev >= 0.0 && r >= rPrev);
1456 rPrev = r;
1457 if (r <= vtol * scale || floor || it + 1 == velIters_) {
1458 lastMomentumResid_ = std::max(lastMomentumResid_, ratio); // EXIT ratio per component
1459 used = it + 1;
1460 break;
1461 }
1462 }
1463 } else if (velTol_ > 0.0) {
1464 double du = sweepColorDu(1);
1465#ifdef PECLET_FLOW_MPI
1466 if (distributed_) {
1467 double gd = 0.0;
1469 du = gd;
1470 }
1471#endif
1472 if (it == 0)
1473 du0 = du;
1474 if (it + 1 >= velMinIters_ && du <= velTol_ * du0) {
1475 used = it + 1;
1476 break;
1477 }
1478 } else {
1479 sweepColor(1);
1480 }
1481 }
1482 lastMomentumSweeps_ += used;
1483}
1484
1485template <class Grid>
1487#ifdef PECLET_FLOW_MPI
1488 return comm_;
1489#else
1490 return nullptr;
1491#endif
1492}
1493
1494template <class Grid>
1496 if (hasBc_) {
1497 const int t = bc_[2 * c];
1498 if ((t == 1 || t == 2 || t == 4) && touchesGlobalFace(2 * c))
1499 zeroPlane(velRes_, e_, c, G);
1500 }
1501 lastAxNorm_ = peclet::flow::maxAbsDiffInner(CCConst(C[c].b), CCConst(velRes_), e_, G);
1502 return maxAbsInner(CCConst(velRes_), e_, G);
1503}
1504
1505template <class Grid>
1506std::function<double()> Solver<Grid>::stencilResidual(int c, bool exchange) {
1507 if (velocityResidualTolerance() <= 0.0)
1508 return nullptr;
1509 if (velRes_.extent(0) != n_)
1510 velRes_ = CCField("velRes", n_);
1511 return [this, c, exchange]() {
1512#ifdef PECLET_FLOW_MPI
1513 if (exchange && distributed_)
1514 velDev_->exchange(C[c].u);
1515#else
1516 (void)exchange;
1517#endif
1518 residualVarPin(velRes_, CCConst(C[c].u), CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1519 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1520 CCConst(C[c].mask), e_, G);
1521 return finishResidual(c);
1522 };
1523}
1524
1525template <class Grid>
1526std::function<double()> Solver<Grid>::constCoeffResidual(int c, double bx, double by, double bz,
1527 double Ac) {
1528 if (velocityResidualTolerance() <= 0.0)
1529 return nullptr;
1530 if (velRes_.extent(0) != n_)
1531 velRes_ = CCField("velRes", n_);
1532 const bool an = u_.aniso;
1533 return [this, c, bx, by, bz, Ac, an]() {
1534 const I3 e{e_.x, e_.y, e_.z};
1535 diffResidual(velRes_, CCConst(C[c].u), CCConst(C[c].b), e, G, bx, by, bz, Ac,
1536 CCConst(bcDcorr_[c]), an);
1537 return finishResidual(c);
1538 };
1539}
1540
1541template <class Grid>
1543 return velocityResidualTolerance() > 0.0 ? maxAbsInner(CCConst(C[c].b), e_, G) : 0.0;
1544}
1545
1546template <class Grid>
1548 if (chebD_.extent(0) != n_)
1549 chebD_ = CCField("chebD", n_);
1550 if (velRes_.extent(0) != n_)
1551 velRes_ = CCField("velRes", n_);
1552 Kokkos::deep_copy(chebD_, 0.0);
1553
1554 auto gmax = [&](double v) {
1555#ifdef PECLET_FLOW_MPI
1556 if (distributed_) {
1557 double g = 0.0;
1558 MPI_Allreduce(&v, &g, 1, MPI_DOUBLE, MPI_MAX, comm_);
1559 return g;
1560 }
1561#endif
1562 return v;
1563 };
1564 auto fill = [&] {
1565#ifdef PECLET_FLOW_MPI
1566 if (distributed_) {
1567 velDev_->exchange(C[c].u);
1568 return;
1569 }
1570#endif
1571 fillGhostsFaces(C[c].u);
1572 };
1573 auto residual = [&] {
1574 residualVarPin(velRes_, CCConst(C[c].u), CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1575 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1576 CCConst(C[c].mask), e_, G);
1577 };
1578
1579 // The interval must be IDENTICAL on every rank or the iterates diverge from one another, so the
1580 // two Gershgorin bounds are reduced over the communicator before the recurrence starts. They are
1581 // bounds, not estimates: -lo is reduced by MAX to get a global MIN without a second reducer.
1582 double lo = 0.0, hi = 0.0;
1583 ibmStencilJacobiBounds(FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1584 FPC(C[c].AB), FPC(C[c].AT), CCConst(C[c].mask), e_, G, lo, hi);
1585 lo = -gmax(-lo);
1586 hi = gmax(hi);
1587 // A degenerate interval means the operator is not the positive-definite Helmholtz this solver
1588 // assumes (an all-pinned block, or a stencil not yet built). Fall back rather than diverge.
1589 if (!(hi > lo) || !(lo > 0.0)) {
1590 smoothComp(c);
1591 return;
1592 }
1593
1594 const double vtol = velocityResidualTolerance();
1595 const bool useRes = vtol > 0.0;
1596 ChebCoeffs cb(lo, hi);
1597 double scale = 0.0, rPrev = -1.0;
1598 if (useRes) {
1599 // Same convergence scale as velSweepLoop: max(|b|, |A u|) from the initial residual, so the
1600 // two solvers stop on the same criterion at the same tolerance.
1601 fill();
1602 residual();
1603 (void)finishResidual(c);
1604 scale = std::max(gmax(maxAbsInner(CCConst(C[c].b), e_, G)), gmax(lastAxNorm_));
1605 }
1606
1607 int used = velChebMaxit_;
1608 for (int it = 0; it < velChebMaxit_; ++it) {
1609 fill();
1610 residual();
1611 if (useRes && (it == 0 || (it + 1) % 4 == 0 || it + 1 == velChebMaxit_)) {
1612 const double r = gmax(finishResidual(c));
1613 // Round-off floor ONLY -- NOT velSweepLoop's "no decrease between checks" guard. Chebyshev
1614 // minimises a polynomial over the spectral interval, so its max-norm residual is not
1615 // monotone: it rises over the first few iterations before it falls. Carrying the Gauss-
1616 // Seidel stagnation test across aborted the solve after three iterations per component and
1617 // left <u> 3.9 % off the red-black answer. Stagnation is instead caught by the cap.
1618 const bool floor = r <= 1e-14 * scale;
1619 rPrev = r;
1620 if (r <= vtol * scale || floor) {
1621 lastMomentumResid_ = std::max(lastMomentumResid_, scale > 0 ? r / scale : 0.0);
1622 used = it; // this iteration's update is not applied: the iterate already passes
1623 break;
1624 }
1625 }
1626 double alpha = 0.0, beta = 0.0;
1627 cb.next(it, alpha, beta);
1628 ibmChebUpdate(C[c].u, chebD_, CCConst(velRes_), FPC(C[c].AC), CCConst(C[c].mask), e_, G, alpha,
1629 beta);
1630 }
1631 lastMomentumSweeps_ += used;
1632}
1633
1634template <class Grid>
1636 if constexpr (Grid::collocated) {
1637 if (hasBc_) { // collocated domain BC: the (all-fluid) IBM diffusion stencil + cell-centered
1638 // wall
1639 // reflection ghosts refreshed each colour (explicit no-slip; no fold). Converges to the
1640 // wall value.
1641 velSweepLoop([&] { fillVelGhostsTo(C[c].u, c, 0); },
1642 [&](int col) {
1643 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1644 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1645 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
1646 },
1647 [&](int col) {
1648 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), FPC(C[c].AC),
1649 FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS),
1650 FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1651 CCConst(C[c].mask), e_, og_, G, col);
1652 },
1653 stencilResidual(c, /*exchange=*/false), stencilBnorm(c));
1654 return;
1655 }
1656 }
1657 if (mixedVelocityMg()) {
1658 // MIXED: immersed solid + domain BCs (the packed bed with an inlet/outlet). Fine = the sharp
1659 // cut-cell stencil, solid pin, clean-fluid exclude + held-face exclude; coarse = staircase
1660 // Helmholtz + domain-face folds (VelocityMG::setStaircaseBc). The BC hook re-imposes the
1661 // level-0 velocity BC after every ghost fill, exactly as the RB-GS path's fillVelGhosts(c,1).
1662 const Off3 off = Grid::offset(c);
1663 ibmVolfrac(vmgTheta_, CCConst(sdf_), e_, off, u_.aniso, u_.hp[0], u_.hp[1], u_.hp[2], u_.w[0],
1664 u_.w[1], u_.w[2]);
1665 ibmCleanFluidMask(vmgClean_, CCConst(sdf_), e_, off);
1666 vmg_.setFineStencil(FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1667 FPC(C[c].AB), FPC(C[c].AT));
1668 // coarse = staircase Helmholtz (+ FOU from the restricted advecting velocity when the fine
1669 // stencil carries implicit advection) + domain-face folds
1670 vmg_.setStaircaseBc(c, CCConst(vmgTheta_), CCConst(C[c].mask), CCConst(vmgClean_), mu_,
1671 rho_ / dt_, 0.5, /*upwind=*/implicitAdv(), rho_);
1672 // fold=0: level 0 is the UNFOLDED cut-cell stencil, so its wall ghosts are reflections
1673 // (exactly the bcStencilPath RB-GS convention); the folded coarse levels hold theirs at 0.
1674 fillVelGhosts(c, 0);
1675 vmg_.setBcApplyL0([this, c](CCField x) { applyVelocityBcCompTo(x, c, 0, true); });
1676 lastMomentumSweeps_ += vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8, velTol_,
1678 lastMomentumResid_ = std::max(lastMomentumResid_, vmg_.lastResidualRatio());
1679 maskVelocity(c);
1680 return;
1681 }
1682 if (bcStencilPath()) {
1683 // Domain BCs solved with the Robust-Scaled cut-cell / FOU stencil (built by setSolid /
1684 // buildAdvStencil) while refreshing the domain-BC ghosts each colour -- explicit walls/inflow
1685 // (reflection, fold=0) + outflow zero-gradient. Mirrors the collocated path above. Used for
1686 // an immersed solid (cut-cell no-slip in the operator) and/or implicit advection (FOU upwind
1687 // in the stencil -> stable at large dt). The const-coeff fold smoothers below are all-fluid,
1688 // diffusion-only: they ignore the solid AND run advection explicitly (CFL-limited).
1689 velSweepLoop([&] { fillVelGhostsTo(C[c].u, c, 0); },
1690 [&](int col) {
1691 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1692 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1693 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
1694 },
1695 [&](int col) {
1696 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1697 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1698 FPC(C[c].AB), FPC(C[c].AT), CCConst(C[c].mask), e_,
1699 og_, G, col);
1700 },
1702 return;
1703 }
1704 if (hasBc_ &&
1705 useVelocityMg_) { // domain-BC velocity multigrid: const-coeff aniso op + no-slip/inflow/
1706 // outflow boundary fold on every level (CUDA setDiffusionConstAllLevels +
1707 // setDiffusionBoundaryFold).
1708 vmg_.setDomainBcOp(c, mu_, rho_ / dt_); // per component (the fold is component-dependent)
1710 c, 1); // set the level-0 boundary ghosts (wall fold=0, inflow value, outflow zero-grad)
1711 // Re-impose the velocity BC on the vel-MG's level-0 iterate each colour/residual (the
1712 // const-coeff smoother updates the held Dirichlet faces) -> the vel-MG converges to the RB-GS
1713 // fixed point (not the ~2% drift CUDA's vmg leaves at the boundary corners).
1714 // (the hook applies the BC only: VelocityMG::fill owns the periodic wrap / halo exchange)
1715 vmg_.setBcApplyL0([this, c](CCField x) { applyVelocityBcCompTo(x, c, 1, true); });
1716 lastMomentumSweeps_ += vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8, velTol_,
1718 lastMomentumResid_ = std::max(lastMomentumResid_, vmg_.lastResidualRatio());
1719 return;
1720 }
1721 if (hasBc_) { // domain-BC (no immersed solid): CUDA's double const-coeff diff_k + dcorr fold
1722 // og_ is the GLOBAL block origin (red-black parity); {0,0,0} single-rank, so byte-identical
1723 // there. It was hard-coded {0,0,0} here, which swaps the colours on any rank whose block
1724 // origin has odd parity — the only smoother in the file that did not carry og_.
1725 const I3 e{e_.x, e_.y, e_.z}, og{og_.x, og_.y, og_.z};
1726 // b_a = mu' * w_a; Ac = rho/dt + 2*((bx+by)+bz) in EXACTLY that association order, which is
1727 // fl(6*mu_) at bx==by==bz (doc/anisotropic_metric.md §2, trap 3).
1728 const double bx = mu_ * u_.w[0], by = mu_ * u_.w[1], bz = mu_ * u_.w[2];
1729 const double Ac = rho_ / dt_ + 2.0 * ((bx + by) + bz);
1730 const bool an = u_.aniso;
1731 velSweepLoop([&] { fillVelGhosts(c, 1); }, // re-impose wall faces (fold) before each color
1732 [&](int col) {
1733 diffSmoothColor(C[c].u, CCConst(C[c].b), e, og, G, bx, by, bz, Ac, col,
1734 CCConst(bcDcorr_[c]), an);
1735 },
1736 [&](int col) {
1737 return diffSmoothColorDu(C[c].u, CCConst(C[c].b), e, og, G, bx, by, bz, Ac, col,
1738 CCConst(bcDcorr_[c]), an);
1739 },
1741 return;
1742 }
1743 if (useVelocityCheb_) {
1744 // Chebyshev momentum solver on the sharp cut-cell stencil. Reached only on the IBM/periodic
1745 // path: the collocated, mixed, bcStencilPath and domain-BC branches above have all returned,
1746 // so a configuration with domain BCs keeps the red-black smoother that owns its ghost fold.
1747 chebSolveComp(c);
1748 maskVelocity(c);
1749 return;
1750 }
1751 if (useVelocityMg_) { // IBM velocity multigrid: fine = sharp As_[c]; coarse op depends on the
1752 // regime.
1753 vmg_.setFineStencil(FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1754 FPC(C[c].AB), FPC(C[c].AT));
1755 if (implicitFou_ && advect_) {
1756 // UPWIND-CONVECTIVE coarse op (advection-dominated): aniso const-coeff diffusion + dt*FOU
1757 // from the restricted advecting velocity (restrictAdvVelocities ran once in step()). No pin
1758 // / no exclude mask.
1759 vmg_.buildUpwindCoarse(c, mu_, rho_ / dt_, rho_);
1760 } else {
1761 // STAIRCASE coarse op (diffusion-only): theta classification + clean-fluid exclude (exact
1762 // == RB-GS).
1763 const Off3 off =
1764 Grid::offset(c); // velocity-unknown placement (staggered: -1/2 face; collocated: 0)
1765 ibmVolfrac(vmgTheta_, CCConst(sdf_), e_, off, u_.aniso, u_.hp[0], u_.hp[1], u_.hp[2], u_.w[0],
1766 u_.w[1], u_.w[2]);
1767 ibmCleanFluidMask(vmgClean_, CCConst(sdf_), e_, off);
1768 vmg_.setStaircase(CCConst(vmgTheta_), CCConst(C[c].mask), CCConst(vmgClean_), mu_, rho_ / dt_,
1769 0.5);
1770 }
1771 lastMomentumSweeps_ += vmg_.solve(CCConst(C[c].b), C[c].u, vmgVcycles_, 2, 2, 8, velTol_,
1773 lastMomentumResid_ = std::max(lastMomentumResid_, vmg_.lastResidualRatio());
1774 maskVelocity(c); // re-impose no-slip at solid (the masked solve leaves them at the pin value)
1775 return;
1776 }
1777 // IBM / periodic: Robust-Scaled cut-cell stencil (float). The 7-point smoother reads faces
1778 // only -> the fused 1-kernel face fill suffices.
1779#ifdef PECLET_FLOW_MPI
1780 if (distributed_ && caMomentum_) {
1781 // Communication-avoiding pair (the momentum counterpart of CutcellMG::smooth's CA path):
1782 // ONE 2-deep exchange per red-black pair instead of one per colour — the velocity block is
1783 // g=2 already. Colour 0 overlaps the exchange with the interior sweep, then sweeps the
1784 // boundary shell PLUS the 1-deep ghost ring, redundantly recomputing the neighbour's
1785 // boundary cells from the same operands the neighbour uses (2-deep u ghosts; the ring rows
1786 // of the stencil/mask/rhs are exchanged below, so they are the owner's bit-exact values).
1787 // Colour 1 then sweeps with NO exchange: its boundary cells read only colour-0 ring cells,
1788 // which equal what a fresh exchange would have delivered — bit-identical at half the halo
1789 // events. The tolerance stop's colour-1 kernel is the ORIGINAL full-inner fused reduction
1790 // (host pencil form intact), so du matches the blocking path exactly.
1791 // Stencil + mask ring exchange: once per (re)build. The per-step machinery (implicit-FOU
1792 // Picard rebuilds, variable properties, implicit drag, eps-conservative porous) rewrites the
1793 // stencil every solve, so those paths re-exchange every solve — mirrors the step() rebuild
1794 // gates; a false positive costs 8 extra exchanges, a false negative would break the np>1
1795 // bit-exactness (the ring rows would read a stale operator).
1796 const bool perStepStencil = implicitAdv() || varProps_ || varRho_ || effVarRho() || hasDrag_;
1797 if (momStencilDirty_[c] || perStepStencil) {
1798 for (FV* a : {&C[c].AC, &C[c].AW, &C[c].AE, &C[c].AS, &C[c].AN, &C[c].AB, &C[c].AT})
1799 velDevF_->exchange(*a);
1800 velDev_->exchange(C[c].mask);
1801 momStencilDirty_[c] = false;
1802 }
1803 velDev_->exchange(C[c].b); // rhs ring (owner's inner values); fixed over the sweeps
1804 const C3 lo{G + 1, G + 1, G + 1}, hi{e_.x - G - 1, e_.y - G - 1, e_.z - G - 1};
1805 const C3 rlo{G - 1, G - 1, G - 1}, rhi{e_.x - G + 1, e_.y - G + 1, e_.z - G + 1};
1806 const C3 z0{0, 0, 0};
1808 [] {},
1809 [&](int col) {
1810 if (col == 0) {
1811 velDev_->exchangeBegin(C[c].u);
1812 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1813 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1814 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, col, lo, hi, z0, z0);
1815 velDev_->exchangeEnd(C[c].u);
1816 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1817 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1818 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, col, rlo, rhi, lo,
1819 hi);
1820 } else {
1821 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE),
1822 FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1823 CCConst(C[c].mask), e_, og_, G, col);
1824 }
1825 },
1826 [&](int col) {
1827 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1828 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1829 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
1830 },
1831 stencilResidual(c, /*exchange=*/true), stencilBnorm(c));
1832 return;
1833 }
1834 if (distributed_) {
1835 // Overlap the per-colour halo with the interior sweep (the momentum counterpart of the MG
1836 // smoothers' split, 3ace962): post the exchange, sweep the interior cells — whose 7-point
1837 // stencil reads no ghost — while the messages fly, complete it, then sweep the boundary
1838 // shell. A colour's cells never read same-colour cells, so interior-then-shell is
1839 // bit-identical to the blocking exchange + full sweep; the tolerance stop's max-increment
1840 // combines the two passes by max (order-independent). Only this periodic/IBM path overlaps:
1841 // the domain-BC paths re-impose ghost BCs each colour and keep the blocking order (the same
1842 // decision as VelocityMG's overlap). The exchange is posted INSIDE the colour lambda (fill
1843 // is a no-op) so the packed send values are exactly the blocking call's.
1844 const C3 ilo{G, G, G}, ihi{e_.x - G, e_.y - G, e_.z - G};
1845 const C3 lo{G + 1, G + 1, G + 1}, hi{e_.x - G - 1, e_.y - G - 1, e_.z - G - 1};
1846 const C3 z0{0, 0, 0};
1848 [] {},
1849 [&](int col) {
1850 velDev_->exchangeBegin(C[c].u);
1851 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE),
1852 FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1853 CCConst(C[c].mask), e_, og_, col, lo, hi, z0, z0);
1854 velDev_->exchangeEnd(C[c].u);
1855 ibmRbgsStencilColorBox(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE),
1856 FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT),
1857 CCConst(C[c].mask), e_, og_, col, ilo, ihi, lo, hi);
1858 },
1859 [&](int col) {
1860 velDev_->exchangeBegin(C[c].u);
1861 const double di = ibmRbgsStencilColorDuBox(
1862 C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS),
1863 FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT), CCConst(C[c].mask), e_, og_, col, lo, hi,
1864 z0, z0);
1865 velDev_->exchangeEnd(C[c].u);
1866 const double ds = ibmRbgsStencilColorDuBox(
1867 C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW), FPC(C[c].AE), FPC(C[c].AS),
1868 FPC(C[c].AN), FPC(C[c].AB), FPC(C[c].AT), CCConst(C[c].mask), e_, og_, col, ilo, ihi,
1869 lo, hi);
1870 return di > ds ? di : ds;
1871 },
1872 stencilResidual(c, /*exchange=*/true), stencilBnorm(c));
1873 return;
1874 }
1875#endif
1876 velSweepLoop([&] { fillGhostsFaces(C[c].u); },
1877 [&](int col) {
1878 ibmRbgsStencilColor(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1879 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN), FPC(C[c].AB),
1880 FPC(C[c].AT), CCConst(C[c].mask), e_, og_, G, col);
1881 },
1882 [&](int col) {
1883 return ibmRbgsStencilColorDu(C[c].u, CCConst(C[c].b), FPC(C[c].AC), FPC(C[c].AW),
1884 FPC(C[c].AE), FPC(C[c].AS), FPC(C[c].AN),
1885 FPC(C[c].AB), FPC(C[c].AT), CCConst(C[c].mask), e_,
1886 og_, G, col);
1887 },
1888 stencilResidual(c, /*exchange=*/false), stencilBnorm(c));
1889}
1890
1891template <class Grid>
1893 CCExec space;
1894 CCField u = C[c].u, m = C[c].mask;
1895 Kokkos::parallel_for(
1896 "vmask", Kokkos::RangePolicy<CCExec>(space, 0, n_), KOKKOS_LAMBDA(std::size_t i) {
1897 if (m(i) > 0.5)
1898 u(i) = 0.0;
1899 });
1900}
1901
1902template <class Grid>
1904 CCExec space;
1905 C3 e = e_;
1906 CCConst f = CCConst(muField_);
1907 double m = 1e300;
1908 Kokkos::parallel_reduce(
1909 "minmu", MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1910 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1911 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1912 if (f(i) < acc)
1913 acc = f(i);
1914 },
1915 Kokkos::Min<double>(m));
1916#ifdef PECLET_FLOW_MPI
1917 if (distributed_) {
1918 double g = m;
1919 MPI_Allreduce(&m, &g, 1, MPI_DOUBLE, MPI_MIN, comm_);
1920 m = g;
1921 }
1922#endif
1923 return m;
1924}
1925
1926template <class Grid>
1928 CCExec space;
1929 C3 e = e_;
1930 double m = 0;
1931 Kokkos::parallel_reduce(
1932 "maxabs", MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
1933 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1934 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1935 const double a = Kokkos::fabs(f(i));
1936 if (a > acc)
1937 acc = a;
1938 },
1939 Kokkos::Max<double>(m));
1940 return m;
1941}
1942
1943template <class Grid>
1945 auto h = Kokkos::create_mirror_view(fld);
1946 Kokkos::deep_copy(h, fld);
1947 std::vector<double> out((std::size_t)nx_ * ny_ * nz_);
1948 for (int z = 0; z < nz_; ++z)
1949 for (int y = 0; y < ny_; ++y)
1950 for (int x = 0; x < nx_; ++x)
1951 out[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_] =
1952 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y);
1953 return out;
1954}
1955
1956template <class Grid>
1957void Solver<Grid>::scatterInner(CCField fld, const std::vector<double>& in) {
1958 if (in.size() != (std::size_t)nx_ * ny_ * nz_)
1959 throw std::runtime_error("flow::setField: array size does not match the inner grid");
1960 auto h = Kokkos::create_mirror_view(fld);
1961 Kokkos::deep_copy(h, fld); // preserve existing ghosts
1962 for (int z = 0; z < nz_; ++z)
1963 for (int y = 0; y < ny_; ++y)
1964 for (int x = 0; x < nx_; ++x)
1965 h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y) =
1966 in[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_];
1967 Kokkos::deep_copy(fld, h);
1968}
1969
1970template <class Grid>
1971CCField Solver<Grid>::addField(const std::string& name) {
1972 if (fields_.has(name))
1973 return fields_.at(name).data;
1974 return fields_.add(name, n_, G, peclet::core::Centering::Cell).data;
1975}
1976
1977template <class Grid>
1978bool Solver<Grid>::hasField(const std::string& name) const {
1979 return fields_.has(name);
1980}
1981
1982template <class Grid>
1983CCField Solver<Grid>::fieldView(const std::string& name) {
1984 return fields_.at(name).data;
1985}
1986
1987template <class Grid>
1988std::vector<std::string> Solver<Grid>::fieldNames() const {
1989 return fields_.names();
1990}
1991
1992template <class Grid>
1993void Solver<Grid>::exchangeField(const std::string& name) {
1994 fillGhosts(fields_.at(name).data);
1995}
1996
1997template <class Grid>
1998void Solver<Grid>::exchangeFieldAdd(const std::string& name) {
1999#ifdef PECLET_FLOW_MPI
2000 if (distributed_ && velHalo_) {
2001 CCField f = fields_.at(name).data;
2002 auto h = Kokkos::create_mirror_view(f);
2003 Kokkos::deep_copy(h, f);
2004 peclet::core::halo::GridFieldView<double> view{h.data()};
2005 velHalo_->reverseAdd(view);
2006 Kokkos::deep_copy(f, h);
2007 }
2008#else
2009 (void)name;
2010#endif
2011}
2012
2013template <class Grid>
2014std::vector<double> Solver<Grid>::getField(const std::string& name) {
2015 return gatherInner(fields_.at(name).data);
2016}
2017
2018template <class Grid>
2019void Solver<Grid>::setField(const std::string& name, const std::vector<double>& v) {
2020 scatterInner(fields_.at(name).data, v);
2021 // The balanced-force split (doc/collocated_varrho_forces.md §4.6.2): a restart that restores
2022 // the pressure must restore "p_balanced" with it, or the next ON step re-splits. Flags only.
2023 if (name == "p")
2024 pWritten_ = true;
2025 else if (name == "p_balanced")
2026 pbWritten_ = true;
2027}
2028
2029template <class Grid>
2030std::array<int, 3> Solver<Grid>::blockShape() const {
2031 return {e_.x, e_.y, e_.z};
2032}
2033
2034template <class Grid>
2036 return G;
2037}
2038
2039template <class Grid>
2040std::array<int, 3> Solver<Grid>::globalResolution() const {
2041#ifdef PECLET_FLOW_MPI
2042 if (distributed_)
2043 return {gnx_, gny_, gnz_};
2044#endif
2045 return {nx_, ny_, nz_};
2046}
2047
2048template <class Grid>
2049std::array<int, 3> Solver<Grid>::blockOrigin() const {
2050 return {og_.x, og_.y, og_.z};
2051}
2052
2053template <class Grid>
2055 rhoFaceHarmonic_ = on;
2056 bfpChebSet_ = false; // a different face mean: the pre-projection's kept bounds are stale
2057}
2058
2059template <class Grid>
2061 exactResidual_ = on;
2062 mg_.setExactResidual(on);
2063}
2064
2065template <class Grid>
2067 outflowOpCoeff_ = on;
2068}
2069
2070template <class Grid>
2072 return outflowOpCoeff_;
2073}
2074
2075template <class Grid>
2077 return exactResidual_;
2078}
2079
2080template <class Grid>
2082 return rhoFaceHarmonic_;
2083}
2084
2085template <class Grid>
2087 outflowRhoCorr_ = on;
2088}
2089
2090template <class Grid>
2092 return outflowRhoCorr_;
2093}
2094
2095template <class Grid>
2096CCField Solver<Grid>::ensureTarget(const std::string& name) {
2097 if (name == "force_x" || name == "force_y" || name == "force_z")
2098 ensureCellForceAll();
2099 return addField(name); // idempotent; returns the (now-existing) buffer
2100}
2101
2102template <class Grid>
2103void Solver<Grid>::ensureCellForceAll() {
2104 static const char* fn[3] = {"force_x", "force_y", "force_z"};
2105 for (int c = 0; c < 3; ++c)
2106 cellForce_[c] = addField(fn[c]); // zero-initialised, registered
2107 hasCellForce_ = true;
2108}
2109
2110} // namespace peclet::flow
2111
2112#endif // PECLET_FLOW_FLOW_IBM_CORE_HPP
void setStrictPressure(bool on)
Throw instead of reporting when the preconditioner returns a non-finite correction.
void setCoarseGhost(bool on)
Neumann coarse ghost on wall/inflow faces before the prolongation (see bcGhost_).
void setAspectThreshold(double theta)
Anisotropic-coarsening aspect threshold theta (see aspectTheta_).
void setAgglomerationMode(int mode)
void setAgglomerationExtent(int cells)
void setExactResidual(bool on)
The exact double flux-form level-0 apply in the residual and the Krylov matvec.
void setMeanRemovalScope(bool all)
void setCommAvoiding(int mask)
Communication-avoiding smoothing mask (kCaMomentum | kCaMg); only kCaMg is read here.
int pressureBottomExtent() const
void setPressureMeanRemoval(bool all)
double multigridAspectThreshold() const
std::array< int, 3 > globalResolution() const
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 fillVelGhosts(int comp, int fold)
void setIncrementalPressure(bool on)
std::function< double()> constCoeffResidual(int c, double bx, double by, double bz, double Ac)
void setVelocityTolerance(double rtol, int minIters)
bool pressureCoarseGhost() const
void scatterInner(CCField fld, const std::vector< double > &in)
void buildOpenFaceField()
Build ufOpen_ = the face field with each open (outflow) domain face overwritten by its zero-gradient ...
void fillGhosts(CCField f)
long lastOuterIterations() const
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...
bool advWallInputs() const
R0 helper: allocate (once) and arm the per-component advective-term stash the reaction-force budget c...
void setUfAdvection(bool on)
Advect the collocated momentum with the PROJECTED, divergence-free MAC face field uf_/vf_/wf_ (DEFAUL...
void fillPropGhosts(CCField f, bool edges=false)
void setFaceInterp(int mode)
std::vector< double > getField(const std::string &name)
void setOutflowRhoCorrection(bool on)
void setVelocityChebyshev(bool on, int maxit=400)
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)
double lastMomentumResidual() const
std::array< double, 3 > domainOrigin() const
Physical lower corner of the GLOBAL inner grid.
void setPressureGraphAmg(bool on)
VelocityMG::Comm vmgComm() const
double velocityResidualTolerance() const
void setPressureTelescope(bool on)
void setPressureStrict(bool on)
Throw instead of reporting when the pressure preconditioner returns a non-finite correction (ISSUES s...
void fillGhostsFaces(CCField f)
bool advectionWallVelocity() const
void exchangeFieldAdd(const std::string &name)
CCConst advVelView(int c) const
The velocity view the advection operators must read for component c: the wall-corrected scratch while...
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 setPressureExactResidual(bool on)
WO-R2 item 3 — the exact (matrix-free, double, flux-form) level-0 operator apply in the residual and ...
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...
bool outflowRhoCorrection() const
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
void requireNoGeometry(const char *who) const
void applyVelocityBcCompTo(CCField f, int comp, int fold, bool doOutflow)
double maxAbsDiffInner(CCConst a, CCConst b)
void fillPeriodicAxes(CCField f, int axes)
void requireIsotropic(const char *what) const
Phase 2 §7 — refuse an ANISOTROPIC domain in a consumer this phase does not carry.
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...
bool velocityChebyshevActive() const
void setVelocityIterations(int it)
double finishResidual(int c)
bool ufAdvVelocity() const
Are the advection operators reading the projected face field this step? Collocated grid,...
void setPressureChebyshev(bool on, int maxit, double rtol)
const UnitScales & unitScales() const
CCConst openFaceView(int c) const
Component c of the face field AS THE ADVECTION AND THE DIVERGENCE DIAGNOSTIC MUST READ IT: advFaceVie...
bool pressureStrict() const
Solver(int nx, int ny, int nz)
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
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...
long lastMomentumSweeps() const
void setPressureLevels(int levels)
std::array< int, 3 > blockOrigin() const
void setFluidOnlyConstraint(int mode)
void setRotationalPressure(bool on)
void setDeferredCorrection(bool on)
void resampleBcProfile(int face)
void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow=true)
static constexpr int G
Definition flow_ibm.hpp:80
SceneMap sceneMap() 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 setField(const std::string &name, const std::vector< double > &v)
void setApertureOrder(int order)
double lastSuperficialVelocityShift() const
The shift the last step() applied (caller's velocity units; 0 when off).
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 closureRefreshUnits(Closure &cl)
Re-derive a closure's internal p / tabY from its physical pPhys / tabYPhys with the current reference...
std::array< int, 3 > blockShape() const
void setRhoFaceHarmonic(bool on)
bool pressureExactResidual() const
long strideOf(int c) const
bool hasField(const std::string &name) const
void setAdvectionScheme(int s)
void throwVelocityMgVariableMu(const char *who) const
int pressureTelescopeCount() const
void setPressurePcg(bool on, int maxit, double rtol)
void setVelocityMultigridAuto(long cellsPerRank, long minGlobalCells=-1, int minBlockExtent=-1)
void allocateBlock(int nx, int ny, int nz)
void setVelocityMultigrid(bool on, int levels, int vcycles)
void setRotationalWeight(double w)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setDecomposition(int levels, double maxImbalance=1.05)
How the shared level-0 MPI decomposition is built.
void setExactCrossings(const std::vector< double > &t)
void setPressureWarmstart(bool on)
void velSweepLoop(Fill &&fill, Color &&sweepColor, ColorDu &&sweepColorDu, std::function< double()> resid=nullptr, double bnorm=0.0)
void setPressureIterations(int it)
Kokkos::View< MReal *, CCMem > FV
Definition flow_ibm.hpp:79
bool velocityMgChebyshev() const
void refreshUnitDerived()
Re-derive every internal (index-unit) quantity from the stored physical inputs.
void setPressureBottomSolver(int engine)
void setPressureTelescopeForceLevel(int level)
CCField fieldView(const std::string &name)
void pushVofMetric()
Hand the anisotropic cell metric to every VoF driver that exists (Phase 3).
void setCollocatedScheme(const std::string &name)
void setVelocityMgChebyshev(bool on, int degree=0, double eig_ratio=10.0)
void setPressureBottomExtent(int cells)
The set_pressure_bottom("auto") criterion: agglomerate once the coarsest GLOBAL grid exceeds this man...
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,...
void setImplicitAdvection(bool on)
void setVelocityResidualTolerance(double rtol)
std::function< double()> stencilResidual(int c, bool exchange=false)
bool mixedVelocityMg() const
void setPressureBottomMode(int mode)
VarFaceProps makeFaceProps(int c)
void setRotationalWallWeight(double w0)
bool pressureTelescope() const
double reduceMaxAbsInner(CCConst f)
void setRho(double r)
Fluid density, in the caller's units.
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 ...
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
CCConst wallVelView(int c) const
bool outflowOperatorCoefficient() const
void setPressureFcg(bool on, int maxit, double rtol)
void setMultigridAspectThreshold(double theta)
The anisotropic-coarsening aspect threshold theta shared by the pressure and velocity multigrids (doc...
void setOuterIterations(int iters)
bool hasPhysicalDomain() const
void setFineStencil(FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT)
void buildUpwindCoarse(int comp, double nu_dt, double idiag, double fouw)
void setChebyshevSmoother(bool on, int degree=0, double eigRatio=10.0)
void setStaircaseBc(int comp, CCConst theta0, CCConst solid0, CCConst resmask0, double nu_dt, double idiag, double thresh, bool upwind=false, double fouw=0.0)
int solve(CCConst b, CCField x, int nvc, int pre, int post, int bottom, double tol=0.0, Comm comm=nullComm(), double resTol=0.0)
void setBcApplyL0(std::function< void(CCField)> fn)
void setDomainBcOp(int comp, double nu_dt, double idiag)
void setAspectThreshold(double theta)
Anisotropic-coarsening aspect threshold theta (doc/anisotropic_metric.md §5.1).
void setStaircase(CCConst theta0, CCConst solid0, CCConst resmask0, double nu_dt, double idiag, double thresh)
VofMetric metric
The anisotropic cell metric (Phase 3).
VofMetric metric
The anisotropic cell metric (Phase 3); {1,1,1} == the pre-Phase-3 arithmetic.
VofMetric metric
The interfacial predicate's wisp threshold — the phase-change driver passes ITS OWN (pcEffInterfaceEp...
VofMetric metric
Which VofWettingPivot anchors the theta-plane (a measured ablation; see wetting.hpp).
void zeroPlane(CCField m, C3 e, int axis, int idx)
void ibmChebUpdate(CCField x, CCField d, CCConst r, FPC AC, CCConst solidmask, C3 e, int g, double alpha, double beta)
One Chebyshev update on the Jacobi-preconditioned system: d <- beta*d + alpha*r/AC ; x += d.
Kokkos::View< const MReal *, CCMem > FPC
void ibmCleanFluidMask(CCField m, CCConst sdf, C3 ext, Off3 off)
Definition mac_ibm.hpp:262
void ibmStencilJacobiBounds(MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, CCConst solidmask, C3 e, int g, double &lo, double &hi)
Gershgorin bounds [lo, hi] on the spectrum of the Jacobi-preconditioned cut-cell Helmholtz diag(AC)^-...
void ibmBuildDiffusionVar(MV AC, MV AW, MV AE, MV AS, MV AN, MV AB, MV AT, int ex, int ey, int ez, int g, FaceProps fp, double wx=1.0, double wy=1.0, double wz=1.0)
void ibmModifyStencil(MV AC, MV AW, MV AE, MV AS, MV AN, MV AB, MV AT, Kokkos::View< double *, IMem > a_inhom, Kokkos::View< double *, IMem > rhs_scale, const OV &ibm, int numActive, typename OV::value_type u_bc_val, Kokkos::View< const double *, IMem > u_bc=Kokkos::View< const double *, IMem >())
void ibmVolfrac(CCField theta, CCConst sdf, C3 ext, Off3 off, bool aniso=false, double hpx=1.0, double hpy=1.0, double hpz=1.0, double wx=1.0, double wy=1.0, double wz=1.0)
Definition mac_ibm.hpp:188
void residualVarPin(CCField r, CCConst x, CCConst b, FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT, CCConst pin, C3 e, int g)
double diffSmoothColorDu(SField c, SConst b, I3 e, I3 og, int g, double bx, double by, double bz, double Ac, int color, SConst dcorr, bool aniso)
void centerToFaceWallAware(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, CCConst sdf, CCConst xcx, CCConst xcy, CCConst xcz, bool useCen, C3 e, int g)
void diffResidual(SField r, SConst c, SConst b, I3 e, int g, double bx, double by, double bz, double Ac, SConst dcorr, bool aniso)
double maxAbsInner(CCConst a, C3 e, int g)
void ccFor3(const char *name, C3 lo, C3 hi, F f)
void centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
double maxAbsDiffInner(CCConst a, CCConst b, C3 e, int g)
void ibmRbgsStencilColorBox(CCField x, CCConst b, MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, CCConst solidmask, C3 ext, C3 og, int color, C3 rlo, C3 rhi, C3 slo, C3 shi)
Definition mac_ibm.hpp:410
Kokkos::DefaultExecutionSpace CCExec
double ibmRbgsStencilColorDuBox(CCField x, CCConst b, MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, CCConst solidmask, C3 ext, C3 og, int color, C3 rlo, C3 rhi, C3 slo, C3 shi)
Definition mac_ibm.hpp:446
void ibmBuildDiffusion(MV AC, MV AW, MV AE, MV AS, MV AN, MV AB, MV AT, int ex, int ey, int ez, double bx, double by, double bz, double idiag)
double ibmRbgsStencilColorDu(CCField x, CCConst b, MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, CCConst solidmask, C3 ext, C3 og, int g, int color)
Definition mac_ibm.hpp:342
bool hostRunSerial(long cells)
void bcNeumannGhost(BField f, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:246
void ibmRbgsStencilColor(CCField x, CCConst b, MC AC, MC AW, MC AE, MC AS, MC AN, MC AB, MC AT, CCConst solidmask, C3 ext, C3 og, int g, int color)
Definition mac_ibm.hpp:287
void diffSmoothColor(SField c, SConst b, I3 e, I3 og, int g, double bx, double by, double bz, double Ac, int color, SConst dcorr, bool aniso)
Kokkos::View< const double *, CCMem > CCConst
The Chebyshev coefficient recurrence (Golub-Varga; the form multigrid Chebyshev smoothers use).
void next(int it, double &alpha, double &beta)
Coefficients for iteration it (0-based). Advances the recurrence.
The map between the solver's INDEX coordinates and the coordinate system the analytic scene is writte...
Definition flow_ibm.hpp:241
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 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
VofMetric metric
The anisotropic cell metric (Phase 3, V4.3); {1,1,1} == the pre-Phase-3 arithmetic.
static constexpr int N
static constexpr double AC
static constexpr double U