flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_ibm_scene.hpp
Go to the documentation of this file.
1
8#ifndef PECLET_FLOW_FLOW_IBM_SCENE_HPP
9#define PECLET_FLOW_FLOW_IBM_SCENE_HPP
10
11namespace peclet::flow {
12
13template <class Grid>
14void Solver<Grid>::setScene(const std::vector<int>& nodeInts, const std::vector<double>& nodeReals,
15 const std::vector<int>& instInts, const std::vector<double>& instReals,
16 bool periodic) {
17 namespace g = peclet::core::geom;
18 g::SceneBuilder<double> b =
19 g::SceneBuilder<double>::decode(nodeInts, nodeReals, instInts, instReals, /*grids=*/{},
20 /*pool=*/{});
21 if (b.instances().empty())
22 throw std::runtime_error("set_scene: at least one instance required");
23 for (const auto& nd : b.nodes())
24 if (nd.kind == g::kGrid)
25 throw std::runtime_error("set_scene: grid leaves are not supported (analytic scenes only)");
26 // Global inner-grid extents: the distributed build carries them (gnx_); single-rank (or a
27 // non-MPI build) the local block IS the global grid.
28 double GX = nx_, GY = ny_, GZ = nz_;
29#ifdef PECLET_FLOW_MPI
30 if (gnx_ > 0) {
31 GX = gnx_;
32 GY = gny_;
33 GZ = gnz_;
34 }
35#endif
36 // The scene is in the caller's PHYSICAL coordinates when a domain is armed (so one scene
37 // serves flow, dem and voro unchanged), and in the historical cell coordinates otherwise.
38 peclet::core::Vec3<double> so{0, 0, 0}, se{GX, GY, GZ};
39 if (u_.physical) {
40 if (u_.cells[0] != (long)GX || u_.cells[1] != (long)GY || u_.cells[2] != (long)GZ)
41 throw std::runtime_error(
42 "set_scene: the physical domain was armed for a different global grid than the solver "
43 "has (pass global_cells = the grid init_mpi gets)");
44 so = peclet::core::Vec3<double>{u_.org[0], u_.org[1], u_.org[2]};
45 se = peclet::core::Vec3<double>{u_.ext[0], u_.ext[1], u_.ext[2]};
46 }
47 g::PeriodicBox<double> box{se.x, se.y, se.z, periodic};
48 sceneB_ = std::make_shared<g::SceneBuilder<double>>(std::move(b));
49 sceneOrigin_ = so;
50 sceneExtent_ = se;
51 scenePeriodic_ = periodic;
53 hasScene_ = true;
54 // Moving-geometry state travels WITH the scene: core's Instance already carries linVel/angVel/
55 // center, so a caller can encode motion directly and it arrives here. CENTRE OF ROTATION: the
56 // encoded `center` when it is nonzero, otherwise the instance's own translation -- which is
57 // what a caller who only placed the body means by "spin it". Set `center` explicitly (or pass
58 // it to set_instance_motion) to spin about some other point.
59 nInst_ = static_cast<int>(sceneB_->instances().size());
60 instCen_.assign((std::size_t)nInst_ * 3, 0.0);
61 instLin_.assign((std::size_t)nInst_ * 3, 0.0);
62 instAng_.assign((std::size_t)nInst_ * 3, 0.0);
63 instCenPinned_.assign((std::size_t)nInst_, 0);
64 for (int i = 0; i < nInst_; ++i) {
65 auto& in = sceneB_->instanceRef(i);
66 // CENTRE OF ROTATION (§7 item 3, resolved 2026-09-02): NaN = follows the body (the
67 // builder's default); any other finite point = PINNED there, the world origin included.
68 // An all-zero centre from a RAW instance array is the legacy 'follows the body' (every
69 // producer wrote zeros before NaN existed); a world-origin pin from a raw array goes
70 // through set_instance_motion(center=...). The decision is an explicit per-instance flag
71 // from here on, never re-inferred from the numbers.
72 // The decoder already resolved the record: an 18-real record carries the flag, a legacy
73 // 17-real record pins only a finite non-zero centre.
74 const bool pinned = in.centerPinned && std::isfinite(in.center.x) &&
75 std::isfinite(in.center.y) && std::isfinite(in.center.z);
76 instCenPinned_[(std::size_t)i] = pinned ? 1 : 0;
77 const auto c = pinned ? in.center : in.transform.translation;
78 in.center = c; // the resolved centre, so no consumer ever reads the NaN sentinel
79 instCen_[3 * (std::size_t)i + 0] = c.x;
80 instCen_[3 * (std::size_t)i + 1] = c.y;
81 instCen_[3 * (std::size_t)i + 2] = c.z;
82 instLin_[3 * (std::size_t)i + 0] = in.linVel.x;
83 instLin_[3 * (std::size_t)i + 1] = in.linVel.y;
84 instLin_[3 * (std::size_t)i + 2] = in.linVel.z;
85 instAng_[3 * (std::size_t)i + 0] = in.angVel.x;
86 instAng_[3 * (std::size_t)i + 1] = in.angVel.y;
87 instAng_[3 * (std::size_t)i + 2] = in.angVel.z;
88 }
91}
92
93template <class Grid>
94std::array<double, 3> Solver<Grid>::instanceCenter(int i) const {
95 if (i < 0 || i >= nInst_)
96 throw std::runtime_error("instance_center: instance index out of range");
97 return {instCen_[3 * (std::size_t)i], instCen_[3 * (std::size_t)i + 1],
98 instCen_[3 * (std::size_t)i + 2]};
99}
100
101template <class Grid>
103 if (i < 0 || i >= nInst_)
104 throw std::runtime_error("instance_center_pinned: instance index out of range");
105 return instCenPinned_[(std::size_t)i] != 0;
106}
107
108template <class Grid>
109void Solver<Grid>::setInstanceMotion(int i, const std::array<double, 3>& lin,
110 const std::array<double, 3>& ang, const double* center) {
111 if (!hasScene_)
112 throw std::runtime_error("set_instance_motion: call set_scene first");
113 if (i < 0 || i >= nInst_)
114 throw std::runtime_error("set_instance_motion: instance index out of range");
115 auto& in = sceneB_->instanceRef(i);
116 for (int k = 0; k < 3; ++k) {
117 instLin_[3 * (std::size_t)i + k] = lin[k];
118 instAng_[3 * (std::size_t)i + k] = ang[k];
119 if (center)
120 instCen_[3 * (std::size_t)i + k] = center[k];
121 }
122 if (center)
123 instCenPinned_[(std::size_t)i] = 1; // an explicit centre is pinned, whatever its value
124 in.linVel = peclet::core::Vec3<double>{lin[0], lin[1], lin[2]};
125 in.angVel = peclet::core::Vec3<double>{ang[0], ang[1], ang[2]};
126 in.center =
127 peclet::core::Vec3<double>{instCen_[3 * (std::size_t)i + 0], instCen_[3 * (std::size_t)i + 1],
128 instCen_[3 * (std::size_t)i + 2]};
130 uploadMotion();
131}
132
133template <class Grid>
134void Solver<Grid>::setInstanceTransform(int i, const std::array<double, 3>& translation,
135 const std::array<double, 4>& quat) {
136 if (!hasScene_)
137 throw std::runtime_error("set_instance_transform: call set_scene first");
138 if (i < 0 || i >= nInst_)
139 throw std::runtime_error("set_instance_transform: instance index out of range");
140 auto& in = sceneB_->instanceRef(i);
141 // Follows-the-body vs pinned is the explicit flag, not a coincidence of the numbers (the old
142 // float comparison turned a pinned centre into a tracked one whenever the body passed
143 // through it).
144 const bool centreTracked = instCenPinned_[(std::size_t)i] == 0;
145 in.transform.translation =
146 peclet::core::Vec3<double>{translation[0], translation[1], translation[2]};
147 in.transform.rotation = peclet::core::Quat<double>{quat[0], quat[1], quat[2], quat[3]};
148 if (centreTracked) {
149 for (int k = 0; k < 3; ++k)
150 instCen_[3 * (std::size_t)i + k] = translation[k];
151 in.center = in.transform.translation;
152 }
153 sceneDirty_ = true;
154}
155
156template <class Grid>
158 if (!hasScene_)
159 throw std::runtime_error("rebuild_geometry: call set_scene first");
160 CCField uSave[3], mSave[3];
161 for (int c = 0; c < 3; ++c) {
162 uSave[c] = CCField("uSave", n_);
163 Kokkos::deep_copy(uSave[c], C[c].u);
164 if (freshSeed_) { // remember which points were SOLID, to find the ones the body uncovers
165 mSave[c] = CCField("mSave", n_);
166 Kokkos::deep_copy(mSave[c], C[c].mask);
167 }
168 }
169 CCField pSave("pSave", n_);
170 Kokkos::deep_copy(pSave, P_);
171 if (sceneDirty_) {
173 sceneDirty_ = false;
174 }
175 // Crossings BEFORE the solid: the overlay build consumes tEx_, so deriving them first means
176 // ONE geometry rebuild per step rather than two.
177 if (sceneCrossings_)
179 setSolidFromScene(cutcellPressure_);
180 for (int c = 0; c < 3; ++c)
181 Kokkos::deep_copy(C[c].u, uSave[c]);
182 Kokkos::deep_copy(P_, pSave);
183 if (freshSeed_)
185}
186
187template <class Grid>
189 if (!hasMotion_)
190 return;
192 const C3 e = e_;
193 for (int c = 0; c < 3; ++c) {
194 if (mOld[c].extent(0) != n_ || uBc_[c].extent(0) != n_)
195 continue;
196 CCField u = C[c].u;
197 CCConst mo = CCConst(mOld[c]), mn = CCConst(C[c].mask), w = CCConst(uBc_[c]);
198 Kokkos::parallel_for(
199 "peclet::flow::seed_fresh", Kokkos::RangePolicy<CCExec>(space, 0, (long)n_),
200 KOKKOS_LAMBDA(long i) {
201 if (mo(i) > 0.5 && mn(i) <= 0.5)
202 u(i) = w(i);
203 });
204 }
205 space.fence();
206}
207
208template <class Grid>
210 freshSeed_ = on;
211}
212
213template <class Grid>
215 return freshSeed_;
216}
217
218template <class Grid>
220 if (!hasScene_)
221 throw std::runtime_error("refresh_wall_velocity: call set_scene first");
222 if (sceneDirty_)
223 throw std::runtime_error(
224 "refresh_wall_velocity: an instance TRANSFORM changed since the last geometry build -- "
225 "this call only refreshes the wall VELOCITY, so the run would continue on stale "
226 "geometry. Call rebuild_geometry() instead.");
229}
230
231template <class Grid>
233 return hasMotion_;
234}
235
236template <class Grid>
238 return nInst_;
239}
240
241template <class Grid>
243 wallFluxDiv_ = on;
244}
245
246template <class Grid>
248 return wallFluxDiv_;
249}
250
251template <class Grid>
253 return hasScene_;
254}
255
256template <class Grid>
257std::vector<int> Solver<Grid>::getCutOwner() const {
258 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
259 std::vector<int> out(n, -1);
260 if (cutOwner_.extent(0) != n)
261 return out;
262 using HostV = Kokkos::View<int*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>;
263 Kokkos::deep_copy(HostV(out.data(), n), cutOwner_);
264 return out;
265}
266
267template <class Grid>
269 if (!hasScene_)
270 throw std::runtime_error("set_solid_from_scene: call set_scene first");
271 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
272 CCField din("peclet::flow::sceneSdf", n);
273 if (cutOwner_.extent(0) != n)
274 cutOwner_ = Kokkos::View<int*, CCMem>("peclet::flow::cutOwner", n);
275 auto own = cutOwner_;
276 const auto q = sceneQ_->view();
277 const int nx = nx_, ny = ny_, nz = nz_;
278 const C3 og = og_;
279 const SceneMap sm = sceneMap();
281 Kokkos::parallel_for(
282 "peclet::flow::scene_sample", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
283 KOKKOS_LAMBDA(int x, int y, int z) {
284 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * (double)(x + og.x),
285 sm.a[1] + sm.b[1] * (double)(y + og.y),
286 sm.a[2] + sm.b[2] * (double)(z + og.z)};
287 // evalOwner is ONE traversal returning bitwise eval's value plus the argmin instance, so
288 // carrying the ownership field costs nothing over the sample it rides on.
289 int oi = -1;
290 const std::size_t idx =
291 (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
292 din(idx) = q.evalOwner(p, oi) * sm.dToInt;
293 own(idx) = oi;
294 });
295 space.fence();
296 // PERIODIC IMAGES ARE A UNION. The query takes the minimum over an instance's 27 neighbour
297 // images -- right for a body straddling a periodic face, and a silent TRAP for a leaf WIDER
298 // than the box: its images overlap, and a cavity carved from it (a container wall built as
299 // slab-minus-cavity) is refilled wherever a neighbouring image's slab covers it. Measured: a
300 // 0.7 L slab minus a 53-cell cavity gave a 38-cell duct; the ten Cate tank ran 30 % narrow
301 // through two campaigns and read as "creeping-valued confinement". Detect it EXACTLY: when
302 // some instance's bounding sphere spans more than the box on a periodic axis, sample the
303 // primary image alone and count the cells whose solid/fluid sign the images changed.
304 imageOverlapCells_ = 0;
305 if (scenePeriodic_) {
306 namespace g = peclet::core::geom;
307 const auto hv = sceneB_->view();
308 const double lmin = std::fmin(sceneExtent_.x, std::fmin(sceneExtent_.y, sceneExtent_.z));
309 bool wide = false;
310 for (int i = 0; i < hv.instanceCount; ++i)
311 if (2.0 * g::instanceBound(hv, i).r > lmin)
312 wide = true;
313 if (wide) {
314 g::PeriodicBox<double> nobox{sceneExtent_.x, sceneExtent_.y, sceneExtent_.z, false};
315 auto qnp = g::SceneQueryDevice<double, CCMem>::build(*sceneB_, sceneOrigin_, sceneExtent_,
316 nobox, /*accelerate=*/false);
317 const auto qv = qnp.view();
318 long cnt = 0;
319 Kokkos::parallel_reduce(
320 "peclet::flow::scene_image_overlap", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
321 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
322 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * (double)(x + og.x),
323 sm.a[1] + sm.b[1] * (double)(y + og.y),
324 sm.a[2] + sm.b[2] * (double)(z + og.z)};
325 int oi = -1;
326 const std::size_t idx =
327 (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
328 const double dp = qv.evalOwner(p, oi);
329 if ((dp < 0.0) != (din(idx) < 0.0))
330 ++acc;
331 },
332 cnt);
333 space.fence();
334 imageOverlapCells_ = cnt;
335 if (cnt > 0)
336 std::fprintf(stderr,
337 "peclet.flow set_solid_from_scene WARNING: an instance is wider than the "
338 "periodic box, and the UNION of its periodic images changes the solid at "
339 "%ld cells on this rank. If it is a container wall (slab minus cavity), "
340 "keep the slab's half-extent at half the box plus the wall thickness -- "
341 "not more -- or the images refill the cavity. "
342 "periodic_image_overlap_cells() returns this count.\n",
343 cnt);
344 }
345 }
347 if (hasMotion_)
349}
350
351template <class Grid>
353 const int nI = nInst_;
354 if (nI <= 0 || cutOwner_.extent(0) != (std::size_t)nx_ * ny_ * nz_)
355 return;
356 // The datum enters the momentum operator only through the IBM overlay's CUT ROWS (staggered
357 // points whose stencil crosses the wall, `idMap >= 0`), not through face apertures: a plane
358 // cutting faces parallel to itself leaves every aperture 0 or 1 and still has cut rows.
359 Kokkos::View<long*, CCMem> cnt("peclet::flow::movingCut", nI); // zero-initialised
360 auto own = cutOwner_;
361 const C3 e = e_;
362 const int nx = nx_, ny = ny_, nz = nz_;
364 for (int c = 0; c < 3; ++c) {
365 if (C[c].idMap.extent(0) != n_)
366 continue;
367 Kokkos::View<const int*, CCMem> idm = C[c].idMap;
368 Kokkos::parallel_for(
369 "peclet::flow::moving_cut_count", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
370 KOKKOS_LAMBDA(int x, int y, int z) {
371 const long ie = (long)(x + G) + (long)(y + G) * e.x + (long)(z + G) * (long)e.x * e.y;
372 if (idm(ie) < 0)
373 return;
374 const int o =
375 own((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
376 if (o >= 0 && o < nI)
377 Kokkos::atomic_add(&cnt(o), (long)1);
378 });
379 }
380 // DEGENERATE points: a staggered point where the sampled sdf is EXACTLY zero is fluid to the
381 // mask (strict < 0) and not a ghost to its neighbours' folds (strict < 0 too), so a wall face
382 // on a lattice plane never folds its datum -- the body is inert. Count them per owner.
383 Kokkos::View<long*, CCMem> deg("peclet::flow::movingDegenerate", nI);
384 {
385 CCConst sd = CCConst(sdf_);
386 for (int c = 0; c < 3; ++c) {
387 const auto po = Grid::offset(c);
388 const double ox = po.x, oy = po.y, oz = po.z;
389 Kokkos::parallel_for(
390 "peclet::flow::moving_degenerate_count", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
391 KOKKOS_LAMBDA(int x, int y, int z) {
392 const double sv = ccSampleExt(sd, e, x + G + ox, y + G + oy, z + G + oz);
393 if (sv != 0.0)
394 return;
395 const int o =
396 own((std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny);
397 if (o >= 0 && o < nI)
398 Kokkos::atomic_add(&deg(o), (long)1);
399 });
400 }
401 }
402 space.fence();
403 auto h = Kokkos::create_mirror_view(cnt);
404 Kokkos::deep_copy(h, cnt);
405 auto hd = Kokkos::create_mirror_view(deg);
406 Kokkos::deep_copy(hd, deg);
407 movingCutCells_.assign((std::size_t)nI, 0);
408 movingDegenerate_.assign((std::size_t)nI, 0);
409 for (int i = 0; i < nI; ++i) {
410 movingCutCells_[(std::size_t)i] = h(i);
411 movingDegenerate_[(std::size_t)i] = hd(i);
412 }
413#ifdef PECLET_FLOW_MPI
414 // A body lives on SOME rank: the count is global, and only rank 0 speaks.
415 if (distributed_) {
416 std::vector<long> g((std::size_t)nI, 0);
417 MPI_Allreduce(movingCutCells_.data(), g.data(), nI, MPI_LONG, MPI_SUM, comm_);
418 movingCutCells_ = g;
419 MPI_Allreduce(movingDegenerate_.data(), g.data(), nI, MPI_LONG, MPI_SUM, comm_);
420 movingDegenerate_ = g;
421 int rank = 0;
422 MPI_Comm_rank(comm_, &rank);
423 if (rank != 0)
424 return;
425 }
426#endif
427 for (int i = 0; i < nI; ++i) {
428 const bool moves =
429 instLin_[3 * (std::size_t)i] != 0.0 || instLin_[3 * (std::size_t)i + 1] != 0.0 ||
430 instLin_[3 * (std::size_t)i + 2] != 0.0 || instAng_[3 * (std::size_t)i] != 0.0 ||
431 instAng_[3 * (std::size_t)i + 1] != 0.0 || instAng_[3 * (std::size_t)i + 2] != 0.0;
432 if (moves && movingCutCells_[(std::size_t)i] == 0)
433 std::fprintf(stderr,
434 "peclet.flow set_solid_from_scene WARNING: instance %d has a velocity but owns "
435 "NO cut row of the momentum operator (sub-cell body?): the wall velocity "
436 "cannot enter and the body behaves as a STATIONARY wall. "
437 "moving_instance_cut_cells() returns the counts.\n",
438 i);
439 if (moves && movingDegenerate_[(std::size_t)i] > 0)
440 std::fprintf(stderr,
441 "peclet.flow set_solid_from_scene WARNING: instance %d has a velocity and its "
442 "surface passes EXACTLY through %ld staggered velocity points (a face on a "
443 "lattice plane). Such points are fluid to the mask and not ghosts to the "
444 "cut-cell fold, so the wall datum never enters there and the face acts as a "
445 "STATIONARY wall. Shift the body off the lattice (any fractional offset). "
446 "moving_instance_degenerate_points() returns the counts.\n",
447 i, movingDegenerate_[(std::size_t)i]);
448 }
449}
450
451template <class Grid>
452std::vector<long> Solver<Grid>::movingInstanceCutCells() const {
453 return movingCutCells_;
454}
455
456template <class Grid>
458 return movingDegenerate_;
459}
460
461template <class Grid>
463 return imageOverlapCells_;
464}
465
466template <class Grid>
468 if (!hasScene_)
469 throw std::runtime_error("set_exact_crossings_from_scene: call set_scene first");
470 const std::size_t n = (std::size_t)nx_ * ny_ * nz_;
471 const auto q = sceneQ_->view();
472 const int nx = nx_, ny = ny_, nz = nz_;
473 const C3 og = og_;
474 const SceneMap sm = sceneMap();
476 for (int c = 0; c < 3; ++c) {
477 // Component c's sample placement comes from the GRID POLICY: staggered puts it on the low
478 // face along axis c (offset -1/2 there), collocated at the cell center (offset 0) -- the
479 // collocated ghost projection consumes tEx_[c][c] at CENTERS, so hardcoding the staggered
480 // offsets here would silently compute crossings from the wrong points on that path.
481 const auto po = Grid::offset(c);
482 const double offc[3] = {(double)po.x, (double)po.y, (double)po.z};
483 for (int a = 0; a < 3; ++a) {
484 tEx_[c][a] = CCField("tEx", n);
485 CCField t = tEx_[c][a];
486 const double ox = offc[0], oy = offc[1], oz = offc[2];
487 const int aa = a;
488 Kokkos::parallel_for(
489 "peclet::flow::scene_crossings", MDRange3<CCExec>(space, {0, 0, 0}, {nx, ny, nz}),
490 KOKKOS_LAMBDA(int x, int y, int z) {
491 // The segment is ONE CELL long along `aa`; `s` stays the dimensionless fraction in
492 // (0,1) the consumer expects, so only the endpoints and the step become physical.
493 const double px = sm.a[0] + sm.b[0] * ((double)(x + og.x) + ox),
494 py = sm.a[1] + sm.b[1] * ((double)(y + og.y) + oy),
495 pz = sm.a[2] + sm.b[2] * ((double)(z + og.z) + oz);
496 const double dx = aa == 0 ? sm.b[0] : 0.0, dy = aa == 1 ? sm.b[1] : 0.0,
497 dz = aa == 2 ? sm.b[2] : 0.0;
498 const auto f = [&](double s) {
499 return q.eval(peclet::core::Vec3<double>{px + s * dx, py + s * dy, pz + s * dz});
500 };
501 const double f0 = f(0.0), f1 = f(1.0);
502 const std::size_t idx =
503 (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
504 if ((f0 < 0.0) == (f1 < 0.0)) { // no sign change -> no crossing on this segment
505 t(idx) = Kokkos::Experimental::quiet_NaN_v<double>;
506 return;
507 }
508 double lo = 0.0, hi = 1.0, flo = f0;
509 for (int it = 0; it < 52; ++it) { // bisection to ~1 ulp of the unit interval
510 const double mid = 0.5 * (lo + hi);
511 const double fm = f(mid);
512 if ((fm < 0.0) == (flo < 0.0)) {
513 lo = mid;
514 flo = fm;
515 } else {
516 hi = mid;
517 }
518 }
519 t(idx) = 0.5 * (lo + hi);
520 });
521 }
522 }
523 space.fence();
524 hasExactCross_ = true;
525 sceneCrossings_ = true; // scene-derived: valid on every rank, unlike the host override path
526}
527
528template <class Grid>
530 namespace g = peclet::core::geom;
531 g::PeriodicBox<double> box{sceneExtent_.x, sceneExtent_.y, sceneExtent_.z, scenePeriodic_};
532 sceneQ_ = std::make_shared<g::SceneQueryDevice<double, CCMem>>(
533 g::SceneQueryDevice<double, CCMem>::build(*sceneB_, sceneOrigin_, sceneExtent_, box));
534}
535
536template <class Grid>
538 hasMotion_ = false;
539 for (std::size_t k = 0; k < instLin_.size(); ++k)
540 if (instLin_[k] != 0.0 || instAng_[k] != 0.0)
541 hasMotion_ = true;
542}
543
544template <class Grid>
546 const std::size_t m = (std::size_t)nInst_ * 3;
547 if (instCenD_.extent(0) != m) {
548 instCenD_ = Kokkos::View<double*, CCMem>("instCen", m);
549 instLinD_ = Kokkos::View<double*, CCMem>("instLin", m);
550 instAngD_ = Kokkos::View<double*, CCMem>("instAng", m);
551 }
552 if (m == 0)
553 return;
554 using HostConst =
555 Kokkos::View<const double*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>>;
556 Kokkos::deep_copy(instCenD_, HostConst(instCen_.data(), m));
557 Kokkos::deep_copy(instLinD_, HostConst(instLin_.data(), m));
558 Kokkos::deep_copy(instAngD_, HostConst(instAng_.data(), m));
559}
560
561template <class Grid>
562peclet::core::geom::InstanceMotionView<double> Solver<Grid>::motionView() const {
563 peclet::core::geom::InstanceMotionView<double> mv;
564 mv.cen = instCenD_.data();
565 mv.lin = instLinD_.data();
566 mv.ang = instAngD_.data();
567 mv.n = nInst_;
568 return mv;
569}
570
571template <class Grid>
573 if (!hasScene_ || !hasMotion_) {
574 // Never moved: the fields stay EMPTY and every consumer takes its old, bit-identical path.
575 // MOVED AND THEN STOPPED is different, and was wrong: wallVelView() keys off the field's
576 // extent, not hasMotion_, so a previously-built uBc_ would keep being folded into the
577 // momentum operator's inhomogeneity after the caller set the velocity back to zero. Zero
578 // them instead of stranding them. Allocation state is unchanged either way, so a run that
579 // never moves is bit-identical to before.
580 for (int c = 0; c < 3; ++c) {
581 if (uBc_[c].extent(0) == n_)
582 Kokkos::deep_copy(uBc_[c], 0.0);
583 if (uwCell_[c].extent(0) == n_)
584 Kokkos::deep_copy(uwCell_[c], 0.0);
585 }
586 return;
587 }
588 for (int c = 0; c < 3; ++c) {
589 if (uBc_[c].extent(0) != n_)
590 uBc_[c] = CCField("uBc", n_);
591 if (uwCell_[c].extent(0) != n_)
592 uwCell_[c] = CCField("uwCell", n_);
593 }
594 const auto q = sceneQ_->view();
595 const auto mv = motionView();
596 CCConst sd = CCConst(sdf_);
597 const C3 e = e_, og = og_;
598 const SceneMap sm = sceneMap();
600 using MD = MDRange3<CCExec>;
601 // rung 2: component c's own staggered point, storing component c
602 for (int c = 0; c < 3; ++c) {
603 const auto po = Grid::offset(c);
604 const double ox = po.x, oy = po.y, oz = po.z;
605 CCField out = uBc_[c];
606 const int cc = c;
607 Kokkos::parallel_for(
608 "peclet::flow::wall_velocity_stag", MD(space, {0, 0, 0}, {e.x, e.y, e.z}),
609 KOKKOS_LAMBDA(int lx, int ly, int lz) {
610 const long i = (long)lx + (long)ly * e.x + (long)lz * (long)e.x * e.y;
611 const double sx = (double)lx + ox, sy = (double)ly + oy, sz = (double)lz + oz;
612 const double s0 = ccSampleExt(sd, e, sx, sy, sz);
613 const peclet::core::Vec3<double> grad{
614 0.5 * (ccSampleExt(sd, e, sx + 1, sy, sz) - ccSampleExt(sd, e, sx - 1, sy, sz)),
615 0.5 * (ccSampleExt(sd, e, sx, sy + 1, sz) - ccSampleExt(sd, e, sx, sy - 1, sz)),
616 0.5 * (ccSampleExt(sd, e, sx, sy, sz + 1) - ccSampleExt(sd, e, sx, sy, sz - 1))};
617 // wallPoint works on the INDEX level set (sd/grad are index-space); map the point and
618 // the wall foot into the scene's coordinates for the instance query, and the scene's
619 // physical wall velocity back into the index velocity the momentum datum carries.
620 const peclet::core::Vec3<double> pi{sx - G + og.x, sy - G + og.y, sz - G + og.z};
621 const peclet::core::Vec3<double> wi = peclet::core::geom::wallPoint(pi, s0, grad);
622 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * pi.x, sm.a[1] + sm.b[1] * pi.y,
623 sm.a[2] + sm.b[2] * pi.z};
624 const peclet::core::Vec3<double> w{sm.a[0] + sm.b[0] * wi.x, sm.a[1] + sm.b[1] * wi.y,
625 sm.a[2] + sm.b[2] * wi.z};
626 const peclet::core::Vec3<double> v =
627 peclet::core::geom::instanceVelocity(mv, q.owner(p), w, q.box);
628 out(i) = cc == 0 ? v.x * sm.velToInt[0]
629 : (cc == 1 ? v.y * sm.velToInt[1] : v.z * sm.velToInt[2]);
630 });
631 }
632 // rung 3: the whole wall velocity at cell centres (the wall-flux divergence source)
633 {
634 CCField ux = uwCell_[0], uy = uwCell_[1], uz = uwCell_[2];
635 Kokkos::parallel_for(
636 "peclet::flow::wall_velocity_cell", MD(space, {0, 0, 0}, {e.x, e.y, e.z}),
637 KOKKOS_LAMBDA(int lx, int ly, int lz) {
638 const long i = (long)lx + (long)ly * e.x + (long)lz * (long)e.x * e.y;
639 const double sx = lx, sy = ly, sz = lz;
640 const double s0 = ccSampleExt(sd, e, sx, sy, sz);
641 const peclet::core::Vec3<double> grad{
642 0.5 * (ccSampleExt(sd, e, sx + 1, sy, sz) - ccSampleExt(sd, e, sx - 1, sy, sz)),
643 0.5 * (ccSampleExt(sd, e, sx, sy + 1, sz) - ccSampleExt(sd, e, sx, sy - 1, sz)),
644 0.5 * (ccSampleExt(sd, e, sx, sy, sz + 1) - ccSampleExt(sd, e, sx, sy, sz - 1))};
645 const peclet::core::Vec3<double> pi{sx - G + og.x, sy - G + og.y, sz - G + og.z};
646 const peclet::core::Vec3<double> wi = peclet::core::geom::wallPoint(pi, s0, grad);
647 const peclet::core::Vec3<double> p{sm.a[0] + sm.b[0] * pi.x, sm.a[1] + sm.b[1] * pi.y,
648 sm.a[2] + sm.b[2] * pi.z};
649 const peclet::core::Vec3<double> w{sm.a[0] + sm.b[0] * wi.x, sm.a[1] + sm.b[1] * wi.y,
650 sm.a[2] + sm.b[2] * wi.z};
651 const peclet::core::Vec3<double> v =
652 peclet::core::geom::instanceVelocity(mv, q.owner(p), w, q.box);
653 ux(i) = v.x * sm.velToInt[0];
654 uy(i) = v.y * sm.velToInt[1];
655 uz(i) = v.z * sm.velToInt[2];
656 });
657 }
658 space.fence();
659 // GHOST PLANES (gate 7, 2026-09-02). Both kernels above take a centred difference of the
660 // SAMPLED sdf through ccSampleExt, which CLAMPS its indices: on the outermost ghost plane the
661 // gradient is wrong, and buildAdvInputs then writes that plane into the advection scratch
662 // where the SOU stencil (reach 2) carries it inward. Under MPI that plane is a neighbour's
663 // interior, computed there with a full stencil; single-rank periodic it is the wrapped
664 // interior plane. Either way the exchange is exact: fill the ghosts from the owner. Measured
665 // before the fix: np=2/4 max|du| 1.45e-07 / 1.14e-05 against 3e-7 (bit-exact at np=1), up to
666 // 3.5 % of max|u| with a body parked on a rank cut. Static scenes never reach this code.
667 for (int c = 0; c < 3; ++c) {
668 exchangeExtRaw(uBc_[c]);
669 exchangeExtRaw(uwCell_[c]);
670 }
671}
672
673template <class Grid>
675#ifdef PECLET_FLOW_MPI
676 if (distributed_) {
677 velDev_->exchange(f);
678 return;
679 }
680#endif
681 fillDomainPeriodic(f); // the periodic axes (§14 H-3(d): one launch when there are two or three)
682}
683
684template <class Grid>
686 if (!hasScene_ || !hasMotion_ || !wallFluxDiv_ || uwCell_[0].extent(0) != n_)
687 return;
689 const C3 e = e_;
690 CCConst oxv = CCConst(ox_), oyv = CCConst(oy_), ozv = CCConst(oz_);
691 CCConst wx = CCConst(uwCell_[0]), wy = CCConst(uwCell_[1]), wz = CCConst(uwCell_[2]);
692 using MD = MDRange3<CCExec>;
693 Kokkos::parallel_for(
694 "peclet::flow::wall_flux_div", MD(space, {G, G, G}, {e.x - G, e.y - G, e.z - G}),
695 KOKKOS_LAMBDA(int x, int y, int z) {
696 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
697 const long i = (long)x + (long)y * sy + (long)z * sz;
698 const double ax = -(oxv(i + sx) - oxv(i));
699 const double ay = -(oyv(i + sy) - oyv(i));
700 const double az = -(ozv(i + sz) - ozv(i));
701 d(i) += wx(i) * ax + wy(i) * ay + wz(i) * az;
702 });
703 space.fence();
704}
705
706} // namespace peclet::flow
707
708#endif // PECLET_FLOW_FLOW_IBM_SCENE_HPP
void setInstanceTransform(int i, const std::array< double, 3 > &translation, const std::array< double, 4 > &quat)
Move one instance (Layer 3 rung 4).
bool hasMovingInstance() const
True when at least one instance carries a nonzero velocity – i.e.
void fillDomainPeriodic(CCField f)
void refreshWallVelocity()
Re-derive ONLY the wall-velocity fields and the momentum operator that folds them in.
void checkMovingInstancesAreCut()
A MOVING instance whose surface produces no fractional face aperture has no path for its wall velocit...
std::vector< long > movingInstanceDegeneratePoints() const
Per instance: staggered points where the sampled sdf is exactly zero (see the warning).
void rebuildGeometry()
Re-derive ALL geometry from the current instance transforms (Layer 3 rung 4): rebuild the accelerated...
void setSolidFromScene(bool cutcellPressure)
Sample the scene onto this rank's inner grid and install it as the solid, entirely on device – no nx*...
void exchangeExtRaw(CCField f)
Raw ghost fill of an extended-block field: the rank halo exchange under MPI, the periodic wrap single...
std::vector< int > getCutOwner() const
Per-inner-cell owning instance (Layer 3 rung 1), x-fastest, -1 where no scene has been sampled yet.
void addWallFluxDivergence(CCField d)
long periodicImageOverlapCells() const
Cells on this rank whose solid/fluid sign was set by a periodic IMAGE of an instance wider than the b...
void setWallFluxDivergence(bool on)
Rung 3 on/off.
void setInstanceMotion(int i, const std::array< double, 3 > &lin, const std::array< double, 3 > &ang, const double *center=nullptr)
Rigid-body motion of one scene instance (Layer 3 rung 2).
std::array< double, 3 > instanceCenter(int i) const
The resolved centre of rotation of instance i and whether it is pinned (explicit) or follows the body...
bool wallFluxDivergence() const
std::vector< long > movingInstanceCutCells() const
Per instance: cut rows of the momentum operator (all three components) at inner points this rank owns...
static constexpr int G
Definition flow_ibm.hpp:80
void seedFreshCells(CCField mOld[3])
FRESH CELLS: the points a moving body has just uncovered.
SceneMap sceneMap() const
void setSolidDevice(CCField din, bool cutcellPressure)
Device entry point (Layer 2): the inner SDF is ALREADY on device, so geometry never round-trips throu...
peclet::core::geom::InstanceMotionView< double > motionView() const
void setExactCrossingsFromScene()
EXACT wall crossings straight from the scene, on device, on every rank – the in-solver replacement fo...
void setScene(const std::vector< int > &nodeInts, const std::vector< double > &nodeReals, const std::vector< int > &instInts, const std::vector< double > &instReals, bool periodic=false)
Install an analytic scene from core's flat node/instance encoding (peclet/core/geom/scene_builder....
void setFreshCellSeed(bool on)
Fresh-cell policy for moving geometry.
bool instanceCenterPinned(int i) const
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
double ccSampleExt(CCConst sdf, C3 ext, double x, double y, double z)
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 org[3]
physical lower corner of the GLOBAL inner grid
Definition flow_ibm.hpp:139
bool physical
an extent was given (the scales are armed)
Definition flow_ibm.hpp:137
long cells[3]
the GLOBAL cell counts the extent spans
Definition flow_ibm.hpp:141
double ext[3]
physical extent of the GLOBAL inner grid
Definition flow_ibm.hpp:140