peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_multilevel.hpp
Go to the documentation of this file.
1
33#ifndef DEM_SOLVER_MULTILEVEL_HPP
34#define DEM_SOLVER_MULTILEVEL_HPP
35
36#include <Kokkos_Core.hpp>
37#include <stdexcept>
38#include <utility>
39#include <vector>
40
41#include "contact_preprocessing.hpp" // ManifoldC, colorKey, CpExec/CpMem
42#include "solver_fused.hpp" // demGridBarrier / FusedSweepCtx (fused coarse cycle)
43#include "solver_velocity.hpp" // PGSManifoldSweep
44
45namespace peclet::dem {
46
49inline constexpr int kMlMaxLevels = 10;
50inline constexpr int kMlSlotSkip = 63;
56inline constexpr long long kMlPendingSkipBit = 1ll << 62;
57
60 int numLevels = 0; // coarse levels built (0 = aggregation found nothing)
61 std::vector<int> groupOff; // per level: offset of its group arrays in the group pools
62 std::vector<int> numGroups; // per level: group count
63 std::vector<int> parentOff; // per level: offset of its parent map (from level l-1 ids)
64 std::vector<int> numColors; // per level: colours used by the crossing-manifold coloring
65};
66
68struct MlScratch {
69 Kokkos::View<long long*, CpMem> colorPacked; // per manifold: 6-bit colour slot per level
70 Kokkos::View<int*, CpMem> parent; // packed parent maps (pool, 5*cap)
71 Kokkos::View<float*, CpMem> invMassG; // packed per-group inverse mass (pool, 4*cap)
72 Kokkos::View<float* [3], CpMem> velG; // packed per-group velocity (pool, 4*cap)
73 Kokkos::View<float* [3], CpMem> velG0; // restriction snapshot (pool, 4*cap)
74 Kokkos::View<float*, CpMem> massG; // packed per-group mass accumulator (pool, 4*cap)
75 Kokkos::View<int*, CpMem> grp; // composed REAL-body -> current-level group map
76 Kokkos::View<int*, CpMem> mate; // matching scratch (per group of the finer level)
77 // Rigid 6-DOF aggregates (docs/contact_physics_followups.md §2.3), allocated lazily on the first
78 // multilevel pass with the group pools' extent: per group the origin O_g, the centre offset c_g
79 // (the centre of mass is O_g + c_g), the angular velocity Omega_g and its restriction snapshot,
80 // and the inverse inertia (xx, yy, zz, xy, xz, yz). accD: the build's per-group DOUBLE
81 // accumulators of one level (sum mu, then sum mu x / sum mu e / the six inertia components).
82 // Empty views: the build skips the geometry (no rigid cycle can run on them).
83 Kokkos::View<float* [3], CpMem> originG;
84 Kokkos::View<float* [3], CpMem> comOffG;
85 Kokkos::View<float* [3], CpMem> angG;
86 Kokkos::View<float* [3], CpMem> angG0;
87 Kokkos::View<float* [6], CpMem> invIG;
88 Kokkos::View<double* [7], CpMem> accD;
89};
90
95 Kokkos::View<const float* [3], CpMem> posPred;
96 Kokkos::View<float* [3], CpMem> angVelPred;
97 Kokkos::View<const float* [4], CpMem> quat;
98 Kokkos::View<const float* [3], CpMem> invIc;
99};
100
101namespace mldetail {
104KOKKOS_INLINE_FUNCTION float effMass(float invMass) {
105 return 1.0f / Kokkos::fmax(invMass, 1e-30f);
106}
107
117inline constexpr int kGatePersistent = 1;
118inline constexpr int kGateCone = 2;
119inline constexpr int kGateSlip = 4;
120
124KOKKOS_INLINE_FUNCTION bool eligible(const ManifoldC& m, int idx,
125 Kokkos::View<const int*, CpMem> mColor,
126 Kokkos::View<const float*, CpMem> vn0,
127 Kokkos::View<const float* [3], CpMem> vt0,
128 Kokkos::View<const unsigned char*, CpMem> persistent,
129 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
130 float qsThr, int gateMask) {
131 if (m.num_points <= 0 || mColor(idx) < 0)
132 return false;
133 if ((gateMask & kGatePersistent) && persistent(idx) == 0)
134 return false;
135 if (Kokkos::fabs(vn0(idx)) > qsThr)
136 return false; // ballistic pair: never aggregated (rebound stays fine + symmetric)
137 if (gateMask & kGateSlip) {
138 // Slip threshold = qsThr. Measured trade-off: a pour-settling bed's load-bearing contacts
139 // slip at 2-8 g dt (a tighter floor excludes them and the bed crushes), while silo bulk
140 // creep slips at <= 8 g dt (so some of it stays in and costs ~7% discharge vs the one-sided
141 // default). The two distributions overlap in g dt units -- no absolute threshold separates
142 // them; a slip-persistence (time-integrated) gate is the identified next refinement.
143 const F3 vt{vt0(idx, 0), vt0(idx, 1), vt0(idx, 2)};
144 if (dot3(vt, vt) > qsThr * qsThr)
145 return false;
146 }
147 if (m.bodyB < 0)
148 return true;
149 if (gateMask & kGateCone) {
150 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
151 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z);
152 return Kokkos::fabs(up) > 0.3f * Kokkos::sqrt(dot3(dx, dx));
153 }
154 return true;
155}
156
158KOKKOS_INLINE_FUNCTION bool canonical(const Kokkos::View<const float* [3], CpMem>& posPred,
159 const Kokkos::View<const int*, CpMem>& realIdx, int slot) {
160 const int r = realIdx(slot);
161 return posPred(slot, 0) == posPred(r, 0) && posPred(slot, 1) == posPred(r, 1) &&
162 posPred(slot, 2) == posPred(r, 2);
163}
164
168template <class VX>
169KOKKOS_INLINE_FUNCTION F3 memberOffset(const VX& posPred, int i,
170 const Kokkos::View<const float* [3], CpMem>& originG,
171 const Kokkos::View<const float* [3], CpMem>& comOffG,
172 int gi) {
173 const F3 e{posPred(i, 0) - originG(gi, 0), posPred(i, 1) - originG(gi, 1),
174 posPred(i, 2) - originG(gi, 2)};
175 return F3{e.x - comOffG(gi, 0), e.y - comOffG(gi, 1), e.z - comOffG(gi, 2)};
176}
177
180KOKKOS_INLINE_FUNCTION F3 spinInertiaTimes(F4 q, F3 invIc, F3 w) {
181 const float ix = 1.0f / Kokkos::fmax(invIc.x, 1e-30f);
182 if (invIc.x == invIc.y && invIc.y == invIc.z)
183 return scale3(w, ix);
184 const float iy = 1.0f / Kokkos::fmax(invIc.y, 1e-30f);
185 const float iz = 1.0f / Kokkos::fmax(invIc.z, 1e-30f);
186 const F3 wl = invRotateVector(q, w);
187 return rotateVector(q, F3{wl.x * ix, wl.y * iy, wl.z * iz});
188}
189
191template <class V6>
192KOKKOS_INLINE_FUNCTION F3 symMul(const V6& S, int g, F3 v) {
193 return F3{S(g, 0) * v.x + S(g, 3) * v.y + S(g, 4) * v.z,
194 S(g, 3) * v.x + S(g, 1) * v.y + S(g, 5) * v.z,
195 S(g, 4) * v.x + S(g, 5) * v.y + S(g, 2) * v.z};
196}
197
199struct D3 {
200 double x, y, z;
201};
202KOKKOS_INLINE_FUNCTION D3 crossD(D3 a, D3 b) {
203 return D3{a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
204}
205KOKKOS_INLINE_FUNCTION D3 rotateD(double qx, double qy, double qz, double qw, D3 v) {
206 const D3 qv{qx, qy, qz};
207 const D3 c = crossD(qv, v);
208 const D3 t{2.0 * c.x, 2.0 * c.y, 2.0 * c.z};
209 const D3 c2 = crossD(qv, t);
210 return D3{v.x + t.x * qw + c2.x, v.y + t.y * qw + c2.y, v.z + t.z * qw + c2.z};
211}
212} // namespace mldetail
213
223inline int buildMlLevelGeometryKokkos(const MlScratch& S, int off, int ng, int numReal,
224 Kokkos::View<const float* [3], CpMem> posPred,
225 Kokkos::View<const float*, CpMem> invMass,
226 Kokkos::View<const float* [4], CpMem> quat,
227 Kokkos::View<const float* [3], CpMem> invIc) {
228 CpExec space;
229 auto grp = S.grp;
230 auto acc = S.accD;
231 auto O = S.originG;
232 auto C = S.comOffG;
233 auto invIG = S.invIG;
234 const Kokkos::View<const float* [3], CpMem> Oc(S.originG), Cc(S.comOffG);
235 Kokkos::parallel_for(
236 "peclet::dem::ml_geo_reset", Kokkos::RangePolicy<CpExec>(space, 0, ng), KOKKOS_LAMBDA(int g) {
237 for (int c = 0; c < 7; ++c)
238 acc(g, c) = 0.0;
239 });
240 Kokkos::parallel_for(
241 "peclet::dem::ml_geo_origin_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
242 KOKKOS_LAMBDA(int i) {
243 const double mu = static_cast<double>(mldetail::effMass(invMass(i)));
244 const int g = grp(i);
245 Kokkos::atomic_add(&acc(g, 0), mu);
246 for (int c = 0; c < 3; ++c)
247 Kokkos::atomic_add(&acc(g, 1 + c), mu * static_cast<double>(posPred(i, c)));
248 });
249 Kokkos::parallel_for(
250 "peclet::dem::ml_geo_origin", Kokkos::RangePolicy<CpExec>(space, 0, ng),
251 KOKKOS_LAMBDA(int g) {
252 for (int c = 0; c < 3; ++c) {
253 O(off + g, c) = static_cast<float>(acc(g, 1 + c) / acc(g, 0));
254 acc(g, 1 + c) = 0.0;
255 }
256 });
257 Kokkos::parallel_for(
258 "peclet::dem::ml_geo_com_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
259 KOKKOS_LAMBDA(int i) {
260 const double mu = static_cast<double>(mldetail::effMass(invMass(i)));
261 const int g = grp(i);
262 for (int c = 0; c < 3; ++c) {
263 const float e = posPred(i, c) - Oc(off + g, c);
264 Kokkos::atomic_add(&acc(g, 1 + c), mu * static_cast<double>(e));
265 }
266 });
267 Kokkos::parallel_for(
268 "peclet::dem::ml_geo_com", Kokkos::RangePolicy<CpExec>(space, 0, ng), KOKKOS_LAMBDA(int g) {
269 for (int c = 0; c < 3; ++c)
270 C(off + g, c) = static_cast<float>(acc(g, 1 + c) / acc(g, 0));
271 for (int c = 1; c < 7; ++c)
272 acc(g, c) = 0.0;
273 });
274 Kokkos::parallel_for(
275 "peclet::dem::ml_geo_inertia_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
276 KOKKOS_LAMBDA(int i) {
277 const double mu = static_cast<double>(mldetail::effMass(invMass(i)));
278 const int g = grp(i);
279 const F3 df = mldetail::memberOffset(posPred, i, Oc, Cc, off + g);
280 const double dx = df.x, dy = df.y, dz = df.z;
281 const double d2 = dx * dx + dy * dy + dz * dz;
282 double J[6];
283 const double ix = 1.0 / Kokkos::fmax(static_cast<double>(invIc(i, 0)), 1e-30);
284 if (invIc(i, 0) == invIc(i, 1) && invIc(i, 1) == invIc(i, 2)) {
285 J[0] = J[1] = J[2] = ix;
286 J[3] = J[4] = J[5] = 0.0;
287 } else {
288 const double iy = 1.0 / Kokkos::fmax(static_cast<double>(invIc(i, 1)), 1e-30);
289 const double iz = 1.0 / Kokkos::fmax(static_cast<double>(invIc(i, 2)), 1e-30);
290 const double qx = quat(i, 0), qy = quat(i, 1), qz = quat(i, 2), qw = quat(i, 3);
291 // columns J e_k = R diag R^T e_k (R^T = rotation by the conjugate)
292 mldetail::D3 col[3];
293 for (int k = 0; k < 3; ++k) {
294 const mldetail::D3 ek{k == 0 ? 1.0 : 0.0, k == 1 ? 1.0 : 0.0, k == 2 ? 1.0 : 0.0};
295 const mldetail::D3 l = mldetail::rotateD(-qx, -qy, -qz, qw, ek);
296 col[k] = mldetail::rotateD(qx, qy, qz, qw, mldetail::D3{l.x * ix, l.y * iy, l.z * iz});
297 }
298 J[0] = col[0].x;
299 J[1] = col[1].y;
300 J[2] = col[2].z;
301 J[3] = 0.5 * (col[1].x + col[0].y);
302 J[4] = 0.5 * (col[2].x + col[0].z);
303 J[5] = 0.5 * (col[2].y + col[1].z);
304 }
305 Kokkos::atomic_add(&acc(g, 1), J[0] + mu * (d2 - dx * dx));
306 Kokkos::atomic_add(&acc(g, 2), J[1] + mu * (d2 - dy * dy));
307 Kokkos::atomic_add(&acc(g, 3), J[2] + mu * (d2 - dz * dz));
308 Kokkos::atomic_add(&acc(g, 4), J[3] - mu * dx * dy);
309 Kokkos::atomic_add(&acc(g, 5), J[4] - mu * dx * dz);
310 Kokkos::atomic_add(&acc(g, 6), J[5] - mu * dy * dz);
311 });
312 int bad = 0;
313 Kokkos::parallel_reduce(
314 "peclet::dem::ml_geo_invert", Kokkos::RangePolicy<CpExec>(space, 0, ng),
315 KOKKOS_LAMBDA(int g, int& nb) {
316 const double xx = acc(g, 1), yy = acc(g, 2), zz = acc(g, 3);
317 const double xy = acc(g, 4), xz = acc(g, 5), yz = acc(g, 6);
318 const double c00 = yy * zz - yz * yz, c01 = xz * yz - xy * zz, c02 = xy * yz - xz * yy;
319 const double c11 = xx * zz - xz * xz, c12 = xy * xz - xx * yz, c22 = xx * yy - xy * xy;
320 const double det = xx * c00 + xy * c01 + xz * c02;
321 if (!(det > 0.0) || !Kokkos::isfinite(det)) {
322 for (int c = 0; c < 6; ++c)
323 invIG(off + g, c) = 0.0f;
324 ++nb;
325 return;
326 }
327 const double inv = 1.0 / det;
328 invIG(off + g, 0) = static_cast<float>(c00 * inv);
329 invIG(off + g, 1) = static_cast<float>(c11 * inv);
330 invIG(off + g, 2) = static_cast<float>(c22 * inv);
331 invIG(off + g, 3) = static_cast<float>(c01 * inv);
332 invIG(off + g, 4) = static_cast<float>(c02 * inv);
333 invIG(off + g, 5) = static_cast<float>(c12 * inv);
334 },
335 bad);
336#ifndef NDEBUG
337 if (bad > 0)
338 throw std::logic_error("multilevel: a rigid aggregate's inertia is singular (§2.3 step 5)");
339#endif
340 return bad;
341}
342
348 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
349 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const int*, CpMem> mColor,
350 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const float* [3], CpMem> vt0,
351 Kokkos::View<const unsigned char*, CpMem> persistent,
352 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
353 Kokkos::View<const float*, CpMem> invMass, float qsThr, int gateMask, int numReal, MlScratch& S,
354 Kokkos::View<long long*, CpMem> winner, Kokkos::View<std::uint64_t*, CpMem> colorMask,
355 bool excludeImmovable = false, Kokkos::View<const unsigned char*, CpMem> asleep = {},
356 const MlBodyViews& body = {}) {
357 ContactHierarchy H;
358 CpExec space;
359 if (numManifolds <= 0 || numReal <= 0)
360 return H;
361 // A sleeper carries a small POSITIVE effective inverse mass (heavy but not rigid, so the fine PGS
362 // does not diverge against it), so the invMass==0 test alone no longer identifies it — exclude by
363 // the asleep flag too. Genuinely-pinned bodies (real invMass 0) stay caught by the ==0 branch.
364 const bool haveAsleep = asleep.extent(0) > 0;
365 { // level-0 composed map = identity; packed colours all "skip"
366 auto grp = S.grp;
367 Kokkos::parallel_for(
368 "peclet::dem::ml_grp_init", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
369 KOKKOS_LAMBDA(int i) { grp(i) = i; });
370 auto cp = S.colorPacked;
371 Kokkos::parallel_for(
372 "peclet::dem::ml_packed_init", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
373 KOKKOS_LAMBDA(int idx) { cp(idx) = ~0ll; }); // every 6-bit slot = 63 (skip)
374 }
375
376 int ngPrev = numReal; // group count of the finer level (level 0 = real bodies)
377 int parentOff = 0, groupOff = 0;
378 const int parentCap = static_cast<int>(S.parent.extent(0));
379 const int groupCap = static_cast<int>(S.invMassG.extent(0));
380 const bool haveGeometry = S.invIG.extent(0) >= S.invMassG.extent(0) &&
381 static_cast<int>(S.accD.extent(0)) >= numReal &&
382 body.quat.extent(0) > 0 && body.invIc.extent(0) > 0;
383
384 for (int lvl = 1; lvl <= kMlMaxLevels; ++lvl) {
385 if (parentOff + ngPrev > parentCap)
386 break; // pool exhausted (matching stalled repeatedly) -- use what we have
387 // ---- greedy random-priority matching on the current group graph ----
388 Kokkos::parallel_for(
389 "peclet::dem::ml_match_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
390 KOKKOS_LAMBDA(int g) { winner(g) = -1; });
391 {
392 auto grp = S.grp;
393 Kokkos::parallel_for(
394 "peclet::dem::ml_match_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
395 KOKKOS_LAMBDA(int idx) {
396 const ManifoldC m = manifolds(idx);
397 if (m.bodyB < 0 || !mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred,
398 gHat, qsThr, gateMask))
399 return;
400 // Never aggregate an immovable (sleeping / pinned) body: a group carrying its huge
401 // effMass wrecks the coarse-solve conditioning and pins the awake partner. (Gated so
402 // the sleeping-off path is bit-identical.)
403 if (excludeImmovable &&
404 (invMass(realIdx(m.bodyA)) == 0.0f || invMass(realIdx(m.bodyB)) == 0.0f ||
405 (haveAsleep && (asleep(realIdx(m.bodyA)) || asleep(realIdx(m.bodyB))))))
406 return;
407 // Aggregation never crosses a periodic wrap (§2.3): an image slot's position is
408 // shifted, and an aggregate holding it would have no consistent geometry.
409 if (!mldetail::canonical(posPred, realIdx, m.bodyA) ||
410 !mldetail::canonical(posPred, realIdx, m.bodyB))
411 return;
412 const int gA = grp(realIdx(m.bodyA)), gB = grp(realIdx(m.bodyB));
413 if (gA == gB)
414 return;
415 const long long key = colorKey(idx);
416 Kokkos::atomic_max(&winner(gA), key);
417 Kokkos::atomic_max(&winner(gB), key);
418 });
419 auto mate = S.mate;
420 Kokkos::parallel_for(
421 "peclet::dem::ml_mate_init", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
422 KOKKOS_LAMBDA(int g) { mate(g) = g; });
423 Kokkos::parallel_for(
424 "peclet::dem::ml_match_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
425 KOKKOS_LAMBDA(int idx) {
426 const ManifoldC m = manifolds(idx);
427 if (m.bodyB < 0 || !mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred,
428 gHat, qsThr, gateMask))
429 return;
430 if (excludeImmovable &&
431 (invMass(realIdx(m.bodyA)) == 0.0f || invMass(realIdx(m.bodyB)) == 0.0f ||
432 (haveAsleep && (asleep(realIdx(m.bodyA)) || asleep(realIdx(m.bodyB))))))
433 return;
434 // Aggregation never crosses a periodic wrap (§2.3): an image slot's position is
435 // shifted, and an aggregate holding it would have no consistent geometry.
436 if (!mldetail::canonical(posPred, realIdx, m.bodyA) ||
437 !mldetail::canonical(posPred, realIdx, m.bodyB))
438 return;
439 const int gA = grp(realIdx(m.bodyA)), gB = grp(realIdx(m.bodyB));
440 if (gA == gB)
441 return;
442 const long long key = colorKey(idx);
443 if (winner(gA) == key && winner(gB) == key) { // sole winner of both endpoints
444 mate(gA) = gB;
445 mate(gB) = gA;
446 }
447 });
448 }
449 // ---- compact new group ids: matched pair -> one id (leader = smaller), singleton keeps ----
450 int ngNew = 0;
451 {
452 auto mate = S.mate;
453 auto parent = S.parent;
454 const int off = parentOff;
455 Kokkos::parallel_scan(
456 "peclet::dem::ml_compact", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
457 KOKKOS_LAMBDA(int g, int& run, const bool final) {
458 const bool leader = (mate(g) >= g);
459 if (leader)
460 ++run;
461 if (final && leader)
462 parent(off + g) = run - 1;
463 },
464 ngNew);
465 Kokkos::parallel_for(
466 "peclet::dem::ml_parent_nonleader", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
467 KOKKOS_LAMBDA(int g) {
468 if (mate(g) < g)
469 parent(off + g) = parent(off + mate(g));
470 });
471 }
472 if (ngNew >= ngPrev || ngNew > (9 * ngPrev) / 10)
473 break; // matching stalled: deeper levels would not shrink the problem
474 if (groupOff + ngNew > groupCap)
475 break;
476 // ---- compose the body -> group map and record the level ----
477 {
478 auto grp = S.grp;
479 auto parent = S.parent;
480 const int off = parentOff;
481 Kokkos::parallel_for(
482 "peclet::dem::ml_grp_compose", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
483 KOKKOS_LAMBDA(int i) { grp(i) = parent(off + grp(i)); });
484 }
485 // ---- per-group inverse mass ----
486 {
487 auto grp = S.grp;
488 auto massG = S.massG;
489 auto invMassG = S.invMassG;
490 const int off = groupOff;
491 Kokkos::parallel_for(
492 "peclet::dem::ml_mass_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
493 KOKKOS_LAMBDA(int g) { massG(off + g) = 0.0f; });
494 Kokkos::parallel_for(
495 "peclet::dem::ml_mass_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
496 KOKKOS_LAMBDA(int i) {
497 Kokkos::atomic_add(&massG(off + grp(i)), mldetail::effMass(invMass(i)));
498 });
499 Kokkos::parallel_for(
500 "peclet::dem::ml_mass_invert", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
501 KOKKOS_LAMBDA(int g) { invMassG(off + g) = 1.0f / massG(off + g); });
502 }
503 // ---- rigid-aggregate geometry of this level (§2.3; the pools are allocated lazily) ----
504 if (haveGeometry)
505 buildMlLevelGeometryKokkos(S, groupOff, ngNew, numReal, posPred, invMass, body.quat,
506 body.invIc);
507 // ---- colour the crossing manifolds of this level on the group graph ----
508 // Same round-based random-priority arbitration as colorManifoldsKokkos, endpoints = groups.
509 // Committed colours land in this level's 6-bit slot of colorPacked (63 stays = skip).
510 {
511 auto grp = S.grp;
512 auto cp = S.colorPacked;
513 const int slotShift = 6 * (lvl - 1);
514 Kokkos::parallel_for(
515 "peclet::dem::ml_color_mask_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
516 KOKKOS_LAMBDA(int g) { colorMask(g) = 0; });
517 // temp per-manifold state via mate-array-free trick: track "uncoloured" in the packed slot
518 // itself (63 = pending here; eligibility recomputed per round).
519 int remaining = 1, prevRemaining = -1, maxc = -1;
520 for (int round = 0; round < 64 && remaining > 0; ++round) {
521 Kokkos::parallel_for(
522 "peclet::dem::ml_color_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
523 KOKKOS_LAMBDA(int g) { winner(g) = -1; });
524 Kokkos::parallel_for(
525 "peclet::dem::ml_color_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
526 KOKKOS_LAMBDA(int idx) {
527 const ManifoldC m = manifolds(idx);
528 if (!mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred, gHat, qsThr,
529 gateMask))
530 return;
531 if (((cp(idx) >> slotShift) & 63) != kMlSlotSkip || !(cp(idx) & kMlPendingSkipBit))
532 return; // committed (or marked skip) in an earlier round
533 const int gA = grp(realIdx(m.bodyA));
534 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
535 if (gB == gA)
536 return; // internal to an aggregate at this level
537 const long long key = colorKey(idx);
538 Kokkos::atomic_max(&winner(gA), key);
539 if (gB >= 0)
540 Kokkos::atomic_max(&winner(gB), key);
541 });
542 int rem = 0;
543 Kokkos::parallel_reduce(
544 "peclet::dem::ml_color_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
545 KOKKOS_LAMBDA(int idx, int& acc) {
546 const ManifoldC m = manifolds(idx);
547 if (!mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred, gHat, qsThr,
548 gateMask))
549 return;
550 if (((cp(idx) >> slotShift) & 63) != kMlSlotSkip || !(cp(idx) & kMlPendingSkipBit))
551 return;
552 const int gA = grp(realIdx(m.bodyA));
553 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
554 if (gB == gA)
555 return;
556 const long long key = colorKey(idx);
557 if (winner(gA) != key || (gB >= 0 && winner(gB) != key)) {
558 acc += 1;
559 return;
560 }
561 std::uint64_t forbidden = colorMask(gA);
562 if (gB >= 0)
563 forbidden |= colorMask(gB);
564 int c = 0; // lowest free colour among 0..62; never forced (§4.4)
565 while (c < kMlSlotSkip && ((forbidden >> c) & 1))
566 ++c;
567 if (c == kMlSlotSkip) {
568 cp(idx) &= ~kMlPendingSkipBit; // keeps slot 63: skipped at this level
569 return;
570 }
571 cp(idx) = (cp(idx) & ~(63ll << slotShift)) | (static_cast<long long>(c) << slotShift);
572 const std::uint64_t bit = std::uint64_t(1) << c;
573 colorMask(gA) |= bit;
574 if (gB >= 0)
575 colorMask(gB) |= bit;
576 },
577 rem);
578 space.fence();
579 if (rem == prevRemaining)
580 break; // safety bound: leftovers keep slot 63 and are skipped at this level
581 prevRemaining = rem;
582 remaining = rem;
583 }
584 Kokkos::parallel_for(
585 "peclet::dem::ml_color_clear_skip", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
586 KOKKOS_LAMBDA(int idx) { cp(idx) |= kMlPendingSkipBit; });
587 Kokkos::parallel_reduce(
588 "peclet::dem::ml_color_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
589 KOKKOS_LAMBDA(int idx, int& mx) {
590 const int c = static_cast<int>((cp(idx) >> slotShift) & 63);
591 if (c != kMlSlotSkip && c > mx)
592 mx = c;
593 },
594 Kokkos::Max<int>(maxc));
595 space.fence();
596 H.numColors.push_back(maxc + 1);
597 }
598 H.groupOff.push_back(groupOff);
599 H.numGroups.push_back(ngNew);
600 H.parentOff.push_back(parentOff);
601 H.numLevels = lvl;
602 parentOff += ngPrev;
603 groupOff += ngNew;
604 ngPrev = ngNew;
605 if (ngNew <= 32)
606 break; // coarse enough: a handful of super-bodies solves in one sweep
607 }
608 space.fence();
609 return H;
610}
611
623inline void buildCoarseBucketsKokkos(const ContactHierarchy& H, MlScratch& S, int numManifolds,
624 Kokkos::View<int*, CpMem> colorScratch,
625 Kokkos::View<int*, CpMem> perm,
626 Kokkos::View<int*, CpMem> cursor,
627 std::vector<std::vector<int>>& offs) {
628 offs.assign(static_cast<std::size_t>(H.numLevels), {});
629 if (numManifolds <= 0)
630 return;
631 CpExec space;
632 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
633 const int slotShift = 6 * (lvl - 1);
634 const int nCol = H.numColors[static_cast<std::size_t>(lvl) - 1];
635 auto cp = S.colorPacked;
636 auto cs = colorScratch;
637 Kokkos::parallel_for(
638 "peclet::dem::ml_bucket_colors", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
639 KOKKOS_LAMBDA(int idx) {
640 const int c = static_cast<int>((cp(idx) >> slotShift) & 63);
641 cs(idx) = (c < nCol) ? c : -1;
642 });
643 auto seg =
644 Kokkos::subview(perm, Kokkos::pair<int, int>((lvl - 1) * numManifolds, lvl * numManifolds));
645 buildColorBucketsKokkos(Kokkos::View<const int*, CpMem>(colorScratch), numManifolds, nCol, seg,
646 cursor, offs[static_cast<std::size_t>(lvl) - 1]);
647 }
648}
649
660 Kokkos::View<const ManifoldC*, CpMem> manifolds;
661 Kokkos::View<const int*, CpMem> realIdx;
662 Kokkos::View<const int*, CpMem> grp;
663 Kokkos::View<float* [3], CpMem> velG;
664 Kokkos::View<const float*, CpMem> invMassG;
665 Kokkos::View<float*, CpMem> lambdaAcc;
666 Kokkos::View<float, CpMem> maxApproach;
667 Kokkos::View<const float*, CpMem> restRel;
668 // the rigid coarse space (§2.3-§2.5)
670 Kokkos::View<const float* [3], CpMem> originG;
671 Kokkos::View<const float* [3], CpMem> comOffG;
672 Kokkos::View<float* [3], CpMem> angG;
673 Kokkos::View<float* [3], CpMem> angG0;
674 Kokkos::View<const float* [6], CpMem> invIG;
675
678 struct Row {
679 int gA, gB;
681 float lenN, vn, sgn, invMA, invMB, w;
682 };
683 KOKKOS_FUNCTION bool row(int idx, int off, Row& r) const {
684 // Poisson release in flight on this pair: the coarse e = 0 solve targets vtil = 0 on
685 // the SHARED accumulator and would retract the just-injected separation velocity —
686 // the releasing contact skips the coarse transport this substep.
687 if (restRel.extent(0) > 0 && restRel(idx) > 0.0f)
688 return false;
689 const ManifoldC m = manifolds(idx);
690 const int gA = grp(realIdx(m.bodyA));
691 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
692 const float invN = 1.0f / static_cast<float>(m.num_points);
693 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
694 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
695 if (lenN < 1e-9f)
696 return false;
697 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
698 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
699 const F3 diffCenters = (gB < 0) ? rAavg : sub3(rAavg, rBavg);
700 const F3 vA{velG(off + gA, 0), velG(off + gA, 1), velG(off + gA, 2)};
701 F3 vB{0, 0, 0};
702 if (gB >= 0)
703 vB = F3{velG(off + gB, 0), velG(off + gB, 1), velG(off + gB, 2)};
704 else
705 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
706 float vn = dot3(sub3(vA, vB), Nsum);
707 const float alignment = dot3(Nsum, diffCenters);
708 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
709 const float invMA = invMassG(off + gA);
710 const float invMB = (gB >= 0) ? invMassG(off + gB) : 0.0f;
711 float w = dot3(Nsum, Nsum) * (invMA + invMB);
712 // The rigid rows: arms about the aggregates' centres of mass, T_A = TauA + d_A x N and
713 // T_B = TauB - d_B x N (canonical positions: a wrap image's arm is relative to its own centre,
714 // the offset d to the canonical body's group), the rotational terms appended to vn and w.
715 const F3 dA =
717 const F3 TA =
719 const F3 wA{angG(off + gA, 0), angG(off + gA, 1), angG(off + gA, 2)};
720 vn = vn + dot3(TA, wA);
721 F3 TB{0, 0, 0};
722 if (gB >= 0) {
723 const F3 dB =
726 cross3v(dB, Nsum));
727 const F3 wB{angG(off + gB, 0), angG(off + gB, 1), angG(off + gB, 2)};
728 vn = vn + dot3(TB, wB);
729 }
730 w = w + dot3(TA, mldetail::symMul(invIG, off + gA, TA));
731 if (gB >= 0)
732 w = w + dot3(TB, mldetail::symMul(invIG, off + gB, TB));
733 if (w <= 0.0f)
734 return false;
735 r = Row{gA, gB, Nsum, TA, TB, lenN, vn, sgn, invMA, invMB, w};
736 return true;
737 }
738
739 KOKKOS_FUNCTION void solveOne(int idx, int off) const {
740 Row r;
741 if (!row(idx, off, r))
742 return;
743 const int gA = r.gA, gB = r.gB;
744 const float w = r.w, invMA = r.invMA, invMB = r.invMB;
745 const float vtil = r.sgn * r.vn;
746 const float dp = vtil / w; // e = 0: target 0 (pure inelastic support)
747 const float pOld = lambdaAcc(idx);
748 float pNew = pOld + dp;
749 if (pNew < 0.0f)
750 pNew = 0.0f;
751 const float d = pNew - pOld;
752 if (d == 0.0f)
753 return;
754 lambdaAcc(idx) = pNew;
755 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(d) * w / r.lenN);
756 const float lambda = -r.sgn * d;
757 const F3 J = scale3(r.Nsum, lambda);
758 velG(off + gA, 0) += J.x * invMA;
759 velG(off + gA, 1) += J.y * invMA;
760 velG(off + gA, 2) += J.z * invMA;
761 if (gB >= 0) {
762 velG(off + gB, 0) -= J.x * invMB;
763 velG(off + gB, 1) -= J.y * invMB;
764 velG(off + gB, 2) -= J.z * invMB;
765 }
766 const F3 dwA = mldetail::symMul(invIG, off + gA, scale3(r.TA, lambda));
767 angG(off + gA, 0) += dwA.x;
768 angG(off + gA, 1) += dwA.y;
769 angG(off + gA, 2) += dwA.z;
770 if (gB >= 0) {
771 const F3 dwB = mldetail::symMul(invIG, off + gB, scale3(r.TB, lambda));
772 angG(off + gB, 0) += dwB.x;
773 angG(off + gB, 1) += dwB.y;
774 angG(off + gB, 2) += dwB.z;
775 }
776 }
777
778 // ---- restrict / prolong, rotational part (§2.4), shared by the launch path and the fused
779 // kernel (the translational restriction stays inline in both) ----
781 template <class VV>
782 KOKKOS_FUNCTION void restrictAngular(int i, int off, int g, float m, const VV& velPred) const {
783 const F3 d = mldetail::memberOffset(body.posPred, i, originG, comOffG, off + g);
784 const F3 v{velPred(i, 0), velPred(i, 1), velPred(i, 2)};
785 const F4 q{body.quat(i, 0), body.quat(i, 1), body.quat(i, 2), body.quat(i, 3)};
786 const F3 ic{body.invIc(i, 0), body.invIc(i, 1), body.invIc(i, 2)};
787 const F3 wq{body.angVelPred(i, 0), body.angVelPred(i, 1), body.angVelPred(i, 2)};
788 const F3 L = add3(scale3(cross3v(d, v), m), mldetail::spinInertiaTimes(q, ic, wq));
789 Kokkos::atomic_add(&angG(off + g, 0), L.x);
790 Kokkos::atomic_add(&angG(off + g, 1), L.y);
791 Kokkos::atomic_add(&angG(off + g, 2), L.z);
792 }
794 KOKKOS_FUNCTION void normalizeAngular(int gi) const {
795 const F3 L{angG(gi, 0), angG(gi, 1), angG(gi, 2)};
796 const F3 W = mldetail::symMul(invIG, gi, L);
797 angG(gi, 0) = angG0(gi, 0) = W.x;
798 angG(gi, 1) = angG0(gi, 1) = W.y;
799 angG(gi, 2) = angG0(gi, 2) = W.z;
800 }
802 template <class VV, class VV0>
803 KOKKOS_FUNCTION void prolongRigid(int i, int off, int g, const VV& velPred,
804 const VV0& velG0) const {
805 const F3 dW{angG(off + g, 0) - angG0(off + g, 0), angG(off + g, 1) - angG0(off + g, 1),
806 angG(off + g, 2) - angG0(off + g, 2)};
807 const F3 d = mldetail::memberOffset(body.posPred, i, originG, comOffG, off + g);
808#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 8
809 const F3 cr{0.0f, 0.0f, 0.0f}; // G-A4 mutant 8: the prolongation drops (Omega - Omega0) x d
810 (void)d;
811#else
812 const F3 cr = cross3v(dW, d);
813#endif
814 velPred(i, 0) += (velG(off + g, 0) - velG0(off + g, 0)) + cr.x;
815 velPred(i, 1) += (velG(off + g, 1) - velG0(off + g, 1)) + cr.y;
816 velPred(i, 2) += (velG(off + g, 2) - velG0(off + g, 2)) + cr.z;
817 body.angVelPred(i, 0) += dW.x;
818 body.angVelPred(i, 1) += dW.y;
819 body.angVelPred(i, 2) += dW.z;
820 }
821};
822
825inline MlCoarseSweep makeMlCoarseSweep(Kokkos::View<const ManifoldC*, CpMem> manifolds,
826 Kokkos::View<const int*, CpMem> realIdx, const MlScratch& S,
827 Kokkos::View<float*, CpMem> lambdaAcc,
828 Kokkos::View<float, CpMem> maxApproach,
829 Kokkos::View<const float*, CpMem> restRel,
830 const MlBodyViews& body) {
831 return MlCoarseSweep{manifolds,
832 realIdx,
833 Kokkos::View<const int*, CpMem>(S.grp),
834 S.velG,
835 Kokkos::View<const float*, CpMem>(S.invMassG),
836 lambdaAcc,
837 maxApproach,
838 restRel,
839 body,
840 Kokkos::View<const float* [3], CpMem>(S.originG),
841 Kokkos::View<const float* [3], CpMem>(S.comOffG),
842 S.angG,
843 S.angG0,
844 Kokkos::View<const float* [6], CpMem>(S.invIG)};
845}
846
850 int numLevels = 0, numReal = 0, coarseSweeps = 0;
855 int permBase[kMlMaxLevels] = {}; // level's segment base in bkPerm
856 int offsBase[kMlMaxLevels] = {}; // level's colour-offset base in the flat device offs
857};
858
862 Kokkos::View<const int*, CpMem> offsDev;
863 Kokkos::View<unsigned*, CpMem> bar;
865 int maxWork = 0;
866};
867
868#ifdef KOKKOS_ENABLE_CUDA
875__device__ inline void demMlCoarseCycleDevice(
876 const MlCoarseSweep& f, const Kokkos::View<const int*, CpMem>& parent,
877 const Kokkos::View<const float*, CpMem>& invMass,
878 const Kokkos::View<float* [3], CpMem>& velPred, const Kokkos::View<float* [3], CpMem>& velG0,
879 const Kokkos::View<const float*, CpMem>& massG, const Kokkos::View<int*, CpMem>& grpW,
880 const Kokkos::View<const int*, CpMem>& bkPerm, const Kokkos::View<const int*, CpMem>& offsDev,
881 const MlFusedMeta& meta, unsigned* bar, unsigned& k, int tid, int stride) {
882 for (int i = tid; i < meta.numReal; i += stride) // reset the composed map to identity
883 grpW(i) = i;
884 demGridBarrier(bar, k++);
885 for (int lvl = 1; lvl <= meta.numLevels; ++lvl) {
886 const int off = meta.groupOff[lvl - 1], ng = meta.numGroups[lvl - 1];
887 const int pOff = meta.parentOff[lvl - 1], nCol = meta.numColors[lvl - 1];
888 for (int i = tid; i < meta.numReal; i += stride) // compose to this level
889 grpW(i) = parent(pOff + grpW(i));
890 demGridBarrier(bar, k++);
891 for (int g = tid; g < ng; g += stride) { // restrict: reset
892 f.velG(off + g, 0) = f.velG(off + g, 1) = f.velG(off + g, 2) = 0.0f;
893 f.angG(off + g, 0) = f.angG(off + g, 1) = f.angG(off + g, 2) = 0.0f;
894 }
895 demGridBarrier(bar, k++);
896 for (int i = tid; i < meta.numReal; i += stride) { // restrict: momentum accumulate
897 const float m = mldetail::effMass(invMass(i));
898 const int g = grpW(i);
899 Kokkos::atomic_add(&f.velG(off + g, 0), m * velPred(i, 0));
900 Kokkos::atomic_add(&f.velG(off + g, 1), m * velPred(i, 1));
901 Kokkos::atomic_add(&f.velG(off + g, 2), m * velPred(i, 2));
902 f.restrictAngular(i, off, g, m, velPred);
903 }
904 demGridBarrier(bar, k++);
905 for (int g = tid; g < ng; g += stride) { // restrict: normalize + snapshot V0
906 const float invM = 1.0f / massG(off + g);
907 for (int c = 0; c < 3; ++c) {
908 f.velG(off + g, c) *= invM;
909 velG0(off + g, c) = f.velG(off + g, c);
910 }
911 f.normalizeAngular(off + g);
912 }
913 demGridBarrier(bar, k++);
914 for (int s = 0; s < meta.coarseSweeps; ++s)
915 for (int color = 0; color < nCol; ++color) {
916 const int b = offsDev(meta.offsBase[lvl - 1] + color);
917 const int e = offsDev(meta.offsBase[lvl - 1] + color + 1);
918 if (b == e)
919 continue; // empty class: nothing written, no barrier (uniform: same offsets read)
920 for (int i2 = b + tid; i2 < e; i2 += stride)
921 f.solveOne(bkPerm(meta.permBase[lvl - 1] + i2), off);
922 demGridBarrier(bar, k++);
923 }
924 for (int i = tid; i < meta.numReal; i += stride) // prolongate: the rigid motion
925 f.prolongRigid(i, off, grpW(i), velPred, velG0);
926 demGridBarrier(bar, k++);
927 }
928}
929
930__global__ void demFusedCoarseCycleK(
931 MlCoarseSweep f, Kokkos::View<const int*, CpMem> parent,
932 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<float* [3], CpMem> velPred,
933 Kokkos::View<float* [3], CpMem> velG0, Kokkos::View<const float*, CpMem> massG,
934 Kokkos::View<int*, CpMem> grpW, Kokkos::View<const int*, CpMem> bkPerm,
935 Kokkos::View<const int*, CpMem> offsDev, MlFusedMeta meta, unsigned* bar) {
936 const int stride = gridDim.x * blockDim.x;
937 const int tid = blockIdx.x * blockDim.x + threadIdx.x;
938 unsigned k = 0;
939 demMlCoarseCycleDevice(f, parent, invMass, velPred, velG0, massG, grpW, bkPerm, offsDev, meta,
940 bar, k, tid, stride);
941}
942
948__global__ void demFusedMlLoopK(
949 PGSManifoldSweep fine, Kokkos::View<const int*, CpMem> vPerm,
950 Kokkos::View<const int*, CpMem> vOffs, int vCols, MlCoarseSweep f,
951 Kokkos::View<const int*, CpMem> parent, Kokkos::View<const float*, CpMem> invMass,
952 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float* [3], CpMem> velG0,
953 Kokkos::View<const float*, CpMem> massG, Kokkos::View<int*, CpMem> grpW,
954 Kokkos::View<const int*, CpMem> bkPerm, Kokkos::View<const int*, CpMem> offsDev,
955 MlFusedMeta meta, int maxIters, float tol, float* resQS, unsigned* bar) {
956 const int stride = gridDim.x * blockDim.x;
957 const int tid = blockIdx.x * blockDim.x + threadIdx.x;
958 unsigned k = 0;
959 for (int it = 0; it < maxIters; ++it) {
960 if (tid == 0)
961 *resQS = 0.0f;
962 demGridBarrier(bar, k++);
963 for (int c = 0; c < vCols; ++c) { // fine colored smoothing sweep
964 const int b = vOffs(c), e = vOffs(c + 1);
965 if (b == e)
966 continue;
967 for (int i = b + tid; i < e; i += stride)
968 fine.solveOne(vPerm(i));
969 demGridBarrier(bar, k++);
970 }
971 demMlCoarseCycleDevice(f, parent, invMass, velPred, velG0, massG, grpW, bkPerm, offsDev, meta,
972 bar, k, tid, stride);
973 const float r = *reinterpret_cast<volatile float*>(resQS);
974 if (r <= tol)
975 break;
976 demGridBarrier(bar, k++); // every block has read resQS before the next zero
977 }
978}
979#endif // KOKKOS_ENABLE_CUDA
980
981#ifdef KOKKOS_ENABLE_CUDA
985inline bool demLaunchFusedMlLoop(
986 CpExec& space, const PGSManifoldSweep& fine, Kokkos::View<const int*, CpMem> vPerm,
987 const FusedSweepCtx& velCtx, int vCols, Kokkos::View<const ManifoldC*, CpMem> manifolds,
988 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const float*, CpMem> invMass,
989 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float*, CpMem> lambdaAcc,
990 Kokkos::View<float, CpMem> maxApproachQS, Kokkos::View<const float*, CpMem> restRel,
991 MlScratch& S, Kokkos::View<const int*, CpMem> bkPerm, const MlFusedCtx& ml, int maxIters,
992 float tol, const MlBodyViews& body) {
993 const int maxGrid =
994 std::min(demFusedMaxGrid(demFusedMlLoopK), (static_cast<int>(ml.bar.extent(0)) - 1) / 8);
995 if (maxGrid <= 0 || vCols <= 0 || velCtx.maxBucket <= 0 || ml.maxWork <= 0)
996 return false;
997 const MlCoarseSweep f =
998 makeMlCoarseSweep(manifolds, realIdx, S, lambdaAcc, maxApproachQS, restRel, body);
999 const int work = std::max(velCtx.maxBucket, ml.maxWork);
1000 const int want = (work + kFusedBlock - 1) / kFusedBlock;
1001 const int grid = want < maxGrid ? want : maxGrid;
1002 cudaStream_t str = space.cuda_stream();
1003 cudaMemsetAsync(ml.bar.data(), 0, (static_cast<std::size_t>(grid) * 8 + 1) * sizeof(unsigned),
1004 str);
1005 demFusedMlLoopK<<<grid, kFusedBlock, 0, str>>>(
1006 fine, vPerm, velCtx.offsDev, vCols, f, Kokkos::View<const int*, CpMem>(S.parent), invMass,
1007 velPred, S.velG0, Kokkos::View<const float*, CpMem>(S.massG), S.grp, bkPerm, ml.offsDev,
1008 ml.meta, maxIters, tol, maxApproachQS.data(), ml.bar.data());
1009 return true;
1010}
1011#endif // KOKKOS_ENABLE_CUDA
1012
1018 const std::vector<std::vector<int>>& bkOffs, int numManifolds,
1019 int numReal, int coarseSweeps,
1020 Kokkos::View<int*, CpMem> offsDev,
1021 Kokkos::View<unsigned*, CpMem> bar) {
1022 MlFusedCtx ctx;
1023#ifdef KOKKOS_ENABLE_CUDA
1024 if (H.numLevels <= 0 || H.numLevels > kMlMaxLevels || numReal <= 0)
1025 return ctx;
1026 std::vector<int> flat;
1027 ctx.meta.numLevels = H.numLevels;
1028 ctx.meta.numReal = numReal;
1029 ctx.meta.coarseSweeps = coarseSweeps;
1030 int maxWork = numReal; // compose/restrict/prolongate phases (ng <= numReal always)
1031 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
1032 const auto& lo = bkOffs[static_cast<std::size_t>(lvl) - 1];
1033 if (lo.size() != static_cast<std::size_t>(H.numColors[lvl - 1]) + 1)
1034 return ctx; // bucket build skipped this level (empty) — keep the launch path
1035 ctx.meta.groupOff[lvl - 1] = H.groupOff[lvl - 1];
1036 ctx.meta.numGroups[lvl - 1] = H.numGroups[lvl - 1];
1037 ctx.meta.parentOff[lvl - 1] = H.parentOff[lvl - 1];
1038 ctx.meta.numColors[lvl - 1] = H.numColors[lvl - 1];
1039 ctx.meta.permBase[lvl - 1] = (lvl - 1) * numManifolds;
1040 ctx.meta.offsBase[lvl - 1] = static_cast<int>(flat.size());
1041 for (std::size_t c = 0; c + 1 < lo.size(); ++c)
1042 maxWork = std::max(maxWork, lo[c + 1] - lo[c]);
1043 flat.insert(flat.end(), lo.begin(), lo.end());
1044 }
1045 if (flat.empty() || flat.size() > offsDev.extent(0))
1046 return ctx;
1047 const Kokkos::View<const int*, Kokkos::HostSpace, Kokkos::MemoryUnmanaged> h(flat.data(),
1048 flat.size());
1049 auto d = Kokkos::subview(offsDev, Kokkos::pair<std::size_t, std::size_t>(0, flat.size()));
1050 Kokkos::deep_copy(space, d, h);
1051 ctx.offsDev = Kokkos::View<const int*, CpMem>(offsDev);
1052 ctx.bar = bar;
1053 ctx.maxWork = maxWork;
1054#else
1055 (void)space;
1056 (void)H;
1057 (void)bkOffs;
1058 (void)numManifolds;
1059 (void)numReal;
1060 (void)coarseSweeps;
1061 (void)offsDev;
1062 (void)bar;
1063#endif
1064 return ctx;
1065}
1066
1068 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
1069 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const float*, CpMem> invMass,
1070 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float*, CpMem> lambdaAcc,
1071 Kokkos::View<float, CpMem> maxApproach, int numReal, const ContactHierarchy& H, MlScratch& S,
1072 int coarseSweeps, const MlBodyViews& body, Kokkos::View<const float*, CpMem> restRel = {},
1073 const std::vector<std::vector<int>>* bkOffs = nullptr,
1074 Kokkos::View<const int*, CpMem> bkPerm = {}, const MlFusedCtx* fused = nullptr) {
1075 CpExec space;
1076 // coarse colored PGS sweeps: e = 0, shared lambda accumulator (the per-manifold body is
1077 // MlCoarseSweep::solveOne — shared verbatim with the fused kernel)
1078 const MlCoarseSweep fSweep =
1079 makeMlCoarseSweep(manifolds, realIdx, S, lambdaAcc, maxApproach, restRel, body);
1080#ifdef KOKKOS_ENABLE_CUDA
1081 if (fused && fused->maxWork > 0) {
1082 const int maxGrid = std::min(demFusedMaxGrid(demFusedCoarseCycleK),
1083 (static_cast<int>(fused->bar.extent(0)) - 1) / 8);
1084 if (maxGrid > 0) {
1085 const int want = (fused->maxWork + kFusedBlock - 1) / kFusedBlock;
1086 const int grid = want < maxGrid ? want : maxGrid;
1087 cudaStream_t str = space.cuda_stream();
1088 cudaMemsetAsync(fused->bar.data(), 0,
1089 (static_cast<std::size_t>(grid) * 8 + 1) * sizeof(unsigned), str);
1090 demFusedCoarseCycleK<<<grid, kFusedBlock, 0, str>>>(
1091 fSweep, Kokkos::View<const int*, CpMem>(S.parent), invMass, velPred, S.velG0,
1092 Kokkos::View<const float*, CpMem>(S.massG), S.grp, bkPerm, fused->offsDev, fused->meta,
1093 fused->bar.data());
1094 return;
1095 }
1096 }
1097#else
1098 (void)fused;
1099#endif
1100 { // reset the composed map to identity; each level applies its parent map on top
1101 auto grp = S.grp;
1102 Kokkos::parallel_for(
1103 "peclet::dem::ml_cycle_grp_init", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
1104 KOKKOS_LAMBDA(int i) { grp(i) = i; });
1105 }
1106 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
1107 const int off = H.groupOff[lvl - 1];
1108 const int ng = H.numGroups[lvl - 1];
1109 const int pOff = H.parentOff[lvl - 1];
1110 const int nCol = H.numColors[lvl - 1];
1111 const int slotShift = 6 * (lvl - 1);
1112 auto grp = S.grp;
1113 { // compose to this level
1114 auto parent = S.parent;
1115 Kokkos::parallel_for(
1116 "peclet::dem::ml_cycle_compose", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
1117 KOKKOS_LAMBDA(int i) { grp(i) = parent(pOff + grp(i)); });
1118 }
1119 { // restrict: V_g = sum(m v) / sum(m) (momentum-conserving), snapshot V0; Omega_g =
1120 // invI_g sum [m d x v + J w] (the M-orthogonal projection onto rigid motions, §2.2)
1121 auto velG = S.velG;
1122 auto velG0 = S.velG0;
1123 auto massG = S.massG;
1124 Kokkos::parallel_for(
1125 "peclet::dem::ml_restrict_reset", Kokkos::RangePolicy<CpExec>(space, 0, ng),
1126 KOKKOS_LAMBDA(int g) {
1127 velG(off + g, 0) = velG(off + g, 1) = velG(off + g, 2) = 0.0f;
1128 fSweep.angG(off + g, 0) = fSweep.angG(off + g, 1) = fSweep.angG(off + g, 2) = 0.0f;
1129 });
1130 Kokkos::parallel_for(
1131 "peclet::dem::ml_restrict_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
1132 KOKKOS_LAMBDA(int i) {
1133 const float m = mldetail::effMass(invMass(i));
1134 const int g = grp(i);
1135 Kokkos::atomic_add(&velG(off + g, 0), m * velPred(i, 0));
1136 Kokkos::atomic_add(&velG(off + g, 1), m * velPred(i, 1));
1137 Kokkos::atomic_add(&velG(off + g, 2), m * velPred(i, 2));
1138 fSweep.restrictAngular(i, off, g, m, velPred);
1139 });
1140 Kokkos::parallel_for(
1141 "peclet::dem::ml_restrict_norm", Kokkos::RangePolicy<CpExec>(space, 0, ng),
1142 KOKKOS_LAMBDA(int g) {
1143 const float invM = 1.0f / massG(off + g);
1144 for (int c = 0; c < 3; ++c) {
1145 velG(off + g, c) *= invM;
1146 velG0(off + g, c) = velG(off + g, c);
1147 }
1148 fSweep.normalizeAngular(off + g);
1149 });
1150 }
1151 for (int s = 0; s < coarseSweeps; ++s) {
1152 for (int color = 0; color < nCol; ++color) {
1153 auto cp = S.colorPacked;
1154 const bool dense = bkOffs != nullptr;
1155 int rb = 0, re = numManifolds;
1156 if (dense) {
1157 const auto& lo = (*bkOffs)[static_cast<std::size_t>(lvl) - 1];
1158 rb = (lvl - 1) * numManifolds + lo[static_cast<std::size_t>(color)];
1159 re = (lvl - 1) * numManifolds + lo[static_cast<std::size_t>(color) + 1];
1160 if (rb == re)
1161 continue;
1162 }
1163 Kokkos::parallel_for(
1164 "peclet::dem::ml_coarse_pgs", Kokkos::RangePolicy<CpExec>(space, rb, re),
1165 KOKKOS_LAMBDA(int i2) {
1166 const int idx = dense ? bkPerm(i2) : i2;
1167 if (!dense && static_cast<int>((cp(idx) >> slotShift) & 63) != color)
1168 return;
1169 fSweep.solveOne(idx, off);
1170 });
1171 }
1172 }
1173 { // prolongate: every member takes its aggregate's rigid motion, dV + dOmega x d, dOmega
1174 auto velG0 = S.velG0;
1175 Kokkos::parallel_for(
1176 "peclet::dem::ml_prolongate", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
1177 KOKKOS_LAMBDA(int i) { fSweep.prolongRigid(i, off, grp(i), velPred, velG0); });
1178 }
1179 }
1180}
1181
1182} // namespace peclet::dem
1183
1184#endif // DEM_SOLVER_MULTILEVEL_HPP
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
bool eligible(const ManifoldC &m, int idx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *[3], CpMem > vt0, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, float qsThr, int gateMask)
Coarse-eligibility of a manifold: active + base-coloured (non-dup) + quasi-static approach (|vn0| <= ...
float effMass(float invMass)
Effective mass with fixed bodies (invMass == 0) mapped to a huge-but-finite mass so the momentum-weig...
F3 symMul(const V6 &S, int g, F3 v)
Symmetric 3x3 (xx, yy, zz, xy, xz, yz) times v.
F3 spinInertiaTimes(F4 q, F3 invIc, F3 w)
J_q w: the member's world-frame spin inertia R diag(1 / max(invIc, 1e-30)) R^T applied to w (§2....
D3 rotateD(double qx, double qy, double qz, double qw, D3 v)
bool canonical(const Kokkos::View< const float *[3], CpMem > &posPred, const Kokkos::View< const int *, CpMem > &realIdx, int slot)
A slot sits at its canonical body's position, bitwise (false for a periodic image).
F3 memberOffset(const VX &posPred, int i, const Kokkos::View< const float *[3], CpMem > &originG, const Kokkos::View< const float *[3], CpMem > &comOffG, int gi)
Member offset from its group's centre of mass, d_q = fl(fl(x_q - O_g) - c_g) (§2.3 step 2): recompute...
constexpr int kGatePersistent
Optional eligibility gates beyond the quasi-static approach test (a SELECTION, not a sink – impulses ...
constexpr int kMlSlotSkip
F3 cross3v(F3 a, F3 b)
void multilevelCoarseCycleKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< float, CpMem > maxApproach, int numReal, const ContactHierarchy &H, MlScratch &S, int coarseSweeps, const MlBodyViews &body, Kokkos::View< const float *, CpMem > restRel={}, const std::vector< std::vector< int > > *bkOffs=nullptr, Kokkos::View< const int *, CpMem > bkPerm={}, const MlFusedCtx *fused=nullptr)
void 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] .
F3 invRotateVector(F4 q, F3 v)
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)
int buildMlLevelGeometryKokkos(const MlScratch &S, int off, int ng, int numReal, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > invIc)
The per-level geometry of the rigid coarse space (docs/contact_physics_followups.md §2....
constexpr long long kMlPendingSkipBit
Transient mark of the coarse colouring (docs/contact_solve_framework.md §4.4, "the multilevel coarse ...
CpExec::memory_space CpMem
void buildCoarseBucketsKokkos(const ContactHierarchy &H, MlScratch &S, int numManifolds, Kokkos::View< int *, CpMem > colorScratch, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< std::vector< int > > &offs)
One multilevel stabilization cycle over an already-built hierarchy: fine colored smoothing is the cal...
F3 scale3(F3 a, float s)
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
F3 add3(F3 a, F3 b)
MlCoarseSweep makeMlCoarseSweep(Kokkos::View< const ManifoldC *, CpMem > manifolds, Kokkos::View< const int *, CpMem > realIdx, const MlScratch &S, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< float, CpMem > maxApproach, Kokkos::View< const float *, CpMem > restRel, const MlBodyViews &body)
The coarse sweep over the multilevel scratch + the body views (the rigid views stay empty when the la...
ContactHierarchy buildContactHierarchyKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *[3], CpMem > vt0, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< const float *, CpMem > invMass, float qsThr, int gateMask, int numReal, MlScratch &S, Kokkos::View< long long *, CpMem > winner, Kokkos::View< std::uint64_t *, CpMem > colorMask, bool excludeImmovable=false, Kokkos::View< const unsigned char *, CpMem > asleep={}, const MlBodyViews &body={})
Build the aggregation hierarchy + per-level crossing-manifold colorings.
MlFusedCtx demMakeMlFusedCtx(CpExec &space, const ContactHierarchy &H, const std::vector< std::vector< int > > &bkOffs, int numManifolds, int numReal, int coarseSweeps, Kokkos::View< int *, CpMem > offsDev, Kokkos::View< unsigned *, CpMem > bar)
Build the fused-coarse-cycle context from an already-built hierarchy + its dense buckets: flatten the...
constexpr int kMlMaxLevels
6-bit colour slots per level in the packed word; slot value 63 = not crossing / not eligible at that ...
Kokkos::DefaultExecutionSpace CpExec
dem — fused colour sweeps: one persistent kernel per sweep instead of one kernel launch per colour.
dem — portable (Kokkos) manifold velocity solve (normal restitution impulse).
Host-side description of one built hierarchy (offsets into the packed group pools).
Portable mirror of ManifoldConstraint.
The per-body inputs of the rigid coarse space (§2.3-§2.5): the predicted positions (the member offset...
Kokkos::View< const float *[3], CpMem > posPred
Kokkos::View< const float *[3], CpMem > invIc
Kokkos::View< float *[3], CpMem > angVelPred
Kokkos::View< const float *[4], CpMem > quat
One coarse row, evaluated up to its effective mass (the part solveOne shares with the oracle test's s...
The per-manifold coarse-PGS body (e = 0, shared fine accumulator) lives in MlCoarseSweep so the per-c...
void prolongRigid(int i, int off, int g, const VV &velPred, const VV0 &velG0) const
v_q += (V_g - V0_g) + (Omega_g - Omega0_g) x d_q; w_q += Omega_g - Omega0_g.
bool row(int idx, int off, Row &r) const
Kokkos::View< float *[3], CpMem > velG
Kokkos::View< float *[3], CpMem > angG
Kokkos::View< float *, CpMem > lambdaAcc
Kokkos::View< const ManifoldC *, CpMem > manifolds
Kokkos::View< const float *[3], CpMem > comOffG
Kokkos::View< const int *, CpMem > grp
Kokkos::View< const float *[3], CpMem > originG
Kokkos::View< const int *, CpMem > realIdx
Kokkos::View< const float *, CpMem > restRel
Kokkos::View< const float *[6], CpMem > invIG
Kokkos::View< float *[3], CpMem > angG0
Kokkos::View< const float *, CpMem > invMassG
void normalizeAngular(int gi) const
Omega_g = invI_g L_g, snapshot Omega0_g.
Kokkos::View< float, CpMem > maxApproach
void solveOne(int idx, int off) const
void restrictAngular(int i, int off, int g, float m, const VV &velPred) const
L_g += mu_q (d_q x v_q) + J_q w_q (3 float atomics per member).
Fused-coarse-cycle context: flat per-level colour offsets on device + barrier + meta.
Kokkos::View< unsigned *, CpMem > bar
Kokkos::View< const int *, CpMem > offsDev
Host-POD description of a built hierarchy for the fused coarse-cycle kernel (fixed-size arrays: kerne...
Device scratch for the multilevel pass, sized once (see Particles::allocate).
Kokkos::View< int *, CpMem > mate
Kokkos::View< float *[3], CpMem > originG
Kokkos::View< float *[3], CpMem > comOffG
Kokkos::View< float *, CpMem > invMassG
Kokkos::View< double *[7], CpMem > accD
Kokkos::View< float *[3], CpMem > angG
Kokkos::View< float *, CpMem > massG
Kokkos::View< float *[3], CpMem > angG0
Kokkos::View< float *[3], CpMem > velG0
Kokkos::View< int *, CpMem > grp
Kokkos::View< long long *, CpMem > colorPacked
Kokkos::View< int *, CpMem > parent
Kokkos::View< float *[6], CpMem > invIG
Kokkos::View< float *[3], CpMem > velG
Double-precision copy of dem_portable's rotateVector (the same formula), for the build.