Skip to content

Fix divideTree assertion failure with KDTreeSingleIndexDynamicAdaptor removePoint/addPoints - #283

Merged
jlblancoc merged 1 commit into
jlblancoc:masterfrom
nh2:fix-dynamic-adaptor-assertion
May 25, 2026
Merged

jlblancoc merged 1 commit into
jlblancoc:masterfrom
nh2:fix-dynamic-adaptor-assertion

Conversation

@nh2

@nh2 nh2 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Disclosure: This is the outcome of me debugging an assertion failure (I encountered from 1.5.5 to 1.7.1) with the Claude Opus 4.6 LLM.

As far as I can tell, the minimal repro it generated for me makes sense.

I am not 100% confident about the fix to the assertion, so please do review that with extra scrutiny @yzabalotski and @jlblancoc (from #250).

LLM output below.


The assertion assert(left < obj.dataset_.kdtree_get_point_count()) added in commit

549a5e32 - Fix middleSplit_ for same points

(PR #250, released in v1.6.2) incorrectly fires when using KDTreeSingleIndexDynamicAdaptor with repeated removePoint/addPoints cycles on the same point indices.

Root cause: addPoints increments the internal pointCount_ unconditionally, even when re-adding a previously removed point. This causes sub-tree vAcc_ vectors to accumulate more entries than dataset_.kdtree_get_point_count(). When buildIndex() is called on such a sub-tree, it sets size_ = vAcc_.size() and calls divideTree(*this, 0, size_, ...). In recursive calls, left (an offset into vAcc_) can exceed kdtree_get_point_count(), triggering the assertion.

However, the actual point indices stored in vAcc_ are always valid (they are always < N). The assertion conflates the array offset with the dataset size, which is only a valid invariant for the static adaptor where vAcc_ is a permutation of [0, N).

Fix: Change the assertion to check what actually matters -- that the point index at vAcc_[left] is within bounds of the dataset.

Add a repro test that exercises repeated removePoint/addPoints cycles. That fails without the fix.

LLM: Found, repro'd, and fixed using Claude Opus 4.6, human review.

… removePoint/addPoints

The assertion `assert(left < obj.dataset_.kdtree_get_point_count())`
added in commit

    549a5e3 - Fix middleSplit_ for same points

(PR jlblancoc#250, released in v1.6.2) incorrectly fires when using
`KDTreeSingleIndexDynamicAdaptor` with repeated `removePoint`/`addPoints`
cycles on the same point indices.

Root cause: `addPoints` increments the internal `pointCount_` unconditionally,
even when re-adding a previously removed point. This causes sub-tree `vAcc_`
vectors to accumulate more entries than `dataset_.kdtree_get_point_count()`.
When `buildIndex()` is called on such a sub-tree, it sets
`size_ = vAcc_.size()` and calls `divideTree(*this, 0, size_, ...)`.
In recursive calls, `left` (an offset into `vAcc_`) can exceed
`kdtree_get_point_count()`, triggering the assertion.

However, the actual point *indices* stored in `vAcc_` are always valid
(they are always < N). The assertion conflates the array offset with the
dataset size, which is only a valid invariant for the static adaptor where
`vAcc_` is a permutation of `[0, N)`.

Fix: Change the assertion to check what actually matters -- that the point
index at `vAcc_[left]` is within bounds of the dataset.

Add a repro test that exercises repeated `removePoint`/`addPoints` cycles.
That fails without the fix.

LLM: Found, repro'd, and fixed using Claude Opus 4.6, human review.
@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.29%. Comparing base (ba47cfc) to head (e18f366).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #283      +/-   ##
==========================================
+ Coverage   83.85%   84.29%   +0.44%     
==========================================
  Files           1        1              
  Lines         675      675              
  Branches      127      128       +1     
==========================================
+ Hits          566      569       +3     
+ Misses        109      106       -3     
Files with missing lines Coverage Δ
include/nanoflann.hpp 84.29% <100.00%> (+0.44%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jlblancoc
jlblancoc merged commit 5e9cdeb into jlblancoc:master May 25, 2026
9 checks passed
pull Bot pushed a commit to daoran/nanoflann that referenced this pull request May 25, 2026
Re-adding a previously-removed point now reactivates the existing entry
in place instead of inserting a duplicate.

removePoint is a lazy deletion (treeIndex_[idx]=-1) that leaves the point
physically present in its sub-tree's vAcc_. Re-adding it via addPoints
re-inserted it: incrementing pointCount_, growing treeIndex_, and pushing
the index into vAcc_ again. Repeated remove/add cycles therefore caused
unbounded growth of the internal vectors, duplicate results in radius/knn
searches, and could eventually overflow treeCount_ (the assertion fixed in
PR jlblancoc#283 was an earlier casualty of the same growth).

treeIndex_'s numeric value is only ever compared to -1, so reactivation
only needs to restore a valid tree mark. removedPoints_ becomes a map from
index to the sub-tree that still holds it; removePoint records it (and now
guards double-removal); addPoints reactivates in place; treeIndex_ is
resized incrementally instead of by the batch count.

Adds test kdtree.dynamic_readd_no_duplicates (fails before, passes after).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
nanoflann 1.10.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>## What's Changed
* Fix an invalid read. by @drwells in jlblancoc/nanoflann#282
* Fix divideTree assertion failure with KDTreeSingleIndexDynamicAdaptor removePoint/addPoints by @nh2 in jlblancoc/nanoflann#283
* Fix duplicate-entry growth on removePoint/addPoints re-add cycles by @jlblancoc in jlblancoc/nanoflann#285
* Fix loadIndex reading past EOF for empty indices (#286) by @jlblancoc in jlblancoc/nanoflann#287
* fix flaky kdtree.L1_vs_bruteforce_rknn test by @jschueller in jlblancoc/nanoflann#288
* Fix KDTreeSingleIndexDynamicAdaptor_ saveIndex/loadIndex infinite recursion by @jlblancoc in jlblancoc/nanoflann#290
* Remove dead worst_dist branch from L1/L2_Adaptor::evalMetric by @jlblancoc in jlblancoc/nanoflann#291
* Fix three type/correctness bugs (operator=, First0Bit, findWithinBox) by @jlblancoc in jlblancoc/nanoflann#292
* Polish docs warnings attrs by @jlblancoc in jlblancoc/nanoflann#293
* Efficiency: remove middleSplit_ heap alloc (§6.2) + else-if in computeInitialDistances (§6.7) by @jlblancoc in jlblancoc/nanoflann#294
* saveIndex/loadIndex: add magic+version+type-size header, throw on mismatch by @jlblancoc in jlblancoc/nanoflann#295
* CI: extend tested C++ standards, and increase warning levels by @jlblancoc in jlblancoc/nanoflann#296
* Deduplicate addPoint, computeBoundingBox, and searchLevel by @jlblancoc in jlblancoc/nanoflann#297
* Minor internal cleanups: allocateBytes, has_flag(), noexcept by @jlblancoc in jlblancoc/nanoflann#298
* Centralize DIM dispatch in veclen() with if constexpr; widen nodiscard by @jlblancoc in jlblancoc/nanoflann#299
* Refactor tree builders and harden Eigen adaptor ownership by @jlblancoc in jlblancoc/nanoflann#300
* Hoist point-length computation out of search inner loops by @jlblancoc in jlblancoc/nanoflann#301
* Incremental self-balancing dynamic k-d tree index (sync + multithreaded) by @jlblancoc in jlblancoc/nanoflann#302

## New Contributors
* @drwells made their first contribution in jlblancoc/nanoflann#282
* @nh2 made their first contribution in jlblancoc/nanoflann#283

**Full Changelog**: https://github.com/jlblancoc/nanoflann/compare/v1.9.0...v1.10.0</pre>
  <p>View the full release notes at <a href="https://github.com/jlblancoc/nanoflann/releases/tag/v1.10.0">https://github.com/jlblancoc/nanoflann/releases/tag/v1.10.0</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!10859
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants