core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
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// Header-only, guarded by PECLET_CORE_HAVE_MORTON; uses the MPI shim.
21#ifndef PECLET_CORE_AMR_DISTRIBUTED_POISSON_HPP
22#define PECLET_CORE_AMR_DISTRIBUTED_POISSON_HPP
23
24#ifdef PECLET_CORE_HAVE_MORTON
25
26#include <array>
27#include <cassert>
28#include <cmath>
29#include <memory>
30#include <vector>
31
36
37namespace peclet::core::amr {
38
39template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
41 public:
42 void init(DistributedOctree<Dim, Bits>& d, double h0) {
43 d_ = &d;
44 h0_ = h0;
45 // Precompute the face-neighbour gather topology once (C1): apply()/jacobi()/residual() run many
46 // matvecs over a fixed decomposition, so the per-face neighborInfo classification need not be
47 // redone every time. Rebuilt here if init() is called again (e.g. after a rebalance).
48 plan_ = d.buildFaceGatherPlan();
49 }
50
51 Index numLeaves() const { return d_->local().numLeaves(); }
52
56 void apply(const std::vector<double>& x, std::vector<double>& y) const {
57 auto g = d_->faceNeighborGather(plan_, x);
58 const Index n = numLeaves();
59 const int F = 2 * Dim;
60 const double inv = 1.0 / (h0_ * h0_);
61 y.assign(static_cast<std::size_t>(n), 0.0);
62 for (Index i = 0; i < n; ++i) {
63 double s = 0.0;
64 for (int f = 0; f < F; ++f)
65 s += g[static_cast<std::size_t>(i) * F + f] - x[static_cast<std::size_t>(i)];
66 y[static_cast<std::size_t>(i)] = inv * s; // L = ∇²
67 }
68 }
69
74 void jacobi(std::vector<double>& x, const std::vector<double>& b, int sweeps,
75 double omega = 0.8) const {
76 const Index n = numLeaves();
77 const int F = 2 * Dim;
78 const double inv = 1.0 / (h0_ * h0_), diag = F * inv;
79 std::vector<double> lx;
80 for (int s = 0; s < sweeps; ++s) {
81 apply(x, lx); // lx = L x; gathers ghosts
82 for (Index i = 0; i < n; ++i)
83 x[static_cast<std::size_t>(i)] +=
84 omega * (lx[static_cast<std::size_t>(i)] - b[static_cast<std::size_t>(i)]) / diag;
85 }
86 }
87
89 double residual(const std::vector<double>& x, const std::vector<double>& b,
90 std::vector<double>& res) const {
91 std::vector<double> ax;
92 apply(x, ax);
93 const Index n = numLeaves();
94 res.assign(static_cast<std::size_t>(n), 0.0);
95 double s = 0.0;
96 for (Index i = 0; i < n; ++i) {
97 double r = b[static_cast<std::size_t>(i)] - ax[static_cast<std::size_t>(i)];
98 res[static_cast<std::size_t>(i)] = r;
99 s += r * r;
100 }
101 double g = 0.0;
102 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
103 return std::sqrt(g);
104 }
105
107 double residualNorm(const std::vector<double>& x, const std::vector<double>& b) const {
108 std::vector<double> ax;
109 apply(x, ax);
110 double s = 0.0;
111 for (Index i = 0; i < numLeaves(); ++i) {
112 double r = b[static_cast<std::size_t>(i)] - ax[static_cast<std::size_t>(i)];
113 s += r * r;
114 }
115 double g = 0.0;
116 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
117 return std::sqrt(g);
118 }
119
120 private:
121 DistributedOctree<Dim, Bits>* d_ = nullptr;
122 double h0_ = 1.0;
123 typename DistributedOctree<Dim, Bits>::FaceGatherPlan plan_; // cached gather topology (C1)
124};
125
141template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
143 public:
147 void build(const IVec<Dim>& g0, const AmrGeometry<Dim>& geo,
148 const std::array<bool, Dim>& periodic, MPI_Comm comm) {
149 levels_.clear();
150 int size = 1;
151 MPI_Comm_size(comm, &size);
152 IVec<Dim> g = g0;
153 double h = geo.h0;
154 for (;;) {
155 auto lvl = std::make_unique<Level>();
157 lg.h0 = h;
158 lvl->d.init(g, /*lmax=*/0, lg, periodic, comm);
159 lvl->op.init(lvl->d, h);
160 levels_.push_back(std::move(lvl));
161 // Can we coarsen one more level?
162 bool ok = true;
163 long prod = 1;
164 IVec<Dim> ng{};
165 for (int d = 0; d < Dim; ++d) {
166 if (g[d] % 2 != 0 || g[d] / 2 < 2)
167 ok = false;
168 ng[d] = g[d] / 2;
169 prod *= ng[d];
170 }
171 if (!ok || prod < size)
172 break;
173 g = ng;
174 h *= 2.0;
175 }
176 // Nested fine→coarse maps (local; asserts nesting holds).
177 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
178 auto& fine = levels_[L]->d;
179 auto& coarse = levels_[L + 1]->d;
180 const Index nf = fine.local().numLeaves();
181 auto& c2p = levels_[L]->c2p;
182 c2p.assign(static_cast<std::size_t>(nf), -1);
183 for (Index i = 0; i < nf; ++i) {
184 IVec<Dim> gf = fine.globalRootOf(i), gc{};
185 for (int d = 0; d < Dim; ++d)
186 gc[d] = gf[d] / 2;
187 Index p = coarse.findGlobalRoot(gc);
188 assert(p >= 0 && "ORB decompositions must nest (power-of-two grid+ranks)");
189 c2p[static_cast<std::size_t>(i)] = p;
190 }
191 }
192 }
193
194 std::size_t numLevels() const { return levels_.size(); }
195 DistributedOctree<Dim, Bits>& octree(std::size_t L = 0) { return levels_[L]->d; }
196 DistributedPoisson<Dim, Bits>& op(std::size_t L = 0) { return levels_[L]->op; }
197 Index numLeaves(std::size_t L = 0) const { return levels_[L]->d.local().numLeaves(); }
198
200 void vcycle(std::vector<double>& x, const std::vector<double>& b, int pre = 2, int post = 2,
201 int bottom = 30, std::size_t L = 0) {
202 auto& op = levels_[L]->op;
203 if (L + 1 == levels_.size()) { // coarsest: a few smooths
204 op.jacobi(x, b, bottom);
205 return;
206 }
207 op.jacobi(x, b, pre);
208 std::vector<double> res;
209 op.residual(x, b, res);
210 // Restrict residual → coarse rhs (local volume-average over children).
211 const Index nc = levels_[L + 1]->d.local().numLeaves();
212 std::vector<double> cb(static_cast<std::size_t>(nc), 0.0);
213 std::vector<double> cn(static_cast<std::size_t>(nc), 0.0);
214 const auto& c2p = levels_[L]->c2p;
215 const Index nf = levels_[L]->d.local().numLeaves();
216 for (Index i = 0; i < nf; ++i) {
217 Index p = c2p[static_cast<std::size_t>(i)];
218 cb[static_cast<std::size_t>(p)] += res[static_cast<std::size_t>(i)];
219 cn[static_cast<std::size_t>(p)] += 1.0;
220 }
221 for (Index p = 0; p < nc; ++p)
222 if (cn[static_cast<std::size_t>(p)] > 0.0)
223 cb[static_cast<std::size_t>(p)] /= cn[static_cast<std::size_t>(p)];
224 // Coarse solve.
225 std::vector<double> cx(static_cast<std::size_t>(nc), 0.0);
226 vcycle(cx, cb, pre, post, bottom, L + 1);
227 // Prolong (piecewise-constant) + correct.
228 for (Index i = 0; i < nf; ++i)
229 x[static_cast<std::size_t>(i)] +=
230 cx[static_cast<std::size_t>(c2p[static_cast<std::size_t>(i)])];
231 op.jacobi(x, b, post);
232 }
233
234 private:
235 struct Level {
238 std::vector<Index> c2p; // fine leaf → coarse leaf (local)
239 };
240 std::vector<std::unique_ptr<Level>> levels_;
241};
242
243} // namespace peclet::core::amr
244
245#endif // PECLET_CORE_HAVE_MORTON
246#endif // PECLET_CORE_AMR_DISTRIBUTED_POISSON_HPP
Distributed geometric-multigrid V-cycle for the plain Laplacian on a uniform (lmax==0) octree,...
DistributedOctree< Dim, Bits > & octree(std::size_t L=0)
DistributedPoisson< Dim, Bits > & op(std::size_t L=0)
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.
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.
double residualNorm(const std::vector< double > &x, const std::vector< double > &b) const
Global L2 norm of b − L x (across ranks).
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 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 = ∇².
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, double h0)
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:69
int MPI_Allreduce(const void *sbuf, void *rbuf, int count, MPI_Datatype dt, MPI_Op, MPI_Comm)
Definition mpi_stub.hpp:80
int MPI_Comm
Definition mpi_stub.hpp:16
#define MPI_SUM
Definition mpi_stub.hpp:42
#define MPI_DOUBLE
Definition mpi_stub.hpp:40
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
Static topology of the face-neighbour gather: which out-slots are filled from a local leaf (directSlo...