flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_bodyforce_ghost_mpi.cpp
Go to the documentation of this file.
1// flow — the per-cell BODY-FORCE ghost ring (VoF blocker WO-G).
2//
3// The defect this test exists for. `applyClosure` (`property_closures.hpp`) writes the INNER cells
4// only — its own comment says "ghosts untouched — refilled by the field's own exchange" — and an
5// external CFD-DEM writer (`field_view` + `exchange_field_add`) likewise leaves the ghost ring
6// holding its deposit residue rather than the owner's value. But NOTHING used to exchange
7// `force_x/y/z`: `fillPropGhosts` was called for rho / mu / drag_beta / eps and never for the force
8// fields, which are zero-initialised at registration. `buildRhsVar` face-interpolates the cell
9// force
10//
11// f_f(i) = 0.5*(fb(i) + fb(i - stride_c))
12//
13// so on the FIRST INNER PLANE of every block (axis c) the face force read exactly HALF of the
14// intended value — at every rank boundary under MPI, and single-rank at the periodic wrap plane.
15// The fix gives the force fields the same per-face-rank-aware ghost fill the other cell properties
16// get (`fillCellForceGhosts` -> `fillPropGhosts`: halo/periodic base + Neumann copy on domain-BC
17// faces, which is what keeps f_f/rho_f == the intended acceleration at a boundary, since rho's own
18// ghost is a Neumann copy).
19//
20// The gates are ABSOLUTE physics, not a distributed-vs-reference comparison — deliberately. The
21// single-rank reference carries the identical defect, so `du = 0` proves nothing here: `per-z` and
22// `per-x` at np = 1 are exactly the single-rank periodic wrap-plane variant the work order asks
23// for, and they FAIL before the fix. Configurations:
24//
25// * `per-z` — fully periodic, UNIFORM rho, UNIFORM force_z from a closure, inviscid, no
26// advection. From rest the momentum solve is (rho/dt) w = (rho/dt) w^n + f_f, so
27// w must be EXACTLY n*F*dt/rho in every cell after n steps; a uniform w is
28// divergence-free so the projection returns it untouched. The gate is the spread
29// max(w) - min(w) — i.e. "the face body force is uniform", read out through the one
30// quantity that is a pure function of it.
31// * `per-x` — the same, force_x written by an EXTERNAL writer (`setField`, no `exchangeField`)
32// rather than a closure: the CFD-DEM feedback path, whose ghost ring holds the
33// deposit residue. Different axis (stride 1) than `per-z` (stride ex*ey).
34// * `walls-z` — UNIFORM rho hydrostatic column, walls +-z, gravity closure force_z = -g*rho, z
35// CUT. This is the WHY-DID-THE-ACID-TEST-PASS control: the wall-normal velocity on
36// the boundary plane is PINNED by the Dirichlet BC (`bcVelocityComp`, comp == axis:
37// `at(bf) = wall`) and that face's flux openness is 0, so the halved force at a
38// GLOBAL wall face is masked completely — this configuration passes at np = 1 even
39// with the defect present, which is exactly why WO-A's hydrostatic acid test read
40// 2.75e-17 throughout. An INTERIOR rank boundary has no such pin, so at np = 2/4
41// with z cut the same configuration breaks in the PRESSURE (dP/dz) while the
42// velocity canary stays clean — the WO-F signature.
43//
44// np = 1 is additionally compared bitwise against a full-grid single-rank reference; np > 1 lands
45// on the usual MPI reduction-order floor (Chebyshev bound estimation + removeMean go through an
46// MPI_SUM whose summation order depends on the rank count) — see `test_vardensity_mpi.cpp`.
47#include <mpi.h>
48
49#include <cmath>
50#include <cstdio>
51#include <cstring>
52#include <Kokkos_Core.hpp>
53#include <vector>
54
55#include "flow_ibm.hpp"
56#include "peclet/core/common/types.hpp"
57#include "peclet/core/decomp/block_decomposer.hpp"
58
60
61static constexpr int NX = 16, NY = 16, NZ = 32, STEPS = 4;
62static constexpr double RHO0 = 1.0, DT = 1.0, FORCE = 0.25, GRAV = 0.1;
63static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
64
65struct Config {
66 const char* name;
67 int comp; // forced velocity component (0=x, 1=y, 2=z)
68 bool walls; // hydrostatic column (walls on the comp axis) vs fully periodic
69 bool closure; // force written by a closure vs by an external setField writer
70 bool needCut; // the forced axis MUST be cut at np > 1 (loud coverage loss if it stops being)
71};
72
73static void configure(IbmSolver& s, const Config& c, int lnx, int lny, int lnz) {
74 static const char* fn[3] = {"force_x", "force_y", "force_z"};
75 s.setRho(RHO0);
76 s.setMu(0.0); // inviscid: the momentum solve is exactly u^n + f_f*dt/rho_f
77 s.setDt(DT);
78 s.setAdvection(false);
79 if (c.walls) {
80 s.setDomainBc(2 * c.comp + 0, 1, 0, 0, 0);
81 s.setDomainBc(2 * c.comp + 1, 1, 0, 0, 0);
82 }
83 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
84 s.setDensityMode(true); // uniform rho field == rho0; routes the RHS through buildRhsVar
85 if (c.closure)
87 c.walls ? std::vector<double>{0.0, -GRAV} : std::vector<double>{FORCE, 0.0});
88 else {
89 s.enableCellForce(); // external-writer path (CFD-DEM): field_view/setField, no exchange
90 s.setField(fn[c.comp], std::vector<double>((std::size_t)lnx * lny * lnz, FORCE));
91 }
92}
93
94// Gather per-rank inner blocks (x-fastest) into the global field on rank 0.
95static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
96 int lnx, int lny, int lnz, int rank, int size) {
97 std::vector<double> global;
98 if (rank == 0)
99 global.assign(GCELLS, 0.0);
100 for (int r = 0; r < size; ++r) {
101 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
102 if (r == 0) {
103 if (rank == 0)
104 for (int z = 0; z < lnz; ++z)
105 for (int y = 0; y < lny; ++y)
106 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
107 (std::size_t)(z + oz) * NX * NY],
108 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
109 (std::size_t)lnx * sizeof(double));
110 continue;
111 }
112 if (rank == r) {
113 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
114 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
115 } else if (rank == 0) {
117 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
118 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
120 for (int z = 0; z < meta[5]; ++z)
121 for (int y = 0; y < meta[4]; ++y)
122 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
123 (std::size_t)(z + meta[2]) * NX * NY],
124 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
125 (std::size_t)meta[3] * sizeof(double));
126 }
127 }
128 return global;
129}
130
131static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
132 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
133 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
134 // a drained open-boundary run). A non-finite difference must fail, so return it.
135 double m = 0;
136 for (std::size_t i = 0; i < b.size(); ++i) {
137 const double d = std::fabs(a[i] - b[i]);
138 if (!(d == d))
139 return d; // NaN
140 m = std::fmax(m, d);
141 }
142 return m;
143}
144static double maxAbs(const std::vector<double>& a) {
145 double m = 0;
146 for (double v : a)
147 m = std::fmax(m, std::fabs(v));
148 return m;
149}
150
151// Hydrostatic gradient along the walled column at (x, y) = (NX/2, NY/2), uniform rho.
152// The plane index of the worst offender is reported: with the defect it is the first inner plane of
153// a rank block, and never the global wall (which the Dirichlet pin masks).
154static double pressureGradientError(const std::vector<double>& p, int* worstPlane) {
155 double perr = 0;
156 *worstPlane = -1;
157 for (int z = 1; z < NZ; ++z) {
158 const std::size_t i0 =
159 (std::size_t)(NX / 2) + (std::size_t)(NY / 2) * NX + (std::size_t)(z - 1) * NX * NY;
160 const double e =
161 std::fabs((p[i0 + (std::size_t)NX * NY] - p[i0]) + GRAV * RHO0) / (GRAV * RHO0);
162 if (e > perr) {
163 perr = e;
164 *worstPlane = z;
165 }
166 }
167 return perr;
168}
169
170int main(int argc, char** argv) {
171 MPI_Init(&argc, &argv);
172 Kokkos::initialize(argc, argv);
173 int fail = 0;
174 {
175 int rank = 0, size = 1;
178
179 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
180 auto blk = dec.block(rank);
181 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
182 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
183 const int gn[3] = {NX, NY, NZ};
184 bool cut[3] = {false, false, false};
185 for (const auto& sz : dec.sizes())
186 for (int a = 0; a < 3; ++a)
187 if ((int)sz[a] != gn[a])
188 cut[a] = true;
189 if (rank == 0)
190 std::printf("BODYFORCE-GHOST MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n",
191 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "",
192 cut[2] ? "z" : "");
193
194 // z is the axis this grid's ORB cuts (np=2 -> z, np=4 -> x and z), so the two z configurations
195 // require the cut; `per-x` runs regardless — at np=2 its rank boundary is the PERIODIC WRAP
196 // plane in x, which is the single-rank half of the same defect and an equally hard gate.
197 const Config configs[] = {{"per-z", 2, false, true, true},
198 {"per-x", 0, false, false, false},
199 {"walls-z", 2, true, true, true}};
200
201 for (const Config& c : configs) {
202 if (c.needCut && size > 1 && !cut[c.comp]) {
203 if (rank == 0)
204 std::printf(
205 " [%-7s np=%d] FAIL — the decomposition does NOT cut the forced axis %d; "
206 "this test exists to gate the force ghost ACROSS a rank boundary (WO-G)\n",
207 c.name, size, c.comp);
208 fail = 1;
209 continue;
210 }
212 sd.initMpi(dec, MPI_COMM_WORLD);
213 configure(sd, c, lnx, lny, lnz);
214 for (int it = 0; it < STEPS; ++it)
215 sd.step();
216
217 std::vector<double> gu[3];
218 for (int comp = 0; comp < 3; ++comp)
219 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
220 const std::vector<double> gp =
221 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
222
223 if (rank == 0) {
224 IbmSolver ref(NX, NY, NZ);
225 configure(ref, c, NX, NY, NZ);
226 for (int it = 0; it < STEPS; ++it)
227 ref.step();
228 double du = 0, umag = 0;
229 for (int comp = 0; comp < 3; ++comp) {
230 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
231 umag = std::fmax(umag, maxAbs(ref.getVelocity(comp)));
232 }
233 const double dp = maxAbsDiff(gp, ref.getPressure());
234 const double pmag = maxAbs(ref.getPressure());
235 const double utol = (size == 1) ? 0.0 : std::fmax(1e-15, 1e-11 * umag);
236 const double ptol = (size == 1) ? 0.0 : std::fmax(1e-12, 1e-11 * pmag);
237 bool ok = du <= utol && dp <= ptol;
238
239 char extra[160] = "";
240 if (!c.walls) {
241 // THE gate: the face body force is uniform <=> the accumulated velocity is uniform.
242 const std::vector<double>& uf = gu[c.comp];
243 double lo = uf[0], hi = uf[0];
244 for (double v : uf) {
245 lo = std::fmin(lo, v);
246 hi = std::fmax(hi, v);
247 }
248 const double want = STEPS * FORCE * DT / RHO0;
249 const double spread = hi - lo, verr = std::fabs(0.5 * (hi + lo) - want) / want;
250 // The other two components must be identically zero (no force, no coupling).
251 double cross = 0;
252 for (int comp = 0; comp < 3; ++comp)
253 if (comp != c.comp)
254 cross = std::fmax(cross, maxAbs(gu[comp]));
255 ok = ok && spread <= 1e-13 * want && verr <= 1e-13 && cross <= 1e-13 * want;
256 std::snprintf(extra, sizeof(extra),
257 " | u_f spread=%.3e (want 0) value=%.17g (want %.17g) cross=%.2e", spread,
258 0.5 * (hi + lo), want, cross);
259 } else {
260 int worst = -1;
261 const double perr = pressureGradientError(gp, &worst);
262 const double umax = std::fmax(maxAbs(gu[0]), std::fmax(maxAbs(gu[1]), maxAbs(gu[2])));
263 ok = ok && umax < 1e-13 && perr < 1e-11;
264 std::snprintf(extra, sizeof(extra), " | max|u|=%.2e dP/dz err=%.3e (worst plane z=%d)",
265 umax, perr, worst);
266 }
267 std::printf(" [%-7s np=%d] du=%.3e dp=%.3e (tol %.1e/%.1e)%s %s\n", c.name, size, du, dp,
268 utol, ptol, extra, ok ? "OK" : "FAIL");
269 if (!ok)
270 fail = 1;
271 }
273 }
274 if (rank == 0)
275 std::printf("BODYFORCE-GHOST MPI (np=%d): %s\n", size, fail ? "FAIL" : "PASS");
276 }
277 Kokkos::finalize();
278 MPI_Finalize();
279 return fail;
280}
void setDomainBc(int face, int type, double vx, double vy, double vz)
void setAdvection(bool on)
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
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 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 constexpr int NY
static constexpr double GRAV
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 constexpr double FORCE
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 std::size_t GCELLS
static constexpr int NZ
static double pressureGradientError(const std::vector< double > &p, int *worstPlane)
static int worstPlane(const std::vector< double > &u, int axis)