peclet-dem 1.1.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, float eGlobal,
28 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
91KOKKOS_INLINE_FUNCTION float hertzSphereRadius(int i, Kokkos::View<const float*, CpMem> rad,
92 Kokkos::View<const float*, CpMem> scale,
93 ScalarI shapeId,
94 Kokkos::View<const ShapeDesc*, CpMem> shapes,
95 float globalScale) {
96 if (shapes.extent(0) > 0 && scale.extent(0) > 0) {
97 const ShapeDesc d = shapes(shapeId(i));
98 if (d.type == SPHERE && d.params.x > 0.0f)
99 return scale(i) * globalScale * d.params.x;
100 }
101 return rad(i);
102}
103
107 Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
108 Kokkos::View<const float* [3], CpMem> pos, Kokkos::View<const float* [3], CpMem> vel,
109 Kokkos::View<const float* [3], CpMem> angVel, Kokkos::View<const float*, CpMem> rad,
110 Kokkos::View<const float*, CpMem> invMass, MatIdView matId, PairTableView pairTable,
111 float eGlobal, float muGlobal, Kokkos::View<const float*, CpMem> hertzE,
112 Kokkos::View<const float*, CpMem> hertzNu, float dt, Kokkos::View<float* [3], CpMem> xi,
113 Kokkos::View<float* [3], CpMem> force, Kokkos::View<float* [3], CpMem> torque,
114 Kokkos::View<const float*, CpMem> scale = {}, ScalarI shapeId = {},
115 Kokkos::View<const ShapeDesc*, CpMem> shapes = {}, float globalScale = 1.0f) {
116 CpExec space;
117 Kokkos::parallel_for(
118 "peclet::dem::hertz_pairs", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
119 KOKKOS_LAMBDA(int idx) {
120 const int i = pairs(idx, 0), j = pairs(idx, 1);
121 const F3 pi = loadF3(pos, i), pj = loadF3(pos, j);
122 const F3 dx = sub3(pi, pj);
123 const float d = Kokkos::sqrt(dot3(dx, dx));
124 const float ri = hertzSphereRadius(i, rad, scale, shapeId, shapes, globalScale),
125 rj = hertzSphereRadius(j, rad, scale, shapeId, shapes, globalScale);
126 const float delta = ri + rj - d;
127 if (delta <= 0.0f || d < 1e-12f) {
128 xi(idx, 0) = xi(idx, 1) = xi(idx, 2) = 0.0f; // contact open: history resets
129 return;
130 }
131 const F3 nhat = scale3(dx, 1.0f / d); // B(j) -> A(i)
132 const F3 rA = scale3(nhat, -(ri - 0.5f * delta));
133 const F3 rB = scale3(nhat, (rj - 0.5f * delta));
134 const F3 vrel = sub3(add3(loadF3(vel, i), cross3v(loadF3(angVel, i), rA)),
135 add3(loadF3(vel, j), cross3v(loadF3(angVel, j), rB)));
136 const int ma = matId(i), mb = matId(j);
137 float e, mu;
138 hertzPairEMu(ma, mb, pairTable, eGlobal, muGlobal, e, mu);
139 const float Ei = hertzE(ma), Ej = hertzE(mb);
140 const float ni = hertzNu(ma), nj = hertzNu(mb);
141 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nj * nj) / Ej);
142 const float gStar =
143 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei + 2.0f * (2.0f - nj) * (1.0f + nj) / Ej);
144 const float rStar = ri * rj / (ri + rj);
145 const float imSum = invMass(i) + invMass(j);
146 const float mStar = (imSum > 0.0f) ? 1.0f / imSum : 0.0f;
147 F3 x{xi(idx, 0), xi(idx, 1), xi(idx, 2)};
148 const F3 f = hertzForce(delta, nhat, vrel, rStar, mStar, eStar, gStar, e, mu, dt, x);
149 xi(idx, 0) = x.x;
150 xi(idx, 1) = x.y;
151 xi(idx, 2) = x.z;
152 const F3 tA = cross3v(rA, f), tB = cross3v(rB, scale3(f, -1.0f));
153 for (int c = 0; c < 3; ++c) {
154 Kokkos::atomic_add(&force(i, c), (&f.x)[c]);
155 Kokkos::atomic_add(&force(j, c), -(&f.x)[c]);
156 Kokkos::atomic_add(&torque(i, c), (&tA.x)[c]);
157 Kokkos::atomic_add(&torque(j, c), (&tB.x)[c]);
158 }
159 });
160 space.fence();
161}
162
166 Kokkos::View<const int*, CpMem> candSlots, int numCand,
167 Kokkos::View<const WallSdf*, CpMem> walls, GridView wallGrid,
168 Kokkos::View<const float* [3], CpMem> pos, Kokkos::View<const float* [3], CpMem> vel,
169 Kokkos::View<const float* [3], CpMem> angVel, Kokkos::View<const float*, CpMem> rad,
170 Kokkos::View<const float*, CpMem> invMass, MatIdView matId, PairTableView pairTable,
171 float eGlobal, float muGlobal, Kokkos::View<const float*, CpMem> hertzE,
172 Kokkos::View<const float*, CpMem> hertzNu, float dt, Kokkos::View<float* [3], CpMem> xiWall,
173 int maxWalls, Kokkos::View<float* [3], CpMem> force, Kokkos::View<float* [3], CpMem> torque,
174 Kokkos::View<const float* [4], CpMem> quat = {}, Kokkos::View<const float*, CpMem> scale = {},
175 ScalarI shapeId = {}, Kokkos::View<const ShapeDesc*, CpMem> shapes = {}, ShellView shell = {},
176 float globalScale = 1.0f, float contactRadiusFrac = 0.5f, bool hasShapes = false,
177 Kokkos::View<float*, CpMem> snWall = {}) {
178 CpExec space;
179 Kokkos::parallel_for(
180 "peclet::dem::hertz_walls", Kokkos::RangePolicy<CpExec>(space, 0, numCand),
181 KOKKOS_LAMBDA(int ci) {
182 const int enc = candSlots(ci);
183 const int i = enc / 8;
184 const int wi = enc % 8;
185 const F3 p = loadF3(pos, i);
186 const float r = hertzSphereRadius(i, rad, scale, shapeId, shapes, globalScale);
187 const WallSdf w = walls(wi);
188 const int slot = i * maxWalls + wi;
189 const int ma = matId(i);
190 if (hasShapes && shapes(shapeId(i)).numPoints > 0) {
191 // Non-spherical: per-point Hertz springs of the shell against the wall SDF, one
192 // Mindlin patch history per (particle, wall).
193 const ShapeDesc dS = shapes(shapeId(i));
194 const F4 qP = F4{quat(i, 0), quat(i, 1), quat(i, 2), quat(i, 3)};
195 const float effScale = scale(i) * globalScale;
196 const float rc = contactRadiusFrac * dS.params.x * effScale;
197 float e, mu;
198 if (w.materialId >= 0 && pairTable.extent(0) > 0)
199 hertzPairEMu(ma, w.materialId, pairTable, eGlobal, muGlobal, e, mu);
200 else {
201 e = w.restitution;
202 mu = w.friction;
203 }
204 const int mw = (w.materialId >= 0) ? w.materialId : ma;
205 const float Ei = hertzE(ma), Ew = hertzE(mw);
206 const float ni = hertzNu(ma), nw = hertzNu(mw);
207 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nw * nw) / Ew);
208 const float gStar = 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei +
209 2.0f * (2.0f - nw) * (1.0f + nw) / Ew);
210 const float mStar = (invMass(i) > 0.0f) ? 1.0f / invMass(i) : 0.0f;
211 const float bd = hertzBetaD(e);
212 const F3 vP = loadF3(vel, i), wP = loadF3(angVel, i);
213 const float snTotPrev = (snWall.extent(0) > 0) ? snWall(slot) : 0.0f;
214 const float dampC = (mStar > 0.0f) ? -2.0f * 0.9128709f * bd * Kokkos::sqrt(mStar) : 0.0f;
215 float fnSum = 0.0f, snSum = 0.0f, dMax = 0.0f;
216 F3 cSum{0, 0, 0}, nSum{0, 0, 0}, fAcc{0, 0, 0}, tAcc{0, 0, 0};
217 for (int k = 0; k < dS.numPoints; ++k) {
218 const int sIdx = dS.shellOffset + k;
219 const F3 pW = add3(
220 p, rotateVector(
221 qP, scale3(F3{shell(sIdx, 0), shell(sIdx, 1), shell(sIdx, 2)}, effScale)));
222 const float sd = sampleWallSdf(pW, w, wallGrid);
223 if (sd >= 0.0f)
224 continue;
225 const float delta = -sd;
226 const float hx = 0.5f / w.grid.invSpacing.x;
227 F3 g{(sampleWallSdf(F3{pW.x + hx, pW.y, pW.z}, w, wallGrid) -
228 sampleWallSdf(F3{pW.x - hx, pW.y, pW.z}, w, wallGrid)),
229 (sampleWallSdf(F3{pW.x, pW.y + hx, pW.z}, w, wallGrid) -
230 sampleWallSdf(F3{pW.x, pW.y - hx, pW.z}, w, wallGrid)),
231 (sampleWallSdf(F3{pW.x, pW.y, pW.z + hx}, w, wallGrid) -
232 sampleWallSdf(F3{pW.x, pW.y, pW.z - hx}, w, wallGrid))};
233 const float gl = len3(g);
234 if (gl < 1e-12f)
235 continue;
236 const F3 nhat = scale3(g, 1.0f / gl);
237 const F3 rA = sub3(pW, p);
238 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(pW, w.center)));
239 const F3 vrelP = sub3(add3(vP, cross3v(wP, rA)), vWall);
240 const float vnP = dot3(vrelP, nhat);
241 const float sq = Kokkos::sqrt(rc * delta);
242 const float fnSpring = (4.0f / 3.0f) * eStar * sq * delta;
243 const float snI = 2.0f * eStar * sq;
244 const float snTot = (snTotPrev > snI) ? snTotPrev : snI;
245 const float etaI = dampC * snI / Kokkos::sqrt(snTot);
246 const F3 f = scale3(nhat, fnSpring - etaI * vnP);
247 fAcc = add3(fAcc, f);
248 tAcc = add3(tAcc, cross3v(rA, f));
249 fnSum += fnSpring;
250 snSum += 2.0f * eStar * sq;
251 cSum = add3(cSum, scale3(pW, fnSpring));
252 nSum = add3(nSum, scale3(nhat, fnSpring));
253 if (delta > dMax)
254 dMax = delta;
255 }
256 if (fnSum <= 0.0f) {
257 xiWall(slot, 0) = xiWall(slot, 1) = xiWall(slot, 2) = 0.0f;
258 if (snWall.extent(0) > 0)
259 snWall(slot) = 0.0f;
260 return;
261 }
262 const F3 cpt = scale3(cSum, 1.0f / fnSum);
263 F3 nhat = nSum;
264 const float nl = len3(nhat);
265 if (nl > 1e-9f)
266 nhat = scale3(nhat, 1.0f / nl);
267 const F3 rA = sub3(cpt, p);
268 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(cpt, w.center)));
269 const F3 vrel = sub3(add3(vP, cross3v(wP, rA)), vWall);
270 if (snWall.extent(0) > 0)
271 snWall(slot) = snSum;
272 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
273 const float st = 8.0f * gStar * Kokkos::sqrt(rc * dMax);
274 const float etaT = -2.0f * 0.9128709f * bd * Kokkos::sqrt(st * mStar);
275 F3 x{xiWall(slot, 0), xiWall(slot, 1), xiWall(slot, 2)};
276 x = add3(x, scale3(vt, dt));
277 x = sub3(x, scale3(nhat, dot3(x, nhat)));
278 F3 ft = add3(scale3(x, -st), scale3(vt, -etaT));
279 const float ftLen = len3(ft);
280 const float ftMax = mu * fnSum;
281 if (ftLen > ftMax) {
282 const float sc2 = (ftLen > 1e-20f) ? ftMax / ftLen : 0.0f;
283 ft = scale3(ft, sc2);
284 if (st > 1e-20f)
285 x = scale3(add3(ft, scale3(vt, etaT)), -1.0f / st);
286 }
287 xiWall(slot, 0) = x.x;
288 xiWall(slot, 1) = x.y;
289 xiWall(slot, 2) = x.z;
290 fAcc = add3(fAcc, ft);
291 tAcc = add3(tAcc, cross3v(rA, ft));
292 for (int c = 0; c < 3; ++c) {
293 Kokkos::atomic_add(&force(i, c), (&fAcc.x)[c]);
294 Kokkos::atomic_add(&torque(i, c), (&tAcc.x)[c]);
295 }
296 return;
297 }
298 {
299 const float s = sampleWallSdf(p, w, wallGrid);
300 const float delta = r - s;
301 if (delta <= 0.0f) {
302 xiWall(slot, 0) = xiWall(slot, 1) = xiWall(slot, 2) = 0.0f;
303 return;
304 }
305 const float hx = 0.5f / w.grid.invSpacing.x;
306 F3 g{(sampleWallSdf(F3{p.x + hx, p.y, p.z}, w, wallGrid) -
307 sampleWallSdf(F3{p.x - hx, p.y, p.z}, w, wallGrid)),
308 (sampleWallSdf(F3{p.x, p.y + hx, p.z}, w, wallGrid) -
309 sampleWallSdf(F3{p.x, p.y - hx, p.z}, w, wallGrid)),
310 (sampleWallSdf(F3{p.x, p.y, p.z + hx}, w, wallGrid) -
311 sampleWallSdf(F3{p.x, p.y, p.z - hx}, w, wallGrid))};
312 const float gl = Kokkos::sqrt(dot3(g, g));
313 if (gl < 1e-12f)
314 return;
315 const F3 nhat = scale3(g, 1.0f / gl); // into the void = push direction on the grain
316 const F3 rA = scale3(nhat, -(r - 0.5f * delta));
317 const F3 cpt = add3(p, rA);
318 const F3 vWall = add3(w.linVel, cross3v(w.angVel, sub3(cpt, w.center)));
319 const F3 vrel = sub3(add3(loadF3(vel, i), cross3v(loadF3(angVel, i), rA)), vWall);
320 float e, mu;
321 if (w.materialId >= 0 && pairTable.extent(0) > 0) {
322 hertzPairEMu(ma, w.materialId, pairTable, eGlobal, muGlobal, e, mu);
323 } else {
324 e = w.restitution;
325 mu = w.friction;
326 }
327 const int mw = (w.materialId >= 0) ? w.materialId : ma;
328 const float Ei = hertzE(ma), Ew = hertzE(mw);
329 const float ni = hertzNu(ma), nw = hertzNu(mw);
330 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nw * nw) / Ew);
331 const float gStar = 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei +
332 2.0f * (2.0f - nw) * (1.0f + nw) / Ew);
333 const float mStar = (invMass(i) > 0.0f) ? 1.0f / invMass(i) : 0.0f;
334 F3 x{xiWall(slot, 0), xiWall(slot, 1), xiWall(slot, 2)};
335 const F3 f = hertzForce(delta, nhat, vrel, r, mStar, eStar, gStar, e, mu, dt, x);
336 xiWall(slot, 0) = x.x;
337 xiWall(slot, 1) = x.y;
338 xiWall(slot, 2) = x.z;
339 const F3 tA = cross3v(rA, f);
340 for (int c = 0; c < 3; ++c) {
341 Kokkos::atomic_add(&force(i, c), (&f.x)[c]);
342 Kokkos::atomic_add(&torque(i, c), (&tA.x)[c]);
343 }
344 }
345 });
346 space.fence();
347}
348
353 int numReal, int numWalls, Kokkos::View<const WallSdf*, CpMem> walls, GridView wallGrid,
354 Kokkos::View<const float* [3], CpMem> pos, Kokkos::View<const float*, CpMem> rad, float skin,
355 Kokkos::View<int*, CpMem> outSlots, Kokkos::View<int, CpMem> outCount) {
356 CpExec space;
357 Kokkos::deep_copy(space, outCount, 0);
358 const int cap = static_cast<int>(outSlots.extent(0));
359 Kokkos::parallel_for(
360 "peclet::dem::hertz_wall_cand", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
361 KOKKOS_LAMBDA(int i) {
362 const F3 p{pos(i, 0), pos(i, 1), pos(i, 2)};
363 for (int wi = 0; wi < numWalls; ++wi) {
364 const float s = sampleWallSdf(p, walls(wi), wallGrid);
365 if (s < rad(i) + skin) {
366 const int at = Kokkos::atomic_fetch_add(&outCount(), 1);
367 if (at < cap)
368 outSlots(at) = i * 8 + wi;
369 }
370 }
371 });
372 space.fence();
373 int n = 0;
374 Kokkos::deep_copy(n, outCount);
375 return n < cap ? n : cap;
376}
377
381 int numReal, Kokkos::View<float* [3], CpMem> force, Kokkos::View<float* [3], CpMem> torque,
382 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<const float* [3], CpMem> invInertia,
383 F3 gravity, float dt, Kokkos::View<float* [3], CpMem> vel,
384 Kokkos::View<float* [3], CpMem> angVel, Kokkos::View<float* [3], CpMem> pos,
385 Kokkos::View<float* [4], CpMem> quat = {}, bool integrateOrientation = false) {
386 CpExec space;
387 Kokkos::parallel_for(
388 "peclet::dem::hertz_integrate", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
389 KOKKOS_LAMBDA(int i) {
390 const float im = invMass(i);
391 if (im > 0.0f) {
392 const F3 iI{invInertia(i, 0), invInertia(i, 1), invInertia(i, 2)};
393 F3 dw;
394 if (iI.x == iI.y && iI.y == iI.z) { // isotropic: no frame rotation needed
395 dw = F3{torque(i, 0) * iI.x, torque(i, 1) * iI.x, torque(i, 2) * iI.x};
396 } else { // world torque -> principal frame -> back
397 const F4 q = loadF4(quat, i);
398 const F3 tl = invRotateVector(q, F3{torque(i, 0), torque(i, 1), torque(i, 2)});
399 dw = rotateVector(q, F3{tl.x * iI.x, tl.y * iI.y, tl.z * iI.z});
400 }
401 for (int c = 0; c < 3; ++c) {
402 vel(i, c) += (force(i, c) * im + (&gravity.x)[c]) * dt;
403 angVel(i, c) += (&dw.x)[c] * dt;
404 pos(i, c) += vel(i, c) * dt;
405 }
406 if (integrateOrientation) {
407 F4 q = loadF4(quat, i);
408 const F3 w{angVel(i, 0), angVel(i, 1), angVel(i, 2)};
409 // q += dt/2 * (0, w) x q, renormalized
410 F4 dq{0.5f * dt * (w.x * q.w + w.y * q.z - w.z * q.y),
411 0.5f * dt * (w.y * q.w + w.z * q.x - w.x * q.z),
412 0.5f * dt * (w.z * q.w + w.x * q.y - w.y * q.x),
413 0.5f * dt * (-w.x * q.x - w.y * q.y - w.z * q.z)};
414 q = F4{q.x + dq.x, q.y + dq.y, q.z + dq.z, q.w + dq.w};
415 const float qn = Kokkos::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
416 if (qn > 1e-12f) {
417 quat(i, 0) = q.x / qn;
418 quat(i, 1) = q.y / qn;
419 quat(i, 2) = q.z / qn;
420 quat(i, 3) = q.w / qn;
421 }
422 }
423 }
424 force(i, 0) = force(i, 1) = force(i, 2) = 0.0f;
425 torque(i, 0) = torque(i, 1) = torque(i, 2) = 0.0f;
426 });
427 space.fence();
428}
429
432inline float hertzMaxDisp2Kokkos(int numReal, Kokkos::View<const float* [3], CpMem> pos,
433 Kokkos::View<const float* [3], CpMem> refPos) {
434 CpExec space;
435 float m = 0.0f;
436 Kokkos::parallel_reduce(
437 "peclet::dem::hertz_maxdisp", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
438 KOKKOS_LAMBDA(int i, float& acc) {
439 const F3 d = sub3(loadF3(pos, i), loadF3(refPos, i));
440 const float dd = dot3(d, d);
441 if (dd > acc)
442 acc = dd;
443 },
444 Kokkos::Max<float>(m));
445 space.fence();
446 return m;
447}
448
457 Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
458 Kokkos::View<const float* [3], CpMem> pos, Kokkos::View<const float* [4], CpMem> quat,
459 Kokkos::View<const float* [3], CpMem> vel, Kokkos::View<const float* [3], CpMem> angVel,
460 Kokkos::View<const float*, CpMem> scale, ScalarI shapeId,
461 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell, GridView sdfGrid,
462 float globalScale, float contactRadiusFrac, Kokkos::View<const float*, CpMem> invMass,
463 MatIdView matId, PairTableView pairTable, float eGlobal, float muGlobal,
464 Kokkos::View<const float*, CpMem> hertzE, Kokkos::View<const float*, CpMem> hertzNu, float dt,
465 Kokkos::View<float* [3], CpMem> xi, Kokkos::View<float*, CpMem> snPrev,
466 Kokkos::View<float* [3], CpMem> force, Kokkos::View<float* [3], CpMem> torque) {
467 CpExec space;
468 Kokkos::parallel_for(
469 "peclet::dem::hertz_shape_pairs", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
470 KOKKOS_LAMBDA(int idx) {
471 const int idA = pairs(idx, 0), idB = pairs(idx, 1);
472 const ShapeDesc dA = shapes(shapeId(idA));
473 const ShapeDesc dB = shapes(shapeId(idB));
474 const F3 posA = loadF3(pos, idA), posB = loadF3(pos, idB);
475 const F4 qA = loadF4(quat, idA), qB = loadF4(quat, idB);
476 const float effScaleA = scale(idA) * globalScale, effScaleB = scale(idB) * globalScale;
477 const int countA = dA.numPoints;
478 const bool sphereA = (dA.type == SPHERE);
479 const int iter = (countA > 0) ? countA : 1;
480
481 const int ma = matId(idA), mb = matId(idB);
482 float e, mu;
483 hertzPairEMu(ma, mb, pairTable, eGlobal, muGlobal, e, mu);
484 const float Ei = hertzE(ma), Ej = hertzE(mb);
485 const float ni = hertzNu(ma), nj = hertzNu(mb);
486 const float eStar = 1.0f / ((1.0f - ni * ni) / Ei + (1.0f - nj * nj) / Ej);
487 const float gStar =
488 1.0f / (2.0f * (2.0f - ni) * (1.0f + ni) / Ei + 2.0f * (2.0f - nj) * (1.0f + nj) / Ej);
489 // contact radii: spheres exact, shapes = frac * bounding radius
490 // params.x = sphere radius or (grid SDF) canonical bounding radius
491 const float rcA = (sphereA ? 1.0f : contactRadiusFrac) * dA.params.x * effScaleA;
492 const float rcB =
493 ((dB.type == SPHERE) ? 1.0f : contactRadiusFrac) * dB.params.x * effScaleB;
494 const float rStar = rcA * rcB / (rcA + rcB);
495 const float imSum = invMass(idA) + invMass(idB);
496 const float mStar = (imSum > 0.0f) ? 1.0f / imSum : 0.0f;
497 const float bd = hertzBetaD(e);
498
499 const F3 vA = loadF3(vel, idA), wA = loadF3(angVel, idA);
500 const F3 vB = loadF3(vel, idB), wB = loadF3(angVel, idB);
501
502 float fnSum = 0.0f, snSum = 0.0f;
503 F3 cSum{0, 0, 0}, nSum{0, 0, 0};
504 float dMax = 0.0f;
505 F3 fAcc{0, 0, 0}, tAccA{0, 0, 0}, tAccB{0, 0, 0};
506 // Per-point dashpots weighted by stiffness against LAST step's patch stiffness sum:
507 // eta_i = 2 beta sn_i sqrt(m*/snTot). Sums to the exact two-body damping AND gives every
508 // patch mode (incl. face rocking) a uniform damping ratio; the patch-level dashpot alone
509 // left rocking modes undamped (measured: cubes tumbling uphill on an incline).
510 const float snTotPrev = snPrev(idx);
511 const float dampC = (mStar > 0.0f) ? -2.0f * 0.9128709f * bd * Kokkos::sqrt(mStar) : 0.0f;
512
513 for (int k = 0; k < iter; ++k) {
514 F3 pLocalA{0, 0, 0};
515 float pointRadius = 0.0f;
516 if (countA > 0) {
517 const int sIdx = dA.shellOffset + k;
518 pLocalA = F3{shell(sIdx, 0), shell(sIdx, 1), shell(sIdx, 2)};
519 } else if (sphereA) {
520 pointRadius = dA.params.x * effScaleA;
521 }
522 const F3 pWorld = add3(posA, rotateVector(qA, scale3(pLocalA, effScaleA)));
523 const F3 pCanB = scale3(invRotateVector(qB, sub3(pWorld, posB)), 1.0f / effScaleB);
524 const float dist = sdfEvalShape(pCanB, dB, sdfGrid) * effScaleB;
525 const float effDist = dist - pointRadius;
526 if (effDist >= 0.0f)
527 continue;
528 const float delta = -effDist;
529 F3 nLoc = sdfGradShape(pCanB, dB, sdfGrid);
530 const float len = len3(nLoc);
531 nLoc = (len > 1e-9f) ? scale3(nLoc, 1.0f / len) : F3{0, 1, 0};
532 const F3 nW = rotateVector(qB, nLoc); // out of B = push direction on A
533 const F3 pSurfA = sub3(pWorld, scale3(nW, pointRadius));
534 const F3 rA = sub3(pSurfA, posA);
535 const F3 rB = sub3(add3(pSurfA, scale3(nW, delta)), posB);
536
537 const F3 vrelP = sub3(add3(vA, cross3v(wA, rA)), add3(vB, cross3v(wB, rB)));
538 const float vnP = dot3(vrelP, nW);
539 const float sq = Kokkos::sqrt(rStar * delta);
540 const float fnSpring = (4.0f / 3.0f) * eStar * sq * delta;
541 const float snI = 2.0f * eStar * sq;
542 const float snTot = (snTotPrev > snI) ? snTotPrev : snI; // first-contact guard
543 const float etaI = dampC * snI / Kokkos::sqrt(snTot);
544 const F3 f = scale3(nW, fnSpring - etaI * vnP);
545 fAcc = add3(fAcc, f);
546 tAccA = add3(tAccA, cross3v(rA, f));
547 tAccB = add3(tAccB, cross3v(rB, scale3(f, -1.0f)));
548 fnSum += fnSpring;
549 snSum += 2.0f * eStar * sq; // patch tangent stiffness = sum of point stiffnesses
550 cSum = add3(cSum, scale3(pSurfA, fnSpring));
551 nSum = add3(nSum, scale3(nW, fnSpring));
552 if (delta > dMax)
553 dMax = delta;
554 }
555
556 if (fnSum <= 0.0f) {
557 xi(idx, 0) = xi(idx, 1) = xi(idx, 2) = 0.0f;
558 snPrev(idx) = 0.0f;
559 return;
560 }
561 // Patch level: ONE normal dashpot and ONE Mindlin tangential spring at the
562 // load-weighted centroid.
563 const F3 cpt = scale3(cSum, 1.0f / fnSum);
564 F3 nhat = nSum;
565 const float nl = len3(nhat);
566 if (nl > 1e-9f)
567 nhat = scale3(nhat, 1.0f / nl);
568 const F3 rA = sub3(cpt, posA), rB = sub3(cpt, posB);
569 const F3 vrel = sub3(add3(vA, cross3v(wA, rA)), add3(vB, cross3v(wB, rB)));
570 snPrev(idx) = snSum;
571 const F3 vt = sub3(vrel, scale3(nhat, dot3(vrel, nhat)));
572 const float st = 8.0f * gStar * Kokkos::sqrt(rStar * dMax);
573 const float etaT = -2.0f * 0.9128709f * bd * Kokkos::sqrt(st * mStar);
574 F3 x{xi(idx, 0), xi(idx, 1), xi(idx, 2)};
575 x = add3(x, scale3(vt, dt));
576 x = sub3(x, scale3(nhat, dot3(x, nhat)));
577 F3 ft = add3(scale3(x, -st), scale3(vt, -etaT));
578 const float ftLen = len3(ft);
579 const float ftMax = mu * fnSum;
580 if (ftLen > ftMax) {
581 const float sc = (ftLen > 1e-20f) ? ftMax / ftLen : 0.0f;
582 ft = scale3(ft, sc);
583 if (st > 1e-20f)
584 x = scale3(add3(ft, scale3(vt, etaT)), -1.0f / st);
585 }
586 xi(idx, 0) = x.x;
587 xi(idx, 1) = x.y;
588 xi(idx, 2) = x.z;
589 fAcc = add3(fAcc, ft);
590 tAccA = add3(tAccA, cross3v(rA, ft));
591 tAccB = add3(tAccB, cross3v(rB, scale3(ft, -1.0f)));
592
593 for (int c = 0; c < 3; ++c) {
594 Kokkos::atomic_add(&force(idA, c), (&fAcc.x)[c]);
595 Kokkos::atomic_add(&force(idB, c), -(&fAcc.x)[c]);
596 Kokkos::atomic_add(&torque(idA, c), (&tAccA.x)[c]);
597 Kokkos::atomic_add(&torque(idB, c), (&tAccB.x)[c]);
598 }
599 });
600 space.fence();
601}
602
603} // 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 ...
F3 sdfGradShape(F3 p, const ShapeDesc &d, GridView grid)
Outward gradient of a shape's SDF at canonical point p, NOT normalised (callers normalise,...
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
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.
float hertzSphereRadius(int i, Kokkos::View< const float *, CpMem > rad, Kokkos::View< const float *, CpMem > scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, float globalScale)
A body's Hertz sphere radius: a SPHERE shape's own radius, scale * globalScale * params....
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)
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...
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).