flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_twophase_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the COUPLED VoF chain (rung V2a, WO-J): the colour field's own
2// g = 3 halo inside the solver, the G = 2 <-> g = 3 bridge, and C -> rho(C)/mu(C) -> the
3// variable-density projection.
4//
5// What is new here relative to `test_vof_advect_mpi.cpp` (rung V1), which gated the SAME advector
6// standalone: there the colour halo and the face velocities were the test's own; here they are the
7// solver's. Three things can only break under MPI and only in this configuration:
8//
9// 1. the colour field's own `GridHaloTopology` at width 3, built on the solver's partition and
10// living alongside the G = 2 velocity halo. Two topologies of DIFFERENT ghost width are in
11// flight in the same step.
12// 2. the face-velocity bridge across a rank boundary. `bridgeVelocityToVof` embeds the whole
13// G = 2 velocity block (inner + both ghost layers, filled by `fillVelGhosts`) into the g = 3
14// block with the low-face -> high-face index shift. A flux at a rank boundary must come out
15// bit-identical on both sides or the WY telescoping — hence conservation — dies exactly there.
16// 3. the non-periodic colour ghost fill. `vof::clampFill` takes the value at the GLOBALLY clamped
17// index precisely so it is decomposition-independent; sequential zero-gradient axis passes
18// over the block are NOT (WO-E finding 2), and the difference reaches C through the MYC
19// stencil of the inner corner cell. `walls-z` cuts the walled axis, so this is exercised.
20//
21// Two configurations on a 16x16x32 grid — the aligned ORB cuts the long z axis at np = 2 and 4:
22//
23// * `walls-z` — the hydrostatic acid test DRIVEN THROUGH C: a sharp two-layer colour field,
24// rho = 1 + (ratio-1)*C by a LinearMix closure, gravity force_z = -g*rho, ratio 1000,
25// walls +-z.
26// Gates dP/dz == -rho_f*g and the pointwise velocity AND pressure against the single-rank
27// reference. The pressure is the sharp gate (WO-F: a rank-unaware wall BC is invisible in the
28// velocity — each sub-column is separately hydrostatic — and reads 4.0e+02 in the pressure).
29//
30// * `shear-per` — the moving-interface case: a ratio-10 sphere in a periodic box under a sheared
31// body force, advection on, 40 steps. Gates the colour field pointwise against the single-rank
32// reference and the GLOBAL colour volume. This is the configuration that catches a bridge or a
33// halo defect: the interface sweeps across the rank boundary in z.
34//
35// Comparison protocol (the `tests/kokkos_mpi` pattern): the distributed run is compared POINTWISE
36// against a full-grid single-rank reference built on rank 0 with the identical configuration.
37// np = 1 is bit-exact on every field. np > 1 cannot be bitwise by construction — Chebyshev's bound
38// estimation and `removeMean` go through MPI_SUM allreduces whose summation order depends on the
39// rank count — so the velocity/pressure/colour tolerances are the same reduction-order floor
40// `test_vardensity_mpi.cpp` uses. The colour field itself has NO reduction in its update, so it is
41// bit-exact whenever the velocity driving it is; what it inherits is the velocity's floor.
42#include <mpi.h>
43
44#include <cmath>
45#include <cstdio>
46#include <cstring>
47#include <Kokkos_Core.hpp>
48#include <vector>
49
50#include "flow_ibm.hpp"
51#include "peclet/core/common/types.hpp"
52#include "peclet/core/decomp/block_decomposer.hpp"
53
55
56static constexpr int NX = 16, NY = 16, NZ = 32;
57static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
58static constexpr double GRAV = 0.1, RHO0 = 1.0;
59
60struct Config {
61 const char* name;
62 bool walls; // hydrostatic acid test through C (else the periodic sheared sphere)
63 double ratio; // density ratio
64 double dt;
65 int steps;
66};
67
68// The colour field of a configuration at a GLOBAL cell index — a pure function of global indices,
69// so a block gets bit-identical data to the corresponding part of the single-rank run.
70static double colourAt(const Config& c, int x, int y, int z) {
71 if (c.walls)
72 return (z < NZ / 2) ? 1.0 : 0.0; // sharp: heavy (liquid) below, stable stratification
73 const double dx = x + 0.5 - NX / 2.0, dy = y + 0.5 - NY / 2.0, dz = z + 0.5 - NZ / 2.0;
74 const double r = std::sqrt(dx * dx + dy * dy + dz * dz);
75 return std::fmin(1.0, std::fmax(0.0, 0.5 - (r - 4.0)));
76}
77static double forceXAt(int, int, int z) {
78 return 5e-4 * std::sin(2 * M_PI * (z + 0.5) / NZ);
79}
80
81template <class Fn>
82static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
83 std::vector<double> v((std::size_t)lnx * lny * lnz);
84 for (int z = 0; z < lnz; ++z)
85 for (int y = 0; y < lny; ++y)
86 for (int x = 0; x < lnx; ++x)
87 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
88 f(x + ox, y + oy, z + oz);
89 return v;
90}
91
92static void configure(IbmSolver& s, const Config& c, int ox, int oy, int oz, int lnx, int lny,
93 int lnz) {
94 s.setRho(RHO0);
95 s.setMu(c.walls ? 0.0 : 0.05); // hydrostatic: inviscid, so the balance is exact
96 s.setDt(c.dt);
97 s.setAdvection(!c.walls);
98 if (c.walls) {
99 s.setDomainBc(4, 1, 0, 0, 0);
100 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z (the CUT axis)
101 }
102 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
103 s.setVof(blockOf([&](int x, int y, int z) { return colourAt(c, x, y, z); }, ox, oy, oz, lnx, lny,
104 lnz));
106 {1.0, c.ratio - 1.0}); // enables the variable-density path
107 if (c.walls) {
108 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -GRAV});
109 } else {
110 s.setPropertyModel("mu", peclet::flow::ClosureKind::LinearMix, "C", "", {0.05, 0.45});
111 s.enableCellForce();
112 s.setField("force_x", blockOf(forceXAt, ox, oy, oz, lnx, lny, lnz));
113 }
114}
115
116// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
117static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
118 int lnx, int lny, int lnz, int rank, int size) {
119 std::vector<double> global;
120 if (rank == 0)
121 global.assign(GCELLS, 0.0);
122 for (int r = 0; r < size; ++r) {
123 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
124 if (r == 0) {
125 if (rank == 0)
126 for (int z = 0; z < lnz; ++z)
127 for (int y = 0; y < lny; ++y)
128 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
129 (std::size_t)(z + oz) * NX * NY],
130 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
131 (std::size_t)lnx * sizeof(double));
132 continue;
133 }
134 if (rank == r) {
135 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
136 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
137 } else if (rank == 0) {
139 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
140 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
142 for (int z = 0; z < meta[5]; ++z)
143 for (int y = 0; y < meta[4]; ++y)
144 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
145 (std::size_t)(z + meta[2]) * NX * NY],
146 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
147 (std::size_t)meta[3] * sizeof(double));
148 }
149 }
150 return global;
151}
152
153static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
154 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
155 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
156 // a drained open-boundary run). A non-finite difference must fail, so return it.
157 double m = 0;
158 for (std::size_t i = 0; i < b.size(); ++i) {
159 const double d = std::fabs(a[i] - b[i]);
160 if (!(d == d))
161 return d; // NaN
162 m = std::fmax(m, d);
163 }
164 return m;
165}
166static double maxAbs(const std::vector<double>& a) {
167 double m = 0;
168 for (double v : a)
169 m = std::fmax(m, std::fabs(v));
170 return m;
171}
172static double sumOf(const std::vector<double>& a) {
173 double s = 0;
174 for (double v : a)
175 s += v;
176 return s;
177}
178
179// Discrete hydrostatic gradient along the walled column, against the ACTUAL colour field.
180static double pressureGradientError(const std::vector<double>& p, const std::vector<double>& c,
181 double ratio) {
182 double perr = 0;
183 for (int z = 1; z < NZ; ++z) {
184 const std::size_t i0 =
185 (std::size_t)(NX / 2) + (std::size_t)(NY / 2) * NX + (std::size_t)(z - 1) * NX * NY;
186 const std::size_t i1 = i0 + (std::size_t)NX * NY;
187 const double rf = 0.5 * ((1.0 + (ratio - 1.0) * c[i1]) + (1.0 + (ratio - 1.0) * c[i0]));
188 perr = std::fmax(perr, std::fabs((p[i1] - p[i0]) + GRAV * rf) / (GRAV * ratio));
189 }
190 return perr;
191}
192
193int main(int argc, char** argv) {
194 MPI_Init(&argc, &argv);
195 Kokkos::initialize(argc, argv);
196 int fail = 0;
197 {
198 int rank = 0, size = 1;
201
202 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
203 auto blk = dec.block(rank);
204 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
205 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
206 const int gn[3] = {NX, NY, NZ};
207 bool cut[3] = {false, false, false};
208 for (const auto& sz : dec.sizes())
209 for (int a = 0; a < 3; ++a)
210 if ((int)sz[a] != gn[a])
211 cut[a] = true;
212 if (rank == 0)
213 std::printf("VOF TWOPHASE MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n", size,
214 NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
215 cut[2] ? "z" : "");
216
217 const Config configs[] = {{"walls-z", true, 1000.0, 1.0, 20},
218 {"shear-per", false, 10.0, 1.0, 40}};
219
220 for (const Config& c : configs) {
221 // The walled/loaded axis must be CUT: `walls-z` exists to gate the colour field's
222 // non-periodic ghost fill and the domain-BC ownership on a cut axis, and `shear-per` exists
223 // to sweep the interface across a rank boundary. Fail loudly if the ORB stops cutting z.
224 if (size > 1 && !cut[2]) {
225 if (rank == 0)
226 std::printf(" [%-9s np=%d] FAIL — the decomposition does NOT cut z\n", c.name, size);
227 fail = 1;
228 continue;
229 }
230 // --- distributed ---
232 sd.initMpi(dec, MPI_COMM_WORLD);
233 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
234 const double v0Loc = sumOf(sd.getVof());
235 double v0 = 0.0;
237 for (int it = 0; it < c.steps; ++it)
238 sd.step();
239 const double v1Loc = sumOf(sd.getVof());
240 double v1 = 0.0;
242
243 std::vector<double> gu[3];
244 for (int comp = 0; comp < 3; ++comp)
245 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
246 const std::vector<double> gp =
247 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
248 const std::vector<double> gc =
249 gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
250
251 // --- single-rank full-grid reference on rank 0 ---
252 if (rank == 0) {
253 IbmSolver ref(NX, NY, NZ);
254 configure(ref, c, 0, 0, 0, NX, NY, NZ);
255 const double rv0 = sumOf(ref.getVof());
256 for (int it = 0; it < c.steps; ++it)
257 ref.step();
258 const double refDrift = (sumOf(ref.getVof()) - rv0) / rv0;
259 double du = 0, umag = 0;
260 for (int comp = 0; comp < 3; ++comp) {
261 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
262 umag = std::fmax(umag, maxAbs(ref.getVelocity(comp)));
263 }
264 const auto refC = ref.getVof();
265 const double dp = maxAbsDiff(gp, ref.getPressure());
266 const double dc = maxAbsDiff(gc, refC);
267 const double pmag = maxAbs(ref.getPressure());
268 const double drift = (v1 - v0) / v0;
269 // np=1 bit-exact; np>1 the MPI reduction-order floor (Chebyshev bounds + removeMean),
270 // which the colour field inherits through the velocity that advects it.
271 // The absolute floor matters on `walls-z`: |u| there IS round-off (6e-13), so a purely
272 // relative tolerance would be gating the reduction-order pattern of noise, not the physics.
273 const double utol = (size == 1) ? 0.0 : std::fmax(1e-12, 1e-11 * umag);
274 const double ptol = (size == 1) ? 0.0 : std::fmax(1e-12, 1e-11 * pmag);
275 const double ctol = (size == 1) ? 0.0 : 1e-11;
276 std::printf(
277 " [%-9s np=%d] du %.3e (tol %.1e, |u| %.2e) dp %.3e (tol %.1e, |P| %.2e) "
278 "dC %.3e (tol %.1e) dV/V %+.3e\n",
279 c.name, size, du, utol, umag, dp, ptol, pmag, dc, ctol, drift);
280 if (!(du <= utol) || !(dp <= ptol) || !(dc <= ctol)) {
281 std::printf(" [%-9s np=%d] FAIL — distributed vs single-rank\n", c.name, size);
282 fail = 1;
283 }
284 // Colour volume. On `shear-per` the interface is transported by a deliberate flow and the
285 // drift must be at the projection's divergence floor. On `walls-z` the colour DOES move
286 // once — the pressure driver's first solve on a fresh field leaves max|u| ~ 8.6e-6 (the
287 // documented varRho transient, doc/variable_density_projection.md §2), and VoF faithfully
288 // advects it into a permanent ~7e-7 displacement before the velocity collapses to ~1e-13.
289 // That is a property of the pressure solve, not of the advection or of the decomposition,
290 // so what this test gates there is that it is DECOMPOSITION-INDEPENDENT.
291 if (c.walls) {
292 std::printf(" [%-9s np=%d] colour drift dist %+.4e ref %+.4e (delta %.2e)\n", c.name,
293 size, drift, refDrift, std::fabs(drift - refDrift));
294 if (!(std::fabs(drift - refDrift) < 1e-13)) {
295 std::printf(" [%-9s np=%d] FAIL — colour drift is decomposition-dependent\n", c.name,
296 size);
297 fail = 1;
298 }
299 } else if (!(std::fabs(drift) < 1e-11)) {
300 std::printf(" [%-9s np=%d] FAIL — colour volume drift %.3e\n", c.name, size, drift);
301 fail = 1;
302 }
303 if (c.walls) {
304 const double perr = pressureGradientError(gp, gc, c.ratio);
305 const double umaxD = std::fmax(maxAbs(gu[0]), std::fmax(maxAbs(gu[1]), maxAbs(gu[2])));
306 std::printf(" [%-9s np=%d] hydrostatic through C: max|u| %.3e dP/dz rel-err %.3e\n",
307 c.name, size, umaxD, perr);
308 if (!(perr < 1e-11) || !(umaxD < 1e-9)) {
309 std::printf(" [%-9s np=%d] FAIL — hydrostatic balance through C\n", c.name, size);
310 fail = 1;
311 }
312 }
313 }
315 }
316 if (rank == 0)
317 std::printf(fail ? "FAILED\n" : "OK\n");
318 }
319 Kokkos::finalize();
320 MPI_Finalize();
321 return fail;
322}
void setDomainBc(int face, int type, double vx, double vy, double vz)
void setAdvection(bool on)
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 setPressureGeometry(const std::vector< double > &sdfInner)
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 setRho(double r)
Fluid density, in the caller's units.
std::vector< double > getPressure()
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
const char * name
static double maxAbs(const std::vector< double > &a)
static constexpr int NY
static constexpr double GRAV
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static constexpr double RHO0
static double forceXAt(int, int, int z)
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 double colourAt(const Config &c, int x, int y, int z)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static double sumOf(const std::vector< double > &a)
static double pressureGradientError(const std::vector< double > &p, const std::vector< double > &c, double ratio)
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ
static void configure(IbmSolver &s, const Config &c, int ox, int oy, int oz, int lnx, int lny, int lnz)