flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_blocks.cpp
Go to the documentation of this file.
1// VoF Part III rung W0 (WO-W0) — the per-bubble BLOCK container, single rank.
2//
3// P plan self-check : the gather/scatter pieces PARTITION a block's box (every cell exactly
4// once, from exactly one owner) for periodic, walled and seam-crossing
5// boxes -- the property that makes arrival order irrelevant.
6// G1 one bubble, bitwise : the V1 LeVeque scene (32^3, T = 3 reversal) with one sphere carried
7// as a block: the UNION field equals the global-field WyAdvector result
8// BIT FOR BIT at every step.
9// G2 two bubbles : two spheres pushed together by a prescribed solenoidal
10// (discrete-curl)
11// field: the two markers OVERLAP in space, each keeps its own volume,
12// the reversal recovers two intact bubbles -- while the single-field
13// control merges them irreversibly. The raison d'etre gate.
14// G3 re-centring : a sphere translated 20 cells; the moving block is bitwise equal to a
15// block large enough never to move, and its volume is exact.
16// W marker at a wall : a sphere cut by the no-slip wall y = 0 (colour in the wall-adjacent
17// layer) in a solenoidal field with v = 0 ON the wall face and v != 0
18// one face in: the block conserves its volume to round-off and equals
19// the global field. The LOW wall face is the advector's global index
20// -1 (high-face convention), outside the domain; a zero-gradient clamp
21// of the block's face velocity there put the first interior face's
22// velocity on the wall (bubble column 2026-09-25, 1e-5 of a marker per
23// step). Fails with that clamp restored.
24// D1 seeded debris : doc/vof_overlap_design.md gate G1(ii) -- two R = 5 markers 16 cells
25// apart plus three cells of B's colour painted into A's interface
26// band, through a real Solver with the block CSF: one step removes
27// the speck, returns its volume to B exactly and leaves A alone. (The
28// speck is more than two cells from any other B colour, so it gets NO
29// curvature estimate at all; the clip's G1(i) gate is gate K of
30// test_vof_blocks_overlap, with a speck at B's own fringe.)
31// M momentum refused : enable_vof_momentum and the block container are refused together,
32// in BOTH call orders, with the message naming the cause (the
33// momentum rides the structured colour's fluxes and step() then skips
34// the slot that advects the markers).
35//
36// Everything is compared against a plain `WyAdvector` on the whole grid seeded with the SAME exact
37// `sphereCellFraction` and driven by the SAME face field, so "bitwise" is a real statement about
38// the container and not about the scene.
39#include <array>
40#include <cmath>
41#include <cstdio>
42#include <cstring>
43#include <Kokkos_Core.hpp>
44#include <limits>
45#include <stdexcept>
46#include <vector>
47
48#include "flow_ibm.hpp"
51#include "vof_advect_scenes.hpp"
52
62
63namespace {
64
65int failures = 0;
66#define CHECK(cond) \
67 do { \
68 if (!(cond)) { \
69 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
70 ++failures; \
71 } \
72 } while (0)
73
74constexpr int G = 3;
75
77struct Patch {
78 WyAdvector adv;
80 int n = 0;
81 double h = 1.0;
82 void init(int nx, double hh) {
83 n = nx;
84 h = hh;
85 adv.init(nx, nx, nx, hh, G);
86 blk = vofscene::blockOf(adv, I3{0, 0, 0});
87 }
88};
89
90std::shared_ptr<VofBlockExchange> serialExchange(VofBlockSet& set, Patch& p) {
91 auto ex = std::make_shared<VofBlockExchange>();
92 std::vector<VofBox> rb(1);
93 rb[0].lo[0] = rb[0].lo[1] = rb[0].lo[2] = 0;
94 rb[0].hi[0] = rb[0].hi[1] = rb[0].hi[2] = p.n;
95 ex->init(I3{p.n, p.n, p.n}, set.periodic(), rb, 0);
96 VofBlockExchange::Patch pp;
97 pp.e = p.adv.extent();
98 pp.n = p.adv.inner();
99 pp.o = I3{0, 0, 0};
100 pp.g = G;
101 ex->setPatch(pp, p.adv.faceU(), p.adv.faceV(), p.adv.faceW());
102 set.setExchange(ex);
103 return ex;
104}
105
107void refSphere(WyAdvector& a, vofscene::Block b, double h, double cx, double cy, double cz,
108 double r) {
109 vofscene::initSphere(a.colour(), b, h, cx, cy, cz, r);
110}
111
113long bitwiseDiffBox(SField a, SField b, I3 e, int n, int g, const int lo[3], const int hi[3],
114 double* worst, double* worstMag) {
115 auto ha = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), a);
116 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), b);
117 long d = 0;
118 double w = 0.0, wm = 0.0;
119 for (int z = 0; z < n; ++z)
120 for (int y = 0; y < n; ++y)
121 for (int x = 0; x < n; ++x) {
122 if (x < lo[0] || x >= hi[0] || y < lo[1] || y >= hi[1] || z < lo[2] || z >= hi[2])
123 continue;
124 const double va = ha(L3(x + g, y + g, z + g, e)), vb = hb(L3(x + g, y + g, z + g, e));
125 if (std::memcmp(&va, &vb, sizeof(double)) != 0) {
126 ++d;
127 w = std::fmax(w, std::fabs(va - vb));
128 wm = std::fmax(wm, std::fmax(std::fabs(va), std::fabs(vb)));
129 }
130 }
131 if (worst)
132 *worst = w;
133 if (worstMag)
134 *worstMag = wm;
135 return d;
136}
137
139std::vector<double> blockOnGrid(const peclet::flow::vof::VofBlock& b, int gn) {
140 std::vector<double> v(static_cast<std::size_t>(gn) * gn * gn,
141 std::numeric_limits<double>::quiet_NaN());
142 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), b.advector().colour());
143 const I3 be = b.advector().extent();
144 const int g = b.advector().ghost();
145 for (int z = 0; z < b.box.n(2); ++z)
146 for (int y = 0; y < b.box.n(1); ++y)
147 for (int x = 0; x < b.box.n(0); ++x) {
148 const int gx = ((b.box.lo[0] + x) % gn + gn) % gn;
149 const int gy = ((b.box.lo[1] + y) % gn + gn) % gn;
150 const int gz = ((b.box.lo[2] + z) % gn + gn) % gn;
151 v[static_cast<std::size_t>(gx) + static_cast<std::size_t>(gy) * gn +
152 static_cast<std::size_t>(gz) * gn * gn] = hb(L3(x + g, y + g, z + g, be));
153 }
154 return v;
155}
156
157std::vector<double> fieldOnGrid(SField f, I3 e, int gn, int g) {
158 std::vector<double> v(static_cast<std::size_t>(gn) * gn * gn, 0.0);
159 auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), f);
160 for (int z = 0; z < gn; ++z)
161 for (int y = 0; y < gn; ++y)
162 for (int x = 0; x < gn; ++x)
163 v[static_cast<std::size_t>(x) + static_cast<std::size_t>(y) * gn +
164 static_cast<std::size_t>(z) * gn * gn] = h(L3(x + g, y + g, z + g, e));
165 return v;
166}
167
169long gridDiff(const std::vector<double>& a, const std::vector<double>& b, double* worst) {
170 long d = 0;
171 double w = 0.0;
172 for (std::size_t i = 0; i < a.size(); ++i) {
173 if (std::isnan(a[i]) || std::isnan(b[i]))
174 continue;
175 if (std::memcmp(&a[i], &b[i], sizeof(double)) != 0) {
176 ++d;
177 w = std::fmax(w, std::fabs(a[i] - b[i]));
178 }
179 }
180 if (worst)
181 *worst = w;
182 return d;
183}
184
185long bitwiseDiff(SField a, SField b, I3 e, int n, int g, double* worst) {
186 auto ha = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), a);
187 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), b);
188 long d = 0;
189 double w = 0.0;
190 for (int z = 0; z < n; ++z)
191 for (int y = 0; y < n; ++y)
192 for (int x = 0; x < n; ++x) {
193 const double va = ha(L3(x + g, y + g, z + g, e)), vb = hb(L3(x + g, y + g, z + g, e));
194 if (std::memcmp(&va, &vb, sizeof(double)) != 0) {
195 ++d;
196 w = std::fmax(w, std::fabs(va - vb));
197 }
198 }
199 if (worst)
200 *worst = w;
201 return d;
202}
203
204double sumInner(SField f, I3 e, int n, int g) {
205 double s = 0.0;
206 Kokkos::parallel_reduce(
207 "sumInner",
208 peclet::flow::MDRange3<SExec>(SExec(), {g, g, g}, {g + n, g + n, g + n}),
209 KOKKOS_LAMBDA(int x, int y, int z, double& a) { a += f(L3(x, y, z, e)); }, s);
210 Kokkos::fence();
211 return s;
212}
213
214// ======================================================================== P: the plan partitions
215void gatePlan() {
216 std::printf("\n=== P the gather/scatter plan partitions the block box exactly once\n");
217 const int gn = 32;
218 const I3 gs{gn, gn, gn};
219 // three rank boxes that tile the grid (a 2x2x1 ORB plus a slab), so overlaps are non-trivial
220 std::vector<VofBox> rb(4);
221 const int cuts[4][6] = {{0, 0, 0, 16, 16, 32},
222 {16, 0, 0, 32, 16, 32},
223 {0, 16, 0, 16, 32, 32},
224 {16, 16, 0, 32, 32, 32}};
225 for (int r = 0; r < 4; ++r)
226 for (int d = 0; d < 3; ++d) {
227 rb[r].lo[d] = cuts[r][d];
228 rb[r].hi[d] = cuts[r][3 + d];
229 }
230 struct Case {
231 VofBox box;
232 std::array<bool, 3> per;
233 const char* what;
234 long expect;
235 int outside = 0; // the face-velocity gather's reach beyond a non-periodic domain face
236 };
237 std::vector<Case> cases = {
238 {VofBox{{4, 4, 4}, {20, 20, 20}}, {true, true, true}, "interior, periodic", 16L * 16 * 16},
239 {VofBox{{-3, 4, 4}, {13, 20, 20}}, {true, true, true}, "crosses the -x seam", 16L * 16 * 16},
240 {VofBox{{28, 4, 4}, {44, 20, 20}}, {true, true, true}, "crosses the +x seam", 16L * 16 * 16},
241 // a walled axis: the part outside the domain has no owner and is the block's own clamp fill
242 {VofBox{{-3, 4, 4}, {13, 20, 20}},
243 {false, true, true},
244 "walled -x, hangs out",
245 13L * 16 * 16},
246 // the face-velocity gather reads the out-of-domain part from the boundary rank's patch ghosts
247 {VofBox{{-3, 4, 4}, {13, 20, 20}},
248 {false, true, true},
249 "walled -x, face gather",
250 16L * 16 * 16,
251 3},
252 {VofBox{{-3, -3, 4}, {35, 13, 20}},
253 {false, false, true},
254 "walled x+y corners, face gather",
255 38L * 16 * 16,
256 3},
257 {VofBox{{-2, -2, -2}, {34, 34, 34}},
258 {true, true, true},
259 "spans the whole grid (clamped)",
260 32L * 32 * 32},
261 };
262 for (auto& c : cases) {
263 // an extended box may hang `outside` cells beyond a walled face; only the inner box is clamped
264 const VofBox box = c.outside ? c.box : peclet::flow::vof::vofClampBox(c.box, gs, c.per);
265 std::vector<VofPiece> pieces;
266 peclet::flow::vof::vofBuildPieces(box, gs, c.per, rb, pieces, c.outside);
267 // mark every block-local cell each piece writes; assert each is written at most once
268 std::vector<int> hits(static_cast<std::size_t>(box.cells()), 0);
269 long total = 0;
270 bool oob = false;
271 for (const auto& p : pieces) {
272 total += p.cells();
273 for (int z = 0; z < p.g.n(2); ++z)
274 for (int y = 0; y < p.g.n(1); ++y)
275 for (int x = 0; x < p.g.n(0); ++x) {
276 const int lx = p.loc[0] + x, ly = p.loc[1] + y, lz = p.loc[2] + z;
277 if (lx < 0 || ly < 0 || lz < 0 || lx >= box.n(0) || ly >= box.n(1) || lz >= box.n(2)) {
278 oob = true;
279 continue;
280 }
281 ++hits[static_cast<std::size_t>(lx) + static_cast<std::size_t>(ly) * box.n(0) +
282 static_cast<std::size_t>(lz) * box.n(0) * box.n(1)];
283 }
284 }
285 long once = 0, twice = 0;
286 for (int v : hits) {
287 if (v == 1)
288 ++once;
289 else if (v > 1)
290 ++twice;
291 }
292 std::printf(" %-32s pieces %2zu cells %7ld written-once %7ld multiply-written %ld\n",
293 c.what, pieces.size(), total, once, twice);
294 CHECK(!oob);
295 CHECK(twice == 0);
296 CHECK(once == c.expect);
297 CHECK(total == c.expect);
298 }
299}
300
301// ======================================================================== G1: one bubble, bitwise
302//
303// Run the SAME scene with two block sets that differ only in `bubbleEps`, the threshold that
304// defines the bubble extent (and hence the box):
305// eps = 0 the box contains the ENTIRE support of the colour, wisps included -> nothing is
306// ever dropped and the block is bitwise the global field for all 768 steps. This is
307// the gate the work order asks for, and it also measures what the wisp WAKE costs:
308// the box grows to cover everywhere the interface has been.
309// eps = 1e-12 the production default: the box tracks the BUBBLE, the round-off wake outside it
310// is dropped, and the block then differs from the global field by exactly that
311// residue (reported).
312void gateOneBubble() {
313 std::printf("\n=== G1 one bubble as a block == the global field, BITWISE (LeVeque, T = 3)\n");
314 const int gn = 32;
315 const double h = 1.0 / gn, T = 3.0;
316 const long steps = 768; // the V1 gate-D schedule at 32^3 (CFL 0.25)
317 const double dt = T / steps;
318 const double cx = 0.35, cy = 0.35, cz = 0.35, r = 0.15;
319
320 Patch patch; // supplies the face velocity; also carries the eps = 0 union
321 Patch patch2; // carries the eps = 1e-12 union
322 patch.init(gn, h);
323 patch2.init(gn, h);
325 ref.init(gn, gn, gn, h, G);
326 const vofscene::Block rblk = vofscene::blockOf(ref, I3{0, 0, 0});
327 const I3 e = ref.extent();
328 ref.exchange = [e](SField f) { vofscene::periodicFill(f, e, G, true, true, true); };
329 refSphere(ref, rblk, h, cx, cy, cz, r);
330 ref.syncGhosts();
331
332 VofBlockSet exact, prod;
333 exact.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
334 prod.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
335 exact.bubbleEps = 0.0;
336 serialExchange(exact, patch);
337 serialExchange(prod, patch2);
338 exact.seedSphere(cx, cy, cz, r);
339 prod.seedSphere(cx, cy, cz, r);
340 exact.scatter(patch.adv.colour());
341 prod.scatter(patch2.adv.colour());
342
343 double worst = 0.0;
344 const long d0 = bitwiseDiff(patch.adv.colour(), ref.colour(), e, gn, G, &worst);
345 std::printf(" seed: bitwise diffs union vs global field %ld\n", d0);
346 CHECK(d0 == 0);
347
348 const double v0 = sumInner(ref.colour(), e, gn, G);
349 long nRecentre = 0, nRecentreP = 0, maxCells = 0, maxCellsP = 0;
350 long firstBad0 = -1;
351 double afterWorst = 0.0;
352 long unionDiffs = 0, unionOther = 0;
353 double unionWorst = 0.0, prodWorst = 0.0;
354 long prodFirst = -1;
355 for (long s = 0; s < steps; ++s) {
356 const double phase = std::cos(M_PI * (s + 0.5) * dt / T);
357 vofscene::fillLeVeque(patch.adv, patch.blk, h, phase);
358 // The block GATHERS its ghost face velocity from the owner of the WRAPPED global cell, so the
359 // reference must consume the wrapped value too. Evaluating the analytic potential at the
360 // out-of-domain index instead agrees only to round-off (sin^2(-pi h) against sin^2(pi(1-h)))
361 // and that 1e-19 alone breaks a bitwise gate -- a property of the scene, not of the container.
362 vofscene::periodicFill(patch.adv.faceU(), e, G, true, true, true);
363 vofscene::periodicFill(patch.adv.faceV(), e, G, true, true, true);
364 vofscene::periodicFill(patch.adv.faceW(), e, G, true, true, true);
365 Kokkos::deep_copy(ref.faceU(), patch.adv.faceU());
366 Kokkos::deep_copy(ref.faceV(), patch.adv.faceV());
367 Kokkos::deep_copy(ref.faceW(), patch.adv.faceW());
368 Kokkos::deep_copy(patch2.adv.faceU(), patch.adv.faceU());
369 Kokkos::deep_copy(patch2.adv.faceV(), patch.adv.faceV());
370 Kokkos::deep_copy(patch2.adv.faceW(), patch.adv.faceW());
371 ref.advect(dt, s);
372 exact.advect(dt, patch.adv.colour());
373 prod.advect(dt, patch2.adv.colour());
374 nRecentre += exact.blocks()[0].stats().recentred ? 1 : 0;
375 nRecentreP += prod.blocks()[0].stats().recentred ? 1 : 0;
376 maxCells = std::max(maxCells, exact.blocks()[0].stats().cells);
377 maxCellsP = std::max(maxCellsP, prod.blocks()[0].stats().cells);
378
379 const std::vector<double> gr = fieldOnGrid(ref.colour(), e, gn, G);
380 // THE gate: the block's own colour == the global field's, bit for bit, over the block's box
381 const std::vector<double> ge = blockOnGrid(exact.blocks()[0], gn);
382 const long dBox = gridDiff(ge, gr, &worst);
383 // The block is bitwise the global field until the Weymouth-Yue round-off RESIDUE — which the
384 // sweeps leave in every cell they touch, down to +-1e-300 and to signed zeros — leaves the
385 // block's support. That residue is part of the global field's state and is deliberately NOT
386 // part of the block's: the block's ghost policy says "outside my box it is pure gas", which is
387 // the marker model (the alternative, ghosting from the UNION, would reproduce the global field
388 // bit for bit and let a neighbouring marker's colour flux in, i.e. coalesce). So record the
389 // BITWISE HORIZON and bound what happens after it, rather than asserting bitwise forever.
390 if (dBox != 0 && firstBad0 < 0)
391 firstBad0 = s;
392 afterWorst = std::fmax(afterWorst, worst);
393 // the production default: the same comparison, now with the wake dropped
394 double wp = 0.0;
395 const long dp = gridDiff(blockOnGrid(prod.blocks()[0], gn), gr, &wp);
396 if (dp != 0 && prodFirst < 0)
397 prodFirst = s;
398 prodWorst = std::fmax(prodWorst, wp);
399 // The UNION is a different object: `C = max_blocks C_block` from an empty union, so it CLIPS
400 // the NEGATIVE Weymouth-Yue round-off residue to an exact 0. Every union/reference difference
401 // must be exactly that, and nothing else.
402 const std::vector<double> gu = fieldOnGrid(patch.adv.colour(), e, gn, G);
403 double wu = 0.0;
404 const long dUni = gridDiff(gu, gr, &wu);
405 long other = 0;
406 for (std::size_t i = 0; i < gu.size(); ++i)
407 if (std::memcmp(&gu[i], &gr[i], sizeof(double)) != 0 && !(gr[i] < 0.0 && gu[i] == 0.0))
408 ++other;
409 unionDiffs = std::max(unionDiffs, dUni);
410 unionWorst = std::fmax(unionWorst, wu);
411 unionOther = std::max(unionOther, other);
412 }
413 const double v1 = sumInner(patch.adv.colour(), e, gn, G);
414 const double v2 = sumInner(patch2.adv.colour(), e, gn, G);
415 const double vr = sumInner(ref.colour(), e, gn, G);
416 const auto& st = exact.blocks()[0].stats();
417 const auto& sp = prod.blocks()[0].stats();
418 std::printf(" eps = 0 : bitwise horizon %ld steps; after it max|d| %.3e over %ld steps\n",
419 firstBad0 < 0 ? steps : firstBad0, afterWorst, steps);
420 std::printf(
421 " %ld re-centrings, largest box %ld cells (%.1f%% of the grid) -- the WAKE\n",
422 nRecentre, maxCells, 100.0 * maxCells / (gn * gn * gn));
423 std::printf(
424 " eps = 1e-12 : bitwise horizon %ld steps; max|d| over the run %.3e, colour dropped %.3e\n",
425 prodFirst < 0 ? steps : prodFirst, prodWorst, sp.discarded);
426 std::printf(" %ld re-centrings, largest box %ld cells (%.1f%% of the grid)\n",
427 nRecentreP, maxCellsP, 100.0 * maxCellsP / (gn * gn * gn));
428 std::printf(
429 " union vs the global field: worst-case %ld cells differ (max|d| %.3e); of those %ld\n"
430 " are NOT the UNPACK_MAX clip of a negative WY residue\n",
431 unionDiffs, unionWorst, unionOther);
432 std::printf(" volume: union(eps=0) %.17g union(prod) %.17g global %.17g drift %.3e / %.3e\n",
433 v1, v2, vr, std::fabs(v1 - v0) / v0, std::fabs(v2 - v0) / v0);
434 std::printf(" block stats: V %.6f centroid (%.4f, %.4f, %.4f) moments (%.3e, %.3e, %.3e)\n",
435 st.volume, st.centroid[0], st.centroid[1], st.centroid[2], st.moment[0], st.moment[1],
436 st.moment[2]);
437 CHECK(firstBad0 != 0); // the first step is bitwise by construction
438 CHECK(afterWorst < 1e-15); // and never more than the WY round-off residue after
439 CHECK(nRecentre > 0);
440 CHECK(nRecentreP > 0);
441 CHECK(unionOther == 0);
442 CHECK(prodWorst < 1e-15);
443 CHECK(std::fabs(sp.discarded) < 1e-15);
444 CHECK(std::fabs(v1 - v0) / v0 < 1e-13);
445 CHECK(std::fabs(v2 - v0) / v0 < 1e-13);
446}
447
448// ======================================================================== G2: two bubbles
459KOKKOS_INLINE_FUNCTION double cellPsi(double x, double y, double A, double phase, double x0) {
460 const double PI = 3.14159265358979323846;
461 return -(A / (2.0 * PI)) * Kokkos::sin(2.0 * PI * (x - x0)) * Kokkos::sin(2.0 * PI * y) * phase;
462}
463void fillCellular(WyAdvector& a, vofscene::Block b, double h, double A, double phase,
464 double x0 = 0.0) {
465 SField u = a.faceU(), v = a.faceV(), w = a.faceW();
466 const double invh = 1.0 / h;
468 b, KOKKOS_LAMBDA(long i, int gx, int gy, int) {
469 const double xn = gx * h, yn = gy * h;
470 const double xp = (gx + 1) * h, yp = (gy + 1) * h;
471 u(i) = (cellPsi(xp, yp, A, phase, x0) - cellPsi(xp, yn, A, phase, x0)) * invh;
472 v(i) = -(cellPsi(xp, yp, A, phase, x0) - cellPsi(xn, yp, A, phase, x0)) * invh;
473 w(i) = 0.0;
474 });
475}
476
477void gateTwoBubbles() {
478 std::printf(
479 "\n=== G2 two bubbles: overlap without coalescence, and the reversal recovers both\n");
480 const int gn = 48;
481 const double h = 1.0 / gn;
482 const double A = 1.0, T = 0.9;
483 const long steps = 216;
484 const double dt = T / steps;
485 // u ~ +A sin(2pi(x - x0)) along y = 1/2, so the ATTRACTING plane is x = x0 + 1/2. Shift it onto
486 // a cell CENTRE: on a cell FACE the two markers can never share a cell and the overlap this gate
487 // looks for is structurally impossible (measured: 0 shared cells with the plane at x = 1/2).
488 const double x0 = 0.5 * h, xatt = x0 + 0.5;
489 const double r = 0.10, y0 = 0.5, z0 = 0.5;
490 const double x1 = xatt - 0.1208, x2 = xatt + 0.1208; // 2 cells of gap between the surfaces
491
492 Patch patch;
493 patch.init(gn, h);
494 const I3 e = patch.adv.extent();
495
496 VofBlockSet set;
497 set.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
498 serialExchange(set, patch);
499 set.seedSphere(x1, y0, z0, r);
500 set.seedSphere(x2, y0, z0, r);
501 set.scatter(patch.adv.colour());
502 SField c0 = vofscene::copyOf(patch.adv.colour(), "c0");
503
504 // the single-field CONTROL: both spheres in ONE colour field
505 WyAdvector ctl;
506 ctl.init(gn, gn, gn, h, G);
507 const vofscene::Block cblk = vofscene::blockOf(ctl, I3{0, 0, 0});
508 ctl.exchange = [e](SField f) { vofscene::periodicFill(f, e, G, true, true, true); };
509 Kokkos::deep_copy(ctl.colour(), c0);
510 ctl.syncGhosts();
511
512 const double vb0[2] = {set.blocks()[0].stats().volume, set.blocks()[1].stats().volume};
513 double drift[2] = {0.0, 0.0};
514 long overlapMax = 0;
515 double unionDeficitMax = 0.0;
516 for (long s = 0; s < steps; ++s) {
517 const double phase = std::cos(M_PI * (s + 0.5) * dt / T);
518 fillCellular(patch.adv, patch.blk, h, A, phase, x0);
519 vofscene::periodicFill(patch.adv.faceU(), e, G, true, true, true);
520 vofscene::periodicFill(patch.adv.faceV(), e, G, true, true, true);
521 vofscene::periodicFill(patch.adv.faceW(), e, G, true, true, true);
522 Kokkos::deep_copy(ctl.faceU(), patch.adv.faceU());
523 Kokkos::deep_copy(ctl.faceV(), patch.adv.faceV());
524 Kokkos::deep_copy(ctl.faceW(), patch.adv.faceW());
525 ctl.advect(dt, s);
526 set.advect(dt, patch.adv.colour());
527 for (int b = 0; b < 2; ++b)
528 drift[b] = std::fmax(drift[b], std::fabs(set.blocks()[b].stats().volume - vb0[b]) / vb0[b]);
529 // cells where BOTH markers carry liquid: impossible in a single field, which is the point
530 const VofBox& b0 = set.blocks()[0].box;
531 const VofBox& b1 = set.blocks()[1].box;
532 const VofBox ov = VofBox::intersect(b0, b1);
533 if (!ov.empty()) {
534 auto h0 = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(),
535 set.blocks()[0].advector().colour());
536 auto h1 = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(),
537 set.blocks()[1].advector().colour());
538 const I3 e0 = set.blocks()[0].advector().extent(), e1 = set.blocks()[1].advector().extent();
539 long cnt = 0;
540 for (int z = ov.lo[2]; z < ov.hi[2]; ++z)
541 for (int y = ov.lo[1]; y < ov.hi[1]; ++y)
542 for (int x = ov.lo[0]; x < ov.hi[0]; ++x) {
543 const double a = h0(L3(x - b0.lo[0] + G, y - b0.lo[1] + G, z - b0.lo[2] + G, e0));
544 const double bb = h1(L3(x - b1.lo[0] + G, y - b1.lo[1] + G, z - b1.lo[2] + G, e1));
545 if (a > 1e-12 && bb > 1e-12)
546 ++cnt;
547 }
548 overlapMax = std::max(overlapMax, cnt);
549 }
550 // the union is a max, so it is SMALLER than the sum of the markers wherever they overlap
551 const double su = sumInner(patch.adv.colour(), e, gn, G);
552 const double sm = set.blocks()[0].stats().volume + set.blocks()[1].stats().volume;
553 unionDeficitMax = std::fmax(unionDeficitMax, sm - su);
554 }
555 // recovery: L1 distance of the final field from the initial one
556 const double l1blk = vofscene::l1Diff(patch.adv.colour(), c0, e, I3{gn, gn, gn}, G);
557 const double l1ctl = vofscene::l1Diff(ctl.colour(), c0, e, I3{gn, gn, gn}, G);
558 // the "neck": liquid exactly halfway between the two seeds at the END (0 => two separate bubbles)
559 auto hu = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), patch.adv.colour());
560 auto hc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), ctl.colour());
561 const int mx = static_cast<int>(xatt * gn), my = static_cast<int>(y0 * gn),
562 mz = static_cast<int>(z0 * gn);
563 const double neckB = hu(L3(mx + G, my + G, mz + G, e));
564 const double neckC = hc(L3(mx + G, my + G, mz + G, e));
565 const double v0 = sumInner(c0, e, gn, G);
566 const double vB = sumInner(patch.adv.colour(), e, gn, G);
567 const double vC = sumInner(ctl.colour(), e, gn, G);
568 std::printf(" %ld steps, CFL %.3f: max cells with BOTH markers liquid = %ld\n", steps,
569 A * dt / h, overlapMax);
570 std::printf(" max union DEFICIT (V_0 + V_1 - V_union) = %.4f cells of shared liquid\n",
571 unionDeficitMax);
572 std::printf(" per-marker volume drift: block0 %.3e block1 %.3e\n", drift[0], drift[1]);
573 std::printf(" union volume drift %.3e control volume drift %.3e\n", std::fabs(vB - v0) / v0,
574 std::fabs(vC - v0) / v0);
575 std::printf(
576 " recovery L1 vs the initial field: BLOCKS %.4e single-field CONTROL %.4e"
577 " (ratio %.2f)\n",
578 l1blk, l1ctl, l1ctl / l1blk);
579 std::printf(" neck cell (%d,%d,%d) at the end: blocks %.3e control %.3e\n", mx, my, mz, neckB,
580 neckC);
581 CHECK(overlapMax > 0);
582 CHECK(unionDeficitMax > 0.0);
583 CHECK(drift[0] < 1e-14);
584 CHECK(drift[1] < 1e-14);
585 CHECK(l1ctl > l1blk);
586}
587
588// ======================================================================== G3: re-centring
589//
590// A sphere translated 20 cells. The moving block (margin 3 + pad 2) is compared with a block large
591// enough that it never has to move, at both `bubbleEps` settings -- so the gate separates "the
592// re-centring copy is exact" (which is bitwise at eps = 0) from "the wake the production threshold
593// drops" (a 1e-19 residue and nothing else).
594void gateRecentre() {
595 std::printf("\n=== G3 re-centring: a block that MOVES is bitwise a block that never has to\n");
596 const int gn = 48;
597 const double h = 1.0 / gn;
598 const double dt = 0.2 * h; // CFL 0.2 with |u| = 1
599 const long steps = 100; // 20 cells of translation
600 const double r = 5.0 * h;
601 const double epsList[2] = {0.0, 1e-12};
602
603 for (int k = 0; k < 2; ++k) {
604 Patch pa, pb;
605 pa.init(gn, h);
606 pb.init(gn, h);
607 vofscene::fillUniform(pa.adv, pa.blk, 1.0, 0.0, 0.0);
608 vofscene::fillUniform(pb.adv, pb.blk, 1.0, 0.0, 0.0);
609
610 VofBlockSet moving, fixed;
611 moving.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
612 fixed.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
613 moving.bubbleEps = epsList[k];
614 fixed.bubbleEps = epsList[k];
615 fixed.allowShrink = false; // a block large enough that it never has to move
616 serialExchange(moving, pa);
617 serialExchange(fixed, pb);
618 moving.seedSphere(10.5 * h, 24.5 * h, 24.5 * h, r);
619 fixed.seedSphere(10.5 * h, 24.5 * h, 24.5 * h, r, 4, 30);
620 moving.scatter(pa.adv.colour());
621 fixed.scatter(pb.adv.colour());
622
623 const double v0 = moving.blocks()[0].stats().volume;
624 const int lo0 = moving.blocks()[0].box.lo[0];
625 long nMove = 0, nFix = 0, firstBad = -1;
626 double worst = 0.0;
627 for (long s = 0; s < steps; ++s) {
628 moving.advect(dt, pa.adv.colour());
629 fixed.advect(dt, pb.adv.colour());
630 nMove += moving.blocks()[0].stats().recentred ? 1 : 0;
631 nFix += fixed.blocks()[0].stats().recentred ? 1 : 0;
632 double w = 0.0;
633 const long d =
634 gridDiff(blockOnGrid(moving.blocks()[0], gn), blockOnGrid(fixed.blocks()[0], gn), &w);
635 worst = std::fmax(worst, w);
636 if (d != 0 && firstBad < 0)
637 firstBad = s;
638 }
639 const auto& sm = moving.blocks()[0].stats();
640 const auto& sf = fixed.blocks()[0].stats();
641 std::printf(" -- bubbleEps = %.0e\n", epsList[k]);
642 std::printf(
643 " moving: box [%d,%d)x[%d,%d)x[%d,%d) lo_x %d -> %d %ld re-centrings, %ld cells\n",
644 sm.lo[0], sm.hi[0], sm.lo[1], sm.hi[1], sm.lo[2], sm.hi[2], lo0, sm.lo[0], nMove, sm.cells);
645 std::printf(" fixed : box [%d,%d)x... %ld re-centrings, %ld cells\n", sf.lo[0], sf.hi[0],
646 nFix, sf.cells);
647 std::printf(" first differing step %ld (-1 = none) max|d| over the run %.3e\n", firstBad,
648 worst);
649 std::printf(
650 " volume %.17g -> %.17g drift %.3e dropped %.3e centroid x %.5f (exact %.5f)\n", v0,
651 sm.volume, std::fabs(sm.volume - v0) / v0, sm.discarded, sm.centroid[0],
652 (10.5 + steps * 0.2) * h);
653 CHECK(nMove > 0);
654 CHECK(nFix == 0);
655 CHECK(std::fabs(sm.volume - v0) / v0 < 1e-14);
656 CHECK(std::fabs(sm.centroid[0] - (10.5 + steps * 0.2) * h) < 0.5 * h);
657 if (k == 0)
658 CHECK(firstBad < 0); // nothing dropped => bitwise
659 else
660 CHECK(worst < 1e-15); // only the dropped wake, at round-off magnitude
661 }
662}
663
664// ================================================= W1: assignment, pool, device pack, area
665//
666// W1a master assignment : round robin vs LPT vs weighted ORB on a 64-bubble swarm of MIXED
667// sizes, at np 1/2/4/8 (the assignment is a pure function of the
668// replicated table, so it is measurable without MPI).
669// W1b device packing : the device-resident pack/unpack produces the same union BITWISE as
670// the W0 host-staged path, and the packing time of both is reported.
671// W1c block pool : `usePool` is bitwise inert, and the allocation census.
672// W1d interface area : a seeded sphere's PLIC area against 4 pi R^2.
673void gateAssignment() {
674 std::printf("\n=== W1a master assignment: round robin vs LPT vs weighted ORB (64 bubbles)\n");
675 // 64 bubbles on a 4x4x4 lattice with radii spanning 2.2x, so the block SIZES differ by ~10x in
676 // cell count -- exactly the regime round robin is blind to.
677 const int gn = 64;
678 const double h = 1.0 / gn;
679 VofBlockSet set;
680 set.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
681 Patch patch;
682 patch.init(gn, h);
683 serialExchange(set, patch);
684 int k = 0;
685 for (int i = 0; i < 4; ++i)
686 for (int j = 0; j < 4; ++j)
687 for (int m = 0; m < 4; ++m, ++k) {
688 // radii 2 .. 9 cells: the block box is the extent + 3 margin + 3 ghost, so the cell
689 // counts span ~10x -- the regime a round robin by block id is blind to.
690 const double r = (2.0 + 7.0 * ((k % 8) / 7.0)) / gn;
691 set.seedSphere((i + 0.5) / 4, (j + 0.5) / 4, (m + 0.5) / 4, r);
692 }
693 CHECK(set.count() == 64);
694 std::vector<long> w(set.count());
695 long tot = 0, wmin = 1L << 60, wmax = 0;
696 for (std::size_t b = 0; b < set.count(); ++b) {
697 w[b] = set.blocks()[b].box.cells();
698 tot += w[b];
699 wmin = std::min(wmin, w[b]);
700 wmax = std::max(wmax, w[b]);
701 }
702 std::printf(" block cell counts: min %ld max %ld (ratio %.1fx) total %ld\n", wmin, wmax,
703 double(wmax) / wmin, tot);
704 std::printf(" %-4s %-14s %-14s %-14s\n", "np", "round robin", "LPT", "weighted ORB");
705 const int nps[4] = {1, 2, 4, 8};
706 for (int q = 0; q < 4; ++q) {
707 const int np = nps[q];
708 double imb[3];
709 for (int mode = 0; mode < 3; ++mode) {
710 std::vector<int> m;
711 if (mode == 0) {
712 m.assign(w.size(), 0);
713 for (std::size_t b = 0; b < w.size(); ++b)
714 m[b] = static_cast<int>(b % np);
715 } else if (mode == 1) {
717 } else {
719 }
720 std::vector<long> load(np, 0);
721 for (std::size_t b = 0; b < w.size(); ++b)
722 load[m[b]] += w[b];
723 long mx = 0;
724 for (long v : load)
725 mx = std::max(mx, v);
726 imb[mode] = double(mx) * np / double(tot);
727 }
728 std::printf(" %-4d %-14.4f %-14.4f %-14.4f\n", np, imb[0], imb[1], imb[2]);
729 // LPT must never be worse than the round robin, and it must be within 4/3 of perfect
730 CHECK(imb[1] <= imb[0] + 1e-12);
731 CHECK(imb[1] <= 4.0 / 3.0 + 1e-12);
732 }
733 // W1d: the interface area of the biggest marker against the sphere formula
734 std::size_t big = 0;
735 for (std::size_t b = 1; b < set.count(); ++b)
736 if (set.blocks()[b].stats().volume > set.blocks()[big].stats().volume)
737 big = b;
738 const auto& st = set.blocks()[big].stats();
739 const double R = std::cbrt(st.volume * 3.0 / (4.0 * M_PI));
740 const double exact = 4.0 * M_PI * R * R;
741 std::printf(" W1d interface area of marker %zu: V %.6e -> R %.5f (%.2f cells), area %.6e vs "
742 "4 pi R^2 %.6e (%+.2f %%)\n",
743 big, st.volume, R, R / h, st.area, exact, 100.0 * (st.area / exact - 1.0));
744 CHECK(std::fabs(st.area / exact - 1.0) < 0.05);
745}
746
747// The whole W1 machinery must not move a single bit: run the SAME 8-bubble LeVeque scene four
748// ways -- (host packing, no pool), (device packing, no pool), (host, pool), (device, pool) -- and
749// demand the four unions and the 8 marker volumes are bitwise identical.
750void gateInert() {
751 std::printf("\n=== W1b/W1c device-resident packing and the block pool are BITWISE inert\n");
752 const int gn = 48;
753 const double h = 1.0 / gn, T = 0.75;
754 const long steps = 360; // CFL 0.2 for the LeVeque field at 48^3
755 const double dt = T / steps;
756 struct Cfg {
757 bool dev, pool;
758 const char* name;
759 };
760 const Cfg cfgs[4] = {{false, false, "host, no pool"},
761 {true, false, "device, no pool"},
762 {false, true, "host, pool"},
763 {true, true, "device, pool"}};
764 std::vector<double> ref;
765 std::vector<double> refVol;
766 double packMs[4] = {0, 0, 0, 0};
767 for (int q = 0; q < 4; ++q) {
768 Patch patch;
769 patch.init(gn, h);
770 VofBlockSet set;
771 set.init(I3{gn, gn, gn}, {true, true, true}, 0, 1, h);
772 auto ex = serialExchange(set, patch);
773 ex->deviceStaging = cfgs[q].dev;
774 set.usePool = cfgs[q].pool;
775 for (int i = 0; i < 2; ++i)
776 for (int j = 0; j < 2; ++j)
777 for (int m = 0; m < 2; ++m)
778 set.seedSphere(0.25 + 0.5 * i, 0.25 + 0.5 * j, 0.25 + 0.5 * m, 0.09);
779 set.scatter(patch.adv.colour());
780 Kokkos::Timer timer;
781 double tPack = 0.0;
782 for (long s = 0; s < steps; ++s) {
783 const double phase = std::cos(M_PI * (s + 0.5) * dt / T);
784 vofscene::fillLeVeque(patch.adv, patch.blk, h, phase);
785 vofscene::periodicFill(patch.adv.faceU(), patch.adv.extent(), G, true, true, true);
786 vofscene::periodicFill(patch.adv.faceV(), patch.adv.extent(), G, true, true, true);
787 vofscene::periodicFill(patch.adv.faceW(), patch.adv.extent(), G, true, true, true);
788 Kokkos::fence();
789 const double t0 = timer.seconds();
790 set.advect(dt, patch.adv.colour());
791 Kokkos::fence();
792 tPack += timer.seconds() - t0;
793 }
794 packMs[q] = 1e3 * tPack / steps;
795 const std::vector<double> got = fieldOnGrid(patch.adv.colour(), patch.adv.extent(), gn, G);
796 std::vector<double> vol(set.count());
797 for (std::size_t b = 0; b < set.count(); ++b)
798 vol[b] = set.blocks()[b].stats().volume;
799 if (q == 0) {
800 ref = got;
801 refVol = vol;
802 } else {
803 double worst = 0.0;
804 const long d = gridDiff(got, ref, &worst);
805 double dv = 0.0;
806 for (std::size_t b = 0; b < vol.size(); ++b)
807 dv = std::fmax(dv, std::fabs(vol[b] - refVol[b]));
808 std::printf(" %-18s vs (host, no pool): %ld cells differ, max|dV| %.3e\n", cfgs[q].name, d,
809 dv);
810 CHECK(d == 0);
811 CHECK(dv == 0.0);
812 }
813 const auto ps = std::array<long, 2>{set.poolHits(), set.poolMisses()};
814 std::printf(" %-18s block-step %.3f ms pool hits %ld misses %ld\n", cfgs[q].name, packMs[q],
815 ps[0], ps[1]);
816 if (cfgs[q].pool)
817 CHECK(ps[0] > 0); // the pool must actually be recycling on a moving swarm
818 }
819 std::printf(" device vs host block step: %.3f ms vs %.3f ms (ratio %.2fx)\n", packMs[1],
820 packMs[0], packMs[0] / packMs[1]);
821}
822
823// C1 (doc/vof_step_performance_design.md §4.6, §5.9): the BATCHED container step -- every stage
824// for all master blocks in one launch -- must be the per-block step bit for bit. Same 8-bubble
825// LeVeque scene as gateInert, run with `batched` on and off, on a fully periodic box and on one
826// with non-periodic y (the clamp pass of the ghost policy), the wisp guard on (so the residue pass
827// runs) and the block CSF on (curvature cascade, CSF face force and its scatter, the debris
828// removal). Compared: the union, every block's own colour, box and every VofBlockStats field, the
829// three scattered CSF face-force fields and the curvature census. Since C2 (§5.10, WO-9) the
830// batched path measures with team reductions, so the measured statistics (volume, centroid,
831// velocity, moments, area) are held to 1e-12 relative instead of bitwise; everything else is
832// still bitwise.
833void gateBatched() {
834 std::printf("\n=== C1 batched container stages == per-block stages, BITWISE\n");
835 const int gn = 48;
836 const double h = 1.0 / gn, T = 0.75;
837 const long steps = 120;
838 const double dt = T / 360; // CFL 0.2 for the LeVeque field at 48^3 (gateInert's schedule)
839 for (int geo = 0; geo < 2; ++geo) {
840 const std::array<bool, 3> per = {true, geo == 0, true};
841 std::vector<double> ref;
842 std::vector<std::vector<double>> refBlk;
843 std::vector<peclet::flow::vof::VofBlockStats> refSt;
844 std::vector<double> refF[3];
846 for (int q = 0; q < 2; ++q) {
847 Patch patch;
848 patch.init(gn, h);
849 VofBlockSet set;
850 set.init(I3{gn, gn, gn}, per, 0, 1, h);
851 serialExchange(set, patch);
852 set.setWispEps(1e-8);
853 set.curvProto.interfaceEps = 1e-8;
854 set.curvProto.pureEps = 1e-8;
855 set.debrisRemove = true;
856 set.enableCsf(0.5);
857 set.batched = (q == 0);
858 SField force[3];
859 const long plen = static_cast<long>(patch.adv.extent().x) * patch.adv.extent().y *
860 patch.adv.extent().z;
861 for (int c = 0; c < 3; ++c)
862 force[c] = SField("gate_c1_force", plen);
863 // with y walls the markers sit 5.8 cells off the walls, so their boxes are clamped at the
864 // domain faces and the clamp pass of the ghost policy has cells to fill
865 const double y0 = geo == 0 ? 0.25 : 0.12, dy = geo == 0 ? 0.5 : 0.76;
866 for (int i = 0; i < 2; ++i)
867 for (int j = 0; j < 2; ++j)
868 for (int m = 0; m < 2; ++m)
869 set.seedSphere(0.25 + 0.5 * i, y0 + dy * j, 0.25 + 0.5 * m, 0.09);
870 set.scatter(patch.adv.colour());
871 long clamped = 0;
872 for (const auto& b : set.blocks())
873 clamped += (b.box.lo[1] == 0 || b.box.hi[1] == gn) ? 1 : 0;
874 if (geo == 1)
875 CHECK(clamped > 0);
876 long recentred = 0;
877 for (long s = 0; s < steps; ++s) {
878 const double phase = std::cos(M_PI * (s + 0.5) * dt / T);
879 vofscene::fillLeVeque(patch.adv, patch.blk, h, phase);
880 vofscene::periodicFill(patch.adv.faceU(), patch.adv.extent(), G, true, true, true);
881 vofscene::periodicFill(patch.adv.faceV(), patch.adv.extent(), G, true, true, true);
882 vofscene::periodicFill(patch.adv.faceW(), patch.adv.extent(), G, true, true, true);
883 Kokkos::fence();
884 set.advect(dt, patch.adv.colour());
885 set.computeCsf(force[0], force[1], force[2]);
886 for (const auto& st : set.statsAll())
887 recentred += st.recentred ? 1 : 0;
888 }
889 std::vector<double> fgot[3];
890 for (int c = 0; c < 3; ++c)
891 fgot[c] = fieldOnGrid(force[c], patch.adv.extent(), gn, G);
892 const auto cs = set.csfCurvatureStats();
893 const std::vector<double> got = fieldOnGrid(patch.adv.colour(), patch.adv.extent(), gn, G);
894 std::vector<std::vector<double>> blk;
895 for (std::size_t b = 0; b < set.count(); ++b)
896 blk.push_back(set.blockColourHost(b));
897 const auto st = set.statsAll();
898 if (q == 0) {
899 ref = got;
900 refBlk = blk;
901 refSt = st;
902 for (int c = 0; c < 3; ++c)
903 refF[c] = fgot[c];
904 refCs = cs;
905 std::printf(" %s: %zu blocks (%ld clamped at a wall), %ld re-centrings over %ld steps\n",
906 geo == 0 ? "periodic" : "y walls", set.count(), clamped, recentred, steps);
907 CHECK(recentred > 0); // the gate must exercise the re-centring after a batched step
908 continue;
909 }
910 double worst = 0.0;
911 const long d = gridDiff(got, ref, &worst);
912 long db = 0, ds = 0;
913 double c2worst = 0.0; // C2: the measured statistics, relative (vectors: max-norm relative)
914 // The centroid velocity is the difference quotient (c^n - c^{n-1}) / dt of two centroids, so
915 // a centroid held to 1e-12 relative bounds it by 2e-12 |c| / dt, not by 1e-12 |v|: c2vel is
916 // its difference expressed as that centroid error, |dv| dt / (2 |c|).
917 double c2vel = 0.0;
918 auto rel = [](const double* x, const double* y, int m) {
919 double dn = 0.0, yn = 0.0;
920 for (int k = 0; k < m; ++k) {
921 dn = std::fmax(dn, std::fabs(x[k] - y[k]));
922 yn = std::fmax(yn, std::fabs(y[k]));
923 }
924 return yn > 0.0 ? dn / yn : dn;
925 };
926 for (std::size_t b = 0; b < blk.size(); ++b) {
927 db += (blk[b].size() == refBlk[b].size() &&
928 std::memcmp(blk[b].data(), refBlk[b].data(), blk[b].size() * sizeof(double)) == 0)
929 ? 0
930 : 1;
931 const auto &x = st[b], &y = refSt[b];
932 c2worst = std::fmax(c2worst, rel(&x.volume, &y.volume, 1));
933 c2worst = std::fmax(c2worst, rel(&x.area, &y.area, 1));
934 c2worst = std::fmax(c2worst, rel(x.centroid, y.centroid, 3));
935 {
936 double dv = 0.0, cm = 0.0;
937 for (int k = 0; k < 3; ++k) {
938 dv = std::fmax(dv, std::fabs(x.velocity[k] - y.velocity[k]));
939 cm = std::fmax(cm, std::fabs(y.centroid[k]));
940 }
941 c2vel = std::fmax(c2vel, cm > 0.0 ? dv * dt / (2.0 * cm) : dv);
942 }
943 c2worst = std::fmax(c2worst, rel(x.moment, y.moment, 6));
944 bool same = x.discarded == y.discarded &&
945 x.debrisCells == y.debrisCells && x.debrisVolume == y.debrisVolume &&
946 x.debrisReturned == y.debrisReturned && x.debrisLost == y.debrisLost &&
947 x.debrisUnresolved == y.debrisUnresolved && x.fullAxis == y.fullAxis &&
948 x.residueReturned == y.residueReturned && x.recentred == y.recentred &&
949 x.cells == y.cells;
950 for (int k = 0; k < 3; ++k)
951 same = same && x.lo[k] == y.lo[k] && x.hi[k] == y.hi[k];
952 ds += same ? 0 : 1;
953 }
954 long df = 0;
955 double fmax = 0.0;
956 for (int c = 0; c < 3; ++c) {
957 df += (std::memcmp(fgot[c].data(), refF[c].data(), fgot[c].size() * sizeof(double)) == 0)
958 ? 0
959 : 1;
960 for (double v : fgot[c])
961 fmax = std::fmax(fmax, std::fabs(v));
962 }
963 const bool sameCs = cs.interfacial == refCs.interfacial && cs.hf == refCs.hf &&
964 cs.hfMixed == refCs.hfMixed && cs.hfFit == refCs.hfFit &&
965 cs.pv == refCs.pv && cs.pvReduced == refCs.pvReduced &&
966 cs.noEstimate == refCs.noEstimate && cs.clipped == refCs.clipped;
967 std::printf(" %s, per-block vs batched: union %ld cells differ, block colours %ld, "
968 "stats %ld (C2 measured stats rel %.2e, velocity as a centroid error %.2e), CSF force fields %ld "
969 "(max|f| %.3e), curvature census %s (interfacial %ld, pv %ld)\n",
970 geo == 0 ? "periodic" : "y walls", d, db, ds, c2worst, c2vel, df, fmax,
971 sameCs ? "same" : "DIFFERS", cs.interfacial, cs.pv);
972 CHECK(df == 0);
973 CHECK(sameCs);
974 CHECK(fmax > 0.0); // the gate must carry a force
975 CHECK(cs.pv > 0); // ... and exercise tier 3
976 CHECK(d == 0);
977 CHECK(db == 0);
978 CHECK(ds == 0);
979 CHECK(c2worst <= 1e-12);
980 CHECK(c2vel <= 1e-12);
981 }
982 }
983}
984
985// ---------------------------------------------------------------------------------------------
986// D1: seeded debris through the Solver (doc/vof_overlap_design.md G1(i), WO-4)
987// ---------------------------------------------------------------------------------------------
988
990double sphereFrac(double cx, double cy, double cz, double R, int x, int y, int z) {
991 const int NS = 8;
992 int in = 0;
993 for (int k = 0; k < NS; ++k)
994 for (int j = 0; j < NS; ++j)
995 for (int i = 0; i < NS; ++i) {
996 const double px = x + (i + 0.5) / NS - cx, py = y + (j + 0.5) / NS - cy,
997 pz = z + (k + 0.5) / NS - cz;
998 in += (px * px + py * py + pz * pz < R * R) ? 1 : 0;
999 }
1000 return static_cast<double>(in) / (NS * NS * NS);
1001}
1002
1003struct DebrisScene {
1004 static constexpr int NX = 48, NY = 32, NZ = 32;
1005 static constexpr double R = 5.0, SIGMA = 320.0, RHO_L = 1.0, RHO_G = 0.1, MU = 0.5333;
1006 // A at x = 16.3; B 16 cells further along x. B's box reaches back into A's +x band (x = 21).
1007 static constexpr double AX = 16.3, BX = 32.3, CY = 16.2, CZ = 16.1;
1008 // The speck: three cells of B's colour in A's interface band, in a line along y.
1009 static constexpr int SX = 21, SY0 = 15, SZ = 16;
1010 static constexpr double SPECK[3] = {1e-5, 1e-2, 3e-3};
1011 std::array<int, 6> box[2] = {{7, 7, 7, 26, 26, 26}, {18, 7, 7, 41, 26, 26}};
1012
1013 std::vector<double> colour(int m, bool speck) const {
1014 const std::array<int, 6>& b = box[m];
1015 std::vector<double> c;
1016 c.reserve(static_cast<std::size_t>(b[3] - b[0]) * (b[4] - b[1]) * (b[5] - b[2]));
1017 for (int z = b[2]; z < b[5]; ++z)
1018 for (int y = b[1]; y < b[4]; ++y)
1019 for (int x = b[0]; x < b[3]; ++x) {
1020 double v = sphereFrac(m == 0 ? AX : BX, CY, CZ, R, x, y, z);
1021 if (m == 1 && speck && x == SX && z == SZ && y >= SY0 && y < SY0 + 3)
1022 v = SPECK[y - SY0];
1023 c.push_back(v);
1024 }
1025 return c;
1026 }
1027 long localIndex(int m, int x, int y, int z) const {
1028 const std::array<int, 6>& b = box[m];
1029 const long nx = b[3] - b[0], ny = b[4] - b[1];
1030 return (x - b[0]) + nx * ((y - b[1]) + ny * static_cast<long>(z - b[2]));
1031 }
1032 void build(peclet::flow::IbmSolver& s, bool speck, bool clip, bool debris = true) const {
1033 s.setRho(RHO_L);
1034 s.setMu(MU);
1035 s.setPressureGeometry(std::vector<double>(static_cast<std::size_t>(NX) * NY * NZ, 10.0));
1036 s.setPressureChebyshev(true, 800, 1e-12);
1037 s.enableVof();
1038 s.setVof(std::vector<double>(static_cast<std::size_t>(NX) * NY * NZ, 0.0));
1039 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "",
1040 {RHO_L, RHO_G - RHO_L});
1041 s.setSurfaceTension(SIGMA);
1042 if (!clip)
1043 s.setVofBlockKappaClip(false);
1044 if (!debris)
1045 s.setVofBlockDebris(false);
1046 std::vector<std::array<int, 6>> boxes = {box[0], box[1]};
1047 std::vector<std::vector<double>> cols = {colour(0, speck), colour(1, speck)};
1048 s.enableVofBlocksFromColours(boxes, cols);
1049 s.enableVofBlockCsf();
1050 }
1051};
1052
1053void gateDebrisSpeck() {
1054 std::printf("\n=== D1 seeded debris through the Solver (vof_overlap_design G1)\n");
1055 DebrisScene sc;
1056 const double spMax = 1e-2; // max |dC| across any face touching the speck
1057 const double bound = sc.SIGMA * spMax; // sigma max|dC_speck| / Delta^2, Delta = 1
1058 // reference: the same two markers without the speck
1059 peclet::flow::IbmSolver ref(sc.NX, sc.NY, sc.NZ);
1060 sc.build(ref, false, true);
1061 peclet::flow::IbmSolver sp(sc.NX, sc.NY, sc.NZ);
1062 sc.build(sp, true, true);
1063 peclet::flow::IbmSolver raw(sc.NX, sc.NY, sc.NZ); // the speck with the clip OFF, for scale
1064 sc.build(raw, true, false);
1065 const auto kc = sp.vofBlockKappa(1), kr = raw.vofBlockKappa(1);
1066 double kmax = 0.0, kraw = 0.0;
1067 for (int j = 0; j < 3; ++j) {
1068 const long q = sc.localIndex(1, sc.SX, sc.SY0 + j, sc.SZ);
1069 std::printf(" speck cell y=%d C_B = %.0e kappa_B clipped %.4f (clip off %.4f)\n",
1070 sc.SY0 + j, sc.SPECK[j], kc[q], kr[q]);
1071 kmax = std::fmax(kmax, std::fabs(kc[q]));
1072 kraw = std::fmax(kraw, std::fabs(kr[q]));
1073 }
1074 double dF = 0.0, dFraw = 0.0;
1075 for (int c = 0; c < 3; ++c) {
1076 const auto f0 = ref.getVofBlockForce(c), f1 = sp.getVofBlockForce(c),
1077 f2 = raw.getVofBlockForce(c);
1078 for (std::size_t i = 0; i < f0.size(); ++i) {
1079 dF = std::fmax(dF, std::fabs(f1[i] - f0[i]));
1080 dFraw = std::fmax(dFraw, std::fabs(f2[i] - f0[i]));
1081 }
1082 }
1083 const auto cs = sp.vofBlockCurvatureStats(), cr = ref.vofBlockCurvatureStats();
1084 std::printf(" max|kappa_B| on the speck %.4f (clip off %.4f); max|F - F_ref| %.4e (clip off "
1085 "%.4e), bound sigma*max|dC|/h^2 = %.4e; clipped: speck run %ld, reference %ld\n",
1086 kmax, kraw, dF, dFraw, bound, cs.clipped, cr.clipped);
1087 CHECK(kmax <= 1.0);
1088 CHECK(dF <= bound);
1089 CHECK(cr.clipped == 0); // the resolved pair never fires the clip
1090 // the debris census sees the speck (and only it) on B, and nothing on A or the reference
1091 const auto ss = sp.vofBlockStats(), rs = ref.vofBlockStats();
1092 std::printf(" debris census before any step: speck run B %ld, reference %ld/%ld\n",
1093 ss[1].debrisCells, rs[0].debrisCells, rs[1].debrisCells);
1094 // G1(ii): one step. The debris pass runs after the advection, so after ONE step the speck
1095 // (no cell of B above 1/2 within two cells) is gone and its volume is on B's interface.
1096 peclet::flow::IbmSolver off(sc.NX, sc.NY, sc.NZ); // the speck with removal OFF (census only)
1097 sc.build(off, true, true, false);
1098 const double vB0 = sp.vofBlockStats()[1].volume;
1099 for (auto* s : {&ref, &sp, &off}) {
1100 s->setDt(0.25 * s->capillaryDt());
1101 s->step();
1102 }
1103 const auto ss1 = sp.vofBlockStats(), rs1 = ref.vofBlockStats(), os1 = off.vofBlockStats();
1104 double sumSpeck = 0.0;
1105 for (double v : sc.SPECK)
1106 sumSpeck += v;
1107 const auto cB = sp.vofBlockColour(1);
1108 const auto& bb = ss1[1];
1109 auto locB = [&](int x, int y, int z) { // B's box may have been re-centred: index its CURRENT box
1110 const long nx = bb.hi[0] - bb.lo[0], ny = bb.hi[1] - bb.lo[1];
1111 return (x - bb.lo[0]) + nx * ((y - bb.lo[1]) + ny * static_cast<long>(z - bb.lo[2]));
1112 };
1113 double speckLeft = 0.0;
1114 for (int j = 0; j < 3; ++j)
1115 speckLeft = std::fmax(speckLeft, std::fabs(cB[locB(sc.SX, sc.SY0 + j, sc.SZ)]));
1116 // The debris pass runs AFTER the step's advection, which fluxes a few 1e-9 of the speck into
1117 // neighbours below the interfacial threshold (1e-8); those are not debris but RESIDUE (§13).
1118 double residue = 0.0;
1119 for (int z = sc.SZ - 3; z <= sc.SZ + 3; ++z)
1120 for (int y = sc.SY0 - 3; y <= sc.SY0 + 5; ++y)
1121 for (int x = bb.lo[0]; x <= sc.SX + 3; ++x)
1122 residue += cB[locB(x, y, z)];
1123 const double dVrel = std::fabs(ss1[1].volume - vB0) / vB0;
1124 const auto cA1 = sp.vofBlockColour(0), cA0 = off.vofBlockColour(0);
1125 long aDiff = (cA1.size() == cA0.size()) ? 0 : -1;
1126 for (std::size_t i = 0; aDiff >= 0 && i < cA1.size(); ++i)
1127 aDiff += (cA1[i] == cA0[i]) ? 0 : 1;
1128 std::printf(" after one step: B removed %ld cells / %.17g (painted %.17g, |d| %.3e), lost %.3e, "
1129 "unresolved %ld; speck cells now max %.3e; B volume rel change %.3e; A: %ld cells "
1130 "differ from the removal-OFF run; census-only run reports %ld / %.17g\n",
1131 ss1[1].debrisCells, ss1[1].debrisVolume, sumSpeck,
1132 std::fabs(ss1[1].debrisVolume - sumSpeck), ss1[1].debrisLost,
1133 ss1[1].debrisUnresolved, speckLeft, dVrel, aDiff, os1[1].debrisCells,
1134 os1[1].debrisVolume);
1135 // the reference pair: nothing fires (the G0 zero-counter statement on a resolved pair)
1136 CHECK(rs1[0].debrisCells == 0 && rs1[1].debrisCells == 0);
1137 CHECK(rs1[0].debrisReturned == 0.0 && rs1[1].debrisReturned == 0.0);
1138 CHECK(ref.vofBlockCurvatureStats().clipped == 0);
1139 CHECK(ss1[0].debrisCells == 0 && ss1[0].debrisReturned == 0.0); // A has no debris
1140 CHECK(ss1[1].debrisCells >= 3);
1141 CHECK(speckLeft == 0.0); // the three painted cells read exactly 0
1142 // §13: the sub-wispEps part of the speck that the step's advection fluxed into neighbours is
1143 // cleared too and returned as RESIDUE (residueReturned, which also carries B's own WY residue),
1144 // so no B colour is left in the window and B's volume is exact.
1145 std::printf(" account: debris removed %.17g, residue returned %.3e (B, whole box), B left "
1146 "in the speck window %.3e (painted %.17g, painted - removed %.3e)\n",
1147 ss1[1].debrisVolume, ss1[1].residueReturned, residue, sumSpeck,
1148 sumSpeck - ss1[1].debrisVolume);
1149 CHECK(residue == 0.0);
1150 CHECK(ss1[1].residueReturned >= sumSpeck - ss1[1].debrisVolume - 1e-15);
1151 CHECK(std::fabs(ss1[1].debrisVolume - sumSpeck) <= 1e-7); // the residue is the step's WY flux
1152 CHECK(ss1[1].debrisReturned == ss1[1].debrisVolume);
1153 CHECK(ss1[1].debrisLost == 0.0);
1154 CHECK(ss1[1].debrisUnresolved == 0);
1155 CHECK(dVrel <= 1e-12); // B's volume: exact return
1156 CHECK(aDiff == 0); // A bitwise untouched by the removal
1157 CHECK(os1[1].debrisCells == ss1[1].debrisCells && os1[1].debrisReturned == 0.0);
1158 double umR = 0.0, umS = 0.0;
1159 for (int c = 0; c < 3; ++c) {
1160 for (double v : ref.getVelocity(c))
1161 umR = std::fmax(umR, std::fabs(v));
1162 for (double v : sp.getVelocity(c))
1163 umS = std::fmax(umS, std::fabs(v));
1164 }
1165 std::printf(" max|u| after one step: reference (no speck) %.6e, speck %.6e (difference %.3e)\n",
1166 umR, umS, umS - umR);
1167 CHECK(umS <= umR + 1e-6);
1168}
1169
1170
1171// ======================================================================== W: a marker at a wall
1174KOKKOS_INLINE_FUNCTION double wallPsi(double x, double y, double A) {
1175 const double PI = 3.14159265358979323846;
1176 return -(A / (2.0 * PI)) * Kokkos::sin(2.0 * PI * x) * 4.0 * y * (1.0 - y);
1177}
1178
1179void gateWall() {
1180 std::printf("\n=== W a marker against the wall y = 0: volume exact, block == global field\n");
1181 const int gn = 32;
1182 const double h = 1.0 / gn, A = 0.5;
1183 const double dt = 0.25 * h / (4.0 * A); // max|u| = 4A on the walls: CFL 0.25
1184 const long steps = 240;
1185 const double cx = 0.5, cy = 0.1, cz = 0.5, r = 0.2; // cut by the wall: a contact line at y = 0
1186 const std::array<bool, 3> per{true, false, true};
1187 const I3 gs{gn, gn, gn};
1188
1189 Patch patch;
1190 patch.init(gn, h);
1191 const I3 e = patch.adv.extent();
1192 { // the analytic discrete curl on the WHOLE extended block, out-of-domain cells included
1193 SField u = patch.adv.faceU(), v = patch.adv.faceV(), w = patch.adv.faceW();
1194 const double invh = 1.0 / h;
1196 patch.blk, KOKKOS_LAMBDA(long i, int gx, int gy, int) {
1197 const double xn = gx * h, xp = (gx + 1) * h, yn = gy * h, yp = (gy + 1) * h;
1198 u(i) = (wallPsi(xp, yp, A) - wallPsi(xp, yn, A)) * invh;
1199 v(i) = -(wallPsi(xp, yp, A) - wallPsi(xn, yp, A)) * invh;
1200 w(i) = 0.0;
1201 });
1202 // the block gathers the WRAPPED owner's value on a periodic axis (see gate G1)
1203 for (SField f : {u, v, w})
1204 vofscene::periodicFill(f, e, G, true, false, true);
1205 }
1206 // the triggering condition, asserted so the gate cannot go vacuous: v = 0 on the wall face
1207 // (advector index -1) and v != 0 on the first interior face (index 0)
1208 double vWall = 0.0, vIn = 0.0;
1209 {
1210 auto hv = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), patch.adv.faceV());
1211 for (int z = 0; z < gn; ++z)
1212 for (int x = 0; x < gn; ++x) {
1213 vWall = std::fmax(vWall, std::fabs(hv(L3(x + G, G - 1, z + G, e))));
1214 vIn = std::fmax(vIn, std::fabs(hv(L3(x + G, G, z + G, e))));
1215 }
1216 }
1217
1219 ref.init(gn, gn, gn, h, G);
1220 const vofscene::Block rblk = vofscene::blockOf(ref, I3{0, 0, 0});
1221 ref.exchange = [rblk, gs](SField f) {
1222 vofscene::periodicFill(f, rblk.e, G, true, false, true);
1223 vofscene::clampFill(f, rblk, gs, true, false, true);
1224 };
1225 refSphere(ref, rblk, h, cx, cy, cz, r);
1226 ref.syncGhosts();
1227 Kokkos::deep_copy(ref.faceU(), patch.adv.faceU());
1228 Kokkos::deep_copy(ref.faceV(), patch.adv.faceV());
1229 Kokkos::deep_copy(ref.faceW(), patch.adv.faceW());
1230
1231 VofBlockSet set;
1232 set.init(gs, per, 0, 1, h);
1233 serialExchange(set, patch);
1234 set.seedSphere(cx, cy, cz, r);
1235 set.scatter(patch.adv.colour());
1236 const auto& b0 = set.blocks()[0];
1237 const double v0 = b0.stats().volume;
1238 double wallLayer = 0.0;
1239 {
1240 const std::vector<double> gb = blockOnGrid(b0, gn);
1241 for (int z = 0; z < gn; ++z)
1242 for (int x = 0; x < gn; ++x) {
1243 const double c = gb[static_cast<std::size_t>(x) + static_cast<std::size_t>(z) * gn * gn];
1244 if (!std::isnan(c))
1245 wallLayer += c;
1246 }
1247 }
1248 const double vr0 = sumInner(ref.colour(), e, gn, G);
1249
1250 double worstRel = 0.0, worstD = 0.0;
1251 long firstDiff = -1;
1252 for (long s = 0; s < steps; ++s) {
1253 ref.advect(dt, s);
1254 set.advect(dt, patch.adv.colour());
1255 worstRel = std::fmax(worstRel, std::fabs(set.blocks()[0].stats().volume / v0 - 1.0));
1256 double wd = 0.0;
1257 const long d = gridDiff(blockOnGrid(set.blocks()[0], gn), fieldOnGrid(ref.colour(), e, gn, G),
1258 &wd);
1259 if (d != 0 && firstDiff < 0)
1260 firstDiff = s;
1261 worstD = std::fmax(worstD, wd);
1262 }
1263 const double vr1 = sumInner(ref.colour(), e, gn, G);
1264 const auto& st = set.blocks()[0].stats();
1265 std::printf(" scene: colour in the wall layer %.3f cells; max|v| on the wall face %.3e, one "
1266 "face in %.3e\n",
1267 wallLayer, vWall, vIn);
1268 std::printf(" %ld steps: block max|V/V0-1| %.3e (discarded %.3e, box y [%d, %d)); global field "
1269 "%.3e\n",
1270 steps, worstRel, st.discarded, set.blocks()[0].box.lo[1], set.blocks()[0].box.hi[1],
1271 std::fabs(vr1 / vr0 - 1.0));
1272 std::printf(" block vs global field: bitwise horizon %ld steps, max|d| %.3e\n",
1273 firstDiff < 0 ? steps : firstDiff, worstD);
1274 CHECK(wallLayer > 1.0); // the marker really occupies the wall-adjacent layer
1275 CHECK(vWall == 0.0);
1276 CHECK(vIn > 1e-3);
1277 CHECK(set.blocks()[0].box.lo[1] == 0);
1278 CHECK(worstRel <= 1e-12);
1279 CHECK(std::fabs(vr1 / vr0 - 1.0) <= 1e-12);
1280 CHECK(worstD < 1e-15);
1281}
1282
1283} // namespace
1284
1286template <class F>
1288 try {
1289 f();
1290 } catch (const std::runtime_error& e) {
1291 std::printf(" refused: %.70s...\n", e.what());
1292 return std::strstr(e.what(), "not composable") != nullptr &&
1293 std::strstr(e.what(), "block markers") != nullptr;
1294 }
1295 return false;
1296}
1297
1299 std::printf("\n=== M enable_vof_momentum x enable_vof_blocks: refused in either order\n");
1300 const int n = 16;
1301 const std::vector<std::array<double, 4>> seeds = {{8.0, 8.0, 8.0, 3.0}};
1302 { // momentum first: the block container is refused
1303 peclet::flow::IbmSolver s(n, n, n);
1304 s.setRho(1.0);
1305 s.setMu(0.1);
1306 s.enableVofMomentum(0.001, 1.0);
1307 CHECK(s.vofMomentumEnabled());
1308 CHECK(refusesMomentumBlocks([&] { s.enableVofBlocks(seeds); }));
1309 CHECK(!s.vofBlocksEnabled());
1310 }
1311 { // blocks first: momentum consistency is refused
1312 peclet::flow::IbmSolver s(n, n, n);
1313 s.setRho(1.0);
1314 s.setMu(0.1);
1315 s.enableVof();
1316 s.enableVofBlocks(seeds);
1317 CHECK(s.vofBlocksEnabled());
1318 CHECK(refusesMomentumBlocks([&] { s.enableVofMomentum(0.001, 1.0); }));
1319 CHECK(!s.vofMomentumEnabled());
1320 }
1321}
1322
1323int main(int argc, char** argv) {
1324 Kokkos::initialize(argc, argv);
1325 {
1326 std::printf("VoF rung W0 (WO-W0) - per-bubble block container, backend: %s\n", SExec::name());
1327 gatePlan();
1328 gateOneBubble();
1329 gateTwoBubbles();
1330 gateRecentre();
1331 gateAssignment();
1332 gateInert();
1333 gateBatched();
1334 gateDebrisSpeck();
1335 gateWall();
1337 std::printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "PASSED", failures,
1338 failures == 1 ? "" : "s");
1339 }
1340 Kokkos::finalize();
1341 return failures ? 1 : 0;
1342}
flow — VoF Part III rung W0: the per-bubble VoF BLOCK, a third container over the same L1 kernels (su...
flow — VoF Part III rung W0: the block gather/scatter (VOF_PLAN.md §10 design items 2 and 4).
The gather / scatter / table replication of the block container.
The container: a replicated table of blocks, the per-step orchestration (gather → advect → re-centre ...
VofCurvature::Stats csfCurvatureStats() const
void enableCsf(double sigmaValue)
Turn on the block CSF.
double bubbleEps
The colour threshold that defines the BUBBLE EXTENT (and hence the box).
void setExchange(std::shared_ptr< VofBlockExchangeBase > x)
bool batched
C1 (doc/vof_step_performance_design.md §4.6, §5.9): run each stage of the container step for ALL mast...
std::vector< VofBlock > & blocks()
The block table.
void computeCsf(SField fx, SField fy, SField fz)
Per-block curvature cascade + the CSF face force, summed into the caller's three face-force patches.
std::array< bool, 3 > periodic() const
VofCurvature curvProto
The curvature cascade's TUNABLES for every block, copied into each block's own VofCurvature when it i...
std::vector< double > blockColourHost(std::size_t idx)
One block's INNER colour, copied to the host in x-fastest order over blocks_[idx]....
void seedSphere(double cx, double cy, double cz, double r, int subLevels=4, int extraPad=0)
Seed a spherical bubble (centre and radius in CELL units, global).
bool debrisRemove
Remove the debris and return its volume (§5.3).
void init(I3 gs, std::array< bool, 3 > per, int rank, int size, double h=1.0)
void scatter(SField cLocal)
Union the current colour into cLocal without advecting (used right after seeding).
void advect(double dt, SField cLocal)
One kinematic block step: gather the face velocity, advect every local block, re-centre,...
std::vector< VofBlockStats > statsAll()
Per-bubble census on the master (empty entries on a non-master).
void setWispEps(double eps)
Set wispEps on the set AND on every live advector (pooled ones take it on reuse).
bool usePool
Rung W1 item (c): recycle the advectors a re-centring retires, keyed by the EXACT extent.
double interfaceEps
Wisp threshold on the interfacial predicate.
double pureEps
Tolerance of the PURITY test that ends a height-function column (hfIsFull/hfIsEmpty),...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
SField colour() const
Colour field on the extended block (x-fastest).
void init(int nx, int ny, int nz, double h, int ghost=3)
void syncGhosts()
Bring the colour ghosts up to date.
std::function< void(SField)> exchange
Refresh EVERY ghost layer of the given cell field (MPI exchange + domain BC fill)....
void advect(double dt, long step)
One Weymouth-Yue step: freeze the dilation flag, then three sweeps in the permutation selected by ste...
SField faceU() const
Face velocity in direction d at the +d face of each cell: uf(i,j,k) sits at (i+1/2,...
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
exact(Ds, K, hc, gs, nmax=1)
Definition conj.py:29
void vofAssignOrb(const std::vector< long > &w, int size, std::vector< int > &master)
Weighted ORB over a 1-D block space: the blocks are laid out as a 1-D "grid" of N cells in block-id o...
VofBox vofClampBox(VofBox b, I3 gs, const std::array< bool, 3 > &per)
Clamp a block box so it is representable: at most the whole axis on a periodic axis,...
void vofAssignLpt(const std::vector< long > &w, int size, std::vector< int > &master)
Longest-processing-time greedy (Graham 1969): heaviest block first onto the currently least-loaded ra...
void vofBuildPieces(const VofBox &box, I3 gs, const std::array< bool, 3 > &per, const std::vector< VofBox > &rankBox, std::vector< VofPiece > &out, int outside=0)
Build the pieces of one block box against every rank's owned box, in canonical order.
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
dict cols
Definition prof.py:162
blocks
Definition prof.py:61
void fillLeVeque(WyAdvector &a, Block b, double h, double phase)
double l1Diff(SField a, SField b, I3 e, I3 n, int g)
sum |a - b| over the inner region (cell-fraction units).
void forEachExtended(Block b, F fn)
Visit every cell of the extended block with (local linear index, global cell index).
SField copyOf(SField src, const char *name)
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...
void fillUniform(WyAdvector &a, Block b, double ux, double uy, double uz)
Uniform translation. Discrete divergence is EXACTLY zero (identical values differenced).
void initSphere(SField c, Block b, double h, double cx, double cy, double cz, double r, int levels=4)
Sphere of fluid (C = 1 inside), exact to ~5e-7 relative volume at R/h = 16 via the octree helper in p...
void clampFill(SField f, Block b, I3 gs, bool px, bool py, bool pz)
Zero-gradient (Neumann) fill of the ghost cells that lie OUTSIDE the global domain on a non-periodic ...
Block blockOf(const WyAdvector &a, I3 origin)
Half-open global cell box [lo, hi).
static VofBox intersect(const VofBox &a, const VofBox &b)
Intersection in global coordinates (may be empty).
Per-call census of the cascade over this block's inner region (LOCAL; a distributed caller sums them ...
One contiguous transfer: the global cells g of a block that rank r owns, and where they sit in the bl...
Where this block sits in the global grid: o is the global index of inner cell (0,0,...
I3 e
extended (inner + 2g) counts
static constexpr int G
static constexpr double F
static bool same(double a, double b)
void gateMomentumRefused()
bool refusesMomentumBlocks(F &&f)
Runs f, returns true iff it threw a std::runtime_error naming the momentum/block conflict.
#define CHECK(cond)
static std::vector< double > colour(int m)
static constexpr double SPECK[3]
static double sphereFrac(double cx, int x, int y, int z)
static double wallPsi(int x, int y)
psi on the z-edge (x, y): zero on both walls, periodic in x with an EXACT wrap (x mod NX).