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
distributed_poisson.hpp
Go to the documentation of this file.
1// core — distributed Poisson operator + smoother on a DistributedOctree.
2//
3// The distributed compute path for the AMR octree: the operator (Laplacian matvec
4// and a weighted-Jacobi smoother) runs per rank on the local block, with the
5// neighbour values across block faces supplied by DistributedOctree's owner-based
6// ghost exchange (faceNeighborGather) — the same NBX halo used for cross-block
7// balance. Jacobi uses only the previous iterate, so a distributed sweep is
8// bit-identical to the serial one (the halo carries exactly the cells a serial
9// single-block solve would read). This is the analog of flow's MPI-folded
10// CutcellMG, at the operator/smoother level.
11//
12// Scope: the plain (uniform, openness-free) Laplacian L = ∇² in grid spacing h0,
13// on a periodic global domain — the clean first distributed milestone. Graded /
14// openness / a full distributed V-cycle build on this + the per-level halos.
15//
16// Sign convention (suite-wide): the operator IS the Laplacian L = ∇² (negative-
17// definite; diagonal −2*Dim/h², off +1/h²), matching AmrPoisson::applyLaplacian,
18// DistributedFvOperator and the device operators. Solvers solve L u = rhs.
19//
20#ifndef PECLET_AMR_DISTRIBUTED_POISSON_HPP
21#define PECLET_AMR_DISTRIBUTED_POISSON_HPP
22
23#include <array>
24#include <cassert>
25#include <cmath>
26#include <memory>
27#include <vector>
28
29#include "peclet/amr/common.hpp"
32#include "peclet/core/common/mpi.hpp"
33#include "peclet/core/common/types.hpp"
34
35namespace peclet::amr {
36
37template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
39 public:
40 void init(DistributedOctree<Dim, Bits>& d, double h0) { init(d, detail::filledVec<Dim>(h0)); }
42 d_ = &d;
43 h0_ = h0;
44 // Precompute the face-neighbour gather topology once (C1): apply()/jacobi()/residual() run many
45 // matvecs over a fixed decomposition, so the per-face neighborInfo classification need not be
46 // redone every time. Rebuilt here if init() is called again (e.g. after a rebalance).
47 plan_ = d.buildFaceGatherPlan();
48 }
49
50 Index numLeaves() const { return d_->local().numLeaves(); }
51
55 void apply(const std::vector<double>& x, std::vector<double>& y) const {
56 auto g = d_->faceNeighborGather(plan_, x);
57 const Index n = numLeaves();
58 const int F = 2 * Dim;
59 // Phase 3 (`docs/amr_anisotropic.md` §3): one weight per AXIS. The gather layout is
60 // `2*axis + (dir>0 ? 0 : 1)`, so the axis of face `f` is `f/2`. On a cubic grid every
61 // `inv[d]` is the same double and the sum is the old `inv * s` term for term.
62 double inv[Dim];
63 for (int d = 0; d < Dim; ++d)
64 inv[d] = 1.0 / (h0_[d] * h0_[d]);
65 y.assign(static_cast<std::size_t>(n), 0.0);
66 for (Index i = 0; i < n; ++i) {
67 double s = 0.0;
68 for (int f = 0; f < F; ++f)
69 s += inv[f / 2] * (g[static_cast<std::size_t>(i) * F + f] - x[static_cast<std::size_t>(i)]);
70 y[static_cast<std::size_t>(i)] = s; // L = ∇², axis-weighted
71 }
72 }
73
78 void jacobi(std::vector<double>& x, const std::vector<double>& b, int sweeps,
79 double omega = 0.8) const {
80 const Index n = numLeaves();
81 const int F = 2 * Dim;
82 // Phase 3: the Jacobi diagonal is the SUM of the per-axis weights (2 faces each), formed as
83 // one parenthesised sum so a cubic grid reproduces the single `F * inv` rounding.
84 double diag = 0.0;
85 for (int d = 0; d < Dim; ++d)
86 diag += 2.0 / (h0_[d] * h0_[d]);
87 std::vector<double> lx;
88 for (int s = 0; s < sweeps; ++s) {
89 apply(x, lx); // lx = L x; gathers ghosts
90 for (Index i = 0; i < n; ++i)
91 x[static_cast<std::size_t>(i)] +=
92 omega * (lx[static_cast<std::size_t>(i)] - b[static_cast<std::size_t>(i)]) / diag;
93 }
94 }
95
97 double residual(const std::vector<double>& x, const std::vector<double>& b,
98 std::vector<double>& res) const {
99 std::vector<double> ax;
100 apply(x, ax);
101 const Index n = numLeaves();
102 res.assign(static_cast<std::size_t>(n), 0.0);
103 double s = 0.0;
104 for (Index i = 0; i < n; ++i) {
105 double r = b[static_cast<std::size_t>(i)] - ax[static_cast<std::size_t>(i)];
106 res[static_cast<std::size_t>(i)] = r;
107 s += r * r;
108 }
109 double g = 0.0;
110 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
111 return std::sqrt(g);
112 }
113
115 double residualNorm(const std::vector<double>& x, const std::vector<double>& b) const {
116 std::vector<double> ax;
117 apply(x, ax);
118 double s = 0.0;
119 for (Index i = 0; i < numLeaves(); ++i) {
120 double r = b[static_cast<std::size_t>(i)] - ax[static_cast<std::size_t>(i)];
121 s += r * r;
122 }
123 double g = 0.0;
124 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
125 return std::sqrt(g);
126 }
127
128 private:
129 DistributedOctree<Dim, Bits>* d_ = nullptr;
130 Vec<Dim> h0_ = detail::filledVec<Dim>(1.0);
131 typename DistributedOctree<Dim, Bits>::FaceGatherPlan plan_; // cached gather topology (C1)
132};
133
149template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
151 public:
155 void build(const IVec<Dim>& g0, const AmrGeometry<Dim>& geo,
156 const std::array<bool, Dim>& periodic, MPI_Comm comm) {
157 levels_.clear();
158 int size = 1;
159 MPI_Comm_size(comm, &size);
160 IVec<Dim> g = g0;
161 // Phase 3: every axis is coarsened by 2 together, so the aspect ratio is carried down the
162 // hierarchy unchanged (`docs/amr_anisotropic.md` §4, decision AM1).
163 Vec<Dim> h = geo.h0;
164 for (;;) {
165 auto lvl = std::make_unique<Level>();
167 lg.h0 = h;
168 lvl->d.init(g, /*lmax=*/0, lg, periodic, comm);
169 lvl->op.init(lvl->d, h);
170 levels_.push_back(std::move(lvl));
171 // Can we coarsen one more level?
172 bool ok = true;
173 long prod = 1;
174 IVec<Dim> ng{};
175 for (int d = 0; d < Dim; ++d) {
176 if (g[d] % 2 != 0 || g[d] / 2 < 2)
177 ok = false;
178 ng[d] = g[d] / 2;
179 prod *= ng[d];
180 }
181 if (!ok || prod < size)
182 break;
183 g = ng;
184 for (int d = 0; d < Dim; ++d) // every axis coarsens by 2 (AM1): the ratio is carried down
185 h[d] *= 2.0;
186 }
187 // Nested fine→coarse maps (local; asserts nesting holds).
188 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
189 auto& fine = levels_[L]->d;
190 auto& coarse = levels_[L + 1]->d;
191 const Index nf = fine.local().numLeaves();
192 auto& c2p = levels_[L]->c2p;
193 c2p.assign(static_cast<std::size_t>(nf), -1);
194 for (Index i = 0; i < nf; ++i) {
195 IVec<Dim> gf = fine.globalRootOf(i), gc{};
196 for (int d = 0; d < Dim; ++d)
197 gc[d] = gf[d] / 2;
198 Index p = coarse.findGlobalRoot(gc);
199 assert(p >= 0 && "ORB decompositions must nest (power-of-two grid+ranks)");
200 c2p[static_cast<std::size_t>(i)] = p;
201 }
202 }
203 }
204
205 std::size_t numLevels() const { return levels_.size(); }
206 DistributedOctree<Dim, Bits>& octree(std::size_t L = 0) { return levels_[L]->d; }
207 DistributedPoisson<Dim, Bits>& op(std::size_t L = 0) { return levels_[L]->op; }
208 Index numLeaves(std::size_t L = 0) const { return levels_[L]->d.local().numLeaves(); }
209
211 void vcycle(std::vector<double>& x, const std::vector<double>& b, int pre = 2, int post = 2,
212 int bottom = 30, std::size_t L = 0) {
213 auto& op = levels_[L]->op;
214 if (L + 1 == levels_.size()) { // coarsest: a few smooths
215 op.jacobi(x, b, bottom);
216 return;
217 }
218 op.jacobi(x, b, pre);
219 std::vector<double> res;
220 op.residual(x, b, res);
221 // Restrict residual → coarse rhs (local volume-average over children).
222 const Index nc = levels_[L + 1]->d.local().numLeaves();
223 std::vector<double> cb(static_cast<std::size_t>(nc), 0.0);
224 std::vector<double> cn(static_cast<std::size_t>(nc), 0.0);
225 const auto& c2p = levels_[L]->c2p;
226 const Index nf = levels_[L]->d.local().numLeaves();
227 for (Index i = 0; i < nf; ++i) {
228 Index p = c2p[static_cast<std::size_t>(i)];
229 cb[static_cast<std::size_t>(p)] += res[static_cast<std::size_t>(i)];
230 cn[static_cast<std::size_t>(p)] += 1.0;
231 }
232 for (Index p = 0; p < nc; ++p)
233 if (cn[static_cast<std::size_t>(p)] > 0.0)
234 cb[static_cast<std::size_t>(p)] /= cn[static_cast<std::size_t>(p)];
235 // Coarse solve.
236 std::vector<double> cx(static_cast<std::size_t>(nc), 0.0);
237 vcycle(cx, cb, pre, post, bottom, L + 1);
238 // Prolong (piecewise-constant) + correct.
239 for (Index i = 0; i < nf; ++i)
240 x[static_cast<std::size_t>(i)] +=
241 cx[static_cast<std::size_t>(c2p[static_cast<std::size_t>(i)])];
242 op.jacobi(x, b, post);
243 }
244
245 private:
246 struct Level {
249 std::vector<Index> c2p; // fine leaf → coarse leaf (local)
250 };
251 std::vector<std::unique_ptr<Level>> levels_;
252};
253
254} // namespace peclet::amr
255
256#endif // PECLET_AMR_DISTRIBUTED_POISSON_HPP
Distributed geometric-multigrid V-cycle for the plain Laplacian on a uniform (lmax==0) octree,...
void build(const IVec< Dim > &g0, const AmrGeometry< Dim > &geo, const std::array< bool, Dim > &periodic, MPI_Comm comm)
Build the hierarchy from the finest global root grid g0 (lmax==0) on comm.
void vcycle(std::vector< double > &x, const std::vector< double > &b, int pre=2, int post=2, int bottom=30, std::size_t L=0)
One V-cycle of A x = b on level L (default the finest), correction scheme.
DistributedOctree< Dim, Bits > & octree(std::size_t L=0)
DistributedPoisson< Dim, Bits > & op(std::size_t L=0)
Index numLeaves(std::size_t L=0) const
std::vector< double > faceNeighborGather(const std::vector< double > &field, double sentinel=kNoNeighbor) const
For each local leaf and each of the 2*Dim faces, the neighbouring leaf's field value.
FaceGatherPlan buildFaceGatherPlan() const
void init(DistributedOctree< Dim, Bits > &d, double h0)
double residualNorm(const std::vector< double > &x, const std::vector< double > &b) const
Global L2 norm of b − L x (across ranks).
void jacobi(std::vector< double > &x, const std::vector< double > &b, int sweeps, double omega=0.8) const
sweeps damped-Jacobi relaxations of L u = b (in place), L = ∇².
double residual(const std::vector< double > &x, const std::vector< double > &b, std::vector< double > &res) const
res = b − L x (local vector); also returns its global L2 norm.
void apply(const std::vector< double > &x, std::vector< double > &y) const
y = L x with L = ∇² (negative-definite: diagonal −2*Dim/h², off +1/h²).
void init(DistributedOctree< Dim, Bits > &d, const Vec< Dim > &h0)
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
World-space placement of a block-local octree: fine coordinate (0,..,0) sits at origin,...
Static topology of the face-neighbour gather: which out-slots are filled from a local leaf (directSlo...