flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_dragbeta_ghost_mpi.cpp
Go to the documentation of this file.
1// flow — the per-cell DRAG-COEFFICIENT ghost ring (VoF blocker WO-I).
2//
3// The defect this test exists for. Under `porous_` (the volume-averaged CFD-DEM gas phase),
4// `addDragDiagonal` builds the staggered momentum diagonal of component c from the FACE drag
5//
6// beta_f(i) = 0.5*(beta(i) + beta(i - stride_c)) [flow_ibm.hpp, addDragDiagonal]
7//
8// so that it matches the projection's SIMPLE-like coefficient and correction,
9// w_f = idt/(idt + beta_f) (`buildPorousCoeffDrag`/`Cons`, `projectCorrectPorous*`) — the
10// (diagonal <-> operator <-> correction) consistency of `doc/porous_drag_scheme.md` §2, the same
11// three-way face-mean argument `doc/variable_density_projection.md` §1/§3 makes for rho.
12//
13// But NO writer of "drag_beta" filled its ghost ring, and the only `fillPropGhosts(dragBeta_)` sat
14// inside `project()` — i.e. AFTER the momentum stencil builds, which all run at (or after) the top
15// of `step()`. `setField` and `applyClosure` write the inner cells only; the external CFD-DEM
16// writer's driver folds its ghost-band deposit onto the owners and then ZEROES that band
17// (single rank) or leaves the reverse-halo residue in it (MPI). So on the FIRST INNER PLANE of
18// every block the momentum diagonal read beta_f = 0.5*beta — a factor-2 error in the drag — while
19// the projection on that same face used the full, freshly exchanged value. That is precisely the
20// mismatch `addDragDiagonal`'s own comment records: the incremental pressure loop gains
21// (idt+beta_f)/(idt+beta_f^momentum) instead of 1 and "the accumulated pressure diverges
22// exponentially". The fix (`fillDragBetaGhosts()`, called next to WO-G's `fillCellForceGhosts()`
23// right after `updateProperties()`) routes the field through the same rank-aware `fillPropGhosts`
24// the projection uses, so both phases see the identical beta_f.
25//
26// Every configuration is a UNIFORM beta in a fully periodic box with a
27// uniform body force, inviscid and with advection off, so the momentum solve is diagonal and
28//
29// u^{n+1} = (idt*u^n + F) / (idt + beta_f), idt = rho/dt,
30//
31// i.e. the velocity IS a read-out of the momentum diagonal. A uniform u is divergence-free (and
32// eps == 1 is uniform, so div(eps u) = 0 with d(eps)/dt = 0), so the porous projection is an exact
33// no-op and the whole step is a pure diagonal recursion. With rho/dt = 1 and beta = 3 the diagonal
34// is 4.0f (exact in float) and every iterate is a dyadic rational, so the healthy answer is
35// bit-exact; the defective plane's diagonal is 2.5f.
36//
37// TWO gates, and the pair is the point:
38//
39// 1. THE DIAGONAL, where the defect lives. `getMomentumDiagonal(c)` reads the assembled float
40// stencil AC back, and every cell must equal 4.0 EXACTLY — including the first inner plane of
41// every block. This is the WO's literal requirement ("the momentum diagonal asserted uniform
42// on the first inner plane") and it localizes the offending plane. It is sampled after the
43// FIRST step, deliberately: in this pure-flow harness nothing re-dirties the ghost, so
44// `project()`'s (late) fill happens to leave a valid ring behind for step 2 onward and the
45// defect is a step-1-only effect here. In the real CFD-DEM loop the driver rewrites
46// "drag_beta" inner-only and FOLDS+ZEROES its ghost band before every `step()`, so every step
47// is a step 1 — which is why the velocity error below, seeded once, is the conservative
48// lower bound on what the coupled path pays.
49// 2. THE VELOCITY, i.e. what the defect costs. Note it does NOT show up as a non-uniform u: the
50// periodic projection removes the non-uniform part of u* and what survives is the (wrong)
51// MEAN — the same mechanism WO-G measured for the halved body force. So the velocity gate that
52// fires is the analytic VALUE, not the spread: measured before the fix, u is uniform to
53// 5.6e-17 but sits at 0.33210449 (per-z, 32 cells) / 0.33217773 (per-x, 16 cells) instead of
54// 0.33203125 — a 2.2e-4 / 4.4e-4 relative error scaling as 1/N_axis, exactly one bad plane's
55// worth. Both are gated; the spread gate is kept because it is what a WALL-bounded or
56// non-uniform-beta variant would trip.
57//
58// The gates are ABSOLUTE physics, not a distributed-vs-reference comparison — deliberately, exactly
59// as in `test_bodyforce_ghost_mpi.cpp`: the single-rank reference carries the identical defect, so
60// `du = 0` proves nothing and np = 1 must (and does) fail on its own.
61//
62// Configurations (NX x NY x NZ = 16 x 16 x 32: np=2 cuts z, np=4 cuts x and z):
63//
64// * `per-z` — porous continuity, non-conservative momentum, beta written by an EXTERNAL writer
65// (`setField`, no exchange — the CFD-DEM path), force_z. Stencil site:
66// `rebuildStencils`. Requires z to be CUT at np > 1.
67// * `per-x` — the same on the x axis (stride 1 instead of ex*ey). At np = 2 its "rank boundary"
68// is the PERIODIC WRAP plane in x, which is the single-rank half of the same defect
69// and an equally hard gate; at np = 4 x is genuinely cut. Runs at every np.
70// * `cons-z` — the eps-CONSERVATIVE porous momentum (`setPorousConservative(true)`): the diagonal
71// is eps_f*rho/dt + beta_f and the RHS goes through `buildRhsVar`, so this covers
72// the variable-density sibling of the stencil build. Requires z to be CUT.
73//
74// np = 1 is additionally compared bitwise against a full-grid single-rank reference; np > 1 lands
75// on the usual MPI reduction-order floor (see `test_vardensity_mpi.cpp`) — though on these
76// configurations the projection is a no-op, so in practice it comes out bitwise there too.
77#include <mpi.h>
78
79#include <cmath>
80#include <cstdio>
81#include <cstring>
82#include <Kokkos_Core.hpp>
83#include <vector>
84
85#include "flow_ibm.hpp"
86#include "peclet/core/common/types.hpp"
87#include "peclet/core/decomp/block_decomposer.hpp"
88
90
91static constexpr int NX = 16, NY = 16, NZ = 32, STEPS = 4;
92static constexpr double RHO0 = 1.0, DT = 1.0, FORCE = 1.0, BETA = 3.0;
93static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
94
95struct Config {
96 const char* name;
97 int comp; // forced velocity component (0=x, 1=y, 2=z) == the axis whose face beta is read
98 bool cons; // eps-conservative porous momentum (buildRhsVar path) vs the plain porous momentum
99 bool needCut; // the forced axis MUST be cut at np > 1 (loud coverage loss if it stops being)
100};
101
102static void configure(IbmSolver& s, const Config& c, int lnx, int lny, int lnz) {
103 s.setRho(RHO0);
104 s.setMu(0.0); // inviscid: the momentum operator is the pure diagonal idt + beta_f
105 s.setDt(DT);
106 s.setAdvection(false);
107 double f[3] = {0.0, 0.0, 0.0};
108 f[c.comp] = FORCE;
109 s.setBodyForce(f[0], f[1], f[2]); // the UNIFORM scalar body force, not a force FIELD: this test
110 // must not depend on WO-G's cell-force ghost path
111 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
112 s.setPorousContinuity(true); // binds "eps" (seeded to 1 over the whole padded block)
113 s.setPorousConservative(c.cons);
114 s.enableDrag(); // allocates "drag_beta" + routes it onto the momentum diagonal
115 // The external CFD-DEM writer path: inner cells only, ghosts left as registered (zero) — exactly
116 // what the coupling driver's ghost fold leaves behind at single rank.
117 s.setField("drag_beta", std::vector<double>((std::size_t)lnx * lny * lnz, BETA));
118}
119
120// The analytic diagonal recursion u^{n+1} = (idt*u^n + F)/(idt + beta), evaluated in double with
121// the same float diagonal the solver builds.
122static double analyticVelocity() {
123 const double ac = (double)(float)(RHO0 / DT + BETA);
124 double u = 0.0;
125 for (int n = 0; n < STEPS; ++n)
126 u = (RHO0 / DT * u + FORCE) / ac;
127 return u;
128}
129
130// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
131static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
132 int lnx, int lny, int lnz, int rank, int size) {
133 std::vector<double> global;
134 if (rank == 0)
135 global.assign(GCELLS, 0.0);
136 for (int r = 0; r < size; ++r) {
137 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
138 if (r == 0) {
139 if (rank == 0)
140 for (int z = 0; z < lnz; ++z)
141 for (int y = 0; y < lny; ++y)
142 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
143 (std::size_t)(z + oz) * NX * NY],
144 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
145 (std::size_t)lnx * sizeof(double));
146 continue;
147 }
148 if (rank == r) {
149 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
150 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
151 } else if (rank == 0) {
153 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
154 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
156 for (int z = 0; z < meta[5]; ++z)
157 for (int y = 0; y < meta[4]; ++y)
158 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
159 (std::size_t)(z + meta[2]) * NX * NY],
160 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
161 (std::size_t)meta[3] * sizeof(double));
162 }
163 }
164 return global;
165}
166
167static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
168 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
169 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
170 // a drained open-boundary run). A non-finite difference must fail, so return it.
171 double m = 0;
172 for (std::size_t i = 0; i < b.size(); ++i) {
173 const double d = std::fabs(a[i] - b[i]);
174 if (!(d == d))
175 return d; // NaN
176 m = std::fmax(m, d);
177 }
178 return m;
179}
180static double maxAbs(const std::vector<double>& a) {
181 double m = 0;
182 for (double v : a)
183 m = std::fmax(m, std::fabs(v));
184 return m;
185}
186
187// Index of the plane (along `axis`) whose velocity deviates most from the global mean — with the
188// defect this is the first inner plane of a rank block (or the periodic wrap plane), never an
189// interior one, so the report names the offender.
190static int worstPlane(const std::vector<double>& u, int axis) {
191 const int n[3] = {NX, NY, NZ};
192 double mean = 0;
193 for (double v : u)
194 mean += v;
195 mean /= (double)u.size();
196 int worst = -1;
197 double worstDev = -1;
198 for (int p = 0; p < n[axis]; ++p) {
199 double dev = 0;
200 for (int b = 0; b < n[(axis + 2) % 3]; ++b)
201 for (int a = 0; a < n[(axis + 1) % 3]; ++a) {
202 int idx[3];
203 idx[axis] = p;
204 idx[(axis + 1) % 3] = a;
205 idx[(axis + 2) % 3] = b;
206 dev = std::fmax(
207 dev,
208 std::fabs(
209 u[(std::size_t)idx[0] + (std::size_t)idx[1] * NX + (std::size_t)idx[2] * NX * NY] -
210 mean));
211 }
212 if (dev > worstDev) {
213 worstDev = dev;
214 worst = p;
215 }
216 }
217 return worst;
218}
219
220int main(int argc, char** argv) {
221 MPI_Init(&argc, &argv);
222 Kokkos::initialize(argc, argv);
223 int fail = 0;
224 {
225 int rank = 0, size = 1;
228
229 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
230 auto blk = dec.block(rank);
231 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
232 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
233 const int gn[3] = {NX, NY, NZ};
234 bool cut[3] = {false, false, false};
235 for (const auto& sz : dec.sizes())
236 for (int a = 0; a < 3; ++a)
237 if ((int)sz[a] != gn[a])
238 cut[a] = true;
239 if (rank == 0)
240 std::printf("DRAGBETA-GHOST MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n",
241 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
242 cut[2] ? "z" : "");
243
244 const Config configs[] = {
245 {"per-z", 2, false, true}, {"per-x", 0, false, false}, {"cons-z", 2, true, true}};
246 const double want = analyticVelocity();
247
248 for (const Config& c : configs) {
249 if (c.needCut && size > 1 && !cut[c.comp]) {
250 if (rank == 0)
251 std::printf(
252 " [%-6s np=%d] FAIL — the decomposition does NOT cut the forced axis %d; "
253 "this test exists to gate the drag_beta ghost ACROSS a rank boundary (WO-I)\n",
254 c.name, size, c.comp);
255 fail = 1;
256 continue;
257 }
259 sd.initMpi(dec, MPI_COMM_WORLD);
260 configure(sd, c, lnx, lny, lnz);
261 std::vector<double> diagLocal;
262 for (int it = 0; it < STEPS; ++it) {
263 sd.step();
264 // The diagonal is sampled after the FIRST step — see the "one step" note in the header:
265 // in this pure-flow harness `project()`'s late fill repairs the ghost from step 2 onward,
266 // whereas the real CFD-DEM writer re-zeroes it before every step.
267 if (it == 0)
269 }
270
271 std::vector<double> gu[3];
272 for (int comp = 0; comp < 3; ++comp)
273 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
274 const std::vector<double> gp =
275 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
276 // THE primary gate: the assembled momentum diagonal itself, read where the face drag is
277 // formed. (The velocity gate below sees the same defect only through the projection, which on
278 // a periodic box turns one bad plane into a uniform mean shift.)
279 const std::vector<double> gd = gatherGlobal(diagLocal, ox, oy, oz, lnx, lny, lnz, rank, size);
280
281 if (rank == 0) {
282 IbmSolver ref(NX, NY, NZ);
283 configure(ref, c, NX, NY, NZ);
284 for (int it = 0; it < STEPS; ++it)
285 ref.step();
286 double du = 0, umag = 0;
287 for (int comp = 0; comp < 3; ++comp) {
288 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
289 umag = std::fmax(umag, maxAbs(ref.getVelocity(comp)));
290 }
291 const double dp = maxAbsDiff(gp, ref.getPressure());
292 const double pmag = maxAbs(ref.getPressure());
293 const double utol = (size == 1) ? 0.0 : std::fmax(1e-15, 1e-11 * umag);
294 const double ptol = (size == 1) ? 0.0 : std::fmax(1e-12, 1e-11 * pmag);
295 bool ok = du <= utol && dp <= ptol;
296
297 // THE gate: the FACE drag on the momentum diagonal is uniform <=> the velocity is uniform
298 // and equal to the analytic diagonal recursion.
299 const std::vector<double>& uf = gu[c.comp];
300 double lo = uf[0], hi = uf[0];
301 for (double v : uf) {
302 lo = std::fmin(lo, v);
303 hi = std::fmax(hi, v);
304 }
305 const double spread = hi - lo, verr = std::fabs(0.5 * (hi + lo) - want) / want;
306 // The other two components must be identically zero (no force, no coupling), and the
307 // accumulated pressure must stay at zero (the projection sees a divergence-free field).
308 double cross = 0;
309 for (int comp = 0; comp < 3; ++comp)
310 if (comp != c.comp)
311 cross = std::fmax(cross, maxAbs(gu[comp]));
312 const double pabs = maxAbs(gp);
313 // The momentum diagonal must be EXACTLY idt + beta on every cell — including the first
314 // inner plane of every block, which is the one that read the ghost.
315 double dlo = gd[0], dhi = gd[0];
316 for (double v : gd) {
317 dlo = std::fmin(dlo, v);
318 dhi = std::fmax(dhi, v);
319 }
320 const double wantDiag = (double)(float)(RHO0 / DT + BETA);
321 const bool diagOk = (dlo == wantDiag) && (dhi == wantDiag);
322 ok = ok && diagOk && spread <= 1e-13 * want && verr <= 1e-13 && cross <= 1e-13 * want &&
323 pabs <= 1e-11;
324 std::printf(
325 " [%-6s np=%d] du=%.3e dp=%.3e (tol %.1e/%.1e) | diag [%.17g, %.17g] "
326 "(want %.17g, worst plane %d) | u spread=%.3e (want 0) value=%.17g "
327 "(want %.17g) cross=%.2e max|P|=%.2e %s\n",
328 c.name, size, du, dp, utol, ptol, dlo, dhi, wantDiag, worstPlane(gd, c.comp), spread,
329 0.5 * (hi + lo), want, cross, pabs, ok ? "OK" : "FAIL");
330 if (!ok)
331 fail = 1;
332 }
334 }
335 if (rank == 0)
336 std::printf("DRAGBETA-GHOST MPI (np=%d): %s\n", size, fail ? "FAIL" : "PASS");
337 }
338 Kokkos::finalize();
339 MPI_Finalize();
340 return fail;
341}
void setAdvection(bool on)
void setPressureGeometry(const std::vector< double > &sdfInner)
std::vector< double > getMomentumDiagonal(int c)
void setField(const std::string &name, const std::vector< double > &v)
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 setPorousConservative(bool on)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setPorousContinuity(bool on)
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 RHO0
static int worstPlane(const std::vector< double > &u, int axis)
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 FORCE
static double analyticVelocity()
static constexpr int STEPS
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr double DT
static constexpr int NX
static void configure(IbmSolver &s, const Config &c, int lnx, int lny, int lnz)
static constexpr double BETA
static constexpr std::size_t GCELLS
static constexpr int NZ
static double mean(const std::vector< double > &a)