peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
sim.hpp
Go to the documentation of this file.
1
8#ifndef DEM_SIM_HPP
9#define DEM_SIM_HPP
10
11#include <algorithm>
12#include <cmath>
13#include <cstdio>
14#include <cstdlib>
15#include <fstream>
16#include <Kokkos_Core.hpp>
17#include <memory>
18#include <stdexcept>
19#include <string>
20#include <utility>
21#include <vector>
22
23#include "broadphase_arborx.hpp"
25#include "integration.hpp"
26#include "io.hpp"
27#include "narrowphase.hpp"
28#include "output_sdf.hpp"
29#include "particles.hpp"
30#include "peclet/core/common/view.hpp" // peclet::core::toVector — single-copy device View -> host std::vector (S2a)
31#include "periodicity.hpp"
32#include "shapes_portable.hpp"
33#include "sleeping.hpp" // island sleeping / freezing (single-GPU statics)
34#include "solve_driver.hpp" // demSolveContacts + SoloSolveHooks + readInt/readFloat
35#include "solve_driver_force.hpp" // demStepForce + HertzMindlinLaw + demStepHertz
36#include "solver_friction.hpp"
37#include "solver_hertz.hpp"
38#include "solver_multilevel.hpp"
39#include "solver_position.hpp"
40#include "solver_velocity.hpp"
41
42#ifdef PECLET_DEM_MPI
43#include "mpi_halo.hpp" // ParticleHalo (gated; default module never includes it)
44#endif
45
46namespace peclet::dem {
47
49inline void demStep(Particles& P) {
50 CpExec space;
51
52 // growth ramp (faithful to CUDA Simulation::step): factor *= exp(rate*dt), capped at 1, then
53 // scale = targetScale * factor. (CUDA stores the unscaled target in d_target_scales.)
54 if (P.growthFactor != -1.0f && P.growthRate != 0.0f) {
55 P.growthFactor *= std::exp(P.growthRate * P.dt);
56 if (P.growthFactor > 1.0f)
57 P.growthFactor = 1.0f;
58 }
59 if (P.growthFactor > 0.0f)
61
62 // ghost band + broadphase margin sized off the ACTUAL max grain radius (post-growth), so SI-unit
63 // particles just work; identical to the old 0.1/1.0*globalScale when globalScale ~ the grain
64 // size.
65 const float maxRad = maxOwnedRadius(P);
66 const float margin = 0.1f * maxRad;
67
71
72 // Island sleeping (single-GPU statics, opt-in, gravity on, no external drag): freeze the
73 // currently-asleep bodies so gravity/prediction do not move them; the wake pass + both-asleep
74 // exclusion + effective-inverse-mass swap happen after the narrow phase (below). See
75 // sleeping.hpp.
76 const float gMag =
77 std::sqrt(P.gravity.x * P.gravity.x + P.gravity.y * P.gravity.y + P.gravity.z * P.gravity.z);
78 const bool sleepStep = P.sleepingEnabled && gMag > 0.0f && !P.extForceActive;
79 const float sleepVRest = 2.0f * P.dt * gMag;
80 if (sleepStep)
82 P.angVelPred);
83
84 {
85 auto ri = P.realIndices;
86 Kokkos::parallel_for(
87 "self", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
88 KOKKOS_LAMBDA(int i) { ri(i) = i; });
89 }
90 Kokkos::deep_copy(space, P.topGhost, P.numReal);
91 // periodic ghost band = max grain radius: the CLOSER particle of any cross-boundary contacting
92 // pair is within one radius of the face, so a band of maxRad ghosts it (sufficient for
93 // sphere-sphere).
94 const float ghostBand = maxRad;
95 // Size the SoA for the ghost boundary layer BEFORE emitting (CUDA did this in initialize() via
96 // calculate_capacity). Without it a Simulation(numReal) leaves capacity==numReal, so every ghost
97 // overflows P.capacity in generateGhostsKokkos and cross-boundary contacts are never detected.
99 generateGhostsKokkos(P.numReal, P.capacity, P.domain, ghostBand, P.pos, P.invMass, P.posPred,
100 P.vel, P.velPred, P.quat, P.quatPred, P.angVel, P.angVelPred, P.scale,
103
104 {
105 auto sc = P.scale;
106 auto rad = P.rad;
107 float gs = P.globalScale, bR = P.baseRadius;
108 Kokkos::parallel_for(
109 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
110 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
111 }
112 // Collision detection runs on the PREDICTED state (speculative positions/orientations), matching
113 // the CUDA solver — the position solve then corrects posPred against these contacts.
114 // findCollisionsGrow fences + reads the pair count back to host and guarantees np ≤ P.pairs
115 // extent (growing the buffer on overflow) so the narrowphase never reads P.pairs out of bounds.
116 // Verlet-cached broadphase (opt-in, non-periodic only: periodic ghosts are regenerated per step
117 // with unstable slot ids, which cached pairs would reference). Skips the ArborX rebuild while
118 // nothing moved more than skin/2 — composes with sleeping (a frozen bed never rebuilds).
119 const bool verletOK = P.verletSkinFrac > 0.0f && !P.domain.periodic_x && !P.domain.periodic_y &&
121 const int np = verletOK ? findCollisionsVerlet(P, margin, maxRad) : findCollisionsGrow(P, margin);
122
123 Kokkos::deep_copy(space, P.contactCount, 0);
124 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
126 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid,
129 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
130 P.maxOverlap);
131 if (P.numWalls > 0)
133 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
135 const int nc = readInt(P.contactCount);
136
138 const int nm = readInt(P.manifoldCount);
139
140 // Island sleeping: wake any sleeper actually disturbed (fast approaching neighbour, moving wall,
141 // or a change in its contact set), then flag the frozen (both-asleep) manifolds/contacts and give
142 // every sleeper effective inverse mass 0 for the solve. Swapping P.invMass -> P.invMassEff makes
143 // a sleeper immovable everywhere in the driver (both-asleep constraints become no-ops; the
144 // exclusion just skips their now-wasted colouring/sweeps/hierarchy work), so the solve is
145 // unchanged for awake bodies and the ledger still carries the frozen force network.
146 Vf savedInvMass = P.invMass;
147 if (sleepStep) {
148 wakeDisturbedKokkos(P.manifolds, nm, P.realIndices, P.velPred, P.wakeScale * sleepVRest,
156 P.invMass = P.invMassEff;
157 }
158
159 // Full modern velocity + position solve (warm-started colored PGS, gravity statics /
160 // stabilization, friction, colored-GS overlap projection) — shared with the distributed step.
162
163 if (sleepStep)
164 P.invMass = savedInvMass; // restore the real inverse mass for the commit / next step
166
167 // Sleep detection: a grounded body whose motion stayed below the resting floor for K substeps
168 // goes to sleep (velocity zeroed). Runs on the committed velocities + the solve's grounded
169 // levels.
170 if (sleepStep)
172 P.asleep, P.sleepCounter, P.sleepScale * sleepVRest, P.sleepK, P.vel,
173 P.angVel);
174
175 // Berendsen thermostat at the end of the step (CUDA Simulation::step), tau>0 enables.
176 if (P.thermostatTau > 0.0f && P.dt > 0.0f)
179}
180
186 CpExec space;
187 const float maxRad = maxOwnedRadius(P);
188 const float margin = 0.1f * maxRad;
189 P.numParticles = P.numReal;
190 Kokkos::deep_copy(P.posPred, P.pos);
191 Kokkos::deep_copy(P.quatPred, P.quat);
192 {
193 auto ri = P.realIndices;
194 Kokkos::parallel_for(
195 "self", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
196 KOKKOS_LAMBDA(int i) { ri(i) = i; });
197 }
198 Kokkos::deep_copy(space, P.topGhost, P.numReal);
199 const float ghostBand = maxRad;
200 // Match demStep: ensure ghost-boundary-layer headroom so cross-boundary overlaps are counted (a
201 // Simulation(numReal) otherwise has capacity==numReal and every ghost overflows). See demStep.
203 generateGhostsKokkos(P.numReal, P.capacity, P.domain, ghostBand, P.pos, P.invMass, P.posPred,
204 P.vel, P.velPred, P.quat, P.quatPred, P.angVel, P.angVelPred, P.scale,
207 {
208 auto sc = P.scale;
209 auto rad = P.rad;
210 float gs = P.globalScale, bR = P.baseRadius;
211 Kokkos::parallel_for(
212 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
213 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
214 }
215 const int np = findCollisionsGrow(P, margin);
216 Kokkos::deep_copy(space, P.contactCount, 0);
217 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
219 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid,
222 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
223 P.maxOverlap);
224 if (P.numWalls > 0)
226 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
228 P.numParticles = P.numReal;
229 float h;
230 Kokkos::deep_copy(h, P.maxOverlap);
231 return h;
232}
233
234#ifdef PECLET_DEM_MPI
243struct MpiSolveHooks {
244 static constexpr bool distributed = true;
245 ParticleHalo& halo;
246 int syncEvery;
247 bool forwardRotation;
248 float allMax(float v) const {
249 float g = v;
250 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MAX, halo.comm());
251 return g;
252 }
253 bool syncPoint(int it) const { return (it + 1) % syncEvery == 0; }
254 void syncVelocities(Particles& P) const {
255 halo.forward(P.velPred);
256 if (forwardRotation)
257 halo.forward(P.angVelPred);
258 }
259 void syncPositions(Particles& P) const {
260 halo.forwardPositions(P.posPred);
261 if (forwardRotation)
262 halo.forward4(P.quatPred);
263 }
264};
265
282inline void demStepMpi(Particles& P, ParticleHalo& halo, double rcut, int syncEvery,
283 bool forwardRotation) {
284 CpExec space;
285 const float margin = 0.1f * maxOwnedRadius(P);
286
287 if (P.growthFactor != -1.0f && P.growthRate != 0.0f) {
288 P.growthFactor *= std::exp(P.growthRate * P.dt);
289 if (P.growthFactor > 1.0f)
290 P.growthFactor = 1.0f;
291 }
292 if (P.growthFactor > 0.0f)
293 updateGrowthScalesKokkos(P.numReal, P.scale, P.targetScale, P.growthFactor);
294
295 // 1. Predict velocity on the owned set (no ghosts yet -> numParticles == numReal).
296 P.numParticles = P.numReal;
297 predictVelocityKokkos(P.numReal, P.pos, P.invMass, P.vel, P.quat, P.angVel, P.invInertia,
298 P.posPred, P.quatPred, P.velPred, P.angVelPred, P.deltaPos, P.deltaQuat,
299 P.deltaVel, P.deltaAngVel, P.constraintCounts, P.gravity, P.dt, P.extForce);
300
301 // 2. Gather ghosts (real mass) from owners over the halo: full state -- including gid,
302 // materialId and the warm grounded level -- into the ghost slots; sets
303 // P.numParticles = numReal + numGhost and self-maps realIndices.
304 halo.gather(P, rcut);
305
306 {
307 auto sc = P.scale;
308 auto rad = P.rad;
309 float gs = P.globalScale, bR = P.baseRadius;
310 Kokkos::parallel_for(
311 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
312 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
313 }
314
315 // 3. Broad/narrow phase + manifold reduction over owned + ghosts (contactSlot map included:
316 // the PGS friction bound and the position-channel Coulomb carry read through it).
317 // findCollisionsGrow fences + reads the pair count back to host and guarantees np <= P.pairs
318 // extent (growing the buffer on overflow) so the narrowphase never reads P.pairs out of bounds.
319 const int np = findCollisionsGrow(P, margin);
320
321 Kokkos::deep_copy(space, P.contactCount, 0);
322 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
323 detectContactsKokkos(P.pairs, np, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes, P.shell,
324 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid,
325 P.materialId, P.pairMaterials);
326 detectBoundaryKokkos(P.numReal, P.numPlanes, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes,
327 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
328 P.maxOverlap);
329 if (P.numWalls > 0)
330 detectWallSdfKokkos(P.numReal, P.numWalls, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes,
331 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
332 P.contactCount, P.maxOverlap, P.materialId, P.pairMaterials);
333 const int nc = readInt(P.contactCount);
334
335 reduceContactsToManifoldsKokkos(P.contacts, nc, P.manifolds, P.manifoldCount, P.contactSlot);
336 const int nm = readInt(P.manifoldCount);
337
338 // 4-6. The shared modern velocity + position solve, distributed: rank-local colouring over the
339 // owned + ghost body slots (nBodies = numParticles; realIndices are self-mapped, so ghost
340 // copies evolve in place between refreshes), persistent-pair keys from the global ids.
341 demSolveContacts(P, nc, nm, P.numParticles, P.gid,
342 MpiSolveHooks{halo, syncEvery < 1 ? 1 : syncEvery, forwardRotation});
343
344 // 7. Commit (owned results kept; ghosts discarded, re-gathered next substep).
345 finalCommitKokkos(P.numReal, P.pos, P.invMass, P.posPred, P.quat, P.quatPred, P.domain);
346
347 if (P.thermostatTau > 0.0f && P.dt > 0.0f)
348 applyThermostatKokkos(P.numReal, P.vel, P.invMass, P.angVel, P.invInertia, P.quat,
349 P.thermostatKB, P.thermostatTau, P.thermostatTemp, P.dt);
350
351 P.numParticles = P.numReal; // restore owned-only active count for getters
352}
353
360struct MpiForceHooks {
361 static constexpr bool distributed = true;
362 ParticleHalo& halo;
363 double band;
364 float allMax(float v) const {
365 float g = v;
366 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MAX, halo.comm());
367 return g;
368 }
369 float allMin(float v) const {
370 float g = v;
371 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MIN, halo.comm());
372 return g;
373 }
374 void gatherGhosts(Particles& P) const {
375 halo.invalidateTopology(); // fresh band + fresh positions at every pair rebuild
376 halo.gather(P, band);
377 fillWorldRadiiKokkos(P.scale, P.rad, P.globalScale, P.baseRadius, P.numParticles);
378 }
379 void refreshGhostState(Particles& P, bool needQuat) const {
380 halo.forwardPositions(P.pos);
381 halo.forward(P.vel);
382 halo.forward(P.angVel);
383 if (needQuat)
384 halo.forward4(P.quat);
385 }
386 void clearGhostScratch(Particles& P) const {
387 zeroForceScratchKokkos(P.deltaVel, P.deltaAngVel, P.numReal, P.numReal + halo.numGhost());
388 }
389};
390
394inline void demStepHertzMpi(Particles& P, ParticleHalo& halo, float dt, int nsteps,
395 float skinFrac) {
396 // Ghost band = worst-case pair cutoff (2 R_max,global) + skin. The driver's skin is
397 // skinFrac * R_min,global <= skinFrac * R_max,global, so this band bounds it.
398 float maxR = maxOwnedRadius(P), maxRg = maxR;
399 MPI_Allreduce(&maxR, &maxRg, 1, MPI_FLOAT, MPI_MAX, halo.comm());
400 const double band = (2.0 + skinFrac) * static_cast<double>(maxRg);
401 demStepForce(P, dt, nsteps, skinFrac, HertzMindlinLaw{}, MpiForceHooks{halo, band});
402 P.numParticles = P.numReal; // restore owned-only active count for getters
403}
404#endif // PECLET_DEM_MPI
405
408 public:
409 explicit Simulation(int capacity) {
410 registry().push_back(this);
411 P_.allocate(capacity, capacity * 64, capacity * 16, /*shapes*/ 1, /*shell*/ 1, /*planes*/ 8);
412 // default sphere shape (radius 1) + identity-ish defaults
413 setSphereShape(1.0f);
414 // A/B hook for the validation battery (mirrors PECLET_DEM_STAB_MODE's role): switch the
415 // restitution model without touching driver scripts.
416 if (const char* e = std::getenv("PECLET_DEM_REST_MODEL"); e && *e)
418 // Island sleeping A/B env (default ON): PECLET_DEM_SLEEP=1 enables, =0 disables; the scales /
419 // K / wake threshold have their own overrides for the tuning battery.
420 if (const char* e = std::getenv("PECLET_DEM_SLEEP"); e && *e)
421 P_.sleepingEnabled = std::atoi(e) != 0;
422 if (const char* e = std::getenv("PECLET_DEM_SLEEP_SCALE"); e && *e)
423 P_.sleepScale = std::atof(e);
424 if (const char* e = std::getenv("PECLET_DEM_SLEEP_K"); e && *e)
425 P_.sleepK = std::atoi(e);
426 if (const char* e = std::getenv("PECLET_DEM_WAKE_SCALE"); e && *e)
427 P_.wakeScale = std::atof(e);
428 if (const char* e = std::getenv("PECLET_DEM_SLEEP_WAKELOST"); e && *e)
429 P_.sleepWakeLostContact = std::atoi(e) != 0;
430 if (const char* e = std::getenv("PECLET_DEM_SLEEP_INVMASS_FRAC"); e && *e)
431 P_.sleepImmovableFrac = std::atof(e);
432 // Verlet-cached impulse broadphase (default OFF): PECLET_DEM_VERLET_SKIN = skin fraction of the
433 // max grain radius (e.g. 0.3). 0 = rebuild every step.
434 if (const char* e = std::getenv("PECLET_DEM_VERLET_SKIN"); e && *e)
435 P_.verletSkinFrac = std::atof(e);
436 }
438 auto& r = registry();
439 r.erase(std::remove(r.begin(), r.end(), this), r.end());
440 }
441
442 // Teardown safety: the Particles SoA holds Kokkos Views, so they MUST be freed before
443 // Kokkos::finalize (else "deallocated after finalize" aborts). releaseAll() (called from the
444 // module's atexit, before finalize) frees every live Sim's Views, so callers need not `del sim;
445 // gc.collect()` themselves.
447 P_ = Particles{};
448#ifdef PECLET_DEM_MPI
449 halo_.reset(); // the halo also owns Kokkos Views (gather/forward buffers + its core
450 // sub-objects') that must be freed before Kokkos::finalize, else "deallocated
451 // after finalize" aborts. Destroying it via the unique_ptr frees them all.
452#endif
453 }
454 static void releaseAll() {
455 for (auto* s : registry())
456 s->releaseViews();
457 }
458 static std::vector<Simulation*>& registry() {
459 static std::vector<Simulation*> r;
460 return r;
461 }
462
463 void setSphereShape(float radius) { initializeShape(SPHERE, radius, 0.0f, 0.0f); }
464
465 // Mirror of CUDA Simulation::initialize(shape_type, radius, height, thickness): builds shape 0's
466 // descriptor + surface point shell (cylinder/box) and records the per-shape base radius and
467 // (uniform-mass=1) inverse inertia applied to every particle by setPositions. shape_type uses the
468 // peclet::dem::ShapeKind values (SPHERE=1, HOLLOW_CYLINDER=2, BOX=3).
469 void initializeShape(int shape_type, float radius, float height, float thickness) {
470 baseRadius_ = radius;
471 P_.baseRadius =
472 radius; // effective radius = scale*globalScale*baseRadius (broadphase + ghost band)
473 F4 params{radius, 0, 0, 0};
474 std::vector<F3> shell;
475
476 if (shape_type == HOLLOW_CYLINDER) {
477 params = F4{radius, height, thickness, 0};
478 // Dynamic spacing (faithful to CUDA): >=4 pts across thickness, >=20 around circumference.
479 float min_dim = std::min(radius, thickness);
480 if (min_dim < 1e-4f)
481 min_dim = radius; // safety if thickness 0
482 float spacing = std::min(radius * 0.3f, min_dim * 0.5f);
483 if (spacing < 1e-3f)
484 spacing = 1e-3f;
485 shell = genCylinderShell(radius, height, thickness, spacing);
486 } else if (shape_type == BOX) {
487 // Cube with half-extent = radius (side = 2*radius).
488 params = F4{radius, radius, radius, 0};
489 float spacing = std::max(radius * 0.5f, 1e-3f);
490 shell = genBoxShell(radius, radius, radius, spacing);
491 } else {
492 shape_type = SPHERE;
493 params = F4{radius, 0, 0, 0}; // sphere: analytic single-probe, no shell
494 }
495
496 // Upload the shell (resize the View; numPoints==0 => analytic single-probe like the sphere).
497 const int nPts = static_cast<int>(shell.size());
498 shellPoints_ = nPts;
499 if (nPts > 0) {
500 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
501 auto hs = Kokkos::create_mirror_view(P_.shell);
502 for (int i = 0; i < nPts; ++i) {
503 hs(i, 0) = shell[i].x;
504 hs(i, 1) = shell[i].y;
505 hs(i, 2) = shell[i].z;
506 }
507 Kokkos::deep_copy(P_.shell, hs);
508 }
509
510 auto h = Kokkos::create_mirror_view(P_.shapes);
511 h(0) = ShapeDesc{shape_type, params, 0, nPts};
512 Kokkos::deep_copy(P_.shapes, h);
513
514 // Per-shape inverse inertia (mass=1), faithful to CUDA Simulation::initialize.
515 float ix = 1.0f, iy = 1.0f, iz = 1.0f;
516 if (shape_type == SPHERE) {
517 if (baseRadius_ > 0.0f) {
518 float v = 2.5f / (baseRadius_ * baseRadius_);
519 ix = iy = iz = v;
520 }
521 } else if (shape_type == HOLLOW_CYLINDER) {
522 float r_out = baseRadius_, r_in = baseRadius_ - thickness;
523 if (r_in < 0)
524 r_in = 0;
525 float term_r = r_out * r_out + r_in * r_in;
526 float I_zz = 0.5f * term_r;
527 float I_xx = (1.0f / 12.0f) * (3.0f * term_r + height * height);
528 if (I_xx > 1e-6f) {
529 ix = 1.0f / I_xx;
530 iy = 1.0f / I_xx;
531 }
532 if (I_zz > 1e-6f)
533 iz = 1.0f / I_zz;
534 } else if (shape_type == BOX) {
535 float L = 2.0f * baseRadius_;
536 float I = (1.0f / 6.0f) * L * L;
537 if (I > 1e-6f) {
538 ix = iy = iz = 1.0f / I;
539 }
540 }
541 defaultInvI_ = F3{ix, iy, iz};
542 }
543
544 // Import a general particle as a grid SDF (canonical, unit-scale space) + a surface point shell +
545 // its unit-mass principal-frame diagonal inverse inertia. Replaces shape 0, so every particle
546 // becomes an instance of this shape (per-particle position/quaternion/scale still apply). `grid`
547 // holds nx*ny*nz signed-distance samples, x-fastest (idx = x + y*nx + z*nx*ny), at lattice nodes
548 // q = origin + (x,y,z)*spacing (negative inside). `shellFlat` is the flat [nPts*3] surface point
549 // set the collision probes body A against. boundingRadius is the canonical radius enclosing the
550 // surface (broad-phase + VTI splat bound). invInertia is the per-unit-mass diagonal inverse
551 // inertia in the body (principal) frame; like the analytic shapes it becomes the default applied
552 // to every particle by setPositions (override afterwards with set_inv_inertia / set_inv_mass for
553 // a real density). See peclet.dem.particle_builder for the Python side that produces these arrays
554 // from an implicit-solid SDF (marching-cubes shell + voxel-integrated mass properties).
555 void setSdfShape(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
556 const std::vector<float>& shellFlat, F3 invInertia, float boundingRadius) {
557 if (nx < 2 || ny < 2 || nz < 2)
558 throw std::runtime_error("setSdfShape: grid dims must be >= 2 on each axis");
559 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
560 throw std::runtime_error("setSdfShape: grid.size() must equal nx*ny*nz");
561 const int nPts = static_cast<int>(shellFlat.size() / 3);
562 if (nPts <= 0)
563 throw std::runtime_error("setSdfShape: empty surface point shell");
564
565 // Upload the signed-distance samples.
566 P_.sdfGrid = Kokkos::View<float*, CpMem>("sdfGrid", grid.size());
567 auto hg = Kokkos::create_mirror_view(P_.sdfGrid);
568 for (size_t i = 0; i < grid.size(); ++i)
569 hg(i) = grid[i];
570 Kokkos::deep_copy(P_.sdfGrid, hg);
571
572 // Upload the surface point shell.
573 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
574 auto hs = Kokkos::create_mirror_view(P_.shell);
575 for (int i = 0; i < nPts; ++i) {
576 hs(i, 0) = shellFlat[3 * i];
577 hs(i, 1) = shellFlat[3 * i + 1];
578 hs(i, 2) = shellFlat[3 * i + 2];
579 }
580 Kokkos::deep_copy(P_.shell, hs);
581
582 // Shape descriptor: grid SDF for the field, shell points for the probes.
583 ShapeDesc sd{};
584 sd.type = SHAPE_GRID_SDF;
585 sd.params = F4{boundingRadius, 0, 0, 0};
586 sd.shellOffset = 0;
587 sd.numPoints = nPts;
588 sd.gridOffset = 0;
589 sd.nx = nx;
590 sd.ny = ny;
591 sd.nz = nz;
592 sd.gridOrigin = origin;
593 sd.gridInvSpacing =
594 F3{spacing.x > 0 ? 1.0f / spacing.x : 0.0f, spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
595 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
596 auto h = Kokkos::create_mirror_view(P_.shapes);
597 h(0) = sd;
598 Kokkos::deep_copy(P_.shapes, h);
599
600 baseRadius_ = boundingRadius;
601 P_.baseRadius = boundingRadius;
602 defaultInvI_ = invInertia;
603 shellPoints_ = nPts;
604 ensureContactCapacity();
605 }
606
607 void setDomain(float lx, float ly, float lz, bool px, bool py, bool pz) {
608 P_.domain = Domain{F3{0, 0, 0}, F3{lx, ly, lz}, F3{lx, ly, lz}, px, py, pz};
609 P_.skin = 0.1f * P_.globalScale;
610 }
611 // CUDA Simulation::set_domain(min, max): arbitrary origin; keeps the current periodicity flags.
612 void setDomainMinMax(F3 mn, F3 mx) {
613 P_.domain = Domain{mn,
614 mx,
615 F3{mx.x - mn.x, mx.y - mn.y, mx.z - mn.z},
618 P_.domain.periodic_z};
619 P_.skin = 0.1f * P_.globalScale;
620 }
621 void enablePeriodicity(bool x, bool y, bool z) {
622 P_.domain.periodic_x = x;
623 P_.domain.periodic_y = y;
624 P_.domain.periodic_z = z;
625 }
626 std::tuple<float, float, float> getDomainMin() const {
627 return {P_.domain.min.x, P_.domain.min.y, P_.domain.min.z};
628 }
629 std::tuple<float, float, float> getDomainMax() const {
630 return {P_.domain.max.x, P_.domain.max.y, P_.domain.max.z};
631 }
632 void setGravity(float gx, float gy, float gz) { P_.gravity = F3{gx, gy, gz}; }
633 void setThermostat(float temperature, float tau, float kB) { // Berendsen; tau=0 disables
634 P_.thermostatTemp = temperature;
635 P_.thermostatTau = tau;
636 P_.thermostatKB = kB;
637 }
638 void setSolverIterations(int pos, int vel) {
639 P_.positionIterations = pos;
640 P_.velocityIterations = vel;
641 }
642 // Select the single-GPU collision solves: true (default) = colored Gauss–Seidel for both the
643 // restitution and the overlap solve, false = count-averaged Jacobi (legacy). For A/B validation.
644 void setVelocityUseGS(bool useGS) { P_.velocityUseGS = useGS; }
649 void setStabilization(bool enabled) { P_.stabilizationMode = enabled ? 1 : 0; }
656 void setStabilizationMode(const std::string& mode) {
657 if (mode == "off")
658 P_.stabilizationMode = 0;
659 else if (mode == "onesided")
660 P_.stabilizationMode = 1;
661 else if (mode == "multilevel")
662 P_.stabilizationMode = 2;
663 else if (mode == "escalate")
664 P_.stabilizationMode = 3;
665 else if (mode == "ordered")
666 P_.stabilizationMode = 4;
667 else
668 throw std::invalid_argument(
669 "set_stabilization_mode: expected 'off', 'onesided', 'multilevel', 'escalate' or "
670 "'ordered'");
671 }
677 void setRestitutionModel(const std::string& model) {
678 if (model == "newton")
679 P_.restitutionModel = 0;
680 else if (model == "poisson")
681 P_.restitutionModel = 1;
682 else
683 throw std::invalid_argument("set_restitution_model: expected 'newton' or 'poisson'");
684 }
698 void setSleeping(bool enabled, float threshold_scale = 2.0f, int consecutive = 64,
699 float wake_scale = 40.0f) {
700 P_.sleepingEnabled = enabled;
701 if (threshold_scale > 0.0f)
702 P_.sleepScale = threshold_scale;
703 if (consecutive > 0)
704 P_.sleepK = consecutive;
705 if (wake_scale > 0.0f)
706 P_.wakeScale = wake_scale; // hysteresis: wake only well above the residual settling jitter
707 }
713 void setVerletSkin(float skin_frac) {
714 P_.verletSkinFrac = skin_frac;
715 P_.impNumPairs = -1; // invalidate the cache
716 }
718 int numAsleep() {
719 int n = 0;
720 auto a = P_.asleep;
721 Kokkos::parallel_reduce(
722 "peclet::dem::count_asleep", Kokkos::RangePolicy<CpExec>(0, P_.numReal),
723 KOKKOS_LAMBDA(int i, int& acc) { acc += a(i) ? 1 : 0; }, n);
724 return n;
725 }
728 void setHertzMaterial(int mat, float youngs, float poisson) {
729 if (mat < 0 || mat >= 8)
730 throw std::invalid_argument("material id out of range");
731 auto he = Kokkos::create_mirror_view(P_.hertzE);
732 auto hn = Kokkos::create_mirror_view(P_.hertzNu);
733 Kokkos::deep_copy(he, P_.hertzE);
734 Kokkos::deep_copy(hn, P_.hertzNu);
735 he(mat) = youngs;
736 hn(mat) = poisson;
737 Kokkos::deep_copy(P_.hertzE, he);
738 Kokkos::deep_copy(P_.hertzNu, hn);
739 }
742 void stepHertz(float dt, int substeps, float skin_frac) {
743 P_.dt = dt;
744 demStepHertz(P_, dt, substeps, skin_frac);
745 }
746 void setGlobalScale(float s) {
747 P_.globalScale = s;
748 P_.skin = 0.1f * s;
749 }
750 void setDt(float dt) { P_.dt = dt; }
751 // (restitution_normal, restitution_tangent, friction) to match CUDA set_material_params; the
752 // Kokkos pipeline currently carries normal restitution + dynamic friction (tangential restitution
753 // unused).
754 void setMaterialParams(float restitution_normal, float restitution_tangent, float friction) {
755 P_.restitutionNormal = restitution_normal;
756 P_.frictionDynamic = friction;
757 P_.restitutionTangent = restitution_tangent; // Walton beta (impact law, cone-clamped)
758 }
762 void setMaterialIds(const std::vector<int>& ids) {
763 auto h = Kokkos::create_mirror_view(P_.materialId);
764 Kokkos::deep_copy(h, P_.materialId);
765 for (int i = 0; i < P_.numReal && i < (int)ids.size(); ++i)
766 h(i) = static_cast<unsigned char>(ids[i]);
767 Kokkos::deep_copy(P_.materialId, h);
768 }
771 void setPairMaterial(int a, int b, float restitution, float friction) {
772 if (a < 0 || b < 0 || a >= kMaxMaterials || b >= kMaxMaterials)
773 throw std::invalid_argument("material id out of range");
774 if (P_.pairMaterials.extent(0) == 0) {
775 P_.pairMaterials =
776 Kokkos::View<float*, CpMem>("pairMaterials", kMaxMaterials * kMaxMaterials * 2);
777 auto h0 = Kokkos::create_mirror_view(P_.pairMaterials);
778 for (int i = 0; i < kMaxMaterials * kMaxMaterials; ++i) {
779 h0(2 * i) = P_.restitutionNormal;
780 h0(2 * i + 1) = P_.frictionDynamic;
781 }
782 Kokkos::deep_copy(P_.pairMaterials, h0);
783 }
784 auto h = Kokkos::create_mirror_view(P_.pairMaterials);
785 Kokkos::deep_copy(h, P_.pairMaterials);
786 for (auto xy : {std::pair<int, int>{a, b}, std::pair<int, int>{b, a}}) {
787 h((xy.first * kMaxMaterials + xy.second) * 2) = restitution;
788 h((xy.first * kMaxMaterials + xy.second) * 2 + 1) = friction;
789 }
790 Kokkos::deep_copy(P_.pairMaterials, h);
791 }
793 void setWallMaterialId(int wid, int mat) {
794 if (wid < 0 || wid >= P_.numWalls)
795 throw std::invalid_argument("wall index out of range");
796 auto h = Kokkos::create_mirror_view(P_.walls);
797 Kokkos::deep_copy(h, P_.walls);
798 h(wid).materialId = mat;
799 Kokkos::deep_copy(P_.walls, h);
800 }
801 void addPlane(float px, float py, float pz, float nx, float ny, float nz) {
802 auto h = Kokkos::create_mirror_view(P_.planes);
803 Kokkos::deep_copy(h, P_.planes);
804 if (P_.numPlanes < static_cast<int>(P_.planes.extent(0)))
805 h(P_.numPlanes++) = PlaneP{F3{px, py, pz}, F3{nx, ny, nz}};
806 Kokkos::deep_copy(P_.planes, h);
807 }
808
809 // Add a static, world-space SDF wall/container the grains collide against (a drum barrel, hopper,
810 // vibrating tray). `grid` is a flat [nx*ny*nz] signed-distance field, x-fastest (idx = x + y*nx +
811 // z*nx*ny), sampled at world nodes origin + (x,y,z)*spacing — POSITIVE in the void where the
812 // grains live, NEGATIVE inside the solid wall (so a grain surface point reads the penetration
813 // depth and the outward gradient is the push-out normal). restitution/friction are the binary
814 // particle–wall material (independent of the body-body material). The wall is motionless but
815 // carries a rigid-body surface-velocity field (set via setWallVelocity) so a grain touching it
816 // feels the wall's motion. Returns the wall's index (for setWallVelocity). Add walls before
817 // stepping.
818 int addSdfWall(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
819 float restitution, float friction) {
820 if (nx < 2 || ny < 2 || nz < 2)
821 throw std::runtime_error("addSdfWall: grid dims must be >= 2 on each axis");
822 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
823 throw std::runtime_error("addSdfWall: grid.size() must equal nx*ny*nz");
824
825 WallSdf w{};
826 w.nx = nx;
827 w.ny = ny;
828 w.nz = nz;
829 w.gridOffset = static_cast<int>(wallGridHost_.size());
830 w.origin = origin;
831 w.invSpacing =
832 F3{spacing.x > 0 ? 1.0f / spacing.x : 0.0f, spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
833 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
834 w.restitution = restitution;
835 w.friction = friction;
836 const int idx = static_cast<int>(wallsHost_.size());
837 wallsHost_.push_back(w);
838 wallGridHost_.insert(wallGridHost_.end(), grid.begin(), grid.end());
839 // Upload the concatenated grid samples (only changes when a wall is added, not per step).
840 P_.wallGrid = Kokkos::View<float*, CpMem>("wallGrid", wallGridHost_.size());
841 auto hg = Kokkos::create_mirror_view(P_.wallGrid);
842 for (size_t i = 0; i < wallGridHost_.size(); ++i)
843 hg(i) = wallGridHost_[i];
844 Kokkos::deep_copy(P_.wallGrid, hg);
845 uploadWalls();
846 P_.numWalls = static_cast<int>(wallsHost_.size());
847 if (friction > P_.wallFrictionMax)
848 P_.wallFrictionMax = friction;
849 ensureContactCapacity();
850 return idx;
851 }
852
853 // Set a wall's rigid-body surface-velocity field v(x) = linVel + angVel × (x − center). A grain
854 // in contact feels this velocity even though the geometry never moves: set angVel for a rotating
855 // drum (about `center` on the axis), or drive linVel sinusoidally each step for a vibrating wall.
856 // Cheap (a few host scalars); safe to call every step.
857 void setWallVelocity(int wallIndex, F3 linVel, F3 angVel, F3 center) {
858 if (wallIndex < 0 || wallIndex >= static_cast<int>(wallsHost_.size()))
859 throw std::runtime_error("setWallVelocity: wall index out of range");
860 wallsHost_[wallIndex].linVel = linVel;
861 wallsHost_[wallIndex].angVel = angVel;
862 wallsHost_[wallIndex].center = center;
863 uploadWalls();
864 }
865
866 // positions: flat [n*3]; (re)sets the real-particle count and default state.
867 void setPositions(const std::vector<float>& xyz) {
868 const int n = static_cast<int>(xyz.size() / 3);
869 P_.numReal = n;
870 P_.numParticles = n;
871 P_.hertzNumPairs = -1; // particle indices changed: invalidate the hertz pair cache
872 P_.hertzPrevCount = 0;
873 P_.prevPairCount = 0; // stale persistent-pair ledger must not warm-start the new set
874#ifdef PECLET_DEM_MPI
875 mpiGidsGlobal_ = false; // new particle set -> re-base the global ids at the next stepMpi
876#endif
877 auto pos = Kokkos::create_mirror_view(P_.pos);
878 auto q = Kokkos::create_mirror_view(P_.quat);
879 auto im = Kokkos::create_mirror_view(P_.invMass);
880 auto sc = Kokkos::create_mirror_view(P_.scale);
881 auto ii = Kokkos::create_mirror_view(P_.invInertia);
882 auto sid = Kokkos::create_mirror_view(P_.shapeId);
883 auto vel = Kokkos::create_mirror_view(P_.vel);
884 auto av = Kokkos::create_mirror_view(P_.angVel);
885 auto gi = Kokkos::create_mirror_view(P_.gid);
886 for (int i = 0; i < n; ++i) {
887 gi(i) = i; // identity global id; the MPI enable re-bases it to a global Exscan offset
888 pos(i, 0) = xyz[3 * i];
889 pos(i, 1) = xyz[3 * i + 1];
890 pos(i, 2) = xyz[3 * i + 2];
891 q(i, 0) = 0;
892 q(i, 1) = 0;
893 q(i, 2) = 0;
894 q(i, 3) = 1;
895 im(i) = 1.0f;
896 sc(i) = 1.0f;
897 sid(i) = 0;
898 ii(i, 0) = defaultInvI_.x;
899 ii(i, 1) = defaultInvI_.y;
900 ii(i, 2) = defaultInvI_.z;
901 vel(i, 0) = vel(i, 1) = vel(i, 2) = 0;
902 av(i, 0) = av(i, 1) = av(i, 2) = 0;
903 }
904 Kokkos::deep_copy(P_.pos, pos);
905 Kokkos::deep_copy(P_.quat, q);
906 Kokkos::deep_copy(P_.invMass, im);
907 Kokkos::deep_copy(P_.scale, sc);
908 Kokkos::deep_copy(P_.invInertia, ii);
909 Kokkos::deep_copy(P_.shapeId, sid);
910 Kokkos::deep_copy(P_.vel, vel);
911 Kokkos::deep_copy(P_.angVel, av);
912 Kokkos::deep_copy(P_.gid, gi);
913 Kokkos::deep_copy(P_.targetScale, P_.scale); // unscaled growth target = the set scale
914 }
915 void setScalesUniform(float s) {
916 auto sc = Kokkos::create_mirror_view(P_.scale);
917 for (int i = 0; i < P_.numReal; ++i)
918 sc(i) = s;
919 Kokkos::deep_copy(P_.scale, sc);
920 Kokkos::deep_copy(P_.targetScale, P_.scale);
921 }
922 // per-particle scales (growth target): flat [n]. scale starts at target (growth factor applies in
923 // step).
924 void setScales(const std::vector<float>& s) {
925 auto tsc = Kokkos::create_mirror_view(P_.targetScale);
926 for (int i = 0; i < P_.numReal && i < (int)s.size(); ++i)
927 tsc(i) = s[i];
928 Kokkos::deep_copy(P_.targetScale, tsc);
929 Kokkos::deep_copy(P_.scale, P_.targetScale);
930 }
931 void setVelocities(const std::vector<float>& v) {
932 auto vel = Kokkos::create_mirror_view(P_.vel);
933 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)v.size(); ++i) {
934 vel(i, 0) = v[3 * i];
935 vel(i, 1) = v[3 * i + 1];
936 vel(i, 2) = v[3 * i + 2];
937 }
938 Kokkos::deep_copy(P_.vel, vel);
939 }
940 // Per-particle external FORCE (fluid drag etc.), an (N,3) flat array. Applied in the next
941 // step()'s velocity predict as dv = F*invMass*dt. Persists across steps until re-set or cleared.
942 void setExternalForces(const std::vector<float>& f) {
943 auto ef = Kokkos::create_mirror_view(P_.extForce);
944 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)f.size(); ++i) {
945 ef(i, 0) = f[3 * i];
946 ef(i, 1) = f[3 * i + 1];
947 ef(i, 2) = f[3 * i + 2];
948 }
949 Kokkos::deep_copy(P_.extForce, ef);
950 }
951 void clearExternalForces() { Kokkos::deep_copy(P_.extForce, 0.0f); }
952 const V3& externalForcesView() const { return P_.extForce; }
953 const Vf& invMassView() const { return P_.invMass; }
954 // rigid-body rotation state (the pipeline integrates the gyroscopic Euler term + quaternion
955 // already)
956 void setQuaternions(const std::vector<float>& q) {
957 auto h = Kokkos::create_mirror_view(P_.quat);
958 for (int i = 0; i < P_.numReal && 4 * i + 3 < (int)q.size(); ++i) {
959 h(i, 0) = q[4 * i];
960 h(i, 1) = q[4 * i + 1];
961 h(i, 2) = q[4 * i + 2];
962 h(i, 3) = q[4 * i + 3];
963 }
964 Kokkos::deep_copy(P_.quat, h);
965 }
966 void setAngularVelocities(const std::vector<float>& w) {
967 auto h = Kokkos::create_mirror_view(P_.angVel);
968 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)w.size(); ++i) {
969 h(i, 0) = w[3 * i];
970 h(i, 1) = w[3 * i + 1];
971 h(i, 2) = w[3 * i + 2];
972 }
973 Kokkos::deep_copy(P_.angVel, h);
974 }
975 void setInvInertia(const std::vector<float>& ii) {
976 auto h = Kokkos::create_mirror_view(P_.invInertia);
977 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)ii.size(); ++i) {
978 h(i, 0) = ii[3 * i];
979 h(i, 1) = ii[3 * i + 1];
980 h(i, 2) = ii[3 * i + 2];
981 }
982 Kokkos::deep_copy(P_.invInertia, h);
983 }
984 void setInvMass(const std::vector<float>& im) {
985 auto h = Kokkos::create_mirror_view(P_.invMass);
986 for (int i = 0; i < P_.numReal && i < (int)im.size(); ++i)
987 h(i) = im[i];
988 Kokkos::deep_copy(P_.invMass, h);
989 }
990 std::vector<float> getAngularVelocities() const {
991 return peclet::core::toVector(
992 Kokkos::subview(P_.angVel, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
993 }
994 std::vector<float> getInvInertia() const {
995 return peclet::core::toVector(
996 Kokkos::subview(P_.invInertia, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
997 }
998 // growth: factor *= exp(rate*dt) per step (capped at 1); new_factor<0 keeps/initialises (0.01 if
999 // inactive).
1000 void setGrowthParams(float rate, float new_factor) {
1001 if (P_.growthFactor == -1.0f)
1002 P_.growthFactor = (new_factor > 0.0f) ? new_factor : 0.01f;
1003 else if (new_factor > 0.0f)
1004 P_.growthFactor = new_factor;
1005 P_.growthRate = rate;
1006 if (P_.growthFactor > 0.0f)
1008 }
1009 float growthFactor() const { return P_.growthFactor; }
1010 float getGrowthRate() const { return P_.growthRate; }
1011 // per-particle mass = 1/invMass (0 for fixed/infinite-mass particles), CUDA
1012 // Simulation::get_masses.
1013 std::vector<float> getMasses() const {
1014 auto im = Kokkos::create_mirror_view(P_.invMass);
1015 Kokkos::deep_copy(im, P_.invMass);
1016 std::vector<float> out(P_.numReal);
1017 for (int i = 0; i < P_.numReal; ++i)
1018 out[i] = (im(i) > 0.0f) ? (1.0f / im(i)) : 0.0f;
1019 return out;
1020 }
1021
1022 std::vector<float> getPositions() const {
1023 return peclet::core::toVector(
1024 Kokkos::subview(P_.pos, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
1025 }
1026 std::vector<float> getVelocities() const {
1027 return peclet::core::toVector(
1028 Kokkos::subview(P_.vel, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
1029 }
1030 std::vector<float> getQuaternions() const {
1031 return peclet::core::toVector(
1032 Kokkos::subview(P_.quat, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
1033 }
1034 std::vector<float> getScales() const {
1035 return peclet::core::toVector(Kokkos::subview(P_.scale, Kokkos::make_pair(0, P_.numReal)));
1036 }
1037
1038 // One XPBD substep (CUDA Simulation::step(dt) semantics): dt>0 sets the timestep; dt==0 is a
1039 // dynamics-free relaxation step (overlap removal only). Drive the loop from Python.
1040 void step(float dt) {
1041 P_.dt = dt;
1042 demStep(P_);
1043 }
1044
1045 // Max pair interpenetration on the current committed state (CUDA Simulation::compute_overlaps).
1047
1048 // LAMMPS "dump custom" of the current committed state (CUDA Simulation::export_lammps). Radius =
1049 // scale*globalScale*baseRadius; bounds computed from the particle AABBs.
1050 void exportLammps(const std::string& filename, int step) const {
1051 const std::vector<float> pos = getPositions(), vel = getVelocities(), quat = getQuaternions();
1052 auto sc = Kokkos::create_mirror_view(P_.scale);
1053 Kokkos::deep_copy(sc, P_.scale);
1054 std::vector<float> radii(P_.numReal);
1055 for (int i = 0; i < P_.numReal; ++i)
1056 radii[i] = sc(i) * P_.globalScale * baseRadius_;
1057 const bool pbc = P_.domain.periodic_x || P_.domain.periodic_y || P_.domain.periodic_z;
1058 peclet::dem::writeLammpsDump(filename, step, pos, vel, quat, radii, nullptr, nullptr, pbc);
1059 }
1060
1061 // SDF field over the domain -> ImageData VTI (CUDA Simulation::export_sdf).
1062 void exportSdf(const std::string& filename, int rx, int ry, int rz) {
1063 const std::vector<float> grid = getSdfGrid(rx, ry, rz);
1064 const float mn[3] = {P_.domain.min.x, P_.domain.min.y, P_.domain.min.z};
1065 const float mx[3] = {P_.domain.max.x, P_.domain.max.y, P_.domain.max.z};
1066 peclet::dem::writeSdfVti(filename, grid, rx, ry, rz, mn, mx);
1067 }
1068
1069#ifdef PECLET_DEM_MPI
1070 // Block decomposition over the GLOBAL domain (once); the per-block solver stays non-periodic, the
1071 // halo supplies the periodic wrap. gsize is the ORB cell grid. Mirror of Simulation::mpi_init.
1072 void initMpi(std::tuple<double, double, double> origin, std::tuple<double, double, double> size,
1073 std::tuple<long, long, long> gsize, std::tuple<bool, bool, bool> periodic,
1074 MPI_Comm comm) {
1075 halo_->initMpi({std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
1076 {std::get<0>(size), std::get<1>(size), std::get<2>(size)},
1077 {std::get<0>(gsize), std::get<1>(gsize), std::get<2>(gsize)},
1078 {std::get<0>(periodic), std::get<1>(periodic), std::get<2>(periodic)}, comm);
1079 }
1080 // Enable the distributed step. rcut is the ghost-band width (default = 1.0*globalScale, the
1081 // periodic skin used by the single-GPU path); sync_every is the owner->ghost refresh interval (1
1082 // = EXACT). rebalance_every: re-decompose by particle count + migrate ownership every N
1083 // distributed steps to keep the per-rank load even as a packing densifies (0 = never; the
1084 // partition is then fixed at the initial decomposition, as before). A pure redistribution — the
1085 // physics result is unchanged.
1086 void enableMpiStep(double rcut, int sync_every = 1, bool forward_rotation = true,
1087 int rebalance_every = 0, double verlet_skin = 0.0) {
1088 mpiRcut_ = rcut;
1089 mpiSyncEvery_ = sync_every < 1 ? 1 : sync_every;
1090 mpiForwardRotation_ = forward_rotation;
1091 mpiRebalanceEvery_ = rebalance_every < 0 ? 0 : rebalance_every;
1092 // Verlet-skin ghost reuse (D2): rebuild the halo topology only when a particle has moved >
1093 // skin, instead of every substep. 0 (default) keeps the exact per-substep rebuild.
1094 halo_->setVerletSkin(static_cast<float>(verlet_skin));
1095 }
1096 // Halo rebuild stats (D2): topology rebuilds vs total gather() calls (for benchmarking).
1097 long mpiRebuilds() const { return halo_->numRebuilds(); }
1098 long mpiGathers() const { return halo_->numGathers(); }
1099 // Migrate ownership now so each rank holds a near-equal particle count. Safe to call at a step
1100 // boundary; returns this rank's new owned count. Exposed for manual / adaptive balancing.
1101 int rebalance() { return halo_->rebalance(P_); }
1102 // Co-rebalance: migrate ownership onto the weighted ORB of per-cell weights `w` (the SAME
1103 // partition the coupled flow solver redistributes onto from the same weight field). Returns new
1104 // owned count.
1105 int migrateToWeights(const std::vector<peclet::core::Real>& w) {
1106 return halo_->migrateToWeights(P_, w);
1107 }
1108 // Globally-unique particle ids (persistent-pair and Mindlin-history keys are gid-based):
1109 // re-base each rank's identity ids by an exclusive scan of the owned counts, once per particle
1110 // set. Migration and rebalance carry gids, so the ids stay stable afterwards; set_positions
1111 // resets the flag. Any pre-MPI ledger was keyed with the identity ids, which the re-base makes
1112 // stale — cold-start both engines' histories (one soft restart, negligible).
1113 void ensureGlobalGids() {
1114 if (mpiGidsGlobal_)
1115 return;
1116 long base = 0, mine = P_.numReal;
1117 MPI_Exscan(&mine, &base, 1, MPI_LONG, MPI_SUM, halo_->comm());
1118 if (halo_->rank() == 0)
1119 base = 0; // MPI_Exscan leaves rank 0's recvbuf undefined
1120 fillGidBaseKokkos(P_.gid, P_.numReal, static_cast<int>(base));
1121 P_.prevPairCount = 0;
1122 P_.hertzPrevCount = 0;
1123 P_.hertzNumPairs = -1;
1124 mpiGidsGlobal_ = true;
1125 }
1126 void stepMpi(int nsteps) {
1127 const double rcut = (mpiRcut_ > 0.0) ? mpiRcut_ : maxOwnedRadius(P_);
1128 ensureGlobalGids();
1129 for (int s = 0; s < nsteps; ++s) {
1130 if (mpiRebalanceEvery_ > 0 && mpiStepCount_ % mpiRebalanceEvery_ == 0)
1131 halo_->rebalance(P_);
1132 demStepMpi(P_, *halo_, rcut, mpiSyncEvery_, mpiForwardRotation_);
1133 ++mpiStepCount_;
1134 }
1135 }
1140 void stepHertzMpi(float dt, int substeps, float skin_frac) {
1141 P_.dt = dt;
1142 ensureGlobalGids();
1143 if (mpiRebalanceEvery_ > 0 && mpiHertzCalls_ % mpiRebalanceEvery_ == 0)
1144 halo_->rebalance(P_);
1145 ++mpiHertzCalls_;
1146 demStepHertzMpi(P_, *halo_, dt, substeps, skin_frac);
1147 }
1148 int rank() const { return halo_->rank(); }
1149 int numGhost() const { return halo_->numGhost(); }
1150#endif // PECLET_DEM_MPI
1151
1152 // SDF grid (get_sdf_grid): Eikonal reconstruction over the domain, flat x-fastest, negative
1153 // inside solid.
1154 std::vector<float> getSdfGrid(int rx, int ry, int rz) {
1156 rx, ry, rz, P_.domain.min, P_.domain.max, P_.numReal, P_.pos, P_.quat, P_.scale, P_.shapeId,
1158 }
1159
1160 int numParticles() const { return P_.numReal; }
1161 // Live device Views of the owned particle state, for the zero-copy device-array export (H2): the
1162 // binding wraps a [0,numReal) subview as a DLPack/__cuda_array_interface__ array (or a NumPy view
1163 // on a host backend) referencing this memory — no device->host copy.
1164 const V3& positionsView() const { return P_.pos; }
1165 const V3& velocitiesView() const { return P_.vel; }
1166 int numContacts() { return readInt(P_.contactCount); }
1167 int numManifolds() { return readInt(P_.manifoldCount); }
1168 // TEST-ONLY colouring self-check: over the LAST solved substep's colourings, count how many
1169 // (manifold, contact) pairs violate the graph-colouring invariant "no two same-colour items share
1170 // a body" — must be exactly (0, 0) for a valid colouring (the incremental warm-start path must
1171 // never import a conflict). Returns {velocity-colour conflicts, position-colour conflicts}.
1172 std::pair<int, int> debugColoringConflicts() {
1173 using peclet::dem::CpExec;
1174 using peclet::dem::CpMem;
1175 CpExec space;
1176 const int nm = readInt(P_.manifoldCount);
1177 const int nc = readInt(P_.contactCount);
1178 const int nb = std::max(P_.numParticles, P_.numReal) + 1;
1179 Kokkos::View<std::uint64_t*, CpMem> seen("dbg_color_seen", nb);
1180 int velConf = 0, posConf = 0;
1181 if (nm > 0) {
1182 Kokkos::deep_copy(space, seen, std::uint64_t(0));
1183 auto manifolds = P_.manifolds;
1184 auto realIdx = P_.realIndices;
1185 auto mColor = P_.manifoldColor;
1186 Kokkos::parallel_reduce(
1187 "peclet::dem::dbg_vel_color", Kokkos::RangePolicy<CpExec>(space, 0, nm),
1188 KOKKOS_LAMBDA(int idx, int& acc) {
1189 const int c = mColor(idx);
1190 if (c < 0)
1191 return;
1192 const auto m = manifolds(idx);
1193 const std::uint64_t bit = std::uint64_t(1) << c;
1194 if ((Kokkos::atomic_fetch_or(&seen(realIdx(m.bodyA)), bit) >> c) & 1)
1195 acc += 1;
1196 if (m.bodyB >= 0 && ((Kokkos::atomic_fetch_or(&seen(realIdx(m.bodyB)), bit) >> c) & 1))
1197 acc += 1;
1198 },
1199 velConf);
1200 }
1201 if (nc > 0) {
1202 Kokkos::deep_copy(space, seen, std::uint64_t(0));
1203 auto contacts = P_.contacts;
1204 auto cColor = P_.contactColor;
1205 Kokkos::parallel_reduce(
1206 "peclet::dem::dbg_pos_color", Kokkos::RangePolicy<CpExec>(space, 0, nc),
1207 KOKKOS_LAMBDA(int idx, int& acc) {
1208 const int c = cColor(idx);
1209 if (c < 0)
1210 return;
1211 const auto ct = contacts(idx);
1212 const std::uint64_t bit = std::uint64_t(1) << c;
1213 if ((Kokkos::atomic_fetch_or(&seen(ct.bodyA), bit) >> c) & 1)
1214 acc += 1;
1215 if (ct.bodyB >= 0 && ((Kokkos::atomic_fetch_or(&seen(ct.bodyB), bit) >> c) & 1))
1216 acc += 1;
1217 },
1218 posConf);
1219 }
1220 space.fence();
1221 return {velConf, posConf};
1222 }
1223 float maxOverlap() {
1224 float h;
1225 Kokkos::deep_copy(h, P_.maxOverlap);
1226 return h;
1227 }
1230 std::tuple<double, float, int> restBankStats() {
1232 }
1234 std::tuple<double, float, int> restOrphanStats() {
1235 return restBankStatsKokkos(P_.bodyOrphan, P_.numReal);
1236 }
1237
1238 // ParaView PolyData (points + Radius + Velocity), faithful to CUDA Simulation::write_vtp:
1239 // Radius = scale * globalScale * baseRadius.
1240 void writeVtp(const std::string& filename) const {
1241 auto pos = Kokkos::create_mirror_view(P_.pos);
1242 Kokkos::deep_copy(pos, P_.pos);
1243 auto sc = Kokkos::create_mirror_view(P_.scale);
1244 Kokkos::deep_copy(sc, P_.scale);
1245 auto vel = Kokkos::create_mirror_view(P_.vel);
1246 Kokkos::deep_copy(vel, P_.vel);
1247 const int n = P_.numReal;
1248
1249 std::ofstream out(filename);
1250 if (!out)
1251 throw std::runtime_error("Could not open file for writing: " + filename);
1252 out << "<?xml version=\"1.0\"?>\n";
1253 out << "<VTKFile type=\"PolyData\" version=\"0.1\" byte_order=\"LittleEndian\">\n";
1254 out << " <PolyData>\n";
1255 out << " <Piece NumberOfPoints=\"" << n << "\" NumberOfVerts=\"0\" "
1256 << "NumberOfLines=\"0\" NumberOfStrips=\"0\" NumberOfPolys=\"0\">\n";
1257 out << " <Points>\n";
1258 out << " <DataArray type=\"Float32\" Name=\"Position\" NumberOfComponents=\"3\" "
1259 "format=\"ascii\">\n";
1260 for (int i = 0; i < n; ++i)
1261 out << pos(i, 0) << " " << pos(i, 1) << " " << pos(i, 2) << " ";
1262 out << "\n </DataArray>\n";
1263 out << " </Points>\n";
1264 out << " <PointData Scalars=\"Radius\">\n";
1265 out << " <DataArray type=\"Float32\" Name=\"Radius\" NumberOfComponents=\"1\" "
1266 "format=\"ascii\">\n";
1267 for (int i = 0; i < n; ++i)
1268 out << sc(i) * P_.globalScale * baseRadius_ << " ";
1269 out << "\n </DataArray>\n";
1270 out << " <DataArray type=\"Float32\" Name=\"Velocity\" NumberOfComponents=\"3\" "
1271 "format=\"ascii\">\n";
1272 for (int i = 0; i < n; ++i)
1273 out << vel(i, 0) << " " << vel(i, 1) << " " << vel(i, 2) << " ";
1274 out << "\n </DataArray>\n";
1275 out << " </PointData>\n";
1276 out << " </Piece>\n";
1277 out << " </PolyData>\n";
1278 out << "</VTKFile>\n";
1279 out.close();
1280 std::printf("Exported VTP: %s\n", filename.c_str());
1281 }
1282
1283 private:
1284 // (Re)upload just the small WallSdf array (velocity fields change every step for a vibrating
1285 // wall; the grid samples are uploaded once in addSdfWall).
1286 void uploadWalls() {
1287 const int n = std::max<int>(1, static_cast<int>(wallsHost_.size()));
1288 if (static_cast<int>(P_.walls.extent(0)) < n)
1289 P_.walls = Kokkos::View<WallSdf*, CpMem>("walls", n);
1290 auto h = Kokkos::create_mirror_view(P_.walls);
1291 for (size_t i = 0; i < wallsHost_.size(); ++i)
1292 h(i) = wallsHost_[i];
1293 Kokkos::deep_copy(P_.walls, h);
1294 }
1295
1296 // Size the contact/manifold buffers so no contact is dropped: a shell point sits inside at most
1297 // one neighbour (body-body ~ capacity*shellPoints) plus one per wall it touches
1298 // (capacity*shellPoints per wall). Boundary/wall contacts are appended AFTER body-body ones, so
1299 // an undersized buffer silently drops them and grains tunnel through walls. Floored at the
1300 // analytic default; grows only.
1301 void ensureContactCapacity() {
1302 const int perParticle = std::max(16, shellPoints_);
1303 const long want = static_cast<long>(P_.capacity) * perParticle +
1304 static_cast<long>(P_.capacity) * std::max(1, shellPoints_) * P_.numWalls;
1305 if (want > P_.maxContacts) {
1306 P_.maxContacts = static_cast<int>(want);
1307 // Reallocate EVERY maxContacts-sized view, not just contacts/manifolds: the solve writes all
1308 // of them up to the live contact/manifold count, so any view left at the old size is an
1309 // out-of-bounds write once the count grows past it (silent device corruption on GPU, heap
1310 // corruption on host backends). Warm-start history is cleared by the fresh zeroed views —
1311 // growth happens at setup (shape/wall registration), so nothing warm is lost mid-run.
1312 P_.contacts = Kokkos::View<ContactC*, CpMem>("contacts", want);
1313 P_.manifolds = Kokkos::View<ManifoldC*, CpMem>("manifolds", want);
1314 P_.manifoldColor = Kokkos::View<int*, CpMem>("manifoldColor", want);
1315 P_.pairKeys = Kokkos::View<unsigned long long*, CpMem>("pairKeys", want);
1316 P_.prevPairKeys = Kokkos::View<unsigned long long*, CpMem>("prevPairKeys", want);
1317 P_.manifoldPersistent = Kokkos::View<unsigned char*, CpMem>("manifoldPersistent", want);
1318 P_.contactColor = Kokkos::View<int*, CpMem>("contactColor", want);
1319 P_.lambdaAcc = Kokkos::View<float*, CpMem>("lambdaAcc", want);
1320 P_.lambdaT = Kokkos::View<float* [3], CpMem>("lambdaT", want);
1321 P_.posLambdaContact = Kokkos::View<float*, CpMem>("posLambdaContact", want);
1322 P_.posImpulse = Kokkos::View<float*, CpMem>("posImpulse", want);
1323 P_.prevPosImpulse = Kokkos::View<float*, CpMem>("prevPosImpulse", want);
1324 P_.restBank = Kokkos::View<float*, CpMem>("restBank", want);
1325 P_.prevRestBank = Kokkos::View<float*, CpMem>("prevRestBank", want);
1326 P_.restRel = Kokkos::View<float*, CpMem>("restRel", want);
1327 P_.restVPeak = Kokkos::View<float*, CpMem>("restVPeak", want);
1328 P_.prevRestVPeak = Kokkos::View<float*, CpMem>("prevRestVPeak", want);
1329 P_.prevMatched = Kokkos::View<unsigned char*, CpMem>("prevMatched", want);
1330 P_.velPerm = Kokkos::View<int*, CpMem>("velPerm", want);
1331 P_.commitPerm = Kokkos::View<int*, CpMem>("commitPerm", want);
1332 P_.sideFlags = Kokkos::View<unsigned char*, CpMem>("sideFlags", want);
1333 P_.prevLambdaT = Kokkos::View<float* [3], CpMem>("prevLambdaT", want);
1334 P_.contactSlot = Kokkos::View<int*, CpMem>("contactSlot", want);
1335 P_.prevLambda = Kokkos::View<float*, CpMem>("prevLambda", want);
1336 P_.vn0 = Kokkos::View<float*, CpMem>("vn0", want);
1337 P_.vt0 = Kokkos::View<float* [3], CpMem>("vt0", want);
1338 P_.levelKey = Kokkos::View<int*, CpMem>("levelKey", want);
1339 P_.levelPerm = Kokkos::View<int*, CpMem>("levelPerm", want);
1340 P_.mlColorPacked = Kokkos::View<long long*, CpMem>("mlColorPacked", want);
1341 // Incremental-colouring ledgers + fused position permutation + sleeping masks are all
1342 // maxContacts-sized and the solve indexes them up to the live contact/manifold count too —
1343 // they MUST grow with the buffer or nc > extent is an out-of-bounds write (NaN / heap
1344 // corruption in dense multi-contact scenes such as the statics column/pour).
1345 P_.prevManifoldColor = Kokkos::View<int*, CpMem>("prevManifoldColor", want);
1346 P_.contactKeys = Kokkos::View<unsigned long long*, CpMem>("contactKeys", want);
1347 P_.prevContactKeys = Kokkos::View<unsigned long long*, CpMem>("prevContactKeys", want);
1348 P_.prevContactColor = Kokkos::View<int*, CpMem>("prevContactColor", want);
1349 P_.posCommitPerm = Kokkos::View<int*, CpMem>("posCommitPerm", want);
1350 P_.posPerm = Kokkos::View<int*, CpMem>("posPerm", want);
1351 P_.manifoldSleep = Kokkos::View<unsigned char*, CpMem>("manifoldSleep", want);
1352 P_.contactSleep = Kokkos::View<unsigned char*, CpMem>("contactSleep", want);
1353 P_.posPrevContactCount = 0; // the cleared position ledger must not be gathered against
1354 P_.prevPairCount = 0; // the cleared prevPairKeys must not be gathered against
1355 }
1356 }
1357
1358 Particles P_;
1359 float baseRadius_ = 1.0f;
1360 int shellPoints_ = 0; // surface-shell size of the active shape (contact-buffer sizing)
1361 std::vector<WallSdf> wallsHost_;
1362 std::vector<float> wallGridHost_;
1363 F3 defaultInvI_{2.5f, 2.5f, 2.5f};
1364#ifdef PECLET_DEM_MPI
1365 std::unique_ptr<ParticleHalo> halo_ = std::make_unique<ParticleHalo>();
1366 double mpiRcut_ = 0.0;
1367 int mpiSyncEvery_ = 1;
1368 bool mpiForwardRotation_ = true;
1369 int mpiRebalanceEvery_ = 0;
1370 long mpiStepCount_ = 0;
1371 long mpiHertzCalls_ =
1372 0; // step_hertz_mpi call count (rebalance_every cadence for the force path)
1373 bool mpiGidsGlobal_ = false; // gids re-based to a global Exscan offset (once per particle set)
1374#endif
1375};
1376
1377} // namespace peclet::dem
1378
1379#endif // DEM_SIM_HPP
dem — portable (ArborX) broad-phase, the Kokkos-native replacement for the CUDA-only cuBQL broad-phas...
Host-facing facade with std::vector setters/getters (binding-agnostic).
Definition sim.hpp:407
void setQuaternions(const std::vector< float > &q)
Definition sim.hpp:956
std::vector< float > getMasses() const
Definition sim.hpp:1013
void setSleeping(bool enabled, float threshold_scale=2.0f, int consecutive=64, float wake_scale=40.0f)
Island sleeping / freezing (single-GPU statics, default ON; PECLET_DEM_SLEEP=0 disables).
Definition sim.hpp:698
void setThermostat(float temperature, float tau, float kB)
Definition sim.hpp:633
std::tuple< float, float, float > getDomainMax() const
Definition sim.hpp:629
std::vector< float > getInvInertia() const
Definition sim.hpp:994
std::pair< int, int > debugColoringConflicts()
Definition sim.hpp:1172
int numAsleep()
Number of currently-sleeping real bodies (diagnostics / tests).
Definition sim.hpp:718
std::vector< float > getVelocities() const
Definition sim.hpp:1026
static void releaseAll()
Definition sim.hpp:454
void setPairMaterial(int a, int b, float restitution, float friction)
Set the symmetric pair material (restitution, friction) for material ids (a, b).
Definition sim.hpp:771
std::vector< float > getScales() const
Definition sim.hpp:1034
void enablePeriodicity(bool x, bool y, bool z)
Definition sim.hpp:621
std::tuple< double, float, int > restOrphanStats()
Orphan-account diagnostics: (sum, max, count>0) of the per-body orphaned budget.
Definition sim.hpp:1234
static std::vector< Simulation * > & registry()
Definition sim.hpp:458
void setVelocityUseGS(bool useGS)
Definition sim.hpp:644
void setSphereShape(float radius)
Definition sim.hpp:463
void setStabilization(bool enabled)
Enable/disable the stabilization pass (default on).
Definition sim.hpp:649
Simulation(int capacity)
Definition sim.hpp:409
std::vector< float > getQuaternions() const
Definition sim.hpp:1030
void setWallVelocity(int wallIndex, F3 linVel, F3 angVel, F3 center)
Definition sim.hpp:857
void exportSdf(const std::string &filename, int rx, int ry, int rz)
Definition sim.hpp:1062
void writeVtp(const std::string &filename) const
Definition sim.hpp:1240
void setVerletSkin(float skin_frac)
Verlet-cached impulse broadphase (single-GPU, non-periodic; default OFF).
Definition sim.hpp:713
int numParticles() const
Definition sim.hpp:1160
void setGlobalScale(float s)
Definition sim.hpp:746
void addPlane(float px, float py, float pz, float nx, float ny, float nz)
Definition sim.hpp:801
void setVelocities(const std::vector< float > &v)
Definition sim.hpp:931
float growthFactor() const
Definition sim.hpp:1009
std::tuple< float, float, float > getDomainMin() const
Definition sim.hpp:626
int addSdfWall(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, float restitution, float friction)
Definition sim.hpp:818
void setPositions(const std::vector< float > &xyz)
Definition sim.hpp:867
void setInvInertia(const std::vector< float > &ii)
Definition sim.hpp:975
const Vf & invMassView() const
Definition sim.hpp:953
void setDt(float dt)
Definition sim.hpp:750
void setAngularVelocities(const std::vector< float > &w)
Definition sim.hpp:966
const V3 & externalForcesView() const
Definition sim.hpp:952
void setHertzMaterial(int mat, float youngs, float poisson)
Per-material Young's modulus + Poisson ratio for the Hertz-Mindlin engine (material ids as in setMate...
Definition sim.hpp:728
void clearExternalForces()
Definition sim.hpp:951
std::vector< float > getAngularVelocities() const
Definition sim.hpp:990
void setScalesUniform(float s)
Definition sim.hpp:915
std::vector< float > getSdfGrid(int rx, int ry, int rz)
Definition sim.hpp:1154
void step(float dt)
Definition sim.hpp:1040
void setDomain(float lx, float ly, float lz, bool px, bool py, bool pz)
Definition sim.hpp:607
void setSolverIterations(int pos, int vel)
Definition sim.hpp:638
void initializeShape(int shape_type, float radius, float height, float thickness)
Definition sim.hpp:469
const V3 & velocitiesView() const
Definition sim.hpp:1165
void setExternalForces(const std::vector< float > &f)
Definition sim.hpp:942
std::vector< float > getPositions() const
Definition sim.hpp:1022
std::tuple< double, float, int > restBankStats()
Poisson-restitution diagnostics: (sum, max, count>0) of the committed per-pair owed separation impuls...
Definition sim.hpp:1230
void setMaterialParams(float restitution_normal, float restitution_tangent, float friction)
Definition sim.hpp:754
void exportLammps(const std::string &filename, int step) const
Definition sim.hpp:1050
float getGrowthRate() const
Definition sim.hpp:1010
void setDomainMinMax(F3 mn, F3 mx)
Definition sim.hpp:612
void setMaterialIds(const std::vector< int > &ids)
Per-particle material ids (0..kMaxMaterials-1); pair (e, mu) values come from setPairMaterial.
Definition sim.hpp:762
const V3 & positionsView() const
Definition sim.hpp:1164
void setInvMass(const std::vector< float > &im)
Definition sim.hpp:984
void setSdfShape(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, const std::vector< float > &shellFlat, F3 invInertia, float boundingRadius)
Definition sim.hpp:555
void setScales(const std::vector< float > &s)
Definition sim.hpp:924
void setGrowthParams(float rate, float new_factor)
Definition sim.hpp:1000
void stepHertz(float dt, int substeps, float skin_frac)
Advance substeps explicit soft-sphere Hertz-Mindlin steps of size dt (device-side loop; the (e,...
Definition sim.hpp:742
void setGravity(float gx, float gy, float gz)
Definition sim.hpp:632
void setWallMaterialId(int wid, int mat)
Give an SDF wall a material id so particle-wall (e, mu) also resolves via the pair table.
Definition sim.hpp:793
void setStabilizationMode(const std::string &mode)
Select the stabilization pass of the staged velocity solve: "off" (pure symmetric PGS),...
Definition sim.hpp:656
void setRestitutionModel(const std::string &model)
Restitution model of the PGS velocity solve: "newton" (default; per-substep restitution on the pre-so...
Definition sim.hpp:677
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
dem — portable (Kokkos) time integration kernels (integration.cu).
dem — portable (CUDA-free) I/O helpers for the Kokkos dem module: a LAMMPS dump writer and a scalar-S...
dem — portable (Kokkos) owner<->ghost particle halo for the distributed XPBD step.
std::vector< F3 > genCylinderShell(float radius, float height, float thickness, float spacing)
void computeContactSleepKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const unsigned char *, CpMem > asleep, Kokkos::View< unsigned char *, CpMem > contactSleep)
Per-contact twin of computeManifoldSleepKokkos for the position colouring.
Definition sleeping.hpp:95
void fillGidBaseKokkos(Vi gid, int n, int base)
gid(i) = base + i over [0, n) — the per-rank global-id re-base of the distributed step (namespace sco...
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={})
Reduce n contacts to manifolds (one per unique canonical pair).
float maxOwnedRadius(const Particles &P)
Largest effective particle radius over the owned set (= max scale × globalScale, growth included).
void detectContactsKokkos(Kokkos::View< const int *[2], CpMem > pairs, int numPairs, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap, GridView sdfGrid=GridView{}, MatIdView matId=MatIdView{}, PairTableView pairTable=PairTableView{})
Pair point-shell vs SDF contacts.
void generateGhostsKokkos(int numReal, int capacity, Domain dom, float skin, V3 pos, Vf invMass, V3 posPred, V3 vel, V3 velPred, V4 quat, V4 quatPred, V3 angVel, V3 angVelPred, Vf scale, Vi shapeId, Vi realIndices, Kokkos::View< int, CpMem > topGhost, Vi gid=Vi{}, Kokkos::View< unsigned char *, CpMem > materialId={})
Generate periodic ghosts for particles [0,numReal).
void zeroForceScratchKokkos(V3 dv, V3 dw, int lo, int hi)
Zero the force/torque accumulator rows [lo, hi) — under MPI the pair kernels atomically accumulate on...
void demSolveContacts(Particles &P, int nc, int nm, int nBodies, Kokkos::View< const int *, CpMem > keyIdx, const Hooks &hooks)
One full velocity + position contact solve over the already-built contacts/manifolds (see file commen...
Kokkos::View< float *, CpMem > Vf
Kokkos::View< float *[3], CpMem > V3
void detectBoundaryKokkos(int numReal, int numPlanes, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, Kokkos::View< const PlaneP *, CpMem > planes, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap)
Per-real-particle contacts against explicit planes (point-shell shapes test each surface point; analy...
void predictVelocityKokkos(int n, V3 pos, Vf invMass, V3 vel, V4 quat, V3 angVel, V3 invInertia, V3 posPred, V4 quatPred, V3 velPred, V3 angVelPred, V3 deltaPos, V4 deltaQuat, V3 deltaVel, V3 deltaAngVel, Vi constraintCounts, F3 gravity, float dt, V3 extForce)
Predict velocity (gravity + gyroscopic precession), speculative position, and clear all deltas.
void writeLammpsDump(const std::string &filename, int step, const std::vector< float > &pos, const std::vector< float > &vel, const std::vector< float > &quat, const std::vector< float > &radii, const float *boxMin, const float *boxMax, bool pbcEnabled)
Definition io.hpp:23
void detectWallSdfKokkos(int numReal, int numWalls, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, Kokkos::View< const WallSdf *, CpMem > walls, GridView wallGrid, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap, MatIdView matId=MatIdView{}, PairTableView pairTable=PairTableView{})
Per-real-particle contacts against a static world-space wall SDF set (a drum barrel,...
void updateSleepKokkos(int numReal, Kokkos::View< const float *[3], CpMem > vel, Kokkos::View< const float *[3], CpMem > angVel, Kokkos::View< const float *, CpMem > rad, Kokkos::View< const unsigned char *, CpMem > grounded, Kokkos::View< const unsigned char *, CpMem > movingWall, Kokkos::View< unsigned char *, CpMem > asleep, Kokkos::View< unsigned char *, CpMem > sleepCounter, float sleepSpeed, int K, Kokkos::View< float *[3], CpMem > velOut, Kokkos::View< float *[3], CpMem > angVelOut)
Sleep detection (after the commit): an AWAKE, grounded body whose linear AND angular motion has staye...
Definition sleeping.hpp:205
void freezeAsleepKokkos(int numReal, Kokkos::View< const unsigned char *, CpMem > asleep, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< float *[3], CpMem > posPred, Kokkos::View< float *[4], CpMem > quatPred, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *[3], CpMem > angVelPred)
After predictVelocity: re-freeze the currently-asleep bodies so gravity/prediction do not move them —...
Definition sleeping.hpp:31
int readInt(Kokkos::View< int, CpMem > v)
void demStepForce(Particles &P, float dt, int nsteps, float skinFrac, const Law &law, const Hooks &hooks)
nsteps explicit force-based DEM steps of size dt with force law Law (see file comment).
CpExec::memory_space CpMem
int findCollisionsGrow(Particles &P, float margin)
Broad phase with an automatically-grown pair buffer.
void finalCommitKokkos(int n, V3 pos, Vf invMass, V3 posPred, V4 quat, V4 quatPred, Domain dom)
Final commit: periodic wrap of the predicted position into the domain, commit position + quat.
void demStepHertz(Particles &P, float dt, int nsteps, float skinFrac)
nsteps of the soft-sphere Hertz-Mindlin engine — the single-GPU instantiation of the force-based driv...
void fillWorldRadiiKokkos(Vf scale, Vf rad, float gs, float bR, int n)
World radii rad(i) = scale(i) * globalScale * baseRadius over [0, n) (n = owned + ghosts after a halo...
void updateGrowthScalesKokkos(int n, Vf scale, Vf targetScale, float factor)
Growth mode: scale = target * factor (when active).
float computeOverlapsKokkos(Particles &P)
Max pair interpenetration on the committed state (faithful to CUDA Simulation::compute_overlaps): cop...
Definition sim.hpp:185
void demStep(Particles &P)
One full XPBD DEM substep over the particle SoA (mirrors simulation.cpp Simulation::step()).
Definition sim.hpp:49
void buildInvMassEffKokkos(int numBodies, Kokkos::View< const unsigned char *, CpMem > asleep, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *, CpMem > invMassEff, float sleeperFrac)
Effective inverse mass for the solve: a sleeping body (real, or a periodic ghost whose real is asleep...
Definition sleeping.hpp:58
void wakeDisturbedKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *[3], CpMem > velPred, float wakeSpeed, Kokkos::View< unsigned char *, CpMem > asleep, Kokkos::View< unsigned char *, CpMem > sleepCounter, Kokkos::View< unsigned char *, CpMem > movingWall, Kokkos::View< int *, CpMem > curCount, int numReal)
Wake pass (before the solve): a sleeper is woken when actually disturbed.
Definition sleeping.hpp:117
std::vector< float > generateSdfKokkos(int rx, int ry, int rz, F3 dmin, F3 dmax, int numReal, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, bool px, bool py, bool pz, GridView sdfGrid=GridView{})
int findCollisionsVerlet(Particles &P, float margin, float maxRad)
Verlet-cached impulse broadphase (single-GPU, non-periodic).
std::vector< F3 > genBoxShell(float hx, float hy, float hz, float spacing)
void computeManifoldSleepKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const unsigned char *, CpMem > asleep, Kokkos::View< unsigned char *, CpMem > manifoldSleep)
Per-manifold "both endpoints asleep" flag (a static wall, bodyB < 0, counts as asleep).
Definition sleeping.hpp:74
std::tuple< double, float, int > restBankStatsKokkos(Kokkos::View< const float *, CpMem > bank, int n)
Poisson-restitution diagnostics: (sum, max, count>0) over the committed owed-impulse store (namespace...
void wakeContactChangeKokkos(int numReal, Kokkos::View< const int *, CpMem > curCount, Kokkos::View< int *, CpMem > prevCount, Kokkos::View< const unsigned char *, CpMem > movingWall, Kokkos::View< unsigned char *, CpMem > asleep, Kokkos::View< unsigned char *, CpMem > sleepCounter, bool wakeOnChange)
Contact-set-change wake rule (b): wake any still-asleep body whose live contact count differs from th...
Definition sleeping.hpp:176
constexpr int kMaxMaterials
Pair-material lookup: flat [K*K*2] table, entry ((a*K + b)*2) = restitution, +1 = friction.
void applyThermostatKokkos(int numReal, V3 vel, Vf invMass, V3 angVel, V3 invInertia, V4 quat, double kB, double tau, double Ttarget, float dt)
Kokkos::DefaultExecutionSpace CpExec
void writeSdfVti(const std::string &filename, const std::vector< float > &grid, int rx, int ry, int rz, const float *minB, const float *maxB)
Definition io.hpp:86
int calculateGhostCapacity(int nReal, Domain dom, float skin)
Padded particle-array capacity that leaves room for the periodic ghosts generateGhostsKokkos will emi...
dem — portable (Kokkos) narrow-phase: SDF point-shell collision + boundary planes.
dem — portable (Kokkos) SDF-grid reconstruction: the get_sdf_grid pipeline.
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
dem — portable (Kokkos) periodic ghost generation (periodicity.cu / integration.cu).
dem — portable (host) surface-shell point generators for the analytic shapes.
dem — island sleeping / freezing for the single-GPU PGS statics path.
dem — the shared contact-solve driver: the full modern velocity + position solve sequence (warm-start...
dem — the force-based DEM step driver: explicit soft-contact time stepping (the engine family that co...
dem — portable (Kokkos) Coulomb friction cluster (the single dissipative friction path).
Soft-sphere Hertz–Mindlin DEM (reference force model) for SPHERES.
dem — multilevel (GraphMG-style) momentum-conserving contact stabilization.
dem — portable (Kokkos) XPBD position solve (pure overlap removal).
dem — portable (Kokkos) manifold velocity solve (normal restitution impulse).
Kokkos::View< WallSdf *, CpMem > walls
Kokkos::View< float *, CpMem > hertzE
Kokkos::View< int *, CpMem > commitPerm
Kokkos::View< unsigned char *, CpMem > asleep
Kokkos::View< unsigned char *, CpMem > materialId
Kokkos::View< unsigned char *, CpMem > sideFlags
Kokkos::View< unsigned long long *, CpMem > prevContactKeys
Kokkos::View< unsigned char *, CpMem > manifoldPersistent
Definition particles.hpp:73
Kokkos::View< int, CpMem > manifoldCount
Kokkos::View< int *, CpMem > velPerm
Kokkos::View< unsigned char *, CpMem > contactSleep
Kokkos::View< float *, CpMem > prevLambda
Definition particles.hpp:86
void ensureCapacity(int newCap)
Kokkos::View< ManifoldC *, CpMem > manifolds
Definition particles.hpp:54
Kokkos::View< float *, CpMem > vn0
Definition particles.hpp:87
Kokkos::View< int *, CpMem > levelKey
Kokkos::View< int *, CpMem > sleepCurCount
Kokkos::View< int *[2], CpMem > pairs
Definition particles.hpp:52
Kokkos::View< int, CpMem > contactCount
Kokkos::View< unsigned char *, CpMem > sleepMovingWall
Kokkos::View< float *[3], CpMem > vt0
Definition particles.hpp:88
Kokkos::View< float *, CpMem > restRel
Kokkos::View< int *, CpMem > levelPerm
Kokkos::View< int *, CpMem > prevManifoldColor
Definition particles.hpp:64
Kokkos::View< unsigned long long *, CpMem > contactKeys
Kokkos::View< float *, CpMem > pairMaterials
Kokkos::View< ShapeDesc *, CpMem > shapes
Kokkos::View< int *, CpMem > prevContactColor
Kokkos::View< unsigned char *, CpMem > prevMatched
Kokkos::View< float *, CpMem > prevRestBank
Kokkos::View< float *, CpMem > restVPeak
Kokkos::View< float *, CpMem > restBank
Kokkos::View< int *, CpMem > posCommitPerm
Kokkos::View< unsigned long long *, CpMem > pairKeys
Definition particles.hpp:71
Kokkos::View< float *, CpMem > hertzNu
Kokkos::View< int, CpMem > topGhost
Kokkos::View< float *, CpMem > bodyOrphan
Kokkos::View< unsigned long long *, CpMem > prevPairKeys
Definition particles.hpp:72
Kokkos::View< int *, CpMem > posPerm
Kokkos::View< float *, CpMem > invMassEff
Kokkos::View< float *, CpMem > prevPosImpulse
Kokkos::View< int *, CpMem > contactColor
Kokkos::View< int *, CpMem > sleepPrevCount
Kokkos::View< float *, CpMem > sdfGrid
Kokkos::View< float *[3], CpMem > lambdaT
Definition particles.hpp:92
Kokkos::View< float *[3], CpMem > prevLambdaT
Definition particles.hpp:93
Kokkos::View< float, CpMem > maxOverlap
Kokkos::View< float *, CpMem > posLambdaContact
Definition particles.hpp:99
Kokkos::View< float *, CpMem > posImpulse
Kokkos::View< int *, CpMem > contactSlot
Kokkos::View< unsigned char *, CpMem > sleepCounter
Kokkos::View< float *, CpMem > lambdaAcc
Definition particles.hpp:85
Kokkos::View< long long *, CpMem > mlColorPacked
Kokkos::View< float *, CpMem > wallGrid
Kokkos::View< unsigned char *, CpMem > manifoldSleep
void allocate(int cap, int maxPairs_, int maxContacts_, int nShapes, int nShell, int nPlanes)
Kokkos::View< float *[3], CpMem > shell
Kokkos::View< float *, CpMem > prevRestVPeak
Kokkos::View< ContactC *, CpMem > contacts
Definition particles.hpp:53
Kokkos::View< unsigned char *, CpMem > groundedLevel
Definition particles.hpp:80
Kokkos::View< PlaneP *, CpMem > planes
Kokkos::View< int *, CpMem > manifoldColor
Definition particles.hpp:58
Portable mirror of ShapeDescriptor (analytic fields + a flat-array point shell).
Single-GPU hooks: no ghost refresh, residuals are already global. Everything inlines away.
Static, world-space SDF container/geometry the particles collide against (a drum barrel,...
static const double L[3]