flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_cutcell_mpi.cpp
Go to the documentation of this file.
1// flow — VoF rung V5a (WO-Q), multi-rank: the colour field transported THROUGH an immersed solid
2// on a decomposition that CUTS the spheres.
3//
4// What can only break here (nothing below is exercised by `test_vof_twophase_mpi.cpp`, which is
5// solid-free):
6//
7// 1. THE GEOMETRY OF THE COLOUR BLOCK. `eps` and the face openness are built on the solver's
8// G = 2 block and embedded into the advector's g = 3 block — the openness with the SAME
9// one-cell shift the face velocity uses — and then run through the colour field's own ghost
10// policy so that the classification at ghost DEPTH 3 is the owner's. That last step is not
11// cosmetic: the solid-band fill reads fluid neighbours at depth 3, so a locally-guessed
12// classification there would make the INNER result decomposition-dependent. Gated bitwise.
13//
14// 2. THE SOLID-BAND FILL. Three passes with a shrinking depth budget (pass k writes solid cells
15// at ghost depth <= 3-k and reads only fluid cells or cells filled in an EARLIER pass),
16// followed by a second exchange. The shrinking budget is exactly what makes every read fall
17// inside the halo the exchange has already made owner-consistent. Gated bitwise, pointwise,
18// through `vof_filled_color()`.
19//
20// 3. THE OPENNESS-WEIGHTED FLUX ACROSS A RANK BOUNDARY. One flux per face, formed once, so the
21// Weymouth-Yue telescoping — hence conservation — holds across the boundary too. The
22// KINEMATIC configuration below gates the transported colour BITWISE at np 1/2/4 by handing
23// every rank the SAME velocity field: each rank builds the full-grid single-rank solver,
24// runs it to a Stokes steady state (a deterministic computation, identical on every rank) and
25// scatters that velocity into its own block. Without that the velocity itself would carry the
26// pressure solve's reduction-order floor and no bitwise statement about the ADVECTION would be
27// possible.
28//
29// 4. The coupled step (`enable_vof` + closures + gravity through the packing) at the reduction
30// floor, with the colour volume drift measured against the projection's own divergence.
31#include <mpi.h>
32
33#include <cmath>
34#include <cstdio>
35#include <cstdlib>
36#include <cstring>
37#include <Kokkos_Core.hpp>
38#include <vector>
39
40#include "flow_ibm.hpp"
41#include "peclet/core/common/types.hpp"
42#include "peclet/core/decomp/block_decomposer.hpp"
43
45
46static constexpr int NX = 16, NY = 16, NZ = 32;
47static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
48
49// Four periodic spheres, placed so the ORB's z cut at np = 2 and 4 goes THROUGH them.
50static double sdfAt(double x, double y, double z) {
51 const double cx[4] = {4.0, 11.0, 5.0, 12.0}, cy[4] = {4.0, 11.0, 12.0, 4.0},
52 cz[4] = {8.0, 16.0, 24.0, 30.0}, r = 4.5;
53 double best = 1e30;
54 for (int s = 0; s < 4; ++s)
55 for (int a = -1; a <= 1; ++a)
56 for (int b = -1; b <= 1; ++b)
57 for (int c = -1; c <= 1; ++c) {
58 const double dx = x - (cx[s] + a * NX), dy = y - (cy[s] + b * NY),
59 dz = z - (cz[s] + c * NZ);
60 const double d = std::sqrt(dx * dx + dy * dy + dz * dz) - r;
61 best = d < best ? d : best;
62 }
63 return best;
64}
65static double colourAt(int, int, int z) {
66 return z < NZ / 2 ? 1.0 : 0.0;
67}
68
69template <class Fn>
70static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
71 std::vector<double> v((std::size_t)lnx * lny * lnz);
72 for (int z = 0; z < lnz; ++z)
73 for (int y = 0; y < lny; ++y)
74 for (int x = 0; x < lnx; ++x)
75 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
76 f(x + ox, y + oy, z + oz);
77 return v;
78}
79static std::vector<double> sliceOf(const std::vector<double>& g, int ox, int oy, int oz, int lnx,
80 int lny, int lnz) {
81 return blockOf(
82 [&](int x, int y, int z) {
83 return g[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY];
84 },
85 ox, oy, oz, lnx, lny, lnz);
86}
87
88static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
89 int lnx, int lny, int lnz, int rank, int size) {
90 std::vector<double> global;
91 if (rank == 0)
92 global.assign(GCELLS, 0.0);
93 for (int r = 0; r < size; ++r) {
94 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
95 if (r == 0) {
96 if (rank == 0)
97 for (int z = 0; z < lnz; ++z)
98 for (int y = 0; y < lny; ++y)
99 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
100 (std::size_t)(z + oz) * NX * NY],
101 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
102 (std::size_t)lnx * sizeof(double));
103 continue;
104 }
105 if (rank == r) {
106 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
107 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
108 } else if (rank == 0) {
110 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
111 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
113 for (int z = 0; z < meta[5]; ++z)
114 for (int y = 0; y < meta[4]; ++y)
115 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
116 (std::size_t)(z + meta[2]) * NX * NY],
117 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
118 (std::size_t)meta[3] * sizeof(double));
119 }
120 }
121 return global;
122}
123
124static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
125 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
126 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
127 // a drained open-boundary run). A non-finite difference must fail, so return it.
128 double m = 0;
129 for (std::size_t i = 0; i < b.size(); ++i) {
130 const double d = std::fabs(a[i] - b[i]);
131 if (!(d == d))
132 return d; // NaN
133 m = std::fmax(m, d);
134 }
135 return m;
136}
137static double sumOf(const std::vector<double>& a) {
138 double s = 0;
139 for (double v : a)
140 s += v;
141 return s;
142}
143
144int main(int argc, char** argv) {
145 std::setvbuf(stdout, nullptr, _IONBF, 0);
146 MPI_Init(&argc, &argv);
147 Kokkos::initialize(argc, argv);
148 int fail = 0;
149 {
150 int rank = 0, size = 1;
153 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
154 auto blk = dec.block(rank);
155 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
156 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
157 const int gn[3] = {NX, NY, NZ};
158 bool cut[3] = {false, false, false};
159 for (const auto& sz : dec.sizes())
160 for (int a = 0; a < 3; ++a)
161 if ((int)sz[a] != gn[a])
162 cut[a] = true;
163 if (rank == 0)
164 std::printf("VOF CUTCELL MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n", size,
165 NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
166 cut[2] ? "z" : "");
167 if (size > 1 && !cut[2]) {
168 if (rank == 0)
169 std::printf(" FAIL — the decomposition does not cut z, so no sphere is cut\n");
170 fail = 1;
171 }
172
173 const std::vector<double> gSdf = blockOf(
174 [](int x, int y, int z) { return sdfAt(x + 0.5, y + 0.5, z + 0.5); }, 0, 0, 0, NX, NY, NZ);
175
176 // ---- the deterministic Stokes velocity, computed identically on EVERY rank ----------------
177 // A full-grid single-rank solver: the same arithmetic on every rank, so the velocity that
178 // drives the kinematic advection carries no reduction-order dependence and the colour gate can
179 // be BITWISE rather than "at the floor".
180 std::vector<double> gU[3];
181 std::vector<double> refColour;
182 double refDrift = 0.0, refDiv = 0.0;
183 const int kinSteps = 40;
184 {
185 IbmSolver ref(NX, NY, NZ);
186 ref.setRho(1.0);
187 ref.setMu(0.2);
188 ref.setDt(1.0);
189 ref.setBodyForce(2e-3, 1e-3, 5e-4);
190 ref.setSolid(gSdf, true);
191 for (int i = 0; i < 40; ++i)
192 ref.step();
193 refDiv = ref.maxOpenDivergence();
194 for (int c = 0; c < 3; ++c)
195 gU[c] = ref.getVelocity(c);
196 ref.enableVof();
197 ref.setVof(blockOf(colourAt, 0, 0, 0, NX, NY, NZ));
198 const double v0 = ref.vofDiagnostics().volume;
199 const double dtA = 0.2 / ref.vofMaxCourant();
200 for (int i = 0; i < kinSteps; ++i)
201 ref.advectVofKinematic(dtA);
202 const auto d1 = ref.vofDiagnostics();
203 refDrift = (d1.volume - v0) / v0;
204 refColour = ref.getVof();
205 if (rank == 0)
206 std::printf(
207 " reference (full grid, every rank): max|div(open u)| %.3e, %d kinematic steps at "
208 "dt %.4g, drift %.3e, clipped %.3e\n",
209 refDiv, kinSteps, dtA, refDrift, d1.clippedVolume);
210 }
211
212 // ---- distributed kinematic run on the SAME velocity ---------------------------------------
213 {
215 sd.initMpi(dec, MPI_COMM_WORLD);
216 sd.setRho(1.0);
217 sd.setMu(0.2);
218 sd.setDt(1.0);
219 sd.setSolid(sliceOf(gSdf, ox, oy, oz, lnx, lny, lnz), true);
220 sd.enableVof();
221 // geometry, pointwise: eps, the three face openness fields and the classification
222 double dg = 0.0;
223 {
224 IbmSolver r2(NX, NY, NZ);
225 r2.setRho(1.0);
226 r2.setMu(0.2);
227 r2.setDt(1.0);
228 r2.setSolid(gSdf, true);
229 r2.enableVof();
230 for (int w = 0; w < 5; ++w) {
231 const auto g = gatherGlobal(sd.getVofGeometry(w), ox, oy, oz, lnx, lny, lnz, rank, size);
232 if (rank == 0)
233 dg = std::fmax(dg, maxAbsDiff(g, r2.getVofGeometry(w)));
234 }
235 }
236 for (int c = 0; c < 3; ++c)
237 sd.setField(c == 0 ? "u" : (c == 1 ? "v" : "w"), sliceOf(gU[c], ox, oy, oz, lnx, lny, lnz));
238 sd.setVof(blockOf(colourAt, ox, oy, oz, lnx, lny, lnz));
239 // the solid-band fill, pointwise
240 double dfill = 0.0;
241 {
242 const auto g = gatherGlobal(sd.getVofFilledColour(), ox, oy, oz, lnx, lny, lnz, rank, size);
243 if (rank == 0) {
244 IbmSolver r2(NX, NY, NZ);
245 r2.setRho(1.0);
246 r2.setMu(0.2);
247 r2.setDt(1.0);
248 r2.setSolid(gSdf, true);
249 r2.enableVof();
250 r2.setVof(blockOf(colourAt, 0, 0, 0, NX, NY, NZ));
252 }
253 }
254 const double divD = sd.maxOpenDivergence();
255 const double locV0 = sd.vofDiagnostics().volume;
256 double v0 = 0.0;
258 double cflLoc = sd.vofMaxCourant();
259 const double dtA = 0.2 / cflLoc;
260 for (int i = 0; i < kinSteps; ++i)
262 const double locV1 = sd.vofDiagnostics().volume;
263 double v1 = 0.0;
265 const auto gc = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
266 if (rank == 0) {
267 const double dc = maxAbsDiff(gc, refColour);
268 const double drift = (v1 - v0) / v0;
269 std::printf(
270 " [kinematic np=%d] geometry %.3e band fill %.3e colour %.3e "
271 "drift %.3e (ref %.3e) max|div| %.3e\n",
272 size, dg, dfill, dc, drift, refDrift, divD);
273 if (!(dg == 0.0) || !(dfill == 0.0) || !(dc == 0.0)) {
274 std::printf(" [kinematic np=%d] FAIL — not bitwise\n", size);
275 fail = 1;
276 }
277 if (!(std::fabs(drift) <= 1e-11)) {
278 std::printf(" [kinematic np=%d] FAIL — colour volume drift %.3e\n", size, drift);
279 fail = 1;
280 }
281 }
282 }
283
284 // ---- coupled draining through the packing (reduction floor) --------------------------------
285 {
286 const int steps = 30;
287 const double ratio = 10.0, grav = 2e-3;
288 auto setup = [&](IbmSolver& s, int aox, int aoy, int aoz, int anx, int any, int anz) {
289 s.setRho(1.0);
290 s.setMu(0.05);
291 s.setDt(0.5);
292 s.setAdvection(true);
293 s.setSolid(sliceOf(gSdf, aox, aoy, aoz, anx, any, anz), true);
296 {1.0, ratio - 1.0});
297 s.setPropertyModel("mu", peclet::flow::ClosureKind::LinearMix, "C", "", {0.05, 0.45});
299 {0.0, -grav});
300 };
302 sd.initMpi(dec, MPI_COMM_WORLD);
303 setup(sd, ox, oy, oz, lnx, lny, lnz);
304 const double lv0 = sd.vofDiagnostics().volume;
305 double v0 = 0.0;
307 long maxIt = 0;
308 for (int i = 0; i < steps; ++i) {
309 sd.step();
310 maxIt = std::max(maxIt, sd.lastPressureIterations());
311 }
312 const double lv1 = sd.vofDiagnostics().volume;
313 double v1 = 0.0;
315 const double lsolid = sd.vofDiagnostics().solidSumC;
316 double solid = 0.0;
318 const double divD = sd.maxOpenDivergence();
319 const auto gc = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
320 std::vector<double> gu[3];
321 for (int c = 0; c < 3; ++c)
322 gu[c] = gatherGlobal(sd.getVelocity(c), ox, oy, oz, lnx, lny, lnz, rank, size);
323 if (rank == 0) {
324 IbmSolver ref(NX, NY, NZ);
325 setup(ref, 0, 0, 0, NX, NY, NZ);
326 for (int i = 0; i < steps; ++i)
327 ref.step();
328 double du = 0;
329 for (int c = 0; c < 3; ++c)
330 du = std::fmax(du, maxAbsDiff(gu[c], ref.getVelocity(c)));
331 const double dc = maxAbsDiff(gc, ref.getVof());
332 const double drift = (v1 - v0) / v0;
333 const double tol = size == 1 ? 0.0 : 1e-9;
334 std::printf(
335 " [coupled np=%d] colour %.3e velocity %.3e drift %.3e solid sum C %.3e "
336 "max|div| %.3e pressure iters max %ld\n",
337 size, dc, du, drift, solid, divD, maxIt);
338 if (!(dc <= tol) || !(du <= tol)) {
339 std::printf(" [coupled np=%d] FAIL — colour %.3e velocity %.3e above tol %.1e\n", size,
340 dc, du, tol);
341 fail = 1;
342 }
343 if (!(solid == 0.0)) {
344 std::printf(" [coupled np=%d] FAIL — colour in solid cells %.3e\n", size, solid);
345 fail = 1;
346 }
347 if (!(std::fabs(drift) <= 1e-8)) {
348 std::printf(" [coupled np=%d] FAIL — colour drift %.3e\n", size, drift);
349 fail = 1;
350 }
351 }
352 }
353 int g = 0;
355 fail = g;
356 if (rank == 0)
357 std::printf(fail ? "VOF CUTCELL MPI: FAILED\n" : "VOF CUTCELL MPI: all checks passed\n");
358 }
359 Kokkos::finalize();
360 MPI_Finalize();
361 return fail;
362}
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
std::vector< double > getVofFilledColour()
void setAdvection(bool on)
std::vector< double > getVofGeometry(int which)
std::vector< double > getVof()
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
void setVof(const std::vector< double > &c)
void setField(const std::string &name, const std::vector< double > &v)
std::vector< double > getVelocity(int c)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void advectVofKinematic(double dtPhysArg)
dt is in the caller's time unit.
void setRho(double r)
Fluid density, in the caller's units.
vof::WyAdvector::Diagnostics vofDiagnostics()
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
double solidSumC
filled by the SOLVER from the canonical G=2 colour field
double volume
sum of eps_eff*C over inner FLUID cells: the EXACTLY conserved functional of the cut-cell scheme (cut...
static double colourAt(int, int, int z)
static constexpr int NY
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static std::vector< double > gatherGlobal(const std::vector< double > &local, int ox, int oy, int oz, int lnx, int lny, int lnz, int rank, int size)
static std::vector< double > sliceOf(const std::vector< double > &g, int ox, int oy, int oz, int lnx, int lny, int lnz)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static double sumOf(const std::vector< double > &a)
static constexpr int NX
static double sdfAt(double x, double y, double z)
static constexpr std::size_t GCELLS
static constexpr int NZ