peclet.voro 1.0.4
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
tess_grid.hpp
Go to the documentation of this file.
1
18#ifndef PECLET_VORO_TESS_GRID_HPP
19#define PECLET_VORO_TESS_GRID_HPP
20
21#include <algorithm>
22#include <cmath>
23#include <cstdint>
24#include <Kokkos_Core.hpp>
25#include <string>
26#include <vector>
27
28#include "morton/morton.hpp" // suite spatial-index primitive (after Kokkos_Core: MORTON_HD->KOKKOS_FUNCTION)
29#include "peclet/core/common/view.hpp"
30#include "peclet/voro/params.hpp" // kSearchWindow
31#include "peclet/voro/tessellation_view.hpp" // gid_t
32
33namespace peclet::voro {
34
38constexpr int kWlOffBias = 128;
39
46KOKKOS_INLINE_FUNCTION int morton3(int x, int y, int z) {
47 return static_cast<int>(morton::Morton<3, 21>::encode(static_cast<std::uint32_t>(x),
48 static_cast<std::uint32_t>(y),
49 static_cast<std::uint32_t>(z))
50 .code());
51}
52
55template <class Real>
56struct TessGrid {
57 using MemSpace = peclet::core::MemSpace;
58 // Grid-sorted inputs (read by the gather).
59 Kokkos::View<int*, MemSpace> binned; // N: grid-sorted slot -> original seed index
60 Kokkos::View<int*, MemSpace>
61 slotOf; // N: original seed index -> grid-sorted slot (inverse of binned)
62 Kokkos::View<Real*, MemSpace> posSorted; // 3N (grid order)
63 Kokkos::View<Real*, MemSpace> wSorted; // N (Power) or 0
64 Kokkos::View<gid_t*, MemSpace> gidSorted; // N (multi-domain) or 0
65 Kokkos::View<int*, MemSpace> cellStart; // ncellEff+1
66 // Per-sub-position worklist (both backends).
67 Kokkos::View<int*, MemSpace> wlOff; // nSub*nOff : packed (dx,dy,dz)+kWlOffBias
68 Kokkos::View<Real*, MemSpace> wlRmin; // nSub*nOff : nearest-corner dist^2 (sorted)
69 // Grid scalars.
70 Real icx = 0, icy = 0, icz = 0, Lx = 0, Ly = 0, Lz = 0, minCsz = 0;
71 int dimx = 0, dimy = 0, dimz = 0, sw = 0, nOff = 0, wlS = 0, N = 0;
72 bool useMorton = false, haveGid = false;
73};
74
82template <class Real>
84 Kokkos::View<int*, peclet::core::MemSpace> wlOff;
85 Kokkos::View<Real*, peclet::core::MemSpace> wlRmin;
86 int nOff = 0, wlS = 0, dimx = -1, dimy = -1, dimz = -1, sw = -1;
87 Real cszx = 0, cszy = 0, cszz = 0;
88 bool valid = false;
89 bool matches(int dx, int dy, int dz, int s, Real cx, Real cy, Real cz) const {
90 return valid && dimx == dx && dimy == dy && dimz == dz && sw == s && cszx == cx && cszy == cy &&
91 cszz == cz;
92 }
93};
94
102template <class Real, bool Weighted>
103TessGrid<Real> buildTessGrid(const Kokkos::View<Real*, peclet::core::MemSpace>& posFlat,
104 const Kokkos::View<Real*, peclet::core::MemSpace>& weight, int N,
105 const Real L[3], int sw = kSearchWindow, int densityCount = -1,
106 Kokkos::View<long*, peclet::core::MemSpace> gid = {},
107 WorklistCache<Real>* wlc = nullptr) {
108 using peclet::core::MemSpace;
109 using Exec = peclet::core::ExecSpace;
110 TessGrid<Real> grid;
111 grid.N = N;
112 grid.sw = sw;
113 // Optional global ids: skip a candidate sharing the cell's own id (periodic self-image).
114 const bool haveGid = gid.extent(0) == static_cast<size_t>(N);
115 grid.haveGid = haveGid;
116
117 // --- grid dimensions: ~kSeedsPerCell seeds per cell ---
118 // A coarser grid than 1 seed/cell makes the neighbour gather touch fewer, denser
119 // grid cells whose seeds are contiguous in the grid-sorted arrays — far fewer
120 // scattered cellStart/posSorted lookups (the gather is memory-latency bound, the
121 // dominant serial cost; voro++ packs ~8 particles/block for the same reason). ~2/cell
122 // measured the best correct trade-off (coarser over-gathers per cell). The expanding
123 // search + the `sw` cap keep cells complete regardless. This is a HOST (CPU) win
124 // only: on a GPU the gather is bandwidth/latency-hidden, not cache-bound, so a coarser
125 // grid just adds per-thread candidate work — GPUs keep 1/cell. Power also keeps 1/cell
126 // (its no-early-out full-sphere gather would blow past MAXCAND at 2/cell).
127 constexpr bool kHostBackend = Kokkos::SpaceAccessibility<Kokkos::HostSpace, MemSpace>::accessible;
128 constexpr Real kSeedsPerCell = (!Weighted && kHostBackend) ? Real(2) : Real(1);
129 const int dens = densityCount > 0 ? densityCount : N;
130 const Real vol = L[0] * L[1] * L[2];
131 const Real spacing = std::cbrt(kSeedsPerCell * vol / Real(dens > 0 ? dens : 1));
132 int dim[3];
133 Real csz[3], invcsz[3];
134 for (int k = 0; k < 3; ++k) {
135 dim[k] = (int)Kokkos::floor(L[k] / spacing);
136 if (dim[k] < 1)
137 dim[k] = 1;
138 csz[k] = L[k] / dim[k];
139 invcsz[k] = Real(1) / csz[k];
140 }
141 const int dimx = dim[0], dimy = dim[1], dimz = dim[2];
142 // The periodic search window must not wrap onto itself: the offset shell reaches sw + 1 cells
143 // each way, so 2(sw + 1) + 1 ≤ min dim (a too-large sw on a small seed set — the pore-mesh
144 // optimiser's default sw = 6 on ~500 seeds — read out of the grid and segfaulted).
145 {
146 const int minDim = std::min({dimx, dimy, dimz});
147 const int swMax = std::max(1, (minDim - 1) / 2 - 1);
148 if (sw > swMax)
149 sw = swMax;
150 grid.sw = sw;
151 }
152 const int ncell = dimx * dimy * dimz;
153
154 // Morton (Z-order) cell indexing clusters each cell's spatial neighbourhood in memory.
155 // GPU ONLY: the spatial order improves the gather's memory coalescing (measured +2.5%
156 // pure / +18% with force geometry on an RTX 5080), and the per-cell encode is hidden by
157 // spare ALU. On the CPU it is a net loss — the encode in the hot gather loop is not
158 // hidden and the ~2-seeds/cell density already supplies the cache locality — so the CPU
159 // keeps the compact linear index. Morton needs a power-of-two-padded range (2^mbits per
160 // axis); also fall back to linear if that padding would inflate the cell array too much
161 // (very rectangular boxes / huge grids).
162 int mbits = 1;
163 {
164 int md = dimx > dimy ? dimx : dimy;
165 md = md > dimz ? md : dimz;
166 while ((1 << mbits) < md)
167 ++mbits;
168 }
169 const size_t mortonNcell = (size_t)1 << (3 * mbits);
170 const bool useMorton = !kHostBackend && mortonNcell <= (size_t)8 * (size_t)ncell;
171 const int ncellEff = useMorton ? (int)mortonNcell : ncell;
172
173 const Real Lx = L[0], Ly = L[1], Lz = L[2];
174 const Real icx = invcsz[0], icy = invcsz[1], icz = invcsz[2];
175 Real minCsz = csz[0] < csz[1] ? csz[0] : csz[1];
176 minCsz = minCsz < csz[2] ? minCsz : csz[2];
177 grid.icx = icx;
178 grid.icy = icy;
179 grid.icz = icz;
180 grid.Lx = Lx;
181 grid.Ly = Ly;
182 grid.Lz = Lz;
183 grid.minCsz = minCsz;
184 grid.dimx = dimx;
185 grid.dimy = dimy;
186 grid.dimz = dimz;
187 grid.useMorton = useMorton;
188
189 // --- counting sort: bin seeds into grid cells (Morton- or linear-indexed) ---
190 Kokkos::View<int*, MemSpace> cellOf("cellOf", N);
191 Kokkos::View<int*, MemSpace> counts("counts", ncellEff + 1);
192 Kokkos::deep_copy(counts, 0);
193 Kokkos::parallel_for(
194 "tess.bin", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(const int i) {
195 int gx = (int)Kokkos::floor(posFlat(3 * i + 0) * icx);
196 int gy = (int)Kokkos::floor(posFlat(3 * i + 1) * icy);
197 int gz = (int)Kokkos::floor(posFlat(3 * i + 2) * icz);
198 gx = ((gx % dimx) + dimx) % dimx;
199 gy = ((gy % dimy) + dimy) % dimy;
200 gz = ((gz % dimz) + dimz) % dimz;
201 int c = useMorton ? morton3(gx, gy, gz) : (gx + gy * dimx + gz * dimx * dimy);
202 cellOf(i) = c;
203 Kokkos::atomic_inc(&counts(c));
204 });
205
206 Kokkos::View<int*, MemSpace> cellStart("cellStart", ncellEff + 1);
207 Kokkos::parallel_scan(
208 "tess.scan", Kokkos::RangePolicy<Exec>(0, ncellEff + 1),
209 KOKKOS_LAMBDA(const int c, int& acc, const bool fin) {
210 int v = (c < ncellEff) ? counts(c) : 0;
211 if (fin)
212 cellStart(c) = acc;
213 acc += v;
214 });
215
216 Kokkos::View<int*, MemSpace> cursor("cursor", ncellEff);
217 Kokkos::parallel_for(
218 "tess.cursor", Kokkos::RangePolicy<Exec>(0, ncellEff),
219 KOKKOS_LAMBDA(const int c) { cursor(c) = cellStart(c); });
220 Kokkos::View<int*, MemSpace> binned("binned", N);
221 Kokkos::parallel_for(
222 "tess.scatter", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(const int i) {
223 int slot = Kokkos::atomic_fetch_add(&cursor(cellOf(i)), 1);
224 binned(slot) = i;
225 });
226
227 // Reorder seed data into grid (binned) order. The build then gathers neighbours
228 // from contiguous memory instead of chasing scattered original indices through
229 // posFlat — the large-N cache-locality win (voro++ processes points block-by-block
230 // for the same reason). Outputs are still written back to the original cell index
231 // (= binned(p)), so the cell i == particle i contract is unchanged. `slotOf` records
232 // the inverse map so the subset gather can address cells by their ORIGINAL index.
233 using Kokkos::view_alloc;
234 using Kokkos::WithoutInitializing;
235 Kokkos::View<Real*, MemSpace> posSorted(view_alloc(std::string("posSorted"), WithoutInitializing),
236 (size_t)N * 3);
237 Kokkos::View<int*, MemSpace> slotOf(view_alloc(std::string("slotOf"), WithoutInitializing), N);
238 Kokkos::View<gid_t*, MemSpace> gidSorted(
239 view_alloc(std::string("gidSorted"), WithoutInitializing), haveGid ? N : 0);
240 Kokkos::View<Real*, MemSpace> wSorted(view_alloc(std::string("wSorted"), WithoutInitializing),
241 Weighted ? N : 0);
242 {
243 const bool haveGidL = haveGid;
244 Kokkos::parallel_for(
245 "tess.reorder", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(const int p) {
246 int o = binned(p);
247 slotOf(o) = p;
248 posSorted(3 * p + 0) = posFlat(3 * o + 0);
249 posSorted(3 * p + 1) = posFlat(3 * o + 1);
250 posSorted(3 * p + 2) = posFlat(3 * o + 2);
251 if (haveGidL)
252 gidSorted(p) = gid(o);
253 if (Weighted)
254 wSorted(p) = weight(o);
255 });
256 }
257 grid.binned = binned;
258 grid.slotOf = slotOf;
259 grid.posSorted = posSorted;
260 grid.gidSorted = gidSorted;
261 grid.wSorted = wSorted;
262 grid.cellStart = cellStart;
263
264 // --- per-sub-position worklist (computed once) ---
265 // The (dx,dy,dz) grid offsets inside the coverage sphere (nearest-corner dist^2 <= sw^2). For
266 // the wlS^3 sub-region a seed lands in, they are sorted by nearest-corner dist^2 (wlRmin,
267 // absolute) and packed (kWlOffBias). The Voronoi build walks this presorted list and breaks
268 // once wlRmin exceeds the security radius (4*rSqMax) — a table lookup, no per-block geometry.
269 std::vector<int> offHx, offHy, offHz;
270 {
271 const int swInit = sw < 2 ? sw : 2;
272 const int sw2 = sw * sw;
273 for (int swl = swInit; swl <= sw; ++swl) {
274 const int lo2 = (swl == swInit) ? -1 : (swl - 1) * (swl - 1);
275 const int hi2 = swl * swl;
276 for (int dz = -(swl + 1); dz <= swl + 1; ++dz) {
277 const int ez = dz < -1 ? -dz - 1 : (dz > 1 ? dz - 1 : 0);
278 for (int dy = -(swl + 1); dy <= swl + 1; ++dy) {
279 const int ey = dy < -1 ? -dy - 1 : (dy > 1 ? dy - 1 : 0);
280 for (int dx = -(swl + 1); dx <= swl + 1; ++dx) {
281 const int ex = dx < -1 ? -dx - 1 : (dx > 1 ? dx - 1 : 0);
282 const int d2 = ex * ex + ey * ey + ez * ez;
283 if (d2 <= lo2 || d2 > hi2 || d2 > sw2)
284 continue;
285 offHx.push_back(dx);
286 offHy.push_back(dy);
287 offHz.push_back(dz);
288 }
289 }
290 }
291 }
292 }
293 const int nOff = (int)offHx.size();
294 const int wlS = 3; // worklist sub-grid per axis
295 const int nSub = wlS * wlS * wlS;
296 grid.nOff = nOff;
297 grid.wlS = wlS;
298 // E3: the worklist table is a pure function of (grid dims, sw, cell sizes). If the cache already
299 // holds it for this key, reuse the device Views and skip the host sort + two H2D copies entirely.
300 if (wlc && wlc->matches(grid.dimx, grid.dimy, grid.dimz, sw, csz[0], csz[1], csz[2])) {
301 grid.wlOff = wlc->wlOff;
302 grid.wlRmin = wlc->wlRmin;
303 return grid;
304 }
305 Kokkos::View<int*, MemSpace> wlOff(view_alloc(std::string("wlOff"), WithoutInitializing),
306 (size_t)nSub * nOff);
307 Kokkos::View<Real*, MemSpace> wlRmin(view_alloc(std::string("wlRmin"), WithoutInitializing),
308 (size_t)nSub * nOff);
309 {
310 auto hOff = Kokkos::create_mirror_view(wlOff);
311 auto hRmin = Kokkos::create_mirror_view(wlRmin);
312 const Real cszx = csz[0], cszy = csz[1], cszz = csz[2];
313 // axis-aligned gap^2 between query sub-interval [a0,a1] and target block [b0,b1].
314 auto axisGap = [](Real a0, Real a1, Real b0, Real b1) {
315 Real g = std::max(Real(0), std::max(b0 - a1, a0 - b1));
316 return g * g;
317 };
318 std::vector<int> idx(nOff);
319 std::vector<Real> rmn(nOff);
320 for (int sz = 0; sz < wlS; ++sz)
321 for (int sy = 0; sy < wlS; ++sy)
322 for (int sx = 0; sx < wlS; ++sx) {
323 const int p = sx + wlS * (sy + wlS * sz);
324 const Real ax0 = (Real)sx / wlS * cszx, ax1 = (Real)(sx + 1) / wlS * cszx;
325 const Real ay0 = (Real)sy / wlS * cszy, ay1 = (Real)(sy + 1) / wlS * cszy;
326 const Real az0 = (Real)sz / wlS * cszz, az1 = (Real)(sz + 1) / wlS * cszz;
327 for (int k = 0; k < nOff; ++k) {
328 const Real bx0 = offHx[k] * cszx, bx1 = (offHx[k] + 1) * cszx;
329 const Real by0 = offHy[k] * cszy, by1 = (offHy[k] + 1) * cszy;
330 const Real bz0 = offHz[k] * cszz, bz1 = (offHz[k] + 1) * cszz;
331 rmn[k] = axisGap(ax0, ax1, bx0, bx1) + axisGap(ay0, ay1, by0, by1) +
332 axisGap(az0, az1, bz0, bz1);
333 idx[k] = k;
334 }
335 std::sort(idx.begin(), idx.end(), [&](int a, int b) { return rmn[a] < rmn[b]; });
336 for (int g = 0; g < nOff; ++g) {
337 const int k = idx[g];
338 const size_t slot = (size_t)p * nOff + g;
339 hOff(slot) = (offHx[k] + kWlOffBias) | ((offHy[k] + kWlOffBias) << 8) |
340 ((offHz[k] + kWlOffBias) << 16);
341 hRmin(slot) = rmn[k];
342 }
343 }
344 Kokkos::deep_copy(wlOff, hOff);
345 Kokkos::deep_copy(wlRmin, hRmin);
346 }
347 grid.wlOff = wlOff;
348 grid.wlRmin = wlRmin;
349 if (wlc) { // populate the cache for subsequent same-key calls (E3)
350 wlc->wlOff = wlOff;
351 wlc->wlRmin = wlRmin;
352 wlc->nOff = nOff;
353 wlc->wlS = wlS;
354 wlc->dimx = grid.dimx;
355 wlc->dimy = grid.dimy;
356 wlc->dimz = grid.dimz;
357 wlc->sw = sw;
358 wlc->cszx = csz[0];
359 wlc->cszy = csz[1];
360 wlc->cszz = csz[2];
361 wlc->valid = true;
362 }
363 return grid;
364}
365
366} // namespace peclet::voro
367
368#endif // PECLET_VORO_TESS_GRID_HPP
Device-native Voronoi tessellation, geometry and finite-volume physics.
Definition convex_cell.hpp:41
TessGrid< Real > buildTessGrid(const Kokkos::View< Real *, peclet::core::MemSpace > &posFlat, const Kokkos::View< Real *, peclet::core::MemSpace > &weight, int N, const Real L[3], int sw=kSearchWindow, int densityCount=-1, Kokkos::View< long *, peclet::core::MemSpace > gid={}, WorklistCache< Real > *wlc=nullptr)
Definition tess_grid.hpp:103
constexpr int kWlOffBias
Definition tess_grid.hpp:38
constexpr int kSearchWindow
Definition params.hpp:28
KOKKOS_INLINE_FUNCTION int morton3(int x, int y, int z)
Definition tess_grid.hpp:46
The named engine defaults of peclet.voro — one header-level home for every capacity and default the l...
Definition tess_grid.hpp:56
int dimz
Definition tess_grid.hpp:71
int dimy
Definition tess_grid.hpp:71
Real minCsz
Definition tess_grid.hpp:70
int wlS
Definition tess_grid.hpp:71
int dimx
Definition tess_grid.hpp:71
Kokkos::View< gid_t *, MemSpace > gidSorted
Definition tess_grid.hpp:64
Kokkos::View< Real *, MemSpace > wSorted
Definition tess_grid.hpp:63
Kokkos::View< int *, MemSpace > binned
Definition tess_grid.hpp:59
Kokkos::View< int *, MemSpace > cellStart
Definition tess_grid.hpp:65
Real icz
Definition tess_grid.hpp:70
bool useMorton
Definition tess_grid.hpp:72
int sw
Definition tess_grid.hpp:71
bool haveGid
Definition tess_grid.hpp:72
Real icx
Definition tess_grid.hpp:70
int nOff
Definition tess_grid.hpp:71
Kokkos::View< Real *, MemSpace > posSorted
Definition tess_grid.hpp:62
Kokkos::View< int *, MemSpace > slotOf
Definition tess_grid.hpp:61
int N
Definition tess_grid.hpp:71
peclet::core::MemSpace MemSpace
Definition tess_grid.hpp:57
Real Ly
Definition tess_grid.hpp:70
Real icy
Definition tess_grid.hpp:70
Real Lx
Definition tess_grid.hpp:70
Real Lz
Definition tess_grid.hpp:70
Kokkos::View< Real *, MemSpace > wlRmin
Definition tess_grid.hpp:68
Kokkos::View< int *, MemSpace > wlOff
Definition tess_grid.hpp:67
Definition tess_grid.hpp:83
Real cszz
Definition tess_grid.hpp:87
int dimz
Definition tess_grid.hpp:86
int dimx
Definition tess_grid.hpp:86
int nOff
Definition tess_grid.hpp:86
int dimy
Definition tess_grid.hpp:86
bool matches(int dx, int dy, int dz, int s, Real cx, Real cy, Real cz) const
Definition tess_grid.hpp:89
bool valid
Definition tess_grid.hpp:88
Real cszx
Definition tess_grid.hpp:87
int wlS
Definition tess_grid.hpp:86
Kokkos::View< int *, peclet::core::MemSpace > wlOff
Definition tess_grid.hpp:84
int sw
Definition tess_grid.hpp:86
Real cszy
Definition tess_grid.hpp:87
Kokkos::View< Real *, peclet::core::MemSpace > wlRmin
Definition tess_grid.hpp:85
Published, read-only device data layer for the tessellation (migration §2.1, §3).