flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_wetting_dynamic.cpp
Go to the documentation of this file.
1// VoF rung V6 (WO-V6) — the DYNAMIC contact angle (Cox-Voinov with an explicit slip length) and
2// advancing/receding HYSTERESIS, as a PURE KERNEL test plus the one end-to-end check the model
3// itself needs.
4//
5// G1a THE MODEL, as arithmetic. `coxVoinovAngle` reproduces theta^3 = theta_e^3 + 9 Ca
6// ln(1/lambda)
7// to round-off, is monotone in Ca (advancing RAISES the angle, receding LOWERS it), and the
8// clamp catches the film-entrainment branch where the cube goes non-positive.
9// G1b THE SIGN CONVENTION, as geometry. `vofWallTangent` + `vofContactLineSpeed` on a vertical
10// interface: the liquid ADVANCES when the flow points from the liquid towards the dry wall,
11// i.e. along +t_hat (the work order says -t_hat; see the header of `wetting_dynamic.hpp`).
12// G1c THE MODEL THROUGH THE SOLVER, kinematically. A flat SDF wall at a QUARTER-integer z with a
13// liquid slab and a UNIFORM wall-tangential velocity U: the box is periodic in x, so the slab
14// has two contact lines with opposite t_hat, one ADVANCING and one RECEDING in the SAME run.
15// Gate: |U_cl| == U exactly, the signs opposite and correct, and the imposed angle equal to
16// the Cox-Voinov value of the reported U_cl to 1e-10, cell by cell.
17// G4a THE HYSTERESIS SELECTOR, as a truth table, and end-to-end: with theta_r <= 90 <= theta_a the
18// vertical interface is PINNED and the imposed angle is the apparent one; outside the window
19// the advancing / receding angle is imposed.
20#include <cmath>
21#include <cstdio>
22#include <cstdlib>
23#include <Kokkos_Core.hpp>
24#include <vector>
25
26#include "flow_ibm.hpp"
28
29namespace {
30using namespace peclet::flow::vof;
31
32int failures = 0;
33#define CHECK(cond) \
34 do { \
35 if (!(cond)) { \
36 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
37 ++failures; \
38 } \
39 } while (0)
40
41const double kDeg = M_PI / 180.0;
42
43double coxRef(double thetaEdeg, double ca, double slip) {
44 const double te = thetaEdeg * kDeg;
45 const double t3 = te * te * te + 9.0 * ca * (-std::log(slip));
46 const double th = t3 > 0.0 ? std::cbrt(t3) : 1.0 * kDeg;
47 return std::fmin(std::fmax(th, 1.0 * kDeg), 179.0 * kDeg);
48}
49
50// ------------------------------------------------------------------------------------------ G1a
51void modelArithmetic() {
52 std::printf(
53 "G1a Cox-Voinov (Afkhami, Zaleski & Bussmann, JCP 228:5370): "
54 "theta^3 = theta_e^3 + 9 Ca ln(Delta/lambda)\n");
55 const double slip = 0.1, lr = -std::log(slip);
56 double worst = 0.0;
57 for (double thE : {30.0, 60.0, 90.0, 120.0}) {
58 for (double ca : {-2e-2, -5e-3, 0.0, 5e-3, 2e-2, 5e-2}) {
59 const double th = coxVoinovAngle(thE * kDeg, ca, lr, 1.0 * kDeg, 179.0 * kDeg);
60 const double ref = coxRef(thE, ca, slip);
61 worst = std::fmax(worst, std::fabs(th - ref));
62 // the identity itself, wherever the clamp did not fire (at theta_e = 30 and Ca = -2e-2 the
63 // cube goes non-positive — the film-entrainment branch — and the clamp is the answer)
64 if (th > 1.0 * kDeg && th < 179.0 * kDeg) {
65 const double id = th * th * th - (thE * kDeg) * (thE * kDeg) * (thE * kDeg) - 9.0 * ca * lr;
66 CHECK(std::fabs(id) <= 1e-14);
67 }
68 }
69 const double up = coxVoinovAngle(thE * kDeg, +1e-2, lr, 1.0 * kDeg, 179.0 * kDeg);
70 const double dn = coxVoinovAngle(thE * kDeg, -1e-2, lr, 1.0 * kDeg, 179.0 * kDeg);
71 std::printf(
72 " theta_e %5.1f -> advancing (Ca=+1e-2) %7.3f deg, receding (Ca=-1e-2) %7.3f\n", thE,
73 up / kDeg, dn / kDeg);
74 CHECK(up > thE * kDeg && dn < thE * kDeg);
75 }
76 // the film-entrainment branch: the cube goes non-positive and the clamp catches it
77 const double ent = coxVoinovAngle(30.0 * kDeg, -1.0, lr, 1.0 * kDeg, 179.0 * kDeg);
78 std::printf(
79 " worst |kernel - host| over the sweep %.3e ; the Ca = -1 (film entrainment) "
80 "branch clamps to %.3f deg\n",
81 worst, ent / kDeg);
82 CHECK(worst <= 1e-15);
83 CHECK(std::fabs(ent - 1.0 * kDeg) <= 1e-15);
84}
85
86// ------------------------------------------------------------------------------------------ G1b
87void signConvention() {
88 std::printf("G1b the sign of U_cl (n_w = +z; the liquid ADVANCES along +t_hat)\n");
89 const double nw[3] = {0.0, 0.0, 1.0};
90 double worst = 0.0;
91 for (double thApp : {45.0, 90.0, 135.0})
92 for (double psi : {0.0, 37.0, 90.0, 200.0}) {
93 const double mf[3] = {std::sin(thApp * kDeg) * std::cos(psi * kDeg),
94 std::sin(thApp * kDeg) * std::sin(psi * kDeg), std::cos(thApp * kDeg)};
95 double that[3], cosApp;
96 const bool ok = vofWallTangent(mf, nw, 1e-6, that, cosApp);
97 CHECK(ok);
98 worst = std::fmax(worst, std::fabs(std::acos(cosApp) - thApp * kDeg));
99 // t_hat is the in-wall azimuth of m, i.e. points from the LIQUID towards the DRY wall
100 CHECK(std::fabs(that[0] - std::cos(psi * kDeg)) <= 1e-14);
101 CHECK(std::fabs(that[1] - std::sin(psi * kDeg)) <= 1e-14);
102 CHECK(std::fabs(that[2]) <= 1e-14);
103 // a flow ALONG t_hat pushes liquid onto the dry wall: advancing, U_cl > 0
104 const double uAdv[3] = {0.3 * that[0], 0.3 * that[1], 0.0};
105 const double uRec[3] = {-0.3 * that[0], -0.3 * that[1], 0.0};
106 CHECK(std::fabs(vofContactLineSpeed(uAdv, that) - 0.3) <= 1e-15);
107 CHECK(std::fabs(vofContactLineSpeed(uRec, that) + 0.3) <= 1e-15);
108 }
109 std::printf(
110 " theta_app recovered from m_f . n_w to %.3e rad; U_cl = +u.t_hat verified over "
111 "3 angles x 4 azimuths\n",
112 worst);
113 CHECK(worst <= 1e-14);
114 // an interface parallel to the wall has no contact-line direction
115 const double mpar[3] = {0.0, 0.0, 1.0};
116 double tp[3], cp;
117 CHECK(!vofWallTangent(mpar, nw, 1e-6, tp, cp));
118}
119
120// ------------------------------------------------------------------------------------------ G4a
121void hysteresisTable() {
122 std::printf("G4a the hysteresis selector (theta_a = 70, theta_r = 50)\n");
123 const double ta = 70.0 * kDeg, tr = 50.0 * kDeg, lr = -std::log(0.1);
124 struct Row {
125 double app, ca;
126 int want;
127 };
128 const Row rows[5] = {{80.0, 0.0, kVofDynAdvancing},
129 {60.0, 0.0, kVofDynPinned},
130 {40.0, 0.0, kVofDynReceding},
131 {80.0, 1e-2, kVofDynAdvancing},
132 {40.0, -1e-2, kVofDynReceding}};
133 for (const Row& r : rows) {
134 double th, ca;
135 // Ca is fed through mu/sigma: mu = r.ca, sigma = 1, U_cl = 1
136 const int st = vofDynamicContactAngle(r.app * kDeg, 1.0, r.ca, 1.0, lr, true, ta, tr,
137 60.0 * kDeg, 1.0 * kDeg, 179.0 * kDeg, th, ca);
138 const char* nm[5] = {"none", "static", "PINNED", "advancing", "receding"};
139 std::printf(" theta_app %5.1f Ca %+8.1e -> %-9s theta_imposed %7.3f deg\n", r.app, r.ca,
140 nm[st], th / kDeg);
141 CHECK(st == r.want);
142 if (r.want == kVofDynPinned)
143 CHECK(std::fabs(th - r.app * kDeg) <= 1e-15); // the fill is idempotent: nothing moves
144 else {
145 const double base = r.want == kVofDynAdvancing ? 70.0 : 50.0;
146 CHECK(std::fabs(th - coxRef(base, r.ca, 0.1)) <= 1e-15);
147 }
148 }
149 // with no hysteresis the static base is always the Cox-Voinov base
150 double th, ca;
151 const int st = vofDynamicContactAngle(80.0 * kDeg, 1.0, 1e-2, 1.0, lr, false, ta, tr, 60.0 * kDeg,
152 1.0 * kDeg, 179.0 * kDeg, th, ca);
153 CHECK(st == kVofDynStatic);
154 CHECK(std::fabs(th - coxRef(60.0, 1e-2, 0.1)) <= 1e-15);
155}
156
157// --------------------------------------------------------------------------------------- G1c/G4b
158// The slit: solid below z = ZW and above NZ - ZW, so the SDF is continuous across the periodic z
159// wrap. ZW is a QUARTER-integer (WO-S finding 5: at a half-integer the wall cell's tangential MAC
160// faces sit ON the SDF zero level and close, and the contact line cannot move at all).
161constexpr int NX = 32, NY = 8, NZ = 24;
162constexpr double ZW = 4.25;
163constexpr double XL = 0.5, XR = 16.5; // the liquid slab, in x
164
165std::vector<double> slitSdf() {
166 std::vector<double> v((std::size_t)NX * NY * NZ);
167 for (int z = 0; z < NZ; ++z) {
168 const double zc = z + 0.5, d = std::fmin(zc - ZW, (NZ - ZW) - zc);
169 for (int y = 0; y < NY; ++y)
170 for (int x = 0; x < NX; ++x)
171 v[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] = d;
172 }
173 return v;
174}
175std::vector<double> slabColour() {
176 std::vector<double> v((std::size_t)NX * NY * NZ);
177 for (int x = 0; x < NX; ++x) {
178 const double lo = std::fmax((double)x, XL), hi = std::fmin((double)x + 1.0, XR);
179 const double c = std::fmax(hi - lo, 0.0);
180 for (int z = 0; z < NZ; ++z)
181 for (int y = 0; y < NY; ++y)
182 v[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] = c;
183 }
184 return v;
185}
186
187void solverGates() {
188 const double thE = 60.0, slip = 0.1, muL = 1.0, sigma = 1.0, U = 0.02;
189 std::printf(
190 "G1c the model through the solver, KINEMATIC (%dx%dx%d, flat SDF wall at z = %.2f, liquid "
191 "slab x in [%.1f, %.1f), uniform u = %.3g, theta_e = %.0f, lambda = %.2f cells, mu_l = %.1f, "
192 "sigma = %.1f)\n",
193 NX, NY, NZ, ZW, XL, XR, U, thE, slip, muL, sigma);
195 s.setRho(1.0);
196 s.setMu(muL);
197 s.setSolid(slitSdf(), true);
198 s.enableVof();
199 s.setVof(slabColour());
200 s.setVelocity(0, std::vector<double>((std::size_t)NX * NY * NZ, U));
201 s.setContactAngleDynamic(thE, slip, muL, sigma);
202
203 const auto imposed = s.getVofDynamicField(0); // degrees
204 const auto apparent = s.getVofDynamicField(1); // degrees
205 const auto ucl = s.getVofDynamicField(2);
206 const auto state = s.getVofDynamicField(4);
207 const auto at = [&](const std::vector<double>& v, int x, int y, int z) {
208 return v[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY];
209 };
210 // the two contact columns, in the first solid row below the wall (z = 3)
211 const int zb = 3;
212 std::printf(
213 " x = 16 (m = +x, liquid on the LOW side): U_cl %+.6f theta_app %.4f theta_imposed "
214 "%.4f\n"
215 " x = 0 (m = -x, liquid on the HIGH side): U_cl %+.6f theta_app %.4f theta_imposed "
216 "%.4f\n",
217 at(ucl, 16, 4, zb), at(apparent, 16, 4, zb), at(imposed, 16, 4, zb), at(ucl, 0, 4, zb),
218 at(apparent, 0, 4, zb), at(imposed, 0, 4, zb));
219 CHECK(std::fabs(at(ucl, 16, 4, zb) - U) <= 1e-12);
220 CHECK(std::fabs(at(ucl, 0, 4, zb) + U) <= 1e-12);
221 CHECK(std::fabs(at(apparent, 16, 4, zb) - 90.0) <= 1e-9);
222 const double adv = coxRef(thE, muL * U / sigma, slip) / kDeg;
223 const double rec = coxRef(thE, -muL * U / sigma, slip) / kDeg;
224 std::printf(
225 " Cox-Voinov reference: advancing %.4f deg, receding %.4f deg (theta_e %.1f) -> "
226 "advancing RAISES and receding LOWERS the imposed angle\n",
227 adv, rec, thE);
228 CHECK(std::fabs(at(imposed, 16, 4, zb) - adv) <= 1e-10);
229 CHECK(std::fabs(at(imposed, 0, 4, zb) - rec) <= 1e-10);
230 CHECK(adv > thE && rec < thE);
231
232 // the whole field: every contact cell's imposed angle IS the Cox-Voinov value of its own U_cl
233 double worst = 0.0;
234 long nc = 0;
235 for (std::size_t i = 0; i < imposed.size(); ++i) {
236 if (state[i] < 0.5)
237 continue;
238 ++nc;
239 const double ref = coxRef(thE, muL * ucl[i] / sigma, slip) / kDeg;
240 worst = std::fmax(worst, std::fabs(imposed[i] - ref));
241 }
242 const auto cd = s.contactAngleDiagnostics();
243 std::printf(
244 " over all %ld contact cells: max |theta_imposed - CoxVoinov(U_cl)| = %.3e deg "
245 "(gate 1e-10)\n"
246 " diagnostics: dynamic %ld pinned %ld advancing %ld receding %ld mean imposed %.4f "
247 "mean apparent %.4f max|Ca_cl| %.4e max|U_cl| %.4e\n",
248 nc, worst, cd.dynamicCells, cd.pinnedCells, cd.advancingCells, cd.recedingCells,
249 cd.meanImposedTheta, cd.meanApparentTheta, cd.maxCaCl, cd.maxContactSpeed);
250 CHECK(worst <= 1e-10);
251 CHECK(nc > 0);
252 CHECK(std::fabs(cd.maxCaCl - muL * U / sigma) <= 1e-12);
253
254 // --- G4b hysteresis, end to end -------------------------------------------------------------
255 std::printf(
256 "G4b hysteresis through the solver, same scene at REST (U_cl = 0 by mu_l -> Ca = 0)\n");
257 struct HRow {
258 double a, r;
259 const char* what;
260 };
261 const HRow hrows[3] = {{120.0, 60.0, "90 inside [60,120] -> PINNED"},
262 {70.0, 50.0, "90 above theta_a=70 -> advancing"},
263 {130.0, 110.0, "90 below theta_r=110 -> receding"}};
264 for (const HRow& h : hrows) {
266 s2.setRho(1.0);
267 s2.setMu(muL);
268 s2.setSolid(slitSdf(), true);
269 s2.enableVof();
270 s2.setVof(slabColour());
271 s2.setContactAngle(thE);
272 s2.setContactAngleHysteresis(h.a, h.r);
273 const auto im = s2.getVofDynamicField(0);
274 const auto st = s2.getVofDynamicField(4);
275 const auto ap = s2.getVofDynamicField(1);
276 const auto d2 = s2.contactAngleDiagnostics();
277 std::printf(
278 " theta_a %5.1f theta_r %5.1f : %s | imposed at (16,4,3) %.4f (apparent %.4f, state "
279 "%.0f) | pinned %ld advancing %ld receding %ld\n",
280 h.a, h.r, h.what, at(im, 16, 4, zb), at(ap, 16, 4, zb), at(st, 16, 4, zb), d2.pinnedCells,
281 d2.advancingCells, d2.recedingCells);
282 const double want = (h.a >= 90.0 && h.r <= 90.0) ? at(ap, 16, 4, zb) : (90.0 > h.a ? h.a : h.r);
283 CHECK(std::fabs(at(im, 16, 4, zb) - want) <= 1e-9);
284 }
285}
286
287} // namespace
288
289int main(int argc, char** argv) {
290 std::setvbuf(stdout, nullptr, _IONBF, 0);
291 Kokkos::initialize(argc, argv);
292 {
296 if (!std::getenv("PECLET_VOF_WETTING_KERNEL_ONLY"))
297 solverGates();
298 }
299 Kokkos::finalize();
300 if (failures)
301 std::fprintf(stderr, "%d check(s) FAILED\n", failures);
302 else
303 std::printf("all vof dynamic-wetting checks passed\n");
304 return failures ? 1 : 0;
305}
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
float worst
Definition cmp.py:18
x
Definition cmp.py:24
y
Definition cmp.py:24
bool vofWallTangent(const double mfIn[3], const double nwIn[3], double tEps, double that[3], double &cosApp, const VofMetric &g)
The in-wall (tangential) unit direction of the interface and its apparent cosine.
double vofContactLineSpeed(const double u[3], const double that[3])
U_cl = +u .
double coxVoinovAngle(double thetaE, double ca, double logRatio, double thetaMin, double thetaMax)
Cox-Voinov / Afkhami-Zaleski-Bussmann grid-scale apparent angle, in RADIANS.
int vofDynamicContactAngle(double thetaApp, double uCl, double muL, double sigma, double logRatio, bool hyst, double thetaA, double thetaR, double thetaE, double thetaMin, double thetaMax, double &thetaOut, double &caOut)
The whole rung in one pure function: hysteresis selector, then Cox-Voinov on the selected base.
@ kVofDynReceding
theta_app < theta_r: theta_r (+ Cox-Voinov)
@ kVofDynPinned
theta_r <= theta_app <= theta_a: the apparent angle is imposed
@ kVofDynAdvancing
theta_app > theta_a: theta_a (+ Cox-Voinov)
@ kVofDynStatic
Cox-Voinov applied to the static base (no hysteresis configured)
static constexpr int NY
static constexpr int NX
static constexpr int NZ
static constexpr double U
CutcellMG::PlanRow Row
#define CHECK(cond)
flow — VoF rung V6 (WO-V6): the DYNAMIC contact angle (Cox-Voinov with an explicit slip length) and a...