flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_hydro.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_HYDRO_HPP
9#define PECLET_FLOW_FLOW_IBM_HYDRO_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
14void Solver<Grid>::scaleForceTorque(std::vector<double>& out, std::size_t m) const {
15 const double kF = u_.forceTotalToPhys(), kT = u_.torqueToPhys();
16 if (kF == 1.0 && kT == 1.0)
17 return;
18 for (std::size_t b = 0; m > 0 && b * m < out.size(); ++b) {
19 const double k = (b == 1) ? kT : kF;
20 for (std::size_t i = b * m; i < (b + 1) * m && i < out.size(); ++i)
21 out[i] *= k;
22 }
23}
24
25template <class Grid>
26std::vector<double> Solver<Grid>::hydroForceTorque() {
27 std::vector<double> out((std::size_t)(nInst_ > 0 ? nInst_ : 0) * 12, 0.0);
28 if (!hasScene_ || nInst_ <= 0 || cutOwner_.extent(0) != (std::size_t)nx_ * ny_ * nz_)
29 return out;
30 const std::size_t m = (std::size_t)nInst_ * 3;
31 Kokkos::View<double*, CCMem> Fd("hydroF", m), Td("hydroT", m), Pd("hydroFp", m), Vd("hydroFv", m);
32 Kokkos::deep_copy(Fd, 0.0);
33 Kokkos::deep_copy(Td, 0.0);
34 Kokkos::deep_copy(Pd, 0.0);
35 Kokkos::deep_copy(Vd, 0.0);
37 const C3 e = e_, og = og_;
38 const int nx = nx_, ny = ny_;
39 const double mu = mu_;
40 // §4.4: kA[a] = V'/h_a' turns the index fragment normal W_a into the physical area vector; the
41 // hp ratios below turn the index-velocity gradient pair into the physical strain rate.
42 const double kA0 = u_.vol / u_.hp[0], kA1 = u_.vol / u_.hp[1], kA2 = u_.vol / u_.hp[2];
43 const double hp0 = u_.hp[0], hp1 = u_.hp[1], hp2 = u_.hp[2];
44 CCConst oxv = CCConst(ox_), oyv = CCConst(oy_), ozv = CCConst(oz_);
45 CCConst U = CCConst(C[0].u), Vv = CCConst(C[1].u), W = CCConst(C[2].u);
46 CCConst Pf = CCConst(P_);
47 CCConst sd = CCConst(sdf_);
48 const bool haveWallVel = (uwCell_[0].extent(0) == n_);
49 CCConst wcx = haveWallVel ? CCConst(uwCell_[0]) : CCConst();
50 CCConst wcy = haveWallVel ? CCConst(uwCell_[1]) : CCConst();
51 CCConst wcz = haveWallVel ? CCConst(uwCell_[2]) : CCConst();
52
53 auto own = cutOwner_;
54 auto cen = instCenD_;
55 const auto box = sceneQ_->view().box;
56 const SceneMap sm = sceneMap();
57 using MD = MDRange3<CCExec>;
58 Kokkos::parallel_for(
59 "peclet::flow::hydro_force", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
60 KOKKOS_LAMBDA(int x, int y, int z) {
61 const long st[3] = {1, e.x, (long)e.x * e.y};
62 const long i = (long)x + (long)y * st[1] + (long)z * st[2];
63 const double A[3] = {-(oxv(i + st[0]) - oxv(i)), -(oyv(i + st[1]) - oyv(i)),
64 -(ozv(i + st[2]) - ozv(i))};
65 if (A[0] == 0.0 && A[1] == 0.0 && A[2] == 0.0)
66 return; // no wall passes through this cell
67 const int oi = own((std::size_t)(x - G) + (std::size_t)(y - G) * nx +
68 (std::size_t)(z - G) * (std::size_t)nx * ny);
69 if (oi < 0)
70 return;
71 // Cell-centred velocity from the staggered faces.
72 //
73 // A SOLID CELL STORES A MASKED ZERO, WHICH IS THE MATERIAL VELOCITY ONLY WHEN THE WALL
74 // IS AT REST. Reading it as physical is harmless for static geometry and a sign error
75 // for moving geometry: the stencil then sees a spurious shear of the wall speed over one
76 // cell across the entire surface. Measured on the Galilean pair -- with the velocity
77 // field itself frame-invariant to 7e-7, the integrated force came out +7.08e+01 in the
78 // lab frame and -1.71e+02 in a frame boosted by 0.7, a ratio of -2.42. A resolved
79 // CFD-DEM loop driven by that does not settle, it runs away. Substituting the wall's own
80 // velocity restores frame invariance; with static geometry uwCell_ is empty and this is
81 // bit-identical to the plain expression.
82 auto uc = [&](int a, long c) {
83 if (haveWallVel && sd(c) < 0.0)
84 return a == 0 ? wcx(c) : (a == 1 ? wcy(c) : wcz(c));
85 const CCConst& F = a == 0 ? U : (a == 1 ? Vv : W);
86 return 0.5 * (F(c) + F(c + st[a]));
87 };
88 // PLAIN CENTRAL DIFFERENCE, as the Layer-4 spec prescribes. It spans 2h while the wall
89 // sits a fraction of a cell away, so it under-reads the wall shear -- measured as a
90 // RESOLUTION-INDEPENDENT ~29% drag deficit that lives almost entirely in the viscous
91 // part. The obvious one-sided repair, differencing to the wall over the crossing
92 // distance theta, was TRIED AND IS WORSE: cut cells with theta -> 0 make 1/theta
93 // unbounded and the drag came out 17x too large. That is precisely why the momentum
94 // operator uses a Robust-Scaled reconstruction rather than a raw one-sided difference,
95 // and it is why a correct wall-aware traction has to come from that machinery (or from
96 // the discrete reaction the operator already applies) rather than from a patch here.
97 // See the design note's OPEN FOR REVIEW.
98 double gu[3][3];
99 for (int a = 0; a < 3; ++a)
100 for (int b = 0; b < 3; ++b)
101 gu[a][b] = 0.5 * (uc(a, i + st[b]) - uc(a, i - st[b]));
102 const double p = Pf(i);
103 const double kA[3] = {kA0, kA1, kA2}; // V'/h_a' (exactly 1.0 isotropic)
104 const double hpv[3] = {hp0, hp1, hp2};
105 double dF[3], dFp[3], dFv[3];
106 for (int a = 0; a < 3; ++a) {
107 // A_wall is fluid-outward; the traction on the BODY takes -A_wall. Pressure and
108 // viscous parts are kept apart because they fail differently: the pressure term reads
109 // one cell-centred value, while the viscous term differences a velocity whose stencil
110 // reaches into solid cells -- so a deficit that lives entirely in one of them says
111 // immediately which.
112 dFp[a] = (p * A[a]) * kA[a];
113 double t = 0.0;
114 for (int b = 0; b < 3; ++b) {
115 const double rab = hpv[a] / hpv[b], rba = hpv[b] / hpv[a];
116 t += mu * (rab * gu[a][b] + rba * gu[b][a]) * (A[b] * kA[b]);
117 }
118 dFv[a] = -t;
119 dF[a] = dFp[a] + dFv[a];
120 }
121 // The instance centre and the min-image box live in the SCENE's coordinates; dF is an
122 // index-unit force, so the lever arm has to come back to index units (a uniform scale, so
123 // min-imaging in either system is the same shortening).
124 const peclet::core::Vec3<double> rp = peclet::core::geom::minImage(
125 peclet::core::Vec3<double>{
126 sm.a[0] + sm.b[0] * (double)(x - G + og.x) - cen[3 * oi + 0],
127 sm.a[1] + sm.b[1] * (double)(y - G + og.y) - cen[3 * oi + 1],
128 sm.a[2] + sm.b[2] * (double)(z - G + og.z) - cen[3 * oi + 2]},
129 box);
130 const peclet::core::Vec3<double> r{rp.x * sm.dToInt, rp.y * sm.dToInt, rp.z * sm.dToInt};
131 for (int a = 0; a < 3; ++a) {
132 Kokkos::atomic_add(&Fd(3 * oi + a), dF[a]);
133 Kokkos::atomic_add(&Pd(3 * oi + a), dFp[a]);
134 Kokkos::atomic_add(&Vd(3 * oi + a), dFv[a]);
135 }
136 Kokkos::atomic_add(&Td(3 * oi + 0), r.y * dF[2] - r.z * dF[1]);
137 Kokkos::atomic_add(&Td(3 * oi + 1), r.z * dF[0] - r.x * dF[2]);
138 Kokkos::atomic_add(&Td(3 * oi + 2), r.x * dF[1] - r.y * dF[0]);
139 });
140 space.fence();
141 using HostV = Kokkos::View<double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>;
142 Kokkos::deep_copy(HostV(out.data(), m), Fd);
143 Kokkos::deep_copy(HostV(out.data() + m, m), Td);
144 Kokkos::deep_copy(HostV(out.data() + 2 * m, m), Pd);
145 Kokkos::deep_copy(HostV(out.data() + 3 * m, m), Vd);
146 // ONCE: the conversion covers all four blocks (force, torque, pressure part, viscous part). A
147 // duplicated call here squared the factor -- the identity in cell units, so no cell-unit gate saw
148 // it, and 0.019x the reaction at L = 1 under a physical domain (tests/python/
149 // test_hydro_force_units.py).
150 scaleForceTorque(out, m);
151#ifdef PECLET_FLOW_MPI
152 if (distributed_) {
153 // Instances are REPLICATED, so each rank integrates only the wall cells inside its own
154 // block; the body's total is the sum over ranks.
155 std::vector<double> g(out.size(), 0.0);
156 MPI_Allreduce(out.data(), g.data(), (int)out.size(), MPI_DOUBLE, MPI_SUM, comm_);
157 out.swap(g);
158 }
159#endif
160 return out;
161}
162
163template <class Grid>
165 std::vector<double> out((std::size_t)(nInst_ > 0 ? nInst_ : 0) * 6, 0.0);
166 if (!hasScene_ || nInst_ <= 0)
167 return out;
168 if constexpr (Grid::collocated)
169 throw std::runtime_error("hydro_force_torque_reaction: staggered only (v1)");
170 if (implicitAdv() || porous_ || varRho_ || varProps_ || hasBc_ || ghostProjection_ || hasDrag_ ||
171 fluidOnlyMode_ != 0)
172 throw std::runtime_error(
173 "hydro_force_torque_reaction: implicit advection / porous / variable-properties / "
174 "domain-BC / ghost-projection / drag / star modes put momentum terms in the step that "
175 "this budget does not carry (v2) -- a missing term is a silently mis-attributed force");
176 if (!haveUStar_)
177 throw std::runtime_error(
178 "hydro_force_torque_reaction: call step() first (u* is stashed "
179 "during the step)");
180 if (advect_ && (!haveAdvRhs_ || advRhs_[0].extent(0) != n_))
181 throw std::runtime_error(
182 "hydro_force_torque_reaction: the advective RHS term was not "
183 "stashed -- set_advection was enabled after the last step()");
184 // u* ghosts: refresh with the standard fill (periodic wrap single-rank, halo exchange under
185 // MPI; hasBc_ is refused above so no BC is imposed). The audit's viscous term reads +-1.
186 for (int c = 0; c < 3; ++c)
187 fillVelGhostsTo(uStar_[c], c, 0);
188 const std::size_t m = (std::size_t)nInst_ * 3;
189 Kokkos::View<double*, CCMem> Fd("reactF", m), Td("reactT", m);
190 Kokkos::deep_copy(Fd, 0.0);
191 Kokkos::deep_copy(Td, 0.0);
193 const C3 e = e_, og = og_;
194 const double idt = rho_ / dt_, mu = mu_;
195 const auto q = sceneQ_->view();
196 auto cen = instCenD_;
197 const auto box = q.box;
198 const SceneMap sm = sceneMap();
199 const bool hasFb = hasCellForce_;
200 // §4.4: the momentum row of component c is a force density in the component-c normalisation, so
201 // the total force carries h_c'*V' (exactly 1.0 isotropic -- an identity multiplication).
202 const double kR[3] = {u_.hp[0] * u_.vol, u_.hp[1] * u_.vol, u_.hp[2] * u_.vol};
203 // ... while the v3 wall-torque traction below carries V'/h_b' on its AREA vector (E3).
204 const double kA0 = u_.vol / u_.hp[0], kA1 = u_.vol / u_.hp[1], kA2 = u_.vol / u_.hp[2];
205 for (int c = 0; c < 3; ++c) {
206 CCConst un = CCConst(old_[c]), uc = CCConst(C[c].u), us = CCConst(uStar_[c]),
207 mk = CCConst(C[c].mask);
208 CCConst fb = hasFb ? CCConst(cellForce_[c]) : CCConst();
209 CCConst av = advect_ ? CCConst(advRhs_[c]) : CCConst();
210 const double fc = f_[c];
211 const auto po = Grid::offset(c);
212 const double offx = po.x, offy = po.y, offz = po.z;
213 const int cc = c;
214 const double kRc = kR[c];
215 Kokkos::parallel_for(
216 "peclet::flow::hydro_reaction",
217 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
218 KOKKOS_LAMBDA(int x, int y, int z) {
219 const long st[3] = {1, e.x, (long)e.x * e.y};
220 const long i = (long)x + (long)y * st[1] + (long)z * st[2];
221 if (mk(i) > 0.5)
222 return; // solid staggered point: no fluid momentum here
223 // the body force exactly as buildRhsForced placed it (Grid::atVelocity)
224 double R = idt * (uc(i) - un(i)) - fc -
225 (fb.data() ? Grid::atVelocity(fb, i, st[cc]) : 0.0) -
226 (av.data() ? av(i) : 0.0);
227 for (int a = 0; a < 3; ++a) {
228 const long jp = i + st[a], jm = i - st[a];
229 if (mk(jp) <= 0.5)
230 R -= mu * (us(jp) - us(i));
231 if (mk(jm) <= 0.5)
232 R -= mu * (us(jm) - us(i));
233 }
234 // The scene (owner query, instance centres, min-image box) is in the CALLER's
235 // coordinates; F is an index-unit force, so the lever arm comes back to index units.
236 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * ((double)(x - G + og.x) + offx),
237 sm.a[1] + sm.b[1] * ((double)(y - G + og.y) + offy),
238 sm.a[2] + sm.b[2] * ((double)(z - G + og.z) + offz)};
239 const int oi = q.owner(p);
240 if (oi < 0)
241 return;
242 // force ON the body = minus the wall force on the fluid, times h_c'V' (§4.4)
243 const double F = -R * kRc;
244 Kokkos::atomic_add(&Fd(3 * oi + cc), F);
245 const peclet::core::Vec3<double> rq = peclet::core::geom::minImage(
246 peclet::core::Vec3<double>{p.x - cen(3 * oi + 0), p.y - cen(3 * oi + 1),
247 p.z - cen(3 * oi + 2)},
248 box);
249 const peclet::core::Vec3<double> r{rq.x * sm.dToInt, rq.y * sm.dToInt, rq.z * sm.dToInt};
250 // torque of the scalar force F e_c at lever r: r x (F e_c)
251 if (cc == 0) {
252 Kokkos::atomic_add(&Td(3 * oi + 1), r.z * F);
253 Kokkos::atomic_add(&Td(3 * oi + 2), -r.y * F);
254 } else if (cc == 1) {
255 Kokkos::atomic_add(&Td(3 * oi + 0), -r.z * F);
256 Kokkos::atomic_add(&Td(3 * oi + 2), r.x * F);
257 } else {
258 Kokkos::atomic_add(&Td(3 * oi + 0), r.y * F);
259 Kokkos::atomic_add(&Td(3 * oi + 1), -r.x * F);
260 }
261 });
262 }
263 space.fence();
264 // v3: the transposed-stress wall torque, mu * r x (n dA x Omega) per cut cell. n dA is the
265 // exact aperture wall-area vector with the BODY-outward orientation, (oE-oW, oN-oS, oT-oB)
266 // per the wallAreaProbe convention (sum x*(oE-oW) = +V_solid). Skipped entirely when no
267 // instance moves, so a static run stays bit-identical; a purely TRANSLATING instance has
268 // Omega = 0 and contributes exact zeros. The FORCE is deliberately left alone: the term's
269 // force integral is identically zero over a closed surface, and adding its discrete
270 // counterpart would only inject aperture-level rounding into an exactly-gated identity.
271 if (hasMotion_ && cutcellPressure_) {
272 CCConst oxv = CCConst(ox_), oyv = CCConst(oy_), ozv = CCConst(oz_);
273 auto ang = instAngD_;
274 Kokkos::parallel_for(
275 "peclet::flow::hydro_reaction_torque_transpose",
276 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
277 KOKKOS_LAMBDA(int x, int y, int z) {
278 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
279 const long i = (long)x + (long)y * sy + (long)z * sz;
280 const double ax = oxv(i + sx) - oxv(i);
281 const double ay = oyv(i + sy) - oyv(i);
282 const double az = ozv(i + sz) - ozv(i);
283 if (ax == 0.0 && ay == 0.0 && az == 0.0)
284 return; // not a cut cell (a sign test: the positive per-axis factors below cannot
285 // change it)
286 // PHASE 2 (doc/anisotropic_metric.md §4.4, E3 reading 2): (ax, ay, az) is an INDEX
287 // aperture area vector; the physical one in hRef^2 is A_b = a_b V'/h_b' -- the same
288 // A the traction paragraph of §4.4 forms -- so the metric multiplies the AREA
289 // component b, which is the index the cross product with Omega consumes, applied
290 // OUTSIDE the existing expression so V'/h_b' = 1.0 reduces exactly.
291 const double Ax = ax * kA0, Ay = ay * kA1, Az = az * kA2;
292 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * (double)(x - G + og.x),
293 sm.a[1] + sm.b[1] * (double)(y - G + og.y),
294 sm.a[2] + sm.b[2] * (double)(z - G + og.z)};
295 const int oi = q.owner(p);
296 if (oi < 0)
297 return;
298 // The scene's angular velocity is 1/time in the caller's units; the index one is
299 // omega*tRef (v = omega x r holds on both sides of the map).
300 const double wx = ang(3 * oi + 0) * sm.angToInt, wy = ang(3 * oi + 1) * sm.angToInt,
301 wz = ang(3 * oi + 2) * sm.angToInt;
302 if (wx == 0.0 && wy == 0.0 && wz == 0.0)
303 return;
304 // v = (n dA) x Omega -- the missing traction integrated over this cell's wall patch
305 const double vx = Ay * wz - Az * wy;
306 const double vy = Az * wx - Ax * wz;
307 const double vz = Ax * wy - Ay * wx;
308 const peclet::core::Vec3<double> rq = peclet::core::geom::minImage(
309 peclet::core::Vec3<double>{p.x - cen(3 * oi + 0), p.y - cen(3 * oi + 1),
310 p.z - cen(3 * oi + 2)},
311 box);
312 const peclet::core::Vec3<double> r{rq.x * sm.dToInt, rq.y * sm.dToInt, rq.z * sm.dToInt};
313 Kokkos::atomic_add(&Td(3 * oi + 0), mu * (r.y * vz - r.z * vy));
314 Kokkos::atomic_add(&Td(3 * oi + 1), mu * (r.z * vx - r.x * vz));
315 Kokkos::atomic_add(&Td(3 * oi + 2), mu * (r.x * vy - r.y * vx));
316 });
317 space.fence();
318 }
319 // v4: owner-boundary attribution correction (see the doc block). Only meaningful with at
320 // least two instances and the incremental pressure (P_ holds the physical pressure).
321 if (nInst_ > 1 && incremental_ && cutcellPressure_) {
322 fillGhosts(P_);
323 CCConst pf = CCConst(P_);
324 for (int c = 0; c < 3; ++c) {
325 CCConst mk = CCConst(C[c].mask);
326 const long strd = (c == 0) ? 1 : (c == 1) ? e_.x : (long)e_.x * e_.y;
327 const auto po = Grid::offset(c);
328 const double offx = po.x, offy = po.y, offz = po.z;
329 const int cc = c;
330 // PHASE 2 (§4.4, a site the note's list does not name): this correction REMOVES a term that
331 // is already inside `F_a = -sum R_a h_a' V'`, so it must carry exactly the factor that term
332 // carries there. The momentum row's pressure gradient is `w_c (P(i) - P(i-s))` since C1/C2,
333 // and F multiplies by `h_c' V'`, so the combined factor on `pi(i)` is
334 // `w_c h_c' V' = V'/h_c'` -- the physical area of the face, as it must be for a pressure
335 // force. Exactly 1.0 isotropic (1.0/1.0), so this is an identity multiplication.
336 const double kP = u_.vol / u_.hp[c];
337 Kokkos::parallel_for(
338 "peclet::flow::hydro_reaction_owner_flux",
339 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
340 KOKKOS_LAMBDA(int x, int y, int z) {
341 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
342 const long j = i + strd; // the +s neighbour: visit once
343 if (mk(i) > 0.5 || mk(j) > 0.5)
344 return; // wall faces stay in the wall force
345 const peclet::core::Vec3<double> pa{
346 sm.a[0] + sm.b[0] * ((double)(x - G + og.x) + offx),
347 sm.a[1] + sm.b[1] * ((double)(y - G + og.y) + offy),
348 sm.a[2] + sm.b[2] * ((double)(z - G + og.z) + offz)};
349 peclet::core::Vec3<double> pb = pa; // one cell along cc, in scene coordinates
350 (cc == 0 ? pb.x : cc == 1 ? pb.y : pb.z) += sm.b[cc];
351 const int oa = q.owner(pa), ob = q.owner(pb);
352 if (oa == ob || oa < 0 || ob < 0)
353 return;
354 // One-sided staggered gradients: point i reads pi(i) - pi(i-s); point j = i+s
355 // reads pi(j) - pi(i). The cell shared by this owner-boundary face is cell i,
356 // entering a's telescoped sum(grad pi) with +pi(i) (via point i) and b's with
357 // -pi(i) (via point j). F_attr = -sum R carries +sum(grad pi), so a's attribution
358 // holds +pi(i) and b's -pi(i) from this face: a pure transfer across the owner
359 // partition that belongs to NEITHER wall. Remove it from both, symmetrically --
360 // the pairwise cancellation is what keeps the total exact.
361 const double flux = pf(i) * kP;
362 Kokkos::atomic_add(&Fd(3 * oa + cc), -flux);
363 Kokkos::atomic_add(&Fd(3 * ob + cc), +flux);
364 const peclet::core::Vec3<double> raq = peclet::core::geom::minImage(
365 peclet::core::Vec3<double>{pa.x - cen(3 * oa + 0), pa.y - cen(3 * oa + 1),
366 pa.z - cen(3 * oa + 2)},
367 box);
368 const peclet::core::Vec3<double> rbq = peclet::core::geom::minImage(
369 peclet::core::Vec3<double>{pa.x - cen(3 * ob + 0), pa.y - cen(3 * ob + 1),
370 pa.z - cen(3 * ob + 2)},
371 box);
372 const peclet::core::Vec3<double> ra{raq.x * sm.dToInt, raq.y * sm.dToInt,
373 raq.z * sm.dToInt};
374 const peclet::core::Vec3<double> rb{rbq.x * sm.dToInt, rbq.y * sm.dToInt,
375 rbq.z * sm.dToInt};
376 if (cc == 0) {
377 Kokkos::atomic_add(&Td(3 * oa + 1), ra.z * -flux);
378 Kokkos::atomic_add(&Td(3 * oa + 2), -ra.y * -flux);
379 Kokkos::atomic_add(&Td(3 * ob + 1), rb.z * +flux);
380 Kokkos::atomic_add(&Td(3 * ob + 2), -rb.y * +flux);
381 } else if (cc == 1) {
382 Kokkos::atomic_add(&Td(3 * oa + 0), -ra.z * -flux);
383 Kokkos::atomic_add(&Td(3 * oa + 2), ra.x * -flux);
384 Kokkos::atomic_add(&Td(3 * ob + 0), -rb.z * +flux);
385 Kokkos::atomic_add(&Td(3 * ob + 2), rb.x * +flux);
386 } else {
387 Kokkos::atomic_add(&Td(3 * oa + 0), ra.y * -flux);
388 Kokkos::atomic_add(&Td(3 * oa + 1), -ra.x * -flux);
389 Kokkos::atomic_add(&Td(3 * ob + 0), rb.y * +flux);
390 Kokkos::atomic_add(&Td(3 * ob + 1), -rb.x * +flux);
391 }
392 });
393 }
394 space.fence();
395 }
396 using HostV = Kokkos::View<double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>;
397 Kokkos::deep_copy(HostV(out.data(), m), Fd);
398 Kokkos::deep_copy(HostV(out.data() + m, m), Td);
399 scaleForceTorque(out, m);
400#ifdef PECLET_FLOW_MPI
401 if (distributed_) {
402 std::vector<double> g(out.size(), 0.0);
403 MPI_Allreduce(out.data(), g.data(), (int)out.size(), MPI_DOUBLE, MPI_SUM, comm_);
404 out.swap(g);
405 }
406#endif
407 return out;
408}
409
410template <class Grid>
411std::array<long, 3> Solver<Grid>::fluidMomentumCells() {
412 std::array<long, 3> out{0, 0, 0};
414 const C3 e = e_;
415 for (int c = 0; c < 3; ++c) {
416 CCConst mk = CCConst(C[c].mask);
417 long n = 0;
418 Kokkos::parallel_reduce(
419 "peclet::flow::fluid_cells",
420 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
421 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
422 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
423 if (mk(i) <= 0.5)
424 ++acc;
425 },
426 n);
427 out[(std::size_t)c] = n;
428 }
429 space.fence();
430#ifdef PECLET_FLOW_MPI
431 if (distributed_) {
432 std::array<long, 3> g{0, 0, 0};
433 MPI_Allreduce(out.data(), g.data(), 3, MPI_LONG, MPI_SUM, comm_);
434 out = g;
435 }
436#endif
437 return out;
438}
439
440template <class Grid>
442 std::vector<double> out(6, 0.0);
444 const C3 e = e_;
445 const double idt = rho_ / dt_;
446 const bool haveA = advect_ && haveAdvRhs_ && advRhs_[0].extent(0) == n_;
447 for (int c = 0; c < 3; ++c) {
448 CCConst un = CCConst(old_[c]), uc = CCConst(C[c].u), mk = CCConst(C[c].mask);
449 CCConst av = haveA ? CCConst(advRhs_[c]) : CCConst();
450 double su = 0.0, sa = 0.0;
451 Kokkos::parallel_reduce(
452 "peclet::flow::budget_terms",
453 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
454 KOKKOS_LAMBDA(int x, int y, int z, double& au, double& aa) {
455 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
456 if (mk(i) > 0.5)
457 return;
458 au += idt * (uc(i) - un(i));
459 aa += av.data() ? av(i) : 0.0;
460 },
461 su, sa);
462 out[(std::size_t)c] = su;
463 out[(std::size_t)c + 3] = sa;
464 }
465 space.fence();
466#ifdef PECLET_FLOW_MPI
467 if (distributed_) {
468 std::vector<double> g(6, 0.0);
469 MPI_Allreduce(out.data(), g.data(), 6, MPI_DOUBLE, MPI_SUM, comm_);
470 out.swap(g);
471 }
472#endif
473 return out;
474}
475
476template <class Grid>
477std::array<double, 3> Solver<Grid>::wallAreaProbe() {
478 std::array<double, 3> out{0, 0, 0};
479 if (!hasScene_)
480 return out;
482 const C3 e = e_, og = og_;
483 CCConst oxv = CCConst(ox_), oyv = CCConst(oy_), ozv = CCConst(oz_);
484 double sx = 0, sy = 0, sz = 0;
485 Kokkos::parallel_reduce(
486 "peclet::flow::wall_area_probe",
487 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
488 KOKKOS_LAMBDA(int x, int y, int z, double& ax, double& ay, double& az) {
489 const long st[3] = {1, e.x, (long)e.x * e.y};
490 const long i = (long)x + (long)y * st[1] + (long)z * st[2];
491 ax += (double)(x - G + og.x) * -(oxv(i + st[0]) - oxv(i));
492 ay += (double)(y - G + og.y) * -(oyv(i + st[1]) - oyv(i));
493 az += (double)(z - G + og.z) * -(ozv(i + st[2]) - ozv(i));
494 },
495 sx, sy, sz);
496 space.fence();
497 out = {sx, sy, sz};
498#ifdef PECLET_FLOW_MPI
499 if (distributed_) {
500 std::array<double, 3> g{0, 0, 0};
501 MPI_Allreduce(out.data(), g.data(), 3, MPI_DOUBLE, MPI_SUM, comm_);
502 out = g;
503 }
504#endif
505 return out;
506}
507
508template <class Grid>
510 if (!hasScene_ || !hasMotion_ || uwCell_[0].extent(0) != n_)
511 return 0.0;
513 const C3 e = e_;
514 CCConst oxv = CCConst(ox_), oyv = CCConst(oy_), ozv = CCConst(oz_);
515 CCConst wx = CCConst(uwCell_[0]), wy = CCConst(uwCell_[1]), wz = CCConst(uwCell_[2]);
516 double sum = 0.0;
517 Kokkos::parallel_reduce(
518 "peclet::flow::wall_flux_sum",
519 MDRange3<CCExec>(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
520 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
521 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
522 const long i = (long)x + (long)y * sy + (long)z * sz;
523 acc += wx(i) * -(oxv(i + sx) - oxv(i)) + wy(i) * -(oyv(i + sy) - oyv(i)) +
524 wz(i) * -(ozv(i + sz) - ozv(i));
525 },
526 sum);
527#ifdef PECLET_FLOW_MPI
528 if (distributed_) {
529 double g = 0;
530 MPI_Allreduce(&sum, &g, 1, MPI_DOUBLE, MPI_SUM, comm_);
531 return g;
532 }
533#endif
534 return sum;
535}
536
537template <class Grid>
539 return uBc_[c].extent(0) == n_ ? CCConst(uBc_[c]) : CCConst();
540}
541
542} // namespace peclet::flow
543
544#endif // PECLET_FLOW_FLOW_IBM_HYDRO_HPP
double wallFluxImbalance()
Net wall flux this rank injects, sum over inner cells of u_w .
void fillGhosts(CCField f)
std::array< long, 3 > fluidMomentumCells()
The number of unmasked (fluid) staggered momentum cells per component – the exact discrete datum the ...
void scaleForceTorque(std::vector< double > &out, std::size_t m) const
Hydrodynamic force and torque on each scene instance (Layer 4 rung 2) – the resolved CFD-DEM feedback...
std::array< double, 3 > wallAreaProbe()
A_wall EXACTNESS PROBE (diagnostic for the Layer-4 force integral).
void fillVelGhostsTo(CCField f, int comp, int fold, bool doOutflow=true)
std::vector< double > hydroForceTorque()
PHASE 2 (anisotropic cells), doc/anisotropic_metric.md §4.4 — ADMITTED since commit C4.
static constexpr int G
Definition flow_ibm.hpp:80
SceneMap sceneMap() const
std::vector< double > reactionBudgetTerms()
R0 DECOMPOSITION PROBE.
CCConst wallVelView(int c) const
std::vector< double > hydroForceTorqueReaction()
Hydrodynamic force and torque per instance from the DISCRETE REACTION (route (b) of the design note's...
Kokkos::DefaultExecutionSpace CCExec
Kokkos::View< const double *, CCMem > CCConst
The map between the solver's INDEX coordinates and the coordinate system the analytic scene is writte...
Definition flow_ibm.hpp:241
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
static constexpr double F
static constexpr double U