flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_bc.cpp
Go to the documentation of this file.
1// VoF rung V-BC (WO-R) — two-phase open boundaries: inflow / outflow / wall colour, the
2// out-of-domain flux rule, the inflow property ghost, and the variable-density outflow correction.
3//
4// Gates, in the order they run:
5//
6// A THE MASK. `vof::buildOutsideMask` must mark exactly the ghost cells that lie outside the
7// GLOBAL domain on a non-periodic axis — the same global-clamp predicate `clampFill` uses,
8// for the same reason (a per-block test would be decomposition-dependent).
9//
10// B THE FLUX RULE IS INERT WHERE THE MASK IS 0. `wyFaceFluxBc` must return `wyFaceFlux`'s value
11// BIT FOR BIT for every donor that is not marked. This is the algebraic half of gate G5: the
12// new branch cannot move an interior flux even by a rounding bit.
13//
14// C THE COLOUR BUDGET (WO-R gate G1, kinematic). A 32x32x64 box, uniform inflow at -z, outflow
15// at +z, walls elsewhere, ratio 1: a liquid slug is injected for 100 steps and then the inlet
16// switches to gas for 400 more. The gate is the EXACT budget
17// sum(eps C)(t) - sum(inflow liquid) + sum(outflow liquid) = const
18// where the fluxes are the advector's OWN boundary face fluxes (`vof_bc_volumes`). It closes
19// to round-off because a WY flux is computed once per face and enters exactly one inner
20// cell's update with one sign — the same telescoping that gives exact conservation in a
21// closed box, now with the boundary faces counted instead of cancelling.
22// Also gated: the slug leaves and nothing is left behind (sum C -> 0), and C stays in [0,1].
23//
24// D THE INFLOW PROPERTY GHOST (WO-R item 5). With rho(C) a LinearMix closure and a LIQUID
25// inlet into a GAS domain, the rho ghost band of the inflow face must read rho_liquid, not
26// the interior's rho_gas. Without the repair the inlet FACE density (the arithmetic mean of
27// inner and ghost, used by BOTH the momentum time term and the projection coefficient) is
28// wrong by up to the full density ratio.
29//
30// E BACKFLOW (inletOutlet). On an outflow face whose velocity REVERSES, the colour ghost must
31// carry the prescribed backflow colour; where it does not reverse, the zero-gradient copy
32// must survive untouched.
33//
34// F THE VARIABLE-DENSITY OUTFLOW CORRECTION KERNEL (WO-R item 4). `bcCorrectOutflowVar` must
35// reduce to `bcCorrectOutflow` BITWISE at uniform rho == rho0, and must differ from it by
36// exactly the mobility factor rho0/rho_f otherwise.
37//
38// F2 ... AND THE VERDICT ON USING IT, which WO-R2 item 1 INVERTED to YES. The solver must reach
39// the sibling kernel (that is what `set_outflow_rho_correction` switches), and the resulting
40// divergence must be measured on the PROJECTED field — `max_open_divergence()` re-imposes the
41// zero-gradient outflow face before measuring and so cannot see the difference at all (it
42// reads 1.26e-02 for both branches at ratio 10, and 1.26e-09 where the projected field is at
43// 1.41e-17). WO-R measured 8.8e-10 plain / 9.2e-03 with the factor, because the multigrid
44// overwrote the variable-density coefficient at the outflow row with the literal openness
45// 1.0. With that operator fixed (`CutcellMG::setOutflowCoefficient`) the same probe reads
46// 9.97e-05 plain / 8.31e-10 with the factor, and the factor is the DEFAULT.
47//
48// G V5a x V-BC COMPOSITION (WO-R2 item 2). The cut-cell flux path and the out-of-domain donor
49// rule must act TOGETHER: a domain-face flux in a geometry-carrying block is
50// `o_f * C_datum * a`, and the per-face ledger must count that same `o_f`-weighted number, so
51// that the exact budget of gate C survives an immersed packing. Two scenes: a sphere array
52// standing 3 cells clear of the inlet and outlet planes, and a second one where a sphere CUTS
53// the outlet plane (o_f < 1 on a domain face).
54#include <cmath>
55#include <cstdio>
56#include <cstdlib>
57#include <Kokkos_Core.hpp>
58#include <vector>
59
60#include "flow_ibm.hpp"
61
62namespace {
67
68int failures = 0;
69#define CHECK(cond) \
70 do { \
71 if (!(cond)) { \
72 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
73 ++failures; \
74 } \
75 } while (0)
76
77std::size_t idx(int x, int y, int z, int nx, int ny) {
78 return (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
79}
80
81// ---------------------------------------------------------------------------------- A: the mask
82void maskGate() {
83 const int n = 8, g = 3;
84 const I3 e{n + 2 * g, n + 2 * g, n + 2 * g};
85 const long len = (long)e.x * e.y * e.z;
86 UCField m("mask", len);
87 // x periodic, y and z not; single block, so the origin is 0 and the global size is n.
88 peclet::flow::vof::buildOutsideMask(m, e, g, I3{0, 0, 0}, I3{n, n, n}, true, false, false);
89 auto h = Kokkos::create_mirror_view(m);
90 Kokkos::deep_copy(h, m);
91 long marked = 0, wrong = 0;
92 for (int z = 0; z < e.z; ++z)
93 for (int y = 0; y < e.y; ++y)
94 for (int x = 0; x < e.x; ++x) {
95 const bool want = (y - g < 0 || y - g >= n) || (z - g < 0 || z - g >= n);
96 const bool got = h(L3(x, y, z, e)) != 0u;
97 if (got != want)
98 ++wrong;
99 marked += got ? 1 : 0;
100 }
101 std::printf("A outside mask: %ld cells marked, %ld disagreements with the global predicate\n",
102 marked, wrong);
103 CHECK(wrong == 0);
104 CHECK(marked > 0);
105}
106
107// -------------------------------------------------------------- B: the flux rule is bitwise inert
108void fluxInertGate() {
109 const int n = 6, g = 3;
110 const I3 e{n + 2 * g, n + 2 * g, n + 2 * g};
111 const long len = (long)e.x * e.y * e.z;
112 SField c("c", len), mx("mx", len), my("my", len), mz("mz", len), al("al", len);
113 UCField zero("zero", len); // all 0: nothing is "outside"
114 auto hc = Kokkos::create_mirror_view(c);
115 auto hm = Kokkos::create_mirror_view(mx);
116 auto hn = Kokkos::create_mirror_view(my);
117 auto ho = Kokkos::create_mirror_view(mz);
118 auto ha = Kokkos::create_mirror_view(al);
119 unsigned seed = 12345u;
120 auto rnd = [&seed]() {
121 seed = seed * 1103515245u + 12345u;
122 return (double)((seed >> 16) & 0x7fff) / 32767.0;
123 };
124 for (long i = 0; i < len; ++i) {
125 hc(i) = rnd(); // a fully mixed field: every donor takes the geometric branch
126 const double a0 = rnd() - 0.5, a1 = rnd() - 0.5, a2 = rnd() - 0.5;
127 const double s = std::fabs(a0) + std::fabs(a1) + std::fabs(a2) + 1e-12;
128 hm(i) = a0 / s;
129 hn(i) = a1 / s;
130 ho(i) = a2 / s;
131 ha(i) = peclet::flow::vof::plicAlpha(hm(i), hn(i), ho(i), hc(i));
132 }
133 Kokkos::deep_copy(c, hc);
134 Kokkos::deep_copy(mx, hm);
135 Kokkos::deep_copy(my, hn);
136 Kokkos::deep_copy(mz, ho);
137 Kokkos::deep_copy(al, ha);
138 Kokkos::deep_copy(zero, (unsigned char)0);
139
140 long diff = 0;
141 for (int d = 0; d < 3; ++d) {
142 const long sd = d == 0 ? 1 : (d == 1 ? e.x : (long)e.x * e.y);
143 long nd = 0;
144 Kokkos::parallel_reduce(
145 "bc::flux_inert",
147 {e.x - 1, e.y - 1, e.z - 1}),
148 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
149 const long p = L3(x, y, z, e);
150 for (int k = -3; k <= 3; ++k) {
151 const double a = 0.07 * k; // both signs and a == 0
152 const double f0 = peclet::flow::vof::wyFaceFlux(a, p, sd, d, c, mx, my, mz, al);
153 const double f1 = peclet::flow::vof::wyFaceFluxBc(a, p, sd, d, c, mx, my, mz, al, zero);
154 if (!(f0 == f1))
155 ++acc;
156 }
157 },
158 nd);
159 Kokkos::fence();
160 diff += nd;
161 }
162 std::printf("B wyFaceFluxBc vs wyFaceFlux with an all-zero mask: %ld bitwise disagreements\n",
163 diff);
164 CHECK(diff == 0);
165}
166
167// ------------------------------------------------------------------- C: the exact colour budget
168// WO-R gate G1, KINEMATIC. Uniform inflow at -z, outflow at +z, walls elsewhere; the advecting
169// field is the uniform w = W, which is EXACTLY discretely divergence-free (du/dx = dv/dy = 0 and w
170// is constant along z), so the budget's floor is round-off and nothing else. `max_open_divergence`
171// is reported to show it. (The COUPLED variant — the same box driven by step() — is in the study
172// script `tests/study/vof_open_boundaries.py`, where the pressure iteration count is recorded
173// against its cap; here the point is the advection identity in isolation.)
174void budgetGate(int nz, int nSlug, int nAfter) {
175 const int nx = 32, ny = 32;
176 const double W = 1.0, dt = 0.2;
177 peclet::flow::IbmSolver s(nx, ny, nz);
178 s.setRho(1.0);
179 s.setMu(0.0);
180 s.setDt(dt);
181 for (int f = 0; f < 4; ++f)
182 s.setDomainBc(f, 1, 0, 0, 0);
183 s.setDomainBc(4, 2, 0.0, 0.0, W); // inflow at -z, uniform
184 s.setDomainBc(5, 3, 0, 0, 0); // outflow at +z
185 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 10.0));
186 s.enableVof();
187 s.setVof(std::vector<double>((std::size_t)nx * ny * nz, 0.0));
188 s.setField("w", std::vector<double>((std::size_t)nx * ny * nz, W));
189 s.setVofInflow(4, 1.0);
190 s.setVofBackflow(5, 0.0);
191
192 double budget0 = 0.0, drift = 0.0, minC = 1.0, maxC = 0.0, ledger = 0.0, peak = 0.0;
193 std::vector<double> peakProfile;
194 for (int i = 0; i < nSlug + nAfter; ++i) {
195 if (i == nSlug)
196 s.setVofInflow(4, 0.0); // the slug ends; gas follows
197 s.advectVof();
198 const auto d = s.vofDiagnostics();
199 const auto v = s.vofBcVolumes();
200 for (int f = 0; f < 6; ++f)
201 ledger += v[f];
202 const double budget = d.sumC - ledger;
203 if (i == 0)
204 budget0 = budget;
205 drift = std::fmax(drift, std::fabs(budget - budget0));
206 minC = std::fmin(minC, d.minC);
207 maxC = std::fmax(maxC, d.maxC);
208 if (d.sumC > peak) { // the slug at its fullest: keep its z-profile
209 peak = d.sumC;
210 const auto C = s.getVof();
211 peakProfile.assign(nz, 0.0);
212 for (int z = 0; z < nz; ++z) {
213 double a = 0.0;
214 for (int y = 0; y < ny; ++y)
215 for (int x = 0; x < nx; ++x)
216 a += C[idx(x, y, z, nx, ny)];
217 peakProfile[z] = a / (nx * ny);
218 }
219 }
220 }
221 { // the slug leaves with its length intact: report the plateau length and the edge width
222 int plateau = 0, edges = 0;
223 for (double v : peakProfile) {
224 if (v > 1.0 - 1e-12)
225 ++plateau;
226 else if (v > 1e-12)
227 ++edges;
228 }
229 std::printf(
230 "C slug at its fullest: sum(C) %.6g over %d full planes + %d partial (expected "
231 "length %.4g cells)\n",
232 peak, plateau, edges, peak / (nx * ny));
233 }
234 const auto d = s.vofDiagnostics();
235 const auto tot = s.vofBcVolumesTotal();
236 const double injected = tot[4];
237 const double rel = (injected > 0.0) ? drift / injected : drift;
238 std::printf(
239 "C budget: injected %.10g, left %.10g, remaining sum(C) %.3e; |budget drift| %.3e "
240 "(rel %.3e)\n",
241 injected, -tot[5], d.sumC, drift, rel);
242 std::printf("C C in [%.3e, %.17g]; max|div(open u)| of the prescribed field %.3e\n", minC, maxC,
243 s.maxOpenDivergence());
244 // the slug's length is preserved: what went in came out
245 std::printf("C in %.10g vs out %.10g (relative closure %.3e)\n", injected, -tot[5],
246 std::fabs(injected + tot[5]) / injected);
247 CHECK(rel < 1e-12);
248 CHECK(minC > -1e-12);
249 CHECK(maxC < 1.0 + 1e-12);
250 CHECK(d.sumC < 1e-9 * injected); // the slug left; nothing behind
251 CHECK(injected > 0.0);
252 CHECK(std::fabs(injected + tot[5]) / injected < 1e-9);
253}
254
255// ----------------------------------------------------------- D: the inflow property ghost (item 5)
256void propGhostGate() {
257 const int nx = 8, ny = 8, nz = 16;
258 const double rhoG = 1.0, rhoL = 1000.0;
259 peclet::flow::IbmSolver s(nx, ny, nz);
260 s.setRho(rhoL);
261 s.setMu(1.0);
262 s.setDt(0.05);
263 for (int f = 0; f < 4; ++f)
264 s.setDomainBc(f, 1, 0, 0, 0);
265 s.setDomainBc(4, 2, 0.0, 0.0, 0.5);
266 s.setDomainBc(5, 3, 0, 0, 0);
267 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 10.0));
268 s.setPressureChebyshev(true, 400, 1e-12);
269 s.enableVof();
270 s.setVof(std::vector<double>((std::size_t)nx * ny * nz, 0.0)); // a GAS domain
271 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "",
272 {rhoG, rhoL - rhoG, 0.0, 0.0});
273 s.setVofInflow(4, 1.0); // ... fed by a LIQUID inlet
274 s.step();
275
276 auto rv = s.fieldView("rho");
277 auto h = Kokkos::create_mirror_view(rv);
278 Kokkos::deep_copy(h, rv);
279 const int G = 2, ex = nx + 2 * G, ey = ny + 2 * G;
280 auto at = [&](int x, int y, int z) { return h(idx(x, y, z, ex, ey)); };
281 const double ghost = at(G + nx / 2, G + ny / 2, G - 1); // first ghost outside the -z inflow
282 const double inner = at(G + nx / 2, G + ny / 2, G); // first inner cell (gas)
283 const double face = 0.5 * (ghost + inner);
284 std::printf("D inflow -z: rho_ghost %.6g (want %.6g), rho_inner %.6g, rho_face %.6g\n", ghost,
285 rhoL, inner, face);
286 CHECK(std::fabs(ghost - rhoL) < 1e-10 * rhoL);
287 // and the Neumann copy would have given rho_face == rho_gas; record the size of the repair
288 std::printf("D the Neumann-copy face density would have been %.6g (ratio %.4g)\n", inner,
289 face / inner);
290 CHECK(face / inner > 100.0);
291}
292
293// ------------------------------------------------------------------------------- E: backflow
294// A pure kernel test of the inletOutlet rule: the sign convention (which velocity sign is "back
295// INTO the domain" on each side), which ghost layers are written, and that a face where the fluid
296// LEAVES keeps its zero-gradient band untouched. Doing it here rather than through a solve is
297// deliberate: the quantity under test is the rule, and a solve would only reach it through a
298// reversal that is itself hard to arrange reproducibly.
299void backflowGate() {
300 const int n = 8, g = 3;
301 const I3 e{n + 2 * g, n + 2 * g, n + 2 * g};
302 const long len = (long)e.x * e.y * e.z;
303 for (int side = 0; side < 2; ++side) {
304 SField c("c", len), w("w", len);
305 Kokkos::deep_copy(c, 1.0); // clampFill's zero-gradient band: liquid everywhere
306 auto hw = Kokkos::create_mirror_view(w);
307 Kokkos::deep_copy(hw, 0.0);
308 // the advector's HIGH-face convention: the boundary z-face of side 0 is the +z face of the
309 // last ghost cell (z-index g-1); of side 1 it is the +z face of the last inner cell.
310 const int fa = (side == 0) ? (g - 1) : (e.z - g - 1);
311 for (int y = 0; y < e.y; ++y)
312 for (int x = 0; x < e.x; ++x)
313 hw(L3(x, y, fa, e)) = (x < e.x / 2) ? -0.3 : +0.3;
314 Kokkos::deep_copy(w, hw);
315 peclet::flow::vof::bcColourBackflow(c, e, g, /*a=*/2, side, w, /*backflow=*/0.0);
316 auto hc = Kokkos::create_mirror_view(c);
317 Kokkos::deep_copy(hc, c);
318 // inward is +z on side 0 and -z on side 1
319 const int inwardHalf = (side == 0) ? 1 : 0; // 1 => the x >= e.x/2 half has inward velocity
320 long wrong = 0, layers = 0;
321 const int lo = (side == 0) ? 0 : (e.z - g);
322 for (int y = 0; y < e.y; ++y)
323 for (int x = 0; x < e.x; ++x) {
324 const bool inward = (x >= e.x / 2) == (inwardHalf == 1);
325 for (int k = 0; k < g; ++k) {
326 const double v = hc(L3(x, y, lo + k, e));
327 if (v != (inward ? 0.0 : 1.0))
328 ++wrong;
329 if (inward && v == 0.0)
330 ++layers;
331 }
332 }
333 std::printf(
334 "E backflow side %d: %ld cells disagree with the rule, %ld ghost cells took the "
335 "backflow colour (all %d layers)\n",
336 side, wrong, layers, g);
337 CHECK(wrong == 0);
338 CHECK(layers == (long)(e.x / 2) * e.y * g);
339 }
340}
341
342// ------------------------------------------------- F: the variable-density outflow correction
343void outflowVarGate() {
344 const int n = 8, g = 2;
345 const peclet::flow::B3 e{n + 2 * g, n + 2 * g, n + 2 * g};
346 const long len = (long)e.x * e.y * e.z;
347 peclet::flow::BField f0("f0", len), f1("f1", len), phi("phi", len), rho("rho", len);
348 auto hp = Kokkos::create_mirror_view(phi);
349 auto hr = Kokkos::create_mirror_view(rho);
350 for (long i = 0; i < len; ++i) {
351 hp(i) = 0.001 * (double)((i * 37) % 101);
352 hr(i) = 1.0;
353 }
354 Kokkos::deep_copy(phi, hp);
355 Kokkos::deep_copy(rho, hr);
356 Kokkos::deep_copy(f0, 0.0);
357 Kokkos::deep_copy(f1, 0.0);
358 peclet::flow::bcCorrectOutflow(f0, phi, e, g, 2);
359 peclet::flow::bcCorrectOutflowVar(f1, phi, rho, 1.0, e, g, 2, false);
360 auto h0 = Kokkos::create_mirror_view(f0);
361 auto h1 = Kokkos::create_mirror_view(f1);
362 Kokkos::deep_copy(h0, f0);
363 Kokkos::deep_copy(h1, f1);
364 long diff = 0;
365 for (long i = 0; i < len; ++i)
366 if (!(h0(i) == h1(i)))
367 ++diff;
368 std::printf(
369 "F bcCorrectOutflowVar at uniform rho == rho0: %ld bitwise disagreements with "
370 "bcCorrectOutflow\n",
371 diff);
372 CHECK(diff == 0);
373
374 // and at a real jump it must differ by exactly rho0/rho_f
375 for (long i = 0; i < len; ++i)
376 hr(i) = 1000.0;
377 Kokkos::deep_copy(rho, hr);
378 Kokkos::deep_copy(f1, 0.0);
379 peclet::flow::bcCorrectOutflowVar(f1, phi, rho, 1.0, e, g, 2, false);
380 Kokkos::deep_copy(h1, f1);
381 double worst = 0.0;
382 const long sa = (long)e.x * e.y;
383 for (int y = g; y < e.y - g; ++y)
384 for (int x = g; x < e.x - g; ++x) {
385 const long bf = (long)x + (long)y * e.x + (long)(e.z - g) * sa;
386 if (h0(bf) != 0.0)
387 worst = std::fmax(worst, std::fabs(h1(bf) / h0(bf) - 1.0 / 1000.0));
388 }
389 std::printf("F at rho == 1000 rho0 the correction is scaled by 1/1000 to %.3e\n", worst);
390 CHECK(worst < 1e-14);
391}
392
393// ------------------------------------- F2: the outflow correction is REACHED by the solver
394// Gate F proves the kernel. This proves the SOLVER calls it: two identical stratified outflow
395// solvers differing only in `setOutflowRhoCorrection`, compared on the OUTFLOW BOUNDARY FACE
396// itself. That face is the low face of the first GHOST cell, so `getVelocity()` (inner cells)
397// cannot see it — reading it needs `fieldView("u")`. A gate that used getVelocity() would report
398// "no difference" and be measuring the wrong quantity.
399double outflowFaceProbe(bool corr, double ratio, double& divOut, double& uInner, double& divMut) {
400 const int nx = 32, ny = 4, nz = 16;
401 const double rhoL = 100.0, rhoG = rhoL / ratio;
402 peclet::flow::IbmSolver s(nx, ny, nz);
403 s.setRho(rhoL);
404 s.setMu(5.0);
405 s.setDt(ratio > 1.0 ? 0.1 : 0.2);
406 s.setDomainBc(4, 1, 0, 0, 0);
407 s.setDomainBc(5, 1, 0, 0, 0);
408 s.setDomainBc(0, 2, 0.1, 0, 0);
409 s.setDomainBc(1, 3, 0, 0, 0);
410 s.setVelocityIterations(60);
411 s.setPressureLevels(6);
412 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1e30));
413 if (ratio > 1.0) {
414 s.enableVof();
415 std::vector<double> C((std::size_t)nx * ny * nz, 0.0);
416 for (int z = 0; z < nz / 2; ++z)
417 for (int y = 0; y < ny; ++y)
418 for (int x = 0; x < nx; ++x)
419 C[idx(x, y, z, nx, ny)] = 1.0; // liquid in the lower half
420 s.setVof(C);
421 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "",
422 {rhoG, rhoL - rhoG, 0.0, 0.0});
423 }
424 s.setPressureFcg(true, 400, 1e-12); // driver LAST (setDensityMode reselects Chebyshev)
425 s.setOutflowRhoCorrection(corr);
426 for (int i = 0; i < 5; ++i)
427 s.step();
428 auto uv = s.fieldView("u");
429 auto h = Kokkos::create_mirror_view(uv);
430 Kokkos::deep_copy(h, uv); // BEFORE any diagnostic: maxOpenDivergence() would erase the face
431 const int G = 2, ex = nx + 2 * G, ey = ny + 2 * G;
432 const double face = h(idx(G + nx, G + ny / 2, G + nz - 1, ex, ey)); // gas side of the outlet
433 uInner = h(idx(G + nx - 1, G + ny / 2, G + nz - 1, ex, ey));
434 divOut = s.maxOpenDivergenceProjected();
435 divMut = s.maxOpenDivergence(); // AFTER the projected read: this one mutates u
436 return face;
437}
438
439void outflowReachedGate() {
440 for (double ratio : {1.0, 10.0}) {
441 double d0 = 0, d1 = 0, i0 = 0, i1 = 0, m0 = 0, m1 = 0;
442 const double on = outflowFaceProbe(true, ratio, d0, i0, m0);
443 const double off = outflowFaceProbe(false, ratio, d1, i1, m1);
444 std::printf(
445 "F2 ratio %-6g outflow face u: with the 1/rho_f factor %.17g, without %.17g "
446 "(delta %.3e)\n",
447 ratio, on, off, on - off);
448 std::printf(
449 "F2 ratio %-6g last inner u %.17g -> the face is %s the zero-gradient copy; "
450 "max|div| projected %.3e / %.3e, mutating diagnostic %.3e / %.3e\n",
451 ratio, i0, (on == i0) ? "STILL" : "NOT", d0, d1, m0, m1);
452 if (ratio == 1.0) { // constant density: the factor is exactly 1, both paths bitwise equal
453 CHECK(on == off);
454 CHECK(on != i0); // ... but the projection's correction IS there
455 CHECK(d0 < 1e-15); // and the projected field is divergence-free to machine precision
456 } else {
457 CHECK(on != off); // the solver must actually reach the sibling kernel
458 CHECK(on != i0);
459 // THE VERDICT, INVERTED BY WO-R2 ITEM 1. WO-R measured the plain correction (d1) as the
460 // consistent one because the multigrid overwrote the variable-density coefficient at the
461 // Dirichlet domain face with the literal openness 1.0. With the operator fixed
462 // (CutcellMG::setOutflowCoefficient) the outflow row carries open_f*rho0/rho_f, so the
463 // correction that matches it is the one WITH the factor (d0) — and it is now the DEFAULT.
464 // Measured here: d0 8.31e-10, d1 9.97e-05.
465 CHECK(d0 < 1e-8);
466 CHECK(d1 > 1e3 * d0);
467 }
468 }
469}
470
471// ------------------------------------------ G: the V5a cut-cell path composed with the V-BC rule
472struct Sph {
473 double x, y, z, r;
474};
475std::vector<double> sphereSdf(int nx, int ny, int nz, const std::vector<Sph>& sp) {
476 std::vector<double> f((std::size_t)nx * ny * nz, 1e30);
477 for (int z = 0; z < nz; ++z)
478 for (int y = 0; y < ny; ++y)
479 for (int x = 0; x < nx; ++x) {
480 double d = 1e30;
481 for (const Sph& q : sp) {
482 const double dx = x + 0.5 - q.x, dy = y + 0.5 - q.y, dz = z + 0.5 - q.z;
483 d = std::fmin(d, std::sqrt(dx * dx + dy * dy + dz * dz) - q.r);
484 }
485 f[idx(x, y, z, nx, ny)] = d; // > 0 outside the spheres == fluid
486 }
487 return f;
488}
489
490// A slug of liquid injected at the -z inlet, carried through a packing by the solver's OWN
491// projected steady field, and out through the +z outlet. Kinematic: the field is frozen after the
492// Stokes solve, so the only thing moving is the colour.
493void composedGate(bool cutOutlet, int nSlug, int nAfter) {
494 const int nx = 20, ny = 20, nz = 32;
495 const double W = 1.0;
496 peclet::flow::IbmSolver s(nx, ny, nz);
497 s.setRho(1.0);
498 s.setMu(0.5);
499 s.setDt(0.25);
500 for (int f = 0; f < 4; ++f)
501 s.setDomainBc(f, 1, 0, 0, 0);
502 s.setDomainBc(4, 2, 0.0, 0.0, W); // inflow at -z
503 s.setDomainBc(5, 3, 0, 0, 0); // outflow at +z
504 // Four spheres in the middle third, every one of them >= 3 cells clear of z = 0 and z = nz.
505 std::vector<Sph> sp{{6, 6, 12, 4.0}, {14, 14, 12, 4.0}, {6, 14, 21, 4.0}, {14, 6, 21, 4.0}};
506 if (cutOutlet)
507 sp.push_back({10, 10, (double)nz, 4.5}); // centred ON the outlet plane: it cuts o_f there
508 s.setSolid(sphereSdf(nx, ny, nz, sp), true);
509 s.setVelocityIterations(60);
510 s.setPressureLevels(4);
511 s.setPressureIterations(400);
512 // The steady field this gate freezes is measured against a 1e-10 divergence floor, and reaching
513 // that floor on a cut-cell outflow duct needs the EXACT (double, flux-form) level-0 apply: with
514 // the float bands the projected max|div(open u)| lands at 9e-11 / 1.6e-09 instead of 2e-15 /
515 // 7e-14. `enableVof()` below turns the flag on, but it is turned on too late for these 80 steps,
516 // so ask for it here. (Until the QUALITY_PLAN D3 env-var sweep the flag was PROCESS-WIDE, so an
517 // earlier gate's enableVof() silently supplied it to this one -- exactly the accident D3 exists
518 // to prevent, and the reason this line reads as new behaviour rather than a new requirement.)
519 s.setPressureExactResidual(true);
520 for (int i = 0; i < 80; ++i) // to a steady duct flow through the packing
521 s.step();
522 const double div = s.maxOpenDivergenceProjected();
523 const long pit = s.lastPressureIterations();
524 s.enableVof();
525 // The upper half starts LIQUID so that liquid leaves through the outlet from step 1 — with an
526 // empty domain the slug never reaches the +z plane inside a test-sized run and the outflow half
527 // of the ledger (the half a solid CUTS in the second scene) would not be exercised at all.
528 std::vector<double> c0((std::size_t)nx * ny * nz, 0.0);
529 for (int z = nz / 2; z < nz; ++z)
530 for (int y = 0; y < ny; ++y)
531 for (int x = 0; x < nx; ++x)
532 c0[idx(x, y, z, nx, ny)] = 1.0;
533 s.setVof(c0);
534 s.setVofInflow(4, 1.0);
535 s.setVofBackflow(5, 0.0);
536 const double vol0 = s.vofDiagnostics().volume;
537 double budget0 = 0.0, drift = 0.0, solidSum = 0.0, ledger = 0.0, clip = 0.0;
538 double mnF = 1e30, mxF = -1e30;
539 // dt is re-picked from the solver's OWN interface-local CUT Courant number every step, the
540 // pattern the study scripts use: the cut-cell Courant is up to 6x the plain one (WO-Q finding 1)
541 // and it grows as the slug reaches the packing's constrictions, so a dt sized once at t = 0
542 // trips the Weymouth-Yue cap part-way through. Conservation is per-step algebraic, so a varying
543 // dt does not weaken the budget identity at all.
544 double dtA = 0.25;
545 for (int i = 0; i < nSlug + nAfter; ++i) {
546 const double c = s.vofMaxCourant(); // at the current dt
547 dtA = (c > 0.0) ? std::fmin(0.25, dtA * 0.12 / c) : 0.25;
548 s.setDt(dtA);
549 if (i == nSlug)
550 s.setVofInflow(4, 0.0);
551 s.advectVof();
552 const auto d = s.vofDiagnostics();
553 const auto v = s.vofBcVolumes();
554 for (int f = 0; f < 6; ++f)
555 ledger += v[f];
556 const double budget = d.volume - ledger;
557 if (i == 0)
558 budget0 = budget;
559 drift = std::fmax(drift, std::fabs(budget - budget0));
560 solidSum = std::fmax(solidSum, std::fabs(d.solidSumC));
561 clip += d.clippedVolume;
562 mnF = std::fmin(mnF, d.minCFluid);
563 mxF = std::fmax(mxF, d.maxCFluid);
564 }
565 const auto d1 = s.vofDiagnostics();
566 const auto tot = s.vofBcVolumesTotal();
567 const double injected = tot[4];
568 const double rel = drift / (vol0 + injected);
569 std::printf(
570 "G %s: %d solid + %d cut cells, max|div(open u)| %.3e (pressure %ld), final dt %.4g\n"
571 " sum(eps_eff C) %.10g -> %.10g; injected %.10g, LEFT %.10g\n"
572 " |budget drift| %.3e (rel %.3e), max|sum C over solid| %.3e, clipped %.3e,\n"
573 " C over uncut fluid in [%.3e, %.17g]\n",
574 cutOutlet ? "a sphere CUTS the outlet plane" : "packing clear of both planes", d1.solidCells,
575 d1.cutCells, div, pit, dtA, vol0, d1.volume, injected, -tot[5], drift, rel, solidSum, clip,
576 mnF, mxF);
577 CHECK(div <= 1e-10);
578 CHECK(rel < 1e-12);
579 CHECK(solidSum == 0.0);
580 CHECK(injected > 0.0);
581 CHECK(-tot[5] > 0.1 * vol0); // the outflow face really carried liquid out
582 CHECK(mnF >= -1e-12);
583 CHECK(mxF <= 1.0 + 1e-12);
584 if (cutOutlet)
585 CHECK(d1.cutCells > 0);
586}
587
588} // namespace
589
590int main(int argc, char** argv) {
591 Kokkos::initialize(argc, argv);
592 {
593 maskGate();
594 fluxInertGate();
595 outflowVarGate();
596 outflowReachedGate();
597 backflowGate();
598 propGhostGate();
599 const bool quick = std::getenv("PECLET_VOF_BC_QUICK") != nullptr;
600 budgetGate(quick ? 32 : 64, quick ? 40 : 100, quick ? 200 : 400);
601 composedGate(false, quick ? 20 : 40, quick ? 120 : 300);
602 composedGate(true, quick ? 20 : 40, quick ? 120 : 300);
603 }
604 Kokkos::finalize();
605 if (failures == 0) {
606 std::printf("OK\n");
607 return 0;
608 }
609 std::fprintf(stderr, "%d failure(s)\n", failures);
610 return 1;
611}
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
float worst
Definition cmp.py:18
x
Definition cmp.py:24
y
Definition cmp.py:24
void zero(const ScalarVec &y)
double wyFaceFluxBc(double a, long p, long sd, int dir, const SF &c, const SF &mx, const SF &my, const SF &mz, const SF &alpha, const MF &outside, double eps=0.0)
WO-R sibling of wyFaceFlux for a face whose DONOR lies outside the global domain.
void bcColourBackflow(SField f, I3 e, int g, int a, int s, SField un, double backflow)
inletOutlet on an outflow face: where the boundary face velocity points INTO the domain,...
void buildOutsideMask(UCField m, I3 e, int g, I3 o, I3 gs, bool px, bool py, bool pz)
1 for every ghost cell of the block that lies OUTSIDE the global domain on a non-periodic axis,...
Definition colour_bc.hpp:84
double wyFaceFlux(double a, long p, long sd, int dir, const SF &c, const SF &mx, const SF &my, const SF &mz, const SF &alpha, double eps=0.0)
Signed Eulerian donor-cell flux through the dir-face between cell p and cell p + sd,...
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
void bcCorrectOutflow(BField f, BField phi, B3 ext, int g, int a, double wa=1.0)
Definition mac_bc.hpp:341
long L3(int x, int y, int z, I3 e)
Kokkos::View< double *, BMem > BField
Definition mac_bc.hpp:21
void bcCorrectOutflowVar(BField f, BField phi, BField rho, double rho0, B3 ext, int g, int a, bool harmonic, double wa=1.0)
Definition mac_bc.hpp:371
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
static std::vector< double > sphereSdf()
static constexpr int G
#define CHECK(cond)
static std::vector< double > ledger(IbmSolver &s)