flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mac_velocity_mg.hpp
Go to the documentation of this file.
1
18#ifndef PECLET_FLOW_MAC_VELOCITY_MG_HPP
19#define PECLET_FLOW_MAC_VELOCITY_MG_HPP
20
21#include <functional>
22#include <Kokkos_Core.hpp>
23#include <string>
24#include <vector>
25
26#include "mac_cheb_momentum.hpp" // ibmStencilJacobiBounds, ibmChebUpdate, ChebCoeffs
27#include "mac_cutcell_mg.hpp" // restrictAvg, prolongAdd, FPV/FPC
28#include "mac_ibm.hpp" // ibmRbgsStencilColor (pin smoother), MConst
29#include "policy.hpp"
30#include "staggered_advection.hpp" // fou_operator_aniso (upwind-convective coarse op)
31
32namespace peclet::flow {
33
34// pin-aware variable-coefficient residual (mg_residual_var_k): r = 0 at pinned (classified-solid)
35// cells, else b - A x with the float operator accumulated in double.
36inline void residualVarPin(CCField r, CCConst x, CCConst b, FPC AC, FPC AW, FPC AE, FPC AS, FPC AN,
37 FPC AB, FPC AT, CCConst pin, C3 e, int g) {
38 const bool hasPin = (pin.extent(0) != 0);
39 ccFor3( // rule H (doc/vof_step_performance_design.md §4.5): host pencil, device MDRange
40 "peclet::flow::vmg_resid", C3{g, g, g}, C3{e.x - g, e.y - g, e.z - g},
41 KOKKOS_LAMBDA(int lx, int ly, int lz) {
42 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
43 const long i = (long)lx + (long)ly * sy + (long)lz * sz;
44 if (hasPin && pin(i) > 0.5) {
45 r(i) = 0.0;
46 return;
47 }
48 const double Ax = (double)AC(i) * x(i) + (double)AE(i) * x(i + sx) +
49 (double)AW(i) * x(i - sx) + (double)AN(i) * x(i + sy) +
50 (double)AS(i) * x(i - sy) + (double)AT(i) * x(i + sz) +
51 (double)AB(i) * x(i - sz);
52 r(i) = b(i) - Ax;
53 });
54}
55
56// masked trilinear prolongation (mg_prolong_masked_k): like prolongAdd but does NOT add the coarse
57// correction into a fine cell whose mask < eps (the clean-fluid exclude mask is 0 at IBM cut+solid
58// cells).
60 C3 finner, C3 ratio, double eps) {
62 using MD = MDRange3<CCExec>;
63 Kokkos::parallel_for(
64 "peclet::flow::vmg_prolong_masked", MD(space, {0, 0, 0}, {finner.x, finner.y, finner.z}),
65 KOKKOS_LAMBDA(int ifx, int ify, int ifz) {
66 const long fi =
67 (long)(ifx + g) + (long)(ify + g) * fext.x + (long)(ifz + g) * (long)fext.x * fext.y;
68 if (mask(fi) < eps)
69 return; // no correction into a cut/solid fine cell
70 const double cx = (ratio.x == 2) ? 0.5 * ifx - 0.25 + g : ifx + g;
71 const double cy = (ratio.y == 2) ? 0.5 * ify - 0.25 + g : ify + g;
72 const double cz = (ratio.z == 2) ? 0.5 * ifz - 0.25 + g : ifz + g;
73 const double fxw = Kokkos::floor(cx), fyw = Kokkos::floor(cy), fzw = Kokkos::floor(cz);
74 const double wx = cx - fxw, wy = cy - fyw, wz = cz - fzw;
75 const int x0 = (int)fxw, y0 = (int)fyw, z0 = (int)fzw;
76 const long sy = cext.x, sz = (long)cext.x * cext.y;
77 auto C = [&](int xx, int yy, int zz) {
78 return coarse((long)xx + (long)yy * sy + (long)zz * sz);
79 };
80 const double c00 = C(x0, y0, z0) * (1 - wx) + C(x0 + 1, y0, z0) * wx;
81 const double c10 = C(x0, y0 + 1, z0) * (1 - wx) + C(x0 + 1, y0 + 1, z0) * wx;
82 const double c01 = C(x0, y0, z0 + 1) * (1 - wx) + C(x0 + 1, y0, z0 + 1) * wx;
83 const double c11 = C(x0, y0 + 1, z0 + 1) * (1 - wx) + C(x0 + 1, y0 + 1, z0 + 1) * wx;
84 const double c0 = c00 * (1 - wy) + c10 * wy, c1 = c01 * (1 - wy) + c11 * wy;
85 fine(fi) += c0 * (1 - wz) + c1 * wz;
86 });
87}
88
89// STAIRCASE coarse operator (mg_build_velocity_op_staircase_k): theta<thresh -> identity (pinned)
90// row; else a plain const-coeff Helmholtz (idiag + 2*(bx+by+bz) diagonal, per-axis -b
91// off-diagonals).
92inline void buildVelocityStaircase(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT,
93 CCConst theta, C3 e, int g, double bx, double by, double bz,
94 double thresh, double idiag) {
96 using MD = MDRange3<CCExec>;
97 Kokkos::parallel_for(
98 "peclet::flow::vmg_staircase", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
99 KOKKOS_LAMBDA(int lx, int ly, int lz) {
100 const long i = (long)lx + (long)ly * e.x + (long)lz * (long)e.x * e.y;
101 if (theta(i) <
102 thresh) { // classified solid -> identity row (smoother/residual pin it to 0)
103 AC(i) = (MReal)1.0;
104 AW(i) = AE(i) = AS(i) = AN(i) = AB(i) = AT(i) = (MReal)0.0;
105 return;
106 }
107 AC(i) = (MReal)(idiag + 2.0 * (bx + by + bz));
108 AW(i) = (MReal)(-bx);
109 AE(i) = (MReal)(-bx);
110 AS(i) = (MReal)(-by);
111 AN(i) = (MReal)(-by);
112 AB(i) = (MReal)(-bz);
113 AT(i) = (MReal)(-bz);
114 });
115}
116
117// classified-solid rows (theta < thresh) -> identity, over the inner cells (the staircase pin
118// applied after an operator builder that wrote every row).
119inline void pinSolidRows(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, CCConst theta,
120 C3 e, int g, double thresh) {
122 using MD = MDRange3<CCExec>;
123 Kokkos::parallel_for(
124 "peclet::flow::vmg_pin_rows", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
125 KOKKOS_LAMBDA(int lx, int ly, int lz) {
126 const long i = (long)lx + (long)ly * e.x + (long)lz * (long)e.x * e.y;
127 if (theta(i) < thresh) {
128 AC(i) = (MReal)1.0;
129 AW(i) = AE(i) = AS(i) = AN(i) = AB(i) = AT(i) = (MReal)0.0;
130 }
131 });
132}
133
134// UPWIND-CONVECTIVE coarse operator (build_adv_coarse_stencil_k): anisotropic const-coeff
135// backward-Euler diffusion (per-axis beta bx/by/bz) PLUS first-order-upwind advection from the
136// restricted coarse advecting velocity (scaled by s_a=1/cfac_a per face axis). M-matrix on every
137// level -> stable in the advection- dominated rows. The fine residual + smoother give the exact
138// sharp-IBM answer; this only sets the rate.
139inline void buildAdvCoarse(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, CCConst U,
140 CCConst V, CCConst W, int comp, C3 e, int g, double bx, double by,
141 double bz, double fouw, double sx, double sy, double sz, double idiag) {
143 using MD = MDRange3<CCExec>;
144 Kokkos::parallel_for(
145 "peclet::flow::vmg_adv_coarse", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
146 KOKKOS_LAMBDA(int x, int y, int z) {
147 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
148 double cC = idiag + 2.0 * (bx + by + bz), cxm = -bx, cxp = -bx, cym = -by, cyp = -by,
149 czm = -bz, czp = -bz;
150 sadv::ViewAcc Ua{U, e.x, e.y}, Va{V, e.x, e.y}, Wa{W, e.x, e.y};
151 sadv::fou_operator_aniso(comp, x, y, z, Ua, Va, Wa, fouw, sx, sy, sz, cC, cxm, cxp, cym,
152 cyp, czm, czp);
153 AC(i) = (MReal)cC;
154 AW(i) = (MReal)cxm;
155 AE(i) = (MReal)cxp;
156 AS(i) = (MReal)cym;
157 AN(i) = (MReal)cyp;
158 AB(i) = (MReal)czm;
159 AT(i) = (MReal)czp;
160 });
161}
162
163// CONST-COEFF anisotropic Helmholtz A = idiag*I - nu_dt*Lap (mg_const_diffusion_op_aniso_k), over
164// the WHOLE extended block (the domain-BC velocity op; coarse spacing via per-axis beta). For
165// cavity/BFS where the fine op is also const-coeff (no IBM stencil).
166inline void buildConstAniso(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, C3 e, double bx,
167 double by, double bz, double idiag) {
169 const std::size_t n = (std::size_t)e.x * e.y * e.z;
170 const MReal c = (MReal)(idiag + 2.0 * (bx + by + bz)), nx = (MReal)(-bx), ny = (MReal)(-by),
171 nz = (MReal)(-bz);
172 Kokkos::parallel_for(
173 "peclet::flow::vmg_const_aniso", Kokkos::RangePolicy<CCExec>(space, 0, n),
174 KOKKOS_LAMBDA(std::size_t i) {
175 AC(i) = c;
176 AW(i) = nx;
177 AE(i) = nx;
178 AS(i) = ny;
179 AN(i) = ny;
180 AB(i) = nz;
181 AT(i) = nz;
182 });
183}
184
185// No-slip face-fold for the const-coeff MG operator (mg_diffusion_bc_fold_k): at a Dirichlet wall
186// the tangential ghost is 2*u_wall - u_inner -> +beta moves onto the boundary-adjacent inner cell's
187// diagonal (AC += beta); the dropped off-diagonal multiplies a held-0 ghost. Over the perp plane of
188// face (a,s).
189inline void boundaryFold(FPV AC, C3 e, int g, int a, int s, double beta) {
191 int dims[3] = {e.x, e.y, e.z};
192 long st[3] = {1, e.x, (long)e.x * e.y};
193 const int b = (a + 1) % 3, c = (a + 2) % 3;
194 const long sa = st[a], sb = st[b], sc = st[c];
195 const int bic = (s == 0) ? g : (dims[a] - g - 1); // boundary-adjacent inner cell along a
196 Kokkos::parallel_for(
197 "peclet::flow::vmg_bc_fold", MDRange2<CCExec>(space, {0, 0}, {dims[b], dims[c]}),
198 KOKKOS_LAMBDA(int p0, int p1) {
199 const long i = (long)p0 * sb + (long)p1 * sc + (long)bic * sa;
200 AC(i) = (MReal)((double)AC(i) + beta);
201 });
202}
203
204// Fill a non-periodic boundary ghost of a coarse correction before trilinear prolongation
205// (mg_fill_bc_ghost_k): Dirichlet (outflow) -> ghost 0; Neumann (wall/inflow) -> ghost = nearest
206// inner (zero-gradient). Plane (a,s).
207inline void fillBcGhost(CCField x, C3 e, int g, int a, int s, int dirichlet) {
209 int dims[3] = {e.x, e.y, e.z};
210 long st[3] = {1, e.x, (long)e.x * e.y};
211 const int b = (a + 1) % 3, c = (a + 2) % 3;
212 const long sa = st[a], sb = st[b], sc = st[c];
213 const int na = dims[a];
214 Kokkos::parallel_for(
215 "peclet::flow::vmg_fill_bc_ghost", MDRange2<CCExec>(space, {0, 0}, {dims[b], dims[c]}),
216 KOKKOS_LAMBDA(int p0, int p1) {
217 const long base = (long)p0 * sb + (long)p1 * sc;
218 if (s == 0) {
219 const double v = dirichlet ? 0.0 : x(base + (long)g * sa);
220 for (int ia = 0; ia < g; ++ia)
221 x(base + (long)ia * sa) = v;
222 } else {
223 const double v = dirichlet ? 0.0 : x(base + (long)(na - g - 1) * sa);
224 for (int ia = na - g; ia < na; ++ia)
225 x(base + (long)ia * sa) = v;
226 }
227 });
228}
229
230// zero a field on the plane at index `idx` along `axis` (held-Dirichlet boundary-face residual
231// exclude).
232inline void zeroPlane(CCField m, C3 e, int axis, int idx) {
234 int dims[3] = {e.x, e.y, e.z};
235 long st[3] = {1, e.x, (long)e.x * e.y};
236 const int b = (axis + 1) % 3, c = (axis + 2) % 3;
237 const long sa = st[axis], sb = st[b], sc = st[c];
238 Kokkos::parallel_for(
239 "peclet::flow::vmg_zero_plane", MDRange2<CCExec>(space, {0, 0}, {dims[b], dims[c]}),
240 KOKKOS_LAMBDA(int p0, int p1) { m((long)p0 * sb + (long)p1 * sc + (long)idx * sa) = 0.0; });
241}
242
243inline void thresholdMask(CCField m, CCConst theta,
244 double thresh) { // m = 1 where theta < thresh (solid)
246 std::size_t n = m.extent(0);
247 CCField mm = m;
248 CCConst th = theta;
249 Kokkos::parallel_for(
250 "peclet::flow::vmg_threshold", Kokkos::RangePolicy<CCExec>(space, 0, n),
251 KOKKOS_LAMBDA(std::size_t i) { mm(i) = (th(i) < thresh) ? 1.0 : 0.0; });
252}
253// max |a| over the inner cells of a G-ghosted block.
254inline double maxAbsInner(CCConst a, C3 e, int g) {
256 using MD = MDRange3<CCExec>;
257 double m = 0.0;
258 Kokkos::parallel_reduce(
259 "peclet::flow::vmg_maxabs", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
260 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
261 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
262 const double d = Kokkos::fabs(a(i));
263 if (d > acc)
264 acc = d;
265 },
266 Kokkos::Max<double>(m));
267 return m;
268}
269// max |a - b| over the inner cells of a G-ghosted block (the V-cycle update norm).
270inline double maxAbsDiffInner(CCConst a, CCConst b, C3 e, int g) {
272 using MD = MDRange3<CCExec>;
273 double m = 0.0;
274 Kokkos::parallel_reduce(
275 "peclet::flow::vmg_maxabsdiff", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
276 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
277 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
278 const double d = Kokkos::fabs(a(i) - b(i));
279 if (d > acc)
280 acc = d;
281 },
282 Kokkos::Max<double>(m));
283 return m;
284}
285inline void mulMask(CCField r, CCConst m) { // r *= m (clean-fluid residual filter)
287 std::size_t n = r.extent(0);
288 CCField rr = r;
289 CCConst mm = m;
290 Kokkos::parallel_for(
291 "peclet::flow::vmg_mulmask", Kokkos::RangePolicy<CCExec>(space, 0, n),
292 KOKKOS_LAMBDA(std::size_t i) { rr(i) *= mm(i); });
293}
294
295// Velocity (momentum) geometric multigrid with the staircase coarse operator. All levels ghost
296// width G=2.
298 public:
299 static constexpr int G = 2;
300#ifdef PECLET_FLOW_MPI
301 using Comm = MPI_Comm;
302 static MPI_Comm nullComm() { return MPI_COMM_NULL; }
303#else
304 using Comm = void*; // non-MPI build: solve()'s communicator argument is inert
305 static void* nullComm() { return nullptr; }
306#endif
307 struct Level {
308 C3 ext, inner, ratio{2, 2, 2}, cfac{1, 1, 1};
309 C3 og{0, 0, 0}; // block inner origin (global red-black parity); {0,0,0} single-rank
310 C3 gdim{0, 0, 0}; // GLOBAL dims of this level (og + inner == gdim -> owns the +face)
311 std::size_t n = 0;
313 CCField cd; // Chebyshev search direction (chebSmooth_; allocated on first use)
314 double chebHi = -1.0; // Gershgorin lambda_max of diag(AC)^-1 A, GLOBAL; set per solve()
315 CCField advU, advV, advW; // restricted advecting velocity (upwind-convective coarse op; L>=1)
316 FPV AC, AW, AE, AS, AN, AB, AT;
317#ifdef PECLET_FLOW_MPI
318 std::shared_ptr<GridHaloTopology<3>> halo; // per-level topology (decomposed)
319 std::shared_ptr<GridHalo<double>> dev; // per-level ghost exchange (ghost width G=2)
320#endif
321 };
322
329 void setMetric(const double w[3], const double hp[3]) {
330 for (int a = 0; a < 3; ++a) {
331 w_[a] = w[a];
332 hp_[a] = hp[a];
333 }
334 aniso_ = !(hp_[0] == 1.0 && hp_[1] == 1.0 && hp_[2] == 1.0);
335 }
338 std::vector<C3> levelRatios() const {
339 std::vector<C3> r;
340 r.reserve(lv_.size());
341 for (const auto& v : lv_)
342 r.push_back(v.ratio);
343 return r;
344 }
345
346 // periodic uniform hierarchy (halve each axis while even and >=2, capped at nLevels).
347 void init(int nx, int ny, int nz, int nLevels) {
348 lv_.clear();
349 C3 inner{nx, ny, nz}, cf{1, 1, 1};
350 for (int L = 0; L < nLevels; ++L) {
351 Level v;
352 v.inner = inner;
353 v.gdim = inner;
354 v.ext = C3{inner.x + 2 * G, inner.y + 2 * G, inner.z + 2 * G};
355 v.cfac = cf;
356 v.n = (std::size_t)v.ext.x * v.ext.y * v.ext.z;
357 auto can = [&](int d) { return (d % 2 == 0) && (d / 2 >= 2); };
358 C3 next = inner;
359 C3 ratio{1, 1, 1};
360 if (L + 1 < nLevels) {
361 // The SHARED aspect-ratio rule (doc/anisotropic_metric.md §5.3): the same helper, the same
362 // hp, so this hierarchy's level table is the pressure hierarchy's on the same grid.
363 const bool canA[3] = {can(inner.x), can(inner.y), can(inner.z)};
364 const double H[3] = {hp_[0] * (double)cf.x, hp_[1] * (double)cf.y, hp_[2] * (double)cf.z};
365 ratio = CutcellMG::mgChooseRatio(H, canA, aniso_, aspectTheta_);
366 if (ratio.x == 2)
367 next.x = inner.x / 2;
368 if (ratio.y == 2)
369 next.y = inner.y / 2;
370 if (ratio.z == 2)
371 next.z = inner.z / 2;
372 }
373 v.ratio = ratio;
374 v.x = CCField("vmg_x", v.n);
375 v.rhs = CCField("vmg_rhs", v.n);
376 v.res = CCField("vmg_res", v.n);
377 v.theta = CCField("vmg_th", v.n);
378 v.pin = CCField("vmg_pin", v.n);
379 if (L > 0) { // coarse advecting velocity for the upwind-convective coarse op
380 v.advU = CCField("vmg_au", v.n);
381 v.advV = CCField("vmg_av", v.n);
382 v.advW = CCField("vmg_aw", v.n);
383 }
384 for (FPV* p : {&v.AC, &v.AW, &v.AE, &v.AS, &v.AN, &v.AB, &v.AT})
385 *p = FPV("vmg_A", v.n);
386 lv_.push_back(v);
387 if (next.x == inner.x && next.y == inner.y && next.z == inner.z)
388 break;
389 inner = next;
390 cf = C3{cf.x * ratio.x, cf.y * ratio.y, cf.z * ratio.z};
391 }
392 lv_[0].resMask =
393 CCField("vmg_resmask0", lv_[0].n); // level 0 only (clean-fluid exclude, staircase path)
394 }
395#ifdef PECLET_FLOW_MPI
396 // Multi-rank velocity-MG: coarsen the GLOBAL grid 2:1 per level; each level gets its own G=2 core
397 // halo. No global reductions here (the velocity op is non-singular -> no mean removal, no
398 // Krylov), so the fold is just fill()->exchange + the block-origin red-black parity. Single-rank
399 // (size 1) == init().
400 void initMpi(int gnx, int gny, int gnz, int nLevels, MPI_Comm comm) {
401 int size = 1;
402 MPI_Comm_size(comm, &size);
403 initMpi(peclet::core::decomp::BlockDecomposer<3>(static_cast<std::size_t>(size),
404 peclet::core::IVec<3>{gnx, gny, gnz}),
405 nLevels, comm, /*inPlace=*/false);
406 }
407 // The solver's hierarchy: level 0 on the caller's decomposition (the velocity block IbmSolver
408 // already exchanges on), coarse levels = that decomposition coarsened IN PLACE, so an axis
409 // coarsens only while every rank's block origin and size are even on it (the same gate as
410 // CutcellMG without telescoping; with inPlace=false a fresh plain ORB is built per level, which
411 // only the standalone ctest wants). Every rank sees the same replicated decomposition, so the
412 // level count agrees globally with no communication.
413 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec0, int nLevels, MPI_Comm comm,
414 bool inPlace = true) {
415 lv_.clear();
416 distributed_ = true;
417 int rank = 0, size = 1;
418 MPI_Comm_rank(comm, &rank);
419 MPI_Comm_size(comm, &size);
420 std::array<bool, 3> per{true, true, true};
421 const auto& g0 = dec0.globalSize();
422 C3 gs{(int)g0[0], (int)g0[1], (int)g0[2]}, cf{1, 1, 1};
423 auto can = [&](int d) { return (d % 2 == 0) && (d / 2 >= 2); };
424 auto evenOn = [](const peclet::core::decomp::BlockDecomposer<3>& d, int ax) {
425 for (std::size_t b = 0; b < d.numBlocks(); ++b) {
426 const auto blk = d.block(b);
427 if (blk.origin[ax] % 2 != 0 || blk.size[ax] % 2 != 0)
428 return false;
429 }
430 return true;
431 };
432 peclet::core::decomp::BlockDecomposer<3> dec = dec0;
433 for (int L = 0; L < nLevels; ++L) {
434 Level v;
435 v.halo = std::make_shared<GridHaloTopology<3>>();
436 if (!inPlace && L > 0)
437 dec = peclet::core::decomp::BlockDecomposer<3>(static_cast<std::size_t>(size),
438 peclet::core::IVec<3>{gs.x, gs.y, gs.z});
439 v.halo->buildTopology(dec, rank, G, per, comm);
440 v.dev = std::make_shared<GridHalo<double>>();
441 v.dev->init(*v.halo);
442 v.dev->setLabel("vmg L" + std::to_string(L));
443 const auto& idx = v.halo->indexer();
444 const auto eg = idx.sizeInclGhost(), ino = idx.sizeInner(), oig = idx.originInclGhost();
445 v.ext = {(int)eg[0], (int)eg[1], (int)eg[2]};
446 v.inner = {(int)ino[0], (int)ino[1], (int)ino[2]};
447 v.og = {(int)oig[0] + G, (int)oig[1] + G, (int)oig[2] + G};
448 v.gdim = gs;
449 v.cfac = cf;
450 v.n = idx.numCellsInclGhost();
451 C3 next = gs, ratio{1, 1, 1};
452 if (L + 1 < nLevels) {
453 // §5.3, distributed: today's candidate set (global can() + the even-block gate), then the
454 // aspect rule. A pure function of replicated data on every rank, like CutcellMG's.
455 const bool canA[3] = {can(gs.x) && (!inPlace || evenOn(dec, 0)),
456 can(gs.y) && (!inPlace || evenOn(dec, 1)),
457 can(gs.z) && (!inPlace || evenOn(dec, 2))};
458 const double H[3] = {hp_[0] * (double)cf.x, hp_[1] * (double)cf.y, hp_[2] * (double)cf.z};
459 ratio = CutcellMG::mgChooseRatio(H, canA, aniso_, aspectTheta_);
460 if (ratio.x == 2)
461 next.x = gs.x / 2;
462 if (ratio.y == 2)
463 next.y = gs.y / 2;
464 if (ratio.z == 2)
465 next.z = gs.z / 2;
466 }
467 v.ratio = ratio;
468 v.x = CCField("vmg_x", v.n);
469 v.rhs = CCField("vmg_rhs", v.n);
470 v.res = CCField("vmg_res", v.n);
471 v.theta = CCField("vmg_th", v.n);
472 v.pin = CCField("vmg_pin", v.n);
473 if (L > 0) {
474 v.advU = CCField("vmg_au", v.n);
475 v.advV = CCField("vmg_av", v.n);
476 v.advW = CCField("vmg_aw", v.n);
477 }
478 for (FPV* p : {&v.AC, &v.AW, &v.AE, &v.AS, &v.AN, &v.AB, &v.AT})
479 *p = FPV("vmg_A", v.n);
480 lv_.push_back(v);
481 if (next.x == gs.x && next.y == gs.y && next.z == gs.z)
482 break;
483 gs = next;
484 cf = C3{cf.x * ratio.x, cf.y * ratio.y, cf.z * ratio.z};
485 if (inPlace)
486 dec = dec.coarsened(peclet::core::IVec<3>{ratio.x, ratio.y, ratio.z});
487 }
488 lv_[0].resMask = CCField("vmg_resmask0", lv_[0].n);
489 }
490#endif
491 // Does this rank's block on level `lv` touch global face f (0=-x,1=+x,2=-y,3=+y,4=-z,5=+z)?
492 // Single-rank: always. The domain-BC fold, the held-face residual exclude, the correction
493 // ghosts and the prolongation ghosts are all applied only by the owning rank (WO-F rule).
494 bool touches(const Level& lv, int f) const {
495 const int a = f / 2;
496 const int o = (a == 0) ? lv.og.x : (a == 1) ? lv.og.y : lv.og.z;
497 const int n = (a == 0) ? lv.inner.x : (a == 1) ? lv.inner.y : lv.inner.z;
498 const int gn = (a == 0) ? lv.gdim.x : (a == 1) ? lv.gdim.y : lv.gdim.z;
499 return (f % 2 == 0) ? (o == 0) : (o + n == gn);
500 }
501 int nLevels() const { return (int)lv_.size(); }
502 Level& level(int L) { return lv_[L]; }
503 // Levels built; 0 means the hierarchy has never been initialised. Dereferencing level 0 in
504 // that state is a segfault, so callers that can reach solve() without init() must ask.
505 int levels() const { return (int)lv_.size(); }
506
507 // level-0 fine operator = the external IBM stencil (7 float arrays on the same G=2 block).
508 void setFineStencil(FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT) {
509 Level& f = lv_[0];
510 Kokkos::deep_copy(f.AC, AC);
511 Kokkos::deep_copy(f.AW, AW);
512 Kokkos::deep_copy(f.AE, AE);
513 Kokkos::deep_copy(f.AS, AS);
514 Kokkos::deep_copy(f.AN, AN);
515 Kokkos::deep_copy(f.AB, AB);
516 Kokkos::deep_copy(f.AT, AT);
517 }
518
519 // staircase coarse op: level-0 pin = fine solid mask, resMask = clean-fluid mask; coarse levels
520 // classify by restricted theta and build a const-coeff Helmholtz. nu_dt = mu, idiag = rho/dt, h0
521 // = 1.
523 double thresh) {
524 usePin_ = true;
525 useResMask_ = true; // staircase: pin classified-solid cells + exclude the IBM band
526 Level& f = lv_[0];
527 Kokkos::deep_copy(f.theta, theta0);
528 Kokkos::deep_copy(f.pin, solid0);
529 Kokkos::deep_copy(f.resMask, resmask0);
530 for (int L = 1; L < (int)lv_.size(); ++L) {
531 Level& c = lv_[L];
532 Level& fin = lv_[L - 1];
533 restrictAvg(c.theta, CCConst(fin.theta), c.ext, fin.ext, G, G, c.inner,
534 fin.ratio); // coarse theta = avg
535 thresholdMask(c.pin, CCConst(c.theta), thresh);
536 const double bx = nu_dt * w_[0] / (double)(c.cfac.x * c.cfac.x),
537 by = nu_dt * w_[1] / (double)(c.cfac.y * c.cfac.y),
538 bz = nu_dt * w_[2] / (double)(c.cfac.z * c.cfac.z);
539 buildVelocityStaircase(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, CCConst(c.theta), c.ext, G,
540 bx, by, bz, thresh, idiag);
541 }
542 }
543
544 // MIXED operator: an immersed solid AND domain BCs (the packed-bed-with-inlet case). Level 0 =
545 // the external cut-cell stencil (setFineStencil) with the solid pin and the clean-fluid exclude,
546 // PLUS the held normal-Dirichlet boundary face excluded from coarsening (as setDomainBcOp does);
547 // coarse levels = the staircase Helmholtz (classified solid pinned) PLUS the domain-face folds
548 // (tangential wall/inflow +beta, outflow -beta) on the owning rank. The non-periodic correction
549 // ghosts are zero (fill() / the smoother keep them so), which is the Dirichlet-0 the fold
550 // assumes. Needs setBC() first. comp = velocity component (the fold is component-dependent).
551 // upwind: the fine stencil carries implicit first-order-upwind advection, so the coarse
552 // operator adds the FOU part from the restricted advecting velocity (restrictAdvVelocities must
553 // have run this step) on the fluid rows, the classified-solid rows staying pinned.
555 double idiag, double thresh, bool upwind = false, double fouw = 0.0) {
556 heldComp_ = comp;
558 if (upwind)
559 for (int L = 1; L < (int)lv_.size(); ++L) {
560 Level& c = lv_[L];
561 const double bx = nu_dt * w_[0] / (double)(c.cfac.x * c.cfac.x),
562 by = nu_dt * w_[1] / (double)(c.cfac.y * c.cfac.y),
563 bz = nu_dt * w_[2] / (double)(c.cfac.z * c.cfac.z);
564 buildAdvCoarse(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, CCConst(c.advU), CCConst(c.advV),
565 CCConst(c.advW), comp, c.ext, G, bx, by, bz, fouw, 1.0 / c.cfac.x,
566 1.0 / c.cfac.y, 1.0 / c.cfac.z, idiag);
567 pinSolidRows(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, CCConst(c.theta), c.ext, G, thresh);
568 }
569 Level& f = lv_[0];
570 {
571 const int t = bc_[2 * comp]; // the -side face of the normal component lands at index G
572 if ((t == 1 || t == 2 || t == 4) && touches(f, 2 * comp))
573 zeroPlane(f.resMask, f.ext, comp, G);
574 }
575 for (int L = 1; L < (int)lv_.size(); ++L) {
576 Level& c = lv_[L];
577 const double bx = nu_dt * w_[0] / (double)(c.cfac.x * c.cfac.x),
578 by = nu_dt * w_[1] / (double)(c.cfac.y * c.cfac.y),
579 bz = nu_dt * w_[2] / (double)(c.cfac.z * c.cfac.z);
580 for (int ff = 0; ff < 6; ++ff) {
581 const int a = ff / 2, sd = ff % 2;
582 const double ba = (a == 0) ? bx : (a == 1) ? by : bz;
583 double dval;
584 if (bc_[ff] == 3)
585 dval = -ba;
586 else if ((bc_[ff] == 1 || bc_[ff] == 2) && a != comp)
587 dval = ba;
588 else
589 continue; // periodic, the normal comp at a wall / free-slip face, or a FREE-SLIP
590 // tangential face -- see the note in setDomainBcOp: the coarse levels keep
591 // the Dirichlet-at-ghost surrogate there, deliberately
592 if (touches(c, ff))
593 boundaryFold(c.AC, c.ext, G, a, sd, dval);
594 }
595 }
596 }
597
598 // UPWIND-CONVECTIVE coarse op (implicit-FOU): restrict the advecting velocity u/v/w (level-0
599 // block) to every coarse level (8:1 average; numerical diffusion is welcome -> keeps the
600 // M-matrix). Call ONCE per Picard iteration, before buildUpwindCoarse for the 3 components (the
601 // velocity is shared/frozen at u^k).
603 for (int L = 1; L < (int)lv_.size(); ++L) {
604 Level& cs = lv_[L];
605 Level& fin = lv_[L - 1];
606 CCConst fu = (L == 1) ? u0 : CCConst(fin.advU), fv = (L == 1) ? v0 : CCConst(fin.advV),
607 fw = (L == 1) ? w0 : CCConst(fin.advW);
608 restrictAvg(cs.advU, fu, cs.ext, fin.ext, G, G, cs.inner, fin.ratio);
609 fill(cs, cs.advU);
610 restrictAvg(cs.advV, fv, cs.ext, fin.ext, G, G, cs.inner, fin.ratio);
611 fill(cs, cs.advV);
612 restrictAvg(cs.advW, fw, cs.ext, fin.ext, G, G, cs.inner, fin.ratio);
613 fill(cs, cs.advW);
614 }
615 }
616 // Build the coarse operators for component comp = aniso const-coeff diffusion + dt*FOU from the
617 // restricted advecting velocity (level 0 stays the fine As_[comp] set by setFineStencil). No pin
618 // / no exclude mask -- the upwind M-matrix is stable; the fine residual gives the exact sharp
619 // answer. Per Picard iter, per comp.
620 void buildUpwindCoarse(int comp, double nu_dt, double idiag, double fouw) {
621 usePin_ = false;
622 useResMask_ = false; // upwind path: pure variable-coeff MG (no pin/exclude)
623 for (int L = 1; L < (int)lv_.size(); ++L) {
624 Level& c = lv_[L];
625 const double bx = nu_dt * w_[0] / (double)(c.cfac.x * c.cfac.x),
626 by = nu_dt * w_[1] / (double)(c.cfac.y * c.cfac.y),
627 bz = nu_dt * w_[2] / (double)(c.cfac.z * c.cfac.z);
628 const double sx = 1.0 / (double)c.cfac.x, sy = 1.0 / (double)c.cfac.y,
629 sz = 1.0 / (double)c.cfac.z;
630 buildAdvCoarse(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, CCConst(c.advU), CCConst(c.advV),
631 CCConst(c.advW), comp, c.ext, G, bx, by, bz, fouw, sx, sy, sz, idiag);
632 }
633 }
634
635 // DOMAIN-BC const-coeff path (cavity/BFS): per-face BC types {-x,+x,-y,+y,-z,+z}
636 // (0=periodic,1=wall, 2=inflow,3=outflow,4=free-slip). Enables the non-periodic fill (periodic
637 // axes wrap; non-periodic ghosts left as the caller / correction set them) + the
638 // Dirichlet/Neumann prolongation ghosts.
639 void setBC(const int bc[6]) {
640 bcMode_ = false;
641 for (int i = 0; i < 6; ++i) {
642 bc_[i] = bc[i];
643 if (bc[i])
644 bcMode_ = true;
645 }
646 }
647 // Re-impose the full velocity BC on the level-0 iterate before each smoother colour + the
648 // residual (exactly as the RB-GS path does via fillVelGhosts(c,1)): the const-coeff smoother
649 // updates the held Dirichlet faces, so without this the boundary corners drift (~2% vs RB-GS, as
650 // the CUDA vmg also does). With it the vel-MG converges to the RB-GS fixed point. IbmSolver
651 // supplies this per component before the solve.
652 void setBcApplyL0(std::function<void(CCField)> fn) { bcApplyL0_ = std::move(fn); }
653
654 // Use a Chebyshev polynomial smoother instead of red-black Gauss-Seidel on every level.
655 // `degree` is taken from the caller's sweep counts (pre/post/bottom), so a 2/2 V-cycle becomes a
656 // degree-2 polynomial and the two are compared at equal nominal work. The interval is
657 // [hi/eigRatio, hi] with hi the level's Gershgorin lambda_max: a SMOOTHER wants the top of the
658 // spectrum (the coarse grid owns the rest), which is why this is not the solver's true lower
659 // bound. eigRatio is the usual MG knob -- PETSc defaults near 10, MueLu near 20.
660 void setChebyshevSmoother(bool on, int degree = 0, double eigRatio = 10.0) {
661 chebSmooth_ = on;
662 chebDegree_ = degree; // 0 = follow pre_/post_/bottom_
663 chebEigRatio_ = eigRatio > 1.0 ? eigRatio : 10.0;
664 }
665 bool chebyshevSmoother() const { return chebSmooth_; }
667 void setAspectThreshold(double theta) { aspectTheta_ = theta; }
668 // const-coeff aniso operator + no-slip/inflow/outflow boundary fold for component comp, on EVERY
669 // level. nu_dt = mu, idiag = rho/dt, h0 = 1. Rebuilt per component (the fold is
670 // component-dependent). No pin. useResMask_: exclude the HELD normal-Dirichlet boundary face
671 // (a==comp, -side, wall/inflow) from coarsening -- that cell's value is pinned by the BC
672 // re-imposition, so its (nonzero) residual would drive a spurious coarse correction into the
673 // boundary (the ~2% drift CUDA's domain-BC vmg leaves). Excluding it makes the V-cycle converge
674 // to the RB-GS fixed point (analogue of the IBM clean-fluid exclude).
675 void setDomainBcOp(int comp, double nu_dt, double idiag) {
676 heldComp_ = comp;
677 usePin_ = false;
678 Level& f = lv_[0];
679 Kokkos::deep_copy(f.resMask, 1.0);
680 useResMask_ = false;
681 for (int s = 0; s < 1;
682 ++s) { // only the -side face index G lands inside the smoother range [G, ext-G)
683 const int t = bc_[2 * comp + s];
684 if (t == 1 || t == 2 || t == 4) {
685 if (touches(f, 2 * comp + s))
686 zeroPlane(f.resMask, f.ext, comp, G);
687 useResMask_ = true; // rank-uniform (the masked transfers must agree across ranks)
688 }
689 }
690 for (int L = 0; L < (int)lv_.size(); ++L) {
691 Level& c = lv_[L];
692 const double bx = nu_dt * w_[0] / (double)(c.cfac.x * c.cfac.x),
693 by = nu_dt * w_[1] / (double)(c.cfac.y * c.cfac.y),
694 bz = nu_dt * w_[2] / (double)(c.cfac.z * c.cfac.z);
695 buildConstAniso(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, c.ext, bx, by, bz, idiag);
696 for (int f = 0; f < 6; ++f) {
697 const int a = f / 2, s = f % 2;
698 const double ba = (a == 0) ? bx : (a == 1) ? by : bz;
699 double dval;
700 if (bc_[f] == 3)
701 dval = -ba; // outflow zero-gradient: every component
702 else if ((bc_[f] == 1 || bc_[f] == 2) && a != comp)
703 dval = ba; // wall/inflow: tangential fold
704 else if (bc_[f] == 4 && a != comp && L == 0)
705 dval = -ba; // free-slip: zero-gradient tangential fold -- LEVEL 0 ONLY. On the
706 // coarse levels the exact Neumann row (-ba, ghost 0) is what the fine
707 // operator has, but with rho/dt -> 0 it leaves the coarse tangential
708 // operator nearly singular (slip on two opposite faces removes its
709 // Dirichlet anchor in that direction), and the V-cycle's few-sweep
710 // coarse solves then return noise instead of a correction. Measured,
711 // sphere in an inflow/outflow channel at nu dt/h^2 = 1e3, residual
712 // after 32 V-cycles: -ba on every level 1.2e-3 (+-z slip) and a
713 // stagnation at 0.89 (all four faces slip); coarse fold dropped 2.5e-7
714 // and 3.3e-4 (walls: 7.5e-8). The coarse levels therefore keep the
715 // Dirichlet-at-ghost-centre surrogate, which is exactly what the
716 // normal component at any Dirichlet face already uses; the fixed point
717 // is set by level 0 alone.
718 else
719 continue; // periodic, the normal comp at a wall / free-slip face, or a coarse slip face
720 if (touches(c, f))
721 boundaryFold(c.AC, c.ext, G, a, s, dval);
722 }
723 }
724 }
725
726 // solve A x = b (b,x on the level-0 G=2 block): up to nvc V-cycles. Solution left in x.
727 // Two optional stops (global max under MPI):
728 // tol > 0: max|x_{k+1} - x_k| over the inner cells <= tol * (first cycle's) -- the update
729 // criterion IbmSolver's RB-GS sweep loop uses, so cycles and sweeps compare;
730 // resTol > 0: max|b - A x| <= resTol * max|b| (the level-0 fine operator, pinned rows 0) --
731 // the criterion that measures convergence itself; a warm start that already
732 // solves the equation stops at once. resTol takes precedence when both are set.
733 // Returns the cycles run; lastResidualRatio() is max|r|/max|b| at exit (resTol mode only).
734 int solve(CCConst b, CCField x, int nvc, int pre, int post, int bottom, double tol = 0.0,
735 Comm comm = nullComm(), double resTol = 0.0) {
736 pre_ = pre;
737 post_ = post;
738 bottom_ = bottom;
739 Level& l0 = lv_[0];
740 Kokkos::deep_copy(l0.rhs, b);
741 Kokkos::deep_copy(l0.x, x);
742 auto gmax = [&](double v) {
743#ifdef PECLET_FLOW_MPI
744 if (distributed_ && comm != MPI_COMM_NULL) {
745 double g = 0.0;
746 MPI_Allreduce(&v, &g, 1, MPI_DOUBLE, MPI_MAX, comm);
747 return g;
748 }
749#endif
750 return v;
751 };
752 // Chebyshev needs an interval that CONTAINS each level's spectrum, and every rank must use
753 // the SAME polynomial or the iterates drift apart. The operator is fixed for the whole solve,
754 // so the Gershgorin reduction runs once per level here -- not inside smooth(), which would put
755 // an all-reduce on every level of every V-cycle and hand back the latency the smoother saves.
756 if (chebSmooth_)
757 for (Level& lv : lv_) {
758 double lo = 0.0, hi = 0.0;
759 ibmStencilJacobiBounds(FPC(lv.AC), FPC(lv.AW), FPC(lv.AE), FPC(lv.AS), FPC(lv.AN),
760 FPC(lv.AB), FPC(lv.AT), usePin_ ? CCConst(lv.pin) : empty_, lv.ext,
761 G, lo, hi);
762 lv.chebHi = gmax(hi);
763 }
764 auto residual = [&]() {
765 fill(l0, l0.x);
766 if (bcApplyL0_)
767 bcApplyL0_(l0.x);
768 residualVarPin(l0.res, CCConst(l0.x), CCConst(l0.rhs), FPC(l0.AC), FPC(l0.AW), FPC(l0.AE),
769 FPC(l0.AS), FPC(l0.AN), FPC(l0.AB), FPC(l0.AT),
770 usePin_ ? CCConst(l0.pin) : empty_, l0.ext, G);
771 // the held normal-Dirichlet face (wall / inflow, -side) is imposed, not solved: not part
772 // of the convergence measure
773 if (bcMode_ && heldComp_ >= 0) {
774 const int t = bc_[2 * heldComp_];
775 if ((t == 1 || t == 2 || t == 4) && touches(l0, 2 * heldComp_))
776 zeroPlane(l0.res, l0.ext, heldComp_, G);
777 }
778 return gmax(maxAbsInner(CCConst(l0.res), l0.ext, G));
779 };
780 // The forcing may enter through a Dirichlet ghost (an inflow) rather than through b, so the
781 // convergence scale is max(max|b|, max|A x|) with max|A x| = max|b - r| from the initial
782 // residual -- max|b| alone is ~0 on the first step from rest and the stop never triggers.
783 const bool useRes = resTol > 0.0;
784 const bool useDu = !useRes && tol > 0.0;
785 if (useDu && prev_.extent(0) != l0.n)
786 prev_ = CCField("vmg_prev", l0.n);
787 double du0 = 0.0, bnorm = 0.0;
788 int used = nvc;
789 lastResRatio_ = -1.0;
790 if (useRes) { // scale only; at least one cycle always runs (see IbmSolver::velSweepLoop)
791 const double r0 = residual();
792 bnorm = std::max(gmax(maxAbsInner(CCConst(l0.rhs), l0.ext, G)),
793 gmax(maxAbsDiffInner(CCConst(l0.rhs), CCConst(l0.res), l0.ext, G)));
794 lastResRatio_ = bnorm > 0.0 ? r0 / bnorm : 0.0;
795 }
796 double rPrev = -1.0;
797 for (int v = 0; v < nvc; ++v) {
798 if (useDu)
799 Kokkos::deep_copy(prev_, l0.x);
800 vcycle(0);
801 if (useRes) {
802 const double r = residual();
803 lastResRatio_ = bnorm > 0.0 ? r / bnorm : 0.0;
804 // round-off floor / stagnation guard (see IbmSolver::velSweepLoop)
805 const bool floor = r <= 1e-14 * bnorm || (rPrev >= 0.0 && r >= rPrev);
806 rPrev = r;
807 if (r <= resTol * bnorm || floor) {
808 used = v + 1;
809 break;
810 }
811 } else if (useDu) {
812 const double du = gmax(maxAbsDiffInner(CCConst(l0.x), CCConst(prev_), l0.ext, G));
813 if (v == 0)
814 du0 = du;
815 if (du <= tol * du0) {
816 used = v + 1;
817 break;
818 }
819 }
820 }
821 Kokkos::deep_copy(x, l0.x);
822 return used;
823 }
824 double lastResidualRatio() const { return lastResRatio_; }
825
826 public: // (public for nvcc extended-lambda)
827 void vcycle(int L) {
828 Level& lv = lv_[L];
829 const bool l0 = (L == 0);
830 if (L + 1 == (int)lv_.size()) {
831 smooth(lv, bottom_, l0);
832 return;
833 } // velocity op non-singular -> no mean removal
834 smooth(lv, pre_, l0);
835 fill(lv, lv.x);
836 if (l0 && bcApplyL0_)
837 bcApplyL0_(lv.x); // domain-BC: re-impose the velocity BC before the level-0 residual
838 residualVarPin(lv.res, CCConst(lv.x), CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE),
839 FPC(lv.AS), FPC(lv.AN), FPC(lv.AB), FPC(lv.AT),
840 usePin_ ? CCConst(lv.pin) : empty_, lv.ext, G);
841 const bool masked =
842 useResMask_ && (lv.resMask.extent(0) == lv.n); // level 0: exclude the IBM cut-cell band
843 if (masked)
844 mulMask(lv.res, CCConst(lv.resMask));
845 Level& cs = lv_[L + 1];
846 restrictAvg(cs.rhs, CCConst(lv.res), cs.ext, lv.ext, G, G, cs.inner, lv.ratio);
847 Kokkos::deep_copy(cs.x, 0.0);
848 vcycle(L + 1);
849 fill(cs, cs.x);
851 cs); // non-periodic boundary ghosts the trilinear prolong samples (domain-BC mode)
852 if (masked)
853 prolongMasked(lv.x, CCConst(cs.x), CCConst(lv.resMask), lv.ext, cs.ext, G, lv.inner, lv.ratio,
854 0.5);
855 else
856 prolongAdd(lv.x, CCConst(cs.x), lv.ext, cs.ext, G, G, lv.inner, lv.ratio);
857 smooth(lv, post_, l0);
858 }
859 void smooth(Level& lv, int sweeps, bool isL0) {
860 const C3 og = lv.og; // global red-black parity (block inner origin); {0,0,0} single-rank
861 CCConst pin = usePin_ ? CCConst(lv.pin) : empty_;
862 if (chebSmooth_ && lv.chebHi > 0.0) {
863 chebSmooth(lv, chebDegree_ > 0 ? chebDegree_ : sweeps, isL0, pin);
864 return;
865 }
866 for (int k = 0; k < sweeps; ++k)
867 for (int color = 0; color < 2; ++color) {
868#ifdef PECLET_FLOW_MPI
869 if (distributed_ && !bcMode_) {
870 // Overlap the per-colour halo with the interior sweep (see CutcellMG::smooth) — only on
871 // the periodic/IBM path: the per-colour BC hook re-imposes ghost/held cells AFTER the
872 // fill in the blocking order, which the split would reorder.
873 const C3 lo{G + 1, G + 1, G + 1};
874 const C3 hi{lv.ext.x - G - 1, lv.ext.y - G - 1, lv.ext.z - G - 1};
875 lv.dev->exchangeBegin(lv.x);
876 ibmRbgsStencilColorBox(lv.x, CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE),
877 FPC(lv.AS), FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), pin, lv.ext, og,
878 color, lo, hi, C3{0, 0, 0}, C3{0, 0, 0});
879 lv.dev->exchangeEnd(lv.x);
880 ibmRbgsStencilColorBox(lv.x, CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE),
881 FPC(lv.AS), FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), pin, lv.ext, og,
882 color, C3{G, G, G}, C3{lv.ext.x - G, lv.ext.y - G, lv.ext.z - G},
883 lo, hi);
884 continue;
885 }
886#endif
887 fill(lv, lv.x);
888 if (isL0 && bcApplyL0_)
889 bcApplyL0_(lv.x); // re-impose the velocity BC (held Dirichlet faces) per colour
890 ibmRbgsStencilColor(lv.x, CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE), FPC(lv.AS),
891 FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), pin, lv.ext, og, G, color);
892 }
893 }
894
895 // Degree-`degree` Chebyshev polynomial smoother on diag(AC)^-1 A over [hi/eigRatio, hi]. One
896 // residual and ONE halo exchange per degree, against red-black's two colour passes and two
897 // exchanges per sweep -- the reason to want it here at all. The direction restarts at zero on
898 // every call: each smooth is an independent polynomial application, not a continued iteration.
899 // lv.res is scratch here; vcycle recomputes it after smoothing, so overwriting it is safe.
900 void chebSmooth(Level& lv, int degree, bool isL0, CCConst pin) {
901 if (degree < 1)
902 return;
903 if (lv.cd.extent(0) != lv.n)
904 lv.cd = CCField("vmg_cd", lv.n);
905 Kokkos::deep_copy(lv.cd, 0.0);
906 ChebCoeffs cb(lv.chebHi / chebEigRatio_, lv.chebHi);
907 for (int k = 0; k < degree; ++k) {
908 fill(lv, lv.x);
909 if (isL0 && bcApplyL0_)
910 bcApplyL0_(lv.x);
911 residualVarPin(lv.res, CCConst(lv.x), CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE),
912 FPC(lv.AS), FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), pin, lv.ext, G);
913 double alpha = 0.0, beta = 0.0;
914 cb.next(k, alpha, beta);
915 ibmChebUpdate(lv.x, lv.cd, CCConst(lv.res), FPC(lv.AC), pin, lv.ext, G, alpha, beta);
916 }
917 }
918 // periodic ghost fill; in domain-BC mode only the periodic axes wrap (non-periodic boundary
919 // ghosts are left as the caller / correction set them -- the boundary fold + held ghost represent
920 // the wall). Distributed (periodic IBM path): the per-level core halo (cross-rank + periodic in
921 // one call).
922 void fill(Level& lv, CCField f) {
923#ifdef PECLET_FLOW_MPI
924 if (distributed_) {
925 lv.dev->exchange(f); // cross-rank + periodic on all three axes
926 if (bcMode_)
927 zeroBcGhosts(lv, f);
928 return;
929 }
930#endif
931 for (int a = 0; a < 3; ++a)
932 if (!bcMode_ || (bc_[2 * a] == 0 && bc_[2 * a + 1] == 0))
933 fillAxis(lv, f, a);
934 }
935 // Non-periodic global-face ghosts of `f` -> 0 on the owning rank (the halo exchange is built
936 // periodic on every axis, so it has just wrapped the opposite side of the domain into them). A
937 // coarse correction wants exactly 0 there; level 0 gets the real BC from the hook afterwards.
939 for (int a = 0; a < 3; ++a)
940 for (int s = 0; s < 2; ++s)
941 if (bc_[2 * a + s] != 0 && touches(lv, 2 * a + s))
942 fillBcGhost(f, lv.ext, G, a, s, /*dirichlet=*/1);
943 }
944 // non-periodic boundary ghosts of a coarse correction before trilinear prolong (Dirichlet outflow
945 // -> 0, Neumann wall/inflow -> zero-gradient).
947 if (!bcMode_)
948 return;
949 for (int a = 0; a < 3; ++a)
950 for (int s = 0; s < 2; ++s) {
951 const int t = bc_[2 * a + s];
952 if (t == 0 || !touches(lv, 2 * a + s))
953 continue;
954 fillBcGhost(lv.x, lv.ext, G, a, s, t == 3 ? 1 : 0);
955 }
956 }
957 void fillAxis(Level& lv, CCField f, int axis) {
959 C3 e = lv.ext;
960 int N3[3] = {lv.inner.x, lv.inner.y, lv.inner.z};
961 int dims[3] = {e.x, e.y, e.z};
962 long st[3] = {1, e.x, (long)e.x * e.y};
963 const int a = axis, b = (axis + 1) % 3, c = (axis + 2) % 3;
964 const long sa = st[a], sb = st[b], sc = st[c];
965 const int N = N3[a];
966 CCField ff = f;
967 Kokkos::parallel_for(
968 "peclet::flow::vmg_pfill", MDRange2<CCExec>(space, {0, 0}, {dims[b], dims[c]}),
969 KOKKOS_LAMBDA(int p0, int p1) {
970 const long base = (long)p0 * sb + (long)p1 * sc;
971 for (int gl = 0; gl < G; ++gl) {
972 ff(base + (long)gl * sa) = ff(base + (long)(gl + N) * sa);
973 ff(base + (long)(G + N + gl) * sa) = ff(base + (long)(G + gl) * sa);
974 }
975 });
976 }
977
978 private:
979 std::vector<Level> lv_;
980 double w_[3] = {1.0, 1.0, 1.0}; // per-axis metric weight (setMetric); 1.0 = isotropic lattice
981 double hp_[3] = {1.0, 1.0, 1.0}; // per-axis spacing h_a' (setMetric); the §5 coarsening rule
982 bool aniso_ = false; // engages that rule; false => today's level table verbatim
983 double aspectTheta_ = 2.0; // anisotropic-coarsening threshold (setAspectThreshold)
984 int pre_ = 2, post_ = 2, bottom_ = 8;
985 bool chebSmooth_ = false; // Chebyshev polynomial smoother instead of red-black GS
986 int chebDegree_ = 0; // 0 = follow pre_/post_/bottom_
987 double chebEigRatio_ = 10.0; // interval is [chebHi/chebEigRatio_, chebHi]
988 bool usePin_ = true,
989 useResMask_ = true; // staircase: pin + clean-fluid exclude; upwind/domain-BC: neither
990 bool bcMode_ = false;
991 int bc_[6] = {0, 0, 0, 0, 0, 0}; // domain-BC (non-periodic) mode
992 std::function<void(CCField)> bcApplyL0_; // re-impose the velocity BC on level 0 (domain-BC mode)
993 CCConst empty_; // zero-extent View -> "no pin / no mask" to the kernels
994 bool distributed_ = false; // multi-rank (initMpi); fill() exchanges on every level
995 CCField prev_; // previous iterate for the update-tolerance stop (solve tol > 0)
996 double lastResRatio_ = -1.0; // max|r|/max|b| at exit of the last resTol-mode solve
997 int heldComp_ = -1; // component of the last domain-BC operator build (held-face exclude)
998};
999
1000} // namespace peclet::flow
1001
1002#endif // PECLET_FLOW_MAC_VELOCITY_MG_HPP
static C3 mgChooseRatio(const double H[3], const bool canA[3], bool aniso, double theta)
void setFineStencil(FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT)
void fillProlongBcGhosts(Level &lv)
void buildUpwindCoarse(int comp, double nu_dt, double idiag, double fouw)
void zeroBcGhosts(Level &lv, CCField f)
void setChebyshevSmoother(bool on, int degree=0, double eigRatio=10.0)
bool touches(const Level &lv, int f) const
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 fillAxis(Level &lv, CCField f, int axis)
void setBcApplyL0(std::function< void(CCField)> fn)
void setDomainBcOp(int comp, double nu_dt, double idiag)
void smooth(Level &lv, int sweeps, bool isL0)
void chebSmooth(Level &lv, int degree, bool isL0, CCConst pin)
void restrictAdvVelocities(CCConst u0, CCConst v0, CCConst w0)
void fill(Level &lv, CCField f)
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)
void setBC(const int bc[6])
void init(int nx, int ny, int nz, int nLevels)
std::vector< C3 > levelRatios() const
The per-level coarsening ratio actually chosen (the level table of §5; the two hierarchies share one ...
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,...
flow — Chebyshev semi-iteration as a standalone momentum (implicit-diffusion) solver,...
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
flow — portable (Kokkos) IBM geometric fields + variable-coefficient RB-GS smoother.
void zeroPlane(CCField m, C3 e, int axis, int idx)
void buildConstAniso(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, C3 e, double bx, double by, double bz, double idiag)
void buildVelocityStaircase(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, CCConst theta, C3 e, int g, double bx, double by, double bz, double thresh, double idiag)
void thresholdMask(CCField m, CCConst theta, double thresh)
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 prolongAdd(CCField fine, CCConst coarse, C3 fext, C3 cext, int gf, int gc, C3 finner, C3 ratio)
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 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)
void boundaryFold(FPV AC, C3 e, int g, int a, int s, double beta)
void fillBcGhost(CCField x, C3 e, int g, int a, int s, int dirichlet)
double maxAbsInner(CCConst a, C3 e, int g)
void ccFor3(const char *name, C3 lo, C3 hi, F f)
Kokkos::View< double *, CCMem > CCField
void restrictAvg(CCField coarse, CCConst fine, C3 cext, C3 fext, int gc, int gf, C3 cinner, C3 ratio)
Kokkos::View< MReal *, CCMem > FPV
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
void mulMask(CCField r, CCConst m)
Kokkos::DefaultExecutionSpace CCExec
void pinSolidRows(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, CCConst theta, C3 e, int g, double thresh)
void prolongMasked(CCField fine, CCConst coarse, CCConst mask, C3 fext, C3 cext, int g, C3 finner, C3 ratio, double eps)
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 buildAdvCoarse(FPV AC, FPV AW, FPV AE, FPV AS, FPV AN, FPV AB, FPV AT, CCConst U, CCConst V, CCConst W, int comp, C3 e, int g, double bx, double by, double bz, double fouw, double sx, double sy, double sz, double idiag)
Kokkos::View< const double *, CCMem > CCConst
void fou_operator_aniso(int comp, int x, int y, int z, A U, A V, A W, double dt, double sx, double sy, double sz, double &cC, double &cxm, double &cxp, double &cym, double &cyp, double &czm, double &czp)
flow — the multidimensional iteration policies: iteration order follows storage.
flow — portable (Kokkos) staggered MAC momentum advection (Koren TVD + FOU).
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.
static constexpr int N
static constexpr double AC
static constexpr double U