flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_collocated_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the COLLOCATED two-phase path (VoF rung V8,
2// doc/collocated_varrho_forces.md).
3//
4// What this gates that no existing MPI test does: the pieces rung V8 adds to `SolverColocated`
5// live on the FACE field or on its ghost ring, and every one of them is assembled from exchanged
6// property ghosts.
7//
8// * the predictor's face accelerations `Phi(i) = (f_const + CSF - (P(i)-P(i-s)))/rho_f(i)` are
9// built on the face range `[g, e-g]`, so the plane at index `e-g` — the face the cell
10// reconstruction of the last inner cell reads as `Phi(i+s)` — is formed on THIS rank from
11// depth-1 ghosts of P and rho. It is bitwise decomposition-independent only if those ghosts are
12// the owner's values and the arithmetic is written in the same order on both sides (it is: the
13// same `0.5*(rho(i)+rho(i-s))`);
14// * the constraint's momentum-weighted face field `(rho_L u_L + rho_R u_R)/(rho_L + rho_R)` and
15// the cell correction (the face-to-cell reconstruction of the face corrections) read that same
16// plane;
17// * the colour is bridged from `uf_/vf_/wf_` after `fillGhosts`, not from the cell field.
18//
19// Two configurations on 16x16x32, whose aligned ORB cuts the LONG z axis at np = 2 and 4 — the axis
20// that carries the walls, the stratification and the interface, deliberately (WO-F: the walled axis
21// must be CUT, and the density jump must sit on a rank boundary).
22//
23// * `hydro-z` — the hydrostatic acid test on the collocated grid, driven through a FROZEN colour
24// field at ratio 1000 with no-slip walls at +-z. The gated quantity on this grid is the FACE
25// field (the CELL field carries the approximate projection's invisible checkerboard, which
26// `centerToFace` annihilates — see tests/kokkos/test_vof_collocated.cpp T1b) plus the pointwise
27// agreement with the single-rank reference.
28// * `vof-z` — a coupled run: `enable_vof` at ratio 10, a sharp interface stacked along the CUT
29// axis, a uniform body force, 20 steps. The colour is transported by the projected face field,
30// so this is the gate on the collocated bridge under a decomposition.
31//
32// Comparison protocol (the `tests/kokkos_mpi` pattern): the distributed run is compared POINTWISE
33// against a full-grid single-rank reference built on rank 0. np = 1 is bit-exact; np > 1 lands on
34// the MPI reduction-order floor (Chebyshev's bound estimation and `removeMean` both go through an
35// MPI_SUM allreduce whose summation order depends on the rank count).
36#include <mpi.h>
37
38#include <cmath>
39#include <cstdio>
40#include <cstring>
41#include <Kokkos_Core.hpp>
42#include <string>
43#include <vector>
44
45#include "flow_ibm.hpp"
46#include "peclet/core/common/types.hpp"
47#include "peclet/core/decomp/block_decomposer.hpp"
48
50
51static constexpr int NX = 16, NY = 16, NZ = 32;
52static constexpr double GRAV = 0.1, DT = 1.0, RHO0 = 1.0;
53static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
54
55struct Config {
56 const char* name;
57 double ratio;
58 bool walls; // hydrostatic acid test (frozen colour) vs the coupled periodic VoF run
59 int steps;
60};
61
62// C = 1 (heavy) in the lower half of z
63static double colourAt(int, int, int z) {
64 return (z < NZ / 2) ? 1.0 : 0.0;
65}
66
67template <class Fn>
68static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
69 std::vector<double> v((std::size_t)lnx * lny * lnz);
70 for (int z = 0; z < lnz; ++z)
71 for (int y = 0; y < lny; ++y)
72 for (int x = 0; x < lnx; ++x)
73 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
74 f(x + ox, y + oy, z + oz);
75 return v;
76}
77
78static void configure(Colo& s, const Config& c, int ox, int oy, int oz, int lnx, int lny, int lnz) {
79 s.setRho(RHO0);
80 s.setMu(c.walls ? 0.0 : 0.05); // hydrostatic: inviscid, so the face balance is exact
81 s.setDt(DT);
82 s.setAdvection(!c.walls);
83 if (c.walls) {
84 s.setDomainBc(4, 1, 0, 0, 0);
85 s.setDomainBc(5, 1, 0, 0, 0);
86 } else {
87 s.setBodyForce(1e-3, 0, 0);
88 }
89 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
90 const auto c0 = blockOf(colourAt, ox, oy, oz, lnx, lny, lnz);
91 if (c.walls) {
92 // FROZEN interface: "C" is a plain registered field the closure reads, enable_vof is never
93 // called, so C supplies rho and nothing else (the `freeze` route of test_vof_twophase.cpp B2).
94 s.addField("C");
95 s.setField("C", c0);
96 s.exchangeField("C");
97 } else {
98 s.enableVof();
99 s.setVof(c0);
100 }
101 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, c.ratio - 1.0});
102 if (c.walls)
103 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -GRAV});
104}
105
106static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
107 int lnx, int lny, int lnz, int rank, int size) {
108 std::vector<double> global;
109 if (rank == 0)
110 global.assign(GCELLS, 0.0);
111 for (int r = 0; r < size; ++r) {
112 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
113 if (r == 0) {
114 if (rank == 0)
115 for (int z = 0; z < lnz; ++z)
116 for (int y = 0; y < lny; ++y)
117 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
118 (std::size_t)(z + oz) * NX * NY],
119 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
120 (std::size_t)lnx * sizeof(double));
121 continue;
122 }
123 if (rank == r) {
124 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
125 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
126 } else if (rank == 0) {
127 MPI_Recv(meta, 6, MPI_INT, r, 100 + r, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
128 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
129 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
130 MPI_STATUS_IGNORE);
131 for (int z = 0; z < meta[5]; ++z)
132 for (int y = 0; y < meta[4]; ++y)
133 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
134 (std::size_t)(z + meta[2]) * NX * NY],
135 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
136 (std::size_t)meta[3] * sizeof(double));
137 }
138 }
139 return global;
140}
141
142static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
143 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
144 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
145 // a drained open-boundary run). A non-finite difference must fail, so return it.
146 double m = 0;
147 for (std::size_t i = 0; i < b.size(); ++i) {
148 const double d = std::fabs(a[i] - b[i]);
149 if (!(d == d))
150 return d; // NaN
151 m = std::fmax(m, d);
152 }
153 return m;
154}
155static double maxAbs(const std::vector<double>& a) {
156 double m = 0;
157 for (double v : a)
158 m = std::fmax(m, std::fabs(v));
159 return m;
160}
161
162int main(int argc, char** argv) {
163 MPI_Init(&argc, &argv);
164 Kokkos::initialize(argc, argv);
165 int fail = 0;
166 {
167 int rank = 0, size = 1;
168 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
169 MPI_Comm_size(MPI_COMM_WORLD, &size);
170
171 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
172 auto blk = dec.block(rank);
173 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
174 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
175 const int gn[3] = {NX, NY, NZ};
176 bool cut[3] = {false, false, false};
177 for (const auto& sz : dec.sizes())
178 for (int a = 0; a < 3; ++a)
179 if ((int)sz[a] != gn[a])
180 cut[a] = true;
181 if (rank == 0)
182 std::printf("VOF COLLOCATED MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n",
183 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
184 cut[2] ? "z" : "");
185
186 const Config configs[] = {{"hydro-z", 1000.0, true, 20}, {"vof-z", 10.0, false, 10}};
187
188 for (const Config& c : configs) {
189 // The stratification / interface axis must be CUT: that is the whole point (the face
190 // accelerations' high-side plane and the colour bridge both live on a block boundary there).
191 if (size > 1 && !cut[2]) {
192 if (rank == 0)
193 std::printf(" [%-7s np=%d] FAIL — the decomposition does NOT cut z\n", c.name, size);
194 fail = 1;
195 continue;
196 }
197 Colo sd(lnx, lny, lnz);
198 sd.initMpi(dec, MPI_COMM_WORLD);
199 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
200 std::vector<long> itd;
201 for (int it = 0; it < c.steps; ++it) {
202 sd.step();
203 itd.push_back(sd.lastPressureIterations());
204 }
205 std::vector<double> gu[3], gf[3];
206 for (int comp = 0; comp < 3; ++comp) {
207 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
208 gf[comp] = gatherGlobal(sd.getFaceVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
209 }
210 const std::vector<double> gp =
211 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
212 const std::vector<double> gc =
213 gatherGlobal(sd.getField("C"), ox, oy, oz, lnx, lny, lnz, rank, size);
214
215 if (rank == 0) {
216 Colo ref(NX, NY, NZ);
217 configure(ref, c, 0, 0, 0, NX, NY, NZ);
218 std::vector<long> itr;
219 for (int it = 0; it < c.steps; ++it) {
220 ref.step();
221 itr.push_back(ref.lastPressureIterations());
222 }
223 double du = 0, df = 0, su = 0, sf = 0;
224 for (int comp = 0; comp < 3; ++comp) {
225 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
226 df = std::fmax(df, maxAbsDiff(gf[comp], ref.getFaceVelocity(comp)));
227 // The SCALE the tolerance is relative to must be the max over ALL THREE components: the
228 // `vof-z` configuration is driven along x, so scaling by the z component alone would
229 // demand bitwise agreement of an O(1e-3) x-field against a zero reference scale.
230 su = std::fmax(su, maxAbs(ref.getVelocity(comp)));
231 sf = std::fmax(sf, maxAbs(ref.getFaceVelocity(comp)));
232 }
233 const double dp = maxAbsDiff(gp, ref.getPressure());
234 const double dc = maxAbsDiff(gc, ref.getField("C"));
235 const double sp = maxAbs(ref.getPressure());
236 long dit = 0, sumd = 0, sumr = 0, maxd = 0, maxr = 0;
237 for (std::size_t k = 0; k < itd.size(); ++k) {
238 dit = std::max(dit, std::labs(itd[k] - itr[k]));
239 sumd += itd[k];
240 sumr += itr[k];
241 maxd = std::max(maxd, itd[k]);
242 maxr = std::max(maxr, itr[k]);
243 }
244 // THE PER-STEP V-CYCLE COUNT IS NOT A DECOMPOSITION INVARIANT HERE, and one cycle of slack
245 // (the `vardensity_mpi` precedent, flow 65b2b6a) is not enough. That commit's argument was
246 // that `maxabs(r) < rtol r0` is a threshold crossing on a quantity carrying a
247 // reduction-order floor, so the crossing index can move by one. `hydro-z` is worse than
248 // that: it is a REST STATE. Its converged face velocity is 1.9e-13 on |P| = 1.2e+03, so the
249 // divergence the projection is asked to remove is not a physical residual at all — it is
250 // the round-off of the hydrostatic balance, and r0 itself therefore differs between two
251 // decompositions by an O(1) RELATIVE amount. The count needed to cut THAT by rtol is a
252 // random walk, not a shifted copy. Measured over the 20 steps at np = 2 (2026-09-14): the
253 // sequences part at ten steps, by one at nine of them and by two at step 11 (10 vs 12) —
254 // and whether the worst gap is one or two depends on the OpenMP THREAD COUNT alone (one at
255 // 1, 4 and 8 threads, two at 2, at both np = 2 and np = 4). `dit <= 1` was a coin toss.
256 //
257 // What a decomposition defect actually does to a pressure solve — a stale ghost, a broken
258 // coarse level, a mis-scaled operator — is inflate it SYSTEMATICALLY; it does not shuffle
259 // it by a cycle. So the invariant with content is the COST ENVELOPE, and that is what is
260 // gated above np = 1: the worst step within one V-cycle of the reference's worst, and the
261 // total within one V-cycle per step. np = 1 keeps its exact zero.
262 const bool itOk = (size == 1) ? (dit == 0)
263 : (maxd <= maxr + 1 &&
264 std::labs(sumd - sumr) <= static_cast<long>(itd.size()));
265 // np = 1 must be BITWISE; np > 1 sits on the MPI reduction-order floor.
266 // THE YARDSTICK FOR A REST STATE IS THE FORCING, NOT THE ANSWER. `hydro-z` is hydrostatic:
267 // its velocity is the decaying rest-state transient (1.2e-3 after 20 steps with the
268 // balanced-force projection OFF, round-off with it ON; mostly the invisible checkerboard,
269 // not physics), so a tolerance relative to |u| would demand a bitwise match of the
270 // reduction order. The scale that means something there is the velocity the body force
271 // would produce in one step, g*dt, which is exactly what the projection has to cancel.
272 const double uRef = std::fmax(su, c.walls ? GRAV * DT : 0.0);
273 const double fRef = std::fmax(sf, c.walls ? GRAV * DT : 0.0);
274 const double tolU = (size == 1) ? 0.0 : 1e-11 * std::fmax(uRef, 1e-12);
275 const double tolF = (size == 1) ? 0.0 : 1e-11 * std::fmax(fRef, 1e-12);
276 const double tolP = (size == 1) ? 0.0 : 1e-9 * std::fmax(sp, 1e-12);
277 const double tolC = (size == 1) ? 0.0 : 1e-11;
278 const bool ok = du <= tolU && df <= tolF && dp <= tolP && dc <= tolC && itOk;
279 std::printf(
280 " [%-7s np=%d] du %.3e (|u| %.3e) duf %.3e (|uf| %.3e) dP %.3e (|P| %.3e)"
281 " dC %.3e d(iters) %ld iters max %ld/%ld sum %ld/%ld %s\n",
282 c.name, size, du, su, df, sf, dp, sp, dc, dit, maxd, maxr, sumd, sumr,
283 ok ? "OK" : "*** FAIL ***");
284 if (c.walls)
285 std::printf(
286 " face |uf| %.3e (the gated hydrostatic quantity), cell |u| %.3e "
287 "(carries the approximate projection's invisible checkerboard)\n",
288 maxAbs(ref.getFaceVelocity(2)), maxAbs(ref.getVelocity(2)));
289 if (dit > 0) {
290 // The evidence the gate's slack is argued from: WHERE in the sequence the counts part,
291 // and by how much. A single step out of step by one is the stopping test landing on the
292 // other side of its threshold; a whole tail shifted would be a different animal.
293 std::printf(" iteration counts (step: np/ref):");
294 for (std::size_t k = 0; k < itd.size(); ++k)
295 if (itd[k] != itr[k])
296 std::printf(" %zu: %ld/%ld", k, itd[k], itr[k]);
297 std::printf("\n");
298 }
299 if (!ok)
300 fail = 1;
301 }
302 MPI_Bcast(&fail, 1, MPI_INT, 0, MPI_COMM_WORLD);
303 }
304 if (rank == 0)
305 std::printf("%s\n", fail ? "FAILED" : "all collocated VoF MPI gates passed");
306 }
307 Kokkos::finalize();
308 MPI_Finalize();
309 return fail;
310}
std::vector< double > getFaceVelocity(int c)
std::vector< double > getField(const std::string &name)
std::vector< double > getVelocity(int c)
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 double colourAt(int, int, int z)
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 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 void configure(Colo &s, const Config &c, 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 constexpr double DT
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ