core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
flow_oracle.hpp
Go to the documentation of this file.
1// core — collocated incompressible Stokes step on a BlockOctree.
2//
3// Wires the two cut-cell IBM halves into one flow-style projection step:
4// * momentum — implicit (backward-Euler) viscous solve per component with the
5// Dirichlet ξ-polynomial cut-cell operator (AmrCutCell): no-slip
6// u = 0 on the immersed boundary. Operator (ρ/dt)I − μ∇².
7// * pressure — the **Almgren–Bell–Colella (ABC) approximate projection** in an
8// **incremental rotational** form (flow's collocated coupling,
9// src/mac_approx_projection.hpp). The predictor carries the old
10// pressure gradient −∇p^n; the openness-weighted (Neumann) Poisson
11// (AmrPoisson) solves ∇²φ = ∇·u*; the cell velocities are corrected
12// by ½(g⁻+g⁺) of the two adjacent FACE φ-gradients (closed/solid
13// face ⇒ zero gradient); and the pressure is updated **rotationally**
14// p += (ρ/dt)φ − μ∇·u*. The −μ∇·u* term removes the projection's
15// boundary-layer splitting error, so the steady drag is dt-INDEPENDENT
16// (plain non-incremental Chorin gives an O(dt) drag error — the
17// reason an earlier version missed Zick & Homsy; see docs/AMR.md).
18// The openness/aperture is flow's gradient-normalised ccFractionCore.
19//
20// This collocated coupling is a deliberate choice. Do NOT replace it with a
21// Rhie–Chow face-velocity interpolation: the small residual *cell* divergence is
22// intrinsic to cell-centered velocity placement (the face field is exactly
23// divergence-free), not a bug to be engineered away (see the amr-octree memory).
24//
25// Navier–Stokes (semi-implicit): implicit viscous diffusion + explicit high-order
26// advection ∇·(u u) (setAdvection(true)); the high-order flux is second-order
27// upwind (SOU) by default, or Koren TVD via setAdvectionScheme(1). setAdvection(false)
28// ⇒ Stokes. 3D. Cut cells and the
29// ±2-cell advection stencil assume same-level neighbours (resolve the boundary in a
30// uniformly-finest band, so the stencils never sit on a 2:1 interface — docs/AMR.md).
31// Header-only, guarded by PECLET_CORE_HAVE_MORTON. Serial/host first.
32#ifndef PECLET_CORE_AMR_FLOW_ORACLE_HPP
33#define PECLET_CORE_AMR_FLOW_ORACLE_HPP
34
35#ifdef PECLET_CORE_HAVE_MORTON
36
37#include <algorithm>
38#include <array>
39#include <cmath>
40#include <cstdio>
41#include <memory>
42#include <vector>
43
44#include "peclet/core/amr/adapt.hpp" // transferField (conservative remap for finishAdapt)
45#include "peclet/core/amr/advect_recon.hpp" // shared high-order face reconstruction (host+device)
47#include "peclet/core/amr/cf_scheme.hpp" // pluggable 2:1 C/F interface schemes (setCfScheme)
49#include "peclet/core/amr/ghost_projection.hpp" // directional ghost overlay (setGhostProjection)
50#include "peclet/core/amr/ghost_projection_sampled.hpp" // mixed-level sampled overlay (setGhostSampled)
53
54// Retired from the production path: this serial Gauss-Seidel host driver is kept ONLY as the
55// development-stage oracle that the device AmrFlow (flow_device.hpp) is validated bit-for-bit
56// against. It is NOT exposed by the Python bindings. Production AMR flow runs on the device path.
58
59template <unsigned Bits = 21u>
60class AmrFlow {
61 public:
63
64 void init(const Octree& t, Real h0, Vec<3> origin = Vec<3>{}) {
65 t_ = &t;
66 h0_ = h0;
67 origin_ = origin;
68 }
69
70 void setDensity(double rho) { rho_ = rho; }
71 void setViscosity(double mu) { mu_ = mu; }
72 void setDt(double dt) { dt_ = dt; }
73 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
74 void setAdvection(bool on) { advect_ = on; }
76 void setAdvectionScheme(int s) { advScheme_ = s; }
81 void setImplicitAdvection(bool on) { implicitFou_ = on; }
90 void setGhostGradient(bool on) { ghostGrad_ = on; }
104 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2) {
105 ghostProjReq_ = on ? 1 : 0;
106 gpMatrixOrder_ = matrixOrder;
107 gpRhsOrder_ = rhsOrder;
108 }
115 void setGhostSampled(bool on) { ghostSampledReq_ = on ? 1 : 0; }
123 void setCfScheme(int scheme) { cfScheme_ = static_cast<CfScheme>(scheme); }
124
127 double advectTerm(int comp, Index i) const { return advectHO(comp, i); }
128
130 template <class SdfFn>
132 mom_.init(*t_, h0_, origin_);
133 mom_.build(sdfFn, /*idiag=*/rho_ / dt_, /*beta=*/mu_ / (h0_ * h0_));
134 pres_.init(*t_, h0_);
135 pres_.setOrigin(origin_);
136 presMG_.build(*t_, h0_);
137 // Resolve the projection mode (mirrors the device AmrFlow). DEFAULT = AUTO: the ghost
138 // (fluid-only) projection, falling back to the aperture with a stderr notice on a
139 // band-margin violation; explicit setGhostProjection(true) keeps the hard throw.
140 ghostProj_ = (ghostProjReq_ != 0);
141 ghostSampled_ = ghostProj_ && (ghostSampledReq_ == 1);
142 if (ghostProj_ && !ghostSampled_) {
143 bool viol = false;
144 gpOv_ = buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
145 if (viol) {
146 if (ghostProjReq_ == 1)
147 throw std::runtime_error(
148 "amr ghost projection: an overlay row's ±2 closure reach crosses a 2:1 level "
149 "boundary — widen the refineToSdf band margin");
150 ghostProj_ = false;
152 "peclet::core AmrFlow(oracle): AUTO scheme fell back to the aperture projection "
153 "(the finest band is too thin for the ghost overlay).\n");
154 }
155 }
156 if (ghostSampled_) // mixed-level cut band: sample-slot overlay, no band-margin throw
157 gpOvS_ = buildGhostOverlaySampled(*t_, pres_, sdfFn, gpMatrixOrder_, gpRhsOrder_, origin_);
158 if (ghostProj_) {
159 // Ghost projection: the pressure geometry is the BINARY openness (a face is open iff both
160 // adjacent centers + the face sample are fluid), on the UNCHANGED MG rails; the closure
161 // physics lives in the overlay. World-coord sampling offset by the origin (the octree's
162 // fine units start at origin_): shift the probe like cellCenter does.
163 auto sdfW = [&](const Vec<3>& p) { return sdfFn(p); };
164 if (ghostSampled_) {
165 // Level-aware canonical openness (actual adjacent leaf centers) — the classification
166 // primitive the sampled overlay's face states are forced to (no double-counted flux).
167 auto binFn = makeBinaryOpenFnMixed(*t_, pres_, sdfW, h0_, origin_);
168 pres_.buildOpenness(binFn);
169 presMG_.setOpenness(binFn);
170 } else {
171 auto binFn = makeBinaryOpenFn(sdfW, h0_);
172 pres_.buildOpenness(binFn);
173 presMG_.setOpenness(binFn);
174 }
175 ghostGrad_ = true; // the directional gradient is part of the scheme
176 // Fragmentation guard (pockets decoupled) + coupled mask (1 = row in the Krylov space).
177 gpPocket_ = findPocketCells(*t_, pres_, mom_.sdfCRaw());
178 maskC_.assign(static_cast<std::size_t>(t_->numLeaves()), 0.0);
179 for (Index i = 0; i < t_->numLeaves(); ++i)
180 maskC_[static_cast<std::size_t>(i)] =
181 (mom_.isFluid(i) && !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(i)]))
182 ? 1.0
183 : 0.0;
184 const GhostOverlay& rows = ghostSampled_ ? gpOvS_.base : gpOv_;
185 for (Index r = 0; r < rows.n; ++r)
186 if (!rows.coupled[static_cast<std::size_t>(r)])
187 maskC_[static_cast<std::size_t>(rows.cell[static_cast<std::size_t>(r)])] = 0.0;
188 } else {
189 gpPocket_.clear();
190 pres_.buildOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
191 presMG_.setOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
192 }
193 // C/F interface scheme overlays (cf_scheme.hpp), built from the SAME host CSR builders the
194 // device uses (parity by construction). Rows: fluid; substitution stencils gated on fluid
195 // tangential neighbours. The momentum delta uses the α=1 velocity geometry (mom_.lap()),
196 // ×μ, on regular (non-cut) fluid rows only (cut rows are finest-band: no C/F faces).
197 if (cfScheme_ != CfScheme::standard) {
198 auto fluidOk = [&](Index j) { return mom_.isFluid(j); };
199 auto rowFluid = [&](Index i) { return mom_.isFluid(i); };
200 auto rowRegular = [&](Index i) { return mom_.isFluid(i) && !mom_.isCut(i); };
201 cfMom_ = buildCfLapDelta(mom_.lap(), *t_, mu_, rowRegular, fluidOk, cfScheme_);
202 cfDiv_ = buildCfDivDelta(pres_, *t_, rowFluid, fluidOk, cfScheme_);
203 cfGrad_ = buildCfGradDelta(pres_, *t_, rowFluid, fluidOk, cfScheme_);
204 cfUf_ = buildCfUfDelta(pres_, *t_, fluidOk, cfScheme_);
205 } else {
206 cfMom_ = CfCsr{};
207 cfDiv_ = CfCompCsr{};
208 cfGrad_ = {};
209 cfUf_ = CfUfDelta{};
210 }
211 const Index n = t_->numLeaves();
212 for (int c = 0; c < 3; ++c)
213 u_[c].assign(static_cast<std::size_t>(n), 0.0);
214 phi_.assign(static_cast<std::size_t>(n), 0.0);
215 p_.assign(static_cast<std::size_t>(n), 0.0);
216 // Face CSR offsets: count the (sub)faces forEachFaceFull emits per cell (6 interior, more for a
217 // cell facing finer neighbours), prefix-sum into faceStart_.
218 faceStart_.assign(static_cast<std::size_t>(n) + 1, 0);
219 for (Index i = 0; i < n; ++i) {
220 Index cnt = 0;
221 pres_.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++cnt; });
222 faceStart_[static_cast<std::size_t>(i) + 1] = faceStart_[static_cast<std::size_t>(i)] + cnt;
223 }
224 uf_.assign(static_cast<std::size_t>(faceStart_[static_cast<std::size_t>(n)]), 0.0);
225 faceFieldBuilt_ = false;
226 }
227
228 const std::vector<double>& velocity(int c) const { return u_[c]; }
229 Index numLeaves() const { return t_->numLeaves(); }
230 bool isFluid(Index i) const { return mom_.isFluid(i); }
231
232 // ---- adaptivity during a run (mirror of the device AmrFlow's beginAdapt/finishAdapt) ----------
233 void beginAdapt() {
234 adaptOldT_ = std::make_unique<Octree>(*t_);
235 adaptU_ = u_;
236 adaptP_ = p_;
237 }
238 template <class SdfFn>
240 if (!adaptOldT_)
241 throw std::runtime_error("oracle::AmrFlow::finishAdapt called without beginAdapt");
242 std::array<std::vector<double>, 3> nu;
243 for (int c = 0; c < 3; ++c)
244 nu[static_cast<std::size_t>(c)] =
245 transferField(*adaptOldT_, adaptU_[static_cast<std::size_t>(c)], *t_, /*linear=*/true);
246 std::vector<double> np = transferField(*adaptOldT_, adaptP_, *t_, /*linear=*/true);
248 for (Index i = 0; i < t_->numLeaves(); ++i) {
249 const bool fl = mom_.isFluid(i);
250 for (int c = 0; c < 3; ++c)
251 u_[static_cast<std::size_t>(c)][static_cast<std::size_t>(i)] =
252 fl ? nu[static_cast<std::size_t>(c)][static_cast<std::size_t>(i)] : 0.0;
253 p_[static_cast<std::size_t>(i)] = fl ? np[static_cast<std::size_t>(i)] : 0.0;
254 }
255 adaptOldT_.reset();
256 }
257
260 void step(int momSweeps = 200, int presIters = 60, int presSweeps = 4) {
261 const Index n = t_->numLeaves();
262 // Advection ∇·(u^n u^n_c), evaluated from u^n BEFORE the predictor mutates u_.
263 // High-order (SOU/TVD) always; first-order-upwind too for the implicit-FOU
264 // deferred correction (the explicit term is ρ·(HO − FOU); FOU is implicit in
265 // the momentum operator, rebuilt here from the lagged u^n).
266 std::array<std::vector<double>, 3> adv; // full explicit advection term (incl. ρ)
267 if (advect_) {
268 if (implicitFou_)
269 mom_.buildAdvectionFou(u_, rho_, uf_, faceStart_,
270 faceFieldBuilt_); // FOU advected by the div-free uf
271 for (int c = 0; c < 3; ++c) {
272 adv[c].assign(static_cast<std::size_t>(n), 0.0);
273 for (Index i = 0; i < n; ++i)
274 if (mom_.isFluid(i)) {
275 double term = rho_ * advectHO(c, i); // ρ·∇·(u u_c), high order
276 if (implicitFou_)
277 term -= mom_.fouApply(i, u_[c]); // − ρ·FOU (deferred correction)
278 adv[c][static_cast<std::size_t>(i)] = term;
279 }
280 }
281 }
282
283 // --- momentum predictor: implicit viscous (+ implicit FOU) + body force − advection ---
284 for (int c = 0; c < 3; ++c) {
285 // C/F-scheme deferred correction on the velocity diffusion: +μ(∇²_scheme − ∇²_std)(uⁿ_c),
286 // lagged like the SOU deferred correction ⇒ the steady operator carries the 2nd-order flux.
287 std::vector<double> cfm;
288 if (cfScheme_ != CfScheme::standard) {
289 cfm.assign(static_cast<std::size_t>(n), 0.0);
290 cfApplyHost(cfMom_, u_[c], cfm);
291 }
292 std::vector<double> src(static_cast<std::size_t>(n), 0.0);
293 for (Index i = 0; i < n; ++i)
294 if (mom_.isFluid(i)) {
295 // incremental predictor: include the old pressure gradient −∇p^n.
296 double s = (rho_ / dt_) * u_[c][static_cast<std::size_t>(i)] + f_[c] - gradP(p_, i, c);
297 if (advect_)
298 s -= adv[c][static_cast<std::size_t>(i)]; // HO (explicit) or HO−FOU (deferred)
299 if (!cfm.empty())
300 s += cfm[static_cast<std::size_t>(i)];
301 src[static_cast<std::size_t>(i)] = s;
302 }
303 std::vector<double> b = mom_.makeRhs(src, /*u_bc=*/0.0);
304 mom_.gaussSeidel(u_[c], b, momSweeps); // u_ now holds u*
305 }
307 }
308
311 void project(int presIters = 60, int presSweeps = 4) {
312 const Index n = t_->numLeaves();
313 std::vector<double> div(static_cast<std::size_t>(n), 0.0);
314 for (Index i = 0; i < n; ++i)
315 if (mom_.isFluid(i))
316 div[static_cast<std::size_t>(i)] = divergence(u_, i);
317 if (cfScheme_ != CfScheme::standard) // 2nd-order C/F face averages in the constraint
318 cfApplyCompHost(cfDiv_, u_, div);
319 if (ghostProj_) { // ghost-closed constraint: binary div (above, binary α) + closure overlay
320 if (ghostSampled_)
321 ghostDivergDeltaSampledHost(gpOvS_, u_, div);
322 else
323 ghostDivergDeltaHost(gpOv_, u_, div);
324 }
325
326 std::fill(phi_.begin(), phi_.end(), 0.0);
328 if (ghostProj_) {
329 // Nonsymmetric ghost operator rho·(L_bin + Delta): MG-preconditioned BiCGStab on the
330 // coupled subspace (decoupled rows pinned 0, coupled volume-weighted mean removed).
331 solveGhostBiCGStab(phi_, div, presIters);
332 } else {
333 // Standard (not quadratic) openness V-cycles: the operator L = div(α grad) is
334 // consistent with the FV divergence / ABC gradient above (D, G, L share the
335 // same face enumeration), so the collocated projection is stable across 2:1
336 // interfaces. (The quadratic C/F flux is for pure-Poisson accuracy, not here.)
337 for (int it = 0; it < presIters; ++it)
338 presMG_.vcycle(0, phi_, div);
339 }
340
341 // Build the divergence-free FACE field from u* (still in u_) + φ, before the cell correction.
343
344 for (int c = 0; c < 3; ++c)
345 for (Index i = 0; i < n; ++i)
346 if (mom_.isFluid(i))
347 u_[c][static_cast<std::size_t>(i)] -= gradP(phi_, i, c);
348
349 // Rotational incremental pressure update: p += (ρ/dt)φ − μ ∇·u* (div is ∇·u*).
350 // The −μ∇·u* rotational term removes the projection's boundary-layer splitting
351 // error, making the steady solution dt-independent (vs plain Chorin).
352 for (Index i = 0; i < n; ++i)
353 if (mom_.isFluid(i))
354 p_[static_cast<std::size_t>(i)] += (rho_ / dt_) * phi_[static_cast<std::size_t>(i)] -
355 mu_ * div[static_cast<std::size_t>(i)];
356 }
357
361 double divergence(const std::array<std::vector<double>, 3>& vel, Index i) const {
362 double d = 0.0;
363 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double alpha) {
364 double ui = vel[axis][static_cast<std::size_t>(i)];
365 double uj = vel[axis][static_cast<std::size_t>(j)]; // solid cells hold 0
366 d += alpha * area * dir * 0.5 * (ui + uj);
367 });
368 return d / pres_.cellVolume(i);
369 }
370
371 double divNormL2(const std::array<std::vector<double>, 3>& vel) const {
372 double s = 0.0;
373 const Index n = t_->numLeaves();
374 for (Index i = 0; i < n; ++i)
375 if (mom_.isFluid(i)) {
376 double d = divergence(vel, i);
377 s += d * d;
378 }
379 return std::sqrt(s);
380 }
381
382 std::array<std::vector<double>, 3>& velocityRef() { return u_; }
383
391 const Index n = t_->numLeaves();
392 for (Index i = 0; i < n; ++i) {
393 Index s = faceStart_[static_cast<std::size_t>(i)];
394 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double) {
395 const double uface =
396 0.5 * (u_[axis][static_cast<std::size_t>(i)] + u_[axis][static_cast<std::size_t>(j)]);
397 // +axis pressure gradient across the face: (φ₊−φ₋)/d, +side = dir>0 ? j : i.
398 const double gphi =
399 (dir > 0)
400 ? (phi_[static_cast<std::size_t>(j)] - phi_[static_cast<std::size_t>(i)]) / dist
401 : (phi_[static_cast<std::size_t>(i)] - phi_[static_cast<std::size_t>(j)]) / dist;
402 uf_[static_cast<std::size_t>(s++)] = uface - gphi;
403 });
404 }
405 // 2nd-order C/F face values (setCfScheme): distance-weighted average + coarse* substitution
406 // on the 2:1 sub-faces — the advecting flux matches the (quad) divergence constraint.
407 if (cfScheme_ != CfScheme::standard && !cfUf_.vel.start.empty()) {
408 cfApplyCompHost(cfUf_.vel, u_, uf_);
409 cfApplyHost(cfUf_.phi, phi_, uf_);
410 }
411 faceFieldBuilt_ = true;
412 }
413
417 double divNormFace() const {
418 double tot = 0.0;
419 const Index n = t_->numLeaves();
420 for (Index i = 0; i < n; ++i) {
421 if (!mom_.isFluid(i))
422 continue;
423 Index s = faceStart_[static_cast<std::size_t>(i)];
424 double d = 0.0;
425 pres_.forEachFaceFull(i, [&](Index, int, int dir, double area, double, double alpha) {
426 d += alpha * area * dir * uf_[static_cast<std::size_t>(s++)];
427 });
428 d /= pres_.cellVolume(i);
429 tot += d * d;
430 }
431 return std::sqrt(tot);
432 }
433
436 const std::vector<double>& faceField() const { return uf_; }
437 const std::vector<Index>& faceStart() const { return faceStart_; }
438
439 private:
440 // ---- Koren TVD advection (faithful port of flow sadv::koren/tvd + cadv::advect) ----
441 static double koren(double up_m1, double up, double down, double vel) {
442 const double num = up - up_m1, den = down - up;
443 double r = (std::fabs(den) < 1e-10) ? 0.0 : num / den;
444 double psi = std::fmax(0.0, std::fmin(2.0 * r, std::fmin((1.0 + 2.0 * r) / 3.0, 2.0)));
445 return vel * (up + 0.5 * psi * (down - up));
446 }
447 static double tvd(double LL, double L, double R, double RR, double vel) {
448 return (vel > 0.0) ? koren(LL, L, R, vel) : koren(RR, R, L, vel);
449 }
450 // High-order face value: the SHARED reconstruction (advect_recon.hpp) the device runs too.
451 double hoFace(double upup, double up, double down) const {
452 return hoFaceValue(upup, up, down, advScheme_);
453 }
454
455 // C/F-consistent high-order advection ∇·(u u_comp) at leaf i: per (sub)face (via
456 // forEachFaceFull, so a coarse cell sums its fine sub-faces — conservative), the
457 // outward advective flux α-free·area·velOut·φ_face, with φ_face the SOU/TVD
458 // reconstruction from the upwind cell + its upstream neighbour (point-probed, so it
459 // works across 2:1 levels). No advection through wall faces. The implicit-FOU half
460 // is mom_.fouApply (same faces/velocities), so the deferred correction is the
461 // exact high-order − FOU difference.
462 double advectHO(int comp, Index i) const {
463 double out = 0.0;
464 Index s = faceFieldBuilt_ ? faceStart_[static_cast<std::size_t>(i)] : 0;
465 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double) {
466 // Advecting velocity = the divergence-free FACE field uf (built by the previous projection),
467 // so the advective flux is conservative (∇·uf = 0) — Bell–Colella–Glaz / Basilisk. Falls back
468 // to the simple cell average ½(u_i+u_j) before the first projection has built uf. (At steady
469 // state φ→0 ⇒ uf = ½(u_i+u_j), so the steady solution is unchanged; the gain is in the
470 // transient.)
471 const double uface = faceFieldBuilt_ ? uf_[static_cast<std::size_t>(s)]
472 : 0.5 * (u_[axis][static_cast<std::size_t>(i)] +
473 u_[axis][static_cast<std::size_t>(j)]);
474 ++s;
475 if (!mom_.isFluid(j))
476 return; // no advection through the immersed boundary
477 double velOut = dir * uface;
478 Index up = (velOut > 0.0) ? i : j, down = (velOut > 0.0) ? j : i;
479 int upDir = (up == i) ? -dir : dir; // upstream direction from the upwind cell
480 Index upup = pres_.periodicNeighbor(up, axis, upDir);
481 double phiUp = u_[comp][static_cast<std::size_t>(up)];
482 double phiUpUp =
483 (upup >= 0 && mom_.isFluid(upup)) ? u_[comp][static_cast<std::size_t>(upup)] : phiUp;
484 double phiFace = hoFace(phiUpUp, phiUp, u_[comp][static_cast<std::size_t>(down)]);
485 out += area * velOut * phiFace;
486 });
487 return out / pres_.cellVolume(i);
488 }
489
490 // Project onto the coupled subspace: pin decoupled rows (solid-centered + overlay rows with no
491 // phi coupling) to 0, remove the volume-weighted mean over the coupled cells (the constant null
492 // mode of the connected fluid region — computed over coupled cells ONLY, like maskSolid).
493 void gpProject(std::vector<double>& v) const {
494 const Index n = t_->numLeaves();
495 double su = 0.0, sv = 0.0;
496 for (Index i = 0; i < n; ++i) {
497 const std::size_t s = static_cast<std::size_t>(i);
498 v[s] *= maskC_[s];
499 if (maskC_[s] > 0.0) {
500 const double V = pres_.cellVolume(i);
501 su += V * v[s];
502 sv += V;
503 }
504 }
505 const double m = (sv > 0.0) ? su / sv : 0.0;
506 for (Index i = 0; i < n; ++i)
507 v[static_cast<std::size_t>(i)] -= maskC_[static_cast<std::size_t>(i)] * m;
508 }
509
510 // MG-preconditioned BiCGStab on the nonsymmetric ghost pressure operator
511 // A(x) = P[rho·(L_bin x + Delta x)] with P the coupled-subspace projection; preconditioner =
512 // two binary-openness V-cycles + P. Stagnation guard: stop when the residual makes no new best
513 // for 6 iterations (the ghost system has a small attainable-residual floor — flow's measured
514 // compatibility gap).
515 void solveGhostBiCGStab(std::vector<double>& x, const std::vector<double>& b, int maxIters,
516 double tol = 1e-10) {
517 const Index n = t_->numLeaves();
518 const std::size_t ns = static_cast<std::size_t>(n);
519 auto applyA = [&](const std::vector<double>& v, std::vector<double>& y) {
520 pres_.applyLaplacian(v, y); // binary-openness L (ghost mode geometry)
521 if (ghostSampled_)
523 else
524 ghostApplyDeltaHost(gpOv_, v, y);
525 gpProject(y);
526 };
527 auto prec = [&](const std::vector<double>& r, std::vector<double>& z) {
528 std::fill(z.begin(), z.end(), 0.0);
529 presMG_.vcycle(0, z, r);
530 presMG_.vcycle(0, z, r);
531 gpProject(z);
532 };
533 auto dot = [&](const std::vector<double>& a, const std::vector<double>& c) {
534 double s = 0.0;
535 for (std::size_t i = 0; i < ns; ++i)
536 s += a[i] * c[i];
537 return s;
538 };
539 std::vector<double> r(ns), rhat(ns), p(ns, 0.0), phat(ns), v(ns, 0.0), s(ns), shat(ns), t(ns);
540 applyA(x, r);
541 for (std::size_t i = 0; i < ns; ++i)
542 r[i] = b[i] - r[i];
543 gpProject(r);
544 rhat = r;
545 const double res0 = std::sqrt(dot(r, r));
546 if (res0 == 0.0)
547 return;
548 double rho = 1, alpha = 1, omega = 1, best = res0;
549 int noImprove = 0;
550 for (int it = 0; it < maxIters; ++it) {
551 const double rhoNew = dot(rhat, r);
552 if (rhoNew == 0.0)
553 break;
554 const double beta = (rhoNew / rho) * (alpha / omega);
555 for (std::size_t i = 0; i < ns; ++i)
556 p[i] = r[i] + beta * (p[i] - omega * v[i]);
557 prec(p, phat);
558 applyA(phat, v);
559 const double rhatV = dot(rhat, v);
560 if (rhatV == 0.0)
561 break;
562 alpha = rhoNew / rhatV;
563 for (std::size_t i = 0; i < ns; ++i)
564 s[i] = r[i] - alpha * v[i];
565 double snorm = std::sqrt(dot(s, s));
566 if (snorm <= tol * res0) {
567 for (std::size_t i = 0; i < ns; ++i)
568 x[i] += alpha * phat[i];
569 break;
570 }
571 prec(s, shat);
572 applyA(shat, t);
573 const double tt = dot(t, t);
574 omega = (tt != 0.0) ? dot(t, s) / tt : 0.0;
575 for (std::size_t i = 0; i < ns; ++i) {
576 x[i] += alpha * phat[i] + omega * shat[i];
577 r[i] = s[i] - omega * t[i];
578 }
579 const double rnorm = std::sqrt(dot(r, r));
580 if (rnorm <= tol * res0)
581 break;
582 if (rnorm < 0.999 * best) {
583 best = rnorm;
584 noImprove = 0;
585 } else if (++noImprove >= 6) {
586 break; // attainable-residual floor (compatibility gap) — stagnation guard
587 }
588 rho = rhoNew;
589 if (omega == 0.0)
590 break;
591 }
592 gpProject(x);
593 }
594
595 // Predictor/correction cell gradient dispatch: the ABC gradOf everywhere, EXCEPT — with
596 // setGhostGradient — on cut cells (fluid with a solid face neighbour), where gradOf reads the
597 // decoupled solid p through partially-open faces (gauge-dependent O(1/h), measured in
598 // tests/study_amr_ghost_apriori.cpp) and the directional ghost gradient below is used instead.
599 double gradP(const std::vector<double>& fld, Index i, int c) const {
600 if (ghostGrad_ && mom_.isCut(i)) {
601 if (ghostSampled_) {
602 // Mixed-level cut band: the row's sample functionals feed the gradient too (pairing —
603 // constraint and gradient from the same closures). Cut cells without a row (clean per
604 // the overlay classification) fall through to the level-aware gradOfDir cascade.
605 const Index r = gpOvS_.rowOf[static_cast<std::size_t>(i)];
606 if (r >= 0)
607 return gpsDirGrad(gpOvS_, r, fld, c, 1.0 / pres_.cellWidth(i));
608 }
609 return gradOfDir(fld, i, c); // cut band: no C/F faces (band contract) ⇒ no cf delta
610 }
611 double g = gradOf(fld, i, c);
612 // 2nd-order C/F face gradients (level-boundary rows); empty unless built by setSolid.
613 if (cfScheme_ != CfScheme::standard && !cfGrad_[static_cast<std::size_t>(c)].start.empty()) {
614 const CfCsr& cs = cfGrad_[static_cast<std::size_t>(c)];
615 for (Index k = cs.start[static_cast<std::size_t>(i)];
616 k < cs.start[static_cast<std::size_t>(i) + 1]; ++k)
617 g += cs.coef[static_cast<std::size_t>(k)] *
618 fld[static_cast<std::size_t>(cs.slot[static_cast<std::size_t>(k)])];
619 }
620 return g;
621 }
622
623 // Directional ghost cell-gradient on a cut-band cell (flow's gpCenterGrad analog). Cut cells
624 // have same-level face neighbours by the finest-band contract, so plain FD applies: central
625 // where both axis-neighbour CENTERS are fluid; 2nd-order one-sided toward the fluid else
626 // ((−3f_i+4f_{+1}−f_{+2})/2h, with a 2-point fallback when the ±2 cell is solid or not
627 // same-level); 0 when sandwiched. Never reads a solid-centered (decoupled) value — O(h²) and
628 // exactly gauge-invariant.
629 double gradOfDir(const std::vector<double>& fld, Index i, int c) const {
630 const double h = pres_.cellWidth(i);
631 auto F = [&](Index j) { return fld[static_cast<std::size_t>(j)]; };
632 // Pocket cells (fragmentation guard) count as solid: their φ is pinned/decoupled.
633 auto ok = [&](Index j) {
634 return j >= 0 && mom_.isFluid(j) && t_->level(j) == t_->level(i) &&
635 !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(j)]);
636 };
637 const Index jp = pres_.periodicNeighbor(i, c, +1);
638 const Index jm = pres_.periodicNeighbor(i, c, -1);
639 const bool ap = ok(jp), am = ok(jm);
640 if (am && ap)
641 return (F(jp) - F(jm)) / (2.0 * h);
642 if (ap) {
643 const Index jpp = pres_.periodicNeighbor(jp, c, +1);
644 return ok(jpp) ? (-3.0 * F(i) + 4.0 * F(jp) - F(jpp)) / (2.0 * h) : (F(jp) - F(i)) / h;
645 }
646 if (am) {
647 const Index jmm = pres_.periodicNeighbor(jm, c, -1);
648 return ok(jmm) ? (3.0 * F(i) - 4.0 * F(jm) + F(jmm)) / (2.0 * h) : (F(i) - F(jm)) / h;
649 }
650 return 0.0;
651 }
652
653 // ABC (Almgren-Bell-Colella) cell-velocity correction gradient in direction `c`:
654 // ½·(g⁻ + g⁺) of the two adjacent FACE pressure-gradients, where a CLOSED face
655 // (openness 0 — solid neighbour) contributes a ZERO gradient (it does NOT read
656 // the solid neighbour's φ). Verbatim form of flow's projectCorrectCenter
657 // (src/mac_approx_projection.hpp) — the collocated approximate projection. This
658 // is the chosen collocated coupling; do NOT substitute a Rhie–Chow face-velocity
659 // interpolation (see docs/AMR.md and the amr-octree memory).
660 double gradOf(const std::vector<double>& fld, Index i, int c) const {
661 const double pi = fld[static_cast<std::size_t>(i)];
662 double gp = 0, gm = 0;
663 int np = 0, nm = 0;
664 // Average the +axis-oriented face gradient (φ_j−φ_i)/dist over each side's
665 // (sub)faces along axis c; a closed face contributes 0 (ABC). C/F-consistent.
666 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double alpha) {
667 if (axis != c || alpha <= 1e-12)
668 return;
669 double g = (dir > 0) ? (fld[static_cast<std::size_t>(j)] - pi) / dist
670 : (pi - fld[static_cast<std::size_t>(j)]) / dist;
671 if (dir > 0) {
672 gp += g;
673 ++np;
674 } else {
675 gm += g;
676 ++nm;
677 }
678 });
679 double gpa = np ? gp / np : 0.0, gma = nm ? gm / nm : 0.0;
680 return 0.5 * (gpa + gma);
681 }
682
683 // Fluid area fraction of a face, the gradient-normalised aperture (a faithful
684 // port of flow's ccFractionCore, src/mac_cutcell.hpp): frac = 0.5 + sd/denom,
685 // sd = SDF at the face centre, denom = (|n_t1| + |n_t2|)·h0 over the two
686 // tangential axes (n = unit SDF gradient). This is a linear interface
687 // reconstruction within the face — 2nd-order accurate, unlike indicator
688 // subsampling (which is only O(1/nsub) on cut faces and made the drag 1st-order).
689 template <class SdfFn>
690 double faceFrac(SdfFn&& sdfFn, const Vec<3>& fc, int axis) const {
691 double sd = sdfFn(fc);
692 if (sd <= 0.0)
693 return 0.0;
694 Vec<3> g{};
695 for (int d = 0; d < 3; ++d) {
696 Vec<3> pp = fc, pm = fc;
697 pp[d] += h0_;
698 pm[d] -= h0_;
699 g[d] = (sdfFn(pp) - sdfFn(pm)) / (2.0 * h0_);
700 }
701 double gmag = std::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
702 if (gmag < 1e-6)
703 gmag = 1e-6;
704 int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
705 double denom = (std::fabs(g[t1]) + std::fabs(g[t2])) / gmag * h0_;
706 if (denom < 1e-9)
707 denom = 1e-9;
708 double frac = 0.5 + sd / denom;
709 return frac < 0.0 ? 0.0 : (frac > 1.0 ? 1.0 : frac);
710 }
711
712 const Octree* t_ = nullptr;
713 Real h0_ = 1.0;
714 Vec<3> origin_{};
715 double rho_ = 1.0, mu_ = 1.0, dt_ = 1e6;
716 bool advect_ = false;
717 bool ghostGrad_ = true; // directional ghost gradient on cut cells (setGhostGradient)
718 bool ghostProj_ = false; // RESOLVED projection mode (set by setSolid from the request)
719 int8_t ghostProjReq_ = -1; // -1 = AUTO (DEFAULT: ghost, aperture fallback on thin band),
720 // 0 = explicit aperture, 1 = explicit ghost
721 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure orders (2,2 = the production pair; the
722 // (1,2) mixed form is march-unstable at scale)
723 GhostOverlay gpOv_; // closure overlay (finest-band rows)
724 bool ghostSampled_ = false; // RESOLVED sampled mode (set by setSolid)
725 int8_t ghostSampledReq_ = 0; // setGhostSampled request (mixed-level prototype)
726 GhostOverlaySampled gpOvS_; // sample-slot overlay (mixed-level cut band)
727 std::vector<double> maskC_; // 1 = coupled row (Krylov subspace), 0 = pinned
728 std::vector<char> gpPocket_; // fragmentation guard: 1 = decoupled pocket cell
729 CfScheme cfScheme_ = CfScheme::standard; // 2:1 C/F interface scheme (setCfScheme)
730 CfCsr cfMom_; // +μ(∇²_scheme − ∇²_std) momentum RHS overlay
731 CfCompCsr cfDiv_; // (D_scheme − D_std) divergence overlay
732 std::array<CfCsr, 3> cfGrad_; // (G_scheme − G_std) per gradient axis
733 CfUfDelta cfUf_; // (uf_scheme − uf_std) face-field overlay (slots)
734 bool implicitFou_ = true; // implicit-FOU deferred-correction advection (stable)
735 int advScheme_ = 0; // 0 = SOU (default), 1 = Koren TVD
736 Vec<3> f_{};
737 AmrCutCell<Bits> mom_;
738 AmrPoisson<3, Bits> pres_; // openness + divergence/gradient access
739 AmrMultigrid<3, Bits> presMG_; // fast (graded-capable) pressure solve
740 std::array<std::vector<double>, 3> u_;
741 std::vector<double> phi_; // pressure-increment potential (per projection)
742 std::vector<double> p_; // accumulated pressure (rotational incremental scheme)
743 // Basilisk/ABC divergence-free FACE field: uf_[faceStart_[i]+s] is the +axis velocity through
744 // cell i's s-th (sub)face, in forEachFaceFull order. Each internal (sub)face is stored from BOTH
745 // incident cells; the orientation-based build keeps the two copies identical. At a 2:1 interface
746 // a coarse cell owns 2^(Dim-1) fine sub-faces and the fine cell owns its single face — so the
747 // face field is at the finest resolution touching each face and the coarse-cell divergence sums
748 // its sub-faces.
749 std::vector<Index> faceStart_; // CSR offsets into uf_, size n+1
750 std::vector<double> uf_; // +axis face velocity per (cell,face) slot
751 std::unique_ptr<Octree> adaptOldT_; // beginAdapt topology snapshot
752 std::array<std::vector<double>, 3> adaptU_; // beginAdapt field snapshots
753 std::vector<double> adaptP_;
754 bool faceFieldBuilt_ =
755 false; // uf_ populated by a projection (else advection falls back to ½(u_i+u_j))
756};
757
758} // namespace peclet::core::amr::oracle
759
760#endif // PECLET_CORE_HAVE_MORTON
761#endif // PECLET_CORE_AMR_FLOW_ORACLE_HPP
Real cellWidth(Index i) const
Definition poisson.hpp:245
void init(const Octree &t, Real h0)
Definition poisson.hpp:54
void setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:67
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:358
void applyLaplacian(const std::vector< double > &u, std::vector< double > &out) const
out = L u (periodic FV Laplacian).
Definition poisson.hpp:506
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:313
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:164
Real cellVolume(Index i) const
Definition poisson.hpp:246
Per-block adaptive octree over block-local Morton codes.
unsigned level(Index i) const
void setGhostGradient(bool on)
Directional ghost cell-gradient for the −∇pⁿ predictor and the projection's cell correction (the AMR ...
void step(int momSweeps=200, int presIters=60, int presSweeps=4)
One Stokes projection step.
const std::vector< double > & velocity(int c) const
void setGhostSampled(bool on)
SAMPLED ghost projection: the mixed-level cut-band prototype (the D1 machinery of docs/amr_mixed_leve...
const std::vector< Index > & faceStart() const
void project(int presIters=60, int presSweeps=4)
Pressure projection of the current velocity in place: solve ∇²φ = ∇·u and correct u -= ∇φ.
std::array< std::vector< double >, 3 > & velocityRef()
void setCfScheme(int scheme)
Coarse/fine (2:1) interface scheme (cf_scheme.hpp): 0 = standard two-point flux (default,...
void setImplicitAdvection(bool on)
Implicit-FOU deferred-correction advection (default ON): the first-order-upwind part is solved implic...
const std::vector< double > & faceField() const
The divergence-free face field (read-only): uf[faceStart()[i]+s] for cell i's s-th forEachFaceFull fa...
double advectTerm(int comp, Index i) const
Conservative Koren-TVD advection term ∇·(u u_comp) at leaf i (physical units; the explicit momentum a...
void buildFaceField()
Build the ABC/Basilisk divergence-free FACE field from the current cell velocity u* and the projectio...
void setAdvectionScheme(int s)
High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
FULL directional ghost-cell projection (the AMR port of flow's collocated set_ghost_projection): the ...
double divergence(const std::array< std::vector< double >, 3 > &vel, Index i) const
Openness-weighted FV divergence at leaf i, C/F-consistent: sum over (sub)faces of α·area·(outward fac...
void setSolid(SdfFn &&sdfFn)
Build the cut-cell operators from an SDF callable sdfFn(worldPoint) (>0 fluid).
double divNormFace() const
L2 norm of the divergence of the FACE field uf_ (the div-free flux).
double divNormL2(const std::array< std::vector< double >, 3 > &vel) const
BlockOctree< 3, Bits > Octree
void setBodyForce(double fx, double fy, double fz)
auto makeBinaryOpenFnMixed(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn sdfFn, double h0, Vec< 3 > origin)
The canonical (sub)face openness for mixed-level cut bands: open iff both adjacent ACTUAL leaf center...
GhostOverlay buildGhostOverlay(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const std::vector< double > &sdfC, int matrixOrder, int rhsOrder, bool *bandViolation=nullptr)
Build the overlay from the octree + the cell-centered SDF samples (AmrCutCell::sdfCRaw — EXTENDED ove...
void cfApplyCompHost(const CfCompCsr &c, const std::array< std::vector< double >, 3 > &u, std::vector< double > &out)
out(i) += Σ coef·u[comp](slot) (the divergence overlay).
CfUfDelta buildCfUfDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, FluidFn &&fluidOk, CfScheme scheme)
void ghostDivergDeltaSampledHost(const GhostOverlaySampled &ov, const std::array< std::vector< double >, 3 > &u, std::vector< double > &d)
Divergence overlay (sampled): the sampled analog of ghostDivergDeltaHost (u_bc = 0).
CfScheme
Coarse/fine interface scheme for the collocated flow operators.
Definition cf_scheme.hpp:55
@ standard
raw coarse value (two-point flux; 1st-order at 2:1 faces)
void ghostApplyDeltaSampledHost(const GhostOverlaySampled &ov, const std::vector< double > &x, std::vector< double > &y)
Matrix overlay (sampled): y currently holds the BINARY L matvec; overwrite overlay rows with y = rho·...
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:53
std::array< CfCsr, 3 > buildCfGradDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(G_scheme − G_std) for the ABC cell gradient gradOf/grad3.
void cfApplyHost(const CfCsr &c, const std::vector< double > &f, std::vector< double > &out)
out(i) += Σ coef·f(slot) (the scalar overlay: momentum ∇² delta, gradient delta per axis).
std::vector< char > findPocketCells(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const std::vector< double > &sdfC)
Fragmentation guard (the AMR port of flow's host-BFS pocket guard): the BINARY coupled-face graph (a ...
void ghostDivergDeltaHost(const GhostOverlay &ov, const std::array< std::vector< double >, 3 > &u, std::vector< double > &d)
Divergence overlay: d currently holds the BINARY divergence (physical); overwrite overlay rows with d...
CfCsr buildCfLapDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, double factor, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(∇²_scheme − ∇²_std) as a scalar CSR, ×factor (pass μ for the momentum deferred-correction RHS,...
void ghostApplyDeltaHost(const GhostOverlay &ov, const std::vector< double > &x, std::vector< double > &y)
Matrix overlay: y currently holds the BINARY L matvec; overwrite the overlay rows with y = rho·(y + i...
CfCompCsr buildCfDivDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(D_scheme − D_std) for the face-average divergence div_i = invV·Σ α·A·dir·(face value).
GhostOverlaySampled buildGhostOverlaySampled(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn &&sdf, int matrixOrder, int rhsOrder, Vec< 3 > origin=Vec< 3 >{})
Build the sampled overlay.
double gpsDirGrad(const GhostOverlaySampled &ov, Index r, const std::vector< double > &fld, int c, double invh)
Directional ghost cell-gradient on a sampled row (the mixed-level gradOfDir): the same cascade — cent...
std::vector< double > transferField(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF, const BlockOctree< Dim, Bits > &newT, bool linear=true, const std::type_identity_t< std::vector< std::array< double, Dim > > > *gradIn=nullptr)
Conservative remap of a leaf field from oldT to newT (same domain).
Definition adapt.hpp:93
MORTON_HD double hoFaceValue(double upup, double up, double down, int scheme)
High-order advected face value from the two upwind cells (upup, up) and the downwind cell (down).
auto makeBinaryOpenFn(SdfFn sdfFn, double h0)
Binary openness callable factory for the MG surrogate: a face is open iff both adjacent centers (prob...
std::array< Real, Dim > Vec
Multi-dimensional real vector.
Definition types.hpp:26
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
Component-tagged CSR overlay (the divergence delta): out(i) += Σ_k coef·u[comp](slot).
Definition cf_scheme.hpp:68
std::vector< Index > start
Definition cf_scheme.hpp:69
Scalar-input CSR overlay: out(i) += Σ_k coef·f(slot) over rows with C/F faces.
Definition cf_scheme.hpp:61
(uf_scheme − uf_std) for the div-free FACE field uf_k = ½(u_i+u_j) − (φ₊−φ₋)/d, one delta row per for...
CfCsr phi
reads the projection potential φ, rows = face slots
CfCompCsr vel
reads the velocity components, rows = face slots
std::vector< Index > rowOf
[numLeaves] row index of a leaf, -1 if none
Host ghost-projection overlay: one row per non-clean fluid leaf (== cut cell: some ±1 center sample s...