flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_cutcell.cpp
Go to the documentation of this file.
1// VoF rung V5a (WO-Q) — the colour field transported THROUGH an immersed solid: openness-weighted
2// Weymouth-Yue fluxes, the update in fluid-volume units, and the neutral (90 deg) solid-band fill.
3//
4// Gates, in the order they are run:
5//
6// G3 THE TWO-BLOCK BRIDGE WITH GEOMETRY. The openness the solver hands the advector must be the
7// SAME face, i.e. shifted by one cell along the component's own axis exactly as the face
8// velocity is (`vof::copyFaceVelocity`, WO-J's 35 %-of-the-volume trap, here for the
9// openness). Two halves: (a) the geometry fields themselves, against an INDEPENDENT build of
10// `buildOpenness` / `buildCellFraction` on a g=3 block with the shift written out by hand;
11// (b) the transported colour, against a standalone `WyAdvector` driven by the same velocity
12// and the same geometry — bitwise.
13//
14// G2 CONSERVATION THROUGH A PACKING (kinematic). A periodic sphere array, the single-phase
15// solver run to a Stokes steady state, then that PROJECTED velocity frozen and a liquid slab
16// advected by `advect_vof` alone. The conserved functional is `sum eps_eff C` (see
17// `vof/cutcell.hpp` rule 1); the floor is the projection's own `max|div(open u)|` and both are
18// reported. Also: zero colour in solid cells EXACTLY, boundedness in the uncut fluid cells,
19// and the clipped volume.
20//
21// G5 THE 90-DEGREE NEUTRAL FILL. A liquid cap on a flat SDF wall at a HALF-INTEGER z (so the wall
22// cells are genuinely cut) with surface tension: a hemisphere is the equilibrium of the
23// neutral fill, and the apparent contact angle measured from the cap volume, its height and
24// its contact radius must come back at 90 deg.
25#include <cmath>
26#include <cstdio>
27#include <cstdlib>
28#include <Kokkos_Core.hpp>
29#include <vector>
30
31#include "flow_ibm.hpp"
32#include "vof/plic.hpp"
33#include "vof_advect_scenes.hpp"
34
35namespace {
43
44int failures = 0;
45#define CHECK(cond) \
46 do { \
47 if (!(cond)) { \
48 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
49 ++failures; \
50 } \
51 } while (0)
52
53std::size_t idx(int x, int y, int z, int nx, int ny) {
54 return (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
55}
56
57// ---------------------------------------------------------------- the scene
58//
59// A periodic array of spheres, given ANALYTICALLY so the same function can be sampled on the
60// solver's inner region and on a g=3 test block (gate G3 needs both). Cell centres are at
61// (i+1/2)h with h = 1 (flow works in cell units on the colour block).
62struct SphereArray {
63 int n = 0;
64 double cx[8], cy[8], cz[8], r[8];
65 int N = 0; // periodic period, in cells
66 double sdfAt(double x, double y, double z) const {
67 double best = 1e30;
68 for (int s = 0; s < n; ++s)
69 for (int px = -1; px <= 1; ++px)
70 for (int py = -1; py <= 1; ++py)
71 for (int pz = -1; pz <= 1; ++pz) {
72 const double dx = x - (cx[s] + px * N), dy = y - (cy[s] + py * N),
73 dz = z - (cz[s] + pz * N);
74 const double d = std::sqrt(dx * dx + dy * dy + dz * dz) - r[s];
75 best = d < best ? d : best;
76 }
77 return best; // > 0 in fluid (flow's convention)
78 }
79};
80
81SphereArray makeArray(int N) {
82 SphereArray a;
83 a.N = N;
84 a.n = 4;
85 const double f = N / 32.0;
86 const double px[4] = {6.0, 20.0, 11.0, 26.0}, py[4] = {7.0, 9.0, 24.0, 22.0},
87 pz[4] = {8.0, 23.0, 19.0, 6.0};
88 for (int s = 0; s < 4; ++s) {
89 a.cx[s] = px[s] * f;
90 a.cy[s] = py[s] * f;
91 a.cz[s] = pz[s] * f;
92 a.r[s] = 6.0 * f;
93 }
94 return a;
95}
96
97std::vector<double> arraySdfInner(const SphereArray& a, int N) {
98 std::vector<double> sd((std::size_t)N * N * N);
99 for (int z = 0; z < N; ++z)
100 for (int y = 0; y < N; ++y)
101 for (int x = 0; x < N; ++x)
102 sd[idx(x, y, z, N, N)] = a.sdfAt(x + 0.5, y + 0.5, z + 0.5);
103 return sd;
104}
105
106// The same SDF on an extended block of ghost width g (gate G3's independent geometry build).
107void arraySdfBlock(const SphereArray& a, CCField dst, I3 e, int g) {
108 auto h = Kokkos::create_mirror_view(dst);
109 for (int z = 0; z < e.z; ++z)
110 for (int y = 0; y < e.y; ++y)
111 for (int x = 0; x < e.x; ++x)
112 h(L3(x, y, z, e)) = a.sdfAt(x - g + 0.5, y - g + 0.5, z - g + 0.5);
113 Kokkos::deep_copy(dst, h);
114}
115
116std::vector<double> hostOf(SField v) {
117 auto h = Kokkos::create_mirror_view(v);
118 Kokkos::deep_copy(h, v);
119 std::vector<double> out(v.extent(0));
120 for (std::size_t i = 0; i < out.size(); ++i)
121 out[i] = h(i);
122 return out;
123}
124
125// Run the single-phase solver to a Stokes steady state on the packing and return it.
126void stokesSteady(peclet::flow::IbmSolver& s, int steps) {
127 for (int i = 0; i < steps; ++i)
128 s.step();
129}
130
131// ================================================================ G3: the bridge with geometry
132void bridgeWithGeometry() {
133 const int N = 24;
134 const SphereArray arr = makeArray(N);
136 s.setRho(1.0);
137 s.setMu(0.2);
138 s.setDt(1.0);
139 s.setBodyForce(2e-3, 1e-3, 0.0);
140 s.setSolid(arraySdfInner(arr, N), true);
141 stokesSteady(s, 40);
142 s.enableVof();
143 CHECK(s.vofHasGeometry());
144
145 // --- (a) the geometry fields, against an independent build on a g=3 block --------------------
147 const I3 e3{N + 2 * g, N + 2 * g, N + 2 * g};
148 const long len = (long)e3.x * e3.y * e3.z;
149 CCField sdf3("t::sdf3", len), ox3("t::ox3", len), oy3("t::oy3", len), oz3("t::oz3", len),
150 cs3("t::cs3", len);
151 arraySdfBlock(arr, sdf3, e3, g);
152 peclet::flow::buildOpenness(ox3, oy3, oz3, CCConst(sdf3), C3{e3.x, e3.y, e3.z}, 1.0, 1.0, 1.0,
153 s.apertureOrder());
154 peclet::flow::buildCellFraction(cs3, CCConst(sdf3), C3{e3.x, e3.y, e3.z}, 1);
155 const std::vector<double> refO[3] = {hostOf(ox3), hostOf(oy3), hostOf(oz3)};
156 const auto refE = hostOf(cs3);
157 const long sd3[3] = {1, (long)e3.x, (long)e3.x * e3.y};
158
159 const auto& adv = s.vofAdvector();
160 double dOpen = 0.0, dEps = 0.0;
161 for (int d = 0; d < 3; ++d) {
162 const auto got = hostOf(adv.faceOpenness(d));
163 for (int z = g; z < g + N; ++z)
164 for (int y = g; y < g + N; ++y)
165 for (int x = g; x < g + N; ++x) {
166 const long i = L3(x, y, z, e3);
167 // buildOpenness gives the LOW (-d) face of each cell; the advector wants the HIGH (+d)
168 // face of the same cell, i.e. the low face of the cell one step up in d. That written-out
169 // shift is exactly what `vof::copyFaceVelocity` does inside the solver.
170 dOpen = std::fmax(dOpen, std::fabs(got[i] - refO[d][i + sd3[d]]));
171 }
172 }
173 {
174 const auto got = hostOf(adv.epsFraction());
175 for (int z = g; z < g + N; ++z)
176 for (int y = g; y < g + N; ++y)
177 for (int x = g; x < g + N; ++x) {
178 const long i = L3(x, y, z, e3);
179 dEps = std::fmax(dEps, std::fabs(got[i] - refE[i]));
180 }
181 }
182 std::printf(
183 "G3a geometry embed: max|openness - independent| %.3e max|eps - independent| %.3e\n", dOpen,
184 dEps);
185 CHECK(dOpen == 0.0);
186 CHECK(dEps == 0.0);
187
188 // --- (b) the transported colour, against a standalone advector -------------------------------
189 // Seed a slab and transport it BOTH ways with the same (frozen, projected) velocity.
190 std::vector<double> c0((std::size_t)N * N * N, 0.0);
191 for (int z = 0; z < N; ++z)
192 for (int y = 0; y < N; ++y)
193 for (int x = 0; x < N; ++x)
194 c0[idx(x, y, z, N, N)] = (z < N / 2) ? 1.0 : ((z == N / 2) ? 0.37 : 0.0);
195 s.setVof(c0);
196
197 WyAdvector ref;
198 ref.init(N, N, N, 1.0, g);
199 // WO-R2 item 4: configure the reference like the solver, wisp tolerance included, or this gate
200 // measures the difference of two mixed-cell predicates (2.594e-09) instead of the bridge.
202 ref.interfaceLocalCfl = true;
203 ref.enableGeometry();
204 // The reference's geometry: the INDEPENDENT fields above, embedded with the shift by hand.
205 {
206 auto ho = Kokkos::create_mirror_view(ref.faceOpenness(0));
207 for (int d = 0; d < 3; ++d) {
208 auto hd = Kokkos::create_mirror_view(ref.faceOpenness(d));
209 for (long i = 0; i < len; ++i)
210 hd(i) = (i + sd3[d] < len) ? refO[d][i + sd3[d]] : 0.0;
211 Kokkos::deep_copy(ref.faceOpenness(d), hd);
212 }
213 auto he = Kokkos::create_mirror_view(ref.epsFraction());
214 for (long i = 0; i < len; ++i)
215 he(i) = refE[i];
216 Kokkos::deep_copy(ref.epsFraction(), he);
217 (void)ho;
218 }
219 ref.exchange = [&ref, e3, g](SField f) {
220 vofscene::periodicFill(f, e3, g, true, true, true);
221 if (ref.hasGeometry() && f.data() == ref.colour().data()) {
222 ref.solidBandFill();
223 vofscene::periodicFill(f, e3, g, true, true, true);
224 }
225 };
226 vofscene::periodicFill(ref.faceOpenness(0), e3, g, true, true, true);
227 vofscene::periodicFill(ref.faceOpenness(1), e3, g, true, true, true);
228 vofscene::periodicFill(ref.faceOpenness(2), e3, g, true, true, true);
229 vofscene::periodicFill(ref.epsFraction(), e3, g, true, true, true);
230 ref.classifyGeometry();
231 vofscene::periodicFill(ref.kindDouble(), e3, g, true, true, true);
232 ref.finalizeGeometry();
233 { // the same colour, with solid cells zeroed exactly as the solver's canonical field is
234 auto hc = Kokkos::create_mirror_view(ref.colour());
235 auto hk = Kokkos::create_mirror_view(ref.kindDouble());
236 Kokkos::deep_copy(hk, ref.kindDouble());
237 for (long i = 0; i < len; ++i)
238 hc(i) = 0.0;
239 for (int z = 0; z < N; ++z)
240 for (int y = 0; y < N; ++y)
241 for (int x = 0; x < N; ++x) {
242 const long i = L3(x + g, y + g, z + g, e3);
243 hc(i) = hk(i) > 0.5 ? 0.0 : c0[idx(x, y, z, N, N)];
244 }
245 Kokkos::deep_copy(ref.colour(), hc);
246 }
247 ref.syncGhosts();
248 // The solver's own face velocity, re-indexed into the advector's high-face convention.
249 const auto uu = s.getVelocity(0), vv = s.getVelocity(1), ww = s.getVelocity(2);
250 {
251 auto hu = Kokkos::create_mirror_view(ref.faceU());
252 auto hv = Kokkos::create_mirror_view(ref.faceV());
253 auto hw = Kokkos::create_mirror_view(ref.faceW());
254 for (int z = 0; z < e3.z; ++z)
255 for (int y = 0; y < e3.y; ++y)
256 for (int x = 0; x < e3.x; ++x) {
257 const int gx = ((x - g) % N + N) % N, gy = ((y - g) % N + N) % N,
258 gz = ((z - g) % N + N) % N;
259 const long i = L3(x, y, z, e3);
260 hu(i) = uu[idx((gx + 1) % N, gy, gz, N, N)];
261 hv(i) = vv[idx(gx, (gy + 1) % N, gz, N, N)];
262 hw(i) = ww[idx(gx, gy, (gz + 1) % N, N, N)];
263 }
264 Kokkos::deep_copy(ref.faceU(), hu);
265 Kokkos::deep_copy(ref.faceV(), hv);
266 Kokkos::deep_copy(ref.faceW(), hw);
267 }
268 const double dth = 1.0; // h = 1
269 double cflRef = ref.maxCourantInterfaceAuto(dth);
270 const double dtA = cflRef > 0 ? 0.2 / cflRef : 1.0;
271 s.setDt(dtA);
272 const int steps = 20;
273 for (int t = 0; t < steps; ++t) {
274 ref.advect(dtA, t);
275 s.advectVofKinematic(dtA);
276 }
277 const auto cs = s.getVof();
278 const auto cr = hostOf(ref.colour());
279 const auto kr = hostOf(ref.kindDouble());
280 // The canonical "C" carries 0 in solid cells while the working block carries the neutral band
281 // fill, so the comparison is over FLUID cells (the fill itself is gated by G5).
282 double dmax = 0.0, fillMax = 0.0;
283 for (int z = 0; z < N; ++z)
284 for (int y = 0; y < N; ++y)
285 for (int x = 0; x < N; ++x) {
286 const long i = L3(x + g, y + g, z + g, e3);
287 if (kr[i] > 0.5) {
288 fillMax = std::fmax(fillMax, cr[i]);
289 continue;
290 }
291 dmax = std::fmax(dmax, std::fabs(cs[idx(x, y, z, N, N)] - cr[i]));
292 }
293 std::printf(
294 "G3b solver vs standalone through the packing (%d steps, cfl 0.2, dt %.4g): max|dC| over "
295 "fluid cells %.3e (standalone band fill reaches %.3f)\n",
296 steps, dtA, dmax, fillMax);
297 CHECK(dmax == 0.0);
298}
299
300// ================================================================ G2: conservation
301void conservationThroughPacking(int N, int nAdv) {
302 const SphereArray arr = makeArray(N);
304 s.setRho(1.0);
305 s.setMu(0.2);
306 s.setDt(1.0);
307 s.setBodyForce(2e-3, 1e-3, 5e-4);
308 s.setSolid(arraySdfInner(arr, N), true);
309 stokesSteady(s, 60);
310 const double div = s.maxOpenDivergence();
311 const long pit = s.lastPressureIterations();
312 s.enableVof();
313 std::vector<double> c0((std::size_t)N * N * N, 0.0);
314 for (int z = 0; z < N; ++z)
315 for (int y = 0; y < N; ++y)
316 for (int x = 0; x < N; ++x)
317 c0[idx(x, y, z, N, N)] = (z < N / 2) ? 1.0 : 0.0;
318 s.setVof(c0);
319
320 const auto d0 = s.vofDiagnostics();
321 const double cfl = s.vofMaxCourant(); // uses the current dt (= 1)
322 const double dtA = cfl > 0 ? 0.2 / cfl : 1.0;
323 double clip = 0.0, mnF = 1e30, mxF = -1e30, solidSum = 0.0;
324 for (int t = 0; t < nAdv; ++t) {
325 s.advectVofKinematic(dtA);
326 const auto d = s.vofDiagnostics();
327 clip += d.clippedVolume;
328 mnF = std::fmin(mnF, d.minCFluid);
329 mxF = std::fmax(mxF, d.maxCFluid);
330 solidSum = std::fmax(solidSum, std::fabs(d.solidSumC));
331 }
332 const auto d1 = s.vofDiagnostics();
333 const double drift = std::fabs(d1.volume - d0.volume) / d0.volume;
334 const double rawDrift = std::fabs(d1.rawVolume - d0.rawVolume) / d0.rawVolume;
335 std::printf(
336 "G2 packing %d^3, %d kinematic steps at cfl 0.2 (dt %.4g):\n"
337 " max|div(open u)| %.3e (pressure iters %ld)\n"
338 " sum eps_eff C: %.15e -> %.15e relative drift %.3e\n"
339 " sum eps C: %.15e -> %.15e relative drift %.3e\n"
340 " solid cells %ld, cut cells %ld, max|sum C over solid| %.3e\n"
341 " min/max C over UNCUT fluid cells %.3e / %.15e\n"
342 " clipped liquid volume, total over the run %.6e (%.3e of the liquid volume)\n",
343 N, nAdv, dtA, div, pit, d0.volume, d1.volume, drift, d0.rawVolume, d1.rawVolume, rawDrift,
344 d1.solidCells, d1.cutCells, solidSum, mnF, mxF, clip, clip / d0.volume);
345 CHECK(div <= 1e-10);
346 CHECK(drift <= 1e-11);
347 CHECK(solidSum == 0.0);
348 CHECK(mnF >= -1e-12);
349 CHECK(mxF <= 1.0 + 1e-12);
350 CHECK(d1.solidCells > 0);
351 CHECK(d1.cutCells > 0);
352}
353
354// ================================================================ G5: the 90-degree neutral fill
355//
356// A liquid cap on a flat SDF wall. The wall occupies z < zw with zw a HALF-INTEGER in cell units,
357// so the cells straddling it are genuinely cut (eps = 1/2) — a wall on a cell face would be a
358// staircase and would not exercise the cut-cell path at all.
359void neutralFillCap(int steps, bool zeroSolid = true) {
360 const int NX = 40, NY = 40, NZ = 28;
361 const double zw = 3.5; // the wall surface, in cell units (cell k spans [k, k+1])
362 const double R = 12.0; // D/dx = 24
363 const double sigma = 1.0;
364 std::vector<double> sd((std::size_t)NX * NY * NZ);
365 for (int z = 0; z < NZ; ++z)
366 for (int y = 0; y < NY; ++y)
367 for (int x = 0; x < NX; ++x)
368 sd[idx(x, y, z, NX, NY)] = (z + 0.5) - zw; // > 0 above the wall = fluid
369 // The initial hemisphere, by 4^3 midpoint subsampling (the same quadrature the fluid fraction
370 // uses, so the initial colour and the geometry agree cell by cell).
371 const double cx = NX * 0.5, cy = NY * 0.5;
372 std::vector<double> c0((std::size_t)NX * NY * NZ, 0.0);
373 for (int z = 0; z < NZ; ++z)
374 for (int y = 0; y < NY; ++y)
375 for (int x = 0; x < NX; ++x) {
376 int in = 0, tot = 0;
377 for (int a = 0; a < 4; ++a)
378 for (int b = 0; b < 4; ++b)
379 for (int c = 0; c < 4; ++c) {
380 const double px = x + (a + 0.5) / 4.0, py = y + (b + 0.5) / 4.0,
381 pz = z + (c + 0.5) / 4.0;
382 if (pz < zw)
383 continue; // solid: C is a fraction of the FLUID volume
384 ++tot;
385 const double dx = px - cx, dy = py - cy, dz = pz - zw;
386 if (dx * dx + dy * dy + dz * dz < R * R)
387 ++in;
388 }
389 c0[idx(x, y, z, NX, NY)] = tot ? (double)in / tot : 0.0;
390 }
391
393 s.setRho(1.0);
394 s.setMu(0.05);
395 s.setSolid(sd, true);
396 s.enableVof();
397 s.setVofSolidColourZero(zeroSolid);
398 s.setVof(c0);
399 s.setSurfaceTension(sigma);
400 const double dtSig = s.capillaryDt();
401 s.setDt(0.5 * dtSig);
402 const double V00 = s.vofDiagnostics().volume;
403 long maxIt = 0;
404 double uTrace[4] = {0, 0, 0, 0};
405 for (int i = 0; i < steps; ++i) {
406 s.step();
407 maxIt = std::max(maxIt, s.lastPressureIterations());
408 if ((i + 1) % (steps / 4) == 0) {
409 const int slot = std::min(3, (i + 1) / (steps / 4) - 1);
410 double m = 0;
411 for (int c = 0; c < 3; ++c)
412 for (double v : s.getVelocity(c))
413 m = std::fmax(m, std::fabs(v));
414 uTrace[slot] = m;
415 }
416 }
417 // The curvature census in the WALL BAND (the two fluid planes above the wall) — the WO asks for
418 // it because the neutral fill is what those stencils read.
419 const auto cens = [&] {
420 s.computeVofCurvature();
421 return s.vofCurvatureStats();
422 }();
423 const auto kap = s.getVofCurvature();
424 const auto kbr = s.getVofCurvatureBranch();
425 double kmaxWall = 0.0;
426 long brWall[7] = {0, 0, 0, 0, 0, 0, 0};
427 for (int z = (int)std::ceil(zw); z < (int)std::ceil(zw) + 2; ++z)
428 for (int y = 0; y < NY; ++y)
429 for (int x = 0; x < NX; ++x) {
430 const std::size_t i = idx(x, y, z, NX, NY);
431 const int b = (int)kbr[i];
432 if (b >= 0 && b < 7)
433 ++brWall[b];
434 if (b != 0)
435 kmaxWall = std::fmax(kmaxWall, std::fabs(kap[i]));
436 }
437 const auto d = s.vofDiagnostics();
438 const auto cc = s.getVof();
439 // h: the liquid column height on the axis (sum of C over the fluid column at the centre).
440 double h = 0.0;
441 {
442 const int x0 = NX / 2, y0 = NY / 2;
443 for (int z = 0; z < NZ; ++z)
444 h += cc[idx(x0, y0, z, NX, NY)] * ((z + 1 <= zw) ? 0.0 : std::fmin(1.0, z + 1 - zw));
445 }
446 // a: from the liquid AREA in the first FULL fluid plane above the wall (more robust than a
447 // contour, and the C = 1/2 contour of a resolved cap is its area radius to O(h^2)).
448 const int z0 = (int)std::ceil(zw);
449 double area = 0.0;
450 for (int y = 0; y < NY; ++y)
451 for (int x = 0; x < NX; ++x)
452 area += cc[idx(x, y, z0, NX, NY)];
453 const double a = std::sqrt(area / M_PI);
454 const double theta = 2.0 * std::atan2(h, a) * 180.0 / M_PI;
455 const double Rc = (a * a + h * h) / (2.0 * h);
456 const double V = d.volume;
457 const double V0 = 2.0 * M_PI * R * R * R / 3.0;
458 // Young-Laplace: the pressure jump across the cap.
459 const auto pf = s.getField("p");
460 double pIn = 0.0, pOut = 0.0;
461 int nIn = 0, nOut = 0;
462 for (int z = 0; z < NZ; ++z)
463 for (int y = 0; y < NY; ++y)
464 for (int x = 0; x < NX; ++x) {
465 const double dx = x + 0.5 - cx, dy = y + 0.5 - cy, dz = z + 0.5 - zw;
466 const double r = std::sqrt(dx * dx + dy * dy + dz * dz);
467 if (dz < 0.5)
468 continue;
469 if (r < 0.55 * R) {
470 pIn += pf[idx(x, y, z, NX, NY)];
471 ++nIn;
472 } else if (r > 1.5 * R && r < 1.8 * R) {
473 pOut += pf[idx(x, y, z, NX, NY)];
474 ++nOut;
475 }
476 }
477 const double dP = (nIn ? pIn / nIn : 0.0) - (nOut ? pOut / nOut : 0.0);
478 // max|u| over the OPEN fluid only (z >= ceil(zw)+1): the velocity DOFs that live on faces inside
479 // or on the wall are IBM-constrained, not flow, and reporting them as a spurious current would
480 // measure the immersed boundary rather than the surface-tension balance.
481 double umax = 0.0, umaxAll = 0.0;
482 for (int c = 0; c < 3; ++c) {
483 const auto v = s.getVelocity(c);
484 for (int z = 0; z < NZ; ++z)
485 for (int y = 0; y < NY; ++y)
486 for (int x = 0; x < NX; ++x) {
487 const double a = std::fabs(v[idx(x, y, z, NX, NY)]);
488 umaxAll = std::fmax(umaxAll, a);
489 if (z >= (int)std::ceil(zw) + 1)
490 umax = std::fmax(umax, a);
491 }
492 }
493 const double Ca = 0.05 * umax / sigma; // mu of this scene
494 std::printf(
495 "G5 cap on a cut wall (D/dx %.0f, %d steps at 0.5 dt_sigma = %.4g, solid colour %s), "
496 "pressure iters max %ld:\n"
497 " volume %.6f -> %.6f (drift %.3e); analytic hemisphere %.6f\n"
498 " h %.4f a %.4f -> apparent theta %.3f deg (target 90)\n"
499 " cap sphere radius %.4f (target %.1f); Young-Laplace dP %.6f vs 2 sigma/R %.6f "
500 "(rel %.3e)\n"
501 " max|u| (open fluid) %.3e -> spurious Ca %.3e ; max|u| incl. the wall band %.3e ;\n"
502 " trace of max|u| (all cells) over the run %.2e %.2e %.2e %.2e\n"
503 " wall band: max|kappa| %.4f, branches 0/1/2/3/4/5/6 = %ld/%ld/%ld/%ld/%ld/%ld/%ld "
504 "(global no_estimate %ld)\n"
505 " clipped volume last step %.3e ; clamped faces %ld ; solid sum C %.3e\n",
506 2 * R, steps, 0.5 * dtSig, zeroSolid ? "ZERO" : "band fill", maxIt, V00, V,
507 std::fabs(V - V00) / V00, V0, h, a, theta, Rc, R, dP, 2.0 * sigma / Rc,
508 std::fabs(dP - 2.0 * sigma / Rc) / (2.0 * sigma / Rc), umax, Ca, umaxAll, uTrace[0],
509 uTrace[1], uTrace[2], uTrace[3], kmaxWall, brWall[0], brWall[1], brWall[2], brWall[3],
510 brWall[4], brWall[5], brWall[6], cens.noEstimate, d.clippedVolume, d.clampedFaces,
511 d.solidSumC);
512 CHECK(std::fabs(theta - 90.0) <= 3.0);
513 CHECK(std::fabs(dP - 2.0 * sigma / Rc) / (2.0 * sigma / Rc) <= 0.01);
514 if (zeroSolid)
515 CHECK(d.solidSumC == 0.0);
516 CHECK(std::fabs(V - V00) / V00 <= 1e-9);
517}
518
519// ================================================================ G8: momentum consistency in cut
520// cells (WO-Q item 8)
521//
522// G8a THE CONSISTENCY IDENTITY. `enable_vof_momentum` + a packing + a UNIFORM velocity: the
523// advected velocity must come back as that same uniform value. Every term of the deviation form
524// `u_new = u_old + dev/(eps^e rho_new)` is a DIFFERENCE OF VELOCITIES, so `dev` is exactly zero
525// for a uniform field whatever `eps^e` and the face openness are — the identity is bitwise in
526// CUT cells too, not only away from them, and there is no tolerance to choose.
527// G8b COUPLED DRAINING through the packing at ratio 10 with gravity and momentum consistency: the
528// colour drift per step, boundedness, `max|u|`, and the pressure iteration count against its
529// cap (rule 3b: a capped run is INVALID).
530void momentumCutCells() {
531 const int N = 24;
532 const SphereArray arr = makeArray(N);
533 const auto sdf = arraySdfInner(arr, N);
534 const double U[3] = {1.0, 0.6, -0.4};
535 // --- G8a ------------------------------------------------------------------------------------
536 for (double R : {10.0, 1e2, 1e3}) {
538 s.setRho(R);
539 s.setMu(0.0);
540 // The CUT-CELL Courant number (`o_f |a_f| / max(eps, 0.1)`) is up to 6x the plain one in this
541 // packing — at dt = 0.2, |U| = 1 the plain CFL is 0.2 but the cut one reads 1.24 and `advect()`
542 // aborts, which is the limiter doing exactly its job. dt = 0.04 puts the cut number at 0.25.
543 s.setDt(0.04);
544 s.setSolid(sdf, true);
545 s.setPressureChebyshev(true, 300, 1e-13);
546 s.enableVof();
547 std::vector<double> c0((std::size_t)N * N * N);
548 for (int z = 0; z < N; ++z)
549 for (int y = 0; y < N; ++y)
550 for (int x = 0; x < N; ++x)
551 c0[idx(x, y, z, N, N)] = peclet::flow::vof::sphereCellFraction(0.5 * N, 0.5 * N, 0.5 * N,
552 0.28 * N, x, y, z, 1.0, 4);
553 s.setVof(c0);
554 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, R - 1.0});
555 s.enableVofMomentum(1.0, R);
556 const std::size_t nc = (std::size_t)N * N * N;
557 s.uploadVelocity(std::vector<double>(nc, U[0]), std::vector<double>(nc, U[1]),
558 std::vector<double>(nc, U[2]));
559 s.advectVofMomentum();
560 double dev = 0.0;
561 for (int c = 0; c < 3; ++c)
562 for (double v : s.getVofAdvectedVelocity(c))
563 dev = std::fmax(dev, std::fabs(v - U[c]));
564 const auto md = s.vofMomentumDiagnostics();
565 std::printf(
566 "G8a packing + uniform U, ratio %-6g: max|u_adv - U| = %.17g%s (rho^e floor hit %ld, "
567 "flux clamps %ld)\n",
568 R, dev, dev == 0.0 ? " BITWISE" : "", md.floored, md.clamped);
569 CHECK(dev == 0.0);
570 CHECK(md.floored == 0);
571 }
572 // --- G8b ------------------------------------------------------------------------------------
573 // The body force is the ZERO-MEAN buoyancy `f_z = -g (rho - <rho>)`, not `-g rho`: in a fully
574 // periodic box the projection removes only GRADIENTS, so a force with a non-zero mean accelerates
575 // the whole fluid without bound and the case has no steady state to measure a drift against.
576 //
577 // MEASURED with the non-zero-mean force (`PECLET_VOF_CUTCELL_NONZERO_FORCE=1`), and RECORDED as
578 // an open question rather than explained: with momentum consistency ON the run is clean and
579 // conservative to 1e-12 for 155 steps — `C^e` inside [0,1] to the last bit, `rho^e` never
580 // floored, 9-10 pressure iterations — and then at step ~160, with `max|u|` around 0.19, `C^e`
581 // goes to +inf inside the ADVECTION and the velocity follows. With momentum consistency OFF the
582 // identical case runs the full 200 steps (drift -2.3e-13, max|u| 0.236). So it is the
583 // momentum-consistent cut-cell path, not the colour transport and not the unbounded acceleration
584 // on its own; the trace shows no bounded-quantity precursor at all, which is why it is reported
585 // instead of patched.
586 for (int momentumOn = 1; momentumOn >= 0; --momentumOn) {
587 const double R = 10.0, grav = 2e-3;
588 const double liq = 0.25, rhoMean = liq * R + (1.0 - liq);
590 s.setRho(1.0);
591 s.setMu(0.05);
592 s.setDt(0.5);
593 s.setSolid(sdf, true);
594 s.enableVof();
595 std::vector<double> c0((std::size_t)N * N * N, 0.0);
596 for (int z = 0; z < N; ++z)
597 for (int y = 0; y < N; ++y)
598 for (int x = 0; x < N; ++x)
599 c0[idx(x, y, z, N, N)] = (z >= 3 * N / 4) ? 1.0 : 0.0; // a liquid layer on top
600 s.setVof(c0);
601 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, R - 1.0});
602 s.setPropertyModel("mu", peclet::flow::ClosureKind::LinearMix, "C", "", {0.05, 0.45});
603 const bool nonZeroMean = std::getenv("PECLET_VOF_CUTCELL_NONZERO_FORCE") != nullptr;
604 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "",
605 {nonZeroMean ? 0.0 : grav * rhoMean, -grav});
606 if (momentumOn)
607 s.enableVofMomentum(1.0, R);
608 const double v0 = s.vofDiagnostics().volume;
609 long maxIt = 0;
610 const int steps = 200;
611 double umax = 0.0, clip = 0.0, mn = 1e30, mx = -1e30;
612 for (int i = 0; i < steps; ++i) {
613 s.step();
614 maxIt = std::max(maxIt, s.lastPressureIterations());
615 const auto d = s.vofDiagnostics();
616 clip += d.clippedVolume;
617 mn = std::fmin(mn, d.minCFluid);
618 mx = std::fmax(mx, d.maxCFluid);
619 if (std::getenv("PECLET_VOF_CUTCELL_TRACE") && (i % 5) == 0) {
620 double um = 0;
621 for (int c = 0; c < 3; ++c)
622 for (double v : s.getVelocity(c))
623 um = um > std::fabs(v) ? um : std::fabs(v);
624 double mnCc = 0, mxCc = 0, mnRho = 0;
625 long floored = 0;
626 if (momentumOn) {
627 const auto md = s.vofMomentumDiagnostics();
628 for (int c = 0; c < 3; ++c) {
629 mnCc = std::fmin(mnCc, md.minCc[c]);
630 mxCc = std::fmax(mxCc, md.maxCc[c]);
631 }
632 mnRho = md.minRhoC;
633 floored = md.floored;
634 }
635 std::printf(
636 " trace %3d vol %.12e minC %.3e maxC %.6f max|u| %.4e cfl %.4f iters %ld "
637 "C^e [%.3e, %.6f] minRho^e %.4e floored %ld\n",
638 i, d.volume, d.minCFluid, d.maxCFluid, um, s.vofLastCourant(),
639 s.lastPressureIterations(), mnCc, mxCc, mnRho, floored);
640 }
641 }
642 for (int c = 0; c < 3; ++c)
643 for (double v : s.getVelocity(c))
644 umax = std::fmax(umax, std::fabs(v));
645 const auto d1 = s.vofDiagnostics();
646 const double drift = (d1.volume - v0) / v0;
647 std::printf(
648 "G8b packing draining, ratio %g, %d coupled steps, momentum consistency %s:\n"
649 " colour drift %.3e (%.3e per step), max|div(open u)| %.3e, pressure iters max %ld\n"
650 " min/max C over uncut fluid %.3e / %.15f, clipped volume total %.3e, max|u| %.3e, "
651 "solid sum C %.3e\n",
652 R, steps, momentumOn ? "ON" : "off", drift, drift / steps, s.maxOpenDivergence(), maxIt, mn,
653 mx, clip, umax, d1.solidSumC);
654 CHECK(!std::isnan(umax) && !std::isnan(drift));
655 CHECK(std::fabs(drift) / steps <= 1e-10);
656 CHECK(d1.solidSumC == 0.0);
657 CHECK(maxIt < 200);
658 }
659}
660
661} // namespace
662
663int main(int argc, char** argv) {
664 std::setvbuf(stdout, nullptr, _IONBF, 0);
665 Kokkos::initialize(argc, argv);
666 {
667 bool quick = true;
668 if (const char* e = std::getenv("PECLET_VOF_CUTCELL_LONG"))
669 quick = (e[0] == '0');
670 bridgeWithGeometry();
671 conservationThroughPacking(quick ? 24 : 48, quick ? 200 : 500);
672 if (!std::getenv("PECLET_VOF_CUTCELL_SKIP_G5")) {
673 neutralFillCap(quick ? 120 : 200);
674 // The measured ablation on what the CANONICAL colour carries inside the solid (the working
675 // block always carries the neutral fill): 0 is the WO-Q contract, but the CSF and the
676 // property closures read the canonical field, so a wall face then sees a full colour jump.
677 if (std::getenv("PECLET_VOF_CUTCELL_FILL_ABLATION"))
678 neutralFillCap(quick ? 120 : 200, false);
679 }
680 momentumCutCells();
681 }
682 Kokkos::finalize();
683 if (failures)
684 std::fprintf(stderr, "%d check(s) FAILED\n", failures);
685 else
686 std::printf("all vof cut-cell checks passed\n");
687 return failures ? 1 : 0;
688}
static constexpr int kVofG
static constexpr double defaultVofWispEps()
The value enableVof starts from.
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
x
Definition cmp.py:24
y
Definition cmp.py:24
Slot slot(const Kokkos::View< double *, CCMem > &ks, int k)
long L3(int x, int y, int z, I3 e)
void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
Kokkos::View< const double *, CCMem > CCConst
Kokkos::View< double *, SMem > SField
void periodicFill(SField f, I3 e, int g, bool px, bool py, bool pz)
Periodic wrap of a single-block extended field (serial reference path; the MPI path uses core's GridH...
flow — thin shim: the container-free VoF PLIC geometric toolbox (rung V0) now live in core.
static constexpr int NY
static constexpr int NX
static constexpr int NZ
static constexpr int N
static double sdfAt(int gx, int gy, int gz, int nx, int ny, int nz, double dref)
static constexpr double U
#define CHECK(cond)