peclet-amr 0.2.0
Block-local-Morton AMR octree + collocated cut-cell Navier–Stokes solver (header-only C++20, Kokkos + MPI)
Loading...
Searching...
No Matches
amg_bottom.hpp
Go to the documentation of this file.
1// amr — the EXACT coarsest-level solve of the pressure multigrid (docs/amr_mg_depth.md §6.6).
2//
3// The §6.2 ladder lifts the root while the grid halves into a cube level. On a badly factored root
4// grid it runs out early — 100^3 stops at 25^3, a 64x64x4 slab stops at 32x32x2 — and 60 damped
5// Jacobi sweeps are then not a solve but a smoother (the §6.6 amplification table: E=4 gives 8e-9
6// in 60 sweeps, E=8 gives 8e-3, E=25 gives 0.60). A V-cycle whose coarsest level is not solved is
7// not mesh-independent, so the bottom there is an AGGLOMERATED direct-ish solve: the level's own FV
8// operator assembled as a sparse SPD matrix and solved by GraphAMG-preconditioned CG. This is
9// flow's `PECLET_FLOW_AGGLOM_EXTENT` bottom, ported onto core's `peclet::core::solver::GraphAMG` —
10// the same class flow uses.
11//
12// WHERE IT RUNS (§5.5). The SETUP is always host — smoothed aggregation is a sequential greedy
13// pass and runs once per `build()`. The per-V-cycle SOLVE follows the bottom's size: at most
14// `kHostMax` = 10^4 rows it stages n_b doubles each way and runs the CG on the host (the round trip
15// is cheaper than the kernel launches at that size); above it the whole CG runs on the device
16// against core's `GraphAMGDevice`, so a big elongated-brick bottom (§5.5: `512x512x4` lifts to
17// `256x256x2` = 131 k cells) never leaves the device. The two paths are the same algorithm; only
18// the dot products reassociate, which perturbs an inner solve converged to `kTol` by ~1e-12.
19// The threshold is §5.5's; §11.11 later MEASURED it to be about an order of magnitude too high
20// (the host CG is ~16 % faster than the 60 sweeps at a 64-cell bottom, ~28 % slower at 4096 rows)
21// and left it, with those numbers, for whoever builds the device path out.
22//
23// It is used by `Multigrid` single-rank and therefore by the replicated stage (§6.5), which IS a
24// single-rank Multigrid on the gathered coarsest in-place level.
25//
26// THE SIGN. §6.6 delegates the sign and volume convention to the implementer, to be confirmed
27// against fv_op.hpp rather than read off the note. The FV operator is
28// (L u)_i = invVol_i · ( Σ_f w_f (u_j − u_i) − bcDiag_i u_i ),
29// so with S_ii = Σ_f w_f + bcDiag_i and S_ij = −w_f (the note's S, symmetric and positive
30// semi-definite):
31// (S u)_i = −(1/invVol_i) · (L u)_i,
32// and the bottom problem L x = b becomes S x = −b/invVol. `Multigrid::setBottomCheck` /
33// `Multigrid::bottomResidual` is the gate that this is right: after every bottom solve it
34// recomputes |b − L x| with the V-CYCLE's own FvOp (§6.6 step 6), which is a different code path
35// from this class's CSR and would not agree if the sign or the volume factor were wrong.
36#ifndef PECLET_AMR_AMG_BOTTOM_HPP
37#define PECLET_AMR_AMG_BOTTOM_HPP
38
39#include <algorithm>
40#include <cmath>
41#include <vector>
42
43#include "peclet/amr/common.hpp"
45#include "peclet/core/common/view.hpp"
46#include "peclet/core/solver/graph_amg.hpp"
47#include "peclet/core/solver/graph_amg_device.hpp"
48
49namespace peclet::amr {
50
62template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
63class AmgBottom {
64 public:
66
68 static constexpr Index kHostMax = 10000;
69
73 void build(const Poisson& ap, bool singular) {
74 const auto A = ap.assembleFv();
75 n_ = static_cast<Index>(A.invVol.size());
76 singular_ = singular;
77 S_ = core::solver::HostCsrOp{};
78 S_.n = n_;
79 S_.start = A.start;
80 S_.nbr = A.nbr;
81 S_.coef.assign(A.coef.size(), 0.0);
82 S_.diag.assign(static_cast<std::size_t>(n_), 0.0);
83 identity_.assign(static_cast<std::size_t>(n_), 0);
84 nIdentity_ = 0;
85 for (Index i = 0; i < n_; ++i) {
86 double s = 0.0;
87 for (Index k = A.start[static_cast<std::size_t>(i)];
88 k < A.start[static_cast<std::size_t>(i) + 1]; ++k) {
89 S_.coef[static_cast<std::size_t>(k)] = -A.coef[static_cast<std::size_t>(k)];
90 s += A.coef[static_cast<std::size_t>(k)]; // resum: S·1 = 0 exactly in the singular case
91 }
92 S_.diag[static_cast<std::size_t>(i)] = s + A.bcDiag[static_cast<std::size_t>(i)];
93 if (S_.diag[static_cast<std::size_t>(i)] <= 0.0) {
94 // A fully closed cell (every face α = 0): an IDENTITY row, rhs 0 and solution 0. Its whole
95 // row and column are already zero, so setting the diagonal to 1 isolates it without
96 // touching any other row — and keeps the matrix SPD for the AMG setup.
97 identity_[static_cast<std::size_t>(i)] = 1;
98 S_.diag[static_cast<std::size_t>(i)] = 1.0;
99 ++nIdentity_;
100 }
101 }
102 buildComponents(A);
103 const core::solver::AmgParams prm; // defaults (§6.6 step 4)
104 onDevice_ = (n_ > kHostMax);
105 dIn_ = View<double>("amgb_in", static_cast<std::size_t>(n_));
106 dOut_ = View<double>("amgb_out", static_cast<std::size_t>(n_));
107 if (onDevice_) {
108 amgD_.build(S_, prm);
109 uploadDevice(A);
110 } else {
111 amg_.build(S_, prm);
112 invVol_ = A.invVol;
113 r_.assign(static_cast<std::size_t>(n_), 0.0);
114 z_.assign(static_cast<std::size_t>(n_), 0.0);
115 p_.assign(static_cast<std::size_t>(n_), 0.0);
116 q_.assign(static_cast<std::size_t>(n_), 0.0);
117 rhs_.assign(static_cast<std::size_t>(n_), 0.0);
118 bHost_.assign(static_cast<std::size_t>(n_), 0.0);
119 xHost_.assign(static_cast<std::size_t>(n_), 0.0);
120 bMirror_ = Kokkos::View<double*, Kokkos::HostSpace>("amgb_b", static_cast<std::size_t>(n_));
121 xMirror_ = Kokkos::View<double*, Kokkos::HostSpace>("amgb_x", static_cast<std::size_t>(n_));
122 }
123 }
124
126 bool ready() const { return n_ > 0; }
128 bool onDevice() const { return onDevice_; }
130 Index size() const { return n_; }
132 Index numIdentityRows() const { return nIdentity_; }
134 int numComponents() const { return nComp_; }
136 int lastIters() const { return lastIters_; }
140 double lastRelResidual() const { return lastRel_; }
141
147 auto in = dIn_;
148 Kokkos::parallel_for("amr::amgb_in", n_, KOKKOS_LAMBDA(const Index i) { in(i) = b(i); });
149 if (onDevice_)
150 solveDevice();
151 else
152 solveHost();
153 auto out = dOut_;
154 Kokkos::parallel_for("amr::amgb_out", n_, KOKKOS_LAMBDA(const Index i) { x(i) = out(i); });
155 }
156
157 private:
158 // ---- host path -----------------------------------------------------------------------------
159 void solveHost() const {
160 Kokkos::deep_copy(bMirror_, dIn_);
161 for (Index i = 0; i < n_; ++i)
162 bHost_[static_cast<std::size_t>(i)] = bMirror_(i);
163 std::fill(xHost_.begin(), xHost_.end(), 0.0);
164 for (Index i = 0; i < n_; ++i)
165 rhs_[static_cast<std::size_t>(i)] =
166 identity_[static_cast<std::size_t>(i)]
167 ? 0.0
168 : -bHost_[static_cast<std::size_t>(i)] / invVol_[static_cast<std::size_t>(i)];
169 project(rhs_);
170 r_ = rhs_;
171 double bnorm = 0.0;
172 for (Index i = 0; i < n_; ++i)
173 bnorm = std::max(bnorm, std::fabs(rhs_[static_cast<std::size_t>(i)]));
174 lastIters_ = 0;
175 lastRel_ = 0.0;
176 if (bnorm > 0.0) {
177 const double rnorm0 = norm2(r_);
178 double rz = 0.0, rzOld = 0.0;
179 for (int it = 0; it < kMaxIters; ++it) {
180 amg_.apply(r_, z_);
181 project(z_); // flow lesson 1: project per COMPONENT, never the all-cell mean
182 rz = dot(r_, z_);
183 if (it == 0) {
184 p_ = z_;
185 } else {
186 const double beta = rz / rzOld;
187 for (Index i = 0; i < n_; ++i)
188 p_[static_cast<std::size_t>(i)] =
189 z_[static_cast<std::size_t>(i)] + beta * p_[static_cast<std::size_t>(i)];
190 }
191 S_.apply(p_, q_);
192 const double pq = dot(p_, q_);
193 if (!(std::fabs(pq) > 0.0))
194 break;
195 const double alpha = rz / pq;
196 for (Index i = 0; i < n_; ++i) {
197 xHost_[static_cast<std::size_t>(i)] += alpha * p_[static_cast<std::size_t>(i)];
198 r_[static_cast<std::size_t>(i)] -= alpha * q_[static_cast<std::size_t>(i)];
199 }
200 rzOld = rz;
201 lastIters_ = it + 1;
202 if (norm2(r_) <= kTol * rnorm0)
203 break;
204 }
205 project(xHost_);
206 for (Index i = 0; i < n_; ++i)
207 if (identity_[static_cast<std::size_t>(i)])
208 xHost_[static_cast<std::size_t>(i)] = 0.0;
209 S_.apply(xHost_, q_);
210 double e = 0.0;
211 for (Index i = 0; i < n_; ++i)
212 e = std::max(
213 e, std::fabs(rhs_[static_cast<std::size_t>(i)] - q_[static_cast<std::size_t>(i)]));
214 lastRel_ = e / bnorm;
215 }
216 for (Index i = 0; i < n_; ++i)
217 xMirror_(i) = xHost_[static_cast<std::size_t>(i)];
218 Kokkos::deep_copy(dOut_, xMirror_);
219 }
220
221 static double dot(const std::vector<double>& a, const std::vector<double>& b) {
222 double s = 0.0;
223 for (std::size_t i = 0; i < a.size(); ++i)
224 s += a[i] * b[i];
225 return s;
226 }
227 static double norm2(const std::vector<double>& a) { return std::sqrt(dot(a, a)); }
228
231 void project(std::vector<double>& v) const {
232 if (!singular_ || nComp_ == 0)
233 return;
234 std::vector<double> sum(static_cast<std::size_t>(nComp_), 0.0);
235 for (Index i = 0; i < n_; ++i) {
236 const Index c = comp_[static_cast<std::size_t>(i)];
237 if (c >= 0)
238 sum[static_cast<std::size_t>(c)] += v[static_cast<std::size_t>(i)];
239 }
240 for (int c = 0; c < nComp_; ++c)
241 sum[static_cast<std::size_t>(c)] /=
242 static_cast<double>(compSize_[static_cast<std::size_t>(c)]);
243 for (Index i = 0; i < n_; ++i) {
244 const Index c = comp_[static_cast<std::size_t>(i)];
245 if (c >= 0)
246 v[static_cast<std::size_t>(i)] -= sum[static_cast<std::size_t>(c)];
247 }
248 }
249
250 // ---- device path ---------------------------------------------------------------------------
251 void uploadDevice(const typename Poisson::FvAssembled& A) {
252 dDiag_ = toDevice(S_.diag, "amgb_diag");
253 dStart_ = toDevice(S_.start, "amgb_start");
254 dNbr_ = toDevice(S_.nbr, "amgb_nbr");
255 dCoef_ = toDevice(S_.coef, "amgb_coef");
256 dInvVol_ = toDevice(A.invVol, "amgb_invvol");
257 dComp_ = toDevice(comp_, "amgb_comp");
258 std::vector<double> ident(static_cast<std::size_t>(n_), 0.0);
259 for (Index i = 0; i < n_; ++i)
260 ident[static_cast<std::size_t>(i)] = identity_[static_cast<std::size_t>(i)] ? 1.0 : 0.0;
261 dIdent_ = toDevice(ident, "amgb_ident");
262 const std::size_t nn = static_cast<std::size_t>(n_);
263 dR_ = View<double>("amgb_r", nn);
264 dZ_ = View<double>("amgb_z", nn);
265 dP_ = View<double>("amgb_p", nn);
266 dQ_ = View<double>("amgb_q", nn);
267 dRhs_ = View<double>("amgb_rhs", nn);
268 }
269
272 void projectDevice(View<double> v) const {
273 if (!singular_ || nComp_ == 0)
274 return;
275 auto comp = dComp_;
276 for (int c = 0; c < nComp_; ++c) {
277 double s = 0.0;
278 Kokkos::parallel_reduce(
279 "amr::amgb_proj_sum", n_,
280 KOKKOS_LAMBDA(const Index i, double& acc) {
281 if (comp(i) == c)
282 acc += v(i);
283 },
284 s);
285 const double m = s / static_cast<double>(compSize_[static_cast<std::size_t>(c)]);
286 Kokkos::parallel_for(
287 "amr::amgb_proj_sub", n_, KOKKOS_LAMBDA(const Index i) {
288 if (comp(i) == c)
289 v(i) -= m;
290 });
291 }
292 }
293
294 void applyS(View<const double> in, View<double> out) const {
295 auto diag = dDiag_;
296 auto start = dStart_;
297 auto nbr = dNbr_;
298 auto coef = dCoef_;
299 Kokkos::parallel_for(
300 "amr::amgb_spmv", n_, KOKKOS_LAMBDA(const Index i) {
301 double s = diag(i) * in(i);
302 for (Index k = start(i); k < start(i + 1); ++k)
303 s += coef(k) * in(nbr(k));
304 out(i) = s;
305 });
306 }
307
308 static double dotDevice(Index n, View<const double> a, View<const double> b) {
309 double s = 0.0;
310 Kokkos::parallel_reduce(
311 "amr::amgb_dot", n, KOKKOS_LAMBDA(const Index i, double& acc) { acc += a(i) * b(i); }, s);
312 return s;
313 }
314 static double maxAbs(Index n, View<const double> a) {
315 double m = 0.0;
316 Kokkos::parallel_reduce(
317 "amr::amgb_maxabs", n,
318 KOKKOS_LAMBDA(const Index i, double& acc) {
319 const double v = a(i) < 0.0 ? -a(i) : a(i);
320 if (v > acc)
321 acc = v;
322 },
323 Kokkos::Max<double>(m));
324 return m;
325 }
326
327 void solveDevice() const {
328 auto rhs = dRhs_;
329 auto invVol = dInvVol_;
330 auto ident = dIdent_;
331 auto b = dIn_;
332 View<double> x = dOut_;
333 Kokkos::parallel_for(
334 "amr::amgb_rhs", n_,
335 KOKKOS_LAMBDA(const Index i) { rhs(i) = (ident(i) != 0.0) ? 0.0 : -b(i) / invVol(i); });
336 projectDevice(dRhs_);
337 Kokkos::deep_copy(x, 0.0);
338 Kokkos::deep_copy(dR_, dRhs_);
339 const double bnorm = maxAbs(n_, View<const double>(dRhs_));
340 lastIters_ = 0;
341 lastRel_ = 0.0;
342 if (bnorm > 0.0) {
343 const double rnorm0 =
344 std::sqrt(dotDevice(n_, View<const double>(dR_), View<const double>(dR_)));
345 double rz = 0.0, rzOld = 0.0;
346 for (int it = 0; it < kMaxIters; ++it) {
347 amgD_.apply(dR_, dZ_);
348 projectDevice(dZ_);
349 rz = dotDevice(n_, View<const double>(dR_), View<const double>(dZ_));
350 if (it == 0) {
351 Kokkos::deep_copy(dP_, dZ_);
352 } else {
353 const double beta = rz / rzOld;
354 auto p = dP_;
355 auto z = dZ_;
356 Kokkos::parallel_for(
357 "amr::amgb_pupd", n_, KOKKOS_LAMBDA(const Index i) { p(i) = z(i) + beta * p(i); });
358 }
359 applyS(View<const double>(dP_), dQ_);
360 const double pq = dotDevice(n_, View<const double>(dP_), View<const double>(dQ_));
361 if (!(std::fabs(pq) > 0.0))
362 break;
363 const double alpha = rz / pq;
364 auto p = dP_;
365 auto q = dQ_;
366 auto r = dR_;
367 Kokkos::parallel_for(
368 "amr::amgb_xupd", n_, KOKKOS_LAMBDA(const Index i) {
369 x(i) += alpha * p(i);
370 r(i) -= alpha * q(i);
371 });
372 rzOld = rz;
373 lastIters_ = it + 1;
374 if (std::sqrt(dotDevice(n_, View<const double>(dR_), View<const double>(dR_))) <=
375 kTol * rnorm0)
376 break;
377 }
378 projectDevice(x);
379 Kokkos::parallel_for(
380 "amr::amgb_ident0", n_, KOKKOS_LAMBDA(const Index i) {
381 if (ident(i) != 0.0)
382 x(i) = 0.0;
383 });
384 applyS(View<const double>(x), dQ_);
385 auto q = dQ_;
386 auto rr = dRhs_;
387 double e = 0.0;
388 Kokkos::parallel_reduce(
389 "amr::amgb_resid", n_,
390 KOKKOS_LAMBDA(const Index i, double& acc) {
391 const double v = rr(i) - q(i);
392 const double av = v < 0.0 ? -v : v;
393 if (av > acc)
394 acc = av;
395 },
396 Kokkos::Max<double>(e));
397 lastRel_ = e / bnorm;
398 }
399 }
400
401 // ---- shared setup --------------------------------------------------------------------------
406 void buildComponents(const typename Poisson::FvAssembled& A) {
407 parent_.resize(static_cast<std::size_t>(n_));
408 for (Index i = 0; i < n_; ++i)
409 parent_[static_cast<std::size_t>(i)] = i;
410 for (Index i = 0; i < n_; ++i) {
411 if (identity_[static_cast<std::size_t>(i)])
412 continue;
413 for (Index k = A.start[static_cast<std::size_t>(i)];
414 k < A.start[static_cast<std::size_t>(i) + 1]; ++k) {
415 if (!(A.coef[static_cast<std::size_t>(k)] > 0.0))
416 continue;
417 const Index j = A.nbr[static_cast<std::size_t>(k)];
418 if (j < 0 || j >= n_ || identity_[static_cast<std::size_t>(j)])
419 continue;
420 unite(i, j);
421 }
422 }
423 comp_.assign(static_cast<std::size_t>(n_), -1);
424 nComp_ = 0;
425 std::vector<Index> label(static_cast<std::size_t>(n_), -1);
426 for (Index i = 0; i < n_; ++i) {
427 if (identity_[static_cast<std::size_t>(i)])
428 continue;
429 const Index r = find(i);
430 if (label[static_cast<std::size_t>(r)] < 0)
431 label[static_cast<std::size_t>(r)] = nComp_++;
432 comp_[static_cast<std::size_t>(i)] = label[static_cast<std::size_t>(r)];
433 }
434 compSize_.assign(static_cast<std::size_t>(nComp_), 0);
435 for (Index i = 0; i < n_; ++i)
436 if (comp_[static_cast<std::size_t>(i)] >= 0)
437 ++compSize_[static_cast<std::size_t>(comp_[static_cast<std::size_t>(i)])];
438 }
439
440 Index find(Index i) const {
441 while (parent_[static_cast<std::size_t>(i)] != i) {
442 parent_[static_cast<std::size_t>(i)] =
443 parent_[static_cast<std::size_t>(parent_[static_cast<std::size_t>(i)])];
444 i = parent_[static_cast<std::size_t>(i)];
445 }
446 return i;
447 }
448 void unite(Index a, Index b) {
449 const Index ra = find(a), rb = find(b);
450 if (ra != rb)
451 parent_[static_cast<std::size_t>(ra)] = rb;
452 }
453
462 static constexpr double kTol = 1e-10;
463 static constexpr int kMaxIters = 100;
464
465 Index n_ = 0, nIdentity_ = 0;
466 bool singular_ = true, onDevice_ = false;
467 int nComp_ = 0;
468 core::solver::HostCsrOp S_;
469 core::solver::GraphAMG amg_;
470 core::solver::GraphAMGDevice amgD_;
471 std::vector<double> invVol_;
472 std::vector<char> identity_;
473 mutable std::vector<Index> parent_;
474 std::vector<Index> comp_, compSize_;
475 mutable std::vector<double> r_, z_, p_, q_, rhs_, bHost_, xHost_;
476 mutable Kokkos::View<double*, Kokkos::HostSpace> bMirror_, xMirror_;
477 View<double> dDiag_, dCoef_, dInvVol_, dIdent_;
478 View<Index> dStart_, dNbr_, dComp_;
479 mutable View<double> dR_, dZ_, dP_, dQ_, dRhs_, dIn_, dOut_;
480 mutable int lastIters_ = 0;
481 mutable double lastRel_ = 0.0;
482};
483
484} // namespace peclet::amr
485
486#endif // PECLET_AMR_AMG_BOTTOM_HPP
The agglomerated exact bottom of the pressure multigrid (docs/amr_mg_depth.md §6.6): the coarsest lev...
double lastRelResidual() const
‖rhs − S x‖∞ / ‖rhs‖∞ of the last solve, measured on the CSR the bottom itself assembled.
int numComponents() const
Connected fluid components; each carries its own constant nullspace mode.
Index numIdentityRows() const
Fully closed cells (every face openness 0), solved as identity rows with solution 0.
void solve(View< const double > b, View< double > x) const
x ← the solution of L x = b on the coarsest level, i.e.
static constexpr Index kHostMax
Above this many rows the per-V-cycle solve runs on the device (§5.5).
bool ready() const
True once build() has assembled a non-empty level.
int lastIters() const
CG iterations of the last solve (0 for a zero right-hand side).
bool onDevice() const
Whether the per-V-cycle solve runs on the device (size() > kHostMax).
Index size() const
Rows of the bottom matrix = cells of the coarsest level.
void build(const Poisson &ap, bool singular)
Assemble S from the coarsest level's AmrPoisson and build the GraphAMG hierarchy.
FvAssembled assembleFv() const
Definition poisson.hpp:474
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
Definition adapt.hpp:51