core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
body_properties.hpp
Go to the documentation of this file.
1// core — rigid-body mass properties of an implicit solid {phi < 0}: volume, mass, centre of mass,
2// the full inertia tensor, and its principal decomposition (three moments + a QUATERNION), by
3// composite implicit quadrature on the same backbone as geom/quadrature.hpp.
4//
5// WHY THIS EXISTS. The DEM particle pipeline needs mass properties at preprocessing time, and its
6// existing voxel integrator (peclet.dem.particle_builder) is percent-level at default settings and
7// SYSTEMATICALLY biased when the field is a non-true-distance bound (measured +3.5% inertia on an
8// ellipsoid) -- exactly what core's non-certified leaves and CSG results emit. This integrator only
9// consumes SIGNS and bracketed roots: along each quadrature line the positive-set boundary is
10// located by bisection, and every leaf in the vocabulary is sign-exact, so bound-only fields lose
11// NOTHING here. The 1-D moments over each interior run are analytic (∫x^k dx over [a,b]), so the
12// error lives in the transverse Gauss rule per cell, whose kinks are CURVES (where the interface
13// crosses cell faces) -- the same obstruction cellVolumeFraction documents, and unlike the face
14// apertures' kink POINTS it is not removed by splitting. MEASURED grade (ctest geom_body):
15// ~1e-4 relative at n=8, ~4e-6 at n=32, converging ~O(n^-2), with ZERO systematic bias on
16// bound-only fields -- against the voxel integrator's 3e-3-with-+3.5e-2-bias. If a consumer ever
17// needs more, the recorded lever is nested 1-D splitting in the transverse plane (bracket the
18// interface's zeros on the cell's cap faces per row); no consumer does today.
19//
20// FRAME CONTRACT. Everything is reported in the INPUT frame of phi:
21// p_input = com + R * p_body, R = `rotation` (columns = principal axes), also as `quat`.
22// A shape whose canonical frame is NOT its principal frame is therefore fully supported at the
23// measurement level; re-expressing the shape ITSELF in the principal frame is the consumer's move
24// (SceneBuilder::addReframed does it exactly for node trees -- one composed transform, no
25// resampling).
26//
27// DEGENERATE FRAMES. Within a cluster of (near-)equal principal moments any orthonormal basis is
28// principal, and a raw eigensolver returns an arbitrary one -- a cube comes back edge-down. Inside
29// each cluster the basis is rotated onto the input axes with the largest projection into that
30// subspace (orthogonal Procrustes), so a sphere or cube reports the IDENTITY rotation and a
31// z-aligned cylinder reports a z-aligned frame. Ported from peclet.dem.particle_builder, where the
32// behaviour is documented and relied on.
33//
34// Host-only (preprocessing); `Phi` is any callable Vec3<Real> -> Real, negative inside --
35// a lambda, a SceneQueryView, a DeviceScene host mirror.
36#ifndef PECLET_CORE_GEOM_BODY_PROPERTIES_HPP
37#define PECLET_CORE_GEOM_BODY_PROPERTIES_HPP
38
39#include <cmath>
40#include <cstddef>
41#include <stdexcept>
42
44
46
47template <class Real>
50 Real mass = 0;
51 Vec3<Real> com{0, 0, 0};
52 Real inertia[3][3] = {};
53 Real principal[3] = {};
54 Real rotation[3][3] = {};
55 Quat<Real> quat{0, 0, 0, 1};
56};
57
58namespace body_detail {
59
65template <class Real, class Phi>
67 Real& m2) {
68 const int NS = nseg < 2 ? 2 : (nseg > 64 ? 64 : nseg);
69 auto at = [&](Real s) { return phi(Vec3<Real>{p0.x + s * d.x, p0.y + s * d.y, p0.z + s * d.z}); };
70 auto root = [&](Real a, Real b, Real fa) {
71 for (int it = 0; it < 40; ++it) {
72 const Real m = Real(0.5) * (a + b);
73 const Real fm = at(m);
74 if ((fm < Real(0)) == (fa < Real(0))) {
75 a = m;
76 fa = fm;
77 } else {
78 b = m;
79 }
80 }
81 return Real(0.5) * (a + b);
82 };
83 m0 = m1 = m2 = Real(0);
84 auto accumulate = [&](Real a, Real b) {
85 m0 += b - a;
86 m1 += Real(0.5) * (b * b - a * a);
87 m2 += (b * b * b - a * a * a) / Real(3);
88 };
89 Real sPrev = 0, fPrev = at(Real(0));
90 Real openAt = fPrev < Real(0) ? Real(0) : Real(-1);
91 for (int i = 1; i <= NS; ++i) {
92 const Real s = Real(i) / Real(NS);
93 const Real f = at(s);
94 if ((f < Real(0)) != (fPrev < Real(0))) {
95 const Real r = root(sPrev, s, fPrev);
96 if (fPrev < Real(0)) {
98 openAt = Real(-1);
99 } else {
100 openAt = r;
101 }
102 }
103 sPrev = s;
104 fPrev = f;
105 }
106 if (openAt >= Real(0))
108}
109
112template <class Real>
113void eigSym3(const Real A[3][3], Real w[3], Real V[3][3]) {
114 Real a[3][3];
115 for (int i = 0; i < 3; ++i)
116 for (int j = 0; j < 3; ++j) {
117 a[i][j] = A[i][j];
118 V[i][j] = (i == j) ? Real(1) : Real(0);
119 }
120 for (int sweep = 0; sweep < 50; ++sweep) {
121 Real off = std::fabs(a[0][1]) + std::fabs(a[0][2]) + std::fabs(a[1][2]);
122 if (off == Real(0))
123 break;
124 for (int p = 0; p < 2; ++p)
125 for (int q = p + 1; q < 3; ++q) {
126 if (a[p][q] == Real(0))
127 continue;
128 const Real theta = (a[q][q] - a[p][p]) / (Real(2) * a[p][q]);
129 const Real t = (theta >= Real(0) ? Real(1) : Real(-1)) /
130 (std::fabs(theta) + std::sqrt(theta * theta + Real(1)));
131 const Real c = Real(1) / std::sqrt(t * t + Real(1)), s = t * c;
132 for (int k = 0; k < 3; ++k) {
133 const Real akp = a[k][p], akq = a[k][q];
134 a[k][p] = c * akp - s * akq;
135 a[k][q] = s * akp + c * akq;
136 }
137 for (int k = 0; k < 3; ++k) {
138 const Real apk = a[p][k], aqk = a[q][k];
139 a[p][k] = c * apk - s * aqk;
140 a[q][k] = s * apk + c * aqk;
141 }
142 for (int k = 0; k < 3; ++k) {
143 const Real vkp = V[k][p], vkq = V[k][q];
144 V[k][p] = c * vkp - s * vkq;
145 V[k][q] = s * vkp + c * vkq;
146 }
147 }
148 }
149 for (int i = 0; i < 3; ++i)
150 w[i] = a[i][i];
151 // sort ascending, carrying columns
152 for (int i = 0; i < 2; ++i)
153 for (int j = i + 1; j < 3; ++j)
154 if (w[j] < w[i]) {
155 std::swap(w[i], w[j]);
156 for (int k = 0; k < 3; ++k)
157 std::swap(V[k][i], V[k][j]);
158 }
159}
160
164template <class Real>
165void resolveDegenerateFrame(Real w[3], Real V[3][3], Real relTol = Real(1e-2)) {
166 const Real scale = std::max(std::max(std::fabs(w[0]), std::fabs(w[2])), Real(1e-30));
167 int i = 0;
168 while (i < 3) {
169 int j = i + 1;
170 while (j < 3 && (w[j] - w[j - 1]) <= relTol * scale)
171 ++j;
172 const int k = j - i;
173 if (k > 1) {
174 // projection sizes of each input axis e_r into the subspace = row norms of V[:, i:j]
175 Real rn[3];
176 for (int r = 0; r < 3; ++r) {
177 rn[r] = Real(0);
178 for (int c = i; c < j; ++c)
179 rn[r] += V[r][c] * V[r][c];
180 }
181 int sel[3], ns = 0; // the k axes with the largest projections, in index order
182 for (int pick = 0; pick < k; ++pick) {
183 int best = -1;
184 for (int r = 0; r < 3; ++r) {
185 bool used = false;
186 for (int u = 0; u < ns; ++u)
187 used = used || (sel[u] == r);
188 if (!used && (best < 0 || rn[r] > rn[best]))
189 best = r;
190 }
191 sel[ns++] = best;
192 }
193 for (int u = 0; u < ns; ++u)
194 for (int v = u + 1; v < ns; ++v)
195 if (sel[v] < sel[u])
196 std::swap(sel[u], sel[v]);
197 // M = V_sub^T E_sel (k x k); its polar factor rotates V_sub onto the chosen axes
198 Real M[3][3] = {};
199 for (int r = 0; r < k; ++r)
200 for (int c = 0; c < k; ++c)
201 M[r][c] = V[sel[c]][i + r];
202 // polar factor via Jacobi on M^T M: U = M (M^T M)^{-1/2}
203 Real MtM[3][3] = {};
204 for (int r = 0; r < k; ++r)
205 for (int c = 0; c < k; ++c)
206 for (int t = 0; t < k; ++t)
207 MtM[r][c] += M[t][r] * M[t][c];
208 for (int r = k; r < 3; ++r)
209 MtM[r][r] = Real(1);
210 Real ew[3], EV[3][3];
211 eigSym3(MtM, ew, EV);
212 Real inv[3][3] = {}; // (M^T M)^{-1/2}
213 for (int r = 0; r < 3; ++r)
214 for (int c = 0; c < 3; ++c)
215 for (int t = 0; t < 3; ++t)
216 inv[r][c] += EV[r][t] * EV[c][t] / std::sqrt(std::max(ew[t], Real(1e-30)));
217 Real U[3][3] = {};
218 for (int r = 0; r < k; ++r)
219 for (int c = 0; c < k; ++c)
220 for (int t = 0; t < k; ++t)
221 U[r][c] += M[r][t] * inv[t][c];
222 // new subspace basis: V_sub <- V_sub U
223 Real NV[3][3];
224 for (int r = 0; r < 3; ++r)
225 for (int c = 0; c < k; ++c) {
226 NV[r][c] = Real(0);
227 for (int t = 0; t < k; ++t)
228 NV[r][c] += V[r][i + t] * U[t][c];
229 }
230 for (int r = 0; r < 3; ++r)
231 for (int c = 0; c < k; ++c)
232 V[r][i + c] = NV[r][c];
233 }
234 i = j;
235 }
236}
237
239template <class Real>
242 const Real tr = R[0][0] + R[1][1] + R[2][2];
243 if (tr > Real(0)) {
244 Real s = std::sqrt(tr + Real(1)) * Real(2);
245 q.w = Real(0.25) * s;
246 q.x = (R[2][1] - R[1][2]) / s;
247 q.y = (R[0][2] - R[2][0]) / s;
248 q.z = (R[1][0] - R[0][1]) / s;
249 } else if (R[0][0] > R[1][1] && R[0][0] > R[2][2]) {
250 Real s = std::sqrt(Real(1) + R[0][0] - R[1][1] - R[2][2]) * Real(2);
251 q.w = (R[2][1] - R[1][2]) / s;
252 q.x = Real(0.25) * s;
253 q.y = (R[0][1] + R[1][0]) / s;
254 q.z = (R[0][2] + R[2][0]) / s;
255 } else if (R[1][1] > R[2][2]) {
256 Real s = std::sqrt(Real(1) + R[1][1] - R[0][0] - R[2][2]) * Real(2);
257 q.w = (R[0][2] - R[2][0]) / s;
258 q.x = (R[0][1] + R[1][0]) / s;
259 q.y = Real(0.25) * s;
260 q.z = (R[1][2] + R[2][1]) / s;
261 } else {
262 Real s = std::sqrt(Real(1) + R[2][2] - R[0][0] - R[1][1]) * Real(2);
263 q.w = (R[1][0] - R[0][1]) / s;
264 q.x = (R[0][2] + R[2][0]) / s;
265 q.y = (R[1][2] + R[2][1]) / s;
266 q.z = Real(0.25) * s;
267 }
268 return q;
269}
270
271} // namespace body_detail
272
281template <class Real, class Phi>
283 int order = 5, int nseg = 8, Real density = Real(1)) {
284 namespace bd = body_detail;
285 if (!(hi.x > lo.x && hi.y > lo.y && hi.z > lo.z))
286 throw std::invalid_argument("bodyProperties: empty box");
287 if (n < 1)
288 throw std::invalid_argument("bodyProperties: n must be >= 1");
289 const Real H[3] = {(hi.x - lo.x) / n, (hi.y - lo.y) / n, (hi.z - lo.z) / n};
290 const Real LO[3] = {lo.x, lo.y, lo.z};
291 const int nq = quad_detail::gaussCount(order);
292
293 // raw moments in the input frame: V, M[a] = ∫x_a, S[a][b] = ∫x_a x_b (symmetric)
294 Real V = 0, M[3] = {0, 0, 0}, S[3][3] = {};
295
296 for (int ci = 0; ci < n; ++ci)
297 for (int cj = 0; cj < n; ++cj)
298 for (int ck = 0; ck < n; ++ck) {
299 const Real o[3] = {LO[0] + ci * H[0], LO[1] + cj * H[1], LO[2] + ck * H[2]};
300 // height axis: the axis of the strongest central difference at the cell centre
301 auto at = [&](Real a, Real b, Real c) {
302 return phi(Vec3<Real>{o[0] + a * H[0], o[1] + b * H[1], o[2] + c * H[2]});
303 };
304 const Real q = Real(0.25);
305 const Real g0 =
306 at(Real(0.5) + q, Real(0.5), Real(0.5)) - at(Real(0.5) - q, Real(0.5), Real(0.5));
307 const Real g1 =
308 at(Real(0.5), Real(0.5) + q, Real(0.5)) - at(Real(0.5), Real(0.5) - q, Real(0.5));
309 const Real g2 =
310 at(Real(0.5), Real(0.5), Real(0.5) + q) - at(Real(0.5), Real(0.5), Real(0.5) - q);
311 auto ab = [](Real v) { return v < Real(0) ? -v : v; };
312 int hA = 0;
313 if (ab(g1) >= ab(g0) && ab(g1) >= ab(g2))
314 hA = 1;
315 else if (ab(g2) >= ab(g0) && ab(g2) >= ab(g1))
316 hA = 2;
317 const int t1 = (hA + 1) % 3, t2 = (hA + 2) % 3;
318 const Real area = H[t1] * H[t2];
319
320 for (int qi = 0; qi < nq; ++qi) {
321 Real ti, wi;
322 quad_detail::gaussLegendre01<Real>(order, qi, ti, wi);
323 for (int qj = 0; qj < nq; ++qj) {
324 Real tj, wj;
325 quad_detail::gaussLegendre01<Real>(order, qj, tj, wj);
326 Real p0[3] = {o[0], o[1], o[2]};
327 p0[t1] += ti * H[t1];
328 p0[t2] += tj * H[t2];
329 Real d[3] = {0, 0, 0};
330 d[hA] = H[hA];
331 Real m0, m1, m2;
332 bd::interiorMoments(phi, Vec3<Real>{p0[0], p0[1], p0[2]}, Vec3<Real>{d[0], d[1], d[2]},
333 nseg, m0, m1, m2);
334 if (m0 == Real(0))
335 continue;
336 const Real W = wi * wj * area;
337 const Real x0 = p0[hA], h = H[hA];
338 const Real L0 = h * m0;
339 const Real L1 = h * (x0 * m0 + h * m1);
340 const Real L2 = h * (x0 * x0 * m0 + Real(2) * x0 * h * m1 + h * h * m2);
341 const Real tw[3] = {p0[0], p0[1], p0[2]}; // transverse coords are constant on the line
342 V += W * L0;
343 M[hA] += W * L1;
344 M[t1] += W * tw[t1] * L0;
345 M[t2] += W * tw[t2] * L0;
346 S[hA][hA] += W * L2;
347 S[t1][t1] += W * tw[t1] * tw[t1] * L0;
348 S[t2][t2] += W * tw[t2] * tw[t2] * L0;
349 S[t1][t2] += W * tw[t1] * tw[t2] * L0;
350 S[hA][t1] += W * tw[t1] * L1;
351 S[hA][t2] += W * tw[t2] * L1;
352 }
353 }
354 }
355 // symmetrise the off-diagonal accumulators (each pair was accumulated once)
356 S[1][0] = S[0][1] += S[1][0];
357 S[2][0] = S[0][2] += S[2][0];
358 S[2][1] = S[1][2] += S[2][1];
359
361 bp.volume = V;
362 bp.mass = density * V;
363 if (!(V > Real(0)))
364 throw std::runtime_error(
365 "bodyProperties: the field encloses no volume in this box -- check the sign convention "
366 "(negative inside) and that the box contains the solid");
367 bp.com = Vec3<Real>{M[0] / V, M[1] / V, M[2] / V};
368 const Real c[3] = {bp.com.x, bp.com.y, bp.com.z};
369 // inertia about the COM: I_ab = rho * [ delta_ab * sum_k (S_kk - V c_k^2) - (S_ab - V c_a c_b) ]
370 Real cs[3][3];
371 for (int a = 0; a < 3; ++a)
372 for (int b = 0; b < 3; ++b)
373 cs[a][b] = S[a][b] - V * c[a] * c[b];
374 const Real trace = cs[0][0] + cs[1][1] + cs[2][2];
375 for (int a = 0; a < 3; ++a)
376 for (int b = 0; b < 3; ++b)
377 bp.inertia[a][b] = density * ((a == b ? trace : Real(0)) - cs[a][b]);
378
379 Real w[3], R[3][3];
380 bd::eigSym3(bp.inertia, w, R);
381 bd::resolveDegenerateFrame(w, R);
382 // keep a proper rotation
383 const Real det = R[0][0] * (R[1][1] * R[2][2] - R[1][2] * R[2][1]) -
384 R[0][1] * (R[1][0] * R[2][2] - R[1][2] * R[2][0]) +
385 R[0][2] * (R[1][0] * R[2][1] - R[1][1] * R[2][0]);
386 if (det < Real(0))
387 for (int r = 0; r < 3; ++r)
388 R[r][0] = -R[r][0];
389 for (int k = 0; k < 3; ++k)
390 bp.principal[k] = w[k];
391 for (int r = 0; r < 3; ++r)
392 for (int cc = 0; cc < 3; ++cc)
393 bp.rotation[r][cc] = R[r][cc];
394 bp.quat = bd::quatFromRotation(R);
395 return bp;
396}
397
398} // namespace peclet::core::geom
399
400#endif // PECLET_CORE_GEOM_BODY_PROPERTIES_HPP
void interiorMoments(const Phi &phi, Vec3< Real > p0, Vec3< Real > d, int nseg, Real &m0, Real &m1, Real &m2)
Moments of the NEGATIVE set {phi < 0} along p0 + s*d, s in [0,1], in the PARAMETER s: m0 = ∫ 1 ds,...
void eigSym3(const Real A[3][3], Real w[3], Real V[3][3])
Jacobi eigensolver for a symmetric 3x3: A = V diag(w) V^T, V orthonormal columns.
Quat< Real > quatFromRotation(const Real R[3][3])
Rotation matrix (columns orthonormal, det +1) -> quaternion (x, y, z, w), Shepperd's method.
void resolveDegenerateFrame(Real w[3], Real V[3][3], Real relTol=Real(1e-2))
Within each cluster of (near-)equal eigenvalues, rotate the eigenbasis onto the input axes with the l...
PECLET_HD int gaussCount(int n)
BodyProperties< Real > bodyProperties(const Phi &phi, Vec3< Real > lo, Vec3< Real > hi, int n=32, int order=5, int nseg=8, Real density=Real(1))
Mass properties of {phi < 0} over the box [lo, hi], composite implicit quadrature: an n^3 cell grid; ...
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
PECLET_HD Vec3< Real > scale(Vec3< Real > a, Real s)
Definition portable.hpp:66
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
Vec3< Real > com
centre of mass, input frame
Real principal[3]
principal moments; principal[k] pairs with column k of R
Real inertia[3][3]
full tensor about the COM, input frame, at the given density
Quat< Real > quat
the same rotation, p_input = com + rotate(quat, p_body)
Real rotation[3][3]
columns = principal axes in the input frame