peclet-dem
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 <fstream>
15#include <Kokkos_Core.hpp>
16#include <memory>
17#include <stdexcept>
18#include <vector>
19
20#include "broadphase_arborx.hpp"
22#include "integration.hpp"
23#include "io.hpp"
24#include "narrowphase.hpp"
25#include "output_sdf.hpp"
26#include "particles.hpp"
27#include "peclet/core/common/view.hpp" // peclet::core::toVector — single-copy device View -> host std::vector (S2a)
28#include "periodicity.hpp"
29#include "shapes_portable.hpp"
30#include "solver_friction.hpp"
31#include "solver_position.hpp"
32#include "solver_velocity.hpp"
33
34#ifdef PECLET_DEM_MPI
35#include "mpi_halo.hpp" // ParticleHalo (gated; default module never includes it)
36#endif
37
38namespace peclet::dem {
39
40inline int readInt(Kokkos::View<int, CpMem> v) {
41 int h;
42 Kokkos::deep_copy(h, v);
43 return h;
44}
45
51inline float maxOwnedRadius(const Particles& P) {
52 if (P.numReal <= 0)
53 return P.globalScale * P.baseRadius;
54 float mx = 0.0f;
55 auto sc = P.scale;
56 Kokkos::parallel_reduce(
57 "peclet::dem::max_scale", Kokkos::RangePolicy<CpExec>(0, P.numReal),
58 KOKKOS_LAMBDA(int i, float& m) { m = sc(i) > m ? sc(i) : m; }, Kokkos::Max<float>(mx));
59 return mx * P.globalScale * P.baseRadius;
60}
61
72inline int findCollisionsGrow(Particles& P, float margin) {
73 const float boxCap = std::max(std::max(P.domain.size.x, P.domain.size.y), P.domain.size.z);
74 int np = findCollisionsArborX(P.posPred, P.crad(), P.numParticles, P.numReal, margin, P.pairs,
75 P.pairCount, boxCap);
76 if (np > static_cast<int>(P.pairs.extent(0))) {
77 const int grown = np + np / 2 + 64; // 1.5× + slack
78 Kokkos::realloc(Kokkos::WithoutInitializing, P.pairs, grown);
79 P.maxPairs = grown;
80 np = findCollisionsArborX(P.posPred, P.crad(), P.numParticles, P.numReal, margin, P.pairs,
81 P.pairCount, boxCap);
82 }
83 return std::min(np, static_cast<int>(P.pairs.extent(0)));
84}
85
87inline void demStep(Particles& P) {
88 CpExec space;
89
90 // growth ramp (faithful to CUDA Simulation::step): factor *= exp(rate*dt), capped at 1, then
91 // scale = targetScale * factor. (CUDA stores the unscaled target in d_target_scales.)
92 if (P.growthFactor != -1.0f && P.growthRate != 0.0f) {
93 P.growthFactor *= std::exp(P.growthRate * P.dt);
94 if (P.growthFactor > 1.0f)
95 P.growthFactor = 1.0f;
96 }
97 if (P.growthFactor > 0.0f)
99
100 // ghost band + broadphase margin sized off the ACTUAL max grain radius (post-growth), so SI-unit
101 // particles just work; identical to the old 0.1/1.0*globalScale when globalScale ~ the grain size.
102 const float maxRad = maxOwnedRadius(P);
103 const float margin = 0.1f * maxRad;
104
108
109 {
110 auto ri = P.realIndices;
111 Kokkos::parallel_for(
112 "self", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
113 KOKKOS_LAMBDA(int i) { ri(i) = i; });
114 }
115 Kokkos::deep_copy(space, P.topGhost, P.numReal);
116 // periodic ghost band = max grain radius: the CLOSER particle of any cross-boundary contacting pair
117 // is within one radius of the face, so a band of maxRad ghosts it (sufficient for sphere-sphere).
118 const float ghostBand = maxRad;
119 // Size the SoA for the ghost boundary layer BEFORE emitting (CUDA did this in initialize() via
120 // calculate_capacity). Without it a Simulation(numReal) leaves capacity==numReal, so every ghost
121 // overflows P.capacity in generateGhostsKokkos and cross-boundary contacts are never detected.
123 generateGhostsKokkos(P.numReal, P.capacity, P.domain, ghostBand, P.pos, P.invMass, P.posPred,
124 P.vel, P.velPred, P.quat, P.quatPred, P.angVel, P.angVelPred, P.scale,
127
128 {
129 auto sc = P.scale;
130 auto rad = P.rad;
131 float gs = P.globalScale, bR = P.baseRadius;
132 Kokkos::parallel_for(
133 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
134 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
135 }
136 // Collision detection runs on the PREDICTED state (speculative positions/orientations), matching
137 // the CUDA solver — the position solve then corrects posPred against these contacts.
138 // findCollisionsGrow fences + reads the pair count back to host and guarantees np ≤ P.pairs extent
139 // (growing the buffer on overflow) so the narrowphase never reads P.pairs out of bounds.
140 const int np = findCollisionsGrow(P, margin);
141
142 Kokkos::deep_copy(space, P.contactCount, 0);
143 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
145 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid);
147 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
148 P.maxOverlap);
149 if (P.numWalls > 0)
151 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
153 const int nc = readInt(P.contactCount);
154
156 const int nm = readInt(P.manifoldCount);
157
158 // A frictional wall drives friction even when the body-body material is frictionless.
159 const bool friction = (P.frictionDynamic > 0.0f || P.wallFrictionMax > 0.0f);
160 if (friction)
162 P.planeFriction);
163
164 for (int it = 0; it < P.velocityIterations; ++it) {
165 if (friction)
170 P.deltaAngVel);
172 }
173 if (friction) {
177 P.deltaAngVel);
179 }
180
182 P.angVelPred, P.posPred, P.quatPred, P.angVel, P.dt);
183
184 for (int it = 0; it < P.positionIterations; ++it) {
189 }
190
192
193 // Berendsen thermostat at the end of the step (CUDA Simulation::step), tau>0 enables.
194 if (P.thermostatTau > 0.0f && P.dt > 0.0f)
197}
198
204 CpExec space;
205 const float maxRad = maxOwnedRadius(P);
206 const float margin = 0.1f * maxRad;
207 P.numParticles = P.numReal;
208 Kokkos::deep_copy(P.posPred, P.pos);
209 Kokkos::deep_copy(P.quatPred, P.quat);
210 {
211 auto ri = P.realIndices;
212 Kokkos::parallel_for(
213 "self", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
214 KOKKOS_LAMBDA(int i) { ri(i) = i; });
215 }
216 Kokkos::deep_copy(space, P.topGhost, P.numReal);
217 const float ghostBand = maxRad;
218 // Match demStep: ensure ghost-boundary-layer headroom so cross-boundary overlaps are counted (a
219 // Simulation(numReal) otherwise has capacity==numReal and every ghost overflows). See demStep.
221 generateGhostsKokkos(P.numReal, P.capacity, P.domain, ghostBand, P.pos, P.invMass, P.posPred,
222 P.vel, P.velPred, P.quat, P.quatPred, P.angVel, P.angVelPred, P.scale,
225 {
226 auto sc = P.scale;
227 auto rad = P.rad;
228 float gs = P.globalScale, bR = P.baseRadius;
229 Kokkos::parallel_for(
230 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
231 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
232 }
233 const int np = findCollisionsGrow(P, margin);
234 Kokkos::deep_copy(space, P.contactCount, 0);
235 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
237 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid);
239 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
240 P.maxOverlap);
241 if (P.numWalls > 0)
243 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
245 P.numParticles = P.numReal;
246 float h;
247 Kokkos::deep_copy(h, P.maxOverlap);
248 return h;
249}
250
251#ifdef PECLET_DEM_MPI
268inline void demStepMpi(Particles& P, ParticleHalo& halo, double rcut, int syncEvery,
269 bool forwardRotation) {
270 CpExec space;
271 const float margin = 0.1f * maxOwnedRadius(P);
272
273 if (P.growthFactor != -1.0f && P.growthRate != 0.0f) {
274 P.growthFactor *= std::exp(P.growthRate * P.dt);
275 if (P.growthFactor > 1.0f)
276 P.growthFactor = 1.0f;
277 }
278 if (P.growthFactor > 0.0f)
279 updateGrowthScalesKokkos(P.numReal, P.scale, P.targetScale, P.growthFactor);
280
281 // 1. Predict velocity on the owned set (no ghosts yet -> numParticles == numReal).
282 P.numParticles = P.numReal;
283 predictVelocityKokkos(P.numReal, P.pos, P.invMass, P.vel, P.quat, P.angVel, P.invInertia,
284 P.posPred, P.quatPred, P.velPred, P.angVelPred, P.deltaPos, P.deltaQuat,
285 P.deltaVel, P.deltaAngVel, P.constraintCounts, P.gravity, P.dt, P.extForce);
286
287 // 2. Gather ghosts (real mass) from owners over the halo: full state into the ghost slots; sets
288 // P.numParticles = numReal + numGhost and self-maps realIndices.
289 halo.gather(P, rcut);
290
291 {
292 auto sc = P.scale;
293 auto rad = P.rad;
294 float gs = P.globalScale, bR = P.baseRadius;
295 Kokkos::parallel_for(
296 "rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numParticles),
297 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
298 }
299
300 // 3. Broad/narrow phase + manifold reduction over owned + ghosts.
301 // findCollisionsGrow fences + reads the pair count back to host and guarantees np ≤ P.pairs extent
302 // (growing the buffer on overflow) so the narrowphase never reads P.pairs out of bounds.
303 const int np = findCollisionsGrow(P, margin);
304
305 Kokkos::deep_copy(space, P.contactCount, 0);
306 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
307 detectContactsKokkos(P.pairs, np, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes, P.shell,
308 P.globalScale, margin, P.contacts, P.contactCount, P.maxOverlap, P.sdfGrid);
309 detectBoundaryKokkos(P.numReal, P.numPlanes, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes,
310 P.shell, P.planes, P.globalScale, margin, P.contacts, P.contactCount,
311 P.maxOverlap);
312 if (P.numWalls > 0)
313 detectWallSdfKokkos(P.numReal, P.numWalls, P.posPred, P.quatPred, P.scale, P.shapeId, P.shapes,
314 P.shell, P.walls, P.wallGrid, P.globalScale, margin, P.contacts,
315 P.contactCount, P.maxOverlap);
316 const int nc = readInt(P.contactCount);
317
318 reduceContactsToManifoldsKokkos(P.contacts, nc, P.manifolds, P.manifoldCount);
319 const int nm = readInt(P.manifoldCount);
320
321 // 4. Velocity solve: normal restitution + Coulomb friction; refresh ghost velocities every
322 // syncEvery iters (+ last). Unlike the original CUDA distributed scheme (pure normal restitution),
323 // the friction cluster runs here too, so a frictional/moving wall drives the distributed step —
324 // e.g. a rotating drum lifts its bed under MPI exactly as single-rank. WALL friction (boundary
325 // contacts, bodyB<0) touches only the OWNED particle, so it is exact across ranks; body-body
326 // friction near a rank boundary uses the ghost's LOCAL contact count in the count-average, so it is
327 // a close approximation there (bounded by the Coulomb clamp), not bit-exact. The gathered ghosts
328 // carry current velPred (forwarded each syncEvery iter), so the force-chain load sees neighbours.
329 const bool friction = (P.frictionDynamic > 0.0f || P.wallFrictionMax > 0.0f);
330 if (friction)
331 computePlaneLoadKokkos(P.contacts, nc, P.invMass, P.invInertia, P.velPred, P.angVelPred,
332 P.planeFriction);
333
334 for (int it = 0; it < P.velocityIterations; ++it) {
335 if (friction)
336 accumulateNormalImpulseKokkos(P.contacts, nc, P.invMass, P.invInertia, P.velPred,
337 P.angVelPred, P.realIndices, P.growthRate);
338 solveVelocityKokkos(P.manifolds, nm, P.invMass, P.invInertia, P.quat, P.velPred, P.angVelPred,
339 P.realIndices, P.growthRate, P.restitutionNormal, P.deltaVel,
340 P.deltaAngVel);
341 applyVelocityDeltasKokkos(P.numParticles, P.velPred, P.angVelPred, P.deltaVel, P.deltaAngVel);
342 if ((it + 1) % syncEvery == 0 || it == P.velocityIterations - 1) {
343 halo.forward(P.velPred);
344 if (forwardRotation)
345 halo.forward(P.angVelPred);
346 }
347 }
348 if (friction) {
349 countFrictionContactsKokkos(P.contacts, nc, P.realIndices, P.planeFriction);
350 solveContactFrictionKokkos(P.contacts, nc, P.invMass, P.invInertia, P.velPred, P.angVelPred,
351 P.realIndices, P.planeFriction, P.frictionDynamic, P.deltaVel,
352 P.deltaAngVel);
353 applyVelocityDeltasKokkos(P.numParticles, P.velPred, P.angVelPred, P.deltaVel, P.deltaAngVel);
354 halo.forward(P.velPred); // publish the friction velocity update to the ghosts
355 if (forwardRotation)
356 halo.forward(P.angVelPred);
357 }
358
359 // 5. Apply velocity & predict position, then refresh ghost predicted positions (+ pose if
360 // rotating).
361 applyVelocityAndPredictPositionKokkos(P.numParticles, P.pos, P.invMass, P.vel, P.quat, P.velPred,
362 P.angVelPred, P.posPred, P.quatPred, P.angVel, P.dt);
363 halo.forwardPositions(P.posPred);
364 if (forwardRotation)
365 halo.forward4(P.quatPred);
366
367 // 6. Position solve (Projected Jacobi); refresh ghost predicted pose every syncEvery iters (+
368 // last).
369 for (int it = 0; it < P.positionIterations; ++it) {
370 solvePositionKokkos(P.contacts, nc, P.invMass, P.posPred, P.quatPred, P.quat, P.invInertia,
371 P.deltaPos, P.deltaQuat, P.constraintCounts, P.maxOverlap);
372 applyUpdatesKokkos(P.numParticles, P.posPred, P.velPred, P.deltaPos, P.deltaVel,
373 P.constraintCounts);
374 if ((it + 1) % syncEvery == 0 || it == P.positionIterations - 1) {
375 halo.forwardPositions(P.posPred);
376 if (forwardRotation)
377 halo.forward4(P.quatPred);
378 }
379 }
380
381 // 7. Commit (owned results kept; ghosts discarded, re-gathered next substep).
382 finalCommitKokkos(P.numReal, P.pos, P.invMass, P.posPred, P.quat, P.quatPred, P.domain);
383
384 if (P.thermostatTau > 0.0f && P.dt > 0.0f)
385 applyThermostatKokkos(P.numReal, P.vel, P.invMass, P.angVel, P.invInertia, P.quat,
386 P.thermostatKB, P.thermostatTau, P.thermostatTemp, P.dt);
387
388 P.numParticles = P.numReal; // restore owned-only active count for getters
389}
390#endif // PECLET_DEM_MPI
391
394 public:
395 explicit Simulation(int capacity) {
396 registry().push_back(this);
397 P_.allocate(capacity, capacity * 64, capacity * 16, /*shapes*/ 1, /*shell*/ 1, /*planes*/ 8);
398 // default sphere shape (radius 1) + identity-ish defaults
399 setSphereShape(1.0f);
400 }
402 auto& r = registry();
403 r.erase(std::remove(r.begin(), r.end(), this), r.end());
404 }
405
406 // Teardown safety: the Particles SoA holds Kokkos Views, so they MUST be freed before
407 // Kokkos::finalize (else "deallocated after finalize" aborts). releaseAll() (called from the
408 // module's atexit, before finalize) frees every live Sim's Views, so callers need not `del sim;
409 // gc.collect()` themselves.
411 P_ = Particles{};
412#ifdef PECLET_DEM_MPI
413 halo_.reset(); // the halo also owns Kokkos Views (gather/forward buffers + its core sub-objects')
414 // that must be freed before Kokkos::finalize, else "deallocated after finalize"
415 // aborts. Destroying it via the unique_ptr frees them all.
416#endif
417 }
418 static void releaseAll() {
419 for (auto* s : registry())
420 s->releaseViews();
421 }
422 static std::vector<Simulation*>& registry() {
423 static std::vector<Simulation*> r;
424 return r;
425 }
426
427 void setSphereShape(float radius) { initializeShape(SPHERE, radius, 0.0f, 0.0f); }
428
429 // Mirror of CUDA Simulation::initialize(shape_type, radius, height, thickness): builds shape 0's
430 // descriptor + surface point shell (cylinder/box) and records the per-shape base radius and
431 // (uniform-mass=1) inverse inertia applied to every particle by setPositions. shape_type uses the
432 // peclet::dem::ShapeKind values (SPHERE=1, HOLLOW_CYLINDER=2, BOX=3).
433 void initializeShape(int shape_type, float radius, float height, float thickness) {
434 baseRadius_ = radius;
435 P_.baseRadius = radius; // effective radius = scale*globalScale*baseRadius (broadphase + ghost band)
436 F4 params{radius, 0, 0, 0};
437 std::vector<F3> shell;
438
439 if (shape_type == HOLLOW_CYLINDER) {
440 params = F4{radius, height, thickness, 0};
441 // Dynamic spacing (faithful to CUDA): >=4 pts across thickness, >=20 around circumference.
442 float min_dim = std::min(radius, thickness);
443 if (min_dim < 1e-4f)
444 min_dim = radius; // safety if thickness 0
445 float spacing = std::min(radius * 0.3f, min_dim * 0.5f);
446 if (spacing < 1e-3f)
447 spacing = 1e-3f;
448 shell = genCylinderShell(radius, height, thickness, spacing);
449 } else if (shape_type == BOX) {
450 // Cube with half-extent = radius (side = 2*radius).
451 params = F4{radius, radius, radius, 0};
452 float spacing = std::max(radius * 0.5f, 1e-3f);
453 shell = genBoxShell(radius, radius, radius, spacing);
454 } else {
455 shape_type = SPHERE;
456 params = F4{radius, 0, 0, 0}; // sphere: analytic single-probe, no shell
457 }
458
459 // Upload the shell (resize the View; numPoints==0 => analytic single-probe like the sphere).
460 const int nPts = static_cast<int>(shell.size());
461 shellPoints_ = nPts;
462 if (nPts > 0) {
463 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
464 auto hs = Kokkos::create_mirror_view(P_.shell);
465 for (int i = 0; i < nPts; ++i) {
466 hs(i, 0) = shell[i].x;
467 hs(i, 1) = shell[i].y;
468 hs(i, 2) = shell[i].z;
469 }
470 Kokkos::deep_copy(P_.shell, hs);
471 }
472
473 auto h = Kokkos::create_mirror_view(P_.shapes);
474 h(0) = ShapeDesc{shape_type, params, 0, nPts};
475 Kokkos::deep_copy(P_.shapes, h);
476
477 // Per-shape inverse inertia (mass=1), faithful to CUDA Simulation::initialize.
478 float ix = 1.0f, iy = 1.0f, iz = 1.0f;
479 if (shape_type == SPHERE) {
480 if (baseRadius_ > 0.0f) {
481 float v = 2.5f / (baseRadius_ * baseRadius_);
482 ix = iy = iz = v;
483 }
484 } else if (shape_type == HOLLOW_CYLINDER) {
485 float r_out = baseRadius_, r_in = baseRadius_ - thickness;
486 if (r_in < 0)
487 r_in = 0;
488 float term_r = r_out * r_out + r_in * r_in;
489 float I_zz = 0.5f * term_r;
490 float I_xx = (1.0f / 12.0f) * (3.0f * term_r + height * height);
491 if (I_xx > 1e-6f) {
492 ix = 1.0f / I_xx;
493 iy = 1.0f / I_xx;
494 }
495 if (I_zz > 1e-6f)
496 iz = 1.0f / I_zz;
497 } else if (shape_type == BOX) {
498 float L = 2.0f * baseRadius_;
499 float I = (1.0f / 6.0f) * L * L;
500 if (I > 1e-6f) {
501 ix = iy = iz = 1.0f / I;
502 }
503 }
504 defaultInvI_ = F3{ix, iy, iz};
505 }
506
507 // Import a general particle as a grid SDF (canonical, unit-scale space) + a surface point shell +
508 // its unit-mass principal-frame diagonal inverse inertia. Replaces shape 0, so every particle
509 // becomes an instance of this shape (per-particle position/quaternion/scale still apply). `grid`
510 // holds nx*ny*nz signed-distance samples, x-fastest (idx = x + y*nx + z*nx*ny), at lattice nodes
511 // q = origin + (x,y,z)*spacing (negative inside). `shellFlat` is the flat [nPts*3] surface point
512 // set the collision probes body A against. boundingRadius is the canonical radius enclosing the
513 // surface (broad-phase + VTI splat bound). invInertia is the per-unit-mass diagonal inverse
514 // inertia in the body (principal) frame; like the analytic shapes it becomes the default applied
515 // to every particle by setPositions (override afterwards with set_inv_inertia / set_inv_mass for a
516 // real density). See peclet.dem.particle_builder for the Python side that produces these arrays
517 // from an implicit-solid SDF (marching-cubes shell + voxel-integrated mass properties).
518 void setSdfShape(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
519 const std::vector<float>& shellFlat, F3 invInertia, float boundingRadius) {
520 if (nx < 2 || ny < 2 || nz < 2)
521 throw std::runtime_error("setSdfShape: grid dims must be >= 2 on each axis");
522 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
523 throw std::runtime_error("setSdfShape: grid.size() must equal nx*ny*nz");
524 const int nPts = static_cast<int>(shellFlat.size() / 3);
525 if (nPts <= 0)
526 throw std::runtime_error("setSdfShape: empty surface point shell");
527
528 // Upload the signed-distance samples.
529 P_.sdfGrid = Kokkos::View<float*, CpMem>("sdfGrid", grid.size());
530 auto hg = Kokkos::create_mirror_view(P_.sdfGrid);
531 for (size_t i = 0; i < grid.size(); ++i)
532 hg(i) = grid[i];
533 Kokkos::deep_copy(P_.sdfGrid, hg);
534
535 // Upload the surface point shell.
536 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
537 auto hs = Kokkos::create_mirror_view(P_.shell);
538 for (int i = 0; i < nPts; ++i) {
539 hs(i, 0) = shellFlat[3 * i];
540 hs(i, 1) = shellFlat[3 * i + 1];
541 hs(i, 2) = shellFlat[3 * i + 2];
542 }
543 Kokkos::deep_copy(P_.shell, hs);
544
545 // Shape descriptor: grid SDF for the field, shell points for the probes.
546 ShapeDesc sd{};
547 sd.type = SHAPE_GRID_SDF;
548 sd.params = F4{boundingRadius, 0, 0, 0};
549 sd.shellOffset = 0;
550 sd.numPoints = nPts;
551 sd.gridOffset = 0;
552 sd.nx = nx;
553 sd.ny = ny;
554 sd.nz = nz;
555 sd.gridOrigin = origin;
556 sd.gridInvSpacing = F3{spacing.x > 0 ? 1.0f / spacing.x : 0.0f,
557 spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
558 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
559 auto h = Kokkos::create_mirror_view(P_.shapes);
560 h(0) = sd;
561 Kokkos::deep_copy(P_.shapes, h);
562
563 baseRadius_ = boundingRadius;
564 P_.baseRadius = boundingRadius;
565 defaultInvI_ = invInertia;
566 shellPoints_ = nPts;
567 ensureContactCapacity();
568 }
569
570 void setDomain(float lx, float ly, float lz, bool px, bool py, bool pz) {
571 P_.domain = Domain{F3{0, 0, 0}, F3{lx, ly, lz}, F3{lx, ly, lz}, px, py, pz};
572 P_.skin = 0.1f * P_.globalScale;
573 }
574 // CUDA Simulation::set_domain(min, max): arbitrary origin; keeps the current periodicity flags.
575 void setDomainMinMax(F3 mn, F3 mx) {
576 P_.domain = Domain{mn,
577 mx,
578 F3{mx.x - mn.x, mx.y - mn.y, mx.z - mn.z},
581 P_.domain.periodic_z};
582 P_.skin = 0.1f * P_.globalScale;
583 }
584 void enablePeriodicity(bool x, bool y, bool z) {
585 P_.domain.periodic_x = x;
586 P_.domain.periodic_y = y;
587 P_.domain.periodic_z = z;
588 }
589 std::tuple<float, float, float> getDomainMin() const {
590 return {P_.domain.min.x, P_.domain.min.y, P_.domain.min.z};
591 }
592 std::tuple<float, float, float> getDomainMax() const {
593 return {P_.domain.max.x, P_.domain.max.y, P_.domain.max.z};
594 }
595 void setGravity(float gx, float gy, float gz) { P_.gravity = F3{gx, gy, gz}; }
596 void setThermostat(float temperature, float tau, float kB) { // Berendsen; tau=0 disables
597 P_.thermostatTemp = temperature;
598 P_.thermostatTau = tau;
599 P_.thermostatKB = kB;
600 }
601 void setSolverIterations(int pos, int vel) {
602 P_.positionIterations = pos;
603 P_.velocityIterations = vel;
604 }
605 void setGlobalScale(float s) {
606 P_.globalScale = s;
607 P_.skin = 0.1f * s;
608 }
609 void setDt(float dt) { P_.dt = dt; }
610 // (restitution_normal, restitution_tangent, friction) to match CUDA set_material_params; the
611 // Kokkos pipeline currently carries normal restitution + dynamic friction (tangential restitution
612 // unused).
613 void setMaterialParams(float restitution_normal, float restitution_tangent, float friction) {
614 P_.restitutionNormal = restitution_normal;
615 P_.frictionDynamic = friction;
616 (void)restitution_tangent;
617 }
618 void addPlane(float px, float py, float pz, float nx, float ny, float nz) {
619 auto h = Kokkos::create_mirror_view(P_.planes);
620 Kokkos::deep_copy(h, P_.planes);
621 if (P_.numPlanes < static_cast<int>(P_.planes.extent(0)))
622 h(P_.numPlanes++) = PlaneP{F3{px, py, pz}, F3{nx, ny, nz}};
623 Kokkos::deep_copy(P_.planes, h);
624 }
625
626 // Add a static, world-space SDF wall/container the grains collide against (a drum barrel, hopper,
627 // vibrating tray). `grid` is a flat [nx*ny*nz] signed-distance field, x-fastest (idx = x + y*nx +
628 // z*nx*ny), sampled at world nodes origin + (x,y,z)*spacing — POSITIVE in the void where the grains
629 // live, NEGATIVE inside the solid wall (so a grain surface point reads the penetration depth and
630 // the outward gradient is the push-out normal). restitution/friction are the binary particle–wall
631 // material (independent of the body-body material). The wall is motionless but carries a rigid-body
632 // surface-velocity field (set via setWallVelocity) so a grain touching it feels the wall's motion.
633 // Returns the wall's index (for setWallVelocity). Add walls before stepping.
634 int addSdfWall(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
635 float restitution, float friction) {
636 if (nx < 2 || ny < 2 || nz < 2)
637 throw std::runtime_error("addSdfWall: grid dims must be >= 2 on each axis");
638 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
639 throw std::runtime_error("addSdfWall: grid.size() must equal nx*ny*nz");
640
641 WallSdf w{};
642 w.nx = nx;
643 w.ny = ny;
644 w.nz = nz;
645 w.gridOffset = static_cast<int>(wallGridHost_.size());
646 w.origin = origin;
647 w.invSpacing = F3{spacing.x > 0 ? 1.0f / spacing.x : 0.0f, spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
648 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
649 w.restitution = restitution;
650 w.friction = friction;
651 const int idx = static_cast<int>(wallsHost_.size());
652 wallsHost_.push_back(w);
653 wallGridHost_.insert(wallGridHost_.end(), grid.begin(), grid.end());
654 // Upload the concatenated grid samples (only changes when a wall is added, not per step).
655 P_.wallGrid = Kokkos::View<float*, CpMem>("wallGrid", wallGridHost_.size());
656 auto hg = Kokkos::create_mirror_view(P_.wallGrid);
657 for (size_t i = 0; i < wallGridHost_.size(); ++i)
658 hg(i) = wallGridHost_[i];
659 Kokkos::deep_copy(P_.wallGrid, hg);
660 uploadWalls();
661 P_.numWalls = static_cast<int>(wallsHost_.size());
662 if (friction > P_.wallFrictionMax)
663 P_.wallFrictionMax = friction;
664 ensureContactCapacity();
665 return idx;
666 }
667
668 // Set a wall's rigid-body surface-velocity field v(x) = linVel + angVel × (x − center). A grain in
669 // contact feels this velocity even though the geometry never moves: set angVel for a rotating drum
670 // (about `center` on the axis), or drive linVel sinusoidally each step for a vibrating wall. Cheap
671 // (a few host scalars); safe to call every step.
672 void setWallVelocity(int wallIndex, F3 linVel, F3 angVel, F3 center) {
673 if (wallIndex < 0 || wallIndex >= static_cast<int>(wallsHost_.size()))
674 throw std::runtime_error("setWallVelocity: wall index out of range");
675 wallsHost_[wallIndex].linVel = linVel;
676 wallsHost_[wallIndex].angVel = angVel;
677 wallsHost_[wallIndex].center = center;
678 uploadWalls();
679 }
680
681 // positions: flat [n*3]; (re)sets the real-particle count and default state.
682 void setPositions(const std::vector<float>& xyz) {
683 const int n = static_cast<int>(xyz.size() / 3);
684 P_.numReal = n;
685 P_.numParticles = n;
686 auto pos = Kokkos::create_mirror_view(P_.pos);
687 auto q = Kokkos::create_mirror_view(P_.quat);
688 auto im = Kokkos::create_mirror_view(P_.invMass);
689 auto sc = Kokkos::create_mirror_view(P_.scale);
690 auto ii = Kokkos::create_mirror_view(P_.invInertia);
691 auto sid = Kokkos::create_mirror_view(P_.shapeId);
692 auto vel = Kokkos::create_mirror_view(P_.vel);
693 auto av = Kokkos::create_mirror_view(P_.angVel);
694 for (int i = 0; i < n; ++i) {
695 pos(i, 0) = xyz[3 * i];
696 pos(i, 1) = xyz[3 * i + 1];
697 pos(i, 2) = xyz[3 * i + 2];
698 q(i, 0) = 0;
699 q(i, 1) = 0;
700 q(i, 2) = 0;
701 q(i, 3) = 1;
702 im(i) = 1.0f;
703 sc(i) = 1.0f;
704 sid(i) = 0;
705 ii(i, 0) = defaultInvI_.x;
706 ii(i, 1) = defaultInvI_.y;
707 ii(i, 2) = defaultInvI_.z;
708 vel(i, 0) = vel(i, 1) = vel(i, 2) = 0;
709 av(i, 0) = av(i, 1) = av(i, 2) = 0;
710 }
711 Kokkos::deep_copy(P_.pos, pos);
712 Kokkos::deep_copy(P_.quat, q);
713 Kokkos::deep_copy(P_.invMass, im);
714 Kokkos::deep_copy(P_.scale, sc);
715 Kokkos::deep_copy(P_.invInertia, ii);
716 Kokkos::deep_copy(P_.shapeId, sid);
717 Kokkos::deep_copy(P_.vel, vel);
718 Kokkos::deep_copy(P_.angVel, av);
719 Kokkos::deep_copy(P_.targetScale, P_.scale); // unscaled growth target = the set scale
720 }
721 void setScalesUniform(float s) {
722 auto sc = Kokkos::create_mirror_view(P_.scale);
723 for (int i = 0; i < P_.numReal; ++i)
724 sc(i) = s;
725 Kokkos::deep_copy(P_.scale, sc);
726 Kokkos::deep_copy(P_.targetScale, P_.scale);
727 }
728 // per-particle scales (growth target): flat [n]. scale starts at target (growth factor applies in
729 // step).
730 void setScales(const std::vector<float>& s) {
731 auto tsc = Kokkos::create_mirror_view(P_.targetScale);
732 for (int i = 0; i < P_.numReal && i < (int)s.size(); ++i)
733 tsc(i) = s[i];
734 Kokkos::deep_copy(P_.targetScale, tsc);
735 Kokkos::deep_copy(P_.scale, P_.targetScale);
736 }
737 void setVelocities(const std::vector<float>& v) {
738 auto vel = Kokkos::create_mirror_view(P_.vel);
739 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)v.size(); ++i) {
740 vel(i, 0) = v[3 * i];
741 vel(i, 1) = v[3 * i + 1];
742 vel(i, 2) = v[3 * i + 2];
743 }
744 Kokkos::deep_copy(P_.vel, vel);
745 }
746 // Per-particle external FORCE (fluid drag etc.), an (N,3) flat array. Applied in the next step()'s
747 // velocity predict as dv = F*invMass*dt. Persists across steps until re-set or cleared.
748 void setExternalForces(const std::vector<float>& f) {
749 auto ef = Kokkos::create_mirror_view(P_.extForce);
750 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)f.size(); ++i) {
751 ef(i, 0) = f[3 * i];
752 ef(i, 1) = f[3 * i + 1];
753 ef(i, 2) = f[3 * i + 2];
754 }
755 Kokkos::deep_copy(P_.extForce, ef);
756 }
757 void clearExternalForces() { Kokkos::deep_copy(P_.extForce, 0.0f); }
758 const V3& externalForcesView() const { return P_.extForce; }
759 // rigid-body rotation state (the pipeline integrates the gyroscopic Euler term + quaternion
760 // already)
761 void setQuaternions(const std::vector<float>& q) {
762 auto h = Kokkos::create_mirror_view(P_.quat);
763 for (int i = 0; i < P_.numReal && 4 * i + 3 < (int)q.size(); ++i) {
764 h(i, 0) = q[4 * i];
765 h(i, 1) = q[4 * i + 1];
766 h(i, 2) = q[4 * i + 2];
767 h(i, 3) = q[4 * i + 3];
768 }
769 Kokkos::deep_copy(P_.quat, h);
770 }
771 void setAngularVelocities(const std::vector<float>& w) {
772 auto h = Kokkos::create_mirror_view(P_.angVel);
773 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)w.size(); ++i) {
774 h(i, 0) = w[3 * i];
775 h(i, 1) = w[3 * i + 1];
776 h(i, 2) = w[3 * i + 2];
777 }
778 Kokkos::deep_copy(P_.angVel, h);
779 }
780 void setInvInertia(const std::vector<float>& ii) {
781 auto h = Kokkos::create_mirror_view(P_.invInertia);
782 for (int i = 0; i < P_.numReal && 3 * i + 2 < (int)ii.size(); ++i) {
783 h(i, 0) = ii[3 * i];
784 h(i, 1) = ii[3 * i + 1];
785 h(i, 2) = ii[3 * i + 2];
786 }
787 Kokkos::deep_copy(P_.invInertia, h);
788 }
789 void setInvMass(const std::vector<float>& im) {
790 auto h = Kokkos::create_mirror_view(P_.invMass);
791 for (int i = 0; i < P_.numReal && i < (int)im.size(); ++i)
792 h(i) = im[i];
793 Kokkos::deep_copy(P_.invMass, h);
794 }
795 std::vector<float> getAngularVelocities() const {
796 return peclet::core::toVector(
797 Kokkos::subview(P_.angVel, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
798 }
799 std::vector<float> getInvInertia() const {
800 return peclet::core::toVector(
801 Kokkos::subview(P_.invInertia, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
802 }
803 // growth: factor *= exp(rate*dt) per step (capped at 1); new_factor<0 keeps/initialises (0.01 if
804 // inactive).
805 void setGrowthParams(float rate, float new_factor) {
806 if (P_.growthFactor == -1.0f)
807 P_.growthFactor = (new_factor > 0.0f) ? new_factor : 0.01f;
808 else if (new_factor > 0.0f)
809 P_.growthFactor = new_factor;
810 P_.growthRate = rate;
811 if (P_.growthFactor > 0.0f)
813 }
814 float growthFactor() const { return P_.growthFactor; }
815 float getGrowthRate() const { return P_.growthRate; }
816 // per-particle mass = 1/invMass (0 for fixed/infinite-mass particles), CUDA
817 // Simulation::get_masses.
818 std::vector<float> getMasses() const {
819 auto im = Kokkos::create_mirror_view(P_.invMass);
820 Kokkos::deep_copy(im, P_.invMass);
821 std::vector<float> out(P_.numReal);
822 for (int i = 0; i < P_.numReal; ++i)
823 out[i] = (im(i) > 0.0f) ? (1.0f / im(i)) : 0.0f;
824 return out;
825 }
826
827 std::vector<float> getPositions() const {
828 return peclet::core::toVector(
829 Kokkos::subview(P_.pos, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
830 }
831 std::vector<float> getVelocities() const {
832 return peclet::core::toVector(
833 Kokkos::subview(P_.vel, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
834 }
835 std::vector<float> getQuaternions() const {
836 return peclet::core::toVector(
837 Kokkos::subview(P_.quat, Kokkos::make_pair(0, P_.numReal), Kokkos::ALL));
838 }
839 std::vector<float> getScales() const {
840 return peclet::core::toVector(Kokkos::subview(P_.scale, Kokkos::make_pair(0, P_.numReal)));
841 }
842
843 // One XPBD substep (CUDA Simulation::step(dt) semantics): dt>0 sets the timestep; dt==0 is a
844 // dynamics-free relaxation step (overlap removal only). Drive the loop from Python.
845 void step(float dt) {
846 P_.dt = dt;
847 demStep(P_);
848 }
849
850 // Max pair interpenetration on the current committed state (CUDA Simulation::compute_overlaps).
852
853 // LAMMPS "dump custom" of the current committed state (CUDA Simulation::export_lammps). Radius =
854 // scale*globalScale*baseRadius; bounds computed from the particle AABBs.
855 void exportLammps(const std::string& filename, int step) const {
856 const std::vector<float> pos = getPositions(), vel = getVelocities(), quat = getQuaternions();
857 auto sc = Kokkos::create_mirror_view(P_.scale);
858 Kokkos::deep_copy(sc, P_.scale);
859 std::vector<float> radii(P_.numReal);
860 for (int i = 0; i < P_.numReal; ++i)
861 radii[i] = sc(i) * P_.globalScale * baseRadius_;
862 const bool pbc = P_.domain.periodic_x || P_.domain.periodic_y || P_.domain.periodic_z;
863 peclet::dem::writeLammpsDump(filename, step, pos, vel, quat, radii, nullptr, nullptr, pbc);
864 }
865
866 // SDF field over the domain -> ImageData VTI (CUDA Simulation::export_sdf).
867 void exportSdf(const std::string& filename, int rx, int ry, int rz) {
868 const std::vector<float> grid = getSdfGrid(rx, ry, rz);
869 const float mn[3] = {P_.domain.min.x, P_.domain.min.y, P_.domain.min.z};
870 const float mx[3] = {P_.domain.max.x, P_.domain.max.y, P_.domain.max.z};
871 peclet::dem::writeSdfVti(filename, grid, rx, ry, rz, mn, mx);
872 }
873
874#ifdef PECLET_DEM_MPI
875 // Block decomposition over the GLOBAL domain (once); the per-block solver stays non-periodic, the
876 // halo supplies the periodic wrap. gsize is the ORB cell grid. Mirror of Simulation::mpi_init.
877 void initMpi(std::tuple<double, double, double> origin, std::tuple<double, double, double> size,
878 std::tuple<long, long, long> gsize, std::tuple<bool, bool, bool> periodic,
879 MPI_Comm comm) {
880 halo_->initMpi({std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
881 {std::get<0>(size), std::get<1>(size), std::get<2>(size)},
882 {std::get<0>(gsize), std::get<1>(gsize), std::get<2>(gsize)},
883 {std::get<0>(periodic), std::get<1>(periodic), std::get<2>(periodic)}, comm);
884 }
885 // Enable the distributed step. rcut is the ghost-band width (default = 1.0*globalScale, the
886 // periodic skin used by the single-GPU path); sync_every is the owner->ghost refresh interval (1
887 // = EXACT). rebalance_every: re-decompose by particle count + migrate ownership every N
888 // distributed steps to keep the per-rank load even as a packing densifies (0 = never; the
889 // partition is then fixed at the initial decomposition, as before). A pure redistribution — the
890 // physics result is unchanged.
891 void enableMpiStep(double rcut, int sync_every = 1, bool forward_rotation = true,
892 int rebalance_every = 0, double verlet_skin = 0.0) {
893 mpiRcut_ = rcut;
894 mpiSyncEvery_ = sync_every < 1 ? 1 : sync_every;
895 mpiForwardRotation_ = forward_rotation;
896 mpiRebalanceEvery_ = rebalance_every < 0 ? 0 : rebalance_every;
897 // Verlet-skin ghost reuse (D2): rebuild the halo topology only when a particle has moved >
898 // skin, instead of every substep. 0 (default) keeps the exact per-substep rebuild.
899 halo_->setVerletSkin(static_cast<float>(verlet_skin));
900 }
901 // Halo rebuild stats (D2): topology rebuilds vs total gather() calls (for benchmarking).
902 long mpiRebuilds() const { return halo_->numRebuilds(); }
903 long mpiGathers() const { return halo_->numGathers(); }
904 // Migrate ownership now so each rank holds a near-equal particle count. Safe to call at a step
905 // boundary; returns this rank's new owned count. Exposed for manual / adaptive balancing.
906 int rebalance() { return halo_->rebalance(P_); }
907 // Co-rebalance: migrate ownership onto the weighted ORB of per-cell weights `w` (the SAME partition
908 // the coupled flow solver redistributes onto from the same weight field). Returns new owned count.
909 int migrateToWeights(const std::vector<peclet::core::Real>& w) {
910 return halo_->migrateToWeights(P_, w);
911 }
912 void stepMpi(int nsteps) {
913 const double rcut = (mpiRcut_ > 0.0) ? mpiRcut_ : maxOwnedRadius(P_);
914 for (int s = 0; s < nsteps; ++s) {
915 if (mpiRebalanceEvery_ > 0 && mpiStepCount_ % mpiRebalanceEvery_ == 0)
916 halo_->rebalance(P_);
917 demStepMpi(P_, *halo_, rcut, mpiSyncEvery_, mpiForwardRotation_);
918 ++mpiStepCount_;
919 }
920 }
921 int rank() const { return halo_->rank(); }
922 int numGhost() const { return halo_->numGhost(); }
923#endif // PECLET_DEM_MPI
924
925 // SDF grid (get_sdf_grid): Eikonal reconstruction over the domain, flat x-fastest, negative
926 // inside solid.
927 std::vector<float> getSdfGrid(int rx, int ry, int rz) {
929 rx, ry, rz, P_.domain.min, P_.domain.max, P_.numReal, P_.pos, P_.quat, P_.scale, P_.shapeId,
931 }
932
933 int numParticles() const { return P_.numReal; }
934 // Live device Views of the owned particle state, for the zero-copy device-array export (H2): the
935 // binding wraps a [0,numReal) subview as a DLPack/__cuda_array_interface__ array (or a NumPy view
936 // on a host backend) referencing this memory — no device->host copy.
937 const V3& positionsView() const { return P_.pos; }
938 const V3& velocitiesView() const { return P_.vel; }
939 int numContacts() { return readInt(P_.contactCount); }
940 int numManifolds() { return readInt(P_.manifoldCount); }
941 float maxOverlap() {
942 float h;
943 Kokkos::deep_copy(h, P_.maxOverlap);
944 return h;
945 }
946
947 // ParaView PolyData (points + Radius + Velocity), faithful to CUDA Simulation::write_vtp:
948 // Radius = scale * globalScale * baseRadius.
949 void writeVtp(const std::string& filename) const {
950 auto pos = Kokkos::create_mirror_view(P_.pos);
951 Kokkos::deep_copy(pos, P_.pos);
952 auto sc = Kokkos::create_mirror_view(P_.scale);
953 Kokkos::deep_copy(sc, P_.scale);
954 auto vel = Kokkos::create_mirror_view(P_.vel);
955 Kokkos::deep_copy(vel, P_.vel);
956 const int n = P_.numReal;
957
958 std::ofstream out(filename);
959 if (!out)
960 throw std::runtime_error("Could not open file for writing: " + filename);
961 out << "<?xml version=\"1.0\"?>\n";
962 out << "<VTKFile type=\"PolyData\" version=\"0.1\" byte_order=\"LittleEndian\">\n";
963 out << " <PolyData>\n";
964 out << " <Piece NumberOfPoints=\"" << n << "\" NumberOfVerts=\"0\" "
965 << "NumberOfLines=\"0\" NumberOfStrips=\"0\" NumberOfPolys=\"0\">\n";
966 out << " <Points>\n";
967 out << " <DataArray type=\"Float32\" Name=\"Position\" NumberOfComponents=\"3\" "
968 "format=\"ascii\">\n";
969 for (int i = 0; i < n; ++i)
970 out << pos(i, 0) << " " << pos(i, 1) << " " << pos(i, 2) << " ";
971 out << "\n </DataArray>\n";
972 out << " </Points>\n";
973 out << " <PointData Scalars=\"Radius\">\n";
974 out << " <DataArray type=\"Float32\" Name=\"Radius\" NumberOfComponents=\"1\" "
975 "format=\"ascii\">\n";
976 for (int i = 0; i < n; ++i)
977 out << sc(i) * P_.globalScale * baseRadius_ << " ";
978 out << "\n </DataArray>\n";
979 out << " <DataArray type=\"Float32\" Name=\"Velocity\" NumberOfComponents=\"3\" "
980 "format=\"ascii\">\n";
981 for (int i = 0; i < n; ++i)
982 out << vel(i, 0) << " " << vel(i, 1) << " " << vel(i, 2) << " ";
983 out << "\n </DataArray>\n";
984 out << " </PointData>\n";
985 out << " </Piece>\n";
986 out << " </PolyData>\n";
987 out << "</VTKFile>\n";
988 out.close();
989 std::printf("Exported VTP: %s\n", filename.c_str());
990 }
991
992 private:
993 // (Re)upload just the small WallSdf array (velocity fields change every step for a vibrating wall;
994 // the grid samples are uploaded once in addSdfWall).
995 void uploadWalls() {
996 const int n = std::max<int>(1, static_cast<int>(wallsHost_.size()));
997 if (static_cast<int>(P_.walls.extent(0)) < n)
998 P_.walls = Kokkos::View<WallSdf*, CpMem>("walls", n);
999 auto h = Kokkos::create_mirror_view(P_.walls);
1000 for (size_t i = 0; i < wallsHost_.size(); ++i)
1001 h(i) = wallsHost_[i];
1002 Kokkos::deep_copy(P_.walls, h);
1003 }
1004
1005 // Size the contact/manifold buffers so no contact is dropped: a shell point sits inside at most one
1006 // neighbour (body-body ~ capacity*shellPoints) plus one per wall it touches (capacity*shellPoints
1007 // per wall). Boundary/wall contacts are appended AFTER body-body ones, so an undersized buffer
1008 // silently drops them and grains tunnel through walls. Floored at the analytic default; grows only.
1009 void ensureContactCapacity() {
1010 const int perParticle = std::max(16, shellPoints_);
1011 const long want = static_cast<long>(P_.capacity) * perParticle +
1012 static_cast<long>(P_.capacity) * std::max(1, shellPoints_) * P_.numWalls;
1013 if (want > P_.maxContacts) {
1014 P_.maxContacts = static_cast<int>(want);
1015 P_.contacts = Kokkos::View<ContactC*, CpMem>("contacts", want);
1016 P_.manifolds = Kokkos::View<ManifoldC*, CpMem>("manifolds", want);
1017 }
1018 }
1019
1020 Particles P_;
1021 float baseRadius_ = 1.0f;
1022 int shellPoints_ = 0; // surface-shell size of the active shape (contact-buffer sizing)
1023 std::vector<WallSdf> wallsHost_;
1024 std::vector<float> wallGridHost_;
1025 F3 defaultInvI_{2.5f, 2.5f, 2.5f};
1026#ifdef PECLET_DEM_MPI
1027 std::unique_ptr<ParticleHalo> halo_ = std::make_unique<ParticleHalo>();
1028 double mpiRcut_ = 0.0;
1029 int mpiSyncEvery_ = 1;
1030 bool mpiForwardRotation_ = true;
1031 int mpiRebalanceEvery_ = 0;
1032 long mpiStepCount_ = 0;
1033#endif
1034};
1035
1036} // namespace peclet::dem
1037
1038#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:393
void setQuaternions(const std::vector< float > &q)
Definition sim.hpp:761
float computeOverlaps()
Definition sim.hpp:851
std::vector< float > getMasses() const
Definition sim.hpp:818
void setThermostat(float temperature, float tau, float kB)
Definition sim.hpp:596
std::tuple< float, float, float > getDomainMax() const
Definition sim.hpp:592
std::vector< float > getInvInertia() const
Definition sim.hpp:799
std::vector< float > getVelocities() const
Definition sim.hpp:831
static void releaseAll()
Definition sim.hpp:418
std::vector< float > getScales() const
Definition sim.hpp:839
void enablePeriodicity(bool x, bool y, bool z)
Definition sim.hpp:584
static std::vector< Simulation * > & registry()
Definition sim.hpp:422
void setSphereShape(float radius)
Definition sim.hpp:427
Simulation(int capacity)
Definition sim.hpp:395
std::vector< float > getQuaternions() const
Definition sim.hpp:835
void setWallVelocity(int wallIndex, F3 linVel, F3 angVel, F3 center)
Definition sim.hpp:672
void exportSdf(const std::string &filename, int rx, int ry, int rz)
Definition sim.hpp:867
void writeVtp(const std::string &filename) const
Definition sim.hpp:949
int numParticles() const
Definition sim.hpp:933
void setGlobalScale(float s)
Definition sim.hpp:605
void addPlane(float px, float py, float pz, float nx, float ny, float nz)
Definition sim.hpp:618
void setVelocities(const std::vector< float > &v)
Definition sim.hpp:737
float growthFactor() const
Definition sim.hpp:814
std::tuple< float, float, float > getDomainMin() const
Definition sim.hpp:589
int addSdfWall(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, float restitution, float friction)
Definition sim.hpp:634
void setPositions(const std::vector< float > &xyz)
Definition sim.hpp:682
void setInvInertia(const std::vector< float > &ii)
Definition sim.hpp:780
void setDt(float dt)
Definition sim.hpp:609
void setAngularVelocities(const std::vector< float > &w)
Definition sim.hpp:771
const V3 & externalForcesView() const
Definition sim.hpp:758
void clearExternalForces()
Definition sim.hpp:757
std::vector< float > getAngularVelocities() const
Definition sim.hpp:795
void setScalesUniform(float s)
Definition sim.hpp:721
std::vector< float > getSdfGrid(int rx, int ry, int rz)
Definition sim.hpp:927
void step(float dt)
Definition sim.hpp:845
void setDomain(float lx, float ly, float lz, bool px, bool py, bool pz)
Definition sim.hpp:570
void setSolverIterations(int pos, int vel)
Definition sim.hpp:601
void initializeShape(int shape_type, float radius, float height, float thickness)
Definition sim.hpp:433
const V3 & velocitiesView() const
Definition sim.hpp:938
void setExternalForces(const std::vector< float > &f)
Definition sim.hpp:748
std::vector< float > getPositions() const
Definition sim.hpp:827
void setMaterialParams(float restitution_normal, float restitution_tangent, float friction)
Definition sim.hpp:613
void exportLammps(const std::string &filename, int step) const
Definition sim.hpp:855
float getGrowthRate() const
Definition sim.hpp:815
void setDomainMinMax(F3 mn, F3 mx)
Definition sim.hpp:575
const V3 & positionsView() const
Definition sim.hpp:937
void setInvMass(const std::vector< float > &im)
Definition sim.hpp:789
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:518
void setScales(const std::vector< float > &s)
Definition sim.hpp:730
void setGrowthParams(float rate, float new_factor)
Definition sim.hpp:805
void setGravity(float gx, float gy, float gz)
Definition sim.hpp:595
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)
int reduceContactsToManifoldsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int n, Kokkos::View< ManifoldC *, CpMem > outManifolds, Kokkos::View< int, CpMem > outCount)
Reduce n contacts to manifolds (one per unique canonical pair).
void computePlaneLoadKokkos(Kokkos::View< ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, FrManifoldCounts planeFriction)
Plane (idB<0) one-shot loads.
float maxOwnedRadius(const Particles &P)
Largest effective particle radius over the owned set (= max scale × globalScale, growth included).
Definition sim.hpp:51
void accumulateNormalImpulseKokkos(Kokkos::View< ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate)
Force-chain normal load, accumulated over the velocity iterations: contacts(idx).friction_lambda_n +=...
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)
Per-real-particle contacts against a static world-space wall SDF set (a drum barrel,...
void solveContactFrictionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, FrManifoldCounts planeFriction, float frictionDynamic, Kokkos::View< float *[3], CpMem > deltaVel, Kokkos::View< float *[3], CpMem > deltaAngVel)
One count-averaged Coulomb friction sweep.
Kokkos::View< float *[3], CpMem > V3
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{})
Pair point-shell vs SDF contacts.
void applyVelocityDeltasKokkos(int n, V3 velPred, V3 angVelPred, V3 deltaVel, V3 deltaAngVel)
Add accumulated velocity/angular deltas onto the predicted velocity, then clear the delta buffers.
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
int readInt(Kokkos::View< int, CpMem > v)
Definition sim.hpp:40
void solvePositionKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > posPred, Kokkos::View< const float *[4], CpMem > quatPred, Kokkos::View< const float *[4], CpMem > quatStatic, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< float *[3], CpMem > deltaPos, Kokkos::View< float *[4], CpMem > deltaQuat, Kokkos::View< int *, CpMem > constraintCounts, Kokkos::View< float, CpMem > maxOverlap)
Accumulate XPBD position corrections for numContacts contacts.
void applyVelocityAndPredictPositionKokkos(int n, V3 pos, Vf invMass, V3 vel, V4 quat, V3 velPred, V3 angVelPred, V3 posPred, V4 quatPred, V3 angVel, float dt)
Re-integration: persist solved velocity, trapezoidal position predict, quaternion integrate.
int findCollisionsGrow(Particles &P, float margin)
Broad phase with an automatically-grown pair buffer.
Definition sim.hpp:72
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 applyUpdatesKokkos(int n, V3 posPred, V3 velPred, V3 deltaPos, V3 deltaVel, Vi constraintCounts)
Jacobi count-averaged apply of position/velocity deltas, then clear deltas + counts.
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:203
void demStep(Particles &P)
One full XPBD DEM substep over the particle SoA (mirrors simulation.cpp Simulation::step()).
Definition sim.hpp:87
void solveVelocityKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > velPred, Kokkos::View< const float *[3], CpMem > angVelPred, Kokkos::View< const int *, CpMem > realIdx, float growthRate, float restitutionNormal, Kokkos::View< float *[3], CpMem > deltaVel, Kokkos::View< float *[3], CpMem > deltaAngVel)
Accumulate normal-restitution velocity deltas for numManifolds manifolds.
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{})
std::vector< F3 > genBoxShell(float hx, float hy, float hz, float spacing)
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)
Generate periodic ghosts for particles [0,numReal).
int findCollisionsArborX(PosV pos, RadV rad, int numParticles, int numReal, float margin, PairsV outPairs, CountV outCount, float boxCap=0.0f)
Emit candidate collision pairs (i<j) for real particles into outPairs/outCount.
void applyThermostatKokkos(int numReal, V3 vel, Vf invMass, V3 angVel, V3 invInertia, V4 quat, double kB, double tau, double Ttarget, float dt)
void countFrictionContactsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int numContacts, Kokkos::View< const int *, CpMem > realIdx, FrManifoldCounts planeFriction)
Per-body active-contact count into planeFriction(:,1).
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 — portable (Kokkos) Coulomb friction cluster (the single dissipative friction path).
dem — portable (Kokkos) XPBD position solve (pure overlap removal).
dem — portable (Kokkos) manifold velocity solve (normal restitution impulse).
Kokkos::View< WallSdf *, CpMem > walls
Definition particles.hpp:61
Kokkos::View< int, CpMem > manifoldCount
Definition particles.hpp:52
void ensureCapacity(int newCap)
Kokkos::View< ManifoldC *, CpMem > manifolds
Definition particles.hpp:49
Kokkos::View< int *[2], CpMem > pairs
Definition particles.hpp:47
Kokkos::View< int, CpMem > contactCount
Definition particles.hpp:52
Kokkos::View< ShapeDesc *, CpMem > shapes
Definition particles.hpp:56
Kokkos::View< int, CpMem > topGhost
Definition particles.hpp:52
Kokkos::View< float *, CpMem > sdfGrid
Definition particles.hpp:59
Kokkos::View< float, CpMem > maxOverlap
Definition particles.hpp:53
Kokkos::View< float *, CpMem > wallGrid
Definition particles.hpp:62
Kokkos::View< const float *, CpMem > crad() const
void allocate(int cap, int maxPairs_, int maxContacts_, int nShapes, int nShell, int nPlanes)
Definition particles.hpp:80
Kokkos::View< float *[2], CpMem > planeFriction
Definition particles.hpp:42
Kokkos::View< float *[3], CpMem > shell
Definition particles.hpp:57
Kokkos::View< ContactC *, CpMem > contacts
Definition particles.hpp:48
Kokkos::View< int, CpMem > pairCount
Definition particles.hpp:52
Kokkos::View< PlaneP *, CpMem > planes
Definition particles.hpp:58
Portable mirror of ShapeDescriptor (analytic fields + a flat-array point shell).
Static, world-space SDF container/geometry the particles collide against (a drum barrel,...
static const double L[3]