core
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 <vector>
41
42#include "peclet/core/amr/advect_recon.hpp" // shared high-order face reconstruction (host+device)
47
48// Retired from the production path: this serial Gauss-Seidel host driver is kept ONLY as the
49// development-stage oracle that the device AmrFlow (flow_device.hpp) is validated bit-for-bit
50// against. It is NOT exposed by the Python bindings. Production AMR flow runs on the device path.
52
53template <unsigned Bits = 21u>
54class AmrFlow {
55 public:
57
58 void init(const Octree& t, Real h0, Vec<3> origin = Vec<3>{}) {
59 t_ = &t;
60 h0_ = h0;
61 origin_ = origin;
62 }
63
64 void setDensity(double rho) { rho_ = rho; }
65 void setViscosity(double mu) { mu_ = mu; }
66 void setDt(double dt) { dt_ = dt; }
67 void setBodyForce(double fx, double fy, double fz) { f_ = {fx, fy, fz}; }
68 void setAdvection(bool on) { advect_ = on; }
70 void setAdvectionScheme(int s) { advScheme_ = s; }
75 void setImplicitAdvection(bool on) { implicitFou_ = on; }
76
79 double advectTerm(int comp, Index i) const { return advectHO(comp, i); }
80
82 template <class SdfFn>
84 mom_.init(*t_, h0_, origin_);
85 mom_.build(sdfFn, /*idiag=*/rho_ / dt_, /*beta=*/mu_ / (h0_ * h0_));
86 pres_.init(*t_, h0_);
87 pres_.setOrigin(origin_);
88 pres_.buildOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
89 presMG_.build(*t_, h0_);
90 presMG_.setOpenness([&](const Vec<3>& fc, int axis) { return faceFrac(sdfFn, fc, axis); });
91 const Index n = t_->numLeaves();
92 for (int c = 0; c < 3; ++c)
93 u_[c].assign(static_cast<std::size_t>(n), 0.0);
94 phi_.assign(static_cast<std::size_t>(n), 0.0);
95 p_.assign(static_cast<std::size_t>(n), 0.0);
96 // Face CSR offsets: count the (sub)faces forEachFaceFull emits per cell (6 interior, more for a
97 // cell facing finer neighbours), prefix-sum into faceStart_.
98 faceStart_.assign(static_cast<std::size_t>(n) + 1, 0);
99 for (Index i = 0; i < n; ++i) {
100 Index cnt = 0;
101 pres_.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++cnt; });
102 faceStart_[static_cast<std::size_t>(i) + 1] = faceStart_[static_cast<std::size_t>(i)] + cnt;
103 }
104 uf_.assign(static_cast<std::size_t>(faceStart_[static_cast<std::size_t>(n)]), 0.0);
105 faceFieldBuilt_ = false;
106 }
107
108 const std::vector<double>& velocity(int c) const { return u_[c]; }
109 Index numLeaves() const { return t_->numLeaves(); }
110 bool isFluid(Index i) const { return mom_.isFluid(i); }
111
114 void step(int momSweeps = 200, int presIters = 60, int presSweeps = 4) {
115 const Index n = t_->numLeaves();
116 // Advection ∇·(u^n u^n_c), evaluated from u^n BEFORE the predictor mutates u_.
117 // High-order (SOU/TVD) always; first-order-upwind too for the implicit-FOU
118 // deferred correction (the explicit term is ρ·(HO − FOU); FOU is implicit in
119 // the momentum operator, rebuilt here from the lagged u^n).
120 std::array<std::vector<double>, 3> adv; // full explicit advection term (incl. ρ)
121 if (advect_) {
122 if (implicitFou_)
123 mom_.buildAdvectionFou(u_, rho_, uf_, faceStart_,
124 faceFieldBuilt_); // FOU advected by the div-free uf
125 for (int c = 0; c < 3; ++c) {
126 adv[c].assign(static_cast<std::size_t>(n), 0.0);
127 for (Index i = 0; i < n; ++i)
128 if (mom_.isFluid(i)) {
129 double term = rho_ * advectHO(c, i); // ρ·∇·(u u_c), high order
130 if (implicitFou_)
131 term -= mom_.fouApply(i, u_[c]); // − ρ·FOU (deferred correction)
132 adv[c][static_cast<std::size_t>(i)] = term;
133 }
134 }
135 }
136
137 // --- momentum predictor: implicit viscous (+ implicit FOU) + body force − advection ---
138 for (int c = 0; c < 3; ++c) {
139 std::vector<double> src(static_cast<std::size_t>(n), 0.0);
140 for (Index i = 0; i < n; ++i)
141 if (mom_.isFluid(i)) {
142 // incremental predictor: include the old pressure gradient −∇p^n.
143 double s = (rho_ / dt_) * u_[c][static_cast<std::size_t>(i)] + f_[c] - gradOf(p_, i, c);
144 if (advect_)
145 s -= adv[c][static_cast<std::size_t>(i)]; // HO (explicit) or HO−FOU (deferred)
146 src[static_cast<std::size_t>(i)] = s;
147 }
148 std::vector<double> b = mom_.makeRhs(src, /*u_bc=*/0.0);
149 mom_.gaussSeidel(u_[c], b, momSweeps); // u_ now holds u*
150 }
152 }
153
156 void project(int presIters = 60, int presSweeps = 4) {
157 const Index n = t_->numLeaves();
158 std::vector<double> div(static_cast<std::size_t>(n), 0.0);
159 for (Index i = 0; i < n; ++i)
160 if (mom_.isFluid(i))
161 div[static_cast<std::size_t>(i)] = divergence(u_, i);
162
163 std::fill(phi_.begin(), phi_.end(), 0.0);
165 // Standard (not quadratic) openness V-cycles: the operator L = div(α grad) is
166 // consistent with the FV divergence / ABC gradient above (D, G, L share the
167 // same face enumeration), so the collocated projection is stable across 2:1
168 // interfaces. (The quadratic C/F flux is for pure-Poisson accuracy, not here.)
169 for (int it = 0; it < presIters; ++it)
170 presMG_.vcycle(0, phi_, div);
171
172 // Build the divergence-free FACE field from u* (still in u_) + φ, before the cell correction.
174
175 for (int c = 0; c < 3; ++c)
176 for (Index i = 0; i < n; ++i)
177 if (mom_.isFluid(i))
178 u_[c][static_cast<std::size_t>(i)] -= gradOf(phi_, i, c);
179
180 // Rotational incremental pressure update: p += (ρ/dt)φ − μ ∇·u* (div is ∇·u*).
181 // The −μ∇·u* rotational term removes the projection's boundary-layer splitting
182 // error, making the steady solution dt-independent (vs plain Chorin).
183 for (Index i = 0; i < n; ++i)
184 if (mom_.isFluid(i))
185 p_[static_cast<std::size_t>(i)] += (rho_ / dt_) * phi_[static_cast<std::size_t>(i)] -
186 mu_ * div[static_cast<std::size_t>(i)];
187 }
188
192 double divergence(const std::array<std::vector<double>, 3>& vel, Index i) const {
193 double d = 0.0;
194 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double alpha) {
195 double ui = vel[axis][static_cast<std::size_t>(i)];
196 double uj = vel[axis][static_cast<std::size_t>(j)]; // solid cells hold 0
197 d += alpha * area * dir * 0.5 * (ui + uj);
198 });
199 return d / pres_.cellVolume(i);
200 }
201
202 double divNormL2(const std::array<std::vector<double>, 3>& vel) const {
203 double s = 0.0;
204 const Index n = t_->numLeaves();
205 for (Index i = 0; i < n; ++i)
206 if (mom_.isFluid(i)) {
207 double d = divergence(vel, i);
208 s += d * d;
209 }
210 return std::sqrt(s);
211 }
212
213 std::array<std::vector<double>, 3>& velocityRef() { return u_; }
214
222 const Index n = t_->numLeaves();
223 for (Index i = 0; i < n; ++i) {
224 Index s = faceStart_[static_cast<std::size_t>(i)];
225 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double) {
226 const double uface =
227 0.5 * (u_[axis][static_cast<std::size_t>(i)] + u_[axis][static_cast<std::size_t>(j)]);
228 // +axis pressure gradient across the face: (φ₊−φ₋)/d, +side = dir>0 ? j : i.
229 const double gphi =
230 (dir > 0)
231 ? (phi_[static_cast<std::size_t>(j)] - phi_[static_cast<std::size_t>(i)]) / dist
232 : (phi_[static_cast<std::size_t>(i)] - phi_[static_cast<std::size_t>(j)]) / dist;
233 uf_[static_cast<std::size_t>(s++)] = uface - gphi;
234 });
235 }
236 faceFieldBuilt_ = true;
237 }
238
242 double divNormFace() const {
243 double tot = 0.0;
244 const Index n = t_->numLeaves();
245 for (Index i = 0; i < n; ++i) {
246 if (!mom_.isFluid(i))
247 continue;
248 Index s = faceStart_[static_cast<std::size_t>(i)];
249 double d = 0.0;
250 pres_.forEachFaceFull(i, [&](Index, int, int dir, double area, double, double alpha) {
251 d += alpha * area * dir * uf_[static_cast<std::size_t>(s++)];
252 });
253 d /= pres_.cellVolume(i);
254 tot += d * d;
255 }
256 return std::sqrt(tot);
257 }
258
261 const std::vector<double>& faceField() const { return uf_; }
262 const std::vector<Index>& faceStart() const { return faceStart_; }
263
264 private:
265 // ---- Koren TVD advection (faithful port of flow sadv::koren/tvd + cadv::advect) ----
266 static double koren(double up_m1, double up, double down, double vel) {
267 const double num = up - up_m1, den = down - up;
268 double r = (std::fabs(den) < 1e-10) ? 0.0 : num / den;
269 double psi = std::fmax(0.0, std::fmin(2.0 * r, std::fmin((1.0 + 2.0 * r) / 3.0, 2.0)));
270 return vel * (up + 0.5 * psi * (down - up));
271 }
272 static double tvd(double LL, double L, double R, double RR, double vel) {
273 return (vel > 0.0) ? koren(LL, L, R, vel) : koren(RR, R, L, vel);
274 }
275 // High-order face value: the SHARED reconstruction (advect_recon.hpp) the device runs too.
276 double hoFace(double upup, double up, double down) const {
277 return hoFaceValue(upup, up, down, advScheme_);
278 }
279
280 // C/F-consistent high-order advection ∇·(u u_comp) at leaf i: per (sub)face (via
281 // forEachFaceFull, so a coarse cell sums its fine sub-faces — conservative), the
282 // outward advective flux α-free·area·velOut·φ_face, with φ_face the SOU/TVD
283 // reconstruction from the upwind cell + its upstream neighbour (point-probed, so it
284 // works across 2:1 levels). No advection through wall faces. The implicit-FOU half
285 // is mom_.fouApply (same faces/velocities), so the deferred correction is the
286 // exact high-order − FOU difference.
287 double advectHO(int comp, Index i) const {
288 double out = 0.0;
289 Index s = faceFieldBuilt_ ? faceStart_[static_cast<std::size_t>(i)] : 0;
290 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double area, double, double) {
291 // Advecting velocity = the divergence-free FACE field uf (built by the previous projection),
292 // so the advective flux is conservative (∇·uf = 0) — Bell–Colella–Glaz / Basilisk. Falls back
293 // to the simple cell average ½(u_i+u_j) before the first projection has built uf. (At steady
294 // state φ→0 ⇒ uf = ½(u_i+u_j), so the steady solution is unchanged; the gain is in the
295 // transient.)
296 const double uface = faceFieldBuilt_ ? uf_[static_cast<std::size_t>(s)]
297 : 0.5 * (u_[axis][static_cast<std::size_t>(i)] +
298 u_[axis][static_cast<std::size_t>(j)]);
299 ++s;
300 if (!mom_.isFluid(j))
301 return; // no advection through the immersed boundary
302 double velOut = dir * uface;
303 Index up = (velOut > 0.0) ? i : j, down = (velOut > 0.0) ? j : i;
304 int upDir = (up == i) ? -dir : dir; // upstream direction from the upwind cell
305 Index upup = pres_.periodicNeighbor(up, axis, upDir);
306 double phiUp = u_[comp][static_cast<std::size_t>(up)];
307 double phiUpUp =
308 (upup >= 0 && mom_.isFluid(upup)) ? u_[comp][static_cast<std::size_t>(upup)] : phiUp;
309 double phiFace = hoFace(phiUpUp, phiUp, u_[comp][static_cast<std::size_t>(down)]);
310 out += area * velOut * phiFace;
311 });
312 return out / pres_.cellVolume(i);
313 }
314
315 // ABC (Almgren-Bell-Colella) cell-velocity correction gradient in direction `c`:
316 // ½·(g⁻ + g⁺) of the two adjacent FACE pressure-gradients, where a CLOSED face
317 // (openness 0 — solid neighbour) contributes a ZERO gradient (it does NOT read
318 // the solid neighbour's φ). Verbatim form of flow's projectCorrectCenter
319 // (src/mac_approx_projection.hpp) — the collocated approximate projection. This
320 // is the chosen collocated coupling; do NOT substitute a Rhie–Chow face-velocity
321 // interpolation (see docs/AMR.md and the amr-octree memory).
322 double gradOf(const std::vector<double>& fld, Index i, int c) const {
323 const double pi = fld[static_cast<std::size_t>(i)];
324 double gp = 0, gm = 0;
325 int np = 0, nm = 0;
326 // Average the +axis-oriented face gradient (φ_j−φ_i)/dist over each side's
327 // (sub)faces along axis c; a closed face contributes 0 (ABC). C/F-consistent.
328 pres_.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double alpha) {
329 if (axis != c || alpha <= 1e-12)
330 return;
331 double g = (dir > 0) ? (fld[static_cast<std::size_t>(j)] - pi) / dist
332 : (pi - fld[static_cast<std::size_t>(j)]) / dist;
333 if (dir > 0) {
334 gp += g;
335 ++np;
336 } else {
337 gm += g;
338 ++nm;
339 }
340 });
341 double gpa = np ? gp / np : 0.0, gma = nm ? gm / nm : 0.0;
342 return 0.5 * (gpa + gma);
343 }
344
345 // Fluid area fraction of a face, the gradient-normalised aperture (a faithful
346 // port of flow's ccFractionCore, src/mac_cutcell.hpp): frac = 0.5 + sd/denom,
347 // sd = SDF at the face centre, denom = (|n_t1| + |n_t2|)·h0 over the two
348 // tangential axes (n = unit SDF gradient). This is a linear interface
349 // reconstruction within the face — 2nd-order accurate, unlike indicator
350 // subsampling (which is only O(1/nsub) on cut faces and made the drag 1st-order).
351 template <class SdfFn>
352 double faceFrac(SdfFn&& sdfFn, const Vec<3>& fc, int axis) const {
353 double sd = sdfFn(fc);
354 if (sd <= 0.0)
355 return 0.0;
356 Vec<3> g{};
357 for (int d = 0; d < 3; ++d) {
358 Vec<3> pp = fc, pm = fc;
359 pp[d] += h0_;
360 pm[d] -= h0_;
361 g[d] = (sdfFn(pp) - sdfFn(pm)) / (2.0 * h0_);
362 }
363 double gmag = std::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
364 if (gmag < 1e-6)
365 gmag = 1e-6;
366 int t1 = (axis + 1) % 3, t2 = (axis + 2) % 3;
367 double denom = (std::fabs(g[t1]) + std::fabs(g[t2])) / gmag * h0_;
368 if (denom < 1e-9)
369 denom = 1e-9;
370 double frac = 0.5 + sd / denom;
371 return frac < 0.0 ? 0.0 : (frac > 1.0 ? 1.0 : frac);
372 }
373
374 const Octree* t_ = nullptr;
375 Real h0_ = 1.0;
376 Vec<3> origin_{};
377 double rho_ = 1.0, mu_ = 1.0, dt_ = 1e6;
378 bool advect_ = false;
379 bool implicitFou_ = true; // implicit-FOU deferred-correction advection (stable)
380 int advScheme_ = 0; // 0 = SOU (default), 1 = Koren TVD
381 Vec<3> f_{};
382 AmrCutCell<Bits> mom_;
383 AmrPoisson<3, Bits> pres_; // openness + divergence/gradient access
384 AmrMultigrid<3, Bits> presMG_; // fast (graded-capable) pressure solve
385 std::array<std::vector<double>, 3> u_;
386 std::vector<double> phi_; // pressure-increment potential (per projection)
387 std::vector<double> p_; // accumulated pressure (rotational incremental scheme)
388 // Basilisk/ABC divergence-free FACE field: uf_[faceStart_[i]+s] is the +axis velocity through
389 // cell i's s-th (sub)face, in forEachFaceFull order. Each internal (sub)face is stored from BOTH
390 // incident cells; the orientation-based build keeps the two copies identical. At a 2:1 interface
391 // a coarse cell owns 2^(Dim-1) fine sub-faces and the fine cell owns its single face — so the
392 // face field is at the finest resolution touching each face and the coarse-cell divergence sums
393 // its sub-faces.
394 std::vector<Index> faceStart_; // CSR offsets into uf_, size n+1
395 std::vector<double> uf_; // +axis face velocity per (cell,face) slot
396 bool faceFieldBuilt_ =
397 false; // uf_ populated by a projection (else advection falls back to ½(u_i+u_j))
398};
399
400} // namespace peclet::core::amr::oracle
401
402#endif // PECLET_CORE_HAVE_MORTON
403#endif // PECLET_CORE_AMR_FLOW_ORACLE_HPP
void init(const Octree &t, Real h0)
Definition poisson.hpp:53
void setOrigin(const Vec< Dim > &o)
Definition poisson.hpp:62
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:261
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:220
void buildOpenness(OpenFn &&openFn)
Build face openness from a geometry callable openFn(faceCentreWorld, axis) -> [0,1] (1 = fully fluid,...
Definition poisson.hpp:86
Real cellVolume(Index i) const
Definition poisson.hpp:156
void step(int momSweeps=200, int presIters=60, int presSweeps=4)
One Stokes projection step.
const std::vector< double > & velocity(int c) const
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 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 >{})
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)
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).
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.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