peclet.voro 1.0.4
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
tessellator.hpp
Go to the documentation of this file.
1
26#ifndef PECLET_VORO_TESSELLATOR_HPP
27#define PECLET_VORO_TESSELLATOR_HPP
28
29#include <array>
30#include <cmath>
31#include <cstdint>
32#include <cstdio>
33#include <Kokkos_Core.hpp>
34#include <string>
35#include <type_traits>
36#include <vector>
37
38#include "morton/morton.hpp" // suite spatial-index primitive (after Kokkos_Core: MORTON_HD->KOKKOS_FUNCTION)
39#include "peclet/core/common/view.hpp"
41#include "peclet/voro/sdf.hpp"
42#include "peclet/voro/tess_grid.hpp" // counting-sort grid + worklist (kWlOffBias, morton3, TessGrid)
44
45namespace peclet::voro {
46
49 kOk = 0,
51 kEmpty = 2,
53 // Power only: a candidate's radical offset d = ½(|r|²+w_i−w_j) went ≤ 0, i.e. the seed lies
54 // outside its own cell w.r.t. that neighbour. The ConvexCell foot-point half-space {n·x ≤ n·n}
55 // always contains the seed, so it cannot represent such a plane; the cut is skipped and the cell
56 // flagged (never silently mis-clipped). Full support needs a generalized (origin-excluding)
57 // half-space representation — a follow-up to this weighted build.
59 // Rung A2a validity diagnostics. kBuried: a power seed lies outside its own cell (d ≤ 0 to some
60 // neighbour) — the engine EMPTIES it, which is wrong in general (the cell exists elsewhere);
61 // never happens for w = r² of non-overlapping spheres. kReachExceeded: the cell's converged
62 // search reach exceeded half the smallest box edge, so the min-image gather may have missed a
63 // non-nearest periodic image and the partition is not guaranteed exact.
64 kBuried = 16,
66};
67
68template <class Real>
71 Kokkos::View<int*, peclet::core::MemSpace> status; // per-cell StatusBit mask
76};
77
96template <class Cell, int MAXF>
97KOKKOS_INLINE_FUNCTION int areaGradEnumerate(const Cell& c, const int* faces, int nf,
98 unsigned char* part, unsigned char* pcnt, bool& full) {
99 constexpr int kMaxFV = Cell::MAXFV;
100 signed char faceOf[256];
101 for (int k = 0; k < c.np; ++k)
102 faceOf[k] = -1;
103 for (int idx = 0; idx < nf; ++idx)
104 faceOf[faces[idx]] = (signed char)idx;
105 for (int idx = 0; idx < nf; ++idx)
106 pcnt[idx] = 0;
107 full = false;
108 for (int t = 0; t < c.nt; ++t) {
109 if (!c.alive[t])
110 continue;
111 const int tp[3] = {c.t0[t], c.t1[t], c.t2[t]};
112 for (int a = 0; a < 3; ++a) {
113 const int fa = faceOf[tp[a]];
114 if (fa < 0)
115 continue;
116 for (int b = 0; b < 3; ++b) {
117 if (b == a)
118 continue;
119 const int fb = faceOf[tp[b]];
120 if (fb < 0)
121 continue;
122 bool seen = false;
123 for (int q = 0; q < pcnt[fa]; ++q)
124 if (part[fa * kMaxFV + q] == (unsigned char)fb) {
125 seen = true;
126 break;
127 }
128 if (seen)
129 continue;
130 if (pcnt[fa] >= kMaxFV) {
131 full = true;
132 continue;
133 }
134 part[fa * kMaxFV + pcnt[fa]] = (unsigned char)fb;
135 ++pcnt[fa];
136 }
137 }
138 }
139 int total = 0;
140 for (int idx = 0; idx < nf; ++idx)
141 total += 1 + pcnt[idx];
142 return total;
143}
144
147template <class Cell, int MAXF, class IView, class RView>
148KOKKOS_INLINE_FUNCTION void areaGradFill(const Cell& c, const int* faces, int nf,
149 const unsigned char* part, const unsigned char* pcnt,
150 int base, int ebase, const IView& oEdgeOff,
151 const IView& oEdgeCnt, const IView& oEdgeFacet,
152 const RView& oEdgeGrad, const RView& oEdgeLen) {
153 using Real = typename RView::non_const_value_type;
154 constexpr int kMaxFV = Cell::MAXFV;
155 // Edge lengths (track C2a′, opt-in when oEdgeLen is sized): each facet-edge slot is visited by
156 // exactly the two dual triangles (cell vertices) it joins; accumulate S = v1 + v2 and
157 // Q = |v1|² + |v2|² in a 4-stride scratch, then |v1 − v2|² = 2Q − |S|² (no per-edge state).
158 const bool wl = oEdgeLen.extent(0) > 0;
159 signed char faceOf[256];
160 for (int k = 0; k < c.np; ++k)
161 faceOf[k] = -1;
162 for (int idx = 0; idx < nf; ++idx)
163 faceOf[faces[idx]] = (signed char)idx;
164 int run = ebase;
165 for (int idx = 0; idx < nf; ++idx) {
166 oEdgeOff(base + idx) = run;
167 oEdgeCnt(base + idx) = 1 + pcnt[idx];
168 oEdgeFacet(run) = base + idx;
169 for (int q = 0; q < pcnt[idx]; ++q)
170 oEdgeFacet(run + 1 + q) = base + part[idx * kMaxFV + q];
171 for (int e = 0; e < 1 + pcnt[idx]; ++e) {
172 for (int cc = 0; cc < 3; ++cc)
173 oEdgeGrad((size_t)(run + e) * 3 + cc) = Real(0);
174 if (wl)
175 for (int cc = 0; cc < 4; ++cc)
176 oEdgeLen((size_t)(run + e) * 4 + cc) = Real(0);
177 }
178 run += 1 + pcnt[idx];
179 }
180 for (int t = 0; t < c.nt; ++t) {
181 if (!c.alive[t])
182 continue;
183 int pl[3];
184 Real contrib[3], grad[3][3][3];
185 c.geomVolumeAreaGrad(t, pl, contrib, grad);
186 for (int a = 0; a < 3; ++a) {
187 const int fa = faceOf[pl[a]];
188 if (fa < 0)
189 continue;
190 const int eoff = oEdgeOff(base + fa);
191 for (int b = 0; b < 3; ++b) {
192 const int fb = faceOf[pl[b]];
193 if (fb < 0)
194 continue;
195 int slot = 0;
196 if (b != a) {
197 slot = -1;
198 for (int q = 0; q < pcnt[fa]; ++q)
199 if (part[fa * kMaxFV + q] == (unsigned char)fb) {
200 slot = 1 + q;
201 break;
202 }
203 if (slot < 0)
204 continue;
205 }
206 const size_t o = (size_t)(eoff + slot) * 3;
207 oEdgeGrad(o + 0) += grad[a][b][0];
208 oEdgeGrad(o + 1) += grad[a][b][1];
209 oEdgeGrad(o + 2) += grad[a][b][2];
210 if (wl && slot > 0) {
211 const size_t l = (size_t)(eoff + slot) * 4;
212 oEdgeLen(l + 0) += c.vx[t];
213 oEdgeLen(l + 1) += c.vy[t];
214 oEdgeLen(l + 2) += c.vz[t];
215 oEdgeLen(l + 3) += c.vx[t] * c.vx[t] + c.vy[t] * c.vy[t] + c.vz[t] * c.vz[t];
216 }
217 }
218 }
219 }
220 if (wl) { // finalize: slot 0 (the facet itself) = 0, partners = |v1 − v2|
221 for (int e = ebase; e < run; ++e) {
222 const size_t l = (size_t)e * 4;
223 const Real S2 = oEdgeLen(l) * oEdgeLen(l) + oEdgeLen(l + 1) * oEdgeLen(l + 1) +
224 oEdgeLen(l + 2) * oEdgeLen(l + 2);
225 const Real d2 = Real(2) * oEdgeLen(l + 3) - S2;
226 oEdgeLen(l) = d2 > Real(0) ? Kokkos::sqrt(d2) : Real(0);
227 }
228 }
229}
230
231template <class Real, bool Weighted, class Sdf, bool TrackAdj = false, int MAXP = kMaxPlanes,
232 int MAXT = kMaxTriangles>
234 using MemSpace = peclet::core::MemSpace;
235 // Cell capacities: 64 planes / 112 dual triangles is the production layout (the topology
236 // store's strides). A static build may ask for more (wall-adapted seed shells around curved
237 // walls need ~100 planes per wall cell — rung C4/B follow-up): buildTessellation<…, MAXP, MAXT>.
238 static constexpr int kMaxP = MAXP; // plane cap (overflow -> kOverflow)
239 static constexpr int kMaxT = MAXT; // dual-triangle (vertex) cap
240 static constexpr int MAXF_TMP = MAXP; // max facets one cell may publish
242 using PlanePolicy = std::conditional_t<Weighted, Power, Voronoi>; // radical vs bisector planes
243
244 // Grid-sorted inputs (read).
245 Kokkos::View<int*, MemSpace> binned;
246 Kokkos::View<Real*, MemSpace> posSorted;
247 Kokkos::View<Real*, MemSpace> wSorted;
248 Kokkos::View<gid_t*, MemSpace> gidSorted;
249 Kokkos::View<int*, MemSpace> cellStart;
250 // Per-sub-position worklist (both backends): for the wlS^3 sub-region a seed lands in, the
251 // block offsets (packed (dx,dy,dz), kWlOffBias) sorted by nearest-corner dist^2 (wlRmin,
252 // absolute). The Voronoi gather walks the presorted list and breaks once wlRmin exceeds the
253 // security radius — a table lookup, no per-block geometry. ConvexCell is lean enough that
254 // the worklist runs on the GPU too (its geometry pass is cheap, so no occupancy penalty).
255 Kokkos::View<int*, MemSpace> wlOff;
256 Kokkos::View<Real*, MemSpace> wlRmin;
257 // Published outputs (write).
258 Kokkos::View<int*, MemSpace> status;
259 Kokkos::View<Real*, MemSpace> cellVol;
260 Kokkos::View<int*, MemSpace> facetCount;
261 Kokkos::View<int*, MemSpace> cellFacetBase;
262 Kokkos::View<int*, MemSpace> oNbr;
263 Kokkos::View<Real*, MemSpace> oArea, oDV, oConn;
264 Kokkos::View<int*, MemSpace> facetCursor;
265 // Grid scalars.
266 Real icx, icy, icz, Lx, Ly, Lz, minCsz;
267 Real wMaxAll; // global max weight (Power only) — bounds the weight-aware worklist reach
270 size_t facetCap;
271 Sdf sdf;
272 // Optional Part-II (moving-points) outputs — emitted only when the views are sized (else left
273 // empty so existing callers are unaffected): the compact resident TOPOLOGY store (np/nt +
274 // pnbr/packed-triangles at kMaxP/kMaxT strides, written from the final clipped cell) and the
275 // per-cell CANDIDATE (skin) list = the neighbour ids this cell examined (within the worklist's
276 // security reach), capped at candCap. Together they let a later step re-evaluate geometry
277 // (ConvexCell::reevalGeometry) and locally repair off the skin list without re-gathering. See
278 // peclet/voro/docs/voronoi_dynamic_update_study.md.
279 Kokkos::View<int*, MemSpace> oNp, oNt, oTopoPnbr;
280 Kokkos::View<unsigned*, MemSpace> oTri;
281 Kokkos::View<unsigned char*, MemSpace>
282 oPoke4; // N*kMaxT*4 local-cert plane set (3 edge-opposite +
283 // 4th-face), emitted only when TrackAdj && emitTopo
284 Kokkos::View<int*, MemSpace> oCand, oCandCnt;
287 // Optional (rung A0): the resident SDF WALL planes of each finalised cell, persisted alongside
288 // the topology so the moving-point path can re-evaluate a wall-clipped cell (a wall plane has no
289 // partner seed to rebuild it from). No-op when the store is unallocated.
291 // Optional (rung A3): the facet-edge AREA-JACOBIAN CSR (TessellationView::edge*). Written only
292 // when `withAreaGrad` (the views are sized); one atomic reservation per cell on `edgeCursor`.
293 static constexpr int kMaxFV = Cell::MAXFV; // edge partners one face may have
294 Kokkos::View<int*, MemSpace> oEdgeOff, oEdgeCnt, oEdgeFacet;
295 Kokkos::View<Real*, MemSpace> oEdgeGrad;
296 Kokkos::View<Real*, MemSpace> oEdgeLen; // 4-stride scratch → edge lengths (C2a′)
297 Kokkos::View<int*, MemSpace> edgeCursor;
298 size_t edgeCap = 0;
299 bool withAreaGrad = false;
300 // Optional (moving-point certificate completeness): per-cell NEAR-MISS list — the examined
301 // candidates whose plane did NOT cut but came within `nearMargin` of a vertex, plus the search
302 // reach extended by the margin so no such candidate hides just beyond the security radius. The
303 // repair's certificate re-tests these planes each step (ConvexCell::planeGap), which closes
304 // the face-gain blind spot of the seed-local certificate. A count of nearCap+1 marks overflow
305 // (the cell is then re-gathered every step). Emitted only when oNear is sized.
306 Kokkos::View<int*, MemSpace> oNear, oNearCnt;
307 int nearCap = 0;
308 Real nearMargin = Real(0);
309 // Optional (rung A1, force half): per-cell wall part of dV/dx and dA_wall/dx by in-kernel
310 // central differences of the SDF clip (sdfWallFD). Emitted only when oWallDV is sized.
311 Kokkos::View<Real*, MemSpace> oWallDV, oWallDA;
312 // Optional (track B, rung B1): per-facet second moment ∫|s|² dA about the foot point (over-buffer
313 // like oArea; packed with the facet CSR). Emitted only when oMoment2 is sized.
314 Kokkos::View<Real*, MemSpace> oMoment2;
315
320 KOKKOS_INLINE_FUNCTION void publishAreaGrad(const Cell& c, int i, int base, const int* faces,
321 int nf) const {
322 unsigned char part[MAXF_TMP * kMaxFV];
323 unsigned char pcnt[MAXF_TMP];
324 bool full = false;
325 const int total = areaGradEnumerate<Cell, MAXF_TMP>(c, faces, nf, part, pcnt, full);
326 const int ebase = Kokkos::atomic_fetch_add(&edgeCursor(0), total);
327 if (full || (size_t)ebase + (size_t)total > edgeCap) {
328 status(i) |= kOverflow;
329 for (int idx = 0; idx < nf; ++idx) {
330 oEdgeOff(base + idx) = 0;
331 oEdgeCnt(base + idx) = 0;
332 }
333 return;
334 }
335 areaGradFill<Cell, MAXF_TMP>(c, faces, nf, part, pcnt, base, ebase, oEdgeOff, oEdgeCnt,
337 }
338
340 KOKKOS_INLINE_FUNCTION void relVec(int q, Real pix, Real piy, Real piz, Real pv[3]) const {
341 Real rx = posSorted(3 * q + 0) - pix, ry = posSorted(3 * q + 1) - piy,
342 rz = posSorted(3 * q + 2) - piz;
343 const Real Lxh = Real(0.5) * Lx, Lyh = Real(0.5) * Ly, Lzh = Real(0.5) * Lz;
344 pv[0] = rx > Lxh ? rx - Lx : (rx < -Lxh ? rx + Lx : rx);
345 pv[1] = ry > Lyh ? ry - Ly : (ry < -Lyh ? ry + Ly : ry);
346 pv[2] = rz > Lzh ? rz - Lz : (rz < -Lzh ? rz + Lz : rz);
347 }
348
350 KOKKOS_INLINE_FUNCTION int gridCell(int rgx, int rgy, int rgz) const {
351 int gx = rgx < 0 ? rgx + dimx : (rgx >= dimx ? rgx - dimx : rgx);
352 int gy = rgy < 0 ? rgy + dimy : (rgy >= dimy ? rgy - dimy : rgy);
353 int gz = rgz < 0 ? rgz + dimz : (rgz >= dimz ? rgz - dimz : rgz);
354 return useMorton ? morton3(gx, gy, gz) : (gx + gy * dimx + gz * dimx * dimy);
355 }
356
358 KOKKOS_INLINE_FUNCTION void homeCell(Real pix, Real piy, Real piz, int& cx, int& cy,
359 int& cz) const {
360 cx = ((((int)Kokkos::floor(pix * icx)) % dimx) + dimx) % dimx;
361 cy = ((((int)Kokkos::floor(piy * icy)) % dimy) + dimy) % dimy;
362 cz = ((((int)Kokkos::floor(piz * icz)) % dimz) + dimz) % dimz;
363 }
364
366 KOKKOS_INLINE_FUNCTION int subBase(Real pix, Real piy, Real piz) const {
367 const Real fxi = pix * icx, fyi = piy * icy, fzi = piz * icz;
368 int sx = (int)((fxi - Kokkos::floor(fxi)) * Real(wlS));
369 int sy = (int)((fyi - Kokkos::floor(fyi)) * Real(wlS));
370 int sz = (int)((fzi - Kokkos::floor(fzi)) * Real(wlS));
371 sx = sx < 0 ? 0 : (sx >= wlS ? wlS - 1 : sx);
372 sy = sy < 0 ? 0 : (sy >= wlS ? wlS - 1 : sy);
373 sz = sz < 0 ? 0 : (sz >= wlS ? wlS - 1 : sz);
374 return (sx + wlS * (sy + wlS * sz)) * nOff;
375 }
376
378 KOKKOS_INLINE_FUNCTION int worklistCell(int base, int g, int cx, int cy, int cz) const {
379 const int packed = wlOff(base + g);
380 const int rgx = cx + ((packed & 0xFF) - kWlOffBias);
381 const int rgy = cy + (((packed >> 8) & 0xFF) - kWlOffBias);
382 const int rgz = cz + (((packed >> 16) & 0xFF) - kWlOffBias);
383 return gridCell(rgx, rgy, rgz);
384 }
385
389 KOKKOS_INLINE_FUNCTION void emitEmptyTopo(int i) const {
390 oNp(i) = 6;
391 oNt(i) = 0;
392 if (oWall.active())
393 oWall.cnt(i) = 0;
394 }
395
399 KOKKOS_INLINE_FUNCTION void finishCell(Cell& c, int i, Real pix, Real piy, Real piz, Real covSq,
400 Real wSelf = Real(0)) const {
401 if (c.overflow) {
402 status(i) = kOverflow;
403 facetCount(i) = 0;
404 cellFacetBase(i) = 0;
405 cellVol(i) = Real(0);
406 if (emitTopo)
407 emitEmptyTopo(i); // an overflowed cell reads as empty, never as its stale predecessor
408 return;
409 }
410 // Complete iff the gathered coverage inscribes past the reachability radius. Voronoi:
411 // blockReachSq = 4·rSqMax (unchanged). Power: the larger weight-aware reach, so a cell whose
412 // heavy far neighbour lies beyond the coverage is correctly flagged kIncomplete.
413 const bool incomplete =
414 !(covSq > PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll));
415 (void)sdf;
416 if constexpr (!std::is_same_v<Sdf, NoSdf>) {
417 const Real seedW[3] = {pix, piy, piz};
418 const bool wallFD = oWallDV.extent(0) > 0;
419 Real dV[3] = {Real(0), Real(0), Real(0)}, dA[3] = {Real(0), Real(0), Real(0)};
420 if (wallFD) {
421 // the un-clipped cell is only needed when the clip will act (seed within the circumradius)
422 const Real phiC = sdf.eval(pix, piy, piz);
423 const Real rad = Kokkos::sqrt(c.maxVertexRsq());
424 if (phiC > Real(0) && phiC <= rad + Real(1e-8)) {
425 const Cell unclipped = c;
426 clipCellAgainstSdf<Real, kMaxP, kMaxT>(c, seedW, sdf);
427 bool hasWall = false;
428 for (int k = 0; k < c.np && !hasWall; ++k)
429 hasWall = (c.pnbr[k] == kBoundaryFacet);
430 if (hasWall && !c.empty() && !c.overflow)
431 sdfWallFD<Real, kMaxP, kMaxT>(unclipped, seedW, sdf, Real(1e-4) * rad, dV, dA);
432 } else {
433 clipCellAgainstSdf<Real, kMaxP, kMaxT>(c, seedW, sdf);
434 }
435 for (int d = 0; d < 3; ++d) {
436 oWallDV(3 * (size_t)i + d) = dV[d];
437 oWallDA(3 * (size_t)i + d) = dA[d];
438 }
439 } else {
440 clipCellAgainstSdf<Real, kMaxP, kMaxT>(c, seedW, sdf);
441 }
442 }
443 int st = kOk;
444 if (c.overflow)
445 st |= kOverflow;
446 // A2a: the converged search reach must stay within the min-image half box.
447 {
448 const Real reachR =
449 Kokkos::sqrt(PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll));
450 Real Lmin = Lx < Ly ? Lx : Ly;
451 Lmin = Lmin < Lz ? Lmin : Lz;
452 if (reachR > Real(0.5) * Lmin)
453 st |= kReachExceeded;
454 }
455 const bool empty = c.empty();
456 if (empty)
457 st |= kEmpty;
458 if (incomplete)
459 st |= kIncomplete;
460 cellVol(i) = (empty || c.overflow) ? Real(0) : c.volumePerVertex();
461
462 // Part-II: persist the final (post-SDF-clip) topology so a later step can re-eval/repair
463 // without a rebuild.
464 if (emitTopo && !empty && !c.overflow) {
465 oNp(i) = c.np;
466 oNt(i) = c.nt;
467 for (int k = 0; k < c.np; ++k)
468 oTopoPnbr[(size_t)i * kMaxP + k] = c.pnbr[k];
469 for (int t = 0; t < c.nt; ++t)
470 oTri[(size_t)i * kMaxT + t] = (unsigned)c.t0[t] | ((unsigned)c.t1[t] << 8) |
471 ((unsigned)c.t2[t] << 16) | ((c.alive[t] ? 1u : 0u) << 24);
472 if constexpr (TrackAdj) // derive the local-cert plane set from the clip-maintained adj (no
473 // findSharing)
474 c.computePoke4(&oPoke4[(size_t)i * kMaxT * 4]);
475 oWall.save(i, c); // wall planes (no-op unless the store is allocated)
476 } else if (emitTopo && empty) {
477 emitEmptyTopo(i);
478 }
479
480 // Collect this cell's live faces (a plane with >=3 incident live triangles), then reserve
481 // a contiguous CSR range with one atomic and write the per-facet geometry straight in.
482 int faces[MAXF_TMP];
483 int nf = 0;
484 if (!empty && !c.overflow) {
485 for (int k = 0; k < c.np; ++k) {
486 int cnt = 0;
487 for (int t = 0; t < c.nt; ++t)
488 if (c.alive[t] && (c.t0[t] == k || c.t1[t] == k || c.t2[t] == k))
489 ++cnt;
490 if (cnt < 3)
491 continue; // not a polygon face
492 if (nf >= MAXF_TMP) {
493 st |= kOverflow;
494 break;
495 }
496 faces[nf++] = k;
497 }
498 }
499 status(i) = st;
500 const int base = Kokkos::atomic_fetch_add(&facetCursor(0), nf);
501 if ((size_t)base + (size_t)nf > facetCap) {
502 status(i) |= kOverflow;
503 facetCount(i) = 0;
504 cellFacetBase(i) = 0;
505 return;
506 }
507 facetCount(i) = nf;
508 cellFacetBase(i) = base;
509 if (withAreaGrad && nf > 0)
510 publishAreaGrad(c, i, base, faces, nf);
511 for (int idx = 0; idx < nf; ++idx) {
512 const int k = faces[idx];
513 Real area[3] = {0, 0, 0}, dv[3] = {0, 0, 0}, conn[3];
514 conn[0] = Real(2) * c.n[k][0];
515 conn[1] = Real(2) * c.n[k][1];
516 conn[2] = Real(2) * c.n[k][2];
517 if (withForceGeom)
518 c.facetGeometry(k, area, dv, conn); // area vector + dV + connector
519 if (oMoment2.extent(0) > 0)
520 oMoment2((size_t)base + idx) = c.faceMoment2(k);
521 oNbr((size_t)base + idx) = c.pnbr[k];
522 const size_t o = ((size_t)base + idx) * 3;
523 for (int cc = 0; cc < 3; ++cc) {
524 oArea(o + cc) = area[cc];
525 oDV(o + cc) = dv[cc];
526 oConn(o + cc) = conn[cc];
527 }
528 }
529 }
530
536 KOKKOS_INLINE_FUNCTION bool gatherCell(int pi, Cell& c, Real& pix, Real& piy, Real& piz,
537 Real& wSelf, int& ncRec, int& nnRec) const {
538 // Voronoi (Weighted=false) uses the bisector plane + security-radius early-out. Power
539 // (Weighted=true) uses the radical plane offset ½(|r|²+w_self−w_nbr) — which can be negative
540 // (the seed can lie OUTSIDE its own cell) so the bisector security certificate is invalid.
541 // P1 keeps the trivially-correct APPLY-ALL path for Power (every worklist candidate, no
542 // early-out); the weight-aware security gate is P2.
543 const int i = binned(pi);
544 pix = posSorted(3 * pi + 0);
545 piy = posSorted(3 * pi + 1);
546 piz = posSorted(3 * pi + 2);
547 int cx, cy, cz;
548 homeCell(pix, piy, piz, cx, cy, cz);
549 const int base = subBase(pix, piy, piz);
550 c.initBox(Lx, Ly, Lz);
551 wSelf = Real(0);
552 if constexpr (Weighted)
553 wSelf = wSorted(pi);
554 bool buried = false; // Power: some neighbour dominates the seed at its own location (d≤0)
555 // Early wall clip (rung C4/B follow-up): the tangent plane at the SEED'S OWN FOOT on the wall
556 // bounds the cell toward the solid before any neighbour is gathered. Without it a cell next
557 // to a curved wall extends through the solid to the far side until the SDF clip runs after
558 // the gather — for nearly cospherical seeds (a wall-adapted shell around a sphere) every
559 // shell seed then neighbours every other through the sphere's centre and the plane cap
560 // overflows at any capacity. The plane is the first wall facet; the full clip refines it.
561 if constexpr (!std::is_same_v<Sdf, NoSdf>) {
562 const Real phi0 = sdf.eval(pix, piy, piz);
563 if (phi0 > Real(0)) {
564 Real g[3];
565 sdfGradient(sdf, pix, piy, piz, g);
566 const Real gl = Kokkos::sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
567 if (gl > Real(0)) {
568 // RETREATED by φ₀ into the solid (plane at 2φ₀): the tangent plane itself would cut the
569 // fluid sliver between it and a convex wall (w²/2R deep at lateral distance w — measured
570 // 1 % of the fluid volume on the sphere array); at 2φ₀ it only bounds the reach, the
571 // vertex-foot clip (A0/A1) then places the exact wall facets.
572 const Real pdir[3] = {-g[0] / gl, -g[1] / gl, -g[2] / gl};
573 c.clip(pdir, Real(2) * phi0, kBoundaryFacet);
574 }
575 }
576 }
577
578 // The worklist is sorted by nearest-corner dist², so once wlRmin exceeds the reachability
579 // radius every remaining block is too far to cut — break. Voronoi keeps the exact bisector
580 // certificate (secR2 = 2·rSqMax, break at wlRmin > 2·secR2 = 4·rSqMax) so the unweighted path
581 // stays byte-identical. Power uses the weight-aware reach (blockReachSq) with the global max
582 // weight.
583 Real secR2 = Real(2) * c.maxVertexRsq(); // Voronoi
584 Real reachSq =
585 PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll); // Pow
586 ncRec = 0; // Part-II: count of recorded candidate (skin) ids for this cell
587 const bool emitNear = oNear.extent(0) > 0 && nearCap > 0;
588 nnRec = 0; // near-miss candidates recorded for this cell
589 // With near-miss emission the worklist reach grows by the margin: a plane can miss the cell
590 // by less than the margin while its seed sits beyond the security radius.
591 const Real nm = emitNear ? nearMargin : Real(0);
592 for (int g = 0; g < nOff && !c.overflow && !buried; ++g) {
593 if constexpr (Weighted) {
594 const Real rr = Kokkos::sqrt(reachSq) + Real(2) * nm;
595 if (wlRmin(base + g) > rr * rr)
596 break;
597 } else {
598 // sorted ⇒ rest are farther; cell closed. Break radius² = 4·rSqMax = 2·secR2, extended to
599 // (2·rmax + 2·margin)² when near-misses are recorded.
600 const Real rr = Kokkos::sqrt(Real(2) * secR2) + Real(2) * nm;
601 if (wlRmin(base + g) > rr * rr)
602 break;
603 }
604 const int gc = worklistCell(base, g, cx, cy, cz);
605 for (int q = cellStart(gc); q < cellStart(gc + 1) && !c.overflow && !buried; ++q) {
606 if (q == pi)
607 continue;
608 if (haveGid && gidSorted(q) == gidSorted(pi))
609 continue;
610 // A seed already clipped is a SECOND PERIODIC IMAGE reaching the worklist (a large cell
611 // whose search radius, block-rounded and extended by the near-miss margin, passes half
612 // the box): keep the first, nearest image — the min-image the re-evaluation rebuilds —
613 // and skip the rest, or the resident topology carries two planes for one neighbour and
614 // re-evaluates to a different cell (measured on a wall-hugging cell: 1.24e-3 vs 2.3e-4).
615 {
616 const int id = binned(q);
617 bool dup = false;
618 for (int k = 6; k < c.np && !dup; ++k)
619 dup = (c.pnbr[k] == id);
620 if (dup)
621 continue;
622 }
623 // record the examined neighbour as a skin candidate (within the worklist's security reach),
624 // capped.
625 if (emitCand && ncRec < candCap)
626 oCand[(size_t)i * candCap + ncRec++] = binned(q);
627 Real pv[3];
628 relVec(q, pix, piy, piz, pv);
629 Real wNbr = Real(0);
630 if constexpr (Weighted)
631 wNbr = wSorted(q);
632 const Real off = PlanePolicy::template offsetFromRel<Real>(pv, wSelf, wNbr);
633 if constexpr (Weighted) {
634 // Power: d = ½(|r|²+w_i−w_j) ≤ 0 means neighbour j has lower power than i at the seed's
635 // own location — the seed is not in its own power cell, so cell i is BURIED (empty). The
636 // foot-point half-space cannot represent this plane anyway; empty the cell and stop.
637 if (off <= Real(0)) {
638 buried = true;
639 break;
640 }
641 // Conservative cull: r·v ≤ |r|·√rSqMax, so |r|²·rSqMax ≤ d² ⇒ the plane cannot cut.
642 const Real rho = pv[0] * pv[0] + pv[1] * pv[1] + pv[2] * pv[2];
643 if (!emitNear && rho * c.maxVertexRsq() <= off * off)
644 continue; // provably beyond reach
645 Real gap;
646 if (c.clip(pv, off, binned(q), emitNear ? &gap : nullptr)) {
647 reachSq = PlanePolicy::template blockReachSq<Real>(c.maxVertexRsq(), wSelf, wMaxAll);
648 } else if (emitNear && gap > -nm) {
649 if (nnRec < nearCap)
650 oNear[(size_t)i * nearCap + nnRec] = binned(q);
651 ++nnRec;
652 }
653 } else {
654 if (!emitNear && off >= secR2)
655 continue; // beyond the radius: cannot cut (bisector certificate)
656 Real gap;
657 if (c.clip(pv, off, binned(q), emitNear ? &gap : nullptr)) {
658 secR2 = Real(2) * c.maxVertexRsq();
659 } else if (emitNear && gap > -nm) {
660 if (nnRec < nearCap)
661 oNear[(size_t)i * nearCap + nnRec] = binned(q);
662 ++nnRec;
663 }
664 }
665 }
666 }
667 return !buried;
668 }
669
673 KOKKOS_INLINE_FUNCTION void buildCell(int pi) const {
674 const int i = binned(pi);
675 Cell c;
676 Real pix, piy, piz, wSelf;
677 int ncRec, nnRec;
678 const bool notBuried = gatherCell(pi, c, pix, piy, piz, wSelf, ncRec, nnRec);
679 (void)notBuried; // only a power cell can be buried
680 if constexpr (Weighted) {
681 if (!notBuried) { // buried power cell: emit as empty (kEmpty), like an in-solid SDF seed
682 status(i) = kEmpty | kBuried;
683 facetCount(i) = 0;
684 cellFacetBase(i) = 0;
685 cellVol(i) = Real(0);
686 if (emitTopo)
687 emitEmptyTopo(i);
688 return;
689 }
690 }
691 if (emitCand)
692 oCandCnt(i) = ncRec;
693 const bool emitNear = oNear.extent(0) > 0 && nearCap > 0;
694 if (emitNear)
695 oNearCnt(i) = nnRec > nearCap ? nearCap + 1 : nnRec; // nearCap+1 = overflow marker
696 // Completeness uses the conservative inscribed-sphere coverage (sw·minCsz)², the same
697 // criterion the legacy gather used: complete iff (sw·minCsz)² > 4·rSqMax.
698 const Real covSq = Real(sw) * minCsz * Real(sw) * minCsz;
699 finishCell(c, i, pix, piy, piz, covSq, wSelf);
700 }
701};
702
727template <class Real, bool Weighted, class Sdf = NoSdf, int MAXP = kMaxPlanes,
728 int MAXT = kMaxTriangles>
730 const Kokkos::View<Real*, peclet::core::MemSpace>& posFlat,
731 const Kokkos::View<Real*, peclet::core::MemSpace>& weight, int N, const Real L[3],
732 int sw = kSearchWindow, int densityCount = -1,
733 Kokkos::View<long*, peclet::core::MemSpace> gid = {}, Sdf sdf = {}, bool withForceGeom = true,
734 int nBuild = -1, Kokkos::View<int*, peclet::core::MemSpace> outNp = {},
735 Kokkos::View<int*, peclet::core::MemSpace> outNt = {},
736 Kokkos::View<int*, peclet::core::MemSpace> outPnbr = {},
737 Kokkos::View<unsigned*, peclet::core::MemSpace> outTri = {},
738 Kokkos::View<int*, peclet::core::MemSpace> outCand = {},
739 Kokkos::View<int*, peclet::core::MemSpace> outCandCnt = {}, int candCap = 0,
740 WorklistCache<Real>* wlc = nullptr, WallStore<Real> outWall = {}, bool withAreaGrad = false,
741 Kokkos::View<int*, peclet::core::MemSpace> outNear = {},
742 Kokkos::View<int*, peclet::core::MemSpace> outNearCnt = {}, int nearCap = 0,
743 Real nearMargin = Real(0), bool withWallFD = false, bool withMoments = false,
744 bool profile = false) {
745 using peclet::core::MemSpace;
746 using Exec = peclet::core::ExecSpace;
747 // Part-II optional outputs (see CellBuilder): emit the resident topology store / candidate skin
748 // list only when the caller supplies sized views (so existing callers, passing none, are
749 // byte-for-byte unaffected).
750 const bool emitTopo = outNp.extent(0) == static_cast<size_t>(N);
751 const bool emitCand = outCand.extent(0) > 0 && candCap > 0;
752 // Optional global ids: skip a candidate sharing the cell's own id (its periodic
753 // self-image, which can wrap exactly onto the seed -> a degenerate zero-distance
754 // cut). Mirrors the legacy processNbrs `itr->id == m_id` guard. In the
755 // single-domain case (gid empty) only the same local index is skipped.
756 const bool haveGid = gid.extent(0) == static_cast<size_t>(N);
757 // Seeds with original index >= nBuildEff are candidate-only (their cell is skipped).
758 const int nBuildEff = (nBuild >= 0 && nBuild < N) ? nBuild : N;
759
760 // `profile`: the timing / over-buffer report on stderr (the API's profile switch —
761 // Tessellation.diagnostics.set_profile — not an environment variable, QUALITY_PLAN D3/G.7).
762 const bool prof = profile;
763 Kokkos::Timer ptimer;
764 double tGrid = 0, tBuild = 0, tCsr = 0;
765
766 // Counting-sort grid + presorted worklist. Factored into buildTessGrid so the SAME grid backs
767 // both this cold build and the moving-point subset gather (device/subset_gather.hpp); pure code
768 // motion, so the cold-build output is byte-for-byte unchanged.
769 auto grid = buildTessGrid<Real, Weighted>(posFlat, weight, N, L, sw, densityCount, gid, wlc);
770 // The window the grid was actually built with: buildTessGrid clamps sw to (minDim-1)/2-1 on a
771 // coarse grid (N ≲ 1300 at sw = 4), and the completeness flag below must judge against the
772 // walked window, not the requested one.
773 sw = grid.sw;
774 const Real Lx = grid.Lx, Ly = grid.Ly, Lz = grid.Lz;
775 const Real icx = grid.icx, icy = grid.icy, icz = grid.icz, minCsz = grid.minCsz;
776 const int dimx = grid.dimx, dimy = grid.dimy, dimz = grid.dimz;
777 const int nOff = grid.nOff, wlS = grid.wlS;
778 const bool useMorton = grid.useMorton;
779
780 if (prof) {
781 Kokkos::fence();
782 tGrid = ptimer.seconds();
783 ptimer.reset();
784 }
785
786 // --- pass 1: build each cell, record volume / facet count / status and the
787 // per-cell facet records (neighbour id + area vector) into a temp slab ---
788 Kokkos::View<Real*, MemSpace> cellVol("cellVol", N);
789 Kokkos::View<int*, MemSpace> facetCount("facetCount", N);
790 Kokkos::View<int*, MemSpace> status("status", N);
791 Kokkos::View<int*, MemSpace> cellFacetBase("cellFacetBase", N); // per-cell facet base
792 // Facet over-buffer + atomic cursor: the build packs each cell's facets directly into
793 // a single contiguous CSR-layout buffer via atomic_fetch_add (one-pass "fusion") — no
794 // fixed-stride temp slab, no exclusive scan, and the connecting vector is the cut's own
795 // plane vector (no minimal-image recompute). The buffer is over-allocated to a tight
796 // facet cap; the used prefix [0,nFacets) is copied to a compact view at the end.
797 // Per-cell facet cap: MAXF_TMP (the class constant) is the most facets one Voronoi/Power
798 // cell may publish (a single cell rarely exceeds ~40 faces); bounds the per-cell `faces[]`
799 // stack array.
800 // Global over-buffer capacity: the published CSR holds the *sum* of all cells'
801 // facets ≈ N × mean-faces-per-cell (~15.5 for random Poisson–Voronoi). Sizing it at
802 // N × MAXF_TMP over-allocated ~3× and OOM'd the GPU at large N (≈15 GB at N=4M). A
803 // mean-facet estimate with headroom (N×18, ~16% over the aggregate mean) is the FIRST
804 // attempt. It is only an estimate: a wall-heavy tessellation (a concave SDF wall runs
805 // every wall cell to the 24-cut cap, ~40 faces per wall cell) exceeds it, and the cells
806 // that lose the race for the tail of the buffer are then published with NO facets — a
807 // thread-scheduling-dependent set (measured: 400–600 of 12000 cavity cells, wall-facet
808 // counts 19983 vs 20845 run to run). The cursor's final value is the EXACT demand (every
809 // cell reserves before it checks), so on overflow the over-buffers are re-allocated at
810 // that demand and the build is re-run — the rare case pays one more build; the result
811 // never depends on which cells finished last.
812 constexpr size_t kMeanFacets = 18;
813 size_t facetCap = (size_t)N * kMeanFacets;
814 using Kokkos::view_alloc;
815 using Kokkos::WithoutInitializing;
816 Kokkos::View<int*, MemSpace> oNbr;
817 Kokkos::View<Real*, MemSpace> oArea, oDV, oConn;
818 Kokkos::View<int*, MemSpace> facetCursor("facetCursor", 1); // zero-initialised
819 // Rung A3: facet-edge area-Jacobian over-buffer (opt-in). A face has ~5.2 edges on average
820 // (Poisson–Voronoi), so ~6.2 entries per facet incl. the self slot; cap at 8 per facet — the
821 // same estimate-then-exact rule as the facet cap (its cursor is the exact edge demand too).
822 size_t edgeCap = withAreaGrad ? facetCap * 8 : 0;
823 Kokkos::View<int*, MemSpace> oEdgeOff, oEdgeCnt, oEdgeFacet, edgeCursor;
824 Kokkos::View<Real*, MemSpace> oEdgeGrad, oEdgeLen;
825 if (withAreaGrad)
826 edgeCursor = Kokkos::View<int*, MemSpace>("edgeCursor", 1);
827 // Track B (B1): per-facet second moments (opt-in), over-buffered like the facet arrays.
828 Kokkos::View<Real*, MemSpace> oMoment2;
829 // (Re)allocate every over-buffer at the current caps and rewind the cursors.
830 auto allocOverBuffers = [&]() {
831 oNbr = Kokkos::View<int*, MemSpace>(view_alloc(std::string("oNbr"), WithoutInitializing),
832 facetCap);
833 oArea = Kokkos::View<Real*, MemSpace>(view_alloc(std::string("oArea"), WithoutInitializing),
834 facetCap * 3);
835 oDV = Kokkos::View<Real*, MemSpace>(view_alloc(std::string("oDV"), WithoutInitializing),
836 facetCap * 3);
837 oConn = Kokkos::View<Real*, MemSpace>(view_alloc(std::string("oConn"), WithoutInitializing),
838 facetCap * 3);
839 Kokkos::deep_copy(facetCursor, 0);
840 if (withAreaGrad) {
841 oEdgeOff = Kokkos::View<int*, MemSpace>(
842 view_alloc(std::string("oEdgeOff"), WithoutInitializing), facetCap);
843 oEdgeCnt = Kokkos::View<int*, MemSpace>(
844 view_alloc(std::string("oEdgeCnt"), WithoutInitializing), facetCap);
845 oEdgeFacet = Kokkos::View<int*, MemSpace>(
846 view_alloc(std::string("oEdgeFacet"), WithoutInitializing), edgeCap);
847 oEdgeGrad = Kokkos::View<Real*, MemSpace>(
848 view_alloc(std::string("oEdgeGrad"), WithoutInitializing), edgeCap * 3);
849 oEdgeLen = Kokkos::View<Real*, MemSpace>(
850 view_alloc(std::string("oEdgeLen"), WithoutInitializing), edgeCap * 4);
851 Kokkos::deep_copy(edgeCursor, 0);
852 }
853 if (withMoments)
854 oMoment2 = Kokkos::View<Real*, MemSpace>(
855 view_alloc(std::string("oMoment2"), WithoutInitializing), facetCap);
856 };
857 allocOverBuffers();
858 // Rung A1 (force half): per-cell wall FD outputs (opt-in; SDF builds only).
859 Kokkos::View<Real*, MemSpace> oWallDV, oWallDA;
860 if (withWallFD && !std::is_same_v<Sdf, NoSdf>) {
861 oWallDV = Kokkos::View<Real*, MemSpace>("cellWallDV", (size_t)N * 3);
862 oWallDA = Kokkos::View<Real*, MemSpace>("cellWallDA", (size_t)N * 3);
863 }
864 if (prof)
865 std::fprintf(stderr, "[worklist] sw=%d nOff=%d\n", sw, nOff);
866
867 // Build each cell: one thread per cell (RangePolicy), the compact ConvexCell in the
868 // per-thread frame, the cut applied on the fly. Same path on every backend (ConvexCell is
869 // lean enough that the worklist + geometry run well on the GPU without a team variant).
870 Kokkos::View<unsigned char*, MemSpace>
871 noPoke4; // cold build does not emit the cert plane set (TrackAdj=false)
872
873 // Global max weight bounds the weight-aware worklist reach (Power only; 0 for Voronoi).
874 Real wMaxAll = Real(0);
875 if constexpr (Weighted) {
876 if (weight.extent(0) == static_cast<size_t>(N)) {
877 Kokkos::parallel_reduce(
878 "wmax", Kokkos::RangePolicy<Exec>(0, N),
879 KOKKOS_LAMBDA(int idx, Real& m) { m = weight(idx) > m ? weight(idx) : m; },
880 Kokkos::Max<Real>(wMaxAll));
881 }
882 }
883
884 CellBuilder<Real, Weighted, Sdf, false, MAXP, MAXT> op{grid.binned,
885 grid.posSorted,
886 grid.wSorted,
887 grid.gidSorted,
888 grid.cellStart,
889 grid.wlOff,
890 grid.wlRmin,
891 status,
892 cellVol,
893 facetCount,
894 cellFacetBase,
895 oNbr,
896 oArea,
897 oDV,
898 oConn,
899 facetCursor,
900 icx,
901 icy,
902 icz,
903 Lx,
904 Ly,
905 Lz,
906 minCsz,
907 wMaxAll,
908 dimx,
909 dimy,
910 dimz,
911 sw,
912 nOff,
913 wlS,
914 useMorton,
915 haveGid,
916 withForceGeom,
917 facetCap,
918 sdf,
919 outNp,
920 outNt,
921 outPnbr,
922 outTri,
923 noPoke4,
924 outCand,
925 outCandCnt,
926 emitTopo,
927 emitCand,
928 candCap,
929 outWall,
930 oEdgeOff,
931 oEdgeCnt,
932 oEdgeFacet,
933 oEdgeGrad,
934 oEdgeLen,
935 edgeCursor,
936 edgeCap,
937 withAreaGrad,
938 outNear,
939 outNearCnt,
940 nearCap,
941 nearMargin,
942 oWallDV,
943 oWallDA,
944 oMoment2};
945 const int nBuildL = nBuildEff;
946 auto binnedV0 = grid.binned;
947 int nFacetsRaw = 0, nEdgesRaw = 0;
948 int overBufferRebuilds = 0;
949 for (int attempt = 0;; ++attempt) {
950 Kokkos::parallel_for(
951 "tess.build", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(const int pi) {
952 if (binnedV0(pi) >= nBuildL)
953 return; // candidate-only seed: skip its cell
954 op.buildCell(pi);
955 });
956 Kokkos::deep_copy(nFacetsRaw, Kokkos::subview(facetCursor, 0));
957 if (withAreaGrad)
958 Kokkos::deep_copy(nEdgesRaw, Kokkos::subview(edgeCursor, 0));
959 const bool facetOver = (size_t)nFacetsRaw > facetCap;
960 const bool edgeOver = withAreaGrad && (size_t)nEdgesRaw > edgeCap;
961 if (!(facetOver || edgeOver) || attempt >= 2)
962 break;
963 // The estimate was too small: the cursors hold the exact demand. Re-allocate at that size
964 // and rebuild; every per-cell output is rewritten by the second pass (the cell builder
965 // assigns, never accumulates, and the cursors are rewound), so nothing stale survives.
966 if (facetOver)
967 facetCap = (size_t)nFacetsRaw;
968 if (edgeOver)
969 edgeCap = (size_t)nEdgesRaw;
970 ++overBufferRebuilds;
971 if (prof)
972 std::fprintf(stderr, "[tess.build] over-buffer exceeded (facets %d, edges %d): rebuilding\n",
973 nFacetsRaw, nEdgesRaw);
974 // Release the first attempt's buffers (both the locals and the builder's copies) BEFORE
975 // allocating the larger ones, so the peak stays at one over-buffer set, not two.
976 Kokkos::fence();
977 op.oNbr = oNbr = Kokkos::View<int*, MemSpace>();
978 op.oArea = oArea = Kokkos::View<Real*, MemSpace>();
979 op.oDV = oDV = Kokkos::View<Real*, MemSpace>();
980 op.oConn = oConn = Kokkos::View<Real*, MemSpace>();
981 op.oEdgeOff = oEdgeOff = Kokkos::View<int*, MemSpace>();
982 op.oEdgeCnt = oEdgeCnt = Kokkos::View<int*, MemSpace>();
983 op.oEdgeFacet = oEdgeFacet = Kokkos::View<int*, MemSpace>();
984 op.oEdgeGrad = oEdgeGrad = Kokkos::View<Real*, MemSpace>();
985 op.oEdgeLen = oEdgeLen = Kokkos::View<Real*, MemSpace>();
986 op.oMoment2 = oMoment2 = Kokkos::View<Real*, MemSpace>();
987 allocOverBuffers();
988 op.oNbr = oNbr;
989 op.oArea = oArea;
990 op.oDV = oDV;
991 op.oConn = oConn;
992 op.facetCap = facetCap;
993 op.oEdgeOff = oEdgeOff;
994 op.oEdgeCnt = oEdgeCnt;
995 op.oEdgeFacet = oEdgeFacet;
996 op.oEdgeGrad = oEdgeGrad;
997 op.oEdgeLen = oEdgeLen;
998 op.edgeCap = edgeCap;
999 op.oMoment2 = oMoment2;
1000 }
1001
1002 if (prof) {
1003 Kokkos::fence();
1004 tBuild = ptimer.seconds();
1005 ptimer.reset();
1006 }
1007
1008 // --- pack the over-buffer prefix [0,nFacets) into a compact CSR ---
1009 // The atomic packing already produced a gapless CSR (in cell-finish order, with each
1010 // cell's facets contiguous at cellFacetBase(i)); we only copy its used prefix into a
1011 // right-sized view (a contiguous read+write — no exclusive scan, no strided gather,
1012 // no minimal-image recompute that the old temp->CSR compaction paid).
1013 // Clamp to capacity: if the over-buffer overflowed (rare; flagged per-cell above),
1014 // the cursor ran past the end and only [0,facetCap) holds valid, indexable facets.
1015 const int nFacets = (size_t)nFacetsRaw > facetCap ? (int)facetCap : nFacetsRaw;
1016
1017 TessellationView<Real> view;
1018 view.cellFacetOffset = cellFacetBase;
1019 view.cellFacetCount = facetCount;
1020 view.cellVolume = cellVol;
1021 view.cellSeedId =
1022 Kokkos::View<gid_t*, MemSpace>(view_alloc(std::string("cellSeedId"), WithoutInitializing), N);
1023 {
1024 auto vSeed = view.cellSeedId;
1025 Kokkos::parallel_for(
1026 "tess.seedId", Kokkos::RangePolicy<Exec>(0, N),
1027 KOKKOS_LAMBDA(const int i) { vSeed(i) = (gid_t)i; });
1028 }
1029
1030 // Pack each over-buffer component into a right-sized published view, then free the
1031 // source immediately. Copying-and-freeing one array at a time holds peak memory at
1032 // ~(over-buffer + one compact array) instead of (over-buffer + full compact CSR) —
1033 // the latter OOM'd the GPU at large N. numFacets() == nFacets, so the CSR contract is
1034 // intact (consumers see correctly sized views). The fence before each free ensures the
1035 // copy completed before the source is released.
1036 view.facetNeighbor = Kokkos::View<gid_t*, MemSpace>(
1037 view_alloc(std::string("facetNeighbor"), WithoutInitializing), nFacets);
1038 {
1039 auto v = view.facetNeighbor;
1040 auto src = oNbr;
1041 Kokkos::parallel_for(
1042 "tess.packNbr", Kokkos::RangePolicy<Exec>(0, nFacets),
1043 KOKKOS_LAMBDA(const int g) { v(g) = (gid_t)src(g); });
1044 }
1045 Kokkos::fence();
1046 oNbr = Kokkos::View<int*, MemSpace>();
1047
1048 view.facetArea = Kokkos::View<Real*, MemSpace>(
1049 view_alloc(std::string("facetArea"), WithoutInitializing), (size_t)nFacets * 3);
1050 {
1051 auto v = view.facetArea;
1052 auto src = oArea;
1053 Kokkos::parallel_for(
1054 "tess.packArea", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
1055 for (int cc = 0; cc < 3; ++cc)
1056 v(3 * g + cc) = src(3 * (size_t)g + cc);
1057 });
1058 }
1059 Kokkos::fence();
1060 oArea = Kokkos::View<Real*, MemSpace>();
1061
1062 view.facetConnect = Kokkos::View<Real*, MemSpace>(
1063 view_alloc(std::string("facetConnect"), WithoutInitializing), (size_t)nFacets * 3);
1064 {
1065 auto v = view.facetConnect;
1066 auto src = oDV;
1067 Kokkos::parallel_for(
1068 "tess.packDV", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
1069 for (int cc = 0; cc < 3; ++cc)
1070 v(3 * g + cc) = src(3 * (size_t)g + cc);
1071 });
1072 }
1073 Kokkos::fence();
1074 oDV = Kokkos::View<Real*, MemSpace>();
1075
1076 view.facetConnVec = Kokkos::View<Real*, MemSpace>(
1077 view_alloc(std::string("facetConnVec"), WithoutInitializing), (size_t)nFacets * 3);
1078 {
1079 auto v = view.facetConnVec;
1080 auto src = oConn;
1081 Kokkos::parallel_for(
1082 "tess.packConn", Kokkos::RangePolicy<Exec>(0, nFacets), KOKKOS_LAMBDA(const int g) {
1083 for (int cc = 0; cc < 3; ++cc)
1084 v(3 * g + cc) = src(3 * (size_t)g + cc);
1085 });
1086 }
1087 // Rung A3: pack the facet-edge area-Jacobian CSR (prefix [0,nEdges) of the over-buffer). The
1088 // per-facet offsets are already global into the over-buffer and the pack keeps indices, so
1089 // they carry over unchanged; the facet permutation of the pack (facet g -> g) is the identity.
1090 if (oWallDV.extent(0) > 0) {
1091 view.cellWallDV = oWallDV;
1092 view.cellWallDA = oWallDA;
1093 }
1094 if (withMoments) {
1095 view.facetMoment2 = Kokkos::View<Real*, MemSpace>(
1096 view_alloc(std::string("facetMoment2"), WithoutInitializing), nFacets);
1097 Kokkos::deep_copy(view.facetMoment2, Kokkos::subview(oMoment2, std::make_pair(0, nFacets)));
1098 }
1099 if (withAreaGrad) {
1100 const int nEdges = (size_t)nEdgesRaw > edgeCap ? (int)edgeCap : nEdgesRaw;
1101 view.facetEdgeOffset = Kokkos::View<int*, MemSpace>(
1102 view_alloc(std::string("facetEdgeOffset"), WithoutInitializing), nFacets);
1103 view.facetEdgeCount = Kokkos::View<int*, MemSpace>(
1104 view_alloc(std::string("facetEdgeCount"), WithoutInitializing), nFacets);
1105 Kokkos::deep_copy(view.facetEdgeOffset, Kokkos::subview(oEdgeOff, std::make_pair(0, nFacets)));
1106 Kokkos::deep_copy(view.facetEdgeCount, Kokkos::subview(oEdgeCnt, std::make_pair(0, nFacets)));
1107 view.edgeFacet = Kokkos::View<int*, MemSpace>(
1108 view_alloc(std::string("edgeFacet"), WithoutInitializing), nEdges);
1109 view.edgeAreaGrad = Kokkos::View<Real*, MemSpace>(
1110 view_alloc(std::string("edgeAreaGrad"), WithoutInitializing), (size_t)nEdges * 3);
1111 Kokkos::deep_copy(view.edgeFacet, Kokkos::subview(oEdgeFacet, std::make_pair(0, nEdges)));
1112 Kokkos::deep_copy(view.edgeAreaGrad,
1113 Kokkos::subview(oEdgeGrad, std::make_pair((size_t)0, (size_t)nEdges * 3)));
1114 view.edgeLength = Kokkos::View<Real*, MemSpace>(
1115 view_alloc(std::string("edgeLength"), WithoutInitializing), nEdges);
1116 {
1117 auto el = view.edgeLength;
1118 auto src = oEdgeLen;
1119 Kokkos::parallel_for(
1120 "edgeLength", Kokkos::RangePolicy<Exec>(0, nEdges),
1121 KOKKOS_LAMBDA(const int e) { el(e) = src((size_t)e * 4); });
1122 }
1123 }
1124 Kokkos::fence();
1125 oConn = Kokkos::View<Real*, MemSpace>();
1126
1127 if (prof) {
1128 tCsr = ptimer.seconds();
1129 std::fprintf(stderr, "[tess N=%d] grid=%.4fs build=%.4fs csr=%.4fs (build %.0f%%)\n", N, tGrid,
1130 tBuild, tCsr, 100.0 * tBuild / (tGrid + tBuild + tCsr));
1131 // Max published facets/cell — the empirical bound for sizing a shrunk shared cell
1132 // (vertices ~ 2·facets - 4). Reduce over the per-cell facet count.
1133 int maxF = 0;
1134 auto fc = facetCount;
1135 Kokkos::parallel_reduce(
1136 "tess.maxFacets", Kokkos::RangePolicy<Exec>(0, N),
1137 KOKKOS_LAMBDA(const int c, int& m) { m = fc(c) > m ? fc(c) : m; }, Kokkos::Max<int>(maxF));
1138 std::fprintf(stderr, "[tess N=%d] maxFacets/cell=%d (=> maxVerts ~ %d; CAP=%d)\n", N, maxF,
1140 }
1141
1142 TessellatorResult<Real> res;
1143 res.view = view;
1144 res.status = status;
1145 res.overBufferRebuilds = overBufferRebuilds;
1146 return res;
1147}
1148
1149} // namespace peclet::voro
1150
1151#endif // PECLET_VORO_TESSELLATOR_HPP
Device-native Voronoi tessellation, geometry and finite-volume physics.
Definition convex_cell.hpp:41
std::int32_t gid_t
Definition tessellation_view.hpp:41
StatusBit
Per-cell status bits written by the build pass.
Definition tessellator.hpp:48
@ kOverflow
Definition tessellator.hpp:50
@ kBuried
Definition tessellator.hpp:64
@ kReachExceeded
Definition tessellator.hpp:65
@ kNegOffset
Definition tessellator.hpp:58
@ kOk
Definition tessellator.hpp:49
@ kIncomplete
Definition tessellator.hpp:52
@ kEmpty
Definition tessellator.hpp:51
constexpr int kMaxPlanes
Definition params.hpp:18
constexpr int kWlOffBias
Definition tess_grid.hpp:38
constexpr int kMaxTriangles
Definition params.hpp:19
constexpr int kBoundaryFacet
Definition sdf.hpp:36
KOKKOS_INLINE_FUNCTION void sdfGradient(const Sdf &s, Real x, Real y, Real z, Real g[3])
Central-difference gradient of any provider with eval() (matches peclet::core::geom::gradient).
Definition sdf.hpp:216
KOKKOS_INLINE_FUNCTION int areaGradEnumerate(const Cell &c, const int *faces, int nf, unsigned char *part, unsigned char *pcnt, bool &full)
Definition tessellator.hpp:97
constexpr int kSearchWindow
Definition params.hpp:28
KOKKOS_INLINE_FUNCTION int morton3(int x, int y, int z)
Definition tess_grid.hpp:46
KOKKOS_INLINE_FUNCTION void areaGradFill(const Cell &c, const int *faces, int nf, const unsigned char *part, const unsigned char *pcnt, int base, int ebase, const IView &oEdgeOff, const IView &oEdgeCnt, const IView &oEdgeFacet, const RView &oEdgeGrad, const RView &oEdgeLen)
Definition tessellator.hpp:148
TessellatorResult< Real > buildTessellation(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={}, Sdf sdf={}, bool withForceGeom=true, int nBuild=-1, Kokkos::View< int *, peclet::core::MemSpace > outNp={}, Kokkos::View< int *, peclet::core::MemSpace > outNt={}, Kokkos::View< int *, peclet::core::MemSpace > outPnbr={}, Kokkos::View< unsigned *, peclet::core::MemSpace > outTri={}, Kokkos::View< int *, peclet::core::MemSpace > outCand={}, Kokkos::View< int *, peclet::core::MemSpace > outCandCnt={}, int candCap=0, WorklistCache< Real > *wlc=nullptr, WallStore< Real > outWall={}, bool withAreaGrad=false, Kokkos::View< int *, peclet::core::MemSpace > outNear={}, Kokkos::View< int *, peclet::core::MemSpace > outNearCnt={}, int nearCap=0, Real nearMargin=Real(0), bool withWallFD=false, bool withMoments=false, bool profile=false)
Definition tessellator.hpp:729
Definition tessellator.hpp:233
bool emitTopo
Definition tessellator.hpp:285
int dimx
Definition tessellator.hpp:268
Kokkos::View< int *, MemSpace > binned
Definition tessellator.hpp:245
Kokkos::View< int *, MemSpace > oNp
Definition tessellator.hpp:279
bool withAreaGrad
Definition tessellator.hpp:299
Kokkos::View< Real *, MemSpace > oEdgeLen
Definition tessellator.hpp:296
Kokkos::View< Real *, MemSpace > oWallDV
Definition tessellator.hpp:311
KOKKOS_INLINE_FUNCTION bool gatherCell(int pi, Cell &c, Real &pix, Real &piy, Real &piz, Real &wSelf, int &ncRec, int &nnRec) const
Definition tessellator.hpp:536
Kokkos::View< Real *, MemSpace > oArea
Definition tessellator.hpp:263
int wlS
Definition tessellator.hpp:268
Kokkos::View< Real *, MemSpace > posSorted
Definition tessellator.hpp:246
Kokkos::View< Real *, MemSpace > oConn
Definition tessellator.hpp:263
Kokkos::View< int *, MemSpace > edgeCursor
Definition tessellator.hpp:297
Kokkos::View< int *, MemSpace > wlOff
Definition tessellator.hpp:255
Kokkos::View< Real *, MemSpace > oEdgeGrad
Definition tessellator.hpp:295
KOKKOS_INLINE_FUNCTION int worklistCell(int base, int g, int cx, int cy, int cz) const
Decode worklist entry g (relative to base) into a grid-cell index.
Definition tessellator.hpp:378
KOKKOS_INLINE_FUNCTION void finishCell(Cell &c, int i, Real pix, Real piy, Real piz, Real covSq, Real wSelf=Real(0)) const
Definition tessellator.hpp:399
int sw
Definition tessellator.hpp:268
bool useMorton
Definition tessellator.hpp:269
Kokkos::View< int *, MemSpace > oEdgeOff
Definition tessellator.hpp:294
Real minCsz
Definition tessellator.hpp:266
Real icx
Definition tessellator.hpp:266
Real Ly
Definition tessellator.hpp:266
Kokkos::View< Real *, MemSpace > wlRmin
Definition tessellator.hpp:256
Kokkos::View< int *, MemSpace > cellFacetBase
Definition tessellator.hpp:261
Real nearMargin
Definition tessellator.hpp:308
KOKKOS_INLINE_FUNCTION int subBase(Real pix, Real piy, Real piz) const
Flat base offset into the worklist tables for the wlS^3 sub-region the seed lands in.
Definition tessellator.hpp:366
KOKKOS_INLINE_FUNCTION void homeCell(Real pix, Real piy, Real piz, int &cx, int &cy, int &cz) const
Home grid cell index (cx,cy,cz) of seed (pix,piy,piz).
Definition tessellator.hpp:358
int nearCap
Definition tessellator.hpp:307
static constexpr int kMaxP
Definition tessellator.hpp:238
Real icz
Definition tessellator.hpp:266
size_t facetCap
Definition tessellator.hpp:270
int dimy
Definition tessellator.hpp:268
KOKKOS_INLINE_FUNCTION void buildCell(int pi) const
Definition tessellator.hpp:673
Kokkos::View< Real *, MemSpace > cellVol
Definition tessellator.hpp:259
KOKKOS_INLINE_FUNCTION int gridCell(int rgx, int rgy, int rgz) const
Linear/Morton index of the grid cell at raw (periodic-unwrapped) offset.
Definition tessellator.hpp:350
Kokkos::View< Real *, MemSpace > oWallDA
Definition tessellator.hpp:311
KOKKOS_INLINE_FUNCTION void relVec(int q, Real pix, Real piy, Real piz, Real pv[3]) const
Minimal-image relative vector from the seed at (pix,piy,piz) to sorted seed q.
Definition tessellator.hpp:340
Kokkos::View< int *, MemSpace > oNear
Definition tessellator.hpp:306
size_t edgeCap
Definition tessellator.hpp:298
KOKKOS_INLINE_FUNCTION void publishAreaGrad(const Cell &c, int i, int base, const int *faces, int nf) const
Definition tessellator.hpp:320
Kokkos::View< int *, MemSpace > cellStart
Definition tessellator.hpp:249
Kokkos::View< gid_t *, MemSpace > gidSorted
Definition tessellator.hpp:248
Kokkos::View< int *, MemSpace > status
Definition tessellator.hpp:258
int candCap
Definition tessellator.hpp:286
Kokkos::View< int *, MemSpace > oNt
Definition tessellator.hpp:279
bool haveGid
Definition tessellator.hpp:269
int nOff
Definition tessellator.hpp:268
Kokkos::View< int *, MemSpace > facetCursor
Definition tessellator.hpp:264
Kokkos::View< unsigned char *, MemSpace > oPoke4
Definition tessellator.hpp:282
Real wMaxAll
Definition tessellator.hpp:267
WallStore< Real > oWall
Definition tessellator.hpp:290
int dimz
Definition tessellator.hpp:268
Kokkos::View< int *, MemSpace > oNbr
Definition tessellator.hpp:262
Kokkos::View< Real *, MemSpace > oDV
Definition tessellator.hpp:263
Kokkos::View< int *, MemSpace > oEdgeCnt
Definition tessellator.hpp:294
Real Lz
Definition tessellator.hpp:266
Kokkos::View< int *, MemSpace > facetCount
Definition tessellator.hpp:260
Kokkos::View< int *, MemSpace > oNearCnt
Definition tessellator.hpp:306
Kokkos::View< Real *, MemSpace > wSorted
Definition tessellator.hpp:247
static constexpr int kMaxFV
Definition tessellator.hpp:293
Real icy
Definition tessellator.hpp:266
bool emitCand
Definition tessellator.hpp:285
Real Lx
Definition tessellator.hpp:266
std::conditional_t< Weighted, Power, Voronoi > PlanePolicy
Definition tessellator.hpp:242
KOKKOS_INLINE_FUNCTION void emitEmptyTopo(int i) const
Definition tessellator.hpp:389
bool withForceGeom
Definition tessellator.hpp:269
Kokkos::View< int *, MemSpace > oCandCnt
Definition tessellator.hpp:284
Kokkos::View< int *, MemSpace > oTopoPnbr
Definition tessellator.hpp:279
Kokkos::View< Real *, MemSpace > oMoment2
Definition tessellator.hpp:314
static constexpr int kMaxT
Definition tessellator.hpp:239
Kokkos::View< int *, MemSpace > oCand
Definition tessellator.hpp:284
peclet::core::MemSpace MemSpace
Definition tessellator.hpp:234
Kokkos::View< int *, MemSpace > oEdgeFacet
Definition tessellator.hpp:294
static constexpr int MAXF_TMP
Definition tessellator.hpp:240
Sdf sdf
Definition tessellator.hpp:271
Kokkos::View< unsigned *, MemSpace > oTri
Definition tessellator.hpp:280
Definition convex_cell.hpp:140
KOKKOS_INLINE_FUNCTION Real volumePerVertex() const
Definition convex_cell.hpp:1192
static constexpr int MAXFV
Definition convex_cell.hpp:916
unsigned char t1[MAXT]
Definition convex_cell.hpp:153
KOKKOS_INLINE_FUNCTION Real faceMoment2(int k) const
Definition convex_cell.hpp:495
KOKKOS_INLINE_FUNCTION void initBox(Real L0, Real L1, Real L2)
Definition convex_cell.hpp:176
bool overflow
set if MAXP/MAXT exceeded -> cell invalid, caller falls back
Definition convex_cell.hpp:157
int nt
triangle high-water mark
Definition convex_cell.hpp:156
Real n[MAXP][3]
Definition convex_cell.hpp:147
KOKKOS_INLINE_FUNCTION bool empty() const
Definition convex_cell.hpp:896
int pnbr[MAXP]
neighbour seed id per plane (<0 => bounding box)
Definition convex_cell.hpp:151
KOKKOS_INLINE_FUNCTION Real maxVertexRsq() const
Largest squared dual-vertex radius over live triangles (drives the security radius).
Definition convex_cell.hpp:591
unsigned char t2[MAXT]
triangle = triple of plane indices
Definition convex_cell.hpp:153
int np
number of planes
Definition convex_cell.hpp:152
KOKKOS_INLINE_FUNCTION bool clip(const Real pdir[3], Real d, int nbr, Real *gapOut=nullptr)
Definition convex_cell.hpp:688
unsigned char t0[MAXT]
Definition convex_cell.hpp:153
KOKKOS_INLINE_FUNCTION void computePoke4(unsigned char *out) const
Definition convex_cell.hpp:394
bool alive[MAXT]
triangle live flag
Definition convex_cell.hpp:155
KOKKOS_INLINE_FUNCTION bool facetGeometry(int k, Real areaVec[3], Real dv[3], Real conn[3]) const
Definition convex_cell.hpp:1789
Definition tessellation_view.hpp:111
Definition tessellator.hpp:69
int overBufferRebuilds
Definition tessellator.hpp:75
Kokkos::View< int *, peclet::core::MemSpace > status
Definition tessellator.hpp:71
TessellationView< Real > view
Definition tessellator.hpp:70
Definition sdf.hpp:352
Published, read-only device data layer for the tessellation (migration §2.1, §3).