flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_varmu_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the VARIABLE-VISCOSITY path (VoF rung V-1, WO-A).
2//
3// Phase 4 (`doc/variable_viscosity_projection.md`, `tests/study/two_layer_couette.py`) was
4// validated host-openmp, single-rank only. The distributed pieces that had never been gated: the
5// "mu" ghost ring (`fillPropGhosts`), the HARMONIC face mean assembled across a rank boundary, and
6// `minMuInner`'s allreduce feeding the rotational term's chi*mu_min coefficient.
7//
8// Two configurations on a 16x32x8 grid — the aligned ORB cuts the LONG y axis at np = 2, 4, which
9// is deliberately the walled/loaded axis of both configurations (WO-F):
10//
11// * `couette-y` — the LITERAL two-layer Couette of tests/study/two_layer_couette.py: plane
12// Couette across y (fixed -y wall, +y lid at U) through a MONOTONE two-layer viscosity stack
13// mu1 | mu2 with a single 10x jump at y = NY/2. The shear stress tau is uniform, so the exact
14// steady Stokes profile is u(y) = tau * int dy/mu — an ANALYTIC, decomposition-independent gate
15// that only the HARMONIC face mean reproduces (the arithmetic mean misses it by O(1%)).
16//
17// WO-A had to ship a SYMMETRIC mu2|mu1|mu2 stack here instead, because `fillPropGhosts` /
18// `fillPorousEpsGhosts` applied their domain-face override only `if (!distributed_)`, leaving
19// the mu ghost on a walled face at its PERIODIC WRAP value in every distributed run. With a
20// monotone stack mu(0) != mu(NY-1), so the wrap value is the WRONG layer and the distributed
21// solver disagreed with the single-rank reference by 2.7e-2 relative ALREADY AT np = 1; the
22// symmetric stack had mu(0) == mu(NY-1) and so hid it. WO-F replaced that guard with the
23// per-face `touchesGlobalFace` ownership test (correct at every np, single-rank byte-identical
24// because the test is always true there), which is exactly what the monotone stack gates.
25//
26// * `per-y` — fully periodic (no domain BC), with the 10x viscosity jump stacked along the CUT
27// axis, so at np = 2 and 4 it sits exactly on a rank boundary and the harmonic face mean there
28// is assembled from an exchanged ghost. Driven by a zero-mean cos(2 pi y / NY) body force in z
29// (a uniform force in a fully periodic box has no steady state). np-consistency gate only — no
30// closed form.
31//
32// WALLED AXIS / MPI (WO-A escalation #1, fixed by WO-F): flow's per-face domain BCs used to be
33// imposed by EVERY rank on its OWN block faces — `applyVelocityBcCompTo`, the flux-openness
34// construction and the pressure MG's per-level boundary openness had no `touchesGlobalFace`
35// ownership test, unlike the scalar-transport BCs (`applyScalarBc`), so a cut walled axis silently
36// split the domain into independent sub-domains. This test now REQUIRES the walled axis to be cut
37// and compares the PRESSURE as well as the velocity — WO-A measured that on a walled column the
38// velocity stays clean (4.5e-17) while the pressure is off by 4.0e+02, i.e. only the pressure sees
39// the defect.
40//
41// Comparison protocol (the `tests/kokkos_mpi` pattern): pointwise against a full-grid single-rank
42// reference on rank 0. np = 1 bit-exact; np > 1 to the MG-PCG reduction-order floor.
43#include <mpi.h>
44
45#include <cmath>
46#include <cstdio>
47#include <cstring>
48#include <Kokkos_Core.hpp>
49#include <string>
50#include <vector>
51
52#include "flow_ibm.hpp"
53#include "peclet/core/common/types.hpp"
54#include "peclet/core/decomp/block_decomposer.hpp"
55
56using peclet::core::IVec;
58
59static constexpr int NX = 16, NY = 32, NZ = 8;
60static constexpr double MU1 = 1.0, MU2 = 0.1, ULID = 1.0, RHO = 1.0, DT = 100.0;
61static constexpr int STEPS_COUETTE = 80, STEPS_PERY = 40;
62static constexpr double FZ = 1e-2;
63static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
64
65struct Config {
66 const char* name;
67 bool couette; // walled two-layer Couette (else the periodic cut-axis sandwich)
68 int steps;
69};
70
71// Both configurations load the CUT y axis: the literal monotone two-layer stack mu1|mu2 (see the
72// header), walled for `couette-y` and periodic for `per-y`.
73static double muAt(const Config&, int, int y, int) {
74 return (y < NY / 2) ? MU1 : MU2;
75}
76static double srcAt(int, int y, int) {
77 return std::cos(2.0 * M_PI * y / NY);
78}
79
80template <class Fn>
81static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
82 std::vector<double> v((std::size_t)lnx * lny * lnz);
83 for (int z = 0; z < lnz; ++z)
84 for (int y = 0; y < lny; ++y)
85 for (int x = 0; x < lnx; ++x)
86 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
87 f(x + ox, y + oy, z + oz);
88 return v;
89}
90
91static void configure(IbmSolver& s, const Config& c, int ox, int oy, int oz, int lnx, int lny,
92 int lnz) {
93 s.setRho(RHO);
94 s.setMu(MU1);
95 s.setDt(DT);
96 s.setAdvection(false); // Stokes: the analytic profile is the steady Stokes solution
97 s.setPressurePcg(true, 200, 1e-10);
99 if (c.couette) {
100 s.setDomainBc(2, 1, 0.0, 0.0, 0.0); // -y fixed wall
101 s.setDomainBc(3, 2, ULID, 0.0, 0.0); // +y lid at U
102 }
103 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
104 s.addField("mu");
105 s.setField("mu", blockOf([&](int x, int y, int z) { return muAt(c, x, y, z); }, ox, oy, oz, lnx,
106 lny, lnz));
107 s.setPropertyMode(true, /*harmonic=*/true);
108 s.exchangeField("mu");
109 if (!c.couette) {
110 s.addField("src");
111 s.setField("src", blockOf(srcAt, ox, oy, oz, lnx, lny, lnz));
112 s.exchangeField("src");
113 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "src", "", {0.0, FZ});
114 // WO-F carried an explicit seed of the SAME field here purely to fill its GHOST ring, because
115 // nothing in `step()` exchanged a `force_*` field. WO-G fixed that (`fillCellForceGhosts`), so
116 // the seed is gone. It was doubly inert in THIS test anyway: with variable mu but constant
117 // density the RHS kernel is `buildRhsForced`, which reads the CELL value `fb(i)` and never
118 // `fb(i-strd)` — only `buildRhsVar` (varRho / eps-conservative porous) face-interpolates the
119 // force and therefore reads the ghost. The WO-G gate for that is `test_bodyforce_ghost_mpi`.
120 }
121}
122
123static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
124 int lnx, int lny, int lnz, int rank, int size) {
125 std::vector<double> global;
126 if (rank == 0)
127 global.assign(GCELLS, 0.0);
128 for (int r = 0; r < size; ++r) {
129 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
130 if (r == 0) {
131 if (rank == 0)
132 for (int z = 0; z < lnz; ++z)
133 for (int y = 0; y < lny; ++y)
134 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
135 (std::size_t)(z + oz) * NX * NY],
136 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
137 (std::size_t)lnx * sizeof(double));
138 continue;
139 }
140 if (rank == r) {
141 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
142 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
143 } else if (rank == 0) {
145 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
146 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
148 for (int z = 0; z < meta[5]; ++z)
149 for (int y = 0; y < meta[4]; ++y)
150 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
151 (std::size_t)(z + meta[2]) * NX * NY],
152 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
153 (std::size_t)meta[3] * sizeof(double));
154 }
155 }
156 return global;
157}
158
159static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
160 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
161 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
162 // a drained open-boundary run). A non-finite difference must fail, so return it.
163 double m = 0;
164 for (std::size_t i = 0; i < b.size(); ++i) {
165 const double d = std::fabs(a[i] - b[i]);
166 if (!(d == d))
167 return d; // NaN
168 m = std::fmax(m, d);
169 }
170 return m;
171}
172static double maxAbs(const std::vector<double>& a) {
173 double m = 0;
174 for (double v : a)
175 m = std::fmax(m, std::fabs(v));
176 return m;
177}
178
179// Exact steady Stokes Couette through the monotone two-layer mu1|mu2 stack: tau is uniform, so
180// u(y) = tau * integral(dy/mu) and U = tau*(0.5/mu1 + 0.5/mu2).
181static double couetteExact(double yc) {
182 const double tau = ULID / (0.5 / MU1 + 0.5 / MU2);
183 if (yc < 0.5)
184 return tau * yc / MU1;
185 return tau * 0.5 / MU1 + tau * (yc - 0.5) / MU2;
186}
187static double couetteError(const std::vector<double>& u) {
188 double e = 0;
189 for (int y = 0; y < NY; ++y) {
190 const std::size_t i =
191 (std::size_t)(NX / 2) + (std::size_t)y * NX + (std::size_t)(NZ / 2) * NX * NY;
192 e = std::fmax(e, std::fabs(u[i] - couetteExact((y + 0.5) / NY)) / ULID);
193 }
194 return e;
195}
196
197int main(int argc, char** argv) {
198 MPI_Init(&argc, &argv);
199 Kokkos::initialize(argc, argv);
200 int fail = 0;
201 {
202 int rank = 0, size = 1;
205
206 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
207 auto blk = dec.block(rank);
208 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
209 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
210 const int gn[3] = {NX, NY, NZ};
211 bool cut[3] = {false, false, false};
212 for (const auto& sz : dec.sizes())
213 for (int a = 0; a < 3; ++a)
214 if ((int)sz[a] != gn[a])
215 cut[a] = true;
216 if (rank == 0)
217 std::printf("VARMU MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s\n", size, NX,
218 NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "");
219
220 const Config configs[] = {{"couette-y", true, STEPS_COUETTE}, {"per-y", false, STEPS_PERY}};
221
222 for (const Config& c : configs) {
223 // WO-F: the walled/loaded y axis must be CUT — that is the case the domain-BC and
224 // property-ghost ownership tests exist for.
225 if (size > 1 && !cut[1]) {
226 if (rank == 0)
227 std::printf(
228 " [%-9s np=%d] FAIL — the decomposition does NOT cut the walled y axis; "
229 "this test exists to gate the domain-BC / property-ghost rank ownership on "
230 "a CUT walled axis (WO-F)\n",
231 c.name, size);
232 fail = 1;
233 continue;
234 }
236 sd.initMpi(dec, MPI_COMM_WORLD);
237 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
238 for (int it = 0; it < c.steps; ++it)
239 sd.step();
240 std::vector<double> gu[3];
241 for (int comp = 0; comp < 3; ++comp)
242 gu[comp] = gatherGlobal(sd.getVelocity(comp), ox, oy, oz, lnx, lny, lnz, rank, size);
243 // WO-F: the PRESSURE is the field that exposes a non-rank-aware domain BC (the velocity of a
244 // split domain stays clean); gate it too.
245 const std::vector<double> gp =
246 gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
247
248 if (rank == 0) {
249 IbmSolver ref(NX, NY, NZ);
250 configure(ref, c, 0, 0, 0, NX, NY, NZ);
251 for (int it = 0; it < c.steps; ++it)
252 ref.step();
253 double du = 0, umag = 0;
254 for (int comp = 0; comp < 3; ++comp) {
255 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
256 umag = std::fmax(umag, maxAbs(ref.getVelocity(comp)));
257 }
258 const double rel = du / (umag + 1e-300);
259 const double dp = maxAbsDiff(gp, ref.getPressure());
260 const double pmag = maxAbs(ref.getPressure());
261 // np=1 bit-exact; np>1 the MPI reduction-order floor. The pressure needs an ABSOLUTE
262 // floor as well as a relative one: this is a pure-shear Stokes flow, so |P| itself is
263 // ~1e-8 (numerical zero) and a purely relative gate would be gating round-off. The
264 // *velocity* is the strong gate here (the analytic Couette across the cut walled axis);
265 // the strong pressure gate lives in test_vardensity_mpi's hydrostatic column, where
266 // |P| = O(800) and the pre-WO-F defect showed up as 4.0e+02.
267 const double utol = (size == 1) ? 0.0 : std::fmax(1e-15, 1e-11 * umag);
268 const double ptol = (size == 1) ? 0.0 : std::fmax(1e-14, 1e-11 * pmag);
269 double aerr = -1;
270 bool ok = du <= utol && dp <= ptol;
271 if (c.couette) {
272 aerr = couetteError(gu[0]);
273 ok = ok && aerr < 5e-3; // the harmonic-mean gate (arithmetic misses by O(1%))
274 }
275 char extra[64] = "";
276 if (c.couette)
277 std::snprintf(extra, sizeof(extra), " analytic err=%.4f%%", aerr * 100.0);
278 std::printf(
279 " [%-9s np=%d] du=%.3e (rel %.2e, tol %.1e) | dp=%.3e (|P|=%.2e, tol %.1e)"
280 "%s %s\n",
281 c.name, size, du, rel, utol, dp, pmag, ptol, extra, ok ? "OK" : "FAIL");
282 if (!ok)
283 fail = 1;
284 }
286 }
287 if (rank == 0)
288 std::printf("VARMU MPI (np=%d): %s\n", size, fail ? "FAIL" : "PASS");
289 }
290 Kokkos::finalize();
291 MPI_Finalize();
292 return fail;
293}
void setDomainBc(int face, int type, double vx, double vy, double vz)
void setPropertyMode(bool variable, bool harmonic)
CCField addField(const std::string &name)
void exchangeField(const std::string &name)
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 setVelocityIterations(int it)
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 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 std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
static constexpr double FZ
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 muAt(const Config &, int, int y, int)
static double couetteError(const std::vector< double > &u)
static double srcAt(int, int y, int)
static constexpr double RHO
static constexpr double MU1
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr int STEPS_PERY
static constexpr double DT
static constexpr double ULID
static double couetteExact(double yc)
static constexpr int NX
static constexpr int STEPS_COUETTE
static constexpr std::size_t GCELLS
static constexpr double MU2
static constexpr int NZ
static void configure(IbmSolver &s, const Config &c, int ox, int oy, int oz, int lnx, int lny, int lnz)