peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_velocity.hpp
Go to the documentation of this file.
1
9#ifndef DEM_SOLVER_VELOCITY_HPP
10#define DEM_SOLVER_VELOCITY_HPP
11
12#include <climits>
13#include <Kokkos_Core.hpp>
14#include <tuple>
15#include <utility>
16#include <vector>
17
18#include "contact_preprocessing.hpp" // ManifoldC, CpExec/CpMem
19#include "dem_portable.hpp"
20#include "solver_fused.hpp"
21
22namespace peclet::dem {
23
24namespace detail {
25KOKKOS_INLINE_FUNCTION F3 ld3(Kokkos::View<const float* [3], CpMem> v, int i) {
26 return F3{v(i, 0), v(i, 1), v(i, 2)};
27}
28// v^T I_world^-1 v with I_world^-1 = R I_local^-1 R^T -> (R^T v) diag(invI_local) (R^T v).
29KOKKOS_INLINE_FUNCTION float genInvMass(F3 tau, F3 invIlocal, F4 q) {
30 // Isotropic inertia (spheres): the principal frame is irrelevant -- skip both rotations.
31 if (invIlocal.x == invIlocal.y && invIlocal.y == invIlocal.z)
32 return dot3(tau, tau) * invIlocal.x;
33 const F3 t = invRotateVector(q, tau);
34 return t.x * t.x * invIlocal.x + t.y * t.y * invIlocal.y + t.z * t.z * invIlocal.z;
35}
36} // namespace detail
37
46inline void countVelocityJacobiKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
47 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
48 Kokkos::View<int*, CpMem> counts, bool dedupTwins = true) {
49 CpExec space;
50 Kokkos::parallel_for(
51 "peclet::dem::count_velocity_jacobi", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
52 KOKKOS_LAMBDA(int idx) {
53 const ManifoldC m = manifolds(idx);
54 if (m.num_points <= 0)
55 return;
56 const int realA = realIdx(m.bodyA);
57 if (m.bodyB >= 0) {
58 const int realB = realIdx(m.bodyB);
59 if (dedupTwins && realA > realB)
60 return; // periodic dedup, as in solveVelocityKokkos
61 Kokkos::atomic_add(&counts(realB), 1);
62 }
63 Kokkos::atomic_add(&counts(realA), 1);
64 });
65 space.fence();
66}
67
77 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
78 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
79 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<const float* [3], CpMem> velPred,
80 Kokkos::View<const float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
81 float growthRate, float restitutionNormal, float restVelThreshold,
82 Kokkos::View<float* [3], CpMem> deltaVel, Kokkos::View<float* [3], CpMem> deltaAngVel,
83 Kokkos::View<int*, CpMem> velCounts, Kokkos::View<const int*, CpMem> onlyColor = {},
84 int colorFilter = 0, Kokkos::View<const unsigned char*, CpMem> persistent = {},
85 Kokkos::View<const float* [3], CpMem> posPred = {}, F3 gHat = {},
86 Kokkos::View<const unsigned char*, CpMem> grounded = {}, bool massSplit = false,
87 bool dedupTwins = true) {
89 using detail::ld3;
90 CpExec space;
91 const bool filt = onlyColor.extent(0) > 0;
92 const bool usePersist = persistent.extent(0) > 0;
93 Kokkos::parallel_for(
94 "peclet::dem::solve_velocity", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
95 KOKKOS_LAMBDA(int idx) {
96 if (filt && onlyColor(idx) != colorFilter)
97 return; // colour filter (kernel unit test only; no production caller)
98 const ManifoldC m = manifolds(idx);
99 if (m.num_points <= 0)
100 return;
101
102 const int idA = m.bodyA, idB = m.bodyB;
103 const int realA = realIdx(idA);
104 int realB = idB;
105 if (idB >= 0) {
106 realB = realIdx(idB);
107 if (dedupTwins && realA > realB)
108 return; // periodic dedup
109 }
110
111 const float invMassA = invMass(realA);
112 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
113 const F3 invIA = ld3(invInertia, realA);
114 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
115 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
116 const F4 qB = (idB >= 0)
117 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
118 : F4{0, 0, 0, 1};
119
120 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
121 F3 vB{0, 0, 0}, wB{0, 0, 0};
122 if (idB >= 0) {
123 vB = ld3(velPred, realB);
124 wB = ld3(angVelPred, realB);
125 }
126
127 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
128 const F3 TauA{m.torque_armA_sum.x, m.torque_armA_sum.y, m.torque_armA_sum.z};
129 const F3 TauB{m.torque_armB_sum.x, m.torque_armB_sum.y, m.torque_armB_sum.z};
130
131 const float invN = 1.0f / static_cast<float>(m.num_points);
132
133 // Moving-wall boundary (idB<0): the static "body B" carries the wall's surface velocity
134 // (count-averaged over the contact patch) so restitution is against the wall's motion, and
135 // its own restitution (a < 0 average keeps the global material — planes, body-body).
136 float restitution = restitutionNormal;
137 if (idB < 0)
138 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
139 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
140 const float ra = m.restitution_sum * invN;
141 if (ra >= 0.0f)
142 restitution = ra;
143 }
144 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
145 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
146
147 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
148 if (lenN < 1e-9f)
149 return;
150
151 // Separation vector for the approaching-sign gate + growth velocity. For a BOUNDARY
152 // (idB<0), rBavg is the ABSOLUTE wall contact point (kept absolute for the position solve's
153 // plane linearisation), NOT a body-relative lever — so rAavg - rBavg would depend on where
154 // the contact sits in world space, flipping `alignment` (and thus the approaching test)
155 // around a curved wall / a wall far from the origin and injecting energy (grains "jump" on
156 // the way down a rotating drum). The grain's own contact lever rAavg is the meaningful
157 // relative vector (dot(Nsum, rAavg) = radius > 0, a consistent convention). Body-body keeps
158 // rAavg-rBavg.
159 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
160 const F3 vGrowth = scale3(diffCenters, growthRate);
161
162 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
163 dot3(wB, TauB);
164 vn += dot3(vGrowth, Nsum);
165
166 const float alignment = dot3(Nsum, diffCenters);
167 if (alignment > 0.0f) {
168 if (vn < 0.0f)
169 return; // sphere convention: approaching if vn>0
170 } else {
171 if (vn > 0.0f)
172 return; // inverted convention
173 }
174
175 const float Nsq = dot3(Nsum, Nsum);
176 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
177 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
178 // Shock propagation (Guendelman et al. 2003) for persistent LOADED body-body contacts: an
179 // inelastic pairwise solve conserves momentum, so a deep column merely homogenises its fall
180 // (the floor drains one layer per sweep and the pile never cools -- the phantom-fall
181 // state). Treating the LOWER body of a loaded contact as static drains the column's
182 // momentum through the support chain into the ground: the upper body is corrected, the
183 // lower keeps its (already supported) velocity. Near-horizontal pairs stay symmetric; new
184 // contacts and g = 0 runs are untouched (momentum-conserving impacts with material
185 // restitution).
186 // ... but ONLY when the lower body is not moving UPWARD: correcting the upper body against
187 // a static or FALLING support strictly removes momentum (monotone drainage into the
188 // ground), while one-sidedness against a RISING support copies its bounce velocity up the
189 // chain with no mass penalty and fountains the whole column. Rising supports (floor
190 // bounces, bubble eruptions) therefore keep the symmetric momentum-conserving impulse.
191 bool applyA = true, applyB = true;
192 if (usePersist && persistent(idx) != 0 && idB >= 0) {
193 const F3 dx = sub3(ldF3(posPred, idA), ldF3(posPred, idB)); // ghost-aware pair geometry
194 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
195 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
196 const float riseThr =
197 4.0f * restVelThreshold; // rise = -v.gHat (gHat points down-gravity)
198 // ... and the support must be GROUNDED (contact path to the floor): a gas-borne emulsion
199 // or lifted slug keeps symmetric momentum-conserving impulses, so its weight stays on
200 // the gas -- only genuinely supported chains drain into the ground.
201 if (up > thr && -dot3(vB, gHat) <= riseThr && grounded(realB) > 0) {
202 wB_n = 0.0f;
203 applyB = false;
204 restitution = 0.0f; // shock pass is inelastic: e > 0 one-sided would bounce bodies
205 // off the ground with unpaid momentum
206 } else if (up < -thr && -dot3(vA, gHat) <= riseThr && grounded(realA) > 0) {
207 wA_n = 0.0f;
208 applyA = false;
209 restitution = 0.0f;
210 }
211 }
212 const float wTotal = wA_n + wB_n;
213 if (wTotal <= 0.0f)
214 return;
215 // Mass-split Jacobi: the same numerator against copies of mass m / n.
216 const float wSolve =
217 massSplit ? static_cast<float>(velCounts(realA)) * wA_n +
218 ((idB >= 0) ? static_cast<float>(velCounts(realB)) * wB_n : 0.0f)
219 : wTotal;
220
221 // Resting-contact regularization (the standard PBD/XPBD restitution threshold): bounce only
222 // when the physical approach speed |vn|/|Nsum| exceeds ~2 g dt (what one substep of free
223 // fall gains). Below it the contact is RESTING — its vn is integration noise, and bouncing
224 // it every substep across a dense pile's contact chains (impulses Jacobi-SUMMED per body,
225 // no mass splitting) pumps energy without bound: a settled 180k glass-bead bed switched to
226 // e=0.8 reached |v| ~ 3e6 cells/s within 15 substeps. With e=0 the impulse still cancels
227 // the approach velocity — exactly the quasi-static dissipation a resting pile needs.
228 if (Kokkos::fabs(vn) < restVelThreshold * lenN)
229 restitution = 0.0f;
230
231 const float lambda = (-restitution * vn - vn) / wSolve;
232
233 const F3 Jlin = scale3(Nsum, lambda);
234 const F3 JangA = scale3(TauA, lambda);
235 const F3 JangB = scale3(TauB, lambda);
236
237 // Linear delta on A.
238 if (applyA) {
239 Kokkos::atomic_add(&deltaVel(realA, 0), Jlin.x * invMassA);
240 Kokkos::atomic_add(&deltaVel(realA, 1), Jlin.y * invMassA);
241 Kokkos::atomic_add(&deltaVel(realA, 2), Jlin.z * invMassA);
242 // Angular delta on A: dw_world = R (invI_local * (R^T Jang)).
243 {
244 const F3 Jl = invRotateVector(qA, JangA);
245 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
246 const F3 dww = rotateVector(qA, dwl);
247 Kokkos::atomic_add(&deltaAngVel(realA, 0), dww.x);
248 Kokkos::atomic_add(&deltaAngVel(realA, 1), dww.y);
249 Kokkos::atomic_add(&deltaAngVel(realA, 2), dww.z);
250 }
251 }
252 if (idB >= 0 && applyB) {
253 Kokkos::atomic_add(&deltaVel(realB, 0), -Jlin.x * invMassB);
254 Kokkos::atomic_add(&deltaVel(realB, 1), -Jlin.y * invMassB);
255 Kokkos::atomic_add(&deltaVel(realB, 2), -Jlin.z * invMassB);
256 const F3 Jl = invRotateVector(qB, JangB);
257 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
258 const F3 dww = rotateVector(qB, dwl);
259 Kokkos::atomic_add(&deltaAngVel(realB, 0), dww.x);
260 Kokkos::atomic_add(&deltaAngVel(realB, 1), dww.y);
261 Kokkos::atomic_add(&deltaAngVel(realB, 2), dww.z);
262 if (!massSplit)
263 Kokkos::atomic_add(&velCounts(realB), 1);
264 }
265 if (applyA && !massSplit)
266 Kokkos::atomic_add(&velCounts(realA), 1);
267 });
268 space.fence();
269}
270
278template <class V3, class Vi>
279inline void applyVelocityDeltasAveragedKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel,
280 V3 deltaAngVel, Vi velCounts, bool averaged = true) {
281 CpExec space;
282 Kokkos::parallel_for(
283 "peclet::dem::apply_vel_avg", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
284 const int count = velCounts(i);
285 if (count <= 0)
286 return;
287 // Over-relaxed average: omega=2 halves the convergence loss of plain 1/count averaging
288 // (the crush-dissipation rate) while staying far below the raw-sum overshoot (omega=count)
289 // that detonates a resting pile at e=0.8. count==1 (binary collision) stays exact.
290 const float f = averaged ? Kokkos::fmin(1.0f, 2.0f / static_cast<float>(count)) : 1.0f;
291 for (int c = 0; c < 3; ++c) {
292 velPred(i, c) += deltaVel(i, c) * f;
293 angVelPred(i, c) += deltaAngVel(i, c) * f;
294 deltaVel(i, c) = 0.0f;
295 deltaAngVel(i, c) = 0.0f;
296 }
297 velCounts(i) = 0;
298 });
299 space.fence();
300}
301
302// ============================ colored Gauss–Seidel velocity solve ============================
303// The Jacobi solve above sums every touching manifold's impulse onto a body, then relaxes the sum
304// by the contact count to stay stable — a stable but UNDER-converged approximation that
305// under-dissipates in dense multi-contact regions (effective restitution rises above the prescribed
306// e). The colored Gauss–Seidel path removes that approximation: graph-colour the manifolds so no
307// two sharing a real body share a colour, then sweep colour-by-colour applying each impulse IN
308// PLACE (read the current velocity, apply, write) — a body sees the updates of every
309// previously-solved contact in the same sweep. Within a colour the manifolds are an independent set
310// (no shared body), so the in-place read-modify-write is race-free WITHOUT atomics or averaging,
311// and the fixed-point is the true coupled multi-contact solution, so the dissipation is correct by
312// construction. count==1 (a binary collision) is identical to the Jacobi path; the difference is
313// confined to dense clusters.
314
323inline int colorManifoldsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
324 Kokkos::View<const int*, CpMem> realIdx, int numReal,
325 Kokkos::View<int*, CpMem> mColor,
326 Kokkos::View<long long*, CpMem> bodyWinner,
327 Kokkos::View<std::uint64_t*, CpMem> bodyMask, int& leftover,
328 Kokkos::View<const unsigned char*, CpMem> sleepMask = {},
329 SlotOverride ov = {}, bool dedupTwins = true, int vertexSpan = 0) {
330 leftover = 0;
331 CpExec space;
332 if (numManifolds <= 0 || numReal <= 0)
333 return 0;
334 // Colouring vertices: realIdx(body), or a hub copy slot through `ov` (§4.4); vertexSpan covers
335 // the copy slots (0 = numReal, the no-copy span).
336 const int nV = vertexSpan > 0 ? vertexSpan : numReal;
337 const bool sleepOn = sleepMask.extent(0) > 0;
338 Kokkos::parallel_for(
339 "peclet::dem::color_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, nV),
340 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
341 // -2 inactive (skip forever), -1 uncoloured, >=0 committed colour.
342 Kokkos::parallel_for(
343 "peclet::dem::color_init_manifolds", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
344 KOKKOS_LAMBDA(int idx) {
345 const ManifoldC m = manifolds(idx);
346 if (m.num_points <= 0 || (sleepOn && sleepMask(idx))) {
347 mColor(idx) = -2; // inactive or a frozen (both-asleep) island manifold
348 return;
349 }
350 // Periodic twin dedup (§4.2 item 2): single-rank only; under MPI ownership already
351 // excludes the non-owned twin (dedupTwins = !Hooks::distributed).
352 if (dedupTwins && m.bodyB >= 0 && realIdx(m.bodyA) > realIdx(m.bodyB)) {
353 mColor(idx) = -2; // periodic dedup: the (realA<=realB) twin carries this contact
354 return;
355 }
356 mColor(idx) = -1;
357 });
358
359 int remaining = 1, prevRemaining = -1;
360 // Round cap (docs/contact_solve_framework.md §12 S7): the vertices coloured are MANIFOLDS, so
361 // the bound is their count + 2. Every round commits at least the globally highest-key
362 // uncoloured manifold (keys are unique: colorKey carries the index in its low word), so the
363 // loop terminates within numManifolds rounds; O(log n) in practice.
364 const int maxRounds = numManifolds + 2;
365 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
366 Kokkos::parallel_for(
367 "peclet::dem::color_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, nV),
368 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
369 Kokkos::parallel_for(
370 "peclet::dem::color_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
371 KOKKOS_LAMBDA(int idx) {
372 if (mColor(idx) != -1)
373 return;
374 const ManifoldC m = manifolds(idx);
375 const long long key = colorKey(idx);
376 Kokkos::atomic_max(&bodyWinner(ov.slotA(idx, realIdx(m.bodyA))), key);
377 if (m.bodyB >= 0)
378 Kokkos::atomic_max(&bodyWinner(ov.slotB(idx, realIdx(m.bodyB))), key);
379 });
380 int rem = 0;
381 Kokkos::parallel_reduce(
382 "peclet::dem::color_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
383 KOKKOS_LAMBDA(int idx, int& acc) {
384 if (mColor(idx) != -1)
385 return;
386 const ManifoldC m = manifolds(idx);
387 const int ea = ov.slotA(idx, realIdx(m.bodyA));
388 const int eb = (m.bodyB >= 0) ? ov.slotB(idx, realIdx(m.bodyB)) : -1;
389 // Winner iff it holds BOTH its endpoints -> no uncoloured conflict, sole writer of ea/eb.
390 const long long key = colorKey(idx);
391 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
392 acc += 1;
393 return;
394 }
395 std::uint64_t forbidden = bodyMask(ea);
396 if (eb >= 0)
397 forbidden |= bodyMask(eb);
398 int c = 0; // lowest free colour of 64; never forced (§4.2 item 1)
399 while (c < kColorPalette && ((forbidden >> c) & 1))
400 ++c;
401#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 3
402 if (c > 62)
403 c = 62; // G13 mutant 3: the old forced colour 62
404#endif
405 if (c == kColorPalette) {
406 mColor(idx) = kColorUncolourable; // no free colour: neither coloured nor remaining
407 return;
408 }
409 mColor(idx) = c;
410 const std::uint64_t bit = std::uint64_t(1) << c;
411 bodyMask(ea) |= bit;
412 if (eb >= 0)
413 bodyMask(eb) |= bit;
414 },
415 rem);
416 space.fence();
417 if (rem == prevRemaining)
418 break; // safety bound only: every round commits (or marks -3) the highest-key contender
419 prevRemaining = rem;
420 remaining = rem;
421 }
422
423 int maxc = -1;
424 Kokkos::parallel_reduce(
425 "peclet::dem::color_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
426 KOKKOS_LAMBDA(int idx, int& mx) {
427 if (mColor(idx) > mx)
428 mx = mColor(idx);
429 },
430 Kokkos::Max<int>(maxc));
431 Kokkos::parallel_reduce(
432 "peclet::dem::color_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
433 KOKKOS_LAMBDA(int idx, int& acc) {
434 if (mColor(idx) == -1 || mColor(idx) == kColorUncolourable)
435 acc += 1;
436 },
437 leftover);
438 space.fence();
439 return maxc + 1;
440}
441
459 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
460 Kokkos::View<const int*, CpMem> realIdx, int numReal,
461 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
462 Kokkos::View<const int*, CpMem> prevColor, int prevCount, Kokkos::View<int*, CpMem> mColor,
463 Kokkos::View<long long*, CpMem> bodyWinner, Kokkos::View<std::uint64_t*, CpMem> bodyMask,
464 int& leftover, bool forceFull, Kokkos::View<const unsigned char*, CpMem> sleepMask = {},
465 bool dedupTwins = true) {
466 leftover = 0;
467 CpExec space;
468 if (numManifolds <= 0 || numReal <= 0)
469 return 0;
470 Kokkos::parallel_for(
471 "peclet::dem::icolor_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
472 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
473 const bool full = forceFull || prevCount <= 0;
474 const bool sleepOn = sleepMask.extent(0) > 0;
475 // Seed each manifold's colour: -2 inactive/dedup/both-asleep, else the carried colour (matched by
476 // pair key) or -1 (new / full recolour).
477 Kokkos::parallel_for(
478 "peclet::dem::icolor_seed", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
479 KOKKOS_LAMBDA(int idx) {
480 const ManifoldC m = manifolds(idx);
481 if (m.num_points <= 0 || (sleepOn && sleepMask(idx))) {
482 mColor(idx) = -2; // inactive or a frozen (both-asleep) island manifold
483 return;
484 }
485 if (dedupTwins && m.bodyB >= 0 && realIdx(m.bodyA) > realIdx(m.bodyB)) {
486 mColor(idx) = -2; // periodic dedup twin
487 return;
488 }
489 int c = -1;
490 if (!full) {
491 const unsigned long long k = pairKeyOf(m, realIdx);
492 int lo = 0, hi = prevCount;
493 while (lo < hi) {
494 const int mid = (lo + hi) >> 1;
495 if (prevKeys(mid) < k)
496 lo = mid + 1;
497 else
498 hi = mid;
499 }
500 if (lo < prevCount && prevKeys(lo) == k) {
501 const int pc = prevColor(lo);
502 if (pc >= 0)
503 c = pc; // carry (leftover -1 last step -> re-arbitrate as new)
504 }
505 }
506 mColor(idx) = c;
507 });
508 // Seed the per-body masks from the carried colours (survivors are conflict-free on single-GPU).
509 if (!full)
510 Kokkos::parallel_for(
511 "peclet::dem::icolor_seed_mask", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
512 KOKKOS_LAMBDA(int idx) {
513 const int c = mColor(idx);
514 if (c < 0)
515 return;
516 const ManifoldC m = manifolds(idx);
517 const std::uint64_t bit = std::uint64_t(1) << c;
518 Kokkos::atomic_or(&bodyMask(realIdx(m.bodyA)), bit);
519 if (m.bodyB >= 0)
520 Kokkos::atomic_or(&bodyMask(realIdx(m.bodyB)), bit);
521 });
522 // Jones-Plassmann arbitration over the uncoloured (-1) set only — identical body of work to
523 // colorManifoldsKokkos, but the frozen carried colours restrict it to the few new manifolds.
524 int remaining = 1, prevRemaining = -1;
525 const int maxRounds = numManifolds + 2; // §12 S7: bounded by the vertices coloured
526 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
527 Kokkos::parallel_for(
528 "peclet::dem::icolor_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
529 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
530 Kokkos::parallel_for(
531 "peclet::dem::icolor_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
532 KOKKOS_LAMBDA(int idx) {
533 if (mColor(idx) != -1)
534 return;
535 const ManifoldC m = manifolds(idx);
536 const long long key = colorKey(idx);
537 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyA)), key);
538 if (m.bodyB >= 0)
539 Kokkos::atomic_max(&bodyWinner(realIdx(m.bodyB)), key);
540 });
541 int rem = 0;
542 Kokkos::parallel_reduce(
543 "peclet::dem::icolor_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
544 KOKKOS_LAMBDA(int idx, int& acc) {
545 if (mColor(idx) != -1)
546 return;
547 const ManifoldC m = manifolds(idx);
548 const int ea = realIdx(m.bodyA);
549 const int eb = (m.bodyB >= 0) ? realIdx(m.bodyB) : -1;
550 const long long key = colorKey(idx);
551 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
552 acc += 1;
553 return;
554 }
555 std::uint64_t forbidden = bodyMask(ea);
556 if (eb >= 0)
557 forbidden |= bodyMask(eb);
558 int c = 0; // lowest free colour of 64; never forced (§4.2 item 1)
559 while (c < kColorPalette && ((forbidden >> c) & 1))
560 ++c;
561#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 3
562 if (c > 62)
563 c = 62; // G13 mutant 3: the old forced colour 62
564#endif
565 if (c == kColorPalette) {
566 mColor(idx) = kColorUncolourable;
567 return;
568 }
569 mColor(idx) = c;
570 const std::uint64_t bit = std::uint64_t(1) << c;
571 bodyMask(ea) |= bit;
572 if (eb >= 0)
573 bodyMask(eb) |= bit;
574 },
575 rem);
576 space.fence();
577 if (rem == prevRemaining)
578 break; // safety bound only (see colorManifoldsKokkos)
579 prevRemaining = rem;
580 remaining = rem;
581 }
582 int maxc = -1;
583 Kokkos::parallel_reduce(
584 "peclet::dem::icolor_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
585 KOKKOS_LAMBDA(int idx, int& mx) {
586 if (mColor(idx) > mx)
587 mx = mColor(idx);
588 },
589 Kokkos::Max<int>(maxc));
590 Kokkos::parallel_reduce(
591 "peclet::dem::icolor_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
592 KOKKOS_LAMBDA(int idx, int& acc) {
593 if (mColor(idx) == -1 || mColor(idx) == kColorUncolourable)
594 acc += 1;
595 },
596 leftover);
597 space.fence();
598 return maxc + 1;
599}
600
612
615inline void computeVn0Kokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
616 Kokkos::View<const float* [3], CpMem> velPred,
617 Kokkos::View<const float* [3], CpMem> angVelPred,
618 Kokkos::View<const int*, CpMem> realIdx, float growthRate,
619 Kokkos::View<float*, CpMem> vn0, Kokkos::View<float* [3], CpMem> vt0,
620 bool dedupTwins = true) {
621 using detail::ld3;
622 CpExec space;
623 Kokkos::parallel_for(
624 "peclet::dem::pgs_vn0", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
625 KOKKOS_LAMBDA(int idx) {
626 const ManifoldC m = manifolds(idx);
627 if (m.num_points <= 0)
628 return;
629 const int idA = m.bodyA, idB = m.bodyB;
630 const int realA = realIdx(idA);
631 if (dedupTwins && idB >= 0 && realA > realIdx(idB))
632 return; // periodic dedup
633 const float invN = 1.0f / static_cast<float>(m.num_points);
634 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
637 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
638 F3 vB{0, 0, 0}, wB{0, 0, 0};
639 if (idB >= 0) {
640 vB = ld3(velPred, realIdx(idB));
641 wB = ld3(angVelPred, realIdx(idB));
642 } else {
643 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
644 }
645 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
646 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
647 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
648 const F3 vGrowth = scale3(diffCenters, growthRate);
649 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
650 dot3(wB, TauB);
651 vn += dot3(vGrowth, Nsum);
652 vn0(idx) = vn;
653 // Pre-solve tangential surface velocity at the averaged contact point (physical units):
654 // the reference for the tangential-restitution target -beta * vt0 (Walton impact law).
655 {
656 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
657 F3 vt{0, 0, 0};
658 if (lenN > 1e-9f) {
659 const F3 nhat = scale3(Nsum, 1.0f / lenN);
660 const int realB2 = (idB >= 0) ? realIdx(idB) : -1;
661 const F3 wAv = ld3(angVelPred, realA);
662 const F3 wBv = (realB2 >= 0) ? ld3(angVelPred, realB2) : F3{0, 0, 0};
663 const F3 vrel = sub3(add3(vA, cross3v(wAv, rAavg)), add3(vB, cross3v(wBv, rBavg)));
664 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
665 }
666 vt0(idx, 0) = vt.x;
667 vt0(idx, 1) = vt.y;
668 vt0(idx, 2) = vt.z;
669 }
670 });
671}
672
686 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
687 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
688 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<const float* [3], CpMem> velPred,
689 Kokkos::View<const float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
690 float growthRate, float restitutionNormal, float restVelThreshold,
691 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const float*, CpMem> lambdaAcc,
692 Kokkos::View<const float*, CpMem> restRel, Kokkos::View<float*, CpMem> restBank,
693 Kokkos::View<float*, CpMem> restVPeak, bool dedupTwins = true) {
694 using detail::genInvMass;
695 using detail::ld3;
696 CpExec space;
697 Kokkos::parallel_for(
698 "peclet::dem::rest_bank_update", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
699 KOKKOS_LAMBDA(int idx) {
700 const ManifoldC m = manifolds(idx);
701 if (m.num_points <= 0)
702 return;
703 const int idA = m.bodyA, idB = m.bodyB;
704 const int realA = realIdx(idA);
705 const int realB = (idB >= 0) ? realIdx(idB) : idB;
706 if (dedupTwins && idB >= 0 && realA > realB)
707 return; // periodic dedup: the canonical twin owns the bank
708 const float invN = 1.0f / static_cast<float>(m.num_points);
709 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
710 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
711 if (lenN < 1e-9f)
712 return;
715 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
716 F3 vB{0, 0, 0}, wB{0, 0, 0};
717 if (idB >= 0) {
718 vB = ld3(velPred, realB);
719 wB = ld3(angVelPred, realB);
720 } else {
721 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
722 }
723 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
724 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
725 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
726 const F3 vGrowth = scale3(diffCenters, growthRate);
727 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
728 dot3(wB, TauB);
729 vn += dot3(vGrowth, Nsum);
730 const float alignment = dot3(Nsum, diffCenters);
731 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
732 // Symmetric effective inverse mass (the release/bank ledger is momentum bookkeeping at
733 // the pair's own masses, independent of any stabilization sidedness this substep).
734 const F3 invIA = ld3(invInertia, realA);
735 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
736 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
737 const F4 qB = (idB >= 0)
738 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
739 : F4{0, 0, 0, 1};
740 const float Nsq = dot3(Nsum, Nsum);
741 const float wTotal = Nsq * invMass(realA) + genInvMass(TauA, invIA, qA) +
742 Nsq * ((idB >= 0) ? invMass(realB) : 0.0f) +
743 genInvMass(TauB, invIB, qB);
744 if (wTotal <= 0.0f)
745 return;
746 float e = restitutionNormal;
747 {
748 const float ra = m.restitution_sum * invN;
749 if (ra >= 0.0f)
750 e = ra;
751 }
752 const float v0til = sgn * vn0(idx);
753 const float vtilEnd = sgn * vn;
754 float owed = restBank(idx);
755 // Event state: refresh the peak on a kinetic approach, age it 1/256 per substep.
756 float vPeak = restVPeak(idx) * (1.0f - 1.0f / 256.0f);
757 if (v0til > restVelThreshold * lenN)
758 vPeak = Kokkos::fmax(vPeak, v0til / lenN);
759 if (vPeak > restVelThreshold) {
760 // Active event: bank e x this substep's compression FLUX (full applied normal impulse
761 // minus the reflection share — the momentum the chain actually transmitted; the
762 // m_eff-scale approach-destruction measure under-banks a chain-loaded impact by ~3000x),
763 // pay down the separation already delivered, and deduct the release channel's spend.
764 // A clean one-substep binary impact nets 0: pTot = (1+e) m_eff v0, pR = e m_eff v0.
765 const float pR = Kokkos::fmax(0.0f, -vtilEnd) / wTotal * lenN;
766 const float pTot = Kokkos::fmax(lambdaAcc(idx), 0.0f) * lenN;
767 const float pC = Kokkos::fmax(0.0f, pTot - pR);
768 owed += e * pC - pR - Kokkos::fmax(restRel(idx), 0.0f) * lenN;
769 restBank(idx) = Kokkos::fmax(owed, 0.0f);
770 restVPeak(idx) = vPeak;
771 } else {
772 restBank(idx) = 0.0f; // event aged out (or none): the residual budget evaporates
773 restVPeak(idx) = 0.0f;
774 }
775 });
776}
777
784inline void decayBodyOrphanKokkos(Kokkos::View<float*, CpMem> orphan,
785 Kokkos::View<float*, CpMem> orphanVPeak, int numOwned,
786 float restVelThreshold) {
787 CpExec space;
788 Kokkos::parallel_for(
789 "peclet::dem::rest_orphan_decay", Kokkos::RangePolicy<CpExec>(space, 0, numOwned),
790 KOKKOS_LAMBDA(int i) {
791 const float decayed = orphanVPeak(i) * (1.0f - 1.0f / 64.0f);
792 if (decayed <= restVelThreshold || orphan(i) <= 0.0f) {
793 orphan(i) = 0.0f;
794 orphanVPeak(i) = 0.0f;
795 } else {
796 orphan(i) *= (1.0f - 1.0f / 64.0f);
797 orphanVPeak(i) = decayed;
798 }
799 });
800}
801
815 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
816 Kokkos::View<const float*, CpMem> prevRestBank, Kokkos::View<const float*, CpMem> prevRestVPeak,
817 Kokkos::View<const unsigned char*, CpMem> matched, int prevCount,
818 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<float*, CpMem> orphan,
819 Kokkos::View<float*, CpMem> orphanVPeak, Kokkos::View<const int*, CpMem> gidSorted = {},
820 Kokkos::View<const int*, CpMem> slotSorted = {}, int ownedLimit = -1) {
821 // ownedLimit >= 0 (the distributed step: numReal): credit a dead entry only on the rank that
822 // owns its LOWER-gid endpoint -- with symmetric visibility (§5.1) exactly the pair's owner, so
823 // an entry carried on both endpoints' ranks (migration pack) is credited once
824 // (docs/contact_solve_framework.md §6.3). -1: no restriction (single rank, byte-identical).
825 CpExec space;
826 const int nMap = static_cast<int>(gidSorted.extent(0));
827 const int nBody = static_cast<int>(orphan.extent(0));
828 Kokkos::parallel_for(
829 "peclet::dem::rest_orphan_scatter", Kokkos::RangePolicy<CpExec>(space, 0, prevCount),
830 KOKKOS_LAMBDA(int e) {
831 if (matched(e))
832 return;
833 const float owed = prevRestBank(e);
834 if (owed <= 0.0f)
835 return;
836 const unsigned long long k = prevKeys(e);
837 if (k == ~0ull)
838 return;
839 const unsigned hi = static_cast<unsigned>(k >> 32);
840 const unsigned lo = static_cast<unsigned>(k & 0xFFFFFFFFu);
841 auto resolve = [&](unsigned id) -> int {
842 if (nMap == 0) // single-GPU: identities ARE real slots
843 return (static_cast<int>(id) < nBody) ? static_cast<int>(id) : -1;
844 int a = 0, b = nMap; // MPI: binary-search the sorted gid -> slot map
845 while (a < b) {
846 const int m = (a + b) >> 1;
847 if (gidSorted(m) < static_cast<int>(id))
848 a = m + 1;
849 else
850 b = m;
851 }
852 return (a < nMap && gidSorted(a) == static_cast<int>(id)) ? slotSorted(a) : -1;
853 };
854 const int sA = resolve(hi);
855 if (ownedLimit >= 0 && !(sA >= 0 && sA < ownedLimit))
856 return; // not the pair's owner: a stale copy is never credited
857 const int sB = (lo != 0xFFFFFFFFu) ? resolve(lo) : -1;
858 const float vpk = prevRestVPeak(e);
859 float shareA = 1.0f; // boundary (wall) pair: everything to the particle
860 if (lo != 0xFFFFFFFFu) {
861 if (sB >= 0 && sA >= 0) {
862 const float wA = invMass(sA), wB = invMass(sB);
863 if (wA + wB > 0.0f)
864 shareA = wB / (wA + wB); // heavier endpoint (smaller invMass) keeps more
865 } else {
866 shareA = 0.5f; // body-body with an unresolvable endpoint (MPI edge): conservative half
867 }
868 }
869 if (sA >= 0 && shareA > 0.0f) {
870 Kokkos::atomic_add(&orphan(sA), owed * shareA);
871 Kokkos::atomic_max(&orphanVPeak(sA), vpk);
872 }
873 if (sB >= 0 && shareA < 1.0f) {
874 Kokkos::atomic_add(&orphan(sB), owed * (1.0f - shareA));
875 Kokkos::atomic_max(&orphanVPeak(sB), vpk);
876 }
877 });
878}
879
882inline std::tuple<double, float, int> restBankStatsKokkos(Kokkos::View<const float*, CpMem> bank,
883 int n) {
884 double s = 0.0;
885 float mx = 0.0f;
886 int cnt = 0;
887 if (n > 0) {
888 Kokkos::parallel_reduce(
889 "peclet::dem::rest_bank_stats", Kokkos::RangePolicy<CpExec>(0, n),
890 KOKKOS_LAMBDA(int i, double& ls, float& lm, int& lc) {
891 const float v = bank(i);
892 ls += v;
893 if (v > lm)
894 lm = v;
895 if (v > 0.0f)
896 ++lc;
897 },
898 s, Kokkos::Max<float>(mx), Kokkos::Sum<int>(cnt));
899 }
900 return {s, mx, cnt};
901}
902
910inline void computeSideFlagsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
911 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
912 Kokkos::View<const unsigned char*, CpMem> persistent,
913 Kokkos::View<const unsigned char*, CpMem> grounded,
914 Kokkos::View<const float* [3], CpMem> posPred,
915 Kokkos::View<const float* [3], CpMem> velPred, F3 gHat,
916 float riseThr, Kokkos::View<unsigned char*, CpMem> sideFlag,
917 Kokkos::View<const float*, CpMem> vn0, float approachThr,
918 bool dedupTwins = true) {
919 using detail::ld3;
920 CpExec space;
921 Kokkos::parallel_for(
922 "peclet::dem::pgs_side_flags", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
923 KOKKOS_LAMBDA(int idx) {
924 const unsigned char wasPersistent = persistent(idx); // read BEFORE the write: the caller
925 sideFlag(idx) = 0; // may alias persistent and sideFlag (flag reuses the persistence view)
926 const ManifoldC m = manifolds(idx);
927 if (m.num_points <= 0 || m.bodyB < 0 || wasPersistent == 0)
928 return;
929 // BALLISTIC GATE: one-sided grounding is a statics device -- a pair whose pre-solve
930 // relative normal speed exceeds the quasi-static scale (a few substeps of free fall)
931 // is shock-loaded or shearing and must stay momentum-conserving. Without this, a fast
932 // impactor meets an infinite-mass bed (measured: a 5 m/s ball stops at the surface of a
933 // 25k bed) and flowing regions over-resist (silo discharge -24%).
934 if (Kokkos::fabs(vn0(idx)) > approachThr)
935 return;
936 const int realA = realIdx(m.bodyA), realB = realIdx(m.bodyB);
937 if (dedupTwins && realA > realB)
938 return; // periodic dedup
939 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
940 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
941 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
942 if (up > thr && -dot3(ld3(velPred, realB), gHat) <= riseThr && grounded(realB) > 0) {
943 sideFlag(idx) = 1;
944 } else if (up < -thr && -dot3(ld3(velPred, realA), gHat) <= riseThr &&
945 grounded(realA) > 0) {
946 sideFlag(idx) = 2;
947 }
948 });
949}
950
952inline void warmStartApplyKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
953 Kokkos::View<const float*, CpMem> invMass,
954 Kokkos::View<const float* [3], CpMem> invInertia,
955 Kokkos::View<const float* [4], CpMem> quat,
956 Kokkos::View<float* [3], CpMem> velPred,
957 Kokkos::View<float* [3], CpMem> angVelPred,
958 Kokkos::View<const int*, CpMem> realIdx,
959 Kokkos::View<const float*, CpMem> warmP,
960 Kokkos::View<float* [3], CpMem> warmT, bool dedupTwins = true) {
961 using detail::ld3;
962 CpExec space;
963 Kokkos::parallel_for(
964 "peclet::dem::pgs_warm_apply", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
965 KOKKOS_LAMBDA(int idx) {
966 const float p = warmP(idx);
967 F3 lt{warmT(idx, 0), warmT(idx, 1), warmT(idx, 2)};
968 const bool hasT = (lt.x != 0.0f || lt.y != 0.0f || lt.z != 0.0f);
969 if (p == 0.0f && !hasT)
970 return;
971 const ManifoldC m = manifolds(idx);
972 if (m.num_points <= 0)
973 return;
974 const int idA = m.bodyA, idB = m.bodyB;
975 const int realA = realIdx(idA);
976 const int realB = (idB >= 0) ? realIdx(idB) : idB;
977 if (dedupTwins && idB >= 0 && realA > realB)
978 return; // periodic dedup (warmP is 0 for dups anyway)
979 const float invN = 1.0f / static_cast<float>(m.num_points);
980 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
983 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
984 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
985 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
986 const float alignment = dot3(Nsum, diffCenters);
987 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
988 const float lambda = -sgn * p;
989 const F3 Jlin = scale3(Nsum, lambda);
990 const float invMassA = invMass(realA);
991 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
992 Kokkos::atomic_add(&velPred(realA, 0), Jlin.x * invMassA);
993 Kokkos::atomic_add(&velPred(realA, 1), Jlin.y * invMassA);
994 Kokkos::atomic_add(&velPred(realA, 2), Jlin.z * invMassA);
995 {
996 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
997 const F3 Jl = invRotateVector(qA, scale3(TauA, lambda));
998 const F3 invIA = ld3(invInertia, realA);
999 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1000 const F3 dww = rotateVector(qA, dwl);
1001 Kokkos::atomic_add(&angVelPred(realA, 0), dww.x);
1002 Kokkos::atomic_add(&angVelPred(realA, 1), dww.y);
1003 Kokkos::atomic_add(&angVelPred(realA, 2), dww.z);
1004 }
1005 if (idB >= 0) {
1006 Kokkos::atomic_add(&velPred(realB, 0), -Jlin.x * invMassB);
1007 Kokkos::atomic_add(&velPred(realB, 1), -Jlin.y * invMassB);
1008 Kokkos::atomic_add(&velPred(realB, 2), -Jlin.z * invMassB);
1009 const F4 qB = F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)};
1010 const F3 Jl = invRotateVector(qB, scale3(TauB, lambda));
1011 const F3 invIB = ld3(invInertia, realB);
1012 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1013 const F3 dww = rotateVector(qB, dwl);
1014 Kokkos::atomic_add(&angVelPred(realB, 0), dww.x);
1015 Kokkos::atomic_add(&angVelPred(realB, 1), dww.y);
1016 Kokkos::atomic_add(&angVelPred(realB, 2), dww.z);
1017 }
1018 // Tangential warm impulse: project the stored world-frame accumulator onto the CURRENT
1019 // tangent plane (the normal moved a little between substeps), write it back so the sweep
1020 // accumulator starts consistent, then apply +lt on A / -lt on B at the averaged arms.
1021 if (hasT) {
1022 const float lenN2 = Kokkos::sqrt(dot3(Nsum, Nsum));
1023 if (lenN2 > 1e-9f) {
1024 const F3 nhat = scale3(Nsum, 1.0f / lenN2);
1025 lt = sub3(lt, scale3(nhat, dot3(lt, nhat)));
1026 }
1027 warmT(idx, 0) = lt.x;
1028 warmT(idx, 1) = lt.y;
1029 warmT(idx, 2) = lt.z;
1030 Kokkos::atomic_add(&velPred(realA, 0), lt.x * invMassA);
1031 Kokkos::atomic_add(&velPred(realA, 1), lt.y * invMassA);
1032 Kokkos::atomic_add(&velPred(realA, 2), lt.z * invMassA);
1033 {
1034 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
1035 const F3 Jl = invRotateVector(qA, cross3v(rAavg, lt));
1036 const F3 invIA = ld3(invInertia, realA);
1037 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1038 const F3 dww = rotateVector(qA, dwl);
1039 Kokkos::atomic_add(&angVelPred(realA, 0), dww.x);
1040 Kokkos::atomic_add(&angVelPred(realA, 1), dww.y);
1041 Kokkos::atomic_add(&angVelPred(realA, 2), dww.z);
1042 }
1043 if (idB >= 0) {
1044 Kokkos::atomic_add(&velPred(realB, 0), -lt.x * invMassB);
1045 Kokkos::atomic_add(&velPred(realB, 1), -lt.y * invMassB);
1046 Kokkos::atomic_add(&velPred(realB, 2), -lt.z * invMassB);
1047 const F4 qB = F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)};
1048 const F3 Jl = invRotateVector(qB, cross3v(rBavg, scale3(lt, -1.0f)));
1049 const F3 invIB = ld3(invInertia, realB);
1050 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1051 const F3 dww = rotateVector(qB, dwl);
1052 Kokkos::atomic_add(&angVelPred(realB, 0), dww.x);
1053 Kokkos::atomic_add(&angVelPred(realB, 1), dww.y);
1054 Kokkos::atomic_add(&angVelPred(realB, 2), dww.z);
1055 }
1056 }
1057 });
1058}
1059
1071 Kokkos::View<const ManifoldC*, CpMem> manifolds;
1072 Kokkos::View<const float*, CpMem> invMass;
1073 Kokkos::View<const float* [3], CpMem> invInertia;
1074 Kokkos::View<const float* [4], CpMem> quat;
1075 Kokkos::View<float* [3], CpMem> velPred;
1076 Kokkos::View<float* [3], CpMem> angVelPred;
1077 Kokkos::View<const int*, CpMem> realIdx;
1081 Kokkos::View<float, CpMem> maxApproach;
1082 // Quasi-static residual (corrections on contacts with |vn0| <= 4 restVelThreshold): the
1083 // multilevel pass's stop criterion. The colored caller aliases this to maxApproach (the
1084 // duplicate atomic_max is idempotent), so main-loop behaviour is unchanged.
1085 Kokkos::View<float, CpMem> maxApproachQS;
1086 Kokkos::View<float*, CpMem> lambdaAcc;
1087 Kokkos::View<const float*, CpMem> vn0;
1088 Kokkos::View<const unsigned char*, CpMem> sideFlag;
1089 Kokkos::View<float* [3], CpMem> lambdaT;
1091 Kokkos::View<const float* [3], CpMem> vt0;
1093 Kokkos::View<const float*, CpMem> posImpulse;
1094 // Event-level (Poisson) restitution release (restitutionModel == 1; both views empty
1095 // otherwise): restBank(idx) is the pair's remaining OWED separation impulse (physical units,
1096 // warm-carried by pair key) and restRel(idx) the per-substep release accumulator (lambda
1097 // units, zeroed each substep). Per-substep Newton restitution stays ALIVE alongside the
1098 // bank — its micro-reflections are genuine returned energy, and the accounting's pR term
1099 // deducts each one from the owed budget so the channels never double-count (measured: forcing
1100 // e = 0 on persistent contacts cost more rebound than the bank recovered). restBank is
1101 // writable: a releasing pair whose own budget rails can DRAW from its bodies' orphan accounts
1102 // (transferred into restBank at the moment of need, so the post-solve accounting sees one
1103 // consistent pair ledger). Same-body contacts never run concurrently (colouring), so the
1104 // in-place body-account read-modify-write is race-free without atomics.
1105 Kokkos::View<float*, CpMem> restBank;
1106 Kokkos::View<float*, CpMem> restRel;
1107 // Event peak approach speed (physical): caps the release separation-velocity target at
1108 // e x vPeak — the event-level rebound speed — so a large flux-banked budget against a light
1109 // partner becomes a SUSTAINED unloading push over many substeps (the Hertz-like collective
1110 // rebound) instead of an impulsive dump (a 190 m/s kick to a 1e-5 kg grain, measured absurd).
1111 Kokkos::View<const float*, CpMem> restVPeak;
1112 // Orphan accounts (see Particles::bodyOrphan): balance + carried event peak speed per REAL
1113 // body. The peak matters as much as the balance — a contact formed late under a decelerating
1114 // impactor only saw the residual approach, so its own e*vPeak target would cap the rebound at
1115 // e x the late-stage speed; the orphaned peak restores the full event's velocity scale.
1116 Kokkos::View<float*, CpMem> restOrphan;
1117 Kokkos::View<const float*, CpMem> restOrphanVPeak;
1118 // Hub-copy slot overrides + split relaxation (docs/contact_solve_framework.md §4.4, §4.5);
1119 // empty = realIdx, no relaxation. Every state access below uses realA / realB.
1121
1122 KOKKOS_FUNCTION void solveOne(int idx) const {
1123 using detail::genInvMass;
1124 using detail::ld3;
1125 {
1126 const ManifoldC m = manifolds(idx);
1127 const int idA = m.bodyA, idB = m.bodyB;
1128 const int realA = ov.slotA(idx, realIdx(idA));
1129 const int realB = (idB >= 0) ? ov.slotB(idx, realIdx(idB)) : idB;
1130 const float invMassA = invMass(realA);
1131 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
1132 const F3 invIA = ld3(invInertia, realA);
1133 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
1134 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
1135 const F4 qB = (idB >= 0) ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
1136 : F4{0, 0, 0, 1};
1137 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
1138 F3 vB{0, 0, 0}, wB{0, 0, 0};
1139 const float invN = 1.0f / static_cast<float>(m.num_points);
1140 float restitution = restitutionNormal;
1141 if (idB >= 0) {
1142 vB = ld3(velPred, realB);
1143 wB = ld3(angVelPred, realB);
1144 } else {
1145 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1146 }
1147 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
1148 const float ra = m.restitution_sum * invN;
1149 if (ra >= 0.0f)
1150 restitution = ra;
1151 }
1152 const float eMat = restitution; // raw material e (release cap), before the event gates
1153 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
1156 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
1157 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
1158 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
1159 if (lenN < 1e-9f)
1160 return;
1161 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
1162 const F3 vGrowth = scale3(diffCenters, growthRate);
1163 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1164 dot3(wB, TauB);
1165 vn += dot3(vGrowth, Nsum);
1166 const float alignment = dot3(Nsum, diffCenters);
1167 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
1168 const float Nsq = dot3(Nsum, Nsum);
1169 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
1170 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
1171 // Shock propagation INSIDE the PGS sweep (Guendelman staged solve, per-contact form):
1172 // sidedness was decided ONCE this substep (computeSideFlagsKokkos) so the warm start,
1173 // accumulator and every sweep share one consistent ledger. The held ground side absorbs
1174 // the reaction (recursively down to the floor); e is forced 0 on one-sided contacts.
1175 bool applyA = true, applyB = true;
1176 const unsigned char sf = sideFlag(idx);
1177 if (sf == 1) {
1178 wB_n = 0.0f;
1179 applyB = false;
1180 restitution = 0.0f;
1181 } else if (sf == 2) {
1182 wA_n = 0.0f;
1183 applyA = false;
1184 restitution = 0.0f;
1185 }
1186 const float wTotal = wA_n + wB_n;
1187 if (wTotal <= 0.0f)
1188 return;
1189 // Restitution bias on the PRE-SOLVE approach (resting threshold as in the one-shot path).
1190 const float v0til = sgn * vn0(idx);
1191 if (Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN)
1192 restitution = 0.0f;
1193 // Poisson mode keeps per-substep Newton restitution ALIVE alongside the bank: the
1194 // micro-reflections it produces are genuine returned energy (measured: forcing e = 0 on
1195 // persistent contacts cost more rebound than the bank recovered), and the accounting's pR
1196 // term deducts every reflection from the owed budget, so the two channels never
1197 // double-count.
1198 //
1199 // The restitution target is Moreau's (docs/contact_physics_followups.md §4, decided
1200 // 2026-09-26): -e v0til on EVERY closed contact whose pre-solve |vn0| reaches the resting
1201 // threshold (restitution is already 0 below it and on one-sided contacts). A pre-separating
1202 // pair driven into approach may approach up to e |v0til| before it resists. For a uniform e
1203 // one converged substep changes the kinetic energy by -1/2 (1-e)/(1+e) lambda^T W lambda <= 0
1204 // (§4.1); Newton's 0 for a pre-separating contact CREATED energy (+13 % in one step of a
1205 // dense e = 1 cluster). The Poisson bank (restRel non-empty) keeps Newton's per-substep
1206 // targets, whose micro-reflections its accounting relies on (R-C4).
1207 const float target =
1208 restRel.extent(0) > 0
1209 ? ((v0til > 0.0f) ? -restitution * v0til : 0.0f)
1210 : ((Kokkos::fabs(vn0(idx)) >= restVelThreshold * lenN) ? -restitution * v0til : 0.0f);
1211 const float vtil = sgn * vn;
1212 float dp = (vtil - target) / wTotal;
1213 if (ov.relax(realA, realB))
1214 dp *= ov.omega; // split relaxation before the clamp (§4.5; omega_vel = 1)
1215 const float pOld = lambdaAcc(idx);
1216 float pNew = pOld + dp;
1217 if (pNew < 0.0f)
1218 pNew = 0.0f;
1219 const float dApplied = pNew - pOld;
1220 if (dApplied != 0.0f) {
1221 lambdaAcc(idx) = pNew;
1222 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(dApplied) * wTotal / lenN);
1223 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1224 Kokkos::atomic_max(&maxApproachQS(), Kokkos::fabs(dApplied) * wTotal / lenN);
1225 const float lambda = -sgn * dApplied;
1226 const F3 Jlin = scale3(Nsum, lambda);
1227 const F3 JangA = scale3(TauA, lambda);
1228 const F3 JangB = scale3(TauB, lambda);
1229 if (applyA) {
1230 velPred(realA, 0) += Jlin.x * invMassA;
1231 velPred(realA, 1) += Jlin.y * invMassA;
1232 velPred(realA, 2) += Jlin.z * invMassA;
1233 {
1234 const F3 Jl = invRotateVector(qA, JangA);
1235 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1236 const F3 dww = rotateVector(qA, dwl);
1237 angVelPred(realA, 0) += dww.x;
1238 angVelPred(realA, 1) += dww.y;
1239 angVelPred(realA, 2) += dww.z;
1240 }
1241 }
1242 if (idB >= 0 && applyB) {
1243 velPred(realB, 0) += -Jlin.x * invMassB;
1244 velPred(realB, 1) += -Jlin.y * invMassB;
1245 velPred(realB, 2) += -Jlin.z * invMassB;
1246 const F3 Jl = invRotateVector(qB, JangB);
1247 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1248 const F3 dww = rotateVector(qB, dwl);
1249 angVelPred(realB, 0) += dww.x;
1250 angVelPred(realB, 1) += dww.y;
1251 angVelPred(realB, 2) += dww.z;
1252 }
1253 } // dApplied != 0
1254
1255 // ---- Event-level (Poisson) restitution release ----
1256 // A pair with banked compression (restBank > 0) that is NOT in a kinetic approach pushes
1257 // toward the event's separation-velocity target -owed*w — what releasing the full remaining
1258 // budget delivers against the pair's effective mass, i.e. the event-level rebound speed.
1259 // The push runs through its OWN accumulator clamped to [0, owed] (the friction-cone-shaped
1260 // budget cap): a free pair leaves at the target speed, while a loaded chain re-absorbs the
1261 // attempt through lambda >= 0 on its other contacts with the total injected impulse bounded
1262 // by the budget — velocity-targeted, never impulsive, no unbounded force fight. One-sided
1263 // (side-flagged) contacts are held externally and never release. Spent budget is deducted
1264 // once per substep by updateRestitutionBankKokkos. Unloading detection: KINETICALLY
1265 // separating pre-solve approach (beyond the resting threshold) — jitter separations during
1266 // compression (position-solve pushback, chain oscillation) fire sub-threshold every substep
1267 // and would drain the bank as fast as it fills (measured: bank plateaued at ~1/6 of the
1268 // event flux with a v0til < 0 gate); the genuine rebound onset separates kinetically.
1269 if (restRel.extent(0) > 0 && sf == 0 && v0til < -restVelThreshold * lenN) {
1270 float owed = restBank(idx);
1271 float vPeak = restVPeak.extent(0) > 0 ? restVPeak(idx) : 0.0f;
1272 // Orphan availability at the endpoints: makes budget-less fresh pairs under an event
1273 // carrier eligible, and lifts the velocity target to the orphaned event peak.
1274 float orphA = 0.0f, orphB = 0.0f;
1275 if (restOrphan.extent(0) > 0) {
1276 orphA = restOrphan(realA);
1277 if (orphA > 0.0f)
1278 vPeak = Kokkos::fmax(vPeak, restOrphanVPeak(realA));
1279 if (idB >= 0) {
1280 orphB = restOrphan(realB);
1281 if (orphB > 0.0f)
1282 vPeak = Kokkos::fmax(vPeak, restOrphanVPeak(realB));
1283 }
1284 }
1285 if ((owed > 0.0f || orphA > 0.0f || orphB > 0.0f) && vPeak > 0.0f) {
1286 const F3 vA3 = ld3(velPred, realA), wA3 = ld3(angVelPred, realA);
1287 F3 vB3{0, 0, 0}, wB3{0, 0, 0};
1288 if (idB >= 0) {
1289 vB3 = ld3(velPred, realB);
1290 wB3 = ld3(angVelPred, realB);
1291 } else {
1292 vB3 = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1293 }
1294 float vn3 = dot3(vA3, Nsum) + dot3(wA3, TauA) + dot3(vB3, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1295 dot3(wB3, TauB);
1296 vn3 += dot3(vGrowth, Nsum);
1297 const float vtil3 = sgn * vn3;
1298 // Symmetric release: both endpoints are pushed. (A one-sided variant that held a
1299 // grounded lower side was measured WORSE on the 25k Dosta impact — +0.60 vs +0.88
1300 // rebound — because the light partner's downward reaction is what re-compresses and
1301 // re-releases the layers below; it was retired with the env A/B in 1.0.0.)
1302 const bool relA = true, relB = (idB >= 0);
1303 const float wRel = wTotal;
1304 if (wRel > 0.0f) {
1305 // Separation-velocity target: the event-level rebound speed e x vPeak. The budget is
1306 // enforced by the accumulator clamp alone — folding it into the velocity target
1307 // (owed * wRel) prematurely stalls a one-sided release against a heavy impactor
1308 // (owed/m_ball ~ cm/s) with most of the budget unspent. Physical velocity -> vtil
1309 // units is x lenN.
1310 const float vTphys = eMat * vPeak;
1311 const float targetR = -vTphys * lenN;
1312 const float dpR = (vtil3 - targetR) / wRel;
1313 float cap = owed / lenN; // physical budget in lambda units
1314 const float rOld = restRel(idx);
1315 float rNew = rOld + dpR;
1316 if (rNew < 0.0f)
1317 rNew = 0.0f;
1318 // Own budget railed with orphan balance at the endpoints: draw the shortfall from the
1319 // body accounts INTO the pair bank (heavier-first order is irrelevant; drained in
1320 // sequence). The drawn amount is spent by this very increment, so the post-solve
1321 // accounting's owed -= released cancels it exactly.
1322 if (rNew > cap && (orphA > 0.0f || orphB > 0.0f)) {
1323 float need = (rNew - cap) * lenN;
1324 float draw = 0.0f;
1325 if (orphA > 0.0f) {
1326 const float d = Kokkos::fmin(need, orphA);
1327 restOrphan(realA) = orphA - d;
1328 need -= d;
1329 draw += d;
1330 }
1331 if (need > 0.0f && orphB > 0.0f) {
1332 const float d = Kokkos::fmin(need, orphB);
1333 restOrphan(realB) = orphB - d;
1334 draw += d;
1335 }
1336 if (draw > 0.0f) {
1337 owed += draw;
1338 restBank(idx) = owed;
1339 cap = owed / lenN;
1340 }
1341 }
1342 if (rNew > cap)
1343 rNew = cap;
1344 const float dR = rNew - rOld;
1345 if (dR != 0.0f) {
1346 restRel(idx) = rNew;
1347 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(dR) * wRel / lenN);
1348 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1349 Kokkos::atomic_max(&maxApproachQS(), Kokkos::fabs(dR) * wRel / lenN);
1350 const float lambdaR = -sgn * dR;
1351 const F3 JlinR = scale3(Nsum, lambdaR);
1352 if (relA) {
1353 velPred(realA, 0) += JlinR.x * invMassA;
1354 velPred(realA, 1) += JlinR.y * invMassA;
1355 velPred(realA, 2) += JlinR.z * invMassA;
1356 const F3 Jl = invRotateVector(qA, scale3(TauA, lambdaR));
1357 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1358 const F3 dww = rotateVector(qA, dwl);
1359 angVelPred(realA, 0) += dww.x;
1360 angVelPred(realA, 1) += dww.y;
1361 angVelPred(realA, 2) += dww.z;
1362 }
1363 if (relB) {
1364 velPred(realB, 0) += -JlinR.x * invMassB;
1365 velPred(realB, 1) += -JlinR.y * invMassB;
1366 velPred(realB, 2) += -JlinR.z * invMassB;
1367 const F3 Jl = invRotateVector(qB, scale3(TauB, lambdaR));
1368 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1369 const F3 dww = rotateVector(qB, dwl);
1370 angVelPred(realB, 0) += dww.x;
1371 angVelPred(realB, 1) += dww.y;
1372 angVelPred(realB, 2) += dww.z;
1373 }
1374 }
1375 }
1376 }
1377 }
1378
1379 // ---- Friction cone (sequential tangential impulse) ----
1380 // Accumulated world-frame tangential impulse lambdaT on body A, updated by the same
1381 // colored nonlinear GS: incremental impulse -vt/w_t along the current slip direction,
1382 // then projection onto the Coulomb disc |lambdaT| <= mu * lambdaN (physical impulse:
1383 // the normal accumulator is scaled by |Nsum|). Static stick falls out naturally: at
1384 // vt = 0 the accumulator holds whatever tangential load the cone admits. Sidedness
1385 // mirrors the normal solve (a held side neither moves nor adds compliance).
1386 {
1387 float mu = frictionDynamic;
1388 const float fa = m.friction_sum * invN;
1389 if (fa >= 0.0f)
1390 mu = fa;
1391 F3 ltOld{lambdaT(idx, 0), lambdaT(idx, 1), lambdaT(idx, 2)};
1392 const bool haveOld = (ltOld.x != 0.0f || ltOld.y != 0.0f || ltOld.z != 0.0f);
1393 if (mu > 0.0f || haveOld) {
1394 const F3 nhat = scale3(Nsum, 1.0f / lenN);
1395 const F3 vA2 = ld3(velPred, realA), wA2 = ld3(angVelPred, realA);
1396 F3 vB2{0, 0, 0}, wB2{0, 0, 0};
1397 if (idB >= 0) {
1398 vB2 = ld3(velPred, realB);
1399 wB2 = ld3(angVelPred, realB);
1400 } else {
1401 vB2 = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1402 }
1403 const F3 vrel = sub3(add3(vA2, cross3v(wA2, rAavg)), add3(vB2, cross3v(wB2, rBavg)));
1404 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
1405 // Walton tangential restitution: for a COLLIDING contact the target surface
1406 // velocity is -beta * vt0 (pre-solve tangential velocity); sustained contacts
1407 // (below the resting threshold) and one-sided stabilization contacts run beta = 0
1408 // -- the same event classification that gates normal restitution. The cone clamp
1409 // below turns this into the stick/slide transition of the (e, mu, beta) law.
1410 float beta = restitutionTangent;
1411 if (beta != 0.0f && (sf != 0 || Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN))
1412 beta = 0.0f;
1413 F3 vtErr = vt;
1414 if (beta != 0.0f && vt0.extent(0) > 0) {
1415 F3 v0{vt0(idx, 0), vt0(idx, 1), vt0(idx, 2)};
1416 v0 = sub3(v0, scale3(nhat, dot3(v0, nhat))); // current tangent plane
1417 vtErr = add3(vt, scale3(v0, beta));
1418 }
1419 const float vtLen = Kokkos::sqrt(dot3(vtErr, vtErr));
1420 F3 ltNew = ltOld;
1421 float wT = invMassA + invMassB;
1422 if (vtLen > 1e-9f) {
1423 const F3 that = scale3(vtErr, 1.0f / vtLen);
1424 float wAt = (sf == 2) ? 0.0f : invMassA + genInvMass(cross3v(rAavg, that), invIA, qA);
1425 float wBt = (sf == 1 || idB < 0)
1426 ? 0.0f
1427 : invMassB + genInvMass(cross3v(rBavg, that), invIB, qB);
1428 wT = wAt + wBt;
1429 if (wT > 1e-9f)
1430 ltNew = add3(ltOld, scale3(that, -vtLen / wT));
1431 }
1432 ltNew = sub3(ltNew, scale3(nhat, dot3(ltNew, nhat)));
1433 // Coulomb bound = mu * TOTAL normal load: velocity-impulse channel (lambdaAcc,
1434 // physical impulse = lambdaAcc * |Nsum|) + the position-projection channel carried
1435 // from last substep (already physical impulse units).
1436 float nTot = Kokkos::fmax(lambdaAcc(idx), 0.0f) * lenN;
1437 // The carry is a QUASI-STATIC corrector: for colliding contacts the one-substep lag
1438 // double-counts (crater contacts already carry a large velocity impulse) -- gate by
1439 // the same event classification as e and beta.
1440 if (posImpulse.extent(0) > 0 && Kokkos::fabs(vn0(idx)) < restVelThreshold * lenN)
1441 nTot += Kokkos::fmax(posImpulse(idx), 0.0f);
1442 const float bound = mu * nTot;
1443 const float ltLen = Kokkos::sqrt(dot3(ltNew, ltNew));
1444 if (ltLen > bound)
1445 ltNew = (bound > 0.0f) ? scale3(ltNew, bound / ltLen) : F3{0, 0, 0};
1446 const F3 dApp = sub3(ltNew, ltOld);
1447 if (dApp.x != 0.0f || dApp.y != 0.0f || dApp.z != 0.0f) {
1448 lambdaT(idx, 0) = ltNew.x;
1449 lambdaT(idx, 1) = ltNew.y;
1450 lambdaT(idx, 2) = ltNew.z;
1451 const float dLen = Kokkos::sqrt(dot3(dApp, dApp));
1452 if (wT > 1e-9f) {
1453 Kokkos::atomic_max(&maxApproach(), dLen * wT);
1454 if (Kokkos::fabs(vn0(idx)) <= 4.0f * restVelThreshold * lenN)
1455 Kokkos::atomic_max(&maxApproachQS(), dLen * wT);
1456 }
1457 if (applyA) {
1458 velPred(realA, 0) += dApp.x * invMassA;
1459 velPred(realA, 1) += dApp.y * invMassA;
1460 velPred(realA, 2) += dApp.z * invMassA;
1461 const F3 Jl = invRotateVector(qA, cross3v(rAavg, dApp));
1462 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1463 const F3 dww = rotateVector(qA, dwl);
1464 angVelPred(realA, 0) += dww.x;
1465 angVelPred(realA, 1) += dww.y;
1466 angVelPred(realA, 2) += dww.z;
1467 }
1468 if (idB >= 0 && applyB) {
1469 velPred(realB, 0) += -dApp.x * invMassB;
1470 velPred(realB, 1) += -dApp.y * invMassB;
1471 velPred(realB, 2) += -dApp.z * invMassB;
1472 const F3 Jl = invRotateVector(qB, cross3v(rBavg, scale3(dApp, -1.0f)));
1473 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1474 const F3 dww = rotateVector(qB, dwl);
1475 angVelPred(realB, 0) += dww.x;
1476 angVelPred(realB, 1) += dww.y;
1477 angVelPred(realB, 2) += dww.z;
1478 }
1479 }
1480 }
1481 }
1482 }
1483 }
1484};
1485
1490 Kokkos::View<const ManifoldC*, CpMem> manifolds, Kokkos::View<const float*, CpMem> invMass,
1491 Kokkos::View<const float* [3], CpMem> invInertia, Kokkos::View<const float* [4], CpMem> quat,
1492 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float* [3], CpMem> angVelPred,
1493 Kokkos::View<const int*, CpMem> realIdx, float growthRate, float restitutionNormal,
1494 float restVelThreshold, Kokkos::View<float, CpMem> maxApproach,
1495 Kokkos::View<float, CpMem> maxApproachQS, Kokkos::View<float*, CpMem> lambdaAcc,
1496 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const unsigned char*, CpMem> sideFlag,
1497 Kokkos::View<float* [3], CpMem> lambdaT, float frictionDynamic,
1498 Kokkos::View<const float* [3], CpMem> vt0, float restitutionTangent,
1499 Kokkos::View<const float*, CpMem> posImpulse, Kokkos::View<float*, CpMem> restBank,
1500 Kokkos::View<float*, CpMem> restRel, Kokkos::View<const float*, CpMem> restVPeak,
1501 Kokkos::View<float*, CpMem> restOrphan, Kokkos::View<const float*, CpMem> restOrphanVPeak,
1502 SlotOverride ov = {}) {
1503 return PGSManifoldSweep{manifolds,
1504 invMass,
1505 invInertia,
1506 quat,
1507 velPred,
1508 angVelPred,
1509 realIdx,
1510 growthRate,
1511 restitutionNormal,
1512 restVelThreshold,
1513 maxApproach,
1514 maxApproachQS.data() ? maxApproachQS : maxApproach,
1515 lambdaAcc,
1516 vn0,
1517 sideFlag,
1518 lambdaT,
1519 frictionDynamic,
1520 vt0,
1521 restitutionTangent,
1522 posImpulse,
1523 restBank,
1524 restRel,
1525 restVPeak,
1526 restOrphan,
1527 restOrphanVPeak,
1528 ov};
1529}
1530
1535 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
1536 Kokkos::View<const int*, CpMem> mColor, int numColors,
1537 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
1538 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<float* [3], CpMem> velPred,
1539 Kokkos::View<float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
1540 float growthRate, float restitutionNormal, float restVelThreshold,
1541 Kokkos::View<float, CpMem> maxApproach, Kokkos::View<float*, CpMem> lambdaAcc,
1542 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const unsigned char*, CpMem> sideFlag,
1543 Kokkos::View<float* [3], CpMem> lambdaT, float frictionDynamic,
1544 Kokkos::View<const float* [3], CpMem> vt0 = {}, float restitutionTangent = 0.0f,
1545 Kokkos::View<const float*, CpMem> posImpulse = {},
1546 Kokkos::View<float, CpMem> maxApproachQS = {}, Kokkos::View<float*, CpMem> restBank = {},
1547 Kokkos::View<float*, CpMem> restRel = {}, Kokkos::View<const float*, CpMem> restVPeak = {},
1548 Kokkos::View<float*, CpMem> restOrphan = {},
1549 Kokkos::View<const float*, CpMem> restOrphanVPeak = {},
1550 Kokkos::View<const int*, CpMem> colorPerm = {}, const std::vector<int>* colorOffs = nullptr,
1551 const FusedSweepCtx* fused = nullptr, const FusedLoopSpec* loop = nullptr,
1552 SlotOverride ov = {}) {
1553 CpExec space;
1554 const PGSManifoldSweep f = makePGSManifoldSweep(
1555 manifolds, invMass, invInertia, quat, velPred, angVelPred, realIdx, growthRate,
1556 restitutionNormal, restVelThreshold, maxApproach, maxApproachQS, lambdaAcc, vn0, sideFlag,
1557 lambdaT, frictionDynamic, vt0, restitutionTangent, posImpulse, restBank, restRel, restVPeak,
1558 restOrphan, restOrphanVPeak, ov);
1559 // Fused mode (CUDA): one persistent kernel iterates the colours device-side with a grid
1560 // barrier between them — same per-manifold math, same colour ordering, bit-identical to the
1561 // launch loop below (see solver_fused.hpp). Loop mode additionally iterates the whole
1562 // adaptive loop on-device against the residual the caller's stop would have read.
1563#ifdef KOKKOS_ENABLE_CUDA
1564 if (loop) {
1565 if (fused && fused->maxBucket > 0 && colorOffs)
1566 return demLaunchFusedSweepLoop(space, f, colorPerm, *fused, numColors, *loop,
1567 (maxApproachQS.data() ? maxApproachQS : maxApproach).data());
1568 return false;
1569 }
1570 if (fused && fused->maxBucket > 0 && colorOffs &&
1571 demLaunchFusedColorSweep(space, f, colorPerm, *fused, numColors))
1572 return true;
1573#else
1574 (void)fused;
1575 if (loop)
1576 return false;
1577#endif
1578 // Dense-bucket mode (colorOffs from buildColorBucketsKokkos): each colour launch covers only
1579 // its own manifolds instead of scanning all of them — bit-identical (colour classes are
1580 // body-disjoint). No fence: the caller's residual readback synchronizes, and an explicit fence
1581 // here would serialize host submission with GPU execution (the step is submission-bound).
1582 for (int color = 0; color < numColors; ++color) {
1583 if (colorOffs) {
1584 const int b = (*colorOffs)[color], e = (*colorOffs)[color + 1];
1585 if (b == e)
1586 continue;
1587 Kokkos::parallel_for(
1588 "peclet::dem::solve_velocity_pgs", Kokkos::RangePolicy<CpExec>(space, b, e),
1589 KOKKOS_LAMBDA(int i2) { f.solveOne(colorPerm(i2)); });
1590 } else {
1591 Kokkos::parallel_for(
1592 "peclet::dem::solve_velocity_pgs", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1593 KOKKOS_LAMBDA(int idx) {
1594 if (mColor(idx) == color)
1595 f.solveOne(idx);
1596 });
1597 }
1598 }
1599 return true;
1600}
1601
1610inline void buildLevelColorBucketsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
1611 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
1612 Kokkos::View<const int*, CpMem> mColor,
1613 Kokkos::View<const int*, CpMem> heights,
1614 Kokkos::View<int*, CpMem> keys,
1615 Kokkos::View<int*, CpMem> perm,
1616 std::vector<std::pair<int, int>>& buckets) {
1617 buckets.clear();
1618 if (numManifolds <= 0)
1619 return;
1620 CpExec space;
1621 Kokkos::parallel_for(
1622 "peclet::dem::level_keys", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1623 KOKKOS_LAMBDA(int idx) {
1624 perm(idx) = idx;
1625 const int c = mColor(idx);
1626 const ManifoldC m = manifolds(idx);
1627 if (m.num_points <= 0 || c < 0) {
1628 keys(idx) = INT_MAX;
1629 return;
1630 }
1631 int h = heights(realIdx(m.bodyA));
1632 if (m.bodyB >= 0) {
1633 const int hB = heights(realIdx(m.bodyB));
1634 if (hB < h)
1635 h = hB;
1636 }
1637 if (h > 1023)
1638 h = 1023;
1639 keys(idx) = h * 64 + c;
1640 });
1641 const auto rng = Kokkos::pair<int, int>(0, numManifolds);
1642 auto kd = Kokkos::subview(keys, rng);
1643 auto pd = Kokkos::subview(perm, rng);
1644 Kokkos::Experimental::sort_by_key(space, kd, pd);
1645 auto hk = Kokkos::create_mirror_view(kd);
1646 Kokkos::deep_copy(space, hk, kd);
1647 for (int b = 0; b < numManifolds && hk(b) != INT_MAX;) {
1648 int e = b + 1;
1649 while (e < numManifolds && hk(e) == hk(b))
1650 ++e;
1651 buckets.emplace_back(b, e);
1652 b = e;
1653 }
1654}
1655
1663 Kokkos::View<const int*, CpMem> perm,
1664 const std::vector<std::pair<int, int>>& buckets,
1665 bool topDown) {
1666 CpExec space;
1667 const int nb = static_cast<int>(buckets.size());
1668 for (int i = 0; i < nb; ++i) {
1669 const auto [b, e] = buckets[topDown ? nb - 1 - i : i];
1670 Kokkos::parallel_for(
1671 "peclet::dem::solve_velocity_pgs_lvl", Kokkos::RangePolicy<CpExec>(space, b, e),
1672 KOKKOS_LAMBDA(int i2) { f.solveOne(perm(i2)); });
1673 }
1674 space.fence();
1675}
1676
1681KOKKOS_INLINE_FUNCTION int xPopcount(unsigned long long m) {
1682 int n = 0;
1683 while (m != 0ull) {
1684 m &= m - 1ull;
1685 ++n;
1686 }
1687 return n;
1688}
1689KOKKOS_INLINE_FUNCTION int xHolder(unsigned long long mask, long long t, int numColors) {
1690 const int tc = static_cast<int>(t % numColors);
1691 if ((mask >> tc) & 1ull)
1692 return tc;
1693 int j = static_cast<int>(t % xPopcount(mask));
1694 for (int b = 0; b < 64; ++b)
1695 if ((mask >> b) & 1ull) {
1696 if (j == 0)
1697 return b;
1698 --j;
1699 }
1700 return -1; // unreachable: popcount(mask) >= 2 at every call
1701}
1706 Kokkos::View<const unsigned long long*, CpMem> mask;
1707 int color = 0, numColors = 1;
1708 long long t = 0;
1709 KOKKOS_INLINE_FUNCTION bool fires(int slotA, int slotB) const {
1710 if (mask.extent(0) == 0)
1711 return true;
1712 const unsigned long long mA = mask(slotA);
1713 if (xPopcount(mA) >= 2 && xHolder(mA, t, numColors) != color)
1714 return false;
1715 if (slotB >= 0) {
1716 const unsigned long long mB = mask(slotB);
1717 if (xPopcount(mB) >= 2 && xHolder(mB, t, numColors) != color)
1718 return false;
1719 }
1720 return true;
1721 }
1722};
1726inline void computeXGateKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds, int nm,
1727 Kokkos::View<const int*, CpMem> realIdx,
1728 Kokkos::View<const unsigned long long*, CpMem> velMask,
1729 Kokkos::View<unsigned char*, CpMem> gate, int color, int numColors,
1730 long long t) {
1731 Kokkos::parallel_for(
1732 "peclet::dem::x_gate", Kokkos::RangePolicy<CpExec>(0, nm), KOKKOS_LAMBDA(int idx) {
1733 const ManifoldC m = manifolds(idx);
1734 bool fire = true;
1735 const unsigned long long mA = velMask(realIdx(m.bodyA));
1736 if (xPopcount(mA) >= 2 && xHolder(mA, t, numColors) != color)
1737 fire = false;
1738 if (m.bodyB >= 0) {
1739 const unsigned long long mB = velMask(realIdx(m.bodyB));
1740 if (xPopcount(mB) >= 2 && xHolder(mB, t, numColors) != color)
1741 fire = false;
1742 }
1743 gate(idx) = fire ? 1 : 0;
1744 });
1745}
1747inline long long countUnfiredKokkos(Kokkos::View<const unsigned char*, CpMem> gate, int nm) {
1748 long long n = 0;
1749 Kokkos::parallel_reduce(
1750 "peclet::dem::x_unfired", Kokkos::RangePolicy<CpExec>(0, nm),
1751 KOKKOS_LAMBDA(int idx, long long& acc) { acc += gate(idx) == 0 ? 1 : 0; }, n);
1752 return n;
1753}
1754
1763 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
1764 Kokkos::View<const int*, CpMem> mColor, int numColors,
1765 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
1766 Kokkos::View<const float* [4], CpMem> quat, Kokkos::View<float* [3], CpMem> velPred,
1767 Kokkos::View<float* [3], CpMem> angVelPred, Kokkos::View<const int*, CpMem> realIdx,
1768 float growthRate, float restitutionNormal, float restVelThreshold,
1769 Kokkos::View<float, CpMem> maxApproach,
1770 Kokkos::View<const unsigned char*, CpMem> persistent = {},
1771 Kokkos::View<const float* [3], CpMem> posPred = {}, F3 gHat = {},
1772 Kokkos::View<const unsigned char*, CpMem> grounded = {}, SlotOverride ov = {},
1773 XGateSpec xg = {}) {
1774 using detail::genInvMass;
1775 using detail::ld3;
1776 CpExec space;
1777 const bool usePersist = persistent.extent(0) > 0;
1778 for (int color = 0; color < numColors; ++color) {
1779 Kokkos::parallel_for(
1780 "peclet::dem::solve_velocity_gs", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
1781 KOKKOS_LAMBDA(int idx) {
1782 if (mColor(idx) != color)
1783 return;
1784 const ManifoldC m = manifolds(idx);
1785
1786 const int idA = m.bodyA, idB = m.bodyB;
1787 // hub-copy slot overrides (§4.4; empty = realIdx): every state access uses realA/B
1788 const int realA = ov.slotA(idx, realIdx(idA));
1789 const int realB = (idB >= 0) ? ov.slotB(idx, realIdx(idB)) : idB;
1790
1791 const float invMassA = invMass(realA);
1792 const float invMassB = (idB >= 0) ? invMass(realB) : 0.0f;
1793 const F3 invIA = ld3(invInertia, realA);
1794 const F3 invIB = (idB >= 0) ? ld3(invInertia, realB) : F3{0, 0, 0};
1795 const F4 qA = F4{quat(realA, 0), quat(realA, 1), quat(realA, 2), quat(realA, 3)};
1796 const F4 qB = (idB >= 0)
1797 ? F4{quat(realB, 0), quat(realB, 1), quat(realB, 2), quat(realB, 3)}
1798 : F4{0, 0, 0, 1};
1799
1800 const F3 vA = ld3(velPred, realA), wA = ld3(angVelPred, realA);
1801 F3 vB{0, 0, 0}, wB{0, 0, 0};
1802 if (idB >= 0) {
1803 vB = ld3(velPred, realB);
1804 wB = ld3(angVelPred, realB);
1805 }
1806
1807 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
1808 const F3 TauA{m.torque_armA_sum.x, m.torque_armA_sum.y, m.torque_armA_sum.z};
1809 const F3 TauB{m.torque_armB_sum.x, m.torque_armB_sum.y, m.torque_armB_sum.z};
1810
1811 const float invN = 1.0f / static_cast<float>(m.num_points);
1812
1813 float restitution = restitutionNormal;
1814 if (idB < 0)
1815 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
1816 { // per-wall AND per-pair material override (a < 0 average keeps the global material)
1817 const float ra = m.restitution_sum * invN;
1818 if (ra >= 0.0f)
1819 restitution = ra;
1820 }
1821 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
1822 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
1823
1824 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
1825 if (lenN < 1e-9f)
1826 return;
1827
1828 const F3 diffCenters = (idB < 0) ? rAavg : sub3(rAavg, rBavg);
1829 const F3 vGrowth = scale3(diffCenters, growthRate);
1830
1831 float vn = dot3(vA, Nsum) + dot3(wA, TauA) + dot3(vB, F3{-Nsum.x, -Nsum.y, -Nsum.z}) +
1832 dot3(wB, TauB);
1833 vn += dot3(vGrowth, Nsum);
1834
1835 const float alignment = dot3(Nsum, diffCenters);
1836 if (alignment > 0.0f) {
1837 if (vn < 0.0f)
1838 return;
1839 } else {
1840 if (vn > 0.0f)
1841 return;
1842 }
1843
1844 const float Nsq = dot3(Nsum, Nsum);
1845 float wA_n = Nsq * invMassA + genInvMass(TauA, invIA, qA);
1846 float wB_n = Nsq * invMassB + genInvMass(TauB, invIB, qB);
1847 // Shock propagation for persistent loaded contacts -- see solveVelocityKokkos.
1848 bool applyA = true, applyB = true;
1849 if (usePersist && persistent(idx) != 0 && idB >= 0) {
1850 const F3 dx = sub3(ldF3(posPred, idA), ldF3(posPred, idB));
1851 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z);
1852 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
1853 const float riseThr = 4.0f * restVelThreshold; // non-rising + grounded support gate
1854 if (up > thr && -dot3(vB, gHat) <= riseThr && grounded(realB) > 0) {
1855 wB_n = 0.0f;
1856 applyB = false;
1857 restitution = 0.0f; // shock pass is inelastic — see solveVelocityKokkos
1858 } else if (up < -thr && -dot3(vA, gHat) <= riseThr && grounded(realA) > 0) {
1859 wA_n = 0.0f;
1860 applyA = false;
1861 restitution = 0.0f;
1862 }
1863 }
1864 const float wTotal = wA_n + wB_n;
1865 if (wTotal <= 0.0f)
1866 return;
1867
1868 // Record this approaching pair's physical approach speed for the adaptive stop: the
1869 // caller ends the velocity loop once no manifold approaches faster than the resting
1870 // threshold.
1871 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(vn) / lenN);
1872 // Rank-level X (docs/contact_solve_framework.md §1.4): a gated contact whose rank does
1873 // not hold one of its rank-split bodies in this sync interval records its approach (the
1874 // stop must not end while it still approaches) but writes nothing.
1875 if (!xg.fires(realIdx(idA), idB >= 0 ? realIdx(idB) : -1))
1876 return;
1877
1878 if (Kokkos::fabs(vn) < restVelThreshold * lenN)
1879 restitution = 0.0f;
1880
1881 const float lambda = (-restitution * vn - vn) / wTotal;
1882
1883 const F3 Jlin = scale3(Nsum, lambda);
1884 const F3 JangA = scale3(TauA, lambda);
1885 const F3 JangB = scale3(TauB, lambda);
1886
1887 // Linear + angular delta on A (dw_world = R (invI_local * (R^T Jang))), applied in place.
1888 if (applyA) {
1889 velPred(realA, 0) += Jlin.x * invMassA;
1890 velPred(realA, 1) += Jlin.y * invMassA;
1891 velPred(realA, 2) += Jlin.z * invMassA;
1892 {
1893 const F3 Jl = invRotateVector(qA, JangA);
1894 const F3 dwl{Jl.x * invIA.x, Jl.y * invIA.y, Jl.z * invIA.z};
1895 const F3 dww = rotateVector(qA, dwl);
1896 angVelPred(realA, 0) += dww.x;
1897 angVelPred(realA, 1) += dww.y;
1898 angVelPred(realA, 2) += dww.z;
1899 }
1900 }
1901 if (idB >= 0 && applyB) {
1902 velPred(realB, 0) += -Jlin.x * invMassB;
1903 velPred(realB, 1) += -Jlin.y * invMassB;
1904 velPred(realB, 2) += -Jlin.z * invMassB;
1905 const F3 Jl = invRotateVector(qB, JangB);
1906 const F3 dwl{Jl.x * invIB.x, Jl.y * invIB.y, Jl.z * invIB.z};
1907 const F3 dww = rotateVector(qB, dwl);
1908 angVelPred(realB, 0) += dww.x;
1909 angVelPred(realB, 1) += dww.y;
1910 angVelPred(realB, 2) += dww.z;
1911 }
1912 });
1913 // No host fence here: consecutive parallel_for on one execution space are stream-ordered on the
1914 // device, so colour c+1's kernel already observes colour c's in-place writes (the Gauss–Seidel
1915 // dependency). A per-colour fence would only stall the host. One fence after the sweep
1916 // suffices.
1917 }
1918 space.fence();
1919}
1920
1921} // namespace peclet::dem
1922
1923#endif // DEM_SOLVER_VELOCITY_HPP
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
dem — portable POD types + math + analytic SDFs shared by the Kokkos kernel ports.
float genInvMass(F3 tau, F3 invIlocal, F4 q)
F3 ld3(Kokkos::View< const float *[3], CpMem > v, int i)
int xHolder(unsigned long long mask, long long t, int numColors)
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 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...
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 xPopcount(unsigned long long m)
Rank-level X (docs/contact_solve_framework.md §1.4): the holder of a body whose velocity activity mas...
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),...
F3 cross3v(F3 a, F3 b)
Kokkos::View< int *, CpMem > Vi
Kokkos::View< float *[3], CpMem > V3
F3 invRotateVector(F4 q, F3 v)
F3 ldF3(const V &v, int i)
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...
F3 rotateVector(F4 q, F3 v)
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 ...
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
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...
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.
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") ...
long long countUnfiredKokkos(Kokkos::View< const unsigned char *, CpMem > gate, int nm)
Diagnostics (iterCounters): the number of gated-off manifolds of one sweep.
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...
F3 scale3(F3 a, float s)
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
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...
constexpr int kColorUncolourable
F3 add3(F3 a, F3 b)
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 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 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,...
constexpr int kColorPalette
Colouring palette (docs/contact_solve_framework.md §4.2 item 1): the greedy takes the lowest colour f...
std::tuple< double, float, int > restBankStatsKokkos(Kokkos::View< const float *, CpMem > bank, int n)
Poisson-restitution diagnostics: (sum, max, count>0) over the committed owed-impulse store (namespace...
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...
Kokkos::DefaultExecutionSpace CpExec
unsigned long long pairKeyOf(const ManifoldC &m, Kokkos::View< const int *, CpMem > realIdx)
Persistent-contact detection for the gravity-gated restitution rule.
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 — fused colour sweeps: one persistent kernel per sweep instead of one kernel launch per colour.
Portable mirror of ManifoldConstraint.
One full colored PGS sweep.
Kokkos::View< float, CpMem > maxApproach
Kokkos::View< float, CpMem > maxApproachQS
Kokkos::View< float *[3], CpMem > lambdaT
Kokkos::View< const float *, CpMem > restOrphanVPeak
Kokkos::View< const float *, CpMem > invMass
Kokkos::View< const float *[4], CpMem > quat
Kokkos::View< const int *, CpMem > realIdx
Kokkos::View< const float *[3], CpMem > vt0
Kokkos::View< const float *[3], CpMem > invInertia
Kokkos::View< const unsigned char *, CpMem > sideFlag
Kokkos::View< float *, CpMem > lambdaAcc
Kokkos::View< float *[3], CpMem > velPred
Kokkos::View< const float *, CpMem > restVPeak
Kokkos::View< const float *, CpMem > vn0
Kokkos::View< float *, CpMem > restOrphan
Kokkos::View< const float *, CpMem > posImpulse
Kokkos::View< const ManifoldC *, CpMem > manifolds
Kokkos::View< float *[3], CpMem > angVelPred
Kokkos::View< float *, CpMem > restRel
Kokkos::View< float *, CpMem > restBank
Per-edge slot overrides of a phase's hub copies (docs/contact_solve_framework.md §4....
int slotA(int e, int def) const
bool relax(int sA, int sB) const
True iff the edge touches a mass-split slot (its step is scaled by omega before the clamp).
int slotB(int e, int def) const
Rank-level X evaluated inline in the one-shot sweep (only for the manifolds of the colour being swept...
Kokkos::View< const unsigned long long *, CpMem > mask
bool fires(int slotA, int slotB) const