fix(bulk_load): keep all leaves on the same depth - #239
Conversation
bulk_load built the tree by recursively partitioning the elements and turning any cluster with at most MAX_SIZE elements into a leaf node. When the element count made sibling clusters straddle MAX_SIZE (some just below, some just above), the larger clusters were split one level deeper than the smaller ones, so leaves ended up on different levels. Such a tree violates the R-tree invariant that all leaves share the same depth. Queries still worked, but a subsequent insert descended the tree assuming a uniform depth and panicked with "This is a bug in rstar." once it reached a node mixing leaf and parent children. This is reproducible with the default parameters in two dimensions, e.g. bulk-loading 25 points and then inserting two more. Compute the target tree depth up front and thread it through the recursion so every branch stops at the same level, placing all leaves on one depth. Add a regression test.
|
Hold off on merging this for a moment, I found a regression in it. The depth calculation I added divides the cluster size by the combined fan-out and rounds down, but It shows up in 2D, where master is clean. Smallest case is 25 collinear points: let pts: Vec<[f64; 2]> = (0..25).map(|i| [i as f64, i as f64]).collect();
let tree = RTree::<[f64; 2], DefaultParams>::bulk_load_with_params(pts);
// a node ends up with 7 children, MAX_SIZE is 6Sweeping n = 1..300 on collinear input, master has no violations and this branch has them from n = 25. My own test here doesn't catch it because it calls Rounding the depth up per axis instead is not enough on its own: it pushes some trees a level deeper and then nodes fall below @adamreichold you suggested changing What does work is fixing the fan-out itself. Splitting every axis by the same number gives a node I have that on a branch. Happy to either fold it into this PR, which would mean you re-reviewing since it touches the code this PR adds, or land this one first with just the depth rounding sorted and do the fan-out separately as a fix for #197. Your call on which is less annoying to review. One thing worth flagging if we go the fan-out route: it makes 4D and up substantially faster (6D nearest-neighbour roughly halves) but 3D range queries get slower, because cutting all three axes in two needs a fan-out of 8 and I used an LLM to build the differential harness and the sweeps behind this, as with the original PR. |
RTree::bulk_loadbuilt the tree by recursively partitioning elements and turning any cluster with ≤MAX_SIZEelements into a leaf. When the element count made sibling clusters straddleMAX_SIZE(some just below, some just above), the larger clusters were split one level deeper than the smaller ones, so leaves ended up on different depths.Such a tree violates the R-tree invariant that all leaves share the same depth (the invariant
ParentNode::sanity_checkasserts). Queries still worked, but a laterinsertdescends assuming uniform depth and panics with"This is a bug in rstar."on reaching a node that mixes leaf and parent children. Reproducible with default parameters in 2-D, e.g. bulk-loading 25 points and then inserting two more:Fix: compute the target tree depth up front and thread it through the recursion so every branch stops on the same level. Adds a regression test and a CHANGELOG entry. Full suite + fmt + clippy pass.
This is distinct from #197 (which is about
MAX_SIZEcompliance in higher dimensions — the repro here keeps every child count withinMAX_SIZE) and from the long-closed #45 (a reinsertion bug; here the tree is already malformed straight out ofbulk_load, before any insert). It does not attempt to fully solve #197.