flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_diagnostics.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_DIAGNOSTICS_HPP
9#define PECLET_FLOW_FLOW_IBM_DIAGNOSTICS_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
16 if (!hasOutflow_)
17 return ob;
19 const C3 e = e_;
20 const int dims[3] = {e.x, e.y, e.z};
21 const long st[3] = {1, e.x, (long)e.x * e.y};
22 // Phase 2 C2 (doc/anisotropic_metric.md §4.2, trap 10): `energyInflux` mixes three velocity
23 // COMPONENTS, which on anisotropic cells carry three different velocity scales, so it cannot
24 // be converted after the fact -- each component is taken to physical units inside the reduce
25 // (k_a = velToPhys(a)) and the density with it. `maxReverse` is scaled there too, i.e. before
26 // the max over face planes whose axes have different velocity scales. Phase 1 left this
27 // diagnostic in index units and the Python binding converts nothing, so in cell units every
28 // factor here is exactly 1.0 and the reported numbers do not move.
29 const double rho = u_.rhoRef * rho_;
30 for (int a = 0; a < 3; ++a)
31 for (int s = 0; s < 2; ++s) {
32 if (bc_[2 * a + s] != 3 || !touchesGlobalFace(2 * a + s))
33 continue;
34 const int b = (a + 1) % 3, c = (a + 2) % 3;
35 const double ka = u_.velToPhys(a), kb = u_.velToPhys(b), kc = u_.velToPhys(c);
36 const long sa = st[a], sb = st[b], sc = st[c];
37 const int bf = (s == 0) ? G : (dims[a] - G); // the boundary normal-velocity plane
38 const int bic = (s == 0) ? G : (dims[a] - G - 1); // the outlet-adjacent inner cell
39 const double sgn = (s == 0) ? 1.0 : -1.0; // u.n < 0 <-> sgn*u > 0
40 CCConst un = Grid::collocated ? CCConst(a == 0 ? uf_ : a == 1 ? vf_ : wf_) : CCConst(C[a].u);
41 CCConst ub = CCConst(C[b].u), uc = CCConst(C[c].u);
42 using MD = MDRange2<CCExec>;
43 double mx = 0.0, en = 0.0;
44 long nrev = 0;
45 Kokkos::parallel_reduce(
46 "peclet::flow::backflow_census", MD(space, {G, G}, {dims[b] - G, dims[c] - G}),
47 KOKKOS_LAMBDA(int p0, int p1, double& lmx, double& len, long& lnr) {
48 const long base = (long)p0 * sb + (long)p1 * sc;
49 const double back = sgn * un(base + (long)bf * sa);
50 if (back > 0.0) {
51 const long ic = base + (long)bic * sa;
52 const double tb = kb * ub(ic), tc = kc * uc(ic);
53 const double bp = ka * back;
54 lmx = Kokkos::fmax(lmx, bp);
55 len += rho * bp * 0.5 * (bp * bp + tb * tb + tc * tc);
56 lnr += 1;
57 }
58 },
59 Kokkos::Max<double>(mx), Kokkos::Sum<double>(en), Kokkos::Sum<long>(nrev));
60 ob.maxReverse = std::max(ob.maxReverse, mx);
61 ob.energyInflux += en;
62 ob.reversed += nrev;
63 ob.total += (long)(dims[b] - 2 * G) * (dims[c] - 2 * G);
64 }
65#ifdef PECLET_FLOW_MPI
66 if (distributed_) {
67 double d[2] = {ob.energyInflux, 0.0}, dg[2];
68 MPI_Allreduce(&ob.maxReverse, &d[1], 1, MPI_DOUBLE, MPI_MAX, comm_);
69 MPI_Allreduce(&d[0], &dg[0], 1, MPI_DOUBLE, MPI_SUM, comm_);
70 long l[2] = {ob.reversed, ob.total}, lg[2];
72 ob.maxReverse = d[1];
73 ob.energyInflux = dg[0];
74 ob.reversed = lg[0];
75 ob.total = lg[1];
76 }
77#endif
78 ob.fraction = ob.total > 0 ? (double)ob.reversed / (double)ob.total : 0.0;
79 return ob;
80}
81
82template <class Grid>
83std::vector<double> Solver<Grid>::getVelocity(int c) {
84 std::vector<double> out = gatherInner(C[c].u);
85 const double k = u_.velToPhys(c); // exactly 1.0 in cell units
86 if (k != 1.0)
87 for (double& x : out)
88 x *= k;
89 return out;
90}
91
92template <class Grid>
93void Solver<Grid>::setVelocity(int c, const std::vector<double>& v) {
94 const double k = u_.velToInt(c);
95 if (k != 1.0) {
96 std::vector<double> vi(v.size());
97 for (std::size_t i = 0; i < v.size(); ++i)
98 vi[i] = v[i] * k;
99 scatterInner(C[c].u, vi);
100 } else {
101 scatterInner(C[c].u, v);
102 }
103 maskVelocity(c);
104 seedFaceFieldFromCells(); // ISSUES sweep item 5 (collocated only; a no-op staggered)
105}
106
107template <class Grid>
108std::vector<double> Solver<Grid>::getFaceVelocity(int c) {
109 std::vector<double> out;
110 if constexpr (Grid::collocated) {
111 CCField fa[3] = {uf_, vf_, wf_};
112 out = gatherInner(fa[c]);
113 } else {
114 out = gatherInner(C[c].u);
115 }
116 const double k = u_.velToPhys(c); // exactly 1.0 in cell units
117 if (k != 1.0)
118 for (double& x : out)
119 x *= k;
120 return out;
121}
122
123template <class Grid>
124std::vector<double> Solver<Grid>::getOpenness(int c) {
125 CCField o[3] = {ox_, oy_, oz_};
126 return gatherInner(o[c]);
127}
128
129template <class Grid>
130std::vector<double> Solver<Grid>::getMomentumDiagonal(int c) {
131 auto h = Kokkos::create_mirror_view(C[c].AC);
132 Kokkos::deep_copy(h, C[c].AC);
133 std::vector<double> out((std::size_t)nx_ * ny_ * nz_);
134 for (int z = 0; z < nz_; ++z)
135 for (int y = 0; y < ny_; ++y)
136 for (int x = 0; x < nx_; ++x)
137 out[(std::size_t)x + (std::size_t)y * nx_ + (std::size_t)z * (std::size_t)nx_ * ny_] =
138 (double)h((long)(x + G) + (long)(y + G) * e_.x + (long)(z + G) * (long)e_.x * e_.y);
139 return out;
140}
141
142template <class Grid>
143std::vector<double> Solver<Grid>::getOpennessProj(int c) {
144 const bool gp = ghostProjection_ && oxb_.extent(0) > 0;
145 CCField o[3] = {gp ? oxb_ : ox_, gp ? oyb_ : oy_, gp ? ozb_ : oz_};
146 return gatherInner(o[c]);
147}
148
149template <class Grid>
150std::vector<double> Solver<Grid>::getPressure() {
151 // Incremental scheme: P_ accumulates the physical pressure. Classical Chorin (!incremental_):
152 // derive it on demand from the last projection potential, p = (rho/dt)*phi (CUDA
153 // press_from_phi_k).
154 std::vector<double> out = gatherInner(incremental_ ? P_ : phi_);
155 // Internal pressure -> the caller's units; the Chorin branch first derives p' = (rho'/dt')*phi.
156 const double ct = (incremental_ ? 1.0 : rho_ / dt_) * u_.pToPhys();
157 if (ct != 1.0)
158 for (double& x : out)
159 x *= ct;
160 return out;
161}
162
163template <class Grid>
167
168template <class Grid>
170 if (!cutcellPressure_)
171 return 0.0;
172 if constexpr (Grid::collocated)
173 return maxOpenDivergenceInternal(); // the collocated branch already measures the face
174 // field
175 // `doOutflow = false` keeps the mass-conserving outflow face the projection wrote -- that is the
176 // whole point of this sibling, and distributed it is `fillVelGhostsTo` that has to defend the
177 // plane against the halo's periodic wrap (SCALING_ISSUES #3/#8). Before that, this diagnostic
178 // returned ~|U_inlet| under MPI on every bed, converged or not.
179 for (int c = 0; c < 3; ++c)
180 fillVelGhostsTo(C[c].u, c, 0, false);
181 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
182 CCConst(oz_), div_, e_, G);
184 double m = reduceMaxAbsInner(CCConst(div_));
185#ifdef PECLET_FLOW_MPI
186 if (distributed_) {
187 double g = 0;
189 return g;
190 }
191#endif
192 return m;
193}
194
195template <class Grid>
199
200template <class Grid>
202 if (!cutcellPressure_)
203 return 0.0;
204 if constexpr (Grid::collocated) {
205 // Report the residual of the PROJECTED face field uf_ (made divergence-free by project(),
206 // ghosts filled). Re-averaging the central-difference-corrected CELL field would instead show
207 // the inherent O(h^2) approximate-projection cell divergence -- a property of the scheme, not
208 // the solver residual. At an outflow, re-impose the zero-gradient face (matching the
209 // staggered diagnostic, whose fillVelGhosts overwrites the mass-conserving outflow
210 // correction): the operator zeroes the alpha-divergence, but the raw beta-divergence at the
211 // open-boundary corner is otherwise spurious.
212 // ...through `openFaceView`, the field `buildOpenFaceField` already prepared for exactly
213 // this -- the advection reads the same one. This diagnostic used to apply the re-imposition
214 // to `uf_` IN PLACE, which is live state between steps (the next predictor's advecting
215 // velocity, and the flux the colour / scalar transport ride), so reading the diagnostic
216 // would have changed the next step's answer for a caller who only asked to look.
217 CCConst du = openFaceView(0), dv = openFaceView(1), dw = openFaceView(2);
218 if (ghostProjection_ && gpNRows_ >= 0) {
219 // Ghost mode: the closed point divergence of the projected face field (same kernel pair
220 // as the RHS) — the mode's true residual.
221 divergOpen(du, dv, dw, CCConst(oxb_), CCConst(oyb_), CCConst(ozb_), div_, e_, G);
222 gpDivergDelta(div_, du, dv, dw, gpOv_, gpNRows_, C3{nx_, ny_, nz_}, e_, G, distributed_);
223 } else
224 divergOpen(du, dv, dw, CCConst(ox_), CCConst(oy_), CCConst(oz_), div_, e_, G);
225 } else {
226 for (int c = 0; c < 3; ++c)
227 fillVelGhosts(c, 0); // ghosts incl. outflow zero-gradient before the divergence
228 if (ghostProjection_ && gpNRows_ >= 0) {
229 // Ghost mode: the closed point divergence (same kernels as the RHS) IS the true residual
230 // of the mode. (EXPLICIT sliver faces read the corrected stored value here vs u* in the
231 // RHS — the only, and rare, departure from the exact identity.)
232 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(oxb_), CCConst(oyb_),
233 CCConst(ozb_), div_, e_, G);
234 gpDivergDelta(div_, CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), gpOv_, gpNRows_,
235 C3{nx_, ny_, nz_}, e_, G, distributed_);
236 } else
237 divergOpen(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
238 CCConst(oz_), div_, e_, G);
239 }
240 // The diagnostic must measure the residual of the constraint the projection actually
241 // solved, so it carries the same wall-flux source (rung 3). Without this the moving case
242 // would report a "divergence error" that is really the wall flux the solve balances.
244 double m = reduceMaxAbsInner(CCConst(div_));
245#ifdef PECLET_FLOW_MPI
246 if (distributed_) {
247 double g = 0;
249 return g;
250 }
251#endif
252 return m;
253}
254
255template <class Grid>
257 if (!porous_ || !cutcellPressure_)
258 return 0.0;
259 for (int c = 0; c < 3; ++c)
260 fillVelGhosts(c, 0);
261 fillPorousEpsGhosts(); // the SAME eps ghost policy the projection used (the coupling deposit
262 // rewrites the ghosts between project() and this diagnostic)
263 divergOpenEps(CCConst(C[0].u), CCConst(C[1].u), CCConst(C[2].u), CCConst(ox_), CCConst(oy_),
264 CCConst(oz_), CCConst(epsField_), div_, e_, G);
265 { // add back the SAME d(eps)/dt source the projection used (depsdt_ from the last project())
267 C3 e = e_; // local copy — capturing e_ in the KOKKOS_LAMBDA would read this-> on the device
268 CCField d = div_, dd = depsdt_;
269 const bool useDt = porousDepsDt_;
270 using MD = MDRange3<CCExec>;
271 Kokkos::parallel_for(
272 "peclet::flow::porous_resid", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
273 KOKKOS_LAMBDA(int x, int y, int z) {
274 const long i = (long)x + (long)y * e.x + (long)z * e.x * e.y;
275 if (useDt)
276 d(i) += dd(i); // residual of the SAME constraint the projection solved
277 });
278 }
279 double m = reduceMaxAbsInner(CCConst(div_));
280#ifdef PECLET_FLOW_MPI
281 if (distributed_) {
282 double g = 0;
284 return g;
285 }
286#endif
287 return m;
288}
289
290template <class Grid>
292 return lastPressureIters_;
293}
294
295template <class Grid>
296std::vector<std::array<int, 3>> Solver<Grid>::pressureMgLevelRatios() const {
297 std::vector<std::array<int, 3>> out;
298 for (const C3& r : mg_.levelRatios())
299 out.push_back({r.x, r.y, r.z});
300 return out;
301}
302
303template <class Grid>
305 return lastPressureFailed_;
306}
307
308template <class Grid>
310 using peclet::core::solver::AndersonRole;
311 // doc/steady_acceleration.md §5.3: each refusal has its own message. Phase change is tested
312 // before VoF because enable_phase_change turns VoF on itself, so in the note's order its own
313 // message could never be reached; drag precedes the cell forces for the same reason (it
314 // registers force_*).
315 auto refuse = [](const std::string& why) {
316 throw std::runtime_error("steady acceleration (march_to_steady / anderson_accelerator): " +
317 why + "; pass accelerate=False");
318 };
319 if constexpr (Grid::collocated) {
320 if (!(ghostProjection_ && faceInterp_ == 0))
321 refuse(
322 "the collocated scheme is not the fluid-only 'ghost' projection -- for this scheme the "
323 "fixed point is not unique / the march is unstable (select "
324 "set_collocated_scheme('ghost') before the geometry)");
325 }
326 if (pcEnabled_)
327 refuse("phase change (enable_phase_change) has no steady fixed point");
328 if (vofEnabled_ || vofMomEnabled_ || vofBlocks_)
329 refuse("VoF (enable_vof / enable_vof_momentum / VoF blocks) is time-accurate, not steady");
330 if (!scalars_.empty())
331 refuse("a transported scalar (add_scalar) is not in the march state");
332 if (porous_)
333 refuse("the porous (volume-averaged) continuity is not supported");
334 bool propClosure = false;
335 for (const auto& cl : closures_)
336 propClosure = propClosure || cl.outName == "rho" || cl.outName == "mu";
337 if (varRho_ || varProps_ || propClosure || hasField("rho") || hasField("mu"))
338 refuse("variable density or viscosity (a property closure or a rho/mu field) is not supported");
339 if (hasDrag_)
340 refuse("a drag_beta field (implicit drag, CFD-DEM) is not supported");
341 if (hasCellForce_)
342 refuse("cell force fields (force_x/force_y/force_z) are not supported");
343 if (hasMotion_)
344 refuse("moving scene instances have no steady fixed point");
345 if (superficialAxis_ >= 0)
346 refuse("set_superficial_velocity is not supported");
347 if (pwarm_)
348 refuse("the pressure warm start (set_pressure_warmstart(True)) diverges on the steady march");
349 if (balancedForceActive())
350 refuse("the balanced-force projection is not supported");
351
352 MarchState ms;
353 for (int c = 0; c < 3; ++c) {
354 ms.fields.push_back(C[c].u);
355 ms.roles.push_back(AndersonRole::Velocity);
356 }
357 ms.fields.push_back(P_);
358 ms.roles.push_back(AndersonRole::Carried);
359 if constexpr (Grid::collocated) {
360 // §3.1 row 3, decided from the CONFIGURATION, not from faceFieldValid_.
361 if (advect_ && ufAdvect_)
362 for (CCField f : {uf_, vf_, wf_}) {
363 ms.fields.push_back(f);
364 ms.roles.push_back(AndersonRole::Carried);
365 }
366 }
367 ms.e = e_;
368 ms.G = G;
369 ms.signature = {dt_,
370 rho_,
371 mu_,
372 f_[0],
373 f_[1],
374 f_[2],
375 advect_ ? 1.0 : 0.0,
376 static_cast<double>(advScheme_),
377 implicitFou_ ? 1.0 : 0.0};
378#ifdef PECLET_FLOW_MPI
379 ms.comm = comm_;
380 ms.distributed = distributed_;
381#endif
382 return ms;
383}
384
385template <class Grid>
387 return tStep_;
388}
389
390template <class Grid>
392 return tPredictor_;
393}
394
395template <class Grid>
397 return tMomentum_;
398}
399
400template <class Grid>
402 return tProjection_;
403}
404
405template <class Grid>
407 return mg_.allreduceSeconds();
408}
409
410template <class Grid>
412 return mg_.allreduceCount();
413}
414
415} // namespace peclet::flow
416
417#endif // PECLET_FLOW_FLOW_IBM_DIAGNOSTICS_HPP
std::vector< C3 > levelRatios() const
The per-level coarsening ratio actually chosen (the level table of §5, gate §8.5).
std::vector< double > getFaceVelocity(int c)
void fillVelGhosts(int comp, int fold)
double maxOpenDivergenceInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
void scatterInner(CCField fld, const std::vector< double > &in)
std::vector< std::array< int, 3 > > pressureMgLevelRatios() const
OutflowBackflow outflowBackflow()
double maxOpenDivergenceProjectedInternal()
The same diagnostic in INDEX units (per tRef), which is what the solver's own guards read.
CCConst openFaceView(int c) const
Component c of the face field AS THE ADVECTION AND THE DIVERGENCE DIAGNOSTIC MUST READ IT: advFaceVie...
void addWallFluxDivergence(CCField d)
std::vector< double > getOpenness(int c)
void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow=true)
static constexpr int G
Definition flow_ibm.hpp:80
std::vector< double > getMomentumDiagonal(int c)
void setVelocity(int c, const std::vector< double > &v)
Write a component's inner velocity from a host vector (x-fastest, inner region) and re-impose the sol...
std::vector< double > getOpennessProj(int c)
std::vector< double > getVelocity(int c)
double reduceMaxAbsInner(CCConst f)
std::vector< double > gatherInner(CCField fld)
std::vector< double > getPressure()
void gpDivergDelta(CCField d, CCConst u, CCConst v, CCConst w, const GpOverlayReal< Real > &ov, int nOv, C3 nn, C3 extb, int gb, bool useGhost=false)
Overlay divergence correction: d(r) = rho_r * (d(r) + closure/BC/explicit face values),...
void divergOpen(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCField d, C3 e, int g)
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
void divergOpenEps(CCConst u, CCConst v, CCConst w, CCConst ox, CCConst oy, CCConst oz, CCConst eps, CCField d, C3 e, int g)
Kokkos::View< const double *, CCMem > CCConst
std::vector< peclet::core::solver::AndersonRole > roles
std::array< double, 9 > signature
std::vector< CCField > fields
OUTFLOW REVERSAL CENSUS – the regime in which the zero-gradient (do-nothing) outflow is only conditio...
double rhoRef
reference density = the first set_rho
Definition flow_ibm.hpp:143
double velToPhys(int a) const
Definition flow_ibm.hpp:151
double velToInt(int a) const
Definition flow_ibm.hpp:150
static constexpr int G
static constexpr double AC