flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_cellforce_mpi.cpp
Go to the documentation of this file.
1// flow — a NON-UNIFORM per-cell body force under MPI, and the init_mpi / geometry call order.
2//
3// The defect this test exists for (2026-09-24, found through coupling's
4// `test_mpi_moving_suspension` at np = 4: rel-err 1.06e-2 against 1e-4). A caller that built the
5// geometry BEFORE `initMpi` (set_pressure_geometry, then init_mpi) got a solver whose momentum
6// solve was distributed but whose pressure multigrid was not: `setSolid` builds the pressure and
7// velocity hierarchies for the partition in force when it runs, which before `initMpi` is the
8// single-rank one, so every rank's hierarchy treated its own block as a periodic domain of its own
9// and `initMpi` never rebuilt it. Each rank then ran an independent pressure solve; every PCG
10// converged, and the projected velocity kept the divergence of its block-local mean. One step of
11// the coupling test's Solver with a fixed Gaussian `force_x`: max|du| = 4.05e-2 at np = 4 (2x2x1,
12// max|u| 0.358), 1.9e-5 at np = 2 (x cut only, where the blobs happen to be block-symmetric). One
13// rank is exact by accident, since its block is the domain. The cell force was the messenger, not
14// the cause: any velocity whose divergence has a non-zero mean over a rank block shows it.
15//
16// `initMpi` now raises when the geometry already exists, at every rank count. Two parts:
17//
18// * `order` — setPressureGeometry, then initMpi, must throw std::runtime_error. This is the
19// part that FAILS before the fix (the call returned and the run went on wrong).
20// * the cell-force comparisons, in the correct order, against a full-grid single-rank
21// reference — the coverage the existing MPI tests lacked: `test_bodyforce_ghost_mpi` drives
22// only UNIFORM forces, which a wrong neighbour, a wrong corner ghost or a block-local solve
23// all leave invisible. Gaussian blobs on the coupling test's particle lattice, including rows
24// centred ON the 16-planes the ORB cuts (np = 4 is 2x2x1 on this grid: x and y cut, each
25// periodic axis with the SAME neighbour on both sides; np = 2 cuts x):
26// - `forced-x`: constant rho -> buildRhsForced (the force read at the cell), force_x, the
27// blob line along x — the reproducer of the report, verbatim.
28// - `var-y`: density mode -> buildRhsVar (the force FACE-AVERAGED, so it reads the ghost
29// ring `fillCellForceGhosts` fills), force_y, the blob line along y.
30// Both written the CFD-DEM way (enableCellForce + setField: inner cells only, no exchange).
31// Two steps, advection on (the default), so step 2 also advects the forced field.
32//
33// Gates: np = 1 bitwise against the reference; np > 1 at the MG-PCG reduction-order floor (the
34// inner-product Allreduce reorders the Krylov path — test_sdflow_mpi, test_bodyforce_ghost_mpi),
35// with the pressure driver pinned to MG-PCG at rtol 1e-12 on both sides so the default-driver
36// difference (standalone V-cycle multi-rank, MG-PCG single-rank) is not what is measured.
37#include <mpi.h>
38
39#include <cmath>
40#include <cstdio>
41#include <cstring>
42#include <Kokkos_Core.hpp>
43#include <stdexcept>
44#include <string>
45#include <vector>
46
47#include "flow_ibm.hpp"
48#include "peclet/core/common/types.hpp"
49#include "peclet/core/decomp/block_decomposer.hpp"
50
52
53static constexpr int N = 32, STEPS = 2;
54static constexpr std::size_t GCELLS = (std::size_t)N * N * N;
55
56struct Config {
57 const char* name;
58 int comp; // forced velocity component, and the axis the six-blob line runs along
59 bool density; // uniform-rho density mode (buildRhsVar) vs constant rho (buildRhsForced)
60};
61
62// The coupling test's lattice: six blobs along axis `line` at (i + 1/2) N/6, times a 3x3 grid at
63// {10, 16, 22} on the other two axes. Unwrapped Gaussians, the same global field at every np.
64static std::vector<double> blobs(int line) {
65 std::vector<double> f(GCELLS, 0.0);
66 const double pl[3] = {10.0, 16.0, 22.0};
67 for (int z = 0; z < N; ++z)
68 for (int y = 0; y < N; ++y)
69 for (int x = 0; x < N; ++x) {
70 const double c[3] = {x + 0.5, y + 0.5, z + 0.5};
71 const int a = (line + 1) % 3, b = (line + 2) % 3;
72 double s = 0.0;
73 for (int i = 0; i < 6; ++i)
74 for (double pa : pl)
75 for (double pb : pl) {
76 const double dl = c[line] - (i + 0.5) * N / 6.0, da = c[a] - pa, db = c[b] - pb;
77 s += -12.0 * std::exp(-(dl * dl + da * da + db * db));
78 }
79 f[(std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N] = s;
80 }
81 return f;
82}
83
84static std::vector<double> slice(const std::vector<double>& g, int ox, int oy, int oz, int lnx,
85 int lny, int lnz) {
86 std::vector<double> l((std::size_t)lnx * lny * lnz);
87 for (int z = 0; z < lnz; ++z)
88 for (int y = 0; y < lny; ++y)
89 for (int x = 0; x < lnx; ++x)
90 l[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
91 g[(std::size_t)(x + ox) + (std::size_t)(y + oy) * N + (std::size_t)(z + oz) * N * N];
92 return l;
93}
94
95// Everything but the geometry, so the order part can put the geometry where it likes.
96static void configure(IbmSolver& s, const Config& c, const std::vector<double>& forceLocal) {
97 static const char* fn[3] = {"force_x", "force_y", "force_z"};
98 s.setRho(1.0);
99 s.setMu(1.0);
100 s.setDt(0.1);
101 if (c.density)
102 s.setDensityMode(true); // uniform rho field == rho0; routes the RHS through buildRhsVar
103 s.setPressurePcg(true, 200, 1e-12); // LAST: the density mode re-selects the driver
104 s.enableCellForce(); // external-writer path (CFD-DEM): setField, no exchange
105 s.setField(fn[c.comp], forceLocal);
106}
107
108// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
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) * N +
121 (std::size_t)(z + oz) * N * N],
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]) * N +
137 (std::size_t)(z + meta[2]) * N * N],
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
145// NaN-propagating (see test_bodyforce_ghost_mpi: std::fmax drops a NaN and a gate built on it
146// passes on a field that has gone entirely NaN).
147static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
148 double m = 0;
149 for (std::size_t i = 0; i < b.size(); ++i) {
150 const double d = std::fabs(a[i] - b[i]);
151 if (!(d == d))
152 return d;
153 m = std::fmax(m, d);
154 }
155 return m;
156}
157static double maxAbs(const std::vector<double>& a) {
158 double m = 0;
159 for (double v : a)
160 m = std::fmax(m, std::fabs(v));
161 return m;
162}
163
164int main(int argc, char** argv) {
165 MPI_Init(&argc, &argv);
166 Kokkos::initialize(argc, argv);
167 int fail = 0;
168 {
169 int rank = 0, size = 1;
172
173 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), N, N, N);
174 auto blk = dec.block(rank);
175 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
176 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
177 bool cut[3] = {false, false, false};
178 for (const auto& sz : dec.sizes())
179 for (int a = 0; a < 3; ++a)
180 if ((int)sz[a] != N)
181 cut[a] = true;
182 if (rank == 0)
183 std::printf("CELLFORCE MPI np=%d grid %d^3 block %dx%dx%d cut axes: %s%s%s\n", size, N,
184 lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "");
185 const std::vector<double> allFluid((std::size_t)lnx * lny * lnz, 10.0);
186
187 // ---- order: a geometry built before initMpi must be refused -------------------------------
188 {
189 IbmSolver s(lnx, lny, lnz);
191 bool threw = false;
192 std::string what;
193 try {
194 s.initMpi(dec, MPI_COMM_WORLD);
195 } catch (const std::runtime_error& e) {
196 threw = true;
197 what = e.what();
198 }
199 // The guard is rank-local and replicated (every rank built its geometry first), so every
200 // rank must agree; a rank that did not throw would be left half-initialised.
201 int t = threw ? 1 : 0, all = 0;
203 const bool ok = all == 1 && what.find("init_mpi") != std::string::npos;
204 if (rank == 0)
205 std::printf(" [order np=%d] set_pressure_geometry -> init_mpi %s %s\n", size,
206 threw ? "raised" : "was ACCEPTED (the rank-local pressure solve)",
207 ok ? "OK" : "FAIL");
208 if (!ok)
209 fail = 1;
210 }
211
212 // ---- non-uniform cell force, correct order, vs the single-rank reference ------------------
213 const Config configs[] = {{"forced-x", 0, false}, {"var-y", 1, true}};
214 for (const Config& c : configs) {
215 const std::vector<double> fg = blobs(c.comp);
217 sd.initMpi(dec, MPI_COMM_WORLD);
219 configure(sd, c, slice(fg, ox, oy, oz, lnx, lny, lnz));
220 for (int it = 0; it < STEPS; ++it)
221 sd.step();
222 std::vector<double> gu[3];
223 for (int comp = 0; comp < 3; ++comp)
224 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
225 const std::vector<double> gp =
226 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
227
228 if (rank == 0) {
229 IbmSolver ref(N, N, N);
230 ref.setPressureGeometry(std::vector<double>(GCELLS, 10.0));
231 configure(ref, c, fg);
232 for (int it = 0; it < STEPS; ++it)
233 ref.step();
234 double du = 0, umag = 0;
235 for (int comp = 0; comp < 3; ++comp) {
236 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
237 umag = std::fmax(umag, maxAbs(ref.getVelocity(comp)));
238 }
239 const double dp = maxAbsDiff(gp, ref.getPressure());
240 const double pmag = maxAbs(ref.getPressure());
241 const double utol = (size == 1) ? 0.0 : std::fmax(1e-15, 1e-11 * umag);
242 const double ptol = (size == 1) ? 0.0 : std::fmax(1e-12, 1e-11 * pmag);
243 // A forced flow, not a trivially zero one: the gate must have something to compare.
244 const bool ok = du <= utol && dp <= ptol && umag > 0.1;
245 std::printf(" [%-8s np=%d] du=%.3e dp=%.3e (tol %.1e/%.1e) max|u|=%.3e max|p|=%.3e %s\n",
246 c.name, size, du, dp, utol, ptol, umag, pmag, ok ? "OK" : "FAIL");
247 if (!ok)
248 fail = 1;
249 }
251 }
252 if (rank == 0)
253 std::printf("CELLFORCE MPI (np=%d): %s\n", size, fail ? "FAIL" : "PASS");
254 }
255 Kokkos::finalize();
256 MPI_Finalize();
257 return fail;
258}
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 setPressurePcg(bool on, int maxit, double rtol)
void setDensityMode(bool variable)
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 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 int STEPS
static constexpr int N
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static std::vector< double > slice(const std::vector< double > &g, int ox, int oy, int oz, int lnx, int lny, int lnz)
static void configure(IbmSolver &s, const Config &c, const std::vector< double > &forceLocal)
static constexpr std::size_t GCELLS
static std::vector< double > blobs()