peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solve_driver.hpp
Go to the documentation of this file.
1
31#ifndef DEM_SOLVE_DRIVER_HPP
32#define DEM_SOLVE_DRIVER_HPP
33
34#include <algorithm>
35#include <cstdlib>
36#include <Kokkos_Core.hpp>
37#include <stdexcept>
38#include <string>
39#include <utility>
40#include <vector>
41
42#include "broadphase_arborx.hpp"
44#include "particles.hpp"
45#include "solve_copies.hpp"
46#include "solver_friction.hpp"
47#include "solver_multilevel.hpp"
48#include "solver_position.hpp"
49#include "solver_velocity.hpp"
50
51namespace peclet::dem {
52
53inline int readInt(Kokkos::View<int, CpMem> v) {
54 int h;
55 Kokkos::deep_copy(h, v);
56 return h;
57}
58inline float readFloat(Kokkos::View<float, CpMem> v) {
59 float h;
60 Kokkos::deep_copy(h, v);
61 return h;
62}
63
66inline void fillGidBaseKokkos(Vi gid, int n, int base) {
67 Kokkos::parallel_for(
68 "peclet::dem::gid_base", Kokkos::RangePolicy<CpExec>(0, n),
69 KOKKOS_LAMBDA(int i) { gid(i) = base + i; });
70 Kokkos::fence();
71}
72
78inline float maxOwnedRadius(const Particles& P) {
79 if (P.numReal <= 0)
80 return P.globalScale * P.baseRadius;
81 float mx = 0.0f;
82 auto sc = P.scale;
83 Kokkos::parallel_reduce(
84 "peclet::dem::max_scale", Kokkos::RangePolicy<CpExec>(0, P.numReal),
85 KOKKOS_LAMBDA(int i, float& m) { m = sc(i) > m ? sc(i) : m; }, Kokkos::Max<float>(mx));
86 return mx * P.globalScale * P.baseRadius;
87}
88
94inline void fillWorldRadiiKokkos(Vf scale, Vf rad, float gs, float bR, int n) {
95 CpExec space;
96 Kokkos::parallel_for(
97 "peclet::dem::world_radii", Kokkos::RangePolicy<CpExec>(space, 0, n),
98 KOKKOS_LAMBDA(int i) { rad(i) = scale(i) * gs * bR; });
99 space.fence();
100}
101
112inline int findCollisionsGrow(Particles& P, float margin) {
113 const float boxCap = std::max(std::max(P.domain.size.x, P.domain.size.y), P.domain.size.z);
114 int np = findCollisionsArborX(P.posPred, P.crad(), P.numParticles, P.numReal, margin, P.pairs,
115 P.pairCount, boxCap);
116 if (np > static_cast<int>(P.pairs.extent(0))) {
117 const int grown = np + np / 2 + 64; // 1.5× + slack
118 Kokkos::realloc(Kokkos::WithoutInitializing, P.pairs, grown);
119 P.maxPairs = grown;
120 np = findCollisionsArborX(P.posPred, P.crad(), P.numParticles, P.numReal, margin, P.pairs,
121 P.pairCount, boxCap);
122 }
123 return std::min(np, static_cast<int>(P.pairs.extent(0)));
124}
125
134inline int findCollisionsVerlet(Particles& P, float margin, float maxRad) {
135 const float skin = P.verletSkinFrac * maxRad;
136 bool rebuild = (P.impNumPairs < 0);
137 if (!rebuild) {
138 float md2 = 0.0f;
139 auto pp = P.posPred;
140 auto rp = P.impRefPos;
141 Kokkos::parallel_reduce(
142 "peclet::dem::verlet_disp", Kokkos::RangePolicy<CpExec>(0, P.numReal),
143 KOKKOS_LAMBDA(int i, float& acc) {
144 const float dx = pp(i, 0) - rp(i, 0), dy = pp(i, 1) - rp(i, 1), dz = pp(i, 2) - rp(i, 2);
145 const float d = dx * dx + dy * dy + dz * dz;
146 if (d > acc)
147 acc = d;
148 },
149 Kokkos::Max<float>(md2));
150 Kokkos::fence();
151 // Growth grows radii between rebuilds; a pair can close by ~2x the radius growth with no CoM
152 // motion, so fold it into the displacement budget.
153 const float grow = std::max(0.0f, maxRad - P.impRefMaxRad);
154 if (std::sqrt(md2) + 2.0f * grow > 0.5f * skin)
155 rebuild = true;
156 }
157 if (rebuild) {
158 const int np = findCollisionsGrow(P, margin + skin); // candidate list at margin + skin
159 const auto rng = Kokkos::pair<int, int>(0, P.numReal);
160 Kokkos::deep_copy(Kokkos::subview(P.impRefPos, rng, Kokkos::ALL),
161 Kokkos::subview(P.posPred, rng, Kokkos::ALL));
162 P.impNumPairs = np;
163 P.impRefMaxRad = maxRad;
164 return np;
165 }
166 return P.impNumPairs; // reuse the cached candidate list (P.pairs is unchanged)
167}
168
169#ifdef KOKKOS_ENABLE_CUDA
177struct CudaIterGraph {
178 cudaGraphExec_t exec = nullptr;
179 template <class F>
180 bool capture(CpExec& space, F&& emit, void*& cacheSlot) {
181 cudaStream_t str = space.cuda_stream();
182 if (cudaStreamBeginCapture(str, cudaStreamCaptureModeThreadLocal) != cudaSuccess) {
183 (void)cudaGetLastError();
184 return false;
185 }
186 emit();
187 cudaGraph_t g = nullptr;
188 if (cudaStreamEndCapture(str, &g) != cudaSuccess || g == nullptr) {
189 (void)cudaGetLastError();
190 return false;
191 }
192 // Instantiation dominates the per-step graph cost (~10 us/node); the topology is stable
193 // step-to-step, so refresh last step's executable in place and only re-instantiate when the
194 // structure genuinely changed (bucket emptiness pattern / colour count shifts).
195 if (cacheSlot != nullptr) {
196 exec = static_cast<cudaGraphExec_t>(cacheSlot);
197 cudaGraphExecUpdateResultInfo ri;
198 if (cudaGraphExecUpdate(exec, g, &ri) != cudaSuccess) {
199 (void)cudaGetLastError();
200 cudaGraphExecDestroy(exec);
201 exec = nullptr;
202 cacheSlot = nullptr;
203 }
204 }
205 if (exec == nullptr) {
206 if (cudaGraphInstantiate(&exec, g, nullptr, nullptr, 0) != cudaSuccess) {
207 (void)cudaGetLastError();
208 exec = nullptr;
209 }
210 cacheSlot = exec;
211 }
212 cudaGraphDestroy(g);
213 return exec != nullptr;
214 }
215 void launch(CpExec& space) { cudaGraphLaunch(exec, space.cuda_stream()); }
216 // the executable is owned by the cross-step cache slot, not this per-step handle
217};
220#define PECLET_DEM_GRAPH_LOOP(useVar, graphVar, emitVar, slotVar) \
221 if constexpr (!Hooks::distributed) { \
222 if (P.cudaGraphs) \
223 useVar = graphVar.capture(space, emitVar, slotVar); \
224 }
225#else
227 template <class F>
228 bool capture(CpExec&, F&&, void*&) {
229 return false;
230 }
231 void launch(CpExec&) {}
232};
233#define PECLET_DEM_GRAPH_LOOP(useVar, graphVar, emitVar, slotVar) (void)graphVar;
234#endif
235
241struct RankK {
242 bool exchanges = false;
243 // Rank-level X (§1.4, WO-6): this rank's colour col(r), the number of colours C of the rank
244 // graph, and the sync interval (solver iterations per sync) that the holder cycle steps with.
245 int color = 0;
246 int numColors = 1;
248};
249
252 static constexpr bool distributed = false;
253 float allMax(float v) const { return v; }
254 float allMaxAny(float v, bool&) const { return v; }
255 bool syncPoint(int) const { return false; }
256 int visibleManifolds(int nm) const { return nm; }
257 void beginSolve(Particles&) const {}
258 void syncVelocities(Particles&) const {}
260 void syncPositions(Particles&) const {}
263 // Rank-level M (docs/contact_solve_framework.md §13.3): a single rank has no remote copies, so
264 // the opening is "step 0" only -- k = max(1, a(own)) = WO-4's groupK, which the driver uses
265 // directly -- and there is no rank-level k to expose.
266 RankK rankK() const { return {}; }
267 void openVelocityPhase(Particles&, bool) const {}
268 void openPositionCounts(Particles&, bool) const {}
270};
271
274inline int* demIterCountSlot(Particles& P, int slot) {
275 if (P.iterCountDev.extent(0) < 2)
276 P.iterCountDev = Kokkos::View<int*, CpMem>("peclet::dem::iter_count", 2);
277 return P.iterCountDev.data() + slot;
278}
281inline int demReadIterCount(const Particles& P, int slot) {
282 if (!P.iterCounters || P.iterCountDev.extent(0) < 2)
283 return -1;
284 int v = -1;
285 Kokkos::deep_copy(v, Kokkos::subview(P.iterCountDev, slot));
286 return v;
287}
288
292template <class Hooks>
293inline void demSolveContacts(Particles& P, int nc, int nm, int nBodies,
294 Kokkos::View<const int*, CpMem> keyIdx, const Hooks& hooks) {
295 CpExec space;
296 hooks.beginSolve(P);
297 // Rank-level X (§1.4): identical on every rank; each substep starts the holder cycle one step
298 // later, so no rank is systematically first. Incremented on every rank and every call (np 1
299 // included, where it is read by nothing).
300 ++P.solveEpoch;
301 Kokkos::Profiling::pushRegion("dem::solve::prep");
302 const int nmVisible = hooks.visibleManifolds(nm);
303
304 // A frictional wall drives friction even when the body-body material is frictionless.
305 const bool friction = (P.frictionDynamic > 0.0f || P.wallFrictionMax > 0.0f);
306 const bool usePersistPre = (P.gravity.x != 0.0f || P.gravity.y != 0.0f || P.gravity.z != 0.0f);
307 // Incremental (warm-started) colouring (set_incremental_coloring, default on; single-GPU PGS
308 // path). Off forces the full per-substep recolour — the pre-incremental behaviour, kept for
309 // validation. The colouring decides the Gauss-Seidel sweep order, so this DOES change results.
310 const bool incrColorOff = !P.incrementalColoring;
311 // Island sleeping (single-GPU statics, gravity on, no external drag): both-asleep manifolds /
312 // contacts are excluded from the colouring / sweeps / multilevel hierarchy (their masks were
313 // filled by the caller). Empty views leave every colouring bit-identical to the sleeping-off
314 // path. The caller has already swapped P.invMass to the effective (sleeper -> 0) inverse mass.
315 const bool sleepOn = P.sleepingEnabled && !Hooks::distributed && usePersistPre &&
317 const Kokkos::View<const unsigned char*, CpMem> mSleep =
318 sleepOn ? Kokkos::View<const unsigned char*, CpMem>(P.manifoldSleep)
319 : Kokkos::View<const unsigned char*, CpMem>();
320 const Kokkos::View<const unsigned char*, CpMem> cSleep =
321 sleepOn ? Kokkos::View<const unsigned char*, CpMem>(P.contactSleep)
322 : Kokkos::View<const unsigned char*, CpMem>();
323 const bool legacyFriction = friction && !(usePersistPre && P.velocityUseGS);
324 if (legacyFriction)
326 P.planeFriction);
327
328 // Colour the manifold graph ONCE (topology-only; reused across the sweeps), then normal
329 // restitution as colored Gauss–Seidel: correct multi-contact dissipation with no count-averaging
330 // (see solver_velocity.hpp). count==1 binary collisions are identical to the old Jacobi path.
331 int velLeftover = 0;
332 int numColors = 0;
333 // Complete colourings (docs/contact_solve_framework.md §4.2 item 4): colour each phase graph
334 // once; if an edge found no free colour, split every vertex above kHubEdgeBudget edges into hub
335 // copies (§4.4) and recolour that phase from scratch on the copy vertices -- guaranteed to
336 // succeed (§1.6). Anything still uncoloured violates the colouring invariant (§12 S3): a single
337 // rank throws here, the distributed step votes it in its first stop Allreduce and every rank
338 // throws. Copy slots are appended at [slotBase, slotBase + nCopies); both phases reuse the range.
339 PhaseCopies& VC = P.velCopies;
340 PhaseCopies& PC = P.posCopies;
341 VC.nHubs = VC.nCopies = VC.nGroups = 0;
342 PC.nHubs = PC.nCopies = PC.nGroups = 0;
343 const int slotBase = P.numParticles;
344 VC.slotBase = PC.slotBase = slotBase;
345 { // mlHubAggregated is the max over the step call's substeps (reset by the step entry points)
346 const int mlAgg = P.splitStats.mlHubAggregated;
347 const long long drift = P.splitStats.driftMigrations; // cumulative (the drift vote, §5.1)
349 P.splitStats.mlHubAggregated = mlAgg;
350 P.splitStats.driftMigrations = drift;
351 }
352 P.mlLast.numLevels = 0;
353 Kokkos::View<int*, CpMem> edgeA, edgeB; // a failed phase's colouring edges (scratch)
354 auto growSlots = [&](int nCopies) {
355 if (slotBase + nCopies > P.capacity)
356 P.ensureCapacity(slotBase + nCopies + 64);
357 };
358 std::string invariantMsg;
359 auto noteInvariant = [&](const char* phase, Kokkos::View<const int*, CpMem> color, int n) {
360 // Name the first uncolourable edge's vertices and their degrees (host readback, error path).
361 auto hc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), color);
362 auto ha = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), edgeA);
363 auto hb = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), edgeB);
364 auto hd = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), P.vertexDegree);
365 int bad = 0, first = -1;
366 for (int e = 0; e < n; ++e)
367 if (hc(e) == -1 || hc(e) == kColorUncolourable) {
368 ++bad;
369 if (first < 0)
370 first = e;
371 }
372 std::string m = std::string("colouring invariant violated: ") + std::to_string(bad) + " " +
373 phase + " uncolourable edges after hub splitting";
374 if (first >= 0) {
375 m += " (first: vertex " + std::to_string(ha(first)) + ", degree " +
376 std::to_string(ha(first) >= 0 ? hd(ha(first)) : 0);
377 if (hb(first) >= 0)
378 m += "; vertex " + std::to_string(hb(first)) + ", degree " + std::to_string(hd(hb(first)));
379 m += ")";
380 }
381 invariantMsg += (invariantMsg.empty() ? "" : "; ") + m;
382 };
383 if (P.velocityUseGS) {
384 // Incremental colouring (single-GPU PGS path only): carry surviving pairs' colours across
385 // substeps, re-arbitrating only the new manifolds. Bit-identical colouring FORM to the full
386 // recolour; forced full on the fresh substep and when the colour count creeps > 1.3x the last
387 // full recolour (compaction keeps the serialized-phase count bounded). Under MPI or g == 0 the
388 // full recolour stays (a carried colour can cross a rank boundary it never arbitrated against).
389 const bool incrColor = usePersistPre && P.velocityUseGS && !Hooks::distributed && !incrColorOff;
390 if (incrColor) {
391 // A substep coloured through hub copies carried per-COPY colours: recolour in full.
392 bool didFull = (P.prevPairCount <= 0) || P.velCopiesLastSubstep;
394 P.manifolds, nm, P.realIndices, nBodies,
395 Kokkos::View<const unsigned long long*, CpMem>(P.prevPairKeys),
396 Kokkos::View<const int*, CpMem>(P.prevManifoldColor), P.prevPairCount, P.manifoldColor,
397 P.bodyWinner, P.bodyColorMask, velLeftover, /*forceFull*/ didFull, mSleep);
398 if (!didFull && P.velLastFullColors > 0 && numColors > (P.velLastFullColors * 13) / 10) {
400 P.manifolds, nm, P.realIndices, nBodies,
401 Kokkos::View<const unsigned long long*, CpMem>(P.prevPairKeys),
402 Kokkos::View<const int*, CpMem>(P.prevManifoldColor), P.prevPairCount, P.manifoldColor,
403 P.bodyWinner, P.bodyColorMask, velLeftover, /*forceFull*/ true, mSleep);
404 didFull = true;
405 }
406 if (didFull)
407 P.velLastFullColors = numColors;
408 } else {
409 numColors = colorManifoldsKokkos(P.manifolds, nm, P.realIndices, nBodies, P.manifoldColor,
410 P.bodyWinner, P.bodyColorMask, velLeftover, mSleep, {},
411 /*dedupTwins*/ !Hooks::distributed);
412 }
413 if (velLeftover > 0 && nm > 0) {
414 // Attempt failed: hub copies over the vertices realIdx(body) (§4.4), full recolour.
415 edgeA = Kokkos::View<int*, CpMem>("peclet::dem::copies_eA", nm);
416 edgeB = Kokkos::View<int*, CpMem>("peclet::dem::copies_eB", nm);
418 Kokkos::View<const int*, CpMem>(P.manifoldColor), edgeA, edgeB);
419 growCopyView(P.vertexDegree, static_cast<std::size_t>(nBodies), "peclet::dem::vertexDegree");
420 vertexDegreeKokkos(edgeA, edgeB, nm, nBodies, P.vertexDegree);
421 buildHubCopiesKokkos(edgeA, edgeB, nm, nBodies, slotBase, P.vertexDegree, VC);
422 growSlots(VC.nCopies);
423 const SlotOverride ovColor{VC.slotA, VC.slotB, {}, 1.0f};
424 numColors = colorManifoldsKokkos(P.manifolds, nm, P.realIndices, nBodies, P.manifoldColor,
425 P.bodyWinner, P.bodyColorMask, velLeftover, mSleep, ovColor,
426 /*dedupTwins*/ !Hooks::distributed, slotBase + VC.nCopies);
427 if (incrColor)
428 P.velLastFullColors = numColors;
429 buildCopyGroupsKokkos(P.realIndices, nBodies, P.numReal, P.vertexDegree, {}, VC,
430 /*identityBodies*/ true);
431 P.splitStats.lightHubs = countLightHubsKokkos(VC, edgeA, edgeB, nm, nBodies,
432 Kokkos::View<const float*, CpMem>(P.invMass));
433 if (velLeftover > 0)
434 noteInvariant("velocity", Kokkos::View<const int*, CpMem>(P.manifoldColor), nm);
435 }
436 P.velCopiesLastSubstep = VC.nCopies > 0;
437 }
438 // Dense colour buckets for the PGS sweeps (bit-identical: colour classes are body-disjoint).
439 std::vector<int> velOffs;
440 const bool velBuckets = P.velocityUseGS && nm > 0 && numColors > 0;
441 if (velBuckets)
442 buildColorBucketsKokkos(Kokkos::View<const int*, CpMem>(P.manifoldColor), nm, numColors,
443 P.velPerm, P.bucketCursor, velOffs);
444 const Kokkos::View<const int*, CpMem> velPermC(P.velPerm);
445 const std::vector<int>* velOffsP = velBuckets ? &velOffs : nullptr;
446 // Fused colour sweeps (CUDA): the whole PGS sweep — and where eligible the whole adaptive
447 // iteration loop — as ONE kernel iterating device-side (see solver_fused.hpp); same colour
448 // ordering, bit-identical. Default policy ("auto"): on exactly where CUDA-graph replay is
449 // unavailable — the distributed step, or set_cuda_graphs(False); set_fused_sweeps("on"/"off")
450 // forces either way.
451 const bool wantFused = demFusedWanted(!Hooks::distributed && P.cudaGraphs, P.fusedSweeps);
452 const FusedSweepCtx velFused = (velBuckets && wantFused)
453 ? demMakeFusedCtx(space, velOffs, P.velOffsDev, P.fusedBar)
454 : FusedSweepCtx{};
455 const FusedSweepCtx* velFusedP = velFused.maxBucket > 0 ? &velFused : nullptr;
456 // Position colouring, moved here from after the velocity phase (docs/contact_solve_framework.md
457 // §4.2 item 3): both phases are coloured at the top of the solve. Its inputs (contacts, sleep
458 // masks, the previous substep's ledger) do not change in between, so the move is byte-identical.
459 // The overlap projection is coloured and swept per position UNIT (§4.3): all contacts of one
460 // body pair (or of one body and one wall, §12 S6) in one work item, in ascending contact order.
461 // A one-point unit is exactly the old per-contact edge, key and arithmetic.
462 int posLeftover = 0;
463 int numPosColors = 0;
464 int numPosUnits = 0;
465 PosUnits posUnits;
466 P.numPosUnits = 0;
467 // Incremental position colouring, same single-GPU PGS gate + creep policy as the velocity path.
468 const bool incrPosColor =
469 usePersistPre && P.velocityUseGS && !Hooks::distributed && !incrColorOff;
470 bool posDidFull = false;
471 if (P.velocityUseGS) {
472 numPosUnits = buildPositionUnitsKokkos(P.contacts, nc, P.unitStart, P.unitContacts, nm);
473 P.numPosUnits = numPosUnits;
474 // All singletons (spheres): the unit map is the identity -- the empty PosUnits, so the
475 // sweeps and passes index contacts directly (no two-level indirection per contact).
476 if (numPosUnits != nc)
477 posUnits = PosUnits{Kokkos::subview(Kokkos::View<const int*, CpMem>(P.unitStart),
478 Kokkos::pair<int, int>(0, numPosUnits + 1)),
479 Kokkos::subview(Kokkos::View<const int*, CpMem>(P.unitContacts),
480 Kokkos::pair<int, int>(0, nc))};
481 if (incrPosColor) {
482 posDidFull = (P.posPrevContactCount <= 0);
483 numPosColors = colorContactsIncrementalKokkos(
484 P.contacts, numPosUnits, P.numParticles,
485 Kokkos::View<const unsigned long long*, CpMem>(P.prevContactKeys),
486 Kokkos::View<const int*, CpMem>(P.prevContactColor), P.posPrevContactCount, P.unitColor,
487 P.contactKeys, P.bodyWinner, P.bodyColorMask, posLeftover,
488 /*forceFull*/ posDidFull, cSleep, posUnits);
489 if (!posDidFull && P.posLastFullColors > 0 &&
490 numPosColors > (P.posLastFullColors * 13) / 10) {
491 numPosColors = colorContactsIncrementalKokkos(
492 P.contacts, numPosUnits, P.numParticles,
493 Kokkos::View<const unsigned long long*, CpMem>(P.prevContactKeys),
494 Kokkos::View<const int*, CpMem>(P.prevContactColor), P.posPrevContactCount, P.unitColor,
495 P.contactKeys, P.bodyWinner, P.bodyColorMask, posLeftover,
496 /*forceFull*/ true, cSleep, posUnits);
497 posDidFull = true;
498 }
499 if (posDidFull)
500 P.posLastFullColors = numPosColors;
501 } else {
502 numPosColors =
504 P.bodyColorMask, posLeftover, cSleep, posUnits);
505 }
506 // Position vertices are raw slots. demStep's periodic images are local copies of their body
507 // (§WO-4 item 2): they join its fold group, which needs the per-slot degree even without hubs.
508 const bool images = !Hooks::distributed && P.numParticles > P.numReal;
509 if ((posLeftover > 0 || images) && numPosUnits > 0) {
510 edgeA = Kokkos::View<int*, CpMem>("peclet::dem::copies_eA", numPosUnits);
511 edgeB = Kokkos::View<int*, CpMem>("peclet::dem::copies_eB", numPosUnits);
512 positionEdgeEndsKokkos(P.contacts, posUnits, numPosUnits,
513 Kokkos::View<const int*, CpMem>(P.unitColor), edgeA, edgeB);
514 growCopyView(P.vertexDegree, static_cast<std::size_t>(P.numParticles),
515 "peclet::dem::vertexDegree");
516 vertexDegreeKokkos(edgeA, edgeB, numPosUnits, P.numParticles, P.vertexDegree);
517 if (posLeftover > 0) {
518 // Attempt failed: hub copies over the raw slots (§4.4), full recolour (the incremental
519 // path falls back to this full path with copies).
520 buildHubCopiesKokkos(edgeA, edgeB, numPosUnits, P.numParticles, slotBase, P.vertexDegree,
521 PC);
522 growSlots(PC.nCopies);
523 const SlotOverride ovColor{PC.slotA, PC.slotB, {}, 1.0f};
524 numPosColors = colorContactsKokkos(P.contacts, numPosUnits, P.numParticles, P.unitColor,
525 P.bodyWinner, P.bodyColorMask, posLeftover, cSleep,
526 posUnits, ovColor, slotBase + PC.nCopies);
527 if (incrPosColor) {
528 posDidFull = true;
529 P.posLastFullColors = numPosColors;
530 }
531 if (posLeftover > 0)
532 noteInvariant("position", Kokkos::View<const int*, CpMem>(P.unitColor), numPosUnits);
533 }
535 images ? Kokkos::View<const float* [3], CpMem>(P.imageShift)
536 : Kokkos::View<const float* [3], CpMem>(),
537 PC, /*identityBodies*/ Hooks::distributed);
538 }
539 // Every contact carries its unit's colour: the per-contact ledger commit below and the
540 // diagnostics read it.
541 expandUnitColorsKokkos(posUnits, numPosUnits, Kokkos::View<const int*, CpMem>(P.unitColor),
542 P.contactColor);
543 if (incrPosColor) {
544 // Commit this substep's (contact key, colour) for next substep's warm gather.
545 commitContactColorKokkos(Kokkos::View<const unsigned long long*, CpMem>(P.contactKeys),
546 Kokkos::View<const int*, CpMem>(P.contactColor), P.prevContactKeys,
548 P.posPrevContactCount = nc;
549 }
550 }
551 // Rank-level M (docs/contact_solve_framework.md §13.3): on a rank that exchanges with a
552 // neighbour, every body touched through a ghost is a mass-split copy set across ranks. The
553 // activity pass gives each slot its local active copy count a per phase (0 or 1 outside a hub
554 // group, the group's count at a hub base) from the final colourings; the openings make the
555 // global k. A rank that neither sends nor receives (np 1 closed, an isolated block) keeps WO-4's
556 // single-rank path (k = groupK), bit for bit: §13.3 "step 0".
557 const bool rankM = Hooks::distributed && hooks.rankK().exchanges;
558 if (rankM && P.velocityUseGS) {
559 const int nAll = P.numParticles + (VC.nCopies > PC.nCopies ? VC.nCopies : PC.nCopies);
560 growCopyView(P.aVel, static_cast<std::size_t>(nAll), "peclet::dem::aVel");
561 growCopyView(P.aPos, static_cast<std::size_t>(nAll), "peclet::dem::aPos");
562 growCopyView(P.kVel, static_cast<std::size_t>(nAll), "peclet::dem::kVel");
563 growCopyView(P.kPos, static_cast<std::size_t>(nAll), "peclet::dem::kPos");
564 growCopyView(P.activityHit, static_cast<std::size_t>(nAll), "peclet::dem::activityHit");
565 if (!usePersistPre) { // rank-level X (§1.4): the g = 0 one-shot's masks and gates
566 growCopyView(P.velMask, static_cast<std::size_t>(nAll), "peclet::dem::velMask");
567 growCopyView(P.xGate, static_cast<std::size_t>(nm > 0 ? nm : 1), "peclet::dem::xGate");
568 }
569 const SlotOverride vOv =
570 VC.nHubs > 0 ? SlotOverride{VC.slotA, VC.slotB, {}, 1.0f} : SlotOverride{};
572 Kokkos::View<const int*, CpMem>(P.manifoldColor), vOv, P.activityHit,
573 P.numParticles + VC.nCopies);
574 finishActivityKokkos(Kokkos::View<const unsigned char*, CpMem>(P.activityHit), P.numParticles,
575 VC, P.aVel);
576 const SlotOverride pOv =
577 PC.nHubs > 0 ? SlotOverride{PC.slotA, PC.slotB, {}, 1.0f} : SlotOverride{};
578 activityHitPositionKokkos(P.contacts, posUnits, numPosUnits,
579 Kokkos::View<const int*, CpMem>(P.unitColor), pOv, P.activityHit,
580 P.numParticles + PC.nCopies);
581 finishActivityKokkos(Kokkos::View<const unsigned char*, CpMem>(P.activityHit), P.numParticles,
582 PC, P.aPos);
583 }
588 // The colouring invariant (§4.2 item 4, §12 S3): a single rank throws now; the distributed step
589 // votes the flag in its first stop Allreduce and every rank throws there (a rank-local throw
590 // would leave the others waiting in a collective).
591 bool invariantPending = !invariantMsg.empty();
592 if constexpr (!Hooks::distributed) {
593 if (invariantPending)
594 throw std::runtime_error(invariantMsg);
595 }
596 auto voteInvariant = [&](float res) {
597 bool bad = invariantPending;
598 invariantPending = false;
599 const float r = hooks.allMaxAny(res, bad);
600 if (bad)
601 throw std::runtime_error(invariantMsg.empty()
602 ? std::string("colouring invariant violated on another rank")
603 : invariantMsg);
604 return r;
605 };
606 // Dense colour buckets + fused sweep for the position projection (same precedent as the
607 // velocity sweeps: colour classes are body-disjoint => bit-identical). The buckets list unit
608 // indices.
609 std::vector<int> posOffs;
610 const bool posBuckets = P.velocityUseGS && numPosUnits > 0 && numPosColors > 0;
611 if (posBuckets)
612 buildColorBucketsKokkos(Kokkos::View<const int*, CpMem>(P.unitColor), numPosUnits, numPosColors,
613 P.posPerm, P.bucketCursor, posOffs);
614 const Kokkos::View<const int*, CpMem> posPermC(P.posPerm);
615 const std::vector<int>* posOffsP = posBuckets ? &posOffs : nullptr;
616 const FusedSweepCtx posFused = (posBuckets && wantFused)
617 ? demMakeFusedCtx(space, posOffs, P.posOffsDev, P.fusedBar)
618 : FusedSweepCtx{};
619 const FusedSweepCtx* posFusedP = posFused.maxBucket > 0 ? &posFused : nullptr;
620 // Persistent-contact restitution, gravity-gated (|g| = 0 leaves behaviour untouched: growth
621 // packing / HCS bit-identical). A pair already in contact LAST substep is loaded, not impacting:
622 // it gets e = 0 (the impulse still cancels the approach — pure inelastic support), so the
623 // velocity solve carries a pile's static weight through impulse chains and a settling column
624 // actually cools; material/wall restitution stays reserved for newly formed contacts.
625 const bool usePersist = (P.gravity.x != 0.0f || P.gravity.y != 0.0f || P.gravity.z != 0.0f);
626 // Warm-started PGS velocity solve (|g| > 0): gather each manifold's previous-substep converged
627 // push impulse by pair key, record the pre-solve approach (restitution bias), and apply the warm
628 // impulses up front -- a static pile's force network is re-established in ~one sweep. g = 0
629 // keeps the original one-shot colored-GS path bit-identical (HCS, growth packing).
630 const bool usePGS = usePersist && P.velocityUseGS;
631 // Rank-level X (§1.4, WO-6): the g = 0 one-shot on a rank that exchanges, when the rank graph
632 // has more than one colour (with one colour no two ranks are adjacent, so no body is split
633 // across ranks and every contact fires).
634 const RankK rk = hooks.rankK();
635#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 4
636 const bool xOn = false; // G13 mutant 4: every rank fires every contact
637#else
638 const bool xOn = rankM && !usePGS && P.velocityUseGS && rk.numColors > 1;
639#endif
640 const float gMagP = Kokkos::sqrt(P.gravity.x * P.gravity.x + P.gravity.y * P.gravity.y +
641 P.gravity.z * P.gravity.z);
642 const F3 gHat =
643 usePersist ? F3{P.gravity.x / gMagP, P.gravity.y / gMagP, P.gravity.z / gMagP} : F3{0, 0, 0};
644 // Event-level (Poisson) restitution: per-pair banked compression budget, released as a
645 // budget-capped separation-velocity target during unloading (see updateRestitutionBankKokkos /
646 // PGSManifoldSweep). Off (empty views) the sweeps run the per-substep Newton path verbatim.
647 const bool poisson = usePGS && P.restitutionModel == 1;
648 const Kokkos::View<float*, CpMem> bankV = poisson ? P.restBank : Kokkos::View<float*, CpMem>();
649 const Kokkos::View<float*, CpMem> relV = poisson ? P.restRel : Kokkos::View<float*, CpMem>();
650 const Kokkos::View<const float*, CpMem> vpkC =
651 poisson ? Kokkos::View<const float*, CpMem>(P.restVPeak)
652 : Kokkos::View<const float*, CpMem>();
653 const Kokkos::View<float*, CpMem> orphV = poisson ? P.bodyOrphan : Kokkos::View<float*, CpMem>();
654 const Kokkos::View<const float*, CpMem> orphPk =
655 poisson ? Kokkos::View<const float*, CpMem>(P.bodyOrphanVPeak)
656 : Kokkos::View<const float*, CpMem>();
657 if (usePGS) {
658 if (poisson && P.prevPairCount > 0) { // reset the prev-ledger survival flags for the gather
659 auto mt = Kokkos::subview(P.prevMatched, Kokkos::pair<int, int>(0, P.prevPairCount));
660 Kokkos::deep_copy(mt, static_cast<unsigned char>(0));
661 }
662 gatherWarmLambdaKokkos(P.manifolds, nmVisible, P.realIndices, keyIdx, P.prevPairKeys,
666 poisson ? P.prevMatched : Kokkos::View<unsigned char*, CpMem>(),
667 /*dedupTwins*/ !Hooks::distributed);
668 if (poisson) {
669 { // per-substep release accumulator starts from zero every substep
670 auto rr = Kokkos::subview(P.restRel, Kokkos::pair<int, int>(0, nm));
671 Kokkos::deep_copy(rr, 0.0f);
672 }
673 // Orphan transfer: age the body accounts (owned range; MPI ghosts are mirrored), then
674 // settle dead pairs' remaining budgets onto their endpoint bodies. Under MPI the pair-key
675 // identities are gids, so the scatter resolves them through a sorted gid -> slot map built
676 // over owned + ghost slots (a ghost-side credit is delivered to the owner by the velocity
677 // reverse at the next sync; the pair's bank lives only in its owning rank's ledger).
678 decayBodyOrphanKokkos(P.bodyOrphan, P.bodyOrphanVPeak, P.numReal, 2.0f * P.dt * gMagP);
679 if (P.prevPairCount > 0) {
680 Kokkos::View<const int*, CpMem> gidSorted, slotSorted;
681 if constexpr (Hooks::distributed) {
682 Kokkos::View<int*, CpMem> gs(
683 Kokkos::view_alloc(space, "peclet::dem::orphan_gids", Kokkos::WithoutInitializing),
684 nBodies);
685 Kokkos::View<int*, CpMem> ss(
686 Kokkos::view_alloc(space, "peclet::dem::orphan_slots", Kokkos::WithoutInitializing),
687 nBodies);
688 auto gid = P.gid;
689 Kokkos::parallel_for(
690 "peclet::dem::orphan_gid_map", Kokkos::RangePolicy<CpExec>(space, 0, nBodies),
691 KOKKOS_LAMBDA(int i) {
692 gs(i) = gid(i);
693 ss(i) = i;
694 });
695 Kokkos::Experimental::sort_by_key(space, gs, ss);
696 gidSorted = gs;
697 slotSorted = ss;
698 }
699 scatterOrphanBanksKokkos(Kokkos::View<const unsigned long long*, CpMem>(P.prevPairKeys),
700 Kokkos::View<const float*, CpMem>(P.prevRestBank),
701 Kokkos::View<const float*, CpMem>(P.prevRestVPeak),
702 Kokkos::View<const unsigned char*, CpMem>(P.prevMatched),
703 P.prevPairCount, Kokkos::View<const float*, CpMem>(P.invMass),
704 P.bodyOrphan, P.bodyOrphanVPeak, gidSorted, slotSorted,
705 Hooks::distributed ? P.numReal : -1);
706 }
707 }
711 P.groundedLevel, nBodies, /*sweeps*/ 8, /*decay*/ 8);
712 // STAGED SOLVE (Guendelman): the main sweeps are fully momentum-conserving (side flags all
713 // zero) -- ballistic impact, discharge and shear see correct physics. One-sided grounding is
714 // reserved for the STABILIZATION pass below, which runs only if the main sweeps leave an
715 // unconverged residual (a deep column mid-collapse that symmetric GS cannot arrest within the
716 // iteration budget).
717 {
718 auto flags = Kokkos::subview(P.sideFlags, Kokkos::pair<int, int>(0, nm));
719 Kokkos::deep_copy(flags, static_cast<unsigned char>(0));
720 }
722 P.vt0, /*dedupTwins*/ !Hooks::distributed);
724 P.realIndices, P.lambdaAcc, P.lambdaT, /*dedupTwins*/ !Hooks::distributed);
725 // The warm impulses just moved every owned AND ghost body: the opening (§13.3 step 5)
726 // reconciles them raw (known impulses), makes the global k_vel / k_pos and forwards the
727 // owners' state, balance and k, so the first sweep reads a consistent ghost state. (An
728 // isolated rank: a no-op, as the sync it replaces.)
729 if constexpr (Hooks::distributed)
730 hooks.openVelocityPhase(P, poisson);
731 } else if (P.velocityUseGS) {
732 // The g = 0 one-shot path: the opening makes the position phase's k_pos (§13.3) and, in the
733 // same round, the velocity activity masks of rank-level X (§1.4, WO-6). The velocity phase's
734 // reconciliation stays c771e07's raw reverse + forward: under X at most one copy of a body
735 // changes per sync interval, so the raw sum is exact.
736 if constexpr (Hooks::distributed)
737 hooks.openPositionCounts(P, /*velocityMask=*/rankM);
738 }
739 // Velocity-phase copies (§4.4 step 6, §4.5): seeded once the warm start -- known impulses on
740 // true masses -- has been applied (and, under MPI, published). The sweeps, the stabilization
741 // passes and the multilevel cycle take the solve views and the slot overrides; the fold runs in
742 // every iteration after the sweep; every rank sync re-marks the seeds (syncVel).
743 const bool velCopiesOn = VC.nGroups > 0;
744 // Rank-level M in the velocity phase (§13.3): the PGS path on an exchanging rank. The solve
745 // views carry each slot's global k_vel (a hub copy its base's); the local fold still divides by
746 // the local count a (groupK); the orphan shares B / k are already set by the opening.
747 const bool velM = rankM && usePGS;
748 Kokkos::View<const float*, CpMem> invMassVel = P.invMass;
749 Kokkos::View<const float* [3], CpMem> invInertiaVel = P.invInertia;
750 SlotOverride velOv{};
751 const Kokkos::View<float*, CpMem> orphPkW =
752 poisson ? P.bodyOrphanVPeak : Kokkos::View<float*, CpMem>();
753 if (velM) {
754 buildSolveViewsRankKKokkos(P, VC, slotBase + (velCopiesOn ? VC.nCopies : 0), P.invMass,
755 P.invInertia, Kokkos::View<const int*, CpMem>(P.kVel));
756 invMassVel = P.invMassSolve;
757 invInertiaVel = P.invInertiaSolve;
758 velOv = velCopiesOn ? SlotOverride{VC.slotA, VC.slotB, P.splitSlot, kSplitOmegaVelocity}
760 if (velCopiesOn) {
762 markCopySeedsKokkos(VC, P.velPred, P.angVelPred, orphV, orphPkW, /*shareFromBase*/ true);
763 }
764 } else if (velCopiesOn) {
765 buildSolveViewsKokkos(P, VC, slotBase + VC.nCopies, P.invMass, P.invInertia);
767 markCopySeedsKokkos(VC, P.velPred, P.angVelPred, orphV, orphPkW);
768 invMassVel = P.invMassSolve;
769 invInertiaVel = P.invInertiaSolve;
771 }
772 auto foldVel = [&] {
773 if (velCopiesOn)
774 foldCopiesKokkos(space, VC, P.velPred, P.angVelPred, orphV, orphPkW, P.maxConsensus);
775 };
776 // §12 S14: a velocity phase with copies (local hub copies, or rank-level M) has not converged
777 // while copies of one body disagree. Its stop votes fold in the largest consensus correction
778 // since the previous vote (the local folds and the M syncs atomic-max it into P.maxConsensus),
779 // through the SAME Allreduce-MAX; it is zeroed after each read. Without copies nothing is read
780 // or written (np 1 without copies stays byte-identical).
781 const bool velCons = velCopiesOn || velM;
782 auto withConsensus = [&](float res, bool cons) {
783 if (!cons)
784 return res;
785 const float c = readFloat(P.maxConsensus);
786 Kokkos::deep_copy(P.maxConsensus, 0.0f);
787 return std::max(res, c);
788 };
789 // §12 S13 (test-only): with P.noAdaptiveStop every loop runs its cap; the votes still run.
790 const bool stopsOn = !P.noAdaptiveStop;
791 const float fusedOff = -1.0f; // a fused device loop's tolerance that never stops
792 // A rank sync inside the velocity phase: orphan accounts back to balances, the raw reverse +
793 // forward, then the copies' seeds re-marked from the reconciled bases.
794 // Under rank-level M (velM) the sync itself is M (the halo's weighted apply; the bases hold
795 // their shares, so no unfold first) and the copies re-seed from their bases' shares.
796 auto syncVel = [&] {
797 if constexpr (Hooks::distributed) {
798 if (velM) {
799 hooks.syncVelocities(P);
800 if (velCopiesOn)
801 markCopySeedsKokkos(VC, P.velPred, P.angVelPred, orphV, orphPkW, /*shareFromBase*/ true);
802 return;
803 }
804 if (velCopiesOn)
805 unfoldOrphanKokkos(VC, orphV);
806 hooks.syncVelocities(P);
807 if (velCopiesOn)
808 markCopySeedsKokkos(VC, P.velPred, P.angVelPred, orphV, orphPkW);
809 }
810 };
811 // Restitution threshold ~ the speed one substep of free fall gains: below it a contact is
812 // RESTING and bounces with e=0 (see solveVelocityKokkos — dense-pile energy-bomb guard).
813 const float vRest = 2.0f * P.dt * gMagP;
814 // One PGS velocity iteration (async residual zero + full colour sweep). Captured as a CUDA
815 // graph and replayed per iteration on the single-GPU path: the step is host-submission-bound
816 // (measured 3,300 launches / ~11 ms per step at 25k), and replay collapses each iteration's
817 // launch storm into one. The residual readback between replays is unchanged, so the adaptive
818 // stop — and the physics — are bit-identical to the submission path.
819 auto emitVelIter = [&] {
820 Kokkos::deep_copy(space, P.maxApproach, 0.0f);
821 solveVelocityPGSKokkos(P.manifolds, nm, P.manifoldColor, numColors, invMassVel, invInertiaVel,
823 P.restitutionNormal, vRest, P.maxApproach, P.lambdaAcc, P.vn0,
824 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT,
826 Kokkos::View<const float*, CpMem>(P.posImpulse), {}, bankV, relV, vpkC,
827 orphV, orphPk, velPermC, velOffsP, velFusedP, nullptr, velOv);
828 foldVel();
829 };
830 // Device-side iteration loop (CUDA, single-rank): the whole adaptive velocity loop as ONE
831 // kernel — same sweeps, same residual, same stop; the per-iteration readback and the graph
832 // capture disappear. The final residual stays in P.maxApproach for the stabilization trigger.
833 bool velLoopDone = false;
834 if constexpr (!Hooks::distributed) {
835 if (usePGS && P.velocityUseGS && !velCopiesOn && velFusedP) {
836 FusedLoopSpec spec{P.velocityIterations, stopsOn ? 0.02f * vRest : fusedOff, false};
837 if (P.iterCounters) // diagnostics (§12 S12): the loop writes its count on the device
838 spec.iters = demIterCountSlot(P, 0);
839 velLoopDone = solveVelocityPGSKokkos(
840 P.manifolds, nm, P.manifoldColor, numColors, P.invMass, P.invInertia, P.quat, P.velPred,
842 P.lambdaAcc, P.vn0, Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT,
844 Kokkos::View<const float*, CpMem>(P.posImpulse), {}, bankV, relV, vpkC, orphV, orphPk,
845 velPermC, velOffsP, velFusedP, &spec, {});
846 }
847 }
848 bool graphVel = false;
849 CudaIterGraph gVel;
850 if (!velLoopDone && usePGS && P.velocityUseGS) {
851 PECLET_DEM_GRAPH_LOOP(graphVel, gVel, emitVelIter, P.graphCache[0])
852 }
853 P.splitStats.velItersUsed = velLoopDone ? demReadIterCount(P, 0) : 0;
854 if (velCons)
855 Kokkos::deep_copy(P.maxConsensus, 0.0f);
856 Kokkos::Profiling::popRegion();
857 Kokkos::Profiling::pushRegion("dem::solve::vel");
858 for (int it = 0; !velLoopDone && it < P.velocityIterations; ++it) {
860 if (legacyFriction)
863 /*dedupTwins*/ !Hooks::distributed);
864 if (P.velocityUseGS) {
865 if (usePGS) {
866 if (graphVel)
867 gVel.launch(space);
868 else
869 emitVelIter();
870 } else {
871 Kokkos::deep_copy(space, P.maxApproach, 0.0f);
872 // Rank-level X (§1.4, WO-6): in sync interval t a contact fires only if this rank holds
873 // every rank-split endpoint; a non-firing contact still records its approach for the stop.
874 XGateSpec xGate; // evaluated inline in the sweep (no separate pass)
875 if (xOn) {
876 const long long t = P.solveEpoch + it / rk.syncInterval;
877 xGate = XGateSpec{Kokkos::View<const unsigned long long*, CpMem>(P.velMask), rk.color,
878 rk.numColors, t};
879 if (P.iterCounters) { // diagnostics only: the explicit gate pass, to count the unfired
881 Kokkos::View<const unsigned long long*, CpMem>(P.velMask), P.xGate,
882 rk.color, rk.numColors, t);
884 }
885 }
886 solveVelocityColoredGSKokkos(P.manifolds, nm, P.manifoldColor, numColors, invMassVel,
887 invInertiaVel, P.quat, P.velPred, P.angVelPred, P.realIndices,
888 P.growthRate, P.restitutionNormal, vRest, P.maxApproach, {},
889 {}, {}, {}, velOv, xGate);
890 foldVel();
891 }
892 // Every active manifold is coloured (§4.2): the count-averaged fallback is gone. The stop's
893 // Allreduce carries the colouring-invariant vote (voteInvariant; no extra message).
894 const float velRes = voteInvariant(withConsensus(readFloat(P.maxApproach), velCons));
895 // Adaptive stop. One-shot GS: end once no pair approaches above the resting threshold. PGS:
896 // maxApproach records the largest APPLIED correction, and meaningful increments are ~g dt
897 // (they propagate a chain one link per sweep), so the tolerance must sit well below vRest or
898 // the stop starves deep-chain convergence permanently (measured: a 113-layer pile plateaued
899 // at vz ~ -5 with the vRest stop). Once the warm-started network is converged the first
900 // sweep's correction is ~0 and the loop still exits immediately. Distributed: the residual
901 // is Allreduce-MAXed so every rank takes the same break (collective-refresh consistency).
902 if (stopsOn && velRes <= (usePGS ? 0.02f * vRest : vRest))
903 break;
904 } else {
905 // Mass-split Jacobi (velocityUseGS off, a global setting; docs/contact_solve_framework.md
906 // §3.1): every manifold is its own copy. Count first, make the counts global (every copy of
907 // a body must see the serial count), solve each manifold against copies of mass m / n with
908 // true-mass deltas, and add them with factor 1 -- conservative at every iterate.
910 /*dedupTwins*/ !Hooks::distributed);
911 hooks.syncContactCounts(P);
914 P.deltaAngVel, P.constraintCounts, {}, 0, {}, {}, {}, {},
915 /*massSplit=*/true, /*dedupTwins*/ !Hooks::distributed);
917 P.deltaAngVel, P.constraintCounts, /*averaged=*/false);
918 }
919 if constexpr (Hooks::distributed) {
920 if (hooks.syncPoint(it))
921 syncVel();
922 }
923 }
924 if constexpr (Hooks::distributed)
925 syncVel(); // final owner->ghost refresh of the main velocity phase
926 Kokkos::Profiling::popRegion();
927 Kokkos::Profiling::pushRegion("dem::solve::stab_fric");
928 // STABILIZATION PASS: if the symmetric sweeps could not drain the residual (a collapsing
929 // column needs ~one sweep per layer to carry its weight to the floor -- unaffordable), arrest
930 // the remaining quasi-static approach with grounded one-sided sweeps. In dynamic scenes the
931 // residual is below the threshold and this pass never runs, so impact/discharge/shear keep
932 // pure momentum-conserving physics. (set_stabilization_mode('off') disables the pass.)
933 if (usePGS) {
934 const float vRestS = 2.0f * P.dt * gMagP;
935 const int smode = P.stabilizationMode;
936 if (smode != 0 && hooks.allMax(readFloat(P.maxApproach)) > vRestS) {
937 if (smode == 1) { // ONE-SIDED grounded pass (default): held-lower-side impulses
939 Kokkos::View<const unsigned char*, CpMem>(P.manifoldPersistent),
940 Kokkos::View<const unsigned char*, CpMem>(P.groundedLevel),
941 P.posPred, P.velPred, gHat, 8.0f * P.dt * gMagP, P.sideFlags, P.vn0,
942 8.0f * P.dt * gMagP, /*dedupTwins*/ !Hooks::distributed);
943 // Arrest budget: 2x the main budget -- the pass must out-pace a violent collapse, and it
944 // only ever runs when the residual says one is happening (adaptive stop ends it early).
945 auto emitOsIter = [&] {
946 Kokkos::deep_copy(space, P.maxApproach, 0.0f);
948 P.manifolds, nm, P.manifoldColor, numColors, invMassVel, invInertiaVel, P.quat,
950 P.maxApproach, P.lambdaAcc, P.vn0,
951 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT, P.frictionDynamic,
952 P.vt0, P.restitutionTangent, Kokkos::View<const float*, CpMem>(P.posImpulse), {},
953 bankV, relV, vpkC, orphV, orphPk, velPermC, velOffsP, velFusedP, nullptr, velOv);
954 foldVel();
955 };
956 bool osLoopDone = false;
957 if constexpr (!Hooks::distributed) {
958 if (!velCopiesOn && velFusedP) {
959 const FusedLoopSpec spec{2 * P.velocityIterations, stopsOn ? vRestS : fusedOff, false};
960 osLoopDone = solveVelocityPGSKokkos(
961 P.manifolds, nm, P.manifoldColor, numColors, P.invMass, P.invInertia, P.quat,
963 P.maxApproach, P.lambdaAcc, P.vn0,
964 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT,
966 Kokkos::View<const float*, CpMem>(P.posImpulse), {}, bankV, relV, vpkC, orphV,
967 orphPk, velPermC, velOffsP, velFusedP, &spec, {});
968 }
969 }
970 bool graphOs = false;
971 CudaIterGraph gOs;
972 if (!osLoopDone) {
973 PECLET_DEM_GRAPH_LOOP(graphOs, gOs, emitOsIter, P.graphCache[1])
974 }
975 if (velCons)
976 Kokkos::deep_copy(P.maxConsensus, 0.0f);
977 for (int it = 0; !osLoopDone && it < 2 * P.velocityIterations; ++it) {
978 if (graphOs)
979 gOs.launch(space);
980 else
981 emitOsIter();
982 const float osRes = hooks.allMax(withConsensus(readFloat(P.maxApproach), velCons));
983 if (stopsOn && osRes <= vRestS)
984 break;
985 if constexpr (Hooks::distributed) {
986 if (hooks.syncPoint(it))
987 syncVel();
988 }
989 }
990 } else if (smode == 2) {
991 // MULTILEVEL (GraphMG) pass: never deletes momentum, only accelerates its transport.
992 // Greedy pairwise aggregation over the quasi-static contact graph builds super-bodies
993 // (summed mass, momentum-weighted velocity); the fine manifolds crossing aggregate
994 // boundaries are re-solved with the AGGREGATE masses -- the supported chain's genuinely
995 // huge inertia plays the role the held lower side faked, so a wall contact drains a
996 // whole column's momentum in one coarse impulse while every impulse stays symmetric.
997 // Ballistic pairs (|vn0| > qsThr) never aggregate: an impactor keeps its fine-level,
998 // momentum-conserving physics and its rebound. Coarse lambda shares the fine
999 // accumulator, so the force-network ledger stays consistent for next substep's warm
1000 // start and the friction cone's Coulomb bound. See solver_multilevel.hpp. Distributed:
1001 // the hierarchy is built rank-locally over owned + ghost bodies (aggregates never cross
1002 // a rank boundary beyond the ghost band; the syncEvery refresh reconciles).
1003 const float qsThr = 8.0f * P.dt * gMagP;
1004 // Eligibility gates (mldetail::kGate*): slip keeps the pass off sustained shear (silo
1005 // bulk) without starving a crushing bed's aggregation. (The env A/B over the mask was
1006 // retired in 1.0.0; the measured-best mask is the only one that ships.)
1007 constexpr int mlGates = mldetail::kGateSlip;
1008 // Coarse vertex masses (docs/contact_solve_framework.md §13.2): after the fold the a(q)
1009 // active local copies of vertex q move together, so the coarse problem sees ONE vertex of
1010 // mass a m / k -- invMassCoarse = invMass k / a. On a single rank (and for WO-4's interim
1011 // rank-local hubs) a = k at every vertex, so invMassCoarse is P.invMass itself: the true
1012 // mass at a folded hub, not the solve view's m / k (which gained (1 - 1/s) m dV per
1013 // coarse cycle at a hub that aggregates).
1014 // Under rank-level M a rank-split vertex has a < k: invMassCoarse = invMass k / max(1, a).
1015#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 6
1016 Kokkos::View<const float*, CpMem> invMassCoarse = P.invMassSolve; // G13 mutant 6
1017#else
1018 Kokkos::View<const float*, CpMem> invMassCoarse = P.invMass;
1019#endif
1020 if (velM) {
1021 growCopyView(P.invMassCoarse, static_cast<std::size_t>(nBodies),
1022 "peclet::dem::invMassCoarse");
1023 buildInvMassCoarseKokkos(P.invMass, Kokkos::View<const int*, CpMem>(P.kVel),
1024 Kokkos::View<const int*, CpMem>(P.aVel), nBodies,
1025 P.invMassCoarse);
1026 invMassCoarse = P.invMassCoarse;
1027 }
1028 // The coarse vertex's inverse inertia is the same fraction
1029 // (docs/contact_physics_followups.md §2.5): P.invInertia itself wherever invMassCoarse is
1030 // P.invMass.
1031 Kokkos::View<const float* [3], CpMem> invInertiaCoarse = P.invInertia;
1032 if (velM) {
1033 growCopyView(P.invInertiaCoarse, static_cast<std::size_t>(nBodies),
1034 "peclet::dem::invInertiaCoarse");
1035 buildInvInertiaCoarseKokkos(P.invInertia, Kokkos::View<const int*, CpMem>(P.kVel),
1036 Kokkos::View<const int*, CpMem>(P.aVel), nBodies,
1038 invInertiaCoarse = P.invInertiaCoarse;
1039 }
1040 // Rigid-aggregate pools (§2.3), allocated on the first multilevel pass only.
1041 if (P.mlInvIG.extent(0) != P.mlInvMassG.extent(0)) {
1042 const std::size_t ng = P.mlInvMassG.extent(0);
1043 const auto wi = Kokkos::WithoutInitializing;
1044 P.mlOriginG = Kokkos::View<float* [3], CpMem>(Kokkos::view_alloc(wi, "mlOriginG"), ng);
1045 P.mlComOffG = Kokkos::View<float* [3], CpMem>(Kokkos::view_alloc(wi, "mlComOffG"), ng);
1046 P.mlAngG = Kokkos::View<float* [3], CpMem>(Kokkos::view_alloc(wi, "mlAngG"), ng);
1047 P.mlAngG0 = Kokkos::View<float* [3], CpMem>(Kokkos::view_alloc(wi, "mlAngG0"), ng);
1048 P.mlInvIG = Kokkos::View<float* [6], CpMem>(Kokkos::view_alloc(wi, "mlInvIG"), ng);
1049 }
1050 growCopyView(P.mlAccD, static_cast<std::size_t>(nBodies), "peclet::dem::mlAccD");
1052 P.mlMassG, P.mlGrp, P.mlMate, P.mlOriginG, P.mlComOffG,
1053 P.mlAngG, P.mlAngG0, P.mlInvIG, P.mlAccD};
1054 const MlBodyViews mlBody{Kokkos::View<const float* [3], CpMem>(P.posPred), P.angVelPred,
1055 Kokkos::View<const float* [4], CpMem>(P.quat), invInertiaCoarse};
1057 P.manifolds, nm, P.realIndices, Kokkos::View<const int*, CpMem>(P.manifoldColor),
1058 Kokkos::View<const float*, CpMem>(P.vn0), Kokkos::View<const float* [3], CpMem>(P.vt0),
1059 Kokkos::View<const unsigned char*, CpMem>(P.manifoldPersistent), P.posPred, gHat,
1060 invMassCoarse, qsThr, mlGates, nBodies, S, P.bodyWinner, P.bodyColorMask,
1061 /*excludeImmovable*/ sleepOn,
1062 sleepOn ? Kokkos::View<const unsigned char*, CpMem>(P.asleep)
1063 : Kokkos::View<const unsigned char*, CpMem>(),
1064 mlBody);
1066#ifndef NDEBUG
1067 // Debug build (§13.5 WO-5 item 4): no multi-member coarse group holds an inactive vertex.
1068 if (velM && H.numLevels > 0 &&
1069 countInactiveAggregatedKokkos(Kokkos::View<const int*, CpMem>(S.parent), H.parentOff[0],
1070 nBodies, H.numGroups[0],
1071 Kokkos::View<const int*, CpMem>(P.aVel)) > 0)
1072 throw std::logic_error("rank-level M: a coarse group holds a vertex with a = 0");
1073#endif
1074 if (H.numLevels > 0 && (velCopiesOn || velM)) // §13.2's positive control (diagnostic)
1076 std::max(P.splitStats.mlHubAggregated,
1078 Kokkos::View<const int*, CpMem>(S.parent), H.parentOff[0], nBodies,
1079 H.numGroups[0], Kokkos::View<const unsigned char*, CpMem>(P.splitSlot)));
1080 P.mlLast.numManifolds = nm;
1081 P.mlLast.numBodies = nBodies;
1084 // Dense per-(level, colour) buckets, built once per hierarchy (see solver_multilevel.hpp).
1085 std::vector<std::vector<int>> mlOffs;
1086 if (H.numLevels > 0) {
1087 if (static_cast<int>(P.mlBucketPerm.extent(0)) < H.numLevels * nm)
1088 Kokkos::realloc(Kokkos::WithoutInitializing, P.mlBucketPerm, H.numLevels * nm);
1090 }
1091 // Fused coarse cycle (CUDA): the whole per-iteration coarse leg as ONE kernel.
1092 const MlFusedCtx mlFused =
1093 (H.numLevels > 0 && wantFused)
1094 ? demMakeMlFusedCtx(space, H, mlOffs, nm, nBodies, /*coarseSweeps*/ 2, P.mlOffsDev,
1095 P.fusedBar)
1096 : MlFusedCtx{};
1097 const MlFusedCtx* mlFusedP = mlFused.maxWork > 0 ? &mlFused : nullptr;
1098 // The loop's stop criterion is the QUASI-STATIC residual (fine corrections on contacts
1099 // with |vn0| <= 4 vRest, plus every coarse correction): the fine sweep's full residual
1100 // is dominated by ballistic contacts in flowing scenes (a discharging silo never gets
1101 // below vRest there), and gating on it burns the full budget of extra fine sweeps every
1102 // substep -- an over-convergence brake on discharge (the escalate effect, measured -7%).
1103 // The pass exists to converge the quasi-static network; once that is done, it is done.
1104 // One stabilization iteration (async QS-residual zero + fine sweep + coarse cycle),
1105 // graph-captured on the single-GPU path — this loop is THE launch storm (up to 16
1106 // iterations x [colour sweeps + per-(level, colour) coarse kernels] per substep).
1107 // With hub copies (§4.4): fold between the fine sweep and the coarse cycle (the coarse
1108 // leg reads the bases, [0, nBodies)), and re-seed the copies from the bases after it.
1109 auto emitMlIter = [&] {
1110 Kokkos::deep_copy(space, P.maxApproachQS, 0.0f);
1112 P.manifolds, nm, P.manifoldColor, numColors, invMassVel, invInertiaVel, P.quat,
1114 P.maxApproach, P.lambdaAcc, P.vn0,
1115 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT, P.frictionDynamic,
1116 P.vt0, P.restitutionTangent, Kokkos::View<const float*, CpMem>(P.posImpulse),
1117 P.maxApproachQS, bankV, relV, vpkC, orphV, orphPk, velPermC, velOffsP, velFusedP,
1118 nullptr, velOv);
1119 foldVel();
1120 if (H.numLevels > 0) {
1121 multilevelCoarseCycleKokkos(P.manifolds, nm, P.realIndices, invMassCoarse, P.velPred,
1122 P.lambdaAcc, P.maxApproachQS, nBodies, H, S,
1123 /*coarseSweeps*/ 2, mlBody,
1124 Kokkos::View<const float*, CpMem>(relV), &mlOffs,
1125 Kokkos::View<const int*, CpMem>(P.mlBucketPerm), mlFusedP);
1126 if (velCopiesOn)
1127 reseedCopiesKokkos(space, VC, P.velPred, P.angVelPred);
1128 }
1129 };
1130 // Device-side stabilization loop (CUDA, single-rank): fine sweep + coarse cycle +
1131 // adaptive stop, all iterations in ONE kernel (see demFusedMlLoopK).
1132 bool mlLoopDone = false;
1133#ifdef KOKKOS_ENABLE_CUDA
1134 if constexpr (!Hooks::distributed) {
1135 if (!velCopiesOn && velFusedP) {
1136 if (H.numLevels > 0 && mlFusedP) {
1140 P.maxApproachQS, P.lambdaAcc, P.vn0,
1141 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT,
1143 Kokkos::View<const float*, CpMem>(P.posImpulse), bankV, relV, vpkC, orphV, orphPk,
1144 {});
1145 mlLoopDone = demLaunchFusedMlLoop(
1146 space, fStab, velPermC, *velFusedP, numColors, P.manifolds, P.realIndices,
1147 Kokkos::View<const float*, CpMem>(P.invMass), P.velPred, P.lambdaAcc,
1148 P.maxApproachQS, Kokkos::View<const float*, CpMem>(relV), S,
1149 Kokkos::View<const int*, CpMem>(P.mlBucketPerm), *mlFusedP,
1150 2 * P.velocityIterations, stopsOn ? vRestS : fusedOff, mlBody);
1151 } else if (H.numLevels == 0) {
1152 // aggregation found nothing: the loop is plain fine sweeps on the QS residual
1153 const FusedLoopSpec spec{2 * P.velocityIterations, stopsOn ? vRestS : fusedOff,
1154 false};
1155 mlLoopDone = solveVelocityPGSKokkos(
1156 P.manifolds, nm, P.manifoldColor, numColors, P.invMass, P.invInertia, P.quat,
1158 P.maxApproach, P.lambdaAcc, P.vn0,
1159 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT,
1161 Kokkos::View<const float*, CpMem>(P.posImpulse), P.maxApproachQS, bankV, relV,
1162 vpkC, orphV, orphPk, velPermC, velOffsP, velFusedP, &spec, {});
1163 }
1164 }
1165 }
1166#endif
1167 bool graphMl = false;
1168 CudaIterGraph gMl;
1169 if (!mlLoopDone) {
1170 PECLET_DEM_GRAPH_LOOP(graphMl, gMl, emitMlIter, P.graphCache[2])
1171 }
1172 if (velCons)
1173 Kokkos::deep_copy(P.maxConsensus, 0.0f);
1174 for (int it = 0; !mlLoopDone && it < 2 * P.velocityIterations; ++it) {
1175 if (graphMl)
1176 gMl.launch(space);
1177 else
1178 emitMlIter();
1179 const float mlRes = hooks.allMax(withConsensus(readFloat(P.maxApproachQS), velCons));
1180 if (stopsOn && mlRes <= vRestS)
1181 break;
1182 if constexpr (Hooks::distributed) {
1183 if (hooks.syncPoint(it))
1184 syncVel();
1185 }
1186 }
1187 } else if (smode == 4) {
1188 // ORDERED (level-ordered symmetric sweeps; measurement mode): fresh height-from-floor
1189 // BFS levels order the manifolds bottom-up + top-down. Fully symmetric, but a pairwise
1190 // inelastic impulse only EQUALIZES velocities, so a deep column still cools one halving
1191 // per cycle -- measured insufficient on the statics battery (kept for A/B comparison
1192 // against the multilevel pass).
1193 computeHeightLevelsKokkos(P.manifolds, nmVisible, P.realIndices, P.posPred, gHat,
1194 P.heightLevel, nBodies);
1195 std::vector<std::pair<int, int>> buckets;
1197 P.manifolds, nm, P.realIndices, Kokkos::View<const int*, CpMem>(P.manifoldColor),
1198 Kokkos::View<const int*, CpMem>(P.heightLevel), P.levelKey, P.levelPerm, buckets);
1199 const PGSManifoldSweep sweep{P.manifolds,
1200 invMassVel,
1201 invInertiaVel,
1202 P.quat,
1203 P.velPred,
1204 P.angVelPred,
1205 P.realIndices,
1206 P.growthRate,
1208 vRestS,
1209 P.maxApproach,
1210 P.maxApproach,
1211 P.lambdaAcc,
1212 P.vn0,
1213 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags),
1214 P.lambdaT,
1216 P.vt0,
1218 Kokkos::View<const float*, CpMem>(P.posImpulse),
1219 bankV,
1220 relV,
1221 vpkC,
1222 orphV,
1223 orphPk,
1224 velOv};
1225 if (velCons)
1226 Kokkos::deep_copy(P.maxConsensus, 0.0f);
1227 for (int it = 0; it < 2 * P.velocityIterations; ++it) {
1228 Kokkos::deep_copy(P.maxApproach, 0.0f);
1229 solveVelocityPGSBucketsKokkos(sweep, Kokkos::View<const int*, CpMem>(P.levelPerm),
1230 buckets, /*topDown*/ false);
1231 solveVelocityPGSBucketsKokkos(sweep, Kokkos::View<const int*, CpMem>(P.levelPerm),
1232 buckets, /*topDown*/ true);
1233 foldVel();
1234 const float orRes = hooks.allMax(withConsensus(readFloat(P.maxApproach), velCons));
1235 if (stopsOn && orRes <= vRestS)
1236 break;
1237 if constexpr (Hooks::distributed) {
1238 if (hooks.syncPoint(it))
1239 syncVel();
1240 }
1241 }
1242 } else if (smode == 3) {
1243 // ESCALATION (diagnostic/fallback): keep running plain symmetric colored sweeps until
1244 // the residual drains or the 256-sweep cap -- provably correct physics, and the sweep
1245 // count it needs bounds what the ordered pass must deliver.
1246 if (velCons)
1247 Kokkos::deep_copy(P.maxConsensus, 0.0f);
1248 for (int it = 0; it < 256; ++it) {
1249 Kokkos::deep_copy(P.maxApproach, 0.0f);
1251 P.manifolds, nm, P.manifoldColor, numColors, invMassVel, invInertiaVel, P.quat,
1253 P.maxApproach, P.lambdaAcc, P.vn0,
1254 Kokkos::View<const unsigned char*, CpMem>(P.sideFlags), P.lambdaT, P.frictionDynamic,
1255 P.vt0, P.restitutionTangent, Kokkos::View<const float*, CpMem>(P.posImpulse), {},
1256 bankV, relV, vpkC, orphV, orphPk, velPermC, velOffsP, velFusedP, nullptr, velOv);
1257 foldVel();
1258 const float esRes = hooks.allMax(withConsensus(readFloat(P.maxApproach), velCons));
1259 if (stopsOn && esRes <= vRestS)
1260 break;
1261 if constexpr (Hooks::distributed) {
1262 if (hooks.syncPoint(it))
1263 syncVel();
1264 }
1265 }
1266 }
1267 if constexpr (Hooks::distributed)
1268 syncVel(); // final refresh of the stabilization phase
1269 }
1270 }
1271 // End of the velocity phase: the orphan accounts back to balances (the bookkeeping, the legacy
1272 // friction pass and the next substep read the bases; copy slots are dead from here on).
1273 // Under rank-level M the owner rows of k_vel > 1 bodies get their balance back (§13.3 step 8;
1274 // the copies' shares are dead from here on, the next gather forwards B again).
1275 if (velM) {
1276 if (poisson)
1277 hooks.restoreOrphanBalance(P);
1278 } else if (velCopiesOn) {
1279 unfoldOrphanKokkos(VC, orphV);
1280 }
1281 // Poisson bookkeeping runs once per substep on the FINAL velocity state (after every phase and
1282 // ghost refresh): bank this substep's kinetic compression, deduct what was returned/released.
1283 if (poisson)
1286 P.growthRate, P.restitutionNormal, 2.0f * P.dt * gMagP, P.vn0, P.lambdaAcc,
1287 Kokkos::View<const float*, CpMem>(P.restRel), P.restBank, P.restVPeak,
1288 /*dedupTwins*/ !Hooks::distributed);
1289 if (usePGS) { // save the converged force network for next substep's warm start
1291 P.pairKeys, P.lambdaAcc, P.lambdaT, Kokkos::View<const float*, CpMem>(P.restBank),
1292 Kokkos::View<const float*, CpMem>(P.restVPeak), P.prevPairKeys, P.prevLambda, P.prevLambdaT,
1294 // Carry the per-manifold colour by pair key (single-GPU incremental
1295 // colouring warm start; empty on the distributed path).
1296 Hooks::distributed ? Kokkos::View<const int*, CpMem>()
1297 : Kokkos::View<const int*, CpMem>(P.manifoldColor),
1298 Hooks::distributed ? Kokkos::View<int*, CpMem>() : P.prevManifoldColor);
1299 P.prevPairCount = nm;
1300 }
1301 if (legacyFriction) {
1303 hooks.syncFrictionCounts(P);
1304 // World-frame inverse inertia at the phase's frozen orientation P.quat (§12 S15).
1307 P.deltaVel, P.deltaAngVel);
1309 if constexpr (Hooks::distributed)
1310 hooks.syncVelocities(P); // publish the friction velocity update to the ghosts
1311 }
1312
1314 P.angVelPred, P.posPred, P.quatPred, P.angVel, P.dt);
1315 if constexpr (Hooks::distributed)
1316 hooks.publishPositions(P);
1317
1318 // Position-phase copies (§4.4, §4.5; demStep's periodic images, §WO-4 item 2): seeded after the
1319 // predict (and, under MPI, the publish). An image is re-seeded to its body's predicted position
1320 // plus its generation shift, so its increments are relative to the body.
1321 const bool posCopiesOn = PC.nGroups > 0;
1322 Kokkos::View<const float*, CpMem> invMassPos = P.invMass;
1323 Kokkos::View<const float* [3], CpMem> invInertiaPos = P.invInertia;
1324 SlotOverride posOv{};
1325 // Rank-level M in the position phase (§13.3 step 9): an exchanging rank on the GS paths (both
1326 // openings made k_pos). The solve views carry each raw slot's global k_pos (a hub copy its
1327 // base's); the local fold still divides by the local count a; the syncs are M in the halo.
1328 const bool posM = rankM && P.velocityUseGS;
1329 if (posM) {
1330 buildSolveViewsRankKKokkos(P, PC, slotBase + (posCopiesOn ? PC.nCopies : 0), P.invMass,
1331 P.invInertia, Kokkos::View<const int*, CpMem>(P.kPos));
1332 invMassPos = P.invMassSolve;
1333 invInertiaPos = P.invInertiaSolve;
1334 if (posCopiesOn) {
1336 markCopySeedsKokkos(PC, P.posPred, {}, {}, {});
1337 if (PC.nHubs > 0)
1338 posOv = SlotOverride{PC.slotA, PC.slotB, {}, 1.0f};
1339#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 7
1340 posOv = SlotOverride{PC.slotA, PC.slotB, P.splitSlot, 1.5f}; // G13 mutant 7
1341#endif
1342 }
1343 } else if (posCopiesOn) {
1344 buildSolveViewsKokkos(P, PC, slotBase + PC.nCopies, P.invMass, P.invInertia);
1346 markCopySeedsKokkos(PC, P.posPred, {}, {}, {});
1347 invMassPos = P.invMassSolve;
1348 invInertiaPos = P.invInertiaSolve;
1349 // The slot overrides only: the overlap projection is never relaxed (§13.1).
1350 if (PC.nHubs > 0)
1351 posOv = SlotOverride{PC.slotA, PC.slotB, {}, 1.0f};
1352#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 7
1353 posOv = SlotOverride{PC.slotA, PC.slotB, P.splitSlot, 1.5f}; // G13 mutant 7
1354#endif
1355 }
1356 Kokkos::Profiling::popRegion();
1357 Kokkos::Profiling::pushRegion("dem::solve::pos");
1358 auto syncPos = [&] {
1359 if constexpr (Hooks::distributed) {
1360 hooks.syncPositions(P);
1361 if (posCopiesOn)
1362 markCopySeedsKokkos(PC, P.posPred, {}, {}, {});
1363 }
1364 };
1365
1366 // Overlap resolved once the deepest penetration falls below ~0.01% of a particle radius.
1367 const float posTol = 1e-4f * P.baseRadius * P.globalScale;
1368 {
1369 auto pc = Kokkos::subview(P.posLambdaContact, Kokkos::pair<int, int>(0, nc));
1370 Kokkos::deep_copy(pc, 0.0f);
1371 }
1372
1373 // One position iteration (async residual zero + colored overlap sweep), graph-captured on
1374 // the single-GPU path like the velocity loops.
1375 // WO-12: the accumulated, retractable projection (§13.5) at omega = P.positionOmega; the stop
1376 // reads the position change (posResidual), maxOverlap the largest violation seen.
1377 auto emitPosIter = [&] {
1378 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
1379 Kokkos::deep_copy(space, P.posResidual, 0.0f);
1380 solvePositionColoredGSKokkos(P.contacts, posUnits, numPosUnits, P.unitColor, numPosColors,
1381 invMassPos, P.posPred, P.quatPred, P.quat, invInertiaPos,
1382 P.maxOverlap, P.posLambdaContact, posPermC, posOffsP, posFusedP,
1383 nullptr, posOv, P.posResidual, P.positionOmega);
1384 if (posCopiesOn)
1385 foldCopiesKokkos(space, PC, P.posPred, {}, {}, {}, P.maxConsensus);
1386 };
1387 // §12 S14: the position phase folds its consensus corrections (|dx| of a local fold or an M
1388 // sync, absolute like maxOverlap) into its stop vote, exactly as the velocity phase does.
1389 const bool posCons = posCopiesOn || posM;
1390 // Device-side position loop (CUDA, single-rank): all overlap-projection iterations + the
1391 // adaptive stop in ONE kernel. Disabled while the phase has copies (the fold runs between
1392 // iterations on the host-driven path).
1393 bool posLoopDone = false;
1394 if constexpr (!Hooks::distributed) {
1395 if (P.velocityUseGS && !posCopiesOn && posFusedP) {
1396 FusedLoopSpec spec{P.positionIterations, stopsOn ? posTol : fusedOff, true};
1397 if (P.iterCounters) // diagnostics (§12 S12)
1398 spec.iters = demIterCountSlot(P, 1);
1399 Kokkos::deep_copy(space, P.maxOverlap, 0.0f); // the largest violation over the device loop
1400 posLoopDone = solvePositionColoredGSKokkos(
1401 P.contacts, posUnits, numPosUnits, P.unitColor, numPosColors, P.invMass, P.posPred,
1402 P.quatPred, P.quat, P.invInertia, P.maxOverlap, P.posLambdaContact, posPermC, posOffsP,
1403 posFusedP, &spec, {}, P.posResidual, P.positionOmega);
1404 }
1405 }
1406 bool graphPos = false;
1407 CudaIterGraph gPos;
1408 if (!posLoopDone && P.velocityUseGS) {
1409 PECLET_DEM_GRAPH_LOOP(graphPos, gPos, emitPosIter, P.graphCache[3])
1410 }
1411 P.splitStats.posItersUsed = posLoopDone ? demReadIterCount(P, 1) : 0;
1412 if (posCons)
1413 Kokkos::deep_copy(P.maxConsensus, 0.0f);
1414 for (int it = 0; !posLoopDone && it < P.positionIterations; ++it) {
1416 if (P.velocityUseGS) {
1417 if (graphPos)
1418 gPos.launch(space);
1419 else
1420 emitPosIter();
1421 // Every active unit is coloured (§4.2): the count-averaged fallback is gone; the stop's
1422 // Allreduce carries the colouring-invariant vote if the velocity loop did not.
1423 const float posRes = voteInvariant(withConsensus(readFloat(P.posResidual), posCons));
1424 // Adaptive stop (WO-12): end once no contact's projection moves it by more than posTol
1425 // (|d| w, retractions included). Fixed positionIterations is the cap. Distributed:
1426 // Allreduce-MAXed so all ranks break together.
1427 if (stopsOn && posRes < posTol)
1428 break;
1429 } else {
1430 // Mass-split Jacobi (see the velocity branch): count, global counts, solve, add.
1432 hooks.syncContactCounts(P);
1435 /*massSplit=*/true);
1437 P.constraintCounts, /*averaged=*/false);
1438 }
1439 if constexpr (Hooks::distributed) {
1440 if (hooks.syncPoint(it))
1441 syncPos();
1442 }
1443 }
1444 if constexpr (Hooks::distributed)
1445 syncPos(); // final owner->ghost refresh of the position phase
1446 // Position-channel Coulomb-bound carry (PGS path): next substep's friction cone sees
1447 // mu * (velocity-impulse channel + this position-channel load). Without it a jostled bed's
1448 // bound under-counts the true normal force and stick leaks (measured: 99% sliding wall
1449 // contacts in the benchmark drum while the Hertz reference sticks).
1450 if (usePGS && nm > 0)
1453 Kokkos::Profiling::popRegion();
1454 (void)space;
1455}
1456
1457} // namespace peclet::dem
1458
1459#endif // DEM_SOLVE_DRIVER_HPP
dem — portable (ArborX) broad-phase, the Kokkos-native replacement for the CUDA-only cuBQL broad-phas...
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
void commitPosImpulseKokkos(Kokkos::View< const float *, CpMem > posLambdaContact, int numContacts, Kokkos::View< const int *, CpMem > contactSlot, Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const unsigned long long *, CpMem > prevKeysSorted, int prevCount, float dt, Kokkos::View< float *, CpMem > scratchManifold, Kokkos::View< float *, CpMem > prevPosImpulse)
After the position solve: convert the per-contact positional lambdas into an impulse- equivalent per ...
void commitContactColorKokkos(Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const int *, CpMem > color, Kokkos::View< unsigned long long *, CpMem > prevKeys, Kokkos::View< int *, CpMem > prevColor, Kokkos::View< int *, CpMem > perm, int numContacts)
Commit this substep's per-contact (key, colour) sorted by key, for next substep's warm gather.
int colorManifoldsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, int numReal, Kokkos::View< int *, CpMem > mColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, SlotOverride ov={}, bool dedupTwins=true, int vertexSpan=0)
Greedy graph-colour the manifolds: no two manifolds sharing a real body get the same colour.
void solvePositionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *[4], CpMem > quatPred, Kokkos::View< const float *[4], CpMem > quatStatic, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< float *[3], CpMem > deltaPos, Kokkos::View< float *[4], CpMem > deltaQuat, Kokkos::View< int *, CpMem > constraintCounts, Kokkos::View< float, CpMem > maxOverlap, Kokkos::View< const int *, CpMem > onlyColor={}, int colorFilter=0, bool massSplit=false)
Accumulate XPBD position corrections for numContacts contacts.
void computeHeightLevelsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< int *, CpMem > heights, int numReal)
void activityHitVelocityKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int nm, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > color, SlotOverride ov, Kokkos::View< unsigned char *, CpMem > hit, int nSlots)
Rank-level activity pass (docs/contact_solve_framework.md §13.3 C4; the distributed step only),...
void computeXGateKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int nm, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const unsigned long long *, CpMem > velMask, Kokkos::View< unsigned char *, CpMem > gate, int color, int numColors, long long t)
The per-manifold fire gate of sync interval t: 1 iff this rank (colour color) holds every endpoint wh...
void computePlaneLoadKokkos(Kokkos::View< ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, FrManifoldCounts planeFriction)
Plane (idB<0) one-shot loads.
void fillGidBaseKokkos(Vi gid, int n, int base)
gid(i) = base + i over [0, n) — the per-rank global-id re-base of the distributed step (namespace sco...
float maxOwnedRadius(const Particles &P)
Largest effective particle radius over the owned set (= max scale × globalScale, growth included).
void buildHubCopiesKokkos(Kokkos::View< const int *, CpMem > eA, Kokkos::View< const int *, CpMem > eB, int nE, int nV, int slotBase, Kokkos::View< const int *, CpMem > deg, PhaseCopies &H)
Build one phase's hub copies (§4.4 steps 2, 3 and 5): every vertex with d > kHubEdgeBudget active edg...
bool solveVelocityPGSKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > mColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const unsigned char *, CpMem > sideFlag, Kokkos::View< float *[3], CpMem > lambdaT, float frictionDynamic, Kokkos::View< const float *[3], CpMem > vt0={}, float restitutionTangent=0.0f, Kokkos::View< const float *, CpMem > posImpulse={}, Kokkos::View< float, CpMem > maxApproachQS={}, Kokkos::View< float *, CpMem > restBank={}, Kokkos::View< float *, CpMem > restRel={}, Kokkos::View< const float *, CpMem > restVPeak={}, Kokkos::View< float *, CpMem > restOrphan={}, Kokkos::View< const float *, CpMem > restOrphanVPeak={}, Kokkos::View< const int *, CpMem > colorPerm={}, const std::vector< int > *colorOffs=nullptr, const FusedSweepCtx *fused=nullptr, const FusedLoopSpec *loop=nullptr, SlotOverride ov={})
Returns true when the sweep (or, with loop, the whole iteration loop) was submitted; false ONLY in lo...
int colorContactsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, int numBodies, Kokkos::View< int *, CpMem > cColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, PosUnits units={}, SlotOverride ov={}, int vertexSpan=0)
Greedy graph-colour the contacts (raw bodyA/bodyB): no two contacts sharing a body get the same colou...
int countSplitBodiesKokkos(const PhaseCopies &H)
Count the bodies whose group has k > 1 (split_stats).
void countVelocityJacobiKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< int *, CpMem > counts, bool dedupTwins=true)
Mass-split Jacobi count pass (docs/contact_solve_framework.md §3.1, D3): per body (REAL index),...
void multilevelCoarseCycleKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< float, CpMem > maxApproach, int numReal, const ContactHierarchy &H, MlScratch &S, int coarseSweeps, const MlBodyViews &body, Kokkos::View< const float *, CpMem > restRel={}, const std::vector< std::vector< int > > *bkOffs=nullptr, Kokkos::View< const int *, CpMem > bkPerm={}, const MlFusedCtx *fused=nullptr)
void demSolveContacts(Particles &P, int nc, int nm, int nBodies, Kokkos::View< const int *, CpMem > keyIdx, const Hooks &hooks)
One full velocity + position contact solve over the already-built contacts/manifolds (see file commen...
Kokkos::View< float *, CpMem > Vf
Kokkos::View< int *, CpMem > Vi
int * demIterCountSlot(Particles &P, int slot)
Diagnostics (docs/contact_solve_framework.md §12 S12): the device scalar a fused main loop writes its...
void buildColorBucketsKokkos(Kokkos::View< const int *, CpMem > colorOf, int n, int numColors, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< int > &offs)
Dense colour buckets (numColors <= 64): perm[offs[c] .
void reseedCopiesKokkos(CpExec space, const PhaseCopies &H, Kokkos::View< float *[3], CpMem > x, Kokkos::View< float *[3], CpMem > w)
Re-seed every member from its base, sigma unchanged (the multilevel pass, after the coarse cycle has ...
int countInactiveAggregatedKokkos(Kokkos::View< const int *, CpMem > parent, int off, int nV, int nGroups1, Kokkos::View< const int *, CpMem > a)
Debug-build check (§13.2, §13.5 WO-5 item 4): the vertices with a = 0 that sit in a level-1 multileve...
void applyVelocityDeltasKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel, V3 deltaAngVel)
Add accumulated velocity/angular deltas onto the predicted velocity, then clear the delta buffers.
void markPersistentManifoldsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > keyIdx, Kokkos::View< const unsigned long long *, CpMem > prevKeys, int prevCount, Kokkos::View< unsigned long long *, CpMem > outKeys, Kokkos::View< unsigned char *, CpMem > outFlags)
keyIdx maps a body slot to the identity the pair key is built from: the REAL index map on the single-...
void computeSideFlagsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const unsigned char *, CpMem > grounded, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *[3], CpMem > velPred, F3 gHat, float riseThr, Kokkos::View< unsigned char *, CpMem > sideFlag, Kokkos::View< const float *, CpMem > vn0, float approachThr, bool dedupTwins=true)
Decide each persistent contact's treatment ONCE per substep (before any impulse is applied): 0 = symm...
bool demFusedWanted(bool graphReplayAvailable, int mode)
Fused-sweep policy.
int colorManifoldsIncrementalKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, int numReal, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const int *, CpMem > prevColor, int prevCount, Kokkos::View< int *, CpMem > mColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, bool forceFull, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, bool dedupTwins=true)
Incremental (warm-started) manifold colouring for the single-GPU PGS path.
void decayBodyOrphanKokkos(Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanVPeak, int numOwned, float restVelThreshold)
Orphan-account aging, once per substep over the OWNED bodies: both the balance and the carried event ...
void gatherWarmLambdaKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > keyIdx, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const float *, CpMem > prevLambda, Kokkos::View< const float *[3], CpMem > prevLambdaT, Kokkos::View< const float *, CpMem > prevPosImpulse, Kokkos::View< const float *, CpMem > prevRestBank, Kokkos::View< const float *, CpMem > prevRestVPeak, int prevCount, Kokkos::View< unsigned long long *, CpMem > outKeys, Kokkos::View< float *, CpMem > outWarm, Kokkos::View< float *[3], CpMem > outWarmT, Kokkos::View< float *, CpMem > outPosImpulse, Kokkos::View< float *, CpMem > outRestBank, Kokkos::View< float *, CpMem > outRestVPeak, Kokkos::View< unsigned char *, CpMem > outMatched={}, bool dedupTwins=true)
Warm-start gather for the PGS velocity solve: per manifold, write its pair key and look up the previo...
void updateGroundedLevelsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< unsigned char *, CpMem > grounded, int numReal, int sweeps, int decay)
Guendelman support levels, warm-started: decay every body's level by decay, re-seed 255 at wall/plane...
int readInt(Kokkos::View< int, CpMem > v)
float readFloat(Kokkos::View< float, CpMem > v)
void buildInvMassCoarseKokkos(Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const int *, CpMem > k, Kokkos::View< const int *, CpMem > a, int n, Kokkos::View< float *, CpMem > out)
The multilevel coarse vertex's inverse mass (§13.2): invMass(q) (k(q) / max(1, a(q))),...
void scatterOrphanBanksKokkos(Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const float *, CpMem > prevRestBank, Kokkos::View< const float *, CpMem > prevRestVPeak, Kokkos::View< const unsigned char *, CpMem > matched, int prevCount, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanVPeak, Kokkos::View< const int *, CpMem > gidSorted={}, Kokkos::View< const int *, CpMem > slotSorted={}, int ownedLimit=-1)
Orphan transfer: previous-ledger entries NOT matched by any current manifold (their pair died this su...
int buildPositionUnitsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int n, Kokkos::View< int *, CpMem > unitStart, Kokkos::View< int *, CpMem > unitContacts, int numManifolds=-1)
Build the position units of contacts [0, n) (docs/contact_solve_framework.md §4.3,...
void applyVelocityAndPredictPositionKokkos(int n, V3 pos, Vf invMass, V3 vel, V4 quat, V3 velPred, V3 angVelPred, V3 posPred, V4 quatPred, V3 angVel, float dt)
Re-integration: persist solved velocity, trapezoidal position predict, quaternion integrate.
CpExec::memory_space CpMem
void solveVelocityKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float *[3], CpMem > deltaVel, Kokkos::View< float *[3], CpMem > deltaAngVel, Kokkos::View< int *, CpMem > velCounts, Kokkos::View< const int *, CpMem > onlyColor={}, int colorFilter=0, Kokkos::View< const unsigned char *, CpMem > persistent={}, Kokkos::View< const float *[3], CpMem > posPred={}, F3 gHat={}, Kokkos::View< const unsigned char *, CpMem > grounded={}, bool massSplit=false, bool dedupTwins=true)
Accumulate normal-restitution velocity deltas for numManifolds manifolds.
int demReadIterCount(const Particles &P, int slot)
The count a fused main loop wrote (one small readback, only with P.iterCounters on); -1 with the coun...
void buildLevelColorBucketsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const int *, CpMem > heights, Kokkos::View< int *, CpMem > keys, Kokkos::View< int *, CpMem > perm, std::vector< std::pair< int, int > > &buckets)
Bucket the active coloured manifolds by (support level, colour) for the level-ordered ("multilevel") ...
void positionEdgeEndsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, const PosUnits &units, int nu, Kokkos::View< const int *, CpMem > color, Kokkos::View< int *, CpMem > eA, Kokkos::View< int *, CpMem > eB)
Position-phase colouring edges: one per unit, the leader contact's raw (bodyA, bodyB); active = the u...
void finishActivityKokkos(Kokkos::View< const unsigned char *, CpMem > hit, int nSlots, PhaseCopies &H, Kokkos::View< int *, CpMem > a)
a(q) from the pass (§13.3): hit(q) for a slot outside any group; at a hub base the number of its grou...
int findCollisionsGrow(Particles &P, float margin)
Broad phase with an automatically-grown pair buffer.
void commitPairKeysLambdaKokkos(Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const float *, CpMem > lambda, Kokkos::View< const float *[3], CpMem > lambdaT, Kokkos::View< const float *, CpMem > restBank, Kokkos::View< const float *, CpMem > restVPeak, Kokkos::View< unsigned long long *, CpMem > prevKeys, Kokkos::View< float *, CpMem > prevLambda, Kokkos::View< float *[3], CpMem > prevLambdaT, Kokkos::View< float *, CpMem > prevRestBank, Kokkos::View< float *, CpMem > prevRestVPeak, Kokkos::View< int *, CpMem > perm, int numManifolds, Kokkos::View< const int *, CpMem > color={}, Kokkos::View< int *, CpMem > prevColor={})
Save this substep's keys + converged impulses (normal AND tangential) and key-sort them for next subs...
long long countUnfiredKokkos(Kokkos::View< const unsigned char *, CpMem > gate, int nm)
Diagnostics (iterCounters): the number of gated-off manifolds of one sweep.
void buildCoarseBucketsKokkos(const ContactHierarchy &H, MlScratch &S, int numManifolds, Kokkos::View< int *, CpMem > colorScratch, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< std::vector< int > > &offs)
One multilevel stabilization cycle over an already-built hierarchy: fine colored smoothing is the cal...
constexpr float kSplitOmegaVelocity
Split relaxation (§4.5, §13.1): the PGS normal step on an edge touching a mass-split slot is scaled b...
void countPositionJacobiKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< int *, CpMem > counts)
Mass-split Jacobi count pass (docs/contact_solve_framework.md §3.1, D3): per body slot,...
void solveVelocityPGSBucketsKokkos(const PGSManifoldSweep &f, Kokkos::View< const int *, CpMem > perm, const std::vector< std::pair< int, int > > &buckets, bool topDown)
Level-ordered symmetric sweep: launch one PGS kernel per (level, colour) bucket, ascending (bottom-up...
void seedCopySlotStateKokkos(Particles &P, const PhaseCopies &H)
Seed the copy slots' read-only state from their hub vertex (§4.4 step 6): orientation,...
void unfoldOrphanKokkos(const PhaseCopies &H, Kokkos::View< float *, CpMem > orphan)
Orphan accounts back from share form (before a rank sync and at the end of the velocity phase): the b...
void buildSolveViewsKokkos(Particles &P, const PhaseCopies &H, int nSlots, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia)
Solve views of a phase (§4.5): invMassSolve / invInertiaSolve = the phase's masses on every slot in [...
void fillWorldRadiiKokkos(Vf scale, Vf rad, float gs, float bR, int n)
World radii rad(i) = scale(i) * globalScale * baseRadius over [0, n): the owned span before a step,...
void applyVelocityDeltasAveragedKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel, V3 deltaAngVel, Vi velCounts, bool averaged=true)
Apply the accumulated velocity deltas AVERAGED by the per-body manifold count — the velocity- solve t...
void buildInvInertiaCoarseKokkos(Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const int *, CpMem > k, Kokkos::View< const int *, CpMem > a, int n, Kokkos::View< float *[3], CpMem > out)
The multilevel coarse vertex's inverse inertia (docs/contact_physics_followups.md §2....
constexpr int kColorUncolourable
void buildCopyGroupsKokkos(Kokkos::View< const int *, CpMem > realIdx, int span, int numReal, Kokkos::View< const int *, CpMem > deg, Kokkos::View< const float *[3], CpMem > imageShift, PhaseCopies &H, bool identityBodies=false)
Build the fold groups of a phase (§4.4 "local fold", §WO-4 item 2): every body with more than one slo...
int countLightHubsKokkos(const PhaseCopies &H, Kokkos::View< const int *, CpMem > eA, Kokkos::View< const int *, CpMem > eB, int nE, int nV, Kokkos::View< const float *, CpMem > invMass)
Light hubs (split_stats, R-P3): velocity hubs whose mass is below 10x the mean mass of their partners...
int findCollisionsVerlet(Particles &P, float margin, float maxRad)
Verlet-cached impulse broadphase (single-GPU, non-periodic).
void markCopySeedsKokkos(const PhaseCopies &H, Kokkos::View< float *[3], CpMem > x, Kokkos::View< float *[3], CpMem > w, Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanPeak, bool shareFromBase=false)
Mark the phase seed sigma of every group and re-seed its members from the base (§4....
void buildSolveViewsRankKKokkos(Particles &P, const PhaseCopies &H, int nSlots, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const int *, CpMem > kRank)
Solve views under rank-level M (§13.3, §4.5): invMassSolve / invInertiaSolve = k x the body's masses ...
void warmStartApplyKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > warmP, Kokkos::View< float *[3], CpMem > warmT, bool dedupTwins=true)
Apply the warm-start impulses up front (order-independent: fixed impulses, atomic adds).
void solveContactFrictionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, FrManifoldCounts planeFriction, float frictionDynamic, Kokkos::View< float *[3], CpMem > deltaVel, Kokkos::View< float *[3], CpMem > deltaAngVel)
One count-averaged Coulomb friction sweep.
void computeVn0Kokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, Kokkos::View< float *, CpMem > vn0, Kokkos::View< float *[3], CpMem > vt0, bool dedupTwins=true)
-— Warm-started projected Gauss-Seidel (PGS) velocity solve -— Nonsmooth contact dynamics (Moreau-Jea...
void applyUpdatesKokkos(int n, V3 posPred, V3 velPred, V3 deltaPos, V3 deltaVel, Vi constraintCounts, bool averaged=true)
Jacobi count-averaged apply of position/velocity deltas, then clear deltas + counts.
void solveVelocityColoredGSKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > mColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< const unsigned char *, CpMem > persistent={}, Kokkos::View< const float *[3], CpMem > posPred={}, F3 gHat={}, Kokkos::View< const unsigned char *, CpMem > grounded={}, SlotOverride ov={}, XGateSpec xg={})
Colored Gauss–Seidel normal-restitution solve: sweep the numColors colour classes in order,...
void vertexDegreeKokkos(Kokkos::View< const int *, CpMem > eA, Kokkos::View< const int *, CpMem > eB, int nE, int nV, Kokkos::View< int *, CpMem > deg)
Per-vertex active degree over [0, nV) (§4.4 step 1). Wall edges have one endpoint.
void accumulateNormalImpulseKokkos(Kokkos::View< ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, bool dedupTwins=true)
Force-chain normal load, accumulated over the velocity iterations: contacts(idx).friction_lambda_n +=...
ContactHierarchy buildContactHierarchyKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *[3], CpMem > vt0, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< const float *, CpMem > invMass, float qsThr, int gateMask, int numReal, MlScratch &S, Kokkos::View< long long *, CpMem > winner, Kokkos::View< std::uint64_t *, CpMem > colorMask, bool excludeImmovable=false, Kokkos::View< const unsigned char *, CpMem > asleep={}, const MlBodyViews &body={})
Build the aggregation hierarchy + per-level crossing-manifold colorings.
void activityHitPositionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, const PosUnits &units, int nu, Kokkos::View< const int *, CpMem > color, SlotOverride ov, Kokkos::View< unsigned char *, CpMem > hit, int nSlots)
The same over the position phase's units (raw slots of the leader contact, through the overrides).
void velocityEdgeEndsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int nm, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > color, Kokkos::View< int *, CpMem > eA, Kokkos::View< int *, CpMem > eB)
Velocity-phase colouring edges: the vertices realIdx(bodyA / bodyB) of each manifold (-1: a wall end,...
MlFusedCtx demMakeMlFusedCtx(CpExec &space, const ContactHierarchy &H, const std::vector< std::vector< int > > &bkOffs, int numManifolds, int numReal, int coarseSweeps, Kokkos::View< int *, CpMem > offsDev, Kokkos::View< unsigned *, CpMem > bar)
Build the fused-coarse-cycle context from an already-built hierarchy + its dense buckets: flatten the...
void growCopyView(V &v, std::size_t n, const char *label)
Grow-only (re)allocation of a copy-machinery view to at least n entries.
PGSManifoldSweep makePGSManifoldSweep(Kokkos::View< const ManifoldC *, CpMem > manifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< float, CpMem > maxApproachQS, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const unsigned char *, CpMem > sideFlag, Kokkos::View< float *[3], CpMem > lambdaT, float frictionDynamic, Kokkos::View< const float *[3], CpMem > vt0, float restitutionTangent, Kokkos::View< const float *, CpMem > posImpulse, Kokkos::View< float *, CpMem > restBank, Kokkos::View< float *, CpMem > restRel, Kokkos::View< const float *, CpMem > restVPeak, Kokkos::View< float *, CpMem > restOrphan, Kokkos::View< const float *, CpMem > restOrphanVPeak, SlotOverride ov={})
Build the shared per-manifold sweep functor (the colored launch loop, the fused kernels and the fused...
int findCollisionsArborX(PosV pos, RadV rad, int numParticles, int numReal, float margin, PairsV outPairs, CountV outCount, float boxCap=0.0f)
Emit candidate collision pairs (i<j) for real particles into outPairs/outCount.
void expandUnitColorsKokkos(const PosUnits &units, int numUnits, Kokkos::View< const int *, CpMem > unitColor, Kokkos::View< int *, CpMem > contactColor)
Copy each position unit's colour onto its contacts (contactColor(i) = unitColor(unit of i)).
void foldCopiesKokkos(CpExec space, const PhaseCopies &H, Kokkos::View< float *[3], CpMem > x, Kokkos::View< float *[3], CpMem > w, Kokkos::View< float *, CpMem > orphan, Kokkos::View< float *, CpMem > orphanPeak, Kokkos::View< float, CpMem > consensus={})
The local fold (§4.4), once per iteration after the sweep: per group T = sum over the ACTIVE members ...
FusedSweepCtx demMakeFusedCtx(CpExec &space, const std::vector< int > &offs, Kokkos::View< int *, CpMem > offsDev, Kokkos::View< unsigned *, CpMem > bar)
Fill a FusedSweepCtx from buildColorBucketsKokkos's host offsets: async-upload them into the pooled d...
int countSplitAggregatedKokkos(Kokkos::View< const int *, CpMem > parent, int off, int nV, int nGroups1, Kokkos::View< const unsigned char *, CpMem > split)
split_stats.mlHubAggregated (§13.2's positive control): the vertices v in [0, nV) with split(v) !...
void countFrictionContactsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const int *, CpMem > realIdx, FrManifoldCounts planeFriction)
Per-body active-contact count into planeFriction(:,1).
bool solvePositionColoredGSKokkos(Kokkos::View< const ContactC *, CpMem > contacts, const PosUnits &units, int numContacts, Kokkos::View< const int *, CpMem > cColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *[3], CpMem > posPred, Kokkos::View< const float *[4], CpMem > quatPred, Kokkos::View< const float *[4], CpMem > quatStatic, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< float, CpMem > maxOverlap, Kokkos::View< float *, CpMem > posLambdaAcc={}, Kokkos::View< const int *, CpMem > colorPerm={}, const std::vector< int > *colorOffs=nullptr, const FusedSweepCtx *fused=nullptr, const FusedLoopSpec *loop=nullptr, SlotOverride ov={}, Kokkos::View< float, CpMem > posResidual={}, float omega=1.0f)
Colored Gauss–Seidel XPBD overlap solve: sweep the numColors colour classes in order,...
Kokkos::DefaultExecutionSpace CpExec
int colorContactsIncrementalKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, int numBodies, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const int *, CpMem > prevColor, int prevCount, Kokkos::View< int *, CpMem > cColor, Kokkos::View< unsigned long long *, CpMem > keysOut, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, bool forceFull, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, PosUnits units={})
Incremental (warm-started) contact colouring for the single-GPU PGS position solve.
void updateRestitutionBankKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, float restVelThreshold, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *, CpMem > lambdaAcc, Kokkos::View< const float *, CpMem > restRel, Kokkos::View< float *, CpMem > restBank, Kokkos::View< float *, CpMem > restVPeak, bool dedupTwins=true)
Event-level (Poisson) restitution bookkeeping, once per substep AFTER all velocity phases (restitutio...
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
dem — body copies of the contact solve (docs/contact_solve_framework.md §4.4, §4.5,...
#define PECLET_DEM_GRAPH_LOOP(useVar, graphVar, emitVar, slotVar)
dem — portable (Kokkos) Coulomb friction cluster (the single dissipative friction path).
dem — multilevel (GraphMG-style) momentum-conserving contact stabilization.
dem — portable (Kokkos) XPBD position solve (pure overlap removal).
dem — portable (Kokkos) manifold velocity solve (normal restitution impulse).
Host-side description of one built hierarchy (offsets into the packed group pools).
bool capture(CpExec &, F &&, void *&)
Device-side iteration loop: run up to maxIters sweeps of the colour classes inside ONE kernel,...
Device-side context for a fused colour sweep: the colour offsets (numColors+1, uploaded from buildCol...
The per-body inputs of the rigid coarse space (§2.3-§2.5): the predicted positions (the member offset...
Fused-coarse-cycle context: flat per-level colour offsets on device + barrier + meta.
Device scratch for the multilevel pass, sized once (see Particles::allocate).
One full colored PGS sweep.
std::vector< int > parentOff
std::vector< int > numGroups
Kokkos::View< float, CpMem > posResidual
Kokkos::View< int *, CpMem > commitPerm
Kokkos::View< unsigned *, CpMem > fusedBar
Kokkos::View< unsigned char *, CpMem > asleep
Kokkos::View< int *, CpMem > bucketCursor
Kokkos::View< unsigned char *, CpMem > sideFlags
Kokkos::View< unsigned long long *, CpMem > prevContactKeys
Kokkos::View< int *, CpMem > mlGrp
Kokkos::View< unsigned char *, CpMem > xGate
Kokkos::View< unsigned char *, CpMem > manifoldPersistent
Kokkos::View< int *, CpMem > aPos
Kokkos::View< float *[3], CpMem > impRefPos
Kokkos::View< int *, CpMem > posOffsDev
Kokkos::View< float *, CpMem > bodyOrphanVPeak
Kokkos::View< int *, CpMem > velPerm
Kokkos::View< unsigned char *, CpMem > contactSleep
Kokkos::View< float *, CpMem > prevLambda
void ensureCapacity(int newCap)
Kokkos::View< int *, CpMem > unitStart
Kokkos::View< ManifoldC *, CpMem > manifolds
Kokkos::View< float *[3], CpMem > mlOriginG
Kokkos::View< float *, CpMem > vn0
Kokkos::View< float *, CpMem > mlMassG
Kokkos::View< int *, CpMem > levelKey
Kokkos::View< int *[2], CpMem > pairs
Kokkos::View< float *[3], CpMem > invInertiaSolve
Kokkos::View< float *[3], CpMem > imageShift
Kokkos::View< float *[3], CpMem > mlAngG0
Kokkos::View< float *[3], CpMem > vt0
Kokkos::View< int *, CpMem > vertexDegree
Kokkos::View< float *, CpMem > restRel
Kokkos::View< int *, CpMem > levelPerm
Kokkos::View< int *, CpMem > prevManifoldColor
Kokkos::View< float *[3], CpMem > mlAngG
Kokkos::View< unsigned char *, CpMem > splitSlot
Kokkos::View< int *, CpMem > kVel
Kokkos::View< float *[3], CpMem > mlComOffG
Kokkos::View< unsigned long long *, CpMem > contactKeys
Kokkos::View< int *, CpMem > prevContactColor
Kokkos::View< unsigned char *, CpMem > prevMatched
Kokkos::View< unsigned char *, CpMem > activityHit
Kokkos::View< float, CpMem > maxApproach
Kokkos::View< float *, CpMem > prevRestBank
Kokkos::View< float *, CpMem > restVPeak
Kokkos::View< float *, CpMem > restBank
Kokkos::View< std::uint64_t *, CpMem > bodyColorMask
Kokkos::View< int *, CpMem > posCommitPerm
Kokkos::View< int *, CpMem > velOffsDev
Kokkos::View< int *, CpMem > mlOffsDev
Kokkos::View< unsigned long long *, CpMem > pairKeys
struct peclet::dem::Particles::MlLast mlLast
Kokkos::View< int *, CpMem > aVel
Kokkos::View< float, CpMem > maxConsensus
Kokkos::View< float *, CpMem > bodyOrphan
Kokkos::View< unsigned long long *, CpMem > prevPairKeys
Kokkos::View< int *, CpMem > posPerm
Kokkos::View< float *, CpMem > prevPosImpulse
Kokkos::View< int *, CpMem > contactColor
Kokkos::View< float *[3], CpMem > mlVelG
Kokkos::View< float *, CpMem > invMassSolve
Kokkos::View< float *[3], CpMem > mlVelG0
Kokkos::View< int *, CpMem > mlBucketPerm
Kokkos::View< float *[3], CpMem > invInertiaCoarse
Kokkos::View< int *, CpMem > mlParent
Kokkos::View< float *[3], CpMem > lambdaT
Kokkos::View< float *[3], CpMem > prevLambdaT
Kokkos::View< int *, CpMem > unitColor
Kokkos::View< int *, CpMem > kPos
Kokkos::View< float, CpMem > maxOverlap
Kokkos::View< float *, CpMem > posLambdaContact
Kokkos::View< unsigned long long *, CpMem > velMask
Kokkos::View< float *, CpMem > posImpulse
Kokkos::View< int *, CpMem > contactSlot
Kokkos::View< float *, CpMem > mlInvMassG
Kokkos::View< int *, CpMem > heightLevel
Kokkos::View< long long *, CpMem > bodyWinner
Kokkos::View< float *, CpMem > lambdaAcc
Kokkos::View< double *[7], CpMem > mlAccD
Kokkos::View< long long *, CpMem > mlColorPacked
Kokkos::View< float *, CpMem > invMassCoarse
Kokkos::View< int *, CpMem > iterCountDev
Kokkos::View< float, CpMem > maxApproachQS
Kokkos::View< unsigned char *, CpMem > manifoldSleep
Kokkos::View< const float *, CpMem > crad() const
Kokkos::View< float *[2], CpMem > planeFriction
Definition particles.hpp:98
Kokkos::View< float *, CpMem > prevRestVPeak
Kokkos::View< int *, CpMem > mlMate
Kokkos::View< ContactC *, CpMem > contacts
Kokkos::View< int, CpMem > pairCount
Kokkos::View< unsigned char *, CpMem > groundedLevel
Kokkos::View< int *, CpMem > unitContacts
Kokkos::View< int *, CpMem > manifoldColor
Kokkos::View< float *[6], CpMem > mlInvIG
One solve phase's body copies (docs/contact_solve_framework.md §4.4, §4.5, §WO-4 item 2): the hub cop...
Definition particles.hpp:44
Kokkos::View< int *, CpMem > slotB
Definition particles.hpp:47
Kokkos::View< int *, CpMem > slotA
Definition particles.hpp:47
The position units of a contact list as a CSR (§4.3): unit u owns the contacts list(start(u)) .
Rank-level M (docs/contact_solve_framework.md §13.3) as the hooks expose it to the driver: whether th...
Per-edge slot overrides of a phase's hub copies (docs/contact_solve_framework.md §4....
Single-GPU hooks: no ghost refresh, residuals are already global. Everything inlines away.
float allMaxAny(float v, bool &) const
static constexpr bool distributed
void beginSolve(Particles &) const
void restoreOrphanBalance(Particles &) const
void publishPositions(Particles &) const
void syncContactCounts(Particles &) const
int visibleManifolds(int nm) const
float allMax(float v) const
void openPositionCounts(Particles &, bool) const
void syncVelocities(Particles &) const
void syncFrictionCounts(Particles &) const
void syncPositions(Particles &) const
void openVelocityPhase(Particles &, bool) const
diagnostics.split_stats() (docs/contact_solve_framework.md §WO-4 item 4, §13.5 WO-4b): the last subst...
Definition particles.hpp:66
long long unfiredSplitContacts
Definition particles.hpp:68
Rank-level X evaluated inline in the one-shot sweep (only for the manifolds of the colour being swept...