peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_hertz.hpp
Go to the documentation of this file.
1#pragma once
15
16#include <Kokkos_Core.hpp>
17#include <Kokkos_Sort.hpp>
18
20#include "narrowphase.hpp"
21#include "particles.hpp"
22
23namespace peclet::dem {
24
27KOKKOS_INLINE_FUNCTION void hertzPairEMu(int matA, int matB, PairTableView pairTable,
28 float eGlobal, float muGlobal, float& e, float& mu) {
29 if (pairTable.extent(0) > 0) {
30 const int t = (matA * kMaxMaterials + matB) * 2;
31 e = pairTable(t);
32 mu = pairTable(t + 1);
33 } else {
34 e = eGlobal;
35 mu = muGlobal;
36 }
37}
38
41KOKKOS_INLINE_FUNCTION float hertzBetaD(float e) {
42 if (e <= 0.0f)
43 return -1.0f;
44 if (e >= 1.0f)
45 return 0.0f;
46 const float le = Kokkos::log(e);
47 return le / Kokkos::sqrt(le * le + 9.8696044f);
48}
49
53KOKKOS_INLINE_FUNCTION F3 hertzForce(float delta, F3 nhat, F3 vrel, float rStar, float mStar,
54 float eStar, float gStar, float e, float mu, float dt,
55 F3& xi) {
56 const float sq = Kokkos::sqrt(rStar * delta);
57 const float sn = 2.0f * eStar * sq;
58 const float st = 8.0f * gStar * sq;
59 const float kn = (4.0f / 3.0f) * eStar * sq; // Fn_spring = kn * delta
60 const float bd = hertzBetaD(e);
61 const float etaN = -2.0f * 0.9128709f * bd * Kokkos::sqrt(sn * mStar); // sqrt(5/6)
62 const float etaT = -2.0f * 0.9128709f * bd * Kokkos::sqrt(st * mStar);
63
64 const float vn = dot3(vrel, nhat); // > 0: separating (A moving away from B)
65 const float fnSpring = kn * delta;
66 const float fn = fnSpring - etaN * vn; // + etaN * ddelta/dt: damping opposes approach
67 const F3 vt = sub3(vrel, scale3(nhat, vn));
68
69 // Mindlin history spring: accumulate tangential displacement, keep it in the tangent plane.
70 xi = add3(xi, scale3(vt, dt));
71 xi = sub3(xi, scale3(nhat, dot3(xi, nhat)));
72 F3 ft = add3(scale3(xi, -st), scale3(vt, -etaT));
73 const float ftLen = Kokkos::sqrt(dot3(ft, ft));
74 const float ftMax = mu * Kokkos::fabs(fnSpring);
75 if (ftLen > ftMax) {
76 // sliding: clamp AND rescale the history so the spring alone carries the clamped force
77 // (the LIGGGHTS shear-history rescale).
78 const float s = (ftLen > 1e-20f) ? ftMax / ftLen : 0.0f;
79 ft = scale3(ft, s);
80 if (st > 1e-20f)
81 xi = scale3(add3(ft, scale3(vt, etaT)), -1.0f / st);
82 }
83 return add3(scale3(nhat, fn), ft);
84}
85
88inline void hertzPairForcesKokkos(Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
89 Kokkos::View<const float* [3], CpMem> pos,
90 Kokkos::View<const float* [3], CpMem> vel,
91 Kokkos::View<const float* [3], CpMem> angVel,
92 Kokkos::View<const float*, CpMem> rad,
93 Kokkos::View<const float*, CpMem> invMass,
94 MatIdView matId, PairTableView pairTable, float eGlobal,
95 float muGlobal, Kokkos::View<const float*, CpMem> hertzE,
96 Kokkos::View<const float*, CpMem> hertzNu, float dt,
97 Kokkos::View<float* [3], CpMem> xi,
98 Kokkos::View<float* [3], CpMem> force,
99 Kokkos::View<float* [3], CpMem> torque) {
100 CpExec space;
101 Kokkos::parallel_for(
102 "peclet::dem::hertz_pairs", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
103 KOKKOS_LAMBDA(int idx) {
104 const int i = pairs(idx, 0), j = pairs(idx, 1);
105 const F3 pi = loadF3(pos, i), pj = loadF3(pos, j);
106 const F3 dx = sub3(pi, pj);
107 const float d = Kokkos::sqrt(dot3(dx, dx));
108 const float ri = rad(i), rj = rad(j);
109 const float delta = ri + rj - d;
110 if (delta <= 0.0f || d < 1e-12f) {
111 xi(idx, 0) = xi(idx, 1) = xi(idx, 2) = 0.0f; // contact open: history resets
112 return;
113 }
114 const F3 nhat = scale3(dx, 1.0f / d); // B(j) -> A(i)
115 const F3 rA = scale3(nhat, -(ri - 0.5f * delta));
116 const F3 rB = scale3(nhat, (rj - 0.5f * delta));
117 const F3 vrel = sub3(add3(loadF3(vel, i), cross3v(loadF3(angVel, i), rA)),
118 add3(loadF3(vel, j), cross3v(loadF3(angVel, j), rB)));
119 const int ma = matId(i), mb = matId(j);
120 float e, mu;
121 hertzPairEMu(ma, mb, pairTable, eGlobal, muGlobal, e, mu);
122 const float Ei = hertzE(ma), Ej = hertzE(mb);
123 const float ni = hertzNu(ma), nj = hertzNu(mb);
124 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nj * nj) / Ej);
125 const float gStar =
126 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei + 2.0f * (2.0f - nj) * (1.0f + nj) / Ej);
127 const float rStar = ri * rj / (ri + rj);
128 const float imSum = invMass(i) + invMass(j);
129 const float mStar = (imSum > 0.0f) ? 1.0f / imSum : 0.0f;
130 F3 x{xi(idx, 0), xi(idx, 1), xi(idx, 2)};
131 const F3 f = hertzForce(delta, nhat, vrel, rStar, mStar, eStar, gStar, e, mu, dt, x);
132 xi(idx, 0) = x.x;
133 xi(idx, 1) = x.y;
134 xi(idx, 2) = x.z;
135 const F3 tA = cross3v(rA, f), tB = cross3v(rB, scale3(f, -1.0f));
136 for (int c = 0; c < 3; ++c) {
137 Kokkos::atomic_add(&force(i, c), (&f.x)[c]);
138 Kokkos::atomic_add(&force(j, c), -(&f.x)[c]);
139 Kokkos::atomic_add(&torque(i, c), (&tA.x)[c]);
140 Kokkos::atomic_add(&torque(j, c), (&tB.x)[c]);
141 }
142 });
143 space.fence();
144}
145
148inline void hertzWallForcesKokkos(Kokkos::View<const int*, CpMem> candSlots, int numCand,
149 Kokkos::View<const WallSdf*, CpMem> walls, GridView wallGrid,
150 Kokkos::View<const float* [3], CpMem> pos,
151 Kokkos::View<const float* [3], CpMem> vel,
152 Kokkos::View<const float* [3], CpMem> angVel,
153 Kokkos::View<const float*, CpMem> rad,
154 Kokkos::View<const float*, CpMem> invMass,
155 MatIdView matId, PairTableView pairTable, float eGlobal,
156 float muGlobal, Kokkos::View<const float*, CpMem> hertzE,
157 Kokkos::View<const float*, CpMem> hertzNu, float dt,
158 Kokkos::View<float* [3], CpMem> xiWall, int maxWalls,
159 Kokkos::View<float* [3], CpMem> force,
160 Kokkos::View<float* [3], CpMem> torque,
161 Kokkos::View<const float* [4], CpMem> quat = {},
162 Kokkos::View<const float*, CpMem> scale = {},
163 ScalarI shapeId = {},
164 Kokkos::View<const ShapeDesc*, CpMem> shapes = {},
165 ShellView shell = {}, float globalScale = 1.0f,
166 float contactRadiusFrac = 0.5f, bool hasShapes = false,
167 Kokkos::View<float*, CpMem> snWall = {}) {
168 CpExec space;
169 Kokkos::parallel_for(
170 "peclet::dem::hertz_walls", Kokkos::RangePolicy<CpExec>(space, 0, numCand),
171 KOKKOS_LAMBDA(int ci) {
172 const int enc = candSlots(ci);
173 const int i = enc / 8;
174 const int wi = enc % 8;
175 const F3 p = loadF3(pos, i);
176 const float r = rad(i);
177 const WallSdf w = walls(wi);
178 const int slot = i * maxWalls + wi;
179 const int ma = matId(i);
180 if (hasShapes && shapes(shapeId(i)).numPoints > 0) {
181 // Non-spherical: per-point Hertz springs of the shell against the wall SDF, one
182 // Mindlin patch history per (particle, wall).
183 const ShapeDesc dS = shapes(shapeId(i));
184 const F4 qP = F4{quat(i, 0), quat(i, 1), quat(i, 2), quat(i, 3)};
185 const float effScale = scale(i) * globalScale;
186 const float rc = contactRadiusFrac * dS.params.x * effScale;
187 float e, mu;
188 if (w.materialId >= 0 && pairTable.extent(0) > 0)
189 hertzPairEMu(ma, w.materialId, pairTable, eGlobal, muGlobal, e, mu);
190 else {
191 e = w.restitution;
192 mu = w.friction;
193 }
194 const int mw = (w.materialId >= 0) ? w.materialId : ma;
195 const float Ei = hertzE(ma), Ew = hertzE(mw);
196 const float ni = hertzNu(ma), nw = hertzNu(mw);
197 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nw * nw) / Ew);
198 const float gStar = 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei +
199 2.0f * (2.0f - nw) * (1.0f + nw) / Ew);
200 const float mStar = (invMass(i) > 0.0f) ? 1.0f / invMass(i) : 0.0f;
201 const float bd = hertzBetaD(e);
202 const F3 vP = loadF3(vel, i), wP = loadF3(angVel, i);
203 const float snTotPrev = (snWall.extent(0) > 0) ? snWall(slot) : 0.0f;
204 const float dampC =
205 (mStar > 0.0f) ? -2.0f * 0.9128709f * bd * Kokkos::sqrt(mStar) : 0.0f;
206 float fnSum = 0.0f, snSum = 0.0f, dMax = 0.0f;
207 F3 cSum{0, 0, 0}, nSum{0, 0, 0}, fAcc{0, 0, 0}, tAcc{0, 0, 0};
208 for (int k = 0; k < dS.numPoints; ++k) {
209 const int sIdx = dS.shellOffset + k;
210 const F3 pW = add3(
211 p, rotateVector(qP, scale3(F3{shell(sIdx, 0), shell(sIdx, 1), shell(sIdx, 2)},
212 effScale)));
213 const float sd = sampleWallSdf(pW, w, wallGrid);
214 if (sd >= 0.0f)
215 continue;
216 const float delta = -sd;
217 const float hx = 0.5f / w.invSpacing.x;
218 F3 g{(sampleWallSdf(F3{pW.x + hx, pW.y, pW.z}, w, wallGrid) -
219 sampleWallSdf(F3{pW.x - hx, pW.y, pW.z}, w, wallGrid)),
220 (sampleWallSdf(F3{pW.x, pW.y + hx, pW.z}, w, wallGrid) -
221 sampleWallSdf(F3{pW.x, pW.y - hx, pW.z}, w, wallGrid)),
222 (sampleWallSdf(F3{pW.x, pW.y, pW.z + hx}, w, wallGrid) -
223 sampleWallSdf(F3{pW.x, pW.y, pW.z - hx}, w, wallGrid))};
224 const float gl = len3(g);
225 if (gl < 1e-12f)
226 continue;
227 const F3 nhat = scale3(g, 1.0f / gl);
228 const F3 rA = sub3(pW, p);
229 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(pW, w.center)));
230 const F3 vrelP = sub3(add3(vP, cross3v(wP, rA)), vWall);
231 const float vnP = dot3(vrelP, nhat);
232 const float sq = Kokkos::sqrt(rc * delta);
233 const float fnSpring = (4.0f / 3.0f) * eStar * sq * delta;
234 const float snI = 2.0f * eStar * sq;
235 const float snTot = (snTotPrev > snI) ? snTotPrev : snI;
236 const float etaI = dampC * snI / Kokkos::sqrt(snTot);
237 const F3 f = scale3(nhat, fnSpring - etaI * vnP);
238 fAcc = add3(fAcc, f);
239 tAcc = add3(tAcc, cross3v(rA, f));
240 fnSum += fnSpring;
241 snSum += 2.0f * eStar * sq;
242 cSum = add3(cSum, scale3(pW, fnSpring));
243 nSum = add3(nSum, scale3(nhat, fnSpring));
244 if (delta > dMax)
245 dMax = delta;
246 }
247 if (fnSum <= 0.0f) {
248 xiWall(slot, 0) = xiWall(slot, 1) = xiWall(slot, 2) = 0.0f;
249 if (snWall.extent(0) > 0)
250 snWall(slot) = 0.0f;
251 return;
252 }
253 const F3 cpt = scale3(cSum, 1.0f / fnSum);
254 F3 nhat = nSum;
255 const float nl = len3(nhat);
256 if (nl > 1e-9f)
257 nhat = scale3(nhat, 1.0f / nl);
258 const F3 rA = sub3(cpt, p);
259 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(cpt, w.center)));
260 const F3 vrel = sub3(add3(vP, cross3v(wP, rA)), vWall);
261 if (snWall.extent(0) > 0)
262 snWall(slot) = snSum;
263 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
264 const float st = 8.0f * gStar * Kokkos::sqrt(rc * dMax);
265 const float etaT = -2.0f * 0.9128709f * bd * Kokkos::sqrt(st * mStar);
266 F3 x{xiWall(slot, 0), xiWall(slot, 1), xiWall(slot, 2)};
267 x = add3(x, scale3(vt, dt));
268 x = sub3(x, scale3(nhat, dot3(x, nhat)));
269 F3 ft = add3(scale3(x, -st), scale3(vt, -etaT));
270 const float ftLen = len3(ft);
271 const float ftMax = mu * fnSum;
272 if (ftLen > ftMax) {
273 const float sc2 = (ftLen > 1e-20f) ? ftMax / ftLen : 0.0f;
274 ft = scale3(ft, sc2);
275 if (st > 1e-20f)
276 x = scale3(add3(ft, scale3(vt, etaT)), -1.0f / st);
277 }
278 xiWall(slot, 0) = x.x;
279 xiWall(slot, 1) = x.y;
280 xiWall(slot, 2) = x.z;
281 fAcc = add3(fAcc, ft);
282 tAcc = add3(tAcc, cross3v(rA, ft));
283 for (int c = 0; c < 3; ++c) {
284 Kokkos::atomic_add(&force(i, c), (&fAcc.x)[c]);
285 Kokkos::atomic_add(&torque(i, c), (&tAcc.x)[c]);
286 }
287 return;
288 }
289 {
290 const float s = sampleWallSdf(p, w, wallGrid);
291 const float delta = r - s;
292 if (delta <= 0.0f) {
293 xiWall(slot, 0) = xiWall(slot, 1) = xiWall(slot, 2) = 0.0f;
294 return;
295 }
296 const float hx = 0.5f / w.invSpacing.x;
297 F3 g{(sampleWallSdf(F3{p.x + hx, p.y, p.z}, w, wallGrid) -
298 sampleWallSdf(F3{p.x - hx, p.y, p.z}, w, wallGrid)),
299 (sampleWallSdf(F3{p.x, p.y + hx, p.z}, w, wallGrid) -
300 sampleWallSdf(F3{p.x, p.y - hx, p.z}, w, wallGrid)),
301 (sampleWallSdf(F3{p.x, p.y, p.z + hx}, w, wallGrid) -
302 sampleWallSdf(F3{p.x, p.y, p.z - hx}, w, wallGrid))};
303 const float gl = Kokkos::sqrt(dot3(g, g));
304 if (gl < 1e-12f)
305 return;
306 const F3 nhat = scale3(g, 1.0f / gl); // into the void = push direction on the grain
307 const F3 rA = scale3(nhat, -(r - 0.5f * delta));
308 const F3 cpt = add3(p, rA);
309 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(cpt, w.center)));
310 const F3 vrel = sub3(add3(loadF3(vel, i), cross3v(loadF3(angVel, i), rA)), vWall);
311 float e, mu;
312 if (w.materialId >= 0 && pairTable.extent(0) > 0) {
313 hertzPairEMu(ma, w.materialId, pairTable, eGlobal, muGlobal, e, mu);
314 } else {
315 e = w.restitution;
316 mu = w.friction;
317 }
318 const int mw = (w.materialId >= 0) ? w.materialId : ma;
319 const float Ei = hertzE(ma), Ew = hertzE(mw);
320 const float ni = hertzNu(ma), nw = hertzNu(mw);
321 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nw * nw) / Ew);
322 const float gStar = 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei +
323 2.0f * (2.0f - nw) * (1.0f + nw) / Ew);
324 const float mStar = (invMass(i) > 0.0f) ? 1.0f / invMass(i) : 0.0f;
325 F3 x{xiWall(slot, 0), xiWall(slot, 1), xiWall(slot, 2)};
326 const F3 f = hertzForce(delta, nhat, vrel, r, mStar, eStar, gStar, e, mu, dt, x);
327 xiWall(slot, 0) = x.x;
328 xiWall(slot, 1) = x.y;
329 xiWall(slot, 2) = x.z;
330 const F3 tA = cross3v(rA, f);
331 for (int c = 0; c < 3; ++c) {
332 Kokkos::atomic_add(&force(i, c), (&f.x)[c]);
333 Kokkos::atomic_add(&torque(i, c), (&tA.x)[c]);
334 }
335 }
336 });
337 space.fence();
338}
339
343inline int hertzBuildWallCandidatesKokkos(int numReal, int numWalls,
344 Kokkos::View<const WallSdf*, CpMem> walls,
345 GridView wallGrid,
346 Kokkos::View<const float* [3], CpMem> pos,
347 Kokkos::View<const float*, CpMem> rad, float skin,
348 Kokkos::View<int*, CpMem> outSlots,
349 Kokkos::View<int, CpMem> outCount) {
350 CpExec space;
351 Kokkos::deep_copy(space, outCount, 0);
352 const int cap = static_cast<int>(outSlots.extent(0));
353 Kokkos::parallel_for(
354 "peclet::dem::hertz_wall_cand", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
355 KOKKOS_LAMBDA(int i) {
356 const F3 p{pos(i, 0), pos(i, 1), pos(i, 2)};
357 for (int wi = 0; wi < numWalls; ++wi) {
358 const float s = sampleWallSdf(p, walls(wi), wallGrid);
359 if (s < rad(i) + skin) {
360 const int at = Kokkos::atomic_fetch_add(&outCount(), 1);
361 if (at < cap)
362 outSlots(at) = i * 8 + wi;
363 }
364 }
365 });
366 space.fence();
367 int n = 0;
368 Kokkos::deep_copy(n, outCount);
369 return n < cap ? n : cap;
370}
371
374inline void hertzIntegrateKokkos(int numReal, Kokkos::View<float* [3], CpMem> force,
375 Kokkos::View<float* [3], CpMem> torque,
376 Kokkos::View<const float*, CpMem> invMass,
377 Kokkos::View<const float* [3], CpMem> invInertia, F3 gravity,
378 float dt, Kokkos::View<float* [3], CpMem> vel,
379 Kokkos::View<float* [3], CpMem> angVel,
380 Kokkos::View<float* [3], CpMem> pos,
381 Kokkos::View<float* [4], CpMem> quat = {},
382 bool integrateOrientation = false) {
383 CpExec space;
384 Kokkos::parallel_for(
385 "peclet::dem::hertz_integrate", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
386 KOKKOS_LAMBDA(int i) {
387 const float im = invMass(i);
388 if (im > 0.0f) {
389 const F3 iI{invInertia(i, 0), invInertia(i, 1), invInertia(i, 2)};
390 F3 dw;
391 if (iI.x == iI.y && iI.y == iI.z) { // isotropic: no frame rotation needed
392 dw = F3{torque(i, 0) * iI.x, torque(i, 1) * iI.x, torque(i, 2) * iI.x};
393 } else { // world torque -> principal frame -> back
394 const F4 q = loadF4(quat, i);
395 const F3 tl = invRotateVector(q, F3{torque(i, 0), torque(i, 1), torque(i, 2)});
396 dw = rotateVector(q, F3{tl.x * iI.x, tl.y * iI.y, tl.z * iI.z});
397 }
398 for (int c = 0; c < 3; ++c) {
399 vel(i, c) += (force(i, c) * im + (&gravity.x)[c]) * dt;
400 angVel(i, c) += (&dw.x)[c] * dt;
401 pos(i, c) += vel(i, c) * dt;
402 }
403 if (integrateOrientation) {
404 F4 q = loadF4(quat, i);
405 const F3 w{angVel(i, 0), angVel(i, 1), angVel(i, 2)};
406 // q += dt/2 * (0, w) x q, renormalized
407 F4 dq{0.5f * dt * (w.x * q.w + w.y * q.z - w.z * q.y),
408 0.5f * dt * (w.y * q.w + w.z * q.x - w.x * q.z),
409 0.5f * dt * (w.z * q.w + w.x * q.y - w.y * q.x),
410 0.5f * dt * (-w.x * q.x - w.y * q.y - w.z * q.z)};
411 q = F4{q.x + dq.x, q.y + dq.y, q.z + dq.z, q.w + dq.w};
412 const float qn = Kokkos::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
413 if (qn > 1e-12f) {
414 quat(i, 0) = q.x / qn;
415 quat(i, 1) = q.y / qn;
416 quat(i, 2) = q.z / qn;
417 quat(i, 3) = q.w / qn;
418 }
419 }
420 }
421 force(i, 0) = force(i, 1) = force(i, 2) = 0.0f;
422 torque(i, 0) = torque(i, 1) = torque(i, 2) = 0.0f;
423 });
424 space.fence();
425}
426
429inline float hertzMaxDisp2Kokkos(int numReal, Kokkos::View<const float* [3], CpMem> pos,
430 Kokkos::View<const float* [3], CpMem> refPos) {
431 CpExec space;
432 float m = 0.0f;
433 Kokkos::parallel_reduce(
434 "peclet::dem::hertz_maxdisp", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
435 KOKKOS_LAMBDA(int i, float& acc) {
436 const F3 d = sub3(loadF3(pos, i), loadF3(refPos, i));
437 const float dd = dot3(d, d);
438 if (dd > acc)
439 acc = dd;
440 },
441 Kokkos::Max<float>(m));
442 space.fence();
443 return m;
444}
445
446
455 Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
456 Kokkos::View<const float* [3], CpMem> pos, Kokkos::View<const float* [4], CpMem> quat,
457 Kokkos::View<const float* [3], CpMem> vel, Kokkos::View<const float* [3], CpMem> angVel,
458 Kokkos::View<const float*, CpMem> scale, ScalarI shapeId,
459 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell, GridView sdfGrid,
460 float globalScale, float contactRadiusFrac, Kokkos::View<const float*, CpMem> invMass,
461 MatIdView matId, PairTableView pairTable, float eGlobal, float muGlobal,
462 Kokkos::View<const float*, CpMem> hertzE, Kokkos::View<const float*, CpMem> hertzNu, float dt,
463 Kokkos::View<float* [3], CpMem> xi, Kokkos::View<float*, CpMem> snPrev,
464 Kokkos::View<float* [3], CpMem> force, Kokkos::View<float* [3], CpMem> torque) {
465 CpExec space;
466 Kokkos::parallel_for(
467 "peclet::dem::hertz_shape_pairs", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
468 KOKKOS_LAMBDA(int idx) {
469 const int idA = pairs(idx, 0), idB = pairs(idx, 1);
470 const ShapeDesc dA = shapes(shapeId(idA));
471 const ShapeDesc dB = shapes(shapeId(idB));
472 const F3 posA = loadF3(pos, idA), posB = loadF3(pos, idB);
473 const F4 qA = loadF4(quat, idA), qB = loadF4(quat, idB);
474 const float effScaleA = scale(idA) * globalScale, effScaleB = scale(idB) * globalScale;
475 const int countA = dA.numPoints;
476 const bool sphereA = (dA.type == SPHERE);
477 const int iter = (countA > 0) ? countA : 1;
478
479 const int ma = matId(idA), mb = matId(idB);
480 float e, mu;
481 hertzPairEMu(ma, mb, pairTable, eGlobal, muGlobal, e, mu);
482 const float Ei = hertzE(ma), Ej = hertzE(mb);
483 const float ni = hertzNu(ma), nj = hertzNu(mb);
484 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nj * nj) / Ej);
485 const float gStar =
486 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei + 2.0f * (2.0f - nj) * (1.0f + nj) / Ej);
487 // contact radii: spheres exact, shapes = frac * bounding radius
488 // params.x = sphere radius or (grid SDF) canonical bounding radius
489 const float rcA = (sphereA ? 1.0f : contactRadiusFrac) * dA.params.x * effScaleA;
490 const float rcB =
491 ((dB.type == SPHERE) ? 1.0f : contactRadiusFrac) * dB.params.x * effScaleB;
492 const float rStar = rcA * rcB / (rcA + rcB);
493 const float imSum = invMass(idA) + invMass(idB);
494 const float mStar = (imSum > 0.0f) ? 1.0f / imSum : 0.0f;
495 const float bd = hertzBetaD(e);
496
497 const F3 vA = loadF3(vel, idA), wA = loadF3(angVel, idA);
498 const F3 vB = loadF3(vel, idB), wB = loadF3(angVel, idB);
499
500 float fnSum = 0.0f, snSum = 0.0f;
501 F3 cSum{0, 0, 0}, nSum{0, 0, 0};
502 float dMax = 0.0f;
503 F3 fAcc{0, 0, 0}, tAccA{0, 0, 0}, tAccB{0, 0, 0};
504 // Per-point dashpots weighted by stiffness against LAST step's patch stiffness sum:
505 // eta_i = 2 beta sn_i sqrt(m*/snTot). Sums to the exact two-body damping AND gives every
506 // patch mode (incl. face rocking) a uniform damping ratio; the patch-level dashpot alone
507 // left rocking modes undamped (measured: cubes tumbling uphill on an incline).
508 const float snTotPrev = snPrev(idx);
509 const float dampC =
510 (mStar > 0.0f) ? -2.0f * 0.9128709f * bd * Kokkos::sqrt(mStar) : 0.0f;
511
512 for (int k = 0; k < iter; ++k) {
513 F3 pLocalA{0, 0, 0};
514 float pointRadius = 0.0f;
515 if (countA > 0) {
516 const int sIdx = dA.shellOffset + k;
517 pLocalA = F3{shell(sIdx, 0), shell(sIdx, 1), shell(sIdx, 2)};
518 } else if (sphereA) {
519 pointRadius = dA.params.x * effScaleA;
520 }
521 const F3 pWorld = add3(posA, rotateVector(qA, scale3(pLocalA, effScaleA)));
522 const F3 pCanB = scale3(invRotateVector(qB, sub3(pWorld, posB)), 1.0f / effScaleB);
523 const float dist = sdfEvalShape(pCanB, dB, sdfGrid) * effScaleB;
524 const float effDist = dist - pointRadius;
525 if (effDist >= 0.0f)
526 continue;
527 const float delta = -effDist;
528 const float eps = 1e-4f;
529 F3 nLoc{sdfEvalShape(F3{pCanB.x + eps, pCanB.y, pCanB.z}, dB, sdfGrid) -
530 sdfEvalShape(F3{pCanB.x - eps, pCanB.y, pCanB.z}, dB, sdfGrid),
531 sdfEvalShape(F3{pCanB.x, pCanB.y + eps, pCanB.z}, dB, sdfGrid) -
532 sdfEvalShape(F3{pCanB.x, pCanB.y - eps, pCanB.z}, dB, sdfGrid),
533 sdfEvalShape(F3{pCanB.x, pCanB.y, pCanB.z + eps}, dB, sdfGrid) -
534 sdfEvalShape(F3{pCanB.x, pCanB.y, pCanB.z - eps}, dB, sdfGrid)};
535 const float len = len3(nLoc);
536 nLoc = (len > 1e-9f) ? scale3(nLoc, 1.0f / len) : F3{0, 1, 0};
537 const F3 nW = rotateVector(qB, nLoc); // out of B = push direction on A
538 const F3 pSurfA = sub3(pWorld, scale3(nW, pointRadius));
539 const F3 rA = sub3(pSurfA, posA);
540 const F3 rB = sub3(add3(pSurfA, scale3(nW, delta)), posB);
541
542 const F3 vrelP = sub3(add3(vA, cross3v(wA, rA)), add3(vB, cross3v(wB, rB)));
543 const float vnP = dot3(vrelP, nW);
544 const float sq = Kokkos::sqrt(rStar * delta);
545 const float fnSpring = (4.0f / 3.0f) * eStar * sq * delta;
546 const float snI = 2.0f * eStar * sq;
547 const float snTot = (snTotPrev > snI) ? snTotPrev : snI; // first-contact guard
548 const float etaI = dampC * snI / Kokkos::sqrt(snTot);
549 const F3 f = scale3(nW, fnSpring - etaI * vnP);
550 fAcc = add3(fAcc, f);
551 tAccA = add3(tAccA, cross3v(rA, f));
552 tAccB = add3(tAccB, cross3v(rB, scale3(f, -1.0f)));
553 fnSum += fnSpring;
554 snSum += 2.0f * eStar * sq; // patch tangent stiffness = sum of point stiffnesses
555 cSum = add3(cSum, scale3(pSurfA, fnSpring));
556 nSum = add3(nSum, scale3(nW, fnSpring));
557 if (delta > dMax)
558 dMax = delta;
559 }
560
561 if (fnSum <= 0.0f) {
562 xi(idx, 0) = xi(idx, 1) = xi(idx, 2) = 0.0f;
563 snPrev(idx) = 0.0f;
564 return;
565 }
566 // Patch level: ONE normal dashpot and ONE Mindlin tangential spring at the
567 // load-weighted centroid.
568 const F3 cpt = scale3(cSum, 1.0f / fnSum);
569 F3 nhat = nSum;
570 const float nl = len3(nhat);
571 if (nl > 1e-9f)
572 nhat = scale3(nhat, 1.0f / nl);
573 const F3 rA = sub3(cpt, posA), rB = sub3(cpt, posB);
574 const F3 vrel = sub3(add3(vA, cross3v(wA, rA)), add3(vB, cross3v(wB, rB)));
575 snPrev(idx) = snSum;
576 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
577 const float st = 8.0f * gStar * Kokkos::sqrt(rStar * dMax);
578 const float etaT = -2.0f * 0.9128709f * bd * Kokkos::sqrt(st * mStar);
579 F3 x{xi(idx, 0), xi(idx, 1), xi(idx, 2)};
580 x = add3(x, scale3(vt, dt));
581 x = sub3(x, scale3(nhat, dot3(x, nhat)));
582 F3 ft = add3(scale3(x, -st), scale3(vt, -etaT));
583 const float ftLen = len3(ft);
584 const float ftMax = mu * fnSum;
585 if (ftLen > ftMax) {
586 const float sc = (ftLen > 1e-20f) ? ftMax / ftLen : 0.0f;
587 ft = scale3(ft, sc);
588 if (st > 1e-20f)
589 x = scale3(add3(ft, scale3(vt, etaT)), -1.0f / st);
590 }
591 xi(idx, 0) = x.x;
592 xi(idx, 1) = x.y;
593 xi(idx, 2) = x.z;
594 fAcc = add3(fAcc, ft);
595 tAccA = add3(tAccA, cross3v(rA, ft));
596 tAccB = add3(tAccB, cross3v(rB, scale3(ft, -1.0f)));
597
598 for (int c = 0; c < 3; ++c) {
599 Kokkos::atomic_add(&force(idA, c), (&fAcc.x)[c]);
600 Kokkos::atomic_add(&force(idB, c), -(&fAcc.x)[c]);
601 Kokkos::atomic_add(&torque(idA, c), (&tAccA.x)[c]);
602 Kokkos::atomic_add(&torque(idB, c), (&tAccB.x)[c]);
603 }
604 });
605 space.fence();
606}
607
608} // namespace peclet::dem
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
float hertzBetaD(float e)
Restitution -> damping ratio beta_d = ln e / sqrt(ln^2 e + pi^2) (e in (0, 1]; e <= 0 -> -1,...
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 ...
Kokkos::View< const float *[3], CpMem > ShellView
F3 cross3v(F3 a, F3 b)
Kokkos::View< const float *, CpMem > GridView
Kokkos::View< const unsigned char *, CpMem > MatIdView
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...
float sampleWallSdf(F3 p, const WallSdf &w, GridView grid)
Trilinearly sample a static world-space wall SDF at world point p.
Kokkos::View< const float *, CpMem > PairTableView
F3 invRotateVector(F4 q, F3 v)
Kokkos::View< const int *, CpMem > ScalarI
F3 loadF3(PosView v, int i)
float sdfEvalShape(F3 p, const ShapeDesc &d, GridView grid)
Canonical-space SDF of a shape: analytic dispatch, or a trilinear grid sample for an imported grid SD...
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.
F3 rotateVector(F4 q, F3 v)
void hertzPairEMu(int matA, int matB, PairTableView pairTable, float eGlobal, float muGlobal, float &e, float &mu)
Pairwise (e, mu) lookup shared with the narrowphase convention: pair table if present,...
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
F3 hertzForce(float delta, F3 nhat, F3 vrel, float rStar, float mStar, float eStar, float gStar, float e, float mu, float dt, F3 &xi)
One Hertz–Mindlin force evaluation for a sphere-sphere or sphere-wall contact.
CpExec::memory_space CpMem
F4 loadF4(QuatView v, int i)
F3 scale3(F3 a, float s)
float len3(F3 v)
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.
F3 add3(F3 a, F3 b)
constexpr int kMaxMaterials
Pair-material lookup: flat [K*K*2] table, entry ((a*K + b)*2) = restitution, +1 = friction.
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) narrow-phase: SDF point-shell collision + boundary planes.
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
Portable mirror of ShapeDescriptor (analytic fields + a flat-array point shell).
static std::array< double, 3 > force(const Vec< 3 > &xi, std::int64_t selfId, std::vector< std::pair< std::int64_t, Vec< 3 > > > &nbr)