flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_vof.hpp
Go to the documentation of this file.
1
9#ifndef PECLET_FLOW_FLOW_IBM_VOF_HPP
10#define PECLET_FLOW_FLOW_IBM_VOF_HPP
11
12namespace peclet::flow {
13
19inline constexpr const char* kVofMomBlocksMsg =
20 "enable_vof_momentum and enable_vof_blocks* are not composable: momentum-consistent transport "
21 "advects rho*u with the geometric fluxes of the ONE structured colour field (at the head of "
22 "step()) and skips the colour stage that advects the block markers, so the markers would never "
23 "move and the momentum fluxes would not be theirs. Use the structured colour field with "
24 "enable_vof_momentum, or the block container without it.";
25
26template <class Grid>
28 vofTiming_ = on;
29 vofAdv_.timingOn = on;
30 vofCurv_.timingOn = on;
31 if (vofBlocks_)
32 vofBlocks_->timingOn = on;
34}
35
36template <class Grid>
38 return vofTiming_;
39}
40
41template <class Grid>
43 vt_ = VofTiming();
44 vofAdv_.resetTiming();
45 vofCurv_.resetTiming();
46 if (vofBlocks_) {
47 vofBlocks_->debrisSeconds = 0.0;
48 vofBlocks_->debrisCalls = 0;
49 }
50 tStepSum_ = tPredSum_ = tMomSum_ = tProjSum_ = 0.0;
51}
52
53template <class Grid>
55 return vt_;
56}
57
58template <class Grid>
60 return vofAdv_.timing();
61}
62
63template <class Grid>
65 return vofCurv_.timing();
66}
67
68template <class Grid>
72
73template <class Grid>
75 return vofCurv_.useWorklist;
76}
77
78template <class Grid>
80 return tStepSum_;
81}
82
83template <class Grid>
85 return tPredSum_;
86}
87
88template <class Grid>
90 return tMomSum_;
91}
92
93template <class Grid>
95 return tProjSum_;
96}
97
98template <class Grid>
100 vofAdv_.useWorklist = on;
101}
102
103template <class Grid>
105 return vofAdv_.useWorklist;
106}
107
108template <class Grid>
110 vofAdv_.init(nx_, ny_, nz_, 1.0, kVofG); // h = 1: flow works in cell units
111 vofAdv_.cflLimit = vofCflLimit_;
112 vofAdv_.interfaceLocalCfl = true; // WO-J item 4 — see maxCourantInterface()
113 const I3 e3 = vofAdv_.extent();
114 e3_ = C3{e3.x, e3.y, e3.z};
115 vofAdv_.exchange = [this](CCField f) { this->vofFillGhosts(f); };
116 vofAdv_.globalMax = nullptr;
117#ifdef PECLET_FLOW_MPI
118 if (distributed_ && dec_) {
119 int rank = 0;
120 MPI_Comm_rank(comm_, &rank);
121 // Periodic on all three axes, exactly like the velocity halo: the halo owns every interior
122 // ghost and wraps the global boundary, and vofFillGhosts then overwrites the out-of-domain
123 // ghosts of any non-periodic axis with the globally-clamped value.
124 std::array<bool, 3> per{true, true, true};
125 vofHalo_ = std::make_shared<GridHaloTopology<3>>();
126 vofHalo_->buildTopology(*dec_, rank, kVofG, per, comm_);
127 vofDev_ = std::make_shared<GridHalo<double>>();
128 vofDev_->setLabel("vof g3");
129 vofDev_->init(*vofHalo_);
130 MPI_Comm cm = comm_;
131 vofAdv_.globalMax = [cm](double v) {
132 double r = v;
133 MPI_Allreduce(&v, &r, 1, MPI_DOUBLE, MPI_MAX, cm);
134 return r;
135 };
136 }
137#endif
138 buildVofGeometry(); // rung V5a (WO-Q): openness + fluid fraction on the colour block
139 // WO-R: the out-of-domain mask and the resampled boundary-colour profiles are properties of
140 // THIS block, so they are (re)built with it — and the mask is only INSTALLED on the advector
141 // when a VoF boundary colour has actually been set, which is what keeps the V1 flux path
142 // bit-identical otherwise (gate G5).
144 // The half-shifted momentum CVs live on this same block, so they are rebuilt with it.
145 vofCurv_.init(nx_, ny_, nz_, kVofG);
146 if (vofMomEnabled_) {
147 vofMom_.init(vofAdv_, vofRhoG_, vofRhoL_);
148 for (int c = 0; c < 3; ++c)
149 if (uAdv_[c].extent(0) != n_)
150 uAdv_[c] = CCField("uAdv", n_);
151 }
152 // The other drivers that live on THIS block and are initialised lazily: their own `ready()` /
153 // `initialized()` predicates ask "has init run", not "is it the right size", so a
154 // re-decomposition has to re-init them here or they keep the previous block's allocations
155 // while every kernel indexes them with the new `e3_`.
156 const std::size_t blockLen = (std::size_t)e3_.x * e3_.y * e3_.z;
157 if (pcAreaC_.ready() && pcAreaC_.area().extent(0) != blockLen)
158 pcAreaC_.init(nx_, ny_, nz_, kVofG);
159 if (pcAreaMc_.ready() && pcAreaMc_.area().extent(0) != blockLen)
160 pcAreaMc_.init(nx_, ny_, nz_, kVofG);
161 if (vofEnergy_.initialized() && vofEnergy_.temperature().extent(0) != blockLen)
162 vofEnergy_.init(vofAdv_, pcRcpG_, pcRcpL_);
163 bindVofBlockPatch(); // rung W0: the block exchange's Views were just reallocated
164}
165
166template <class Grid>
168 if (!vofEnabled_ || !contactAngleSet_)
169 return 0;
170 int m = 0;
171 for (int f = 0; f < 6; ++f)
172 if (bc_[f] == 1 || bc_[f] == 4)
173 m |= (1 << f);
174 return m;
175}
176
177template <class Grid>
179 int mask) {
180 const int gi[3] = {gx, gy, gz};
181 const int q[3] = {gs.x, gs.y, gs.z};
182 double d = 1e30;
183 for (int a = 0; a < 3; ++a) {
184 const double c = (double)gi[a] + 0.5;
185 if (mask & (1 << (2 * a)))
186 d = Kokkos::fmin(d, c);
187 if (mask & (1 << (2 * a + 1)))
188 d = Kokkos::fmin(d, (double)q[a] - c);
189 }
190 return d;
191}
192
193template <class Grid>
195 if (!vofEnabled_)
196 return;
197 const int wet = vofWetWallMask();
198 if ((!hasSolid_ || !cutcellPressure_) && !wet) {
199 vofAdv_.disableGeometry(); // all-fluid: the V1 kernels run byte-identically
200 vofAdv_.disableWetting();
201 vofSolidG2_ = CCField();
202 return;
203 }
204 if (wet && !cutcellPressure_)
205 throw std::runtime_error(
206 "set_contact_angle: a wetting DOMAIN wall (bc type 1/4) needs a cut-cell pressure "
207 "operator, because the colour transport weights its geometric fluxes with the face "
208 "openness. Build one with set_pressure_geometry(all_fluid_sdf) (all-fluid) or "
209 "set_solid(sdf, cutcell_pressure=True).");
210 vofAdv_.enableGeometry();
211 if (hasSolid_ && cutcellPressure_) {
212 if (vofCs_.extent(0) != n_)
213 vofCs_ = CCField("vofCs", n_);
214 buildCellFraction(vofCs_, CCConst(sdf_), e_,
215 G); // inner region; ghosts come from the exchange
216 copyInner(vofAdv_.epsFraction(), e3_, kVofG, CCConst(vofCs_), e_, G);
217 vofExchangeRaw(vofAdv_.epsFraction());
218 CCField oa[3] = {ox_, oy_, oz_};
219 for (int d = 0; d < 3; ++d) {
220 vof::copyFaceVelocity(vofAdv_.faceOpenness(d), I3{e3_.x, e3_.y, e3_.z}, kVofG, oa[d],
221 I3{e_.x, e_.y, e_.z}, G, d);
222 vofExchangeRaw(vofAdv_.faceOpenness(d));
223 }
224 } else {
225 // Domain walls only: the trivial geometry (every cell whole, every face open) is exactly
226 // what the uncut kernels compute, and `applyDomainWallGeometry` then closes the band.
227 Kokkos::deep_copy(vofAdv_.epsFraction(), 1.0);
228 for (int d = 0; d < 3; ++d)
229 Kokkos::deep_copy(vofAdv_.faceOpenness(d), 1.0);
230 }
231 if (wet)
233 vofAdv_.classifyGeometry();
234 vofExchangeRaw(vofAdv_.kindDouble()); // the owner's classification into every ghost layer
235 // ISSUES sweep item 3: that exchange ends in `clampFill`, whose zero-gradient copy would set
236 // the out-of-domain band back to the first INNER cell's classification (fluid) -- the band
237 // is the one region with no owner, so it has to be re-imposed here. Without this the whole
238 // domain-wall fill is silently inert (measured: contact census 0, equilibrium ~141 deg at
239 // every prescribed angle, i.e. the perfectly non-wetting empty band).
240 if (wet)
242 vofAdv_.finalizeGeometry();
243 // The G=2 mirror of the classification: the canonical "C" field reports EXACTLY 0 in solid
244 // cells (gate G2), while the g=3 working block carries the neutral band fill that the MYC and
245 // height-function stencils need. The fill is regenerated deterministically by every
246 // `vofFillGhosts`, so nothing is lost by not persisting it.
247 if (vofSolidG2_.extent(0) != n_)
248 vofSolidG2_ = CCField("vofSolidG2", n_);
249 copyInner(vofSolidG2_, e_, G, CCConst(vofAdv_.kindDouble()), e3_, kVofG);
250 applyContactAngle(); // rung V5b (WO-S): re-wire the theta field / wall SDF onto the new block
252}
253
254template <class Grid>
256 const I3 gs = vofGlobalSize(), org = vofOrigin();
257 const C3 e = e3_;
258 const int g = kVofG, mk = mask;
259 CCField ep = vofAdv_.epsFraction();
260 CCField of[3] = {vofAdv_.faceOpenness(0), vofAdv_.faceOpenness(1), vofAdv_.faceOpenness(2)};
261 CCField ofx = of[0], ofy = of[1], ofz = of[2];
262 Kokkos::parallel_for(
263 "peclet::flow::vof_domain_wall_geom", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {e.x, e.y, e.z}),
264 KOKKOS_LAMBDA(int x, int y, int z) {
265 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
266 const int gx = x - g + org.x, gy = y - g + org.y, gz = z - g + org.z;
267 auto inside = [&](int ax, int ay, int az) {
268 const int gi[3] = {ax, ay, az};
269 const int q[3] = {gs.x, gs.y, gs.z};
270 for (int a = 0; a < 3; ++a) {
271 if ((mk & (1 << (2 * a))) && gi[a] < 0)
272 return false;
273 if ((mk & (1 << (2 * a + 1))) && gi[a] >= q[a])
274 return false;
275 }
276 return true;
277 };
278 const bool in0 = inside(gx, gy, gz);
279 if (!in0)
280 ep(i) = 0.0;
281 // the advector's HIGH-face convention: of[d](i) is the +d face of cell i
282 if (!in0 || !inside(gx + 1, gy, gz))
283 ofx(i) = 0.0;
284 if (!in0 || !inside(gx, gy + 1, gz))
285 ofy(i) = 0.0;
286 if (!in0 || !inside(gx, gy, gz + 1))
287 ofz(i) = 0.0;
288 });
289 CCExec().fence();
290}
291
292template <class Grid>
294 const I3 gs = vofGlobalSize(), org = vofOrigin();
295 const C3 e = e3_;
296 const int g = kVofG, mk = mask;
297 CCField kd = vofAdv_.kindDouble();
298 Kokkos::parallel_for(
299 "peclet::flow::vof_domain_wall_kind", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {e.x, e.y, e.z}),
300 KOKKOS_LAMBDA(int x, int y, int z) {
301 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
302 const int gi[3] = {x - g + org.x, y - g + org.y, z - g + org.z};
303 const int q[3] = {gs.x, gs.y, gs.z};
304 for (int a = 0; a < 3; ++a) {
305 if (((mk & (1 << (2 * a))) && gi[a] < 0) ||
306 ((mk & (1 << (2 * a + 1))) && gi[a] >= q[a])) {
307 kd(i) = 1.0;
308 return;
309 }
310 }
311 });
312 CCExec().fence();
313}
314
315template <class Grid>
317 const I3 gs = vofGlobalSize(), org = vofOrigin();
318 const C3 e = e3_;
319 const int g = kVofG;
320 for (int a = 0; a < 3; ++a)
321 for (int sd = 0; sd < 2; ++sd) {
322 if (!(mask & (1 << (2 * a + sd))))
323 continue;
324 const int q = (a == 0) ? gs.x : (a == 1) ? gs.y : gs.z;
325 const int o = (a == 0) ? org.x : (a == 1) ? org.y : org.z;
326 // block-local index of the depth-3 and depth-2 layers on this face
327 const int i3 = (sd == 0) ? (-3 - o + g) : (q - o + g + 2);
328 const int i2 = (sd == 0) ? (-2 - o + g) : (q - o + g + 1);
329 const int dims[3] = {e.x, e.y, e.z};
330 if (i3 < 0 || i3 >= dims[a] || i2 < 0 || i2 >= dims[a])
331 continue; // this rank's block does not own that face
332 const long st[3] = {1, (long)e.x, (long)e.x * (long)e.y};
333 const int b = (a + 1) % 3, c = (a + 2) % 3;
334 const long sa = st[a], sb = st[b], sc = st[c];
335 CCField ff = f;
336 Kokkos::parallel_for(
337 "peclet::flow::vof_extend_wall_band",
338 MDRange2<CCExec>(CCExec(), {0, 0}, {dims[b], dims[c]}), KOKKOS_LAMBDA(int p0, int p1) {
339 const long base = (long)p0 * sb + (long)p1 * sc;
340 ff(base + (long)i3 * sa) = ff(base + (long)i2 * sa);
341 });
342 }
343 CCExec().fence();
344}
345
346template <class Grid>
348 if (!vofEnabled_ || !vofAdv_.hasGeometry() || !vofSolidG2_.extent(0) || !vofSolidZero_)
349 return;
350 // `vofSolidG2_` lives on the EXTENDED G=2 block (copyInner wrote it at (x+G, y+G, z+G)), so it
351 // is indexed exactly like cField_ — indexing it as an inner-sized array reads the wrong cells
352 // and silently zeroes live fluid colour (measured: 0.5 % of the liquid volume lost per step).
353 CCField c = cField_;
354 CCConst sl = CCConst(vofSolidG2_);
355 const int ex = e_.x, ey = e_.y, g = G;
356 Kokkos::parallel_for(
357 "peclet::flow::vof_zero_solid", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {nx_, ny_, nz_}),
358 KOKKOS_LAMBDA(int x, int y, int z) {
359 const long i = (long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey;
360 if (sl(i) > 0.5)
361 c(i) = 0.0;
362 });
363 CCExec().fence();
364}
365
366template <class Grid>
368 return bc_[2 * a] == 0 && bc_[2 * a + 1] == 0;
369}
370
371template <class Grid>
374 // Rung V5a (WO-Q): the neutral (90 deg) solid-band fill, for the COLOUR field only. It is a
375 // stencil device, not transported data — the MYC 3^3 stencil and the V3 height-function
376 // columns of a near-wall cell reach into the solid, and leaving those cells at 0 makes every
377 // wall perfectly non-wetting. Three passes with a shrinking depth budget (`vof/cutcell.hpp`),
378 // then a SECOND exchange so the outermost ghost layer holds its owner's filled value (the
379 // passes only reach ghost depth 2, and the curvature cascade reads depth 3).
380 if (vofAdv_.hasGeometry() && f.data() == vofAdv_.colour().data()) {
381 // Rung V5b (WO-S): the theta-consistent pass 1 reads the FLUID-ONLY Youngs normal of the
382 // anchor fluid cell, which the pass may reach at ghost depth 3 — one deeper than a 3^3
383 // stencil can be evaluated on this block. Build it on the INNER region and run it through
384 // the block's own ghost policy, exactly as the geometry classification is: every read the
385 // theta pass then makes is the OWNER's value, which is what keeps the inner fill
386 // decomposition-independent.
387 if (vofAdv_.hasWetting()) {
388 vofAdv_.buildWettingNormals();
389 for (int d = 0; d < 3; ++d)
390 vofExchangeRaw(vofAdv_.wettingNormal(d));
391 // Rung V6 (WO-V6): produce the theta FIELD the pass is about to read. Pass A measures the
392 // apparent angle and the raw contact-line speed at ghost depth <= 2 from already-exchanged
393 // data; both are then exchanged so the 3-point in-wall smoothing of pass B, which reaches
394 // depth 3, reads the OWNER's values — the same argument that makes the V5b fill bitwise
395 // across np. Five extra exchanges per fill (three velocity components + two measurements),
396 // skipped entirely when no dynamic angle is configured.
397 if (vofDyn_.active() && vofDyn_.allocated()) {
399 vofDyn_.sigma = effectiveContactSigma();
400 if (!(vofDyn_.sigma > 0.0) && vofDyn_.dynamic && !contactSigmaWarned_) {
401 contactSigmaWarned_ = true;
402 std::fprintf(stderr,
403 "peclet.flow: set_contact_angle_dynamic is active but sigma is 0 "
404 "(no set_surface_tension and no explicit sigma) - Ca_cl is 0 and the "
405 "Cox-Voinov correction is inert.\n");
406 }
407 vofDyn_.measure(vofAdv_);
408 vofExchangeRaw(vofDyn_.uclRaw());
409 vofExchangeRaw(vofDyn_.valid());
410 vofDyn_.impose(vofAdv_);
411 }
412 }
413 vofAdv_.solidBandFill();
414 if (const int wet = vofWetWallMask())
415 vofExtendWallBand(f, wet); // ISSUES sweep item 3: the depth-3 layer no pass can write
417 }
418}
419
420template <class Grid>
422 const bool px = vofAxisPeriodic(0), py = vofAxisPeriodic(1), pz = vofAxisPeriodic(2);
423#ifdef PECLET_FLOW_MPI
424 if (distributed_ && vofDev_)
425 vofDev_->exchange(f);
426 else
427#endif
428 vof::periodicFill(f, I3{e3_.x, e3_.y, e3_.z}, kVofG, px, py, pz);
429 if (px && py && pz)
430 return;
431 const I3 gs = vofGlobalSize(), org = vofOrigin();
432 // ISSUES sweep item 3: the theta band of a wetting DOMAIN wall lives in exactly the ghost
433 // cells `clampFill` would overwrite with the zero-gradient copy, so the colour field skips
434 // the clamp there and `vofFillGhosts` owns that band instead. 0 for every other field and
435 // for every configuration without a domain-wall contact angle -> byte-identical.
436 const int skip = (f.data() == vofAdv_.colour().data()) ? vofWetWallMask() : 0;
437 vof::clampFill(f, I3{e3_.x, e3_.y, e3_.z}, kVofG, org, gs, px, py, pz, skip);
438 vofApplyColourBc(f); // WO-R: inflow / inletOutlet backflow; a no-op unless one is set
439}
440
441template <class Grid>
443#ifdef PECLET_FLOW_MPI
444 if (distributed_)
445 return I3{gnx_, gny_, gnz_};
446#endif
447 return I3{nx_, ny_, nz_};
448}
449
450template <class Grid>
452 return I3{og_.x, og_.y, og_.z};
453}
454
455template <class Grid>
457 // Rung V8 (WO-T): on the collocated grid the divergence-free field is the PROJECTED MAC face
458 // field uf_/vf_/wf_, not the cell field — and it is in flow's own low-face convention
459 // (`uf(i) = 1/2(U(i)+U(i-1))` sits at i-1/2, the -x face of cell i), the SAME convention
460 // `getFaceVelocity` reports and `copyFaceVelocity` shifts. So the bridge is the identical call
461 // on a different source view; the cell field never enters the colour transport.
462 if constexpr (Grid::collocated) {
463 CCField fa[3] = {uf_, vf_, wf_};
464 for (int c = 0; c < 3; ++c) {
465 fillGhosts(fa[c]); // the face field's own ghost policy (project() does exactly this)
466 vof::copyFaceVelocity(vofAdv_.faceVel(c), I3{e3_.x, e3_.y, e3_.z}, kVofG, fa[c],
467 I3{e_.x, e_.y, e_.z}, G, c);
468 }
469 return;
470 }
471 for (int c = 0; c < 3; ++c) {
472 // KEEP the projection's outflow-face correction when there IS one (see
473 // fillVelGhostsKeepOutflow): the full fill would overwrite it with the zero-gradient copy
474 // and hand the advector a field that is not divergence-free at the outlet. When no
475 // projection has run since the last full fill — the KINEMATIC path, where the caller
476 // prescribes the velocity on the inner cells and the boundary face has never been set —
477 // the zero-gradient fill is exactly what supplies that face, so run the full one.
478 // With no outflow face at all the two are identical.
479 if (outflowCorrValid_)
480 fillVelGhostsTo(C[c].u, c, 0, false);
481 else
482 fillVelGhosts(c, 0);
483 // The uniform face-velocity seam (getFaceVelocity): staggered C[c].u already lives on the
484 // faces. copyFaceVelocity carries the low-face -> high-face index shift; see
485 // colour_field.hpp.
486 vof::copyFaceVelocity(vofAdv_.faceVel(c), I3{e3_.x, e3_.y, e3_.z}, kVofG, C[c].u,
487 I3{e_.x, e_.y, e_.z}, G, c);
488 }
489}
490
491template <class Grid>
492std::vector<vof::VofBox> Solver<Grid>::vofBlockRankBoxes(int size) const {
493 std::vector<vof::VofBox> rb(static_cast<std::size_t>(size));
494 const I3 gs = vofGlobalSize();
495#ifdef PECLET_FLOW_MPI
496 if (distributed_ && dec_) {
497 for (int r = 0; r < size; ++r) {
498 const auto b = dec_->block(static_cast<std::size_t>(r));
499 for (int d = 0; d < 3; ++d) {
500 rb[r].lo[d] = static_cast<int>(b.origin[d]);
501 rb[r].hi[d] = static_cast<int>(b.origin[d] + b.size[d]);
502 }
503 }
504 return rb;
505 }
506#endif
507 rb[0].hi[0] = gs.x;
508 rb[0].hi[1] = gs.y;
509 rb[0].hi[2] = gs.z;
510 return rb;
511}
512
513template <class Grid>
515 if (!vofBlockExch_)
516 return;
517 // The rank table first, for the same reason: `buildVofBlock` runs on every re-decomposition
518 // (redistribute -> initMpi), and the pieces are meaningless against the old boxes.
519 int size = 1;
520#ifdef PECLET_FLOW_MPI
521 if (distributed_)
522 MPI_Comm_size(comm_, &size);
523#endif
524 vofBlockExch_->setRankBoxes(vofBlockRankBoxes(size));
526 pp.e = I3{e3_.x, e3_.y, e3_.z};
527 pp.n = I3{nx_, ny_, nz_};
528 pp.o = vofOrigin();
529 pp.g = kVofG;
530 vofBlockExch_->setPatch(pp, vofAdv_.faceVel(0), vofAdv_.faceVel(1), vofAdv_.faceVel(2));
531}
532
533template <class Grid>
535 if (vofOverlapDensity_ && vofBlkS_.extent(0)) {
536 // vof_overlap_design §5.6, opt-in: the overlap volume as film liquid (the "tent").
537 const long len3 = static_cast<long>(e3_.x) * e3_.y * e3_.z;
538 if (static_cast<long>(vofBlkCeff_.extent(0)) != len3)
539 vofBlkCeff_ = SField("vof::blockcsf::ceff", len3);
540 const I3 e = I3{e3_.x, e3_.y, e3_.z};
541 const int g = kVofG;
542 SField un = vofAdv_.colour(), sm = vofBlkS_, ce = vofBlkCeff_;
543 Kokkos::parallel_for(
544 "vof::block::tent", MDRange3<SExec>(SExec(), {g, g, g}, {g + nx_, g + ny_, g + nz_}),
545 KOKKOS_LAMBDA(int x, int y, int z) {
546 const long i = L3(x, y, z, e);
547 const double sv = sm(i);
548 ce(i) = (sv <= 1.0 + 1e-8) ? un(i) : Kokkos::fmax(0.0, 2.0 - sv);
549 });
550 Kokkos::fence();
551 copyInner(cField_, e_, G, CCConst(vofBlkCeff_), e3_, kVofG);
552 } else {
553 copyInner(cField_, e_, G, CCConst(vofAdv_.colour()), e3_, kVofG);
554 }
555 fillPropGhosts(cField_);
556}
557
558template <class Grid>
560 vofOverlap_ = VofOverlapCensus{};
561 if (!vofBlocks_ || !(vofBlocks_->csfEnabled || vofOverlapDensity_))
562 return;
563 const long len3 = static_cast<long>(e3_.x) * e3_.y * e3_.z;
564 if (static_cast<long>(vofBlkS_.extent(0)) != len3)
565 vofBlkS_ = SField("vof::blockcsf::sum", len3);
566 vofBlocks_->scatterSum(vofBlkS_);
567 const I3 e = I3{e3_.x, e3_.y, e3_.z};
568 const int g = kVofG;
569 SField sm = vofBlkS_;
570 double mx = 0.0, ex = 0.0;
571 long nc = 0;
572 Kokkos::parallel_reduce(
573 "vof::block::overlap_census",
574 MDRange3<SExec>(SExec(), {g, g, g}, {g + nx_, g + ny_, g + nz_}),
575 KOKKOS_LAMBDA(int x, int y, int z, double& m, double& a, long& c) {
576 const double sv = sm(L3(x, y, z, e));
577 m = Kokkos::fmax(m, sv);
578 if (sv > 1.0 + 1e-8) {
579 a += sv - 1.0;
580 ++c;
581 }
582 },
583 Kokkos::Max<double>(mx), ex, nc);
584 Kokkos::fence();
585#ifdef PECLET_FLOW_MPI
586 if (distributed_) {
587 double gm = 0.0, ge = 0.0;
588 long gc = 0;
589 MPI_Allreduce(&mx, &gm, 1, MPI_DOUBLE, MPI_MAX, comm_);
591 MPI_Allreduce(&nc, &gc, 1, MPI_LONG, MPI_SUM, comm_);
592 mx = gm;
593 ex = ge;
594 nc = gc;
595 }
596#endif
597 vofOverlap_.maxSum = mx;
598 vofOverlap_.excess = ex;
599 vofOverlap_.cells = nc;
600 // Under the opt-in overlap density the phantom is an ordinary liquid-gas interface (C_eff runs
601 // 0.5 -> 0 across it), so the base bound is the right one there (vof_overlap_design §5.6, G7a).
602 // The TRIGGER is S > 1 + 1e-2, not the census's 1 + 1e-8 (vof_overlap_design §11): Weymouth-Yue
603 // residue of one marker inside another keeps S a hair above 1 on nearly every step after a
604 // contact (measured: bound on 5028 of 5907 shear steps, every channel_18 step), whereas a
605 // phantom interface worth a capillary mode means a marker's band inside another's body.
606 constexpr double kPhantomTrigger = 1e-2;
607 vofOverlap_.active = vofPhantomBound_ && vofBlocks_->csfEnabled && !vofOverlapDensity_ &&
608 mx > 1.0 + kPhantomTrigger;
609}
610
611template <class Grid>
613 vofPhantomBound_ = enabled;
614 vofOverlap_.active = vofOverlap_.active && enabled; // takes effect for the very next step
615}
616
617template <class Grid>
619 vofBlockDebris_ = enabled ? 1 : 0;
620 if (vofBlocks_ && vofBlocks_->csfEnabled)
621 vofBlocks_->debrisRemove = enabled;
622}
623
624template <class Grid>
626 if (!vofBlocks_)
627 throw std::runtime_error("set_vof_block_overlap_density: call enable_vof_blocks first");
628 vofOverlapDensity_ = enabled;
631}
632
633template <class Grid>
635 copyInner(vofAdv_.colour(), e3_, kVofG, CCConst(cField_), e_, G);
636 vofFillGhosts(vofAdv_.colour());
637}
638
639template <class Grid>
642 C3 e = e_;
643 CCField bb = dst;
644 const int hi = faceRange ? 1 : 0;
645 CCConst rs = CCConst(C[c].rscale), cv = CCConst(cField_), kp = CCConst(kappaField_),
646 kb = CCConst(kappaBranch_);
647 const long strd = strideOf(c);
648 // Phase 3 (V3.1): the CSF carries the PRESSURE-GRADIENT WEIGHT of this component's axis,
649 // `w_a = 1/h_a'^2` — the same symbol Phase 2 puts on `-(P(i) - P(i - s_a))`. Exactly 1.0 on
650 // every isotropic run, so `x / 1.0 == x` keeps this kernel bit-identical.
651 const double sig = sigmaCsf_, h = 1.0 / u_.w[c];
652 using MD = MDRange3<CCExec>;
653 Kokkos::parallel_for(
654 "csf_rhs", MD(space, {G, G, G}, {e.x - G + hi, e.y - G + hi, e.z - G + hi}),
655 KOKKOS_LAMBDA(int x, int y, int z) {
656 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
657 const double dC = cv(i) - cv(i - strd);
658 if (dC == 0.0)
659 return; // no interface across this face -> no force, and no orphan either
660 double kf = 0.0;
661 vof::csfFaceCurvature(kp(i - strd), kb(i - strd), kp(i), kb(i), kf);
662 if (rowScaled)
663 bb(i) += rs(i) * vof::csfFaceForce(sig, kf, dC, h);
664 else
665 bb(i) += vof::csfFaceForce(sig, kf, dC, h);
666 });
667}
668
669template <class Grid>
672 C3 e = e_;
673 CCField bb = dst;
674 const int hi = faceRange ? 1 : 0;
675 CCConst rs = CCConst(C[c].rscale), cv = CCConst(cField_), kp = CCConst(kappaField_),
676 kb = CCConst(kappaBranch_);
677 const long strd = strideOf(c);
678 const double sig = sigmaCsf_, h = 1.0 / u_.w[c]; // the ablation takes the same weight (V3.1)
679 using MD = MDRange3<CCExec>;
680 Kokkos::parallel_for(
681 "csf_rhs_cellinterp", MD(space, {G, G, G}, {e.x - G + hi, e.y - G + hi, e.z - G + hi}),
682 KOKKOS_LAMBDA(int x, int y, int z) {
683 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
684 double f[2] = {0.0, 0.0};
685 for (int q = 0; q < 2; ++q) { // q = 0 -> cell i-s_c, q = 1 -> cell i
686 const long j = i - (1 - q) * strd;
687 if (!vof::csfKappaDefined(kb(j)))
688 continue;
689 f[q] = sig * kp(j) * 0.5 * (cv(j + strd) - cv(j - strd)) / h;
690 }
691 if (rowScaled)
692 bb(i) += rs(i) * 0.5 * (f[0] + f[1]);
693 else
694 bb(i) += 0.5 * (f[0] + f[1]);
695 });
696}
697
698template <class Grid>
701 C3 e = e_;
702 CCField bb = C[c].b;
703 CCConst rs = CCConst(C[c].rscale), fb = CCConst(csfBlkF_[c]);
704 using MD = MDRange3<CCExec>;
705 Kokkos::parallel_for(
706 "csf_rhs_blocks", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
707 KOKKOS_LAMBDA(int x, int y, int z) {
708 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
709 bb(i) += rs(i) * fb(i);
710 });
711}
712
713template <class Grid>
715 if (!vofEnabled_ || !kappaField_.extent(0))
716 throw std::runtime_error(
717 "csf_diagnostics: needs VoF + a curvature field (call set_surface_tension and step, or "
718 "compute_vof_curvature)");
720 C3 e = e_;
721 CCConst cv = CCConst(cField_), kp = CCConst(kappaField_), kb = CCConst(kappaBranch_);
722 const double sig = sigmaCsf_, eps = csfInterfaceEps_;
723 using MD = MDRange3<CCExec>;
724 for (int c = 0; c < 3; ++c) {
725 const double h = 1.0 / u_.w[c]; // the V3.1 per-axis gradient weight of this component
726 const long strd = strideOf(c);
727 double mx = 0.0;
728 long orph = 0, forced = 0;
729 Kokkos::parallel_reduce(
730 "csf_diag", MD(CCExec(), {G, G, G}, {e.x - G, e.y - G, e.z - G}),
731 KOKKOS_LAMBDA(int x, int y, int z, double& m, long& o, long& f) {
732 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
733 const double dC = cv(i) - cv(i - strd);
734 if (dC == 0.0)
735 return;
736 double kf = 0.0;
737 const bool ok = vof::csfFaceCurvature(kp(i - strd), kb(i - strd), kp(i), kb(i), kf);
738 if (ok)
739 ++f;
740 else if (Kokkos::fabs(dC) > eps)
741 ++o;
742 m = Kokkos::fmax(m, Kokkos::fabs(vof::csfFaceForce(sig, kf, dC, h)));
743 },
744 Kokkos::Max<double>(mx), orph, forced);
745 d.maxForce[c] = mx;
746 d.orphanFaces[c] = orph;
747 d.forcedFaces[c] = forced;
748 }
749 return d;
750}
751
752template <class Grid>
754 // PHASE 3 LIFTED Phase 2's `requireIsotropic` refusal here (flow/doc/anisotropic_vof.md).
755 // The colour transport itself needed nothing — a stretched cell IS the unit cube of the index
756 // coordinates, so every volume fraction, PLIC plane<->volume relation, slab flux and
757 // Weymouth-Yue sweep is metric-free (§3, decision V1). What took the metric is everything
758 // that reads a DIRECTION, a LENGTH or an AREA: the height-function curvature over physical
759 // column and transverse spacings, the two paraboloid fits in the physical frame (§4), the CSF
760 // face force carrying THIS axis's pressure-gradient weight `w_a` so the balanced-force
761 // identity survives (§5), the theta rotation of the wetting fill (§6), and the phase-change
762 // layer's normals, areas, sample distances and `V_cell` (§7).
763
764 if constexpr (Grid::collocated) {
765 // Rung V8: allowed. The colour is advected by the PROJECTED face field uf_/vf_/wf_ — which is
766 // what the ABC approximate projection makes exactly divergence-free, i.e. precisely the field
767 // Weymouth-Yue's conservation proof needs — and the CSF enters the implicit predictor as the
768 // mass-adjoint face integral (collocated_varrho.hpp). ALL-FLUID only at this rung.
769 if (hasSolid_)
770 throw std::runtime_error(
771 "enable_vof: geometric VoF on SolverColocated (rung V8) is ALL-FLUID only — immersed "
772 "solids on this path are the next package (doc/collocated_multiphase_solids_plan.md). "
773 "Use the staggered Solver (rung V5a supports cut cells).");
774 collocatedV8AutoFallback("geometric VoF on the collocated grid");
775 }
776 if (vofEnabled_)
777 return;
778 cField_ = addField("C"); // the G=2 registry mirror (closure input / IO / redistribute)
779 vofEnabled_ = true;
780 // WO-R2 item 3 — the EXACT (matrix-free, double, flux-form) level-0 residual/matvec becomes
781 // the default the moment there is an interface. Measured on Hysing case 2 (WO-R item 6,
782 // 64x128x4, adaptive dt): it removes 7.5 orders of the projected flux divergence
783 // (1.85e-03 -> 5.15e-11) and moves NOTHING else — 116/600 pressure iterations, 1123 steps, the
784 // dt-limit census and both published functionals identical to every printed digit. The defect
785 // it removes is the float operator's broken row-sum identity A*1 = 0, and a two-phase
786 // coefficient contrast is precisely what amplifies it. `set_pressure_exact_residual(False)`
787 // after `enable_vof` is the ablation.
789 // WO-R2 item 4 — the wisp tolerance. 0 is V1 verbatim (and stays the standalone advector's
790 // default); an open-boundary domain that DRAINS reaches C ~ 1e-18 everywhere, where the MYC
791 // normal is degenerate and plicAlpha divides by it (measured: sum C -> -inf -> NaN in three
792 // steps). Same 1e-8 the curvature predicate uses under surface tension.
793 vofAdv_.wispEps = vofWispEps_;
795}
796
797template <class Grid>
799 if (!(eps >= 0.0) || eps >= 0.5)
800 throw std::runtime_error("set_vof_wisp_eps: the threshold must be in [0, 0.5)");
801 vofWispEps_ = eps;
802 vofAdv_.wispEps = eps;
803 if (vofBlocks_)
804 vofBlocks_->setWispEps(eps); // §12.1: one wisp guard for the global and the block advectors
805}
806
807template <class Grid>
809 return vofWispEps_;
810}
811
812template <class Grid>
814 return 1e-8;
815}
816
817template <class Grid>
819 return vofEnabled_;
820}
821
822template <class Grid>
823void Solver<Grid>::setVof(const std::vector<double>& c) {
824 enableVof();
825 scatterInner(cField_, c);
826 zeroSolidColour(); // rung V5a: solid cells carry no colour (the band fill is regenerated)
827 fillPropGhosts(cField_);
828 pcMaskFresh_ = false; // WO-P3g: the Dirichlet/plane geometry no longer describes this colour
829}
830
831template <class Grid>
832std::vector<double> Solver<Grid>::getVof() {
833 return gatherInner(cField_);
834}
835
836template <class Grid>
838 if (!vofEnabled_)
839 throw std::runtime_error("vof_diagnostics: VoF is not enabled (call enable_vof/set_vof)");
841 auto d = vofAdv_.diagnostics();
842 // `solidSumC` is the census of the CANONICAL field: the working block's solid cells carry the
843 // neutral band fill (reported as `solidFillSum`), while "C" itself is 0 there — that is the
844 // quantity gate G2 of WO-Q asks for.
845 d.solidSumC = vofSolidColourSum();
846 return d;
847}
848
849template <class Grid>
851 if (!vofEnabled_ || !vofAdv_.hasGeometry() || !vofSolidG2_.extent(0))
852 return 0.0;
853 CCConst c = CCConst(cField_), sl = CCConst(vofSolidG2_);
854 const int ex = e_.x, ey = e_.y, g = G;
855 double acc = 0.0;
856 Kokkos::parallel_reduce(
857 "peclet::flow::vof_solid_sum", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {nx_, ny_, nz_}),
858 KOKKOS_LAMBDA(int x, int y, int z, double& a) {
859 const long i = (long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey;
860 if (sl(i) > 0.5)
861 a += c(i);
862 },
863 acc);
864 Kokkos::fence();
865 return acc;
866}
867
868template <class Grid>
870 if (!vofEnabled_)
871 return 0.0;
874 const double loc = vofAdv_.maxCourantInterfaceAuto(dt_ / vofAdv_.h());
875 return vofAdv_.globalMax ? vofAdv_.globalMax(loc) : loc;
876}
877
878template <class Grid>
880 return vofEnabled_ ? vofAdv_.lastCfl() : 0.0;
881}
882
883template <class Grid>
885 vofCflLimit_ = v;
886 if (vofEnabled_)
887 vofAdv_.cflLimit = v;
888}
889
890template <class Grid>
892 return vofCflLimit_;
893}
894
895template <class Grid>
897 if constexpr (Grid::collocated) {
898 // Rung V8 scope, re-checked here because `set_solid` can follow `enable_vof`.
899 if (hasSolid_)
900 throw std::runtime_error(
901 std::string(who) +
902 ": geometric VoF on SolverColocated (rung V8) is ALL-FLUID only. The cut-cell colour "
903 "transport (rung V5a) is validated on the STAGGERED solver.");
904 }
905 if (!hasSolid_ || vofAdv_.hasGeometry())
906 return;
907 std::string m(who);
908 m += ": an immersed solid is present but the colour block has no cut-cell geometry. Rung V5a "
909 "needs the cut-cell openness, i.e. set_solid(sdf, cutcell_pressure=True) (the staircase "
910 "pressure operator has no face openness to weight the geometric fluxes with).";
911 throw std::runtime_error(m);
912}
913
914template <class Grid>
916 return vofAdv_.hasGeometry();
917}
918
919template <class Grid>
921 vofAdv_.cutFluxClamp = on;
922}
923
924template <class Grid>
926 return vofAdv_.cutFluxClamp;
927}
928
929template <class Grid>
931 vofSolidZero_ = on;
932}
933
934template <class Grid>
936 return vofSolidZero_;
937}
938
939template <class Grid>
940std::vector<double> Solver<Grid>::getVofFilledColour() {
941 if (!vofEnabled_)
942 throw std::runtime_error("vof_filled_color: VoF is not enabled");
944 CCField t("vofFilled", n_);
945 copyInner(t, e_, G, CCConst(vofAdv_.colour()), e3_, kVofG);
946 return gatherInner(t);
947}
948
949template <class Grid>
950std::vector<double> Solver<Grid>::getVofGeometry(int which) {
951 if (!vofEnabled_)
952 throw std::runtime_error("vof_geometry: VoF is not enabled (needs enable_vof)");
954 throw std::runtime_error("vof_geometry: `which` must be 0..4");
955 // ISSUES sweep item 4. An ALL-FLUID VoF solver has no cut-cell geometry block, but the
956 // geometry it would carry is not undefined -- it is the trivial one, and it is exactly what
957 // the V1 transport kernels execute: every cell fully fluid (eps = 1), every face fully open
958 // (o = 1), no solid (kind = 0). Returning it lets ONE diagnostic --
959 // `vof_geometry(0) * (1 - get_vof())` for the gas volume, say -- serve a packed scene and its
960 // all-fluid control, instead of forcing the caller to branch on `vof_has_geometry()` and
961 // synthesise the ones itself (which is what examples/bubble-through-packing had to do).
962 if (!vofAdv_.hasGeometry())
963 return std::vector<double>((std::size_t)nx_ * ny_ * nz_, which == 4 ? 0.0 : 1.0);
964 CCField t("vofGeom", n_);
965 CCConst src;
966 if (which == 0)
967 src = CCConst(vofAdv_.epsFraction());
968 else if (which < 4)
969 src = CCConst(vofAdv_.faceOpenness(which - 1));
970 else
971 src = CCConst(vofAdv_.kindDouble());
972 copyInner(t, e_, G, src, e3_, kVofG);
973 return gatherInner(t);
974}
975
976template <class Grid>
978 if (!(thetaDeg >= 0.0 && thetaDeg <= 180.0))
979 throw std::runtime_error("set_contact_angle: theta must be in [0, 180] degrees");
980 contactAngleDeg_ = thetaDeg;
981 contactAngleField_.clear();
982 contactAngleSet_ = true;
984 buildVofGeometry(); // item 3: a wetting DOMAIN wall changes the colour block's geometry
986}
987
988template <class Grid>
990 if (!vofEnabled_ || hasSolid_ || vofWetWallMask() != 0)
991 return;
992 throw std::runtime_error(
993 "set_contact_angle: this solver has no wetting wall - no immersed SDF solid (set_solid) "
994 "and no impermeable domain face (set_domain_bc(face, 1) no-slip or 4 free-slip). The "
995 "angle would be imposed nowhere. (Inflow/outflow faces carry no contact line.)");
996}
997
998template <class Grid>
999void Solver<Grid>::setContactAngleField(const std::vector<double>& thetaDeg) {
1000 if (thetaDeg.size() != (std::size_t)nx_ * ny_ * nz_)
1001 throw std::runtime_error("set_contact_angle_field: expected nx*ny*nz values");
1002 contactAngleField_ = thetaDeg;
1003 contactAngleSet_ = true;
1005 buildVofGeometry(); // item 3
1007}
1008
1009template <class Grid>
1011 return contactAngleSet_;
1012}
1013
1014template <class Grid>
1016 return contactAngleDeg_;
1017}
1018
1019template <class Grid>
1021 if (mode < 0 || mode > 3)
1022 throw std::runtime_error("set_contact_angle_pivot: mode must be 0..3");
1023 contactPivot_ = mode;
1024 vofAdv_.wettingPivot = mode;
1025}
1026
1027template <class Grid>
1029 return contactPivot_;
1030}
1031
1032template <class Grid>
1033void Solver<Grid>::setContactAngleDynamic(double thetaEDeg, double slipCells, double muLiquid,
1034 double sigma) {
1035 // `slipCells` is a LENGTH in the caller's units (cells when no physical domain is armed);
1036 // the model needs lambda/Delta, which is what the internal value is.
1037 const double slipInt = slipCells * u_.lenToInt();
1038 if (!(slipInt > 0.0 && slipInt < 1.0))
1039 throw std::runtime_error(
1040 "set_contact_angle_dynamic: the slip length must lie in (0, 1) cell (lambda < Delta)");
1041 if (!(muLiquid > 0.0))
1042 throw std::runtime_error("set_contact_angle_dynamic: mu_liquid must be positive");
1043 vofDyn_.dynamic = true;
1044 vofDyn_.slip = slipInt;
1045 vofDyn_.muLiquid = muLiquid * u_.muToInt();
1046 contactSigmaOverride_ = sigma;
1047 // ONE lambda across the two halves of Afkhami-Zaleski-Bussmann (WO-V6b): this call and
1048 // set_wall_slip_length write the SAME stored value, so the angle model's inner cut-off and
1049 // the momentum wall closure can never disagree (last call wins). Whether the MOMENTUM half is
1050 // active is a separate switch, set only by set_wall_slip_length -- so every result WO-V6
1051 // validated with the angle half alone stays exactly what it was.
1052 slipPhys_ = slipCells;
1053 slipLambda_ = slipInt;
1054 if (wallSlip_ && hasSolid_ && !Grid::collocated) {
1055 buildVelocityOverlays(/*resetU=*/false);
1057 }
1058 setContactAngle(thetaEDeg); // sets the static base and (re)wires the theta field
1059}
1060
1061template <class Grid>
1063 if (!(lambdaCells >= 0.0))
1064 throw std::runtime_error("set_wall_slip_length: lambda must be >= 0");
1065 const double lambdaInt = lambdaCells * u_.lenToInt();
1066 if (lambdaCells > 0.0 && Grid::collocated)
1067 throw std::runtime_error(
1068 "set_wall_slip_length: the Navier wall closure is a staggered-grid feature (the "
1069 "collocated scheme carries its own wall treatment)");
1070 slipPhys_ = lambdaCells;
1071 slipLambda_ = lambdaInt;
1072 wallSlip_ = lambdaCells > 0.0;
1073 if (vofDyn_.dynamic && lambdaCells > 0.0)
1074 vofDyn_.slip = lambdaInt;
1075 if (hasSolid_) {
1076 buildVelocityOverlays(/*resetU=*/false);
1078 }
1079}
1080
1081template <class Grid>
1083 return wallSlip_ ? slipPhys_ : 0.0;
1084}
1085
1086template <class Grid>
1087std::array<int, 3> Solver<Grid>::wallSlipSandwichCells() const {
1088 return slipSandwich_;
1089}
1090
1091template <class Grid>
1093 if (!(thetaADeg >= thetaRDeg))
1094 throw std::runtime_error("set_contact_angle_hysteresis: theta_a must be >= theta_r");
1095 if (!(thetaRDeg >= 0.0 && thetaADeg <= 180.0))
1096 throw std::runtime_error("set_contact_angle_hysteresis: angles must lie in [0, 180]");
1097 const double toRad = 3.14159265358979323846 / 180.0;
1098 vofDyn_.hysteresis = true;
1099 vofDyn_.thetaA = thetaADeg * toRad;
1100 vofDyn_.thetaR = thetaRDeg * toRad;
1101 if (!contactAngleSet_)
1102 setContactAngle(0.5 * (thetaADeg + thetaRDeg)); // a base is required; the mid angle
1103 else
1105}
1106
1107template <class Grid>
1109 vofDyn_.dynamic = false;
1110 vofDyn_.hysteresis = false;
1112}
1113
1114template <class Grid>
1116 return vofDyn_.dynamic;
1117}
1118
1119template <class Grid>
1121 return vofDyn_.hysteresis;
1122}
1123
1124template <class Grid>
1126 return vofDyn_.slip;
1127}
1128
1129template <class Grid>
1131 vofDyn_.smooth = on;
1132}
1133
1134template <class Grid>
1136 if (!(loDeg > 0.0 && hiDeg < 180.0 && loDeg < hiDeg))
1137 throw std::runtime_error("set_contact_angle_clamp: need 0 < lo < hi < 180");
1138 const double toRad = 3.14159265358979323846 / 180.0;
1139 vofDyn_.thetaMin = loDeg * toRad;
1140 vofDyn_.thetaMax = hiDeg * toRad;
1141}
1142
1143template <class Grid>
1145 return contactSigmaOverride_ > 0.0 ? contactSigmaOverride_ : sigmaCsf_;
1146}
1147
1148template <class Grid>
1149std::vector<double> Solver<Grid>::getVofDynamicField(int which) {
1150 if (!vofEnabled_ || !vofDyn_.active() || !vofDyn_.allocated())
1151 throw std::runtime_error(
1152 "vof_dynamic_field: no dynamic contact angle (call set_contact_angle_dynamic / "
1153 "set_contact_angle_hysteresis first)");
1154 bridgeColourToVof(); // regenerates the fill, hence the dynamic pass
1155 CCField t("vofDyn", n_);
1156 const double toDeg = 180.0 / 3.14159265358979323846;
1157 CCConst src;
1158 if (which == 0)
1159 src = CCConst(vofDyn_.imposed());
1160 else if (which == 1)
1161 src = CCConst(vofDyn_.apparent());
1162 else if (which == 2)
1163 src = CCConst(vofDyn_.speed());
1164 else if (which == 3)
1165 src = CCConst(vofDyn_.capillary());
1166 else
1167 src = CCConst(vofDyn_.stateField());
1168 copyInner(t, e_, G, src, e3_, kVofG);
1169 auto v = gatherInner(t);
1170 if (which <= 1)
1171 for (auto& q : v)
1172 q *= toDeg;
1173 return v;
1174}
1175
1176template <class Grid>
1178 if (!vofEnabled_)
1179 throw std::runtime_error("contact_angle_diagnostics: VoF is not enabled");
1181 d.setAngle = contactAngleDeg_;
1182 if (!vofAdv_.hasWetting())
1183 return d;
1184 bridgeColourToVof(); // regenerates the fill, hence the census
1185 long counts[vof::kVofWetCount];
1186 long nApp = 0;
1187 // ISSUES sweep item 3: a wetting DOMAIN wall's band lives in the GHOST layers, so the
1188 // census has to reach them or `contact_cells` reads 0 -- the very symptom the item was
1189 // reported for. 0 (the inner region) when no domain wall is wetting.
1190 vofAdv_.wettingCensusGhost(counts, d.meanApparentAngle, nApp, vofWetWallMask() ? kVofG : 0);
1191 d.unfilledCells = counts[vof::kVofWetNone];
1192 d.contactCells = counts[vof::kVofWetTheta];
1193 d.neighbourCells = counts[vof::kVofWetNeighbour];
1194 d.pureCells = counts[vof::kVofWetPure];
1195 d.parallelCells = counts[vof::kVofWetParallel];
1196 d.neutralCells = counts[vof::kVofWetNeutral];
1197 if (vofDyn_.active() && vofDyn_.allocated()) {
1198 const auto cs = vofDyn_.census(vofAdv_);
1199 d.dynamicCells = cs.contactCells;
1200 d.pinnedCells = cs.cells[vof::kVofDynPinned];
1201 d.advancingCells = cs.cells[vof::kVofDynAdvancing];
1202 d.recedingCells = cs.cells[vof::kVofDynReceding];
1203 d.meanImposedTheta = cs.meanImposedDeg;
1204 d.meanApparentTheta = cs.meanApparentDeg;
1205 d.maxCaCl = cs.maxCa;
1206 d.maxContactSpeed = cs.maxSpeed;
1207 }
1208 return d;
1209}
1210
1211template <class Grid>
1213 if (!contactAngleSet_ || !vofEnabled_ || !vofAdv_.hasGeometry())
1214 return; // remembered; buildVofGeometry calls back once the geometry exists
1215 vofAdv_.enableWetting();
1216 vofAdv_.wettingPivot = contactPivot_;
1217 // (a) the SDF on the colour block: inner region from the solver's own sdf_, then the colour
1218 // field's ghost policy, so the central-difference wall normal at ghost depth <= 2 is the
1219 // OWNER's (the WO-Q finding-5 argument, applied to the wall normal).
1220 if (hasSolid_)
1221 copyInner(vofAdv_.wallSdf(), e3_, kVofG, CCConst(sdf_), e_, G);
1222 else
1223 Kokkos::deep_copy(vofAdv_.wallSdf(), 1e30);
1224 vofExchangeRaw(vofAdv_.wallSdf());
1225 // ISSUES sweep item 3: fold the wetting DOMAIN wall planes into the same field, on the WHOLE
1226 // block (the plane distance is a closed form of the global index, so no exchange is needed and
1227 // the out-of-domain band gets the value the clamp cannot produce). `min` is the SDF of the
1228 // union of the solids, so an SDF body and a domain wall compose; and the central difference
1229 // the theta pass takes of this field is exactly the inward face normal in the band.
1230 if (const int wet = vofWetWallMask()) {
1231 const I3 gs = vofGlobalSize(), org = vofOrigin();
1232 const C3 e = e3_;
1233 const int g = kVofG;
1234 CCField sd = vofAdv_.wallSdf();
1235 Kokkos::parallel_for(
1236 "peclet::flow::vof_wall_sdf_domain", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {e.x, e.y, e.z}),
1237 KOKKOS_LAMBDA(int x, int y, int z) {
1238 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
1239 const double d = vofWallPlaneSdf(x - g + org.x, y - g + org.y, z - g + org.z, gs, wet);
1240 sd(i) = Kokkos::fmin(sd(i), d);
1241 });
1242 CCExec().fence();
1243 }
1244 // (b) theta, in radians.
1245 const double toRad = 3.14159265358979323846 / 180.0;
1246 if (contactAngleField_.empty()) {
1247 Kokkos::deep_copy(vofAdv_.contactAngle(), contactAngleDeg_ * toRad);
1248 } else {
1249 CCField t("thetaG2", n_);
1250 std::vector<double> rad(contactAngleField_.size());
1251 for (std::size_t i = 0; i < rad.size(); ++i)
1252 rad[i] = contactAngleField_[i] * toRad;
1253 scatterInner(t, rad);
1254 copyInner(vofAdv_.contactAngle(), e3_, kVofG, CCConst(t), e_, G);
1255 vofExchangeRaw(vofAdv_.contactAngle());
1256 }
1257 // (c) rung V6: the STATIC base the dynamic selector starts from. The V6 pass OVERWRITES
1258 // `contactAngle()` every fill, so it must never read back its own previous output.
1259 if (vofDyn_.active()) {
1260 vofDyn_.allocate(vofAdv_.size());
1261 Kokkos::deep_copy(vofDyn_.base(), vofAdv_.contactAngle());
1262 }
1263}
1264
1265template <class Grid>
1267 if (!vofDyn_.allocated())
1268 return;
1269 if constexpr (Grid::collocated)
1270 throw std::runtime_error("set_contact_angle_dynamic: SolverColocated has no immersed solid");
1271 const long st[3] = {1, (long)e_.x, (long)e_.x * (long)e_.y};
1272 for (int c = 0; c < 3; ++c) {
1273 // The cell-centre mean reads the face ONE cell out on the component's own axis, so the
1274 // velocity ghost ring has to be valid. Same rule as `bridgeVelocityToVof`: keep the
1275 // projection's outflow-face correction when there is one, otherwise the full fill (the
1276 // kinematic path, where the zero-gradient rule is what supplies the boundary face at all).
1277 if (outflowCorrValid_)
1278 fillVelGhostsTo(C[c].u, c, 0, false);
1279 else
1280 fillVelGhosts(c, 0);
1281 if (vofDynVel_[c].extent(0) != n_)
1282 vofDynVel_[c] = CCField("vofDynVel", n_);
1283 CCField t = vofDynVel_[c];
1284 CCConst u = CCConst(C[c].u);
1285 const long sc = st[c];
1286 const int ex = e_.x, ey = e_.y, g = G;
1287 Kokkos::parallel_for(
1288 "peclet::flow::vof_dyn_cellvel", MDRange3<CCExec>(CCExec(), {0, 0, 0}, {nx_, ny_, nz_}),
1289 KOKKOS_LAMBDA(int x, int y, int z) {
1290 const long i = (long)(x + g) + (long)(y + g) * ex + (long)(z + g) * (long)ex * ey;
1291 t(i) = 0.5 * (u(i) + u(i + sc));
1292 });
1293 CCExec().fence();
1294 copyInner(vofDyn_.cellVel(c), e3_, kVofG, CCConst(t), e_, G);
1295 vofExchangeRaw(vofDyn_.cellVel(c));
1296 }
1297}
1298
1299template <class Grid>
1301 return vofAdv_;
1302}
1303
1304template <class Grid>
1306 vofStep_ = n;
1307 if (vofBlocks_)
1308 vofBlocks_->setStep(n);
1309}
1310
1311template <class Grid>
1313 return vofStep_;
1314}
1315
1316template <class Grid>
1318 const double dt = dtPhysArg * u_.timeToInt();
1319 if (!vofEnabled_)
1320 throw std::runtime_error("advect_vof: VoF is not enabled (call enable_vof / set_vof first)");
1321 requireVofGeometry("advect_vof");
1322 // ISSUES sweep item 5: on the collocated grid the advecting field is the MAC FACE field, and
1323 // a face field that was never built is all zeros — which the divergence guard below happily
1324 // certifies as solenoidal, so the whole advection became a silent no-op. `set_state` /
1325 // `set_velocity` now seed it (seedFaceFieldFromCells) and a projection builds it; refuse the
1326 // one remaining way to reach the trap.
1327 if constexpr (Grid::collocated) {
1328 if (!faceFieldValid_)
1329 throw std::runtime_error(
1330 "advect_vof: SolverColocated advects the colour with the MAC FACE field uf_/vf_/wf_ "
1331 "(the only discretely divergence-free field on this grid), and it has never been "
1332 "built — it is all zeros, which the divergence guard would certify as solenoidal and "
1333 "the advection would be a silent no-op. Call set_state()/set_velocity() (which now "
1334 "seed the face field through the same centerToFace map project() uses) or step() "
1335 "first.");
1336 }
1337 // WO-R2 item 4a (found by the E1 gallery page): the divergence guard below was INERT on a bare
1338 // box. `maxOpenDivergence()` returns 0.0 when there is no cut-cell pressure operator, so a
1339 // cell-centre-sampled analytic field — LeVeque, whose true max|div(open u)| is 0.612 — sailed
1340 // through the check and lost 4.93 % of the liquid in 50 steps with no diagnostic at all.
1341 // Refuse the configuration instead of measuring nothing.
1342 if (!cutcellPressure_)
1343 throw std::runtime_error(
1344 "advect_vof: no cut-cell pressure operator, so the divergence guard cannot measure "
1345 "anything (max_open_divergence() would return 0 whatever the field is). Build one with "
1346 "set_pressure_geometry(sdf) on an all-fluid box, or set_solid(sdf, "
1347 "cutcell_pressure=True) — Weymouth-Yue conservation is conditional on "
1348 "sum_f o_f u_f = 0 per cell and a cell-centre-sampled analytic field does NOT satisfy "
1349 "it (measured on LeVeque: max|div| 0.612, 4.93 % of the liquid lost in 50 steps).");
1350 // and measure the field the caller actually has: max_open_divergence() re-imposes the
1351 // zero-gradient outflow face before measuring, i.e. it MUTATES u and reports a field the
1352 // advector will not be handed (WO-R). The projected sibling does neither.
1353 const double div = maxOpenDivergenceProjectedInternal(); // the 1e-10 gate is index-unit
1354 if (!(div <= 1e-10)) {
1355 char msg[320];
1356 std::snprintf(msg, sizeof(msg),
1357 "advect_vof: the current face velocity is not discretely divergence-free "
1358 "(max|div(open*u)| = %.6g > 1e-10). Weymouth-Yue conservation is conditional "
1359 "on it; project the field first (step() / project()).",
1360 div);
1361 throw std::runtime_error(msg);
1362 }
1365 if (pcEnergy_) { // WO-P23: the temperature rides the same sweeps here too
1366 ScalarField& sc = scalarField(pcTName_);
1367 copyInner(vofEnergy_.temperature(), e3_, kVofG, CCConst(sc.c), e_, G);
1368 vofEnergy_.advect(vofAdv_, dt, vofStep_++);
1369 copyInner(sc.c, e_, G, CCConst(vofEnergy_.temperature()), e3_, kVofG);
1370 scalarFillGhosts(sc);
1371 } else {
1372 vofAdv_.advect(dt, vofStep_++);
1373 }
1374 copyInner(cField_, e_, G, CCConst(vofAdv_.colour()), e3_, kVofG);
1376 fillPropGhosts(cField_);
1378}
1379
1380template <class Grid>
1381void Solver<Grid>::enableVofBlocks(const std::vector<std::array<double, 4>>& seeds) {
1383 for (const auto& sd : seeds)
1384 vofBlocks_->seedSphere(sd[0], sd[1], sd[2], sd[3]);
1385 vofBlocks_->scatter(vofAdv_.colour());
1387}
1388
1389template <class Grid>
1391 if (!vofEnabled_)
1392 throw std::runtime_error("enable_vof_blocks: VoF is not enabled (call enable_vof first)");
1393 if (vofMomEnabled_)
1394 throw std::runtime_error(kVofMomBlocksMsg);
1395 if (hasSolid_)
1396 throw std::runtime_error(
1397 "enable_vof_blocks: the block container is ALL-FLUID at rung W0 (the cut-cell block is "
1398 "rung W12). Drop set_solid, or use the structured colour field (advect_vof).");
1399 int rank = 0, size = 1;
1400#ifdef PECLET_FLOW_MPI
1401 if (distributed_) {
1402 MPI_Comm_rank(comm_, &rank);
1403 MPI_Comm_size(comm_, &size);
1404 }
1405#endif
1406 vofBlocks_ = std::make_shared<vof::VofBlockSet>();
1407 const std::array<bool, 3> per{vofAxisPeriodic(0), vofAxisPeriodic(1), vofAxisPeriodic(2)};
1408 vofBlocks_->init(vofGlobalSize(), per, rank, size, 1.0); // flow works in cell units
1409 // The anisotropic metric: `pushVofMetric` runs from `refreshUnitDerived`, which in the documented
1410 // order (Solver(extent) -> set_rho -> ... -> enable_vof_blocks) fires BEFORE the block set
1411 // exists, so the set is created here with the metric already in force (review2 finding 1: the
1412 // block CSF silently ran on the unit metric on every anisotropic box).
1413 vofBlocks_->setMetric(u_.vofMetric());
1414 vofBlocks_->cflLimit = vofCflLimit_;
1415 // vof_overlap_design §12.1: the block advectors run the SAME wisp guard as the global one (the
1416 // enable_vof value); at 0 they reconstruct 1e-33 residue into NaN (channel_18, step 10908).
1417 vofBlocks_->setWispEps(vofAdv_.wispEps);
1418 vofBlockExch_ = std::make_shared<vof::VofBlockExchange>();
1419 vofBlockExch_->init(vofGlobalSize(), per, vofBlockRankBoxes(size), rank);
1420#ifdef PECLET_FLOW_MPI
1421 if (distributed_)
1422 vofBlockExch_->setComm(comm_);
1423#endif
1425 vofBlocks_->setExchange(vofBlockExch_);
1426}
1427
1428template <class Grid>
1430 vofBlocks_->assignMasters();
1431 vofBlocks_->scatter(vofAdv_.colour());
1433}
1434
1435template <class Grid>
1437 return static_cast<bool>(vofBlocks_);
1438}
1439
1440template <class Grid>
1442 vofBlocks_.reset();
1443 vofBlockExch_.reset();
1444}
1445
1446template <class Grid>
1448 const double dt = dtPhysArg * u_.timeToInt();
1449 if (!vofBlocks_)
1450 throw std::runtime_error("advect_vof_blocks: call enable_vof_blocks first");
1451 const double div = requireSolenoidal ? maxOpenDivergenceInternal() : 0.0; // index-unit gate
1452 if (!(div <= 1e-10)) {
1453 char msg[320];
1454 std::snprintf(msg, sizeof(msg),
1455 "advect_vof_blocks: the current face velocity is not discretely "
1456 "divergence-free (max|div(open*u)| = %.6g > 1e-10). Weymouth-Yue conservation "
1457 "is conditional on it; project the field first (step() / project()).",
1458 div);
1459 throw std::runtime_error(msg);
1460 }
1461 const double _tv0 = vofTick();
1462 vofBlocks_->timingOn = vofTiming_;
1463 const double _td0 = vofBlocks_->debrisSeconds;
1464 bridgeVelocityToVof(); // the block exchange reads THESE views (advector high-face convention)
1465 vofBlocks_->advect(dt, vofAdv_.colour());
1466 updateVofBlockOverlap(); // vof_overlap_design §5.6: S, the census, the phantom-bound gate
1468 if (vofTiming_) {
1469 vofAdd(vt_.blockAdvect, _tv0);
1470 vt_.blockDebris += vofBlocks_->debrisSeconds - _td0;
1471 }
1472}
1473
1474template <class Grid>
1475std::vector<vof::VofBlockStats> Solver<Grid>::vofBlockStats() const {
1476 if (!vofBlocks_)
1477 throw std::runtime_error("vof_block_stats: call enable_vof_blocks first");
1478 return vofBlocks_->statsAll();
1479}
1480
1481template <class Grid>
1483 if (!vofBlocks_)
1484 throw std::runtime_error("vof_block_imbalance: call enable_vof_blocks first");
1485 return vofBlocks_->cellImbalance();
1486}
1487
1488template <class Grid>
1489void Solver<Grid>::vofBlockCensus(std::vector<long>& masters, std::vector<long>& cells) const {
1490 if (!vofBlocks_)
1491 throw std::runtime_error("vof_block_census: call enable_vof_blocks first");
1492 vofBlocks_->masterCensus(masters, cells);
1493}
1494
1495template <class Grid>
1497 if (!vofBlocks_)
1498 throw std::runtime_error("set_vof_block_assign: call enable_vof_blocks first");
1499 vofBlocks_->assignMode = static_cast<vof::VofMasterAssign>(mode);
1500 vofBlocks_->reassignEvery = every;
1501 vofBlocks_->assignMasters();
1502}
1503
1504template <class Grid>
1506 if (!vofBlocks_)
1507 throw std::runtime_error("vof_block_assign: call enable_vof_blocks first");
1508 return static_cast<int>(vofBlocks_->assignMode);
1509}
1510
1511template <class Grid>
1513 if (!vofBlocks_)
1514 throw std::runtime_error("vof_block_imbalance_of: call enable_vof_blocks first");
1515 const auto save = vofBlocks_->assignMode;
1516 vofBlocks_->assignMode = static_cast<vof::VofMasterAssign>(mode);
1517 const std::vector<int> m = vofBlocks_->plannedMasters();
1518 vofBlocks_->assignMode = save;
1519 const int np = vofBlocks_->size();
1520 std::vector<long> load(static_cast<std::size_t>(np), 0);
1521 long tot = 0, mx = 0;
1522 for (std::size_t i = 0; i < m.size(); ++i) {
1523 const long w = vofBlocks_->blocks()[i].box.cells();
1524 load[static_cast<std::size_t>(m[i])] += w;
1525 tot += w;
1526 }
1527 for (long v : load)
1528 mx = std::max(mx, v);
1529 return tot == 0 ? 1.0 : static_cast<double>(mx) * np / static_cast<double>(tot);
1530}
1531
1532template <class Grid>
1534 if (!vofBlockExch_)
1535 throw std::runtime_error("set_vof_block_device_staging: call enable_vof_blocks first");
1536 vofBlockExch_->deviceStaging = on;
1537}
1538
1539template <class Grid>
1541 if (!vofBlocks_)
1542 throw std::runtime_error("set_vof_block_pool: call enable_vof_blocks first");
1543 vofBlocks_->usePool = on;
1544 if (!on)
1545 vofBlocks_->clearPool();
1546}
1547
1548template <class Grid>
1549std::array<long, 2> Solver<Grid>::vofBlockPoolStats() const {
1550 if (!vofBlocks_)
1551 throw std::runtime_error("vof_block_pool_stats: call enable_vof_blocks first");
1552 return {vofBlocks_->poolHits(), vofBlocks_->poolMisses()};
1553}
1554
1555template <class Grid>
1556void Solver<Grid>::enableVofBlocksFromField(const std::vector<std::array<int, 6>>& boxes) {
1558 for (const auto& q : boxes) {
1560 for (int d = 0; d < 3; ++d) {
1561 bb.lo[d] = q[d];
1562 bb.hi[d] = q[3 + d];
1563 }
1564 vofBlocks_->seedBox(bb);
1565 }
1566 bridgeColourToVof(); // the caller's `set_vof` field -> the g=3 patch the gather reads
1567 vofBlocks_->finishSeeding(vofAdv_.colour());
1569}
1570
1571template <class Grid>
1572std::vector<double> Solver<Grid>::vofBlockColour(long id) {
1573 if (!vofBlocks_)
1574 throw std::runtime_error("vof_block_color: call enable_vof_blocks first");
1575 if (id < 0 || static_cast<std::size_t>(id) >= vofBlocks_->count())
1576 throw std::runtime_error("vof_block_color: no such block id");
1577 return vofBlocks_->blockColourHost(static_cast<std::size_t>(id));
1578}
1579
1580template <class Grid>
1581std::vector<double> Solver<Grid>::vofBlockKappa(long id) {
1582 if (!vofBlocks_)
1583 throw std::runtime_error("vof_block_kappa: call enable_vof_blocks first");
1584 if (id < 0 || static_cast<std::size_t>(id) >= vofBlocks_->count())
1585 throw std::runtime_error("vof_block_kappa: no such block id");
1586 return vofBlocks_->blockKappaHost(static_cast<std::size_t>(id));
1587}
1588
1589template <class Grid>
1590void Solver<Grid>::enableVofBlocksFromColours(const std::vector<std::array<int, 6>>& boxes,
1591 const std::vector<std::vector<double>>& colours) {
1592 if (boxes.size() != colours.size())
1593 throw std::runtime_error("enable_vof_blocks_from_colors: one color array per box is required");
1595 for (std::size_t i = 0; i < boxes.size(); ++i) {
1597 for (int d = 0; d < 3; ++d) {
1598 bb.lo[d] = boxes[i][d];
1599 bb.hi[d] = boxes[i][3 + d];
1600 }
1601 vofBlocks_->seedBoxWithColour(bb, colours[i]);
1602 }
1604}
1605
1606template <class Grid>
1608 if (!vofBlocks_)
1609 throw std::runtime_error("enable_vof_block_csf: call enable_vof_blocks first");
1610 if (!(sigmaCsf_ > 0.0))
1611 throw std::runtime_error("enable_vof_block_csf: set_surface_tension(sigma) first (sigma > 0)");
1612 if constexpr (Grid::collocated)
1613 throw std::runtime_error(
1614 "enable_vof_block_csf: the block CSF is STAGGERED-only at rung W2 (the collocated face "
1615 "-acceleration form is V8's, and composing the two is not this rung).");
1616 // The block cascade must be the SAME estimator as the structured one -- above all the wisp
1617 // guard: without `interfaceEps` a Weymouth-Yue round-off wisp is an "interfacial" cell whose
1618 // zero-area PLIC polygon returns |kappa| ~ 1e8, and the face between it and a real interface
1619 // carries a force eight orders too large. Measured consequence when it was missing: the
1620 // distributed run's CSF force differed from the single-rank one by 6.7e-3 after two steps,
1621 // amplified from a 3e-16 colour difference by a flipped cascade branch.
1622 // Every tunable from the structured cascade in ONE copy (review2 finding 2: a hand-written list
1623 // here once dropped the debug switches), then the three the block path sets itself.
1624 vofBlocks_->curvProto.copyTunablesFrom(vofCurv_);
1625 vofBlocks_->curvProto.interfaceEps = csfInterfaceEps_;
1626 // Same contract as the structured cascade: the purity test follows the advector
1627 // (VofCurvature::pureEps).
1628 vofBlocks_->curvProto.pureEps = vofAdv_.wispEps;
1629 vofBlocks_->setWispEps(vofAdv_.wispEps); // §12.1: the advectors follow the same value
1630 vofBlocks_->curvProto.kappaMax = vofBlockKappaMax_; // the clip: BLOCK path only (§5.1, §11)
1631 vofBlocks_->debrisRemove = (vofBlockDebris_ != 0); // debris removal: default ON (§5.3)
1632 vofBlocks_->enableCsf(sigmaCsf_);
1633 const long len3 = static_cast<long>(e3_.x) * e3_.y * e3_.z;
1634 for (int c = 0; c < 3; ++c) {
1635 vofBlkF_[c] = SField("vof::blockcsf::patch", len3);
1636 csfBlkF_[c] = CCField("vof::blockcsf", n_);
1637 }
1638 updateVofBlockOverlap(); // an overlap present at seeding gates the very first step
1640}
1641
1642template <class Grid>
1644 if (!vofBlocks_ || !vofBlocks_->csfEnabled)
1645 return;
1646 vofBlocks_->computeCsf(vofBlkF_[0], vofBlkF_[1], vofBlkF_[2]);
1647 for (int c = 0; c < 3; ++c)
1648 copyInner(csfBlkF_[c], e_, G, CCConst(vofBlkF_[c]), e3_, kVofG);
1649}
1650
1651template <class Grid>
1652std::vector<double> Solver<Grid>::getVofBlockForce(int c) {
1653 if (!csfBlkF_[c].extent(0))
1654 throw std::runtime_error("vof_block_force: call enable_vof_block_csf() first");
1655 return gatherInner(csfBlkF_[c]);
1656}
1657
1658template <class Grid>
1660 if (!vofBlocks_)
1661 throw std::runtime_error("vof_block_curvature_stats: call enable_vof_blocks first");
1662 return vofBlocks_->csfCurvatureStats();
1663}
1664
1665template <class Grid>
1666void Solver<Grid>::setVofInflow(int f, double value) {
1667 checkVofBcFace(f, 2, "set_vof_inflow");
1668 vofInflowSet_[f] = true;
1669 vofInflowC_[f] = value;
1670 vofInflowProfRaw_[f].clear();
1671 vofBcArm();
1672}
1673
1674template <class Grid>
1675void Solver<Grid>::setVofInflowProfile(int f, const std::vector<double>& prof, int nb, int nc) {
1676 checkVofBcFace(f, 2, "set_vof_inflow_profile");
1677 if ((int)prof.size() != nb * nc)
1678 throw std::runtime_error("set_vof_inflow_profile: profile size != nb*nc");
1679 vofInflowSet_[f] = true;
1680 vofInflowProfRaw_[f] = prof;
1681 vofInflowProfNb_[f] = nb;
1682 vofInflowProfNc_[f] = nc;
1683 vofBcArm();
1684}
1685
1686template <class Grid>
1687void Solver<Grid>::setVofBackflow(int f, double value) {
1688 checkVofBcFace(f, 3, "set_vof_backflow");
1689 vofBackflowSet_[f] = true;
1690 vofBackflowC_[f] = value;
1691 vofBcArm();
1692}
1693
1694template <class Grid>
1696 return vofBcActive_;
1697}
1698
1699template <class Grid>
1700std::vector<double> Solver<Grid>::vofBcVolumes() const {
1701 return std::vector<double>(vofBcVol_, vofBcVol_ + 6);
1702}
1703
1704template <class Grid>
1705std::vector<double> Solver<Grid>::vofBcVolumesTotal() const {
1706 return std::vector<double>(vofBcVolTotal_, vofBcVolTotal_ + 6);
1707}
1708
1709template <class Grid>
1711 for (int f = 0; f < 6; ++f)
1712 vofBcVol_[f] = vofBcVolTotal_[f] = 0.0;
1713}
1714
1715template <class Grid>
1717 if (!vofBcActive_ || !vofEnabled_)
1718 return;
1719 if (f.data() != vofAdv_.colour().data())
1720 return;
1721 const I3 e3{e3_.x, e3_.y, e3_.z};
1722 for (int face = 0; face < 6; ++face) {
1723 if (!touchesGlobalFace(face))
1724 continue; // rank-owned global faces only (the WO-F rule)
1725 const int a = face / 2, sd = face % 2;
1726 if (bc_[face] == 2 && vofInflowSet_[face]) {
1727 if (vofInflowProf3_[face].extent(0))
1728 vof::bcColourProfile(f, e3, kVofG, a, sd, vofInflowProf3_[face], vofProf3Nc_[face]);
1729 else
1730 vof::bcColourConst(f, e3, kVofG, a, sd, vofInflowC_[face]);
1731 } else if (bc_[face] == 3 && vofBackflowSet_[face]) {
1732 // reads the face velocity the advector is about to flux with, so it must run after
1733 // bridgeVelocityToVof() — which it does: advectVof bridges the velocity first.
1734 vof::bcColourBackflow(f, e3, kVofG, a, sd, vofAdv_.faceVel(a), vofBackflowC_[face]);
1735 }
1736 }
1737}
1738
1739template <class Grid>
1741 if (!vofBcActive_ || !vofEnabled_)
1742 return;
1743 const bool isColour = cField_.extent(0) && f.data() == cField_.data();
1744 const I3 e2{e_.x, e_.y, e_.z};
1745 for (int face = 0; face < 6; ++face) {
1746 if (bc_[face] != 2 || !vofInflowSet_[face] || !touchesGlobalFace(face))
1747 continue;
1748 const int a = face / 2, sd = face % 2;
1749 if (isColour) {
1750 if (vofInflowProfG2_[face].extent(0))
1751 vof::bcColourProfile(f, e2, G, a, sd, vofInflowProfG2_[face], vofProfG2Nc_[face]);
1752 else
1753 vof::bcColourConst(f, e2, G, a, sd, vofInflowC_[face]);
1754 } else {
1755 for (const auto& cl : closures_)
1756 if (cl.out.data() == f.data())
1757 applyClosureFaceGhost(cl, e_, G, a, sd);
1758 }
1759 }
1760}
1761
1762template <class Grid>
1764 if (!vofEnabled_)
1765 return;
1766 const I3 e3{e3_.x, e3_.y, e3_.z};
1767 const bool px = vofAxisPeriodic(0), py = vofAxisPeriodic(1), pz = vofAxisPeriodic(2);
1768 if (vofBcActive_) {
1769 vofOutside_ = vof::UCField("vof::outside", vofAdv_.size());
1770 vof::buildOutsideMask(vofOutside_, e3, kVofG, vofOrigin(), vofGlobalSize(), px, py, pz);
1771 vofAdv_.setOutsideMask(vofOutside_);
1772 } else {
1773 vofAdv_.setOutsideMask(vof::UCField());
1774 }
1775 // the boundary-flux ledger counts only the GLOBAL domain faces this rank owns
1776 for (int face = 0; face < 6; ++face)
1777 vofAdv_.setBcFaceOwned(face, vofBcActive_ && bc_[face] != 0 && touchesGlobalFace(face));
1778 for (int face = 0; face < 6; ++face) {
1779 vofInflowProf3_[face] = CCField();
1780 vofInflowProfG2_[face] = CCField();
1781 if (vofInflowProfRaw_[face].empty())
1782 continue;
1783 vofInflowProf3_[face] =
1784 resampleFaceScalar(vofInflowProfRaw_[face], vofInflowProfNb_[face], vofInflowProfNc_[face],
1785 face, e3_, kVofG, vofProf3Nc_[face]);
1786 vofInflowProfG2_[face] =
1787 resampleFaceScalar(vofInflowProfRaw_[face], vofInflowProfNb_[face], vofInflowProfNc_[face],
1788 face, e_, G, vofProfG2Nc_[face]);
1789 }
1790}
1791
1792template <class Grid>
1793CCField Solver<Grid>::resampleFaceScalar(const std::vector<double>& prof, int nb, int nc, int face,
1794 C3 ext, int g, int& outNc) {
1795 const int a = face / 2;
1796 const int dims[3] = {ext.x, ext.y, ext.z};
1797 const int bax = (a + 1) % 3, cax = (a + 2) % 3;
1798 const int Lb = dims[bax], Lc = dims[cax];
1799 CCField pf("vof::bcprof", (std::size_t)Lb * Lc);
1800 auto h = Kokkos::create_mirror_view(pf);
1801 auto cl = [](int v, int n) { return v < 0 ? 0 : (v >= n ? n - 1 : v); };
1802 for (int p0 = 0; p0 < Lb; ++p0)
1803 for (int p1 = 0; p1 < Lc; ++p1)
1804 h((long)p0 * Lc + p1) = prof[(std::size_t)cl(p0 - g, nb) * nc + cl(p1 - g, nc)];
1805 Kokkos::deep_copy(pf, h);
1806 outNc = Lc;
1807 return pf;
1808}
1809
1810template <class Grid>
1811void Solver<Grid>::checkVofBcFace(int f, int wantType, const char* who) {
1812 if (f < 0 || f > 5)
1813 throw std::runtime_error(std::string(who) + ": face must be 0..5 (-x,+x,-y,+y,-z,+z)");
1814 enableVof();
1815 if (bc_[f] != wantType)
1816 throw std::runtime_error(std::string(who) + ": face " + std::to_string(f) +
1817 " has domain BC type " + std::to_string(bc_[f]) + ", not " +
1818 std::to_string(wantType) + " — call set_domain_bc(face, " +
1819 std::to_string(wantType) +
1820 ", ...) first (2 = inflow, "
1821 "3 = outflow).");
1822}
1823
1824template <class Grid>
1826 vofBcActive_ = true;
1828 // Refresh C's G=2 ghost band NOW. The property ghosts derive from it (vofBcPropGhosts) and the
1829 // first step's `project()` fills rho's ghosts BEFORE the colour advection refills C's, so
1830 // without this the first step would evaluate rho(C) at the inflow ghost on the pre-BC (Neumann
1831 // copy) colour — i.e. on the interior's phase.
1832 if (vofEnabled_ && cField_.extent(0))
1833 fillPropGhosts(cField_);
1834}
1835
1836template <class Grid>
1838 if (!vofBcActive_)
1839 return;
1840 for (int f = 0; f < 6; ++f) {
1841 vofBcVol_[f] = vofAdv_.bcFaceVolume(f);
1842 vofBcVolTotal_[f] += vofBcVol_[f];
1843 }
1844}
1845
1846template <class Grid>
1848 if (!vofEnabled_)
1849 throw std::runtime_error(
1850 "compute_vof_curvature: VoF is not enabled (call enable_vof / set_vof first)");
1851 if (!kappaField_.extent(0)) {
1852 kappaField_ = addField("kappa");
1853 kappaBranch_ = addField("kappa_branch");
1854 }
1856 // The cascade's purity test must agree with the one the colour field was ADVECTED under. Set
1857 // here, at the point of use, rather than at enable_vof: `set_vof_wisp_eps` and
1858 // `enable_phase_change` both move `wispEps` afterwards, and a stale copy degrades the height
1859 // function silently (see `vof::VofCurvature::pureEps` for the mechanism and the measurement).
1860 vofCurv_.pureEps = vofAdv_.wispEps;
1861 vofCurvStats_ = vofCurv_.compute(vofAdv_.colour());
1862 copyInner(kappaField_, e_, G, CCConst(vofCurv_.kappa()), e3_, kVofG);
1863 copyInner(kappaBranch_, e_, G, CCConst(vofCurv_.branch()), e3_, kVofG);
1864 // kappa is face-interpolated by the V4 surface-tension force exactly as the properties are, so
1865 // it gets the same rank-aware ghost policy they do (WO-G / WO-I).
1866 fillPropGhosts(kappaField_);
1867 fillPropGhosts(kappaBranch_);
1868}
1869
1870template <class Grid>
1872 return vofCurvStats_;
1873}
1874
1875template <class Grid>
1876std::vector<double> Solver<Grid>::getVofCurvature() {
1877 if (!kappaField_.extent(0))
1878 throw std::runtime_error("vof_curvature: call compute_vof_curvature() first");
1879 std::vector<double> out = gatherInner(kappaField_);
1880 const double k = u_.curvToPhys();
1881 if (k != 1.0)
1882 for (double& x : out)
1883 x *= k;
1884 return out;
1885}
1886
1887template <class Grid>
1889 if (!kappaBranch_.extent(0))
1890 throw std::runtime_error("vof_curvature_branch: call compute_vof_curvature() first");
1891 return gatherInner(kappaBranch_);
1892}
1893
1894template <class Grid>
1896 vofCurv_.weightWidth = d;
1897}
1898
1899template <class Grid>
1901 return vofCurv_.weightWidth;
1902}
1903
1904template <class Grid>
1908
1909template <class Grid>
1911 return vofCurv_.useMixedHeightFit;
1912}
1913
1914template <class Grid>
1916 if (!(sigma >= 0.0))
1917 throw std::runtime_error("set_surface_tension: sigma must be >= 0");
1918 if (sigma > 0.0) {
1919 enableVof();
1920 if (!kappaField_.extent(0)) {
1921 kappaField_ = addField("kappa");
1922 kappaBranch_ = addField("kappa_branch");
1923 }
1924 // Wisp guard on the curvature's interfacial predicate. NOT optional once the curvature feeds
1925 // a force: Weymouth-Yue leaves round-off colour residue (measured down to -3e-35) in every
1926 // cell its sweeps touch, those cells satisfy `0 < C < 1`, and the cascade returns |kappa| up
1927 // to 1e8 for them off a zero-area PLIC polygon. A face between one of them and a real
1928 // interfacial cell then carries a force eight orders too large. See
1929 // `vof::VofCurvature::interfaceEps` for the measurement; the V3 default (0) is unchanged for
1930 // anyone calling `compute_vof_curvature()` without surface tension.
1931 vofCurv_.interfaceEps = csfInterfaceEps_;
1932 }
1933 // sigma is PHYSICAL (force per unit length); internally sigma*tRef^2/(rhoRef*hRef^3), which is
1934 // what makes p' = sigma'*kappa' hold with kappa' = kappa*hRef.
1935 sigmaPhys_ = sigma;
1936 sigmaCsf_ = sigma * u_.sigmaToInt();
1937 registerBalancedForceState(); // V8 (collocated CSF) turns the balanced-force projection on
1938}
1939
1940template <class Grid>
1942 return sigmaPhys_;
1943}
1944
1945template <class Grid>
1947 csfInterfaceEps_ = eps;
1948 if (sigmaCsf_ > 0.0)
1949 vofCurv_.interfaceEps = eps;
1950}
1951
1952template <class Grid>
1954 return csfInterfaceEps_;
1955}
1956
1957template <class Grid>
1959 double km = 0.0; // off
1960 if (enabled) {
1961 if (kappaMaxPhys < 0.0)
1962 km = -1.0; // the default 1/Delta_min, evaluated against the cascade's own metric
1963 else if (kappaMaxPhys > 0.0)
1964 km = kappaMaxPhys * u_.lenToPhys(); // kappa' = kappa * hRef (index units)
1965 else
1966 throw std::runtime_error(
1967 "set_vof_block_kappa_clip: kappa_max must be > 0 (or None for 1/Delta_min)");
1968 }
1969 vofBlockKappaMax_ = km; // the single-field cascade (vofCurv_) never clips (§11)
1970 if (vofBlocks_)
1971 vofBlocks_->setKappaMax(km);
1972}
1973
1974template <class Grid>
1976 csfMode_ = m;
1977}
1978
1979template <class Grid>
1981 return csfMode_;
1982}
1983
1984template <class Grid>
1986 return static_cast<bool>(vofBlocks_) && vofBlocks_->csfEnabled;
1987}
1988
1989template <class Grid>
1991 return vofEnabled_ && sigmaCsf_ > 0.0 && (kappaField_.extent(0) != 0 || vofBlockCsf());
1992}
1993
1994template <class Grid>
1996 kappaFrozen_ = on;
1997}
1998
1999template <class Grid>
2001 return kappaFrozen_;
2002}
2003
2004template <class Grid>
2006 enableVof();
2007 if (!kappaField_.extent(0)) {
2008 kappaField_ = addField("kappa");
2009 kappaBranch_ = addField("kappa_branch");
2010 }
2011 Kokkos::deep_copy(kappaField_, kappa / u_.curvToPhys());
2012 Kokkos::deep_copy(kappaBranch_, (double)vof::kCurvHf);
2013 kappaFrozen_ = true;
2014}
2015
2016template <class Grid>
2018 return capillaryDtInternal() * u_.timeToPhys();
2019}
2020
2021template <class Grid>
2023 if (!(sigmaCsf_ > 0.0))
2024 return std::numeric_limits<double>::infinity();
2025 // Phase 3 (V3.2): the SMALLEST spacing sets the shortest resolvable capillary wave. `hpMin`
2026 // is exactly 1.0 on every isotropic run, so the number is unchanged.
2027 double hpMin = u_.hp[0];
2028 for (int a = 1; a < 3; ++a)
2029 hpMin = hpMin < u_.hp[a] ? hpMin : u_.hp[a];
2030 // vof_overlap_design §5.6: while two block markers overlap, the part of A's surface inside B
2031 // is an interface with rho_min on BOTH sides, so its Brackbill bound uses 2 rho_min.
2032 if (vofOverlap_.active && vofBlockCsf()) {
2033 double lo = 0.0, hi = 0.0;
2034 phaseDensityRange(lo, hi);
2035 return vof::capillaryDt(2.0 * lo, vofEnabled_ ? hpMin : 1.0, sigmaCsf_);
2036 }
2037 return vof::capillaryDt(phaseDensitySum(), vofEnabled_ ? hpMin : 1.0, sigmaCsf_);
2038}
2039
2040template <class Grid>
2042 capillaryCfl_ = f;
2043}
2044
2045template <class Grid>
2047 return capillaryCfl_;
2048}
2049
2050template <class Grid>
2052 VofStepLimits L;
2053 L.courant = vofMaxCourant();
2054 L.cflDt = (L.courant > 0.0) ? dtPhys_ * vofCflLimit_ / L.courant
2055 : std::numeric_limits<double>::infinity();
2056 L.capillaryDt = capillaryDt();
2057 const double cap = capillaryCfl_ * L.capillaryDt;
2058 L.capillaryBinds = cap < L.cflDt;
2059 L.binding = L.capillaryBinds ? cap : L.cflDt;
2060 return L;
2061}
2062
2063template <class Grid>
2065 double lo = 0.0, hi = 0.0;
2066 phaseDensityRange(lo, hi);
2067 return lo + hi;
2068}
2069
2070template <class Grid>
2072 if (vofMomEnabled_) {
2073 rlo = std::fmin(vofRhoG_, vofRhoL_);
2074 rhi = std::fmax(vofRhoG_, vofRhoL_);
2075 return;
2076 }
2077 if (!effVarRho()) {
2078 rlo = rho_;
2079 rhi = rho_;
2080 return;
2081 }
2082 CCExec space;
2083 C3 e = e_;
2084 double lo = 1e300, hi = -1e300;
2085 using MD = MDRange3<CCExec>;
2086 // A closure-driven rho field is produced by updateProperties() at the head of the step, so
2087 // before the FIRST step it is still all zeros and a naive min+max would report 0 — and this is
2088 // a diagnostic users call while choosing dt, i.e. exactly then. Refresh and retry once in that
2089 // case; the step's own call site runs after updateProperties() and never takes the branch.
2090 for (int pass = 0; pass < 2; ++pass) {
2092 lo = 1e300;
2093 hi = -1e300;
2094 Kokkos::parallel_reduce(
2095 "rho_minmax", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
2096 KOKKOS_LAMBDA(int x, int y, int z, double& mn, double& mx) {
2097 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
2098 mn = Kokkos::fmin(mn, f(i));
2099 mx = Kokkos::fmax(mx, f(i));
2100 },
2101 Kokkos::Min<double>(lo), Kokkos::Max<double>(hi));
2102 if (hi > 0.0 || pass == 1)
2103 break;
2105 }
2106#ifdef PECLET_FLOW_MPI
2107 if (distributed_) {
2108 double g[2] = {lo, -hi}, r[2];
2110 lo = r[0];
2111 hi = -r[1];
2112 }
2113#endif
2114 rlo = lo;
2115 rhi = hi;
2116}
2117
2118template <class Grid>
2120 if (!vofEnabled_)
2121 throw std::runtime_error(
2122 "step_adaptive: needs enable_vof() (it picks dt from the two "
2123 "explicit two-phase limits vof_step_limits() reports)");
2124 if (!(cflTarget > 0.0) || !(capillaryCflTarget > 0.0) || !(dtMax > 0.0))
2125 throw std::runtime_error("step_adaptive: cfl_target, capillary_cfl and dt_max must be > 0");
2126 const VofStepLimits L = vofStepLimits();
2127 double dt = cflTarget * L.cflDt;
2128 const double dc = capillaryCflTarget * L.capillaryDt;
2129 if (dc < dt)
2130 dt = dc;
2131 if (dtMax < dt)
2132 dt = dtMax;
2133 if (!(dt > 0.0) || !std::isfinite(dt))
2134 throw std::runtime_error("step_adaptive: no finite dt (cfl_dt = " + std::to_string(L.cflDt) +
2135 ", capillary_dt = " + std::to_string(L.capillaryDt) +
2136 ") - pass a finite dt_max when neither limit is active");
2137 setDt(dt);
2138 // The limits were just evaluated on this exact state, so `step()`'s own head-of-step
2139 // pre-check (item 1) would repeat the interface-local reduction and the g=3 fill for nothing.
2140 vofPrecheckDone_ = true;
2141 step();
2142 return dt;
2143}
2144
2145template <class Grid>
2147 const bool skip = vofPrecheckDone_;
2148 vofPrecheckDone_ = false;
2149 if (!vofEnabled_ || skip)
2150 return;
2151 if (csfActive()) {
2152 const double cap = capillaryCfl_ * capillaryDtInternal();
2153 if (!(dt_ <= cap))
2154 throw std::runtime_error(capillaryThrowMessage(cap));
2155 }
2156 // The block container carries one advector per marker with its own colour support; the union
2157 // field on `vofAdv_` is a derived quantity there, so the cap is left to the blocks themselves.
2158 if (vofBlocks_)
2159 return;
2160 const double cfl = vofMaxCourant();
2161 if (!(cfl <= vofCflLimit_))
2162 throw std::runtime_error(
2163 "peclet::flow::vof::WyAdvector: CFL = max|uf| dt/h = " + std::to_string(cfl) +
2164 " exceeds the Weymouth-Yue boundedness cap " + std::to_string(vofCflLimit_) +
2165 " (dt = " + std::to_string(dt_) + ", h = " + std::to_string(vofAdv_.h()) +
2166 ") - reduce dt. Rejected at the HEAD of step(): no field has been advanced, so a retry "
2167 "at a smaller dt is exact (see set_vof_cfl_limit / step_adaptive).");
2168}
2169
2170template <class Grid>
2171std::string Solver<Grid>::capillaryThrowMessage(double cap) const {
2172 const std::string rho = (vofOverlap_.active && vofBlockCsf())
2173 ? "2 rho_min [block markers overlap: the phantom gas-gas interface, "
2174 "vof_overlap_design 5.6]"
2175 : "rho_1+rho_2";
2176 return "surface tension: dt = " + std::to_string(dt_) + " exceeds the capillary limit " +
2177 std::to_string(cap) + " (Brackbill sqrt((" + rho + ") h^3/(4 pi sigma)) = " +
2178 std::to_string(cap / (capillaryCfl_ > 0.0 ? capillaryCfl_ : 1.0)) + " x safety factor " +
2179 std::to_string(capillaryCfl_) +
2180 "). Surface tension is EXPLICIT: this is a hard stability boundary (Denner & van "
2181 "Wachem 2015), not a margin. Reduce dt, or raise set_capillary_cfl deliberately.";
2182}
2183
2184template <class Grid>
2186 if (!csfActive())
2187 return;
2188 if (vofBlockCsf())
2189 computeVofBlockCsf(); // rung W2: per-block cascade + the CSF face force, scattered SUM
2190 else if (!kappaFrozen_)
2192 const double cap = capillaryCfl_ * capillaryDtInternal();
2193 if (!(dt_ <= cap))
2194 throw std::runtime_error(capillaryThrowMessage(cap));
2195}
2196
2197template <class Grid>
2199 if (!vofEnabled_)
2200 return;
2201 // Rung W2: with the block container on, the colour is the BLOCKS' state and the registered
2202 // "C" is the union derived from it — so the step's colour stage is the block advection, in
2203 // exactly this slot and for exactly the same reason (the face field has just been projected).
2204 // `vofBlocks_` is null unless `enable_vof_blocks` ran, so every other path is unchanged.
2205 if (vofBlocks_) {
2206 advectVofBlocks(dt_, /*requireSolenoidal=*/false);
2207 return;
2208 }
2209 requireVofGeometry("enable_vof");
2210 const double _tv0 = vofTick(); // WO-V9: the whole colour stage
2211 double _tb = 0.0; // ... of which the G=2 <-> g=3 bridges
2212 const double _tb0 = vofTick();
2215 vofAdd(_tb, _tb0);
2216 vofAdv_.resetBcFaceVolume(); // WO-R: the per-step boundary liquid ledger
2217 if (pcEnergy_) {
2218 // WO-P23: the CONSISTENT rho c_p T transport. The temperature rides the SAME sweeps, planes
2219 // and fluxes as the colour (`vof/energy_advect.hpp`), so it has to be bridged onto the g=3
2220 // block first and taken off it after; the scalar module then does diffusion ONLY (its
2221 // `energy` flag switches off the Koren advective term and the constant-D operator).
2222 ScalarField& sc = scalarField(pcTName_);
2223 copyInner(vofEnergy_.temperature(), e3_, kVofG, CCConst(sc.c), e_, G);
2224 vofEnergy_.advect(vofAdv_, dt_, vofStep_++);
2225 copyInner(sc.c, e_, G, CCConst(vofEnergy_.temperature()), e3_, kVofG);
2226 scalarFillGhosts(sc);
2227 } else {
2228 vofAdv_.advect(dt_, vofStep_++);
2229 }
2231 // Back to the G=2 registry mirror, then ITS ghost policy: the closures write inner cells only,
2232 // but the property face means (rho_f in the momentum diagonal, the projection coefficient, the
2233 // face body force) read the ghost ring, so C's ghosts must be filled with the SAME policy rho
2234 // uses or the derived rho ghost is inconsistent with the interior at a boundary.
2235 const double _tb1 = vofTick();
2236 copyInner(cField_, e_, G, CCConst(vofAdv_.colour()), e3_, kVofG);
2237 zeroSolidColour(); // rung V5a: the canonical field carries 0 in solid cells, not the fill
2238 fillPropGhosts(cField_);
2239 vofAdd(_tb, _tb1);
2240 vofAdd(vt_.advect, _tv0);
2241 vt_.bridge += _tb;
2242}
2243
2244template <class Grid>
2246 const double rhoGas = rhoGasPhys * u_.rhoToInt(), rhoLiquid = rhoLiquidPhys * u_.rhoToInt();
2247 if constexpr (Grid::collocated)
2248 throw std::runtime_error(
2249 "enable_vof_momentum: momentum-consistent VoF transport is STAGGERED-ONLY (rung V2b); the "
2250 "collocated construction is Favre-averaged face states, rung V8.");
2251 enableVof();
2252 if (porous_)
2253 throw std::runtime_error(
2254 "enable_vof_momentum: the volume-averaged porous momentum and the momentum-consistent VoF "
2255 "transport both own the momentum time term; they are not composable at this rung.");
2256 if (!(rhoGas > 0.0) || !(rhoLiquid > 0.0))
2257 throw std::runtime_error("enable_vof_momentum: both phase densities must be > 0");
2258 if (vofBlocks_)
2259 throw std::runtime_error(kVofMomBlocksMsg);
2260 vofRhoG_ = rhoGas;
2261 vofRhoL_ = rhoLiquid;
2262 vofMomEnabled_ = true;
2263 for (int c = 0; c < 3; ++c)
2264 if (uAdv_[c].extent(0) != n_)
2265 uAdv_[c] = CCField("uAdv", n_);
2266 vofMom_.init(vofAdv_, vofRhoG_, vofRhoL_);
2267}
2268
2269template <class Grid>
2271 return vofMomEnabled_;
2272}
2273
2274template <class Grid>
2276 vofMom_.rhoFloorFrac = f;
2277}
2278
2279template <class Grid>
2281 return vofMom_.rhoFloorFrac;
2282}
2283
2284template <class Grid>
2286 return vofMom_.lastRhoFloor();
2287}
2288
2289template <class Grid>
2291 vofMom_.momentumMuscl = on;
2292}
2293
2294template <class Grid>
2298
2299template <class Grid>
2301 vofMom_.clampFluxes = on;
2302}
2303
2304template <class Grid>
2306 if (!vofMomEnabled_)
2307 throw std::runtime_error("vof_momentum_diagnostics: enable_vof_momentum was never called");
2308 return vofMom_.diagnostics();
2309}
2310
2311template <class Grid>
2312std::vector<double> Solver<Grid>::getVofAdvectedVelocity(int c) {
2313 if (!vofMomEnabled_)
2314 throw std::runtime_error("vof_advected_velocity: enable_vof_momentum was never called");
2315 return gatherInner(uAdv_[c]);
2316}
2317
2318template <class Grid>
2320 if (!vofMomEnabled_)
2321 return;
2322 requireVofGeometry("enable_vof_momentum");
2323 if (implicitAdv())
2324 throw std::runtime_error(
2325 "enable_vof_momentum is incompatible with implicit advection (set_implicit_advection / a "
2326 "domain-BC stencil path): the momentum advection is already done conservatively by the "
2327 "VoF fluxes, and the implicit-FOU operator would add a second one.");
2328 if (!effVarRho())
2329 throw std::runtime_error(
2330 "enable_vof_momentum requires the variable-density momentum/projection path: register a "
2331 "density closure on C (set_property_model('rho','linear','C',[rho_g, rho_l-rho_g])) or "
2332 "call set_density_mode('variable').");
2333 if (vofMom_.phaseRhoG() != vofRhoG_ || vofMom_.phaseRhoL() != vofRhoL_)
2334 vofMom_.setPhaseDensities(vofRhoG_, vofRhoL_);
2335 const double _tv0 = vofTick(); // WO-V9: the whole colour + momentum stage
2336 double _tb = 0.0;
2337 const double _tb0 = vofTick();
2340 vofAdd(_tb, _tb0);
2341 vofAdv_.resetBcFaceVolume(); // WO-R: the per-step boundary liquid ledger
2342 vofMom_.advect(vofAdv_, dt_, vofStep_++);
2344 // Colour back to the G=2 registry mirror (same contract as advectVof), and the advected
2345 // velocity back onto the solver's velocity index convention.
2346 const double _tb1 = vofTick();
2347 copyInner(cField_, e_, G, CCConst(vofAdv_.colour()), e3_, kVofG);
2349 fillPropGhosts(cField_);
2350 // A plain inner-to-inner copy: the momentum control volumes are indexed in the solver's own
2351 // low-face convention (vof/momentum_advect.hpp "Indexing"), so CV_c(i) IS the solver's u_c(i)
2352 // and there is no shift here to get wrong.
2353 for (int c = 0; c < 3; ++c)
2354 copyInner(uAdv_[c], e_, G, CCConst(vofMom_.advectedVelocity(c)), e3_, kVofG);
2355 vofAdd(_tb, _tb1);
2356 vofAdd(vt_.momAdvect, _tv0);
2357 vt_.momBridge += _tb;
2358}
2359
2360template <class Grid>
2362 const bool px = vofAxisPeriodic(0), py = vofAxisPeriodic(1), pz = vofAxisPeriodic(2);
2363#ifdef PECLET_FLOW_MPI
2364 if (distributed_ && vofDev_)
2365 vofDev_->exchange(f);
2366 else
2367#endif
2368 vof::periodicFill(f, I3{e3_.x, e3_.y, e3_.z}, kVofG, px, py, pz);
2369 if (px && py && pz)
2370 return;
2371 const I3 gs = vofGlobalSize(), org = vofOrigin();
2372 vof::clampFill(f, I3{e3_.x, e3_.y, e3_.z}, kVofG, org, gs, px, py, pz);
2373}
2374
2375template <class Grid>
2376double Solver<Grid>::vofTick() const {
2377 if (!vofTiming_)
2378 return 0.0;
2379 Kokkos::fence();
2381}
2382
2383template <class Grid>
2384void Solver<Grid>::vofAdd(double& acc, double t0) {
2385 if (!vofTiming_)
2386 return;
2387 Kokkos::fence();
2388 acc += vof::WyAdvector::wallSeconds() - t0;
2389}
2390
2391} // namespace peclet::flow
2392
2393#endif // PECLET_FLOW_FLOW_IBM_VOF_HPP
void imposeDomainWallKind(int mask)
void vofExchangeScalar(CCField f)
The colour block's ghost policy WITHOUT the colour-specific rules (no solid-band fill,...
void setVofMomentumMuscl(bool on)
void setVofCurvatureMixedHeightFit(bool on)
void setContactAngleClamp(double loDeg, double hiDeg)
const VofTiming & vofTimingReport() const
bool vofBlocksEnabled() const
void setVofRhoFloorFrac(double f)
void enableVofBlocksFromField(const std::vector< std::array< int, 6 > > &boxes)
static constexpr int kVofG
void setContactAngleSmoothing(bool on)
void vofApplyColourBc(CCField f)
The colour field's boundary rules, applied at the END of vofFillGhosts — i.e.
void fillVelGhosts(int comp, int fold)
double maxOpenDivergenceInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
void addCsfRhs(int c, CCField dst, bool rowScaled, bool faceRange=false)
double capillaryCfl() const
void scatterInner(CCField fld, const std::vector< double > &in)
void setContactAnglePivot(int mode)
void setVofBlockPool(bool on)
bool contactAngleSet() const
ScalarField & scalarField(const std::string &name)
void setVofKappaConstant(double kappa)
kappa is a PHYSICAL curvature, 1/length (the internal field is 1/h; see vof_curvature()).
void fillGhosts(CCField f)
std::vector< double > getVofCurvature()
The interface curvature kappa = 2H in the caller's units, i.e.
void fillPropGhosts(CCField f, bool edges=false)
void checkVofBcFace(int f, int wantType, const char *who)
A VoF boundary colour is only meaningful on a face that already carries the matching domain BC,...
void setVofTiming(bool on)
std::vector< double > vofBlockKappa(long id)
void addCsfRhsBlocks(int c)
void setVofMomentumCellFlag(bool on)
CCField addField(const std::string &name)
void vofBcArm()
Arm the rung: install the mask, rebuild the profiles, and zero the boundary volume ledger.
bool vofHasGeometry() const
std::vector< double > getVofFilledColour()
void enableVofBlocks(const std::vector< std::array< double, 4 > > &seeds)
void pcUpdateEnergyProps()
Refresh k(C) and (rho c_p)(C) on the G=2 block from the current colour.
std::vector< double > getVofAdvectedVelocity(int c)
void setContactAngleDynamic(double thetaEDeg, double slipCells, double muLiquid, double sigma=0.0)
void setPressureExactResidual(bool on)
WO-R2 item 3 — the exact (matrix-free, double, flux-form) level-0 operator apply in the residual and ...
std::vector< double > getVofGeometry(int which)
void vofRebuildBcBlock()
(Re)build everything that lives on the g=3 block for this rung: the out-of-domain mask and the resamp...
std::vector< double > vofBcVolumes() const
Signed liquid volume that crossed each of the six domain faces during the LAST colour advection,...
double maxOpenDivergenceProjectedInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
std::vector< double > getVof()
double surfaceTension() const
The surface tension in the caller's units (what set_surface_tension was given).
void copyInner(CCField dst, C3 de, int dg, CCConst src, C3 se, int sg)
double capillaryDtInternal()
The same limit in the solver's index time (per tRef) — what the step's own guards compare to.
double vofTimingProjectionSeconds() const
CsfDiagnostics csfDiagnostics()
void setSurfaceTension(double sigma)
void vofHarvestBcVolumes()
Move the advector's per-face boundary volume ledger into the solver's, once per advection.
void enableVofMomentum(double rhoGasPhys, double rhoLiquidPhys)
rhoGas / rhoLiquid are PHYSICAL densities, the same numbers the rho closure is given.
std::vector< double > getVofCurvatureBranch()
void setVofInflowProfile(int f, const std::vector< double > &prof, int nb, int nc)
Per-position inflow colour on face f: prof is (nb, nc) on the INNER grid of the face's two perpendicu...
double vofBlockImbalanceOf(int mode) const
void requireVofGeometry(const char *who)
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)
static constexpr double defaultVofWispEps()
The value enableVof starts from.
void buildVelocityOverlays(bool resetU)
Build the three per-component Robust-Scaled cut-cell overlays + solid masks from the CURRENT sdf_ (ex...
vof::VofCurvature::Stats vofBlockCurvatureStats() const
void applyDomainWallGeometry(int mask)
std::vector< double > getVofBlockForce(int c)
bool vofKappaFrozen() const
std::array< long, 2 > vofBlockPoolStats() const
void setVofCutFluxClamp(bool on)
ContactAngleDiagnostics contactAngleDiagnostics()
void setVofCurvatureWeightWidth(double d)
bool vofCutFluxClamp() const
double vofRhoFloorFrac() const
double vofTimingPredictorSeconds() const
void vofExchangeRaw(CCField f)
double wallSlipLength() const
The Navier slip length in the caller's units (0 = no-slip).
double vofInterfaceEps() const
void setVofBlockAssign(int mode, long every)
double contactAngle() const
void setWallSlipLength(double lambdaCells)
WO-V6b – the VELOCITY half of the dynamic contact line.
void requireWettingWall() const
void vofBlockCensus(std::vector< long > &masters, std::vector< long > &cells) const
void setVofBackflow(int f, double value)
inletOutlet backflow colour on outflow face f (default 0 = gas): where the boundary face velocity poi...
vof::MomentumConsistentAdvector::Diagnostics vofMomentumDiagnostics()
std::vector< double > vofBlockColour(long id)
void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow=true)
std::vector< double > getVofDynamicField(int which)
void setVofCurvatureWorklist(bool on)
VofCurvature::useWorklist — run the height-function cascade and the PV fallback over a COMPACTED list...
static constexpr int G
Definition flow_ibm.hpp:80
void setContactAngleField(const std::vector< double > &thetaDeg)
void setCapillaryCfl(double f)
const vof::VofCurvature::Timing & vofCurvatureTiming() const
CCField resampleFaceScalar(const std::vector< double > &prof, int nb, int nc, int face, C3 ext, int g, int &outNc)
Clamp-resample a per-position face scalar from the user's (nb, nc) INNER grid onto the ghost-inclusiv...
bool vofMomentumEnabled() const
vof::VofCurvature::Stats vofCurvatureStats() const
void setVofPhantomCapillaryBound(bool enabled)
int contactAnglePivot() const
std::vector< double > vofBcVolumesTotal() const
The same, accumulated since enable_vof() (or the last resetVofBcVolumes()).
void setVofWorklist(bool on)
WyAdvector::useWorklist — the compaction of the PLIC reconstruction pass onto the mixed cells.
void vofExtendWallBand(CCField f, int mask)
void setVofInflow(int f, double value)
Colour of the fluid entering through inflow face f (0..5 = -x,+x,-y,+y,-z,+z), in [0,...
bool contactAngleHysteresis() const
void setVofStepParity(long n)
long strideOf(int c) const
const vof::WyAdvector::Timing & vofKernelTiming() const
void collocatedV8AutoFallback(const char *why)
std::array< int, 3 > wallSlipSandwichCells() const
Cut-cell axes at which a one-cell fluid gap kept the no-slip closure (per component).
void setVofBlockDeviceStaging(bool on)
void setVofKappaFrozen(bool on)
double vofRhoFloor() const
double stepAdaptive(double cflTarget, double capillaryCflTarget, double dtMax)
void setVofBlockOverlapDensity(bool enabled)
static double vofWallPlaneSdf(int gx, int gy, int gz, I3 gs, int mask)
void enableVofBlocksFromColours(const std::vector< std::array< int, 6 > > &boxes, const std::vector< std::vector< double > > &colours)
void setVofBlockDebris(bool enabled)
void advectVofKinematic(double dtPhysArg)
dt is in the caller's time unit.
void setVofInterfaceEps(double eps)
void setVofWispEps(double eps)
WO-R2 item 4 — the wisp threshold on the advector's mixed-cell predicate and on the interface Courant...
double effectiveContactSigma() const
void setVofBlockKappaClip(bool enabled, double kappaMaxPhys=-1.0)
double vofLastCourant() const
std::string capillaryThrowMessage(double cap) const
bool contactAngleDynamic() const
double vofTimingMomentumSeconds() const
double vofTimingStepSeconds() const
void setVofSolidColourZero(bool on)
std::vector< vof::VofBox > vofBlockRankBoxes(int size) const
double vofCurvatureWeightWidth() const
void phaseDensityRange(double &lo, double &hi)
void vofBcPropGhosts(CCField f)
WO-R item 5 — the Neumann property policy at an inflow face.
void vofFillGhosts(CCField f)
void addCsfRhsCellInterp(int c, CCField dst, bool rowScaled, bool faceRange=false)
const vof::WyAdvector & vofAdvector() const
vof::WyAdvector::Diagnostics vofDiagnostics()
bool vofAxisPeriodic(int a) const
void setVofCflLimit(double v)
VofStepLimits vofStepLimits()
Both limits at the current state, in the CALLER's time unit (courant is dimensionless).
std::vector< double > gatherInner(CCField fld)
double vofWispEps() const
bool vofSolidColourZero() const
bool vofCurvatureWorklist() const
double vofBlockImbalance() const
std::vector< vof::VofBlockStats > vofBlockStats() const
void advectVofBlocks(double dtPhysArg, bool requireSolenoidal=true)
dt is in the caller's time unit.
void setContactAngle(double thetaDeg)
void setVofFluxClamp(bool on)
bool vofCurvatureMixedHeightFit() const
double contactAngleSlip() const
double vofCflLimit() const
bool clampFluxes
Ablation: drop the Weymouth flux clamp of point 3 (the raw geometric flux).
SField advectedVelocity(int e) const
The advected velocity of component e, indexed exactly like the solver's u_e (inner cells).
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 setPhaseDensities(double rhoG, double rhoL)
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)
double interfaceEps
Wisp threshold on the interfacial predicate.
double pureEps
Tolerance of the PURITY test that ends a height-function column (hfIsFull/hfIsEmpty),...
bool useMixedHeightFit
Tier 2b, and it ships OFF — a measured decision, see the WO-O findings entry.
double weightWidth
Wendland support width d of the PV fit, in cell units (Han et al. §5: 2.5 with S = 5).
Stats compute(SField c)
Compute the curvature over the inner region from a colour field on the SAME extended block.
void init(int nx, int ny, int nz, int ghost)
SField temperature() const
The transported temperature on the advector's block (inner cells mirror the solver's scalar).
void advect(WyAdvector &w, double dt, long step)
One consistent colour + energy Weymouth-Yue step.
void init(const WyAdvector &w, double rcpG, double rcpL)
void init(int nx, int ny, int nz, int ghost)
void init(int nx, int ny, int nz, int ghost)
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.
double bcFaceVolume(int f) const
Signed liquid volume that crossed domain face f (0..5 = −x,+x,−y,+y,−z,+z) since the last resetBcFace...
double maxCourantInterfaceAuto(double dth) const
The interface-local Courant number for this dt/h, using the CUT-CELL rule when geometry is attached (...
SField wettingNormal(int d) const
The fluid-only Youngs normal of each fluid cell (buildWettingNormals, then exchanged).
bool cutFluxClamp
Weymouth's admissible-interval clamp on the openness-weighted flux (cutcell.hpp vofCutFluxClamp).
void classifyGeometry()
Classify every cell from eps and the six face openness values (cutcell.hpp rule 1) into kindDouble().
SField colour() const
Colour field on the extended block (x-fastest).
void buildWettingNormals()
The fluid-only Youngs normal at every FLUID cell of the INNER region (the block's own ghost policy th...
void wettingCensusGhost(long counts[kVofWetCount], double &meanAppDeg, long &nApp, int ghost) const
bool interfaceLocalCfl
Take the CFL over INTERFACE-ADJACENT cells only (mixed cells and their face neighbours) instead of ov...
double cflLimit
Abort threshold on max |uf| dt / h.
void init(int nx, int ny, int nz, double h, int ghost=3)
bool useWorklist
Compaction of the reconstruction pass onto the mixed cells.
SField wallSdf() const
The SDF on this block, in cells, > 0 in fluid (the solver embeds + exchanges it).
SField faceOpenness(int d) const
Face openness of the +d face of each cell (the advector's own high-face convention — the SAME convent...
double wispEps
WO-R2 item 4 — the wisp tolerance on the mixed-cell predicate (wyIsMixed(c, wispEps)),...
std::function< double(double)> globalMax
All-reduce max across ranks; identity when unset (single block).
void solidBandFill()
The three-pass neutral (90 deg) solid-band fill of cutcell.hpp.
std::function< void(SField)> exchange
Refresh EVERY ghost layer of the given cell field (MPI exchange + domain BC fill)....
SField kindDouble() const
The cell classification as a DOUBLE (1 = solid), so it can ride the block's own halo exchange.
void setOutsideMask(UCField m)
Install the OUT-OF-DOMAIN mask (vof/colour_bc.hppbuildOutsideMask): 1 on every ghost cell outside the...
void finalizeGeometry()
kindDouble() (ghost-exchanged) -> the unsigned-char classification every kernel reads.
void disableGeometry()
Drop the geometry (back to the uncut kernels, byte-identically).
void advect(double dt, long step)
One Weymouth-Yue step: freeze the dilation flag, then three sweeps in the permutation selected by ste...
void setBcFaceOwned(int f, bool on)
Which of the six BLOCK boundary faces are GLOBAL domain faces this block owns.
SField epsFraction() const
Fluid volume fraction of each cell (buildCellFraction, a multiple of 1/64).
Diagnostics diagnostics() const
Local census over the inner region.
const Timing & timing() const
SField faceVel(int d) const
void enableGeometry()
Allocate the geometry views. Idempotent; does not classify.
double capillaryDt(double rhoSum, double h, double sigma)
Brackbill capillary time-step limit sqrt((ρ₁+ρ₂) h³ / (4πσ)).
bool csfKappaDefined(double branch)
Is the curvature of a cell carrying branch code branch usable?
void bcColourBackflow(SField f, I3 e, int g, int a, int s, SField un, double backflow)
inletOutlet on an outflow face: where the boundary face velocity points INTO the domain,...
void bcColourConst(SField f, I3 e, int g, int a, int s, double value)
Overwrite ALL g ghost layers of domain face (axis a, side s) with a constant colour.
void bcColourProfile(SField f, I3 e, int g, int a, int s, SField prof, int nc)
Same, with a per-position colour on the face's (b, c) plane.
void buildOutsideMask(UCField m, I3 e, int g, I3 o, I3 gs, bool px, bool py, bool pz)
1 for every ghost cell of the block that lies OUTSIDE the global domain on a non-periodic axis,...
Definition colour_bc.hpp:84
@ 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)
VofMasterAssign
How a block's MASTER rank is chosen.
void clampFill(SField f, I3 e, int g, I3 o, I3 gs, bool px, bool py, bool pz, int skip=0)
Zero-gradient (Neumann) fill of the ghost cells that lie OUTSIDE the global domain on a non-periodic ...
void copyFaceVelocity(SField dst, I3 de, int dg, SField src, I3 se, int sg, int dir)
Embed the ENTIRE source extended block (inner + its sg ghost layers) into a LARGER destination extend...
void periodicFill(SField f, I3 e, int g, bool px, bool py, bool pz)
Periodic wrap of a single-block extended field at ghost width g (the single-rank ghost path; under MP...
double csfFaceForce(double sigma, double kf, double dC, double hGrad)
The balanced-force CSF contribution to the momentum RHS at one staggered velocity unknown,...
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
bool csfFaceCurvature(double k0, double b0, double k1, double b1, double &kf)
Face curvature for the balanced-force CSF, from the two cells sharing the face.
long L3(int x, int y, int z, I3 e)
void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g)
void applyClosureFaceGhost(const Closure &cl, C3 e, int g, int a, int side)
constexpr const char * kVofMomBlocksMsg
enable_vof_momentum x enable_vof_blocks*: refused in EITHER call order.
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< const double *, CCMem > CCConst
Kokkos::View< double *, SMem > SField
Definition prof.py:1
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
vof::VofMetric vofMetric() const
Definition flow_ibm.hpp:189
double hp[3]
h_a' = h_a / hRef (>= 1, exactly 1 on the finest axis)
Definition flow_ibm.hpp:178
long cells
cells with S > 1 + 1e-8 (global)
bool active
the phantom-aware capillary bound is in force
double maxSum
max_cells S (global)
double excess
sum over cells of (S - 1)^+ (global, cell volumes)
double advect
the colour advection stage as a whole (advectVof)
double bridge
... of which: the G=2 <-> g=3 bridges + the C ghost policy
double momBridge
... of which: its bridges
double blockAdvect
the block-container colour stage as a whole (advectVofBlocks)
double momAdvect
the momentum-consistent colour+rho^c u stage (advectVofMomentum)
double blockDebris
... of which: the debris/residue pass (§5.3/§13 of the note)
This rank's own patch: the extended block the face velocity and the union colour live on.
Half-open global cell box [lo, hi).
Per-call census of the cascade over this block's inner region (LOCAL; a distributed caller sums them ...
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 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....