peclet-dem 1.1.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
34#ifndef DEM_SOLVE_DRIVER_FORCE_HPP
35#define DEM_SOLVE_DRIVER_FORCE_HPP
36
37#include <cstdio>
38#include <cstdlib>
39#include <Kokkos_Core.hpp>
40#include <Kokkos_Sort.hpp>
41#include <stdexcept>
42
43#include "particles.hpp"
44#include "solve_driver.hpp" // findCollisionsGrow, maxOwnedRadius, readInt/readFloat
45#include "solver_hertz.hpp"
46
47namespace peclet::dem {
48
53inline void hertzRebuildPairs(Particles& P, float skin) {
54 CpExec space;
55 Kokkos::deep_copy(space, P.posPred, P.pos); // broadphase reads posPred
56 const int np = findCollisionsGrow(P, skin);
57 const size_t need = P.pairs.extent(0);
58 if (P.hertzXi.extent(0) < need) {
59 P.hertzXi = Kokkos::View<float* [3], CpMem>("hertzXi", need);
60 P.hertzSnPair = Kokkos::View<float*, CpMem>("hertzSnPair", need);
61 P.hertzKeys = Kokkos::View<unsigned long long*, CpMem>("hertzKeys", need);
62 P.hertzPrevKeys = Kokkos::View<unsigned long long*, CpMem>("hertzPrevKeys", need);
63 P.hertzPrevXi = Kokkos::View<float* [3], CpMem>("hertzPrevXi", need);
64 }
65 // keys for the new list + history carry from the previous sorted list
66 {
67 auto pairs = P.pairs;
68 auto keys = P.hertzKeys;
69 auto xi = P.hertzXi;
70 auto pk = P.hertzPrevKeys;
71 auto px = P.hertzPrevXi;
72 auto gid = P.gid;
73 const int pc = P.hertzPrevCount;
74 Kokkos::parallel_for(
75 "peclet::dem::hertz_carry", Kokkos::RangePolicy<CpExec>(space, 0, np),
76 KOKKOS_LAMBDA(int idx) {
77 // Canonical orientation: the lower-gid body first. The Mindlin spring xi is the
78 // tangential displacement of pairs(idx, 0) relative to pairs(idx, 1), so it flips sign
79 // with the order; the two owners of a cross-rank pair (owned slots precede ghosts) and
80 // a rank that received the pair by migration would otherwise hold it in different
81 // orientations, and a gid-keyed carry would hand one of them the wrong sign
82 // (docs/contact_solve_framework.md §12 S21; measured: dP 9e-4 in hertz_shear at the
83 // first drift migration). At np 1 gid == slot and the broad phase already emits
84 // (lower, higher): unchanged.
85 const int a0 = pairs(idx, 0), b0 = pairs(idx, 1);
86 if (gid(a0) > gid(b0)) {
87 pairs(idx, 0) = b0;
88 pairs(idx, 1) = a0;
89 }
90 const unsigned long long k =
91 pairKeyFromGids((unsigned)gid(pairs(idx, 0)), (unsigned)gid(pairs(idx, 1)));
92 keys(idx) = k;
93 const int l = lowerBoundKey(pk, pc, k);
94 const bool hit = (l < pc && pk(l) == k);
95 xi(idx, 0) = hit ? px(l, 0) : 0.0f;
96 xi(idx, 1) = hit ? px(l, 1) : 0.0f;
97 xi(idx, 2) = hit ? px(l, 2) : 0.0f;
98 });
99 }
100 P.hertzNumPairs = np;
101 { // lagged patch-stiffness store: reset at rebuild (one mis-damped step, harmless)
102 auto sn = Kokkos::subview(P.hertzSnPair, Kokkos::pair<int, int>(0, np));
103 Kokkos::deep_copy(space, sn, 0.0f);
104 }
105 Kokkos::deep_copy(space, P.hertzRefPos, P.pos);
106 Kokkos::deep_copy(space, P.hertzDispMax, 0.0f);
107 space.fence();
108}
109
112 const int n = P.hertzNumPairs;
113 if (n < 0)
114 return; // no valid LIVE list (fresh sim, or just after an ownership migration): the prev
115 // store may hold carried history for the coming rebuild — do not wipe it.
116 if (n == 0) {
117 P.hertzPrevCount = 0;
118 return;
119 }
120 CpExec space;
121 const auto rng = Kokkos::pair<int, int>(0, n);
122 auto kd = Kokkos::subview(P.hertzPrevKeys, rng);
123 Kokkos::deep_copy(space, kd, Kokkos::subview(P.hertzKeys, rng));
124 Kokkos::View<int*, CpMem> perm(
125 Kokkos::view_alloc(space, "peclet::dem::hertz_perm", Kokkos::WithoutInitializing), n);
126 Kokkos::parallel_for(
127 "peclet::dem::hertz_iota", Kokkos::RangePolicy<CpExec>(space, 0, n),
128 KOKKOS_LAMBDA(int i) { perm(i) = i; });
129 Kokkos::Experimental::sort_by_key(space, kd, perm);
130 auto xi = P.hertzXi;
131 auto px = P.hertzPrevXi;
132 Kokkos::parallel_for(
133 "peclet::dem::hertz_gather", Kokkos::RangePolicy<CpExec>(space, 0, n), KOKKOS_LAMBDA(int i) {
134 const int j = perm(i);
135 px(i, 0) = xi(j, 0);
136 px(i, 1) = xi(j, 1);
137 px(i, 2) = xi(j, 2);
138 });
139 P.hertzPrevCount = n;
140 space.fence();
141}
142
149 void rebuild(Particles& P, float skin) const {
150 hertzRebuildPairs(P, skin);
151 if (P.numWalls > 0)
155 }
156 void pairForces(Particles& P, float dt, bool hasShapes) const {
157 if (hasShapes)
159 P.shapeId, P.shapes, P.shell, P.sdfGrid, P.globalScale,
162 P.frictionDynamic, P.hertzE, P.hertzNu, dt, P.hertzXi,
164 else
166 P.pairs, P.hertzNumPairs, P.pos, P.vel, P.angVel, P.rad, P.invMass,
169 Kokkos::View<const float*, CpMem>(P.scale), P.shapeId, P.shapes, P.globalScale);
170 }
171 void wallForces(Particles& P, float dt, bool hasShapes) const {
172 if (P.numWalls > 0 && P.hertzNumWallCand > 0)
174 Kokkos::View<const int*, CpMem>(P.hertzWallCand), P.hertzNumWallCand, P.walls, P.wallGrid,
175 P.pos, P.vel, P.angVel, P.rad, P.invMass, MatIdView(P.materialId),
178 Kokkos::View<const float* [4], CpMem>(P.quat), Kokkos::View<const float*, CpMem>(P.scale),
179 P.shapeId, P.shapes, P.shell, P.globalScale, P.hertzContactRadiusFrac, hasShapes,
180 P.hertzSnWall);
181 }
183 void validate(const Particles& P) const {
185 throw std::runtime_error("step_hertz: periodic domains not supported");
186 if (P.numPlanes > 0)
187 throw std::runtime_error("step_hertz: analytic planes not supported (use an SDF wall)");
189 throw std::runtime_error("step_hertz: too many SDF walls");
190 }
191};
192
196inline void zeroForceScratchKokkos(V3 dv, V3 dw, int lo, int hi) {
197 if (hi <= lo)
198 return;
199 CpExec space;
200 Kokkos::parallel_for(
201 "peclet::dem::force_zero_ghost", Kokkos::RangePolicy<CpExec>(space, lo, hi),
202 KOKKOS_LAMBDA(int i) {
203 for (int c = 0; c < 3; ++c) {
204 dv(i, c) = 0.0f;
205 dw(i, c) = 0.0f;
206 }
207 });
208 space.fence();
209}
210
213 static constexpr bool distributed = false;
214 float allMax(float v) const { return v; }
215 float allMin(float v) const { return v; }
216 void gatherGhosts(Particles&) const {}
217 void refreshGhostState(Particles&, bool) const {}
219};
220
223template <class Law, class Hooks>
224inline void demStepForce(Particles& P, float dt, int nsteps, float skinFrac, const Law& law,
225 const Hooks& hooks) {
226 law.validate(P);
227 // world radii of the owned span (a halo gather re-fills owned + ghosts)
229 float minRad = 0.0f;
230 {
231 CpExec space;
232 auto rad = P.rad;
233 float m = 3.4e38f;
234 Kokkos::parallel_reduce(
235 "peclet::dem::hertz_minrad", Kokkos::RangePolicy<CpExec>(space, 0, P.numReal),
236 KOKKOS_LAMBDA(int i, float& acc) {
237 if (rad(i) < acc)
238 acc = rad(i);
239 },
240 Kokkos::Min<float>(m));
241 space.fence();
242 minRad = m;
243 }
244 // Skin off the SMALLEST radius (globally under MPI: every rank must use the same skin, so the
245 // rebuild threshold — and thus the collective gather schedule — is identical): a max-radius skin
246 // inflates every small-grain pair cutoff by the big body's margin (measured ~15x more cached
247 // pairs in the ball-impact case).
248 const float skin = skinFrac * hooks.allMin(minRad);
249 const float rebuildAt = 0.25f * skin * skin; // (skin/2)^2 on |dx|^2
250 const F3 g = P.gravity;
251 // Cache-validity must be a COLLECTIVE decision too: after a migration every rank invalidates,
252 // but a single desynchronised flag would desynchronise the gather below.
253 bool rebuild = hooks.allMax(P.hertzNumPairs < 0 ? 1.0f : 0.0f) > 0.0f;
254 // Non-spherical dispatch: any shape with a point shell routes pairs through the per-point
255 // Hertz kernel and enables orientation integration.
256 bool hasShapes = false;
257 {
258 auto hs = Kokkos::create_mirror_view(P.shapes);
259 Kokkos::deep_copy(hs, P.shapes);
260 for (size_t si = 0; si < hs.extent(0); ++si)
261 if (hs(si).numPoints > 0)
262 hasShapes = true;
263 }
264 static const bool profile = std::getenv("PECLET_DEM_HERTZ_PROFILE") != nullptr;
265 double tPair = 0, tWall = 0, tInt = 0, tRebuild = 0, tCheck = 0;
266 int nRebuilds = 0;
267 Kokkos::Timer timer;
268 for (int k = 0; k < nsteps; ++k) {
269 if (rebuild) {
270 law.commitHistory(P);
271 // Distributed: re-gather the ghost band (pair cutoff + skin) with a fresh topology, THEN
272 // build the rank-local pair list over owned + ghosts. Between rebuilds the Verlet argument
273 // covers the ghosts too: nothing outside the band can reach a cached pair before the next
274 // displacement-triggered rebuild.
275 hooks.gatherGhosts(P);
276 law.rebuild(P, skin);
277 rebuild = false;
278 ++nRebuilds;
279 if (profile) {
280 tRebuild += timer.seconds();
281 timer.reset();
282 }
283 } else {
284 // Ghosts must track their owners every step (positions/velocities changed by integrate).
285 hooks.refreshGhostState(P, hasShapes);
286 }
287 hooks.clearGhostScratch(P); // ghost force/torque slots: accumulated but never consumed
288 law.pairForces(P, dt, hasShapes);
289 if (profile) {
290 tPair += timer.seconds();
291 timer.reset();
292 }
293 law.wallForces(P, dt, hasShapes);
294 if (profile) {
295 tWall += timer.seconds();
296 timer.reset();
297 }
299 P.vel, P.angVel, P.pos, P.quat, hasShapes);
300 if (profile) {
301 tInt += timer.seconds();
302 timer.reset();
303 }
304 if ((k & 15) == 15 || k == nsteps - 1) {
305 if (hooks.allMax(hertzMaxDisp2Kokkos(P.numReal, P.pos, P.hertzRefPos)) > rebuildAt)
306 rebuild = true;
307 if (profile) {
308 tCheck += timer.seconds();
309 timer.reset();
310 }
311 }
312 }
313 if (profile)
314 std::printf(
315 "[hertz profile] steps=%d pairs=%d wallcand=%d rebuilds=%d | pair %.3fs wall %.3fs "
316 "integrate %.3fs check %.3fs rebuild %.3fs\n",
317 nsteps, P.hertzNumPairs, P.hertzNumWallCand, nRebuilds, tPair, tWall, tInt, tCheck,
318 tRebuild);
319 CpExec space;
320 Kokkos::deep_copy(space, P.posPred, P.pos); // keep the impulse-path views coherent
321 Kokkos::deep_copy(space, P.velPred, P.vel);
322 space.fence();
323}
324
327inline void demStepHertz(Particles& P, float dt, int nsteps, float skinFrac) {
328 demStepForce(P, dt, nsteps, skinFrac, HertzMindlinLaw{}, SoloForceHooks{});
329}
330
331} // namespace peclet::dem
332
333#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< const unsigned char *, CpMem > MatIdView
void hertzCommitHistory(Particles &P)
Save the current list (sorted by key) so the NEXT rebuild can carry the history.
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
unsigned long long pairKeyFromGids(unsigned a, unsigned b)
The one 64-bit pair identity every gid-keyed ledger uses (the XPBD persistent-contact and warm-start ...
int 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): the owned span before a step,...
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.
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, Kokkos::View< const float *, CpMem > scale={}, ScalarI shapeId={}, Kokkos::View< const ShapeDesc *, CpMem > shapes={}, float globalScale=1.0f)
Cached-pair forces: overlap from current positions; history resets when a cached pair is currently se...
int lowerBoundKey(const KeysView &keys, int count, unsigned long long key)
lower_bound over a ledger's sorted keys: the first index whose key is >= key (count when none).
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
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
Kokkos::View< float *[3], CpMem > hertzRefPos
Kokkos::View< float *, CpMem > sdfGrid
Kokkos::View< float *, CpMem > hertzSnPair
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