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_fv.hpp
Go to the documentation of this file.
1// core — consistent graded FV Laplacian on a *distributed* octree.
2//
3// The distributed analog of AmrPoisson / Multigrid's consistent operator:
4// the conservative two-point FV Laplacian
5// (L u)_i = invVol_i · Σ_f w_f (u[nbr_f] − u_i), w_f = A_f / d_f,
6// on a graded DistributedOctree (refined + cross-block 2:1 balanced), with the
7// coarse side of every 2:1 interface carrying one face per fine sub-neighbour —
8// including sub-neighbours that live on another rank.
9//
10// Topology built once: each leaf's face stencil is enumerated exactly like
11// AmrPoisson::forEachFaceNeighbor (same axis/dir/sub-k order), with in-block
12// neighbours resolved locally and cross-block neighbours turned into *ghost*
13// entries. Ghost cells are discovered by one owner gather of the across-face
14// covering level (DistributedOctree::coverLevels) — enough to know whether a
15// remote face is same/coarser (one neighbour) or finer (2^(Dim-1) sub-neighbours,
16// each possibly on a different owner). Each matvec then fills the ghost values with
17// one owner gather (coverValues). Because the per-entry term w(val−u_i) is summed
18// in the same face order whether a neighbour is local or ghost, the operator is
19// bit-identical across rank counts (COMM_WORLD == COMM_SELF) and, on a single
20// block, bit-identical to host AmrPoisson::applyLaplacian.
21//
22// Scope: openness-free (w_f = A_f/d_f); the smoother is weighted Jacobi. Openness
23// (cut-cell) and a graded distributed *multigrid* hierarchy build on this.
24//
25#ifndef PECLET_AMR_DISTRIBUTED_FV_HPP
26#define PECLET_AMR_DISTRIBUTED_FV_HPP
27
28#include <array>
29#include <cmath>
30#include <map>
31#include <memory>
32#include <vector>
33
34#include "peclet/amr/common.hpp"
37#include "peclet/core/common/mpi.hpp"
38#include "peclet/core/common/types.hpp"
39
40namespace peclet::amr {
41
42template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
44 public:
46 using M = typename DO::M;
47 using Code = typename DO::Code;
48 using Coord = typename DO::Coord;
49
51 void init(DO& d) {
52 init(d, [](const Vec<Dim>&, int) { return 1.0; });
53 }
54
61 template <class OpenFn>
62 void init(DO& d, OpenFn&& openFn) {
63 d_ = &d;
64 h0_ = d.h0();
65 const auto& t = d.local();
66 const Index n = t.numLeaves();
67 const IVec<Dim> bfo = d.blockFineOrigin();
68 const Vec<Dim> org = d.globalGeometry().origin;
69 invVol_.assign(static_cast<std::size_t>(n), 0.0);
70 diag_.assign(static_cast<std::size_t>(n), 0.0);
71
72 // α at the finer side's (sub-)face: flo = finer cell global fine lower corner,
73 // s its size (fine units), faceDir its outward normal toward the neighbour.
74 auto alphaOf = [&](const std::array<long, Dim>& flo, Coord s, int axis, int faceDir) -> double {
75 Vec<Dim> fc{};
76 long plane = flo[axis] + (faceDir > 0 ? static_cast<long>(s) : 0L);
77 for (int d2 = 0; d2 < Dim; ++d2)
78 fc[d2] =
79 (d2 == axis)
80 ? org[d2] + static_cast<double>(plane) * h0_[d2]
81 : org[d2] + (static_cast<double>(flo[d2]) + 0.5 * static_cast<double>(s)) * h0_[d2];
82 double a = openFn(fc, axis);
83 return a < 0.0 ? 0.0 : (a > 1.0 ? 1.0 : a);
84 };
85 auto gLo = [&](const std::array<Coord, Dim>& lc) {
86 std::array<long, Dim> g{};
87 for (int d2 = 0; d2 < Dim; ++d2)
88 g[d2] = static_cast<long>(lc[d2]) + static_cast<long>(bfo[d2]);
89 return g;
90 };
91 // Global fine lower corner of the k-th fine sub-neighbour across a coarse cell's
92 // (axis,dir) face: clo is the coarse cell's global lo, si its size, sj=si/2. Used
93 // for the sub-face α centroid — the *finer* cell's lo, not the probe point.
94 auto fineLoGlobal = [&](const std::array<long, Dim>& clo, Coord si, Coord sj, int axis, int dir,
95 int k) {
96 std::array<long, Dim> flo{};
97 flo[axis] = (dir > 0) ? clo[axis] + static_cast<long>(si) : clo[axis] - static_cast<long>(sj);
98 int bit = 0;
99 for (int t = 0; t < Dim; ++t) {
100 if (t == axis)
101 continue;
102 flo[t] = clo[t] + (((k >> bit) & 1) ? static_cast<long>(sj) : 0L);
103 ++bit;
104 }
105 for (int d2 = 0; d2 < Dim; ++d2) {
106 long gf = static_cast<long>(d.globalFineSize()[d2]);
107 if (flo[d2] < 0)
108 flo[d2] += gf;
109 else if (flo[d2] >= gf)
110 flo[d2] -= gf;
111 }
112 return flo;
113 };
114
115 // Pass 1: per-leaf entries in face order; remote faces become markers.
116 struct E {
117 double w = 0.0;
118 Index localRef = -1; // >=0 local neighbour slot
119 int rfId = -1; // >=0 unresolved remote face
120 };
121 std::vector<std::vector<E>> ent(static_cast<std::size_t>(n));
122 struct RFace {
123 std::array<Coord, Dim> gc{};
124 int Li = 0;
125 Coord si = 1;
126 int axis = 0;
127 int dir = 1;
128 std::array<long, Dim> iLo{}; // requesting leaf's global fine lower corner
129 };
130 std::vector<RFace> rfaces;
131
132 for (Index i = 0; i < n; ++i) {
133 const int Li = static_cast<int>(t.level(i));
134 const Coord si = Coord(Coord(1) << Li);
135 invVol_[static_cast<std::size_t>(i)] = 1.0 / cellVol(Li);
136 auto b = t.bounds(i);
137 const auto& lo = b[0];
138 for (int axis = 0; axis < Dim; ++axis)
139 for (int dir = -1; dir <= 1; dir += 2) {
140 typename DO::FaceInfo fi = d.faceAcross(i, axis, dir);
141 if (fi.state == 2)
142 continue; // DomainNone
143 if (fi.state == 0) { // InBlock: enumerate locally
144 Index j = fi.localNb;
145 if (j < 0)
146 continue;
147 const int Lj = static_cast<int>(t.level(j));
148 if (Lj >= Li) { // finer side is i: α at i's face
149 const double a = alphaOf(gLo(lo), si, axis, dir);
150 ent[static_cast<std::size_t>(i)].push_back(
151 E{a * coeff(si, Coord(Coord(1) << Lj), axis), j, -1});
152 } else { // finer side is the sub-neighbour jj: α at jj's face (−dir)
153 const Coord sj = Coord(si >> 1);
154 const long pc = (dir > 0) ? static_cast<long>(lo[axis]) + static_cast<long>(si)
155 : static_cast<long>(lo[axis]) - 1;
156 const int nsub = 1 << (Dim - 1);
157 for (int k = 0; k < nsub; ++k) {
158 std::array<Coord, Dim> q = lo;
159 q[axis] = static_cast<Coord>(pc);
160 int bit = 0;
161 for (int tt = 0; tt < Dim; ++tt) {
162 if (tt == axis)
163 continue;
164 const Coord off = ((k >> bit) & 1) ? sj : Coord(0);
165 q[tt] = static_cast<Coord>(lo[tt] + off);
166 ++bit;
167 }
168 Index jj = t.find(M::encode(q).code());
169 const double a =
170 alphaOf(fineLoGlobal(gLo(lo), si, sj, axis, dir, k), sj, axis, -dir);
171 ent[static_cast<std::size_t>(i)].push_back(E{a * coeff(si, sj, axis), jj, -1});
172 }
173 }
174 } else { // Remote: defer (need covering level)
175 E e;
176 e.rfId = static_cast<int>(rfaces.size());
177 ent[static_cast<std::size_t>(i)].push_back(e);
178 RFace rf{fi.gc, Li, si, axis, dir, {}};
179 rf.iLo = gLo(lo);
180 rfaces.push_back(rf);
181 }
182 }
183 }
184
185 // One owner gather of the across-face covering level for every remote face.
186 std::vector<std::array<Coord, Dim>> coverCoords(rfaces.size());
187 for (std::size_t r = 0; r < rfaces.size(); ++r)
188 coverCoords[r] = rfaces[r].gc;
189 std::vector<int> coverLv = d.coverLevels(coverCoords);
190
191 // Pass 2: expand into the final CSR; ghost coords deduped to ghost slots.
192 std::map<std::array<Coord, Dim>, Index> ghostSlot;
193 std::vector<std::array<Coord, Dim>> ghostCoords;
194 auto ghostRef = [&](const std::array<Coord, Dim>& gc) -> Index {
195 auto it = ghostSlot.find(gc);
196 if (it != ghostSlot.end())
197 return it->second;
198 Index s = static_cast<Index>(ghostCoords.size());
199 ghostSlot.emplace(gc, s);
200 ghostCoords.push_back(gc);
201 return s;
202 };
203
204 start_.assign(static_cast<std::size_t>(n) + 1, 0);
205 for (Index i = 0; i < n; ++i) {
206 for (const E& e : ent[static_cast<std::size_t>(i)]) {
207 if (e.rfId < 0) {
208 ref_.push_back(e.localRef);
209 w_.push_back(e.w);
210 diag_[static_cast<std::size_t>(i)] += e.w;
211 } else {
212 const RFace& rf = rfaces[static_cast<std::size_t>(e.rfId)];
213 const int Lj = coverLv[static_cast<std::size_t>(e.rfId)];
214 if (Lj < 0)
215 continue; // no neighbour
216 if (Lj >= rf.Li) { // finer side is i: α at i's face
217 const double a = alphaOf(rf.iLo, rf.si, rf.axis, rf.dir);
218 const double ww = a * coeff(rf.si, Coord(Coord(1) << Lj), rf.axis);
219 ref_.push_back(d_->local().numLeaves() + ghostRef(rf.gc));
220 w_.push_back(ww);
221 diag_[static_cast<std::size_t>(i)] += ww;
222 } else { // finer side is each remote sub-neighbour: α at its face (−dir)
223 const Coord sj = Coord(rf.si >> 1);
224 const int nsub = 1 << (Dim - 1);
225 for (int k = 0; k < nsub; ++k) {
226 std::array<Coord, Dim> sc = subCoord(rf.gc, rf.axis, k, sj);
227 const double a = alphaOf(fineLoGlobal(rf.iLo, rf.si, sj, rf.axis, rf.dir, k), sj,
228 rf.axis, -rf.dir);
229 const double ww = a * coeff(rf.si, sj, rf.axis);
230 ref_.push_back(d_->local().numLeaves() + ghostRef(sc));
231 w_.push_back(ww);
232 diag_[static_cast<std::size_t>(i)] += ww;
233 }
234 }
235 }
236 }
237 start_[static_cast<std::size_t>(i) + 1] = static_cast<Index>(ref_.size());
238 }
239 ghostCoords_ = std::move(ghostCoords);
240 n_ = n;
241 }
242
243 Index numLeaves() const { return n_; }
244
246 void apply(const std::vector<double>& u, std::vector<double>& Lu) const {
247 std::vector<double> g = d_->coverValues(ghostCoords_, u);
248 Lu.assign(static_cast<std::size_t>(n_), 0.0);
249 for (Index i = 0; i < n_; ++i) {
250 const double ui = u[static_cast<std::size_t>(i)];
251 double acc = 0.0;
252 for (Index k = start_[static_cast<std::size_t>(i)];
254 const Index r = ref_[static_cast<std::size_t>(k)];
255 const double val =
256 (r < n_) ? u[static_cast<std::size_t>(r)] : g[static_cast<std::size_t>(r - n_)];
257 acc += w_[static_cast<std::size_t>(k)] * (val - ui);
258 }
259 Lu[static_cast<std::size_t>(i)] = invVol_[static_cast<std::size_t>(i)] * acc;
260 }
261 }
262
264 double residual(const std::vector<double>& u, const std::vector<double>& rhs,
265 std::vector<double>& res) const {
266 std::vector<double> lu;
267 apply(u, lu);
268 res.assign(static_cast<std::size_t>(n_), 0.0);
269 double s = 0.0;
270 for (Index i = 0; i < n_; ++i) {
271 double r = rhs[static_cast<std::size_t>(i)] - lu[static_cast<std::size_t>(i)];
272 res[static_cast<std::size_t>(i)] = r;
273 s += r * r / invVol_[static_cast<std::size_t>(i)]; // V_i r²
274 }
275 double g = 0.0;
276 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
277 return std::sqrt(g);
278 }
279
280 double residualNorm(const std::vector<double>& u, const std::vector<double>& rhs) const {
281 std::vector<double> res;
282 return residual(u, rhs, res);
283 }
284
288 void jacobi(std::vector<double>& u, const std::vector<double>& rhs, int sweeps,
289 double omega = 0.8) const {
290 std::vector<double> tmp(static_cast<std::size_t>(n_));
291 for (int s = 0; s < sweeps; ++s) {
292 std::vector<double> g = d_->coverValues(ghostCoords_, u);
293 for (Index i = 0; i < n_; ++i) {
294 double sumOff = 0.0;
295 for (Index k = start_[static_cast<std::size_t>(i)];
297 const Index r = ref_[static_cast<std::size_t>(k)];
298 const double val =
299 (r < n_) ? u[static_cast<std::size_t>(r)] : g[static_cast<std::size_t>(r - n_)];
300 sumOff += w_[static_cast<std::size_t>(k)] * val;
301 }
302 const double Vrhs = rhs[static_cast<std::size_t>(i)] / invVol_[static_cast<std::size_t>(i)];
303 tmp[static_cast<std::size_t>(i)] =
304 (diag_[static_cast<std::size_t>(i)] != 0.0)
305 ? (sumOff - Vrhs) / diag_[static_cast<std::size_t>(i)]
306 : u[static_cast<std::size_t>(i)];
307 }
308 for (Index i = 0; i < n_; ++i)
309 u[static_cast<std::size_t>(i)] = (1.0 - omega) * u[static_cast<std::size_t>(i)] +
310 omega * tmp[static_cast<std::size_t>(i)];
311 }
312 }
313
314 private:
316 double cellWidth(int level, int axis) const {
317 return h0_[axis] * static_cast<double>(Index(1) << level);
318 }
319 double cellWidth(int level) const { return cellWidth(level, 0); }
320 double cellVol(int level) const {
321 const double f = static_cast<double>(Index(1) << level);
322 double v = 1.0;
323 for (int d = 0; d < Dim; ++d)
324 v *= h0_[d] * f;
325 return v;
326 }
328 double areaOf(Coord s, int axis) const {
329 double a = 1.0;
330 for (int d = 0; d < Dim; ++d)
331 if (d != axis)
332 a *= static_cast<double>(s) * h0_[d];
333 return a;
334 }
335 double areaOf(Coord s) const { return areaOf(s, 0); }
336 double coeff(Coord si, Coord sj, int axis) const {
337 double dist = 0.5 * (static_cast<double>(si) + static_cast<double>(sj)) * h0_[axis];
338 return areaOf(si < sj ? si : sj, axis) / dist;
339 }
340 double coeff(Coord si, Coord sj) const { return coeff(si, sj, 0); }
341
342 // tangential sub-coord for a finer 2:1 face (global, wrapped to the domain).
343 std::array<Coord, Dim> subCoord(const std::array<Coord, Dim>& gc, int axis, int k,
344 Coord sj) const {
345 std::array<Coord, Dim> q = gc;
346 int bit = 0;
347 for (int tt = 0; tt < Dim; ++tt) {
348 if (tt == axis)
349 continue;
350 const Coord off = ((k >> bit) & 1) ? sj : Coord(0);
351 long v = static_cast<long>(gc[tt]) + static_cast<long>(off);
352 const long gf = static_cast<long>(d_->globalFineSize()[tt]);
353 if (v >= gf)
354 v -= gf; // periodic tangential wrap (in-domain otherwise)
355 q[tt] = static_cast<Coord>(v);
356 ++bit;
357 }
358 return q;
359 }
360
361 DO* d_ = nullptr;
362 Vec<Dim> h0_ = detail::filledVec<Dim>(1.0);
363 Index n_ = 0;
364 std::vector<double> invVol_, diag_, w_;
365 std::vector<Index> start_, ref_; // face CSR (ref<n local, else ghost)
366 std::vector<std::array<Coord, Dim>> ghostCoords_; // dedup ghost cells (slot = ref−n)
367};
368
386template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
388 public:
390 using BO = typename DO::Octree;
391 using M = typename DO::M;
392
395 void build(const DO& finest) {
396 buildImpl(finest, [](const Vec<Dim>&, int) { return 1.0; }, false);
397 }
398
403 template <class OpenFn>
404 void build(const DO& finest, OpenFn&& openFn) {
405 buildImpl(finest, std::forward<OpenFn>(openFn), true);
406 }
407
408 private:
409 template <class OpenFn>
410 void buildImpl(const DO& finest, OpenFn&& openFn, bool hasOpen) {
411 hasOpen_ = hasOpen;
412 levels_.clear();
413 const IVec<Dim> g = finest.globalRootSize();
414 const unsigned lmax = finest.lmax();
415 const AmrGeometry<Dim> geo = finest.globalGeometry();
416 const auto per = finest.periodic();
417 MPI_Comm comm = finest.comm();
418
419 auto l0 = std::make_unique<Level>();
420 l0->d = finest; // copy (level 0 owned by the hierarchy)
421 levels_.push_back(std::move(l0));
422 for (;;) {
423 BO coarse = levels_.back()->d.local(); // copy
424 const Index before = coarse.numLeaves();
425 coarse.coarsenIf([](typename DO::Code, unsigned) { return true; });
426 if (coarse.numLeaves() == before)
427 break; // uniform root brick reached
428 auto lv = std::make_unique<Level>();
429 lv->d.init(g, lmax, geo, per, comm);
430 lv->d.local() = coarse; // same ORB block, coarsened local octree
431 levels_.push_back(std::move(lv));
432 }
433 // The coarsening above stops per-rank when this block reaches its root brick, so a
434 // rank owning shallow blocks builds FEWER levels than one owning deep refinement.
435 // Each level's op.init / vcycle drives a COLLECTIVE owner request/reply (coverLevels,
436 // coverValues over `comm`), so an unequal level count would mismatch the collectives
437 // and deadlock at np>1. Pad every rank up to the global max with repeats of its root
438 // brick: the extra levels are identity transfers (c2p maps a root cell to itself), a
439 // no-op in the V-cycle — and they exactly mirror what the whole-domain COMM_SELF
440 // reference does in its already-coarse regions, so the bit-exact WORLD==SELF check is
441 // preserved. (Allreduce on COMM_SELF is a no-op, leaving the serial path unchanged.)
442 {
443 int localLevels = static_cast<int>(levels_.size());
446 while (static_cast<int>(levels_.size()) < globalLevels) {
447 auto lv = std::make_unique<Level>();
448 lv->d.init(g, lmax, geo, per, comm);
449 lv->d.local() = levels_.back()->d.local(); // repeat the root brick (identity level)
450 levels_.push_back(std::move(lv));
451 }
452 }
453 for (auto& lv : levels_) {
454 lv->op.init(lv->d, openFn);
455 const Index n = lv->d.local().numLeaves();
456 lv->x.assign(static_cast<std::size_t>(n), 0.0);
457 lv->b.assign(static_cast<std::size_t>(n), 0.0);
458 lv->res.assign(static_cast<std::size_t>(n), 0.0);
459 }
460 // local fine→coarse maps (covering coarse leaf, same block).
461 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
462 const BO& f = levels_[L]->d.local();
463 const BO& c = levels_[L + 1]->d.local();
464 const Index nf = f.numLeaves();
465 auto& c2p = levels_[L]->c2p;
466 c2p.assign(static_cast<std::size_t>(nf), -1);
467 for (Index i = 0; i < nf; ++i)
468 c2p[static_cast<std::size_t>(i)] = c.find(f.code(i));
469 }
470
471 // Bottom solver (openness-free only): a uniform DistributedMultigrid on the root
472 // grid, below the root brick. The coarsest graded level IS the uniform root brick
473 // (root cells at level lmax); its operator is L=∇² at spacing h0·2^lmax, the same
474 // operator the uniform MG's finest applies (DistributedPoisson is also L=∇²) on
475 // the same root grid — so the bottom solve of L e = res is inner.vcycle(e, res).
476 // With openness the uniform inner is inconsistent, so the coarsest is bottom-solved
477 // with Jacobi on its own (openness-carrying) operator instead.
478 if (!hasOpen_) {
480 for (int d = 0; d < Dim; ++d) // root-cell width, per axis
481 ig.h0[d] = geo.h0[d] * static_cast<double>(Index(1) << lmax);
482 inner_ = std::make_unique<DistributedMultigrid<Dim, Bits>>();
483 inner_->build(g, ig, per, comm);
484 DO& coarsest = levels_.back()->d;
485 nCoarse_ = coarsest.local().numLeaves();
486 innerMap_.assign(static_cast<std::size_t>(nCoarse_), -1);
487 for (Index i = 0; i < nCoarse_; ++i)
488 innerMap_[static_cast<std::size_t>(i)] =
489 inner_->octree(0).findGlobalRoot(coarsest.globalRootOf(i));
490 }
491 }
492
493 public:
494 std::size_t numLevels() const { return levels_.size(); }
495 DistributedFvOperator<Dim, Bits>& op(std::size_t L = 0) { return levels_[L]->op; }
496 Index numLeaves(std::size_t L = 0) const { return levels_[L]->d.local().numLeaves(); }
497
501 void vcycle(std::vector<double>& x, const std::vector<double>& b, int pre = 2, int post = 2,
502 int innerCycles = 6, double omega = 0.8, std::size_t L = 0) {
503 auto& lv = *levels_[L];
504 if (L + 1 == levels_.size()) {
505 if (inner_)
506 bottomSolve(lv.op, x, b, innerCycles); // openness-free: chained uniform MG
507 else
508 lv.op.jacobi(x, b, bottomSweeps_, omega); // openness: Jacobi on the correct op
509 return;
510 }
511 lv.op.jacobi(x, b, pre, omega);
512 std::vector<double> res;
513 lv.op.residual(x, b, res);
514 // restrict residual → coarse rhs (local average over children).
515 auto& cl = *levels_[L + 1];
516 const Index nc = cl.d.local().numLeaves();
517 std::vector<double> cb(static_cast<std::size_t>(nc), 0.0),
518 cn(static_cast<std::size_t>(nc), 0.0);
519 const auto& c2p = lv.c2p;
520 const Index nf = lv.d.local().numLeaves();
521 for (Index i = 0; i < nf; ++i) {
522 Index p = c2p[static_cast<std::size_t>(i)];
523 if (p < 0)
524 continue;
525 cb[static_cast<std::size_t>(p)] += res[static_cast<std::size_t>(i)];
526 cn[static_cast<std::size_t>(p)] += 1.0;
527 }
528 for (Index p = 0; p < nc; ++p)
529 if (cn[static_cast<std::size_t>(p)] > 0.0)
530 cb[static_cast<std::size_t>(p)] /= cn[static_cast<std::size_t>(p)];
531 std::vector<double> cx(static_cast<std::size_t>(nc), 0.0);
532 vcycle(cx, cb, pre, post, innerCycles, omega, L + 1);
533 for (Index i = 0; i < nf; ++i) {
534 Index p = c2p[static_cast<std::size_t>(i)];
535 if (p >= 0)
536 x[static_cast<std::size_t>(i)] += cx[static_cast<std::size_t>(p)];
537 }
538 lv.op.jacobi(x, b, post, omega);
539 }
540
541 private:
542 // Solve L x = b on the uniform root brick via the inner uniform MG (correction
543 // scheme): res = b − Lx; solve L e = res with `cycles` inner V-cycles; x += e.
544 // Both operators are L = ∇² (same suite-wide sign), so no sign flip is needed.
545 // Index map coarsest→inner is by global root cell (identity in practice).
546 void bottomSolve(DistributedFvOperator<Dim, Bits>& op, std::vector<double>& x,
547 const std::vector<double>& b, int cycles) {
548 std::vector<double> res;
549 op.residual(x, b, res);
550 const Index ni = inner_->numLeaves(0);
551 std::vector<double> bi(static_cast<std::size_t>(ni), 0.0),
552 ei(static_cast<std::size_t>(ni), 0.0);
553 for (Index i = 0; i < nCoarse_; ++i) {
554 Index m = innerMap_[static_cast<std::size_t>(i)];
555 if (m >= 0)
556 bi[static_cast<std::size_t>(m)] = res[static_cast<std::size_t>(i)];
557 }
558 for (int c = 0; c < cycles; ++c)
559 inner_->vcycle(ei, bi);
560 for (Index i = 0; i < nCoarse_; ++i) {
561 Index m = innerMap_[static_cast<std::size_t>(i)];
562 if (m >= 0)
563 x[static_cast<std::size_t>(i)] += ei[static_cast<std::size_t>(m)];
564 }
565 }
566
567 struct Level {
568 DO d;
569 DistributedFvOperator<Dim, Bits> op;
570 std::vector<Index> c2p; // fine leaf → covering coarse leaf (local)
571 std::vector<double> x, b, res; // scratch
572 };
573 std::vector<std::unique_ptr<Level>> levels_;
574 std::unique_ptr<DistributedMultigrid<Dim, Bits>> inner_; // uniform bottom solver (openness-free)
575 std::vector<Index> innerMap_; // coarsest leaf → inner finest leaf
576 Index nCoarse_ = 0;
577 bool hasOpen_ = false;
578 int bottomSweeps_ = 400; // Jacobi sweeps on the coarsest op when openness present
579};
580
581} // namespace peclet::amr
582
583#endif // PECLET_AMR_DISTRIBUTED_FV_HPP
void jacobi(std::vector< double > &u, const std::vector< double > &rhs, int sweeps, double omega=0.8) const
sweeps weighted-Jacobi relaxations of L u = rhs (point solve u_i ← (Σ w u_nb − V_i rhs_i)/Σ w).
void init(DO &d, OpenFn &&openFn)
Build with cut-cell openness: w_f = α_f · A_f/d_f, where α_f = openFn(face centroid,...
void init(DO &d)
Build the consistent face stencil, openness-free (w_f = A_f/d_f).
double residualNorm(const std::vector< double > &u, const std::vector< double > &rhs) const
double residual(const std::vector< double > &u, const std::vector< double > &rhs, std::vector< double > &res) const
res = rhs − L u; returns the global L2 norm (volume-weighted).
DistributedOctree< Dim, Bits > DO
void apply(const std::vector< double > &u, std::vector< double > &Lu) const
Lu = L u (consistent conservative FV Laplacian, negative-definite).
std::vector< int > coverLevels(const std::vector< std::array< Coord, Dim > > &coords) const
For each global fine coord (already wrapped into the domain), the level of the covering leaf on its o...
const Vec< Dim > & h0() const
Phase 3: the FINEST spacing per axis (docs/amr_anisotropic.md §2).
const AmrGeometry< Dim > & globalGeometry() const
std::vector< double > coverValues(const std::vector< std::array< Coord, Dim > > &coords, const std::vector< double > &field, double sentinel=kNoNeighbor) const
For each global fine coord, the covering leaf's field value on its owner (sentinel if none).
const IVec< Dim > & blockFineOrigin() const
FaceInfo faceAcross(Index i, int axis, int dir) const
const IVec< Dim > & globalFineSize() const
Geometric-multigrid V-cycle on a graded distributed octree, built on DistributedFvOperator.
Index numLeaves(std::size_t L=0) const
DistributedFvOperator< Dim, Bits > & op(std::size_t L=0)
void build(const DO &finest, OpenFn &&openFn)
Build with cut-cell openness openFn on every level (each level re-samples the geometry at its own fac...
void vcycle(std::vector< double > &x, const std::vector< double > &b, int pre=2, int post=2, int innerCycles=6, double omega=0.8, std::size_t L=0)
One V-cycle of L u = rhs on level L (default finest), correction scheme.
void build(const DO &finest)
Build the hierarchy from an already-graded, 2:1-balanced finest octree (openness-free; the coarsest i...
DistributedOctree< Dim, Bits > DO
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,...