peclet.voro 1.0.0
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
repair.hpp
Go to the documentation of this file.
1
31#ifndef PECLET_VORO_REPAIR_HPP
32#define PECLET_VORO_REPAIR_HPP
33
34#include <cmath>
35#include <Kokkos_Core.hpp>
36#include <string>
37
38#include "peclet/core/common/view.hpp"
44
45namespace peclet::voro {
46
50 enum Route { kTwoPass = 0, kDilated = 1, kRebuildGate = 2 };
51 int pass1 = 0;
52 int pass1Raw = 0;
53 int pass2 = 0;
54 int extra = 0;
55 int surgical = 0;
56 int verifyPasses = 0;
58 bool fellBack = false;
59};
60
63template <class Real, int MAXP = 64, int MAXT = 112, bool Weighted = false>
65 using Mem = peclet::core::MemSpace;
66 using Exec = peclet::core::ExecSpace;
67 using PlanePolicy = std::conditional_t<Weighted, Power, Voronoi>; // radical vs bisector planes
68 // The certificate/load/re-eval path reads the resident `poke4` plane set by pointer and never
69 // touches the edge adjacency, so it uses a LEAN cell with NO `adj` member (TrackAdj=false) —
70 // ~MAXT*3*4 bytes smaller per thread, which lifts occupancy on the full-N certify (the GPU
71 // repair's hot kernel). Only the cell-BUILDING paths (the gather's CellBuilder, rebuildAdjAll,
72 // surgicalRepair) need `adj` to derive poke4, so they use the TrackAdj=true BuildCell.
76
77 int N = 0;
78 int nProc =
79 0;
81 int sw = 4, densityCount = -1;
82 Real L[3] = {1, 1, 1};
83 Real tol =
84 0;
85 Real skin = 0;
86 int verifyCap = 2;
87
88 // ---- Phase 3: adaptive three-way gate (decided after the free certificate, before any gather)
89 // ----
90 bool useGate = true;
92 false;
96 double churnThresh =
97 0.55;
100 0.15;
102 int clusterNbhd = 18;
104 // ---- Phase 4: single-face surgical repair (no grid gather; gated, default OFF — see note) ----
105 // NEGATIVE RESULT (measured): on this engine surgical re-clip is BOTH slower and not robustly
106 // exact, so it stays off. (1) Slower: the cold gather clips closest-first with the
107 // security-radius early-out (the cell tightens fast, few live triangles per clip), whereas
108 // re-clipping the stored+partner candidates unsorted keeps the cell large and costs more O(#tri)
109 // horizon scans — the gather wins. (2) Not exact: a surgically-rebuilt cell that GAINS a
110 // neighbour absent from its candidate set stays convex, so the certificate can't see the gain
111 // (§1c) and the verify loop can't catch it; the small per-step error then COMPOUNDS in the
112 // resident store (maxRelV grew to ~0.5 over a sweep). Making it exact needs the maintained
113 // 1-/2-ring candidate set (the ConnectivityArena the plan parks). Kept as the implemented
114 // experiment behind this flag; the production path is the Phase-3 gated two-pass gather.
115 bool surgical =
116 false;
117 static constexpr int kExtraCap = 8;
118 // ---- Lawson local convexity certificate (the cheap O(nt) convexity test) ----
120 true;
127 bool adjFresh =
128 false;
132 bool useLocalNow = false;
133
135 Kokkos::View<Real*, Mem> vol; // N : cell volumes (the published geometry scalar)
136 Kokkos::View<Real*, Mem> xRef; // 3N : positions at the last (re)build (Verlet reference)
137 Kokkos::View<Real*, Mem> weight; // N : per-seed power weights (empty for Voronoi); DOFs alongside
138 // positions. Set via setWeights before rebuild/step.
139
141 void setWeights(const Kokkos::View<Real*, Mem>& w) { weight = w; }
142 // scratch
143 Kokkos::View<int*, Mem> mask, mask2, mover, rebuilt, wl1, wl2, wlM;
144 Kokkos::View<int*, Mem> cellFlag; // per linear grid-cell flagged-seed count (dilation)
145 Kokkos::View<int*, Mem> extraNbr, extraCnt; // Phase-4 partner-discovered candidate neighbours
146 Kokkos::View<int, Mem> counter;
147 int gridNcell = 0;
148
153 void alloc(int n, const Real Lbox[3], Real tol_, Real skin_, int sw_ = 4, int densityCount_ = -1,
154 int nProc_ = -1) {
155 N = n;
156 nProc = (nProc_ < 0) ? n : nProc_;
157 sw = sw_;
158 densityCount = densityCount_ < 0 ? n : densityCount_;
159 tol = tol_;
160 skin = skin_;
161 for (int k = 0; k < 3; ++k)
162 L[k] = Lbox[k];
163 store.alloc(n);
164 store.allocPoke4(); // resident local-cert plane set (computed by the gather, read each step by
165 // the cert)
166 using Kokkos::view_alloc;
167 using Kokkos::WithoutInitializing;
168 vol = Kokkos::View<Real*, Mem>("mt.vol", n);
169 xRef = Kokkos::View<Real*, Mem>(view_alloc(std::string("mt.xRef"), WithoutInitializing),
170 (size_t)n * 3);
171 mask = Kokkos::View<int*, Mem>("mt.mask", n);
172 mask2 = Kokkos::View<int*, Mem>("mt.mask2", n);
173 mover = Kokkos::View<int*, Mem>("mt.mover", n);
174 rebuilt = Kokkos::View<int*, Mem>("mt.rebuilt", n);
175 wl1 = Kokkos::View<int*, Mem>(view_alloc(std::string("mt.wl1"), WithoutInitializing), n);
176 wl2 = Kokkos::View<int*, Mem>(view_alloc(std::string("mt.wl2"), WithoutInitializing), n);
177 wlM = Kokkos::View<int*, Mem>(view_alloc(std::string("mt.wlM"), WithoutInitializing), n);
178 extraNbr = Kokkos::View<int*, Mem>(view_alloc(std::string("mt.extraNbr"), WithoutInitializing),
179 (size_t)n * kExtraCap);
180 extraCnt = Kokkos::View<int*, Mem>("mt.extraCnt", n);
181 counter = Kokkos::View<int, Mem>("mt.counter");
182 // Per-backend gate threshold: rebuild is cheap + clip-bound on GPU (repair loses past ~50%
183 // flagged), expensive on the CPU paths (repair pays out to ~85%). Hand-set from the
184 // cross-device sweep; the plan's Phase-3 tuning would fit these from logged (signals vs
185 // verify-clean) per backend.
186 constexpr bool host = Kokkos::SpaceAccessibility<Kokkos::HostSpace, Mem>::accessible;
187 churnThresh = host ? 0.70 : 0.50;
188 }
189
198 void rebuild(const Kokkos::View<Real*, Mem>& pos, bool eagerAdj = true) {
199 Kokkos::View<long*, Mem> gd;
200 const int nBuild =
201 (nProc == N) ? -1 : nProc; // build only the owned cells; ghosts are candidates
202 auto res = buildTessellation<Real, Weighted>(pos, weight, N, L, sw, densityCount, gd, NoSdf{},
203 /*withForceGeom=*/false, nBuild, store.np, store.nt,
205 Kokkos::deep_copy(vol, res.view.cellVolume);
206 Kokkos::deep_copy(xRef, pos);
207 adjFresh = false; // the cold build does not emit adjacency
208 if (eagerAdj)
209 maybeBuildAdj(pos); // the whole topology changed -> repopulate store.poke4 (unless gating)
210 }
211
213 int compact(const Kokkos::View<int*, Mem>& m, const Kokkos::View<int*, Mem>& wl) {
214 Kokkos::deep_copy(counter, 0);
215 auto M = m;
216 auto W = wl;
217 auto c = counter;
218 Kokkos::parallel_for(
219 "mt.compact", Kokkos::RangePolicy<Exec>(0, nProc), KOKKOS_LAMBDA(int i) {
220 if (M(i))
221 W(Kokkos::atomic_fetch_add(&c(), 1)) = i;
222 });
223 int h = 0;
224 Kokkos::deep_copy(h, counter);
225 return h;
226 }
227
236 template <bool Local>
237 void certify(const Kokkos::View<Real*, Mem>& pos, const Kokkos::View<int*, Mem>& outMask,
238 const Kokkos::View<int*, Mem>& outMover, bool useSkin) {
239 Kokkos::deep_copy(outMask, 0);
240 if (useSkin)
241 Kokkos::deep_copy(outMover, 0);
242 // Phase-4: in the Pass-1 certify, also record partner-discovered new neighbours for the
243 // surgical path. The gaining cells of a flip are the violated-plane partners; each pair gains
244 // the other, so scatter every other partner of a flagging cell onto each partner's
245 // extra-candidate list (no dedup — duplicates are harmless no-op clips; overflow past kExtraCap
246 // just truncates and the cell falls back to a full gather at verify). Only in Pass 1 (useSkin),
247 // and only when surgical is enabled.
248 const bool doExtra = surgical && useSkin;
249 if (doExtra)
250 Kokkos::deep_copy(extraCnt, 0);
251 auto EN = extraNbr;
252 auto EC = extraCnt;
253 constexpr int kCap = kExtraCap;
254 auto P = pos;
255 auto XR = xRef;
256 auto Vv = vol;
257 auto M = outMask;
258 auto Mv = outMover;
259 Store st = store;
260 auto Pk4 = store.poke4;
261 const Real Lx = L[0], Ly = L[1], Lz = L[2];
262 const Real Lxh = Real(0.5) * Lx, Lyh = Real(0.5) * Ly, Lzh = Real(0.5) * Lz;
263 const Real tolL = tol, half2 = Real(0.25) * skin * skin;
264 const bool skinOn = useSkin;
265 const int nP_ = nProc;
266 auto Wt = weight; // power weights (empty for Voronoi; captured for reevalGeometry<PlanePolicy>)
267 Kokkos::parallel_for(
268 "mt.certify", Kokkos::RangePolicy<Exec>(0, nProc), KOKKOS_LAMBDA(int i) {
269 CertCell c;
270 st.load(i, c, Lx, Ly, Lz);
271 const Real* wPtr = Wt.data(); // force-capture Wt OUTSIDE the constexpr-if (nvcc rule)
272 Real wSelfI = Real(0);
273 if constexpr (Weighted) wSelfI = Wt(i);
274 c.template reevalGeometry<PlanePolicy>(P(3 * i), P(3 * i + 1), P(3 * i + 2), P.data(), Lx,
275 wSelfI, wPtr);
276 Vv(i) = c.volumePerVertex();
277 int partners[32];
278 int nP = 0;
279 bool ok;
280 // Force-capture every lambda capture used in the branches OUTSIDE the constexpr-if (nvcc:
281 // an extended __host__ __device__ lambda cannot FIRST-capture a variable inside a
282 // constexpr-if).
283 const unsigned char* pk4 = &Pk4((size_t)i * MAXT * 4); // poke4 is always allocated
284 const Real tl = tolL;
285 if constexpr (Local)
286 ok = c.isLocallyConvexPartners(pk4, tl, partners, 32, nP);
287 else {
288 (void)pk4;
289 ok = c.isSelfConsistentPartners(tl, partners, 32, nP);
290 }
291 bool mover = false;
292 if (skinOn) {
293 Real dx = P(3 * i) - XR(3 * i), dy = P(3 * i + 1) - XR(3 * i + 1),
294 dz = P(3 * i + 2) - XR(3 * i + 2);
295 dx = dx > Lxh ? dx - Lx : (dx < -Lxh ? dx + Lx : dx);
296 dy = dy > Lyh ? dy - Ly : (dy < -Lyh ? dy + Ly : dy);
297 dz = dz > Lzh ? dz - Lz : (dz < -Lzh ? dz + Lz : dz);
298 mover = (dx * dx + dy * dy + dz * dz) > half2;
299 if (mover)
300 Mv(i) = 1;
301 }
302 if (!ok || mover)
303 M(i) = 1;
304 // only mark partners we MAINTAIN (owned); a ghost partner is the owning rank's
305 // responsibility.
306 for (int q = 0; q < nP; ++q)
307 if (partners[q] < nP_)
308 Kokkos::atomic_exchange(&M(partners[q]), 1);
309 if (doExtra) { // each pair of gaining cells gains the other -> seed the surgical
310 // candidate lists
311 for (int q = 0; q < nP; ++q) {
312 const int cell = partners[q];
313 if (cell < 0 || cell >= nP_)
314 continue;
315 for (int r = 0; r < nP; ++r) {
316 if (r == q)
317 continue;
318 const int nb = partners[r];
319 if (nb < 0)
320 continue;
321 const int slot = Kokkos::atomic_fetch_add(&EC(cell), 1);
322 if (slot < kCap)
323 EN((size_t)cell * kCap + slot) = nb;
324 }
325 }
326 }
327 });
328 }
329
338 template <bool Local>
339 void certifyList(const Kokkos::View<Real*, Mem>& pos, const Kokkos::View<int*, Mem>& list, int n,
340 const Kokkos::View<int*, Mem>& outMask) {
341 Kokkos::deep_copy(outMask, 0);
342 if (n <= 0)
343 return;
344 auto P = pos;
345 auto Vv = vol;
346 auto M = outMask;
347 Store st = store;
348 auto Pk4 = store.poke4;
349 const Real Lx = L[0], Ly = L[1], Lz = L[2];
350 const Real tolL = tol;
351 const int nP_ = nProc;
352 auto Wt = weight;
353 Kokkos::parallel_for(
354 "mt.certifyList", Kokkos::RangePolicy<Exec>(0, n), KOKKOS_LAMBDA(int s) {
355 const int i = list(s);
356 CertCell c;
357 st.load(i, c, Lx, Ly, Lz);
358 const Real* wPtr = Wt.data(); // force-capture Wt OUTSIDE the constexpr-if (nvcc rule)
359 Real wSelfI = Real(0);
360 if constexpr (Weighted) wSelfI = Wt(i);
361 c.template reevalGeometry<PlanePolicy>(P(3 * i), P(3 * i + 1), P(3 * i + 2), P.data(), Lx,
362 wSelfI, wPtr);
363 Vv(i) = c.volumePerVertex();
364 int partners[32];
365 int nP = 0;
366 bool ok;
367 const unsigned char* pk4 =
368 &Pk4((size_t)i * MAXT * 4); // force-capture poke4 + tol OUTSIDE the
369 const Real tl = tolL; // constexpr-if (nvcc extended-lambda rule)
370 if constexpr (Local)
371 ok = c.isLocallyConvexPartners(pk4, tl, partners, 32, nP);
372 else {
373 (void)pk4;
374 ok = c.isSelfConsistentPartners(tl, partners, 32, nP);
375 }
376 if (!ok)
377 M(i) = 1;
378 for (int q = 0; q < nP; ++q)
379 if (partners[q] < nP_)
380 Kokkos::atomic_exchange(&M(partners[q]), 1);
381 });
382 }
383
389 void rebuildAdjAll(const Kokkos::View<Real*, Mem>& pos) {
390 if (!localCert)
391 return;
392 Store st = store;
393 auto P = pos;
394 auto Pk4 = store.poke4;
395 const Real Lx = L[0], Ly = L[1], Lz = L[2];
396 auto Wt = weight;
397 Kokkos::parallel_for(
398 "mt.rebuildAdjAll", Kokkos::RangePolicy<Exec>(0, nProc), KOKKOS_LAMBDA(int i) {
399 BuildCell c;
400 st.load(i, c, Lx, Ly,
401 Lz); // load brings topology; adj is rebuilt to derive the cert planes
402 const Real* wPtr = Wt.data(); // force-capture Wt OUTSIDE the constexpr-if (nvcc rule)
403 Real wSelfI = Real(0);
404 if constexpr (Weighted) wSelfI = Wt(i);
405 c.template reevalGeometry<PlanePolicy>(P(3 * i), P(3 * i + 1), P(3 * i + 2), P.data(), Lx,
406 wSelfI, wPtr); // computePoke4 needs vertices
408 c.computePoke4(&Pk4((size_t)i * MAXT *
409 4)); // the only thing this pass produces (topology unchanged)
410 });
411 }
417 void maybeBuildAdj(const Kokkos::View<Real*, Mem>& pos) {
418 if (localCert && !adjFresh) {
419 rebuildAdjAll(pos);
420 adjFresh = true;
421 useLocalNow = true;
422 }
423 }
424
429 void gatherSet(const TessGrid<Real>& grid, const Kokkos::View<int*, Mem>& wl, int n,
430 const Kokkos::View<Real*, Mem>& pos) {
431 if (n <= 0)
432 return;
433 // TrackAdj gather: clip stitches the edge adjacency incrementally; the builder derives the cert
434 // plane set from it (computePoke4, no findSharing) into store.poke4, so the local cert stays
435 // valid for the gathered cells with no separate maintenance pass.
436 subsetGather<Real, Weighted, true>(grid, wl, n, store.np, store.nt, store.pnbr, store.tri, vol,
437 store.poke4, NoSdf{}, /*withForceGeom=*/false);
438 auto W = wl;
439 auto rb = rebuilt;
440 auto P = pos;
441 auto XR = xRef;
442 Kokkos::parallel_for(
443 "mt.markRebuilt", Kokkos::RangePolicy<Exec>(0, n), KOKKOS_LAMBDA(int s) {
444 const int i = W(s);
445 rb(i) = 1;
446 XR(3 * i) = P(3 * i);
447 XR(3 * i + 1) = P(3 * i + 1);
448 XR(3 * i + 2) = P(3 * i + 2);
449 });
450 }
451
454 void collectNewNbrs(const Kokkos::View<int*, Mem>& wl, int n,
455 const Kokkos::View<int*, Mem>& outMask) {
456 Kokkos::deep_copy(outMask, 0);
457 if (n <= 0)
458 return;
459 Store st = store;
460 auto rb = rebuilt;
461 auto M = outMask;
462 const int nP_ = nProc;
463 Kokkos::parallel_for(
464 "mt.collectNbrs", Kokkos::RangePolicy<Exec>(0, n), KOKKOS_LAMBDA(int s) {
465 const int i = wl(s);
466 const int np = st.np(i), nt = st.nt(i);
467 for (int k = 6; k < np; ++k) {
468 const int j = st.pnbr((size_t)i * MAXP + k);
469 if (j < 0 || j >= nP_ || rb(j))
470 continue; // only expand neighbours we MAINTAIN (owned)
471 int cnt = 0; // face = ≥3 incident live triangles
472 for (int t = 0; t < nt; ++t) {
473 const unsigned w = st.tri((size_t)i * MAXT + t);
474 if (!((w >> 24) & 1u))
475 continue;
476 const int a = (int)(w & 0xffu), b = (int)((w >> 8) & 0xffu),
477 cc = (int)((w >> 16) & 0xffu);
478 if (a == k || b == k || cc == k)
479 ++cnt;
480 }
481 if (cnt >= 3)
482 Kokkos::atomic_exchange(&M(j), 1);
483 }
484 });
485 }
486
495 void surgicalRepair(const Kokkos::View<int*, Mem>& wl, int n,
496 const Kokkos::View<Real*, Mem>& pos) {
497 if (n <= 0)
498 return;
499 auto W = wl;
500 auto P = pos;
501 auto rb = rebuilt;
502 auto XR = xRef;
503 auto Vv = vol;
504 auto EN = extraNbr;
505 auto EC = extraCnt;
506 constexpr int kCap = kExtraCap;
507 Store st = store;
508 auto Pk4 = store.poke4;
509 const Real Lx = L[0], Ly = L[1], Lz = L[2];
510 const Real Lxh = Real(0.5) * Lx, Lyh = Real(0.5) * Ly, Lzh = Real(0.5) * Lz;
511 Kokkos::parallel_for(
512 "mt.surgical", Kokkos::RangePolicy<Exec>(0, n), KOKKOS_LAMBDA(int s) {
513 const int i = W(s);
514 BuildCell c;
515 c.initBox(Lx, Ly, Lz);
516 const Real sx = P(3 * i), sy = P(3 * i + 1), sz = P(3 * i + 2);
517 const int npi = st.np(i);
518 for (int k = 6; k < npi && !c.overflow; ++k) { // stored neighbours, current positions
519 const int j = st.pnbr((size_t)i * MAXP + k);
520 if (j < 0)
521 continue;
522 Real rx = P(3 * j) - sx, ry = P(3 * j + 1) - sy, rz = P(3 * j + 2) - sz;
523 rx = rx > Lxh ? rx - Lx : (rx < -Lxh ? rx + Lx : rx);
524 ry = ry > Lyh ? ry - Ly : (ry < -Lyh ? ry + Ly : ry);
525 rz = rz > Lzh ? rz - Lz : (rz < -Lzh ? rz + Lz : rz);
526 const Real nrm[3] = {rx, ry, rz};
527 c.clip(nrm, Real(0.5) * (rx * rx + ry * ry + rz * rz), j);
528 }
529 const int ne = EC(i) < kCap ? EC(i) : kCap;
530 for (int e = 0; e < ne && !c.overflow; ++e) { // partner-discovered new neighbours
531 const int j = EN((size_t)i * kCap + e);
532 if (j < 0 || j == i)
533 continue;
534 Real rx = P(3 * j) - sx, ry = P(3 * j + 1) - sy, rz = P(3 * j + 2) - sz;
535 rx = rx > Lxh ? rx - Lx : (rx < -Lxh ? rx + Lx : rx);
536 ry = ry > Lyh ? ry - Ly : (ry < -Lyh ? ry + Ly : ry);
537 rz = rz > Lzh ? rz - Lz : (rz < -Lzh ? rz + Lz : rz);
538 const Real nrm[3] = {rx, ry, rz};
539 c.clip(nrm, Real(0.5) * (rx * rx + ry * ry + rz * rz), j);
540 }
541 if (!c.overflow) {
542 st.save(i, c);
543 Vv(i) = c.volumePerVertex();
544 c.computePoke4(
545 &Pk4((size_t)i * MAXT * 4)); // refresh the cert plane set for this re-clipped cell
546 }
547 rb(i) = 1;
548 XR(3 * i) = sx;
549 XR(3 * i + 1) = sy;
550 XR(3 * i + 2) = sz;
551 });
552 }
553
560 bool dilate(const TessGrid<Real>& grid, const Kokkos::View<Real*, Mem>& pos) {
561 const int dimx = grid.dimx, dimy = grid.dimy, dimz = grid.dimz;
562 const int ncell = dimx * dimy * dimz;
563 if ((int)cellFlag.extent(0) < ncell)
564 cellFlag = Kokkos::View<int*, Mem>(
565 Kokkos::view_alloc(std::string("mt.cellFlag"), Kokkos::WithoutInitializing), ncell);
566 Kokkos::deep_copy(cellFlag, 0);
567 const Real icx = grid.icx, icy = grid.icy, icz = grid.icz;
568 auto P = pos;
569 auto M = mask;
570 auto CF = cellFlag;
571 Kokkos::parallel_for(
572 "mt.dilate.count", Kokkos::RangePolicy<Exec>(0, nProc), KOKKOS_LAMBDA(int i) {
573 if (!M(i))
574 return;
575 const int gx = ((int)Kokkos::floor(P(3 * i) * icx) % dimx + dimx) % dimx;
576 const int gy = ((int)Kokkos::floor(P(3 * i + 1) * icy) % dimy + dimy) % dimy;
577 const int gz = ((int)Kokkos::floor(P(3 * i + 2) * icz) % dimz + dimz) % dimz;
578 Kokkos::atomic_inc(&CF(gx + gy * dimx + gz * dimx * dimy));
579 });
580 const int thr = clusterNbhd;
581 long added = 0;
582 Kokkos::parallel_reduce(
583 "mt.dilate.mark", Kokkos::RangePolicy<Exec>(0, nProc),
584 KOKKOS_LAMBDA(int i, long& a) {
585 if (M(i))
586 return;
587 const int gx = ((int)Kokkos::floor(P(3 * i) * icx) % dimx + dimx) % dimx;
588 const int gy = ((int)Kokkos::floor(P(3 * i + 1) * icy) % dimy + dimy) % dimy;
589 const int gz = ((int)Kokkos::floor(P(3 * i + 2) * icz) % dimz + dimz) % dimz;
590 int sum = 0;
591 for (int dz = -1; dz <= 1; ++dz)
592 for (int dy = -1; dy <= 1; ++dy)
593 for (int dx = -1; dx <= 1; ++dx) {
594 const int nx = (gx + dx + dimx) % dimx, ny = (gy + dy + dimy) % dimy,
595 nz = (gz + dz + dimz) % dimz;
596 sum += CF(nx + ny * dimx + nz * dimx * dimy);
597 }
598 if (sum > thr) {
599 M(i) = 1;
600 ++a;
601 }
602 },
603 added);
604 return added > 0;
605 }
606
610 void certifyDispatch(const Kokkos::View<Real*, Mem>& pos, const Kokkos::View<int*, Mem>& outMask,
611 const Kokkos::View<int*, Mem>& outMover, bool useSkin) {
612 if (useLocalNow)
613 certify<true>(pos, outMask, outMover, useSkin);
614 else
615 certify<false>(pos, outMask, outMover, useSkin);
616 }
617 void certifyListDispatch(const Kokkos::View<Real*, Mem>& pos, const Kokkos::View<int*, Mem>& list,
618 int n, const Kokkos::View<int*, Mem>& outMask) {
619 if (useLocalNow)
620 certifyList<true>(pos, list, n, outMask);
621 else
622 certifyList<false>(pos, list, n, outMask);
623 }
624
626 RepairStats step(const Kokkos::View<Real*, Mem>& pos) {
627 RepairStats s;
628 auto grid = buildTessGrid<Real, Weighted>(pos, weight, N, L, sw, densityCount,
629 Kokkos::View<long*, Mem>());
630 Kokkos::deep_copy(rebuilt, 0);
631 // Use the cheap local certificate whenever store.poke4 is valid for the current topology. With
632 // the 4th-face plane included the local cert matches the brute flag set (it is complete, not a
633 // subset), so no per-step displacement gate or brute fallback is needed — large moves and
634 // teleports are handled by the same detect→gather→verify path. (adj/face4 are invalidated only
635 // by a cold rebuild.)
637
638 // Pass 1: detect (flagged ∪ partners ∪ skin-movers). flagged-commons + their violated-plane
639 // partners (the gaining cells) fully cover an isolated flip — no Pass 2 for flips.
640 certifyDispatch(pos, mask, mover, /*useSkin=*/true);
641 int n1 = compact(mask, wl1);
642 s.pass1Raw = n1;
643
644 // Small-displacement fast path: the certificate found every cell consistent and no skin-mover
645 // (n1 counts flagged ∪ partners ∪ movers). By the §1 completeness argument a stale topology
646 // always flags some cell, so n1==0 ⇒ the stored topology is still Voronoi on the new positions
647 // — the re-eval already refreshed every volume, so publish with NO gather and NO verify pass.
648 // This is the "almost no cells invalidated" regime: the per-step cost collapses to one re-eval
649 // + the grid.
650 if (n1 == 0) {
651 maybeBuildAdj(pos);
652 return s;
653 }
654
655 // Phase-3 adaptive gate (decided here, after the FREE certificate, before any gather):
656 // high global churn -> a full rebuild is cheaper than repairing this many cells;
657 // dense local cluster -> dilate the Pass-1 set by a grid-cell buffer (one regional gather);
658 // else (sparse) -> the two-pass repair below.
659 if (useGate && (double)n1 / (nProc > 0 ? nProc : 1) > churnThresh) {
660 rebuild(pos,
661 /*eagerAdj=*/false); // high-churn regime: stay on brute, don't pay adj maintenance
663 return s;
664 }
665 if (useGate && useDilation && n1 > 0 && (double)n1 / nProc < dilateMaxChurn &&
666 dilate(grid, pos)) {
667 n1 = compact(mask, wl1);
669 }
670 s.pass1 = n1;
671 int nM = compact(mover, wlM);
672 // Pass 1 gather. Phase 4: FLIP cells (flagged non-movers) are repaired SURGICALLY (re-clip
673 // known candidates, no grid gather); far-mover insertions keep the full gather (their new
674 // neighbours are in no candidate list). Default (surgical off): the whole Pass-1 set goes
675 // through the full gather.
676 if (surgical) {
677 auto Msk = mask;
678 auto Mv = mover;
679 auto M2 = mask2;
680 Kokkos::parallel_for(
681 "mt.surgMask", Kokkos::RangePolicy<Exec>(0, nProc),
682 KOKKOS_LAMBDA(int i) { M2(i) = (Msk(i) && !Mv(i)) ? 1 : 0; });
683 int nS =
684 compact(mask2, wl1); // wl1's Pass-1 list no longer needed -> reuse for the flip list
685 s.surgical = nS;
686 surgicalRepair(wl1, nS, pos); // flips: no gather
687 gatherSet(grid, wlM, nM, pos); // movers: full gather
688 } else {
689 gatherSet(grid, wl1, n1, pos);
690 }
691
692 // Pass 2: ONLY the new face-neighbours of the skin-MOVERS (the insertion side §1c — those
693 // neighbours never flag and are never partners). Expanding every Pass-1 cell would re-clip ~all
694 // neighbours of every flip cluster for nothing; movers are the only cells whose rebuild reveals
695 // an unflagged gainer.
697 int n2 = compact(mask2, wl2);
698 s.pass2 = n2;
699 gatherSet(grid, wl2, n2, pos);
701 pos); // first low-churn step after a rebuild: populate adj so verify + next steps go local
702
703 // Verify (scoped): re-certify only the GATHERED cells (every non-gathered cell is provably
704 // unchanged — see certifyList). Clean ⇒ publish; else gather the still-flagged ∪ partners
705 // (residual coupled flips / tol-degenerate cells) and re-verify, bounded.
706 for (int vp = 0; vp < verifyCap + 1; ++vp) {
707 const int ng = compact(rebuilt, wl1); // all cells gathered so far this step
708 // Verify with the step's certificate: a far/teleport step already forced useLocalNow=false,
709 // so its verify is the COMPLETE brute form (catches far-pokes the gather left on a gathered
710 // cell). A small- displacement local step uses the cheap local verify over the
711 // freshly-gathered (locally convex) set.
712 certifyListDispatch(pos, wl1, ng, mask);
713 int nv = compact(mask, wl2);
714 s.verifyPasses = vp + 1;
715 if (nv == 0)
716 return s; // clean — done
717 if (vp == verifyCap)
718 break; // out of budget — fall back
719 s.extra += nv;
720 gatherSet(grid, wl2, nv, pos);
721 }
722
723 // Still dirty after the verify budget: cold rebuild (always exact). Rare; the data point that
724 // says "for this step, rebuild was cheaper" — the adaptive gate (Phase 3) will pre-empt it up
725 // front.
726 rebuild(pos);
727 s.fellBack = true;
728 return s;
729 }
730};
731
732} // namespace peclet::voro
733
734#endif // PECLET_VORO_REPAIR_HPP
Definition convex_cell.hpp:40
Definition convex_cell.hpp:139
KOKKOS_INLINE_FUNCTION Real volumePerVertex() const
Definition convex_cell.hpp:1036
KOKKOS_INLINE_FUNCTION bool isLocallyConvexPartners(const unsigned char *poke4, Real tol, int *outPartners, int maxPartners, int &nPartners) const
Definition convex_cell.hpp:453
KOKKOS_INLINE_FUNCTION void initBox(Real L0, Real L1, Real L2)
Definition convex_cell.hpp:173
bool overflow
set if MAXP/MAXT exceeded -> cell invalid, caller falls back
Definition convex_cell.hpp:154
KOKKOS_INLINE_FUNCTION void rebuildAdjacency()
Definition convex_cell.hpp:523
KOKKOS_INLINE_FUNCTION bool isSelfConsistentPartners(Real tol, int *outPartners, int maxPartners, int &nPartners) const
Definition convex_cell.hpp:342
KOKKOS_INLINE_FUNCTION bool clip(const Real pdir[3], Real d, int nbr)
Definition convex_cell.hpp:584
KOKKOS_INLINE_FUNCTION void computePoke4(unsigned char *out) const
Definition convex_cell.hpp:391
Definition repair.hpp:64
int clusterNbhd
Definition repair.hpp:102
bool dilate(const TessGrid< Real > &grid, const Kokkos::View< Real *, Mem > &pos)
Definition repair.hpp:560
void surgicalRepair(const Kokkos::View< int *, Mem > &wl, int n, const Kokkos::View< Real *, Mem > &pos)
Definition repair.hpp:495
bool useDilation
Definition repair.hpp:91
int densityCount
Definition repair.hpp:81
void certifyDispatch(const Kokkos::View< Real *, Mem > &pos, const Kokkos::View< int *, Mem > &outMask, const Kokkos::View< int *, Mem > &outMover, bool useSkin)
Definition repair.hpp:610
int nProc
Definition repair.hpp:78
Real skin
Verlet skin width (absolute); a particle moving > skin/2 is a Pass-1 mover.
Definition repair.hpp:85
Kokkos::View< int *, Mem > wl1
Definition repair.hpp:143
bool surgical
repair flip cells by re-clipping known candidates instead of gathering
Definition repair.hpp:115
Kokkos::View< int *, Mem > extraNbr
Definition repair.hpp:145
Kokkos::View< Real *, Mem > vol
Definition repair.hpp:135
Kokkos::View< int *, Mem > mask2
Definition repair.hpp:143
void rebuild(const Kokkos::View< Real *, Mem > &pos, bool eagerAdj=true)
Definition repair.hpp:198
bool useGate
high-churn → rebuild routing (the "never slower than rebuild" guard)
Definition repair.hpp:90
Real tol
certificate tolerance (absolute distance); ~1e-4·spacing FP64, ~2e-3·spacing FP32
Definition repair.hpp:83
int N
total cells in the resident arrays (single-domain: all; MPI: owned+ghost)
Definition repair.hpp:77
Kokkos::View< int, Mem > counter
Definition repair.hpp:146
bool useLocalNow
this step's certify uses the local cert (= localCert && adjFresh)
Definition repair.hpp:132
bool localCert
Definition repair.hpp:119
int verifyCap
max verify extra-passes before falling back to a full rebuild
Definition repair.hpp:86
std::conditional_t< Weighted, Power, Voronoi > PlanePolicy
Definition repair.hpp:67
int gridNcell
Definition repair.hpp:147
Kokkos::View< int *, Mem > wl2
Definition repair.hpp:143
void rebuildAdjAll(const Kokkos::View< Real *, Mem > &pos)
Definition repair.hpp:389
void gatherSet(const TessGrid< Real > &grid, const Kokkos::View< int *, Mem > &wl, int n, const Kokkos::View< Real *, Mem > &pos)
Definition repair.hpp:429
int compact(const Kokkos::View< int *, Mem > &m, const Kokkos::View< int *, Mem > &wl)
Compact the nonzero entries of m[0..nProc) into wl, returning the count.
Definition repair.hpp:213
peclet::core::ExecSpace Exec
Definition repair.hpp:66
void collectNewNbrs(const Kokkos::View< int *, Mem > &wl, int n, const Kokkos::View< int *, Mem > &outMask)
Definition repair.hpp:454
Kokkos::View< Real *, Mem > weight
Definition repair.hpp:137
static constexpr int kExtraCap
per-cell capacity for partner-discovered new neighbours
Definition repair.hpp:117
void certify(const Kokkos::View< Real *, Mem > &pos, const Kokkos::View< int *, Mem > &outMask, const Kokkos::View< int *, Mem > &outMover, bool useSkin)
Definition repair.hpp:237
int sw
Definition repair.hpp:81
void setWeights(const Kokkos::View< Real *, Mem > &w)
Set the per-seed power weights (Weighted only). Held by reference; the caller keeps it alive.
Definition repair.hpp:141
Kokkos::View< int *, Mem > cellFlag
Definition repair.hpp:144
Kokkos::View< int *, Mem > rebuilt
Definition repair.hpp:143
Kokkos::View< Real *, Mem > xRef
Definition repair.hpp:136
void certifyList(const Kokkos::View< Real *, Mem > &pos, const Kokkos::View< int *, Mem > &list, int n, const Kokkos::View< int *, Mem > &outMask)
Definition repair.hpp:339
Kokkos::View< int *, Mem > mask
Definition repair.hpp:143
Kokkos::View< int *, Mem > wlM
Definition repair.hpp:143
RepairStats step(const Kokkos::View< Real *, Mem > &pos)
One moving-point update step. Updates store + vol in place from pos.
Definition repair.hpp:626
double dilateMaxChurn
Definition repair.hpp:99
bool adjFresh
Definition repair.hpp:127
double churnThresh
Definition repair.hpp:96
Kokkos::View< int *, Mem > extraCnt
Definition repair.hpp:145
void certifyListDispatch(const Kokkos::View< Real *, Mem > &pos, const Kokkos::View< int *, Mem > &list, int n, const Kokkos::View< int *, Mem > &outMask)
Definition repair.hpp:617
Store store
Definition repair.hpp:134
Kokkos::View< int *, Mem > mover
Definition repair.hpp:143
Real L[3]
Definition repair.hpp:82
void alloc(int n, const Real Lbox[3], Real tol_, Real skin_, int sw_=4, int densityCount_=-1, int nProc_=-1)
Definition repair.hpp:153
void maybeBuildAdj(const Kokkos::View< Real *, Mem > &pos)
Definition repair.hpp:417
peclet::core::MemSpace Mem
Definition repair.hpp:65
Sentinel "no geometry" provider — the default; the clip stage is skipped.
Definition sdf.hpp:36
Per-step repair telemetry.
Definition repair.hpp:48
int extra
cells gathered across the verify extra-passes
Definition repair.hpp:54
int pass1
cells gathered in Pass 1 (flagged ∪ partners ∪ skin-movers, after dilation)
Definition repair.hpp:51
int surgical
Pass-1 cells repaired surgically (Phase 4, no grid gather)
Definition repair.hpp:55
int pass2
cells gathered in Pass 2 (new face-neighbours of the movers)
Definition repair.hpp:53
bool fellBack
true if the cold-build fallback was triggered (repair did not close)
Definition repair.hpp:58
Route route
Definition repair.hpp:57
int pass1Raw
flagged count BEFORE dilation (the gate signal: pass1Raw/nProc = churn)
Definition repair.hpp:52
Route
Which path the Phase-3 adaptive gate took this step.
Definition repair.hpp:50
@ kDilated
Definition repair.hpp:50
@ kTwoPass
Definition repair.hpp:50
@ kRebuildGate
Definition repair.hpp:50
int verifyPasses
number of verify iterations run
Definition repair.hpp:56
Definition tess_grid.hpp:55
int dimz
Definition tess_grid.hpp:70
int dimy
Definition tess_grid.hpp:70
int dimx
Definition tess_grid.hpp:70
Real icz
Definition tess_grid.hpp:69
Real icx
Definition tess_grid.hpp:69
Real icy
Definition tess_grid.hpp:69
Definition topology_store.hpp:39
void alloc(int n)
Definition topology_store.hpp:53
Kokkos::View< int *, MemSpace > np
Definition topology_store.hpp:42
Kokkos::View< unsigned char *, MemSpace > poke4
Definition topology_store.hpp:47
KOKKOS_INLINE_FUNCTION void load(int i, Cell &c, Real L0, Real L1, Real L2) const
Definition topology_store.hpp:93
Kokkos::View< unsigned *, MemSpace > tri
Definition topology_store.hpp:45
KOKKOS_INLINE_FUNCTION void save(int i, const Cell &c) const
Persist cell c at slot i (call after the cell is finalised — clipped + complete).
Definition topology_store.hpp:75
Kokkos::View< int *, MemSpace > nt
Definition topology_store.hpp:43
Kokkos::View< int *, MemSpace > pnbr
Definition topology_store.hpp:44
void allocPoke4()
Definition topology_store.hpp:67