flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_blocks_ns_mpi.cpp
Go to the documentation of this file.
1// flow — VoF Part III rung W2 (WO-W12) gate 5: the BLOCK container coupled to Navier-Stokes,
2// distributed.
3//
4// The container itself is gated bitwise at np 1/2/4/8 by `test_vof_blocks_mpi` (rung W0/W1, a
5// standalone harness). What is gated HERE is the coupling: the union colour driving the closures,
6// the per-block curvature cascade, and the block CSF face force scattered UNPACK_SUM into the
7// momentum RHS, all inside a real `IbmSolver::step()`.
8//
9// B1 the coupled step against a full-grid single-rank reference — u, v, w, P and the union C
10// after 12 steps of a real two-phase run with the block CSF on. np = 1 must be BITWISE; np > 1
11// carries the documented reduction-order floor of the pressure driver's allreduces
12// (`doc/variable_density_projection.md` §3.1), so it is a tolerance comparison.
13// B2 the per-marker volumes agree with the single-rank run to the reduction floor, and the
14// markers OVERLAP (the union carries less liquid than the markers do) -- the property that
15// makes the whole container worth having, now measured through the NS coupling.
16//
17// Grid 24x24x48 with two bubbles stacked along z: the aligned ORB cuts the long z axis at np = 2
18// and 4, and the bubbles are placed so the cut passes THROUGH one of them -- so a block's gather
19// draws from several owners, its scatter lands on several, and its CSF force is summed across a
20// rank boundary. Masters are round-robin by block id, so at np >= 2 the two markers have different
21// masters.
22#include <mpi.h>
23
24#include <cmath>
25#include <cstdlib>
26#include <cstdio>
27#include <cstring>
28#include <Kokkos_Core.hpp>
29#include <vector>
30
31#include "flow_ibm.hpp"
32#include "peclet/core/common/types.hpp"
33#include "peclet/core/decomp/block_decomposer.hpp"
34
37
38static constexpr int NX = 24, NY = 24, NZ = 48;
39static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
40// ratio 10, the rung's rated regime with motion; sigma chosen so the capillary dt is not absurd.
41static constexpr double RHO_L = 10.0, RHO_G = 1.0, MU_L = 0.5, MU_G = 0.05, SIGMA = 4.0;
42static constexpr double RB = 4.5, GRAV = 2.0e-3;
43// the two markers, stacked along the CUT axis; the ORB's np=2 cut at z = 24 passes through the
44// upper one's band.
45static const double CX[2] = {NX / 2.0 + 0.13, NX / 2.0 + 0.13};
46static const double CY[2] = {NY / 2.0 - 0.21, NY / 2.0 - 0.21};
47static const double CZ[2] = {14.0 + 0.31, 24.0 + 0.31};
48static const int ZSPLIT = 20; // the seed boxes are clipped here so each marker gets ONE sphere
49
50static double bubbleAt(int b, int x, int y, int z) {
51 const int NS = 12;
52 double acc = 0.0;
53 for (int j = 0; j < NS; ++j)
54 for (int i = 0; i < NS; ++i) {
55 const double px = x + (i + 0.5) / NS - CX[b], py = y + (j + 0.5) / NS - CY[b];
56 const double r2 = RB * RB - px * px - py * py;
57 if (r2 <= 0.0)
58 continue;
59 const double h = std::sqrt(r2);
60 const double lo = std::fmax(CZ[b] - h, (double)z), hi = std::fmin(CZ[b] + h, (double)z + 1);
61 if (hi > lo)
62 acc += hi - lo;
63 }
64 return acc / (NS * NS);
65}
66
67static void configure(IbmSolver& s, int ox, int oy, int oz, int lnx, int lny, int lnz,
68 bool csf) {
69 s.setRho(RHO_L);
70 s.setMu(MU_L);
71 s.setDomainBc(4, 1, 0, 0, 0);
72 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z (the CUT axis)
73 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
74 s.setPressureChebyshev(true, 400, 1e-14);
75 s.enableVof();
76 // the UNION convention: C = 1 inside a marker (the dispersed phase)
77 std::vector<double> c((std::size_t)lnx * lny * lnz, 0.0);
78 for (int z = 0; z < lnz; ++z)
79 for (int y = 0; y < lny; ++y)
80 for (int x = 0; x < lnx; ++x) {
81 const double v = std::fmax(bubbleAt(0, x + ox, y + oy, z + oz),
82 bubbleAt(1, x + ox, y + oy, z + oz));
83 c[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] = v;
84 }
85 s.setVof(c);
86 s.setPropertyModel("rho", ClosureKind::LinearMix, "C", "", {RHO_L, RHO_G - RHO_L});
87 s.setPropertyModel("mu", ClosureKind::LinearMix, "C", "", {MU_L, MU_G - MU_L});
88 s.setPropertyModel("force_z", ClosureKind::LinearMix, "C", "",
89 {-RHO_L * GRAV, -(RHO_G - RHO_L) * GRAV});
90 if (csf)
92 std::vector<std::array<int, 6>> boxes;
93 for (int b = 0; b < 2; ++b) {
94 std::array<int, 6> q;
95 const double c3[3] = {CX[b], CY[b], CZ[b]};
96 for (int d = 0; d < 3; ++d) {
97 q[d] = (int)std::floor(c3[d] - RB) - 1;
98 q[3 + d] = (int)std::ceil(c3[d] + RB) + 1;
99 }
100 if (b == 0)
101 q[5] = ZSPLIT;
102 else
103 q[2] = ZSPLIT;
104 boxes.push_back(q);
105 }
107 if (csf) {
109 s.setDt(0.25 * s.capillaryDt());
110 } else {
111 s.setDt(0.05);
112 }
113}
114
115static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
116 int lnx, int lny, int lnz, int rank, int size) {
117 std::vector<double> global;
118 if (rank == 0)
119 global.assign(GCELLS, 0.0);
120 for (int r = 0; r < size; ++r) {
121 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
122 if (r == 0) {
123 if (rank == 0)
124 for (int z = 0; z < lnz; ++z)
125 for (int y = 0; y < lny; ++y)
126 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
127 (std::size_t)(z + oz) * NX * NY],
128 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
129 (std::size_t)lnx * sizeof(double));
130 continue;
131 }
132 if (rank == r) {
133 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
134 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
135 } else if (rank == 0) {
137 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
138 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
140 for (int z = 0; z < meta[5]; ++z)
141 for (int y = 0; y < meta[4]; ++y)
142 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
143 (std::size_t)(z + meta[2]) * NX * NY],
144 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
145 (std::size_t)meta[3] * sizeof(double));
146 }
147 }
148 return global;
149}
150
151static double maxDiff(const std::vector<double>& a, const std::vector<double>& b) {
152 double m = 0;
153 for (std::size_t i = 0; i < b.size(); ++i)
154 m = std::fmax(m, std::fabs(a[i] - b[i]));
155 return m;
156}
157static long countBitDiff(const std::vector<double>& a, const std::vector<double>& b, double& mx) {
158 long n = 0;
159 mx = 0.0;
160 for (std::size_t i = 0; i < b.size(); ++i)
161 if (a[i] != b[i]) {
162 ++n;
163 mx = std::fmax(mx, std::fabs(a[i] - b[i]));
164 }
165 return n;
166}
167
168int main(int argc, char** argv) {
169 MPI_Init(&argc, &argv);
170 Kokkos::initialize(argc, argv);
171 int fail = 0;
172 {
173 int rank = 0, size = 1;
176
177 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
178 auto blk = dec.block(rank);
179 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
180 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
181 const int gn[3] = {NX, NY, NZ};
182 bool cut[3] = {false, false, false};
183 for (const auto& sz : dec.sizes())
184 for (int a = 0; a < 3; ++a)
185 if ((int)sz[a] != gn[a])
186 cut[a] = true;
187 if (rank == 0)
188 std::printf(
189 "VOF BLOCK NS MPI (rung W2, WO-W12) np=%d grid %dx%dx%d block %dx%dx%d cut axes: "
190 "%s%s%s\n",
191 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "");
192 if (size > 1 && !cut[2]) {
193 if (rank == 0)
194 std::printf(" FAIL - the decomposition does NOT cut z, so no marker is cut\n");
195 fail = 1;
196 }
197
198 const int STEPS = (argc > 1) ? std::atoi(argv[1]) : 12;
199 for (int cfg = 0; cfg < 2; ++cfg) {
200 const bool csf = (cfg == 1);
201 if (rank == 0)
202 std::printf(" --- configuration: block advection + varRho%s\n",
203 csf ? " + the BLOCK CSF" : " (surface tension OFF)");
205 sd.initMpi(dec, MPI_COMM_WORLD);
206 configure(sd, ox, oy, oz, lnx, lny, lnz, csf);
207 // vof_overlap_design G0: the overlap rules must be INERT on this non-colliding pair -- no
208 // clipped curvature, no debris, no marker overlap (so no phantom capillary bound) -- at
209 // every step, on every rank.
210 long nClip = 0, nDebris = 0, nOverlap = 0;
211 for (int k = 0; k < STEPS; ++k) {
212 sd.step();
213 if (csf) {
215 for (const auto& q : sd.vofBlockStats())
216 nDebris += q.debrisCells + q.debrisUnresolved + (q.debrisReturned != 0.0 ? 1 : 0);
218 }
219 }
220 {
221 long loc[3] = {nClip, nDebris, nOverlap}, glb[3];
223 if (rank == 0)
224 std::printf(" overlap-design counters over %d steps: clipped %ld, debris cells %ld, "
225 "overlap cells %ld\n", STEPS, glb[0], glb[1], glb[2]);
226 if (glb[0] != 0 || glb[1] != 0 || glb[2] != 0) {
227 if (rank == 0)
228 std::printf(" FAIL: an overlap-design rule fired on a non-colliding pair\n");
229 fail = 1;
230 }
231 }
232
233 // per-marker volumes: only the master carries them, so a SUM over ranks is the census
234 const auto stats = sd.vofBlockStats();
235 if (rank == 0)
236 for (const auto& q : stats)
237 std::printf(" dist block %ld master %d box [%d,%d)x[%d,%d)x[%d,%d)\n", q.id, q.master,
238 q.lo[0], q.hi[0], q.lo[1], q.hi[1], q.lo[2], q.hi[2]);
239 std::vector<double> vLoc(stats.size(), 0.0), vAll(stats.size(), 0.0);
240 std::vector<double> aLoc(stats.size(), 0.0), aAll(stats.size(), 0.0);
241 for (std::size_t i = 0; i < stats.size(); ++i) {
242 vLoc[i] = stats[i].volume;
243 aLoc[i] = stats[i].area;
244 }
245 MPI_Allreduce(vLoc.data(), vAll.data(), (int)vLoc.size(), MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
246 MPI_Allreduce(aLoc.data(), aAll.data(), (int)aLoc.size(), MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
247
248 std::vector<double> gf[3];
249 if (csf)
250 for (int c = 0; c < 3; ++c)
251 gf[c] = gatherGlobal(sd.getVofBlockForce(c), ox, oy, oz, lnx, lny, lnz, rank, size);
252 std::vector<double> g[5];
253 g[0] = gatherGlobal(sd.getVelocity(0), ox, oy, oz, lnx, lny, lnz, rank, size);
254 g[1] = gatherGlobal(sd.getVelocity(1), ox, oy, oz, lnx, lny, lnz, rank, size);
255 g[2] = gatherGlobal(sd.getVelocity(2), ox, oy, oz, lnx, lny, lnz, rank, size);
256 g[3] = gatherGlobal(sd.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
257 g[4] = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
258
259 if (rank == 0) {
260 IbmSolver ref(NX, NY, NZ);
261 configure(ref, 0, 0, 0, NX, NY, NZ, csf);
262 // The distributed solve keeps the host GraphAMG bottom (doc/vof_step_performance_design.md
263 // §13.9 Q-D7); on a GPU the single-rank solver would take the device bottom, so the np = 1
264 // bit-exact gate pins the reference to the engine the distributed path runs.
265 ref.setPressureBottomSolver(2); // 'algebraic'
266 for (int k = 0; k < STEPS; ++k)
267 ref.step();
268 std::vector<double> r[5] = {ref.getVelocity(0), ref.getVelocity(1), ref.getVelocity(2),
269 ref.getPressure(), ref.getVof()};
270 const char* fn[5] = {"u", "v", "w", "P", "C"};
271 double d[5];
272 long nb[5];
273 double mb[5];
274 for (int i = 0; i < 5; ++i) {
275 d[i] = maxDiff(g[i], r[i]);
276 nb[i] = countBitDiff(g[i], r[i], mb[i]);
277 }
278 std::printf(" np=%d after %d steps:", size, STEPS);
279 for (int i = 0; i < 5; ++i)
280 std::printf(" d%s %.2e", fn[i], d[i]);
281 std::printf("\n");
282 std::printf(" bitwise-differing cells:");
283 for (int i = 0; i < 5; ++i)
284 std::printf(" %s %ld", fn[i], nb[i]);
285 std::printf("\n");
286 if (csf) {
287 double df = 0.0;
288 long nf = 0;
289 for (int c = 0; c < 3; ++c) {
290 const std::vector<double> rf = ref.getVofBlockForce(c);
291 double m = 0.0;
292 nf += countBitDiff(gf[c], rf, m);
293 df = std::fmax(df, maxDiff(gf[c], rf));
294 }
295 std::printf(" scattered CSF face force vs np=1: max|d| %.3e over %ld bit-differing "
296 "cells\n", df, nf);
297 }
298 for (const auto& q : ref.vofBlockStats())
299 std::printf(" ref block %ld box [%d,%d)x[%d,%d)x[%d,%d)\n", q.id, q.lo[0], q.hi[0],
300 q.lo[1], q.hi[1], q.lo[2], q.hi[2]);
301 const auto rs = ref.vofBlockStats();
302 double dv = 0.0, da = 0.0, sumMark = 0.0, sumUnion = 0.0;
303 for (std::size_t i = 0; i < rs.size(); ++i) {
304 dv = std::fmax(dv, std::fabs(vAll[i] - rs[i].volume));
305 da = std::fmax(da, std::fabs(aAll[i] - rs[i].area));
306 sumMark += rs[i].volume;
307 }
308 for (double q : r[4])
309 sumUnion += q;
310 std::printf(" markers: V %.6f / %.6f, area %.2f / %.2f; |dV| vs np=1 %.2e, |dA| %.2e\n",
311 rs[0].volume, rs[1].volume, rs[0].area, rs[1].area, dv, da);
312 std::printf(" UNION deficit sum(V_marker) - sum(C_union) = %.4f cells (the shared liquid; "
313 "> 0 means the two markers OVERLAP, which a single field cannot do)\n",
314 sumMark - sumUnion);
315 // np = 1 bitwise; np > 1 at the pressure driver's own reduction-order floor, the SAME
316 // tolerances `test_vof_surface_tension_mpi` uses for its coupled gate.
317 const double tolU = (size == 1) ? 0.0 : 1e-12;
318 const double tolP = (size == 1) ? 0.0 : 1e-9;
319 const double tolC = (size == 1) ? 0.0 : 1e-12;
320 const double tol[5] = {tolU, tolU, tolU, tolP, tolC};
321 for (int i = 0; i < 5; ++i)
322 if (!(d[i] <= tol[i])) {
323 std::printf(" FAIL: d%s = %.3e exceeds %.1e\n", fn[i], d[i], tol[i]);
324 fail = 1;
325 }
326 if (!(dv <= (size == 1 ? 0.0 : 1e-11))) {
327 std::printf(" FAIL: marker volume drift %.3e\n", dv);
328 fail = 1;
329 }
330 }
332 }
333 if (rank == 0)
334 std::printf("%s\n", fail ? "FAILED" : "PASSED");
335 }
336 Kokkos::finalize();
337 MPI_Finalize();
338 return fail;
339}
void enableVofBlocksFromField(const std::vector< std::array< int, 6 > > &boxes)
void setDomainBc(int face, int type, double vx, double vy, double vz)
std::vector< double > getVof()
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
void setSurfaceTension(double sigma)
void setPressureChebyshev(bool on, int maxit, double rtol)
double capillaryDt()
The Brackbill capillary limit in the CALLER's time unit.
void setVof(const std::vector< double > &c)
vof::VofCurvature::Stats vofBlockCurvatureStats() const
std::vector< double > getVofBlockForce(int c)
void setPressureGeometry(const std::vector< double > &sdfInner)
std::vector< double > getVelocity(int c)
VofOverlapCensus vofBlockOverlapCensus() const
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setRho(double r)
Fluid density, in the caller's units.
std::vector< vof::VofBlockStats > vofBlockStats() const
std::vector< double > getPressure()
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
long cells
cells with S > 1 + 1e-8 (global)
long clipped
Cells whose curvature the admissibility clip (kappaMax) bounded this call.
static constexpr int STEPS
static constexpr int NY
static constexpr double GRAV
static constexpr double MU_G
static constexpr double SIGMA
static const int ZSPLIT
static double maxDiff(const std::vector< double > &a, const std::vector< double > &b)
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 void configure(IbmSolver &s, int ox, int oy, int oz, int lnx, int lny, int lnz, bool csf)
static const double CY[2]
static long countBitDiff(const std::vector< double > &a, const std::vector< double > &b, double &mx)
static constexpr double RHO_G
static constexpr double RB
static const double CZ[2]
static double bubbleAt(int b, int x, int y, int z)
static constexpr int NX
static constexpr double RHO_L
static constexpr double MU_L
static constexpr std::size_t GCELLS
static const double CX[2]
static constexpr int NZ