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