core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
scene_query.hpp
Go to the documentation of this file.
1// core — accelerated scene queries: periodic evaluation, geometry-driven candidate grids, and the
2// sphere-union fast paths the porous-media consumers live on.
3//
4// Layer 2-for-core of suite/docs/ANALYTIC_SDF_GEOMETRY.md, shaped by the AMR consumer's measured
5// requirements (docs/AMR_GEOMETRY_SETUP_REQUIREMENTS.md): 94% of AmrFlow::setSolid on the
6// 180-sphere RCP bed is SDF evaluation — 101 evals/leaf x 1.18 us/eval brute force over all
7// primitives. This header owns the cost-per-eval side of that product:
8//
9// PeriodicBox / minImage min-image periodicity, the AMR stopgap's EXACT expression
10// (d -= L*nearbyint(d/L)) so results are BIT-IDENTICAL, not merely
11// sign-agreeing at the probe scale.
12// SphereUnionView compact union-of-spheres form (the workhorse geometry) with an
13// equal-radius fast path: min over SQUARED center distances, ONE
14// sqrt per query instead of one per sphere.
15// CandidateGridView + build per-bin candidate lists over a UNIFORM auxiliary lattice, built
16// geometry-driven and pruned with exact bounds, so a query costs
17// bin arithmetic + a 1-5 entry list instead of a scan over every
18// primitive.
19// SphereBedQuery the host-callable object that slots straight into
20// AmrFlow::setSolid's templated SdfFn parameter — the stopgap
21// retirement path.
22//
23// WHY UNIFORM BINS AND NOT OCTREE-LEAF LISTS. The directive behind contract 9 is that geometry
24// drives (each primitive splats onto the cells its band overlaps) and that far-field cells never
25// traverse an acceleration structure. Keying the lists by a fixed auxiliary lattice satisfies both
26// with no octree coupling at all: a query is O(1) bin arithmetic from the POINT's own coordinates,
27// so leaves of every level — and probe points at any h, virtual positions across 2:1 jumps, coarse
28// MG face centers — hit the right list with no per-level machinery and no leavesInBox dependency.
29// A leaf-keyed variant can be layered on top by the AMR side (list(leaf) = list(binOf(center))
30// unioned over the leaf's span) without any new API here.
31//
32// EXACTNESS CONTRACT (the float-sign classification requirement). The candidate list of a bin is a
33// conservative superset of the argmin set over every point of the bin, built with the classic
34// bound pruning: keep primitive i iff lower_i(B) <= U(B), where lower_i is a lower bound of i's
35// contribution anywhere in B and U(B) = min_j upper_j(B) is an upper bound of the union anywhere
36// in B. min over a superset that contains the argmin IS the min — bit-identically, since each
37// member's value is computed with the identical expression — so acceleration changes NOTHING about
38// the result: not the value, not the sign, not under list reordering (min is order-independent as
39// a value). Queries outside the grid's coverage, or in a bin whose list is empty (possible far
40// from all geometry when bounds never reached it), FALL BACK to the full scan — exact, and rare
41// where it matters.
42//
43// The bounds argument needs eval_i >= (distance to a ball containing i's solid) pointwise, which
44// holds for exact-distance leaves and is PRESERVED by union (min over children, all certified),
45// intersection and difference (max(a, ...) >= a, so the LEFT child's ball and certificate
46// suffice). It FAILS for the under-estimating leaves (ellipsoid, superquadric,
47// HollowCylinderShell — measured under-run up to 4x in Layer 0's gate) and for grid leaves: those
48// instances are never pruned — they ride on an ALWAYS list evaluated for every query. The
49// workhorse geometries (sphere beds, box/cylinder/capsule/torus stirrers and their CSG) prune.
50//
51// Host-compilable (PECLET_HD); the Kokkos-owning DeviceScene and batched drivers live in
52// device_scene.hpp.
53#ifndef PECLET_CORE_GEOM_SCENE_QUERY_HPP
54#define PECLET_CORE_GEOM_SCENE_QUERY_HPP
55
56#include <cstddef>
57#include <stdexcept>
58#include <vector>
59
62
63namespace peclet::core::geom {
64
65// ---------------------------------------------------------------------------------------------
66// Periodicity (AMR requirement 4)
67// ---------------------------------------------------------------------------------------------
68
70template <class Real>
72 Real Lx = 0, Ly = 0, Lz = 0;
73 bool on = false;
74};
75
83template <class Real>
85 namespace cd = peclet::core::detail;
86 if (b.on) {
87 d.x = cd::hdFma(-cd::hdNearbyint(d.x / b.Lx), b.Lx, d.x);
88 d.y = cd::hdFma(-cd::hdNearbyint(d.y / b.Ly), b.Ly, d.y);
89 d.z = cd::hdFma(-cd::hdNearbyint(d.z / b.Lz), b.Lz, d.z);
90 }
91 return d;
92}
93
94// ---------------------------------------------------------------------------------------------
95// Sphere union (the porous-media workhorse)
96// ---------------------------------------------------------------------------------------------
97
100template <class Real>
102 const Real* cx = nullptr;
103 const Real* cy = nullptr;
104 const Real* cz = nullptr;
105 const Real* r = nullptr;
106 int n = 0;
107 bool equalR = false;
108 Real r0 = 0;
109};
110
114template <class Real>
116 const PeriodicBox<Real>& box) {
117 namespace cd = peclet::core::detail;
118 const Vec3<Real> d = minImage(Vec3<Real>{p.x - u.cx[i], p.y - u.cy[i], p.z - u.cz[i]}, box);
119 return cd::hdFma(d.z, d.z, cd::hdFma(d.y, d.y, d.x * d.x));
120}
121
126template <class Real>
128 const PeriodicBox<Real>& box, const int* items, int count) {
129 if (u.equalR) {
130 Real best2 = Real(1e300);
131 for (int k = 0; k < count; ++k) {
132 const Real d2 = sphereDist2(u, items[k], p, box);
133 if (d2 < best2)
134 best2 = d2;
135 }
137 }
138 Real best = Real(1e300);
139 for (int k = 0; k < count; ++k) {
140 const int i = items[k];
141 const Real v = peclet::core::detail::hdSqrt(sphereDist2(u, i, p, box)) - u.r[i];
142 if (v < best)
143 best = v;
144 }
145 return best;
146}
147
150template <class Real>
152 const PeriodicBox<Real>& box) {
153 if (u.equalR) {
154 Real best2 = Real(1e300);
155 for (int i = 0; i < u.n; ++i) {
156 const Real d2 = sphereDist2(u, i, p, box);
157 if (d2 < best2)
158 best2 = d2;
159 }
161 }
162 Real best = Real(1e300);
163 for (int i = 0; i < u.n; ++i) {
164 const Real v = peclet::core::detail::hdSqrt(sphereDist2(u, i, p, box)) - u.r[i];
165 if (v < best)
166 best = v;
167 }
168 return best;
169}
170
171// ---------------------------------------------------------------------------------------------
172// Candidate grid
173// ---------------------------------------------------------------------------------------------
174
177template <class Real>
179 int nx = 0, ny = 0, nz = 0;
180 Real ox = 0, oy = 0, oz = 0; // origin
181 Real bx = 1, by = 1, bz = 1; // bin size
182 const int* offsets = nullptr; // nx*ny*nz + 1
183 const int* items = nullptr;
184 const int* always = nullptr; // non-prunable entries, evaluated for every query
185 int alwaysCount = 0;
186 bool wrap = false; // periodic bin lookup
187
190 PECLET_HD long binOf(Vec3<Real> p) const {
191 long i = (long)peclet::core::detail::hdFloor((p.x - ox) / bx);
192 long j = (long)peclet::core::detail::hdFloor((p.y - oy) / by);
193 long k = (long)peclet::core::detail::hdFloor((p.z - oz) / bz);
194 if (wrap) {
195 i = ((i % nx) + nx) % nx;
196 j = ((j % ny) + ny) % ny;
197 k = ((k % nz) + nz) % nz;
198 } else if (i < 0 || i >= nx || j < 0 || j >= ny || k < 0 || k >= nz) {
199 return -1;
200 }
201 return i + (long)nx * (j + (long)ny * k);
202 }
203};
204
208template <class Real>
210 const PeriodicBox<Real>& box, const CandidateGridView<Real>& g) {
211 const long b = g.binOf(p);
212 if (b < 0)
213 return evalSphereUnion(u, p, box);
214 const int lo = g.offsets[b], hi = g.offsets[b + 1];
215 if (lo == hi)
216 return evalSphereUnion(u, p, box);
217 return evalSphereUnionSubset(u, p, box, g.items + lo, hi - lo);
218}
219
221template <class Real>
223 std::vector<int> offsets, items, always;
224 std::vector<Real> instBoundR; // per-instance world bounding radii (general-scene builds)
225 CandidateGridView<Real> meta; // pointers unset; use view()
226
229 v.offsets = offsets.data();
230 v.items = items.data();
231 v.always = always.data();
232 v.alwaysCount = static_cast<int>(always.size());
233 return v;
234 }
235};
236
246template <class Real>
248 Vec3<Real> extent, const PeriodicBox<Real>& box,
249 int nbHint = 0) {
250 if (u.n <= 0)
251 throw std::invalid_argument("buildSphereCandidateGrid: empty union");
252 Real rMean = 0;
253 for (int i = 0; i < u.n; ++i)
254 rMean += u.r[i];
255 rMean /= u.n;
256 auto nbAxis = [&](Real L) {
257 if (nbHint > 0)
258 return nbHint;
259 int nb = rMean > 0 ? static_cast<int>(L / (rMean * Real(0.5))) : 16;
260 return nb < 2 ? 2 : (nb > 96 ? 96 : nb);
261 };
263 auto& m = g.meta;
264 m.nx = nbAxis(extent.x);
265 m.ny = nbAxis(extent.y);
266 m.nz = nbAxis(extent.z);
267 m.ox = origin.x;
268 m.oy = origin.y;
269 m.oz = origin.z;
270 m.bx = extent.x / m.nx;
271 m.by = extent.y / m.ny;
272 m.bz = extent.z / m.nz;
273 m.wrap = box.on;
274 const long nbins = (long)m.nx * m.ny * m.nz;
275 const Real halfDiag = Real(0.5) * std::sqrt(m.bx * m.bx + m.by * m.by + m.bz * m.bz);
276 // Conservative slack absorbing the float rounding of the bounds themselves; supersets stay
277 // supersets, lists grow by (rarely) one entry.
278 const Real slack = Real(1e-12) * (extent.x + extent.y + extent.z);
279
280 auto binCenter = [&](long b, Vec3<Real>& c) {
281 const long i = b % m.nx, j = (b / m.nx) % m.ny, k = b / ((long)m.nx * m.ny);
282 c = Vec3<Real>{m.ox + (i + Real(0.5)) * m.bx, m.oy + (j + Real(0.5)) * m.by,
283 m.oz + (k + Real(0.5)) * m.bz};
284 };
285 auto dist = [&](const Vec3<Real>& a, int i) {
286 const Vec3<Real> d = minImage(Vec3<Real>{a.x - u.cx[i], a.y - u.cy[i], a.z - u.cz[i]}, box);
287 return std::sqrt(d.x * d.x + d.y * d.y + d.z * d.z);
288 };
289 // The bin neighbourhood a sphere splats: bins whose center lies within `reach` of the sphere
290 // center (min-image). Enumerated as an index box around the sphere, wrapped when periodic.
291 auto forEachBinNear = [&](int i, Real reach, auto&& fn) {
292 const int riX = static_cast<int>(reach / m.bx) + 1;
293 const int riY = static_cast<int>(reach / m.by) + 1;
294 const int riZ = static_cast<int>(reach / m.bz) + 1;
295 const int ci = static_cast<int>(peclet::core::detail::hdFloor((u.cx[i] - m.ox) / m.bx));
296 const int cj = static_cast<int>(peclet::core::detail::hdFloor((u.cy[i] - m.oy) / m.by));
297 const int ck = static_cast<int>(peclet::core::detail::hdFloor((u.cz[i] - m.oz) / m.bz));
298 for (int dk = -riZ; dk <= riZ; ++dk)
299 for (int dj = -riY; dj <= riY; ++dj)
300 for (int di = -riX; di <= riX; ++di) {
301 long ii = ci + di, jj = cj + dj, kk = ck + dk;
302 if (m.wrap) {
303 ii = ((ii % m.nx) + m.nx) % m.nx;
304 jj = ((jj % m.ny) + m.ny) % m.ny;
305 kk = ((kk % m.nz) + m.nz) % m.nz;
306 } else if (ii < 0 || ii >= m.nx || jj < 0 || jj >= m.ny || kk < 0 || kk >= m.nz) {
307 continue;
308 }
309 fn(ii + (long)m.nx * (jj + (long)m.ny * kk));
310 }
311 };
312 // Pass 1: U(B) = min_i upper_i(B), splat within each sphere's competitive reach. The reach must
313 // cover every bin where this sphere could OWN the best upper bound; beyond max over spheres of
314 // (their own reach), U stays +inf and queries there fall back. Reach heuristic: the largest
315 // min-image distance possible is bounded by the box half-diagonal, so cap there.
316 std::vector<Real> U((std::size_t)nbins, Real(1e300));
317 const Real capReach =
318 box.on ? Real(0.5) * std::sqrt(box.Lx * box.Lx + box.Ly * box.Ly + box.Lz * box.Lz) + halfDiag
319 : std::sqrt(extent.x * extent.x + extent.y * extent.y + extent.z * extent.z);
320 for (int i = 0; i < u.n; ++i)
321 forEachBinNear(i, capReach, [&](long b) {
322 Vec3<Real> c;
323 binCenter(b, c);
324 const Real up = dist(c, i) + halfDiag - u.r[i];
325 if (up < U[(std::size_t)b])
326 U[(std::size_t)b] = up;
327 });
328 // Pass 2: membership by the exact criterion. Each sphere visits bins within its own competitive
329 // reach: lower_i(B) <= U(B) needs d <= r_i + halfDiag + U(B); U is bounded above by the max
330 // over the field, so cap the walk there (plus its own radius + diag).
331 Real Umax = 0;
332 for (Real v : U)
333 if (v < Real(1e300) && v > Umax)
334 Umax = v;
335 std::vector<std::vector<int>> lists((std::size_t)nbins);
336 for (int i = 0; i < u.n; ++i)
337 forEachBinNear(i, u.r[i] + halfDiag + Umax + slack, [&](long b) {
338 Vec3<Real> c;
339 binCenter(b, c);
340 const Real lower = dist(c, i) - halfDiag - u.r[i];
341 if (lower <= U[(std::size_t)b] + slack)
342 lists[(std::size_t)b].push_back(i);
343 });
344 g.offsets.resize((std::size_t)nbins + 1);
345 g.offsets[0] = 0;
346 for (long b = 0; b < nbins; ++b)
347 g.offsets[(std::size_t)b + 1] =
348 g.offsets[(std::size_t)b] + static_cast<int>(lists[(std::size_t)b].size());
349 g.items.resize((std::size_t)g.offsets[(std::size_t)nbins]);
350 for (long b = 0; b < nbins; ++b)
351 std::copy(lists[(std::size_t)b].begin(), lists[(std::size_t)b].end(),
352 g.items.begin() + g.offsets[(std::size_t)b]);
353 return g;
354}
355
356// ---------------------------------------------------------------------------------------------
357// General scenes: periodic evaluation, bounds, candidate grids
358// ---------------------------------------------------------------------------------------------
359
371template <class Real>
373 const PeriodicBox<Real>& box, Real boundR = Real(-1)) {
374 if (i < 0 || i >= sc.instanceCount)
375 return Real(1e9);
376 const Instance<Real>& inst = sc.instances[i];
377 if (!box.on) {
378 const Vec3<Real> q =
379 scale(invRotate(inst.transform.rotation, sub(p, inst.transform.translation)),
380 Real(1) / inst.transform.scale);
381 return inst.transform.scale *
382 evalTree<Real>(TablePtr<ShapeNode<Real>>{sc.nodes}, sc.nodeCount, inst.shapeRoot, q,
383 TablePtr<GridDesc<Real>>{sc.grids}, PoolPtr<float>{sc.samples});
384 }
385 const Vec3<Real> d0 = minImage(sub(p, inst.transform.translation), box);
386 // which axes need the neighbouring image? |d_a| > L_a/2 - boundR (all of them when unknown)
387 const Real Ls[3] = {box.Lx, box.Ly, box.Lz};
388 const Real da[3] = {d0.x, d0.y, d0.z};
389 int nAlt[3] = {1, 1, 1};
390 Real alt[3][2] = {{d0.x, 0}, {d0.y, 0}, {d0.z, 0}};
391 for (int a = 0; a < 3; ++a) {
392 const bool need =
393 boundR < Real(0) || (da[a] < Real(0) ? -da[a] : da[a]) > Ls[a] * Real(0.5) - boundR;
394 if (need) {
395 alt[a][1] = da[a] < Real(0) ? da[a] + Ls[a] : da[a] - Ls[a];
396 nAlt[a] = 2;
397 }
398 }
399 Real best = Real(1e300);
400 for (int kz = 0; kz < nAlt[2]; ++kz)
401 for (int ky = 0; ky < nAlt[1]; ++ky)
402 for (int kx = 0; kx < nAlt[0]; ++kx) {
403 const Vec3<Real> d{alt[0][kx], alt[1][ky], alt[2][kz]};
404 const Vec3<Real> q =
405 scale(invRotate(inst.transform.rotation, d), Real(1) / inst.transform.scale);
406 const Real v = inst.transform.scale * evalTree<Real>(TablePtr<ShapeNode<Real>>{sc.nodes},
407 sc.nodeCount, inst.shapeRoot, q,
409 PoolPtr<float>{sc.samples});
410 if (v < best)
411 best = v;
412 }
413 return best;
414}
415
419template <class Real>
421 const PeriodicBox<Real>& box, const Real* boundR = nullptr) {
422 Real m = Real(1e9);
423 for (int i = 0; i < sc.instanceCount; ++i) {
424 const Real d = evalInstancePeriodic(sc, i, p, box, boundR ? boundR[i] : Real(-1));
425 if (d < m)
426 m = d;
427 }
428 return m;
429}
430
438template <class Real>
440 Vec3<Real> c{0, 0, 0};
441 Real r = 0;
442 bool certified = false;
443};
444
445namespace query_detail {
446
447template <class Real>
448struct NodeBound {
449 Vec3<Real> c{0, 0, 0}; // in the node's PARENT frame
450 Real r = 0;
451 bool certified = false;
452};
453
454template <class Real>
457 if (node < 0 || node >= sc.nodeCount)
458 return nb;
459 const ShapeNode<Real>& n = sc.nodes[node];
460 Real localR = 0;
461 bool cert = false;
462 if (n.kind < kCsgBase) {
463 switch (n.kind) {
464 case kSphere:
465 localR = n.params[0];
466 cert = true;
467 break;
468 case kBox:
469 localR = std::sqrt(n.params[0] * n.params[0] + n.params[1] * n.params[1] +
470 n.params[2] * n.params[2]);
471 cert = true;
472 break;
473 case kHollowCylinder: { // (rOuter, height, thickness), about y -- distance-exact
474 const Real ro = n.params[0], h2 = n.params[1] * Real(0.5);
475 localR = std::sqrt(ro * ro + h2 * h2);
476 cert = true;
477 break;
478 }
479 case kCapsule:
480 localR = n.params[1] + n.params[0];
481 cert = true;
482 break;
483 case kTorus:
484 localR = n.params[0] + n.params[1];
485 cert = true;
486 break;
487 case kCone: {
488 const Real rb = std::fmax(n.params[0], n.params[1]), hh = n.params[2];
489 localR = std::sqrt(rb * rb + hh * hh);
490 cert = true;
491 break;
492 }
493 case kHollowCylinderShell: { // sign-exact only: bounded but NOT certified
494 const Real ro = n.params[0], h2 = n.params[2] * Real(0.5);
495 localR = std::sqrt(ro * ro + h2 * h2);
496 cert = false;
497 break;
498 }
499 case kEllipsoid:
500 localR = std::fmax(n.params[0], std::fmax(n.params[1], n.params[2]));
501 cert = false;
502 break;
503 case kSuperquadric:
504 localR = std::sqrt(n.params[0] * n.params[0] + n.params[1] * n.params[1] +
505 n.params[2] * n.params[2]);
506 cert = false;
507 break;
508 default: // kGrid or unknown: no useful bound; never pruned
509 localR = Real(1e30);
510 cert = false;
511 break;
512 }
513 nb.c = n.transform.translation; // canonical leaves are origin-centred
514 nb.r = n.transform.scale * localR;
515 nb.certified = cert;
516 return nb;
517 }
518 // CSG: children live in this node's frame.
519 const NodeBound<Real> a = nodeBound(sc, n.aux0);
520 const NodeBound<Real> b = nodeBound(sc, n.aux1);
522 if (n.kind == kUnion) {
523 // enclosing sphere of the two child spheres; certified iff BOTH children are
524 const Vec3<Real> d = sub(b.c, a.c);
525 const Real dist = std::sqrt(d.x * d.x + d.y * d.y + d.z * d.z);
526 if (dist + b.r <= a.r) {
527 comb.c = a.c;
528 comb.r = a.r;
529 } else if (dist + a.r <= b.r) {
530 comb.c = b.c;
531 comb.r = b.r;
532 } else {
533 const Real R = (dist + a.r + b.r) * Real(0.5);
534 const Real t = dist > Real(0) ? (R - a.r) / dist : Real(0);
535 comb.c = Vec3<Real>{a.c.x + d.x * t, a.c.y + d.y * t, a.c.z + d.z * t};
536 comb.r = R;
537 }
538 comb.certified = a.certified && b.certified;
539 } else { // intersection / difference: the LEFT child's ball + certificate suffice
540 comb = a;
541 }
542 // apply this node's own transform to the child-frame bound
543 comb.c =
544 add(rotate(n.transform.rotation, scale(comb.c, n.transform.scale)), n.transform.translation);
545 comb.r *= n.transform.scale;
546 return comb;
547}
548
549} // namespace query_detail
550
552template <class Real>
555 if (i < 0 || i >= sc.instanceCount)
556 return ib;
557 const Instance<Real>& inst = sc.instances[i];
559 ib.c = add(rotate(inst.transform.rotation, scale(nb.c, inst.transform.scale)),
560 inst.transform.translation);
561 ib.r = inst.transform.scale * nb.r;
562 ib.certified = nb.certified && nb.r < Real(1e29);
563 return ib;
564}
565
569template <class Real>
571 const CandidateGridView<Real>& g, const Real* boundR = nullptr) {
572 Real m = Real(1e9);
573 for (int k = 0; k < g.alwaysCount; ++k) {
574 const int i = g.always[k];
575 const Real d = evalInstancePeriodic(sc, i, p, box, boundR ? boundR[i] : Real(-1));
576 if (d < m)
577 m = d;
578 }
579 const long b = g.binOf(p);
580 if (b < 0) { // out of coverage: full scan of the certified instances too
581 const Real full = evalScenePeriodic(sc, p, box, boundR);
582 return full < m ? full : m;
583 }
584 const int lo = g.offsets[b], hi = g.offsets[b + 1];
585 if (lo == hi) {
586 // An EMPTY certified list means no certified instance's band reached this bin — the far
587 // field — where a certified instance may still be the minimum. Fall back to the full scan
588 // (the always-list min in m composes via min; re-evaluating those instances is harmless).
589 // Returning only the always-list here was a bug: probes in uncovered bins got the distance
590 // to the nearest UNCERTIFIED body only.
591 const Real full = evalScenePeriodic(sc, p, box, boundR);
592 return full < m ? full : m;
593 }
594 for (int k = lo; k < hi; ++k) {
595 const int i = g.items[k];
596 const Real d = evalInstancePeriodic(sc, i, p, box, boundR ? boundR[i] : Real(-1));
597 if (d < m)
598 m = d;
599 }
600 return m;
601}
602
608template <class Real>
610 Vec3<Real> extent, const PeriodicBox<Real>& box,
611 Real binSizeHint = 0) {
613 auto& m = g.meta;
614 // default bin ~ half the median certified bounding radius (mirrors the sphere build's target)
615 std::vector<InstanceBound<Real>> ib((std::size_t)sc.instanceCount);
616 g.instBoundR.resize((std::size_t)sc.instanceCount);
617 Real rSum = 0;
618 int nCert = 0;
619 for (int i = 0; i < sc.instanceCount; ++i) {
620 ib[(std::size_t)i] = instanceBound(sc, i);
621 g.instBoundR[(std::size_t)i] = ib[(std::size_t)i].r;
622 if (ib[(std::size_t)i].certified) {
623 rSum += ib[(std::size_t)i].r;
624 ++nCert;
625 } else {
626 g.always.push_back(i);
627 }
628 }
629 const Real rMean = nCert ? rSum / nCert : extent.x / 8;
630 const Real bin = binSizeHint > 0 ? binSizeHint : std::fmax(rMean * Real(0.5), extent.x / 96);
631 auto nbAxis = [&](Real L) {
632 int nb = static_cast<int>(L / bin);
633 return nb < 2 ? 2 : (nb > 96 ? 96 : nb);
634 };
635 m.nx = nbAxis(extent.x);
636 m.ny = nbAxis(extent.y);
637 m.nz = nbAxis(extent.z);
638 m.ox = origin.x;
639 m.oy = origin.y;
640 m.oz = origin.z;
641 m.bx = extent.x / m.nx;
642 m.by = extent.y / m.ny;
643 m.bz = extent.z / m.nz;
644 m.wrap = box.on;
645 const long nbins = (long)m.nx * m.ny * m.nz;
646 const Real halfDiag = Real(0.5) * std::sqrt(m.bx * m.bx + m.by * m.by + m.bz * m.bz);
647 const Real slack = Real(1e-12) * (extent.x + extent.y + extent.z);
648
649 auto binCenter = [&](long b) {
650 const long i = b % m.nx, j = (b / m.nx) % m.ny, k = b / ((long)m.nx * m.ny);
651 return Vec3<Real>{m.ox + (i + Real(0.5)) * m.bx, m.oy + (j + Real(0.5)) * m.by,
652 m.oz + (k + Real(0.5)) * m.bz};
653 };
654 auto forEachBinNear = [&](const Vec3<Real>& c, Real reach, auto&& fn) {
655 const int riX = static_cast<int>(reach / m.bx) + 1;
656 const int riY = static_cast<int>(reach / m.by) + 1;
657 const int riZ = static_cast<int>(reach / m.bz) + 1;
658 const int ci = static_cast<int>(std::floor((c.x - m.ox) / m.bx));
659 const int cj = static_cast<int>(std::floor((c.y - m.oy) / m.by));
660 const int ck = static_cast<int>(std::floor((c.z - m.oz) / m.bz));
661 for (int dk = -riZ; dk <= riZ; ++dk)
662 for (int dj = -riY; dj <= riY; ++dj)
663 for (int di = -riX; di <= riX; ++di) {
664 long ii = ci + di, jj = cj + dj, kk = ck + dk;
665 if (m.wrap) {
666 ii = ((ii % m.nx) + m.nx) % m.nx;
667 jj = ((jj % m.ny) + m.ny) % m.ny;
668 kk = ((kk % m.nz) + m.nz) % m.nz;
669 } else if (ii < 0 || ii >= m.nx || jj < 0 || jj >= m.ny || kk < 0 || kk >= m.nz) {
670 continue;
671 }
672 fn(ii + (long)m.nx * (jj + (long)m.ny * kk));
673 }
674 };
675 // Certified instances are 1-Lipschitz (their periodic eval is the true torus distance to the
676 // body), so at a bin center bc: upper(B) = eval(bc) + halfDiag, lower(B) = eval(bc) - halfDiag.
677 //
678 // COVERAGE MUST BE TOTAL. A bounded per-instance splat reach reproduces the far-field bug this
679 // replaced: a bin beyond instance i's reach never membership-tests i, so its (non-empty) list
680 // can miss the true argmin — measured as O(1e-2) absolute distance errors in the far field.
681 // Every (bin, certified instance) pair is therefore evaluated once, row-cached per bin:
682 // U(B) = min_i (e_i + hd), keep i iff e_i - hd <= U(B). Cost = nbins x nCertified tree walks —
683 // fine for stirrer-scale scenes (~1e6 walks); MANY-instance scenes belong on the sphere-union
684 // path, and the build() wrapper refuses to build a general grid beyond a cost cap rather than
685 // silently taking minutes.
686 std::vector<int> certIdx;
687 certIdx.reserve((std::size_t)nCert);
688 for (int i = 0; i < sc.instanceCount; ++i)
689 if (ib[(std::size_t)i].certified)
690 certIdx.push_back(i);
691 std::vector<Real> row(certIdx.size());
692 std::vector<std::vector<int>> lists((std::size_t)nbins);
693 for (long b = 0; b < nbins; ++b) {
694 const Vec3<Real> bc = binCenter(b);
695 Real U = Real(1e300);
696 for (std::size_t k = 0; k < certIdx.size(); ++k) {
697 const int i = certIdx[k];
698 row[k] = evalInstancePeriodic(sc, i, bc, box, ib[(std::size_t)i].r);
699 const Real up = row[k] + halfDiag;
700 if (up < U)
701 U = up;
702 }
703 for (std::size_t k = 0; k < certIdx.size(); ++k)
704 if (row[k] - halfDiag <= U + slack)
705 lists[(std::size_t)b].push_back(certIdx[k]);
706 }
707 g.offsets.resize((std::size_t)nbins + 1);
708 g.offsets[0] = 0;
709 for (long b = 0; b < nbins; ++b)
710 g.offsets[(std::size_t)b + 1] =
711 g.offsets[(std::size_t)b] + static_cast<int>(lists[(std::size_t)b].size());
712 g.items.resize((std::size_t)g.offsets[(std::size_t)nbins]);
713 for (long b = 0; b < nbins; ++b)
714 std::copy(lists[(std::size_t)b].begin(), lists[(std::size_t)b].end(),
715 g.items.begin() + g.offsets[(std::size_t)b]);
716 return g;
717}
718
719// ---------------------------------------------------------------------------------------------
720// Cut ownership (Layer 3 rung 1 of suite/docs/ANALYTIC_SDF_GEOMETRY.md)
721//
722// "Which body owns the surface nearest to p" — the attribution a moving-geometry solver needs to
723// read a wall velocity off the right instance, and a resolved CFD-DEM solver needs to post a
724// hydrodynamic force back to the right grain.
725//
726// DETERMINISM CONTRACT. The winner is the argmin of exactly the value `eval` returns, and ties
727// break to the LOWEST index — enforced by the comparison itself (`v < best || (v == best && i <
728// best_i)`), NOT by scan order. That matters because the same point can be answered through three
729// different orders: a candidate list, the always-list + candidate list, or the full-scan fallback.
730// All three therefore agree, and a per-bin shuffle of the candidate lists cannot change the owner.
731//
732// The equal-radius sphere path carries SQUARED center distances through the scan and converts
733// once at the end, exactly as evalSphereUnion does — so `evalOwner` returns a value BITWISE equal
734// to `eval`'s, and the argmin is the same one sqrt(.) - r0 would have chosen (it is strictly
735// increasing in d2).
736//
737// EVERY owner path is written as an eval-AND-owner primitive, with `owner()` discarding the value.
738// One traversal answers both, which is what keeps a per-cell owner field free for a consumer that
739// is sampling the scene anyway (flow's set_solid_from_scene).
740// ---------------------------------------------------------------------------------------------
741
742namespace owner_detail {
744template <class Real>
745PECLET_HD void take(Real v, int i, Real& best, int& bestI) {
746 if (v < best || (v == best && i < bestI)) {
747 best = v;
748 bestI = i;
749 }
750}
751} // namespace owner_detail
752
756template <class Real>
758 const PeriodicBox<Real>& box, const int* items, int count,
759 Real& best, int& bestI) {
760 if (u.equalR) {
761 for (int k = 0; k < count; ++k) {
762 const int i = items[k];
763 owner_detail::take(sphereDist2(u, i, p, box), i, best, bestI);
764 }
765 return;
766 }
767 for (int k = 0; k < count; ++k) {
768 const int i = items[k];
770 bestI);
771 }
772}
773
776template <class Real>
778 const PeriodicBox<Real>& box, int& own) {
779 Real best = Real(1e300);
780 int bestI = -1;
781 if (u.equalR) {
782 for (int i = 0; i < u.n; ++i)
783 owner_detail::take(sphereDist2(u, i, p, box), i, best, bestI);
784 own = bestI;
786 }
787 for (int i = 0; i < u.n; ++i)
789 bestI);
790 own = bestI;
791 return best;
792}
793
796template <class Real>
798 const PeriodicBox<Real>& box,
799 const CandidateGridView<Real>& g, int& own) {
800 const long b = g.binOf(p);
801 if (b < 0)
802 return sphereUnionEvalOwner(u, p, box, own);
803 const int lo = g.offsets[b], hi = g.offsets[b + 1];
804 if (lo == hi)
805 return sphereUnionEvalOwner(u, p, box, own);
806 Real best = Real(1e300);
807 int bestI = -1;
808 sphereUnionOwnerSubset(u, p, box, g.items + lo, hi - lo, best, bestI);
809 own = bestI;
811}
812
814template <class Real>
816 const PeriodicBox<Real>& box, int& own,
817 const Real* boundR = nullptr) {
818 Real best = Real(1e9);
819 int bestI = -1;
820 for (int i = 0; i < sc.instanceCount; ++i)
822 bestI);
823 own = bestI;
824 return best;
825}
826
830template <class Real>
832 const PeriodicBox<Real>& box, const CandidateGridView<Real>& g,
833 int& own, const Real* boundR = nullptr) {
834 Real best = Real(1e9);
835 int bestI = -1;
836 for (int k = 0; k < g.alwaysCount; ++k) {
837 const int i = g.always[k];
839 bestI);
840 }
841 const long b = g.binOf(p);
842 const int lo = b < 0 ? 0 : g.offsets[b], hi = b < 0 ? 0 : g.offsets[b + 1];
843 if (b < 0 || lo == hi) {
844 for (int i = 0; i < sc.instanceCount; ++i)
846 best, bestI);
847 own = bestI;
848 return best;
849 }
850 for (int k = lo; k < hi; ++k) {
851 const int i = g.items[k];
853 bestI);
854 }
855 own = bestI;
856 return best;
857}
858
860template <class Real>
862 const PeriodicBox<Real>& box) {
863 int own = -1;
864 (void)sphereUnionEvalOwner(u, p, box, own);
865 return own;
866}
867template <class Real>
869 const PeriodicBox<Real>& box, const CandidateGridView<Real>& g) {
870 int own = -1;
871 (void)sphereUnionEvalOwnerGrid(u, p, box, g, own);
872 return own;
873}
874template <class Real>
876 const PeriodicBox<Real>& box, const Real* boundR = nullptr) {
877 int own = -1;
879 return own;
880}
881template <class Real>
883 const CandidateGridView<Real>& g, const Real* boundR = nullptr) {
884 int own = -1;
885 (void)sceneEvalOwnerGrid(sc, p, box, g, own, boundR);
886 return own;
887}
888
889// ---------------------------------------------------------------------------------------------
890// Rigid-body instance motion (Layer 3 rung 2 of suite/docs/ANALYTIC_SDF_GEOMETRY.md)
891//
892// The kinematic half of moving geometry: given the owner of a wall point (above), what velocity
893// does that wall carry? Kept here, beside the query it pairs with, so every consumer -- flow's
894// staggered momentum operator today, the AMR solver when its campaign resumes -- reads the same
895// rigid-body law instead of re-deriving it.
896// ---------------------------------------------------------------------------------------------
897
902template <class Real>
904 const Real* cen = nullptr; // 3*n world centres of rotation
905 const Real* lin = nullptr; // 3*n linear velocities
906 const Real* ang = nullptr; // 3*n angular velocities
907 int n = 0;
908};
909
918template <class Real>
920 const PeriodicBox<Real>& box) {
921 Vec3<Real> v{0, 0, 0};
922 if (i < 0 || i >= m.n)
923 return v;
924 if (m.lin) {
925 v.x = m.lin[3 * i + 0];
926 v.y = m.lin[3 * i + 1];
927 v.z = m.lin[3 * i + 2];
928 }
929 if (!m.ang || !m.cen)
930 return v;
931 const Real wx = m.ang[3 * i + 0], wy = m.ang[3 * i + 1], wz = m.ang[3 * i + 2];
932 if (wx == Real(0) && wy == Real(0) && wz == Real(0))
933 return v;
934 const Vec3<Real> r = minImage(
935 Vec3<Real>{p.x - m.cen[3 * i + 0], p.y - m.cen[3 * i + 1], p.z - m.cen[3 * i + 2]}, box);
936 v.x += wy * r.z - wz * r.y;
937 v.y += wz * r.x - wx * r.z;
938 v.z += wx * r.y - wy * r.x;
939 return v;
940}
941
949template <class Real>
951 namespace cd = peclet::core::detail;
952 const Real g2 = cd::hdFma(grad.z, grad.z, cd::hdFma(grad.y, grad.y, grad.x * grad.x));
953 if (!(g2 > Real(0)))
954 return p;
955 const Real s = sdf / cd::hdSqrt(g2);
956 return Vec3<Real>{p.x - s * grad.x, p.y - s * grad.y, p.z - s * grad.z};
957}
958
961template <class Real>
963 if (m.n <= 0)
964 return true;
965 for (int i = 0; i < 3 * m.n; ++i) {
966 if (m.lin && m.lin[i] != Real(0))
967 return false;
968 if (m.ang && m.ang[i] != Real(0))
969 return false;
970 }
971 return true;
972}
973
979template <class Real>
981 SphereUnionView<Real> u{}; // u.n > 0 => sphere fast path
982 SceneView<Real> scene{}; // otherwise: general scene
983 CandidateGridView<Real> grid{}; // offsets == nullptr => no acceleration
985 const Real* instBoundR = nullptr; // per-instance bounds (general periodic seam handling)
986
988 if (u.n > 0)
989 return grid.offsets ? evalSphereUnionGrid(u, p, box, grid) : evalSphereUnion(u, p, box);
990 return grid.offsets ? evalSceneGrid(scene, p, box, grid, instBoundR)
992 }
993
999 if (u.n > 0)
1000 return grid.offsets ? sphereUnionEvalOwnerGrid(u, p, box, grid, own)
1001 : sphereUnionEvalOwner(u, p, box, own);
1002 return grid.offsets ? sceneEvalOwnerGrid(scene, p, box, grid, own, instBoundR)
1004 }
1005
1008 int own = -1;
1009 (void)evalOwner(p, own);
1010 return own;
1011 }
1012};
1013
1017template <class Real>
1018inline bool extractSphereUnion(const SceneView<Real>& sc, std::vector<Real>& cx,
1019 std::vector<Real>& cy, std::vector<Real>& cz, std::vector<Real>& r) {
1020 cx.clear();
1021 cy.clear();
1022 cz.clear();
1023 r.clear();
1024 auto identity = [](const Transform<Real>& t, bool allowTranslate) {
1025 const bool q = t.rotation.x == Real(0) && t.rotation.y == Real(0) && t.rotation.z == Real(0) &&
1026 t.rotation.w == Real(1) && t.scale == Real(1);
1027 const bool tr = allowTranslate || (t.translation.x == Real(0) && t.translation.y == Real(0) &&
1028 t.translation.z == Real(0));
1029 return q && tr;
1030 };
1031 for (int i = 0; i < sc.instanceCount; ++i) {
1032 const Instance<Real>& inst = sc.instances[i];
1033 if (inst.shapeRoot < 0 || inst.shapeRoot >= sc.nodeCount)
1034 return false;
1035 const ShapeNode<Real>& n = sc.nodes[inst.shapeRoot];
1036 if (n.kind != kSphere || !identity(n.transform, false) || !identity(inst.transform, true))
1037 return false;
1038 cx.push_back(inst.transform.translation.x);
1039 cy.push_back(inst.transform.translation.y);
1040 cz.push_back(inst.transform.translation.z);
1041 r.push_back(n.params[0]);
1042 }
1043 return !cx.empty();
1044}
1045
1046// ---------------------------------------------------------------------------------------------
1047// The callable that retires the AMR stopgap
1048// ---------------------------------------------------------------------------------------------
1049
1054 public:
1055 SphereBedQuery(std::vector<double> cx, std::vector<double> cy, std::vector<double> cz,
1056 std::vector<double> r, Vec<3> origin, Vec<3> extent, bool periodic, int nbHint = 0)
1057 : cx_(std::move(cx)), cy_(std::move(cy)), cz_(std::move(cz)), r_(std::move(r)) {
1058 const int n = static_cast<int>(cx_.size());
1059 if (n == 0 || cy_.size() != cx_.size() || cz_.size() != cx_.size() || r_.size() != cx_.size())
1060 throw std::invalid_argument("SphereBedQuery: centers/radii sizes disagree");
1061 box_.on = periodic;
1062 box_.Lx = extent[0];
1063 box_.Ly = extent[1];
1064 box_.Lz = extent[2];
1065 union_.cx = cx_.data();
1066 union_.cy = cy_.data();
1067 union_.cz = cz_.data();
1068 union_.r = r_.data();
1069 union_.n = n;
1070 union_.equalR = true;
1071 for (int i = 1; i < n; ++i)
1072 if (r_[i] != r_[0])
1073 union_.equalR = false;
1074 union_.r0 = r_[0];
1075 grid_ = buildSphereCandidateGrid(union_, Vec3<double>{origin[0], origin[1], origin[2]},
1076 Vec3<double>{extent[0], extent[1], extent[2]}, box_, nbHint);
1077 gview_ = grid_.view();
1078 }
1079
1080 double operator()(const Vec<3>& p) const {
1081 return evalSphereUnionGrid(union_, Vec3<double>{p[0], p[1], p[2]}, box_, gview_);
1082 }
1083
1084 const SphereUnionView<double>& sphereUnion() const { return union_; }
1085 const PeriodicBox<double>& box() const { return box_; }
1086 const CandidateGridView<double>& grid() const { return gview_; }
1088 double meanListLength() const {
1089 return grid_.offsets.empty() ? 0.0
1090 : double(grid_.items.size()) / double(grid_.offsets.size() - 1);
1091 }
1092
1093 private:
1094 std::vector<double> cx_, cy_, cz_, r_;
1096 PeriodicBox<double> box_{};
1099};
1100
1101} // namespace peclet::core::geom
1102
1103#endif // PECLET_CORE_GEOM_SCENE_QUERY_HPP
Owning, host-side accelerated union-of-spheres query.
const SphereUnionView< double > & sphereUnion() const
double meanListLength() const
Mean candidate-list length (diagnostics for the acceptance report).
SphereBedQuery(std::vector< double > cx, std::vector< double > cy, std::vector< double > cz, std::vector< double > r, Vec< 3 > origin, Vec< 3 > extent, bool periodic, int nbHint=0)
double operator()(const Vec< 3 > &p) const
const PeriodicBox< double > & box() const
const CandidateGridView< double > & grid() const
PECLET_HD Real hdFloor(Real v)
Definition portable.hpp:167
PECLET_HD Real hdSqrt(Real v)
Definition portable.hpp:143
PECLET_HD void take(Real v, int i, Real &best, int &bestI)
The single tie-break rule (lowest index wins an exact tie), shared by every owner path.
NodeBound< Real > nodeBound(const SceneView< Real > &sc, int node)
PECLET_HD Real evalSphereUnionGrid(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g)
Union SDF through the candidate grid: bin lookup + short list, full-scan fallback when the bin is out...
PECLET_HD Real sphereUnionEvalOwner(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, int &own)
Brute-force value + owner (all spheres) — the reference the candidate path must match,...
PECLET_HD int sphereUnionOwner(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box)
Owner-only wrappers (the value is discarded by the compiler on every backend).
PECLET_HD Real evalSceneGrid(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g, const Real *boundR=nullptr)
Union over a candidate list + the always-list, min-image periodic.
CandidateGrid< Real > buildSceneCandidateGrid(const SceneView< Real > &sc, Vec3< Real > origin, Vec3< Real > extent, const PeriodicBox< Real > &box, Real binSizeHint=0)
Candidate grid for a GENERAL scene: certified instances splat Lipschitz bounds evaluated at bin cente...
PECLET_HD Real sphereDist2(const SphereUnionView< Real > &u, int i, Vec3< Real > p, const PeriodicBox< Real > &box)
Squared min-image distance from p to sphere i — THE per-sphere expression, shared by every path (brut...
bool extractSphereUnion(const SceneView< Real > &sc, std::vector< Real > &cx, std::vector< Real > &cy, std::vector< Real > &cz, std::vector< Real > &r)
Try to view a scene as a PLAIN sphere union: every instance a single kSphere leaf with a bitwise-iden...
InstanceBound< Real > instanceBound(const SceneView< Real > &sc, int i)
World bounding sphere + certificate of one instance (host-side; used by candidate builds).
PECLET_HD void sphereUnionOwnerSubset(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, const int *items, int count, Real &best, int &bestI)
Accumulate the argmin over an explicit index subset (a candidate list) into (best,...
PECLET_HD Real sphereUnionEvalOwnerGrid(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g, int &own)
Value + owner through the candidate grid.
bool motionIsStatic(const InstanceMotionView< Real > &m)
True when no instance carries motion – the predicate a consumer uses to keep the static path bit-iden...
@ kSphere
params[0] = radius
Definition scene.hpp:37
@ kEllipsoid
params[0..2] = rx, ry, rz (BOUND, under-estimates)
Definition scene.hpp:44
@ kTorus
params[0..1] = majorRadius, minorRadius (exact, y axis)
Definition scene.hpp:42
@ kBox
params[0..2] = hx, hy, hz
Definition scene.hpp:39
@ kSuperquadric
params[0..3] = rx, ry, rz, exponent (BOUND, under-estimates)
Definition scene.hpp:45
@ kHollowCylinderShell
params[0..2] = rOuter, rInner, height (sign-exact, z axis)
Definition scene.hpp:40
@ kCapsule
params[0..1] = radius, halfLength (exact, y axis)
Definition scene.hpp:41
@ kCone
params[0..2] = rBottom, rTop, halfHeight (exact, y axis)
Definition scene.hpp:43
@ kHollowCylinder
params[0..2] = rOuter, height, thickness (distance-exact, y axis)
Definition scene.hpp:38
@ kUnion
min(L, R) — aux0/aux1 = child node indices
Definition scene.hpp:48
PECLET_HD Real evalScenePeriodic(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, const Real *boundR=nullptr)
Union over every instance, min-image periodic.
PECLET_HD Real evalInstancePeriodic(const SceneView< Real > &sc, int i, Vec3< Real > p, const PeriodicBox< Real > &box, Real boundR=Real(-1))
Instance evaluation with MIN-IMAGE periodicity.
PECLET_HD Vec3< Real > instanceVelocity(const Instance< Real > &inst, Vec3< Real > p)
Definition scene.hpp:219
PECLET_HD Real sceneEvalOwnerPeriodic(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, int &own, const Real *boundR=nullptr)
Value + owner over every instance of a general scene, min-image periodic.
PECLET_HD Vec3< Real > wallPoint(Vec3< Real > p, Real sdf, Vec3< Real > grad)
Project a probe onto the zero level set along the local gradient: w = p - sdf(p) * n_hat.
PECLET_HD int sceneOwnerPeriodic(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, const Real *boundR=nullptr)
PECLET_HD int sphereUnionOwnerGrid(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g)
PECLET_HD Vec3< Real > minImage(Vec3< Real > d, const PeriodicBox< Real > &b)
Min-image a displacement.
PECLET_HD Real evalSphereUnion(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box)
Brute-force union SDF (all spheres) — the reference the candidate path must match bit-for-bit,...
PECLET_HD Real evalSphereUnionSubset(const SphereUnionView< Real > &u, Vec3< Real > p, const PeriodicBox< Real > &box, const int *items, int count)
Union SDF over an index subset (a candidate list).
PECLET_HD int sceneOwnerGrid(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g, const Real *boundR=nullptr)
PECLET_HD Real sceneEvalOwnerGrid(const SceneView< Real > &sc, Vec3< Real > p, const PeriodicBox< Real > &box, const CandidateGridView< Real > &g, int &own, const Real *boundR=nullptr)
Value + owner through a general-scene candidate grid: always-list ∪ candidate list,...
CandidateGrid< Real > buildSphereCandidateGrid(const SphereUnionView< Real > &u, Vec3< Real > origin, Vec3< Real > extent, const PeriodicBox< Real > &box, int nbHint=0)
Geometry-driven candidate build for a sphere union over [origin, origin+extent).
PECLET_HD Vec3< Real > rotate(Quat< Real > q, Vec3< Real > v)
Rotate v by q.
Definition portable.hpp:81
PECLET_HD Vec3< Real > add(Vec3< Real > a, Vec3< Real > b)
Definition portable.hpp:58
PECLET_HD Vec3< Real > invRotate(Quat< Real > q, Vec3< Real > v)
Rotate v by the conjugate of q — dem invRotateVector.
Definition portable.hpp:89
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
PECLET_HD Vec3< Real > sub(Vec3< Real > a, Vec3< Real > b)
Definition portable.hpp:62
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
#define PECLET_HD
Definition portable.hpp:35
Non-owning candidate grid: uniform bins over [origin, origin+extent), CSR lists of primitive (or inst...
PECLET_HD long binOf(Vec3< Real > p) const
Flat bin index of a point, or -1 when the point is outside a non-periodic grid's coverage (the caller...
Owning host-side candidate grid.
CandidateGridView< Real > meta
CandidateGridView< Real > view() const
World bounding sphere of an instance + the PRUNING CERTIFICATE.
Non-owning, POD, device-copyable rigid-body motion state for a scene's instances.
Min-image box. on = false disables wrapping (all extents ignored).
The one POD every consumer captures: sphere-union fast path when the scene is a plain sphere union,...
PECLET_HD int owner(Vec3< Real > p) const
Owner only.
PECLET_HD Real evalOwner(Vec3< Real > p, int &own) const
eval(p) AND the index of the instance (sphere-union mode: the sphere) whose surface is nearest — the ...
PECLET_HD Real eval(Vec3< Real > p) const
CandidateGridView< Real > grid
int aux0
CSG: left child index. kGrid: GridDesc table index.
Definition scene.hpp:103
Transform< Real > transform
Definition scene.hpp:106
int aux1
CSG: right child index.
Definition scene.hpp:104
Non-owning union-of-spheres view.