flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_closures.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_CLOSURES_HPP
9#define PECLET_FLOW_FLOW_IBM_CLOSURES_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
15 fillGhosts(f);
16 for (int face = 0; face < 6; ++face)
17 if (bc_[face] != 0 && touchesGlobalFace(face))
18 applyScalarBcFace(f, face / 2, face % 2, 1, 0.0, edges); // type 1 = Neumann copy
19 vofBcPropGhosts(f); // WO-R item 5; a no-op unless a VoF inflow colour is set
20}
21
22template <class Grid>
24 // edges: the staggered cross-face viscosity reads mu at i - s_c +- s_a, a diagonal ghost at a
25 // wall row -- which must be the zero-gradient copy, not the opposite wall's periodic wrap.
26 fillPropGhosts(muField_, /*edges=*/true);
27}
28
29template <class Grid>
31 if (!hasCellForce_)
32 return;
33 for (int c = 0; c < 3; ++c)
34 fillPropGhosts(cellForce_[c]);
35}
36
37template <class Grid>
39 if (!hasDrag_)
40 return;
41 fillPropGhosts(dragBeta_);
42}
43
44template <class Grid>
46 fillGhosts(epsField_);
47 for (int face = 0; face < 6; ++face) {
48 const int t = bc_[face];
49 if (t == 0 || !touchesGlobalFace(face))
50 continue; // rank-owned faces only (see fillPropGhosts)
51 if (t == 2 || t == 3)
52 applyScalarBcFace(epsField_, face / 2, face % 2, 2, 1.0); // open face: face eps == 1
53 else
54 applyScalarBcFace(epsField_, face / 2, face % 2, 1, 0.0); // wall: zero-gradient
55 }
56}
57
58template <class Grid>
60 return varRho_ || (porous_ && porousCons_);
61}
62
63template <class Grid>
65 return varRho_ ? rhoField_ : epsRho_;
66}
67
68template <class Grid>
71 CCField er = epsRho_;
72 CCConst ep = CCConst(epsField_);
73 const double rho = rho_;
74 Kokkos::parallel_for(
75 "peclet::flow::eps_rho", Kokkos::RangePolicy<CCExec>(space, 0, n_),
76 KOKKOS_LAMBDA(std::size_t i) { er(i) = ep(i) * rho; });
77}
78
79template <class Grid>
82 fp.haveMu = varProps_;
83 if (varProps_) {
84 fp.mu = CCConst(muField_);
85 fp.msc = Grid::collocated ? 0 : strideOf(c); // mu on the staggered unknown's own faces
86 } else
87 fp.muC = mu_;
88 fp.harmMu = harmonicMu_;
89 fp.haveRho = effVarRho();
90 if (effVarRho()) {
91 fp.rho = CCConst(effRhoField());
92 fp.idt = 1.0 / dt_;
93 // Placement of the velocity unknown: the staggered unknown sits on the -c FACE, so its time
94 // diagonal is the arithmetic face mean of rho; the COLLOCATED unknown sits at the cell CENTRE
95 // (Grid::offset == 0), so it is rho(i) itself — which is what stride 0 gives,
96 // 0.5*(rho(i)+rho(i)) == rho(i) exactly in floating point. Inert until rung V8: `haveRho` was
97 // unreachable on the collocated grid (set_density_mode and set_porous_continuity both threw).
98 fp.sc = Grid::collocated ? 0 : strideOf(c);
99 } else
100 fp.rhoIdtC = rho_ / dt_;
101 return fp;
102}
103
104template <class Grid>
107 C3 e = e_;
108 CCField dv = divAdv_;
109 CCConst U = CCConst(C[0].u), V = CCConst(C[1].u), W = CCConst(C[2].u);
110 using MD = MDRange3<CCExec>;
111 Kokkos::parallel_for(
112 "peclet::flow::div_adv",
113 MD(space, {G - 1, G - 1, G - 1}, {e.x - G + 1, e.y - G + 1, e.z - G + 1}),
114 KOKKOS_LAMBDA(int x, int y, int z) {
115 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
116 const long i = (long)x + (long)y * sy + (long)z * sz;
117 dv(i) = (U(i + sx) - U(i)) + (V(i + sy) - V(i)) + (W(i + sz) - W(i));
118 });
119}
120
121template <class Grid>
123 const std::string& in0, const std::string& in1,
124 const std::vector<double>& params) {
125 if (target == "mu" && vmgExplicit_ && useVelocityMg_) // before anything is registered
126 throwVelocityMgVariableMu("set_property_model('mu', ...)");
127 Closure cl;
128 cl.kind = kind;
129 cl.out = ensureTarget(target);
130 cl.in0 = CCConst(fields_.at(in0).data);
131 if (!in1.empty())
132 cl.in1 = CCConst(fields_.at(in1).data);
133 cl.outName = target; // kept so a redistribute can re-resolve the handles (rebindFieldAliases)
134 cl.in0Name = in0;
135 cl.in1Name = in1;
136 // UNITS. A closure writes a registered field, and registered fields carry the solver's
137 // INTERNAL units. Its inputs are UNSCALED fields (a phase fraction, a temperature, a
138 // concentration — the solver never rescales a transported scalar), so the output's dimension
139 // sits in the parameters, and on a target of known dimension the ones carrying it convert by
140 // that target's factor: all of LinearMix's (`out = p0 + p1*in0 + p2*in1`, which is what makes
141 // set_property_model("rho", "linear", "C", [rho_gas, rho_liquid - rho_gas]) take PHYSICAL
142 // densities), Boussinesq's rho0 (`out = rho0*g*beta*(T - T0)` into force_a), Arrhenius' mu_ref.
143 // The physical values are kept, so a scale pinned later re-derives them. Exactly the identity
144 // in cell units.
145 for (int k = 0; k < 4 && k < (int)params.size(); ++k)
146 cl.pPhys[k] = params[k];
149 closures_.push_back(cl);
150 if (target == "mu") // a closure driving mu turns on variable viscosity
151 setPropertyMode(true, harmonicMu_);
152 if (target == "rho") // a closure driving rho turns on the variable-density path
153 setDensityMode(true);
154}
155
156template <class Grid>
158 if (variable)
159 collocatedV8AutoFallback("variable density on the collocated grid");
160 varRho_ = variable;
161 if (variable) {
162 if (fields_.has("rho"))
163 rhoField_ = fields_.at("rho").data;
164 else {
165 rhoField_ = addField("rho");
166 Kokkos::deep_copy(rhoField_, rho_);
167 }
168 if (rho1_.extent(0) == 0) { // g=1 MG-block scratch for the projection coefficients
169 rho1_ = CCField("rho1", n1_);
170 cx1_ = CCField("cx1", n1_);
171 cy1_ = CCField("cy1", n1_);
172 cz1_ = CCField("cz1", n1_);
173 }
174 ensureCellForceAll(); // buildRhsVar reads the per-cell force (zero until a closure sets it)
175 useVelocityMg_ = false; // scalar-coefficient velocity MG (variable-coeff deferred)
176 // Pressure driver: CHEBYSHEV by default under variable density — but NOT for the reason this
177 // comment used to give ("MG-PCG stalls on the rho-scaled coefficient operator"), which WO-B
178 // refuted: the stall it described is a DOMAIN-BC defect at constant density, repaired by
179 // WO-H (CutcellMG::applyNeumannGhost), and on the periodic pore-scale operator MG-PCG beats
180 // Chebyshev ~10x at density ratio 1e4. The reason that survives measurement is narrower and
181 // real: at a high density CONTRAST the arithmetic coarsening of the face coefficient makes
182 // the V-cycle preconditioner INDEFINITE (measured on a dense sym(M): a negative pivot from
183 // ratio ~1e3), and no Krylov CG survives that, while Chebyshev — which needs only real
184 // spectrum bounds, re-estimated on every coefficient rebuild — is healthy on every
185 // configuration measured. Coefficient-aware coarsening (VOF_PLAN S3) is what would lift it.
186 // An explicit set_pressure_pcg/_fcg/_chebyshev AFTER set_density_mode still wins (last set),
187 // and since WO-H set_pressure_pcg's `on` flag genuinely honours that promise.
188 useChebyshev_ = true;
189 chebBoundsSet_ = false;
190 chebWarmOk_ = false;
191 bfpChebSet_ = false;
192 }
193 registerBalancedForceState(); // V8 turns the balanced-force projection on by default
194}
195
196template <class Grid>
198 return cutcellPressure_;
199}
200
201template <class Grid>
203 if constexpr (Grid::collocated) {
204 if (on)
205 throw std::runtime_error("set_porous_continuity: staggered-only (v1)");
206 }
207 porous_ = on;
208 if (on) {
209 if (fields_.has("eps"))
210 epsField_ = fields_.at("eps").data;
211 else {
212 epsField_ = addField("eps");
213 Kokkos::deep_copy(epsField_, 1.0); // no particles -> eps=1 -> reduces to div(u)=0
214 }
215 if (epsPrev_.extent(0) == 0) {
216 epsPrev_ = CCField("epsPrev", n_);
217 depsdt_ = CCField("depsdt", n_);
218 }
219 if (divAdv_.extent(0) == 0)
220 divAdv_ = CCField("divAdv", n_); // cell div(u) for the porous advection-form compensation
221 if (epsRho_.extent(0) == 0)
222 epsRho_ = CCField("epsRho", n_); // rho_eff = eps*rho (eps-conservative momentum)
223 // The eps-conservative momentum path (porousCons_) routes through buildRhsVar, which reads
224 // the per-cell force unconditionally — allocate it (zero) like setDensityMode does. Without
225 // this a porous run with NO drag/closure (never the coupled case, which enables drag and
226 // thereby the force fields) dereferences an empty device View.
227 ensureCellForceAll();
228 Kokkos::deep_copy(epsPrev_, epsField_); // d(eps)/dt=0 on the first step
229 if (eps1_.extent(0) == 0)
230 eps1_ = CCField("eps1", n1_);
231 if (beta1_.extent(0) == 0)
232 beta1_ = CCField("beta1", n1_);
233 if (rho1_.extent(0) == 0) { // share the g=1 coefficient scratch with the varRho path
234 rho1_ = CCField("rho1", n1_);
235 cx1_ = CCField("cx1", n1_);
236 cy1_ = CCField("cy1", n1_);
237 cz1_ = CCField("cz1", n1_);
238 }
239 // CHEBYSHEV by default (as for variable density). CAVEAT on the original justification
240 // ("MG-PCG stalls on the eps-scaled coefficient operator"): it rests on the same kind of
241 // observation WO-B refuted for varRho, and it was recorded through the setter whose `on` flag
242 // was a no-op until WO-H — so a "PCG" run made that way actually measured Chebyshev. The
243 // default is kept because it is safe (Chebyshev needs only real spectrum bounds and is the
244 // one driver healthy on every high-contrast coefficient configuration measured), NOT because
245 // the eps-scaled PCG stall has been re-measured; that re-measurement is still owed
246 // (doc/vof_workorders.md, WO-B escalation #1). Bounds re-estimated on every coefficient
247 // rebuild (chebBoundsSet_ invalidation in project()). An explicit driver set afterwards wins.
248 useChebyshev_ = true;
249 chebBoundsSet_ = false;
250 chebWarmOk_ = false;
251 bfpChebSet_ = false;
252 configurePorousDragSolver(); // if drag already on, switch to GraphAMG+PCG (Chebyshev
253 // diverges)
254 }
255}
256
257template <class Grid>
259 if (porous_)
260 Kokkos::deep_copy(epsPrev_, epsField_);
261}
262
263template <class Grid>
265 porousDepsDt_ = on;
266}
267
268template <class Grid>
270 porousCons_ = on;
271}
272
273template <class Grid>
275 pressUnderRelax_ = w;
276}
277
278template <class Grid>
279void Solver<Grid>::setPropertyMode(bool variable, bool harmonic) {
280 // An EXPLICIT velocity multigrid is refused in either order; the AUTO rule is simply re-decided
281 // (setSolidVelocityMgAuto excludes varProps_), so only an explicit request can conflict.
282 if (variable && vmgExplicit_ && useVelocityMg_)
283 throwVelocityMgVariableMu("set_property_mode('variable')");
284 varProps_ = variable;
285 harmonicMu_ = harmonic;
286 if (variable) {
287 if (fields_.has("mu"))
288 muField_ = fields_.at("mu").data;
289 else {
290 muField_ = addField("mu");
291 Kokkos::deep_copy(muField_,
292 mu_); // default to the scalar mu until a closure/set_field sets it
293 }
294 useVelocityMg_ =
295 false; // the velocity multigrid takes a scalar mu (variable-coeff vmg deferred)
296 }
297}
298
299template <class Grid>
301 varRotMode_ = mode < 0 ? 0 : (mode > 2 ? 2 : mode);
302 varRotChi_ = chi < 0.0 ? 0.0 : chi;
303}
304
305template <class Grid>
306void Solver<Grid>::setPropertyTable(const std::string& target, const std::string& in0,
307 const std::vector<double>& xs, const std::vector<double>& ys) {
308 Closure cl;
310 cl.out = ensureTarget(target);
311 cl.in0 = CCConst(fields_.at(in0).data);
312 cl.outName = target; // see setPropertyModel
313 cl.in0Name = in0;
314 cl.nTab = (int)std::min(xs.size(), ys.size());
315 cl.tabX = CCField(target + "_tabx", cl.nTab);
316 cl.tabY = CCField(target + "_taby", cl.nTab);
317 auto hx = Kokkos::create_mirror_view(cl.tabX);
318 auto hy = Kokkos::create_mirror_view(cl.tabY);
319 for (int k = 0; k < cl.nTab; ++k) {
320 hx(k) = xs[k];
321 hy(k) = ys[k];
322 }
323 Kokkos::deep_copy(cl.tabX, hx);
324 Kokkos::deep_copy(cl.tabY, hy);
325 cl.tabYPhys.assign(ys.begin(), ys.begin() + cl.nTab);
327 closureRefreshUnits(cl); // ys are in the target's physical units (identity in cell units)
328 closures_.push_back(cl);
329}
330
331template <class Grid>
332double Solver<Grid>::closureTargetToInt(const std::string& target) const {
333 if (target == "rho")
334 return u_.rhoToInt();
335 if (target == "mu")
336 return u_.muToInt();
337 if (target == "force_x" || target == "force_y" || target == "force_z")
338 return u_.forceToInt(target[6] - 'x');
339 return 0.0;
340}
341
342template <class Grid>
343void Solver<Grid>::closureUnitsNotice(const std::string& target) const {
344 if (u_.physical && closureTargetToInt(target) == 0.0)
345 std::fprintf(stderr,
346 "peclet.flow set_property_model NOTICE: a physical domain is armed and target "
347 "'%s' is not rho/mu/force_x/y/z, so this closure's parameters are taken in "
348 "that field's INTERNAL units (see the `unit_scales` property).\n",
349 target.c_str());
350}
351
352template <class Grid>
354 double kp = closureTargetToInt(cl.outName);
355 if (kp == 0.0)
356 kp = 1.0; // a field of unknown dimension: the caller's numbers are internal
357 if (cl.kind == ClosureKind::Table1D) {
358 if (!u_.physical)
359 return; // cell units: the table was uploaded verbatim at registration
360 auto hy = Kokkos::create_mirror_view(cl.tabY);
361 for (int k = 0; k < cl.nTab; ++k)
362 hy(k) = cl.tabYPhys[(std::size_t)k] * kp;
363 Kokkos::deep_copy(cl.tabY, hy);
364 return;
365 }
366 // the parameters that carry the output's dimension; the others are kept as given
367 const int nScaled = (cl.kind == ClosureKind::LinearMix) ? 3 : 1;
368 for (int k = 0; k < 4; ++k)
369 cl.p[k] = (k < nScaled) ? cl.pPhys[k] * kp : cl.pPhys[k];
370}
371
372template <class Grid>
374 for (auto& cl : closures_)
375 applyClosure(cl, e_, G);
376}
377
378template <class Grid>
380 ensureCellForceAll();
381}
382
383template <class Grid>
385 if (!fields_.has("drag_beta"))
386 dragBeta_ = addField("drag_beta");
387 else
388 dragBeta_ = fields_.at("drag_beta").data;
389 ensureCellForceAll(); // force_* carries beta*u_p (the implicit-drag RHS target)
390 hasDrag_ = true;
392}
393
394template <class Grid>
396 if (!(porous_ && hasDrag_))
397 return;
398 pressGraphAmg_ = true; // GraphAMG bottom (domain-BC operators: buildAmg skips
399 // the wrap across non-periodic faces and pcgAmg keeps the
400 // mean only when the operator is singular)
401 if (cutcellPressure_) // MG already built (set_solid ran) -> apply now
403 useChebyshev_ = false; // PCG, not Chebyshev (diverges on the high w_f ratio)
404 chebBoundsSet_ = false;
405 chebWarmOk_ = false;
406 bfpChebSet_ = false;
407}
408
409template <class Grid>
412 C3 e = e_;
413 FV AC = C[c].AC;
414 CCConst beta = CCConst(dragBeta_);
415 const long sc = strideOf(c);
416 // Porous continuity: the projection's operator/correction carry the FACE drag relaxation
417 // w_f = idt/(idt + beta_f), beta_f = 1/2(beta(i)+beta(i-sc)) (buildPorousCoeffDrag /
418 // projectCorrectPorousDrag). The staggered momentum diagonal of u_c(i) — the face between cells
419 // i-sc and i — must carry the SAME beta_f: then a pressure perturbation deltaP produces
420 // du* = -grad(deltaP)/(idt+beta_f) and the projection returns phi = -deltaP/idt exactly (same
421 // operator), so the incremental predictor cancels pressure errors in one step. With the cell
422 // value beta(i) the loop has gain (idt+beta_f)/(idt+beta_cell) at a beta jump (bed top: ~3) and
423 // the accumulated pressure diverges exponentially.
424 // The drag is VOLUMETRIC, so beta is placed where the unknown lives by the GRID, not by the
425 // continuity model: the face mean on the MAC grid (porous or not -- the non-porous path used the
426 // cell value, half a cell off the face, while its RHS target beta*u_p in force_* is placed by the
427 // same Grid::atVelocity), the cell value collocated. Target and diagonal must agree, or a uniform
428 // particle velocity does not give a uniform steady gas velocity.
429 using MD = MDRange3<CCExec>;
430 Kokkos::parallel_for(
431 "peclet::flow::add_drag_diag", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
432 KOKKOS_LAMBDA(int x, int y, int z) {
433 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
434 const double bd = Grid::atVelocity(beta, i, sc);
435 AC(i) = (MReal)((double)AC(i) + bd);
436 });
437}
438
439} // namespace peclet::flow
440
441#endif // PECLET_FLOW_FLOW_IBM_CLOSURES_HPP
void setAgglomerationMode(int mode)
void fillGhosts(CCField f)
void fillPropGhosts(CCField f, bool edges=false)
void setPropertyMode(bool variable, bool harmonic)
CCField addField(const std::string &name)
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
void applyScalarBcFace(CCField c, int a, int side, int type, double val, bool edges=false)
void setVariableRotational(int mode, double chi)
void setPressureUnderRelax(double w)
void closureUnitsNotice(const std::string &target) const
Under a physical domain, say on stderr that a closure on a target of unknown dimension takes its para...
static constexpr int G
Definition flow_ibm.hpp:80
void closureRefreshUnits(Closure &cl)
Re-derive a closure's internal p / tabY from its physical pPhys / tabYPhys with the current reference...
void setPorousConservative(bool on)
long strideOf(int c) const
void collocatedV8AutoFallback(const char *why)
void throwVelocityMgVariableMu(const char *who) const
void setDensityMode(bool variable)
Kokkos::View< MReal *, CCMem > FV
Definition flow_ibm.hpp:79
void setPropertyTable(const std::string &target, const std::string &in0, const std::vector< double > &xs, const std::vector< double > &ys)
double closureTargetToInt(const std::string &target) const
The internal-per-physical factor of a closure TARGET: rho, mu, force_x/y/z; 0.0 for any other field (...
VarFaceProps makeFaceProps(int c)
void vofBcPropGhosts(CCField f)
WO-R item 5 — the Neumann property policy at an inflow face.
void setPorousContinuity(bool on)
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
void applyClosure(const Closure &cl, C3 e, int g)
Kokkos::View< const double *, CCMem > CCConst
double forceToInt(int a) const
Definition flow_ibm.hpp:157
bool physical
an extent was given (the scales are armed)
Definition flow_ibm.hpp:137
static constexpr double AC
static constexpr double U