flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
wetting_dynamic.hpp
Go to the documentation of this file.
1
70#ifndef PECLET_FLOW_VOF_WETTING_DYNAMIC_HPP
71#define PECLET_FLOW_VOF_WETTING_DYNAMIC_HPP
72
73#include <cmath>
74#include <functional>
75#include <Kokkos_Core.hpp>
76#include <Kokkos_MathematicalFunctions.hpp>
77
78#include "policy.hpp"
79#include "vof/advect_wy.hpp"
80
81namespace peclet::flow::vof {
82
92
99KOKKOS_INLINE_FUNCTION double coxVoinovAngle(double thetaE, double ca, double logRatio,
100 double thetaMin, double thetaMax) {
101 const double t3 = thetaE * thetaE * thetaE + 9.0 * ca * logRatio;
102 double th = t3 > 0.0 ? Kokkos::pow(t3, 1.0 / 3.0) : thetaMin;
103 if (!(th == th)) // NaN guard: a caller-supplied NaN velocity must not reach the fill
104 th = thetaE;
105 return th < thetaMin ? thetaMin : (th > thetaMax ? thetaMax : th);
106}
107
110KOKKOS_INLINE_FUNCTION int vofHysteresisBase(double thetaApp, double thetaA, double thetaR,
111 double& base) {
112 if (thetaApp > thetaA) {
113 base = thetaA;
114 return kVofDynAdvancing;
115 }
116 if (thetaApp < thetaR) {
117 base = thetaR;
118 return kVofDynReceding;
119 }
120 base = thetaApp;
121 return kVofDynPinned;
122}
123
136 double sigma, double logRatio, bool hyst,
137 double thetaA, double thetaR, double thetaE,
138 double thetaMin, double thetaMax,
139 double& thetaOut, double& caOut) {
140 caOut = sigma > 0.0 ? muL * uCl / sigma : 0.0;
141 double base = thetaE;
142 int state = kVofDynStatic;
143 if (hyst) {
144 state = vofHysteresisBase(thetaApp, thetaA, thetaR, base);
145 if (state == kVofDynPinned) {
146 // The fill is idempotent, so imposing the apparent angle reproduces the current interface
147 // and the line does not move. No Cox-Voinov correction: the line is not moving.
148 thetaOut = base < thetaMin ? thetaMin : (base > thetaMax ? thetaMax : base);
149 return state;
150 }
151 }
152 thetaOut = coxVoinovAngle(base, caOut, logRatio, thetaMin, thetaMax);
153 return state;
154}
155
160KOKKOS_INLINE_FUNCTION bool vofWallTangent(const double mfIn[3], const double nwIn[3], double tEps,
161 double that[3], double& cosApp, const VofMetric& g) {
162 // Phase 3 (V4.3): the apparent angle and the in-wall direction are PHYSICAL, and both inputs
163 // arrive as index-space gradients — `vofPhysNormal` is the pullback, exactly as in
164 // `vofWettingPlane`. At the unit metric it divides by 1.0 and returns `m/|m|`, so the two
165 // normalisations below are the ones this function always did, bit for bit. `that` comes back as
166 // a PHYSICAL unit direction; the caller that steps on the index lattice pulls it back itself.
167 double mh[3] = {mfIn[0], mfIn[1], mfIn[2]};
168 if (!(vofPhysNormal(mfIn, g, mh) > 0.0)) {
169 mh[0] = mfIn[0];
170 mh[1] = mfIn[1];
171 mh[2] = mfIn[2];
172 }
173 double nw[3] = {nwIn[0], nwIn[1], nwIn[2]};
174 if (!(vofPhysNormal(nwIn, g, nw) > 0.0)) {
175 nw[0] = nwIn[0];
176 nw[1] = nwIn[1];
177 nw[2] = nwIn[2];
178 }
179 cosApp = mh[0] * nw[0] + mh[1] * nw[1] + mh[2] * nw[2];
180 double t[3] = {mh[0] - cosApp * nw[0], mh[1] - cosApp * nw[1], mh[2] - cosApp * nw[2]};
181 const double tn = Kokkos::sqrt(t[0] * t[0] + t[1] * t[1] + t[2] * t[2]);
182 if (tn < tEps) {
183 that[0] = that[1] = that[2] = 0.0;
184 return false;
185 }
186 for (int d = 0; d < 3; ++d)
187 that[d] = t[d] / tn;
188 return true;
189}
190
192KOKKOS_INLINE_FUNCTION bool vofWallTangent(const double mfIn[3], const double nwIn[3], double tEps,
193 double that[3], double& cosApp) {
194 return vofWallTangent(mfIn, nwIn, tEps, that, cosApp, VofMetric{});
195}
196
209KOKKOS_INLINE_FUNCTION double vofContactLineSpeed(const double u[3], const double that[3]) {
210 return u[0] * that[0] + u[1] * that[1] + u[2] * that[2];
211}
212
222 // ---- configuration (all angles in RADIANS) -------------------------------------------------
223 bool dynamic = false;
224 bool hysteresis = false;
225 double thetaA = 0.0, thetaR = 0.0;
227 VofMetric metric;
228 double slip = 0.1;
229 double muLiquid = 0.0;
230 double sigma = 0.0;
231 double thetaMin = 1.0 * 3.14159265358979323846 / 180.0;
232 double thetaMax = 179.0 * 3.14159265358979323846 / 180.0;
233 bool smooth = true;
234 double pureEps = 1e-8;
235 double tanEps = 1e-6;
236
237 bool active() const { return dynamic || hysteresis; }
241 double logRatio() const { return dynamic ? -std::log(slip) : 0.0; }
242
243 // ---- state ---------------------------------------------------------------------------------
244 bool allocated() const { return base_.extent(0) > 0; }
248 SField base() const { return base_; }
249 SField imposed() const { return imposed_; }
250 SField apparent() const { return app_; }
251 SField speed() const { return uclS_; }
252 SField capillary() const { return ca_; }
253 SField stateField() const { return stateD_; }
254 SField cellVel(int d) const { return uc_[d]; }
255
256 void allocate(long len) {
257 if (base_.extent(0) == (std::size_t)len)
258 return;
259 base_ = SField("vof::dyn::base", len);
260 imposed_ = SField("vof::dyn::imposed", len);
261 app_ = SField("vof::dyn::apparent", len);
262 ucl_ = SField("vof::dyn::uclRaw", len);
263 uclS_ = SField("vof::dyn::ucl", len);
264 wgt_ = SField("vof::dyn::valid", len);
265 ca_ = SField("vof::dyn::ca", len);
266 stateD_ = SField("vof::dyn::state", len);
267 for (int d = 0; d < 3; ++d) {
268 uc_[d] = SField("vof::dyn::cellVel", len);
269 th_[d] = SField("vof::dyn::tangent", len);
270 }
271 }
272
278 void measure(const WyAdvector& adv) {
279 const I3 e = adv.extent(), n = adv.inner();
280 const int g = adv.ghost();
281 const long sx = 1, sy = e.x, sz = static_cast<long>(e.x) * e.y;
282 SField c = adv.colour(), sdf = adv.wallSdf();
283 SField mx = adv.wettingNormal(0), my = adv.wettingNormal(1), mz = adv.wettingNormal(2);
284 SField ux = uc_[0], uy = uc_[1], uz = uc_[2];
285 SField ucl = ucl_, wg = wgt_, ap = app_, tx = th_[0], ty = th_[1], tz = th_[2];
286 UCField kk = adv.cellKind();
287 const double pe = pureEps, te = tanEps;
288 const VofMetric gme = metric; // `g` is the ghost width in this scope
289 Kokkos::parallel_for(
290 "vof::dyn::measure", MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
291 KOKKOS_LAMBDA(int x, int y, int z) {
292 const long i = L3(x, y, z, e);
293 ucl(i) = 0.0;
294 wg(i) = 0.0;
295 ap(i) = 0.0;
296 tx(i) = ty(i) = tz(i) = 0.0;
297 if (vofGhostDepth(x, y, z, g, n.x, n.y, n.z) > 2)
298 return;
299 if (kk(i) != kVofSolid)
300 return;
301 double nw[3] = {0.5 * (sdf(i + sx) - sdf(i - sx)), 0.5 * (sdf(i + sy) - sdf(i - sy)),
302 0.5 * (sdf(i + sz) - sdf(i - sz))};
303 // Phase 3 (V4.2): the WALK follows the physical wall normal expressed in index steps,
304 // `H^-1 n_w ~ H^-2 grad_xi(sdf)`, with ONE normalisation — identical to `nw/|nw|` at the
305 // unit metric. `nw` itself stays the raw index gradient: `vofWallTangent` pulls it back.
306 double wk[3] = {nw[0] / (gme.h[0] * gme.h[0]), nw[1] / (gme.h[1] * gme.h[1]),
307 nw[2] / (gme.h[2] * gme.h[2])};
308 const double nn = Kokkos::sqrt(wk[0] * wk[0] + wk[1] * wk[1] + wk[2] * wk[2]);
309 if (!(nn > 1e-12))
310 return;
311 for (int d = 0; d < 3; ++d)
312 wk[d] /= nn;
313 for (int step = 1; step <= 4; ++step) {
314 const int fx = x + static_cast<int>(Kokkos::round(step * wk[0]));
315 const int fy = y + static_cast<int>(Kokkos::round(step * wk[1]));
316 const int fz = z + static_cast<int>(Kokkos::round(step * wk[2]));
317 if (fx < 0 || fy < 0 || fz < 0 || fx >= e.x || fy >= e.y || fz >= e.z)
318 return;
319 const long fi = L3(fx, fy, fz, e);
320 if (kk(fi) != kVofFluid)
321 continue;
322 double mAcc[3] = {0.0, 0.0, 0.0}, uAcc[3] = {0.0, 0.0, 0.0};
323 int cnt = 0;
324 const double cf = c(fi);
325 const double ml = Kokkos::fabs(mx(fi)) + Kokkos::fabs(my(fi)) + Kokkos::fabs(mz(fi));
326 if (cf > pe && cf < 1.0 - pe && ml > 0.0) {
327 mAcc[0] = mx(fi);
328 mAcc[1] = my(fi);
329 mAcc[2] = mz(fi);
330 uAcc[0] = ux(fi);
331 uAcc[1] = uy(fi);
332 uAcc[2] = uz(fi);
333 cnt = 1;
334 } else {
335 // The anchor column carries no interface: average over its MIXED fluid neighbours,
336 // the same set `solidBandFillPassWetting`'s neighbour branch averages over.
337 const bool inner =
338 fx >= 1 && fy >= 1 && fz >= 1 && fx + 1 < e.x && fy + 1 < e.y && fz + 1 < e.z;
339 if (inner)
340 for (int kz = -1; kz <= 1; ++kz)
341 for (int ky = -1; ky <= 1; ++ky)
342 for (int kx = -1; kx <= 1; ++kx) {
343 const long gi = L3(fx + kx, fy + ky, fz + kz, e);
344 if (kk(gi) != kVofFluid)
345 continue;
346 const double cg = c(gi);
347 if (cg <= pe || cg >= 1.0 - pe)
348 continue;
349 const double gm[3] = {mx(gi), my(gi), mz(gi)};
350 const double gl =
351 Kokkos::fabs(gm[0]) + Kokkos::fabs(gm[1]) + Kokkos::fabs(gm[2]);
352 if (!(gl > 0.0))
353 continue;
354 const double g2 = Kokkos::sqrt(gm[0] * gm[0] + gm[1] * gm[1] + gm[2] * gm[2]);
355 for (int d = 0; d < 3; ++d)
356 mAcc[d] += gm[d] / g2;
357 uAcc[0] += ux(gi);
358 uAcc[1] += uy(gi);
359 uAcc[2] += uz(gi);
360 ++cnt;
361 }
362 }
363 if (cnt == 0)
364 return;
365 const double inv = 1.0 / static_cast<double>(cnt);
366 // `u_a = h_a v_a` (the index -> physical velocity map, Phase 1 `velToPhys(a)` without
367 // the tRef that cancels in `U_cl/|u|`): the contact-line speed is a PHYSICAL projection
368 // on a PHYSICAL direction. Identity at the unit metric.
369 const double uu[3] = {uAcc[0] * inv * gme.h[0], uAcc[1] * inv * gme.h[1],
370 uAcc[2] * inv * gme.h[2]};
371 double that[3], cosApp;
372 const bool ok = vofWallTangent(mAcc, nw, te, that, cosApp, gme);
373 ap(i) = Kokkos::acos(cosApp < -1.0 ? -1.0 : (cosApp > 1.0 ? 1.0 : cosApp));
374 wg(i) = 1.0;
375 if (ok) {
376 // The stored tangent is what `impose()` STEPS along on the index lattice, so it is
377 // the index pullback `H^-1 t_hat`, renormalized once.
378 //
379 // RULE B(3) GUARD (`flow/doc/anisotropic_vof.md` §1) — the ONLY one on the VoF half.
380 // `that` is already an L2-unit vector, but its norm is 1.0 only to round-off, so a
381 // second normalisation would perturb the stored bits on the isotropic path. The
382 // pullback is the identity there, so it is skipped outright.
383 double ti[3] = {that[0], that[1], that[2]};
384 if (!gme.isotropic()) {
385 for (int d = 0; d < 3; ++d)
386 ti[d] = that[d] / gme.h[d];
387 const double tin = Kokkos::sqrt(ti[0] * ti[0] + ti[1] * ti[1] + ti[2] * ti[2]);
388 if (tin > 0.0)
389 for (int d = 0; d < 3; ++d)
390 ti[d] /= tin;
391 }
392 tx(i) = ti[0];
393 ty(i) = ti[1];
394 tz(i) = ti[2];
396 }
397 return;
398 }
399 });
400 Kokkos::fence();
401 }
402
406 void impose(const WyAdvector& adv) {
407 const I3 e = adv.extent(), n = adv.inner();
408 const int g = adv.ghost();
409 SField th = adv.contactAngle();
410 SField bs = base_, ap = app_, ucl = ucl_, wg = wgt_, us = uclS_, ca = ca_, st = stateD_;
411 SField tx = th_[0], ty = th_[1], tz = th_[2], imp = imposed_;
412 UCField kk = adv.cellKind();
413 const bool hyst = hysteresis, sm = smooth;
414 const double ta = thetaA, tr = thetaR, mu = muLiquid, sg = sigma, lr = logRatio();
415 const double tmin = thetaMin, tmax = thetaMax;
416 Kokkos::parallel_for(
417 "vof::dyn::impose", MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
418 KOKKOS_LAMBDA(int x, int y, int z) {
419 const long i = L3(x, y, z, e);
420 us(i) = 0.0;
421 ca(i) = 0.0;
422 st(i) = static_cast<double>(kVofDynNone);
423 imp(i) = bs(i);
424 if (vofGhostDepth(x, y, z, g, n.x, n.y, n.z) > 2)
425 return;
426 if (kk(i) != kVofSolid) {
427 th(i) = bs(i);
428 return;
429 }
430 if (!(wg(i) > 0.0)) {
431 th(i) = bs(i); // no contact-line data: the static base stands
432 return;
433 }
434 double su = ucl(i), sw = 1.0;
435 const double td[3] = {tx(i), ty(i), tz(i)};
436 if (sm && (td[0] != 0.0 || td[1] != 0.0 || td[2] != 0.0)) {
437 for (int s = -1; s <= 1; s += 2) {
438 const int dx = static_cast<int>(Kokkos::round(s * td[0]));
439 const int dy = static_cast<int>(Kokkos::round(s * td[1]));
440 const int dz = static_cast<int>(Kokkos::round(s * td[2]));
441 if (dx == 0 && dy == 0 && dz == 0)
442 continue;
443 const int px = x + dx, py = y + dy, pz = z + dz;
444 if (px < 0 || py < 0 || pz < 0 || px >= e.x || py >= e.y || pz >= e.z)
445 continue;
446 const long j = L3(px, py, pz, e);
447 if (!(wg(j) > 0.0))
448 continue;
449 su += ucl(j);
450 sw += 1.0;
451 }
452 }
453 const double u = su / sw;
454 double thOut, caOut;
455 const int state = vofDynamicContactAngle(ap(i), u, mu, sg, lr, hyst, ta, tr, bs(i), tmin,
456 tmax, thOut, caOut);
457 us(i) = u;
458 ca(i) = caOut;
459 st(i) = static_cast<double>(state);
460 imp(i) = thOut;
461 th(i) = thOut;
462 });
463 Kokkos::fence();
464 }
465
466 SField uclRaw() const { return ucl_; }
467 SField valid() const { return wgt_; }
468
471 struct Census {
472 long cells[kVofDynStateCount] = {0, 0, 0, 0, 0};
473 double meanImposedDeg = 0.0, meanApparentDeg = 0.0, maxCa = 0.0, maxSpeed = 0.0;
474 long contactCells = 0;
475 };
476 Census census(const WyAdvector& adv) const {
477 Census r;
478 if (!allocated())
479 return r;
480 const I3 e = adv.extent(), n = adv.inner();
481 const int g = adv.ghost();
482 SField wg = wgt_, imp = imposed_, ap = app_, ca = ca_, st = stateD_, us = uclS_;
483 UCField kk = adv.cellKind();
484 using MD = MDRange3<SExec>;
485 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
486 for (int s = 0; s < kVofDynStateCount; ++s) {
487 long acc = 0;
488 const double sv = static_cast<double>(s);
489 Kokkos::parallel_reduce(
490 "vof::dyn::census_state", pol,
491 KOKKOS_LAMBDA(int x, int y, int z, long& a) {
492 const long i = L3(x, y, z, e);
493 if (kk(i) == kVofSolid && wg(i) > 0.0 && st(i) == sv)
494 ++a;
495 },
496 acc);
497 r.cells[s] = acc;
498 }
499 double si = 0.0, sa = 0.0, mc = 0.0, ms = 0.0;
500 long cnt = 0;
501 Kokkos::parallel_reduce(
502 "vof::dyn::census_mean", pol,
503 KOKKOS_LAMBDA(int x, int y, int z, double& ai, double& aa, long& ac) {
504 const long i = L3(x, y, z, e);
505 if (kk(i) == kVofSolid && wg(i) > 0.0) {
506 ai += imp(i);
507 aa += ap(i);
508 ++ac;
509 }
510 },
511 si, sa, cnt);
512 Kokkos::parallel_reduce(
513 "vof::dyn::census_maxca", pol,
514 KOKKOS_LAMBDA(int x, int y, int z, double& am) {
515 const long i = L3(x, y, z, e);
516 if (kk(i) == kVofSolid && wg(i) > 0.0) {
517 const double v = Kokkos::fabs(ca(i));
518 if (v > am)
519 am = v;
520 }
521 },
522 Kokkos::Max<double>(mc));
523 Kokkos::parallel_reduce(
524 "vof::dyn::census_maxu", pol,
525 KOKKOS_LAMBDA(int x, int y, int z, double& as) {
526 const long i = L3(x, y, z, e);
527 if (kk(i) == kVofSolid && wg(i) > 0.0) {
528 const double w = Kokkos::fabs(us(i));
529 if (w > as)
530 as = w;
531 }
532 },
533 Kokkos::Max<double>(ms));
534 Kokkos::fence();
535 const double toDeg = 180.0 / 3.14159265358979323846;
536 r.contactCells = cnt;
537 r.meanImposedDeg = cnt ? si / static_cast<double>(cnt) * toDeg : 0.0;
538 r.meanApparentDeg = cnt ? sa / static_cast<double>(cnt) * toDeg : 0.0;
539 r.maxCa = mc;
540 r.maxSpeed = ms;
541 return r;
542 }
543
544 private:
545 SField base_, imposed_, app_, ucl_, uclS_, wgt_, ca_, stateD_;
546 SField uc_[3], th_[3];
547};
548
549} // namespace peclet::flow::vof
550
551#endif // PECLET_FLOW_VOF_WETTING_DYNAMIC_HPP
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
SField contactAngle() const
The prescribed contact angle per cell, in RADIANS, measured through the liquid.
UCField cellKind() const
The cell classification (VofCellKind).
SField wettingNormal(int d) const
The fluid-only Youngs normal of each fluid cell (buildWettingNormals, then exchanged).
SField colour() const
Colour field on the extended block (x-fastest).
SField wallSdf() const
The SDF on this block, in cells, > 0 in fluid (the solver embeds + exchanges it).
int vofHysteresisBase(double thetaApp, double thetaA, double thetaR, double &base)
The hysteresis selector: which base angle the Cox-Voinov correction is applied to,...
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.
VofDynamicState
Which rule produced a band cell's imposed angle (vof_dynamic_field(4)).
@ kVofDynNone
not a contact cell (no anchor / no usable normal): theta = the 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)
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
flow — the multidimensional iteration policies: iteration order follows storage.
Census over the INNER region: cells per state, mean imposed / apparent angle (degrees) over the conta...
The driver: produces WyAdvector::contactAngle() from the current colour, geometry,...
SField stateField() const
VofDynamicState, as a double
SField cellVel(int d) const
cell-centre velocity (the solver fills it)
bool smooth
the 3-point in-wall mean of U_cl (ablation)
SField base() const
The STATIC per-cell base angle (radians) the selector starts from.
double muLiquid
the LIQUID dynamic viscosity used in Ca_cl
void measure(const WyAdvector &adv)
Pass A — the measurement.
SField speed() const
the SMOOTHED U_cl
bool hysteresis
theta_a / theta_r are configured
double pureEps
a donor this close to 0/1 carries no interface
double logRatio() const
ln(Delta/lambda) at Delta = 1 cell.
VofMetric metric
The anisotropic cell metric (Phase 3, V4.3); {1,1,1} == the pre-Phase-3 arithmetic.
double tanEps
below this the interface is parallel to the wall
double slip
lambda, in CELLS (Delta = 1 cell), so logRatio = -ln(slip)
Census census(const WyAdvector &adv) const
bool dynamic
the Cox-Voinov correction is configured
SField imposed() const
the angle written this fill (rad)
double sigma
the surface tension used in Ca_cl (the solver resolves it)
SField apparent() const
the measured apparent angle (rad)
void impose(const WyAdvector &adv)
Pass B — the 3-point in-wall smoothing of U_cl, the selector and the write of theta into adv....
static void cg(CCField x, CCConst b, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, Exch &&exchange, const Reduce &red, long gCells, int iters)