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
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#ifndef PECLET_AMR_FLOW_ORACLE_HPP
32#define PECLET_AMR_FLOW_ORACLE_HPP
33
34#include <algorithm>
35#include <array>
36#include <cmath>
37#include <cstdio>
38#include <memory>
39#include <stdexcept>
40#include <utility>
41#include <vector>
42
43#include "peclet/amr/adapt.hpp" // transferField (conservative remap for finishAdapt)
44#include "peclet/amr/advect_recon.hpp" // shared high-order face reconstruction (host+device)
46#include "peclet/amr/cf_scheme.hpp" // pluggable 2:1 C/F interface schemes (setCfScheme)
47#include "peclet/amr/common.hpp"
49#include "peclet/amr/ghost_projection.hpp" // directional ghost overlay (setGhostProjection)
50#include "peclet/amr/ghost_projection_sampled.hpp" // mixed-level sampled overlay (setGhostSampled)
52#include "peclet/core/common/types.hpp"
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 init(t, detail::filledVec<3>(h0), origin);
66 }
67 void init(const Octree& t, const Vec<3>& h0, Vec<3> origin = Vec<3>{}) {
68 t_ = &t;
69 h0_ = h0;
70 origin_ = origin;
71 }
72
73 void setDensity(double rho) { rho_ = rho; }
74 void setViscosity(double mu) { mu_ = mu; }
75 void setDt(double dt) { dt_ = dt; }
76 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
77 void setAdvection(bool on) { advect_ = on; }
79 void setAdvectionScheme(int s) { advScheme_ = s; }
84 void setImplicitAdvection(bool on) { implicitFou_ = on; }
93 void setGhostGradient(bool on) { ghostGrad_ = on; }
109 void setApertureOrder(int order) { apertureOrder_ = order; }
110 void setGhostProjection(bool on, int matrixOrder = 2, int rhsOrder = 2) {
111 ghostProjReq_ = on ? 1 : 0;
112 gpMatrixOrder_ = matrixOrder;
113 gpRhsOrder_ = rhsOrder;
114 }
121 void setGhostSampled(bool on) { ghostSampledReq_ = on ? 1 : 0; }
129 void setCfScheme(int scheme) { cfScheme_ = static_cast<CfScheme>(scheme); }
130
133 double advectTerm(int comp, Index i) const { return advectHO(comp, i); }
134
136 template <class SdfFn>
138 mom_.init(*t_, h0_, origin_);
139 Vec<3> betaA{};
140 for (int d = 0; d < 3; ++d)
141 betaA[d] = mu_ / (h0_[d] * h0_[d]);
142 mom_.build(sdfFn, /*idiag=*/rho_ / dt_, /*beta=*/betaA);
143 pres_.init(*t_, h0_);
144 pres_.setOrigin(origin_);
145 presMG_.build(*t_, h0_);
146 // Resolve the projection mode (mirrors the device AmrFlow). DEFAULT = AUTO: the ghost
147 // (fluid-only) projection, falling back to the aperture with a stderr notice on a
148 // band-margin violation; explicit setGhostProjection(true) keeps the hard throw.
149 ghostProj_ = (ghostProjReq_ != 0);
150 ghostSampled_ = ghostProj_ && (ghostSampledReq_ == 1);
151 if (ghostProj_ && !ghostSampled_) {
152 bool viol = false;
153 gpOv_ = buildGhostOverlay(*t_, pres_, mom_.sdfCRaw(), gpMatrixOrder_, gpRhsOrder_, &viol);
154 if (viol) {
155 if (ghostProjReq_ == 1)
156 throw std::runtime_error(
157 "amr ghost projection: an overlay row's ±2 closure reach crosses a 2:1 level "
158 "boundary — widen the refineToSdf band margin");
159 ghostProj_ = false;
161 "peclet::core AmrFlow(oracle): AUTO scheme fell back to the aperture projection "
162 "(the finest band is too thin for the ghost overlay).\n");
163 }
164 }
165 if (ghostSampled_) { // mixed-level cut band: sample-slot overlay, no band-margin throw
166 gpOvS_ = buildGhostOverlaySampled(*t_, pres_, sdfFn, gpMatrixOrder_, gpRhsOrder_, origin_);
167 // Momentum ξ-row seam correction (plan §6.2): lagged deferred-correction CSR replacing
168 // the position-inconsistent (and wrong-β) raw ξ rows by the row-local virtual stencil.
169 long momSkipped = 0;
170 gpsMomDelta_ =
171 buildMomSeamDelta(gpOvS_, *t_, pres_, mom_, sdfFn, origin_, rho_ / dt_, mu_, &momSkipped);
172 if (momSkipped)
173 std::fprintf(stderr,
174 "[peclet.amr] mom seam delta: %ld raw-regular/virtually-ghost rows "
175 "skipped (rung-1 scope)\n",
176 momSkipped);
177 }
178 if (ghostProj_) {
179 // Ghost projection: the pressure geometry is the BINARY openness (a face is open iff both
180 // adjacent centers + the face sample are fluid), on the UNCHANGED MG rails; the closure
181 // physics lives in the overlay. World-coord sampling offset by the origin (the octree's
182 // fine units start at origin_): shift the probe like cellCenter does.
183 auto sdfW = [&](const Vec<3>& p) { return sdfFn(p); };
184 if (ghostSampled_) {
185 // Level-aware canonical openness (actual adjacent leaf centers) — the classification
186 // primitive the sampled overlay's face states are forced to (no double-counted flux).
187 auto binFn = makeBinaryOpenFnMixed(*t_, pres_, sdfW, h0_, origin_);
188 pres_.buildOpenness(binFn);
189 presMG_.setOpenness(binFn);
190 } else {
191 auto binFn = makeBinaryOpenFn(sdfW, h0_);
192 pres_.buildOpenness(binFn);
193 presMG_.setOpenness(binFn);
194 }
195 ghostGrad_ = true; // the directional gradient is part of the scheme
196 // Fragmentation guard (pockets decoupled) + coupled mask (1 = row in the Krylov space).
197 gpPocket_ = findPocketCells(*t_, pres_, mom_.sdfCRaw());
198 maskC_.assign(static_cast<std::size_t>(t_->numLeaves()), 0.0);
199 for (Index i = 0; i < t_->numLeaves(); ++i)
200 maskC_[static_cast<std::size_t>(i)] =
201 (mom_.isFluid(i) && !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(i)]))
202 ? 1.0
203 : 0.0;
204 const GhostOverlay& rows = ghostSampled_ ? gpOvS_.base : gpOv_;
205 for (Index r = 0; r < rows.n; ++r)
206 if (!rows.coupled[static_cast<std::size_t>(r)])
207 maskC_[static_cast<std::size_t>(rows.cell[static_cast<std::size_t>(r)])] = 0.0;
208 } else {
209 gpPocket_.clear();
210 pres_.buildOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
211 presMG_.setOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
212 }
213 // C/F interface scheme overlays (cf_scheme.hpp), built from the SAME host CSR builders the
214 // device uses (parity by construction). The projection family is gated PER FACE —
215 // `cfFace(i, j) = level mismatch && regular(i) && regular(j)` — through one emitter, so the
216 // divergence RHS, the ABC gradient substitution and the advecting face field carry the same
217 // correction on the same faces and `D_std(Δuf) ≡ Δ_cfDiv` holds identically. See flow.hpp's
218 // cf section and docs/amr_cf_flux_gate.md.
219 // The per-FACE C/F gate's predicate (docs/amr_cf_flux_gate.md §6.1). The oracle is
220 // single-rank, so there is no ghost tail to exchange (§6.4): `regular` is the local flag.
221 {
222 const Index nl = t_->numLeaves();
223 cfRegular_.assign(static_cast<std::size_t>(nl), 0);
224 for (Index i = 0; i < nl; ++i)
225 cfRegular_[static_cast<std::size_t>(i)] = (mom_.isFluid(i) && !mom_.isCut(i)) ? 1 : 0;
226 }
227 auto regularOk = [&](Index s) { // programming-error guard (§6.4)
228 if (s < 0 || s >= static_cast<Index>(cfRegular_.size()))
229 throw std::logic_error("peclet::amr oracle: C/F regular flag queried outside [0, n)");
230 return cfRegular_[static_cast<std::size_t>(s)] != 0;
231 };
232 cfCutFaces_ = countCfCutFaces(pres_, *t_, regularOk, [&](Index s) { return mom_.isFluid(s); });
233 if (cfScheme_ != CfScheme::standard) {
234 auto fluidOk = [&](Index j) { return mom_.isFluid(j); };
235 auto rowRegular = [&](Index i) { return mom_.isFluid(i) && !mom_.isCut(i); };
236 cfMom_ = buildCfLapDelta(mom_.lap(), *t_, mu_, rowRegular, fluidOk, cfScheme_);
237 cfDiv_ = buildCfDivDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
238 cfGrad_ = buildCfGradDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
239 cfUf_ = buildCfUfDelta(pres_, *t_, regularOk, fluidOk, cfScheme_);
240 } else {
241 cfMom_ = CfCsr{};
242 cfDiv_ = CfCompCsr{};
243 cfGrad_ = {};
244 cfUf_ = CfUfDelta{};
245 }
246 const Index n = t_->numLeaves();
247 for (int c = 0; c < 3; ++c)
248 u_[c].assign(static_cast<std::size_t>(n), 0.0);
249 phi_.assign(static_cast<std::size_t>(n), 0.0);
250 p_.assign(static_cast<std::size_t>(n), 0.0);
251 // Face CSR offsets: count the (sub)faces forEachFaceFull emits per cell (6 interior, more for a
252 // cell facing finer neighbours), prefix-sum into faceStart_.
253 faceStart_.assign(static_cast<std::size_t>(n) + 1, 0);
254 for (Index i = 0; i < n; ++i) {
255 Index cnt = 0;
256 pres_.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++cnt; });
257 faceStart_[static_cast<std::size_t>(i) + 1] = faceStart_[static_cast<std::size_t>(i)] + cnt;
258 }
259 uf_.assign(static_cast<std::size_t>(faceStart_[static_cast<std::size_t>(n)]), 0.0);
260 faceFieldBuilt_ = false;
261 }
262
263 const std::vector<double>& velocity(int c) const { return u_[c]; }
264 Index numLeaves() const { return t_->numLeaves(); }
265 bool isFluid(Index i) const { return mom_.isFluid(i); }
266 bool isCut(Index i) const { return mom_.isCut(i); }
268 Index numCfCutFaces() const { return cfCutFaces_; }
269
270 // ---- adaptivity during a run (mirror of the device AmrFlow's beginAdapt/finishAdapt) ----------
271 void beginAdapt() {
272 adaptOldT_ = std::make_unique<Octree>(*t_);
273 adaptU_ = u_;
274 adaptP_ = p_;
275 }
276 template <class SdfFn>
278 if (!adaptOldT_)
279 throw std::runtime_error("oracle::AmrFlow::finishAdapt called without beginAdapt");
280 std::array<std::vector<double>, 3> nu;
281 for (int c = 0; c < 3; ++c)
282 nu[static_cast<std::size_t>(c)] =
283 transferField(*adaptOldT_, adaptU_[static_cast<std::size_t>(c)], *t_, /*linear=*/true);
284 std::vector<double> np = transferField(*adaptOldT_, adaptP_, *t_, /*linear=*/true);
286 for (Index i = 0; i < t_->numLeaves(); ++i) {
287 const bool fl = mom_.isFluid(i);
288 for (int c = 0; c < 3; ++c)
289 u_[static_cast<std::size_t>(c)][static_cast<std::size_t>(i)] =
290 fl ? nu[static_cast<std::size_t>(c)][static_cast<std::size_t>(i)] : 0.0;
291 p_[static_cast<std::size_t>(i)] = fl ? np[static_cast<std::size_t>(i)] : 0.0;
292 }
293 adaptOldT_.reset();
294 }
295
298 void step(int momSweeps = 200, int presIters = 60, int presSweeps = 4) {
299 const Index n = t_->numLeaves();
300 // Advection ∇·(u^n u^n_c), evaluated from u^n BEFORE the predictor mutates u_.
301 // High-order (SOU/TVD) always; first-order-upwind too for the implicit-FOU
302 // deferred correction (the explicit term is ρ·(HO − FOU); FOU is implicit in
303 // the momentum operator, rebuilt here from the lagged u^n).
304 std::array<std::vector<double>, 3> adv; // full explicit advection term (incl. ρ)
305 if (advect_) {
306 if (implicitFou_)
307 mom_.buildAdvectionFou(u_, rho_, uf_, faceStart_,
308 faceFieldBuilt_); // FOU advected by the div-free uf
309 for (int c = 0; c < 3; ++c) {
310 adv[c].assign(static_cast<std::size_t>(n), 0.0);
311 for (Index i = 0; i < n; ++i)
312 if (mom_.isFluid(i)) {
313 double term = rho_ * advectHO(c, i); // ρ·∇·(u u_c), high order
314 if (implicitFou_)
315 term -= mom_.fouApply(i, u_[c]); // − ρ·FOU (deferred correction)
316 adv[c][static_cast<std::size_t>(i)] = term;
317 }
318 }
319 }
320
321 // --- momentum predictor: implicit viscous (+ implicit FOU) + body force − advection ---
322 for (int c = 0; c < 3; ++c) {
323 // C/F-scheme deferred correction on the velocity diffusion: +μ(∇²_scheme − ∇²_std)(uⁿ_c),
324 // lagged like the SOU deferred correction ⇒ the steady operator carries the 2nd-order flux.
325 std::vector<double> cfm;
326 if (cfScheme_ != CfScheme::standard) {
327 cfm.assign(static_cast<std::size_t>(n), 0.0);
328 cfApplyHost(cfMom_, u_[c], cfm);
329 }
330 // Momentum ξ-row seam correction (sampled mode, plan §6.2): same lagged pattern; the
331 // coefficients already fold 1/rscale so makeRhs's rscale multiply is exact.
332 if (ghostSampled_ && !gpsMomDelta_.start.empty()) {
333 if (cfm.empty())
334 cfm.assign(static_cast<std::size_t>(n), 0.0);
335 cfApplyHost(gpsMomDelta_, u_[c], cfm);
336 }
337 std::vector<double> src(static_cast<std::size_t>(n), 0.0);
338 for (Index i = 0; i < n; ++i)
339 if (mom_.isFluid(i)) {
340 // incremental predictor: include the old pressure gradient −∇p^n.
341 double s = (rho_ / dt_) * u_[c][static_cast<std::size_t>(i)] + f_[c] - gradP(p_, i, c);
342 if (advect_)
343 s -= adv[c][static_cast<std::size_t>(i)]; // HO (explicit) or HO−FOU (deferred)
344 if (!cfm.empty())
345 s += cfm[static_cast<std::size_t>(i)];
346 src[static_cast<std::size_t>(i)] = s;
347 }
348 std::vector<double> b = mom_.makeRhs(src, /*u_bc=*/0.0);
349 mom_.gaussSeidel(u_[c], b, momSweeps); // u_ now holds u*
350 }
352 }
353
356 void project(int presIters = 60, int presSweeps = 4) {
357 const Index n = t_->numLeaves();
358 std::vector<double> div(static_cast<std::size_t>(n), 0.0);
359 for (Index i = 0; i < n; ++i)
360 if (mom_.isFluid(i))
361 div[static_cast<std::size_t>(i)] = divergence(u_, i);
362 if (cfScheme_ != CfScheme::standard) // 2nd-order C/F face averages in the constraint
363 cfApplyCompHost(cfDiv_, u_, div);
364 if (ghostProj_) { // ghost-closed constraint: binary div (above, binary α) + closure overlay
365 if (ghostSampled_)
366 ghostDivergDeltaSampledHost(gpOvS_, u_, div);
367 else
368 ghostDivergDeltaHost(gpOv_, u_, div);
369 }
370
371 std::fill(phi_.begin(), phi_.end(), 0.0);
373 if (ghostProj_) {
374 // Nonsymmetric ghost operator rho·(L_bin + Delta): MG-preconditioned BiCGStab on the
375 // coupled subspace (decoupled rows pinned 0, coupled volume-weighted mean removed).
376 solveGhostBiCGStab(phi_, div, presIters);
377 } else {
378 // Standard (not quadratic) openness V-cycles: the operator L = div(α grad) is
379 // consistent with the FV divergence / ABC gradient above (D, G, L share the
380 // same face enumeration), so the collocated projection is stable across 2:1
381 // interfaces. (The quadratic C/F flux is for pure-Poisson accuracy, not here.)
382 for (int it = 0; it < presIters; ++it)
383 presMG_.vcycle(0, phi_, div);
384 }
385
386 // Build the divergence-free FACE field from u* (still in u_) + φ, before the cell correction.
388
389 for (int c = 0; c < 3; ++c)
390 for (Index i = 0; i < n; ++i)
391 if (mom_.isFluid(i))
392 u_[c][static_cast<std::size_t>(i)] -= gradP(phi_, i, c);
393
394 // Rotational incremental pressure update: p += (ρ/dt)φ − μ ∇·u* (div is ∇·u*).
395 // The −μ∇·u* rotational term removes the projection's boundary-layer splitting
396 // error, making the steady solution dt-independent (vs plain Chorin).
397 for (Index i = 0; i < n; ++i)
398 if (mom_.isFluid(i))
399 p_[static_cast<std::size_t>(i)] += (rho_ / dt_) * phi_[static_cast<std::size_t>(i)] -
400 mu_ * div[static_cast<std::size_t>(i)];
401 }
402
406 double divergence(const std::array<std::vector<double>, 3>& vel, Index i) const {
407 double d = 0.0;
408 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double alpha) {
409 double ui = vel[axis][static_cast<std::size_t>(i)];
410 double uj = vel[axis][static_cast<std::size_t>(j)]; // solid cells hold 0
411 d += alpha * area * dir * 0.5 * (ui + uj);
412 });
413 return d / pres_.cellVolume(i);
414 }
415
416 double divNormL2(const std::array<std::vector<double>, 3>& vel) const {
417 double s = 0.0;
418 const Index n = t_->numLeaves();
419 for (Index i = 0; i < n; ++i)
420 if (mom_.isFluid(i)) {
421 double d = divergence(vel, i);
422 s += d * d;
423 }
424 return std::sqrt(s);
425 }
426
429 double divNormL2Regular(const std::array<std::vector<double>, 3>& vel) const {
430 double s = 0.0;
431 const Index n = t_->numLeaves();
432 for (Index i = 0; i < n; ++i)
433 if (mom_.isFluid(i) && !mom_.isCut(i)) {
434 const double d = divergence(vel, i);
435 s += d * d;
436 }
437 return std::sqrt(s);
438 }
439
448 double divNormFaceRegular() const {
449 double tot = 0.0;
450 const Index n = t_->numLeaves();
451 for (Index i = 0; i < n; ++i) {
452 Index s = faceStart_[static_cast<std::size_t>(i)];
453 if (!mom_.isFluid(i) || mom_.isCut(i)) {
454 pres_.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++s; });
455 continue;
456 }
457 double d = 0.0;
458 pres_.forEachFaceFull(i, [&](Index, int, int dir, double area, double, double alpha) {
459 d += alpha * area * dir * uf_[static_cast<std::size_t>(s++)];
460 });
461 d /= pres_.cellVolume(i);
462 tot += d * d;
463 }
464 return std::sqrt(tot);
465 }
466
476 std::pair<double, double> cfFluxIdentity() const {
477 const Index n = t_->numLeaves();
478 std::vector<double> dcell(static_cast<std::size_t>(n), 0.0);
479 cfApplyCompHost(cfDiv_, u_, dcell); // Δ_cfDiv
480 const std::size_t nSlots =
481 cfUf_.vel.start.empty() ? 0u : cfUf_.vel.start.size() - 1; // == faceStart_[n]
482 std::vector<double> duf(nSlots, 0.0);
483 cfApplyCompHost(cfUf_.vel, u_, duf); // Δuf on the face slots
484 double err = 0.0, ref = 0.0;
485 for (Index i = 0; i < n; ++i) {
486 double d = 0.0;
487 Index s = faceStart_[static_cast<std::size_t>(i)];
488 pres_.forEachFaceFull(i, [&](Index, int, int dir, double area, double, double alpha) {
489 if (static_cast<std::size_t>(s) < nSlots)
490 d += alpha * area * dir * duf[static_cast<std::size_t>(s)];
491 ++s;
492 });
493 d /= pres_.cellVolume(i); // D_std(Δuf) at row i
494 const double r = d - dcell[static_cast<std::size_t>(i)];
495 err += r * r;
496 ref += dcell[static_cast<std::size_t>(i)] * dcell[static_cast<std::size_t>(i)];
497 }
498 return {std::sqrt(err), std::sqrt(ref)};
499 }
500
501 std::array<std::vector<double>, 3>& velocityRef() { return u_; }
502
510 const Index n = t_->numLeaves();
511 for (Index i = 0; i < n; ++i) {
512 Index s = faceStart_[static_cast<std::size_t>(i)];
513 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double) {
514 const double uface =
515 0.5 * (u_[axis][static_cast<std::size_t>(i)] + u_[axis][static_cast<std::size_t>(j)]);
516 // +axis pressure gradient across the face: (φ₊−φ₋)/d, +side = dir>0 ? j : i.
517 const double gphi =
518 (dir > 0)
519 ? (phi_[static_cast<std::size_t>(j)] - phi_[static_cast<std::size_t>(i)]) / dist
520 : (phi_[static_cast<std::size_t>(i)] - phi_[static_cast<std::size_t>(j)]) / dist;
521 uf_[static_cast<std::size_t>(s++)] = uface - gphi;
522 });
523 }
524 // 2nd-order C/F face values (setCfScheme): distance-weighted average + coarse* substitution
525 // on the 2:1 sub-faces — the advecting flux matches the (quad) divergence constraint.
526 if (cfScheme_ != CfScheme::standard && !cfUf_.vel.start.empty()) {
527 cfApplyCompHost(cfUf_.vel, u_, uf_);
528 // The φ part is deliberately NOT applied — see the long note in flow.hpp::finishProjection.
529 // It puts a quadratic coarse* into uf'''s face gradient that L = D_std·G_std never inverted,
530 // which is the whole of the flux imbalance that stops uf being conservative.
531 }
532 faceFieldBuilt_ = true;
533 }
534
538 double divNormFace() const {
539 double tot = 0.0;
540 const Index n = t_->numLeaves();
541 for (Index i = 0; i < n; ++i) {
542 if (!mom_.isFluid(i))
543 continue;
544 Index s = faceStart_[static_cast<std::size_t>(i)];
545 double d = 0.0;
546 pres_.forEachFaceFull(i, [&](Index, int, int dir, double area, double, double alpha) {
547 d += alpha * area * dir * uf_[static_cast<std::size_t>(s++)];
548 });
549 d /= pres_.cellVolume(i);
550 tot += d * d;
551 }
552 return std::sqrt(tot);
553 }
554
557 const std::vector<double>& faceField() const { return uf_; }
558 const std::vector<Index>& faceStart() const { return faceStart_; }
559
560 private:
561 // ---- Koren TVD advection (faithful port of flow sadv::koren/tvd + cadv::advect) ----
562 static double koren(double up_m1, double up, double down, double vel) {
563 const double num = up - up_m1, den = down - up;
564 double r = (std::fabs(den) < 1e-10) ? 0.0 : num / den;
565 double psi = std::fmax(0.0, std::fmin(2.0 * r, std::fmin((1.0 + 2.0 * r) / 3.0, 2.0)));
566 return vel * (up + 0.5 * psi * (down - up));
567 }
568 static double tvd(double LL, double L, double R, double RR, double vel) {
569 return (vel > 0.0) ? koren(LL, L, R, vel) : koren(RR, R, L, vel);
570 }
571 // High-order face value: the SHARED reconstruction (advect_recon.hpp) the device runs too.
572 double hoFace(double upup, double up, double down) const {
573 return hoFaceValue(upup, up, down, advScheme_);
574 }
575
576 // C/F-consistent high-order advection ∇·(u u_comp) at leaf i: per (sub)face (via
577 // forEachFaceFull, so a coarse cell sums its fine sub-faces — conservative), the
578 // outward advective flux α-free·area·velOut·φ_face, with φ_face the SOU/TVD
579 // reconstruction from the upwind cell + its upstream neighbour (point-probed, so it
580 // works across 2:1 levels). No advection through wall faces. The implicit-FOU half
581 // is mom_.fouApply (same faces/velocities), so the deferred correction is the
582 // exact high-order − FOU difference.
583 double advectHO(int comp, Index i) const {
584 double out = 0.0;
585 Index s = faceFieldBuilt_ ? faceStart_[static_cast<std::size_t>(i)] : 0;
586 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double) {
587 // Advecting velocity = the divergence-free FACE field uf (built by the previous projection),
588 // so the advective flux is conservative (∇·uf = 0) — Bell–Colella–Glaz / Basilisk. Falls back
589 // to the simple cell average ½(u_i+u_j) before the first projection has built uf. (At steady
590 // state φ→0 ⇒ uf = ½(u_i+u_j), so the steady solution is unchanged; the gain is in the
591 // transient.)
592 const double uface = faceFieldBuilt_ ? uf_[static_cast<std::size_t>(s)]
593 : 0.5 * (u_[axis][static_cast<std::size_t>(i)] +
594 u_[axis][static_cast<std::size_t>(j)]);
595 ++s;
596 if (!mom_.isFluid(j))
597 return; // no advection through the immersed boundary
598 double velOut = dir * uface;
599 Index up = (velOut > 0.0) ? i : j, down = (velOut > 0.0) ? j : i;
600 int upDir = (up == i) ? -dir : dir; // upstream direction from the upwind cell
601 Index upup = pres_.periodicNeighbor(up, axis, upDir);
602 double phiUp = u_[comp][static_cast<std::size_t>(up)];
603 double phiUpUp =
604 (upup >= 0 && mom_.isFluid(upup)) ? u_[comp][static_cast<std::size_t>(upup)] : phiUp;
605 double phiFace = hoFace(phiUpUp, phiUp, u_[comp][static_cast<std::size_t>(down)]);
606 out += area * velOut * phiFace;
607 });
608 return out / pres_.cellVolume(i);
609 }
610
611 // Project onto the coupled subspace: pin decoupled rows (solid-centered + overlay rows with no
612 // phi coupling) to 0, remove the volume-weighted mean over the coupled cells (the constant null
613 // mode of the connected fluid region — computed over coupled cells ONLY, like maskSolid).
614 void gpProject(std::vector<double>& v) const {
615 const Index n = t_->numLeaves();
616 double su = 0.0, sv = 0.0;
617 for (Index i = 0; i < n; ++i) {
618 const std::size_t s = static_cast<std::size_t>(i);
619 v[s] *= maskC_[s];
620 if (maskC_[s] > 0.0) {
621 const double V = pres_.cellVolume(i);
622 su += V * v[s];
623 sv += V;
624 }
625 }
626 const double m = (sv > 0.0) ? su / sv : 0.0;
627 for (Index i = 0; i < n; ++i)
628 v[static_cast<std::size_t>(i)] -= maskC_[static_cast<std::size_t>(i)] * m;
629 }
630
631 // MG-preconditioned BiCGStab on the nonsymmetric ghost pressure operator
632 // A(x) = P[rho·(L_bin x + Delta x)] with P the coupled-subspace projection; preconditioner =
633 // two binary-openness V-cycles + P. Stagnation guard: stop when the residual makes no new best
634 // for 6 iterations (the ghost system has a small attainable-residual floor — flow's measured
635 // compatibility gap).
636 void solveGhostBiCGStab(std::vector<double>& x, const std::vector<double>& b, int maxIters,
637 double tol = 1e-10) {
638 const Index n = t_->numLeaves();
639 const std::size_t ns = static_cast<std::size_t>(n);
640 auto applyA = [&](const std::vector<double>& v, std::vector<double>& y) {
641 pres_.applyLaplacian(v, y); // binary-openness L (ghost mode geometry)
642 if (ghostSampled_)
644 else
645 ghostApplyDeltaHost(gpOv_, v, y);
646 gpProject(y);
647 };
648 auto prec = [&](const std::vector<double>& r, std::vector<double>& z) {
649 std::fill(z.begin(), z.end(), 0.0);
650 presMG_.vcycle(0, z, r);
651 presMG_.vcycle(0, z, r);
652 gpProject(z);
653 };
654 auto dot = [&](const std::vector<double>& a, const std::vector<double>& c) {
655 double s = 0.0;
656 for (std::size_t i = 0; i < ns; ++i)
657 s += a[i] * c[i];
658 return s;
659 };
660 std::vector<double> r(ns), rhat(ns), p(ns, 0.0), phat(ns), v(ns, 0.0), s(ns), shat(ns), t(ns);
661 applyA(x, r);
662 for (std::size_t i = 0; i < ns; ++i)
663 r[i] = b[i] - r[i];
664 gpProject(r);
665 rhat = r;
666 const double res0 = std::sqrt(dot(r, r));
667 if (res0 == 0.0)
668 return;
669 double rho = 1, alpha = 1, omega = 1, best = res0;
670 int noImprove = 0;
671 for (int it = 0; it < maxIters; ++it) {
672 const double rhoNew = dot(rhat, r);
673 if (rhoNew == 0.0)
674 break;
675 const double beta = (rhoNew / rho) * (alpha / omega);
676 for (std::size_t i = 0; i < ns; ++i)
677 p[i] = r[i] + beta * (p[i] - omega * v[i]);
678 prec(p, phat);
679 applyA(phat, v);
680 const double rhatV = dot(rhat, v);
681 if (rhatV == 0.0)
682 break;
683 alpha = rhoNew / rhatV;
684 for (std::size_t i = 0; i < ns; ++i)
685 s[i] = r[i] - alpha * v[i];
686 double snorm = std::sqrt(dot(s, s));
687 if (snorm <= tol * res0) {
688 for (std::size_t i = 0; i < ns; ++i)
689 x[i] += alpha * phat[i];
690 break;
691 }
692 prec(s, shat);
693 applyA(shat, t);
694 const double tt = dot(t, t);
695 omega = (tt != 0.0) ? dot(t, s) / tt : 0.0;
696 for (std::size_t i = 0; i < ns; ++i) {
697 x[i] += alpha * phat[i] + omega * shat[i];
698 r[i] = s[i] - omega * t[i];
699 }
700 const double rnorm = std::sqrt(dot(r, r));
701 if (rnorm <= tol * res0)
702 break;
703 if (rnorm < 0.999 * best) {
704 best = rnorm;
705 noImprove = 0;
706 } else if (++noImprove >= 6) {
707 break; // attainable-residual floor (compatibility gap) — stagnation guard
708 }
709 rho = rhoNew;
710 if (omega == 0.0)
711 break;
712 }
713 gpProject(x);
714 }
715
716 // Predictor/correction cell gradient dispatch: the ABC gradOf everywhere, EXCEPT — with
717 // setGhostGradient — on cut cells (fluid with a solid face neighbour), where gradOf reads the
718 // decoupled solid p through partially-open faces (gauge-dependent O(1/h), measured in
719 // tests/study_amr_ghost_apriori.cpp) and the directional ghost gradient below is used instead.
720 double gradP(const std::vector<double>& fld, Index i, int c) const {
721 if (ghostGrad_ && mom_.isCut(i)) {
722 if (ghostSampled_) {
723 // Mixed-level cut band: the row's sample functionals feed the gradient too (pairing —
724 // constraint and gradient from the same closures). Cut cells without a row (clean per
725 // the overlay classification) fall through to the level-aware gradOfDir cascade.
726 const Index r = gpOvS_.rowOf[static_cast<std::size_t>(i)];
727 if (r >= 0)
728 return gpsDirGrad(gpOvS_, r, fld, c, 1.0 / pres_.cellWidth(i));
729 }
730 return gradOfDir(fld, i, c); // cut band: no C/F faces (band contract) ⇒ no cf delta
731 }
732 double g = gradOf(fld, i, c);
733 // 2nd-order C/F face gradients (level-boundary rows); empty unless built by setSolid.
734 if (cfScheme_ != CfScheme::standard && !cfGrad_[static_cast<std::size_t>(c)].start.empty()) {
735 const CfCsr& cs = cfGrad_[static_cast<std::size_t>(c)];
736 for (Index k = cs.start[static_cast<std::size_t>(i)];
737 k < cs.start[static_cast<std::size_t>(i) + 1]; ++k)
738 g += cs.coef[static_cast<std::size_t>(k)] *
739 fld[static_cast<std::size_t>(cs.slot[static_cast<std::size_t>(k)])];
740 }
741 return g;
742 }
743
744 // Directional ghost cell-gradient on a cut-band cell (flow's gpCenterGrad analog). Cut cells
745 // have same-level face neighbours by the finest-band contract, so plain FD applies: central
746 // where both axis-neighbour CENTERS are fluid; 2nd-order one-sided toward the fluid else
747 // ((−3f_i+4f_{+1}−f_{+2})/2h, with a 2-point fallback when the ±2 cell is solid or not
748 // same-level); 0 when sandwiched. Never reads a solid-centered (decoupled) value — O(h²) and
749 // exactly gauge-invariant.
750 double gradOfDir(const std::vector<double>& fld, Index i, int c) const {
751 const double h = pres_.cellWidth(i);
752 auto F = [&](Index j) { return fld[static_cast<std::size_t>(j)]; };
753 // Pocket cells (fragmentation guard) count as solid: their φ is pinned/decoupled.
754 auto ok = [&](Index j) {
755 return j >= 0 && mom_.isFluid(j) && t_->level(j) == t_->level(i) &&
756 !(!gpPocket_.empty() && gpPocket_[static_cast<std::size_t>(j)]);
757 };
758 const Index jp = pres_.periodicNeighbor(i, c, +1);
759 const Index jm = pres_.periodicNeighbor(i, c, -1);
760 const bool ap = ok(jp), am = ok(jm);
761 if (am && ap)
762 return (F(jp) - F(jm)) / (2.0 * h);
763 if (ap) {
764 const Index jpp = pres_.periodicNeighbor(jp, c, +1);
765 return ok(jpp) ? (-3.0 * F(i) + 4.0 * F(jp) - F(jpp)) / (2.0 * h) : (F(jp) - F(i)) / h;
766 }
767 if (am) {
768 const Index jmm = pres_.periodicNeighbor(jm, c, -1);
769 return ok(jmm) ? (3.0 * F(i) - 4.0 * F(jm) + F(jmm)) / (2.0 * h) : (F(i) - F(jm)) / h;
770 }
771 return 0.0;
772 }
773
774 // ABC (Almgren-Bell-Colella) cell-velocity correction gradient in direction `c`:
775 // ½·(g⁻ + g⁺) of the two adjacent FACE pressure-gradients, where a CLOSED face
776 // (openness 0 — solid neighbour) contributes a ZERO gradient (it does NOT read
777 // the solid neighbour's φ). Verbatim form of flow's projectCorrectCenter
778 // (src/mac_approx_projection.hpp) — the collocated approximate projection. This
779 // is the chosen collocated coupling; do NOT substitute a Rhie–Chow face-velocity
780 // interpolation (see docs/AMR.md and the amr-octree memory).
781 double gradOf(const std::vector<double>& fld, Index i, int c) const {
782 const double pi = fld[static_cast<std::size_t>(i)];
783 double gp = 0, gm = 0;
784 int np = 0, nm = 0;
785 // Average the +axis-oriented face gradient (φ_j−φ_i)/dist over each side's
786 // (sub)faces along axis c; a closed face contributes 0 (ABC). C/F-consistent.
787 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double alpha) {
788 if (axis != c || alpha <= 1e-12)
789 return;
790 double g = (dir > 0) ? (fld[static_cast<std::size_t>(j)] - pi) / dist
791 : (pi - fld[static_cast<std::size_t>(j)]) / dist;
792 if (dir > 0) {
793 gp += g;
794 ++np;
795 } else {
796 gm += g;
797 ++nm;
798 }
799 });
800 double gpa = np ? gp / np : 0.0, gma = nm ? gm / nm : 0.0;
801 return 0.5 * (gpa + gma);
802 }
803
804 // Fluid area fraction of a face. DEFAULT (order 2, 2026-08-26 user decision): triangle-fan
805 // marching squares on FIVE ANALYTIC samples (4 corners + centre) -- exact linear fraction per
806 // triangle, O(h^2), no saddle ambiguity, and the sub-resolution floor 1e-6 (alpha ~ 1e-12 rows
807 // destroy the pressure conditioning -- flow tracker rows 51/52). Because the AMR solver holds
808 // the analytic sdfFn, this samples the TRUE geometry (no trilinear ceiling). order 1 = the
809 // legacy one-sample gradient-normalised linear model (kept for A/B), which carries a SIGNED
810 // convexity bias measured at +0.59%/+0.27% bed permeability at R=8/12 (decay ~h^2).
811 static double triFrac(double a, double b, double c) {
812 const bool pa = a >= 0.0, pb = b >= 0.0, pc = c >= 0.0;
813 const int np = (pa ? 1 : 0) + (pb ? 1 : 0) + (pc ? 1 : 0);
814 if (np == 3)
815 return 1.0;
816 if (np == 0)
817 return 0.0;
818 double x, y, z;
819 if (np == 1) {
820 if (pa) {
821 x = a;
822 y = b;
823 z = c;
824 } else if (pb) {
825 x = b;
826 y = c;
827 z = a;
828 } else {
829 x = c;
830 y = a;
831 z = b;
832 }
833 const double den = (x - y) * (x - z);
834 return den > 1e-300 ? (x * x) / den : 1.0;
835 }
836 if (!pa) {
837 x = a;
838 y = b;
839 z = c;
840 } else if (!pb) {
841 x = b;
842 y = c;
843 z = a;
844 } else {
845 x = c;
846 y = a;
847 z = b;
848 }
849 const double den = (x - y) * (x - z);
850 return 1.0 - (den > 1e-300 ? (x * x) / den : 1.0);
851 }
852 template <class SdfFn>
853 double faceFrac(SdfFn&& sdfFn, const Vec<3>& fc, int axis) const {
854 if (apertureOrder_ >= 2) {
855 if (sdfFn(fc) <= 0.0)
856 return 0.0; // center gate (kept from order 1; see flow ccFaceOpenMS)
857 const int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
858 const double e1 = 0.5 * h0_[t1], e2 = 0.5 * h0_[t2];
859 auto at = [&](double d1, double d2) {
860 Vec<3> p = fc;
861 p[t1] += d1;
862 p[t2] += d2;
863 return sdfFn(p);
864 };
865 const double c00 = at(-e1, -e2), c10 = at(e1, -e2), c11 = at(e1, e2), c01 = at(-e1, e2);
866 const double cc = sdfFn(fc);
867 const double frac = 0.25 * (triFrac(c00, c10, cc) + triFrac(c10, c11, cc) +
868 triFrac(c11, c01, cc) + triFrac(c01, c00, cc));
869 return frac < 1e-3 ? 0.0 : (frac > 1.0 - 1e-12 ? 1.0 : frac); // floor: see flow ccFaceOpenMS
870 }
871 double sd = sdfFn(fc);
872 if (sd <= 0.0)
873 return 0.0;
874 Vec<3> g{};
875 for (int d = 0; d < 3; ++d) {
876 Vec<3> pp = fc, pm = fc;
877 pp[d] += h0_[d];
878 pm[d] -= h0_[d];
879 g[d] = (sdfFn(pp) - sdfFn(pm)) / (2.0 * h0_[d]);
880 }
881 double gmag = std::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
882 if (gmag < 1e-6)
883 gmag = 1e-6;
884 int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
885 double denom = (std::fabs(g[t1]) * h0_[t1] + std::fabs(g[t2]) * h0_[t2]) / gmag;
886 if (denom < 1e-9)
887 denom = 1e-9;
888 double frac = 0.5 + sd / denom;
889 return frac < 0.0 ? 0.0 : (frac > 1.0 ? 1.0 : frac);
890 }
891
892 const Octree* t_ = nullptr;
893 Vec<3> h0_ = detail::filledVec<3>(1.0);
894 Vec<3> origin_{};
895 double rho_ = 1.0, mu_ = 1.0, dt_ = 1e6;
896 bool advect_ = false;
897 bool ghostGrad_ = true; // directional ghost gradient on cut cells (setGhostGradient)
898 bool ghostProj_ = false; // RESOLVED projection mode (set by setSolid from the request)
899 int8_t ghostProjReq_ = -1; // -1 = AUTO (DEFAULT: ghost, aperture fallback on thin band),
900 // 0 = explicit aperture, 1 = explicit ghost
901 int apertureOrder_ = 2; // aperture estimator order (setApertureOrder; default 2)
902 int gpMatrixOrder_ = 2, gpRhsOrder_ = 2; // closure orders (2,2 = the production pair; the
903 // (1,2) mixed form is march-unstable at scale)
904 GhostOverlay gpOv_; // closure overlay (finest-band rows)
905 bool ghostSampled_ = false; // RESOLVED sampled mode (set by setSolid)
906 int8_t ghostSampledReq_ = 0; // setGhostSampled request (mixed-level prototype)
907 GhostOverlaySampled gpOvS_; // sample-slot overlay (mixed-level cut band)
908 CfCsr gpsMomDelta_; // momentum ξ-row seam correction (plan §6.2)
909 std::vector<double> maskC_; // 1 = coupled row (Krylov subspace), 0 = pinned
910 std::vector<char> gpPocket_; // fragmentation guard: 1 = decoupled pocket cell
911 // DEFAULT since 2026-09-21 (user decision): the Martin-Cartwright tangential quadratic, NOT the
912 // standard two-point flux. The register had already settled that "cf=1 is not optional on graded
913 // meshes" and rejected cf=0 there, but the shipped default stayed standard, so a user who built a
914 // graded mesh silently got the configuration the project had decided against. Measured on a
915 // self-similar ladder (docs/amr_graded_convergence.md §3), as orders:
916 //
917 // standard quadratic
918 // interface NORMAL to the variation 2.00 2.00 (inert: no tangential variation to
919 // interface TANGENTIAL 0.41 1.60 correct, so the delta is empty)
920 //
921 // with the absolute gap on the tangential arm widening 3.8x at n=32 to 8.8x at n=64. INERT BY
922 // GEOMETRY on any uniform or finest-band mesh: no C/F faces means an empty delta CSR and the
923 // identical code path, so only graded runs move.
924 CfScheme cfScheme_ = CfScheme::quadratic; // 2:1 C/F interface scheme (setCfScheme)
925 CfCsr cfMom_; // +μ(∇²_scheme − ∇²_std) momentum RHS overlay
926 std::vector<char> cfRegular_; // regular = fluid && !cut, the per-FACE C/F gate
927 Index cfCutFaces_ = 0; // C/F slots where that gate withholds the quadratic
928 CfCompCsr cfDiv_; // (D_scheme − D_std) divergence overlay
929 std::array<CfCsr, 3> cfGrad_; // (G_scheme − G_std) per gradient axis
930 CfUfDelta cfUf_; // (uf_scheme − uf_std) face-field overlay (slots)
931 bool implicitFou_ = true; // implicit-FOU deferred-correction advection (stable)
932 int advScheme_ = 0; // 0 = SOU (default), 1 = Koren TVD
933 Vec<3> f_{};
934 AmrCutCell<Bits> mom_;
935 AmrPoisson<3, Bits> pres_; // openness + divergence/gradient access
936 AmrMultigrid<3, Bits> presMG_; // fast (graded-capable) pressure solve
937 std::array<std::vector<double>, 3> u_;
938 std::vector<double> phi_; // pressure-increment potential (per projection)
939 std::vector<double> p_; // accumulated pressure (rotational incremental scheme)
940 // Basilisk/ABC divergence-free FACE field: uf_[faceStart_[i]+s] is the +axis velocity through
941 // cell i's s-th (sub)face, in forEachFaceFull order. Each internal (sub)face is stored from BOTH
942 // incident cells; the orientation-based build keeps the two copies identical. At a 2:1 interface
943 // a coarse cell owns 2^(Dim-1) fine sub-faces and the fine cell owns its single face — so the
944 // face field is at the finest resolution touching each face and the coarse-cell divergence sums
945 // its sub-faces.
946 std::vector<Index> faceStart_; // CSR offsets into uf_, size n+1
947 std::vector<double> uf_; // +axis face velocity per (cell,face) slot
948 std::unique_ptr<Octree> adaptOldT_; // beginAdapt topology snapshot
949 std::array<std::vector<double>, 3> adaptU_; // beginAdapt field snapshots
950 std::vector<double> adaptP_;
951 bool faceFieldBuilt_ =
952 false; // uf_ populated by a projection (else advection falls back to ½(u_i+u_j))
953};
954
955} // namespace peclet::amr::oracle
956
957#endif // PECLET_AMR_FLOW_ORACLE_HPP
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:167
Real cellWidth(Index i, int axis) const
Cell width along axis (Phase 3: per axis).
Definition poisson.hpp:255
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:329
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 setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:71
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
Real cellVolume(Index i) const
Definition poisson.hpp:262
Per-block adaptive octree over block-local Morton codes.
unsigned level(Index i) 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,...
const std::vector< Index > & faceStart() const
void setBodyForce(double fx, double fy, double fz)
double advectTerm(int comp, Index i) const
Conservative Koren-TVD advection term ∇·(u u_comp) at leaf i (physical units; the explicit momentum a...
double divNormFace() const
L2 norm of the divergence of the FACE field uf_ (the div-free flux).
void setGhostProjection(bool on, int matrixOrder=2, int rhsOrder=2)
bool isCut(Index i) const
BlockOctree< 3, Bits > Octree
void setGhostGradient(bool on)
Directional ghost cell-gradient for the −∇pⁿ predictor and the projection's cell correction (the AMR ...
bool isFluid(Index i) const
void setImplicitAdvection(bool on)
Implicit-FOU deferred-correction advection (default ON): the first-order-upwind part is solved implic...
void init(const Octree &t, Real h0, Vec< 3 > origin=Vec< 3 >{})
void setGhostSampled(bool on)
SAMPLED ghost projection: the mixed-level cut-band prototype (the D1 machinery of docs/amr_mixed_leve...
double divNormFaceRegular() const
‖D_std(uf)‖₂ over REGULAR fluid rows only.
void setAdvectionScheme(int s)
High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.
void init(const Octree &t, const Vec< 3 > &h0, Vec< 3 > origin=Vec< 3 >{})
std::pair< double, double > cfFluxIdentity() const
Rule (I) of docs/amr_cf_flux_gate.md §4, measured (§6.6): the advecting face field's C/F delta and th...
void step(int momSweeps=200, int presIters=60, int presSweeps=4)
One Stokes projection step.
const std::vector< double > & velocity(int c) const
void setApertureOrder(int order)
FULL directional ghost-cell projection (the AMR port of flow's collocated set_ghost_projection): the ...
double divNormL2Regular(const std::array< std::vector< double >, 3 > &vel) const
divNormL2 restricted to REGULAR fluid rows (fluid and not cut) — the companion of divNormFaceRegular ...
void finishAdapt(SdfFn &&sdfFn)
void buildFaceField()
Build the ABC/Basilisk divergence-free FACE field from the current cell velocity u* and the projectio...
void setSolid(SdfFn &&sdfFn)
Build the cut-cell operators from an SDF callable sdfFn(worldPoint) (>0 fluid).
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...
Index numCfCutFaces() const
The C/F census — see AmrFlow::numCfCutFaces (docs/amr_cf_flux_gate.md §6.5).
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 divNormL2(const std::array< std::vector< double >, 3 > &vel) const
Index countCfCutFaces(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk)
The C/F CENSUS (docs/amr_cf_flux_gate.md §6.5): how many OWNED forEachFaceFull slots (i,...
auto makeBinaryOpenFnMixed(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn sdfFn, const Vec< 3 > &h0, Vec< 3 > origin, std::array< long, 3 > frameShift={})
The canonical (sub)face openness for mixed-level cut bands: open iff both adjacent ACTUAL leaf center...
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...
GhostOverlaySampled buildGhostOverlaySampled(const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, SdfFn &&sdf, int matrixOrder, int rhsOrder, Vec< 3 > origin=Vec< 3 >{}, const std::array< long, 3 > *globalFine=nullptr, std::array< long, 3 > frameShift={}, bool discovery=false, double rhoFactor=2.2, long maxSamples=0)
Build the sampled overlay.
CfCompCsr buildCfDivDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(D_scheme − D_std) for the face-average divergence div_i = invV·Σ α·A·dir·(face value).
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).
CfUfDelta buildCfUfDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
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 ...
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:91
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).
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).
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...
auto makeBinaryOpenFn(SdfFn sdfFn, const Vec< 3 > &h0)
Binary openness callable factory for the MG surrogate: a face is open iff both adjacent centers (prob...
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 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::array< CfCsr, 3 > buildCfGradDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(G_scheme − G_std) for the ABC cell gradient gradOf/grad3.
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< 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
CfCsr buildMomSeamDelta(const GhostOverlaySampled &ov, const BlockOctree< 3, Bits > &t, const AmrPoisson< 3, Bits > &pres, const Mom &mom, SdfFn &&sdf, Vec< 3 > origin, double idiag, double mu, long *nSkipped=nullptr, std::array< long, 3 > frameShift={})
Momentum ξ-overlay seam correction (plan §6.2, rung 1): at overlay rows with a different-level ±1 nei...
CfScheme
Coarse/fine interface scheme for the collocated flow operators.
Definition cf_scheme.hpp:73
@ standard
raw coarse value (two-point flux; 1st-order at 2:1 faces)
@ quadratic
Martin–Cartwright tangential quadratic (2nd-order at 2:1 faces)
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 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·...
Component-tagged CSR overlay (the divergence delta): out(i) += Σ_k coef·u[comp](slot).
Definition cf_scheme.hpp:86
std::vector< Index > start
Definition cf_scheme.hpp:87
Scalar-input CSR overlay: out(i) += Σ_k coef·f(slot) over rows with C/F faces.
Definition cf_scheme.hpp:79
std::vector< Index > start
size n+1
Definition cf_scheme.hpp:80
(uf_scheme − uf_std) for the div-free FACE field uf_k = ½(u_i+u_j) − (φ₊−φ₋)/d, one delta row per for...
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...