flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_surface_tension_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the balanced-force CSF (rung V4, WO-P).
2//
3// Two things are gated, and they are different in kind.
4//
5// S1 THE EXACTNESS GATE, AT EVERY DECOMPOSITION. A stationary droplet with a CONSTANT curvature
6// and a uniform density: the surface-tension force is then exactly the discrete gradient of
7// `sigma*kappa*C`, the projection annihilates it, and `max|u|` must sit at MACHINE ZERO on
8// every rank at np = 1, 2 and 4. This is the momentum analogue of the hydrostatic acid test's
9// multi-rank half (`test_vardensity_mpi`'s `walls-z`), and it catches the same class of defect
10// it does: a face force assembled from a ghost the exchange did not fill is invisible in the
11// single-rank run and shows up here as a rank-boundary velocity. The colour field's ghost, the
12// curvature field's ghost and the branch field's ghost all feed that one face value.
13//
14// S2 THE COUPLED STEP AGAINST A FULL-GRID SINGLE-RANK REFERENCE — velocity, pressure, colour AND
15// curvature after 10 steps of a real two-phase run with surface tension on. np = 1 must be
16// BITWISE; np > 1 carries the documented reduction-order floor of the pressure driver's
17// allreduces (`doc/variable_density_projection.md` §3.1), so it is a tolerance comparison —
18// except for kappa and the branch field, which contain no reduction at all and must be bitwise
19// at every np, exactly as `test_vof_curvature_mpi` asserts.
20//
21// Grid 16x16x32: the aligned ORB cuts the long z axis at np = 2 and 4, and the droplet is centred
22// so its interface straddles that cut. `walls-z` puts domain walls on the CUT axis so the colour
23// and curvature ghosts go through the non-periodic (clamped) fill on a rank boundary.
24#include <mpi.h>
25
26#include <cmath>
27#include <cstdio>
28#include <cstring>
29#include <Kokkos_Core.hpp>
30#include <vector>
31
32#include "flow_ibm.hpp"
33#include "peclet/core/common/types.hpp"
34#include "peclet/core/decomp/block_decomposer.hpp"
35
38
39static constexpr int NX = 16, NY = 16, NZ = 32;
40static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
41static constexpr double RHO_G = 1.0, RHO_L = 10.0, SIGMA = 1.0, MU = 0.05, RDROP = 5.0;
42
43struct Config {
44 const char* name;
45 bool walls; // walls +-z (the CUT axis) vs fully periodic
46 bool constKappa; // S1: freeze kappa at a constant and use a uniform density
47};
48
49// Volume fractions of a sphere at a GLOBAL cell index — a pure function of global indices, so each
50// block gets bit-identical data to the corresponding part of the full-grid run. Exact in z,
51// 16 x 16 subsampled in (x,y) (a crude 3-D subsample manufactures full/empty cell PAIRS whose
52// shared face carries an interface no cell knows about — the CSF's orphan-face defect).
53static double colourAt(const Config& c, int x, int y, int z) {
54 const double cx = NX / 2.0 + 0.137, cy = NY / 2.0 - 0.311;
55 const double cz = c.walls ? NZ / 4.0 + 0.241 : NZ / 2.0 + 0.241;
56 const int NS = 16;
57 double acc = 0.0;
58 for (int j = 0; j < NS; ++j)
59 for (int i = 0; i < NS; ++i) {
60 const double px = x + (i + 0.5) / NS - cx, py = y + (j + 0.5) / NS - cy;
61 const double r2 = RDROP * RDROP - px * px - py * py;
62 if (r2 <= 0.0)
63 continue;
64 const double h = std::sqrt(r2);
65 const double lo = std::fmax(cz - h, (double)z), hi = std::fmin(cz + h, (double)z + 1);
66 if (hi > lo)
67 acc += hi - lo;
68 }
69 return acc / (NS * NS);
70}
71
72template <class Fn>
73static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
74 std::vector<double> v((std::size_t)lnx * lny * lnz);
75 for (int z = 0; z < lnz; ++z)
76 for (int y = 0; y < lny; ++y)
77 for (int x = 0; x < lnx; ++x)
78 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
79 f(x + ox, y + oy, z + oz);
80 return v;
81}
82
83static void configure(IbmSolver& s, const Config& c, int ox, int oy, int oz, int lnx, int lny,
84 int lnz) {
85 const double rl = c.constKappa ? RHO_G : RHO_L;
86 s.setRho(rl);
87 s.setMu(MU);
88 s.setDt(1.0);
89 // The balanced-force exactness gates (max|u| < 1e-13) need the momentum solve at machine
90 // precision: the default residual stop (1e-5, 2026-09-02) leaves a 5e-12 spurious current, so
91 // these tests keep the legacy fixed-sweep loop.
93 if (c.walls) {
94 s.setDomainBc(4, 1, 0, 0, 0);
95 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z (the CUT axis)
96 }
97 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
98 s.setPressureChebyshev(true, 400, 1e-14);
99 s.enableVof();
100 s.setVof(blockOf([&](int x, int y, int z) { return colourAt(c, x, y, z); }, ox, oy, oz, lnx, lny,
101 lnz));
102 s.setPropertyModel("rho", ClosureKind::LinearMix, "C", "", {RHO_G, rl - RHO_G});
104 if (c.constKappa)
105 s.setVofKappaConstant(2.0 / RDROP);
106 s.setDt(0.5 * s.capillaryDt());
107}
108
109static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
110 int lnx, int lny, int lnz, int rank, int size) {
111 std::vector<double> global;
112 if (rank == 0)
113 global.assign(GCELLS, 0.0);
114 for (int r = 0; r < size; ++r) {
115 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
116 if (r == 0) {
117 if (rank == 0)
118 for (int z = 0; z < lnz; ++z)
119 for (int y = 0; y < lny; ++y)
120 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
121 (std::size_t)(z + oz) * NX * NY],
122 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
123 (std::size_t)lnx * sizeof(double));
124 continue;
125 }
126 if (rank == r) {
127 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
128 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
129 } else if (rank == 0) {
131 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
132 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
134 for (int z = 0; z < meta[5]; ++z)
135 for (int y = 0; y < meta[4]; ++y)
136 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
137 (std::size_t)(z + meta[2]) * NX * NY],
138 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
139 (std::size_t)meta[3] * sizeof(double));
140 }
141 }
142 return global;
143}
144
145static double maxDiff(const std::vector<double>& a, const std::vector<double>& b) {
146 double m = 0;
147 for (std::size_t i = 0; i < b.size(); ++i)
148 m = std::fmax(m, std::fabs(a[i] - b[i]));
149 return m;
150}
151static long countBitDiff(const std::vector<double>& a, const std::vector<double>& b, double& mx) {
152 long n = 0;
153 mx = 0.0;
154 for (std::size_t i = 0; i < b.size(); ++i)
155 if (a[i] != b[i]) {
156 ++n;
157 mx = std::fmax(mx, std::fabs(a[i] - b[i]));
158 }
159 return n;
160}
161static double maxAbs(const std::vector<double>& v) {
162 double m = 0;
163 for (double x : v)
164 m = std::fmax(m, std::fabs(x));
165 return m;
166}
167
168int main(int argc, char** argv) {
169 MPI_Init(&argc, &argv);
170 Kokkos::initialize(argc, argv);
171 int fail = 0;
172 {
173 int rank = 0, size = 1;
176
177 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
178 auto blk = dec.block(rank);
179 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
180 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
181 const int gn[3] = {NX, NY, NZ};
182 bool cut[3] = {false, false, false};
183 for (const auto& sz : dec.sizes())
184 for (int a = 0; a < 3; ++a)
185 if ((int)sz[a] != gn[a])
186 cut[a] = true;
187 if (rank == 0)
188 std::printf(
189 "VOF SURFACE TENSION MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n", size,
190 NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "");
191
192 const Config configs[] = {{"exact-per", false, true},
193 {"exact-walls-z", true, true},
194 {"drop-per", false, false},
195 {"drop-walls-z", true, false}};
196
197 for (const Config& c : configs) {
198 if (size > 1 && !cut[2]) {
199 if (rank == 0)
200 std::printf(" [%-14s np=%d] FAIL — the decomposition does NOT cut z\n", c.name, size);
201 fail = 1;
202 continue;
203 }
204
206 sd.initMpi(dec, MPI_COMM_WORLD);
207 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
208 for (int k = 0; k < 10; ++k)
209 sd.step();
210
211 // S1: the exactness gate is a per-rank statement — reduce the max over ranks.
212 double loc = std::fmax(std::fmax(maxAbs(sd.getVelocity(0)), maxAbs(sd.getVelocity(1))),
213 maxAbs(sd.getVelocity(2)));
214 double gmax = loc;
216
217 std::vector<double> g[6];
218 const char* fn[6] = {"u", "v", "w", "P", "C", "kappa"};
219 g[0] = gatherGlobal(sd.getVelocity(0), ox, oy, oz, lnx, lny, lnz, rank, size);
220 g[1] = gatherGlobal(sd.getVelocity(1), ox, oy, oz, lnx, lny, lnz, rank, size);
221 g[2] = gatherGlobal(sd.getVelocity(2), ox, oy, oz, lnx, lny, lnz, rank, size);
222 g[3] = gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
223 g[4] = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
224 g[5] = gatherGlobal(sd.getVofCurvature(), ox, oy, oz, lnx, lny, lnz, rank, size);
225 const std::vector<double> gb =
226 gatherGlobal(sd.getVofCurvatureBranch(), ox, oy, oz, lnx, lny, lnz, rank, size);
227
228 if (rank == 0) {
229 IbmSolver ref(NX, NY, NZ);
230 configure(ref, c, 0, 0, 0, NX, NY, NZ);
231 for (int k = 0; k < 10; ++k)
232 ref.step();
233 std::vector<double> r[6] = {ref.getVelocity(0), ref.getVelocity(1), ref.getVelocity(2),
234 ref.getPressure(), ref.getVof(), ref.getVofCurvature()};
235 double d[6];
236 for (int i = 0; i < 6; ++i)
237 d[i] = maxDiff(g[i], r[i]);
238 double dkb = 0.0, dkk = 0.0;
239 const long nkb = countBitDiff(gb, ref.getVofCurvatureBranch(), dkb);
240 const long nkk = countBitDiff(g[5], r[5], dkk);
241
242 std::printf(
243 " [%-14s np=%d] max|u| = %.4e du %.2e dv %.2e dw %.2e dP %.2e"
244 " dC %.2e dkappa %.2e\n",
245 c.name, size, gmax, d[0], d[1], d[2], d[3], d[4], d[5]);
246 (void)fn;
247
248 // The BRANCH field is bitwise at every np: the cascade contains no reduction, and the
249 // branch is a discrete label, so a colour perturbed at 1e-15 still lands on the same tier.
250 // kappa itself is bitwise only where the colour is — i.e. at np = 1, and in the frozen
251 // (`exact-*`) configurations at every np. In a COUPLED run at np > 1 the colour carries the
252 // pressure driver's reduction-order residue (dC below) and kappa carries its image; that is
253 // WO-J's documented floor, not a defect of this rung.
254 if (nkb != 0) {
255 std::printf(" FAIL — the curvature BRANCH is decomposition-dependent (%ld cells)\n",
256 nkb);
257 fail = 1;
258 }
259 if (size == 1 && nkk != 0) {
260 std::printf(" FAIL — kappa is not bitwise at np = 1 (%ld cells, max |d| %.3e)\n",
261 nkk, dkk);
262 fail = 1;
263 }
264 if (c.constKappa) {
265 // S1. With a constant kappa the force IS a discrete gradient of sigma*kappa*C. In the
266 // PERIODIC box the semi-implicit momentum operator commutes with that gradient (uniform
267 // rho, no boundary), so the projection annihilates it in ONE step and max|u| is at
268 // machine zero. With WALLS it does not commute — the face-folded viscous operator does
269 // not leave a pressure-gradient field invariant next to a Dirichlet boundary — so the
270 // same fixed point is approached instead of hit: measured 2.1e-10 at 10 steps,
271 // 1.2e-10 at 30, 2.9e-11 at 100, 3.6e-12 at 300, and 1.1e-16 at mu = 0. The multi-rank
272 // statement there is the du/dv/dw/dP comparison below, which is what this test is for.
273 const double tol = c.walls ? 1e-8 : 1e-13;
274 if (!(gmax < tol)) {
275 std::printf(" FAIL — the exactness gate is above %.0e (%.3e)\n", tol, gmax);
276 fail = 1;
277 }
278 }
279 // S2: np = 1 bitwise; np > 1 at the pressure driver's reduction-order floor.
280 const double tolU = (size == 1) ? 0.0 : 1e-12;
281 const double tolP = (size == 1) ? 0.0 : 1e-9;
282 const double tolC = (size == 1) ? 0.0 : 1e-12;
283 const double tolK = (size == 1) ? 0.0 : 1e-12;
284 if (d[5] > tolK) {
285 std::printf(" FAIL — kappa differs by more than the colour's own floor (%.2e)\n",
286 d[5]);
287 fail = 1;
288 }
289 if (d[0] > tolU || d[1] > tolU || d[2] > tolU || d[3] > tolP || d[4] > tolC) {
290 std::printf(
291 " FAIL — the distributed step does not match the single-rank reference"
292 " (tolerances u %.0e P %.0e C %.0e)\n",
293 tolU, tolP, tolC);
294 fail = 1;
295 }
296 }
298 }
299 if (rank == 0)
300 std::printf("%s\n", fail ? "FAILED" : "PASSED");
301 }
302 Kokkos::finalize();
303 MPI_Finalize();
304 return fail;
305}
void setVofKappaConstant(double kappa)
kappa is a PHYSICAL curvature, 1/length (the internal field is 1/h; see vof_curvature()).
std::vector< double > getVofCurvature()
The interface curvature kappa = 2H in the caller's units, i.e.
void setDomainBc(int face, int type, double vx, double vy, double vz)
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 setSurfaceTension(double sigma)
std::vector< double > getVofCurvatureBranch()
void setPressureChebyshev(bool on, int maxit, double rtol)
double capillaryDt()
The Brackbill capillary limit in the CALLER's time unit.
void setVof(const std::vector< double > &c)
void setPressureGeometry(const std::vector< double > &sdfInner)
std::vector< double > getVelocity(int c)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setVelocityResidualTolerance(double rtol)
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 > &v)
static constexpr int NY
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static constexpr double SIGMA
static double maxDiff(const std::vector< double > &a, const std::vector< double > &b)
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 constexpr double MU
static long countBitDiff(const std::vector< double > &a, const std::vector< double > &b, double &mx)
static constexpr double RHO_G
static double colourAt(const Config &c, int x, int y, int z)
static constexpr int NX
static constexpr double RHO_L
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)
static constexpr double RDROP