peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solve_driver_force.hpp
Go to the documentation of this file.
1
31#ifndef DEM_SOLVE_DRIVER_FORCE_HPP
32#define DEM_SOLVE_DRIVER_FORCE_HPP
33
34#include <cstdio>
35#include <cstdlib>
36#include <Kokkos_Core.hpp>
37#include <Kokkos_Sort.hpp>
38#include <stdexcept>
39
40#include "particles.hpp"
41#include "solve_driver.hpp" // findCollisionsGrow, maxOwnedRadius, readInt/readFloat
42#include "solver_hertz.hpp"
43
44namespace peclet::dem {
45
50inline void hertzRebuildPairs(Particles& P, float skin) {
51 CpExec space;
52 Kokkos::deep_copy(space, P.posPred, P.pos); // broadphase reads posPred
53 const int np = findCollisionsGrow(P, skin);
54 const size_t need = P.pairs.extent(0);
55 if (P.hertzXi.extent(0) < need) {
56 P.hertzXi = Kokkos::View<float* [3], CpMem>("hertzXi", need);
57 P.hertzSnPair = Kokkos::View<float*, CpMem>("hertzSnPair", need);
58 P.hertzKeys = Kokkos::View<unsigned long long*, CpMem>("hertzKeys", need);
59 P.hertzPrevKeys = Kokkos::View<unsigned long long*, CpMem>("hertzPrevKeys", need);
60 P.hertzPrevXi = Kokkos::View<float* [3], CpMem>("hertzPrevXi", need);
61 }
62 // keys for the new list + history carry from the previous sorted list
63 {
64 auto pairs = P.pairs;
65 auto keys = P.hertzKeys;
66 auto xi = P.hertzXi;
67 auto pk = P.hertzPrevKeys;
68 auto px = P.hertzPrevXi;
69 auto gid = P.gid;
70 const int pc = P.hertzPrevCount;
71 Kokkos::parallel_for(
72 "peclet::dem::hertz_carry", Kokkos::RangePolicy<CpExec>(space, 0, np),
73 KOKKOS_LAMBDA(int idx) {
74 const unsigned a = (unsigned)gid(pairs(idx, 0)), b = (unsigned)gid(pairs(idx, 1));
75 const unsigned lo = a < b ? a : b, hi = a < b ? b : a;
76 const unsigned long long k = ((unsigned long long)hi << 32) | lo;
77 keys(idx) = k;
78 int l = 0, h = pc;
79 while (l < h) {
80 const int m = (l + h) >> 1;
81 if (pk(m) < k)
82 l = m + 1;
83 else
84 h = m;
85 }
86 const bool hit = (l < pc && pk(l) == k);
87 xi(idx, 0) = hit ? px(l, 0) : 0.0f;
88 xi(idx, 1) = hit ? px(l, 1) : 0.0f;
89 xi(idx, 2) = hit ? px(l, 2) : 0.0f;
90 });
91 }
92 P.hertzNumPairs = np;
93 { // lagged patch-stiffness store: reset at rebuild (one mis-damped step, harmless)
94 auto sn = Kokkos::subview(P.hertzSnPair, Kokkos::pair<int, int>(0, np));
95 Kokkos::deep_copy(space, sn, 0.0f);
96 }
97 Kokkos::deep_copy(space, P.hertzRefPos, P.pos);
98 Kokkos::deep_copy(space, P.hertzDispMax, 0.0f);
99 space.fence();
100}
101
104 const int n = P.hertzNumPairs;
105 if (n < 0)
106 return; // no valid LIVE list (fresh sim, or just after an ownership migration): the prev
107 // store may hold carried history for the coming rebuild — do not wipe it.
108 if (n == 0) {
109 P.hertzPrevCount = 0;
110 return;
111 }
112 CpExec space;
113 const auto rng = Kokkos::pair<int, int>(0, n);
114 auto kd = Kokkos::subview(P.hertzPrevKeys, rng);
115 Kokkos::deep_copy(space, kd, Kokkos::subview(P.hertzKeys, rng));
116 Kokkos::View<int*, CpMem> perm(
117 Kokkos::view_alloc(space, "peclet::dem::hertz_perm", Kokkos::WithoutInitializing), n);
118 Kokkos::parallel_for(
119 "peclet::dem::hertz_iota", Kokkos::RangePolicy<CpExec>(space, 0, n),
120 KOKKOS_LAMBDA(int i) { perm(i) = i; });
121 Kokkos::Experimental::sort_by_key(space, kd, perm);
122 auto xi = P.hertzXi;
123 auto px = P.hertzPrevXi;
124 Kokkos::parallel_for(
125 "peclet::dem::hertz_gather", Kokkos::RangePolicy<CpExec>(space, 0, n),
126 KOKKOS_LAMBDA(int i) {
127 const int j = perm(i);
128 px(i, 0) = xi(j, 0);
129 px(i, 1) = xi(j, 1);
130 px(i, 2) = xi(j, 2);
131 });
132 P.hertzPrevCount = n;
133 space.fence();
134}
135
142 void rebuild(Particles& P, float skin) const {
143 hertzRebuildPairs(P, skin);
144 if (P.numWalls > 0)
148 }
163 void wallForces(Particles& P, float dt, bool hasShapes) const {
164 if (P.numWalls > 0 && P.hertzNumWallCand > 0)
165 hertzWallForcesKokkos(Kokkos::View<const int*, CpMem>(P.hertzWallCand), P.hertzNumWallCand,
166 P.walls, P.wallGrid, P.pos, P.vel, P.angVel,
171 Kokkos::View<const float* [4], CpMem>(P.quat),
172 Kokkos::View<const float*, CpMem>(P.scale), P.shapeId, P.shapes,
173 P.shell, P.globalScale, P.hertzContactRadiusFrac, hasShapes,
174 P.hertzSnWall);
175 }
177 void validate(const Particles& P) const {
179 throw std::runtime_error("step_hertz: periodic domains not supported");
180 if (P.numPlanes > 0)
181 throw std::runtime_error("step_hertz: analytic planes not supported (use an SDF wall)");
183 throw std::runtime_error("step_hertz: too many SDF walls");
184 }
185};
186
190inline void zeroForceScratchKokkos(V3 dv, V3 dw, int lo, int hi) {
191 if (hi <= lo)
192 return;
193 CpExec space;
194 Kokkos::parallel_for(
195 "peclet::dem::force_zero_ghost", Kokkos::RangePolicy<CpExec>(space, lo, hi),
196 KOKKOS_LAMBDA(int i) {
197 for (int c = 0; c < 3; ++c) {
198 dv(i, c) = 0.0f;
199 dw(i, c) = 0.0f;
200 }
201 });
202 space.fence();
203}
204
207inline void fillWorldRadiiKokkos(Vf scale, Vf rad, float gs, float bR, int n) {
208 CpExec space;
209 Kokkos::parallel_for(
210 "peclet::dem::force_rad", Kokkos::RangePolicy<CpExec>(space, 0, n),
211 KOKKOS_LAMBDA(int i) { rad(i) = scale(i) * gs * bR; });
212 space.fence();
213}
214
217 static constexpr bool distributed = false;
218 float allMax(float v) const { return v; }
219 float allMin(float v) const { return v; }
220 void gatherGhosts(Particles&) const {}
221 void refreshGhostState(Particles&, bool) const {}
223};
224
227template <class Law, class Hooks>
228inline void demStepForce(Particles& P, float dt, int nsteps, float skinFrac, const Law& law,
229 const Hooks& hooks) {
230 law.validate(P);
231 { // world radii (the impulse step fills these; the force path must too)
232 CpExec space;
233 auto sc = P.scale;
234 auto rad = P.rad;
235 float gs = P.globalScale, bR = P.baseRadius;
236 Kokkos::parallel_for(
237 "peclet::dem::hertz_rad", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
238 KOKKOS_LAMBDA(int i) { rad(i) = sc(i) * gs * bR; });
239 space.fence();
240 }
241 float minRad = 0.0f;
242 {
243 CpExec space;
244 auto rad = P.rad;
245 float m = 3.4e38f;
246 Kokkos::parallel_reduce(
247 "peclet::dem::hertz_minrad", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
248 KOKKOS_LAMBDA(int i, float& acc) {
249 if (rad(i) < acc)
250 acc = rad(i);
251 },
252 Kokkos::Min<float>(m));
253 space.fence();
254 minRad = m;
255 }
256 // Skin off the SMALLEST radius (globally under MPI: every rank must use the same skin, so the
257 // rebuild threshold — and thus the collective gather schedule — is identical): a max-radius skin
258 // inflates every small-grain pair cutoff by the big body's margin (measured ~15x more cached
259 // pairs in the ball-impact case).
260 const float skin = skinFrac * hooks.allMin(minRad);
261 const float rebuildAt = 0.25f * skin * skin; // (skin/2)^2 on |dx|^2
262 const F3 g = P.gravity;
263 // Cache-validity must be a COLLECTIVE decision too: after a migration every rank invalidates,
264 // but a single desynchronised flag would desynchronise the gather below.
265 bool rebuild = hooks.allMax(P.hertzNumPairs < 0 ? 1.0f : 0.0f) > 0.0f;
266 // Non-spherical dispatch: any shape with a point shell routes pairs through the per-point
267 // Hertz kernel and enables orientation integration.
268 bool hasShapes = false;
269 {
270 auto hs = Kokkos::create_mirror_view(P.shapes);
271 Kokkos::deep_copy(hs, P.shapes);
272 for (size_t si = 0; si < hs.extent(0); ++si)
273 if (hs(si).numPoints > 0)
274 hasShapes = true;
275 }
276 static const bool profile = std::getenv("PECLET_DEM_HERTZ_PROFILE") != nullptr;
277 double tPair = 0, tWall = 0, tInt = 0, tRebuild = 0, tCheck = 0;
278 int nRebuilds = 0;
279 Kokkos::Timer timer;
280 for (int k = 0; k < nsteps; ++k) {
281 if (rebuild) {
282 law.commitHistory(P);
283 // Distributed: re-gather the ghost band (pair cutoff + skin) with a fresh topology, THEN
284 // build the rank-local pair list over owned + ghosts. Between rebuilds the Verlet argument
285 // covers the ghosts too: nothing outside the band can reach a cached pair before the next
286 // displacement-triggered rebuild.
287 hooks.gatherGhosts(P);
288 law.rebuild(P, skin);
289 rebuild = false;
290 ++nRebuilds;
291 if (profile) {
292 tRebuild += timer.seconds();
293 timer.reset();
294 }
295 } else {
296 // Ghosts must track their owners every step (positions/velocities changed by integrate).
297 hooks.refreshGhostState(P, hasShapes);
298 }
299 hooks.clearGhostScratch(P); // ghost force/torque slots: accumulated but never consumed
300 law.pairForces(P, dt, hasShapes);
301 if (profile) {
302 tPair += timer.seconds();
303 timer.reset();
304 }
305 law.wallForces(P, dt, hasShapes);
306 if (profile) {
307 tWall += timer.seconds();
308 timer.reset();
309 }
311 P.vel, P.angVel, P.pos, P.quat, hasShapes);
312 if (profile) {
313 tInt += timer.seconds();
314 timer.reset();
315 }
316 if ((k & 15) == 15 || k == nsteps - 1) {
317 if (hooks.allMax(hertzMaxDisp2Kokkos(P.numReal, P.pos, P.hertzRefPos)) > rebuildAt)
318 rebuild = true;
319 if (profile) {
320 tCheck += timer.seconds();
321 timer.reset();
322 }
323 }
324 }
325 if (profile)
326 std::printf("[hertz profile] steps=%d pairs=%d wallcand=%d rebuilds=%d | pair %.3fs wall %.3fs "
327 "integrate %.3fs check %.3fs rebuild %.3fs\n",
328 nsteps, P.hertzNumPairs, P.hertzNumWallCand, nRebuilds, tPair, tWall, tInt, tCheck,
329 tRebuild);
330 CpExec space;
331 Kokkos::deep_copy(space, P.posPred, P.pos); // keep the impulse-path views coherent
332 Kokkos::deep_copy(space, P.velPred, P.vel);
333 space.fence();
334}
335
338inline void demStepHertz(Particles& P, float dt, int nsteps, float skinFrac) {
339 demStepForce(P, dt, nsteps, skinFrac, HertzMindlinLaw{}, SoloForceHooks{});
340}
341
342} // namespace peclet::dem
343
344#endif // DEM_SOLVE_DRIVER_FORCE_HPP
float hertzMaxDisp2Kokkos(int numReal, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *[3], CpMem > refPos)
Max squared displacement since the last pair build (called only at rebuild checks – keeping this out ...
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...
Kokkos::View< float *, CpMem > Vf
Kokkos::View< const unsigned char *, CpMem > MatIdView
void hertzCommitHistory(Particles &P)
Save the current list (sorted by key) so the NEXT rebuild can carry the history.
void hertzPairForcesKokkos(Kokkos::View< const int *[2], CpMem > pairs, int numPairs, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *[3], CpMem > vel, Kokkos::View< const float *[3], CpMem > angVel, Kokkos::View< const float *, CpMem > rad, Kokkos::View< const float *, CpMem > invMass, MatIdView matId, PairTableView pairTable, float eGlobal, float muGlobal, Kokkos::View< const float *, CpMem > hertzE, Kokkos::View< const float *, CpMem > hertzNu, float dt, Kokkos::View< float *[3], CpMem > xi, Kokkos::View< float *[3], CpMem > force, Kokkos::View< float *[3], CpMem > torque)
Cached-pair forces: overlap from current positions; history resets when a cached pair is currently se...
Kokkos::View< const float *, CpMem > PairTableView
Kokkos::View< float *[3], CpMem > V3
void hertzIntegrateKokkos(int numReal, Kokkos::View< float *[3], CpMem > force, Kokkos::View< float *[3], CpMem > torque, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< const float *[3], CpMem > invInertia, F3 gravity, float dt, Kokkos::View< float *[3], CpMem > vel, Kokkos::View< float *[3], CpMem > angVel, Kokkos::View< float *[3], CpMem > pos, Kokkos::View< float *[4], CpMem > quat={}, bool integrateOrientation=false)
Symplectic-Euler kick-drift (MUSEN-style) + displacement tracking for the Verlet rebuild.
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 hertzRebuildPairs(Particles &P, float skin)
Rebuild the Hertz cached pair list with margin skin, carrying the Mindlin shear history across by pai...
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 hertzWallForcesKokkos(Kokkos::View< const int *, CpMem > candSlots, int numCand, Kokkos::View< const WallSdf *, CpMem > walls, GridView wallGrid, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *[3], CpMem > vel, Kokkos::View< const float *[3], CpMem > angVel, Kokkos::View< const float *, CpMem > rad, Kokkos::View< const float *, CpMem > invMass, MatIdView matId, PairTableView pairTable, float eGlobal, float muGlobal, Kokkos::View< const float *, CpMem > hertzE, Kokkos::View< const float *, CpMem > hertzNu, float dt, Kokkos::View< float *[3], CpMem > xiWall, int maxWalls, Kokkos::View< float *[3], CpMem > force, Kokkos::View< float *[3], CpMem > torque, Kokkos::View< const float *[4], CpMem > quat={}, Kokkos::View< const float *, CpMem > scale={}, ScalarI shapeId={}, Kokkos::View< const ShapeDesc *, CpMem > shapes={}, ShellView shell={}, float globalScale=1.0f, float contactRadiusFrac=0.5f, bool hasShapes=false, Kokkos::View< float *, CpMem > snWall={})
SDF-wall forces (per particle x wall).
int hertzBuildWallCandidatesKokkos(int numReal, int numWalls, Kokkos::View< const WallSdf *, CpMem > walls, GridView wallGrid, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *, CpMem > rad, float skin, Kokkos::View< int *, CpMem > outSlots, Kokkos::View< int, CpMem > outCount)
Build the wall candidate list: particles within (radius + skin) of any wall's zero level.
Kokkos::DefaultExecutionSpace CpExec
void hertzShapePairForcesKokkos(Kokkos::View< const int *[2], CpMem > pairs, int numPairs, Kokkos::View< const float *[3], CpMem > pos, Kokkos::View< const float *[4], CpMem > quat, Kokkos::View< const float *[3], CpMem > vel, Kokkos::View< const float *[3], CpMem > angVel, Kokkos::View< const float *, CpMem > scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, GridView sdfGrid, float globalScale, float contactRadiusFrac, Kokkos::View< const float *, CpMem > invMass, MatIdView matId, PairTableView pairTable, float eGlobal, float muGlobal, Kokkos::View< const float *, CpMem > hertzE, Kokkos::View< const float *, CpMem > hertzNu, float dt, Kokkos::View< float *[3], CpMem > xi, Kokkos::View< float *, CpMem > snPrev, Kokkos::View< float *[3], CpMem > force, Kokkos::View< float *[3], CpMem > torque)
Non-spherical pairs: per-point Hertz springs over A's point shell against B's SDF (the same one-sided...
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
dem — the shared contact-solve driver: the full modern velocity + position solve sequence (warm-start...
Soft-sphere Hertz–Mindlin DEM (reference force model) for SPHERES.
Hertz–Mindlin force-law policy (the first force law; see file comment).
void wallForces(Particles &P, float dt, bool hasShapes) const
void rebuild(Particles &P, float skin) const
void commitHistory(Particles &P) const
void validate(const Particles &P) const
Engine-support restrictions of this law (probed once per call).
void pairForces(Particles &P, float dt, bool hasShapes) const
Kokkos::View< WallSdf *, CpMem > walls
Kokkos::View< float *, CpMem > hertzE
Kokkos::View< float *[3], CpMem > hertzXi
Kokkos::View< unsigned char *, CpMem > materialId
Kokkos::View< int, CpMem > hertzWallCandCount
Kokkos::View< int *[2], CpMem > pairs
Definition particles.hpp:52
Kokkos::View< int *, CpMem > hertzWallCand
Kokkos::View< float, CpMem > hertzDispMax
static constexpr int kHertzMaxWalls
Kokkos::View< float *, CpMem > pairMaterials
Kokkos::View< ShapeDesc *, CpMem > shapes
Kokkos::View< unsigned long long *, CpMem > hertzPrevKeys
Kokkos::View< float *, CpMem > hertzNu
Kokkos::View< unsigned long long *, CpMem > hertzKeys
Kokkos::View< float *, CpMem > hertzSnWall
Definition particles.hpp:98
Kokkos::View< float *[3], CpMem > hertzRefPos
Kokkos::View< float *, CpMem > sdfGrid
Kokkos::View< float *, CpMem > hertzSnPair
Definition particles.hpp:97
Kokkos::View< float *, CpMem > wallGrid
Kokkos::View< float *[3], CpMem > shell
Kokkos::View< float *[3], CpMem > hertzXiWall
Kokkos::View< float *[3], CpMem > hertzPrevXi
Single-GPU hooks: no ghosts, reductions are already global. Everything inlines away.
static constexpr bool distributed
void clearGhostScratch(Particles &) const
void gatherGhosts(Particles &) const
void refreshGhostState(Particles &, bool) const