peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
contact_preprocessing.hpp
Go to the documentation of this file.
1
16#ifndef DEM_CONTACT_PREPROCESSING_HPP
17#define DEM_CONTACT_PREPROCESSING_HPP
18
19#include <cstddef>
20#include <cstdint>
21#include <Kokkos_Core.hpp>
22#include <Kokkos_Sort.hpp>
23#include <utility>
24
25#include "dem_portable.hpp" // F4, cross3
26
27namespace peclet::dem {
28
29using CpExec = Kokkos::DefaultExecutionSpace;
30using CpMem = CpExec::memory_space;
31
33struct ContactC {
34 int bodyA;
35 int bodyB; // < 0 => boundary/static: -1 - wallIndex (wallContactId)
36 F4 normal; // .xyz = world normal
37 F4 rA; // lever arm on A
38 F4 rB; // lever arm on B
39 float dist; // signed penetration (>0 => inactive)
41 float weight;
42 // --- moving / per-material boundary (idB<0) extension; sentinels for body-body & static planes
43 // so the solvers fall back to the global material and a zero wall velocity (unchanged behaviour).
44 // ---
45 F4 boundaryVel{0.0f, 0.0f, 0.0f, 0.0f}; // wall surface velocity at the contact point (xyz)
46 float boundaryRestitution{-1.0f}; // per-wall normal restitution; < 0 => use the global one
47 float boundaryFriction{-1.0f}; // per-wall Coulomb friction; < 0 => use the global one
48};
49
54KOKKOS_INLINE_FUNCTION bool normalOfA(const ContactC& c) {
55 return c.normal.w != 0.0f;
56}
57
63KOKKOS_INLINE_FUNCTION constexpr int wallContactId(int wallIndex) {
64 return -1 - wallIndex;
65}
66KOKKOS_INLINE_FUNCTION constexpr int wallIndexOf(int bodyB) {
67 return -1 - bodyB;
68}
69
71struct ManifoldC {
72 int bodyA;
73 int bodyB;
80 // Σ over the manifold's active contacts of the boundary (idB<0) extension; the velocity solve
81 // averages by num_points. wallVel_sum -> the wall's surface velocity seen by this contact patch;
82 // restitution_sum -> per-wall restitution (a < 0 average keeps the global material).
83 F4 wallVel_sum{0.0f, 0.0f, 0.0f, 0.0f};
84 float restitution_sum{0.0f};
85 float friction_sum{0.0f}; // per-contact mu (pair table / wall); a < 0 average = global material
86};
87
92KOKKOS_INLINE_FUNCTION unsigned long long pairKeyFromGids(unsigned a, unsigned b) {
93 const unsigned hi = a < b ? a : b, lo = a < b ? b : a;
94 return (static_cast<unsigned long long>(hi) << 32) | lo;
95}
96
100template <class KeysView>
101KOKKOS_INLINE_FUNCTION int lowerBoundKey(const KeysView& keys, int count, unsigned long long key) {
102 int lo = 0, hi = count;
103 while (lo < hi) {
104 const int mid = (lo + hi) >> 1;
105 if (keys(mid) < key)
106 lo = mid + 1;
107 else
108 hi = mid;
109 }
110 return lo;
111}
112
118KOKKOS_INLINE_FUNCTION unsigned long long pairKeyOf(const ManifoldC& m,
119 Kokkos::View<const int*, CpMem> realIdx) {
120 const unsigned a = static_cast<unsigned>(realIdx(m.bodyA));
121 const unsigned b = (m.bodyB >= 0) ? static_cast<unsigned>(realIdx(m.bodyB)) : 0xFFFFFFFFu;
122 return pairKeyFromGids(a, b);
123}
124
128inline void markPersistentManifoldsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
129 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
130 Kokkos::View<const int*, CpMem> keyIdx,
131 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
132 int prevCount,
133 Kokkos::View<unsigned long long*, CpMem> outKeys,
134 Kokkos::View<unsigned char*, CpMem> outFlags) {
135 (void)realIdx;
136 CpExec space;
137 Kokkos::parallel_for(
138 "peclet::dem::mark_persistent", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
139 KOKKOS_LAMBDA(int idx) {
140 const ManifoldC m = manifolds(idx);
141 if (m.num_points <= 0) {
142 outKeys(idx) = ~0ull; // hi = 0xFFFFFFFF: can never match a live manifold's key
143 outFlags(idx) = 0;
144 return;
145 }
146 const unsigned long long k = pairKeyOf(m, keyIdx);
147 outKeys(idx) = k;
148 const int lo = lowerBoundKey(prevKeys, prevCount, k);
149 outFlags(idx) = (lo < prevCount && prevKeys(lo) == k) ? 1 : 0;
150 });
151}
152
159 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
160 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const int*, CpMem> keyIdx,
161 Kokkos::View<const unsigned long long*, CpMem> prevKeys,
162 Kokkos::View<const float*, CpMem> prevLambda, Kokkos::View<const float* [3], CpMem> prevLambdaT,
163 Kokkos::View<const float*, CpMem> prevPosImpulse,
164 Kokkos::View<const float*, CpMem> prevRestBank, Kokkos::View<const float*, CpMem> prevRestVPeak,
165 int prevCount, Kokkos::View<unsigned long long*, CpMem> outKeys,
166 Kokkos::View<float*, CpMem> outWarm, Kokkos::View<float* [3], CpMem> outWarmT,
167 Kokkos::View<float*, CpMem> outPosImpulse, Kokkos::View<float*, CpMem> outRestBank,
168 Kokkos::View<float*, CpMem> outRestVPeak, Kokkos::View<unsigned char*, CpMem> outMatched = {},
169 bool dedupTwins = true) {
170 CpExec space;
171 Kokkos::parallel_for(
172 "peclet::dem::gather_warm", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
173 KOKKOS_LAMBDA(int idx) {
174 const ManifoldC m = manifolds(idx);
175 bool dup = false;
176 if (dedupTwins && m.num_points > 0 && m.bodyB >= 0 && realIdx(m.bodyA) > realIdx(m.bodyB))
177 dup = true; // periodic dedup (single rank; §4.2 item 2)
178 if (m.num_points <= 0 || dup) {
179 outKeys(idx) = ~0ull;
180 outWarm(idx) = 0.0f;
181 outWarmT(idx, 0) = outWarmT(idx, 1) = outWarmT(idx, 2) = 0.0f;
182 outPosImpulse(idx) = 0.0f;
183 outRestBank(idx) = 0.0f;
184 outRestVPeak(idx) = 0.0f;
185 return;
186 }
187 const unsigned long long k = pairKeyOf(m, keyIdx);
188 outKeys(idx) = k;
189 const int lo = lowerBoundKey(prevKeys, prevCount, k);
190 const bool hit = (lo < prevCount && prevKeys(lo) == k);
191 if (hit && outMatched.extent(0) > 0)
192 outMatched(lo) = 1; // prev entry survives; unmatched entries orphan their bank
193 outWarm(idx) = hit ? prevLambda(lo) : 0.0f;
194 outWarmT(idx, 0) = hit ? prevLambdaT(lo, 0) : 0.0f;
195 outWarmT(idx, 1) = hit ? prevLambdaT(lo, 1) : 0.0f;
196 outWarmT(idx, 2) = hit ? prevLambdaT(lo, 2) : 0.0f;
197 outPosImpulse(idx) = hit ? prevPosImpulse(lo) : 0.0f;
198 outRestBank(idx) = hit ? prevRestBank(lo) : 0.0f;
199 outRestVPeak(idx) = hit ? prevRestVPeak(lo) : 0.0f;
200 });
201}
202
206 Kokkos::View<const unsigned long long*, CpMem> keys, Kokkos::View<const float*, CpMem> lambda,
207 Kokkos::View<const float* [3], CpMem> lambdaT, Kokkos::View<const float*, CpMem> restBank,
208 Kokkos::View<const float*, CpMem> restVPeak, Kokkos::View<unsigned long long*, CpMem> prevKeys,
209 Kokkos::View<float*, CpMem> prevLambda, Kokkos::View<float* [3], CpMem> prevLambdaT,
210 Kokkos::View<float*, CpMem> prevRestBank, Kokkos::View<float*, CpMem> prevRestVPeak,
211 Kokkos::View<int*, CpMem> perm, // pooled scratch, >= n
212 int numManifolds,
213 // Optional: carry the per-manifold colour by pair key too
214 // (single-GPU incremental colouring; empty views = skip).
215 Kokkos::View<const int*, CpMem> color = {}, Kokkos::View<int*, CpMem> prevColor = {}) {
216 if (numManifolds <= 0)
217 return;
218 CpExec space;
219 const int n = numManifolds;
220 const auto rng = Kokkos::pair<int, int>(0, n);
221 auto kd = Kokkos::subview(prevKeys, rng);
222 Kokkos::deep_copy(space, kd, Kokkos::subview(keys, rng));
223 Kokkos::parallel_for(
224 "peclet::dem::commit_iota", Kokkos::RangePolicy<CpExec>(space, 0, n),
225 KOKKOS_LAMBDA(int i) { perm(i) = i; });
226 {
227 auto pd = Kokkos::subview(perm, rng);
228 Kokkos::Experimental::sort_by_key(space, kd, pd);
229 }
230 Kokkos::View<float*, CpMem> pl = prevLambda;
231 Kokkos::View<float* [3], CpMem> plt = prevLambdaT;
232 Kokkos::View<float*, CpMem> prb = prevRestBank;
233 Kokkos::View<float*, CpMem> prv = prevRestVPeak;
234 const bool carryColor = color.extent(0) > 0 && prevColor.extent(0) > 0;
235 Kokkos::View<const int*, CpMem> col = color;
236 Kokkos::View<int*, CpMem> pcol = prevColor;
237 Kokkos::parallel_for(
238 "peclet::dem::commit_gather", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
239 const int j = perm(i);
240 pl(i) = lambda(j);
241 plt(i, 0) = lambdaT(j, 0);
242 plt(i, 1) = lambdaT(j, 1);
243 plt(i, 2) = lambdaT(j, 2);
244 prb(i) = restBank(j);
245 prv(i) = restVPeak(j);
246 if (carryColor)
247 pcol(i) = col(j);
248 });
249 space.fence();
250}
251
253inline void commitPairKeysKokkos(Kokkos::View<const unsigned long long*, CpMem> keys,
254 Kokkos::View<unsigned long long*, CpMem> prevKeys,
255 int numManifolds) {
256 if (numManifolds <= 0)
257 return;
258 CpExec space;
259 auto src = Kokkos::subview(keys, Kokkos::pair<int, int>(0, numManifolds));
260 auto dst = Kokkos::subview(prevKeys, Kokkos::pair<int, int>(0, numManifolds));
261 Kokkos::deep_copy(space, dst, src);
262 Kokkos::sort(space, dst);
263 space.fence();
264}
265
271inline void updateGroundedLevelsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
272 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
273 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
274 Kokkos::View<unsigned char*, CpMem> grounded, int numReal,
275 int sweeps, int decay) {
276 CpExec space;
277 Kokkos::parallel_for(
278 "peclet::dem::grounded_decay", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
279 KOKKOS_LAMBDA(int i) {
280 const int g = static_cast<int>(grounded(i)) - decay;
281 grounded(i) = static_cast<unsigned char>(g > 0 ? g : 0);
282 });
283 for (int s = 0; s < sweeps; ++s) {
284 Kokkos::parallel_for(
285 "peclet::dem::grounded_sweep", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
286 KOKKOS_LAMBDA(int idx) {
287 const ManifoldC m = manifolds(idx);
288 if (m.num_points <= 0)
289 return;
290 const int realA = realIdx(m.bodyA);
291 if (m.bodyB < 0) {
292 Kokkos::atomic_max(&grounded(realA), static_cast<unsigned char>(255));
293 return;
294 }
295 const int realB = realIdx(m.bodyB);
296 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
297 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
298 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
299 if (up > thr) { // B supports A
300 const int lvl = static_cast<int>(grounded(realB)) - 1;
301 if (lvl > 0)
302 Kokkos::atomic_max(&grounded(realA), static_cast<unsigned char>(lvl));
303 } else if (up < -thr) { // A supports B
304 const int lvl = static_cast<int>(grounded(realA)) - 1;
305 if (lvl > 0)
306 Kokkos::atomic_max(&grounded(realB), static_cast<unsigned char>(lvl));
307 }
308 });
309 }
310}
311
319inline constexpr int kLevelInf = 1 << 28;
320inline void computeHeightLevelsKokkos(Kokkos::View<const ManifoldC*, CpMem> manifolds,
321 int numManifolds, Kokkos::View<const int*, CpMem> realIdx,
322 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
323 Kokkos::View<int*, CpMem> heights, int numReal) {
324 CpExec space;
325 Kokkos::parallel_for(
326 "peclet::dem::height_init", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
327 KOKKOS_LAMBDA(int i) { heights(i) = kLevelInf; });
328 const int maxSweeps = 1024; // >= deepest supported column; early exit ends real runs sooner
329 for (int s = 0; s < maxSweeps; ++s) {
330 int changed = 0;
331 Kokkos::parallel_reduce(
332 "peclet::dem::height_sweep", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
333 KOKKOS_LAMBDA(int idx, int& acc) {
334 const ManifoldC m = manifolds(idx);
335 if (m.num_points <= 0)
336 return;
337 const int realA = realIdx(m.bodyA);
338 if (m.bodyB < 0) {
339 if (Kokkos::atomic_fetch_min(&heights(realA), 0) > 0)
340 acc += 1;
341 return;
342 }
343 const int realB = realIdx(m.bodyB);
344 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
345 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z); // >0: A above B
346 const float thr = 0.3f * Kokkos::sqrt(dot3(dx, dx));
347 if (up > thr) { // B supports A
348 const int cand = heights(realB);
349 if (cand < kLevelInf && Kokkos::atomic_fetch_min(&heights(realA), cand + 1) > cand + 1)
350 acc += 1;
351 } else if (up < -thr) { // A supports B
352 const int cand = heights(realA);
353 if (cand < kLevelInf && Kokkos::atomic_fetch_min(&heights(realB), cand + 1) > cand + 1)
354 acc += 1;
355 }
356 },
357 changed);
358 if (changed == 0)
359 break;
360 }
361 space.fence();
362}
363
371inline void buildColorBucketsKokkos(Kokkos::View<const int*, CpMem> colorOf, int n, int numColors,
372 Kokkos::View<int*, CpMem> perm,
373 Kokkos::View<int*, CpMem> cursor, // scratch, >= 64 ints
374 std::vector<int>& offs) {
375 offs.assign(static_cast<std::size_t>(numColors) + 1, 0);
376 if (n <= 0 || numColors <= 0)
377 return;
378 CpExec space;
379 auto cnt = Kokkos::subview(cursor, Kokkos::pair<int, int>(0, numColors));
380 Kokkos::deep_copy(space, cnt, 0);
381 Kokkos::parallel_for(
382 "peclet::dem::bucket_hist", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
383 const int c = colorOf(i);
384 if (c >= 0)
385 Kokkos::atomic_add(&cursor(c), 1);
386 });
387 auto hCnt = Kokkos::create_mirror_view(cnt);
388 Kokkos::deep_copy(space, hCnt, cnt);
389 space.fence();
390 for (int c = 0; c < numColors; ++c)
391 offs[static_cast<std::size_t>(c) + 1] = offs[static_cast<std::size_t>(c)] + hCnt(c);
392 for (int c = 0; c < numColors; ++c)
393 hCnt(c) = offs[static_cast<std::size_t>(c)];
394 Kokkos::deep_copy(space, cnt, hCnt);
395 Kokkos::parallel_for(
396 "peclet::dem::bucket_scatter", Kokkos::RangePolicy<CpExec>(space, 0, n),
397 KOKKOS_LAMBDA(int i) {
398 const int c = colorOf(i);
399 if (c >= 0)
400 perm(Kokkos::atomic_fetch_add(&cursor(c), 1)) = i;
401 });
402}
403
407KOKKOS_INLINE_FUNCTION long long colorKey(int idx) {
408 unsigned int z = static_cast<unsigned>(idx) + 0x9e3779b9u;
409 z = (z ^ (z >> 16)) * 0x21f0aaadu;
410 z = (z ^ (z >> 15)) * 0x735a2d97u;
411 z ^= (z >> 15);
412 // priority in the high word, unique index in the low word (unique key per edge; >= 0)
413 return (static_cast<long long>(z & 0x7fffffffu) << 32) | static_cast<unsigned>(idx);
414}
415
417KOKKOS_INLINE_FUNCTION std::uint64_t pairKey(const ContactC& c) {
418 const int idA = c.bodyA, idB = c.bodyB;
419 if (idB < 0)
420 return (static_cast<std::uint64_t>(static_cast<unsigned>(idA)) << 32) | 0xFFFFFFFFu;
421 const unsigned u = (idA < idB) ? idA : idB;
422 const unsigned v = (idA < idB) ? idB : idA;
423 return (static_cast<std::uint64_t>(u) << 32) | v;
424}
425
429inline constexpr std::uint64_t kNonOwnedPairBit = 1ull << 63;
430
435inline constexpr int kColorPalette = 64;
436inline constexpr int kColorUncolourable = -3;
437
445 Kokkos::View<const int*, CpMem> a, b;
446 Kokkos::View<const unsigned char*, CpMem> split; // per slot: 1 = its body has k > 1
447 float omega = 1.0f; // the phase's split relaxation (§4.5)
448 KOKKOS_INLINE_FUNCTION int slotA(int e, int def) const {
449 if (a.extent(0) > 0) {
450 const int s = a(e);
451 if (s >= 0)
452 return s;
453 }
454 return def;
455 }
456 KOKKOS_INLINE_FUNCTION int slotB(int e, int def) const {
457 if (b.extent(0) > 0) {
458 const int s = b(e);
459 if (s >= 0)
460 return s;
461 }
462 return def;
463 }
465 KOKKOS_INLINE_FUNCTION bool relax(int sA, int sB) const {
466 return split.extent(0) > 0 && (split(sA) != 0 || (sB >= 0 && split(sB) != 0));
467 }
468};
469
472KOKKOS_INLINE_FUNCTION void decodeKey(std::uint64_t key, int& bodyA, int& bodyB) {
473 const unsigned v = static_cast<unsigned>(key & 0xFFFFFFFFu);
474 bodyA = static_cast<int>((key >> 32) & 0x7FFFFFFFu);
475 bodyB = (v == 0xFFFFFFFFu) ? -1 : static_cast<int>(v);
476}
477
481KOKKOS_INLINE_FUNCTION ManifoldC transformContact(const ContactC& c) {
482 ManifoldC m{};
483 const int idA = c.bodyA, idB = c.bodyB;
484 if (c.dist > 0.0f) {
485 m.num_points = 0;
486 return m; // inactive (sums already zero)
487 }
488 const bool flip = (idB >= 0 && idB < idA);
489 m.num_points = 1;
490
491 const F4 n_vec = c.normal;
492 F4 n_aligned = flip ? n_vec : F4{-n_vec.x, -n_vec.y, -n_vec.z, 0.0f};
493
494 const F4 shift{c.normal.x * c.dist * 0.5f, c.normal.y * c.dist * 0.5f, c.normal.z * c.dist * 0.5f,
495 0.0f};
496 const F4 rA_mid{c.rA.x - shift.x, c.rA.y - shift.y, c.rA.z - shift.z, 0.0f};
497 const F4 rB_mid{c.rB.x + shift.x, c.rB.y + shift.y, c.rB.z + shift.z, 0.0f};
498
499 const F4 r_can = flip ? rB_mid : rA_mid;
500 const F4 r_other = flip ? rA_mid : rB_mid;
501
502 const F4 tau_can = cross3(r_can, n_aligned);
503 const F4 tau_other = cross3(r_other, F4{-n_aligned.x, -n_aligned.y, -n_aligned.z, 0.0f});
504
505 m.normal_sum = F4{n_aligned.x, n_aligned.y, n_aligned.z, 0.0f};
506 m.torque_armA_sum = F4{tau_can.x, tau_can.y, tau_can.z, 0.0f};
507 m.torque_armB_sum = F4{tau_other.x, tau_other.y, tau_other.z, 0.0f};
508 m.rA_sum = F4{r_can.x, r_can.y, r_can.z, 0.0f};
509 m.rB_sum = F4{r_other.x, r_other.y, r_other.z, 0.0f};
510 // Boundary (idB<0) moving-wall extension: carry the wall velocity + per-wall restitution through
511 // to the (count-averaged) velocity solve. Zero / -1 sentinel for body-body & static planes.
512 m.wallVel_sum = F4{c.boundaryVel.x, c.boundaryVel.y, c.boundaryVel.z, 0.0f};
513 m.restitution_sum = c.boundaryRestitution;
514 m.friction_sum = c.boundaryFriction;
515 return m;
516}
517
523template <class Owns>
524inline int partitionContactsKokkos(Kokkos::View<ContactC*, CpMem> contacts, int n,
525 const Owns& owns) {
526 if (n == 0)
527 return 0;
528 CpExec space;
529 Kokkos::View<int*, CpMem> before(
530 Kokkos::view_alloc(space, "peclet::dem::cp::partBefore", Kokkos::WithoutInitializing), n);
531 Kokkos::View<ContactC*, CpMem> ct = contacts;
532 int ncOwned = 0;
533 Kokkos::parallel_scan(
534 "peclet::dem::cp::partScan", Kokkos::RangePolicy<CpExec>(space, 0, n),
535 KOKKOS_LAMBDA(int i, int& run, const bool final) {
536 const int f = owns(ct(i)) ? 1 : 0;
537 if (final)
538 before(i) = run; // exclusive
539 run += f;
540 },
541 ncOwned);
542 Kokkos::View<ContactC*, CpMem> part(
543 Kokkos::view_alloc(space, "peclet::dem::cp::part", Kokkos::WithoutInitializing), n);
544 const int no = ncOwned;
545 Kokkos::parallel_for(
546 "peclet::dem::cp::partScatter", Kokkos::RangePolicy<CpExec>(space, 0, n),
547 KOKKOS_LAMBDA(int i) {
548 const bool f = owns(ct(i));
549 part(f ? before(i) : no + (i - before(i))) = ct(i);
550 });
551 Kokkos::deep_copy(space,
552 Kokkos::subview(contacts, std::pair<std::size_t, std::size_t>(
553 0, static_cast<std::size_t>(n))),
554 part);
555 space.fence();
556 return ncOwned;
557}
558
567inline int reduceContactsToManifoldsKokkos(Kokkos::View<const ContactC*, CpMem> contacts, int n,
568 Kokkos::View<ManifoldC*, CpMem> outManifolds,
569 Kokkos::View<int, CpMem> outCount,
570 Kokkos::View<int*, CpMem> contactSlot = {},
571 int numOwnedContacts = -1,
572 int* numOwnedManifolds = nullptr) {
573 CpExec space;
574 if (n == 0) {
575 Kokkos::deep_copy(space, outCount, 0);
576 if (numOwnedManifolds)
577 *numOwnedManifolds = 0;
578 return 0;
579 }
580
581 // 1. Key every contact and seed an identity permutation.
582 Kokkos::View<std::uint64_t*, CpMem> keys(
583 Kokkos::view_alloc(space, "peclet::dem::cp::keys", Kokkos::WithoutInitializing), n);
584 Kokkos::View<int*, CpMem> perm(
585 Kokkos::view_alloc(space, "peclet::dem::cp::perm", Kokkos::WithoutInitializing), n);
586 const int nOwnedC = numOwnedContacts;
587 Kokkos::parallel_for(
588 "peclet::dem::cp::key", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
589 keys(i) = pairKey(contacts(i)) |
590 ((nOwnedC >= 0 && i >= nOwnedC) ? kNonOwnedPairBit : std::uint64_t{0});
591 perm(i) = i;
592 });
593
594 // 2. Sort the permutation by key (groups equal pairs contiguously).
595 Kokkos::Experimental::sort_by_key(space, keys, perm);
596
597 // 3. Segment id per sorted position: inclusive scan of "key changed" minus 1.
598 Kokkos::View<int*, CpMem> segId(
599 Kokkos::view_alloc(space, "peclet::dem::cp::segId", Kokkos::WithoutInitializing), n);
600 int numSeg = 0;
601 Kokkos::parallel_scan(
602 "peclet::dem::cp::segscan", Kokkos::RangePolicy<CpExec>(space, 0, n),
603 KOKKOS_LAMBDA(int p, int& run, const bool final) {
604 const bool isNew = (p == 0) || (keys(p) != keys(p - 1));
605 if (isNew)
606 ++run;
607 if (final)
608 segId(p) = run - 1; // 0-based segment index
609 },
610 numSeg);
611 if (numOwnedManifolds) { // owned segment leaders (their keys carry no kNonOwnedPairBit)
612 Kokkos::View<std::uint64_t*, CpMem> kk = keys;
613 int nOwnedM = 0;
614 Kokkos::parallel_reduce(
615 "peclet::dem::cp::ownedSegs", Kokkos::RangePolicy<CpExec>(space, 0, n),
616 KOKKOS_LAMBDA(int p, int& cnt) {
617 const bool leader = (p == 0) || (kk(p) != kk(p - 1));
618 if (leader && (kk(p) & kNonOwnedPairBit) == 0)
619 ++cnt;
620 },
621 nOwnedM);
622 *numOwnedManifolds = nOwnedM;
623 }
624
625 // 4. Initialise one manifold per segment (canonical ids from the key, sums zero).
626 Kokkos::View<std::uint64_t*, CpMem> k = keys;
627 Kokkos::View<int*, CpMem> sid = segId;
628 Kokkos::View<ManifoldC*, CpMem> out = outManifolds;
629 Kokkos::parallel_for(
630 "peclet::dem::cp::init", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int p) {
631 const bool leader = (p == 0) || (k(p) != k(p - 1));
632 if (leader) {
633 ManifoldC m{};
634 decodeKey(k(p), m.bodyA, m.bodyB);
635 out(sid(p)) = m;
636 }
637 });
638
639 // 5. Accumulate each contact's transformed manifold into its segment (atomic; order-independent).
640 Kokkos::View<const ContactC*, CpMem> ct = contacts;
641 Kokkos::View<int*, CpMem> pm = perm;
642 Kokkos::parallel_for(
643 "peclet::dem::cp::accum", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int p) {
644 const ManifoldC m = transformContact(ct(pm(p)));
645 const int s = sid(p);
646 Kokkos::atomic_add(&out(s).num_points, m.num_points);
647 Kokkos::atomic_add(&out(s).normal_sum.x, m.normal_sum.x);
648 Kokkos::atomic_add(&out(s).normal_sum.y, m.normal_sum.y);
649 Kokkos::atomic_add(&out(s).normal_sum.z, m.normal_sum.z);
650 Kokkos::atomic_add(&out(s).torque_armA_sum.x, m.torque_armA_sum.x);
651 Kokkos::atomic_add(&out(s).torque_armA_sum.y, m.torque_armA_sum.y);
652 Kokkos::atomic_add(&out(s).torque_armA_sum.z, m.torque_armA_sum.z);
653 Kokkos::atomic_add(&out(s).torque_armB_sum.x, m.torque_armB_sum.x);
654 Kokkos::atomic_add(&out(s).torque_armB_sum.y, m.torque_armB_sum.y);
655 Kokkos::atomic_add(&out(s).torque_armB_sum.z, m.torque_armB_sum.z);
656 Kokkos::atomic_add(&out(s).rA_sum.x, m.rA_sum.x);
657 Kokkos::atomic_add(&out(s).rA_sum.y, m.rA_sum.y);
658 Kokkos::atomic_add(&out(s).rA_sum.z, m.rA_sum.z);
659 Kokkos::atomic_add(&out(s).rB_sum.x, m.rB_sum.x);
660 Kokkos::atomic_add(&out(s).rB_sum.y, m.rB_sum.y);
661 Kokkos::atomic_add(&out(s).rB_sum.z, m.rB_sum.z);
662 Kokkos::atomic_add(&out(s).wallVel_sum.x, m.wallVel_sum.x);
663 Kokkos::atomic_add(&out(s).wallVel_sum.y, m.wallVel_sum.y);
664 Kokkos::atomic_add(&out(s).wallVel_sum.z, m.wallVel_sum.z);
665 Kokkos::atomic_add(&out(s).restitution_sum, m.restitution_sum);
666 Kokkos::atomic_add(&out(s).friction_sum, m.friction_sum);
667 });
668 // Optional contact -> manifold slot map (PGS friction bound reads lambdaAcc through it).
669 if (contactSlot.extent(0) >= (size_t)n) {
670 Kokkos::View<int*, CpMem> cs = contactSlot;
671 Kokkos::parallel_for(
672 "peclet::dem::cp::slotmap", Kokkos::RangePolicy<CpExec>(space, 0, n),
673 KOKKOS_LAMBDA(int p) { cs(pm(p)) = sid(p); });
674 }
675 space.fence();
676
677 Kokkos::deep_copy(space, outCount, numSeg);
678 return numSeg;
679}
680
685KOKKOS_INLINE_FUNCTION std::uint64_t unitKey(const ContactC& c) {
686 if (c.bodyB < 0)
687 return (static_cast<std::uint64_t>(static_cast<unsigned>(c.bodyA)) << 32) |
688 static_cast<unsigned>(c.bodyB);
689 return pairKey(c);
690}
691
697struct PosUnits {
698 Kokkos::View<const int*, CpMem> start; // numUnits + 1
699 Kokkos::View<const int*, CpMem> list; // numContacts
700 KOKKOS_INLINE_FUNCTION bool on() const { return start.extent(0) > 0; }
701 KOKKOS_INLINE_FUNCTION int begin(int u) const { return on() ? start(u) : u; }
702 KOKKOS_INLINE_FUNCTION int end(int u) const { return on() ? start(u + 1) : u + 1; }
703 KOKKOS_INLINE_FUNCTION int contact(int k) const { return on() ? list(k) : k; }
704 KOKKOS_INLINE_FUNCTION int leader(int u) const { return on() ? list(start(u)) : u; }
705};
706
716inline int buildPositionUnitsKokkos(Kokkos::View<const ContactC*, CpMem> contacts, int n,
717 Kokkos::View<int*, CpMem> unitStart,
718 Kokkos::View<int*, CpMem> unitContacts, int numManifolds = -1) {
719 CpExec space;
720 if (n <= 0) {
721 Kokkos::deep_copy(space, Kokkos::subview(unitStart, 0), 0);
722 space.fence();
723 return 0;
724 }
725 auto writeIdentity = [&]() {
726 Kokkos::View<int*, CpMem> us = unitStart;
727 Kokkos::View<int*, CpMem> uc = unitContacts;
728 Kokkos::parallel_for(
729 "peclet::dem::cp::uident", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int p) {
730 us(p) = p;
731 uc(p) = p;
732 });
733 Kokkos::deep_copy(space, Kokkos::subview(unitStart, n), n);
734 space.fence();
735 return n;
736 };
737 if (numManifolds == n)
738 return writeIdentity();
739 using Kokkos::view_alloc;
740 using Kokkos::WithoutInitializing;
741 Kokkos::View<std::uint64_t*, CpMem> keys(
742 view_alloc(space, "peclet::dem::cp::ukeys", WithoutInitializing), n);
743 Kokkos::View<int*, CpMem> perm(view_alloc(space, "peclet::dem::cp::uperm", WithoutInitializing),
744 n);
745 Kokkos::parallel_for(
746 "peclet::dem::cp::ukey", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
747 keys(i) = unitKey(contacts(i));
748 perm(i) = i;
749 });
750 Kokkos::Experimental::sort_by_key(space, keys, perm);
751 Kokkos::View<int*, CpMem> segId(view_alloc(space, "peclet::dem::cp::useg", WithoutInitializing),
752 n);
753 int numSeg = 0;
754 Kokkos::parallel_scan(
755 "peclet::dem::cp::usegscan", Kokkos::RangePolicy<CpExec>(space, 0, n),
756 KOKKOS_LAMBDA(int p, int& run, const bool final) {
757 if (p == 0 || keys(p) != keys(p - 1))
758 ++run;
759 if (final)
760 segId(p) = run - 1;
761 },
762 numSeg);
763 if (numSeg == n) {
764 // Every unit is one contact (spheres): the leader is the contact itself and the canonical
765 // order is the contact order, so the CSR is the identity -- written directly, without the
766 // second sort. The sweeps then take the empty PosUnits (no indirection; bitwise identical).
767 return writeIdentity();
768 }
769 Kokkos::View<int*, CpMem> segLead(
770 view_alloc(space, "peclet::dem::cp::ulead", WithoutInitializing), numSeg);
771 Kokkos::deep_copy(space, segLead, n);
772 Kokkos::parallel_for(
773 "peclet::dem::cp::uleadmin", Kokkos::RangePolicy<CpExec>(space, 0, n),
774 KOKKOS_LAMBDA(int p) { Kokkos::atomic_min(&segLead(segId(p)), perm(p)); });
775 // Second key: (leader << 32) | contact index -- unique, so the order does not depend on the
776 // sort's stability: units by ascending leader, contacts ascending inside each unit. perm(p)
777 // already holds the key's low word (the contact index), so it is the value the sort carries.
778 Kokkos::parallel_for(
779 "peclet::dem::cp::ukey2", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int p) {
780 const int i = perm(p);
781 keys(p) = (static_cast<std::uint64_t>(static_cast<unsigned>(segLead(segId(p)))) << 32) |
782 static_cast<unsigned>(i);
783 });
784 Kokkos::Experimental::sort_by_key(space, keys, perm);
785 Kokkos::View<int*, CpMem> us = unitStart;
786 Kokkos::View<int*, CpMem> uc = unitContacts;
787 int nu = 0;
788 Kokkos::parallel_scan(
789 "peclet::dem::cp::ustart", Kokkos::RangePolicy<CpExec>(space, 0, n),
790 KOKKOS_LAMBDA(int p, int& run, const bool final) {
791 const bool isNew = (p == 0) || ((keys(p) >> 32) != (keys(p - 1) >> 32));
792 if (final) {
793 uc(p) = perm(p);
794 if (isNew)
795 us(run) = p;
796 }
797 if (isNew)
798 ++run;
799 },
800 nu);
801 Kokkos::deep_copy(space, Kokkos::subview(unitStart, nu), n);
802 space.fence();
803 return nu;
804}
805
810inline void frictionBoundFromLambdaKokkos(Kokkos::View<ContactC*, CpMem> contacts, int numContacts,
811 Kokkos::View<const int*, CpMem> contactSlot,
812 Kokkos::View<const ManifoldC*, CpMem> manifolds,
813 Kokkos::View<const float*, CpMem> lambdaAcc) {
814 CpExec space;
815 Kokkos::parallel_for(
816 "peclet::dem::cp::pgs_friction_bound", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
817 KOKKOS_LAMBDA(int i) {
818 const int s = contactSlot(i);
819 const ManifoldC m = manifolds(s);
820 const int np = (m.num_points > 0) ? m.num_points : 1;
821 contacts(i).friction_lambda_n = lambdaAcc(s) / static_cast<float>(np);
822 });
823 space.fence();
824}
825
833 Kokkos::View<const float*, CpMem> posLambdaContact, int numContacts,
834 Kokkos::View<const int*, CpMem> contactSlot, Kokkos::View<const ManifoldC*, CpMem> manifolds,
835 int numManifolds, Kokkos::View<const unsigned long long*, CpMem> keys,
836 Kokkos::View<const unsigned long long*, CpMem> prevKeysSorted, int prevCount, float dt,
837 Kokkos::View<float*, CpMem> scratchManifold, Kokkos::View<float*, CpMem> prevPosImpulse) {
838 CpExec space;
839 auto sm = Kokkos::subview(scratchManifold, Kokkos::pair<int, int>(0, numManifolds));
840 Kokkos::deep_copy(space, sm, 0.0f);
841 Kokkos::parallel_for(
842 "peclet::dem::pos_load_reduce", Kokkos::RangePolicy<CpExec>(space, 0, numContacts),
843 KOKKOS_LAMBDA(int i) {
844 const float l = posLambdaContact(i);
845 if (l != 0.0f)
846 Kokkos::atomic_add(&scratchManifold(contactSlot(i)), l / dt);
847 });
848 Kokkos::parallel_for(
849 "peclet::dem::pos_load_scatter", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
850 KOKKOS_LAMBDA(int idx) {
851 const unsigned long long k = keys(idx);
852 if (k == ~0ull)
853 return;
854 int lo = 0, hi = prevCount;
855 while (lo < hi) {
856 const int m = (lo + hi) >> 1;
857 if (prevKeysSorted(m) < k)
858 lo = m + 1;
859 else
860 hi = m;
861 }
862 if (lo < prevCount && prevKeysSorted(lo) == k)
863 prevPosImpulse(lo) = scratchManifold(idx);
864 });
865 space.fence();
866}
867
868} // namespace peclet::dem
869
870#endif // DEM_CONTACT_PREPROCESSING_HPP
dem — portable POD types + math + analytic SDFs shared by the Kokkos kernel ports.
void commitPosImpulseKokkos(Kokkos::View< const float *, CpMem > posLambdaContact, int numContacts, Kokkos::View< const int *, CpMem > contactSlot, Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const unsigned long long *, CpMem > prevKeysSorted, int prevCount, float dt, Kokkos::View< float *, CpMem > scratchManifold, Kokkos::View< float *, CpMem > prevPosImpulse)
After the position solve: convert the per-contact positional lambdas into an impulse- equivalent per ...
void frictionBoundFromLambdaKokkos(Kokkos::View< ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const int *, CpMem > contactSlot, Kokkos::View< const ManifoldC *, CpMem > manifolds, Kokkos::View< const float *, CpMem > lambdaAcc)
PGS friction bound: overwrite each contact's friction_lambda_n with its manifold's converged PGS push...
int partitionContactsKokkos(Kokkos::View< ContactC *, CpMem > contacts, int n, const Owns &owns)
Stable partition of contacts [0, n) by owns(contact): the owned ones first, [0, ncOwned),...
void computeHeightLevelsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< int *, CpMem > heights, int numReal)
std::uint64_t pairKey(const ContactC &c)
Canonical pair key: (min<<32)|max, or (idA<<32)|0xFFFFFFFF for a boundary (idB<0) contact.
ManifoldC transformContact(const ContactC &c)
Per-contact transform to a single-point manifold, aligned to the canonical pair.
std::uint64_t unitKey(const ContactC &c)
Position-unit key (docs/contact_solve_framework.md §4.3, §12 S6): the canonical pair key for a body-b...
constexpr int wallContactId(int wallIndex)
Wall identity of a boundary contact (docs/contact_solve_framework.md §12 S6): a contact with a wall c...
void decodeKey(std::uint64_t key, int &bodyA, int &bodyB)
Decode the canonical (bodyA, bodyB) from a pair key (bodyB = -1 for boundary).
void buildColorBucketsKokkos(Kokkos::View< const int *, CpMem > colorOf, int n, int numColors, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< int > &offs)
Dense colour buckets (numColors <= 64): perm[offs[c] .
constexpr std::uint64_t kNonOwnedPairBit
Distributed step: bit 63 of a manifold-reduction key marks a contact this rank sees but does not own ...
F3 ldF3(const V &v, int i)
void markPersistentManifoldsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > keyIdx, Kokkos::View< const unsigned long long *, CpMem > prevKeys, int prevCount, Kokkos::View< unsigned long long *, CpMem > outKeys, Kokkos::View< unsigned char *, CpMem > outFlags)
keyIdx maps a body slot to the identity the pair key is built from: the REAL index map on the single-...
constexpr int wallIndexOf(int bodyB)
void gatherWarmLambdaKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > keyIdx, Kokkos::View< const unsigned long long *, CpMem > prevKeys, Kokkos::View< const float *, CpMem > prevLambda, Kokkos::View< const float *[3], CpMem > prevLambdaT, Kokkos::View< const float *, CpMem > prevPosImpulse, Kokkos::View< const float *, CpMem > prevRestBank, Kokkos::View< const float *, CpMem > prevRestVPeak, int prevCount, Kokkos::View< unsigned long long *, CpMem > outKeys, Kokkos::View< float *, CpMem > outWarm, Kokkos::View< float *[3], CpMem > outWarmT, Kokkos::View< float *, CpMem > outPosImpulse, Kokkos::View< float *, CpMem > outRestBank, Kokkos::View< float *, CpMem > outRestVPeak, Kokkos::View< unsigned char *, CpMem > outMatched={}, bool dedupTwins=true)
Warm-start gather for the PGS velocity solve: per manifold, write its pair key and look up the previo...
void updateGroundedLevelsKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< unsigned char *, CpMem > grounded, int numReal, int sweeps, int decay)
Guendelman support levels, warm-started: decay every body's level by decay, re-seed 255 at wall/plane...
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
int buildPositionUnitsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int n, Kokkos::View< int *, CpMem > unitStart, Kokkos::View< int *, CpMem > unitContacts, int numManifolds=-1)
Build the position units of contacts [0, n) (docs/contact_solve_framework.md §4.3,...
CpExec::memory_space CpMem
unsigned long long pairKeyFromGids(unsigned a, unsigned b)
The one 64-bit pair identity every gid-keyed ledger uses (the XPBD persistent-contact and warm-start ...
int reduceContactsToManifoldsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int n, Kokkos::View< ManifoldC *, CpMem > outManifolds, Kokkos::View< int, CpMem > outCount, Kokkos::View< int *, CpMem > contactSlot={}, int numOwnedContacts=-1, int *numOwnedManifolds=nullptr)
Reduce n contacts to manifolds (one per unique canonical pair).
void commitPairKeysLambdaKokkos(Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< const float *, CpMem > lambda, Kokkos::View< const float *[3], CpMem > lambdaT, Kokkos::View< const float *, CpMem > restBank, Kokkos::View< const float *, CpMem > restVPeak, Kokkos::View< unsigned long long *, CpMem > prevKeys, Kokkos::View< float *, CpMem > prevLambda, Kokkos::View< float *[3], CpMem > prevLambdaT, Kokkos::View< float *, CpMem > prevRestBank, Kokkos::View< float *, CpMem > prevRestVPeak, Kokkos::View< int *, CpMem > perm, int numManifolds, Kokkos::View< const int *, CpMem > color={}, Kokkos::View< int *, CpMem > prevColor={})
Save this substep's keys + converged impulses (normal AND tangential) and key-sort them for next subs...
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
constexpr int kLevelInf
Height-from-floor BFS levels for the level-ordered ("multilevel") stabilization pass: 0 at a wall/pla...
constexpr int kColorUncolourable
void commitPairKeysKokkos(Kokkos::View< const unsigned long long *, CpMem > keys, Kokkos::View< unsigned long long *, CpMem > prevKeys, int numManifolds)
Copy this substep's keys into prevKeys and sort them for next substep's binary search.
constexpr int kColorPalette
Colouring palette (docs/contact_solve_framework.md §4.2 item 1): the greedy takes the lowest colour f...
int lowerBoundKey(const KeysView &keys, int count, unsigned long long key)
lower_bound over a ledger's sorted keys: the first index whose key is >= key (count when none).
bool normalOfA(const ContactC &c)
Which body's surface a contact's normal belongs to.
F4 cross3(F4 a, F4 b)
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.
Portable mirror of ParticleSystem.cuh ContactConstraint (the fields this reduction touches).
Portable mirror of ManifoldConstraint.
The position units of a contact list as a CSR (§4.3): unit u owns the contacts list(start(u)) .
Kokkos::View< const int *, CpMem > start
Kokkos::View< const int *, CpMem > list
Per-edge slot overrides of a phase's hub copies (docs/contact_solve_framework.md §4....
Kokkos::View< const unsigned char *, CpMem > split
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
Kokkos::View< const int *, CpMem > a
Kokkos::View< const int *, CpMem > b