include/pops/numerics/elliptic/poisson/poisson_fft_solver.hpp Source FileΒΆ

adc_cpp: include/pops/numerics/elliptic/poisson/poisson_fft_solver.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
poisson_fft_solver.hpp
Go to the documentation of this file.
1#pragma once
2
26
38
39#include <cassert>
40#include <cstddef>
41#include <functional>
42#include <stdexcept>
43#include <vector>
44
45#ifdef POPS_HAS_MPI
46#include <mpi.h>
47#endif
48
49namespace pops {
50
52 public:
55 PoissonFFTSolver(const Geometry& geom, const BoxArray& ba, const BCRec& = BCRec{},
56 std::function<bool(Real, Real)> = {}, bool spectral = false)
57 : geom_(geom),
58 dm_(ba.size(), n_ranks()),
59 phi_(ba, dm_, 1, 1),
60 rhs_(ba, dm_, 1, 0),
61 res_(ba, dm_, 1, 0),
62 fft_(geom.domain.nx(), geom.domain.ny(), geom.xhi - geom.xlo, geom.yhi - geom.ylo,
63 spectral) {
64 // HARD guard (active in Release, NDEBUG does NOT remove it): this direct solver is single-rank /
65 // single box. Under a system DistributionMapping with n_ranks()>1, some ranks have no local
66 // box (local_size()==0) and solve() would dereference a non-existent fab(0) -> SIGSEGV. The old
67 // assert vanished in Release and the protection silently disappeared. We throw on ALL ranks
68 // (each one constructs the object), so no deadlock. For the distributed periodic case:
69 // DistributedFFTSolver (slabs, MPI_Alltoall).
70 if (n_ranks() != 1)
71 throw std::runtime_error(
72 "fft solver unsupported in MPI (n_ranks>1): use geometric_mg or the distributed fft "
73 "solver (DistributedFFTSolver)");
74 if (ba.size() != 1)
75 throw std::runtime_error(
76 "PoissonFFTSolver: single box required (ba.size()==1); for a distributed multi-box "
77 "domain, use DistributedFFTSolver or geometric_mg");
78 }
79
80 MultiFab& rhs() { return rhs_; }
81 MultiFab& phi() { return phi_; }
82 const Geometry& geom() const { return geom_; }
83
84 // Solve lap(phi) = rhs in place (direct, one forward FFT + inverse).
85 void solve() {
86 const int Nx = geom_.domain.nx(), Ny = geom_.domain.ny();
87 const ConstArray4 f = rhs_.fab(0).const_array();
88 const Box2D v = rhs_.box(0);
89 std::vector<double> rho(static_cast<std::size_t>(Nx) * Ny), phil;
90 for (int j = 0; j < Ny; ++j)
91 for (int i = 0; i < Nx; ++i)
92 rho[static_cast<std::size_t>(j) * Nx + i] = f(v.lo[0] + i, v.lo[1] + j);
93 fft_.solve(rho, phil); // mode k=0 (constant) set to zero -> phi with zero mean
94 Array4 p = phi_.fab(0).array();
95 for (int j = 0; j < Ny; ++j)
96 for (int i = 0; i < Nx; ++i)
97 p(v.lo[0] + i, v.lo[1] + j) = phil[static_cast<std::size_t>(j) * Nx + i];
98 // phi GHOSTS: the aux derivation (centered grad phi, solve_fields) reads the i+-1/j+-1
99 // neighbors, hence the domain-boundary ghosts. GeometricMG fills them while smoothing; this
100 // direct solver writes ONLY the valid cells -> without this exchange, the boundary gradient
101 // would read stale ghosts (latent bug exposed by an electric source wired onto 'fft':
102 // wrong Ex on the boundary ring). Pure periodic by construction (ctor guards).
103 fill_boundary(phi_, geom_.domain, Periodicity{true, true});
104 }
105
106 // Discrete residual ||lap(phi) - rhs|| (~ round-off for this direct solver).
108 BCRec bc; // periodic
109 poisson_residual(phi_, rhs_, geom_, bc, res_);
110 return norm_inf(res_);
111 }
112
113 private:
114 Geometry geom_;
116 MultiFab phi_, rhs_, res_;
117 PoissonFFT fft_;
118};
119
120static_assert(EllipticSolver<PoissonFFTSolver>, "PoissonFFTSolver must model EllipticSolver");
121
144 public:
146 std::function<bool(Real, Real)> = {})
147 : geom_(geom),
148 Nx_(geom.domain.nx()),
149 nyl_(geom.domain.ny() / n_ranks()),
150 fft_(geom.domain.nx(), geom.domain.ny(), geom.xhi - geom.xlo, geom.yhi - geom.ylo) {
151 const int np = n_ranks();
152 // HARD guard (active in Release): nyl_ = Ny / np is already computed in the init list.
153 // If Ny is not divisible by np, the slabs would be mis-sized and solve() would read out
154 // of bounds. The old assert vanished under NDEBUG. Aligned with PoissonFFTSolver (throw).
155 if (geom.domain.ny() % np != 0)
156 throw std::runtime_error("DistributedFFTSolver: Ny must be divisible by n_ranks()");
157 std::vector<Box2D> slabs;
158 for (int r = 0; r < np; ++r)
159 slabs.push_back(Box2D{{0, r * nyl_}, {Nx_ - 1, (r + 1) * nyl_ - 1}});
160 BoxArray ba(std::move(slabs));
161 dm_ = DistributionMapping(np, np); // slab r -> rank r
162 phi_ = MultiFab(ba, dm_, 1, 1);
163 rhs_ = MultiFab(ba, dm_, 1, 0);
164 res_ = MultiFab(ba, dm_, 1, 0);
165 }
166
167 MultiFab& rhs() { return rhs_; }
168 MultiFab& phi() { return phi_; }
169 const Geometry& geom() const { return geom_; }
170
171 // Solve lap(phi) = rhs in place: local slab -> flat array -> PoissonFFT (internal MPI transpose)
172 // -> rewrite the local slab. Mode k=0 set to zero (phi with zero mean).
173 void solve() {
174 const ConstArray4 f = rhs_.fab(0).const_array();
175 const Box2D v = rhs_.box(0); // local slab [0..Nx-1] x [y0..y0+nyl-1]
176 std::vector<double> rho(static_cast<std::size_t>(nyl_) * Nx_), phil;
177 for (int jl = 0; jl < nyl_; ++jl)
178 for (int i = 0; i < Nx_; ++i)
179 rho[static_cast<std::size_t>(jl) * Nx_ + i] = f(v.lo[0] + i, v.lo[1] + jl);
180 fft_.solve(rho, phil);
181 Array4 p = phi_.fab(0).array();
182 for (int jl = 0; jl < nyl_; ++jl)
183 for (int i = 0; i < Nx_; ++i)
184 p(v.lo[0] + i, v.lo[1] + jl) = phil[static_cast<std::size_t>(jl) * Nx_ + i];
185 }
186
187 // Discrete residual ||lap(phi) - rhs|| reduced over all ranks (~round-off: direct solve exact).
189 BCRec bc; // periodic
190 fill_boundary(phi_, geom_.domain, Periodicity{true, true}); // inter-slab halos (MPI)
191 poisson_residual(phi_, rhs_, geom_, bc, res_);
192 return all_reduce_max(norm_inf(res_));
193 }
194
195 private:
196 Geometry geom_;
197 int Nx_, nyl_;
199 MultiFab phi_, rhs_, res_;
200 PoissonFFT fft_;
201};
202
203static_assert(EllipticSolver<DistributedFFTSolver>,
204 "DistributedFFTSolver must model EllipticSolver");
205
242 public:
246 RemappedFFTSolver(const Geometry& geom, const BoxArray& ba, const BCRec& = BCRec{},
247 std::function<bool(Real, Real)> = {}, bool spectral = false)
248 : geom_(geom),
249 dm_(ba.size(), n_ranks()), // SAME layout System uses: box i -> rank i % np
250 phi_(ba, dm_, 1, 1),
251 rhs_(ba, dm_, 1, 0),
252 res_(ba, dm_, 1, 0),
253 Nx_(geom.domain.nx()),
254 Ny_(geom.domain.ny()),
255 nyl_(geom.domain.ny() / n_ranks()),
256 owner_rank_(ba.size() > 0 ? dm_[0] : 0),
257 fft_(geom.domain.nx(), geom.domain.ny(), geom.xhi - geom.xlo, geom.yhi - geom.ylo,
258 spectral) {
259 // CONTRACT (ADC-273 vote on ADC-287): this solver is valid ONLY for System's round-robin
260 // single-box Cartesian layout, where box 0 is owned by rank dm_[0] (= owner_rank_) and every other
261 // rank holds an empty fab. It presents that layout outward (rhs()/phi() on ba/dm) and hides the
262 // box<->slab transpose inside solve(). For a genuinely slab-distributed domain use
263 // DistributedFFTSolver; for wall/non-constant-coefficient cases use geometric_mg.
264 // COLLECTIVE hard guards (throw on ALL ranks -> no deadlock; ensure_elliptic is collective and
265 // every rank constructs this object). Ny must split into np equal slabs for the internal transpose.
266 if (ba.size() != 1) {
267 throw std::runtime_error(
268 "RemappedFFTSolver: System single-box layout required (ba.size()==1); for a "
269 "slab-distributed "
270 "domain use DistributedFFTSolver or geometric_mg");
271 }
272 if (geom.domain.ny() % n_ranks() != 0) {
273 throw std::runtime_error(
274 "RemappedFFTSolver: Ny must be divisible by n_ranks() for the slab FFT");
275 }
276 }
277
278 MultiFab& rhs() { return rhs_; }
279 MultiFab& phi() { return phi_; }
280 const Geometry& geom() const { return geom_; }
281
282 // Solve lap(phi) = rhs in place. THREE MPI collectives, in order: (1) MPI_Scatter the owner's box-major
283 // rhs into per-rank nyl_ x Nx_ slabs; (2) PoissonFFT::solve runs its internal MPI_Alltoall (y<->x
284 // transpose) + FFT on each slab; (3) MPI_Gather the phi slabs back onto the owner. Then the owner fills
285 // periodic ghosts on the System layout (intra-box wrap, no messages -> deadlock-free). The caller's
286 // device_fence() after ell_solve() (system_field_solver.hpp) covers the GPU read of the centered grad
287 // phi, identical to the PoissonFFTSolver path. Mode k=0 set to zero (phi with zero mean).
288 void solve() {
289 // box-major rhs lives ENTIRELY on owner_rank_; each rank ends up with one nyl_ x Nx_ slab.
290 std::vector<double> rho_local(static_cast<std::size_t>(nyl_) * Nx_), phi_local;
291#ifdef POPS_HAS_MPI
292 std::vector<double> sendbuf; // only filled on the owner
293 if (my_rank() == owner_rank_) {
294 const ConstArray4 f = rhs_.fab(0).const_array();
295 const Box2D v = rhs_.box(0); // valid box = whole domain [0..Nx-1] x [0..Ny-1]
296 sendbuf.resize(static_cast<std::size_t>(Nx_) * Ny_);
297 // pack per destination rank r: rows [r*nyl_, (r+1)*nyl_) contiguous, row-major full x.
298 for (int r = 0; r < n_ranks(); ++r)
299 for (int jl = 0; jl < nyl_; ++jl)
300 for (int i = 0; i < Nx_; ++i)
301 sendbuf[(static_cast<std::size_t>(r) * nyl_ + jl) * Nx_ + i] =
302 f(v.lo[0] + i, v.lo[1] + r * nyl_ + jl);
303 }
304 const int cnt = nyl_ * Nx_;
305 MPI_Scatter(my_rank() == owner_rank_ ? sendbuf.data() : nullptr, cnt, MPI_DOUBLE,
306 rho_local.data(), cnt, MPI_DOUBLE, owner_rank_, MPI_COMM_WORLD);
307#else
308 {
309 const ConstArray4 f = rhs_.fab(0).const_array();
310 const Box2D v = rhs_.box(0);
311 for (int j = 0; j < Ny_; ++j)
312 for (int i = 0; i < Nx_; ++i)
313 rho_local[static_cast<std::size_t>(j) * Nx_ + i] = f(v.lo[0] + i, v.lo[1] + j);
314 }
315#endif
316 fft_.solve(rho_local, phi_local); // PROVEN slab FFT (internal MPI_Alltoall), zero-mean phi
317#ifdef POPS_HAS_MPI
318 std::vector<double> recvbuf;
319 if (my_rank() == owner_rank_)
320 recvbuf.resize(static_cast<std::size_t>(Nx_) * Ny_);
321 MPI_Gather(phi_local.data(), cnt, MPI_DOUBLE,
322 my_rank() == owner_rank_ ? recvbuf.data() : nullptr, cnt, MPI_DOUBLE, owner_rank_,
323 MPI_COMM_WORLD);
324 if (my_rank() == owner_rank_) {
325 Array4 p = phi_.fab(0).array();
326 const Box2D v = phi_.box(0);
327 for (int r = 0; r < n_ranks(); ++r)
328 for (int jl = 0; jl < nyl_; ++jl)
329 for (int i = 0; i < Nx_; ++i)
330 p(v.lo[0] + i, v.lo[1] + r * nyl_ + jl) =
331 recvbuf[(static_cast<std::size_t>(r) * nyl_ + jl) * Nx_ + i];
332 }
333#else
334 {
335 Array4 p = phi_.fab(0).array();
336 const Box2D v = phi_.box(0);
337 for (int j = 0; j < Ny_; ++j)
338 for (int i = 0; i < Nx_; ++i)
339 p(v.lo[0] + i, v.lo[1] + j) = phi_local[static_cast<std::size_t>(j) * Nx_ + i];
340 }
341#endif
342 // Periodic ghosts on the System layout: only the owner has a fab -> pure intra-box wrap (self-copy),
343 // no inter-rank messages, deadlock-free. SAME role as in PoissonFFTSolver::solve() (the centered grad
344 // phi of the aux derivation reads the i+-1 / j+-1 ghosts).
345 fill_boundary(phi_, geom_.domain, Periodicity{true, true});
346 // PR #254 managed-buffer / device-ordering discipline: owner-only device_fence so the host gather +
347 // ghost wrap are settled before phi() is read on the device. The caller's device_fence() after
348 // ell_solve() (system_field_solver.hpp) already brackets the grad-phi read, so this is belt-and-
349 // suspenders, but it self-documents the ordering at the solver seam. Gated on the owner because only
350 // it holds a fab (no-op on the empty ranks); no-op on the CPU/Serial backend.
351 if (my_rank() == owner_rank_)
352 device_fence();
353 }
354
355 // Discrete residual ||lap(phi) - rhs|| reduced over all ranks (~round-off: direct solve exact). The
356 // box is owner-only, so poisson_residual runs on the owner's fab and is 0 elsewhere; the all_reduce_max
357 // is COLLECTIVE (every rank participates).
359 BCRec bc; // periodic
360 poisson_residual(phi_, rhs_, geom_, bc, res_);
361 return all_reduce_max(norm_inf(res_));
362 }
363
364 private:
365 Geometry geom_;
367 MultiFab phi_, rhs_, res_;
368 int Nx_, Ny_, nyl_, owner_rank_;
369 PoissonFFT fft_;
370};
371
372static_assert(EllipticSolver<RemappedFFTSolver>, "RemappedFFTSolver must model EllipticSolver");
373
374} // namespace pops
BoxArray: the set of boxes tiling a level (disjoint, covering).
Ordered list of boxes tiling a level.
Definition box_array.hpp:22
int size() const
Number of boxes in the tiling.
Definition box_array.hpp:42
DIRECT periodic Poisson solver (spectral FFT) DISTRIBUTED, models EllipticSolver.
Definition poisson_fft_solver.hpp:143
MultiFab & phi()
Definition poisson_fft_solver.hpp:168
void solve()
Definition poisson_fft_solver.hpp:173
DistributedFFTSolver(const Geometry &geom, const BCRec &=BCRec{}, std::function< bool(Real, Real)>={})
Definition poisson_fft_solver.hpp:145
Real residual()
Definition poisson_fft_solver.hpp:188
MultiFab & rhs()
Definition poisson_fft_solver.hpp:167
const Geometry & geom() const
Definition poisson_fft_solver.hpp:169
Owning MPI rank of each box, indexed by GLOBAL box index (parallel to a BoxArray).
Definition distribution_mapping.hpp:19
ConstArray4 const_array() const
READ handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:96
Array4 array()
WRITE handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:91
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
Fab2D & fab(int li)
Local fab at index li (0 <= li < local_size()), for writing.
Definition multifab.hpp:67
const Box2D & box(int li) const
VALID box of local fab li.
Definition multifab.hpp:71
Definition poisson_fft_solver.hpp:51
Real residual()
Definition poisson_fft_solver.hpp:107
const Geometry & geom() const
Definition poisson_fft_solver.hpp:82
MultiFab & phi()
Definition poisson_fft_solver.hpp:81
PoissonFFTSolver(const Geometry &geom, const BoxArray &ba, const BCRec &=BCRec{}, std::function< bool(Real, Real)>={}, bool spectral=false)
spectral: Laplacian symbol (false = discrete 5-point stencil, bit-identical default; true = continuou...
Definition poisson_fft_solver.hpp:55
MultiFab & rhs()
Definition poisson_fft_solver.hpp:80
void solve()
Definition poisson_fft_solver.hpp:85
Definition poisson_fft.hpp:101
void solve(const std::vector< double > &rho_local, std::vector< double > &phi_local)
Definition poisson_fft.hpp:126
DIRECT periodic Poisson solver (spectral FFT) under MPI, presenting the SYSTEM LAYOUT,...
Definition poisson_fft_solver.hpp:241
void solve()
Definition poisson_fft_solver.hpp:288
RemappedFFTSolver(const Geometry &geom, const BoxArray &ba, const BCRec &=BCRec{}, std::function< bool(Real, Real)>={}, bool spectral=false)
spectral: Laplacian symbol forwarded to PoissonFFT (false = discrete 5-point stencil,...
Definition poisson_fft_solver.hpp:246
MultiFab & phi()
Definition poisson_fft_solver.hpp:279
Real residual()
Definition poisson_fft_solver.hpp:358
MultiFab & rhs()
Definition poisson_fft_solver.hpp:278
const Geometry & geom() const
Definition poisson_fft_solver.hpp:280
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
DistributionMapping: maps each box (by global index) to its owning MPI rank.
EllipticSolver concept: common contract for elliptic solvers at the MultiFab level (solve D phi = f),...
fill_boundary: INTRA-level halo exchange (fills ghosts from neighbors).
Geometry: index-space (Box2D) <-> Cartesian physical-space mapping; PolarGeometry: SIBLING for a glob...
MultiFab arithmetic (saxpy, lincomb, norm_inf, dot) over VALID cells.
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
Definition amr_hierarchy.hpp:29
double Real
Definition types.hpp:30
int n_ranks()
Definition comm.hpp:139
void device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
void fill_boundary(MultiFab &mf, const Box2D &domain, Periodicity per={})
BLOCKING halo exchange: begin then end immediately (no overlap).
Definition fill_boundary.hpp:333
int my_rank()
Definition comm.hpp:136
double all_reduce_max(double x)
Definition comm.hpp:146
void poisson_residual(MultiFab &phi, const MultiFab &f, const Geometry &geom, const BCRec &bc, MultiFab &res, const MultiFab *mask=nullptr, const MultiFab *coef=nullptr, const MultiFab *eps=nullptr, const MultiFab *kappa=nullptr, const MultiFab *eps_y=nullptr, const MultiFab *a_xy=nullptr, const MultiFab *a_yx=nullptr)
Definition poisson_operator.hpp:271
Real norm_inf(const MultiFab &mf, int comp=0)
Infinity norm max |f(.,.,comp)| over the valid cells (LOCAL, without MPI all_reduce).
Definition mf_arith.hpp:123
PHYSICAL boundary conditions at the domain edge (BCType, BCRec, fill_physical_bc, fill_ghosts).
Low-level FFT brick: PoissonFFT (spectral periodic Poisson solver, slab-distributed) plus 1D radix-2 ...
Free functions of the elliptic operator: apply_laplacian (matvec), poisson_residual (residual),...
WRITE POD handle (raw pointer + strides) over a Fab2D buffer, indexed by (i, j, c) IN GLOBAL INDICES ...
Definition fab2d.hpp:29
Boundary conditions for the FOUR faces of the domain (type + associated Dirichlet value).
Definition physical_bc.hpp:29
2D integer index space, cell-centered.
Definition box2d.hpp:37
POPS_HD int nx() const
Width (direction 0). POPS_HD (called from Geometry::dx() in a device kernel).
Definition box2d.hpp:50
POPS_HD int ny() const
Height (direction 1). POPS_HD (called from Geometry::dy() in a device kernel).
Definition box2d.hpp:52
int lo[2]
Definition box2d.hpp:38
READ-only handle (const counterpart of Array4): same layout and same contract (POD device-copyable,...
Definition fab2d.hpp:44
Cartesian geometry of a level: index domain + physical bounds [xlo, xhi] x [ylo, yhi].
Definition geometry.hpp:20
Box2D domain
Definition geometry.hpp:21
Per-direction periodicity: halo wrapping in x and/or y during the exchange (false = open edge,...
Definition fill_boundary.hpp:37