peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_position.hpp
Go to the documentation of this file.
1
10#ifndef DEM_SOLVER_POSITION_HPP
11#define DEM_SOLVER_POSITION_HPP
12
13#include <Kokkos_Core.hpp>
14#include <vector>
15
16#include "contact_preprocessing.hpp" // ContactC, CpExec/CpMem
17#include "dem_portable.hpp"
18#include "solver_fused.hpp"
19
20namespace peclet::dem {
21
22namespace detail {
23KOKKOS_INLINE_FUNCTION float computeW(F3 r, F3 dir, float invM, F3 invI) {
24 const F3 rn = cross3v(r, dir);
25 return invM + rn.x * rn.x * invI.x + rn.y * rn.y * invI.y + rn.z * rn.z * invI.z;
26}
27} // namespace detail
28
33inline void countPositionJacobiKokkos(Kokkos::View<const ContactC*, CpMem> contacts,
34 int numContacts, Kokkos::View<int*, CpMem> counts) {
35 CpExec space;
36 Kokkos::parallel_for(
37 "peclet::dem::count_position_jacobi", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
38 KOKKOS_LAMBDA(int idx) {
39 const ContactC c = contacts(idx);
40 Kokkos::atomic_add(&counts(c.bodyA), 1);
41 if (c.bodyB >= 0)
42 Kokkos::atomic_add(&counts(c.bodyB), 1);
43 });
44 space.fence();
45}
46
56 Kokkos::View<const ContactC*, CpMem> contacts, int numContacts,
57 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> posPred,
58 Kokkos::View<const float* [4], CpMem> quatPred,
59 Kokkos::View<const float* [4], CpMem> quatStatic,
60 Kokkos::View<const float* [3], CpMem> invInertia, Kokkos::View<float* [3], CpMem> deltaPos,
61 Kokkos::View<float* [4], CpMem> deltaQuat, Kokkos::View<int*, CpMem> constraintCounts,
62 Kokkos::View<float, CpMem> maxOverlap, Kokkos::View<const int*, CpMem> onlyColor = {},
63 int colorFilter = 0, bool massSplit = false) {
64 (void)invInertia; // translation-only diagonal (below); kept in the signature for the callers
65 (void)deltaQuat; // never written: the rotation was never applied (applyUpdatesKokkos)
66 CpExec space;
67 const bool filt = onlyColor.extent(0) > 0;
68 Kokkos::parallel_for(
69 "peclet::dem::solve_position", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
70 KOKKOS_LAMBDA(int idx) {
71 if (filt && onlyColor(idx) != colorFilter)
72 return; // colour filter (kernel unit test only; no production caller)
73 const ContactC c = contacts(idx);
74 const int idA = c.bodyA, idB = c.bodyB;
75 const float invMassA = invMass(idA);
76 const float invMassB = (idB >= 0) ? invMass(idB) : 0.0f;
77
78 const F3 pA = ldF3(posPred, idA);
79 const F4 qA = ldF4(quatPred, idA);
80 F3 pB{0, 0, 0};
81 F4 qB{0, 0, 0, 1};
82 if (idB >= 0) {
83 pB = ldF3(posPred, idB);
84 qB = ldF4(quatPred, idB);
85 }
86
87 // Delta-rotate the stored lever arms / normal from the static frame to the predicted one.
88 const F4 qAdelta = quatMult(qA, quatInverse(ldF4(quatStatic, idA)));
89 F3 rA = rotateVector(qAdelta, F3{c.rA.x, c.rA.y, c.rA.z});
90 F3 rB{c.rB.x, c.rB.y, c.rB.z};
91 F3 n{c.normal.x, c.normal.y, c.normal.z};
92 if (idB >= 0) {
93 const F4 qBdelta = quatMult(qB, quatInverse(ldF4(quatStatic, idB)));
94 rB = rotateVector(qBdelta, rB);
95 n = rotateVector(normalOfA(c) ? qAdelta : qBdelta, n); // the normal's own body
96 }
97
98 float C;
99 if (idB < 0) {
100 n = F3{c.normal.x, c.normal.y, c.normal.z}; // wall normal is static
101 const F3 pAsurf = add3(pA, rA);
102 C = dot3(sub3(pAsurf, F3{c.rB.x, c.rB.y, c.rB.z}), n);
103 } else {
104 const F3 pAc = add3(pA, rA);
105 const F3 pBc = add3(pB, rB);
106 C = dot3(sub3(pAc, pBc), n);
107 }
108 if (C >= 0.0f)
109 return;
110
111 // The translational effective mass of the solve views: the correction is translation
112 // only, so the diagonal carries no rotational term (docs/contact_physics_followups.md
113 // §3.3, WO-B1).
114 const float wA = invMassA;
115 const float wB = invMassB;
116 const float wTotal = wA + wB;
117 if (wTotal < 1e-6f)
118 return;
119 // Mass-split Jacobi: the same numerator against copies of mass m / n.
120 const float wSolve =
121 massSplit ? static_cast<float>(constraintCounts(idA)) * wA +
122 ((idB >= 0) ? static_cast<float>(constraintCounts(idB)) * wB : 0.0f)
123 : wTotal;
124
125 const float dLambda = -C / wSolve;
126
127 // Translation-only correction (applyUpdatesKokkos commits deltaPos only).
128 Kokkos::atomic_add(&deltaPos(idA, 0), n.x * dLambda * invMassA);
129 Kokkos::atomic_add(&deltaPos(idA, 1), n.y * dLambda * invMassA);
130 Kokkos::atomic_add(&deltaPos(idA, 2), n.z * dLambda * invMassA);
131 if (idB >= 0) {
132 Kokkos::atomic_add(&deltaPos(idB, 0), -n.x * dLambda * invMassB);
133 Kokkos::atomic_add(&deltaPos(idB, 1), -n.y * dLambda * invMassB);
134 Kokkos::atomic_add(&deltaPos(idB, 2), -n.z * dLambda * invMassB);
135 if (!massSplit)
136 Kokkos::atomic_add(&constraintCounts(idB), 1);
137 }
138 if (!massSplit)
139 Kokkos::atomic_add(&constraintCounts(idA), 1);
140
141 if (C < 0.0f)
142 Kokkos::atomic_max(&maxOverlap(), -C);
143 });
144 space.fence();
145}
146
147// ============================ colored Gauss–Seidel position solve ============================
148// The overlap solve above is the position-side twin of the Jacobi restitution solve: one thread per
149// contact scatters an XPBD non-penetration correction, and the caller relaxes the per-body SUM by
150// the contact count (applyUpdatesKokkos, 1/count). Same trade-off — stable but under-converged, so
151// a body wedged by many neighbours keeps residual overlap. The colored path removes the averaging:
152// graph- colour the CONTACT graph (vertices = body slots, edges = contacts; raw bodyA/bodyB, the
153// same indices the solve writes) so no two contacts sharing a body get one colour, then sweep
154// colour-by- colour applying each correction IN PLACE. Within a colour the contacts are an
155// independent set, so the read-modify-write is race-free without atomics, and each sweep sees the
156// previous colours' moves — a true sequential projection that resolves stacked contacts far better
157// per iteration. Translation only, matching the Jacobi path (applyUpdatesKokkos applies deltaPos,
158// not deltaQuat; the angular contact response lives in the velocity solve), and it never touches
159// velocity — overlap removal stays decoupled from the velocity update.
160
162KOKKOS_INLINE_FUNCTION bool unitAsleep(const PosUnits& units,
163 const Kokkos::View<const unsigned char*, CpMem>& sleepMask,
164 int u) {
165 for (int k = units.begin(u); k < units.end(u); ++k)
166 if (!sleepMask(units.contact(k)))
167 return false;
168 return true;
169}
170
180inline int colorContactsKokkos(Kokkos::View<const ContactC*, CpMem> contacts, int numContacts,
181 int numBodies, Kokkos::View<int*, CpMem> cColor,
182 Kokkos::View<long long*, CpMem> bodyWinner,
183 Kokkos::View<std::uint64_t*, CpMem> bodyMask, int& leftover,
184 Kokkos::View<const unsigned char*, CpMem> sleepMask = {},
185 PosUnits units = {}, SlotOverride ov = {}, int vertexSpan = 0) {
186 leftover = 0;
187 CpExec space;
188 if (numContacts <= 0 || numBodies <= 0)
189 return 0;
190 // Colouring vertices: the raw contact body slot, or a hub copy slot through `ov` (§4.4);
191 // vertexSpan covers the copy slots (0 = numBodies, the no-copy span).
192 const int nV = vertexSpan > 0 ? vertexSpan : numBodies;
193 const bool sleepOn = sleepMask.extent(0) > 0;
194 Kokkos::parallel_for(
195 "peclet::dem::pcolor_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, nV),
196 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
197 Kokkos::parallel_for(
198 "peclet::dem::pcolor_init_contacts", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
199 KOKKOS_LAMBDA(int idx) {
200 cColor(idx) = (sleepOn && unitAsleep(units, sleepMask, idx)) ? -2 : -1;
201 });
202
203 int remaining = 1, prevRemaining = -1;
204 // Round cap (docs/contact_solve_framework.md §12 S7): the vertices coloured are the UNITS
205 // (numContacts here), so the bound is their count + 2. Each round commits at least the
206 // globally highest-key uncoloured unit (colorKey(leader) is unique: the leader index is its
207 // low word), so the loop terminates within numContacts rounds; O(log n) in practice.
208 const int maxRounds = numContacts + 2;
209 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
210 Kokkos::parallel_for(
211 "peclet::dem::pcolor_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, nV),
212 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
213 Kokkos::parallel_for(
214 "peclet::dem::pcolor_contend", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
215 KOKKOS_LAMBDA(int idx) {
216 if (cColor(idx) != -1)
217 return;
218 const int lead = units.leader(idx); // a unit's edge: its leader's bodies and key
219 const ContactC c = contacts(lead);
220 const long long key = colorKey(lead); // hashed priority (see solver_velocity.hpp)
221 Kokkos::atomic_max(&bodyWinner(ov.slotA(idx, c.bodyA)), key);
222 if (c.bodyB >= 0)
223 Kokkos::atomic_max(&bodyWinner(ov.slotB(idx, c.bodyB)), key);
224 });
225 int rem = 0;
226 Kokkos::parallel_reduce(
227 "peclet::dem::pcolor_commit", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
228 KOKKOS_LAMBDA(int idx, int& acc) {
229 if (cColor(idx) != -1)
230 return;
231 const int lead = units.leader(idx);
232 const ContactC c = contacts(lead);
233 const int ea = ov.slotA(idx, c.bodyA);
234 const int eb = (c.bodyB >= 0) ? ov.slotB(idx, c.bodyB) : -1; // <0: wall/boundary
235 const long long key = colorKey(lead);
236 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
237 acc += 1;
238 return;
239 }
240 std::uint64_t forbidden = bodyMask(ea);
241 if (eb >= 0)
242 forbidden |= bodyMask(eb);
243 int col = 0; // lowest free colour of 64; never forced (§4.2 item 1)
244 while (col < kColorPalette && ((forbidden >> col) & 1))
245 ++col;
246#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 3
247 if (col > 62)
248 col = 62; // G13 mutant 3: the old forced colour 62
249#endif
250 if (col == kColorPalette) {
251 cColor(idx) = kColorUncolourable; // no free colour: neither coloured nor remaining
252 return;
253 }
254 cColor(idx) = col;
255 const std::uint64_t bit = std::uint64_t(1) << col;
256 bodyMask(ea) |= bit;
257 if (eb >= 0)
258 bodyMask(eb) |= bit;
259 },
260 rem);
261 space.fence();
262 // Stall break: a safety bound only -- every round commits (or marks -3) the highest-key
263 // contender, so rem strictly decreases (§12 S7). Anything left -1 is a leftover.
264 if (rem == prevRemaining)
265 break;
266 prevRemaining = rem;
267 remaining = rem;
268 }
269
270 int maxc = -1;
271 Kokkos::parallel_reduce(
272 "peclet::dem::pcolor_max", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
273 KOKKOS_LAMBDA(int idx, int& mx) {
274 if (cColor(idx) > mx)
275 mx = cColor(idx);
276 },
277 Kokkos::Max<int>(maxc));
278 Kokkos::parallel_reduce(
279 "peclet::dem::pcolor_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
280 KOKKOS_LAMBDA(int idx, int& acc) {
281 if (cColor(idx) == -1 || cColor(idx) == kColorUncolourable)
282 acc += 1;
283 },
284 leftover);
285 space.fence();
286 return maxc + 1;
287}
288
299 Kokkos::View<const ContactC*, CpMem> contacts, int numContacts, int numBodies,
300 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
301 Kokkos::View<const int*, CpMem> prevColor, int prevCount, Kokkos::View<int*, CpMem> cColor,
302 Kokkos::View<unsigned long long*, CpMem> keysOut, Kokkos::View<long long*, CpMem> bodyWinner,
303 Kokkos::View<std::uint64_t*, CpMem> bodyMask, int& leftover, bool forceFull,
304 Kokkos::View<const unsigned char*, CpMem> sleepMask = {}, PosUnits units = {}) {
305 leftover = 0;
306 CpExec space;
307 if (numContacts <= 0 || numBodies <= 0)
308 return 0;
309 const bool full0 = forceFull || prevCount <= 0;
310 const bool sleepOn = sleepMask.extent(0) > 0;
311 // Key every contact and seed its colour from the carried ledger (or -1 on a full recolour, or
312 // -2 for a frozen both-asleep contact — excluded from the sweeps like an inactive one).
313 Kokkos::parallel_for(
314 "peclet::dem::pcolor_i_seed", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
315 KOKKOS_LAMBDA(int idx) {
316 // Every contact of the unit records its own pair key for the per-contact commit; the unit
317 // is seeded from its leader's key (identical to the per-contact seed for a one-point unit).
318 for (int k2 = units.begin(idx); k2 < units.end(idx); ++k2) {
319 const int ci = units.contact(k2);
320 keysOut(ci) = pairKey(contacts(ci));
321 }
322 const unsigned long long k = keysOut(units.leader(idx));
323 if (sleepOn && unitAsleep(units, sleepMask, idx)) {
324 cColor(idx) = -2;
325 return;
326 }
327 int col = -1;
328 if (!full0) {
329 int lo = 0, hi = prevCount;
330 while (lo < hi) {
331 const int mid = (lo + hi) >> 1;
332 if (prevKeys(mid) < k)
333 lo = mid + 1;
334 else
335 hi = mid;
336 }
337 if (lo < prevCount && prevKeys(lo) == k) {
338 const int pc = prevColor(lo);
339 if (pc >= 0)
340 col = pc;
341 }
342 }
343 cColor(idx) = col;
344 });
345 Kokkos::parallel_for(
346 "peclet::dem::pcolor_i_init_bodies", Kokkos::RangePolicy<CpExec>(space, 0, numBodies),
347 KOKKOS_LAMBDA(int i) { bodyMask(i) = 0; });
348 if (!full0) {
349 // Seed the per-body masks from carried colours, self-healing any conflict WITHOUT a host sync
350 // (a host readback here stalls the async submission pipeline and measured net-slower than the
351 // launches it saves). atomic_fetch_or serialises the claim: a contact that finds its colour bit
352 // already set at either endpoint DEMOTES itself to -1 and is re-arbitrated in the rounds below.
353 // A demoted contact may leave a spurious set bit at its other endpoint, but a spurious bit only
354 // over-constrains (forbids one colour there) — it never lets two same-colour contacts share a
355 // body, so the colouring stays valid. The sphere / non-periodic bed has no conflicts (one
356 // contact per pair, stable slots) and hits the pure carry path.
357 Kokkos::parallel_for(
358 "peclet::dem::pcolor_i_mask", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
359 KOKKOS_LAMBDA(int idx) {
360 const int col = cColor(idx);
361 if (col < 0)
362 return;
363 const ContactC c = contacts(units.leader(idx));
364 const std::uint64_t bit = std::uint64_t(1) << col;
365 bool conflict = ((Kokkos::atomic_fetch_or(&bodyMask(c.bodyA), bit) >> col) & 1) != 0;
366 if (c.bodyB >= 0)
367 conflict |= ((Kokkos::atomic_fetch_or(&bodyMask(c.bodyB), bit) >> col) & 1) != 0;
368 if (conflict)
369 cColor(idx) = -1; // re-arbitrate in the rounds
370 });
371 }
372 // Jones-Plassmann rounds over the uncoloured (-1) contacts only (identical to
373 // colorContactsKokkos).
374 int remaining = 1, prevRemaining = -1;
375 // Round cap (docs/contact_solve_framework.md §12 S7): the vertices coloured are the UNITS
376 // (numContacts here), so the bound is their count + 2. Each round commits at least the
377 // globally highest-key uncoloured unit (colorKey(leader) is unique: the leader index is its
378 // low word), so the loop terminates within numContacts rounds; O(log n) in practice.
379 const int maxRounds = numContacts + 2;
380 for (int round = 0; round < maxRounds && remaining > 0; ++round) {
381 Kokkos::parallel_for(
382 "peclet::dem::pcolor_i_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, numBodies),
383 KOKKOS_LAMBDA(int i) { bodyWinner(i) = -1; });
384 Kokkos::parallel_for(
385 "peclet::dem::pcolor_i_contend", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
386 KOKKOS_LAMBDA(int idx) {
387 if (cColor(idx) != -1)
388 return;
389 const int lead = units.leader(idx); // a unit's edge: its leader's bodies and key
390 const ContactC c = contacts(lead);
391 const long long key = colorKey(lead);
392 Kokkos::atomic_max(&bodyWinner(c.bodyA), key);
393 if (c.bodyB >= 0)
394 Kokkos::atomic_max(&bodyWinner(c.bodyB), key);
395 });
396 int rem = 0;
397 Kokkos::parallel_reduce(
398 "peclet::dem::pcolor_i_commit", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
399 KOKKOS_LAMBDA(int idx, int& acc) {
400 if (cColor(idx) != -1)
401 return;
402 const int lead = units.leader(idx);
403 const ContactC c = contacts(lead);
404 const int ea = c.bodyA;
405 const int eb = c.bodyB;
406 const long long key = colorKey(lead);
407 if (bodyWinner(ea) != key || (eb >= 0 && bodyWinner(eb) != key)) {
408 acc += 1;
409 return;
410 }
411 std::uint64_t forbidden = bodyMask(ea);
412 if (eb >= 0)
413 forbidden |= bodyMask(eb);
414 int col = 0; // lowest free colour of 64; never forced (§4.2 item 1)
415 while (col < kColorPalette && ((forbidden >> col) & 1))
416 ++col;
417#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 3
418 if (col > 62)
419 col = 62; // G13 mutant 3: the old forced colour 62
420#endif
421 if (col == kColorPalette) {
422 cColor(idx) = kColorUncolourable;
423 return;
424 }
425 cColor(idx) = col;
426 const std::uint64_t bit = std::uint64_t(1) << col;
427 bodyMask(ea) |= bit;
428 if (eb >= 0)
429 bodyMask(eb) |= bit;
430 },
431 rem);
432 space.fence();
433 if (rem == prevRemaining)
434 break;
435 prevRemaining = rem;
436 remaining = rem;
437 }
438 int maxc = -1;
439 Kokkos::parallel_reduce(
440 "peclet::dem::pcolor_i_max", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
441 KOKKOS_LAMBDA(int idx, int& mx) {
442 if (cColor(idx) > mx)
443 mx = cColor(idx);
444 },
445 Kokkos::Max<int>(maxc));
446 Kokkos::parallel_reduce(
447 "peclet::dem::pcolor_i_leftover", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
448 KOKKOS_LAMBDA(int idx, int& acc) {
449 if (cColor(idx) == -1 || cColor(idx) == kColorUncolourable)
450 acc += 1;
451 },
452 leftover);
453 space.fence();
454 return maxc + 1;
455}
456
458inline void expandUnitColorsKokkos(const PosUnits& units, int numUnits,
459 Kokkos::View<const int*, CpMem> unitColor,
460 Kokkos::View<int*, CpMem> contactColor) {
461 if (numUnits <= 0)
462 return;
463 CpExec space;
464 Kokkos::parallel_for(
465 "peclet::dem::pcolor_expand", Kokkos::RangePolicy<CpExec>(space, 0, numUnits),
466 KOKKOS_LAMBDA(int u) {
467 const int col = unitColor(u);
468 for (int k = units.begin(u); k < units.end(u); ++k)
469 contactColor(units.contact(k)) = col;
470 });
471}
472
474inline void commitContactColorKokkos(Kokkos::View<const unsigned long long*, CpMem> keys,
475 Kokkos::View<const int*, CpMem> color,
476 Kokkos::View<unsigned long long*, CpMem> prevKeys,
477 Kokkos::View<int*, CpMem> prevColor,
478 Kokkos::View<int*, CpMem> perm, int numContacts) {
479 if (numContacts <= 0)
480 return;
481 CpExec space;
482 const int n = numContacts;
483 const auto rng = Kokkos::pair<int, int>(0, n);
484 auto kd = Kokkos::subview(prevKeys, rng);
485 Kokkos::deep_copy(space, kd, Kokkos::subview(keys, rng));
486 Kokkos::parallel_for(
487 "peclet::dem::pcolor_commit_iota", Kokkos::RangePolicy<CpExec>(space, 0, n),
488 KOKKOS_LAMBDA(int i) { perm(i) = i; });
489 {
490 auto pd = Kokkos::subview(perm, rng);
491 Kokkos::Experimental::sort_by_key(space, kd, pd);
492 }
493 Kokkos::View<int*, CpMem> pc = prevColor;
494 Kokkos::View<const int*, CpMem> c = color;
495 Kokkos::parallel_for(
496 "peclet::dem::pcolor_commit_gather", Kokkos::RangePolicy<CpExec>(space, 0, n),
497 KOKKOS_LAMBDA(int i) { pc(i) = c(perm(i)); });
498 space.fence();
499}
500
505 Kokkos::View<const ContactC*, CpMem> contacts;
506 Kokkos::View<const float*, CpMem> invMass;
507 Kokkos::View<float* [3], CpMem> posPred;
508 Kokkos::View<const float* [4], CpMem> quatPred;
509 Kokkos::View<const float* [4], CpMem> quatStatic;
510 Kokkos::View<const float* [3], CpMem> invInertia;
511 Kokkos::View<float, CpMem> maxOverlap;
512 Kokkos::View<float*, CpMem> posLambdaAcc;
513
514 PosUnits units; // empty = one contact per work item (the identity)
515 // Hub-copy slot overrides per UNIT (docs/contact_solve_framework.md §4.4); empty = the raw
516 // contact bodies.
518 // Accumulated projection (WO-12, §13.5; USER 2026-09-26): with a stop residual view and the
519 // per-contact ledger posLambdaAcc, each contact's net push Lambda >= 0 is projected,
520 // Lambda' = max(0, Lambda - omega C / w), and the CHANGE d = Lambda' - Lambda is applied -- it
521 // may be negative (an overshoot is retracted), so over-relaxation omega in (0, 2) is legitimate
522 // and the fixed point is the unique least-displacement solution. posResidual collects max |d| w
523 // (the position change), the stop quantity; maxOverlap keeps "the largest violation seen". Empty
524 // posResidual = the pre-WO-12 incremental projection at omega 1 (kernel unit tests only).
525 Kokkos::View<float, CpMem> posResidual{};
526 float omega = 1.0f;
527
531 KOKKOS_FUNCTION void solveOne(int u) const {
532 for (int k = units.begin(u); k < units.end(u); ++k)
534 }
535
536 KOKKOS_FUNCTION void solveContact(int idx, int u) const {
537 const ContactC c = contacts(idx);
538 int idA = c.bodyA, idB = c.bodyB;
539 if (ov.a.extent(0) > 0) {
540 // The unit's overrides are keyed by its leader's (bodyA, bodyB); a contact of the unit may
541 // list the pair in either order, so map each end by body.
542 const ContactC L = contacts(units.leader(u));
543 idA = (c.bodyA == L.bodyA) ? ov.slotA(u, c.bodyA) : ov.slotB(u, c.bodyA);
544 if (c.bodyB >= 0)
545 idB = (c.bodyB == L.bodyA) ? ov.slotA(u, c.bodyB) : ov.slotB(u, c.bodyB);
546 }
547 const float invMassA = invMass(idA);
548 const float invMassB = (idB >= 0) ? invMass(idB) : 0.0f;
549
550 const F3 pA = ldF3(posPred, idA);
551 const F4 qA = ldF4(quatPred, idA);
552 F3 pB{0, 0, 0};
553 F4 qB{0, 0, 0, 1};
554 if (idB >= 0) {
555 pB = ldF3(posPred, idB);
556 qB = ldF4(quatPred, idB);
557 }
558
559 const F4 qAdelta = quatMult(qA, quatInverse(ldF4(quatStatic, idA)));
560 F3 rA = rotateVector(qAdelta, F3{c.rA.x, c.rA.y, c.rA.z});
561 F3 rB{c.rB.x, c.rB.y, c.rB.z};
562 F3 n{c.normal.x, c.normal.y, c.normal.z};
563 if (idB >= 0) {
564 const F4 qBdelta = quatMult(qB, quatInverse(ldF4(quatStatic, idB)));
565 rB = rotateVector(qBdelta, rB);
566 n = rotateVector(normalOfA(c) ? qAdelta : qBdelta, n); // the normal's own body
567 }
568
569 float C;
570 if (idB < 0) {
571 n = F3{c.normal.x, c.normal.y, c.normal.z};
572 const F3 pAsurf = add3(pA, rA);
573 C = dot3(sub3(pAsurf, F3{c.rB.x, c.rB.y, c.rB.z}), n);
574 } else {
575 const F3 pAc = add3(pA, rA);
576 const F3 pBc = add3(pB, rB);
577 C = dot3(sub3(pAc, pBc), n);
578 }
579 const bool accumulated = posResidual.extent(0) > 0 && posLambdaAcc.extent(0) > 0;
580 const float lam = accumulated ? posLambdaAcc(idx) : 0.0f;
581 if (C >= 0.0f && lam <= 0.0f)
582 return; // separated and never pushed: nothing to project or retract
583
584 // The translational effective mass of the solve views (invMass* = k invM under mass
585 // splitting; a wall side 0): the correction below is translation only, so the diagonal has
586 // no rotational term, and |dLambda| wTotal is the true relative position change
587 // (docs/contact_physics_followups.md §3.3, WO-B1).
588 const float wTotal = invMassA + invMassB;
589 if (wTotal < 1e-6f)
590 return;
591 if (C < 0.0f)
592 Kokkos::atomic_max(&maxOverlap(), -C);
593 float dLambda;
594 if (accumulated) {
595#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 7
596 // G13 mutant 7: over-relaxed WITHOUT retraction (the pre-WO-12 defect: a permanent gap).
597 const float lamNew = Kokkos::fmax(lam, lam - omega * C / wTotal);
598#else
599 const float lamNew = Kokkos::fmax(0.0f, lam - omega * C / wTotal);
600#endif
601 dLambda = lamNew - lam;
602 if (dLambda == 0.0f)
603 return;
604 posLambdaAcc(idx) = lamNew; // the unit's work item owns the contact: plain RMW
605 Kokkos::atomic_max(&posResidual(), Kokkos::fabs(dLambda) * wTotal);
606 } else {
607 dLambda = -C / wTotal;
608 if (posLambdaAcc.extent(0) > 0)
609 Kokkos::atomic_add(&posLambdaAcc(idx), dLambda);
610 }
611 // Position-channel normal load bookkeeping: posLambdaAcc is the contact's NET position
612 // impulse (the friction cone must see the TOTAL normal force; the caller converts it to
613 // impulse units and carries it into the next substep's Coulomb bound -- else a jostled bed's
614 // bound under-counts and stick leaks, measured as 99% sliding wall contacts in the drum).
615
616 // Translation-only correction, in place (rotation discarded to match applyUpdatesKokkos).
617 posPred(idA, 0) += n.x * dLambda * invMassA;
618 posPred(idA, 1) += n.y * dLambda * invMassA;
619 posPred(idA, 2) += n.z * dLambda * invMassA;
620 if (idB >= 0) {
621 posPred(idB, 0) += -n.x * dLambda * invMassB;
622 posPred(idB, 1) += -n.y * dLambda * invMassB;
623 posPred(idB, 2) += -n.z * dLambda * invMassB;
624 }
625 }
626};
627
637 Kokkos::View<const ContactC*, CpMem> contacts, const PosUnits& units, int numContacts,
638 Kokkos::View<const int*, CpMem> cColor, int numColors,
639 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<float* [3], CpMem> posPred,
640 Kokkos::View<const float* [4], CpMem> quatPred,
641 Kokkos::View<const float* [4], CpMem> quatStatic,
642 Kokkos::View<const float* [3], CpMem> invInertia, Kokkos::View<float, CpMem> maxOverlap,
643 Kokkos::View<float*, CpMem> posLambdaAcc = {}, Kokkos::View<const int*, CpMem> colorPerm = {},
644 const std::vector<int>* colorOffs = nullptr, const FusedSweepCtx* fused = nullptr,
645 const FusedLoopSpec* loop = nullptr, SlotOverride ov = {},
646 Kokkos::View<float, CpMem> posResidual = {}, float omega = 1.0f) {
647 CpExec space;
648 const PositionContactSweep f{contacts, invMass, posPred, quatPred, quatStatic, invInertia,
649 maxOverlap, posLambdaAcc, units, ov, posResidual, omega};
650#ifdef KOKKOS_ENABLE_CUDA
651 if (loop) {
652 // The device loop's stop residual: the position change under the accumulated projection
653 // (WO-12), else the overlap.
654 if (fused && fused->maxBucket > 0 && colorOffs)
655 return demLaunchFusedSweepLoop(
656 space, f, colorPerm, *fused, numColors, *loop,
657 posResidual.extent(0) > 0 ? posResidual.data() : maxOverlap.data());
658 return false;
659 }
660 if (fused && fused->maxBucket > 0 && colorOffs &&
661 demLaunchFusedColorSweep(space, f, colorPerm, *fused, numColors))
662 return true;
663#else
664 (void)fused;
665 if (loop)
666 return false;
667#endif
668 for (int color = 0; color < numColors; ++color) {
669 if (colorOffs) {
670 const int b = (*colorOffs)[color], e = (*colorOffs)[color + 1];
671 if (b == e)
672 continue;
673 Kokkos::parallel_for(
674 "peclet::dem::solve_position_gs", Kokkos::RangePolicy<CpExec>(space, b, e),
675 KOKKOS_LAMBDA(int i2) { f.solveOne(colorPerm(i2)); });
676 } else {
677 Kokkos::parallel_for(
678 "peclet::dem::solve_position_gs", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
679 KOKKOS_LAMBDA(int idx) {
680 if (cColor(idx) == color)
681 f.solveOne(idx);
682 });
683 }
684 // Stream-ordered on the device, so colour c+1 already sees colour c's moves — no host fence per
685 // colour (that would only stall the host). No trailing fence either: the caller's residual
686 // readback synchronizes, and a fence here would break CUDA-graph capture of the sweep.
687 }
688 return true;
689}
690
691} // namespace peclet::dem
692
693#endif // DEM_SOLVER_POSITION_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 computeW(F3 r, F3 dir, float invM, F3 invI)
void commitContactColorKokkos(Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const int *, CpMem > color, Kokkos::View< unsigned long long *, CpMem > prevKeys, Kokkos::View< int *, CpMem > prevColor, Kokkos::View< int *, CpMem > perm, int numContacts)
Commit this substep's per-contact (key, colour) sorted by key, for next substep's warm gather.
F4 quatMult(F4 a, F4 b)
void solvePositionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *[4], CpMem > quatPred, Kokkos::View< const float *[4], CpMem > quatStatic, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< float *[3], CpMem > deltaPos, Kokkos::View< float *[4], CpMem > deltaQuat, Kokkos::View< int *, CpMem > constraintCounts, Kokkos::View< float, CpMem > maxOverlap, Kokkos::View< const int *, CpMem > onlyColor={}, int colorFilter=0, bool massSplit=false)
Accumulate XPBD position corrections for numContacts contacts.
std::uint64_t pairKey(const ContactC &c)
Canonical pair key: (min<<32)|max, or (idA<<32)|0xFFFFFFFF for a boundary (idB<0) contact.
F4 quatInverse(F4 q)
int colorContactsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, int numBodies, Kokkos::View< int *, CpMem > cColor, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, PosUnits units={}, SlotOverride ov={}, int vertexSpan=0)
Greedy graph-colour the contacts (raw bodyA/bodyB): no two contacts sharing a body get the same colou...
F3 cross3v(F3 a, F3 b)
bool unitAsleep(const PosUnits &units, const Kokkos::View< const unsigned char *, CpMem > &sleepMask, int u)
A position unit is sleep-inactive iff all its contacts are sleep-masked (§4.3).
F3 ldF3(const V &v, int i)
F3 rotateVector(F4 q, F3 v)
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
CpExec::memory_space CpMem
F4 ldF4(const V &v, int i)
void countPositionJacobiKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< int *, CpMem > counts)
Mass-split Jacobi count pass (docs/contact_solve_framework.md §3.1, D3): per body slot,...
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
constexpr int kColorUncolourable
F3 add3(F3 a, F3 b)
constexpr int kColorPalette
Colouring palette (docs/contact_solve_framework.md §4.2 item 1): the greedy takes the lowest colour f...
bool normalOfA(const ContactC &c)
Which body's surface a contact's normal belongs to.
void expandUnitColorsKokkos(const PosUnits &units, int numUnits, Kokkos::View< const int *, CpMem > unitColor, Kokkos::View< int *, CpMem > contactColor)
Copy each position unit's colour onto its contacts (contactColor(i) = unitColor(unit of i)).
bool solvePositionColoredGSKokkos(Kokkos::View< const ContactC *, CpMem > contacts, const PosUnits &units, int numContacts, Kokkos::View< const int *, CpMem > cColor, int numColors, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *[3], CpMem > posPred, Kokkos::View< const float *[4], CpMem > quatPred, Kokkos::View< const float *[4], CpMem > quatStatic, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< float, CpMem > maxOverlap, Kokkos::View< float *, CpMem > posLambdaAcc={}, Kokkos::View< const int *, CpMem > colorPerm={}, const std::vector< int > *colorOffs=nullptr, const FusedSweepCtx *fused=nullptr, const FusedLoopSpec *loop=nullptr, SlotOverride ov={}, Kokkos::View< float, CpMem > posResidual={}, float omega=1.0f)
Colored Gauss–Seidel XPBD overlap solve: sweep the numColors colour classes in order,...
Kokkos::DefaultExecutionSpace CpExec
int colorContactsIncrementalKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, int numBodies, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const int *, CpMem > prevColor, int prevCount, Kokkos::View< int *, CpMem > cColor, Kokkos::View< unsigned long long *, CpMem > keysOut, Kokkos::View< long long *, CpMem > bodyWinner, Kokkos::View< std::uint64_t *, CpMem > bodyMask, int &leftover, bool forceFull, Kokkos::View< const unsigned char *, CpMem > sleepMask={}, PosUnits units={})
Incremental (warm-started) contact colouring for the single-GPU PGS position solve.
dem — fused colour sweeps: one persistent kernel per sweep instead of one kernel launch per colour.
Portable mirror of ParticleSystem.cuh ContactConstraint (the fields this reduction touches).
The position units of a contact list as a CSR (§4.3): unit u owns the contacts list(start(u)) .
The per-contact overlap-projection body lives in PositionContactSweep so the colored launch loop and ...
Kokkos::View< float *, CpMem > posLambdaAcc
Kokkos::View< const float *[4], CpMem > quatPred
Kokkos::View< float, CpMem > posResidual
Kokkos::View< const float *[4], CpMem > quatStatic
Kokkos::View< float, CpMem > maxOverlap
Kokkos::View< const float *[3], CpMem > invInertia
void solveContact(int idx, int u) const
Kokkos::View< const ContactC *, CpMem > contacts
Kokkos::View< float *[3], CpMem > posPred
void solveOne(int u) const
One work item: every contact of position unit u, sequentially in ascending contact index (a pair's po...
Kokkos::View< const float *, CpMem > invMass
Per-edge slot overrides of a phase's hub copies (docs/contact_solve_framework.md §4....
int slotA(int e, int def) const
int slotB(int e, int def) const
Kokkos::View< const int *, CpMem > a