flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_scalars.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_SCALARS_HPP
9#define PECLET_FLOW_FLOW_IBM_SCALARS_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
14void Solver<Grid>::addScalar(const std::string& name, double D, int scheme, int iters,
15 bool cutcell) {
16 ScalarField sc;
17 sc.name = name;
18 sc.c = addField(name); // registered, zero-initialised, on the G=2 block
19 sc.cOld = CCField(name + "_old", n_);
20 sc.b = CCField(name + "_b", n_);
21 sc.AC = CCField(name + "_AC", n_);
22 sc.AW = CCField(name + "_AW", n_);
23 sc.AE = CCField(name + "_AE", n_);
24 sc.AS = CCField(name + "_AS", n_);
25 sc.AN = CCField(name + "_AN", n_);
26 sc.AB = CCField(name + "_AB", n_);
27 sc.AT = CCField(name + "_AT", n_);
28 // PHYSICAL diffusivity in (L^2/T), kept verbatim; the operator takes the cell Fourier number
29 // D' = D tRef/hRef^2, re-derived by `refreshUnitDerived` if tRef is pinned after this call
30 // (exactly D in cell units). The per-axis Laplacian weight w_a is applied where the stencil
31 // is built, as for mu.
32 sc.Dphys = D;
33 sc.D = D * u_.diffToInt();
34 sc.scheme = scheme;
35 sc.iters = iters < 1 ? 1 : iters;
36 if (cutcell) { // doc/scalar_ibm_design.md: the operator state lives beside the legacy bands
37 sc.cutcell = true;
38 sc.cut = std::make_shared<ScalarCutState>();
39 }
40 scalars_.push_back(sc);
41}
42
43template <class Grid>
44bool Solver<Grid>::hasScalar(const std::string& name) const {
45 for (const auto& sc : scalars_)
46 if (sc.name == name)
47 return true;
48 return false;
49}
50
51template <class Grid>
52void Solver<Grid>::setScalarBc(const std::string& name, int face, int type, double value) {
53 for (auto& sc : scalars_)
54 if (sc.name == name) {
55 sc.bc[face] = type;
56 sc.bcVal[face] = value;
57 if (sc.cutcell) // a constant value replaces an earlier per-face profile
58 sc.cut->hasProfile[face] = false;
59 return;
60 }
61 throw std::runtime_error("set_scalar_bc: no scalar named '" + name + "'");
62}
63
64template <class Grid>
66 if (scalars_.empty())
67 return;
68 const double idt = 1.0 / dt_;
69 CCField Uf, Vf, Wf;
70 if constexpr (Grid::collocated) {
71 Uf = uf_;
72 Vf = vf_;
73 Wf = wf_;
74 } else {
75 Uf = C[0].u;
76 Vf = C[1].u;
77 Wf = C[2].u;
78 }
81 fillGhosts(Wf); // face velocities need the ±2 advection reach
82 // Phase 3 (V5.4): the cell metric the scalar/energy GFM rows pull back through. The per-axis
83 // Laplacian weights are `u_.w[a]` — Phase 2's, passed at each call site in its own spelling.
84 const vof::VofMetric gmS = u_.vofMetric();
85 for (auto& sc : scalars_) {
86 if (sc.cutcell) { // doc/scalar_ibm_design.md §5.5: the cut-cell path, legacy untouched
88 continue;
89 }
90 // WO-P23: the CONSISTENT-ENERGY branch — per-cell k(C) in the bands, a rho c_p(C) time term,
91 // and NO advective term (the transport was already done geometrically with the colour's own
92 // fluxes in advectVof). Taken OUTSIDE the kernels, so a scalar without `energy` executes the
93 // validated constant-D bodies verbatim.
94 const bool energy = sc.energy && sc.kcell.extent(0) == n_;
95 const bool hasMask = sc.dmask.extent(0) == n_;
96 if (energy) {
97 scalarBuildDiffusionVarK(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, CCConst(ox_),
98 CCConst(oy_), CCConst(oz_), CCConst(sc.kcell), CCConst(sc.rcp),
99 CCConst(sc.dmask), idt, e_, G, u_.w[0], u_.w[1], u_.w[2]);
100 applyScalarBcStencilVar(sc);
101 } else {
102 scalarBuildDiffusionOpen(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, CCConst(ox_),
103 CCConst(oy_), CCConst(oz_), sc.D, idt, e_, G, u_.w[0], u_.w[1],
104 u_.w[2]);
105 applyScalarBcStencil(sc); // re-open Dirichlet domain faces (set_domain_bc closes openness)
106 }
107 // WO-P23: the PLANE-ANCHORED (ghost-fluid) form of that Dirichlet set — the pure cells'
108 // rows carry the condition at the PLIC plane and never read the interfacial cell's value.
109 const bool gfm = hasMask && pcPlaneDir_ && sc.gfmB.extent(0) == n_ && pcGphi_.extent(0) == n_;
110 // WO-P3g items 2 + 3: the second-order (Gibou-Fedkiw) row and/or the curvature-consistent
111 // `theta`. The branch is OUTSIDE the kernel, so the shipped configuration executes the
112 // validated `scalarMaskGfm` body verbatim.
113 const bool gfm2 = gfm && (pcGfmOrder_ >= 2 || (pcCurvDist_ && pcKappa_.extent(0) == n_));
114 if (gfm2)
115 scalarMaskGfm2(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, sc.gfmB, CCConst(ox_),
116 CCConst(oy_), CCConst(oz_), CCConst(sc.dmask), CCConst(pcTgam_),
117 CCConst(pcGn_[0]), CCConst(pcGn_[1]), CCConst(pcGn_[2]), CCConst(pcGphi_),
118 CCConst(pcCurvDist_ && pcKappa_.extent(0) == n_ ? pcKappa_ : pcGphi_),
119 CCConst(sc.kcell), sc.D, energy, pcGfmThMin_, pcGfmThMax_, pcGfmOrder_,
120 pcCurvDist_ && pcKappa_.extent(0) == n_, e_, G, u_.w[0], u_.w[1], u_.w[2],
121 gmS);
122 else if (gfm)
123 scalarMaskGfm(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, sc.gfmB, CCConst(ox_),
124 CCConst(oy_), CCConst(oz_), CCConst(sc.dmask), CCConst(pcTgam_),
125 CCConst(pcGn_[0]), CCConst(pcGn_[1]), CCConst(pcGn_[2]), CCConst(pcGphi_),
126 CCConst(sc.kcell), sc.D, energy, pcGfmThMin_, pcGfmThMax_, e_, G, u_.w[0],
127 u_.w[1], u_.w[2], gmS);
128 // WO-P01: the optional PER-CELL Dirichlet set (interfacial cells at T_sat). Inert — and the
129 // operator therefore bit-identical — until a caller allocates the mask.
130 if (hasMask)
131 scalarMaskStencil(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, CCConst(sc.dmask), e_, G);
132 Kokkos::deep_copy(sc.cOld, sc.c);
133 // WO-P3f: add the enthalpy the interfacial cells' overwrite gave back (option, inert off).
134 if (hasMask && pcCarryConserve_ && pcCarrySrc_.extent(0) == n_)
135 pcCarryApply(sc);
136 scalarFillGhosts(sc);
137 if (energy)
138 scalarBuildRhsHeat(sc.b, CCConst(sc.cOld), CCConst(sc.rcp), idt, e_, G);
139 else
140 scalarBuildRhs(sc.b, CCConst(sc.cOld), CCConst(Uf), CCConst(Vf), CCConst(Wf), CCConst(ox_),
141 CCConst(oy_), CCConst(oz_), idt, sc.scheme, e_, G);
142 if (gfm)
143 scalarAddGfmRhs(sc.b, CCConst(sc.gfmB), CCConst(sc.dmask), e_, G);
144 // WO-P3f: the energy budget's PRE snapshot — taken here because `scalarMaskRhs` on the
145 // next line is where a newly interfacial cell's transported temperature is discarded.
146 // Inert (no kernel, no allocation) unless `set_phase_change_budget(true)` ran.
147 if (hasMask && pcBudgetOn_ && pcClsPrev_.extent(0) == n_)
148 pcBudgetPre(sc);
149 if (hasMask)
150 scalarMaskRhs(sc.b, sc.c, CCConst(sc.dmask), CCConst(sc.dval), e_, G);
151 // implicit diffusion: red-black Gauss-Seidel with a ghost fill before each color sweep.
152 for (int it = 0; it < sc.iters; ++it) {
153 scalarFillGhosts(sc);
154 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
155 og_, G, 0);
156 scalarFillGhosts(sc);
157 cutcellSmoothColor(sc.c, CCConst(sc.b), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_,
158 og_, G, 1);
159 }
160 scalarFillGhosts(sc);
161 if (hasMask && pcBudgetOn_ && pcClsPrev_.extent(0) == n_)
162 pcBudgetPost(sc); // WO-P3f, inert when the instrument is off
163 }
164}
165
166template <class Grid>
167ScalarField& Solver<Grid>::scalarField(const std::string& name) {
168 for (auto& sc : scalars_)
169 if (sc.name == name)
170 return sc;
171 throw std::runtime_error("no scalar named '" + name + "'");
172}
173
174template <class Grid>
175void Solver<Grid>::scalarDirichletMask(const std::string& name) {
176 ScalarField& sc = scalarField(name);
177 if (sc.cutcell)
178 throw std::invalid_argument(
179 "the per-cell Dirichlet mask is not available on the cut-cell "
180 "scalar '" +
181 name + "' (doc/scalar_ibm_design.md §8.1)");
182 if (sc.dmask.extent(0) != n_) {
183 sc.dmask = CCField(name + "_dmask", n_);
184 sc.dval = CCField(name + "_dval", n_);
185 sc.gfmB = CCField(name + "_gfmb", n_);
186 }
187 if (pcGphi_.extent(0) != n_) {
188 pcTgam_ = CCField("pc_tgam", n_);
189 pcGn_[0] = CCField("pc_gnx", n_);
190 pcGn_[1] = CCField("pc_gny", n_);
191 pcGn_[2] = CCField("pc_gnz", n_);
192 pcGphi_ = CCField("pc_gphi", n_);
193 }
194}
195
196template <class Grid>
198 fillGhosts(sc.c);
199 applyScalarBc(sc);
200}
201
202template <class Grid>
203void Solver<Grid>::applyScalarBc(ScalarField& sc) {
204 for (int f = 0; f < 6; ++f)
205 if (sc.bc[f] != 0 && touchesGlobalFace(f))
206 applyScalarBcFace(sc.c, f / 2, f % 2, sc.bc[f], sc.bcVal[f]);
207}
208
209template <class Grid>
210bool Solver<Grid>::touchesGlobalFace(int f) const {
211#ifdef PECLET_FLOW_MPI
212 if (distributed_) {
213 const int a = f / 2;
214 const int o = (a == 0) ? og_.x : (a == 1) ? og_.y : og_.z;
215 const int n = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
216 const int gn = (a == 0) ? gnx_ : (a == 1) ? gny_ : gnz_;
217 return (f % 2 == 0) ? (o == 0) : (o + n == gn);
218 }
219#endif
220 (void)f;
221 return true;
222}
223
224template <class Grid>
225void Solver<Grid>::applyScalarBcStencilVar(ScalarField& sc) {
226 for (int f = 0; f < 6; ++f) {
227 if (sc.bc[f] != 2 || !touchesGlobalFace(f))
228 continue;
229 const int a = f / 2, side = f % 2;
230 CCField band = (a == 0) ? (side == 0 ? sc.AW : sc.AE)
231 : (a == 1) ? (side == 0 ? sc.AS : sc.AN)
232 : (side == 0 ? sc.AB : sc.AT);
233 patchScalarDirichletFaceVar(sc.AC, band, sc.kcell, a, side);
234 }
235}
236
237template <class Grid>
238void Solver<Grid>::applyScalarBcStencil(ScalarField& sc) {
239 for (int f = 0; f < 6; ++f) {
240 if (sc.bc[f] != 2 || !touchesGlobalFace(f))
241 continue; // only Dirichlet reopens; Neumann/periodic leave the (closed/interior) band
242 const int a = f / 2, side = f % 2;
243 CCField band = (a == 0) ? (side == 0 ? sc.AW : sc.AE)
244 : (a == 1) ? (side == 0 ? sc.AS : sc.AN)
245 : (side == 0 ? sc.AB : sc.AT);
246 patchScalarDirichletFace(sc.AC, band, sc.D, a, side);
247 }
248}
249
250template <class Grid>
252 int side) {
253 const double wa = u_.w[a]; // V5.4: this axis's Laplacian weight (exactly 1.0 isotropic)
254 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
255 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
256 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
257 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
258 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
259 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
260 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
261 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
262 const int aInner = (side == 0) ? G : (G + na - 1);
264 Kokkos::parallel_for(
265 "peclet::flow::scalar_bc_stencil_var", MDRange2<CCExec>(space, {G, G}, {G + nt1, G + nt2}),
266 KOKKOS_LAMBDA(int j1, int j2) {
267 const long i = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
268 const double D = kc(i) * wa; // V5.4: this axis's Laplacian weight (1.0 isotropic)
269 AC(i) += D + band(i);
270 band(i) = -D;
271 });
272}
273
274template <class Grid>
275void Solver<Grid>::patchScalarDirichletFace(CCField AC, CCField band, double Din, int a, int side) {
276 const double D = Din * u_.w[a]; // V5.4: this axis's Laplacian weight (exactly 1.0 isotropic)
277 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
278 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
279 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
280 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
281 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
282 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
283 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
284 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
285 const int aInner = (side == 0) ? G : (G + na - 1);
287 Kokkos::parallel_for(
288 "peclet::flow::scalar_bc_stencil", MDRange2<CCExec>(space, {G, G}, {G + nt1, G + nt2}),
289 KOKKOS_LAMBDA(int j1, int j2) {
290 const long i = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
291 // base build put band(i) = -D*open_face and A_C += D*open_face; force the face fully open
292 // (band -> -D, A_C gains D*(1-open)) without double-counting when it was already open.
293 AC(i) += D + band(i);
294 band(i) = -D;
295 });
296}
297
298template <class Grid>
299void Solver<Grid>::applyScalarBcFace(CCField c, int a, int side, int type, double val, bool edges) {
300 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
301 const int nt1 = (t1 == 0) ? nx_ : (t1 == 1) ? ny_ : nz_;
302 const int nt2 = (t2 == 0) ? nx_ : (t2 == 1) ? ny_ : nz_;
303 const int na = (a == 0) ? nx_ : (a == 1) ? ny_ : nz_;
304 const long sx = 1, sy = e_.x, sz = (long)e_.x * e_.y;
305 const long sa = (a == 0) ? sx : (a == 1) ? sy : sz;
306 const long st1 = (t1 == 0) ? sx : (t1 == 1) ? sy : sz;
307 const long st2 = (t2 == 0) ? sx : (t2 == 1) ? sy : sz;
308 const int aInner = (side == 0) ? G : (G + na - 1); // inner boundary cell a-index
309 const int dir = (side == 0) ? -1 : +1; // toward the ghost
310 // edges: also the tangential ghost rows (their inner-normal values are the periodic/halo fill),
311 // so the edge and corner ghosts at this face carry the BC too, not a wrap from the far side.
312 const int t0 = edges ? 0 : G, t1e = edges ? nt1 + 2 * G : G + nt1,
313 t2e = edges ? nt2 + 2 * G : G + nt2;
315 Kokkos::parallel_for(
316 "peclet::flow::scalar_bc_face", MDRange2<CCExec>(space, {t0, t0}, {t1e, t2e}),
317 KOKKOS_LAMBDA(int j1, int j2) {
318 const long base = (long)aInner * sa + (long)j1 * st1 + (long)j2 * st2;
319 for (int L = 1; L <= 2; ++L) {
320 const long gcell = base + (long)dir * L * sa;
321 const long icell = base - (long)dir * (L - 1) * sa;
322 c(gcell) = (type == 2) ? (2.0 * val - c(icell)) : c(icell);
323 }
324 });
325}
326
327} // namespace peclet::flow
328
329#endif // PECLET_FLOW_FLOW_IBM_SCALARS_HPP
ScalarField & scalarField(const std::string &name)
void fillGhosts(CCField f)
void patchScalarDirichletFaceVar(CCField AC, CCField band, CCField kc, int a, int side)
CCField addField(const std::string &name)
void advanceScalarCutCell(ScalarField &sc)
void addScalar(const std::string &name, double D, int scheme, int iters, bool cutcell=false)
void applyScalarBcFace(CCField c, int a, int side, int type, double val, bool edges=false)
void setScalarBc(const std::string &name, int face, int type, double value)
void pcBudgetPost(ScalarField &sc)
WO-P3f, part 2: the state AFTER the energy solve, the heat the plane-anchored rows actually delivered...
void pcBudgetPre(ScalarField &sc)
WO-P3f, the energy-budget instrument, part 1: the state BEFORE the energy solve, and the class-change...
static constexpr int G
Definition flow_ibm.hpp:80
bool hasScalar(const std::string &name) const
void pcCarryApply(ScalarField &sc)
WO-P3f: consume pcCarrySrc_ into the energy solve's time base.
void patchScalarDirichletFace(CCField AC, CCField band, double Din, int a, int side)
void scalarDirichletMask(const std::string &name)
Allocate (idempotently) the per-cell Dirichlet mask + value of a registered scalar.
void scalarMaskRhs(CCField b, CCField c, CCConst mask, CCConst val, C3 e, int g)
void scalarMaskGfm2(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCField gfmB, CCConst ox, CCConst oy, CCConst oz, CCConst mask, CCConst tgam, CCConst gnx, CCConst gny, CCConst gnz, CCConst gphi, CCConst kap, CCConst kc, double Dconst, bool useK, double thMin, double thMax, int order, bool useKappa, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0, vof::VofMetric gm=vof::VofMetric{})
void scalarBuildDiffusionOpen(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst ox, CCConst oy, CCConst oz, double D, double idt, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void scalarAddGfmRhs(CCField b, CCConst gfmB, CCConst mask, C3 e, int g)
void scalarMaskGfm(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCField gfmB, CCConst ox, CCConst oy, CCConst oz, CCConst mask, CCConst tgam, CCConst gnx, CCConst gny, CCConst gnz, CCConst gphi, CCConst kc, double Dconst, bool useK, double thMin, double thMax, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0, vof::VofMetric gm=vof::VofMetric{})
void scalarMaskStencil(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst mask, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
void cutcellSmoothColor(CCField phi, CCConst b, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, C3 og, int g, int color)
Kokkos::DefaultExecutionSpace CCExec
void scalarBuildDiffusionVarK(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst ox, CCConst oy, CCConst oz, CCConst kc, CCConst rcp, CCConst mask, double idt, C3 e, int g, double wx=1.0, double wy=1.0, double wz=1.0)
void scalarBuildRhsHeat(CCField b, CCConst cOld, CCConst rcp, double idt, C3 e, int g)
Kokkos::View< const double *, CCMem > CCConst
void scalarBuildRhs(CCField b, CCConst cOld, CCConst U, CCConst V, CCConst W, CCConst ox, CCConst oy, CCConst oz, double idt, int scheme, C3 e, int g)
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
vof::VofMetric vofMetric() const
Definition flow_ibm.hpp:189
static constexpr double AC