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
pcg.hpp
Go to the documentation of this file.
1// core — device (Kokkos) multigrid-preconditioned CG for the AMR FV Poisson.
2//
3// A Krylov accelerator on top of the existing device machinery: the matvec is the
4// consistent conservative FV Laplacian `applyFv` (poisson.hpp), the
5// preconditioner is one (or a few) Multigrid V-cycle(s) (multigrid.hpp),
6// and the inner products / vector updates are Kokkos reductions / parallel_fors. This
7// is exactly flow's structured MG-PCG, ported onto the AMR octree CSR: CG accelerates
8// the geometric MG so a given residual is reached in far fewer fine-grid matvecs than
9// stationary V-cycling, on whatever backend Kokkos targets (CUDA / HIP / OpenMP).
10//
11// SPD subtlety: the FV operator L = D^{-1} S (D = diag(cell volume), S the symmetric
12// stencil) is *not* symmetric in the Euclidean inner product, but it is symmetric and
13// negative-definite in the volume-weighted inner product <u,v>_D = Σ V_i u_i v_i. So CG
14// runs on A := −L (SPD in <·,·>_D) and every dot product is volume-weighted. The V-cycle
15// preconditioner solves L z = −r (correction scheme) ⇒ z ≈ A^{-1} r.
16//
17// Singular (periodic, pure-Neumann) case: A has the constant nullspace. The RHS, the
18// residual, and the preconditioned residual are projected volume-weighted-mean-zero each
19// iteration (deflated CG) so the iteration stays in the range space. The homogeneous-
20// Dirichlet build is non-singular (bcDiag > 0) ⇒ no projection.
21//
22// The MG preconditioner is bit-exact deterministic (Jacobi smoother); CG itself depends
23// on global-reduction summation order, so this is the *performance* path — validated by
24// convergence + matching the V-cycle's converged solution, not by host bit-exactness.
25//
26#ifndef PECLET_AMR_PCG_HPP
27#define PECLET_AMR_PCG_HPP
28
29#include <cmath>
30#include <functional>
31
32#include "peclet/amr/common.hpp"
33#include "peclet/amr/fv_op.hpp"
35#include "peclet/core/common/view.hpp"
36#include "peclet/core/solver/vector_ops.hpp"
37
38namespace peclet::amr {
39
40// ---- small device vector primitives (volume-weighted where the FV operator needs it) ----
41
44 Index n) {
45 double s = 0.0;
46 Kokkos::parallel_reduce(
47 "amr::pcg_dotvol", n,
48 KOKKOS_LAMBDA(const Index i, double& acc) { acc += (u(i) * v(i)) / invVol(i); }, s);
49 return s;
50}
51
57inline void buildFluidMask(const FvOp& op, View<double> mask, Index n) {
58 auto start = op.faceStart;
59 auto w = op.faceW;
60 auto bc = op.bcDiag;
61 Kokkos::parallel_for(
62 "amr::pcg_fluidmask", n, KOKKOS_LAMBDA(const Index i) {
63 double d = bc(i);
64 for (Index k = start(i); k < start(i + 1); ++k)
65 d += w(k);
66 mask(i) = (d > 1e-30) ? 1.0 : 0.0;
67 });
68}
69
72 Kokkos::parallel_for("amr::pcg_masksolid", n, KOKKOS_LAMBDA(const Index i) { u(i) *= mask(i); });
73}
74
81 Index n, const std::function<double(double)>& reduce) {
82 Kokkos::parallel_for("amr::pcg_masksolid", n, KOKKOS_LAMBDA(const Index i) { u(i) *= mask(i); });
83 double su = 0.0, sv = 0.0;
84 Kokkos::parallel_reduce(
85 "amr::pcg_meannum", n,
86 KOKKOS_LAMBDA(const Index i, double& a) { a += mask(i) * u(i) / invVol(i); }, su);
87 Kokkos::parallel_reduce(
88 "amr::pcg_meanden", n, KOKKOS_LAMBDA(const Index i, double& a) { a += mask(i) / invVol(i); },
89 sv);
90 if (reduce) {
91 su = reduce(su);
92 sv = reduce(sv);
93 }
94 const double m = (sv > 0.0) ? su / sv : 0.0;
95 Kokkos::parallel_for(
96 "amr::pcg_meansub", n, KOKKOS_LAMBDA(const Index i) { u(i) -= mask(i) * m; });
97}
98
100 Index n) {
101 removeMeanVolReduced(u, invVol, mask, n, {});
102}
103
104// axpy / zpby / negate: the shared device vector primitives (peclet/core/solver/vector_ops.hpp,
105// lifted 2026-09-10) — the same functions under their AMR spelling.
106using solver::axpy;
107using solver::negate;
108using solver::zpby;
109
110// ---------------------------------------------------------------------------
111// MG-preconditioned CG over a Multigrid, driving the system L x = rhs on its
112// finest level. Owns the Krylov scratch; reuses the multigrid's own finest x/b as
113// transient preconditioner storage. Solves A x = b_A with A := −L (SPD in <·,·>_D).
114// ---------------------------------------------------------------------------
115template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
116class PCG {
117 public:
119
120 struct Result {
121 int iters = 0;
122 double res0 = 0.0;
123 double res = 0.0;
124 };
125
127 void setVcycle(int pre, int post, int bottom, double omega) {
128 pre_ = pre;
129 post_ = post;
130 bottom_ = bottom;
131 omega_ = omega;
132 }
134 void setCyclesPerPrec(int k) { cyclesPerPrec_ = k; }
137 void setSingular(bool s) { singular_ = s; }
138
147 void setDistributed(std::function<void(View<double>)> refresh,
148 std::function<double(double)> dotReduce, Index nExt) {
149 haloFn_ = std::move(refresh);
150 dotReduce_ = std::move(dotReduce);
151 nExt_ = nExt;
152 }
153
159 template <class MGT = MG>
161 double tol = 1e-10) {
162 const Index n = mg.numLeaves(0);
163 const FvOp& op = mg.op(0);
164 View<const double> invVol(op.invVol);
165 ensure(n);
166 buildFluidMask(op, mask_, n);
168 Result R;
169 // Project onto the fluid range: always zero the solid cells (their per-region null modes); for
170 // the singular (periodic/all-Neumann) operator also remove the fluid constant. Applied to every
171 // Krylov quantity so the iteration stays in the well-posed fluid range (mirrors flow
172 // removeMean∘maskSolid).
173 auto project = [&](View<double> u) {
174 if (singular_)
175 removeMeanVolReduced(u, invVol, mask, n, dotReduce_);
176 else
177 maskSolid(u, mask, n);
178 };
180 const double s = dotVol(a, b, invVol, n);
181 return dotReduce_ ? dotReduce_(s) : s;
182 };
183
184 // x = 0 ; r = b_A − A·0 = b_A = −rhs (A = −L, b_A = −rhs). Element copy over the local
185 // rows (not deep_copy): rhs may be local-sized while the scratch carries a ghost tail.
186 Kokkos::deep_copy(x, 0.0);
187 {
188 auto r = r_;
189 Kokkos::parallel_for("amr::pcg_r0", n, KOKKOS_LAMBDA(const Index i) { r(i) = -rhs(i); });
190 }
191 project(r_);
192 R.res0 = std::sqrt(vdot(View<const double>(r_), View<const double>(r_)));
193 if (R.res0 == 0.0)
194 return R;
195
196 applyPrec(mg, r_, z_, n); // z = M^{-1} r ≈ A^{-1} r
197 project(z_);
198 Kokkos::deep_copy(p_, z_);
199 double rz = vdot(View<const double>(r_), View<const double>(z_));
200
201 int it = 0;
202 double rnorm = R.res0;
203 for (; it < maxIters; ++it) {
204 // Ap = A p = −L p, projected back onto the fluid range (keeps the search directions there).
205 sync(p_); // ghost tail of the direction before the matvec (no-op single-rank)
206 applyFv(op, View<const double>(p_), Ap_);
207 negate(Ap_, n);
208 project(Ap_);
209 double pAp = vdot(View<const double>(p_), View<const double>(Ap_));
210 if (pAp == 0.0)
211 break;
212 double alpha = rz / pAp;
213 axpy(x, alpha, View<const double>(p_), n); // x += α p
214 axpy(r_, -alpha, View<const double>(Ap_), n); // r −= α Ap
215 project(r_);
216 rnorm = std::sqrt(vdot(View<const double>(r_), View<const double>(r_)));
217 if (rnorm <= tol * R.res0) {
218 ++it;
219 break;
220 }
221 applyPrec(mg, r_, z_, n);
222 project(z_);
224 double beta = rzNew / rz;
225 zpby(p_, View<const double>(z_), beta, n); // p = z + β p
226 rz = rzNew;
227 }
228 project(x); // solid cells exactly 0; fluid mean removed (singular)
229 R.iters = it;
230 R.res = rnorm;
231 return R;
232 }
233
234 private:
235 // z = M^{-1} r : solve L z = −r with `cyclesPerPrec_` V-cycles (correction scheme),
236 // using the multigrid's own finest x/b as scratch. (A = −L ⇒ A z = r ⟺ L z = −r.)
237 // The distributed multigrid sizes its finest x/b at the same nExt as this solver's scratch,
238 // so the deep_copies stay extent-matched.
239 template <class MGT>
240 void applyPrec(MGT& mg, View<double> r, View<double> z, Index n) {
241 Kokkos::deep_copy(mg.b(0), r);
242 negate(mg.b(0), n);
243 Kokkos::deep_copy(mg.x(0), 0.0);
244 for (int k = 0; k < cyclesPerPrec_; ++k)
245 mg.vcycle(pre_, post_, bottom_, omega_);
246 Kokkos::deep_copy(z, mg.x(0));
247 }
248
250 void sync(View<double> v) const {
251 if (haloFn_)
252 haloFn_(v);
253 }
254
255 void ensure(Index n) {
256 if (nExt_ > n)
257 n = nExt_; // scratch carries the ghost tail in distributed solves
258 if (r_.extent(0) == static_cast<std::size_t>(n))
259 return;
260 r_ = View<double>("pcg_r", static_cast<std::size_t>(n));
261 z_ = View<double>("pcg_z", static_cast<std::size_t>(n));
262 p_ = View<double>("pcg_p", static_cast<std::size_t>(n));
263 Ap_ = View<double>("pcg_Ap", static_cast<std::size_t>(n));
264 mask_ = View<double>("pcg_fluidmask", static_cast<std::size_t>(n));
265 }
266
267 View<double> r_, z_, p_, Ap_, mask_;
268 int pre_ = 2, post_ = 2, bottom_ = 40;
269 double omega_ = 0.8;
270 int cyclesPerPrec_ = 1;
271 bool singular_ = true;
272 std::function<void(View<double>)> haloFn_; // ghost-tail refresh (unset ⇒ no-op)
273 std::function<double(double)> dotReduce_; // global reduction (unset ⇒ local)
274 Index nExt_ = 0; // extended (local+ghost) scratch size
275};
276
277} // namespace peclet::amr
278
279#endif // PECLET_AMR_PCG_HPP
void setSingular(bool s)
Whether to project out the constant nullspace (default true; set false for the non-singular homogeneo...
Definition pcg.hpp:137
Result solve(MGT &mg, View< double > x, View< const double > rhs, int maxIters=200, double tol=1e-10)
Solve L x = rhs on mg's finest level into x (size n; nExt distributed).
Definition pcg.hpp:160
void setVcycle(int pre, int post, int bottom, double omega)
V-cycle parameters used for the preconditioner application.
Definition pcg.hpp:127
void setDistributed(std::function< void(View< double >)> refresh, std::function< double(double)> dotReduce, Index nExt)
Distributed solve (docs/amr_distributed_flow.md, rung 3): refresh re-fills the ghost tail [n,...
Definition pcg.hpp:147
void setCyclesPerPrec(int k)
Number of V-cycles per preconditioner application (default 1).
Definition pcg.hpp:134
void removeMeanVol(View< double > u, View< const double > invVol, View< const double > mask, Index n)
Definition pcg.hpp:99
void removeMeanVolReduced(View< double > u, View< const double > invVol, View< const double > mask, Index n, const std::function< double(double)> &reduce)
Project u onto the FLUID range: zero solid cells, then subtract the volume-weighted mean over the flu...
Definition pcg.hpp:80
void buildFluidMask(const FvOp &op, View< double > mask, Index n)
Build the fluid mask: mask(i)=1 where the operator diagonal Σ_f w_f (+ bcDiag) is non-trivial,...
Definition pcg.hpp:57
double dotVol(View< const double > u, View< const double > v, View< const double > invVol, Index n)
Volume-weighted dot <u,v>_D = Σ_i V_i u_i v_i, V_i = 1/invVol_i.
Definition pcg.hpp:43
void maskSolid(View< double > u, View< const double > mask, Index n)
Zero the solid cells (project out the solid null modes).
Definition pcg.hpp:71
void applyFv(const FvOp &op, View< const double > u, View< double > Lu)
Hu = (c0·I + cD·L) u (consistent conservative FV Laplacian, c0=0/cD=1 ⇒ pure L).
Definition fv_op.hpp:112
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
View< Index > faceStart
CSR row offsets, size n+1.
Definition fv_op.hpp:80
View< double > bcDiag
Dirichlet boundary diagonal per cell (0 if periodic), size n.
Definition fv_op.hpp:83
View< double > invVol
1/V_i, size n
Definition fv_op.hpp:79
View< double > faceW
w_f = openness·A_f/d_f per face, size nFaces
Definition fv_op.hpp:82
double res0
initial volume-weighted residual norm
Definition pcg.hpp:122
double res
final volume-weighted residual norm
Definition pcg.hpp:123