flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_scalars_cutcell.hpp
Go to the documentation of this file.
1
12#ifndef PECLET_FLOW_FLOW_IBM_SCALARS_CUTCELL_HPP
13#define PECLET_FLOW_FLOW_IBM_SCALARS_CUTCELL_HPP
14
15namespace peclet::flow {
16
17template <class Grid>
19 ++scgVersion_;
20 scalarOpenValid_ = false; // WO-5b: the captured open-face planes belong to the old block
21}
22
23template <class Grid>
25 if (!geometryBuilt_)
26 throw std::runtime_error(
27 "scalar cut-cell geometry: no geometry yet -- call set_solid, set_solid_from_scene or "
28 "set_pressure_geometry first");
29 if (scg_.version == scgVersion_)
30 return;
31 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
33 r.kappa = CCField("peclet::flow::scg_kappa", n_);
34 r.sax = CCField("peclet::flow::scg_sax", n_);
35 r.say = CCField("peclet::flow::scg_say", n_);
36 r.saz = CCField("peclet::flow::scg_saz", n_);
37 r.unknown = CCField("peclet::flow::scg_unknown", n_);
38 const CCConst sd(sdf_);
39 // 1. apertures (every face of every inner cell + the high face at ext - G; not exchanged)
40 scg::buildApertures(r.sax, r.say, r.saz, sd, e_, G);
41 // 2. kappa, unknown flags, facet counts
42 scg::CellPass cp{Kokkos::View<std::int8_t*, CCMem>("peclet::flow::scg_nfac", nInner),
43 Kokkos::View<std::int8_t*, CCMem>("peclet::flow::scg_kind", nInner)};
44 scg::buildCells(r.kappa, r.unknown, cp, sd, r.sax, r.say, r.saz, e_, G, u_.hp);
45 // 3. compact facet list in x-fastest cell order + the cut-cell CSR
46 scg::buildFacets(r.fac, cp, sd, r.sax, r.say, r.saz, e_, G, u_.hp);
47 // 4. halo kappa / flags (the ordinary G = 2 exchange, which wraps periodically), then clear the
48 // flags beyond a non-periodic global face (rank rule touchesGlobalFace)
49 fillGhosts(r.kappa);
50 fillGhosts(r.unknown);
51 for (int face = 0; face < 6; ++face)
52 if (bc_[face] != 0 && touchesGlobalFace(face))
53 scg::zeroGhostSlab(r.unknown, e_, G, face);
54 // body id per facet: the scene owner AT the facet centroid (the hydro-force precedent: query at
55 // the point, never from the cell-centred cutOwner_); 0 with raw-SDF geometry (§2.7)
56 if (hasScene_ && sceneQ_ && r.fac.n > 0) {
57 const auto q = sceneQ_->view();
58 const SceneMap sm = sceneMap();
59 const C3 og = og_, e = e_;
60 const int g = G;
61 const double h0 = u_.hp[0], h1 = u_.hp[1], h2 = u_.hp[2];
62 auto fcell = r.fac.cell;
63 auto fcen = r.fac.centroid;
64 auto fbody = r.fac.body;
66 Kokkos::parallel_for(
67 "peclet::flow::scg_body", Kokkos::RangePolicy<CCExec>(space, 0, r.fac.n),
68 KOKKOS_LAMBDA(const long f) {
69 const long i = fcell(f);
70 const long x = i % e.x, y = (i / e.x) % e.y, z = i / ((long)e.x * e.y);
71 const peclet::core::Vec3<double> p{
72 sm.a[0] + sm.b[0] * ((double)(x - g + og.x) + fcen(f, 0) / h0),
73 sm.a[1] + sm.b[1] * ((double)(y - g + og.y) + fcen(f, 1) / h1),
74 sm.a[2] + sm.b[2] * ((double)(z - g + og.z) + fcen(f, 2) / h2)};
75 fbody(f) = q.owner(p);
76 });
77 space.fence();
78 }
79 // 5. the fluid probe ladder per facet (§3.3)
80 scg::buildFluidProbes(r.fac, sd, CCConst(r.unknown), e_, u_.hp);
81 // census, summed over ranks
83 scg::localCensus(r.fac, cp, CCConst(r.kappa), CCConst(r.unknown), CCConst(r.sax),
84 CCConst(r.say), CCConst(r.saz), e_, G, u_.vol);
85#ifdef PECLET_FLOW_MPI
86 if (distributed_) {
87 long cnt[10] = {c.numUnknowns, c.numCutCells, c.numFacets, c.numTwoSided, c.numThinSolid,
88 c.numSealed, c.rungs[0], c.rungs[1], c.rungs[2], c.rungs[3]};
89 long sum[10];
90 MPI_Allreduce(cnt, sum, 10, MPI_LONG, MPI_SUM, comm_);
91 double v = c.sealedVolume, vs = 0.0;
93 double vf = c.fluidVolume, vfs = 0.0;
95 c.fluidVolume = vfs;
96 c.numUnknowns = sum[0];
97 c.numCutCells = sum[1];
98 c.numFacets = sum[2];
99 c.numTwoSided = sum[3];
100 c.numThinSolid = sum[4];
101 c.numSealed = sum[5];
102 for (int k = 0; k < 4; ++k)
103 c.rungs[k] = sum[6 + k];
104 c.sealedVolume = vs;
105 }
106#endif
107 const double l = u_.lenToPhys();
108 c.sealedVolume *= l * l * l;
109 c.fluidVolume *= l * l * l;
110 r.census = c;
111 r.version = scgVersion_;
112 scg_ = r;
113}
114
115template <class Grid>
120
121template <class Grid>
122std::vector<double> Solver<Grid>::scalarGeometryField(int which) {
124 const CCField f[5] = {scg_.kappa, scg_.sax, scg_.say, scg_.saz, scg_.unknown};
126 throw std::runtime_error("scalarGeometryField: which must be 0..4");
127 return gatherInner(f[which]);
128}
129
130template <class Grid>
135
136// ================================================================================================
137// WO-3: the single-phase cut-cell operator, the Krylov solve, steady + transient diffusion.
138// ================================================================================================
139
140template <class Grid>
141bool Solver<Grid>::isCutcellScalar(const std::string& name) const {
142 for (const auto& sc : scalars_)
143 if (sc.name == name)
144 return sc.cutcell;
145 return false;
146}
147
148template <class Grid>
149ScalarField& Solver<Grid>::cutcellScalar(const std::string& name, const char* who) {
150 for (auto& sc : scalars_)
151 if (sc.name == name) {
152 if (!sc.cutcell)
153 throw std::invalid_argument(std::string(who) + ": '" + name +
154 "' is not a cut-cell scalar (add_scalar(..., cutcell=True))");
155 return sc;
156 }
157 throw std::invalid_argument(std::string(who) + ": no scalar named '" + name + "'");
158}
159
160template <class Grid>
162 return (hasScene_ && nInst_ > 0) ? nInst_ : 1;
163}
164
165template <class Grid>
166void Solver<Grid>::setScalarWall(const std::string& name, int type, double value,
167 double coefficient, int instance) {
168 ScalarCutState& st = *cutcellScalar(name, "set_scalar_wall").cut;
169 if (type < 0 || type > 2)
170 throw std::invalid_argument("set_scalar_wall: type must be 'neumann', 'dirichlet' or 'robin'");
171 if (!std::isfinite(value))
172 throw std::invalid_argument("set_scalar_wall: value must be finite");
173 if (type == 2 && !(coefficient >= 0.0))
174 throw std::invalid_argument("set_scalar_wall: the robin coefficient k must be >= 0");
175 const ScalarWallSpec w{type, value, type == 2 ? coefficient : 0.0};
176 if (instance < 0) { // every body: the default, and no override survives
177 st.wallDefault = w;
178 st.wallInstance.clear();
179 return;
180 }
181 if (!hasScene_)
182 throw std::invalid_argument(
183 "set_scalar_wall: instance= needs a scene (set_scene + set_solid_from_scene); raw-SDF "
184 "geometry has one body, set with instance=None");
185 if (instance >= nInst_)
186 throw std::invalid_argument("set_scalar_wall: instance " + std::to_string(instance) +
187 " out of range (the scene has " + std::to_string(nInst_) + ")");
188 for (auto it = st.wallInstance.begin(); it != st.wallInstance.end(); ++it)
189 if (it->first == instance) {
190 st.wallInstance.erase(it);
191 break;
192 }
193 st.wallInstance.emplace_back(instance, w);
194}
195
196template <class Grid>
197void Solver<Grid>::setScalarSolid(const std::string& name, double D, double capacity,
198 double partition, double contactResistance, int instance) {
199 ScalarField& sc = cutcellScalar(name, "set_scalar_solid");
200 ScalarCutState& st = *sc.cut;
201 if (!std::isfinite(D) || !(D >= 0.0))
202 throw std::invalid_argument("set_scalar_solid: diffusivity must be finite and >= 0");
203 if (!std::isfinite(capacity) || !(capacity > 0.0))
204 throw std::invalid_argument("set_scalar_solid: capacity must be finite and > 0");
205 if (!std::isfinite(partition) || !(partition > 0.0))
206 throw std::invalid_argument("set_scalar_solid: partition must be finite and > 0");
207 if (!std::isfinite(contactResistance) || !(contactResistance >= 0.0))
208 throw std::invalid_argument("set_scalar_solid: contact_resistance must be finite and >= 0");
209 if (instance >= 0 && !hasScene_)
210 throw std::invalid_argument(
211 "set_scalar_solid: instance= needs a scene (set_scene + set_solid_from_scene); raw-SDF "
212 "geometry has one body, set with instance=None");
213 if (instance >= 0 && instance >= nInst_)
214 throw std::invalid_argument("set_scalar_solid: instance " + std::to_string(instance) +
215 " out of range (the scene has " + std::to_string(nInst_) + ")");
217 w.type = 3;
218 w.solidD = D;
219 w.solidC = capacity;
220 w.solidK = partition;
221 w.solidRc = contactResistance;
222 if (st.solid.extent(0) != n_) // §1.3: the solid field, registered, on the first call
223 st.solid = addField(name + "_solid");
224 if (instance < 0) {
225 st.wallDefault = w;
226 st.wallInstance.clear();
227 return;
228 }
229 for (auto it = st.wallInstance.begin(); it != st.wallInstance.end(); ++it)
230 if (it->first == instance) {
231 st.wallInstance.erase(it);
232 break;
233 }
234 st.wallInstance.emplace_back(instance, w);
235}
236
237template <class Grid>
239 ScalarCutState& st = *sc.cut;
240 const int nb = scalarNumBodies();
241 std::vector<ScalarWallSpec> spec((std::size_t)nb, st.wallDefault);
242 for (const auto& [inst, w] : st.wallInstance)
243 if (inst >= 0 && inst < nb)
244 spec[(std::size_t)inst] = w;
245 conj.assign((std::size_t)nb, 0);
246 st.mtLam.assign((std::size_t)nb, 0.0);
247 st.mtCK.assign((std::size_t)nb, 1.0);
248 st.mtK.assign((std::size_t)nb, 1.0);
249 st.mtRc.assign((std::size_t)nb, 0.0);
250 st.maxSolidD = 0.0;
251 const double dI = u_.diffToInt(), rI = u_.resistToInt();
252 for (int b = 0; b < nb; ++b) {
253 const ScalarWallSpec& w = spec[(std::size_t)b];
254 if (w.type != 3)
255 continue;
256 conj[(std::size_t)b] = 1;
257 // psi form (§1.1): Lam_s = C_s D_s K, capacity C_s K; internal by §1.2 at use time
258 st.mtLam[(std::size_t)b] = (w.solidC * w.solidD * w.solidK) * dI;
259 st.mtCK[(std::size_t)b] = w.solidC * w.solidK;
260 st.mtK[(std::size_t)b] = w.solidK;
261 st.mtRc[(std::size_t)b] = w.solidRc * rI;
262 st.maxSolidD = std::fmax(st.maxSolidD, w.solidD * dI);
263 }
264 if (st.mtConjV.extent(0) != (std::size_t)nb) { // persistent, refilled below (finding 7)
265 st.mtConjV = Kokkos::View<int*, CCMem>("peclet::flow::sco_mt_conj", nb);
266 st.mtLamV = Kokkos::View<double*, CCMem>("peclet::flow::sco_mt_lam", nb);
267 st.mtCKV = Kokkos::View<double*, CCMem>("peclet::flow::sco_mt_ck", nb);
268 st.mtKV = Kokkos::View<double*, CCMem>("peclet::flow::sco_mt_k", nb);
269 st.mtRcV = Kokkos::View<double*, CCMem>("peclet::flow::sco_mt_rc", nb);
270 }
272 mt.nb = nb;
273 mt.conj = st.mtConjV;
274 mt.lam = st.mtLamV;
275 mt.ck = st.mtCKV;
276 mt.kp = st.mtKV;
277 mt.rc = st.mtRcV;
278 auto hc = Kokkos::create_mirror_view(mt.conj);
279 auto hl = Kokkos::create_mirror_view(mt.lam);
280 auto hk = Kokkos::create_mirror_view(mt.ck);
281 auto hp = Kokkos::create_mirror_view(mt.kp);
282 auto hr = Kokkos::create_mirror_view(mt.rc);
283 for (int b = 0; b < nb; ++b) {
284 hc(b) = conj[(std::size_t)b];
285 hl(b) = st.mtLam[(std::size_t)b];
286 hk(b) = st.mtCK[(std::size_t)b];
287 hp(b) = st.mtK[(std::size_t)b];
288 hr(b) = st.mtRc[(std::size_t)b];
289 }
290 Kokkos::deep_copy(mt.conj, hc);
291 Kokkos::deep_copy(mt.lam, hl);
292 Kokkos::deep_copy(mt.ck, hk);
293 Kokkos::deep_copy(mt.kp, hp);
294 Kokkos::deep_copy(mt.rc, hr);
295 return mt;
296}
297
298template <class Grid>
299void Solver<Grid>::ensureScalarSolidRecord(ScalarField& sc, const std::vector<char>& conj) {
301 ScalarCutState& st = *sc.cut;
302 if (st.solidVersion == scg_.version && st.solidKey == conj && st.mat.extent(0) == n_)
303 return;
304 const scg::ScalarCutGeometry& gm = scg_;
305 const auto& fo = gm.fac;
306 if (st.mat.extent(0) != n_) {
307 st.mat = CCField(sc.name + "_cc_mat", n_);
308 st.sunk = CCField(sc.name + "_cc_sunk", n_);
309 }
310 // the material table needs only the conjugate flags here (§2.7)
312 const int nb = (int)conj.size();
313 mt.nb = nb;
314 mt.conj = Kokkos::View<int*, CCMem>("peclet::flow::sco_rec_conj", nb);
315 {
316 auto hc = Kokkos::create_mirror_view(mt.conj);
317 for (int b = 0; b < nb; ++b)
318 hc(b) = conj[(std::size_t)b];
319 Kokkos::deep_copy(mt.conj, hc);
320 }
321 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
322 const bool hasOwner = hasScene_ && cutOwner_.extent(0) == nInner;
323 double svol[2] = {0.0, 0.0};
324 sco::buildSolidMaterial(st.mat, st.sunk, CCConst(gm.kappa), CCConst(gm.sax), CCConst(gm.say),
325 CCConst(gm.saz), fo, Kokkos::View<const int*, CCMem>(cutOwner_), hasOwner,
326 mt, e_, G, svol);
327 fillGhosts(st.mat);
328 fillGhosts(st.sunk);
329 for (int face = 0; face < 6; ++face)
330 if (bc_[face] != 0 && touchesGlobalFace(face))
331 scg::zeroGhostSlab(st.sunk, e_, G, face);
332 // the facets of conjugate bodies, and their solid probes (§3.4)
333 st.conjF = Kokkos::View<std::uint8_t*, CCMem>(sc.name + "_cc_conjF", fo.n);
334 st.sS = Kokkos::View<double*, CCMem>(sc.name + "_cc_sS", fo.n);
335 st.offS = decltype(st.offS)(sc.name + "_cc_offS", fo.n);
336 st.wS = decltype(st.wS)(sc.name + "_cc_wS", fo.n);
337 st.rungS = Kokkos::View<std::uint8_t*, CCMem>(sc.name + "_cc_rungS", fo.n);
338 {
339 auto cf = st.conjF;
340 auto fbody = fo.body;
341 auto cj = mt.conj;
343 Kokkos::parallel_for(
344 "peclet::flow::sco_conj_facets", Kokkos::RangePolicy<CCExec>(space, 0, fo.n),
345 KOKKOS_LAMBDA(const long f) {
346 cf(f) = cj(sco::bodyIndex((double)fbody(f), nb)) != 0 ? 1 : 0;
347 });
348 space.fence();
349 }
350 sco::buildSolidProbes(st.sS, st.offS, st.wS, st.rungS,
351 Kokkos::View<const std::uint8_t*, CCMem>(st.conjF), fo, CCConst(sdf_),
352 CCConst(st.sunk), e_, u_.hp);
353 // census: solid unknowns (inner) and the solid rungs (conjugate facets)
354 long cnt[5] = {0, 0, 0, 0, 0};
355 {
356 CCConst su(st.sunk);
357 const C3 e = e_;
358 const int g = G;
359 double nsu = 0.0;
360 ccReduce3(
361 "peclet::flow::sco_count_solid", C3{g, g, g}, C3{e.x - g, e.y - g, e.z - g},
362 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
363 acc += su((long)x + (long)y * e.x + (long)z * (long)e.x * e.y) > 0.5 ? 1.0 : 0.0;
364 },
365 nsu);
366 cnt[0] = (long)nsu;
367 auto rs = st.rungS;
368 auto cf = st.conjF;
369 for (int k = 0; k < 4; ++k) {
370 long c = 0;
371 Kokkos::parallel_reduce(
372 "peclet::flow::sco_solid_rungs", Kokkos::RangePolicy<CCExec>(0, fo.n),
373 KOKKOS_LAMBDA(const long f, long& acc) {
374 acc += (cf(f) != 0 && (int)rs(f) == k) ? 1 : 0;
375 },
376 c);
377 cnt[1 + k] = c;
378 }
379 }
380#ifdef PECLET_FLOW_MPI
381 if (distributed_) {
382 long o[5];
383 MPI_Allreduce(cnt, o, 5, MPI_LONG, MPI_SUM, comm_);
384 for (int k = 0; k < 5; ++k)
385 cnt[k] = o[k];
386 double ov[2];
388 svol[0] = ov[0];
389 svol[1] = ov[1];
390 }
391#endif
392 const double lv = u_.lenToPhys(); // D-WO7-1: the census's sealed solid volume, physical
393 st.solidVolume = svol[0] * u_.vol * lv * lv * lv;
394 st.sealedSolidVolume = svol[1] * u_.vol * lv * lv * lv;
395 st.numSolidUnknowns = cnt[0];
396 for (int k = 0; k < 4; ++k)
397 st.solidRungs[k] = cnt[1 + k];
398 st.solidVersion = gm.version;
399 st.solidKey = conj;
400}
401
402template <class Grid>
403std::vector<double> Solver<Grid>::getScalarSolid(const std::string& name) {
404 ScalarField& sc = cutcellScalar(name, "get_scalar_solid");
405 ScalarCutState& st = *sc.cut;
406 const std::size_t nInner = (std::size_t)nx_ * ny_ * nz_;
407 std::vector<double> out(nInner, std::numeric_limits<double>::quiet_NaN());
408 if (!st.built || !st.conj || st.solid.extent(0) != n_ || st.sunk.extent(0) != n_)
409 return out;
410 const auto ps = gatherInner(st.solid);
411 const auto su = gatherInner(st.sunk);
412 const auto mt = gatherInner(st.mat);
413 const int nb = (int)st.mtK.size();
414 for (std::size_t i = 0; i < nInner; ++i) {
415 if (!(su[i] > 0.5))
416 continue;
417 int b = (int)mt[i];
418 if (b < 0 || b >= nb)
419 b = 0;
420 out[i] = st.mtK[(std::size_t)b] * ps[i];
421 }
422 return out;
423}
424
425template <class Grid>
426std::vector<double> Solver<Grid>::scalarSolidGeometryField(const std::string& name, int which) {
427 ScalarField& sc = cutcellScalar(name, "scalar_geometry");
428 ScalarCutState& st = *sc.cut;
429 std::vector<char> conj;
432 if (which == 1)
433 return gatherInner(st.sunk);
434 std::vector<double> k = gatherInner(scg_.kappa);
435 for (auto& v : k)
436 v = 1.0 - v;
437 return k;
438}
439
440template <class Grid>
441void Solver<Grid>::setScalarSource(const std::string& name, double S) {
442 ScalarCutState& st = *cutcellScalar(name, "set_scalar_source").cut;
443 if (!std::isfinite(S))
444 throw std::invalid_argument("set_scalar_source: the source must be finite");
445 st.sourceConst = S;
446 st.sourceField = CCField();
447 st.sourceIsField = false;
448}
449
450template <class Grid>
451void Solver<Grid>::setScalarSourceField(const std::string& name, const std::vector<double>& S) {
452 ScalarCutState& st = *cutcellScalar(name, "set_scalar_source").cut;
453 if (S.size() != (std::size_t)nx_ * ny_ * nz_)
454 throw std::invalid_argument(
455 "set_scalar_source: the array must have this rank's (nx, ny, nz) "
456 "shape");
457 if (st.sourceField.extent(0) != n_)
458 st.sourceField = CCField(name + "_cc_source", n_);
459 scatterInner(st.sourceField, S);
460 st.sourceConst = 0.0;
461 st.sourceIsField = true;
462}
463
464template <class Grid>
465void Solver<Grid>::setScalarTolerance(const std::string& name, double rtol) {
466 ScalarCutState& st = *cutcellScalar(name, "set_scalar_tolerance").cut;
467 if (!(rtol > 0.0) || !std::isfinite(rtol))
468 throw std::invalid_argument("set_scalar_tolerance: rtol must be finite and > 0");
469 st.rtol = rtol;
470}
471
472template <class Grid>
473void Solver<Grid>::setScalarMeanGradient(const std::string& name, const std::array<double, 3>& G) {
474 ScalarCutState& st = *cutcellScalar(name, "set_scalar_mean_gradient").cut;
475 for (double v : G)
476 if (!std::isfinite(v))
477 throw std::invalid_argument("set_scalar_mean_gradient: the gradient must be finite");
478 for (int a = 0; a < 3; ++a)
479 st.meanGrad[a] = G[(std::size_t)a];
480}
481
482template <class Grid>
485 const int og[3] = {og_.x, og_.y, og_.z};
486 for (int a = 0; a < 3; ++a) {
487 p.G[a] = Gphys[a];
488 p.org[a] = u_.org[a];
489 p.h[a] = u_.h[a];
490 p.base[a] = (long)og[a] - G;
491 }
492 p.sy = e_.x;
493 p.sz = (long)e_.x * e_.y;
494 return p;
495}
496
497template <class Grid>
498std::array<double, 3> Solver<Grid>::scalarMeanFlux(const std::string& name) {
499 ScalarField& sc = cutcellScalar(name, "scalar_mean_flux");
500 ScalarCutState& st = *sc.cut;
501 std::array<double, 3> out{0.0, 0.0, 0.0};
502 if (!st.built)
503 return out;
504 const scg::ScalarCutGeometry& gm = scg_;
505 fillGhosts(sc.c);
506 // the face flux of the last solve (read only when it advected; any field otherwise)
507 const bool adv = st.advecting;
508 double d[3], v[3];
510 CCConst(gm.say), CCConst(gm.saz), CCConst(adv ? st.phi[0] : sc.c),
511 CCConst(adv ? st.phi[1] : sc.c), CCConst(adv ? st.phi[2] : sc.c), adv, st.lam,
512 st.gInt, u_.hp, st.ubar, e_, G, d, v);
513 if (st.conj) { // WO-7: the solid faces' diffusive flux (§8.1: "over both phases")
514 double ds[3];
515 fillGhosts(st.solid);
516 sco::solidMeanFluxLocal(CCConst(st.solid), CCConst(st.sunk), CCConst(st.spx), CCConst(st.spy),
517 CCConst(st.spz), st.gInt, u_.hp, e_, G, ds);
518 for (int a = 0; a < 3; ++a)
519 d[a] += ds[a];
520 }
521 double s[6] = {d[0], d[1], d[2], v[0], v[1], v[2]};
522#ifdef PECLET_FLOW_MPI
523 if (distributed_) {
524 double o[6];
526 for (int j = 0; j < 6; ++j)
527 s[j] = o[j];
528 }
529#endif
530 // the box average: per unit full-cell volume sums over the GLOBAL cell count, then physical
531 const auto gc = globalCells();
532 const double nBox = (double)gc[0] * (double)gc[1] * (double)gc[2];
533 const double toPhys = 1.0 / u_.speedToInt();
534 for (int a = 0; a < 3; ++a)
535 out[(std::size_t)a] = (s[a] + s[3 + a]) / nBox * toPhys;
536 return out;
537}
538
539template <class Grid>
540void Solver<Grid>::setScalarMaxIterations(const std::string& name, int maxit) {
541 ScalarCutState& st = *cutcellScalar(name, "set_scalar_max_iterations").cut;
542 if (maxit < 1)
543 throw std::invalid_argument("set_scalar_max_iterations: maxit must be >= 1");
544 st.maxit = maxit;
545}
546
547template <class Grid>
548void Solver<Grid>::setScalarBcProfile(const std::string& name, int face,
549 const std::vector<double>& prof, int n1, int n2) {
550 ScalarField& sc = cutcellScalar(name, "set_scalar_bc");
551 if (face < 0 || face > 5)
552 throw std::invalid_argument("set_scalar_bc: face out of range");
553 int t1, t2;
554 sco::faceTangents(face / 2, t1, t2);
555 const int n[3] = {nx_, ny_, nz_};
556 if (n1 != n[t1] || n2 != n[t2] || prof.size() != (std::size_t)n1 * n2)
557 throw std::invalid_argument("set_scalar_bc: a profile must have shape (N_t1, N_t2) = (" +
558 std::to_string(n[t1]) + ", " + std::to_string(n[t2]) +
559 ") -- this rank's face cells in the tangential axes in x, y, z "
560 "order");
561 for (double v : prof)
562 if (!std::isfinite(v))
563 throw std::invalid_argument("set_scalar_bc: the profile must be finite");
564 sc.bc[face] = 2; // a profile is a Dirichlet value
565 sc.bcVal[face] = 0.0;
566 sc.cut->hasProfile[face] = true;
567 sc.cut->profile[face] = prof;
568 sc.cut->profileN[face][0] = n1;
569 sc.cut->profileN[face][1] = n2;
570}
571
572template <class Grid>
574 const std::string who = "cut-cell scalar '" + sc.name + "': ";
575 if (porous_)
576 throw std::runtime_error(who + "porous (volume-averaged) continuity is not supported");
577 if (hasMovingInstance())
578 throw std::runtime_error(who + "moving scene instances are not supported (static geometry)");
579 if (sc.dmask.extent(0) == n_)
580 throw std::runtime_error(who + "the per-cell Dirichlet mask is not supported");
581 // §13 Q13, decided by G9b (WO-5): the ghost projection's constraint is the binary-openness
582 // divergence PLUS wall-anchored closure terms (gpDivergDelta), so no openness-weighted face flux
583 // of its field is discretely divergence-free — a constant concentration drifted by 0.80 in 50
584 // Stokes steps at bulk Courant 0.47 (staggered and every other collocated scheme: 0 exactly).
585 // Covers the collocated 'ghost' scheme (the AUTO default) and the staggered verification path.
586 if (ghostProjection_)
587 throw std::runtime_error(
588 who + "the ghost projection ('ghost' collocated scheme, or diagnostics." +
589 "set_ghost_projection) leaves no discretely divergence-free face flux to advect with " +
590 "(doc/scalar_ibm_design.md §6.1, gate G9b); the accepted projections are the collocated " +
591 "schemes 'gauge-exact', 'plain' and 'embed' (set_collocated_scheme, before set_solid) "
592 "and " +
593 "the staggered Solver without set_ghost_projection");
594 static const char* kFace[6] = {"-x", "+x", "-y", "+y", "-z", "+z"};
595 // §1.5 / §8.1: theta is periodic along every axis the mean gradient has a component on
596 for (int f = 0; f < 6; ++f)
597 if (sc.cut->meanGrad[f / 2] != 0.0 && (bc_[f] != 0 || sc.bc[f] != 0))
598 throw std::runtime_error(who + "the mean gradient has a component along " +
599 std::string(1, "xyz"[f / 2]) + " but the domain face " + kFace[f] +
600 " is not periodic (set_scalar_mean_gradient needs periodic faces "
601 "on every axis it has a component on)");
602 for (int f = 0; f < 6; ++f) {
603 const bool flowPeriodic = bc_[f] == 0, scalarPeriodic = sc.bc[f] == 0;
605 throw std::runtime_error(
606 who + "face " + kFace[f] + " is " + (scalarPeriodic ? "periodic" : "non-periodic") +
607 " for the scalar but " + (flowPeriodic ? "periodic" : "non-periodic") +
608 " for the flow (set_scalar_bc / set_domain_bc must agree)");
609 if (bc_[f] == 2 && sc.bc[f] != 2)
610 throw std::runtime_error(who + "the inflow face " + kFace[f] +
611 " needs a scalar 'dirichlet' value (set_scalar_bc)");
612 // WO-5b (open, reported): on the collocated grid the projected face field does not keep the
613 // flux the projection constrained on a high-side open face -- projectCorrectVelocities' plain
614 // ghost fill of uf_/vf_/wf_ wraps that plane from the opposite face before the outflow
615 // correction is added to it -- so there is no divergence-free boundary flux to capture (a
616 // constant drifted by 0.48 in the outlet column in one step). Refused until decided.
617 if constexpr (Grid::collocated)
618 if (bc_[f] == 2 || bc_[f] == 3)
619 throw std::runtime_error(
620 who + "face " + kFace[f] + " is a flow " + (bc_[f] == 2 ? "inflow" : "outflow") +
621 " face: on the collocated grid the projected face field does not keep the boundary "
622 "flux the projection constrained, so open domain faces are not supported for a "
623 "cut-cell scalar there yet -- use the staggered Solver (doc/scalar_ibm_design.md "
624 "§6.5)");
625 }
626}
627
628template <class Grid>
630 ScalarCutState& st = *sc.cut;
631 fillGhosts(x); // one G = 2 exchange covers the 7-point part and the +-2-reach probes
632 applyCutcellOp(y, CCConst(x), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_, G);
633 sco::overlayApply(y, CCConst(x), scg_.fac, st.cw);
634}
635
636template <class Grid>
638 CCField xs) {
639 ScalarCutState& st = *sc.cut;
640 fillGhosts(xf); // one G = 2 exchange per phase field (§4.1)
641 fillGhosts(xs);
642 applyCutcellOp(yf, CCConst(xf), sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, e_, G);
643 applyCutcellOp(ys, CCConst(xs), st.ASC, st.ASW, st.ASE, st.ASS, st.ASN, st.ASB, st.AST, e_, G);
644 sco::conjOverlayApply(yf, ys, CCConst(xf), CCConst(xs), scg_.fac, st.cw, st.cc, st.offS, st.wS);
645}
646
647template <class Grid>
649 // §6.1 (D5): constant preservation needs sum_f F = 0 per cell, and only the projection's own
650 // divergence kernel states which openness that sum carries: `divergOpen(f, oxb_)` (+ the
651 // closure delta) in ghost-projection mode, `constraintDivergence` = `divergOpen(f, ox_)` in every
652 // other non-porous branch of both grids (porous continuity is refused for a cut-cell scalar).
653 const bool gp = ghostProjection_ && oxb_.extent(0) > 0;
654 const CCField o[3] = {gp ? oxb_ : ox_, gp ? oyb_ : oy_, gp ? ozb_ : oz_};
656 f.open = o[a];
657 if constexpr (Grid::collocated)
658 f.vel = a == 0 ? uf_ : (a == 1 ? vf_ : wf_); // the projected MAC face field
659 else
660 f.vel = C[a].u; // the stored staggered velocity IS the projected face velocity
661 return f;
662}
663
664template <class Grid>
666 // WO-5b (§6.5, ruling D-WO5-3). Called right after the projection: the high-side boundary face
667 // is the first GHOST index, which the next plain ghost fill (advanceScalars' own, the VoF
668 // bridge's) wraps from the opposite face, so the flux the projection constrained there is read
669 // now. The plane spans the full transverse extent (its transverse ghost rows fed the locally
670 // computed ghost-layer small flags until D-WOR-6 exchanged them; kept, a plane copy).
671 if (!hasBc_)
672 return;
673 // Captured whether or not a cut-cell scalar exists yet (one plane copy per open face), so a
674 // scalar added to a developed flow can be solved before the next step.
675 bool anyOpen = false;
676 for (int f = 0; f < 6; ++f)
677 anyOpen = anyOpen || bc_[f] == 2 || bc_[f] == 3;
678 if (!anyOpen)
679 return;
680 const int ext[3] = {e_.x, e_.y, e_.z};
681 const long st[3] = {1, (long)e_.x, (long)e_.x * e_.y};
682 for (int f = 0; f < 6; ++f) {
683 if (!(bc_[f] == 2 || bc_[f] == 3) || !touchesGlobalFace(f))
684 continue;
685 const int a = f / 2, side = f % 2;
686 int t1, t2;
688 const int m1 = ext[t1], m2 = ext[t2];
689 if (scalarOpenFlux_[f].extent(0) != (std::size_t)m1 * m2)
690 scalarOpenFlux_[f] = CCField("peclet::flow::scalar_open_flux", (std::size_t)m1 * m2);
692 const CCConst open(ff.open), vel(ff.vel);
693 CCField pl = scalarOpenFlux_[f];
694 const long sa = st[a], s1 = st[t1], s2 = st[t2];
695 const long bfa = side == 0 ? G : ext[a] - G;
697 Kokkos::parallel_for(
698 "peclet::flow::scalar_capture_open_flux", MDRange2<CCExec>(space, {0, 0}, {m1, m2}),
699 KOKKOS_LAMBDA(int j1, int j2) {
700 const long bf = bfa * sa + (long)j1 * s1 + (long)j2 * s2;
701 pl((long)j1 + (long)j2 * m1) = open(bf) * vel(bf);
702 });
703 space.fence();
704 }
705 scalarOpenValid_ = true;
706}
707
708template <class Grid>
710 ScalarCutState& st = *sc.cut;
711 const scg::ScalarCutGeometry& gm = scg_;
712 const CCConst unk(gm.unknown), kap(gm.kappa);
713 for (int a = 0; a < 3; ++a)
714 if (st.phi[a].extent(0) != n_)
715 st.phi[a] = CCField(sc.name + "_cc_phi", n_);
716 // the face flux of the projection in force, guarded (sco::faceFluxes). A steady solve is called
717 // outside step(), so it fills the face velocity's ghosts itself (advanceScalars did for an
718 // advance).
719 // WO-5b (§6.5): the open (inflow/outflow) global faces this rank touches take the boundary-face
720 // flux the last projection constrained (scalarCaptureOpenFaceFlux), written over the guard's 0.
721 bool anyOpen = false, openHere[6];
722 for (int f = 0; f < 6; ++f) {
723 const bool o = bc_[f] == 2 || bc_[f] == 3;
724 anyOpen = anyOpen || o;
725 openHere[f] = o && scalarOpenValid_ && touchesGlobalFace(f);
726 st.openFace[f] = false;
727 st.openInflow[f] = bc_[f] == 2;
728 }
729 long nFlux = 0, nGuard = 0;
730 double vmax = 0.0;
731 for (int a = 0; a < 3; ++a) {
733 if (steady)
734 fillGhosts(ff.vel);
735 vmax = std::fmax(vmax, sco::maxabsLocal(CCConst(ff.vel), e_, G));
736 sco::faceFluxes(st.phi[a], CCConst(ff.vel), CCConst(ff.open), unk, a, e_);
737 for (int side = 0; side < 2; ++side)
738 if (openHere[2 * a + side])
739 sco::openFaceFluxes(st.phi[a], CCConst(scalarOpenFlux_[2 * a + side]), unk, a, side, e_, G);
740 long n1 = 0, n2 = 0;
741 sco::faceFluxCountsLocal(CCConst(st.phi[a]), CCConst(ff.vel), CCConst(ff.open), a, e_, G, n1,
742 n2);
743 nFlux += n1;
744 nGuard += n2;
745 }
746 // (the third entry: A2's census max_cell_peclet, max |phi_a| / (Lam' w_a) over interior faces;
747 // the fourth, collocated only: the CELL velocity, which moves while the face field may not exist)
748 double vcell = 0.0;
749 if constexpr (Grid::collocated)
750 for (int a = 0; a < 3; ++a)
751 vcell = std::fmax(vcell, sco::maxabsLocal(CCConst(C[a].u), e_, G));
752 double mx[4] = {
753 sco::maxAbsFluxLocal(CCConst(st.phi[0]), CCConst(st.phi[1]), CCConst(st.phi[2]), e_, G), vmax,
754 sco::maxCellPecletLocal(CCConst(st.phi[0]), CCConst(st.phi[1]), CCConst(st.phi[2]), unk,
755 st.lam, u_.w, e_, G),
756 vcell};
757#ifdef PECLET_FLOW_MPI
758 if (distributed_) {
759 double o[4] = {0.0, 0.0, 0.0, 0.0};
761 for (int k = 0; k < 4; ++k)
762 mx[k] = o[k];
763 }
764#endif
765 st.maxCellPeclet = mx[2];
766 // Review finding 1 (ruling D-WOR-1): on the collocated grid the advecting field is the projected
767 // MAC face field uf_/vf_/wf_. set_solid zeroes it and a raw set_field('u', ...) does not seed it,
768 // so before the first projection a moving fluid would read max|uf| = 0 and be silently left
769 // unadvected (the trap advect_vof closed for VoF, ISSUES sweep item 5). Refused, as there.
770 if constexpr (Grid::collocated)
771 if (!faceFieldValid_ && mx[3] > 0.0)
772 throw std::runtime_error(
773 "cut-cell scalar '" + sc.name +
774 "': SolverColocated advects with the projected MAC FACE field uf_/vf_/wf_ (the only "
775 "discretely divergence-free field on this grid), and it has never been built since the "
776 "geometry was set -- it is all zeros, so the moving fluid would be silently ignored. "
777 "Call set_state()/set_velocity() (which seed the face field through the same "
778 "centerToFace map project() uses) or step() first (doc/scalar_ibm_design.md §6.1)");
779 // WO-5 (open, reported): without the cut-cell pressure operator no projection runs and no
780 // openness exists (set_solid(..., cutcell_pressure=False)), so there is no discretely
781 // divergence-free face flux to advect with (§6.1); a moving fluid is refused rather than left
782 // silently unadvected. A fluid at rest (diffusion only, WO-3/WO-4) is unaffected.
783 if (!cutcellPressure_ && mx[1] > 0.0)
784 throw std::runtime_error("cut-cell scalar '" + sc.name +
785 "': the fluid moves but no cut-cell projection exists to advect "
786 "with -- use set_solid(..., cutcell_pressure=True) or "
787 "set_pressure_geometry (doc/scalar_ibm_design.md §6.1)");
788 // WO-5b: the flux of an open domain face is the one the projection constrained, captured by
789 // step(); before the first projection there is none to advect with.
790 if (anyOpen && !scalarOpenValid_ && mx[1] > 0.0)
791 throw std::runtime_error("cut-cell scalar '" + sc.name +
792 "': the fluid moves and the domain has an inflow/outflow face, but no "
793 "projection has supplied that face's flux yet -- advance with step() "
794 "(doc/scalar_ibm_design.md §6.5)");
795 st.advecting = mx[0] > 0.0;
796 st.numSmall = 0;
797 st.numImplicitFaces = 0;
798 st.bulkCourant = 0.0;
799 st.numFluxFaces = 0;
800 st.numGuardedFaces = 0;
801 st.numBackflowFaces = 0;
802 if (!st.advecting) {
803 long cnt[1] = {nGuard};
804#ifdef PECLET_FLOW_MPI
805 if (distributed_) {
806 long o[1];
807 MPI_Allreduce(cnt, o, 1, MPI_LONG, MPI_SUM, comm_);
808 cnt[0] = o[0];
809 }
810#endif
811 st.numGuardedFaces = cnt[0];
812 return;
813 }
814 if (st.small.extent(0) != n_)
815 st.small = CCField(sc.name + "_cc_small", n_);
816 if (st.outflow.extent(0) != n_)
817 st.outflow = CCField(sc.name + "_cc_outflow", n_);
818 const CCConst px(st.phi[0]), py(st.phi[1]), pz(st.phi[2]);
819 if (steady) { // §6.7: implicit FOU on every face; no classification
820 Kokkos::deep_copy(CCExec(), st.small, 0.0);
821 } else {
822 // §6.3: C_bulk = the global MAX over full cells of dt Out / V, then the per-cell flags
823 double cb = sco::bulkCourantLocal(px, py, pz, kap, unk, CCConst(gm.sax), CCConst(gm.say),
824 CCConst(gm.saz), dt_, e_, G);
825#ifdef PECLET_FLOW_MPI
826 if (distributed_) {
827 double o = 0.0;
828 MPI_Allreduce(&cb, &o, 1, MPI_DOUBLE, MPI_MAX, comm_);
829 cb = o;
830 }
831#endif
832 st.bulkCourant = cb;
833 // Review finding 7 (D-WO5-1): koren with forward Euler is TVD only to bulk Courant 1/2 (min
834 // -21 at C = 0.9 with no solid); legacy-identical, so said once rather than refused.
835 if (sc.scheme == 1 && cb > 0.5 && !st.warnedKoren) {
836 st.warnedKoren = true;
837 if (scalarRootRank())
838 std::fprintf(stderr,
839 "peclet.flow: cut-cell scalar '%s': scheme 'koren' at bulk Courant %.3f > 1/2 "
840 "-- the explicit limiter is bounded only to 1/2 and overshoots above it; "
841 "reduce dt or use scheme='fou' (diagnostics.scalar_census 'bulk_courant', "
842 "printed once)\n",
843 sc.name.c_str(), cb);
844 }
845 sco::smallCells(st.small, px, py, pz, kap, unk, dt_, std::fmax(0.5, cb), e_, G);
846 // Review finding 6 (ruling D-WOR-6): the ghost flags are the OWNER's, by one exchange per
847 // advance, so a face's implicit/explicit split agrees across ranks whatever the ghost fluxes
848 // (formerly ghost layer 1 was recomputed locally, which agreed only through flow's
849 // outflow-correction and ghost-fill ranges). The periodic wrap beyond a non-periodic face is
850 // then cleared with the unknown flags, as the local computation had it.
851 fillGhosts(st.small);
852 sco::zeroNonUnknown(st.small, unk, e_, 0);
853 }
854 long cnt[5] = {sco::countSmallLocal(CCConst(st.small), e_, G), 0, nFlux, nGuard, 0};
855 for (int a = 0; a < 3; ++a)
856 cnt[1] += sco::countImplicitLocal(CCConst(st.phi[a]), CCConst(st.small), unk, a, steady, e_, G);
857 for (int f = 0; f < 6; ++f) {
858 st.openFace[f] = openHere[f];
859 if (!openHere[f])
860 continue;
861 long n1 = 0, n2 = 0, n3 = 0;
862 sco::openFaceCountsLocal(CCConst(st.phi[f / 2]), CCConst(st.small), unk, st.openInflow[f],
863 f / 2, f % 2, steady, e_, G, n1, n2, n3);
864 cnt[2] += n1;
865 cnt[1] += n2;
866 cnt[4] += n3;
867 }
868#ifdef PECLET_FLOW_MPI
869 if (distributed_) {
870 long o[5];
871 MPI_Allreduce(cnt, o, 5, MPI_LONG, MPI_SUM, comm_);
872 for (int k = 0; k < 5; ++k)
873 cnt[k] = o[k];
874 }
875#endif
876 st.numSmall = cnt[0];
877 st.numImplicitFaces = cnt[1];
878 st.numFluxFaces = cnt[2];
879 st.numGuardedFaces = cnt[3];
880 st.numBackflowFaces = cnt[4];
881 // Review finding 2a (ruling D-WOR-2a): A2 requires an implicit backflow row to be reported. Such
882 // a row is AC += F_out < 0 (inflow at the zero-gradient value c_i); it costs the column its
883 // dominance (no M-matrix there), and A2's coarse mass omega_open is clamped at 0 on it.
884 if (steady && st.numBackflowFaces > 0 && !st.warnedBackflow) {
885 st.warnedBackflow = true;
886 if (scalarRootRank())
887 std::fprintf(stderr,
888 "peclet.flow: cut-cell scalar '%s': %ld outflow-face row(s) carry BACKFLOW in "
889 "the steady solve (flow entering through an 'outflow' face). Their inflow "
890 "takes the zero-gradient value c_i, which is not a well-posed inflow condition "
891 "(doc/scalar_ibm_design.md §6.5): the matrix loses column dominance there, and "
892 "the advective preconditioner's coarse mass omega_open is clamped at 0 on those "
893 "rows (A2). Prefer an 'inflow' face where fluid enters "
894 "(diagnostics.scalar_census 'num_backflow_faces', printed once)\n",
895 sc.name.c_str(), st.numBackflowFaces);
896 }
897}
898
899template <class Grid>
903
904template <class Grid>
905void Solver<Grid>::solveScalarSteady(const std::string& name) {
906 scalarCutAssembleSolve(cutcellScalar(name, "solve_scalar_steady"), true);
907}
908
909template <class Grid>
913 if (!steady && !(dt_ > 0.0))
914 throw std::runtime_error("cut-cell scalar '" + sc.name + "': set_dt first");
915 ScalarCutState& st = *sc.cut;
916 const scg::ScalarCutGeometry& gm = scg_;
917 const CCConst unk(gm.unknown), kap(gm.kappa);
918 auto alloc = [&](CCField& f, const char* tag) {
919 if (f.extent(0) != n_)
920 f = CCField(sc.name + tag, n_);
921 };
922 alloc(st.SAC, "_cc_sac");
923 alloc(st.kr, "_cc_r");
924 alloc(st.krh, "_cc_rh");
925 alloc(st.kp, "_cc_p");
926 alloc(st.kv, "_cc_v");
927 alloc(st.kt, "_cc_t");
928 alloc(st.kz, "_cc_z");
929 alloc(st.kz2, "_cc_z2");
930 if (st.cw.extent(0) != (std::size_t)gm.fac.n) {
931 st.cw = Kokkos::View<double*, CCMem>(sc.name + "_cc_cw", gm.fac.n);
932 st.rw = Kokkos::View<double*, CCMem>(sc.name + "_cc_rw", gm.fac.n);
933 }
934 // internal coefficients from the stored physical values (§1.2, at advance time)
935 const double lam = sc.Dphys * u_.diffToInt();
936 const double idt = steady ? 0.0 : 1.0 / dt_;
937 const double srcFactor = u_.divToInt();
938 st.lam = lam;
939 st.idt = idt;
940 st.dt = steady ? 0.0 : dt_;
941 st.srcFactor = srcFactor;
942 st.steady = steady;
943 st.numUnknowns = gm.census.numUnknowns;
944 // mean-gradient mode (§1.5): G' = G hRef; every G term below is skipped when G = 0
945 st.meanGradOn = st.meanGrad[0] != 0.0 || st.meanGrad[1] != 0.0 || st.meanGrad[2] != 0.0;
946 for (int a = 0; a < 3; ++a) {
947 st.gPhys[a] = st.meanGrad[a];
948 st.gInt[a] = st.meanGrad[a] * u_.lenToPhys();
949 st.ubar[a] = 0.0;
950 }
952 // the field: identity-row cells hold exactly 0 (§1.3), the time base is c^n
953 sco::zeroNonUnknown(sc.c, unk, e_, G);
954 Kokkos::deep_copy(CCExec(), sc.cOld, sc.c);
955 // bands
956 sco::buildBands(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, kap, unk, CCConst(gm.sax),
957 CCConst(gm.say), CCConst(gm.saz), lam, idt, u_.w, e_, G);
958 // the per-body wall table, resolved and converted (§4.2)
959 const int nb = scalarNumBodies();
960 st.wtType.assign(nb, st.wallDefault.type);
961 st.wtK.assign(nb, st.wallDefault.coefficient);
962 st.wtG.assign(nb, st.wallDefault.value);
963 st.wtQ.assign(nb, st.wallDefault.value);
964 for (const auto& [inst, w] : st.wallInstance)
965 if (inst >= 0 && inst < nb) {
966 st.wtType[inst] = w.type;
967 st.wtK[inst] = w.coefficient;
968 st.wtG[inst] = w.value;
969 st.wtQ[inst] = w.value;
970 }
971 const double sp = u_.speedToInt();
972 for (int b = 0; b < nb; ++b) {
973 st.wtK[b] = st.wtType[b] == 2 ? st.wtK[b] * sp : 0.0;
974 st.wtQ[b] = st.wtType[b] == 0 ? st.wtQ[b] * sp : 0.0;
975 st.wtG[b] = st.wtType[b] == 0 ? 0.0 : st.wtG[b];
976 }
977 if (st.wtTypeV.extent(0) != (std::size_t)nb) { // persistent, refilled below (finding 7)
978 st.wtTypeV = Kokkos::View<int*, CCMem>("peclet::flow::sco_wt_type", nb);
979 st.wtKV = Kokkos::View<double*, CCMem>("peclet::flow::sco_wt_k", nb);
980 st.wtGV = Kokkos::View<double*, CCMem>("peclet::flow::sco_wt_g", nb);
981 st.wtQV = Kokkos::View<double*, CCMem>("peclet::flow::sco_wt_q", nb);
982 }
984 wt.nb = nb;
985 wt.type = st.wtTypeV;
986 wt.k = st.wtKV;
987 wt.gval = st.wtGV;
988 wt.q = st.wtQV;
989 {
990 auto ht = Kokkos::create_mirror_view(wt.type);
991 auto hk = Kokkos::create_mirror_view(wt.k);
992 auto hg = Kokkos::create_mirror_view(wt.gval);
993 auto hq = Kokkos::create_mirror_view(wt.q);
994 for (int b = 0; b < nb; ++b) {
995 ht(b) = st.wtType[b];
996 hk(b) = st.wtK[b];
997 hg(b) = st.wtG[b];
998 hq(b) = st.wtQ[b];
999 }
1000 Kokkos::deep_copy(wt.type, ht);
1001 Kokkos::deep_copy(wt.k, hk);
1002 Kokkos::deep_copy(wt.gval, hg);
1003 Kokkos::deep_copy(wt.q, hq);
1004 }
1005 sco::facetCoefficients(st.cw, st.rw, gm.fac, unk, wt, lam);
1006 if (st.meanGradOn) // §1.5: rw = alpha G_phi (g - sum_k w_k G.x_k) on Dirichlet/Robin facets
1007 sco::meanGradientProbeShift(st.rw, gm.fac, st.cw, mgPos);
1008 // WO-7: a conjugate body makes this a two-field solve (§1.1, D8); without one the single-phase
1009 // path below runs unchanged
1010 st.conj = false;
1011 for (int b = 0; b < nb; ++b)
1012 st.conj = st.conj || st.wtType[(std::size_t)b] == 3;
1013 st.conj = st.conj && st.solid.extent(0) == n_;
1015 if (st.conj) {
1016 std::vector<char> conjB;
1019 }
1021 // right-hand side: kappa (idt c^n + S') + the walls' rw
1022 if (st.sourceIsField && st.sourceField.extent(0) != n_)
1023 throw std::runtime_error("cut-cell scalar '" + sc.name +
1024 "': the per-cell source no longer matches this rank's block "
1025 "(redistributed?) -- call set_scalar_source again");
1026 const bool useField = st.sourceIsField;
1027 sco::buildRhs(sc.b, CCConst(sc.cOld), kap, unk, idt, st.sourceConst,
1028 CCConst(useField ? st.sourceField : sc.cOld), useField, srcFactor, e_, G);
1029 sco::overlayRhs(sc.b, gm.fac, st.rw);
1030 // Dirichlet domain faces (§1.4): fold 2 Lam w_a a_bf into the diagonal and the rhs
1031 const CCField saAx[3] = {gm.sax, gm.say, gm.saz};
1032 bool anyDir = false;
1033 for (int f = 0; f < 6; ++f) {
1034 st.dirFace[f] = false;
1035 if (sc.bc[f] != 2 || bc_[f] == 0 || !touchesGlobalFace(f))
1036 continue;
1037 const int a = f / 2, side = f % 2;
1038 int t1, t2;
1039 sco::faceTangents(a, t1, t2);
1040 const int n[3] = {nx_, ny_, nz_};
1041 const long nf = (long)n[t1] * n[t2];
1042 if (st.gFace[f].extent(0) != (std::size_t)nf) // persistent, refilled below (finding 7)
1043 st.gFace[f] = Kokkos::View<double*, CCMem>(sc.name + "_cc_gface", nf);
1044 Kokkos::View<double*, CCMem> gv = st.gFace[f];
1045 if (st.hasProfile[f] && (st.profileN[f][0] != n[t1] || st.profileN[f][1] != n[t2]))
1046 throw std::runtime_error("cut-cell scalar '" + sc.name +
1047 "': the domain-face profile no longer matches this rank's block");
1048 if (st.hasProfile[f] || st.meanGradOn) {
1049 // §1.5: in mean-gradient mode the face holds theta_bf = g - G.x_bf (x_bf the boundary face
1050 // centre; G_a = 0 on this non-periodic axis by the refusals)
1051 const int og[3] = {og_.x, og_.y, og_.z};
1052 const double xa = u_.org[a] + (double)(og[a] + (side == 0 ? 0 : n[a])) * u_.h[a];
1053 auto hv = Kokkos::create_mirror_view(gv);
1054 for (int j2 = 0; j2 < n[t2]; ++j2)
1055 for (int j1 = 0; j1 < n[t1]; ++j1) {
1056 const long k = (long)j1 + (long)j2 * n[t1];
1057 double v = st.hasProfile[f] ? st.profile[f][(std::size_t)k] : sc.bcVal[f];
1058 if (st.meanGradOn) {
1059 double p[3];
1060 p[a] = xa;
1061 p[t1] = u_.org[t1] + ((double)(og[t1] + j1) + 0.5) * u_.h[t1];
1062 p[t2] = u_.org[t2] + ((double)(og[t2] + j2) + 0.5) * u_.h[t2];
1063 v -= (st.gPhys[0] * p[0] + st.gPhys[1] * p[1]) + st.gPhys[2] * p[2];
1064 }
1065 hv(k) = v;
1066 }
1067 Kokkos::deep_copy(gv, hv);
1068 } else {
1069 Kokkos::deep_copy(gv, sc.bcVal[f]);
1070 }
1071 st.dirFace[f] = true;
1072 anyDir = true;
1073 sco::dirichletFaceFold(sc.AC, sc.b, unk, CCConst(saAx[a]), gv, lam * u_.w[a], a, side, e_, G);
1074 }
1075 if (st.meanGradOn) { // §1.5 faces: the G.x part of every coupled face term on the rhs
1076 double gf[3];
1077 for (int a = 0; a < 3; ++a)
1078 gf[a] = lam * u_.w[a] * u_.hp[a] * st.gInt[a];
1079 sco::meanGradientFaceRhs(sc.b, unk, CCConst(gm.sax), CCConst(gm.say), CCConst(gm.saz), gf, e_,
1080 G);
1081 }
1082 // advection (WO-5, §6): the face flux of the projection, the small-cell split, the implicit FOU
1083 // bands (their outflow lumps into the surrogate diagonal through AC) and the explicit faces' net
1084 // outflow at c^n on the rhs. Skipped entirely when no face carries flux (the WO-4 operator).
1085 scalarCutAdvection(sc, steady);
1086 if (st.advecting) {
1087 const CCConst px(st.phi[0]), py(st.phi[1]), pz(st.phi[2]);
1088 sco::advectionBands(sc.AC, sc.AW, sc.AE, sc.AS, sc.AN, sc.AB, sc.AT, st.outflow, px, py, pz,
1089 CCConst(st.small), unk, steady, e_, G);
1090 if (!steady) {
1091 fillGhosts(sc.cOld); // c^n's ghosts: the reconstruction reaches two cells past the face
1092 for (int a = 0; a < 3; ++a) {
1093 if (st.Phi[a].extent(0) != n_)
1094 st.Phi[a] = CCField(sc.name + "_cc_Phi", n_);
1095 sco::explicitFaceFluxes(st.Phi[a], CCConst(st.phi[a]), CCConst(sc.cOld), unk,
1096 CCConst(st.small), a, sc.scheme, false, e_, G);
1097 }
1098 sco::explicitAdvectionRhs(sc.b, CCConst(st.Phi[0]), CCConst(st.Phi[1]), CCConst(st.Phi[2]),
1099 unk, e_, G);
1100 }
1101 // WO-5b: the open domain faces' rows (§1.4): inflow F_in g on the rhs, outflow F_out c_i.
1102 // A2: a steady solve also collects their implicit outflow in omega_open (the advective
1103 // surrogate's coarse mass), clamped at 0.
1104 if (steady) {
1105 if (st.omegaOpen.extent(0) != n_)
1106 st.omegaOpen = CCField(sc.name + "_cc_omega_open", n_);
1107 Kokkos::deep_copy(CCExec(), st.omegaOpen, 0.0);
1108 }
1109 for (int f = 0; f < 6; ++f)
1110 if (st.openFace[f])
1111 sco::openFaceAdvection(sc.AC, sc.b, st.outflow, CCConst(sc.cOld), CCConst(st.phi[f / 2]),
1112 CCConst(st.small), unk, st.gFace[f], st.openInflow[f], f / 2, f % 2,
1113 steady, e_, G, st.omegaOpen, steady);
1114 if (steady)
1115 sco::clampNonNegative(st.omegaOpen, e_, G);
1116 if (st.meanGradOn) {
1117 // §1.5 advection: theta is advected; the linear part enters exactly as the source
1118 // -G'.(U'_i/V - kappa_i U'bar), U'bar = sum U'_i / sum kappa_i V (the moving frame)
1119 const double hh[3] = {0.5 * u_.hp[0], 0.5 * u_.hp[1], 0.5 * u_.hp[2]};
1120 double m[4];
1121 sco::cellVelocitySumLocal(px, py, pz, unk, hh, e_, G, m);
1122 double kc = 0.0;
1123 sco::kappaMomentsLocal(CCConst(sc.c), kap, unk, e_, G, kc, m[3]);
1124#ifdef PECLET_FLOW_MPI
1125 if (distributed_) {
1126 double o[4];
1128 for (int j = 0; j < 4; ++j)
1129 m[j] = o[j];
1130 }
1131#endif
1132 for (int a = 0; a < 3; ++a)
1133 st.ubar[a] = m[3] > 0.0 ? m[a] / m[3] : 0.0;
1134 sco::meanGradientAdvectionRhs(sc.b, px, py, pz, kap, unk, st.gInt, hh, st.ubar, e_, G);
1135 }
1136 }
1137 if (st.conj) {
1138 // WO-7: the solid phase's operator (§1.4 solid row, §4.1): psi_s^n, the face products, the
1139 // mass C_s K kappa_s idt, the 7 bands, the rhs; the per-facet interface coupling cc; the
1140 // mean-gradient terms of §1.5 on the solid faces and the conjugate facets
1141 const CCConst sunk(st.sunk), matv(st.mat);
1142 alloc(st.spx, "_cc_spx");
1143 alloc(st.spy, "_cc_spy");
1144 alloc(st.spz, "_cc_spz");
1145 alloc(st.ms, "_cc_ms");
1146 alloc(st.ASC, "_cc_asc");
1147 alloc(st.ASW, "_cc_asw");
1148 alloc(st.ASE, "_cc_ase");
1149 alloc(st.ASS, "_cc_ass");
1150 alloc(st.ASN, "_cc_asn");
1151 alloc(st.ASB, "_cc_asb");
1152 alloc(st.AST, "_cc_ast");
1153 alloc(st.bS, "_cc_bs");
1154 alloc(st.sOld, "_cc_sold");
1155 alloc(st.SACs, "_cc_sacs");
1156 alloc(st.W0, "_cc_w0");
1157 if (st.cc.extent(0) != (std::size_t)gm.fac.n)
1158 st.cc = Kokkos::View<double*, CCMem>(sc.name + "_cc_cc", gm.fac.n);
1159 sco::zeroNonUnknown(st.solid, sunk, e_, G);
1160 Kokkos::deep_copy(CCExec(), st.sOld, st.solid);
1161 sco::solidFaceProducts(st.spx, st.spy, st.spz, sunk, matv, CCConst(gm.sax), CCConst(gm.say),
1162 CCConst(gm.saz), mt, e_);
1163 sco::solidMass(st.ms, kap, sunk, matv, mt, idt, e_, G);
1164 sco::solidBands(st.ASC, st.ASW, st.ASE, st.ASS, st.ASN, st.ASB, st.AST, CCConst(st.ms), sunk,
1165 CCConst(st.spx), CCConst(st.spy), CCConst(st.spz), u_.w, e_, G);
1166 sco::solidRhs(st.bS, CCConst(st.ms), CCConst(st.sOld), e_, G);
1167 sco::conjugateCoefficients(st.cc, gm.fac, Kokkos::View<const std::uint8_t*, CCMem>(st.conjF),
1168 Kokkos::View<const double*, CCMem>(st.sS), unk, sunk, mt, lam);
1169 if (st.meanGradOn) {
1170 double gs[3];
1171 for (int a = 0; a < 3; ++a)
1172 gs[a] = u_.w[a] * u_.hp[a] * st.gInt[a];
1173 sco::solidMeanGradientFaceRhs(st.bS, sunk, CCConst(st.spx), CCConst(st.spy), CCConst(st.spz),
1174 gs, e_, G);
1175 sco::conjMeanGradientShift(sc.b, st.bS, gm.fac, st.cc, st.offS, st.wS, mgPos);
1176 }
1177 // the two-field level-0 surrogate (§4.3)
1178 sco::conjSurrogate(st.SAC, st.SACs, st.W0, CCConst(sc.AC), CCConst(st.ASC), gm.fac, st.cw,
1179 st.cc);
1180 } else {
1181 // the level-0 surrogate diagonal (§4.3)
1182 sco::surrogateDiagonal(st.SAC, CCConst(sc.AC), gm.fac, st.cw);
1183 }
1184 // the singular case (§5.1): steady, no facet with G > 0, no Dirichlet domain face anywhere
1185 double sing[2] = {sco::maxFacetLocal(st.cw), anyDir ? 1.0 : 0.0};
1186#ifdef PECLET_FLOW_MPI
1187 if (distributed_) {
1188 double gs[2];
1190 sing[0] = gs[0];
1191 sing[1] = gs[1];
1192 }
1193#endif
1194 // Review finding 2b (ruling D-WOR-2b): an open domain face carrying flux takes the problem out
1195 // of the uniform-mean case. An outflow face's column sums to its F_out, not 0, so 1 is not a
1196 // left null vector and the uniform-mean projection and gauge below are not the compatibility
1197 // conditions; they are never applied there. (An inflow face needs a scalar Dirichlet value, so
1198 // that case was never singular.) With open faces but no inflow face, continuity forces backflow
1199 // and the operator stays singular (A 1 = 0 up to the projection's divergence) with a NON-uniform
1200 // left null vector: the solve converges for a compatible right-hand side (no source, insulating
1201 // walls) and otherwise reports non-convergence; the backflow warning above names the cause.
1202 // `bc_` is global and `st.advecting` a global MAX, so every rank agrees. Without flux an open
1203 // face is a plain Neumann/Dirichlet scalar face, and the diffusion-only rule stands.
1204 bool anyOpenFlux = false;
1205 for (int f = 0; f < 6; ++f)
1206 anyOpenFlux = anyOpenFlux || bc_[f] == 2 || bc_[f] == 3;
1207 anyOpenFlux = anyOpenFlux && st.advecting;
1208 st.singular = steady && !(sing[0] > 0.0) && !(sing[1] > 0.0) && !anyOpenFlux;
1209 st.built = true;
1210 // ScalarMG (§5.2): the level table once per geometry version and block (VelocityMG's rule), the
1211 // surrogate on every level rebuilt with the operator (ruling D-WO3-3)
1212 if (!st.mg || st.mgVersion != gm.version || st.mgN != n_) {
1213 st.mg = std::make_shared<ScalarMG>();
1214 st.mg->setMetric(u_.w, u_.hp);
1215 const bool per[3] = {bc_[0] == 0, bc_[2] == 0, bc_[4] == 0};
1216 st.mg->setPeriodic(per);
1217 ScalarMG::Fill fill0 = [this](CCField f) { fillGhosts(f); };
1218#ifdef PECLET_FLOW_MPI
1219 if (distributed_)
1220 st.mg->initMpi(*dec_, comm_, og_, fill0);
1221 else
1222#endif
1223 {
1224 st.mg->init(nx_, ny_, nz_, fill0);
1225 st.mg->setLevel0Wrap(true); // single rank: fillGhosts = the periodic wrap of x, y, z
1226 }
1227 st.mgVersion = gm.version;
1228 st.mgN = n_;
1229 }
1230 {
1232 in.lam = lam;
1233 in.idt = idt;
1234 // the level rule: transient with kappa_A = 1 + 4 dt' D' sum_a w_a < ScalarMG::kFullTableKappa
1235 // -> level 0 alone (WO-7: max(D', D_s') over the conjugate materials, D_s = Lam_s/(C_s K))
1236 const double kA = 1.0 + 4.0 * st.dt * (st.conj ? std::fmax(lam, st.maxSolidD) : lam) *
1237 ((u_.w[0] + u_.w[1]) + u_.w[2]);
1238 in.fullTable = steady || !(kA < ScalarMG::kFullTableKappa);
1239 in.singular = st.singular;
1240 in.SAC = st.SAC;
1241 in.kappa = kap;
1242 in.unknown = unk;
1243 in.sax = CCConst(gm.sax);
1244 in.say = CCConst(gm.say);
1245 in.saz = CCConst(gm.saz);
1246 in.fac = &gm.fac;
1247 in.facetW = st.cw;
1248 for (int f = 0; f < 6; ++f)
1249 in.dirFace[f] = st.dirFace[f];
1250 if (steady && st.advecting) {
1251 // A2 (§6.7): the advective surrogate — the operator's bands (SAC the diagonal), the face
1252 // flux for the coarse advection, omega_open on the coarse mass. `st.advecting` is an
1253 // all-rank MAX, so every rank takes this path.
1254 in.advective = true;
1255 in.AW = sc.AW;
1256 in.AE = sc.AE;
1257 in.AS = sc.AS;
1258 in.AN = sc.AN;
1259 in.AB = sc.AB;
1260 in.AT = sc.AT;
1261 for (int a = 0; a < 3; ++a)
1262 in.phi[a] = CCConst(st.phi[a]);
1263 in.omegaOpen = CCConst(st.omegaOpen);
1264 } else if (st.advecting) { // the lumped implicit outflow onto the coarse levels (§5.2)
1265 in.outflow = CCConst(st.outflow);
1266 in.hasOutflow = true;
1267 }
1268 if (st.conj) { // WO-7: the coupled two-field levels (§5.2)
1269 in.twoPhase = true;
1270 in.SACs = st.SACs;
1271 in.W0 = st.W0;
1272 in.sunk = CCConst(st.sunk);
1273 in.spx = CCConst(st.spx);
1274 in.spy = CCConst(st.spy);
1275 in.spz = CCConst(st.spz);
1276 in.ms0 = CCConst(st.ms);
1277 in.facetC = st.cc;
1278 }
1279 st.mg->build(in);
1280 st.mgLevels = st.mg->levelsUsed();
1281 // Review finding 4 (ruling D-WOR-4): the full table's bottom is a fixed number of smoothing
1282 // sweeps on whatever level the table stops at -- there is no agglomerated exact coarse solve
1283 // (CutcellMG's 'auto' bottom). An axis that cannot halve (odd, 2-poor, or an odd MPI block or
1284 // origin) stops the table on a large grid, and the iterations then grow with it. Warned once
1285 // per scalar, against the pressure's own bottom extent (set_pressure_bottom_extent).
1286 if (in.fullTable && !st.warnedBottom) {
1287 const auto& lc = st.mg->level(st.mg->levelsUsed() - 1);
1288 const int be = pressureBottomExtent();
1289 if (lc.gdim.x > be || lc.gdim.y > be || lc.gdim.z > be) {
1290 st.warnedBottom = true;
1291 if (scalarRootRank())
1292 std::fprintf(stderr,
1293 "peclet.flow: cut-cell scalar '%s': the ScalarMG level table stops at %d x "
1294 "%d x %d cells (%d levels), larger than the pressure's bottom extent %d on "
1295 "some axis -- an axis that cannot halve (odd or 2-poor size, or an odd MPI "
1296 "block). Its bottom is smoothing only, not an exact solve, so the iteration "
1297 "count grows with the grid (78 iterations at n = 77 against 6 at n = 80). "
1298 "Size the grid and the blocks with factors of two (printed once)\n",
1299 sc.name.c_str(), lc.gdim.x, lc.gdim.y, lc.gdim.z, st.mg->levelsUsed(), be);
1300 }
1301 }
1302 }
1303
1304 // ---- reductions (+ MPI) ----
1305 auto allSum = [&](double* v, int k) {
1306#ifdef PECLET_FLOW_MPI
1307 if (distributed_) {
1308 std::vector<double> o((std::size_t)k);
1309 MPI_Allreduce(v, o.data(), k, MPI_DOUBLE, MPI_SUM, comm_);
1310 for (int j = 0; j < k; ++j)
1311 v[j] = o[(std::size_t)j];
1312 }
1313#endif
1314 (void)v;
1315 (void)k;
1316 };
1317 auto allMax = [&](double v) {
1318#ifdef PECLET_FLOW_MPI
1319 if (distributed_) {
1320 double o = 0.0;
1321 MPI_Allreduce(&v, &o, 1, MPI_DOUBLE, MPI_MAX, comm_);
1322 return o;
1323 }
1324#endif
1325 return v;
1326 };
1327 const double nUnk = (double)st.numUnknowns;
1328 auto removeMean = [&](CCField a) {
1329 double s[2];
1330 sco::sumUnknownLocal(CCConst(a), unk, e_, G, s[0], s[1]);
1331 allSum(s, 1);
1332 if (nUnk > 0.0)
1333 sco::addOnUnknown(a, unk, -s[0] / nUnk, e_, G);
1334 };
1336 if (!st.conj) {
1337 // the right-hand side the Krylov sees: b, or (singular) its projection onto the range
1338 CCField bK = sc.b;
1339 st.incompatibility = 0.0;
1340 if (st.singular) {
1341 alloc(st.kb, "_cc_bproj");
1342 alloc(st.kq, "_cc_q");
1343 Kokkos::deep_copy(CCExec(), st.kb, sc.b);
1344 double s[2];
1345 sco::sumUnknownLocal(CCConst(st.kb), unk, e_, G, s[0], s[1]);
1346 allSum(s, 2);
1347 st.incompatibility = s[1] > 0.0 ? std::fabs(s[0]) / s[1] : 0.0;
1348 if (nUnk > 0.0)
1349 sco::addOnUnknown(st.kb, unk, -s[0] / nUnk, e_, G);
1350 bK = st.kb;
1351 }
1352 // gauge of the singular solve: sum kappa V c is kept at its pre-solve value (§5.1)
1353 // (in mean-gradient mode the target is 0: sum kappa V theta = 0, §5.1)
1354 double kc0 = 0.0, k0 = 0.0;
1355 if (st.singular) {
1356 double m[2];
1357 sco::kappaMomentsLocal(CCConst(sc.c), kap, unk, e_, G, m[0], m[1]);
1358 allSum(m, 2);
1359 kc0 = st.meanGradOn ? 0.0 : m[0];
1360 k0 = m[1];
1361 }
1362
1364 ops.e = e_;
1365 ops.g = G;
1366 ops.matvec = [&](const ScalarVec& y, const ScalarVec& x) { scalarCutMatvec(sc, y.f, x.f); };
1367 // One ScalarMG V-cycle on the surrogate (§5.2; level 0 alone under the transient level rule).
1368 ops.precond = [&](const ScalarVec& z, const ScalarVec& r) {
1369 CCField rr = r.f;
1370 if (st.singular) {
1371 Kokkos::deep_copy(CCExec(), st.kq, r.f);
1372 removeMean(st.kq);
1373 rr = st.kq;
1374 }
1375 st.mg->apply(z.f, rr);
1376 if (st.singular)
1377 removeMean(z.f);
1378 };
1379 ops.dot = [&](const ScalarVec& a, const ScalarVec& b) {
1380 double s = sco::dotLocal(CCConst(a.f), CCConst(b.f), e_, G);
1381 allSum(&s, 1);
1382 return s;
1383 };
1384 ops.dot2 = [&](const ScalarVec& a, const ScalarVec& b, const ScalarVec& c, double& ab,
1385 double& cc) {
1386 double s[2];
1387 sco::dot2Local(CCConst(a.f), CCConst(b.f), CCConst(c.f), e_, G, s[0], s[1]);
1388 allSum(s, 2);
1389 ab = s[0];
1390 cc = s[1];
1391 };
1392 ops.maxabs = [&](const ScalarVec& a) { return allMax(sco::maxabsLocal(CCConst(a.f), e_, G)); };
1393 ops.removeMean = [&](const ScalarVec& a) { removeMean(a.f); };
1394 if (!distributed_) { // WO-9b: the device-resident pass (scalar_krylov.hpp), single rank
1395 residentKrylov(st, ops);
1396 ops.dotTo = [&](const ScalarVec& a, const ScalarVec& b, skr::Slot s) {
1397 sco::dotReduce(CCConst(a.f), CCConst(b.f), e_, G, s);
1398 };
1399 ops.dot2To = [&](const ScalarVec& a, const ScalarVec& b, const ScalarVec& c, skr::Slot s1,
1400 skr::Slot s2) {
1401 sco::dot2Reduce(CCConst(a.f), CCConst(b.f), CCConst(c.f), e_, G, s1, s2);
1402 };
1403 ops.maxabsTo = [&](const ScalarVec& a, skr::Slot sf, skr::Slot) {
1404 sco::maxabsReduce(CCConst(a.f), e_, G, Kokkos::Max<double, CCMem>(sf));
1405 };
1406 }
1407 auto vec = [](CCField f) {
1408 ScalarVec v;
1409 v.f = f;
1410 return v;
1411 };
1412 kr = scalarBiCGStab(ops, vec(bK), vec(sc.c), vec(st.kr), vec(st.krh), vec(st.kp), vec(st.kv),
1413 vec(st.kt), vec(st.kz), vec(st.kz2), st.maxit, st.rtol, st.singular);
1414 if (st.singular && k0 > 0.0) {
1415 double m[2];
1416 sco::kappaMomentsLocal(CCConst(sc.c), kap, unk, e_, G, m[0], m[1]);
1417 allSum(m, 2);
1418 sco::addOnUnknown(sc.c, unk, (kc0 - m[0]) / k0, e_, G);
1419 // Review finding 5 (ruling D-WOR-5): the gauge shift delta adds delta A 1 = delta div to the
1420 // residual, so the reported residual and flag are re-measured after it -- the Krylov's own
1421 // true-residual definition (the projected rhs, mean removed), its reference and its 10 rtol.
1422 if (kr.ref > 0.0) {
1423 const ScalarVec t = vec(st.kt);
1424 ops.matvec(t, vec(sc.c));
1425 skr::residualFrom(t, vec(bK), e_, G);
1426 ops.removeMean(t);
1427 kr.trueRes = ops.maxabs(t);
1428 kr.converged = kr.trueRes <= 10.0 * (st.rtol * kr.ref);
1429 }
1430 }
1431
1432 } else {
1433 // WO-7: the two-field solve (§5.1 on both phases): the same BiCGStab, ScalarVec {c, psi_s};
1434 // dots and maxima over both fields; the singular projection and gauge over both phases'
1435 // unknowns (the null vector is constant in psi), the gauge weights kappa and C_s K kappa_s
1436 const CCConst sunk(st.sunk), matv(st.mat);
1437 alloc(st.krS, "_cc_rs");
1438 alloc(st.krhS, "_cc_rhs");
1439 alloc(st.kpS, "_cc_ps");
1440 alloc(st.kvS, "_cc_vs");
1441 alloc(st.ktS, "_cc_ts");
1442 alloc(st.kzS, "_cc_zs");
1443 alloc(st.kz2S, "_cc_z2s");
1444 const double nTot = (double)st.numUnknowns + (double)st.numSolidUnknowns;
1445 auto removeMeanTwo = [&](CCField a, CCField as) {
1446 double sf[2], ss[2];
1447 sco::sumUnknownLocal(CCConst(a), unk, e_, G, sf[0], sf[1]);
1448 sco::sumUnknownLocal(CCConst(as), sunk, e_, G, ss[0], ss[1]);
1449 double v[1] = {sf[0] + ss[0]};
1450 allSum(v, 1);
1451 if (nTot > 0.0) {
1452 sco::addOnUnknown(a, unk, -v[0] / nTot, e_, G);
1453 sco::addOnUnknown(as, sunk, -v[0] / nTot, e_, G);
1454 }
1455 };
1456 CCField bKf = sc.b, bKs = st.bS;
1457 st.incompatibility = 0.0;
1458 if (st.singular) {
1459 alloc(st.kb, "_cc_bproj");
1460 alloc(st.kq, "_cc_q");
1461 alloc(st.kbS, "_cc_bprojs");
1462 alloc(st.kqS, "_cc_qs");
1463 Kokkos::deep_copy(CCExec(), st.kb, sc.b);
1464 Kokkos::deep_copy(CCExec(), st.kbS, st.bS);
1465 double sf[2], ss[2];
1466 sco::sumUnknownLocal(CCConst(st.kb), unk, e_, G, sf[0], sf[1]);
1467 sco::sumUnknownLocal(CCConst(st.kbS), sunk, e_, G, ss[0], ss[1]);
1468 double v[2] = {sf[0] + ss[0], sf[1] + ss[1]};
1469 allSum(v, 2);
1470 st.incompatibility = v[1] > 0.0 ? std::fabs(v[0]) / v[1] : 0.0;
1471 if (nTot > 0.0) {
1472 sco::addOnUnknown(st.kb, unk, -v[0] / nTot, e_, G);
1473 sco::addOnUnknown(st.kbS, sunk, -v[0] / nTot, e_, G);
1474 }
1475 bKf = st.kb;
1476 bKs = st.kbS;
1477 }
1478 // gauge (§5.1): sum kappa V c + sum C_s K kappa_s V psi_s kept (0 in mean-gradient mode)
1479 auto moments = [&](double m[2]) {
1480 double a[2], b2[2];
1481 sco::kappaMomentsLocal(CCConst(sc.c), kap, unk, e_, G, a[0], a[1]);
1482 sco::solidMomentsLocal(CCConst(st.solid), kap, sunk, matv, mt, e_, G, b2[0], b2[1]);
1483 m[0] = a[0] + b2[0];
1484 m[1] = a[1] + b2[1];
1485 allSum(m, 2);
1486 };
1487 double kc0 = 0.0, k0 = 0.0;
1488 if (st.singular) {
1489 double m[2];
1490 moments(m);
1491 kc0 = st.meanGradOn ? 0.0 : m[0];
1492 k0 = m[1];
1493 }
1495 ops.e = e_;
1496 ops.g = G;
1497 ops.matvec = [&](const ScalarVec& y, const ScalarVec& x) {
1498 scalarCutMatvecTwo(sc, y.f, y.s, x.f, x.s);
1499 };
1500 ops.precond = [&](const ScalarVec& z, const ScalarVec& r) {
1501 CCField rf = r.f, rs = r.s;
1502 if (st.singular) {
1503 Kokkos::deep_copy(CCExec(), st.kq, r.f);
1504 Kokkos::deep_copy(CCExec(), st.kqS, r.s);
1505 removeMeanTwo(st.kq, st.kqS);
1506 rf = st.kq;
1507 rs = st.kqS;
1508 }
1509 st.mg->applyTwo(z.f, z.s, rf, rs);
1510 if (st.singular)
1511 removeMeanTwo(z.f, z.s);
1512 };
1513 ops.dot = [&](const ScalarVec& a, const ScalarVec& b) {
1514 double v = sco::dotTwoLocal(CCConst(a.f), CCConst(b.f), CCConst(a.s), CCConst(b.s), e_, G);
1515 allSum(&v, 1);
1516 return v;
1517 };
1518 ops.dot2 = [&](const ScalarVec& a, const ScalarVec& b, const ScalarVec& c, double& ab,
1519 double& cc) {
1520 double v[2];
1521 sco::dot2TwoLocal(CCConst(a.f), CCConst(b.f), CCConst(c.f), CCConst(a.s), CCConst(b.s),
1522 CCConst(c.s), e_, G, v[0], v[1]);
1523 allSum(v, 2);
1524 ab = v[0];
1525 cc = v[1];
1526 };
1527 ops.maxabs = [&](const ScalarVec& a) {
1528 return allMax(
1529 std::fmax(sco::maxabsLocal(CCConst(a.f), e_, G), sco::maxabsLocal(CCConst(a.s), e_, G)));
1530 };
1531 ops.removeMean = [&](const ScalarVec& a) { removeMeanTwo(a.f, a.s); };
1532 if (!distributed_) { // WO-9b: the device-resident pass (scalar_krylov.hpp), single rank
1533 residentKrylov(st, ops);
1534 ops.dotTo = [&](const ScalarVec& a, const ScalarVec& b, skr::Slot s) {
1535 sco::dotTwoReduce(CCConst(a.f), CCConst(b.f), CCConst(a.s), CCConst(b.s), e_, G, s);
1536 };
1537 ops.dot2To = [&](const ScalarVec& a, const ScalarVec& b, const ScalarVec& c, skr::Slot s1,
1538 skr::Slot s2) {
1539 sco::dot2TwoReduce(CCConst(a.f), CCConst(b.f), CCConst(c.f), CCConst(a.s), CCConst(b.s),
1540 CCConst(c.s), e_, G, s1, s2);
1541 };
1542 ops.maxabsTo = [&](const ScalarVec& a, skr::Slot sf, skr::Slot ss) {
1543 sco::maxabsReduce(CCConst(a.f), e_, G, Kokkos::Max<double, CCMem>(sf));
1544 sco::maxabsReduce(CCConst(a.s), e_, G, Kokkos::Max<double, CCMem>(ss));
1545 };
1546 }
1547 auto vec2 = [](CCField f, CCField s2) {
1548 ScalarVec v;
1549 v.f = f;
1550 v.s = s2;
1551 v.solid = true;
1552 return v;
1553 };
1554 kr = scalarBiCGStab(ops, vec2(bKf, bKs), vec2(sc.c, st.solid), vec2(st.kr, st.krS),
1555 vec2(st.krh, st.krhS), vec2(st.kp, st.kpS), vec2(st.kv, st.kvS),
1556 vec2(st.kt, st.ktS), vec2(st.kz, st.kzS), vec2(st.kz2, st.kz2S), st.maxit,
1557 st.rtol, st.singular);
1558 if (st.singular && k0 > 0.0) {
1559 double m[2];
1560 moments(m);
1561 const double d = (kc0 - m[0]) / k0;
1562 sco::addOnUnknown(sc.c, unk, d, e_, G);
1563 sco::addOnUnknown(st.solid, sunk, d, e_, G);
1564 if (kr.ref > 0.0) { // review finding 5: the residual after the gauge shift, as above
1565 const ScalarVec t = vec2(st.kt, st.ktS);
1566 ops.matvec(t, vec2(sc.c, st.solid));
1567 skr::residualFrom(t, vec2(bKf, bKs), e_, G);
1568 ops.removeMean(t);
1569 kr.trueRes = ops.maxabs(t);
1570 kr.converged = kr.trueRes <= 10.0 * (st.rtol * kr.ref);
1571 }
1572 }
1573 fillGhosts(st.solid);
1574 }
1575 fillGhosts(sc.c);
1576 st.iterations = kr.iterations;
1577 st.residual = kr.ref > 0.0 ? kr.trueRes / kr.ref : 0.0;
1578 st.converged = kr.converged;
1579 if (!kr.converged && !st.warnedNoConv) {
1580 st.warnedNoConv = true;
1581 bool root = true;
1582#ifdef PECLET_FLOW_MPI
1583 if (distributed_) {
1584 int r = 0;
1585 MPI_Comm_rank(comm_, &r);
1586 root = r == 0;
1587 }
1588#endif
1589 if (root)
1590 std::fprintf(stderr,
1591 "peclet.flow: cut-cell scalar '%s': the solve did not converge in %d "
1592 "iterations (residual %.3e of the reference, tolerance %.1e); the iterate is "
1593 "kept -- see diagnostics.scalar_census (printed once)\n",
1594 sc.name.c_str(), kr.iterations, st.residual, st.rtol);
1595 }
1596}
1597
1598template <class Grid>
1600#ifdef PECLET_FLOW_MPI
1601 if (distributed_) {
1602 int r = 0;
1603 MPI_Comm_rank(comm_, &r);
1604 return r == 0;
1605 }
1606#endif
1607 return true;
1608}
1609
1610template <class Grid>
1612 ScalarCutState& st = *sc.cut;
1613 if (st.warnedGeometryVersion == scg_.version)
1614 return;
1615 st.warnedGeometryVersion = scg_.version;
1616 bool root = true;
1617#ifdef PECLET_FLOW_MPI
1618 if (distributed_) {
1619 int r = 0;
1620 MPI_Comm_rank(comm_, &r);
1621 root = r == 0;
1622 }
1623#endif
1624 if (!root)
1625 return;
1626 const scg::ScalarCutCensus& c = scg_.census;
1627 const char* nm = sc.name.c_str();
1628 if (c.numThinSolid > 0)
1629 std::fprintf(
1630 stderr,
1631 "peclet.flow: cut-cell scalar '%s': %ld thin-solid cell(s) -- a solid thinner than "
1632 "about a cell, so one fluid unknown spans both of its sides and the scalar passes "
1633 "through it (a resolution warning; diagnostics.scalar_census 'num_thin_solid', "
1634 "printed once)\n",
1635 nm, c.numThinSolid);
1636 const auto r2Warn = [&](const char* side, long r2, long total) {
1637 if (total > 0 && 100 * r2 > total)
1638 std::fprintf(stderr,
1639 "peclet.flow: cut-cell scalar '%s': %ld of %ld %s probes (%.2f %%, more than "
1640 "1 %%) fell back to the first-order last rung R2 -- contacts, thin gaps or "
1641 "corners under-resolved (diagnostics.scalar_census 'probe_rungs', printed "
1642 "once)\n",
1643 nm, r2, total, side, 100.0 * (double)r2 / (double)total);
1644 };
1645 r2Warn("fluid", c.rungs[3], c.rungs[0] + c.rungs[1] + c.rungs[2] + c.rungs[3]);
1646 if (st.conj)
1647 r2Warn("solid", st.solidRungs[3],
1648 st.solidRungs[0] + st.solidRungs[1] + st.solidRungs[2] + st.solidRungs[3]);
1649 if (c.numSealed > 0 && c.sealedVolume > 1e-6 * c.fluidVolume)
1650 std::fprintf(stderr,
1651 "peclet.flow: cut-cell scalar '%s': %ld sealed cell(s) (fluid behind closed "
1652 "faces, not unknowns) hold %.2e of the fluid volume, more than 1e-6 "
1653 "(diagnostics.scalar_census 'num_sealed' / 'sealed_volume', printed once)\n",
1654 nm, c.numSealed, c.fluidVolume > 0.0 ? c.sealedVolume / c.fluidVolume : 0.0);
1655}
1656
1657template <class Grid>
1658std::vector<double> Solver<Grid>::scalarWallFlux(const std::string& name) {
1659 ScalarField& sc = cutcellScalar(name, "scalar_wall_flux");
1660 ScalarCutState& st = *sc.cut;
1661 const int nb = scalarNumBodies();
1662 std::vector<double> out((std::size_t)nb, 0.0);
1663 if (!st.built)
1664 return out;
1665 const auto& fo = scg_.fac;
1666 Kokkos::View<double*, CCMem> up("peclet::flow::sco_up", fo.n), qin("peclet::flow::sco_qin", fo.n);
1667 fillGhosts(sc.c);
1668 if (st.conj) { // WO-7: a conjugate body's flux into the fluid is its interface flux cc (u_ps -
1669 // u_pf)
1670 Kokkos::View<double*, CCMem> us("peclet::flow::sco_us", fo.n);
1671 fillGhosts(st.solid);
1672 sco::conjFacetFlux(up, us, qin, CCConst(sc.c), CCConst(st.solid), fo, st.cw, st.rw, st.cc,
1673 st.offS, st.wS, scalarMeanGradPosition(st.gPhys), st.meanGradOn);
1674 } else {
1675 sco::facetFlux(up, qin, CCConst(sc.c), fo, st.cw, st.rw);
1676 }
1677 auto hq = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), qin);
1678 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.body);
1679 for (long f = 0; f < fo.n; ++f) { // facet order: deterministic
1680 int b = hb(f);
1681 if (b < 0 || b >= nb)
1682 b = 0;
1683 out[(std::size_t)b] += u_.vol * hq(f);
1684 }
1685#ifdef PECLET_FLOW_MPI
1686 if (distributed_) {
1687 std::vector<double> o(out.size());
1688 MPI_Allreduce(out.data(), o.data(), nb, MPI_DOUBLE, MPI_SUM, comm_);
1689 out = o;
1690 }
1691#endif
1692 const double toPhys = u_.volToPhys() * u_.divToPhys();
1693 for (auto& v : out)
1694 v *= toPhys;
1695 return out;
1696}
1697
1698template <class Grid>
1700 ScalarField& sc = cutcellScalar(name, "scalar_budget");
1701 ScalarCutState& st = *sc.cut;
1702 ScalarCutBudget out;
1703 if (!st.built)
1704 return out;
1705 const scg::ScalarCutGeometry& gm = scg_;
1706 const CCConst unk(gm.unknown), kap(gm.kappa);
1707 const double V = u_.vol;
1708 // the residual of the stored system at the stored solution, r = b - A c, in flux form
1709 // (sco::residualFluxForm: the same r in exact arithmetic, round-off at the scale of the fluxes)
1710 fillGhosts(sc.c);
1711 sco::residualFluxForm(st.kt, CCConst(sc.c), CCConst(sc.b), kap, unk, CCConst(gm.sax),
1712 CCConst(gm.say), CCConst(gm.saz), st.lam, st.idt, u_.w, e_, G);
1713 if (st.conj) { // WO-7: the solid rows (flux form) and the two-field overlay, both subtracted
1714 fillGhosts(st.solid);
1715 sco::solidResidualFluxForm(st.ktS, CCConst(st.solid), CCConst(st.bS), CCConst(st.ms),
1716 CCConst(st.sunk), CCConst(st.spx), CCConst(st.spy), CCConst(st.spz),
1717 u_.w, e_, G);
1718 sco::conjOverlayApply(st.kt, st.ktS, CCConst(sc.c), CCConst(st.solid), gm.fac, st.cw, st.cc,
1719 st.offS, st.wS, /*subtract=*/true);
1720 } else {
1721 sco::overlayApply(st.kt, CCConst(sc.c), gm.fac, st.cw, /*subtract=*/true);
1722 }
1723 if (st.advecting) // the implicit faces' outflow (the explicit faces' sits in b)
1724 sco::advectionResidual(st.kt, CCConst(sc.c), CCConst(st.phi[0]), CCConst(st.phi[1]),
1725 CCConst(st.phi[2]), CCConst(st.small), unk, st.steady, e_, G);
1726 if (st.advecting) // WO-5b: the open faces' implicit outflow
1727 for (int f = 0; f < 6; ++f)
1728 if (st.openFace[f])
1729 sco::openFaceResidual(st.kt, CCConst(sc.c), CCConst(st.phi[f / 2]), CCConst(st.small), unk,
1730 st.openInflow[f], f / 2, f % 2, st.steady, e_, G);
1731 {
1732 const CCField saAx[3] = {gm.sax, gm.say, gm.saz};
1733 for (int f = 0; f < 6; ++f)
1734 if (st.dirFace[f])
1735 sco::dirichletFaceResidual(st.kt, CCConst(sc.c), unk, CCConst(saAx[f / 2]),
1736 st.lam * u_.w[f / 2], f / 2, f % 2, e_, G);
1737 }
1738 double v[6] = {0, 0, 0, 0, 0, 0}; // sum r, kappa c, kappa c^n, wall, boundary, source
1739 {
1740 double sumAbs = 0.0, kc = 0.0, k = 0.0;
1741 sco::sumUnknownLocal(CCConst(st.kt), unk, e_, G, v[0], sumAbs); // identity rows: r = 0
1742 sco::kappaMomentsLocal(CCConst(sc.c), kap, unk, e_, G, kc, k);
1743 v[1] = kc;
1744 sco::kappaMomentsLocal(CCConst(sc.cOld), kap, unk, e_, G, kc, k);
1745 v[2] = kc;
1746 }
1747 {
1748 const auto& fo = gm.fac;
1749 Kokkos::View<double*, CCMem> up("peclet::flow::sco_up", fo.n),
1750 qin("peclet::flow::sco_qin", fo.n);
1751 sco::facetFlux(up, qin, CCConst(sc.c), fo, st.cw, st.rw);
1752 auto hq = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), qin);
1753 double w = 0.0;
1754 for (long f = 0; f < fo.n; ++f)
1755 w += hq(f);
1756 v[3] = w;
1757 }
1758 {
1759 const CCField saAx[3] = {gm.sax, gm.say, gm.saz};
1760 double bsum = 0.0;
1761 for (int f = 0; f < 6; ++f)
1762 if (st.dirFace[f])
1763 bsum += sco::dirichletFaceInflux(CCConst(sc.c), unk, CCConst(saAx[f / 2]), st.gFace[f],
1764 st.lam * u_.w[f / 2], 1.0, f / 2, f % 2, e_, G);
1765 if (st.advecting) // WO-5b: the advective flux through the open faces
1766 for (int f = 0; f < 6; ++f)
1767 if (st.openFace[f])
1768 bsum += sco::openFaceInflux(CCConst(sc.c), CCConst(sc.cOld), CCConst(st.phi[f / 2]),
1769 CCConst(st.small), unk, st.gFace[f], st.openInflow[f], 1.0,
1770 f / 2, f % 2, st.steady, e_, G);
1771 v[4] = bsum;
1772 }
1773 {
1774 const bool useField = st.sourceField.extent(0) == n_;
1775 v[5] = sco::sourceSumLocal(kap, unk, st.sourceConst, CCConst(useField ? st.sourceField : sc.c),
1776 useField, st.srcFactor, e_, G);
1777 }
1778#ifdef PECLET_FLOW_MPI
1779 if (distributed_) {
1780 double o[6];
1782 for (int j = 0; j < 6; ++j)
1783 v[j] = o[j];
1784 }
1785#endif
1786 // internal: per unit V sums times V
1787 double sumR = V * v[0];
1788 const double mass = V * v[1], massOld = V * v[2], wall = V * v[3], bnd = V * v[4], src = V * v[5];
1789 // WO-7: the solid rows' residual and the solid mass sum C_s K kappa_s V psi_s (now and at the
1790 // time base). The conjugate facets' flux is internal (it enters both rows), so `wall_in` keeps
1791 // the wall facets alone and `d_mass` is the change of the TOTAL (fluid + solid) mass.
1792 double massS = 0.0, massSOld = 0.0;
1793 if (st.conj) {
1795 std::vector<char> conjB;
1797 double vs[3] = {0.0, 0.0, 0.0}, sAbs = 0.0, w = 0.0;
1798 sco::sumUnknownLocal(CCConst(st.ktS), CCConst(st.sunk), e_, G, vs[0], sAbs);
1799 sco::solidMomentsLocal(CCConst(st.solid), kap, CCConst(st.sunk), CCConst(st.mat), mt, e_, G,
1800 vs[1], w);
1801 sco::solidMomentsLocal(CCConst(st.sOld), kap, CCConst(st.sunk), CCConst(st.mat), mt, e_, G,
1802 vs[2], w);
1803#ifdef PECLET_FLOW_MPI
1804 if (distributed_) {
1805 double o[3];
1807 for (int j = 0; j < 3; ++j)
1808 vs[j] = o[j];
1809 }
1810#endif
1811 sumR = V * (v[0] + vs[0]);
1812 massS = V * vs[1];
1813 massSOld = V * vs[2];
1814 }
1815 const double toMass = u_.volToPhys(), toRate = u_.volToPhys() * u_.divToPhys();
1816 out.mass = mass * toMass;
1817 out.massSolid = massS * toMass;
1818 out.wallIn = wall * toRate;
1819 out.boundaryIn = bnd * toRate;
1820 out.sourceIn = src * toRate;
1821 if (st.steady) { // a rate balance: no storage term, the defect is sum V r per unit time
1822 out.dMass = 0.0;
1823 out.defect = sumR * toRate;
1824 out.identityError = (-(wall + bnd + src) + sumR) * toRate;
1825 } else {
1826 const double dm = st.conj ? (mass + massS) - (massOld + massSOld) : mass - massOld;
1827 out.dMass = dm * toMass;
1828 out.defect = st.dt * sumR * toMass;
1829 out.identityError = (dm - st.dt * (wall + bnd + src) + st.dt * sumR) * toMass;
1830 }
1831 return out;
1832}
1833
1834template <class Grid>
1836 ScalarField& sc = cutcellScalar(name, "scalar_facets");
1837 ScalarCutState& st = *sc.cut;
1839 const auto& fo = scg_.fac;
1840 ScalarCutFacets out;
1841 const long n = fo.n;
1842 out.centroid.assign((std::size_t)n * 3, 0.0);
1843 out.normal.assign((std::size_t)n * 3, 0.0);
1844 out.area.assign((std::size_t)n, 0.0);
1845 out.wallValue.assign((std::size_t)n, std::numeric_limits<double>::quiet_NaN());
1846 out.flux.assign((std::size_t)n, 0.0);
1847 out.instance.assign((std::size_t)n, 0);
1848 out.rung.assign((std::size_t)n, 0);
1849 auto hcell = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.cell);
1850 auto halpha = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.alpha);
1851 auto hn = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.normal);
1852 auto hc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.centroid);
1853 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.body);
1854 auto hs = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.sF);
1855 auto hr = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.rungF);
1856 auto hu = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), scg_.unknown);
1857 Kokkos::View<double*, CCMem> up("peclet::flow::sco_up", n), qin("peclet::flow::sco_qin", n);
1858 Kokkos::View<double*, CCMem> usv("peclet::flow::sco_us", n);
1859 const bool have = st.built && st.cw.extent(0) == (std::size_t)n;
1860 const bool conj = have && st.conj && st.cc.extent(0) == (std::size_t)n;
1861 if (have) {
1862 fillGhosts(sc.c);
1863 if (conj) {
1864 fillGhosts(st.solid);
1865 sco::conjFacetFlux(up, usv, qin, CCConst(sc.c), CCConst(st.solid), fo, st.cw, st.rw, st.cc,
1866 st.offS, st.wS, scalarMeanGradPosition(st.gPhys), st.meanGradOn);
1867 } else {
1868 sco::facetFlux(up, qin, CCConst(sc.c), fo, st.cw, st.rw);
1869 }
1870 }
1871 auto hup = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), up);
1872 auto hq = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), qin);
1873 // mean-gradient mode (§1.5): the probe value of c is that of theta plus sum_k w_k G.x_k
1874 const bool mgOn = have && st.meanGradOn;
1876 auto hoff = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.offF);
1877 auto hw = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fo.wF);
1878 const int og[3] = {og_.x, og_.y, og_.z};
1879 const double l = u_.lenToPhys();
1880 const int nb = scalarNumBodies();
1881 for (long f = 0; f < n; ++f) {
1882 const long i = hcell(f);
1883 const long xyz[3] = {i % e_.x, (i / e_.x) % e_.y, i / ((long)e_.x * e_.y)};
1884 for (int d = 0; d < 3; ++d) {
1885 out.centroid[(std::size_t)(3 * f + d)] =
1886 u_.org[d] + ((double)(xyz[d] - G + og[d]) + 0.5) * u_.h[d] + hc(f, d) * l;
1887 out.normal[(std::size_t)(3 * f + d)] = hn(f, d);
1888 }
1889 out.area[(std::size_t)f] = halpha(f) * u_.vol * u_.areaToPhys();
1890 out.instance[(std::size_t)f] = hb(f);
1891 out.rung[(std::size_t)f] = (int)hr(f);
1892 if (!have || !(hu(i) > 0.5))
1893 continue;
1894 int b = hb(f);
1895 if (b < 0 || b >= nb)
1896 b = 0;
1897 double u = hup(f);
1898 if (mgOn)
1899 for (int k = 0; k < 8; ++k)
1900 u += hw(f, k) * mgPos(i + (long)hoff(f, k));
1901 const double a = halpha(f), s = hs(f), lam = st.lam;
1902 const int t = st.wtType[(std::size_t)b];
1903 double cG;
1904 if (t == 3) { // WO-7, conjugate: psi_Gamma,f = u_pf + q s_f / Lam_f, q = flux into the fluid
1905 const double q = a > 0.0 ? hq(f) / a : 0.0;
1906 cG = lam > 0.0 ? u + q * s / lam : u;
1907 } else if (t == 1)
1908 cG = st.wtG[(std::size_t)b];
1909 else if (t == 2) {
1910 const double k = st.wtK[(std::size_t)b];
1911 cG = (lam + k * s) > 0.0 ? (lam * u + k * s * st.wtG[(std::size_t)b]) / (lam + k * s) : u;
1912 } else
1913 cG = lam > 0.0 ? u + st.wtQ[(std::size_t)b] * s / lam : u;
1914 out.wallValue[(std::size_t)f] = cG;
1915 out.flux[(std::size_t)f] = a > 0.0 ? hq(f) / a / u_.speedToInt() : 0.0;
1916 }
1917 return out;
1918}
1919
1920} // namespace peclet::flow
1921
1922#endif // PECLET_FLOW_FLOW_IBM_SCALARS_CUTCELL_HPP
std::function< void(CCField)> Fill
static constexpr double kFullTableKappa
The transient level rule (design §5.2, Amendment A3 + ruling D-WO9-3): kappa_A = 1 + 4 dt' D' sum_a w...
int pressureBottomExtent() const
void setScalarSource(const std::string &name, double S)
void scalarCutMatvecTwo(ScalarField &sc, CCField yf, CCField ys, CCField xf, CCField xs)
std::vector< double > getScalarSolid(const std::string &name)
void setScalarWall(const std::string &name, int type, double value, double coefficient, int instance)
std::vector< double > scalarGeometryField(int which)
void scatterInner(CCField fld, const std::vector< double > &in)
void setScalarSourceField(const std::string &name, const std::vector< double > &S)
std::array< double, 3 > scalarMeanFlux(const std::string &name)
bool hasMovingInstance() const
True when at least one instance carries a nonzero velocity – i.e.
void fillGhosts(CCField f)
sco::MeanGradPosition scalarMeanGradPosition(const double Gphys[3]) const
std::vector< double > scalarSolidGeometryField(const std::string &name, int which)
CCField addField(const std::string &name)
void scalarCutRefusals(const ScalarField &sc) const
void scalarCutMatvec(ScalarField &sc, CCField y, CCField x)
ScalarField & cutcellScalar(const std::string &name, const char *who)
ScalarCutFacets scalarFacets(const std::string &name)
void scalarCutAssembleSolve(ScalarField &sc, bool steady)
void advanceScalarCutCell(ScalarField &sc)
std::array< long, 3 > globalCells() const
The GLOBAL cell counts (this rank's block single-rank).
void setScalarBcProfile(const std::string &name, int face, const std::vector< double > &prof, int n1, int n2)
void scalarCutAdvection(ScalarField &sc, bool steady)
const scg::ScalarCutGeometry & scalarCutGeometry()
ScalarFaceFlux scalarFaceFlux(int a) const
ScalarCutBudget scalarBudget(const std::string &name)
std::vector< double > scalarWallFlux(const std::string &name)
void scalarCutWarnings(ScalarField &sc)
void ensureScalarSolidRecord(ScalarField &sc, const std::vector< char > &conj)
bool scalarRootRank() const
rank 0 (or not distributed): the one that prints a warning
void setScalarMeanGradient(const std::string &name, const std::array< double, 3 > &G)
void setScalarSolid(const std::string &name, double D, double capacity, double partition, double contactResistance, int instance)
scg::ScalarCutCensus scalarCutCensus()
static constexpr int G
Definition flow_ibm.hpp:80
SceneMap sceneMap() const
void solveScalarSteady(const std::string &name)
void setScalarTolerance(const std::string &name, double rtol)
void setScalarMaxIterations(const std::string &name, int maxit)
sco::MaterialTable scalarMaterialTable(ScalarField &sc, std::vector< char > &conj)
std::vector< double > gatherInner(CCField fld)
bool isCutcellScalar(const std::string &name) const
Definition conj.py:1
void buildFacets(ScalarFacetOverlay &fo, const CellPass &cp, CCConst sdf, CCConst sax, CCConst say, CCConst saz, C3 e, int g, const double hp[3])
Kernel 3 (§2.5): exclusive scans of the facet counts and of the cut-cell indicator in x-fastest inner...
void buildFluidProbes(ScalarFacetOverlay &fo, CCConst sdf, CCConst unknown, C3 e, const double hp[3])
Kernel 5 (§2.5, §3.3): the fluid probe ladder of every facet.
void zeroGhostSlab(CCField f, C3 e, int g, int face)
Zero f on every ghost layer beyond face face (0..5 = -x,+x,-y,+y,-z,+z), full transverse extent: the ...
void buildCells(CCField kappa, CCField unknown, const CellPass &cp, CCConst sdf, CCConst sax, CCConst say, CCConst saz, C3 e, int g, const double hp[3])
Kernel 2 (§2.5): kappa, the unknown flag and the per-cell facet count/kind on the inner cells.
void buildApertures(CCField sax, CCField say, CCField saz, CCConst sdf, C3 e, int g)
Kernel 1 (§2.5): the snapped scalar apertures of every face of every inner cell, including the high-s...
ScalarCutCensus localCensus(const ScalarFacetOverlay &fo, const CellPass &cp, CCConst kappa, CCConst unknown, CCConst sax, CCConst say, CCConst saz, C3 e, int g, double vol)
The census of the record on this rank (not yet summed over ranks); sealedVolume in internal volume un...
double dotTwoLocal(CCConst af, CCConst bf, CCConst as, CCConst bs, C3 e, int g)
void conjugateCoefficients(Kokkos::View< double *, CCMem > cc, const scg::ScalarFacetOverlay &fo, Kokkos::View< const std::uint8_t *, CCMem > conjF, Kokkos::View< const double *, CCMem > sS, CCConst unk, CCConst sunk, const MaterialTable &mt, double lamF)
Per facet (§1.4, §4.1): cc = alpha G_c, G_c = 1/(s_f/Lam_f + R_c + s_s/Lam_s), on a facet of a conjug...
void explicitFaceFluxes(CCField Phi, CCConst phi, CCConst c, CCConst unk, CCConst small, int a, int scheme, bool steady, C3 e, int g)
The explicit face fluxes of §6.2 on the low a-faces of the cells g .
void faceFluxes(CCField phi, CCConst vel, CCConst open, CCConst unk, int a, C3 e)
phi = open * vel on every face whose two cells are inside the block (0 on the block's lowest layer),...
void openFaceResidual(CCField r, CCConst c, CCConst phi, CCConst small, CCConst unk, bool inflow, int a, int side, bool steady, C3 e, int g)
r(i) -= F_out c_i on the implicit outflow faces of (a, side) (the budget's flux-form residual; the in...
void meanFluxLocal(CCConst th, CCConst kappa, CCConst unk, CCConst sax, CCConst say, CCConst saz, CCConst phx, CCConst phy, CCConst phz, bool advecting, double lam, const double gi[3], const double hp[3], const double ub[3], C3 e, int g, double diff[3], double adv[3])
scalar_mean_flux (§8.1), this rank's sums (internal; the caller divides by the global cell count): di...
void cellVelocitySumLocal(CCConst phx, CCConst phy, CCConst phz, CCConst unk, const double hh[3], C3 e, int g, double s[3])
(sum U'_x/V, sum U'_y/V, sum U'_z/V) over the unknown inner cells (the moving frame's numerator; its ...
void zeroNonUnknown(CCField c, CCConst unk, C3 e, int g)
Zero c on every inner cell that is not a fluid unknown (§1.3: re-zeroed at the start of every advance...
double openFaceInflux(CCConst c, CCConst cOld, CCConst phi, CCConst small, CCConst unk, Kokkos::View< const double *, CCMem > gv, bool inflow, double vol, int a, int side, bool steady, C3 e, int g)
Sum over one open face of V times the advective flux INTO the domain through it, per internal time (b...
void conjOverlayApply(CCField yf, CCField ys, CCConst xf, CCConst xs, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, Kokkos::View< const double *, CCMem > cc, Kokkos::View< int *[8], Kokkos::LayoutLeft, CCMem > offS, Kokkos::View< double *[8], Kokkos::LayoutLeft, CCMem > wS, bool subtract=false)
The two-field overlay (§4.1 step 3), one thread per cut cell, facets in CSR order: y_f(i) += sum cw u...
void dot2TwoReduce(CCConst af, CCConst bf, CCConst cf, CCConst as, CCConst bs, CCConst cs, C3 e, int g, R1 &&r1, R2 &&r2)
(sum a b, sum c c) over inner cells, both fields, in one pass, into (r1, r2).
double sourceSumLocal(CCConst kappa, CCConst unk, double srcConst, CCConst src, bool useField, double srcFactor, C3 e, int g)
sum over unknowns of kappa S' (the source part of the right-hand side, per unit V).
void solidFaceProducts(CCField spx, CCField spy, CCField spz, CCConst sunk, CCConst mat, CCConst sax, CCConst say, CCConst saz, const MaterialTable &mt, C3 e)
The level-0 solid face products P_a(i) = Lam_face a_s on the LOW a-face of cell i when cells i - s_a ...
long countImplicitLocal(CCConst phi, CCConst small, CCConst unk, int a, bool steady, C3 e, int g)
Faces carrying flux that are implicit, over the low faces of the inner cells (census num_implicit_fac...
void dot2Reduce(CCConst a, CCConst b, CCConst c, C3 e, int g, R1 &&r1, R2 &&r2)
(sum a b, sum c c) over inner cells in one pass, into (r1, r2).
void meanGradientFaceRhs(CCField b, CCConst unk, CCConst sax, CCConst say, CCConst saz, const double gf[3], C3 e, int g)
Faces (§1.5): b(i) += sum_a gf_a (a_a^+ - a_a^-) over the coupled faces (the bands' guard),...
void dot2Local(CCConst a, CCConst b, CCConst c, C3 e, int g, double &ab, double &cc)
void solidBands(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst ms, CCConst sunk, CCConst spx, CCConst spy, CCConst spz, const double w[3], C3 e, int g)
The solid phase's 7 bands (§4.1): AC = m_s + the six face terms w_a P, off-diagonals -w_a P; identity...
void openFaceAdvection(CCField AC, CCField b, CCField outflow, CCConst cOld, CCConst phi, CCConst small, CCConst unk, Kokkos::View< const double *, CCMem > gv, bool inflow, int a, int side, bool steady, C3 e, int g, CCField omega=CCField(), bool wOmega=false)
The open face's row terms on the unknown inner boundary cells of (a, side) (table above): inflow b +=...
void maxabsReduce(CCConst a, C3 e, int g, Red red)
max over inner cells of |a|, through the Max reducer red (Kokkos::Max over a host double or a device ...
void conjFacetFlux(Kokkos::View< double *, CCMem > up, Kokkos::View< double *, CCMem > us, Kokkos::View< double *, CCMem > qin, CCConst xf, CCConst xs, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, Kokkos::View< const double *, CCMem > rw, Kokkos::View< const double *, CCMem > cc, Kokkos::View< int *[8], Kokkos::LayoutLeft, CCMem > offS, Kokkos::View< double *[8], Kokkos::LayoutLeft, CCMem > wS, const MeanGradPosition &pos, bool mgOn)
Per facet: the probe values and the flux INTO the fluid per unit cell volume per internal time,...
long countSmallLocal(CCConst small, C3 e, int g)
Count of small inner cells (census num_small_cells).
double maxCellPecletLocal(CCConst phx, CCConst phy, CCConst phz, CCConst unk, double lam, const double w[3], C3 e, int g)
A2 census max_cell_peclet on this rank: the max over the interior flux faces (the low faces of the in...
void sumUnknownLocal(CCConst a, CCConst unk, C3 e, int g, double &sum, double &sumAbs)
(sum a, sum |a|) over the inner cells that are unknowns.
void solidRhs(CCField bs, CCConst ms, CCConst sOld, C3 e, int g)
The solid right-hand side b_s = m_s psi_s^n (no solid source in v1, §13 Q17); 0 elsewhere.
void openFaceFluxes(CCField phi, Kokkos::View< const double *, CCMem > plane, CCConst unk, int a, int side, C3 e, int g)
phi(bf) = plane value on the boundary face of (a, side), over the full transverse extent,...
double dotLocal(CCConst a, CCConst b, C3 e, int g)
double bulkCourantLocal(CCConst phx, CCConst phy, CCConst phz, CCConst kappa, CCConst unk, CCConst sax, CCConst say, CCConst saz, double dt, C3 e, int g)
C_bulk of §6.3 on this rank: max over the inner FULL cells (an unknown with kappa = 1 and all six sna...
void solidMomentsLocal(CCConst xs, CCConst kappa, CCConst sunk, CCConst mat, const MaterialTable &mt, C3 e, int g, double &m, double &w)
(sum C_s K kappa_s psi_s, sum C_s K kappa_s) over the solid unknowns (times V by the caller): the sol...
double dirichletFaceInflux(CCConst c, CCConst unk, CCConst saA, Kokkos::View< const double *, CCMem > gv, double lamW, double vol, int a, int side, C3 e, int g)
Sum over one Dirichlet domain face of V t (g_bf - c_i): the flux INTO the domain through it,...
void solidResidualFluxForm(CCField r, CCConst x, CCConst b, CCConst ms, CCConst sunk, CCConst spx, CCConst spy, CCConst spz, const double w[3], C3 e, int g)
The solid residual in FLUX form (the budget, §9): r = b - m_s psi - sum_faces t (psi_i - psi_nb); the...
void solidMeanGradientFaceRhs(CCField bs, CCConst sunk, CCConst spx, CCConst spy, CCConst spz, const double gs[3], C3 e, int g)
Mean-gradient mode, solid faces (§1.5): b_s(i) += sum_a gs_a (P_a(i + s_a) - P_a(i)),...
void dot2TwoLocal(CCConst af, CCConst bf, CCConst cf, CCConst as, CCConst bs, CCConst cs, C3 e, int g, double &ab, double &cc)
void clampNonNegative(CCField omega, C3 e, int g)
omega = max(omega, 0) on the inner cells (A2: implicit backflow never enters the coarse mass).
void buildSolidProbes(Kokkos::View< double *, CCMem > sS, Kokkos::View< int *[8], Kokkos::LayoutLeft, CCMem > offS, Kokkos::View< double *[8], Kokkos::LayoutLeft, CCMem > wS, Kokkos::View< std::uint8_t *, CCMem > rungS, Kokkos::View< const std::uint8_t *, CCMem > conjF, const scg::ScalarFacetOverlay &fo, CCConst sdf, CCConst sunk, C3 e, const double hp[3])
The solid probe ladder (§3.4): per facet of a conjugate body (conjF), the ladder of §3....
void solidMass(CCField ms, CCConst kappa, CCConst sunk, CCConst mat, const MaterialTable &mt, double idt, C3 e, int g)
The solid mass m_s = C_s K kappa_s idt on the solid unknowns, 0 elsewhere (inner cells).
void facetFlux(Kokkos::View< double *, CCMem > up, Kokkos::View< double *, CCMem > qin, CCConst x, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, Kokkos::View< const double *, CCMem > rw)
Per facet: the probe value u_p of x and the wall flux INTO the fluid per unit cell volume per interna...
void overlayApply(CCField y, CCConst x, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, bool subtract=false)
y(i) += sum over the facets of cut cell i of cw * (probe interpolation of x): the overlay pass of the...
double maxAbsFluxLocal(CCConst phx, CCConst phy, CCConst phz, C3 e, int g)
max |phi| over the low faces of the inner cells (0: no advection this step).
void solidMeanFluxLocal(CCConst th, CCConst sunk, CCConst spx, CCConst spy, CCConst spz, const double gi[3], const double hp[3], C3 e, int g, double diff[3])
The solid part of scalar_mean_flux's diffusive sum (§8.1): over the low faces of the inner cells whos...
void faceFluxCountsLocal(CCConst phi, CCConst vel, CCConst open, int a, C3 e, int g, long &nFlux, long &nGuarded)
(faces carrying flux, faces whose nonzero open * vel the guard zeroed) over the low faces of the inne...
void dotTwoReduce(CCConst af, CCConst bf, CCConst as, CCConst bs, C3 e, int g, R &&result)
sum over inner cells of af bf + as bs (the two-field dot).
void conjSurrogate(CCField SACf, CCField SACs, CCField W0, CCConst ACf, CCConst ACs, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, Kokkos::View< const double *, CCMem > cc)
The two-field level-0 surrogate (§4.3): SAC_f = AC_f + sum (cw + cc), SAC_s = AC_s + sum cc,...
void residualFluxForm(CCField r, CCConst c, CCConst b, CCConst kappa, CCConst unk, CCConst sax, CCConst say, CCConst saz, double lam, double idt, const double w[3], C3 e, int g)
The budget's residual r = b - A c in FLUX form (§9): r = b - kappa idt c - sum_faces t (c_i - c_nb) w...
void smallCells(CCField small, CCConst phx, CCConst phy, CCConst phz, CCConst kappa, CCConst unk, double dt, double thr, C3 e, int g)
The small-cell flags of §6.3 over the inner cells: small(i) = 1 iff i is an unknown,...
void meanGradientAdvectionRhs(CCField b, CCConst phx, CCConst phy, CCConst phz, CCConst kappa, CCConst unk, const double gi[3], const double hh[3], const double ub[3], C3 e, int g)
Advection (§1.5): b(i) -= G'.
double maxabsLocal(CCConst a, C3 e, int g)
void buildSolidMaterial(CCField mat, CCField sunk, CCConst kappa, CCConst sax, CCConst say, CCConst saz, const scg::ScalarFacetOverlay &fo, Kokkos::View< const int *, CCMem > owner, bool hasOwner, const MaterialTable &mt, C3 e, int g, double vol[2])
The material per cell and the solid-unknown flags on the inner cells (§1.3, §2.7).
void advectionBands(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCField outflow, CCConst phx, CCConst phy, CCConst phz, CCConst small, CCConst unk, bool steady, C3 e, int g)
The implicit FOU part of the operator (§1.4, §6.3; all faces when steady, §6.7): on every unknown inn...
void buildBands(CCField AC, CCField AW, CCField AE, CCField AS, CCField AN, CCField AB, CCField AT, CCConst kappa, CCConst unk, CCConst sax, CCConst say, CCConst saz, double lam, double idt, const double w[3], C3 e, int g)
The 7 bands (§4.1): AC = kappa idt + sum of the six face terms Lam w_a a (guarded toward a cell that ...
void dirichletFaceFold(CCField AC, CCField b, CCConst unk, CCConst saA, Kokkos::View< const double *, CCMem > gv, double lamW, int a, int side, C3 e, int g)
Dirichlet domain face (§1.4 B_i): for the inner boundary cell i of face (a, side),...
int bodyIndex(double m, int nb)
A body id clamped into the table (out of range -> 0, as facetCoefficients does).
void explicitAdvectionRhs(CCField b, CCConst Px, CCConst Py, CCConst Pz, CCConst unk, C3 e, int g)
b(i) -= the net explicit outflow sum_a (Phi_a(i + s_a) - Phi_a(i)) on the unknown inner cells.
void kappaMomentsLocal(CCConst c, CCConst kappa, CCConst unk, C3 e, int g, double &kc, double &k)
(sum kappa c, sum kappa) over the unknowns (times V by the caller).
void openFaceCountsLocal(CCConst phi, CCConst small, CCConst unk, bool inflow, int a, int side, bool steady, C3 e, int g, long &nFlux, long &nImpl, long &nBack)
The census counts of one open face over this rank's inner boundary cells: (faces carrying flux on the...
void dirichletFaceResidual(CCField r, CCConst c, CCConst unk, CCConst saA, double lamW, int a, int side, C3 e, int g)
r(i) -= 2 Lam w_a a_bf c_i on the inner boundary cells of a Dirichlet domain face (the LHS part of di...
void facetCoefficients(Kokkos::View< double *, CCMem > cw, Kokkos::View< double *, CCMem > rw, const scg::ScalarFacetOverlay &fo, CCConst unk, const WallTable &wt, double lam)
Per-facet overlay coefficients (§4.1): cw = alpha G, rw = alpha G g (Dirichlet/Robin) or cw = 0,...
double maxFacetLocal(Kokkos::View< const double *, CCMem > cw)
max over facets of cw (0 when there is no Dirichlet/Robin facet: the steady singular test).
void faceTangents(int a, int &t1, int &t2)
The tangential axes of face axis a, in x, y, z order (t1 < t2): the layout of a domain-face profile (...
void surrogateDiagonal(CCField SAC, CCConst AC, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw)
The level-0 surrogate diagonal (§4.3): SAC = AC + sum of the cut cell's cw (the probe weights lumped,...
void dotReduce(CCConst a, CCConst b, C3 e, int g, R &&result)
sum over inner cells of a(i) b(i), into result.
void buildRhs(CCField b, CCConst cOld, CCConst kappa, CCConst unk, double idt, double srcConst, CCConst src, bool useField, double srcFactor, C3 e, int g)
Base right-hand side: b = kappa (idt c^n + S'), identity rows 0.
void advectionResidual(CCField r, CCConst c, CCConst phx, CCConst phy, CCConst phz, CCConst small, CCConst unk, bool steady, C3 e, int g)
r(i) -= the implicit faces' net outflow sum F_out c_up at c (the budget's flux-form residual,...
void meanGradientProbeShift(Kokkos::View< double *, CCMem > rw, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cw, const MeanGradPosition &pos)
Dirichlet/Robin probes (§1.5): rw(f) -= cw(f) sum_k w_k G.x_k, so that rw = alpha G_phi (g - sum_k w_...
void addOnUnknown(CCField a, CCConst unk, double d, C3 e, int g)
a(i) += d on the inner cells that are unknowns.
void overlayRhs(CCField b, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > rw)
b(i) += sum over the facets of cut cell i of rw (the wall part of the right-hand side).
void conjMeanGradientShift(CCField bf, CCField bs, const scg::ScalarFacetOverlay &fo, Kokkos::View< const double *, CCMem > cc, Kokkos::View< int *[8], Kokkos::LayoutLeft, CCMem > offS, Kokkos::View< double *[8], Kokkos::LayoutLeft, CCMem > wS, const MeanGradPosition &pos)
Mean-gradient mode, conjugate facets (§1.5): the fluid row carries + cc (u_pf - u_ps) with u = theta ...
void residualFrom(CCField y, CCConst b, C3 e, int g)
y = b - y over inner cells (the residual from a matvec result held in y).
Kokkos::View< double, CCMem > Slot
ScalarKrylovResult scalarBiCGStab(Ops &ops, const ScalarVec &b, const ScalarVec &x, const ScalarVec &r, const ScalarVec &rh, const ScalarVec &p, const ScalarVec &v, const ScalarVec &t, const ScalarVec &z, const ScalarVec &z2, int maxit, double rtol, bool singular)
BiCGStab on A x = b.
void ccReduce3(const char *name, C3 lo, C3 hi, F f, R &&... reducers)
void residentKrylov(State &st, ScalarKrylovOps &ops)
Arm ops for the resident pass with the persistent slots of st (ks, pk, allocated once).
void applyCutcellOp(CCField y, CCConst x, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
Kokkos::View< const double *, CCMem > CCConst
Definition prof.py:1
The per-cut-cell-scalar state (ScalarField::cut).
std::shared_ptr< ScalarCutState > cut
The injected pieces as callables (host-side calls that launch kernels; the overhead of the indirectio...
Outcome of one scalarBiCGStab call.
int iterations
BiCGStab iterations (both passes when it restarted)
double trueRes
max|b - A x| at exit (after the restart, if any)
double ref
max(max|b|, max|A x0|)
bool converged
trueRes <= 10 rtol ref
Everything build reads from the scalar operator and the geometry record.
A Krylov vector (§5.1): the fluid field and, for a conjugate scalar, the solid field (WO-7).
Wall condition per body (§1.1, §8.1 set_scalar_wall), in the caller's PHYSICAL units; the conversion ...
diagnostics.scalar_budget (§8.2), physical; collective under MPI.
diagnostics.scalar_facets (§8.2): this rank's facets, physical; per facet 3 values for the centroid a...
The ONE predicate for the cut-cell scalar's advective face flux (doc/scalar_ibm_design....
The map between the solver's INDEX coordinates and the coordinate system the analytic scene is writte...
Definition flow_ibm.hpp:241
double w[3]
w_a = 1 / h_a'^2 (<= 1)
Definition flow_ibm.hpp:179
double org[3]
physical lower corner of the GLOBAL inner grid
Definition flow_ibm.hpp:139
double h[3]
cell size per axis = extent / cells
Definition flow_ibm.hpp:138
double hp[3]
h_a' = h_a / hRef (>= 1, exactly 1 on the finest axis)
Definition flow_ibm.hpp:178
double vol
V' = h_x' h_y' h_z' (cell volume in hRef^3)
Definition flow_ibm.hpp:180
Per-cell flags of the record pass, on the INNER grid (x fastest).
The geometry part of diagnostics.scalar_census (§8.2).
The whole record: per-cell fields on the extended block (kappa and unknown haloed; the apertures comp...
long version
the Solver's geometry version this record was built for
The per-body material of a conjugate scalar, internal units (§4.2): conj 1 on a conjugate body; lam =...
G.x at the centre of extended-block cell j (physical; cellCentres' formula, unwrapped).
Device copy of the per-body wall table, resolved (instance override or default) and converted to inte...
static constexpr int G