Add PointCloudHashGrid: uniform-grid counterpart to PointCloudBVH#108
Conversation
Dev/ held the four superseded point-cloud examples (replaced by ClosestPointBVH/NearestNeighborBVH on this branch) and the GridNN prototype (promoted to the PointCloudHashGrid class, PR #108). It was always marked DELETE_BEFORE_MERGE; removing it now, along with the temporary Dev/** annotation block in REUSE.toml. reuse lint stays compliant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
a1f0b7b to
2309355
Compare
…issue #107) PointCloudHashGrid answers the same nearest-neighbor / closest-point queries over a particle cloud as PointCloudBVH and exposes the same public interface (same Hit, same closestPoint/closestPoints/nearestNeighbor/nearestNeighbors/allNearestNeighbors, same O(N) brute-force references and position()/metadata() accessors), so the two are drop-in interchangeable -- but it circumvents the tree entirely. Points are counting-sorted into a dense CSR bucket array keyed by integer cell coordinates (an O(N) build, no tree). Queries are an expanding-shell search from the query point's cell with an exact stopping rule -- after searching Chebyshev radius R the world region within the covered faces is fully searched, so once the k-th best distance is inside that bound no neighbor can be missed. Handles external queries and queries outside the grid box (clamped cell). k>1 carries a sorted k-best set through the shells. For a near-uniform 500k cloud this builds ~5x faster and queries ~2x faster than PointCloudBVH (Examples/NearestNeighborHashGrid: build ~16 ms, query ~0.29 us/pt vs the BVH's ~76 ms / ~0.55). The class docs and ImplemBVH.rst spell out the trade-off: a global cell size only suits near-uniform density, the dense grid is bounded-domain (O(bboxVol/h^3) cells), and it is not a BVH (no composition into an outer BVH/CSG) -- prefer PointCloudBVH for clustered clouds, external-heavy workloads, and composition. Adds the class (header + implem), EBGeometry.hpp include, InstantiateAll coverage, TestPointCloudHashGrid (independent brute-force oracle, both precisions, incl. the outside-the-box case), the NearestNeighborHashGrid example, and docs. Follow-up to PointCloudBVH; the Dev/GridNN prototype this grew out of is superseded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
Match the terse, intrinsic style of the sibling example READMEs: drop the speedup-extrapolation and the detailed grid-vs-tree performance breakdown, keeping the functional description, the brief intrinsic guidance on when the grid vs the BVH fits, and the brute-force correctness check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
Both TestPointCloudBVH.cpp and TestPointCloudHashGrid.cpp declared the comparison margin as `const T tol = tightMargin<T>()`, but tightMargin<T>() returns double and withinAbsT()'s margin parameter is double -- so with T=float every withinAbsT(target, tol) promoted tol float->double at the call site, which clang's -Wdouble-promotion flags under -Werror (the Sanitizers CI job, EBGEOMETRY_TEST_BOTH_PRECISIONS). Declare tol as double in both. Verified: both build clean under clang++-18 with both precisions and -Wdouble-promotion -Werror, and still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
…on page Mirror the BVH/SFC docs structure: PointCloud.rst becomes a Concepts page describing point-cloud proximity queries and the two acceleration ideas (a hierarchical tree vs a uniform grid) without naming implementation classes, and a new ImplemPointCloud.rst is the Implementation page -- a short shared-interface intro followed by two sections, PointCloudBVH and PointCloudHashGrid. - PointCloud.rst: rewritten as concepts; relabelled Chap:PointCloud and moved to the Concepts toctree (after BVH). - ImplemPointCloud.rst: new; keeps the Chap:ImplemPointCloud label (Examples.rst links to it) and takes the Implementation toctree slot PointCloud.rst used to hold. The HashGrid section that was temporarily appended to PointCloud.rst during the rebase now lives here as its own section, and the migrated PointCloudBVH prose is updated from "particle" to "point" terminology. - index.rst: toctrees updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
d53308d to
0a97c6d
Compare
…tch PointCloudBVH) Bring PointCloudHashGrid in line with the audited PointCloudBVH: - Doc style: convert the class's Qt-style /*! ... */ blocks and //!< trailing docs to the JavaDoc /** ... */ and ///< used everywhere else in Source/; expand one-line doc blocks to multi-line and fully document the private cell/query/brute-force helpers. - Terminology: manages points, not particles -- rename throughout, including public API (numParticles() -> numPoints()) and the self-query index parameter (a_particle -> a_point). Update the test call site and the example comments accordingly. - Debug hooks: static_assert (floating-point T) and EBGEOMETRY_EXPECT preconditions throughout the implem -- finite input/query coordinates, index bounds, a_k >= 1, non-null out buffer, exclude-index validity, and per-cell coordinate/index invariants. - Hit gains the same not-found-sentinel note as PointCloudBVH::Hit; brute-force methods carry an explicit @warning (debug/testing only); the example follows the blank-line-around-loops convention. TestPointCloudHashGrid passes (2971 assertions, debug/assertions-on); InstantiateAll compiles; the example compiles clean under g++ and clang in both precisions and runs correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
… BVH example Scrub the internals and mechanism prose (point count, unit cube, seed, kNN knob, code snippet, squared-distance/hot-path detail) so the README mirrors the terse, user-oriented Examples/NearestNeighborBVH README -- a short description, the near-uniform-vs-clustered guidance, and a terse Running section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
|
@claude review |
|
Claude finished @rmrsk's task in 4m 54s —— View job Claude's ReviewTasks
Overall this is a clean, well-designed addition. The counting-sort build, the CSR bucket layout, and the expanding-shell query with the exact Chebyshev stopping bound are all correct as far as I traced them (including the clamped-cell handling for out-of-box queries, and the "hollow shell" cell-enumeration trick, which I hand-verified against several boundary-clamped cases). The public interface genuinely mirrors One correctness/robustness concern worth a look before merge, plus a minor test-coverage gap: 1. Unbounded/UB grid-dimension computation for non-uniform bounding boxes
m_h = std::cbrt(vol / T(numPoints) * a_targetPerCell);
...
m_nx = std::max(1, int((hi[0] - m_lo[0]) * m_invH) + 1);
The PR description's "Side-effects" section documents the general wasteful-memory case for a sparse cloud spread over a large box, but this is a sharper failure mode: a single outlier can turn an otherwise-uniform, well-behaved cloud into a crash, and there's currently no 2. No test coverage for degenerate/non-uniform clouds
The test suite (like Minor
Nothing else stood out — the query logic, self-query exclusion, brute-force references, and doc cross-references all look solid. |
…HashGrid assertions - Member variables: replace the trailing ///< docs on the m_ member variables of PointCloudBVH and PointCloudHashGrid with /** @brief ... */ block comments (Hit and BuildResult keep their trailing docs). - Include guards: uppercase the guards on the point-cloud headers to match the house convention (EBGEOMETRY_<NAME>_HPP), e.g. EBGeometry_PointCloudBVH -> EBGEOMETRY_POINTCLOUDBVH_HPP; all four point-cloud headers were the only C++ files in the tree whose guards were not all-caps. - PointCloudHashGrid: add the remaining build-path EBGEOMETRY_EXPECTs -- the cloud fits uint32_t cell storage, the derived cell size and grid dimensions are positive, and the counting sort is consistent (m_cellStart[nCells] == numPoints). Both point-cloud test suites pass (assertions on); InstantiateAll compiles; all touched files are clang-format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
…page Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
…-case tests Audit follow-ups: - Exact stopping rule: the covered-cell face was reconstructed with m_h while cell membership uses m_invH (a rounded reciprocal of m_h), so a face at the searched-box boundary could be off by a fraction of a cell and make the prune bound marginally optimistic (a ~1-ULP risk of stopping one shell early). Pull each guaranteed-covered face one whole cell inside the searched box -- that full cell of slack dwarfs the sub-ULP reconstruction error for any realistic grid -- and only evaluate the bound for r >= 1 (so the query cell lies strictly inside the box). Costs at most one extra shell. - Grid size: a pathologically small a_targetPerCell drove the cell size toward zero, blowing up the dense cell array and risking int overflow in the grid dimensions. Enforce a minimum cell size that keeps the total cell count within a budget proportional to the point count; coarsening changes only resolution, never results (the query is exact for any cell size). - Tests: add a PointCloudHashGrid edge-cases case mirroring the BVH one -- empty cloud (not-found sentinel), single point, coincident points (one cell), planar/zero-volume cloud, k > cloud size, and extreme cell-size targets (exercising the grid-size clamp) -- each checked against brute force. Passes in both precisions with assertions on (6058 assertions across 4 cases); clang-format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
|
@claude review |
|
Claude finished @rmrsk's task in 3m 40s —— View job Claude's ReviewTasks
This PR has evolved since the last
Remaining concern: the grid-size cap doesn't bound cell count for an anisotropic (clustered + outlier) cloud
The new cap only raises Worked example: 999 points uniform in
Concretely: None of the new "edge cases" or "extreme cell-size targets" tests exercise an anisotropic bounding box (they vary Suggest capping directly on the computed Minor
|
…review) The previous grid-size cap raised m_h only when it fell below an isotropic hMin = cbrt(vol/maxCells). As the review noted, that never engages for an anisotropic bounding box -- a tight cluster plus one distant outlier stretches the box along one axis, and both m_h and hMin scale with the same power of the outlier distance, so the clamp is a no-op while nx grows like D^(2/3): an unbounded m_cellStart allocation, or undefined behaviour once int((hi-lo)*m_invH) overflows int. Cap on the *actual* per-axis cell counts instead: coarsen m_h until the product of the (double-valued) per-axis counts fits a budget proportional to the point count, and derive the integer dimensions from those same double counts so the int casts cannot overflow. The budget is capped to INT_MAX so no single axis dimension can overflow int either. New debug assertions bound the converged cell count (predictedCells <= maxCells, nCells <= maxCells). Coarsening changes only grid resolution, never results (the query is exact for any cell size). Adds a regression test: a 999-point unit-cube cluster plus one outlier at x = 1e6. Before the fix this built an unbounded grid; now it stays bounded and every nearest-neighbor query (including the outlier's own) matches brute force, in both precisions. Passes both precisions with assertions on (6116 assertions across 4 cases); clang-format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS5LFQihKoMrE6nbPaQW9R
|
@claude review |
|
Claude finished @rmrsk's task in 1m 46s —— View job Claude's ReviewTasks
This round ( The fix is correct.
Test coverage: All previously-flagged items are now resolved:
I wasn't able to actually configure/build/run Nothing else stood out — the rest of the class (CSR build, expanding-shell query, self-query exclusion, docs) was already reviewed as solid in the prior two passes and is unchanged here. |
Summary
Adds
PointCloudHashGrid<T, Meta>, a uniform-grid alternative toPointCloudBVHthat answers thesame nearest-neighbor / closest-point queries through the identical interface (drop-in
interchangeable), and is faster to build and query on near-uniform clouds. Ships with its own
example and tests, and restructures the point-cloud documentation into a concepts page and an
implementation page covering both backends.
Closes #107.
Background
PointCloudBVH(merged in #106) gives turnkey nearest-neighbor / closest-point queries over a pointcloud, backed by a BVH. While finishing that work we prototyped a uniform spatial grid as an
alternative that circumvents the tree entirely, and found it clearly faster to build on near-uniform
clouds. Issue #107 proposed promoting that prototype into a first-class class mirroring
PointCloudBVH. This PR does so, on top ofmainnow that #106 has merged.Solution
Adds
PointCloudHashGrid<T, Meta>, which answers the same queries asPointCloudBVHand exposesthe same public interface — same
Hit, sameclosestPoint/closestPoints/nearestNeighbor/
nearestNeighbors/allNearestNeighbors, sameO(N)brute-force reference queries, sameposition()/metadata()/numPoints()accessors — so the two are drop-in interchangeable.coordinates (
O(N), no tree, no recursion). Cell size is derived from a target average occupancy(~1 point/cell by default).
rule — after searching Chebyshev radius
R, the world region within the covered cell faces is fullysearched, so once the k-th best distance is inside that bound no neighbor can be missed. Handles
external queries and queries outside the grid box (clamped cell);
k > 1carries a sorted k-bestset through the shells; self-queries exclude the point itself.
On a near-uniform 500k cloud the grid builds and queries fast (see
Examples/NearestNeighborHashGridvs
Examples/NearestNeighborBVHfor measured numbers on your machine).Included alongside the class:
EBGeometry.hppinclude andTests/InstantiateAll.cppcoverage (both precisions).TestPointCloudHashGrid— checked against an independent brute-force oracle in both precisions,including the outside-the-box query case.
Examples/NearestNeighborHashGrid.PointCloud.rstbecomes a Concepts page (point-cloud proximity queries; tree vs uniform-grid acceleration), and
a new
ImplemPointCloud.rstis the Implementation page with two sections,PointCloudBVHandPointCloudHashGrid. Plus full doxygen.The class follows the same conventions as
PointCloudBVH: JavaDoc doc style,pointterminology, andEBGEOMETRY_EXPECT/static_assertdebug hooks (finite coordinates, index bounds, buffer /a_kvalidity, per-cell invariants).
Side-effects
PointCloudBVHit cannot be composed as a primitive inside anouter BVH/CSG union. It serves only point queries.
O(bboxVolume / h³)memory — fine for a compact cloud, wasteful for one sparse but spread over a large box.
clouds; for clustered / multi-scale clouds
PointCloudBVH's density adaptivity is the better choice— documented in the class comments,
PointCloud.rst/ImplemPointCloud.rst, and the example READMEso callers can pick.
PointCloud.rst, which documentedPointCloudBVH) is restructured —PointCloud.rstis now conceptual and the implementation prosemoves to the new
ImplemPointCloud.rst. TheChap:ImplemPointCloudlabel moves with it to the newpage (existing cross-references stay valid); a new
Chap:PointCloudlabel names the concepts page.Alternative solutions
PointCloudBVHandPointCloudHashGridso callers canselect a structure polymorphically. Deferred — the two share an interface by convention here; a
formal shared interface is a separate refactor (noted in Add a PointCloudHashGrid class: uniform-grid counterpart to PointCloudBVH for nearest-neighbor queries #107).
unordered_map<cellId, bucket>) instead of a dense array, to bound memoryto
O(N)on unbounded/sparse domains. Deferred; the dense grid is the fast bounded-domain case andthe natural first cut. The same query logic would apply.
general k-best path is already fast for k=1.
Reviewer checklist (to be completed by a human)
@claude review.