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
cut_cell.hpp
Go to the documentation of this file.
1// core — Robust-Scaled cut-cell Dirichlet operator on a BlockOctree.
2//
3// A faithful port of flow's ξ-polynomial sub-cell boundary scheme
4// (flow/src/cut_cell_ibm.hpp: poly_*, ibmFillEntry, ibmModifyStencil) onto the
5// cell-centered octree. Where the openness/aperture scheme (poisson.hpp) imposes a
6// *Neumann* wall (no flux through solid faces), this imposes a *Dirichlet* value
7// u = u_bc on the immersed boundary located at the true sub-cell distance ξ·h
8// (Shortley–Weller): for a cut cell whose neighbour in some direction is solid,
9// the 7-point stencil is modified by the boundary-distance polynomials, with
10// D_rescale row-scaling for the small-cell problem. This is the velocity-diffusion
11// / scalar-Dirichlet half of the cut-cell IBM (the user's "ξ-polynomial sub-cell
12// BC"); the cell fluid-volume fraction κ classifies solid/fluid/cut cells.
13//
14// Cut cells are assumed to have same-level face neighbours (the suite contract:
15// resolve the immersed boundary in a uniformly-finest band, so cut cells never sit
16// on a 2:1 interface — see docs/AMR.md). 3D (flow's 6-direction scheme).
17#ifndef PECLET_AMR_CUT_CELL_HPP
18#define PECLET_AMR_CUT_CELL_HPP
19
20#include <algorithm>
21#include <array>
22#include <cmath>
23#include <vector>
24
26#include "peclet/amr/common.hpp"
27#include "peclet/amr/face_csr.hpp" // shared host+device assembled-operator row kernels
30#include "peclet/core/common/host_parallel.hpp"
31#include "peclet/core/common/types.hpp"
32#include "peclet/core/scheme/cut_cell_closure.hpp" // the shared poly_* (QUALITY_PLAN G.2 lift)
33
34namespace peclet::amr {
35
36// ---- boundary-distance polynomials (QUALITY_PLAN G.2: the templated copy shared with flow now
37// lives in core/include/peclet/core/scheme/cut_cell_closure.hpp; these are double-precision
38// forwards so every `cc::poly_*` call site below is unchanged) ----
39namespace cc {
40// MORTON_HD (from face_csr.hpp): KOKKOS_FUNCTION on a Kokkos build, empty otherwise — so
41// buildCutStencil and these polynomials are device-callable under the AMR device assembler
42// (assembly.hpp) yet compile unchanged in the pure-C++ host oracle build. poly_abs replaces
43// std::fabs (host-only under a CUDA device pass); it is bit-identical to std::fabs for every value
44// buildCutStencil feeds it (the only difference, fabs(-0.0)=+0.0 vs −0.0, never occurs and would
45// not change the |·|< comparisons). NOT part of the closure family (core keeps no fabs
46// substitute), so it stays local.
47MORTON_HD inline double poly_abs(double x) {
48 return x < 0.0 ? -x : x;
49}
50MORTON_HD inline double poly_D(double xi) {
51 return peclet::core::scheme::poly_D<double>(xi);
52}
53MORTON_HD inline double poly_N_nb(double xi) {
54 return peclet::core::scheme::poly_N_nb<double>(xi);
55}
56MORTON_HD inline double poly_Nc(double xi) {
57 return peclet::core::scheme::poly_Nc<double>(xi);
58}
59MORTON_HD inline double poly_Nbc(double xi) {
60 return peclet::core::scheme::poly_Nbc<double>(xi);
61}
62MORTON_HD inline double poly_D_sandwich(double xm, double xp) {
63 return peclet::core::scheme::poly_D_sandwich<double>(xm, xp);
64}
65MORTON_HD inline double poly_N_c_sandwich(double xm, double xp) {
66 return peclet::core::scheme::poly_N_c_sandwich<double>(xm, xp);
67}
68MORTON_HD inline double poly_Nbc_pp_sw(double xm, double xp) {
69 return peclet::core::scheme::poly_Nbc_pp_sw<double>(xm, xp);
70}
71MORTON_HD inline double poly_Nbc_mp_sw(double xm, double xp) {
72 return peclet::core::scheme::poly_Nbc_mp_sw<double>(xm, xp);
73}
74} // namespace cc
75
76template <unsigned Bits = 21u>
78 public:
79 static constexpr int Dim = 3;
81 using M = typename Octree::M;
82 using Code = typename Octree::Code;
83 using Coord = typename Octree::Coord;
84
85 // direction k: 0=+x,1=-x,2=+y,3=-y,4=+z,5=-z (flow order); OPP swaps sides.
86 static constexpr int OPP[6] = {1, 0, 3, 2, 5, 4};
87
88 void init(const Octree& t, Real h0, Vec<3> origin = Vec<3>{}) {
89 init(t, detail::filledVec<3>(h0), origin);
90 }
91 void init(const Octree& t, const Vec<3>& h0, Vec<3> origin = Vec<3>{}) {
92 t_ = &t;
93 h0_ = h0;
94 origin_ = origin;
95 extResolve_ = nullptr; // distributed seam re-installed by the owner after every init
96 ghostLo_.clear();
97 ghostLv_.clear();
98 frameShift_ = {};
99 for (int d = 0; d < 3; ++d)
100 fineExt_[d] = static_cast<Coord>(t.brick()[d] * (Index(1) << t.lmax()));
101 }
102
103 // ---- distributed seam (docs/amr_distributed_flow.md): mirrors AmrPoisson's. build() forwards
104 // all three into the internal lap_ (which it re-inits), so the whole momentum geometry —
105 // regular C/F rows and the ξ-overlay band — resolves cross-block probes through the LeafHalo
106 // registry and evaluates the SDF in the GLOBAL frame (bit-identical samples on every rank).
108 void setResolver(ExtResolver r) { extResolve_ = std::move(r); }
112 void setGhosts(std::vector<std::array<long, 3>> lo, std::vector<unsigned> lv) {
113 ghostLo_ = std::move(lo);
114 ghostLv_ = std::move(lv);
115 }
116 Index numGhosts() const { return static_cast<Index>(ghostLv_.size()); }
117 void setFrameShift(const std::array<long, 3>& s) { frameShift_ = s; }
118
119 Index numLeaves() const { return t_->numLeaves(); }
120 bool isFluid(Index i) const { return fluid_[static_cast<std::size_t>(i)]; }
124 bool isCut(Index i) const { return cut_[static_cast<std::size_t>(i)] != 0; }
126 Index neighborOf(Index i, int k) const { return nb_[static_cast<std::size_t>(i) * 6 + k]; }
127 double kappa(Index i) const { return kappa_[static_cast<std::size_t>(i)]; }
128 double rhsScale(Index i) const { return rscale_[static_cast<std::size_t>(i)]; }
129
130 // ---- read-only views of the built geometry, for the device assembler (momentum_assembly.hpp)
131 // to stage to the device and reproduce build()/assembleOperator there. Mirrors how AmrPoisson
132 // exposes its openness to the device FV assembler.
133 const AmrPoisson<3, Bits>& lap() const {
134 return lap_;
135 }
136 const std::vector<double>& sdfCRaw() const {
137 return sdfC_;
138 }
139 const std::vector<Index>& nbRaw() const { return nb_; }
140 const std::vector<char>& fluidRaw() const { return fluid_; }
141 const std::vector<char>& cutRaw() const { return cut_; }
142 const std::vector<double>& acRaw() const { return AC_; }
143 const std::vector<double>& offRaw() const { return off_; }
144 const std::vector<double>& rscaleRaw() const { return rscale_; }
145 double idiag() const { return idiag_; }
146 double mu() const { return mu_; }
148 Vec<3> beta() const {
149 Vec<3> b{};
150 for (int d = 0; d < 3; ++d)
151 b[d] = mu_ / (h0_[d] * h0_[d]);
152 return b;
153 }
154 bool hasAdv() const { return hasAdv_; }
155 const std::vector<double>& advDiagRaw() const { return advDiag_; }
156 const std::vector<double>& advCoefRaw() const { return advCoef_; }
157 const std::vector<Index>& advStartRaw() const { return advStart_; }
158 const std::vector<Index>& advNbrRaw() const { return advNbr_; }
159
163 template <class SdfFn>
164 void build(SdfFn&& sdfFn, double idiag, double beta, int nsub = 4) {
165 build(std::forward<SdfFn>(sdfFn), idiag, detail::filledVec<3>(beta), nsub);
166 }
168 template <class SdfFn>
169 void build(SdfFn&& sdfFn, double idiag = 0.0, const Vec<3>& beta = detail::filledVec<3>(1.0),
170 int nsub = 4) {
171 const Index n = numLeaves();
172 const Index ng = numGhosts();
173 // C/F-aware Laplacian provider for regular (non-cut) fluid cells: an AmrPoisson
174 // with NO openness (α=1) -> the plain ∇² with 2:1-interface coeff(si,sj). The
175 // cut cells (finest, same-level) keep the ξ overlay below.
176 lap_.init(*t_, h0_);
177 if (extResolve_) { // distributed: forward the seam into the freshly-init'd lap_
178 lap_.setResolver(extResolve_);
179 lap_.setGhosts(ghostLo_, ghostLv_);
180 lap_.setFrameShift(frameShift_);
181 }
182 idiag_ = idiag;
183 mu_ = beta[0] * h0_[0] * h0_[0]; // physical μ (operator A = idiag·I − μ∇²)
184 sdfC_.assign(static_cast<std::size_t>(n + ng), 0.0);
185 kappa_.assign(static_cast<std::size_t>(n), 0.0);
186 fluid_.assign(static_cast<std::size_t>(n + ng), false);
187 cut_.assign(static_cast<std::size_t>(n), 0);
188 hasAdv_ = false; // advection FOU is rebuilt per step via buildAdvectionFou
189 AC_.assign(static_cast<std::size_t>(n), 1.0);
190 rscale_.assign(static_cast<std::size_t>(n), 1.0);
191 inhom_.assign(static_cast<std::size_t>(n), 0.0);
192 off_.assign(static_cast<std::size_t>(n) * 6, 0.0);
193 nb_.assign(static_cast<std::size_t>(n) * 6, -1);
194
195 // Host-parallel leaf loop (setup-parallel plan rung 3; distributed included since the F1
196 // resolution). `neighbor()` goes through AmrPoisson::probeSlot, which in a distributed
197 // build calls `extResolve_` = LeafHalo::resolveGlobal; that path is now thread-safe — the
198 // miss registration (the ONLY mutation reachable from a build pass) is mutex-guarded, and
199 // the miss SET is order-canonical (coord-keyed std::map), so ghost numbering and the
200 // fixpoint's rounds are bitwise-identical to the serial build at any thread count. See
201 // LeafHalo::resolve and docs/amr_setup_parallel_plan.md ## Findings (F1 RESOLVED).
202 auto forLeaves = [&](auto&& body) { hostParFor(n, body); };
203
204 // Pass 1: cell-centre SDF, κ (subsampled), fluid flag, neighbour indices.
205 // Single-rank: every write is to leaf i's OWN slots (sdfC_[i], fluid_[i], kappa_[i],
206 // nb_[6i..6i+5]) and sdfFn is a pure geometry query — the disjoint-write pattern,
207 // bitwise-invariant under any thread count. Pass 2 reads NEIGHBOURS' sdfC_, so it must be a
208 // separate parallel region (the shim fences between them).
209 forLeaves([&](Index i) {
210 Vec<3> c = cellCenter(i);
211 double sc = sdfFn(c);
212 sdfC_[static_cast<std::size_t>(i)] = sc;
213 fluid_[static_cast<std::size_t>(i)] = sc > 0.0;
214 kappa_[static_cast<std::size_t>(i)] = volumeFraction(i, sdfFn, nsub);
215 for (int k = 0; k < 6; ++k)
216 nb_[static_cast<std::size_t>(i) * 6 + k] = neighbor(i, k);
217 });
218 // Ghost metadata (distributed): the SAME world SdfFn sampled at the ghost cell centre in
219 // the GLOBAL frame — bit-identical to the owner's Pass-1 sample of that leaf, so the
220 // ξ-classifications (and hence the CSR coefficients) agree across ranks exactly.
221 for (Index g = 0; g < ng; ++g) {
222 const auto& lo = ghostLo_[static_cast<std::size_t>(g)];
223 const double s = static_cast<double>(1L << ghostLv_[static_cast<std::size_t>(g)]);
224 Vec<3> c{};
225 for (int d = 0; d < 3; ++d)
226 c[d] = origin_[d] + (static_cast<double>(lo[d] + frameShift_[d]) + 0.5 * s) * h0_[d];
227 const double sc = sdfFn(c);
228 sdfC_[static_cast<std::size_t>(n + g)] = sc;
229 fluid_[static_cast<std::size_t>(n + g)] = sc > 0.0;
230 }
231
232 // Pass 2: build per-leaf stencil.
233 // Phase 3: the diagonal is 2*(beta_x + beta_y + beta_z), parenthesised as ONE sum so a cubic
234 // octree reproduces the single correctly-rounded `6.0*beta` (Rule B).
235 //
236 // beta arrives as mu/h_finest^2 (AmrFlow::betaPerAxis), so it is the coefficient for a leaf at
237 // the FINEST level. A leaf at level L has width 2^L * h_finest, so its viscous coefficient is
238 // beta / 4^L. Scaling here, per leaf, rather than hoisting one beta/AC0 out of the loop: the
239 // xi-overlay row is a pure multiple of the beta it is handed, so a level-L cut cell built with
240 // the finest beta is 4^L too stiff -- an effective viscosity mu*4^L in the wall cells only.
241 // That was measured as a silent constant velocity offset of -(3/8)(F/mu) h_leaf^2 (1 - 4^-L)
242 // on plane Poiseuille (3.6-4.7% at L=1..3), with a perfect-looking parabola; see
243 // docs/ROADMAP.md A5. Every other row in the operator was already level-aware: the regular
244 // fluid rows use physical areas and volumes (assembleOperator below, -mu*invV*(a*c)), the
245 // velocity-MG coarse operator computes mu/cellWidth^2 per row (velocity_mg.hpp), and the
246 // mixed-level seam delta does exactly this scaling for seam rows only
247 // (ghost_projection_sampled.hpp "the ROW-LOCAL beta per axis") -- which is why a uniformly
248 // COARSE band, having no seam rows, kept the wrong coefficient.
249 //
250 // Exactly inert at the finest level: f == 1.0, so betaArr and AC0 are bit-for-bit the old beta
251 // and AC0 expressions, and any run whose cut cells are all finest is byte-identical.
252 forLeaves([&](Index i) { // own-leaf slots only (rung 3, architect-pre-cleared: no
253 // neighbour-indexed stores in this body)
254 const double f = static_cast<double>(Index(1) << t_->level(i)); // h_leaf / h_finest
255 const double inv = 1.0 / (f * f);
256 const double betaArr[3] = {beta[0] * inv, beta[1] * inv, beta[2] * inv};
257 const double AC0 = idiag + ((2.0 * betaArr[0] + 2.0 * betaArr[1]) + 2.0 * betaArr[2]);
258 if (!fluid_[static_cast<std::size_t>(i)]) { // solid: identity row u=0
259 AC_[static_cast<std::size_t>(i)] = 1.0;
260 for (int k = 0; k < 6; ++k)
261 off_[static_cast<std::size_t>(i) * 6 + k] = 0.0;
262 return;
263 }
264 double sdf_n[6];
265 bool anyGhost = false;
266 for (int k = 0; k < 6; ++k) {
267 Index j = nb_[static_cast<std::size_t>(i) * 6 + k];
268 sdf_n[k] = (j >= 0) ? sdfC_[static_cast<std::size_t>(j)] : -1.0; // missing => solid
269 if (sdf_n[k] < 0.0)
270 anyGhost = true;
271 }
272 cut_[static_cast<std::size_t>(i)] = anyGhost ? 1 : 0;
273 double AC = AC0, off[6];
274 for (int k = 0; k < 6; ++k)
275 off[k] = -betaArr[k / 2]; // face k belongs to axis k/2
276 double rscale = 1.0, inhomCoef = 0.0;
277 if (anyGhost)
278 buildCutStencil(sdfC_[static_cast<std::size_t>(i)], sdf_n, betaArr, AC0, AC, off, rscale,
279 inhomCoef);
280 AC_[static_cast<std::size_t>(i)] = AC;
281 for (int k = 0; k < 6; ++k)
282 off_[static_cast<std::size_t>(i) * 6 + k] = off[k];
283 rscale_[static_cast<std::size_t>(i)] = rscale;
284 inhom_[static_cast<std::size_t>(i)] = inhomCoef;
285 });
286 }
287
295 struct Assembled {
296 std::vector<double> diag;
297 std::vector<Index> start;
298 std::vector<Index> nbr;
299 std::vector<double> coef;
300 };
311 const Index n = numLeaves();
312 Assembled A;
313 A.diag.assign(static_cast<std::size_t>(n), 0.0);
314 A.start.assign(static_cast<std::size_t>(n) + 1, 0);
315 std::vector<std::vector<std::pair<Index, double>>> rows(static_cast<std::size_t>(n));
316 for (Index i = 0; i < n; ++i) {
317 const std::size_t s = static_cast<std::size_t>(i);
318 if (!fluid_[s]) { // solid: identity row (u = u_bc)
319 A.diag[s] = 1.0;
320 continue;
321 }
322 if (cut_[s]) { // ξ-overlay stencil (same-level neighbours)
323 A.diag[s] = AC_[s];
324 for (int k = 0; k < 6; ++k) {
325 double a = off_[s * 6 + k];
326 if (a == 0.0)
327 continue;
328 Index j = nb_[s * 6 + k];
329 if (j >= 0)
330 rows[s].emplace_back(j, a);
331 }
332 } else { // regular fluid: idiag·I − μ∇² with C/F-aware face coupling
333 const double invV = 1.0 / lap_.cellVolume(i);
334 double dsum = 0.0;
335 lap_.forEachFaceNeighbor(i, [&](Index j, Real c, int, double a) {
336 rows[s].emplace_back(j, -mu_ * invV * (a * c));
337 dsum += a * c;
338 });
339 A.diag[s] = idiag_ + mu_ * invV * dsum;
340 }
341 if (hasAdv_) { // implicit-FOU advection: diagonal (outflow) + CSR (inflow)
342 const double as = scaleAdvByRscale ? rscale_[s] : 1.0;
343 A.diag[s] += as * advDiag_[s];
344 for (std::size_t p = static_cast<std::size_t>(advStart_[s]);
345 p < static_cast<std::size_t>(advStart_[static_cast<std::size_t>(i) + 1]); ++p)
346 rows[s].emplace_back(advNbr_[p], as * advCoef_[p]);
347 }
348 }
349 for (Index i = 0; i < n; ++i)
350 A.start[static_cast<std::size_t>(i) + 1] =
351 A.start[static_cast<std::size_t>(i)] +
352 static_cast<Index>(rows[static_cast<std::size_t>(i)].size());
353 const Index nnz = A.start[static_cast<std::size_t>(n)];
354 A.nbr.resize(static_cast<std::size_t>(nnz));
355 A.coef.resize(static_cast<std::size_t>(nnz));
356 for (Index i = 0; i < n; ++i) {
357 Index k = A.start[static_cast<std::size_t>(i)];
358 for (auto& e : rows[static_cast<std::size_t>(i)]) {
359 A.nbr[static_cast<std::size_t>(k)] = e.first;
360 A.coef[static_cast<std::size_t>(k)] = e.second;
361 ++k;
362 }
363 }
364 return A;
365 }
366
367 // ---- Runtime operator: the assembled CSR applied with the SHARED face_csr.hpp row kernels — the
368 // exact same arithmetic the device runs (momentum.hpp), executed serially here. assembleOperator
369 // folds the implicit-FOU advection into the single CSR, so the FaceCsrOpT view sets hasAdv=false.
370 // The *Geometric variants above are the independent reference the oracle tests check this
371 // against.
372
376 v.n = numLeaves();
377 v.diag = HostArr<double>(A.diag.data());
378 v.coef = HostArr<double>(A.coef.data());
379 v.start = HostArr<Index>(A.start.data());
380 v.nbr = HostArr<Index>(A.nbr.data());
381 v.hasAdv = false; // advection already folded into the single CSR by assembleOperator
382 return v;
383 }
384
386 void applyOp(const std::vector<double>& u, std::vector<double>& out) const {
387 const Assembled A = assembleOperator();
388 const auto op = hostOp(A);
389 const Index n = numLeaves();
390 out.assign(static_cast<std::size_t>(n), 0.0);
391 const HostArr<double> uacc(u.data());
392 for (Index i = 0; i < n; ++i)
393 out[static_cast<std::size_t>(i)] = faceCsrApplyRow(op, i, uacc);
394 }
395
396 double residual(const std::vector<double>& u, const std::vector<double>& b,
397 std::vector<double>& res) const {
398 const Assembled A = assembleOperator();
399 const auto op = hostOp(A);
400 const Index n = numLeaves();
401 res.assign(static_cast<std::size_t>(n), 0.0);
402 const HostArr<double> uacc(u.data());
403 double s = 0.0;
404 for (Index i = 0; i < n; ++i) {
405 double r = b[static_cast<std::size_t>(i)] - faceCsrApplyRow(op, i, uacc);
406 res[static_cast<std::size_t>(i)] = r;
407 if (fluid_[static_cast<std::size_t>(i)])
408 s += r * r;
409 }
410 return std::sqrt(s);
411 }
412
416 void gaussSeidel(std::vector<double>& u, const std::vector<double>& b, int sweeps) const {
417 const Assembled A = assembleOperator();
418 const auto op = hostOp(A);
419 const Index n = numLeaves();
420 const HostArr<double> uacc(u.data());
421 for (int s = 0; s < sweeps; ++s)
422 for (Index i = 0; i < n; ++i) {
423 double off, d;
424 faceCsrOffDiag(op, i, uacc, off, d);
425 u[static_cast<std::size_t>(i)] = faceCsrPointUpdate(b[static_cast<std::size_t>(i)], off, d,
426 u[static_cast<std::size_t>(i)], 1.0);
427 }
428 }
429
436 void applyOpGeometric(const std::vector<double>& u, std::vector<double>& out) const {
437 const Index n = numLeaves();
438 out.assign(static_cast<std::size_t>(n), 0.0);
439 std::vector<double> Lu;
440 lap_.applyLaplacian(u, Lu); // C/F-aware ∇² (α=1); used only for regular fluid cells
441 for (Index i = 0; i < n; ++i) {
442 const std::size_t s = static_cast<std::size_t>(i);
443 if (!fluid_[s]) {
444 out[s] = u[s];
445 } else if (cut_[s]) {
446 double acc = AC_[s] * u[s];
447 for (int k = 0; k < 6; ++k) {
448 double a = off_[s * 6 + k];
449 if (a == 0.0)
450 continue;
451 Index j = nb_[s * 6 + k];
452 if (j >= 0)
453 acc += a * u[static_cast<std::size_t>(j)];
454 }
455 out[s] = acc;
456 } else {
457 out[s] = idiag_ * u[s] - mu_ * Lu[s]; // regular fluid (C/F-consistent)
458 }
459 if (hasAdv_ && fluid_[s])
460 out[s] += advApply(i, u); // implicit FOU advection
461 }
462 }
463
476 void buildAdvectionFou(const std::array<std::vector<double>, 3>& uadv, double rho,
477 const std::vector<double>& uf, const std::vector<Index>& faceStart,
478 bool useFace) {
479 const Index n = numLeaves();
480 advDiag_.assign(static_cast<std::size_t>(n), 0.0);
481 advStart_.assign(static_cast<std::size_t>(n) + 1, 0);
482 hasAdv_ = true;
483 auto velOutOf = [&](Index i, Index j, int axis, int dir, Index slot) {
484 return useFace ? dir * uf[static_cast<std::size_t>(slot)]
485 : dir * 0.5 *
486 (uadv[axis][static_cast<std::size_t>(i)] +
487 uadv[axis][static_cast<std::size_t>(j)]);
488 };
489 // pass 1: count inflow (off-diagonal) fluid faces per cell.
490 for (Index i = 0; i < n; ++i) {
491 if (!fluid_[static_cast<std::size_t>(i)])
492 continue;
493 int cnt = 0;
494 Index s = useFace ? faceStart[static_cast<std::size_t>(i)] : 0;
495 lap_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double, double) {
496 if (fluid_[static_cast<std::size_t>(j)] && velOutOf(i, j, axis, dir, s) < 0.0)
497 ++cnt;
498 ++s;
499 });
500 advStart_[static_cast<std::size_t>(i) + 1] = cnt;
501 }
502 for (Index i = 0; i < n; ++i)
503 advStart_[static_cast<std::size_t>(i) + 1] += advStart_[static_cast<std::size_t>(i)];
504 advNbr_.assign(advStart_[static_cast<std::size_t>(n)], -1);
505 advCoef_.assign(advStart_[static_cast<std::size_t>(n)], 0.0);
506 // pass 2: fill diagonal (outflow) + CSR off-diagonals (inflow).
507 for (Index i = 0; i < n; ++i) {
508 if (!fluid_[static_cast<std::size_t>(i)])
509 continue;
510 const double Vi = lap_.cellVolume(i);
511 std::size_t pos = static_cast<std::size_t>(advStart_[static_cast<std::size_t>(i)]);
512 Index s = useFace ? faceStart[static_cast<std::size_t>(i)] : 0;
513 lap_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double) {
514 const Index slot = s++;
515 if (!fluid_[static_cast<std::size_t>(j)])
516 return;
517 double velOut = velOutOf(i, j, axis, dir, slot);
518 double w = rho * area * velOut / Vi;
519 if (velOut < 0.0) { // inflow → couple to upstream neighbour j (matches pass 1)
520 advNbr_[pos] = j;
521 advCoef_[pos] = w;
522 ++pos;
523 } else {
524 advDiag_[static_cast<std::size_t>(i)] += w; // outflow → diagonal
525 }
526 });
527 }
528 }
529
532 std::vector<double> makeRhs(const std::vector<double>& src, double u_bc) const {
533 const Index n = numLeaves();
534 std::vector<double> b(static_cast<std::size_t>(n), 0.0);
535 for (Index i = 0; i < n; ++i) {
536 if (!fluid_[static_cast<std::size_t>(i)]) {
537 b[static_cast<std::size_t>(i)] = u_bc; // solid held at u_bc
538 continue;
539 }
540 b[static_cast<std::size_t>(i)] =
541 src[static_cast<std::size_t>(i)] * rscale_[static_cast<std::size_t>(i)] +
542 inhom_[static_cast<std::size_t>(i)] * u_bc;
543 }
544 return b;
545 }
546
547 double residualGeometric(const std::vector<double>& u, const std::vector<double>& b,
548 std::vector<double>& res) const {
549 applyOpGeometric(u, res);
550 double s = 0.0;
551 const Index n = numLeaves();
552 for (Index i = 0; i < n; ++i) {
553 double r = b[static_cast<std::size_t>(i)] - res[static_cast<std::size_t>(i)];
554 res[static_cast<std::size_t>(i)] = r;
555 if (fluid_[static_cast<std::size_t>(i)])
556 s += r * r;
557 }
558 return std::sqrt(s);
559 }
560
561 void gaussSeidelGeometric(std::vector<double>& u, const std::vector<double>& b,
562 int sweeps) const {
563 const Index n = numLeaves();
564 for (int s = 0; s < sweeps; ++s)
565 for (Index i = 0; i < n; ++i) {
566 const std::size_t si = static_cast<std::size_t>(i);
567 if (!fluid_[si]) {
568 u[si] = b[si];
569 } else if (cut_[si]) { // ξ-overlay stencil (same-level neighbours)
570 double sum = b[si];
571 for (int k = 0; k < 6; ++k) {
572 double a = off_[si * 6 + k];
573 if (a == 0.0)
574 continue;
575 Index j = nb_[si * 6 + k];
576 if (j >= 0)
577 sum -= a * u[static_cast<std::size_t>(j)];
578 }
579 double d = AC_[si];
580 if (hasAdv_) {
581 sum -= advOffSum(i, u);
582 d += advDiag_[si];
583 }
584 if (d != 0.0)
585 u[si] = sum / d;
586 } else { // regular fluid: idiag·u − μ∇² with C/F-aware face coupling
587 double offsum = 0.0, dsum = 0.0;
588 lap_.forEachFaceNeighbor(i, [&](Index j, Real c, int, double) {
589 offsum += c * u[static_cast<std::size_t>(j)];
590 dsum += c;
591 });
592 double Vi = lap_.cellVolume(i);
593 double diagA = idiag_ + mu_ * dsum / Vi;
594 double sum = b[si] + mu_ * offsum / Vi;
595 if (hasAdv_) {
596 sum -= advOffSum(i, u);
597 diagA += advDiag_[si];
598 }
599 u[si] = sum / diagA;
600 }
601 }
602 }
603
604 public:
608 double fouApply(Index i, const std::vector<double>& field) const {
609 return advDiag_[static_cast<std::size_t>(i)] * field[static_cast<std::size_t>(i)] +
610 advOffSum(i, field);
611 }
612 bool hasAdvection() const { return hasAdv_; }
613
614 private:
615 // Σ_csr coef·field[nbr] — the implicit-FOU advection off-diagonal coupling.
616 double advOffSum(Index i, const std::vector<double>& field) const {
617 double s = 0.0;
618 for (std::size_t p = static_cast<std::size_t>(advStart_[static_cast<std::size_t>(i)]);
619 p < static_cast<std::size_t>(advStart_[static_cast<std::size_t>(i) + 1]); ++p)
620 s += advCoef_[p] * field[static_cast<std::size_t>(advNbr_[p])];
621 return s;
622 }
623 double advApply(Index i, const std::vector<double>& u) const { return fouApply(i, u); }
624
625 public:
626 // Port of ibmFillEntry<0> + ibmModifyStencil for one cut cell (Dirichlet). Public + MORTON_HD so
627 // the device assembler (momentum_assembly.hpp) runs the SAME per-cell stencil build on device.
630 MORTON_HD static void buildCutStencil(double sdf_c, const double sdf_n[6], const double beta[3],
631 double AC0, double& ACout, double off[6], double& rscaleOut,
632 double& inhomOut) {
633 bool ghost[6];
634 double xi[6], D[6];
635 for (int k = 0; k < 6; ++k) {
636 if (sdf_n[k] < 0.0) {
637 ghost[k] = true;
638 double th = sdf_c / (sdf_c - sdf_n[k]);
639 th = th < 1e-4 ? 1e-4 : (th > 1.0 ? 1.0 : th);
640 xi[k] = th;
641 D[k] = cc::poly_D(th);
642 } else {
643 ghost[k] = false;
644 xi[k] = 1.0;
645 D[k] = 1e9;
646 }
647 }
648 bool sand[3] = {ghost[0] && ghost[1], ghost[2] && ghost[3], ghost[4] && ghost[5]};
649 double Dsand[3] = {0, 0, 0};
650 for (int a = 0; a < 3; ++a)
651 if (sand[a])
652 Dsand[a] = cc::poly_D_sandwich(xi[2 * a + 1], xi[2 * a]);
653 double minAbs = 1e30, descale = 1.0;
654 auto upd = [&](double v) {
655 if (cc::poly_abs(v) < minAbs) {
657 descale = v;
658 }
659 };
660 for (int a = 0; a < 3; ++a) {
661 if (sand[a])
662 upd(Dsand[a]);
663 else {
664 if (ghost[2 * a])
665 upd(D[2 * a]);
666 if (ghost[2 * a + 1])
667 upd(D[2 * a + 1]);
668 }
669 }
670 double K[6] = {0}, Mf[6] = {1, 1, 1, 1, 1, 1}, X[6] = {0}, Nbc[6] = {0},
671 R[6] = {1, 1, 1, 1, 1, 1};
672 for (int a = 0; a < 3; ++a) {
673 int km = 2 * a + 1, kp = 2 * a;
674 double Daxis = sand[a] ? Dsand[a] : (ghost[kp] ? D[kp] : (ghost[km] ? D[km] : descale));
675 double r = descale / Daxis;
676 if (cc::poly_abs(Daxis) < 1e-9)
677 r = 1.0;
678 R[kp] = R[km] = r;
679 if (sand[a]) {
680 K[kp] = cc::poly_N_c_sandwich(xi[km], xi[kp]) * r;
681 K[km] = cc::poly_N_c_sandwich(xi[kp], xi[km]) * r;
684 Mf[kp] = Mf[km] = 0.0;
685 } else {
686 for (int side = 0; side < 2; ++side) {
687 int kk = side == 0 ? kp : km;
688 if (ghost[kk]) {
689 K[kk] = cc::poly_Nc(xi[kk]) * r;
690 X[kk] = cc::poly_N_nb(xi[kk]) * r;
691 Nbc[kk] = cc::poly_Nbc(xi[kk]) * r;
692 Mf[kk] = 0.0;
693 } else {
694 K[kk] = 0.0;
695 Mf[kk] = 1.0;
696 X[kk] = 0.0;
697 Nbc[kk] = 0.0;
698 }
699 }
700 }
701 }
702 // ibmModifyStencil (orig off-diagonal = -beta for every direction). OPP is a function-local
703 // constexpr (not the static member) so the runtime index mod[OPP[k]] is device-safe under CUDA.
704 constexpr int OPP_[6] = {1, 0, 3, 2, 5, 4};
705 double aC = AC0 * descale, mod[6] = {0, 0, 0, 0, 0, 0}, inhom = 0.0;
706 for (int k = 0; k < 6; ++k) {
707 double vnb = -beta[k / 2];
708 aC += vnb * K[k];
709 inhom += Nbc[k] * vnb;
710 mod[k] += vnb * (descale * Mf[k] - 1.0);
711 mod[OPP_[k]] += vnb * X[k];
712 }
713 ACout = aC;
714 for (int k = 0; k < 6; ++k)
715 off[k] = -beta[k / 2] + mod[k];
717 inhomOut = inhom;
718 }
719
720 private:
721 Vec<3> cellCenter(Index i) const {
722 auto b = t_->bounds(i);
723 double s = static_cast<double>(Index(1) << t_->level(i));
724 Vec<3> c{};
725 for (int d = 0; d < 3; ++d)
726 c[d] = origin_[d] +
727 (static_cast<double>(static_cast<long>(b[0][d]) + frameShift_[d]) + 0.5 * s) * h0_[d];
728 return c;
729 }
730
731 template <class SdfFn>
732 double volumeFraction(Index i, SdfFn&& sdfFn, int nsub) const {
733 auto b = t_->bounds(i);
734 double s = static_cast<double>(Index(1) << t_->level(i));
735 // Phase 3: the subsample grid spans the BOX leaf, so the step is per axis.
736 Vec<3> w{};
737 int inside = 0, total = nsub * nsub * nsub;
738 Vec<3> base{};
739 for (int d = 0; d < 3; ++d) {
740 w[d] = s * h0_[d];
741 base[d] =
742 origin_[d] + static_cast<double>(static_cast<long>(b[0][d]) + frameShift_[d]) * h0_[d];
743 }
744 for (int a = 0; a < nsub; ++a)
745 for (int bb = 0; bb < nsub; ++bb)
746 for (int cc2 = 0; cc2 < nsub; ++cc2) {
747 Vec<3> p{base[0] + (a + 0.5) / nsub * w[0], base[1] + (bb + 0.5) / nsub * w[1],
748 base[2] + (cc2 + 0.5) / nsub * w[2]};
749 if (sdfFn(p) > 0.0)
750 ++inside;
751 }
752 return static_cast<double>(inside) / total;
753 }
754
758 Index neighbor(Index i, int k) const {
759 return lap_.periodicNeighbor(i, k / 2, (k % 2 == 0) ? +1 : -1);
760 }
761
762 const Octree* t_ = nullptr;
763 Vec<3> h0_ = detail::filledVec<3>(1.0);
764 Vec<3> origin_{};
765 std::array<Coord, 3> fineExt_{};
766 ExtResolver extResolve_; // distributed seam (forwarded into lap_ by build)
767 std::vector<std::array<long, 3>> ghostLo_; // ghost slot → block-local lo (longs)
768 std::vector<unsigned> ghostLv_; // ghost slot → covering-leaf level
769 std::array<long, 3> frameShift_{}; // block global fine origin (0 single-rank)
770 std::vector<double> sdfC_, kappa_, AC_, rscale_, inhom_, off_;
771 std::vector<Index> nb_;
772 std::vector<char> fluid_, cut_;
773 AmrPoisson<3, Bits> lap_; // C/F-aware ∇² provider for regular fluid cells (α=1)
774 double idiag_ = 0.0, mu_ = 1.0;
775 std::vector<double> advDiag_, advCoef_; // implicit-FOU advection (rebuilt per step)
776 std::vector<Index> advStart_, advNbr_; // CSR off-diagonals (C/F-conservative)
777 bool hasAdv_ = false;
778};
779
780} // namespace peclet::amr
781
782#endif // PECLET_AMR_CUT_CELL_HPP
const std::vector< double > & advCoefRaw() const
Definition cut_cell.hpp:156
void gaussSeidel(std::vector< double > &u, const std::vector< double > &b, int sweeps) const
sweeps true serial Gauss–Seidel sweeps (ω=1, in place) over the assembled CSR using the shared point-...
Definition cut_cell.hpp:416
FaceCsrOpT< HostArr< double >, HostArr< Index > > hostOp(const Assembled &A) const
View a host Assembled as a backend-agnostic FaceCsrOpT for the shared row kernels.
Definition cut_cell.hpp:374
void applyOp(const std::vector< double > &u, std::vector< double > &out) const
out = A u, via the shared kernel over the assembled CSR (== device applyMom arithmetic).
Definition cut_cell.hpp:386
void build(SdfFn &&sdfFn, double idiag=0.0, const Vec< 3 > &beta=detail::filledVec< 3 >(1.0), int nsub=4)
Phase 3: beta[a] = mu/h0_a² (the octree twin of the structured fold's beta_b).
Definition cut_cell.hpp:169
const std::vector< double > & acRaw() const
Definition cut_cell.hpp:142
Index numLeaves() const
Definition cut_cell.hpp:119
double kappa(Index i) const
Definition cut_cell.hpp:127
void applyOpGeometric(const std::vector< double > &u, std::vector< double > &out) const
Geometric operator apply (walks the octree live) — the INDEPENDENT reference encoding,...
Definition cut_cell.hpp:436
void gaussSeidelGeometric(std::vector< double > &u, const std::vector< double > &b, int sweeps) const
Definition cut_cell.hpp:561
double fouApply(Index i, const std::vector< double > &field) const
The implicit-FOU advection operator applied to field at leaf i: advDiag·field_i + Σ_csr coef·field_nb...
Definition cut_cell.hpp:608
const std::vector< double > & advDiagRaw() const
Definition cut_cell.hpp:155
const std::vector< char > & fluidRaw() const
Definition cut_cell.hpp:140
void setFrameShift(const std::array< long, 3 > &s)
Definition cut_cell.hpp:117
const std::vector< double > & offRaw() const
n·6 ξ-overlay off-diagonals
Definition cut_cell.hpp:143
typename Octree::Code Code
Definition cut_cell.hpp:82
const std::vector< Index > & nbRaw() const
n·6 periodic face-neighbour indices
Definition cut_cell.hpp:139
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
Definition cut_cell.hpp:88
Assembled assembleOperator(bool scaleAdvByRscale=false) const
scaleAdvByRscale (default false ⇒ reproduces applyOp/gaussSeidel exactly, for the matvec test): when ...
Definition cut_cell.hpp:310
bool hasAdvection() const
Definition cut_cell.hpp:612
const std::vector< char > & cutRaw() const
Definition cut_cell.hpp:141
const std::vector< Index > & advNbrRaw() const
Definition cut_cell.hpp:158
double idiag() const
Definition cut_cell.hpp:145
void setGhosts(std::vector< std::array< long, 3 > > lo, std::vector< unsigned > lv)
Ghost slots [n, n+nGhost): block-local lo (longs) + covering-leaf level.
Definition cut_cell.hpp:112
const AmrPoisson< 3, Bits > & lap() const
α=1 C/F ∇² geometry for regular cells
Definition cut_cell.hpp:133
const std::vector< double > & sdfCRaw() const
per-cell SDF sample (build Pass 1)
Definition cut_cell.hpp:136
void build(SdfFn &&sdfFn, double idiag, double beta, int nsub=4)
Build the cut-cell stencils from an SDF callable sdfFn(worldPoint) (>0 fluid, <0 solid).
Definition cut_cell.hpp:164
void setResolver(ExtResolver r)
Definition cut_cell.hpp:108
static void buildCutStencil(double sdf_c, const double sdf_n[6], const double beta[3], double AC0, double &ACout, double off[6], double &rscaleOut, double &inhomOut)
Phase 3: beta[3] — the viscous coefficient of each AXIS; face k uses beta[k/2].
Definition cut_cell.hpp:630
BlockOctree< 3, Bits > Octree
Definition cut_cell.hpp:80
Index numGhosts() const
Definition cut_cell.hpp:116
const std::vector< Index > & advStartRaw() const
Definition cut_cell.hpp:157
double residualGeometric(const std::vector< double > &u, const std::vector< double > &b, std::vector< double > &res) const
Definition cut_cell.hpp:547
bool isCut(Index i) const
True for a cut cell: a fluid cell with at least one solid face neighbour (the row-scaled ξ-overlay ba...
Definition cut_cell.hpp:124
typename Octree::M M
Definition cut_cell.hpp:81
std::vector< double > makeRhs(const std::vector< double > &src, double u_bc) const
Effective RHS for source src (≈ -h^2 f at cell centres) and wall value u_bc: row-scaled by D_rescale ...
Definition cut_cell.hpp:532
static constexpr int OPP[6]
Definition cut_cell.hpp:86
bool isFluid(Index i) const
Definition cut_cell.hpp:120
double rhsScale(Index i) const
Definition cut_cell.hpp:128
Index neighborOf(Index i, int k) const
Periodic face neighbour of leaf i in direction k (0=+x,1=-x,2=+y,3=-y,4=+z,5=-z).
Definition cut_cell.hpp:126
double residual(const std::vector< double > &u, const std::vector< double > &b, std::vector< double > &res) const
Definition cut_cell.hpp:396
void init(const Octree &t, const Vec< 3 > &h0, Vec< 3 > origin=Vec< 3 >{})
Definition cut_cell.hpp:91
static constexpr int Dim
Definition cut_cell.hpp:79
Vec< 3 > beta() const
buildCutStencil's β PER AXIS (= mu_/h0_a²) — Phase 3, docs/amr_anisotropic.md §3/§4.
Definition cut_cell.hpp:148
typename AmrPoisson< 3, Bits >::ExtResolver ExtResolver
Definition cut_cell.hpp:107
const std::vector< double > & rscaleRaw() const
Definition cut_cell.hpp:144
void buildAdvectionFou(const std::array< std::vector< double >, 3 > &uadv, double rho, const std::vector< double > &uf, const std::vector< Index > &faceStart, bool useFace)
Build the implicit first-order-upwind advection operator from a (lagged) advecting velocity field uad...
Definition cut_cell.hpp:476
typename Octree::Coord Coord
Definition cut_cell.hpp:83
Cell-centered FV Poisson operator on one (periodic) block octree.
Definition poisson.hpp:43
void setFrameShift(const std::array< long, Dim > &s)
Distributed frame shift: this block's global fine origin.
Definition poisson.hpp:96
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:329
void forEachFaceNeighbor(Index i, Fn &&fn) const
Visit each face neighbour of leaf i: fn(neighbourSlot, coeff, axis, alpha) where coeff = A_f / d_f (p...
Definition poisson.hpp:274
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:375
void setGhosts(std::vector< std::array< long, Dim > > lo, std::vector< unsigned > lv)
Declare the ghost slots [n, n+nGhost): block-local lo corner (longs — ghosts lie outside the block) a...
Definition poisson.hpp:86
std::function< Index(const std::array< long, Dim > &)> ExtResolver
Route probes that exit the block through an external resolver (the LeafHalo registry): fn(blockLocalP...
Definition poisson.hpp:80
void init(const Octree &t, Real h0)
Cubic-cell overload — the pre-Phase-3 spelling, and exactly the per-axis one with all three spacings ...
Definition poisson.hpp:56
void applyLaplacian(const std::vector< double > &u, std::vector< double > &out) const
out = L u (periodic FV Laplacian).
Definition poisson.hpp:525
void setResolver(ExtResolver r)
Definition poisson.hpp:81
Real cellVolume(Index i) const
Definition poisson.hpp:262
Per-block adaptive octree over block-local Morton codes.
morton::Morton< Dim, Bits > M
unsigned level(Index i) const
std::array< std::array< Coord, Dim >, 2 > bounds(Index i) const
Inclusive integer bounds [lo, hi] of leaf i in fine units.
typename M::coord_type Coord
typename M::code_type Code
double poly_Nbc_mp_sw(double xm, double xp)
Definition cut_cell.hpp:71
double poly_Nbc(double xi)
Definition cut_cell.hpp:59
double poly_D_sandwich(double xm, double xp)
Definition cut_cell.hpp:62
double poly_Nbc_pp_sw(double xm, double xp)
Definition cut_cell.hpp:68
double poly_D(double xi)
Definition cut_cell.hpp:50
double poly_abs(double x)
Definition cut_cell.hpp:47
double poly_N_c_sandwich(double xm, double xp)
Definition cut_cell.hpp:65
double poly_Nc(double xi)
Definition cut_cell.hpp:56
double poly_N_nb(double xi)
Definition cut_cell.hpp:53
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
The assembled linear operator A as a per-cell diagonal + face CSR: (A u)_i = diag[i]·u_i + Σ_{k∈[star...
Definition cut_cell.hpp:295
std::vector< Index > nbr
neighbour leaf per off-diagonal, size nnz
Definition cut_cell.hpp:298
std::vector< double > coef
off-diagonal coefficient, size nnz
Definition cut_cell.hpp:299
std::vector< Index > start
CSR row offsets, size n+1.
Definition cut_cell.hpp:297
std::vector< double > diag
size n
Definition cut_cell.hpp:296