flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_wetting_dynamic_mpi.cpp
Go to the documentation of this file.
1// flow — VoF rung V6 (WO-V6), multi-rank: the DYNAMIC contact angle and hysteresis on a
2// decomposition that CUTS the contact line.
3//
4// What can only break here (the V5b MPI gate exercises none of it):
5//
6// 1. THE CELL-CENTRE VELOCITY. `U_cl` is measured at the ANCHOR fluid cell, which the walk along
7// n_w can reach at ghost depth 3. The velocity is therefore built on the INNER region from the
8// solver's own faces and run through the colour block's ghost policy, exactly as the wall SDF
9// and the fluid-only normals are. Building it "wherever the faces are" instead is a
10// decomposition dependence that shows up ONLY here.
11//
12// 2. THE 3-POINT IN-WALL SMOOTHING. Pass B averages `U_cl` over the band cell and its two
13// neighbours along t_hat, i.e. it READS depth 3 while it WRITES depth <= 2. That is why pass A
14// computes at depth <= 2 and its two outputs (`U_cl` and the validity flag) are EXCHANGED
15// before pass B runs. Without the exchange the smoothed value at a block-boundary band cell is
16// a local guess, and it reaches the inner colour through the theta the fill imposes.
17//
18// Gates: the imposed angle, the apparent angle, U_cl and Ca_cl pointwise BITWISE at np 1/2/4
19// against a full-grid single-rank reference; the theta band fill bitwise; and a coupled
20// surface-tension run at the reduction floor.
21#include <mpi.h>
22
23#include <cmath>
24#include <cstdio>
25#include <cstdlib>
26#include <cstring>
27#include <Kokkos_Core.hpp>
28#include <vector>
29
30#include "flow_ibm.hpp"
31#include "peclet/core/common/types.hpp"
32#include "peclet/core/decomp/block_decomposer.hpp"
33
35
36// A flat SDF wall with normal +x at a QUARTER-integer x (WO-S finding 5), with a liquid slab whose
37// two contact lines have OPPOSITE in-wall directions — one advancing and one receding under the
38// single uniform wall-tangential velocity U. The ORB cuts z at np = 2 and x+z at np = 4, so both
39// contact lines are cut.
40static constexpr int NX = 16, NY = 8, NZ = 32;
41static constexpr double XW = 4.25, THETA_E = 60.0, SLIP = 0.1, MU = 1.0, SIGMA = 1.0, UVEL = 0.02;
42static constexpr double ZL = 8.5, ZR = 24.5; // the liquid slab, in z
43static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
44
45static double sdfAt(double x, double, double) {
46 return x - XW;
47}
48static double colourAt(int, int, int iz) {
49 const double lo = std::fmax((double)iz, ZL), hi = std::fmin((double)iz + 1.0, ZR);
50 return std::fmax(hi - lo, 0.0);
51}
52
53template <class Fn>
54static std::vector<double> blockOf(Fn f, int ox, int oy, int oz, int lnx, int lny, int lnz) {
55 std::vector<double> v((std::size_t)lnx * lny * lnz);
56 for (int z = 0; z < lnz; ++z)
57 for (int y = 0; y < lny; ++y)
58 for (int x = 0; x < lnx; ++x)
59 v[(std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny] =
60 f(x + ox, y + oy, z + oz);
61 return v;
62}
63static std::vector<double> sliceOf(const std::vector<double>& g, int ox, int oy, int oz, int lnx,
64 int lny, int lnz) {
65 return blockOf(
66 [&](int x, int y, int z) {
67 return g[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY];
68 },
69 ox, oy, oz, lnx, lny, lnz);
70}
71static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
72 int lnx, int lny, int lnz, int rank, int size) {
73 std::vector<double> global;
74 if (rank == 0)
75 global.assign(GCELLS, 0.0);
76 for (int r = 0; r < size; ++r) {
77 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
78 if (r == 0) {
79 if (rank == 0)
80 for (int z = 0; z < lnz; ++z)
81 for (int y = 0; y < lny; ++y)
82 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
83 (std::size_t)(z + oz) * NX * NY],
84 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
85 (std::size_t)lnx * sizeof(double));
86 continue;
87 }
88 if (rank == r) {
89 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
90 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
91 } else if (rank == 0) {
93 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
94 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
96 for (int z = 0; z < meta[5]; ++z)
97 for (int y = 0; y < meta[4]; ++y)
98 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
99 (std::size_t)(z + meta[2]) * NX * NY],
100 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
101 (std::size_t)meta[3] * sizeof(double));
102 }
103 }
104 return global;
105}
106// NaN-safe: `fmax(m, NaN) == m`, so a NaN field would pass a naive bitwise gate (WO-R found this).
107static double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
108 double m = 0;
109 for (std::size_t i = 0; i < b.size(); ++i) {
110 const double d = std::fabs(a[i] - b[i]);
111 if (!(d <= m))
112 m = d;
113 }
114 return m;
115}
116
117int main(int argc, char** argv) {
118 std::setvbuf(stdout, nullptr, _IONBF, 0);
119 MPI_Init(&argc, &argv);
120 Kokkos::initialize(argc, argv);
121 int fail = 0;
122 {
123 int rank = 0, size = 1;
126 auto dec = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
127 auto blk = dec.block(rank);
128 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
129 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
130 const int gn[3] = {NX, NY, NZ};
131 bool cut[3] = {false, false, false};
132 for (const auto& sz : dec.sizes())
133 for (int a = 0; a < 3; ++a)
134 if ((int)sz[a] != gn[a])
135 cut[a] = true;
136 if (rank == 0)
137 std::printf(
138 "VOF DYNAMIC WETTING MPI np=%d grid %dx%dx%d block %dx%dx%d cut axes: %s%s%s "
139 "theta_e = %.0f deg, lambda = %.2f cells, u = %.3g\n",
140 size, NX, NY, NZ, lnx, lny, lnz, cut[0] ? "x" : "", cut[1] ? "y" : "", cut[2] ? "z" : "",
141 THETA_E, SLIP, UVEL);
142 if (size > 1 && !cut[2]) {
143 if (rank == 0)
144 std::printf(" FAIL — the decomposition does not cut z, so no contact line is cut\n");
145 fail = 1;
146 }
147 const std::vector<double> gSdf = blockOf(
148 [](int x, int y, int z) { return sdfAt(x + 0.5, y + 0.5, z + 0.5); }, 0, 0, 0, NX, NY, NZ);
149
150 auto setup = [&](IbmSolver& s, int aox, int aoy, int aoz, int anx, int any, int anz,
151 bool hyst) {
152 s.setRho(1.0);
153 s.setMu(MU);
154 s.setSolid(sliceOf(gSdf, aox, aoy, aoz, anx, any, anz), true);
155 s.enableVof();
158 // a UNIFORM wall-tangential velocity: the slab's two contact lines then have opposite t_hat,
159 // so one is advancing and one receding in the same field
160 s.setVelocity(2, std::vector<double>((std::size_t)anx * any * anz, UVEL));
162 if (hyst)
163 s.setContactAngleHysteresis(100.0, 80.0);
164 s.setDt(0.15 * s.capillaryDt());
165 };
166
167 // ---- 1. the imposed angle, pointwise -------------------------------------------------------
168 for (int hyst = 0; hyst < 2; ++hyst) {
170 sd.initMpi(dec, MPI_COMM_WORLD);
171 setup(sd, ox, oy, oz, lnx, lny, lnz, hyst != 0);
172 const auto cdLoc = sd.contactAngleDiagnostics();
173 long loc[4] = {cdLoc.dynamicCells, cdLoc.pinnedCells, cdLoc.advancingCells,
174 cdLoc.recedingCells};
175 long tot[4] = {0, 0, 0, 0};
177 std::vector<double> g[5];
178 for (int q = 0; q < 5; ++q)
179 g[q] = gatherGlobal(sd.getVofDynamicField(q), ox, oy, oz, lnx, lny, lnz, rank, size);
180 const auto gf = gatherGlobal(sd.getVofFilledColour(), ox, oy, oz, lnx, lny, lnz, rank, size);
181 if (rank == 0) {
182 IbmSolver ref(NX, NY, NZ);
183 setup(ref, 0, 0, 0, NX, NY, NZ, hyst != 0);
184 const auto rd = ref.contactAngleDiagnostics();
185 double d[5];
186 for (int q = 0; q < 5; ++q)
187 d[q] = maxAbsDiff(g[q], ref.getVofDynamicField(q));
188 const double df = maxAbsDiff(gf, ref.getVofFilledColour());
189 std::printf(
190 " [%s np=%d] imposed %.3e apparent %.3e U_cl %.3e Ca_cl %.3e state %.3e "
191 "band fill %.3e (all bitwise required)\n"
192 " census dynamic/pinned/advancing/receding %ld/%ld/%ld/%ld "
193 "(ref %ld/%ld/%ld/%ld) mean imposed %.6f mean apparent %.6f max|Ca_cl| %.6e\n",
194 hyst ? "hyst " : "dyn ", size, d[0], d[1], d[2], d[3], d[4], df, tot[0], tot[1],
195 tot[2], tot[3], rd.dynamicCells, rd.pinnedCells, rd.advancingCells, rd.recedingCells,
196 rd.meanImposedTheta, rd.meanApparentTheta, rd.maxCaCl);
197 for (int q = 0; q < 5; ++q)
198 if (!(d[q] == 0.0)) {
199 std::printf(" [np=%d] FAIL — dynamic field %d is not bitwise (%.3e)\n", size, q, d[q]);
200 fail = 1;
201 }
202 if (!(df == 0.0)) {
203 std::printf(" [np=%d] FAIL — the theta band fill is not bitwise (%.3e)\n", size, df);
204 fail = 1;
205 }
206 if (tot[0] != rd.dynamicCells || tot[1] != rd.pinnedCells || tot[2] != rd.advancingCells ||
207 tot[3] != rd.recedingCells) {
208 std::printf(" [np=%d] FAIL — the state census differs from the reference\n", size);
209 fail = 1;
210 }
211 }
212 }
213
214 // ---- 2. a coupled surface-tension run, at the reduction floor -------------------------------
215 {
216 const int steps = 20;
218 sd.initMpi(dec, MPI_COMM_WORLD);
219 setup(sd, ox, oy, oz, lnx, lny, lnz, false);
220 const double lv0 = sd.vofDiagnostics().volume;
221 double v0 = 0.0;
223 long maxIt = 0;
224 for (int i = 0; i < steps; ++i) {
225 sd.step();
226 maxIt = std::max(maxIt, sd.lastPressureIterations());
227 }
228 const double lv1 = sd.vofDiagnostics().volume, ls = sd.vofDiagnostics().solidSumC;
229 double v1 = 0.0, solid = 0.0;
232 const auto gc = gatherGlobal(sd.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
233 const auto gi = gatherGlobal(sd.getVofDynamicField(0), ox, oy, oz, lnx, lny, lnz, rank, size);
234 std::vector<double> gu[3];
235 for (int c = 0; c < 3; ++c)
236 gu[c] = gatherGlobal(sd.getVelocity(c), ox, oy, oz, lnx, lny, lnz, rank, size);
237 if (rank == 0) {
238 IbmSolver ref(NX, NY, NZ);
239 setup(ref, 0, 0, 0, NX, NY, NZ, false);
240 for (int i = 0; i < steps; ++i)
241 ref.step();
242 double du = 0;
243 for (int c = 0; c < 3; ++c)
244 du = std::fmax(du, maxAbsDiff(gu[c], ref.getVelocity(c)));
245 const double dc = maxAbsDiff(gc, ref.getVof());
246 const double di = maxAbsDiff(gi, ref.getVofDynamicField(0));
247 const double drift = (v1 - v0) / v0;
248 const double tol = size == 1 ? 0.0 : 1e-9;
249 std::printf(
250 " [coupled np=%d] colour %.3e velocity %.3e imposed theta %.3e drift %.3e "
251 "solid sum C %.3e pressure iters max %ld\n",
252 size, dc, du, di, drift, solid, maxIt);
253 if (!(dc <= tol) || !(du <= tol) || !(di <= 1e-9)) {
254 std::printf(" [coupled np=%d] FAIL — above tol %.1e\n", size, tol);
255 fail = 1;
256 }
257 if (!(solid == 0.0) || !(std::fabs(drift) <= 1e-9)) {
258 std::printf(" [coupled np=%d] FAIL — solid sum %.3e drift %.3e\n", size, solid, drift);
259 fail = 1;
260 }
261 }
262 }
263
264 int g = 0;
266 fail = g;
267 if (rank == 0)
268 std::printf(fail ? "VOF DYNAMIC WETTING MPI: FAILED\n"
269 : "VOF DYNAMIC WETTING MPI: all checks passed\n");
270 }
271 Kokkos::finalize();
272 MPI_Finalize();
273 return fail;
274}
void setSolid(const std::vector< double > &sdfInner, bool cutcellPressure)
Host entry point: upload the inner SDF once and delegate.
std::vector< double > getVofFilledColour()
void setContactAngleDynamic(double thetaEDeg, double slipCells, double muLiquid, double sigma=0.0)
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)
void setContactAngleHysteresis(double thetaADeg, double thetaRDeg)
ContactAngleDiagnostics contactAngleDiagnostics()
std::vector< double > getVofDynamicField(int which)
void setVelocity(int c, const std::vector< double > &v)
Write a component's inner velocity from a host vector (x-fastest, inner region) and re-impose the sol...
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()
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 double SLIP
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 SIGMA
static constexpr double UVEL
static double colourAt(int, int, int iz)
static constexpr double THETA_E
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 ZR
static constexpr double MU
static std::vector< double > sliceOf(const std::vector< double > &g, int ox, int oy, int oz, int lnx, int lny, int lnz)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static constexpr double ZL
static constexpr int NX
static constexpr std::size_t GCELLS
static constexpr int NZ
static double sdfAt(double x, double, double)