flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mac_cutcell_mg.hpp
Go to the documentation of this file.
1
14#ifndef PECLET_FLOW_MAC_CUTCELL_MG_HPP
15#define PECLET_FLOW_MAC_CUTCELL_MG_HPP
16
17#include <cmath>
18#include <Kokkos_Core.hpp>
19#include <memory>
20#include <vector>
21
22#include "mac_bc.hpp"
23#include "mac_pressure.hpp"
24#include "peclet/core/solver/graph_amg.hpp" // decomposition-agnostic algebraic bottom solve
25
26// Multi-rank (MPI) path is opt-in: the single-GPU module never links MPI, so all distributed code
27// is gated (mirrors the CUDA PECLET_FLOW_BUILD_MPI gating). When PECLET_FLOW_MPI is off, CutcellMG
28// is byte-identical to before.
29#ifdef PECLET_FLOW_MPI
30#include <memory>
31
32#include "peclet/core/decomp/block_decomposer.hpp"
33#include "peclet/core/decomp/grid_redistribute.hpp"
34#include "peclet/core/halo/grid_halo.hpp"
35#include "peclet/core/halo/grid_halo_topology.hpp"
36#endif
37
38namespace peclet::flow {
39
40#ifdef PECLET_FLOW_MPI
41using peclet::core::halo::GridHalo;
42using peclet::core::halo::GridHaloTopology;
43#endif
44
45using MReal = float; // operator storage = CUDA mreal
46using FPV = Kokkos::View<MReal*, CCMem>;
47using FPC = Kokkos::View<const MReal*, CCMem>;
48
49// coarsen staggered face openness: each coarse face = average of the ratio_b*ratio_c fine sub-faces
50// it spans (mg_coarsen_open_avg_k port).
51inline void coarsenOpenAvg(CCField oxc, CCField oyc, CCField ozc, CCConst oxf, CCConst oyf,
52 CCConst ozf, C3 cext, C3 fext, int g, C3 cinner, C3 ratio) {
53 CCExec space;
54 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
55 Kokkos::parallel_for(
56 "peclet::flow::coarsen_open", MD(space, {0, 0, 0}, {cinner.x, cinner.y, cinner.z}),
57 KOKKOS_LAMBDA(int icx, int icy, int icz) {
58 const int rx = ratio.x, ry = ratio.y, rz = ratio.z;
59 const int fx0 = rx * icx + g, fy0 = ry * icy + g, fz0 = rz * icz + g;
60 const long fsy = fext.x, fsz = (long)fext.x * fext.y;
61 auto F = [&](CCConst T, int x, int y, int z) {
62 return T((long)x + (long)y * fsy + (long)z * fsz);
63 };
64 double sx = 0, sy = 0, sz = 0;
65 for (int a = 0; a < ry; ++a)
66 for (int b = 0; b < rz; ++b)
67 sx += F(oxf, fx0, fy0 + a, fz0 + b);
68 for (int a = 0; a < rx; ++a)
69 for (int b = 0; b < rz; ++b)
70 sy += F(oyf, fx0 + a, fy0, fz0 + b);
71 for (int a = 0; a < rx; ++a)
72 for (int b = 0; b < ry; ++b)
73 sz += F(ozf, fx0 + a, fy0 + b, fz0);
74 const long ci =
75 (long)(icx + g) + (long)(icy + g) * cext.x + (long)(icz + g) * (long)cext.x * cext.y;
76 oxc(ci) = sx / (double)(ry * rz);
77 oyc(ci) = sy / (double)(rx * rz);
78 ozc(ci) = sz / (double)(rx * ry);
79 });
80}
81
82// residual r = b - A x for the float operator (mg_residual_var_k port).
83inline void residualCutcell(CCField r, CCConst x, CCConst b, FPC AC, FPC AW, FPC AE, FPC AS, FPC AN,
84 FPC AB, FPC AT, C3 e, int g) {
85 CCExec space;
86 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
87 Kokkos::parallel_for(
88 "peclet::flow::cc_residual", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
89 KOKKOS_LAMBDA(int lx, int ly, int lz) {
90 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
91 const long i = (long)lx + (long)ly * sy + (long)lz * sz;
92 const double Ax = (double)AC(i) * x(i) + (double)AE(i) * x(i + sx) +
93 (double)AW(i) * x(i - sx) + (double)AN(i) * x(i + sy) +
94 (double)AS(i) * x(i - sy) + (double)AT(i) * x(i + sz) +
95 (double)AB(i) * x(i - sz);
96 r(i) = b(i) - Ax;
97 });
98}
99
100// average restriction (coarse = mean of ratio^3 fine children; mg_restrict_k) + trilinear
101// prolongation (added to fine; mg_prolong_k). Both over inner cells.
102inline void restrictAvg(CCField coarse, CCConst fine, C3 cext, C3 fext, int g, C3 cinner,
103 C3 ratio) {
104 CCExec space;
105 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
106 Kokkos::parallel_for(
107 "peclet::flow::restrict", MD(space, {0, 0, 0}, {cinner.x, cinner.y, cinner.z}),
108 KOKKOS_LAMBDA(int icx, int icy, int icz) {
109 const long fsy = fext.x, fsz = (long)fext.x * fext.y;
110 double s = 0;
111 for (int dz = 0; dz < ratio.z; ++dz)
112 for (int dy = 0; dy < ratio.y; ++dy)
113 for (int dx = 0; dx < ratio.x; ++dx) {
114 const int fx = ratio.x * icx + dx + g, fy = ratio.y * icy + dy + g,
115 fz = ratio.z * icz + dz + g;
116 s += fine((long)fx + (long)fy * fsy + (long)fz * fsz);
117 }
118 const long ci =
119 (long)(icx + g) + (long)(icy + g) * cext.x + (long)(icz + g) * (long)cext.x * cext.y;
120 coarse(ci) = s / (double)(ratio.x * ratio.y * ratio.z);
121 });
122}
123inline void prolongAdd(CCField fine, CCConst coarse, C3 fext, C3 cext, int g, C3 finner, C3 ratio) {
124 CCExec space;
125 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
126 Kokkos::parallel_for(
127 "peclet::flow::prolong", MD(space, {0, 0, 0}, {finner.x, finner.y, finner.z}),
128 KOKKOS_LAMBDA(int ifx, int ify, int ifz) {
129 // coarse sample coord: coarsened axis (ratio 2) -> 0.5*ifine - 0.25 + g; kept axis (ratio
130 // 1) -> ifine+g
131 const double cx = (ratio.x == 2) ? 0.5 * ifx - 0.25 + g : ifx + g;
132 const double cy = (ratio.y == 2) ? 0.5 * ify - 0.25 + g : ify + g;
133 const double cz = (ratio.z == 2) ? 0.5 * ifz - 0.25 + g : ifz + g;
134 const double fxw = Kokkos::floor(cx), fyw = Kokkos::floor(cy), fzw = Kokkos::floor(cz);
135 const double wx = cx - fxw, wy = cy - fyw, wz = cz - fzw;
136 const int x0 = (int)fxw, y0 = (int)fyw, z0 = (int)fzw;
137 const long sy = cext.x, sz = (long)cext.x * cext.y;
138 auto C = [&](int xx, int yy, int zz) {
139 return coarse((long)xx + (long)yy * sy + (long)zz * sz);
140 };
141 const double c00 = C(x0, y0, z0) * (1 - wx) + C(x0 + 1, y0, z0) * wx;
142 const double c10 = C(x0, y0 + 1, z0) * (1 - wx) + C(x0 + 1, y0 + 1, z0) * wx;
143 const double c01 = C(x0, y0, z0 + 1) * (1 - wx) + C(x0 + 1, y0, z0 + 1) * wx;
144 const double c11 = C(x0, y0 + 1, z0 + 1) * (1 - wx) + C(x0 + 1, y0 + 1, z0 + 1) * wx;
145 const double c0 = c00 * (1 - wy) + c10 * wy, c1 = c01 * (1 - wy) + c11 * wy;
146 const long fi =
147 (long)(ifx + g) + (long)(ify + g) * fext.x + (long)(ifz + g) * (long)fext.x * fext.y;
148 fine(fi) += c0 * (1 - wz) + c1 * wz;
149 });
150}
151
153 public:
154 struct Level {
155 C3 ext, inner, ratio{2, 2, 2}, cfac{1, 1, 1};
156 C3 og{0, 0, 0}; // block inner origin (global red-black parity); {0,0,0} single-rank
157 std::size_t n = 0;
159 FPV AC, AW, AE, AS, AN, AB, AT;
160#ifdef PECLET_FLOW_MPI
161 std::shared_ptr<GridHaloTopology<3>> halo; // per-level topology (decomposed)
162 std::shared_ptr<GridHalo<double>> dev; // per-level ghost exchange
163#endif
164 };
165 static constexpr int G = 1;
166
167 // build the periodic level hierarchy: per axis, halve inner while even and >=2 (uniform when
168 // cubic), capped at nLevels (mirrors DistributedPoissonMG::init uniform path).
169 void init(int nx, int ny, int nz, int nLevels) {
170 lv_.clear();
171 amg_.reset();
172 gnxF_ = nx;
173 gnyF_ = ny;
174 gnzF_ = nz;
175 C3 inner{nx, ny, nz}, cf{1, 1, 1};
176 for (int L = 0; L < nLevels; ++L) {
177 Level v;
178 v.inner = inner;
179 v.ext = C3{inner.x + 2 * G, inner.y + 2 * G, inner.z + 2 * G};
180 v.cfac = cf;
181 v.n = (std::size_t)v.ext.x * v.ext.y * v.ext.z;
182 auto can = [&](int d) { return (d % 2 == 0) && (d / 2 >= 2); };
183 C3 next = inner;
184 C3 ratio{1, 1, 1};
185 if (L + 1 < nLevels) {
186 if (can(inner.x)) {
187 ratio.x = 2;
188 next.x = inner.x / 2;
189 }
190 if (can(inner.y)) {
191 ratio.y = 2;
192 next.y = inner.y / 2;
193 }
194 if (can(inner.z)) {
195 ratio.z = 2;
196 next.z = inner.z / 2;
197 }
198 }
199 v.ratio = ratio;
200 v.x = CCField("mg_x", v.n);
201 v.rhs = CCField("mg_rhs", v.n);
202 v.res = CCField("mg_res", v.n);
203 v.ox = CCField("mg_ox", v.n);
204 v.oy = CCField("mg_oy", v.n);
205 v.oz = CCField("mg_oz", v.n);
206 for (FPV* p : {&v.AC, &v.AW, &v.AE, &v.AS, &v.AN, &v.AB, &v.AT})
207 *p = FPV("mg_A", v.n);
208 lv_.push_back(v);
209 if (next.x == inner.x && next.y == inner.y && next.z == inner.z)
210 break; // nothing coarsens
211 inner = next;
212 cf = C3{cf.x * ratio.x, cf.y * ratio.y, cf.z * ratio.z};
213 }
214 }
215#ifdef PECLET_FLOW_MPI
216 // Multi-rank hierarchy: coarsen the GLOBAL grid 2:1 per level; each level gets its own core halo
217 // over a BlockDecomposer of that level's grid (the ORB decomposition coarsens cleanly so
218 // restrict/prolong stay local). Sets the distributed flag -> fill() exchanges, the reductions
219 // Allreduce, the smoother uses the block's global-origin parity. Single-rank (size 1) reproduces
220 // init()'s field exactly.
221 // dec0: OPTIONAL shared level-0 decomposition (load-balance / CFD-DEM co-decomposition). When
222 // given, level 0 uses it so the MG's level-0 block matches the caller's (possibly weighted)
223 // block; the coarse levels keep the equal-weight ORB of the coarsened grid. For a weighted dec0
224 // the coarse-level transfer is only clean when nLevels==1 (pure RB-GS) — use that (or the
225 // decomposition-agnostic GraphAMG) for a weighted co-decomposition. nullptr => equal-weight
226 // everywhere (the original behaviour, byte-identical).
227 void initMpi(int gnx, int gny, int gnz, int nLevels, MPI_Comm comm,
228 const peclet::core::decomp::BlockDecomposer<3>* dec0 = nullptr) {
229 lv_.clear();
230 amg_.reset();
231 distributed_ = true;
232 comm_ = comm;
233 gnxF_ = gnx;
234 gnyF_ = gny;
235 gnzF_ = gnz;
236 int rank = 0, size = 1;
237 MPI_Comm_rank(comm, &rank);
238 MPI_Comm_size(comm, &size);
239 std::array<bool, 3> per{true, true, true};
240 C3 gs{gnx, gny, gnz}, cf{1, 1, 1};
241 auto can = [&](int d) { return (d % 2 == 0) && (d / 2 >= 2); };
242 for (int L = 0; L < nLevels; ++L) {
243 Level v;
244 v.halo = std::make_shared<GridHaloTopology<3>>();
245 peclet::core::decomp::BlockDecomposer<3> decOwn(static_cast<std::size_t>(size),
246 peclet::core::IVec<3>{gs.x, gs.y, gs.z});
247 const peclet::core::decomp::BlockDecomposer<3>& dec = (L == 0 && dec0) ? *dec0 : decOwn;
248 v.halo->buildTopology(dec, rank, G, per, comm);
249 v.dev = std::make_shared<GridHalo<double>>();
250 v.dev->init(*v.halo);
251 const auto& idx = v.halo->indexer();
252 const auto eg = idx.sizeInclGhost(), ino = idx.sizeInner(), oig = idx.originInclGhost();
253 v.ext = {(int)eg[0], (int)eg[1], (int)eg[2]};
254 v.inner = {(int)ino[0], (int)ino[1], (int)ino[2]};
255 v.og = {(int)oig[0] + G, (int)oig[1] + G,
256 (int)oig[2] + G}; // inner origin == single-rank og=0 at origin 0
257 v.cfac = cf;
258 v.n = idx.numCellsInclGhost();
259 C3 next = gs, ratio{1, 1, 1};
260 if (L + 1 < nLevels) {
261 if (can(gs.x)) {
262 ratio.x = 2;
263 next.x = gs.x / 2;
264 }
265 if (can(gs.y)) {
266 ratio.y = 2;
267 next.y = gs.y / 2;
268 }
269 if (can(gs.z)) {
270 ratio.z = 2;
271 next.z = gs.z / 2;
272 }
273 }
274 v.ratio = ratio;
275 v.x = CCField("mg_x", v.n);
276 v.rhs = CCField("mg_rhs", v.n);
277 v.res = CCField("mg_res", v.n);
278 v.ox = CCField("mg_ox", v.n);
279 v.oy = CCField("mg_oy", v.n);
280 v.oz = CCField("mg_oz", v.n);
281 for (FPV* p : {&v.AC, &v.AW, &v.AE, &v.AS, &v.AN, &v.AB, &v.AT})
282 *p = FPV("mg_A", v.n);
283 lv_.push_back(v);
284 if (next.x == gs.x && next.y == gs.y && next.z == gs.z)
285 break;
286 gs = next;
287 cf = C3{cf.x * ratio.x, cf.y * ratio.y, cf.z * ratio.z};
288 }
289 }
290#endif
291 int nLevels() const { return (int)lv_.size(); }
292 Level& level(int L) { return lv_[L]; }
293
294 // per-face domain BC types {-x,+x,-y,+y,-z,+z}: 0=periodic, 1/2=Neumann (wall/inflow),
295 // 3=Dirichlet (outflow). Default all-periodic -> applyBoundaryOpenness is a no-op (periodic/IBM
296 // path byte-identical).
297 void setBoundaryConditions(const int bc[6]) {
298 hasBC_ = false;
299 hasOutflow_ = false;
300 for (int i = 0; i < 6; ++i) {
301 bc_[i] = bc[i];
302 if (bc[i])
303 hasBC_ = true;
304 if (bc[i] == 3)
305 hasOutflow_ = true;
306 }
307 removeMean_ =
308 !hasOutflow_; // singular all-Neumann -> remove mean; Dirichlet outflow -> non-singular
309 }
310 // hold the pressure/correction ghost at 0 on outflow faces (open face -> Dirichlet p=0). Call
311 // after every (periodic) fill of a solution / search-direction field, on the level it lives.
313 if (!hasOutflow_)
314 return;
315 B3 e{ext.x, ext.y, ext.z};
316 for (int a = 0; a < 3; ++a)
317 for (int s = 0; s < 2; ++s)
318 if (bc_[2 * a + s] == 3)
319 bcZeroPressureGhost(x, e, G, a, s);
320 }
321 // re-impose the non-periodic boundary openness a periodic fill leaves wrong: Neumann wall/inflow
322 // -> 0 (closed), Dirichlet outflow -> left open. Call after every (periodic) openness fill, per
323 // level.
325 if (!hasBC_)
326 return;
327 B3 e{lv.ext.x, lv.ext.y, lv.ext.z};
328 CCField oa[3] = {lv.ox, lv.oy, lv.oz};
329 for (int a = 0; a < 3; ++a)
330 for (int s = 0; s < 2; ++s) {
331 const int t = bc_[2 * a + s];
332 if (t == 1 || t == 2)
333 bcSetOpenness(oa[a], e, G, a, s, 0.0); // wall/inflow Neumann -> closed
334 else if (t == 3)
335 bcSetOpenness(oa[a], e, G, a, s, 1.0); // outflow -> open (periodic fill wraps wrong)
336 }
337 }
338
339 // rediscretized cut-cell operator on every level from the fine face openness (idx2 = 1/dx^2
340 // fine).
341 void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2) {
342 Level& f = lv_[0];
343 Kokkos::deep_copy(f.ox, ox);
344 Kokkos::deep_copy(f.oy, oy);
345 Kokkos::deep_copy(f.oz, oz);
347 f); // periodic fine-level openness ghosts (the operator reads the + neighbour face);
348 // idempotent when the caller already filled them, required when it passed inner-only.
350 f); // re-impose non-periodic wall/inflow faces the periodic fill clobbered
351 buildCutcellOp(f.AC, f.AW, f.AE, f.AS, f.AN, f.AB, f.AT, CCConst(f.ox), CCConst(f.oy),
352 CCConst(f.oz), f.ext, G, idx2, idy2, idz2);
353 for (int L = 1; L < (int)lv_.size(); ++L) {
354 Level& c = lv_[L];
355 Level& fin = lv_[L - 1];
356 coarsenOpenAvg(c.ox, c.oy, c.oz, CCConst(fin.ox), CCConst(fin.oy), CCConst(fin.oz), c.ext,
357 fin.ext, G, c.inner, fin.ratio);
358 fillOpenness(c); // periodic ghost openness (operator build reads the + neighbour face)
359 applyBoundaryOpenness(c); // re-impose non-periodic boundary faces per coarse level
360 const double sx = 1.0 / (double)(c.cfac.x * c.cfac.x),
361 sy = 1.0 / (double)(c.cfac.y * c.cfac.y),
362 sz = 1.0 / (double)(c.cfac.z * c.cfac.z);
363 buildCutcellOp(c.AC, c.AW, c.AE, c.AS, c.AN, c.AB, c.AT, CCConst(c.ox), CCConst(c.oy),
364 CCConst(c.oz), c.ext, G, idx2 * sx, idy2 * sy, idz2 * sz);
365 }
366 }
367
368 // CG preconditioned by one symmetric V-cycle (solve_pcg port). rhs on level 0; solution left in
369 // level-0 x. Returns the iteration count. Scratch supplied by the caller (level-0-sized fields).
371 double rtol, int pre, int post, int bottom) {
372 pre_ = pre;
373 post_ = post;
374 bottom_ = bottom;
375 Level& l0 = lv_[0];
376 Kokkos::deep_copy(l0.x, x);
377 auto matvec = [&](CCField y, CCField v) {
378 fill(l0, v);
379 applyOutflowGhost(l0.ext, v);
380 applyCutcellOp(y, CCConst(v), FPC(l0.AC), FPC(l0.AW), FPC(l0.AE), FPC(l0.AS), FPC(l0.AN),
381 FPC(l0.AB), FPC(l0.AT), l0.ext, G);
382 };
383 auto precond = [&](CCField zz, CCField rr) {
384 Kokkos::deep_copy(l0.rhs, rr);
385 Kokkos::deep_copy(l0.x, 0.0);
386 vcycle(0, /*sym=*/true);
387 Kokkos::deep_copy(zz, l0.x);
388 };
389 matvec(Ap, x); // r = b - A x
390 Kokkos::deep_copy(r, b);
391 axpy(r, -1.0, Ap);
392 removeMean(l0, r); // compatibility: project rhs/residual onto the range
393 const double r0 = maxabs(l0, r);
394 int it = 0;
395 if (r0 > 0.0) {
396 precond(z, r);
397 Kokkos::deep_copy(p, z);
398 double rz = dot(l0, r, z);
399 for (; it < maxit; ++it) {
400 matvec(Ap, p);
401 removeMean(l0, Ap);
402 const double pAp = dot(l0, p, Ap);
403 if (pAp <= 1e-300)
404 break;
405 const double alpha = rz / pAp;
406 axpy(x, alpha, p);
407 axpy(r, -alpha, Ap);
408 removeMean(l0, r);
409 if (maxabs(l0, r) < rtol * r0) {
410 ++it;
411 break;
412 }
413 precond(z, r);
414 const double rznew = dot(l0, r, z), beta = rznew / rz;
415 aypx(p, beta, z);
416 rz = rznew;
417 }
418 }
419 Kokkos::deep_copy(l0.x, x);
420 removeMean(l0, l0.x);
421 Kokkos::deep_copy(x, l0.x);
422 return it;
423 }
424
425 public: // (public for nvcc extended-lambda rule)
426 void vcycle(int L, bool sym) {
427 Level& lv = lv_[L];
428 if (L + 1 == (int)lv_.size()) {
429 if (useGraphAmgBottom_)
431 lv); // agglomerated mesh-agnostic coarse solve (decomposition-agnostic)
432 else
433 smooth(lv, bottom_, false);
434 removeMean(lv, lv.x);
435 return;
436 }
437 smooth(lv, pre_, false);
438 residualCutcell(lv.res, CCConst(lv.x), CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE),
439 FPC(lv.AS), FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), lv.ext, G);
440 Level& cs = lv_[L + 1];
441 restrictAvg(cs.rhs, CCConst(lv.res), cs.ext, lv.ext, G, cs.inner, lv.ratio);
442 Kokkos::deep_copy(cs.x, 0.0);
443 vcycle(L + 1, sym);
444 fill(cs, cs.x);
445 applyOutflowGhost(cs.ext, cs.x);
446 prolongAdd(lv.x, CCConst(cs.x), lv.ext, cs.ext, G, lv.inner, lv.ratio);
447 smooth(lv, post_, /*reverse=*/sym);
448 removeMean(lv, lv.x);
449 }
450 void smooth(Level& lv, int sweeps, bool reverse) {
451 const C3 og = lv.og; // global red-black parity (block inner origin); {0,0,0} single-rank
452 for (int k = 0; k < sweeps; ++k)
453 for (int s = 0; s < 2; ++s) {
454 const int color = reverse ? (1 - s) : s;
455 fill(lv, lv.x);
456 applyOutflowGhost(lv.ext, lv.x);
457 cutcellSmoothColor(lv.x, CCConst(lv.rhs), FPC(lv.AC), FPC(lv.AW), FPC(lv.AE), FPC(lv.AS),
458 FPC(lv.AN), FPC(lv.AB), FPC(lv.AT), lv.ext, og, G, color);
459 }
460 }
461
462 // --- Agglomerated GraphAMG bottom solve --------------------------------------------------------
463 // Assemble the coarsest level's cut-cell operator as a GLOBAL CSR (gathered to rank 0) and build
464 // a mesh-agnostic smoothed-aggregation AMG on it. Decomposition-agnostic: the CSR is keyed by
465 // GLOBAL cell id (periodic-wrapped neighbours), so any (weighted) ORB gives the SAME operator.
466 void buildAmg(Level& lv) {
467 int gbx = gnxF_, gby = gnyF_,
468 gbz = gnzF_; // bottom global dims (coarsen by the ratios above it)
469 for (int L = 0; L + 1 < (int)lv_.size(); ++L) {
470 gbx /= lv_[L].ratio.x;
471 gby /= lv_[L].ratio.y;
472 gbz /= lv_[L].ratio.z;
473 }
474 amgGlobalN_ = gbx * gby * gbz;
475 const int nx = lv.inner.x, ny = lv.inner.y, nz = lv.inner.z, ex = lv.ext.x, ey = lv.ext.y;
476 auto host = [](FPV v) {
477 auto h = Kokkos::create_mirror_view(v);
478 Kokkos::deep_copy(h, v);
479 return h;
480 };
481 auto hC = host(lv.AC), hW = host(lv.AW), hE = host(lv.AE), hS = host(lv.AS), hN = host(lv.AN),
482 hB = host(lv.AB), hT = host(lv.AT);
483 // this rank's rows: (gid, diag) and off-diagonals (gid -> ngid, coef), periodic-wrapped.
484 std::vector<int> lgid, lrow, lcol;
485 std::vector<double> ldiag, lval;
486 amgGlobalOfLocal_.clear();
487 const int band[6][3] = {{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}};
488 for (int k = 0; k < nz; ++k)
489 for (int j = 0; j < ny; ++j)
490 for (int i = 0; i < nx; ++i) {
491 const long p = (long)(i + G) + (long)(j + G) * ex + (long)(k + G) * ex * ey;
492 const int gx = lv.og.x + i, gy = lv.og.y + j, gz = lv.og.z + k;
493 const int gid = gx + gy * gbx + gz * gbx * gby;
494 amgGlobalOfLocal_.push_back(gid);
495 lgid.push_back(gid);
496 // solid cells (all faces closed => diag 0, no coupling) get an identity row so D^-1 is
497 // finite; their rhs is 0, so x stays 0 (correct — no flow inside the solid).
498 const double dc = (double)hC(p);
499 ldiag.push_back(dc != 0.0 ? dc : 1.0);
500 const double bc[6] = {(double)hW(p), (double)hE(p), (double)hS(p),
501 (double)hN(p), (double)hB(p), (double)hT(p)};
502 for (int d = 0; d < 6; ++d) {
503 if (bc[d] == 0.0)
504 continue; // closed face (wall) -> no coupling
505 // A face crossing the domain boundary couples to the wrapped cell ONLY on a periodic
506 // axis (bc_ type 0). On a non-periodic axis an OPEN boundary face is the Dirichlet
507 // outflow anchor: its coefficient lives in the diagonal (already in AC) with NO
508 // off-diagonal partner — wrapping it would add a spurious top<->bottom coupling and
509 // (with the mean projection skipped) a wrong, possibly indefinite bottom matrix.
510 const int rx = gx + band[d][0], ry = gy + band[d][1], rz = gz + band[d][2];
511 const int axis = d / 2;
512 const bool crosses = (axis == 0 && (rx < 0 || rx >= gbx)) ||
513 (axis == 1 && (ry < 0 || ry >= gby)) ||
514 (axis == 2 && (rz < 0 || rz >= gbz));
515 if (crosses && bc_[d] != 0)
516 continue; // non-periodic boundary face: Dirichlet anchor stays diagonal-only
517 const int ngx = (rx % gbx + gbx) % gbx;
518 const int ngy = (ry % gby + gby) % gby;
519 const int ngz = (rz % gbz + gbz) % gbz;
520 lrow.push_back(gid);
521 lcol.push_back(ngx + ngy * gbx + ngz * gbx * gby);
522 lval.push_back(bc[d]);
523 }
524 }
525 // gather every rank's rows to rank 0 (no-op / identity single-rank).
526 std::vector<int> ggid = lgid, grow = lrow, gcol = lcol;
527 std::vector<double> gdiag = ldiag, gval = lval;
528 int rank0 = true;
529#ifdef PECLET_FLOW_MPI
530 if (distributed_) {
531 int rank = 0;
532 MPI_Comm_rank(comm_, &rank);
533 rank0 = (rank == 0);
534 gatherv(lgid, ggid);
536 gatherv(lrow, grow);
537 gatherv(lcol, gcol);
538 gatherv(lval, gval);
539 }
540#endif
541 if (rank0) { // assemble the global CSR keyed by gid
542 peclet::core::solver::HostCsrOp A;
543 A.n = amgGlobalN_;
544 A.diag.assign((std::size_t)amgGlobalN_, 0.0);
545 for (std::size_t r = 0; r < ggid.size(); ++r)
546 A.diag[(std::size_t)ggid[r]] = gdiag[r];
547 std::vector<std::vector<std::pair<int, double>>> rows((std::size_t)amgGlobalN_);
548 for (std::size_t e = 0; e < grow.size(); ++e)
549 rows[(std::size_t)grow[e]].push_back({gcol[e], gval[e]});
550 A.start.assign((std::size_t)amgGlobalN_ + 1, 0);
551 for (int r = 0; r < amgGlobalN_; ++r)
552 A.start[(std::size_t)r + 1] = A.start[(std::size_t)r] + (long)rows[(std::size_t)r].size();
553 A.nbr.reserve(grow.size());
554 A.coef.reserve(grow.size());
555 for (int r = 0; r < amgGlobalN_; ++r)
556 for (auto& [c, v] : rows[(std::size_t)r]) {
557 A.nbr.push_back(c);
558 A.coef.push_back(v);
559 }
560 amgA_ = A;
561 amg_ = std::make_shared<peclet::core::solver::GraphAMG>();
562 amg_->build(A);
563 }
564 }
565 // Solve the coarsest level with the agglomerated AMG: gather rhs -> rank 0,
566 // GraphAMG-preconditioned CG on the singular (mean-removed) Poisson, scatter the solution back.
568 if (!amg_ && !distributed_)
569 buildAmg(lv);
570#ifdef PECLET_FLOW_MPI
571 if (distributed_ && amgGlobalN_ == 0)
572 buildAmg(lv);
573#endif
574 const int nx = lv.inner.x, ny = lv.inner.y, nz = lv.inner.z, ex = lv.ext.x, ey = lv.ext.y;
575 auto hrhs = Kokkos::create_mirror_view(lv.rhs);
576 Kokkos::deep_copy(hrhs, lv.rhs);
577 std::vector<double> lb;
578 lb.reserve(amgGlobalOfLocal_.size());
579 for (int k = 0; k < nz; ++k)
580 for (int j = 0; j < ny; ++j)
581 for (int i = 0; i < nx; ++i)
582 lb.push_back((double)hrhs((long)(i + G) + (long)(j + G) * ex + (long)(k + G) * ex * ey));
583 // gather rhs by global id to rank 0 -> b; solve; keep the (rank 0) solution to scatter.
584 std::vector<double> z((std::size_t)std::max(amgGlobalN_, 1), 0.0);
585 bool rank0 = true;
586#ifdef PECLET_FLOW_MPI
587 int rank = 0;
588 if (distributed_) {
589 MPI_Comm_rank(comm_, &rank);
590 rank0 = (rank == 0);
591 std::vector<int> ggid;
592 std::vector<double> gb;
593 gatherv(amgGlobalOfLocal_, ggid);
594 gatherv(lb, gb);
595 if (rank0) {
596 std::vector<double> b((std::size_t)amgGlobalN_, 0.0);
597 for (std::size_t r = 0; r < ggid.size(); ++r)
598 b[(std::size_t)ggid[r]] = gb[r];
599 pcgAmg(b, z);
600 }
601 MPI_Bcast(z.data(), amgGlobalN_, MPI_DOUBLE, 0, comm_);
602 } else
603#endif
604 {
605 std::vector<double> b(lb.begin(), lb.end());
606 pcgAmg(b, z);
607 }
608 (void)rank0;
609 // scatter z[gid] back into this rank's inner cells.
610 auto hx = Kokkos::create_mirror_view(lv.x);
611 Kokkos::deep_copy(hx, 0.0);
612 std::size_t c = 0;
613 for (int k = 0; k < nz; ++k)
614 for (int j = 0; j < ny; ++j)
615 for (int i = 0; i < nx; ++i)
616 hx((long)(i + G) + (long)(j + G) * ex + (long)(k + G) * ex * ey) =
617 z[(std::size_t)amgGlobalOfLocal_[c++]];
618 Kokkos::deep_copy(lv.x, hx);
619 }
620 // GraphAMG-preconditioned CG on the global bottom operator. For the periodic/all-Neumann case the
621 // operator is singular (constant null space) and the mean must be projected out of the rhs and
622 // the preconditioned residual (compatibility). With a Dirichlet outflow (removeMean_ == false)
623 // the operator is NON-singular and the projection must be SKIPPED — removing the constant from a
624 // non-singular system returns a wrong bottom correction and the V-cycle around it diverges.
625 // Runs on rank 0 only.
626 void pcgAmg(std::vector<double>& b, std::vector<double>& x) {
627 const std::size_t n = (std::size_t)amgGlobalN_;
628 auto meanZero = [&](std::vector<double>& v) {
629 if (!removeMean_)
630 return; // Dirichlet-anchored (outflow) operator: non-singular, no null space to project
631 double m = 0;
632 for (double e : v)
633 m += e;
634 m /= (double)n;
635 for (double& e : v)
636 e -= m;
637 };
638 meanZero(b);
639 x.assign(n, 0.0);
640 std::vector<double> r = b, z(n), p(n), Ap(n);
641 amg_->apply(r, z);
642 meanZero(z);
643 p = z;
644 auto dot = [&](const std::vector<double>& a, const std::vector<double>& c) {
645 double s = 0;
646 for (std::size_t i = 0; i < n; ++i)
647 s += a[i] * c[i];
648 return s;
649 };
650 double rz = dot(r, z), r0 = std::sqrt(dot(r, r));
651 for (int it = 0; it < 200 && r0 > 0; ++it) {
652 amgA_.apply(p, Ap);
653 const double a = rz / dot(p, Ap);
654 for (std::size_t i = 0; i < n; ++i) {
655 x[i] += a * p[i];
656 r[i] -= a * Ap[i];
657 }
658 if (std::sqrt(dot(r, r)) <= 1e-10 * r0)
659 break;
660 amg_->apply(r, z);
661 meanZero(z);
662 const double rzn = dot(r, z);
663 const double beta = rzn / rz;
664 rz = rzn;
665 for (std::size_t i = 0; i < n; ++i)
666 p[i] = z[i] + beta * p[i];
667 }
668 meanZero(x);
669 }
670#ifdef PECLET_FLOW_MPI
671 template <class T>
672 void gatherv(const std::vector<T>& local, std::vector<T>& all) {
673 int rank = 0, size = 1;
674 MPI_Comm_rank(comm_, &rank);
675 MPI_Comm_size(comm_, &size);
676 const int lbytes = (int)(local.size() * sizeof(T));
677 std::vector<int> bc(size), bd(size, 0);
678 MPI_Gather(&lbytes, 1, MPI_INT, bc.data(), 1, MPI_INT, 0, comm_);
679 int tot = 0;
680 if (rank == 0) {
681 for (int r = 0; r < size; ++r) {
682 bd[r] = tot;
683 tot += bc[r];
684 }
685 all.resize((std::size_t)tot / sizeof(T));
686 }
687 MPI_Gatherv(local.data(), lbytes, MPI_BYTE, rank == 0 ? all.data() : nullptr,
688 rank == 0 ? bc.data() : nullptr, rank == 0 ? bd.data() : nullptr, MPI_BYTE, 0,
689 comm_);
690 }
691#endif
692
693 // periodic ghost fill (3 axes) of a level-sized field / the openness triple. Distributed: the
694 // per-level core halo (cross-rank + periodic in one call).
695 void fill(Level& lv, CCField f) {
696#ifdef PECLET_FLOW_MPI
697 if (distributed_) {
698 lv.dev->exchange(f);
699 return;
700 }
701#endif
702 fillAxis(lv, f, 0);
703 fillAxis(lv, f, 1);
704 fillAxis(lv, f, 2);
705 }
706 void fillOpenness(Level& lv) {
707 fill(lv, lv.ox);
708 fill(lv, lv.oy);
709 fill(lv, lv.oz);
710 }
711 void fillAxis(Level& lv, CCField f, int axis) {
713 C3 e = lv.ext;
714 int N3[3] = {lv.inner.x, lv.inner.y, lv.inner.z};
715 int dims[3] = {e.x, e.y, e.z};
716 long st[3] = {1, e.x, (long)e.x * e.y};
717 const int a = axis, b = (axis + 1) % 3, c = (axis + 2) % 3;
718 const long sa = st[a], sb = st[b], sc = st[c];
719 const int N = N3[a];
720 CCField ff = f;
721 Kokkos::parallel_for(
722 "peclet::flow::mg_pfill",
723 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<2>>(space, {0, 0}, {dims[b], dims[c]}),
724 KOKKOS_LAMBDA(int p0, int p1) {
725 const long base = (long)p0 * sb + (long)p1 * sc;
726 for (int gl = 0; gl < G; ++gl) {
727 ff(base + (long)gl * sa) = ff(base + (long)(gl + N) * sa);
728 ff(base + (long)(G + N + gl) * sa) = ff(base + (long)(G + gl) * sa);
729 }
730 });
731 }
732 void axpy(CCField y, double a, CCField x) {
734 CCField yy = y, xx = x;
735 std::size_t n = y.extent(0);
736 Kokkos::parallel_for(
737 "mgaxpy", Kokkos::RangePolicy<CCExec>(space, 0, n),
738 KOKKOS_LAMBDA(std::size_t i) { yy(i) += a * xx(i); });
739 }
740 void aypx(CCField y, double a, CCField x) {
742 CCField yy = y, xx = x;
743 std::size_t n = y.extent(0);
744 Kokkos::parallel_for(
745 "mgaypx", Kokkos::RangePolicy<CCExec>(space, 0, n),
746 KOKKOS_LAMBDA(std::size_t i) { yy(i) = xx(i) + a * yy(i); });
747 }
748 void scale(CCField y, double a) {
750 CCField yy = y;
751 std::size_t n = y.extent(0);
752 Kokkos::parallel_for(
753 "mgscale", Kokkos::RangePolicy<CCExec>(space, 0, n),
754 KOKKOS_LAMBDA(std::size_t i) { yy(i) *= a; });
755 }
756 void lin(CCField out, double a, CCField x, double b, CCField y) { // out = a*x + b*y (mg_lin_k)
758 CCField oo = out, xx = x, yy = y;
759 std::size_t n = out.extent(0);
760 Kokkos::parallel_for(
761 "mglin", Kokkos::RangePolicy<CCExec>(space, 0, n),
762 KOKKOS_LAMBDA(std::size_t i) { oo(i) = a * xx(i) + b * yy(i); });
763 }
764 // zero the solid-cell entries (AC<=tiny) -> project out the solid null modes (mg_mask_solid_k).
765 void maskSolid(Level& lv, CCField f) {
767 CCField ff = f;
768 FPV ac = lv.AC;
769 std::size_t n = f.extent(0);
770 Kokkos::parallel_for(
771 "mgmasksolid", Kokkos::RangePolicy<CCExec>(space, 0, n), KOKKOS_LAMBDA(std::size_t i) {
772 if (!(ac(i) > 1e-30f))
773 ff(i) = 0.0;
774 });
775 }
776
777 // Estimate the spectral bounds [lmin,lmax] of M^{-1}A (M^{-1} = one symmetric V-cycle) by power
778 // iteration (direct for the max + a shifted iteration for the min), seeded by `seed`.
779 // Communication-heavy, so the CUDA driver runs it once on step 1 and reuses the bounds. Port of
780 // estimate_eigenvalues.
781 void estimateEigenvalues(CCConst seed, double& lmin, double& lmax, int iters, int pre, int post,
782 int bottom) {
783 pre_ = pre;
784 post_ = post;
785 bottom_ = bottom;
786 Level& l0 = lv_[0];
787 const std::size_t n = l0.n;
788 CCField v("ev_v", n), w("ev_w", n), z("ev_z", n), srhs("ev_srhs", n);
789 Kokkos::deep_copy(srhs, seed);
790 auto matvec = [&](CCField y, CCField x) {
791 fill(l0, x);
792 applyOutflowGhost(l0.ext, x);
793 applyCutcellOp(y, CCConst(x), FPC(l0.AC), FPC(l0.AW), FPC(l0.AE), FPC(l0.AS), FPC(l0.AN),
794 FPC(l0.AB), FPC(l0.AT), l0.ext, G);
795 };
796 auto applyT = [&](CCField out,
797 CCField in) { // out = M^{-1} A in, projected onto the fluid range
798 matvec(w, in);
799 Kokkos::deep_copy(l0.rhs, w);
800 Kokkos::deep_copy(l0.x, 0.0);
801 vcycle(0, /*sym=*/true);
802 Kokkos::deep_copy(out, l0.x);
803 removeMean(l0, out);
804 maskSolid(l0, out);
805 };
806 auto normalize = [&](CCField x) {
807 double nr = std::sqrt(dot(l0, x, x));
808 if (nr > 0)
809 scale(x, 1.0 / nr);
810 };
811 auto seedf = [&](CCField x) {
812 Kokkos::deep_copy(x, srhs);
813 removeMean(l0, x);
814 maskSolid(l0, x);
815 normalize(x);
816 };
817 seedf(v);
818 lmax = 1.0;
819 for (int k = 0; k < iters; ++k) {
820 applyT(z, v);
821 lmax = dot(l0, v, z);
822 Kokkos::deep_copy(v, z);
823 normalize(v);
824 }
825 seedf(v);
826 double mu = 0.0;
827 for (int k = 0; k < iters; ++k) {
828 applyT(z, v);
829 lin(z, lmax, v, -1.0, z); // z = lmax*v - T v
830 mu = dot(l0, v, z);
831 Kokkos::deep_copy(v, z);
832 normalize(v);
833 }
834 double e_hi = lmax, e_lo = lmax - mu; // direct (max) + shifted (min) Rayleigh estimates
835 lmin = e_lo < e_hi ? e_lo : e_hi;
836 lmax = e_lo < e_hi ? e_hi : e_lo; // robust min/max bracket
837 if (lmin < 0.02 * lmax)
838 lmin = 0.02 * lmax;
839 }
840
841 // Chebyshev semi-iteration preconditioned by ONE symmetric V-cycle -- same goal as solvePCG but
842 // the step coefficients come from the spectral bounds [a,b], so NO per-iteration global
843 // dot-products (communication- light at scale). rhs on level-0 supplied as `b`; solution left in
844 // `x`. Returns the V-cycle count. Port of solve_chebyshev.
845 int solveChebyshev(CCField b, CCField x, int maxit, double rtol, int pre, int post, int bottom,
846 double a, double bnd) {
847 pre_ = pre;
848 post_ = post;
849 bottom_ = bottom;
850 Level& l0 = lv_[0];
851 const std::size_t n = l0.n;
852 if (a > bnd) {
853 double t = a;
854 a = bnd;
855 bnd = t;
856 } // robust to swapped bounds
857 a *= 0.95;
858 bnd *= 1.05; // safety margin: [a,b] must bracket the spectrum
859 CCField r("cb_r", n), z("cb_z", n), d("cb_d", n), w("cb_w", n);
860 auto matvec = [&](CCField y, CCField v) {
861 fill(l0, v);
862 applyOutflowGhost(l0.ext, v);
863 applyCutcellOp(y, CCConst(v), FPC(l0.AC), FPC(l0.AW), FPC(l0.AE), FPC(l0.AS), FPC(l0.AN),
864 FPC(l0.AB), FPC(l0.AT), l0.ext, G);
865 };
866 auto precond = [&](CCField zz, CCField rr) {
867 Kokkos::deep_copy(l0.rhs, rr);
868 Kokkos::deep_copy(l0.x, 0.0);
869 vcycle(0, /*sym=*/true);
870 Kokkos::deep_copy(zz, l0.x);
871 };
872 const double theta = 0.5 * (bnd + a), delta = 0.5 * (bnd - a), sigma1 = theta / delta;
873 double rho = 1.0 / sigma1;
874 matvec(w, x); // r = b - A x
875 Kokkos::deep_copy(r, b);
876 axpy(r, -1.0, w);
877 removeMean(l0, r);
878 const double r0 = maxabs(l0, r);
879 int nvc = 0;
880 if (r0 > 0.0) {
881 precond(z, r);
882 ++nvc; // z = M^{-1} r
883 lin(d, 1.0 / theta, z, 0.0, z);
884 axpy(x, 1.0, d); // d = z/theta; x += d
885 for (int i = 1; i < maxit; ++i) {
886 matvec(w, d);
887 axpy(r, -1.0, w);
888 removeMean(l0, r); // r -= A d
889 if (maxabs(l0, r) < rtol * r0)
890 break;
891 precond(z, r);
892 ++nvc;
893 const double rho_new = 1.0 / (2.0 * sigma1 - rho);
894 lin(d, rho_new * rho, d, 2.0 * rho_new / delta, z);
895 axpy(x, 1.0, d); // d update; x += d
896 rho = rho_new;
897 }
898 }
899 removeMean(l0, x);
900 return nvc;
901 }
902 // reductions / mean removal over inner FLUID cells (AC>tiny) of a level.
903 double dot(Level& lv, CCField a, CCField b) {
905 C3 e = lv.ext;
906 CCField aa = a, bb = b;
907 FPV ac = lv.AC;
908 double s = 0;
909 Kokkos::parallel_reduce(
910 "mgdot",
911 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
912 {e.x - G, e.y - G, e.z - G}),
913 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
914 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
915 if (ac(i) > 1e-30f)
916 acc += aa(i) * bb(i);
917 },
918 s);
919 return allreduce(s, MPI_SUM_);
920 }
921 double maxabs(Level& lv, CCField a) {
923 C3 e = lv.ext;
924 CCField aa = a;
925 FPV ac = lv.AC;
926 double m = 0;
927 Kokkos::parallel_reduce(
928 "mgmax",
929 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
930 {e.x - G, e.y - G, e.z - G}),
931 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
932 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
933 if (ac(i) > 1e-30f) {
934 const double v = Kokkos::fabs(aa(i));
935 if (v > acc)
936 acc = v;
937 }
938 },
939 Kokkos::Max<double>(m));
940 return allreduce(m, MPI_MAX_);
941 }
942 void removeMean(Level& lv, CCField f) {
943 if (!removeMean_)
944 return; // non-singular operator (Dirichlet outflow present) -> no null-space projection
946 C3 e = lv.ext;
947 CCField ff = f;
948 FPV ac = lv.AC;
949 double sum = 0;
950 long cnt = 0;
951 Kokkos::parallel_reduce(
952 "mgmeanr",
953 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
954 {e.x - G, e.y - G, e.z - G}),
955 KOKKOS_LAMBDA(int x, int y, int z, double& s, long& k) {
956 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
957 if (ac(i) > 1e-30f) {
958 s += ff(i);
959 k += 1;
960 }
961 },
962 sum, cnt);
963 sum = allreduce(sum, MPI_SUM_);
964 cnt = (long)allreduce((double)cnt, MPI_SUM_); // global fluid sum + count
965 if (cnt == 0)
966 return;
967 const double mean = sum / (double)cnt;
968 Kokkos::parallel_for(
969 "mgmeans",
970 Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>(space, {G, G, G},
971 {e.x - G, e.y - G, e.z - G}),
972 KOKKOS_LAMBDA(int x, int y, int z) {
973 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
974 if (ac(i) > 1e-30f)
975 ff(i) -= mean;
976 });
977 }
978
979 private:
980 enum AllOp { kSum, kMax };
981 // Global reduction over ranks (no-op single-rank / non-distributed -> byte-identical to the local
982 // reduce).
983 double allreduce(double v, AllOp op) {
984#ifdef PECLET_FLOW_MPI
985 if (distributed_) {
986 double g = 0;
987 MPI_Allreduce(&v, &g, 1, MPI_DOUBLE, op == kSum ? MPI_SUM : MPI_MAX, comm_);
988 return g;
989 }
990#endif
991 (void)op;
992 return v;
993 }
994 static constexpr AllOp MPI_SUM_ = kSum, MPI_MAX_ = kMax;
995
996 std::vector<Level> lv_;
997 int pre_ = 2, post_ = 2, bottom_ = 4;
998 int bc_[6] = {0, 0, 0, 0, 0, 0};
999 bool hasBC_ = false, removeMean_ = true, hasOutflow_ = false;
1000 bool distributed_ = false;
1001 // --- decomposition-agnostic algebraic bottom solve (GraphAMG) ---
1002 // The geometric coarse hierarchy needs a cleanly-coarsening (equal-weight) ORB. Under a WEIGHTED
1003 // decomposition the coarse levels misalign, so the multilevel path is unavailable and only pure
1004 // RB-GS (nLevels==1) works. With this enabled, the coarsest level is solved by an AGGLOMERATED
1005 // algebraic multigrid: the operator + rhs of the coarsest level are gathered to rank 0, solved by
1006 // a mesh-agnostic smoothed-aggregation AMG (core::solver::GraphAMG, exact by construction on any
1007 // decomposition), and the solution scattered back. With nLevels==1 this makes the whole pressure
1008 // solve mesh-independent AND decomposition-agnostic.
1009 bool useGraphAmgBottom_ = false;
1010 int gnxF_ = 0, gnyF_ = 0, gnzF_ = 0; // GLOBAL fine dims (== local single-rank)
1011 mutable std::shared_ptr<peclet::core::solver::GraphAMG>
1012 amg_; // built once from the bottom operator
1013 mutable peclet::core::solver::HostCsrOp
1014 amgA_; // rank 0: the assembled global bottom operator (CG matvec)
1015 mutable std::vector<int> amgOwnerCount_; // rank 0: #bottom cells each rank owns (gather layout)
1016 mutable std::vector<int>
1017 amgGlobalOfLocal_; // this rank's bottom inner cells -> global bottom index
1018 mutable int amgGlobalN_ = 0; // total bottom global cells (rank 0)
1019#ifdef PECLET_FLOW_MPI
1021#endif
1022
1023 public:
1024 // Enable the agglomerated GraphAMG bottom solve (decomposition-agnostic multigrid coarse solve).
1025 // Rebuilds lazily on the next solve. Safe single-rank (local assemble + serial AMG).
1027 useGraphAmgBottom_ = on;
1028 amg_.reset();
1029 }
1030
1031 private:
1032};
1033
1034} // namespace peclet::flow
1035
1036#endif // PECLET_FLOW_MAC_CUTCELL_MG_HPP
void axpy(CCField y, double a, CCField x)
void scale(CCField y, double a)
int solveChebyshev(CCField b, CCField x, int maxit, double rtol, int pre, int post, int bottom, double a, double bnd)
void applyOutflowGhost(C3 ext, CCField x)
void aypx(CCField y, double a, CCField x)
void fillAxis(Level &lv, CCField f, int axis)
void vcycle(int L, bool sym)
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
int solvePCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField Ap, int maxit, double rtol, int pre, int post, int bottom)
void pcgAmg(std::vector< double > &b, std::vector< double > &x)
void setBoundaryConditions(const int bc[6])
double maxabs(Level &lv, CCField a)
static constexpr int G
void maskSolid(Level &lv, CCField f)
void lin(CCField out, double a, CCField x, double b, CCField y)
double dot(Level &lv, CCField a, CCField b)
void fill(Level &lv, CCField f)
void removeMean(Level &lv, CCField f)
void applyBoundaryOpenness(Level &lv)
void graphAmgSolveBottom(Level &lv)
void smooth(Level &lv, int sweeps, bool reverse)
void init(int nx, int ny, int nz, int nLevels)
void estimateEigenvalues(CCConst seed, double &lmin, double &lmax, int iters, int pre, int post, int bottom)
flow — portable (Kokkos) native per-face domain boundary conditions for the MAC grid.
flow — portable (Kokkos) cut-cell pressure operator + Chorin projection.
void bcSetOpenness(BField oa, B3 ext, int g, int a, int s, double val)
Definition mac_bc.hpp:234
Kokkos::View< const MReal *, CCMem > FPC
void bcZeroPressureGhost(BField phi, B3 ext, int g, int a, int s)
Definition mac_bc.hpp:193
void coarsenOpenAvg(CCField oxc, CCField oyc, CCField ozc, CCConst oxf, CCConst oyf, CCConst ozf, C3 cext, C3 fext, int g, C3 cinner, C3 ratio)
void prolongAdd(CCField fine, CCConst coarse, C3 fext, C3 cext, int g, C3 finner, C3 ratio)
void residualCutcell(CCField r, CCConst x, CCConst b, FPC AC, FPC AW, FPC AE, FPC AS, FPC AN, FPC AB, FPC AT, C3 e, int g)
void applyCutcellOp(CCField y, CCConst x, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, int g)
void restrictAvg(CCField coarse, CCConst fine, C3 cext, C3 fext, int g, C3 cinner, C3 ratio)
void buildCutcellOp(OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, CCConst ox, CCConst oy, CCConst oz, C3 e, int g, double gfx, double gfy, double gfz)
Kokkos::View< double *, CCMem > CCField
Kokkos::View< MReal *, CCMem > FPV
void cutcellSmoothColor(CCField phi, CCConst b, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, C3 og, int g, int color)
Kokkos::DefaultExecutionSpace CCExec
Kokkos::View< const double *, CCMem > CCConst
std::unique_ptr< GridHalo< double > > dev
std::unique_ptr< GridHaloTopology< kDim > > halo
static constexpr double AC
static constexpr int N
static constexpr double F