flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_wetting_mpi.cpp
Go to the documentation of this file.
1// flow — VoF rung V5b (WO-S), multi-rank: the theta-consistent solid-band fill on a decomposition
2// that CUTS both the wall and the drop.
3//
4// What can only break here (WO-Q's cut-cell MPI gate is the neutral fill and does not exercise any
5// of it):
6//
7// 1. THE WALL NORMAL. `n_w` is a central difference of the SDF ON THE COLOUR BLOCK, evaluated at
8// solid cells out to ghost depth 2 — i.e. it reads depth 3. The SDF therefore has to go
9// through the colour field's own ghost policy after it is embedded, exactly as the
10// classification does (WO-Q finding 5), or the wall normal in the ghost band is a local guess
11// and the INNER fill inherits it through passes 2-3.
12//
13// 2. THE FLUID-ONLY YOUNGS NORMAL. The theta pass reads it at the ANCHOR fluid cell, which the
14// walk along `n_w` can reach at ghost depth 3 — one deeper than a 3^3 stencil can be evaluated
15// on a g = 3 block. It is therefore built on the INNER region only and exchanged, which is
16// what makes it the owner's value everywhere. Building it "wherever the stencil fits" instead
17// would be a decomposition dependence that shows up ONLY here.
18//
19// 3. The theta pass writes solid cells at ghost depth <= 2 like WO-Q's pass 1, so the shrinking
20// depth budget and the second exchange still have to compose.
21//
22// Gates: the filled colour (`vof_filled_color`) pointwise BITWISE at np 1/2/4 against a full-grid
23// single-rank reference, the band census identical, and a coupled surface-tension run at the
24// reduction floor.
25#include <mpi.h>
26
27#include <cmath>
28#include <cstdio>
29#include <cstdlib>
30#include <cstring>
31#include <Kokkos_Core.hpp>
32#include <vector>
33
34#include "flow_ibm.hpp"
35#include "peclet/core/common/types.hpp"
36#include "peclet/core/decomp/block_decomposer.hpp"
37
39
40// The wall normal is +x (a flat SDF wall at a FRACTIONAL x, so the wall cells are genuinely cut;
41// the quarter-cell offset rather than a half is deliberate — at exactly k + 1/2 the tangential MAC
42// faces of the wall cell sit ON the SDF zero level and `buildOpenness` closes them, which isolates
43// that cell tangentially, see the WO-S findings)
44// and the drop sits on it centred at (xw, 8, 16). The ORB cuts z at np = 2 and x+z at np = 4, so
45// the contact LINE (a circle in the plane x = xw) is cut at both.
46static constexpr int NX = 16, NY = 16, NZ = 32;
47static constexpr double XW = 5.25, RD = 6.0, THETA = 60.0;
48static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
49
50static double sdfAt(double x, double, double) {
51 return x - XW;
52}
53// The hemispherical cap, by the same 4^3 subsampling the fluid fraction uses (C is a fraction of
54// the FLUID volume).
55static double colourAt(int ix, int iy, int iz) {
56 int in = 0, tot = 0;
57 for (int a = 0; a < 4; ++a)
58 for (int b = 0; b < 4; ++b)
59 for (int c = 0; c < 4; ++c) {
60 const double px = ix + (a + 0.5) / 4.0, py = iy + (b + 0.5) / 4.0,
61 pz = iz + (c + 0.5) / 4.0;
62 if (px < XW)
63 continue;
64 ++tot;
65 const double dx = px - XW, dy = py - 8.0, dz = pz - 16.0;
66 if (dx * dx + dy * dy + dz * dz < RD * RD)
67 ++in;
68 }
69 return tot ? (double)in / tot : 0.0;
70}
71
72template <class Fn>
73static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
74 std::vector<double> v((std::size_t)lnx * lny * lnz);
75 for (int z = 0; z < lnz; ++z)
76 for (int y = 0; y < lny; ++y)
77 for (int x = 0; x < lnx; ++x)
78 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
79 f(x + ox, y + oy, z + oz);
80 return v;
81}
82static std::vector<double> sliceOf(const std::vector<double>& g, int ox, int oy, int oz, int lnx,
83 int lny, int lnz) {
84 return blockOf(
85 [&](int x, int y, int z) {
86 return g[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY];
87 },
88 ox, oy, oz, lnx, lny, lnz);
89}
90static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
91 int lnx, int lny, int lnz, int rank, int size) {
92 std::vector<double> global;
93 if (rank == 0)
94 global.assign(GCELLS, 0.0);
95 for (int r = 0; r < size; ++r) {
96 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
97 if (r == 0) {
98 if (rank == 0)
99 for (int z = 0; z < lnz; ++z)
100 for (int y = 0; y < lny; ++y)
101 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
102 (std::size_t)(z + oz) * NX * NY],
103 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
104 (std::size_t)lnx * sizeof(double));
105 continue;
106 }
107 if (rank == r) {
108 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
109 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
110 } else if (rank == 0) {
112 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
113 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
115 for (int z = 0; z < meta[5]; ++z)
116 for (int y = 0; y < meta[4]; ++y)
117 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
118 (std::size_t)(z + meta[2]) * NX * NY],
119 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
120 (std::size_t)meta[3] * sizeof(double));
121 }
122 }
123 return global;
124}
125static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
126 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
127 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
128 // a drained open-boundary run). A non-finite difference must fail, so return it.
129 double m = 0;
130 for (std::size_t i = 0; i < b.size(); ++i) {
131 const double d = std::fabs(a[i] - b[i]);
132 if (!(d == d))
133 return d; // NaN
134 m = std::fmax(m, d);
135 }
136 return m;
137}
138
139int main(int argc, char** argv) {
140 std::setvbuf(stdout, nullptr, _IONBF, 0);
141 MPI_Init(&argc, &argv);
142 Kokkos::initialize(argc, argv);
143 int fail = 0;
144 {
145 int rank = 0, size = 1;
148 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
149 auto blk = dec.block(rank);
150 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
151 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
152 const int gn[3] = {NX, NY, NZ};
153 bool cut[3] = {false, false, false};
154 for (const auto& sz : dec.sizes())
155 for (int a = 0; a < 3; ++a)
156 if ((int)sz[a] != gn[a])
157 cut[a] = true;
158 if (rank == 0)
159 std::printf(
160 "VOF WETTING MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s "
161 "theta = %.0f deg\n",
162 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "",
163 THETA);
164 if (size > 1 && !cut[2]) {
165 if (rank == 0)
166 std::printf(" FAIL — the decomposition does not cut z, so the contact line is not cut\n");
167 fail = 1;
168 }
169 const std::vector<double> gSdf = blockOf(
170 [](int x, int y, int z) { return sdfAt(x + 0.5, y + 0.5, z + 0.5); }, 0, 0, 0, NX, NY, NZ);
171
172 auto setup = [&](IbmSolver& s, int aox, int aoy, int aoz, int anx, int any, int anz,
173 bool angle) {
174 s.setRho(1.0);
175 s.setMu(0.05);
176 s.setSolid(sliceOf(gSdf, aox, aoy, aoz, anx, any, anz), true);
177 s.enableVof();
179 s.setSurfaceTension(1.0);
180 if (angle)
182 s.setDt(0.15 * s.capillaryDt());
183 };
184
185 // ---- 1. the theta fill, pointwise ---------------------------------------------------------
186 {
188 sd.initMpi(dec, MPI_COMM_WORLD);
189 setup(sd, ox, oy, oz, lnx, lny, lnz, true);
190 const auto cdLoc = sd.contactAngleDiagnostics();
191 long loc[5] = {cdLoc.contactCells, cdLoc.neighbourCells, cdLoc.pureCells, cdLoc.parallelCells,
192 cdLoc.neutralCells};
193 long tot[5] = {0, 0, 0, 0, 0};
195 const auto g = gatherGlobal(sd.getVofFilledColour(), ox, oy, oz, lnx, lny, lnz, rank, size);
196 // the neutral fill of the SAME scene, for the ablation number
197 const auto gn0 = [&] {
198 IbmSolver s2(lnx, lny, lnz);
199 s2.initMpi(dec, MPI_COMM_WORLD);
200 setup(s2, ox, oy, oz, lnx, lny, lnz, false);
201 return gatherGlobal(s2.getVofFilledColour(), ox, oy, oz, lnx, lny, lnz, rank, size);
202 }();
203 if (rank == 0) {
204 IbmSolver ref(NX, NY, NZ);
205 setup(ref, 0, 0, 0, NX, NY, NZ, true);
206 const auto rd = ref.contactAngleDiagnostics();
207 const double d = maxAbsDiff(g, ref.getVofFilledColour());
208 const double dn = maxAbsDiff(g, gn0);
209 std::printf(
210 " [fill np=%d] theta band fill %.3e (bitwise required) | band census "
211 "theta/neighbour/pure/parallel/neutral %ld/%ld/%ld/%ld/%ld "
212 "(ref %ld/%ld/%ld/%ld/%ld) mean apparent %.3f deg | "
213 "max |theta-fill - neutral-fill| %.3e\n",
214 size, d, tot[0], tot[1], tot[2], tot[3], tot[4], rd.contactCells, rd.neighbourCells,
215 rd.pureCells, rd.parallelCells, rd.neutralCells, rd.meanApparentAngle, dn);
216 if (!(d == 0.0)) {
217 std::printf(" [fill np=%d] FAIL — the theta band fill is not bitwise\n", size);
218 fail = 1;
219 }
220 if (tot[0] != rd.contactCells || tot[1] != rd.neighbourCells || tot[2] != rd.pureCells ||
221 tot[3] != rd.parallelCells || tot[4] != rd.neutralCells) {
222 std::printf(" [fill np=%d] FAIL — the band census differs from the reference\n", size);
223 fail = 1;
224 }
225 }
226 }
227
228 // ---- 2. a coupled surface-tension run, at the reduction floor -------------------------------
229 {
230 const int steps = 25;
232 sd.initMpi(dec, MPI_COMM_WORLD);
233 setup(sd, ox, oy, oz, lnx, lny, lnz, true);
234 const double lv0 = sd.vofDiagnostics().volume;
235 double v0 = 0.0;
237 long maxIt = 0;
238 for (int i = 0; i < steps; ++i) {
239 sd.step();
240 maxIt = std::max(maxIt, sd.lastPressureIterations());
241 }
242 const double lv1 = sd.vofDiagnostics().volume, ls = sd.vofDiagnostics().solidSumC;
243 double v1 = 0.0, solid = 0.0;
246 const auto gc = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
247 std::vector<double> gu[3];
248 for (int c = 0; c < 3; ++c)
249 gu[c] = gatherGlobal(sd.getVelocity(c), ox, oy, oz, lnx, lny, lnz, rank, size);
250 if (rank == 0) {
251 IbmSolver ref(NX, NY, NZ);
252 setup(ref, 0, 0, 0, NX, NY, NZ, true);
253 for (int i = 0; i < steps; ++i)
254 ref.step();
255 double du = 0;
256 for (int c = 0; c < 3; ++c)
257 du = std::fmax(du, maxAbsDiff(gu[c], ref.getVelocity(c)));
258 const double dc = maxAbsDiff(gc, ref.getVof());
259 const double drift = (v1 - v0) / v0;
260 const double tol = size == 1 ? 0.0 : 1e-9;
261 std::printf(
262 " [coupled np=%d] colour %.3e velocity %.3e drift %.3e solid sum C %.3e "
263 "pressure iters max %ld\n",
264 size, dc, du, drift, solid, maxIt);
265 if (!(dc <= tol) || !(du <= tol)) {
266 std::printf(" [coupled np=%d] FAIL — colour %.3e velocity %.3e above tol %.1e\n", size,
267 dc, du, tol);
268 fail = 1;
269 }
270 if (!(solid == 0.0) || !(std::fabs(drift) <= 1e-9)) {
271 std::printf(" [coupled np=%d] FAIL — solid sum %.3e drift %.3e\n", size, solid, drift);
272 fail = 1;
273 }
274 }
275 }
276
277 int g = 0;
279 fail = g;
280 if (rank == 0)
281 std::printf(fail ? "VOF WETTING MPI: FAILED\n" : "VOF WETTING MPI: all checks passed\n");
282 }
283 Kokkos::finalize();
284 MPI_Finalize();
285 return fail;
286}
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
std::vector< double > getVofFilledColour()
std::vector< double > getVof()
void setSurfaceTension(double sigma)
double capillaryDt()
The Brackbill capillary limit in the CALLER's time unit.
void setVof(const std::vector< double > &c)
ContactAngleDiagnostics contactAngleDiagnostics()
std::vector< double > getVelocity(int c)
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setRho(double r)
Fluid density, in the caller's units.
vof::WyAdvector::Diagnostics vofDiagnostics()
void setContactAngle(double thetaDeg)
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
double solidSumC
filled by the SOLVER from the canonical G=2 colour field
double volume
sum of eps_eff*C over inner FLUID cells: the EXACTLY conserved functional of the cut-cell scheme (cut...
static constexpr double XW
static constexpr int NY
static constexpr double RD
static constexpr double THETA
static std::vector< double > blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz)
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 std::vector< double > sliceOf(const std::vector< double > &g, int ox, int oy, int oz, int lnx, int lny, int lnz)
static double colourAt(int ix, int iy, int iz)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ
static double sdfAt(double x, double, double)