flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_movingscene_advect_mpi.cpp
Go to the documentation of this file.
1// flow — a MOVING analytic scene with ADVECTION ON, distributed (advective cut-wall gate 7).
2//
3// THE GAP THIS CLOSES. Every other ctest in tests/kokkos_mpi runs `setAdvection(false)`, and none
4// of them moves a scene instance. So the suite had no distributed measurement of the two things
5// the advective cut-wall campaign (`doc/advective_cutwall_flux_plan.md`) actually changed:
6//
7// * rung A0 fills the SOLID-MASKED rows of the advection's scratch inputs with the local
8// rigid-body wall velocity taken from `uBc_`, ANALYTIC-POINTWISE over the extended block —
9// ghosts included. The plan argues that because the fill is pointwise from replicated instance
10// state, no halo exchange is needed for it under MPI. That is an ARGUMENT. This test turns it
11// into a measurement: if a ghost solid row were left holding the masked zero on one rank and
12// the wall velocity on another, the SOU/Koren stencils (which reach 2 cells into the solid)
13// would read different data either side of a rank boundary and the distributed march would
14// drift from single-rank.
15// * the moving-geometry machinery around it — `rebuildGeometry()` per step (fresh cells, the
16// cut-cell overlay, the apertures and the pressure operator all re-derived from the instance
17// transforms), the wall's own volume flux in the projection, and the discrete REACTION force
18// `hydroForceTorqueReaction()`, which is what the campaign's Newton audit reads.
19//
20// THE CASE. Periodic 48^3 box, ONE analytic sphere (d = 8 cells) TOWED diagonally through fluid
21// initially at rest, with explicit SOU advection on. The centre is off-lattice (+0.3 in every
22// axis — a grid-plane-aligned moving face produces zero cut cells and goes silently inert, a trap
23// recorded in the campaign plan) and the tow path deliberately CROSSES the ORB cut planes in both
24// x and y at np = 2 and np = 4: the body, its wall velocity and its cut band are all in flight
25// across rank boundaries, which is the only configuration in which the A0 ghost claim can fail.
26// Re = U d / nu = 20, dt chosen for CFL = 0.2 (advective, not diffusive, limited), 60 steps, so
27// the sphere traverses 12 cells = 1.5 diameters and rebuilds geometry 60 times.
28//
29// THE GATE. Distributed vs the full-grid SINGLE-RANK reference built in the same executable:
30// * velocity (all 3 components) and pressure, gathered to rank 0 and differenced cell by cell;
31// * the per-instance REACTION force and torque (the coupling force the campaign cares about).
32// np = 1 must be BIT-EXACT in the FIELDS (tol 0.0 — initMpi on one rank must not perturb a single
33// bit of the moving + advective path). The reaction force is held to a double-precision
34// accumulation floor instead, and NOT because of MPI: `hydroForceTorqueReaction` sums with
35// `Kokkos::atomic_add` over an unordered device traversal, so it is tolerance-reproducible rather
36// than bitwise even between two runs whose fields agree bit for bit. np > 1 lands on the usual
37// MG-PCG reduction-order floor — the Krylov inner-product Allreduce reorders with the rank count
38// and 60 nonlinear steps amplify it — the established 3e-7-relative class of the other MPI gates.
39//
40// WHAT IT MEASURED (2026-09-02, first run, RTX 5080 / nvidia-cuda prefix). np = 1 is bit-exact in
41// every field (du = dp = 0.000e+00) — but **np = 2 and np = 4 FAIL, and the A0 fill is the cause**.
42// The 2x2 ablation, same case, np = 2, max|u_dist - u_ref| over the whole grid (|u|max = 4.03e-02):
43//
44// moving + advection ON (the shipped case) du = 1.45e-07 <- FAIL (tol 1.21e-08)
45// moving + advection OFF (GATE7_ADV=0) du = 5.99e-16
46// static + advection ON (GATE7_MOVE=0) du = 3.47e-17
47// moving + advection ON, A0 fill disabled
48// (set_advection_wall_velocity(False)) du = 1.28e-16
49//
50// So neither the moving-geometry machinery nor the advection is decomposition-dependent on its
51// own, and with the A0 fill turned off the moving+advective march is bit-clean across ranks. The
52// np-dependence lives entirely in `buildAdvInputs`. It is also LOCAL to the rank boundary: with
53// the identical case translated so the body never comes within the ghost ring of a cut plane
54// (GATE7_SHIFT=-8) np = 2 reads du = 2.57e-15. The likely mechanism is that `uBc_` is built by
55// `buildWallVelocity` over the extended block from a CENTRED difference of `sdf_`
56// (`ccSampleExt(sd, e, sx +- 1, ...)`), which at the OUTERMOST ghost plane has no neighbour to
57// difference against and clamps — and `uBc_` is never halo-exchanged, so those planes hold a
58// different wall velocity than the single-rank run holds at the same global points, and the SOU
59// stencil (reach 2) carries it inward. The plan's claim that "the scene is analytic, so ghost
60// solid rows are computable pointwise and no extra exchange is needed" is right about the SCENE
61// and wrong about this fill, which reads the sampled SDF, not the scene.
62//
63// An earlier variant of this case whose body ended with its wall band sitting IN rank 0's outer
64// ghost planes (centre stopping at x = 27.5 against a cut at x = 32) measured du = 1.385e-03 at
65// np = 2 and 1.703e-03 at np = 4 — 3.5 % of max|u| — so the magnitude depends on where the body is
66// when the fields are compared, and the worst case is a body parked on a rank boundary.
67//
68// ABLATION KNOBS (all default to the shipped case): GATE7_ADV=0 turns advection off, GATE7_MOVE=0
69// freezes the instance and drives the flow with a body force instead, GATE7_SHIFT=<cells> slides
70// the whole tow path, and the solver's own set_advection_wall_velocity(False) disables the
71// A0 fill.
72#include <mpi.h>
73
74#include <array>
75#include <cmath>
76#include <cstdio>
77#include <cstdlib>
78#include <cstring>
79#include <Kokkos_Core.hpp>
80#include <vector>
81
82#include "flow_ibm.hpp"
83#include "peclet/core/common/types.hpp"
84#include "peclet/core/decomp/block_decomposer.hpp"
85#include "peclet/core/geom/scene_builder.hpp"
86
88
89static constexpr int N = 48, STEPS = 60;
90static constexpr std::size_t GCELLS = (std::size_t)N * N * N;
91static constexpr double DIAM = 8.0; // sphere diameter, cells
92static constexpr double U = 0.05; // tow speed, cells per time unit
93static constexpr double RE = 20.0; // U * DIAM / MU -> finite-Re, advection matters
94static constexpr double MU = U * DIAM / RE;
95static constexpr double RHO = 1.0;
96static constexpr double DT = 4.0; // CFL = U*DT/h = 0.2; 0.2 cells of body motion per step
97static constexpr double OFF = 0.3; // off-lattice centre (grid-aligned walls go inert)
98// Tow direction (unit): diagonal in x-y so the body crosses the ORB cut planes on BOTH cut axes.
99static constexpr double DIRX = 0.6, DIRY = 0.8;
100// Start so that the 60-step path (7.2 cells in x, 9.6 in y) straddles the np=2/np=4 cut at 24.
101static constexpr double X0C = 0.60 * N + OFF, Y0C = 0.57 * N + OFF, Z0 = 0.50 * N + OFF;
102// Ablation instruments (see the 2x2 table in the header). GATE7_SHIFT moves the whole tow path by
103// a constant so the body stays CLEAR of every rank boundary — the control that localises a failure
104// to the boundary rather than to "advection under MPI" in general.
105static double gShift() {
106 const char* v = std::getenv("GATE7_SHIFT");
107 return v ? std::atof(v) : 0.0;
108}
109static double X0() {
110 return X0C + gShift();
111}
112static double Y0() {
113 return Y0C + gShift();
114}
115
116// The flat scene encoding (peclet/core/geom/scene_builder.hpp): one sphere leaf, one instance.
117static void sphereScene(std::vector<int>& ni, std::vector<double>& nr, std::vector<int>& ii,
118 std::vector<double>& ir) {
119 namespace g = peclet::core::geom;
120 ni.assign(g::kNodeIntStride, 0);
121 nr.assign(g::kNodeRealStride, 0.0);
122 ni[0] = g::kSphere;
123 ni[1] = -1;
124 ni[2] = -1;
125 nr[0] = 0.5 * DIAM; // params[0] = radius
126 nr[14] = 1.0; // rotation w
127 nr[15] = 1.0; // scale
128 ii.assign(g::kInstanceIntStride, 0);
129 ir.assign(g::kInstanceRealStride, 0.0);
130 ii[0] = 0; // shapeRoot
131 ii[1] = -1; // materialId
132 ir[0] = X0();
133 ir[1] = Y0();
134 ir[2] = Z0;
135 ir[6] = 1.0; // rotation w
136 ir[7] = 1.0; // scale
137}
138
139static bool envOn(const char* k) {
140 const char* v = std::getenv(k);
141 return !v || std::atoi(v) != 0;
142}
143
144static void configure(IbmSolver& s) {
145 s.setRho(RHO);
146 s.setMu(MU);
147 s.setDt(DT);
148 s.setAdvection(envOn("GATE7_ADV")); // THE point of this test (ablation knob)
149 s.setAdvectionScheme(0); // SOU (the default)
152 s.setPressurePcg(true, 200, 1e-10);
153 std::vector<int> ni, ii;
154 std::vector<double> nr, ir;
155 sphereScene(ni, nr, ii, ir);
156 s.setScene(ni, nr, ii, ir, /*periodic=*/true);
157 s.setSolidFromScene(/*cutcell_pressure=*/true);
158 const double m = envOn("GATE7_MOVE") ? 1.0 : 0.0;
159 if (m == 0.0)
160 s.setBodyForce(2e-4, 0.0, 0.0); // ablation: a static body needs a driver
161 s.setInstanceMotion(0, {m * DIRX * U, m * DIRY * U, 0.0}, {0.0, 0.0, 0.0}, nullptr);
162}
163
164// One towed step: place the body where it is at the START of the step, keep its velocity, rebuild
165// the geometry from the transforms, advance. Identical in the distributed and the reference run.
166static void towStep(IbmSolver& s, int it) {
167 const double m = envOn("GATE7_MOVE") ? 1.0 : 0.0;
168 const double x = X0() + m * DIRX * U * DT * it;
169 const double y = Y0() + m * DIRY * U * DT * it;
170 s.setInstanceTransform(0, {x, y, Z0}, {0.0, 0.0, 0.0, 1.0});
171 s.setInstanceMotion(0, {m * DIRX * U, m * DIRY * U, 0.0}, {0.0, 0.0, 0.0}, nullptr);
172 s.rebuildGeometry();
173 s.step();
174}
175
176// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
177static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
178 int lnx, int lny, int lnz, int rank, int size) {
179 std::vector<double> global;
180 if (rank == 0)
181 global.assign(GCELLS, 0.0);
182 for (int r = 0; r < size; ++r) {
183 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
184 if (r == 0) {
185 if (rank == 0)
186 for (int z = 0; z < lnz; ++z)
187 for (int y = 0; y < lny; ++y)
188 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * N +
189 (std::size_t)(z + oz) * N * N],
190 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
191 (std::size_t)lnx * sizeof(double));
192 continue;
193 }
194 if (rank == r) {
195 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
196 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
197 } else if (rank == 0) {
199 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
200 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
202 for (int z = 0; z < meta[5]; ++z)
203 for (int y = 0; y < meta[4]; ++y)
204 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * N +
205 (std::size_t)(z + meta[2]) * N * N],
206 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
207 (std::size_t)meta[3] * sizeof(double));
208 }
209 }
210 return global;
211}
212
213static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
214 double m = 0;
215 for (std::size_t i = 0; i < b.size(); ++i)
216 m = std::fmax(m, std::fabs(a[i] - b[i]));
217 return m;
218}
219static double maxAbs(const std::vector<double>& a) {
220 double m = 0;
221 for (double v : a)
222 m = std::fmax(m, std::fabs(v));
223 return m;
224}
225
226int main(int argc, char** argv) {
227 MPI_Init(&argc, &argv);
228 Kokkos::initialize(argc, argv);
229 int fail = 0;
230 {
231 int rank = 0, size = 1;
234
235 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), N, N, N);
236 auto blk = dec.block(rank);
237 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
238 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
239 bool cut[3] = {false, false, false};
240 for (const auto& sz : dec.sizes())
241 for (int a = 0; a < 3; ++a)
242 if ((int)sz[a] != N)
243 cut[a] = true;
244
245 // The body must actually FLY ACROSS a rank boundary — otherwise this test silently degrades
246 // into "a moving body that happens to sit inside one block", which gates nothing about the A0
247 // ghost claim. Report the crossings; demand at least one when the decomposition cuts.
248 const double x1 = X0() + DIRX * U * DT * (STEPS - 1), y1 = Y0() + DIRY * U * DT * (STEPS - 1);
249 int crossings = 0;
250 for (std::size_t r = 0; r < dec.sizes().size(); ++r) {
251 const auto b = dec.block(r);
252 const double bx = (double)b.origin[0], by = (double)b.origin[1];
253 if (bx > 0.0 && ((X0() - bx) * (x1 - bx) < 0.0))
254 ++crossings;
255 if (by > 0.0 && ((Y0() - by) * (y1 - by) < 0.0))
256 ++crossings;
257 }
258 if (rank == 0)
259 std::printf(
260 "MOVING-SCENE ADVECT MPI np=%d grid %d^3 block %dx%dx%d cut axes: %s%s%s "
261 "sphere d=%g Re=%g mu=%g dt=%g steps=%d tow (%.2f,%.2f)->(%.2f,%.2f) "
262 "cut-plane crossings=%d\n",
263 size, N, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "", DIAM, RE,
264 MU, DT, STEPS, X0(), Y0(), x1, y1, crossings);
265 const bool plainRun = gShift() == 0.0 && envOn("GATE7_MOVE") && envOn("GATE7_ADV");
266 if (size > 1 && plainRun && crossings == 0) {
267 if (rank == 0)
268 std::fprintf(stderr,
269 " FAIL — the towed sphere never crosses an ORB cut plane; this test exists "
270 "to gate the moving wall + its advective inputs ACROSS a rank boundary\n");
271 fail = 1;
272 }
273
274 // --- distributed run ---
276 sd.initMpi(dec, MPI_COMM_WORLD);
277 configure(sd);
278 if (!sd.hasMovingInstance()) {
279 if (rank == 0)
280 std::fprintf(stderr, " FAIL — the scene instance is not moving (setInstanceMotion)\n");
281 fail = 1;
282 }
283 for (int it = 0; it < STEPS; ++it)
284 towStep(sd, it);
285 const double divDist = sd.maxOpenDivergence();
286 const std::vector<double> frDist = sd.hydroForceTorqueReaction(); // Allreduced in C++
287
288 std::vector<double> gu[3];
289 for (int c = 0; c < 3; ++c)
290 gu[c] = gatherGlobal(sd.getVelocity(c), ox, oy, oz, lnx, lny, lnz, rank, size);
291 const std::vector<double> gp =
292 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
293
294 // --- full-grid single-rank reference (the validated single-GPU path) on rank 0 ---
295 if (rank == 0) {
296 IbmSolver ref(N, N, N);
297 configure(ref);
298 // The distributed solve keeps the host GraphAMG bottom (doc/vof_step_performance_design.md
299 // §13.9 Q-D7); on a GPU the single-rank solver would take the device bottom, so the np = 1
300 // bit-exact gate pins the reference to the engine the distributed path runs.
301 ref.setPressureBottomSolver(2); // 'algebraic'
302 for (int it = 0; it < STEPS; ++it)
303 towStep(ref, it);
304 const std::vector<double> frRef = ref.hydroForceTorqueReaction();
305
306 double du = 0, umag = 0;
307 for (int c = 0; c < 3; ++c) {
308 du = std::fmax(du, maxAbsDiff(gu[c], ref.getVelocity(c)));
309 umag = std::fmax(umag, maxAbs(ref.getVelocity(c)));
310 }
311 const double dp = maxAbsDiff(gp, ref.getPressure());
312 const double pmag = maxAbs(ref.getPressure());
313 double dF = 0, dT = 0;
314 for (int k = 0; k < 3; ++k) {
315 dF = std::fmax(dF, std::fabs(frDist[k] - frRef[k]));
316 dT = std::fmax(dT, std::fabs(frDist[3 + k] - frRef[3 + k]));
317 }
318 const double fmagv =
319 std::fmax(std::fmax(std::fabs(frRef[0]), std::fabs(frRef[1])), std::fabs(frRef[2]));
320 const double tmagv =
321 std::fmax(std::fmax(std::fabs(frRef[3]), std::fabs(frRef[4])), std::fabs(frRef[5]));
322
323 // FIELDS: np=1 BIT-EXACT (tol 0.0); np>1 the established 3e-7-relative class of the other
324 // scene/field MPI gates (the MG-PCG inner-product Allreduce reorders with the rank count).
325 const double rel = (size == 1) ? 0.0 : 3e-7;
326 const double utol = rel * std::fmax(umag, 1e-300);
327 const double ptol = rel * std::fmax(pmag, 1e-300);
328 // FORCE/TORQUE: NOT bitwise even at np=1, and not because of MPI. `hydroForceTorqueReaction`
329 // accumulates with `Kokkos::atomic_add` over an unordered device traversal, so the summation
330 // order is not reproducible between two runs holding BIT-IDENTICAL fields (the same caveat
331 // `mpi_scene_gate.py` states: "tolerance-reproducible, not bitwise"). np=1 therefore gets a
332 // double-precision accumulation floor, not zero; np>1 the field class. Torque is scaled by
333 // the lever arm the force acts on.
334 const double relF = (size == 1) ? 1e-12 : 3e-7;
335 const double ftol = relF * std::fmax(fmagv, 1e-300);
336 const double ttol = relF * std::fmax(tmagv + fmagv * DIAM, 1e-300);
337
338 // The case must be non-degenerate: a real flow, a real reaction force, a clean projection.
339 const bool live = umag > 1e-4 && fmagv > 1e-6;
340 const bool ok =
341 du <= utol && dp <= ptol && dF <= ftol && dT <= ttol && live && divDist < 1e-6;
342 std::printf(
343 " np=%d du=%.3e (|u|=%.4e) dp=%.3e (|p|=%.4e) dF=%.3e dT=%.3e "
344 "div=%.2e tol %.1e/%.1e rel\n",
345 size, du, umag, dp, pmag, dF, dT, divDist, rel, relF);
346 std::printf(" np=%d F_dist=(%.17g, %.17g, %.17g)\n", size, frDist[0], frDist[1], frDist[2]);
347 std::printf(" np=%d F_ref =(%.17g, %.17g, %.17g) T_ref=(%.3e, %.3e, %.3e)\n", size,
348 frRef[0], frRef[1], frRef[2], frRef[3], frRef[4], frRef[5]);
349 if (!live)
350 std::fprintf(stderr, " FAIL — degenerate case (|u|=%.3e, |F|=%.3e)\n", umag, fmagv);
351 if (!ok)
352 fail = 1;
353 }
355 if (rank == 0)
356 std::printf("MOVING-SCENE ADVECT MPI (np=%d): %s\n", size, fail ? "FAIL" : "PASS");
357 }
358 Kokkos::finalize();
359 MPI_Finalize();
360 return fail;
361}
void setInstanceTransform(int i, const std::array< double, 3 > &translation, const std::array< double, 4 > &quat)
Move one instance (Layer 3 rung 4).
bool hasMovingInstance() const
True when at least one instance carries a nonzero velocity – i.e.
void rebuildGeometry()
Re-derive ALL geometry from the current instance transforms (Layer 3 rung 4): rebuild the accelerated...
void setAdvection(bool on)
void setSolidFromScene(bool cutcellPressure)
Sample the scene onto this rank's inner grid and install it as the solid, entirely on device – no nx*...
void setVelocityIterations(int it)
void setPressureLevels(int levels)
void setInstanceMotion(int i, const std::array< double, 3 > &lin, const std::array< double, 3 > &ang, const double *center=nullptr)
Rigid-body motion of one scene instance (Layer 3 rung 2).
void setBodyForce(double fx, double fy, double fz)
Body force per unit volume, in the caller's units (e.g. a mean pressure gradient, or rho*g).
std::vector< double > getVelocity(int c)
void setAdvectionScheme(int s)
void setPressurePcg(bool on, int maxit, double rtol)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setScene(const std::vector< int > &nodeInts, const std::vector< double > &nodeReals, const std::vector< int > &instInts, const std::vector< double > &instReals, bool periodic=false)
Install an analytic scene from core's flat node/instance encoding (peclet/core/geom/scene_builder....
void setRho(double r)
Fluid density, in the caller's units.
std::vector< double > hydroForceTorqueReaction()
Hydrodynamic force and torque per instance from the DISCRETE REACTION (route (b) of the design note's...
std::vector< double > getPressure()
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
static double Y0()
static double maxAbs(const std::vector< double > &a)
static double X0()
static constexpr double X0C
static constexpr double RE
static void towStep(IbmSolver &s, int it)
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 gShift()
static constexpr double Z0
static constexpr double RHO
static constexpr double MU
static constexpr double Y0C
static constexpr double DIRX
static constexpr int STEPS
static constexpr double DIRY
static constexpr int N
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static void sphereScene(std::vector< int > &ni, std::vector< double > &nr, std::vector< int > &ii, std::vector< double > &ir)
static constexpr double OFF
static constexpr double DT
static constexpr double DIAM
static void configure(IbmSolver &s)
static bool envOn(const char *k)
static constexpr std::size_t GCELLS
static constexpr double U