flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_balanced_force_mpi.cpp
Go to the documentation of this file.
1// flow — multi-rank validation of the balanced-force projection (doc/collocated_varrho_forces.md
2// §4.6, §8 WO-P3, gate G5-ON), on BOTH grids.
3//
4// The option adds, once per step, a second solve of the projection's own operator whose right-hand
5// side is the constraint divergence of the face force c*beta, and a split P += X - P_b, P_b = X.
6// Under a decomposition that touches: the face force's high-side plane (built on this rank from
7// depth-1 ghosts of the cell force, the colour/curvature and rho), the global max|b| == 0 test (an
8// allreduce), the warm start from the registered field "p_balanced", and redistribute, which must
9// carry "p_balanced" with the rest of the registry.
10//
11// Configurations on 16x16x32 (the aligned ORB cuts the long z axis at np = 2 and 4, the axis that
12// carries the walls, the stratification and the interface):
13// * `colo-hydro` — SolverColocated, frozen colour, ratio 1000, walls at +-z (the option is the
14// V8 DEFAULT here: not set explicitly);
15// * `stag-hydro` — the same column on the staggered Solver, option ON;
16// * `stag-vof` — staggered, enable_vof, ratio 10, a uniform x drive, option ON;
17// * `colo-vof-rebalance` — SolverColocated, enable_vof, ratio 10 (V8 default ON), Stokes; after 3
18// steps
19// `rebalanceByWeights` moves the partition (a weight 4 in the lower half), then 5 more steps.
20// The single-rank reference never rebalances.
21//
22// Protocol (the tests/kokkos_mpi pattern): gather the distributed fields to rank 0 and compare with
23// a full-grid single-rank reference. np = 1 is bitwise; np > 1 lands on the allreduce reduction-
24// order floor: u 1e-11, P and P_b 1e-9, C 1e-11 relative (the scale of a rest state is g*dt, see
25// test_vof_collocated_mpi). Iterations, BOTH counts: exact at np = 1; above it the cost envelope of
26// test_vof_collocated_mpi (worst step within one iteration of the reference's worst, total within
27// one per step) -- a warm-started solve of a rest state works on round-off, whose per-step count is
28// a random walk across decompositions (measured: +-2 per step with identical totals).
29#include <mpi.h>
30
31#include <cmath>
32#include <cstdio>
33#include <cstring>
34#include <Kokkos_Core.hpp>
35#include <string>
36#include <vector>
37
38#include "flow_ibm.hpp"
39#include "peclet/core/common/types.hpp"
40#include "peclet/core/decomp/block_decomposer.hpp"
41
44
45static constexpr int NX = 16, NY = 16, NZ = 32;
46static constexpr double GRAV = 0.1, DT = 1.0, RHO0 = 1.0;
47static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
48
49struct Config {
50 const char* name;
51 double ratio;
52 bool walls; // hydrostatic (frozen colour) vs the coupled VoF run
53 int steps; // total steps
54 int rebalance; // step index after which the partition moves (-1: never)
55 bool explicitOn; // call setBalancedForceProjection(true) (false: rely on the solver default)
56 bool advect; // momentum advection
57};
58
59static double colourAt(int, int, int z) {
60 return (z < NZ / 2) ? 1.0 : 0.0;
61}
62
63template <class Fn>
64static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
65 std::vector<double> v((std::size_t)lnx * lny * lnz);
66 for (int z = 0; z < lnz; ++z)
67 for (int y = 0; y < lny; ++y)
68 for (int x = 0; x < lnx; ++x)
69 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
70 f(x + ox, y + oy, z + oz);
71 return v;
72}
73
74template <class S>
75static void configure(S& s, const Config& c, int ox, int oy, int oz, int lnx, int lny, int lnz) {
76 s.setRho(RHO0);
77 s.setMu(c.walls ? 0.1 : 0.05);
78 s.setDt(DT);
79 s.setAdvection(c.advect);
80 if (c.walls) {
81 s.setDomainBc(4, 1, 0, 0, 0);
82 s.setDomainBc(5, 1, 0, 0, 0);
83 } else {
84 // a z component, so the face force c*beta = rho0 f_z/rho_f varies across the interface and
85 // the balanced-force solve has a non-zero right-hand side (an x drive alone is divergence-free)
86 s.setBodyForce(1e-3, 0, 5e-4);
87 }
88 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
89 const auto c0 = blockOf(colourAt, ox, oy, oz, lnx, lny, lnz);
90 if (c.walls) {
91 s.addField("C");
92 s.setField("C", c0);
93 s.exchangeField("C");
94 } else {
95 s.enableVof();
96 s.setVof(c0);
97 }
98 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, c.ratio - 1.0});
99 if (c.walls)
100 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -GRAV});
101 if (c.explicitOn)
102 s.setBalancedForceProjection(true);
103}
104
105static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
106 int lnx, int lny, int lnz, int rank, int size) {
107 std::vector<double> global;
108 if (rank == 0)
109 global.assign(GCELLS, 0.0);
110 for (int r = 0; r < size; ++r) {
111 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
112 if (r == 0) {
113 if (rank == 0)
114 for (int z = 0; z < lnz; ++z)
115 for (int y = 0; y < lny; ++y)
116 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
117 (std::size_t)(z + oz) * NX * NY],
118 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
119 (std::size_t)lnx * sizeof(double));
120 continue;
121 }
122 if (rank == r) {
123 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
124 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
125 } else if (rank == 0) {
126 MPI_Recv(meta, 6, MPI_INT, r, 100 + r, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
127 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
128 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
129 MPI_STATUS_IGNORE);
130 for (int z = 0; z < meta[5]; ++z)
131 for (int y = 0; y < meta[4]; ++y)
132 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
133 (std::size_t)(z + meta[2]) * NX * NY],
134 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
135 (std::size_t)meta[3] * sizeof(double));
136 }
137 }
138 return global;
139}
140
141static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
142 double m = 0; // NaN-propagating (WO-R2)
143 for (std::size_t i = 0; i < b.size(); ++i) {
144 const double d = std::fabs(a[i] - b[i]);
145 if (!(d == d))
146 return d;
147 m = std::fmax(m, d);
148 }
149 return m;
150}
151static double maxAbs(const std::vector<double>& a) {
152 double m = 0;
153 for (double v : a)
154 m = std::fmax(m, std::fabs(v));
155 return m;
156}
157
158template <class S>
159static int runCase(const Config& c, const peclet::core::decomp::BlockDecomposer<3>& dec, int rank,
160 int size) {
161 auto blk = dec.block(rank);
162 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
163 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
164 S sd(lnx, lny, lnz);
165 sd.initMpi(dec, MPI_COMM_WORLD);
166 configure(sd, c, ox, oy, oz, lnx, lny, lnz);
167 std::vector<long> pd, bd;
168 bool moved = false;
169 for (int it = 0; it < c.steps; ++it) {
170 sd.step();
171 pd.push_back(sd.lastPressureIterations());
172 bd.push_back(sd.lastBalancedForceIterations());
173 if (it == c.rebalance) {
174 std::vector<peclet::core::Real> w(GCELLS, 1.0);
175 for (int z = 0; z < NZ / 2; ++z)
176 for (int y = 0; y < NY; ++y)
177 for (int x = 0; x < NX; ++x)
178 w[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] = 4.0;
179 const auto o0 = sd.blockOrigin();
180 const long n0 = sd.nx() * sd.ny() * sd.nz();
181 sd.rebalanceByWeights(w);
182 const auto o1 = sd.blockOrigin();
183 int mv = (o0 != o1 || n0 != sd.nx() * sd.ny() * sd.nz()) ? 1 : 0, gmv = 0;
184 MPI_Allreduce(&mv, &gmv, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
185 moved = gmv != 0;
186 }
187 }
188 const auto o = sd.blockOrigin();
189 const int px = o[0], py = o[1], pz = o[2];
190 const int qx = (int)sd.nx(), qy = (int)sd.ny(), qz = (int)sd.nz();
191 std::vector<double> gu[3];
192 for (int comp = 0; comp < 3; ++comp)
193 gu[comp] = gatherGlobal(sd.getVelocity(comp), px, py, pz, qx, qy, qz, rank, size);
194 const auto gp = gatherGlobal(sd.getPressure(), px, py, pz, qx, qy, qz, rank, size);
195 const auto gb = gatherGlobal(sd.getField("p_balanced"), px, py, pz, qx, qy, qz, rank, size);
196 const auto gc = gatherGlobal(sd.getField("C"), px, py, pz, qx, qy, qz, rank, size);
197 int fail = 0;
198 if (rank == 0) {
199 S ref(NX, NY, NZ);
200 configure(ref, c, 0, 0, 0, NX, NY, NZ);
201 std::vector<long> pr, br;
202 for (int it = 0; it < c.steps; ++it) {
203 ref.step();
204 pr.push_back(ref.lastPressureIterations());
205 br.push_back(ref.lastBalancedForceIterations());
206 }
207 double du = 0, su = 0;
208 for (int comp = 0; comp < 3; ++comp) {
209 du = std::fmax(du, maxAbsDiff(gu[comp], ref.getVelocity(comp)));
210 su = std::fmax(su, maxAbs(ref.getVelocity(comp)));
211 }
212 const double dp = maxAbsDiff(gp, ref.getPressure()), sp = maxAbs(ref.getPressure());
213 const double db = maxAbsDiff(gb, ref.getField("p_balanced"));
214 const double dc = maxAbsDiff(gc, ref.getField("C"));
215 long dbit = 0, bsum = 0, maxd = 0, maxr = 0, sumd = 0, sumr = 0;
216 for (std::size_t k = 0; k < pd.size(); ++k) {
217 dbit = std::max(dbit, std::labs(bd[k] - br[k]));
218 bsum += br[k];
219 maxd = std::max(maxd, pd[k]);
220 maxr = std::max(maxr, pr[k]);
221 sumd += pd[k];
222 sumr += pr[k];
223 }
224 // The balanced-force count gets the SAME cost-envelope rule as the main count. Before WO-P5
225 // (warm start, initial-residual stop) the rest-state columns parted by up to 2 per step with
226 // identical totals -- the solve worked on round-off. With the increment solve and its
227 // full-RHS stop the rest state skips after step 1 (14 iterations in 20 steps, identical at
228 // np 1/2/4), so the envelope is now slack rather than load-bearing. np = 1 stays exact.
229 long bmaxd = 0, bmaxr = 0, bsumd = 0;
230 for (std::size_t k = 0; k < bd.size(); ++k) {
231 bmaxd = std::max(bmaxd, bd[k]);
232 bmaxr = std::max(bmaxr, br[k]);
233 bsumd += bd[k];
234 }
235 const bool bfOk = (size == 1) ? (dbit == 0)
236 : (bmaxd <= bmaxr + 1 &&
237 std::labs(bsumd - bsum) <= static_cast<long>(bd.size()));
238 const long dpit = std::labs(sumd - sumr);
239 const bool pOk =
240 (size == 1) ? (pd == pr) : (maxd <= maxr + 1 && dpit <= static_cast<long>(pd.size()));
241 const double uRef = std::fmax(su, c.walls ? GRAV * DT : 1e-3 * DT);
242 const double tolU = (size == 1) ? 0.0 : 1e-11 * uRef;
243 const double tolP = (size == 1) ? 0.0 : 1e-9 * std::fmax(sp, 1e-12);
244 const double tolC = (size == 1) ? 0.0 : 1e-11;
245 const bool moveOk = c.rebalance < 0 || size == 1 || moved;
246 const bool ok = du <= tolU && dp <= tolP && db <= tolP && dc <= tolC && bfOk && pOk && moveOk;
247 std::printf(
248 " [%-18s np=%d] du %.3e (|u| %.3e) dP %.3e dP_b %.3e (|P| %.3e) dC %.3e "
249 "bf-iters sum %ld d/step %ld p-iters max %ld/%ld sum %ld/%ld%s %s\n",
250 c.name, size, du, su, dp, db, sp, dc, bsum, dbit, maxd, maxr, sumd, sumr,
251 c.rebalance >= 0 ? (moved ? " (partition MOVED)" : " (partition did NOT move)") : "",
252 ok ? "OK" : "*** FAIL ***");
253 if (!ok)
254 fail = 1;
255 }
256 MPI_Bcast(&fail, 1, MPI_INT, 0, MPI_COMM_WORLD);
257 return fail;
258}
259
260int main(int argc, char** argv) {
261 MPI_Init(&argc, &argv);
262 Kokkos::initialize(argc, argv);
263 int fail = 0;
264 {
265 int rank = 0, size = 1;
266 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
267 MPI_Comm_size(MPI_COMM_WORLD, &size);
268 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
269 if (rank == 0)
270 std::printf("BALANCED-FORCE PROJECTION MPI np=%d grid %dx%dx%d\n", size, NX, NY, NZ);
271 fail |= runCase<Colo>({"colo-hydro", 1000.0, true, 20, -1, false, false}, dec, rank, size);
272 fail |= runCase<Stag>({"stag-hydro", 1000.0, true, 20, -1, true, false}, dec, rank, size);
273 fail |= runCase<Stag>({"stag-vof", 10.0, false, 10, -1, true, true}, dec, rank, size);
274 // Stokes: the collocated advecting face field uf_ is not carried by redistribute (a
275 // pre-existing gap, off this option: with the option OFF an np = 1 "move" already differs by
276 // 5e-8 in u when advection is on), so the move is gated on the option's own state.
277 fail |= runCase<Colo>({"colo-vof-rebalance", 10.0, false, 8, 2, false, false}, dec, rank, size);
278 if (rank == 0)
279 std::printf("%s\n", fail ? "FAILED" : "all balanced-force MPI gates passed");
280 }
281 Kokkos::finalize();
282 MPI_Finalize();
283 return fail;
284}
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 double colourAt(int, int, int z)
static constexpr int NY
static void configure(S &s, const Config &c, int ox, int oy, int oz, int lnx, int lny, int lnz)
static constexpr double GRAV
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
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 int runCase(const Config &c, const peclet::core::decomp::BlockDecomposer< 3 > &dec, int rank, int size)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr double DT
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ