flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
momentum_advect.hpp
Go to the documentation of this file.
1
124#ifndef PECLET_FLOW_VOF_MOMENTUM_ADVECT_HPP
125#define PECLET_FLOW_VOF_MOMENTUM_ADVECT_HPP
126
127#include <Kokkos_Core.hpp>
128#include <stdexcept>
129
130#include "policy.hpp"
131#include "vof/advect_wy.hpp"
132#include "vof/cutcell.hpp"
133
134namespace peclet::flow::vof {
135
139KOKKOS_INLINE_FUNCTION double vofPhaseRho(double rhoG, double rhoL, double c) {
140 return rhoG * (1.0 - c) + rhoL * c;
141}
142
143KOKKOS_INLINE_FUNCTION double vofMinmod(double a, double b) {
144 if (!(a * b > 0.0))
145 return 0.0;
146 return Kokkos::fabs(a) < Kokkos::fabs(b) ? a : b;
147}
148
152KOKKOS_INLINE_FUNCTION double vofCellBox(const SField& c, const SField& mx, const SField& my,
153 const SField& mz, const SField& al, long q,
154 const double lo[3], const double hi[3], double eps = 0.0) {
155 const double cq = c(q);
156 const double vol = (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2]);
157 // WO-R2 item 4: the SAME wisp tolerance the colour reconstruction used, or this reads a plane
158 // the reconstruction pass never wrote for a wisp cell.
159 if (!wyIsMixed(cq, eps))
160 return cq * vol;
161 return plicBoxVolume(mx(q), my(q), mz(q), al(q), lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]);
162}
163
168 public:
169 struct Diagnostics {
170 double minCc[3] = {0, 0, 0};
171 double maxCc[3] = {0, 0, 0};
172 double minRhoC = 0.0;
173 long floored = 0;
174 long clamped = 0;
175 double sumM[3] = {0, 0, 0};
176 double sumM0[3] = {0, 0, 0};
177 };
178
182 void init(const WyAdvector& w, double rhoG, double rhoL) {
183 if (!(rhoG > 0.0) || !(rhoL > 0.0))
184 throw std::invalid_argument(
185 "peclet::flow::vof::MomentumConsistentAdvector: both phase densities must be > 0");
186 n_ = w.inner();
187 e_ = w.extent();
188 g_ = w.ghost();
189 len_ = static_cast<long>(e_.x) * e_.y * e_.z;
190 rhoG_ = rhoG;
191 rhoL_ = rhoL;
192 for (int c = 0; c < 3; ++c) {
193 cc_[c] = SField("vof::Cc", len_);
194 vel_[c] = SField("vof::uAdv", len_);
195 flag_[c] = UCField("vof::ccFlag", len_);
196 }
197 fluxC_ = SField("vof::momFluxC", len_);
198 fluxU_ = SField("vof::momFluxU", len_);
199 aFace_ = SField("vof::momAFace", len_);
200 }
201
202 bool initialized() const { return len_ > 0; }
203 void setPhaseDensities(double rhoG, double rhoL) {
204 rhoG_ = rhoG;
205 rhoL_ = rhoL;
206 }
208 SField advectedVelocity(int e) const { return vel_[e]; }
210 SField shiftedColour(int e) const { return cc_[e]; }
211
214 double rhoFloor = 0.0;
215 double rhoFloorFrac = 1e-6;
232 bool momentumMuscl = false;
239 bool clampFluxes = true;
240
241 double phaseRhoG() const { return rhoG_; }
242 double phaseRhoL() const { return rhoL_; }
243 double lastRhoFloor() const { return floorUsed_; }
244 long clampedFluxes() const { return clamped_; }
245
250 void advect(WyAdvector& w, double dt, long step) {
251 if (!initialized())
252 throw std::runtime_error(
253 "peclet::flow::vof::MomentumConsistentAdvector: init() was never called");
254 if (!(w.cflLimit < 0.5))
255 throw std::runtime_error(
256 "peclet::flow::vof::MomentumConsistentAdvector: the shifted control volume needs "
257 "|u| dt/h < 1/2 strictly (its flux slab lives inside ONE half cell), so the "
258 "Weymouth-Yue cap must be < 0.5 - the default 0.25 is the proven 3D bound");
259 w.requireExchange();
260 w.checkCourant(dt); // same guard, same lastCfl_, as WyAdvector::advect
261 const double dth = dt / w.h();
262 clamped_ = 0;
263 floored_ = 0;
264
265 w.freezeDilationFlag(); // the pressure-cell flag, for the colour update
266 w.reconstruct(); // planes of C^n: what C^e is clipped out of
267 if (w.hasGeometry())
269 else
272
273 const int* perm = kWySweepPerm[static_cast<int>(step % 6)];
274 for (int s = 0; s < 3; ++s) {
275 const int d = perm[s];
276 // The flux at the `-d` face of the first owned CV reads the donor one cell below, so C^e
277 // needs a valid ghost ring for the clamp of point 3. C^e is an ordinary cell field on this
278 // block (see the Indexing note), so the colour field's own exchange carries it.
279 for (int e = 0; e < 3; ++e) {
280 w.exchangeTimed(cc_[e]);
281 w.exchangeTimed(vel_[e]);
282 }
283 w.reconstruct(); // idempotent for s = 0; re-reconstructs after each applied sweep
284 w.computeFluxes(d, dth);
285 for (int e = 0; e < 3; ++e) {
286 if (w.hasGeometry()) { // rung V5a: openness-weighted, in fluid-volume units
287 momentumFluxesCut(w, d, e, dth);
288 momentumUpdateCut(w, d, e);
289 } else {
290 momentumFluxes(w, d, e, dth);
291 momentumUpdate(w, d, e);
292 }
293 }
294 w.applySweep(d, dth);
295 w.exchangeTimed(w.colour());
296 }
297 }
298
301 const I3 e = e_, n = n_;
302 const int g = g_;
303 const double rg = rhoG_, rl = rhoL_;
304 using MD = MDRange3<SExec>;
305 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
306 double gmin = 1e300;
307 for (int c = 0; c < 3; ++c) {
308 SField cf = cc_[c], mf = vel_[c];
309 double mn = 0.0, mx = 0.0, sm = 0.0, rmin = 0.0;
310 Kokkos::parallel_reduce(
311 "vof::mom::min", pol,
312 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
313 acc = Kokkos::fmin(acc, cf(L3(x, y, z, e)));
314 },
315 Kokkos::Min<double>(mn));
316 Kokkos::parallel_reduce(
317 "vof::mom::max", pol,
318 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
319 acc = Kokkos::fmax(acc, cf(L3(x, y, z, e)));
320 },
321 Kokkos::Max<double>(mx));
322 Kokkos::parallel_reduce(
323 "vof::mom::sum", pol,
324 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
325 const long i = L3(x, y, z, e);
326 acc += vofPhaseRho(rg, rl, cf(i)) * mf(i);
327 },
328 sm);
329 Kokkos::parallel_reduce(
330 "vof::mom::rhomin", pol,
331 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
332 acc = Kokkos::fmin(acc, vofPhaseRho(rg, rl, cf(L3(x, y, z, e))));
333 },
334 Kokkos::Min<double>(rmin));
335 Kokkos::fence();
336 dg.minCc[c] = mn;
337 dg.maxCc[c] = mx;
338 dg.sumM[c] = sm;
339 gmin = gmin < rmin ? gmin : rmin;
340 }
341 dg.minRhoC = gmin;
342 dg.floored = floored_;
343 dg.clamped = clamped_;
344 for (int c = 0; c < 3; ++c)
345 dg.sumM0[c] = seedM_[c];
346 return dg;
347 }
348
349 public:
350 // ---- implementation detail (public for the same nvcc reason as in WyAdvector) ----------------
351
354 const I3 e = e_, n = n_;
355 const int g = g_;
356 SField c = w.colour(), mx = w.planeM(0), my = w.planeM(1), mz = w.planeM(2),
357 al = w.planeAlpha();
358 const double weps = w.wispEps; // WO-R2 item 4: the colour reconstruction's own tolerance
359 using MD = MDRange3<SExec>;
360 for (int comp = 0; comp < 3; ++comp) {
361 const long se = strideOf(comp);
362 const int ec = comp;
363 SField out = cc_[comp];
364 Kokkos::parallel_for(
365 "vof::mom::shifted_colour", MD(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
366 KOKKOS_LAMBDA(int x, int y, int z) {
367 const long i = L3(x, y, z, e);
368 double lo[3] = {0.0, 0.0, 0.0}, hi[3] = {1.0, 1.0, 1.0};
369 lo[ec] = 0.5;
370 hi[ec] = 1.0;
371 const double hiHalf = vofCellBox(c, mx, my, mz, al, i - se, lo, hi, weps);
372 lo[ec] = 0.0;
373 hi[ec] = 0.5;
374 const double loHalf = vofCellBox(c, mx, my, mz, al, i, lo, hi, weps);
375 out(i) = hiHalf + loHalf;
376 });
377 }
378 Kokkos::fence();
379 }
380
383 const I3 e = e_, n = n_;
384 const int g = g_;
385 const bool cellFlag = useCellDilationFlag;
386 UCField ccFlag = w.dilationFlag();
387 using MD = MDRange3<SExec>;
388 const double rg = rhoG_, rl = rhoL_;
389 for (int comp = 0; comp < 3; ++comp) {
390 const long se = strideOf(comp);
391 SField cf = cc_[comp], mf = vel_[comp], uf = w.faceVel(comp);
392 UCField fl = flag_[comp];
393 Kokkos::parallel_for(
394 "vof::mom::freeze", MD(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
395 KOKKOS_LAMBDA(int x, int y, int z) {
396 const long i = L3(x, y, z, e);
397 fl(i) = cellFlag ? ccFlag(i) : (cf(i) > 0.5 ? 1u : 0u);
398 mf(i) = uf(i - se); // uf_e(i-s_e) IS the velocity at control volume i
399 });
400 // The momentum census at the START of this step's advection, so the conservation claim can be
401 // made about the ADVECTION rather than about the whole coupled step (the momentum solve and
402 // the projection both change u afterwards, and they conserve `rho_f u` — the arithmetic face
403 // mean — not `rho^e u`).
404 double sm = 0.0;
405 Kokkos::parallel_reduce(
406 "vof::mom::seedsum", MD(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
407 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
408 const long i = L3(x, y, z, e);
409 acc += vofPhaseRho(rg, rl, cf(i)) * mf(i);
410 },
411 sm);
412 Kokkos::fence();
413 seedM_[comp] = sm;
414 }
415 Kokkos::fence();
416 }
417
422 void momentumFluxes(const WyAdvector& w, int d, int comp, double dth) {
423 const I3 e = e_;
424 const int g = g_;
425 const long sd = strideOf(d), se = strideOf(comp);
426 const int dd = d, ec = comp;
427 const double rg = rhoG_, rl = rhoL_;
428 const bool muscl = momentumMuscl, clamp = clampFluxes;
429 SField c = w.colour(), mx = w.planeM(0), my = w.planeM(1), mz = w.planeM(2),
430 al = w.planeAlpha();
431 const double weps = w.wispEps;
432 SField ud = w.faceVel(d), cvU = vel_[comp], cvC = cc_[comp];
433 SField fc = fluxC_, fu = fluxU_, af = aFace_;
434 int lo3[3] = {g, g, g};
435 const int hi3[3] = {g + n_.x, g + n_.y, g + n_.z};
436 lo3[d] -= 1; // the `-d` face of the first owned CV is the `+d` face of the CV below it
437 long nclamp = 0;
438 Kokkos::parallel_reduce(
439 "vof::mom::flux",
440 MDRange3<SExec>(SExec(), {lo3[0], lo3[1], lo3[2]}, {hi3[0], hi3[1], hi3[2]}),
441 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
442 const long p = L3(x, y, z, e);
443 // The shifted CV's face velocity is the `comp`-average of the two staggered faces; summed
444 // over the three sweeps this is 1/2 (div_{p-s_e} + div_p) — zero for a projected field.
445 const double a = 0.5 * (ud(p - se) + ud(p)) * dth;
446 af(p) = a;
447 if (a == 0.0) {
448 fc(p) = 0.0;
449 fu(p) = cvU(p); // any finite value; the update multiplies it by a == F == 0
450 return;
451 }
452 const double aa = a > 0.0 ? a : -a;
453 double lo[3] = {0.0, 0.0, 0.0}, hi[3] = {1.0, 1.0, 1.0};
454 double F;
455 if (dd == ec) {
456 // The CV face sits at the CENTRE of cell p; the |a|-deep slab therefore lives inside
457 // that one cell's upwind half. |a| < 1/2 keeps it there.
458 if (a > 0.0) {
459 lo[ec] = 0.5 - aa;
460 hi[ec] = 0.5;
461 } else {
462 lo[ec] = 0.5;
463 hi[ec] = 0.5 + aa;
464 }
465 F = vofCellBox(c, mx, my, mz, al, p, lo, hi, weps);
466 } else {
467 // Transverse sweep: the CV face is the pressure cells' own `d` face, and the donor slab
468 // is the union of the `+e` half of one cell and the `-e` half of its `e` neighbour.
469 long q;
470 if (a > 0.0) {
471 q = p;
472 lo[dd] = 1.0 - aa;
473 hi[dd] = 1.0;
474 } else {
475 q = p + sd;
476 lo[dd] = 0.0;
477 hi[dd] = aa;
478 }
479 lo[ec] = 0.5;
480 hi[ec] = 1.0;
481 const double hiHalf = vofCellBox(c, mx, my, mz, al, q - se, lo, hi, weps);
482 lo[ec] = 0.0;
483 hi[ec] = 0.5;
484 const double loHalf = vofCellBox(c, mx, my, mz, al, q, lo, hi, weps);
485 F = hiHalf + loHalf;
486 }
487 // Weymouth's admissible interval on the DONOR control volume's own colour (thesis
488 // Appendix A) — the hypothesis of his boundedness proof. Collapses to the algebraic value
489 // for a full or empty donor, so full/empty CVs stay exactly stationary.
490 if (clamp) {
491 const double cd = cvC(a > 0.0 ? p : p + sd);
492 const double up = aa < cd ? aa : cd;
493 const double dn = aa - (1.0 - cd);
494 const double lob = dn > 0.0 ? dn : 0.0;
495 const double Fc = F > up ? up : (F < lob ? lob : F);
496 if (Fc != F)
497 ++acc;
498 F = Fc;
499 }
500 if (a < 0.0)
501 F = -F;
502 // Donor-CV velocity, MinMod-limited to the flux slab's centroid. A uniform field gives
503 // both MinMod arguments exactly 0, hence u^ = u bit for bit — the uniform-velocity gate.
504 const long don = a > 0.0 ? p : p + sd;
505 double slope = 0.0;
506 if (muscl)
507 slope = vofMinmod(cvU(don) - cvU(don - sd), cvU(don + sd) - cvU(don));
508 const double sgn = a > 0.0 ? 1.0 : -1.0;
509 // Only the FACE quantities are stored — the liquid flux, the Courant number and the
510 // donor velocity. The mass flux `rho_g a + (rho_l - rho_g) F` and the momentum flux are
511 // assembled in `momentumUpdate`, in a form that cancels exactly; see the note there.
512 fc(p) = F;
513 fu(p) = cvU(don) + sgn * (0.5 - 0.5 * aa) * slope;
514 },
515 nclamp);
516 Kokkos::fence();
517 clamped_ += nclamp;
518 }
519
522 void momentumUpdate(const WyAdvector& w, int d, int comp) {
523 const I3 e = e_, n = n_;
524 const int g = g_;
525 const long sd = strideOf(d), se = strideOf(comp);
526 const double rg = rhoG_, rl = rhoL_, dr = rhoL_ - rhoG_;
527 const double rmin = rhoG_ < rhoL_ ? rhoG_ : rhoL_;
528 const double fl0 = rhoFloor > 0.0 ? rhoFloor : rhoFloorFrac * rmin;
529 floorUsed_ = fl0;
530 SField cf = cc_[comp], vf = vel_[comp], uf = w.faceVel(comp);
531 SField fc = fluxC_, fu = fluxU_, af = aFace_;
532 UCField fl = flag_[comp];
533 long nfloor = 0;
534 Kokkos::parallel_reduce(
535 "vof::mom::update", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
536 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
537 const long i = L3(x, y, z, e);
538 const double aP = af(i), aM = af(i - sd);
539 const unsigned char k = fl(i);
540 const double dil = k ? (aP - aM) : 0.0;
541 // The colour increment, formed ONCE and reused by the momentum below: that literal
542 // sharing is what makes the uniform-velocity identity exact rather than merely true in
543 // exact arithmetic.
544 const double dC = (fc(i - sd) - fc(i)) + dil;
545 const double cNew = cf(i) + dC;
546 cf(i) = cNew;
547 // The conservative update of this control volume is
548 // rho_new u_new = rho_old u_old - (Psi_+ u^_+ - Psi_- u^_-) + rho^ u_old (a_+ - a_-)
549 // with `Psi = rho_g a + drho F` the mass flux and `u^` the donor reconstruction of the
550 // CURRENT velocity. Stored and evolved as `rho^e u_e`, that is a sum of rho_l-scaled
551 // terms whose result is only rho_g-scaled in a gas control volume, and a volume a
552 // directional sweep fills and the next one empties passes through an intermediate `rho^e`
553 // a factor `ratio` larger than where it starts and ends; the rounding of that excursion
554 // is eps*ratio*|a| and does NOT cancel against the colour's own. Measured, it degraded
555 // the uniform-velocity gate LINEARLY IN THE DENSITY RATIO (6.7e-16 at 1e2 -> 3.2e-14 at
556 // 1e4) — wearing exactly the signature of the defect this rung exists to remove, for a
557 // purely floating-point reason.
558 //
559 // Subtracting `rho_new u_old` from both sides is algebraically free (substitute
560 // `rho_old = rho_new - drho dC` and watch every `u_old` term cancel) and removes it:
561 //
562 // u_new = u_old + [ rho_g (a_- dv_- - a_+ dv_+) + drho (F_- dv_- - F_+ dv_+)
563 // ]/rho_new dv_+/- = u^_+/- - u_old
564 //
565 // Every term is a DIFFERENCE OF VELOCITIES. For a uniform field each is exactly zero (the
566 // same double, subtracted), so `u_new == u_old` bit for bit at ANY density ratio and for
567 // any dC — the gate is flat in the ratio by construction, not by tolerance. This is still
568 // the conservative scheme, with `rho_new` the advected colour's own density; only the
569 // arithmetic is conditioned. (An earlier version fluxed the FROZEN u^n instead of the
570 // current velocity; the `drho dC (u^n_i - u_old)/rho_new` term that then survives has
571 // gain `ratio` on a control volume a sweep empties, and it amplified the solver's own
572 // 1e-7 float-storage noise to 1e-7 at ratio 1e4 while ratio 1e3 was clean — the gain, not
573 // a defect in the fluxes. Transporting the current state removes the term identically and
574 // is also the faithful directional split.)
575 const double uo = vf(i);
576 const double dvM = fu(i - sd) - uo, dvP = fu(i) - uo;
577 // The DILATION coefficient is `rho^ u^n`, with `u^n` the velocity FROZEN at the start of
578 // the step — the exact analogue of Weymouth-Yue freezing `H(C^n - 1/2)`, and required for
579 // the same reason. Summed over the three sweeps the dilation contributes
580 // `rho^ u (a_+ - a_-)` per sweep, and only a coefficient that is a CONSTANT OF THE STEP
581 // factors out of that sum to leave `rho^ u * div = 0`. With the running velocity there
582 // instead, the three sweeps see three different `u` and the cancellation is lost:
583 // MEASURED, per-step momentum conservation degraded to 1.4e-7 relative — first order in
584 // dt, not round-off — while the colour, whose flag IS frozen, stayed exact. Written as
585 // `rho^ (u^n - u_old)` because the `rho^ u_old` part has already been absorbed into the
586 // deviation form above; for a uniform field `u^n == u_old` and the term is exactly zero,
587 // so the consistency identity is untouched.
588 const double dilCoef = (k ? rl : rg) * (uf(i - se) - uo) * (aP - aM);
589 const double dev =
590 rg * (aM * dvM - aP * dvP) + dr * (fc(i - sd) * dvM - fc(i) * dvP) + dilCoef;
591 const double rn = vofPhaseRho(rg, rl, cNew);
592 if (rn < fl0)
593 ++acc;
594 vf(i) = uo + dev / (rn < fl0 ? fl0 : rn);
595 },
596 nfloor);
597 Kokkos::fence();
598 floored_ += nfloor;
599 }
600
601 // ---- rung V5a (WO-Q item 8): the same construction on a CUT control volume -------------------
602 // Siblings of the three kernels above, reached only through `w.hasGeometry()`. The uncut bodies
603 // are untouched, so a solid-free run is byte-identical by construction.
604
609 const I3 e = e_, n = n_;
610 const int g = g_;
611 SField c = w.colour(), mx = w.planeM(0), my = w.planeM(1), mz = w.planeM(2),
612 al = w.planeAlpha(), ep = w.epsFraction();
613 UCField kd = w.cellKind();
614 const double weps = w.wispEps;
615 using MD = MDRange3<SExec>;
616 for (int comp = 0; comp < 3; ++comp) {
617 const long se = strideOf(comp);
618 const int ec = comp;
619 SField out = cc_[comp];
620 Kokkos::parallel_for(
621 "vof::mom::shifted_colour_cut", MD(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
622 KOKKOS_LAMBDA(int x, int y, int z) {
623 const long i = L3(x, y, z, e);
624 const double ea = vofEpsEff(ep(i - se), kd(i - se) == kVofSolid);
625 const double eb = vofEpsEff(ep(i), kd(i) == kVofSolid);
626 const double ecv = 0.5 * (ea + eb);
627 if (!(ecv > 0.0)) {
628 out(i) = 0.0; // the whole control volume is inside the solid
629 return;
630 }
631 double lo[3] = {0.0, 0.0, 0.0}, hi[3] = {1.0, 1.0, 1.0};
632 lo[ec] = 0.5;
633 hi[ec] = 1.0;
634 const double hiHalf = vofCellBox(c, mx, my, mz, al, i - se, lo, hi, weps);
635 lo[ec] = 0.0;
636 hi[ec] = 0.5;
637 const double loHalf = vofCellBox(c, mx, my, mz, al, i, lo, hi, weps);
638 out(i) = (ea * hiHalf + eb * loHalf) / ecv;
639 });
640 }
641 Kokkos::fence();
642 }
643
645 KOKKOS_INLINE_FUNCTION static double cvEps(const SField& ep, const UCField& kd, long i, long se) {
646 return 0.5 *
647 (vofEpsEff(ep(i - se), kd(i - se) == kVofSolid) + vofEpsEff(ep(i), kd(i) == kVofSolid));
648 }
649
653 void momentumFluxesCut(const WyAdvector& w, int d, int comp, double dth) {
654 const I3 e = e_;
655 const int g = g_;
656 const long sd = strideOf(d), se = strideOf(comp);
657 const int dd = d, ec = comp;
658 const bool muscl = momentumMuscl, clamp = clampFluxes;
659 SField c = w.colour(), mx = w.planeM(0), my = w.planeM(1), mz = w.planeM(2),
660 al = w.planeAlpha(), ep = w.epsFraction(), od = w.faceOpenness(d);
661 UCField kd = w.cellKind();
662 const double weps = w.wispEps;
663 SField ud = w.faceVel(d), cvU = vel_[comp], cvC = cc_[comp];
664 SField fc = fluxC_, fu = fluxU_, af = aFace_;
665 int lo3[3] = {g, g, g};
666 const int hi3[3] = {g + n_.x, g + n_.y, g + n_.z};
667 lo3[d] -= 1;
668 long nclamp = 0;
669 Kokkos::parallel_reduce(
670 "vof::mom::flux_cut",
671 MDRange3<SExec>(SExec(), {lo3[0], lo3[1], lo3[2]}, {hi3[0], hi3[1], hi3[2]}),
672 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
673 const long p = L3(x, y, z, e);
674 // The CV's face open area: the AXIAL face is the cell-centre plane of cell p, so its
675 // fluid area fraction is that cell's fluid VOLUME fraction; a transverse face is the two
676 // half faces of cells p - s_e and p, so it is their average.
677 const double ofac =
678 (dd == ec) ? vofEpsEff(ep(p), kd(p) == kVofSolid) : 0.5 * (od(p - se) + od(p));
679 const double a = 0.5 * (ud(p - se) + ud(p)) * dth;
680 af(p) = ofac * a;
681 if (a == 0.0 || ofac == 0.0) {
682 fc(p) = 0.0;
683 fu(p) = cvU(p);
684 return;
685 }
686 const double aa = a > 0.0 ? a : -a;
687 double lo[3] = {0.0, 0.0, 0.0}, hi[3] = {1.0, 1.0, 1.0};
688 double F;
689 if (dd == ec) {
690 if (a > 0.0) {
691 lo[ec] = 0.5 - aa;
692 hi[ec] = 0.5;
693 } else {
694 lo[ec] = 0.5;
695 hi[ec] = 0.5 + aa;
696 }
697 F = vofCellBox(c, mx, my, mz, al, p, lo, hi, weps);
698 } else {
699 long q;
700 if (a > 0.0) {
701 q = p;
702 lo[dd] = 1.0 - aa;
703 hi[dd] = 1.0;
704 } else {
705 q = p + sd;
706 lo[dd] = 0.0;
707 hi[dd] = aa;
708 }
709 lo[ec] = 0.5;
710 hi[ec] = 1.0;
711 const double ea = vofEpsEff(ep(q - se), kd(q - se) == kVofSolid);
712 const double eb = vofEpsEff(ep(q), kd(q) == kVofSolid);
713 const double ecv = 0.5 * (ea + eb);
714 const double hiHalf = vofCellBox(c, mx, my, mz, al, q - se, lo, hi, weps);
715 lo[ec] = 0.0;
716 hi[ec] = 0.5;
717 const double loHalf = vofCellBox(c, mx, my, mz, al, q, lo, hi, weps);
718 // the same fluid-volume weighting as C^e, renormalized so a uniformly liquid field
719 // gives F = |a| exactly (and hence, times `ofac`, the dilation's own `o|a|`)
720 F = ecv > 0.0 ? (ea * hiHalf + eb * loHalf) / ecv : 0.0;
721 }
722 F *= ofac;
723 if (clamp) {
724 const long don = a > 0.0 ? p : p + sd;
725 const double cd = cvC(don);
726 const double sweep = ofac * aa;
727 const double ecvDon = cvEps(ep, kd, don, se);
728 const double Fc = vofCutFluxClamp(F, sweep, ecvDon, cd);
729 if (Fc != F)
730 ++acc;
731 F = Fc;
732 }
733 if (a < 0.0)
734 F = -F;
735 const long don = a > 0.0 ? p : p + sd;
736 double slope = 0.0;
737 if (muscl)
738 slope = vofMinmod(cvU(don) - cvU(don - sd), cvU(don + sd) - cvU(don));
739 const double sgn = a > 0.0 ? 1.0 : -1.0;
740 fc(p) = F;
741 fu(p) = cvU(don) + sgn * (0.5 - 0.5 * aa) * slope;
742 },
743 nclamp);
744 Kokkos::fence();
745 clamped_ += nclamp;
746 }
747
751 void momentumUpdateCut(const WyAdvector& w, int d, int comp) {
752 const I3 e = e_, n = n_;
753 const int g = g_;
754 const long sd = strideOf(d), se = strideOf(comp);
755 const double rg = rhoG_, rl = rhoL_, dr = rhoL_ - rhoG_;
756 const double rmin = rhoG_ < rhoL_ ? rhoG_ : rhoL_;
757 const double fl0 = rhoFloor > 0.0 ? rhoFloor : rhoFloorFrac * rmin;
758 floorUsed_ = fl0;
759 SField cf = cc_[comp], vf = vel_[comp], uf = w.faceVel(comp), ep = w.epsFraction();
760 UCField kd = w.cellKind();
761 SField fc = fluxC_, fu = fluxU_, af = aFace_;
762 UCField fl = flag_[comp];
763 long nfloor = 0;
764 Kokkos::parallel_reduce(
765 "vof::mom::update_cut", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
766 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
767 const long i = L3(x, y, z, e);
768 const double ecv = cvEps(ep, kd, i, se);
769 if (!(ecv > 0.0))
770 return; // the control volume is entirely inside the solid
771 const double aP = af(i), aM = af(i - sd);
772 const unsigned char k = fl(i);
773 const double dil = k ? (aP - aM) : 0.0;
774 const double dC = ((fc(i - sd) - fc(i)) + dil) / ecv;
775 const double cNew = cf(i) + dC;
776 cf(i) = cNew;
777 const double uo = vf(i);
778 const double dvM = fu(i - sd) - uo, dvP = fu(i) - uo;
779 const double dilCoef = (k ? rl : rg) * (uf(i - se) - uo) * (aP - aM);
780 const double dev =
781 rg * (aM * dvM - aP * dvP) + dr * (fc(i - sd) * dvM - fc(i) * dvP) + dilCoef;
782 const double rn = vofPhaseRho(rg, rl, cNew);
783 if (rn < fl0)
784 ++acc;
785 vf(i) = uo + dev / (ecv * (rn < fl0 ? fl0 : rn));
786 },
787 nfloor);
788 Kokkos::fence();
789 floored_ += nfloor;
790 }
791
792 long strideOf(int d) const {
793 return d == 0 ? 1 : (d == 1 ? static_cast<long>(e_.x) : static_cast<long>(e_.x) * e_.y);
794 }
795
796 private:
797 I3 n_{0, 0, 0}, e_{0, 0, 0};
798 int g_ = 3;
799 long len_ = 0;
800 double rhoG_ = 1.0, rhoL_ = 1.0, floorUsed_ = 0.0;
801 long floored_ = 0, clamped_ = 0;
802 double seedM_[3] = {0, 0, 0};
803 SField cc_[3], vel_[3], fluxC_, fluxU_, aFace_;
804 UCField flag_[3];
805};
806
807} // namespace peclet::flow::vof
808
809#endif // PECLET_FLOW_VOF_MOMENTUM_ADVECT_HPP
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
Momentum-consistent companion to WyAdvector.
bool clampFluxes
Ablation: drop the Weymouth flux clamp of point 3 (the raw geometric flux).
void momentumUpdate(const WyAdvector &w, int d, int comp)
C^e_i += (F_- - F_+) + flag (a_+ - a_-) and M_i += (G_- - G_+) + rho^ u^n_i (a_+ - a_-),...
double rhoFloor
Floor on rho^e in the recovery divide, in the same units as the phase densities.
void freezeShiftedState(const WyAdvector &w)
Freeze flag^e = H(C^{e,n} - 1/2) and seed M = rho^e u^n_e.
void momentumFluxesCut(const WyAdvector &w, int d, int comp, double dth)
Face Courant number, geometric liquid flux and momentum flux on a CUT shifted CV.
SField advectedVelocity(int e) const
The advected velocity of component e, indexed exactly like the solver's u_e (inner cells).
void momentumFluxes(const WyAdvector &w, int d, int comp, double dth)
Face Courant number, geometric liquid flux and momentum flux of the shifted CV of component comp,...
bool momentumMuscl
Opt-in MinMod-limited linear reconstruction of the donor velocity in the momentum flux,...
bool useCellDilationFlag
Ablation: use the PRESSURE-cell frozen dilation flag H(C^n - 1/2) on the shifted CV instead of the st...
void momentumUpdateCut(const WyAdvector &w, int d, int comp)
The cut update: both increments divided by the CV's fluid volume, everything else identical — in part...
void buildShiftedColour(const WyAdvector &w)
C^e(i) = slab(cell i-s_e, e, [1/2,1]) + slab(cell i, e, [0,1/2]), over the inner region.
void setPhaseDensities(double rhoG, double rhoL)
static double cvEps(const SField &ep, const UCField &kd, long i, long se)
The CV's fluid volume eps^e(i) (0 when the whole CV is solid).
void advect(WyAdvector &w, double dt, long step)
One momentum-consistent Weymouth-Yue step: advances BOTH the colour field inside w and the half-shift...
void init(const WyAdvector &w, double rhoG, double rhoL)
SField shiftedColour(int e) const
The half-shifted colour of component e, same indexing.
void buildShiftedColourCut(const WyAdvector &w)
C^e = [eps(i-s_e) slabHi(i-s_e) + eps(i) slabLo(i)] / eps^e(i) — each half cell's PLIC slab weighted ...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
flow — thin shim: the container-free VoF cut-cell colour-transport rules (rung V5a) now live in core.
constexpr int kWySweepPerm[6][3]
The six permutations of (x, y, z), cycled by step index so no direction is systematically favoured (p...
bool wyIsMixed(double c)
The mixed-cell predicate.
Definition advect_wy.hpp:95
double vofPhaseRho(double rhoG, double rhoL, double c)
Two-phase density from a fraction, written so that C = 1 returns rho_l and C = 0 returns rho_g EXACTL...
double vofMinmod(double a, double b)
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
double vofCellBox(const SField &c, const SField &mx, const SField &my, const SField &mz, const SField &al, long q, const double lo[3], const double hi[3], double eps=0.0)
Liquid volume of the PLIC polyhedron of cell q inside a box given as per-axis intervals,...
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.
long clamped
fluxes Weymouth's admissible interval had to clamp, last step
long floored
inner CVs where the rho^e floor bit in the last recovery
double sumM0[3]
the same sum at the START of the last advection (seeding)
double sumM[3]
sum of rho^e u_e over inner CVs AFTER the last advection
static constexpr double F
static double perm(const std::vector< double > &gsdf, const BlockDecomposer< 3 > &dec, int rank, bool amg, int levels, MPI_Comm comm)