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
momentum.hpp
Go to the documentation of this file.
1// core — the AMR momentum solve: the Galerkin momentum multigrid (MomentumMG) over the shared
2// face-CSR solver layer.
3//
4// The assembled operator (MomentumOp), its device kernels (apply / residual / weighted Jacobi /
5// multicolour Gauss–Seidel), the greedy graph colouring and the preconditioned BiCGStab
6// (MomentumSolver) were LIFTED verbatim to peclet/core/solver/ on 2026-09-10 (suite/docs/
7// QUALITY_PLAN.md G.2) — voro's mesh optimiser consumes them, so they are core infrastructure, not
8// AMR. This header keeps every AMR spelling resolving (the using-declarations and the
9// `MomentumSolver<Bits>` alias template below) and holds what IS octree-specific: MomentumMG,
10// whose Galerkin hierarchy is the uniformly-coarsened octree.
11//
12#ifndef PECLET_AMR_MOMENTUM_HPP
13#define PECLET_AMR_MOMENTUM_HPP
14
15#include <cstddef>
16#include <map>
17#include <vector>
18
20#include "peclet/amr/common.hpp"
21#include "peclet/amr/face_csr.hpp" // shared host+device assembled-operator row kernels
22#include "peclet/amr/multigrid.hpp" // restrictField / prolongAdd transfer kernels
23#include "peclet/core/common/view.hpp"
24#include "peclet/core/solver/coloring.hpp"
25#include "peclet/core/solver/csr_bicgstab.hpp"
26#include "peclet/core/solver/csr_operator.hpp"
27#include "peclet/core/solver/vector_ops.hpp"
28
29namespace peclet::amr {
30
31using solver::applyMom;
32using solver::bicgPUpdate;
33using solver::Coloring;
34using solver::dotPlain;
35using solver::greedyColoring;
36using solver::jacobiMom;
37using solver::MomentumOp;
38using solver::momView;
39using solver::multicolorGSMom;
40using solver::residualMom;
41
44template <unsigned Bits = 21u>
45using MomentumSolver = solver::MomentumSolver;
46
47// ===========================================================================
48// MomentumMG — Galerkin geometric multigrid for the momentum operator.
49//
50// The cut-cell momentum operator carries the ξ-polynomial Dirichlet overlay and its
51// D_rescale row scaling, so a *rediscretised* coarse operator (the openness-Helmholtz
52// attempt) mismatches it and makes a poor preconditioner. Instead the coarse operators are
53// built by **Galerkin coarsening** A_c = R·A·P of the exact assembled fine CSR: R = volume
54// average over a coarse cell's children, P = piecewise-constant injection (the same transfer
55// pair the pressure MG uses). This is consistent with the fine operator by construction — it
56// inherits the cut-cell stencil and row scaling, and a coarse cell whose children are all
57// solid (identity rows) stays an identity row (ε-solid-on-coarse emerges for free). The
58// hierarchy is the uniformly-coarsened octree; the smoother is jacobiMom, the residual
59// restriction / correction prolongation are the shared restrictField / prolongAdd.
60// Used as the momentum BiCGStab preconditioner ⇒ the iteration count stays ~flat with N.
61// ===========================================================================
62template <unsigned Bits = 21u>
64 public:
66 using M = typename Octree::M;
67 using Code = typename Octree::Code;
68
71 void build(const Octree& finest, const std::vector<double>& diag0,
72 const std::vector<Index>& start0, const std::vector<Index>& nbr0,
73 const std::vector<double>& coef0) {
74 octs_.clear();
75 octs_.push_back(finest);
76 for (;;) {
77 Octree c = octs_.back();
78 Index merged = c.coarsenIf([](Code, unsigned) { return true; });
79 if (merged == 0 || c.numLeaves() == octs_.back().numLeaves())
80 break;
81 octs_.push_back(c);
82 if (c.numLeaves() == 1)
83 break;
84 }
85 const std::size_t nl = octs_.size();
86 levels_.clear();
87 levels_.resize(nl);
88
89 std::vector<double> hdiag = diag0, hcoef = coef0;
90 std::vector<Index> hstart = start0, hnbr = nbr0;
91 uploadLevel(0, hdiag, hstart, hnbr, hcoef);
92
93 for (std::size_t L = 0; L + 1 < nl; ++L) {
94 const Octree& f = octs_[L];
95 const Octree& c = octs_[L + 1];
96 const Index nf = f.numLeaves(), nc = c.numLeaves();
97 std::vector<Index> c2p(static_cast<std::size_t>(nf));
98 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
99 for (Index i = 0; i < nf; ++i) {
100 // Covering-leaf c2p (see multigrid.hpp): == ancestor+find for merged children, correct
101 // (identity) for root-level rows in mixed-depth ladders, block-alignment-independent.
102 Index p = c.find(f.code(i));
103 c2p[static_cast<std::size_t>(i)] = p;
104 if (p >= 0)
105 ++cnt[static_cast<std::size_t>(p)];
106 }
107 std::vector<Index> cstart(static_cast<std::size_t>(nc) + 1, 0);
108 for (Index p = 0; p < nc; ++p)
109 cstart[static_cast<std::size_t>(p) + 1] =
110 cstart[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
111 std::vector<Index> cidx(static_cast<std::size_t>(nf));
112 std::vector<Index> cur(cstart.begin(), cstart.end() - 1);
113 for (Index i = 0; i < nf; ++i) {
114 Index p = c2p[static_cast<std::size_t>(i)];
115 if (p >= 0)
116 cidx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
117 }
118 levels_[L].c2p = toDevice(c2p, "mmg_c2p");
119 levels_[L].childStart = toDevice(cstart, "mmg_cstart");
120 levels_[L].childIdx = toDevice(cidx, "mmg_cidx");
121
122 // Galerkin A_c[p][q] = (1/n_ch[p]) Σ_{i child of p} ( A[i] entries mapped to parents ).
123 std::vector<std::map<Index, double>> acc(static_cast<std::size_t>(nc));
124 for (Index i = 0; i < nf; ++i) {
125 Index p = c2p[static_cast<std::size_t>(i)];
126 if (p < 0)
127 continue;
128 const double w = 1.0 / static_cast<double>(cnt[static_cast<std::size_t>(p)]);
129 acc[static_cast<std::size_t>(p)][p] += w * hdiag[static_cast<std::size_t>(i)];
130 for (Index k = hstart[static_cast<std::size_t>(i)];
132 Index q = c2p[static_cast<std::size_t>(hnbr[static_cast<std::size_t>(k)])];
133 if (q < 0)
134 continue;
135 acc[static_cast<std::size_t>(p)][q] += w * hcoef[static_cast<std::size_t>(k)];
136 }
137 }
138 std::vector<double> cdiag(static_cast<std::size_t>(nc), 0.0);
139 std::vector<Index> cs(static_cast<std::size_t>(nc) + 1, 0);
140 for (Index p = 0; p < nc; ++p) {
141 int off = 0;
142 for (auto& e : acc[static_cast<std::size_t>(p)]) {
143 if (e.first == p)
144 cdiag[static_cast<std::size_t>(p)] = e.second;
145 else
146 ++off;
147 }
148 cs[static_cast<std::size_t>(p) + 1] = cs[static_cast<std::size_t>(p)] + off;
149 }
150 std::vector<Index> cn(static_cast<std::size_t>(cs[static_cast<std::size_t>(nc)]));
151 std::vector<double> ccoef(static_cast<std::size_t>(cs[static_cast<std::size_t>(nc)]));
152 for (Index p = 0; p < nc; ++p) {
153 Index k = cs[static_cast<std::size_t>(p)];
154 for (auto& e : acc[static_cast<std::size_t>(p)])
155 if (e.first != p) {
156 cn[static_cast<std::size_t>(k)] = e.first;
157 ccoef[static_cast<std::size_t>(k)] = e.second;
158 ++k;
159 }
160 }
161 uploadLevel(L + 1, cdiag, cs, cn, ccoef);
162 hdiag = cdiag;
163 hstart = cs;
164 hnbr = cn;
165 hcoef = ccoef;
166 }
167 for (auto& lv : levels_) {
168 lv.x = View<double>("mmg_x", static_cast<std::size_t>(lv.op.n));
169 lv.b = View<double>("mmg_b", static_cast<std::size_t>(lv.op.n));
170 lv.res = View<double>("mmg_res", static_cast<std::size_t>(lv.op.n));
171 lv.tmp = View<double>("mmg_tmp", static_cast<std::size_t>(lv.op.n));
172 }
173 }
174
177 void setGaussSeidel(bool on) { useGS_ = on; }
178
180 void vcycle(int pre = 2, int post = 2, int bottom = 30, double omega = 0.7, std::size_t L = 0) {
181 Level& lv = levels_[L];
183 if (L + 1 == levels_.size()) {
184 smooth(lv, bottom, omega);
185 return;
186 }
187 smooth(lv, pre, omega);
188 residualMom(lv.op, View<const double>(lv.x), bc, lv.res);
189 Level& cl = levels_[L + 1];
190 restrictField(lv.childStart, lv.childIdx, View<const double>(lv.res), cl.b, cl.op.n);
191 Kokkos::deep_copy(cl.x, 0.0);
192 vcycle(pre, post, bottom, omega, L + 1);
193 prolongAdd(lv.c2p, View<const double>(cl.x), lv.x, lv.op.n);
194 smooth(lv, post, omega);
195 }
196
197 std::size_t numLevels() const { return levels_.size(); }
198 Index numLeaves(std::size_t L = 0) const { return levels_[L].op.n; }
199 View<double> x(std::size_t L = 0) { return levels_[L].x; }
200 View<double> b(std::size_t L = 0) { return levels_[L].b; }
201 const MomentumOp& op(std::size_t L = 0) const { return levels_[L].op; }
202
203 private:
204 struct Level {
205 MomentumOp op;
206 View<double> x, b, res, tmp;
207 View<Index> c2p, childStart, childIdx;
208 Coloring col;
209 };
210 void smooth(Level& lv, int sweeps, double omega) {
212 if (useGS_)
213 // Multicolour GS is stable undamped on this diagonally-dominant operator (ρ/dt + 6μ + FOU >
214 // 0); the passed omega is Jacobi's damping limit (~0.7) and would needlessly weaken GS, so
215 // use 1.0.
216 for (int s = 0; s < sweeps; ++s)
217 multicolorGSMom(lv.op, lv.x, bc, lv.col, 1.0);
218 else
219 for (int s = 0; s < sweeps; ++s)
220 jacobiMom(lv.op, lv.x, bc, lv.tmp, omega);
221 }
222 void uploadLevel(std::size_t L, const std::vector<double>& diag, const std::vector<Index>& start,
223 const std::vector<Index>& nbr, const std::vector<double>& coef) {
224 Level& lv = levels_[L];
225 lv.op.n = static_cast<Index>(diag.size());
226 lv.op.diag = toDevice(diag, "mmg_diag");
227 lv.op.faceStart = toDevice(start, "mmg_start");
228 lv.op.faceNbr = toDevice(nbr, "mmg_nbr");
229 lv.op.faceCoef = toDevice(coef, "mmg_coef");
230 lv.col = greedyColoring(start, nbr, lv.op.n);
231 }
232 std::vector<Octree> octs_;
233 std::vector<Level> levels_;
234 bool useGS_ = false; // multicolour Gauss–Seidel smoother (opt-in; default weighted Jacobi)
235};
236
237} // namespace peclet::amr
238
239#endif // PECLET_AMR_MOMENTUM_HPP
Per-block adaptive octree over block-local Morton codes.
morton::Morton< Dim, Bits > M
typename M::code_type Code
void setGaussSeidel(bool on)
Opt-in: use multicolour Gauss–Seidel as the smoother (per-level colouring built at build) instead of ...
Definition momentum.hpp:177
Index numLeaves(std::size_t L=0) const
Definition momentum.hpp:198
std::size_t numLevels() const
Definition momentum.hpp:197
typename Octree::Code Code
Definition momentum.hpp:67
void vcycle(int pre=2, int post=2, int bottom=30, double omega=0.7, std::size_t L=0)
One V-cycle on level L solving A u = b (correction scheme).
Definition momentum.hpp:180
typename Octree::M M
Definition momentum.hpp:66
const MomentumOp & op(std::size_t L=0) const
Definition momentum.hpp:201
View< double > b(std::size_t L=0)
Definition momentum.hpp:200
void build(const Octree &finest, const std::vector< double > &diag0, const std::vector< Index > &start0, const std::vector< Index > &nbr0, const std::vector< double > &coef0)
Build the Galerkin hierarchy from the finest octree + the assembled fine operator CSR (diag + face CS...
Definition momentum.hpp:71
View< double > x(std::size_t L=0)
Definition momentum.hpp:199
solver::MomentumSolver MomentumSolver
The BiCGStab solver never touched the octree: its Bits parameter was vestigial and is gone in core.
Definition momentum.hpp:45
void restrictField(View< const Index > childStart, View< const Index > childIdx, View< const double > fine, View< double > coarse, Index nCoarse)
Restrict: coarse(p) = mean over p's children (CSR fixed order ⇒ deterministic).
Definition multigrid.hpp:46
void prolongAdd(View< const Index > c2p, View< const double > coarse, View< double > fine, Index nFine)
Prolong (piecewise-constant) + correct: fine(i) += coarse(c2p(i)).
Definition multigrid.hpp:82
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