Skip to content

Commit 1d9cd8e

Browse files
committed
Fix numerical bug in point query
1 parent 9b5ce72 commit 1d9cd8e

File tree

6 files changed

+186
-5
lines changed

6 files changed

+186
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
4+
## [0.6.10] - 2025-11-01
5+
- Fixed bug query_circle_points() and query_nearest_k_points() for very small r
6+
- Added get_point()
7+
38
## [0.6.9] - 2025-11-01
49
- New queries query_circle_points() and query_nearest_k_points() add_point()
510

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
name = "aabb"
4-
version = "0.6.9"
4+
version = "0.6.10"
55
description = "Static AABB spatial index for 2D queries"
66
rust-version = "1.88"
77
edition = "2024"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ The Hilbert R-tree stores bounding boxes in a flat array and sorts them by their
9292
- `HilbertRTreeI32::new()` or `AABBI32::new()` - Create a new empty tree
9393
- `HilbertRTreeI32::with_capacity(capacity)` or `AABBI32::with_capacity(capacity)` - Create a new tree with preallocated capacity
9494
- `add(min_x, min_y, max_x, max_y)` - `(f64, i32)` Add a bounding box
95-
- `add_point(x, y)` - `(f64)` Add a point (convenience method - internally stores as (x, x, y, y))
95+
- `add_point(x, y)` - `(f64)` Add a point (convenience method - internally stores as (x, y, x, y))
9696
- `build()` - `(f64, i32)` Build the spatial index (required before querying)
9797
- `get(item_id)` - `(f64, i32)` Retrieve the bounding box for an item by its ID
98+
- `get_point(item_id)` - `(f64)` Retrieve a point as (x, y) for items added with `add_point()`
9899
- `save(path)` - `(f64, i32)` Save the built tree to a file for fast loading later
99100
- `load(path)` - `(f64, i32)` Load a previously saved tree from a file
100101

src/hilbert_rtree.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ pub struct HilbertRTree {
5151
total_nodes: usize,
5252
/// Pre-allocated capacity in bytes (0 if not pre-allocated)
5353
allocated_capacity: usize,
54+
/// Inverse mapping: original item ID -> position in sorted tree
55+
/// After build(), sorted_order[original_id] = position in memory
56+
/// This allows get(item_id) to find the correct box despite Hilbert sorting
57+
pub(crate) sorted_order: Vec<usize>,
5458
}
5559

5660
const MAX_HILBERT: u32 = u16::MAX as u32;
@@ -119,6 +123,7 @@ impl HilbertRTree {
119123
position: 0,
120124
bounds: Box::new(f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
121125
total_nodes: 0,
126+
sorted_order: Vec::new(),
122127
}
123128
}
124129

@@ -181,7 +186,31 @@ impl HilbertRTree {
181186
/// // Results contain points 0 and 1 within distance 1.5
182187
/// ```
183188
pub fn add_point(&mut self, x: f64, y: f64) {
184-
self.add(x, x, y, y);
189+
self.add(x, y, x, y);
190+
}
191+
192+
/// Retrieves a point that was added with `add_point()`.
193+
///
194+
/// This is a convenience method for retrieving point data. Returns `Some((x, y))` if the
195+
/// point exists, or `None` if the index is out of bounds.
196+
///
197+
/// # Arguments
198+
/// * `item_id` - The index of the point (0-based, in order added)
199+
///
200+
/// # Example
201+
/// ```
202+
/// use aabb::prelude::*;
203+
/// let mut tree = AABB::with_capacity(2);
204+
/// tree.add_point(1.5, 2.5);
205+
/// tree.add_point(3.0, 4.0);
206+
/// tree.build();
207+
///
208+
/// assert_eq!(tree.get_point(0), Some((1.5, 2.5)));
209+
/// assert_eq!(tree.get_point(1), Some((3.0, 4.0)));
210+
/// assert_eq!(tree.get_point(2), None);
211+
/// ```
212+
pub fn get_point(&self, item_id: usize) -> Option<(f64, f64)> {
213+
self.get(item_id).map(|(min_x, min_y, _max_x, _max_y)| (min_x, min_y))
185214
}
186215

187216
/// Builds the Hilbert R-tree index
@@ -260,6 +289,9 @@ impl HilbertRTree {
260289
unsafe {
261290
std::ptr::write_unaligned(root_idx_ptr, 0_u32 << 2_u32); // First child at position 0
262291
}
292+
293+
// For single-node case, no sorting happens, so sorted_order is identity mapping
294+
self.sorted_order = (0..num_items).collect();
263295
return;
264296
}
265297

@@ -315,6 +347,16 @@ impl HilbertRTree {
315347

316348
// Initialize leaf indices AFTER sorting - map new position to original box ID
317349
let indices_start = HEADER_SIZE + total_nodes * size_of::<Box>();
350+
351+
// Build inverse mapping: original_id -> sorted_position
352+
// This allows get(item_id) to find the correct box despite Hilbert sorting
353+
let mut sorted_order = vec![0usize; num_items];
354+
for sorted_pos in 0..num_items {
355+
let original_id = sort_indices[sorted_pos];
356+
sorted_order[original_id] = sorted_pos;
357+
}
358+
self.sorted_order = sorted_order;
359+
318360
for i in 0..num_items {
319361
let idx_ptr = &mut self.data[indices_start + i * size_of::<u32>()] as *mut u8 as *mut u32;
320362
unsafe {
@@ -1739,7 +1781,16 @@ impl HilbertRTree {
17391781
return None;
17401782
}
17411783

1742-
let box_data = self.get_box(item_id);
1784+
// After build(), boxes are sorted by Hilbert curve order
1785+
// sorted_order[item_id] gives us the position of this item in the sorted tree
1786+
let pos = if self.sorted_order.is_empty() {
1787+
// If tree hasn't been built yet, items are in original order
1788+
item_id
1789+
} else {
1790+
self.sorted_order[item_id]
1791+
};
1792+
1793+
let box_data = self.get_box(pos);
17431794
Some((box_data.min_x, box_data.min_y, box_data.max_x, box_data.max_y))
17441795
}
17451796

@@ -1948,6 +1999,7 @@ impl HilbertRTree {
19481999
bounds,
19492000
total_nodes,
19502001
allocated_capacity: data_len,
2002+
sorted_order: Vec::new(),
19512003
})
19522004
}
19532005
}

src/test_point_optimizations.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,127 @@ mod test_point_optimizations {
322322
assert!(results[0] != 999);
323323
assert!(results[1] != 888);
324324
}
325+
326+
/// Test get_point() convenience method
327+
#[test]
328+
fn test_get_point_method() {
329+
let mut tree = AABB::with_capacity(4);
330+
tree.add_point(0.0, 0.0);
331+
tree.add_point(1.5, 2.5);
332+
tree.add_point(3.0, 4.0);
333+
tree.add_point(-1.0, -1.0);
334+
tree.build();
335+
336+
// Verify get_point returns correct coordinates
337+
assert_eq!(tree.get_point(0), Some((0.0, 0.0)));
338+
assert_eq!(tree.get_point(1), Some((1.5, 2.5)));
339+
assert_eq!(tree.get_point(2), Some((3.0, 4.0)));
340+
assert_eq!(tree.get_point(3), Some((-1.0, -1.0)));
341+
342+
// Out of bounds should return None
343+
assert_eq!(tree.get_point(4), None);
344+
}
345+
346+
/// Test that get_point works before build
347+
#[test]
348+
fn test_get_point_before_build() {
349+
let mut tree = AABB::with_capacity(2);
350+
tree.add_point(1.0, 2.0);
351+
tree.add_point(3.0, 4.0);
352+
353+
// Should work even before build()
354+
assert_eq!(tree.get_point(0), Some((1.0, 2.0)));
355+
assert_eq!(tree.get_point(1), Some((3.0, 4.0)));
356+
}
357+
358+
/// Test query with very small radius (1e-9) - critical for exact point matching
359+
#[test]
360+
fn test_query_circle_points_very_small_radius() {
361+
let mut tree = AABB::with_capacity(5);
362+
tree.add_point(0.0, 0.0);
363+
tree.add_point(1.0, 0.0);
364+
tree.add_point(0.0, 1.0);
365+
tree.add_point(1.0, 1.0);
366+
tree.add_point(0.5, 0.5);
367+
tree.build();
368+
369+
// Query with radius 1e-9 should only return exact point
370+
let mut results = Vec::new();
371+
tree.query_circle_points(0.0, 0.0, 1e-9, &mut results);
372+
assert_eq!(results, vec![0], "Query with radius 1e-9 should only return exact point");
373+
374+
// Query with radius 1e-6 should still only return exact point
375+
tree.query_circle_points(0.0, 0.0, 1e-6, &mut results);
376+
assert_eq!(results, vec![0], "Query with radius 1e-6 should only return exact point");
377+
}
378+
379+
/// Test point storage correctness after build with Hilbert sorting
380+
#[test]
381+
fn test_point_storage_after_build() {
382+
let mut tree = AABB::with_capacity(5);
383+
384+
// Add points in specific order
385+
tree.add_point(0.0, 0.0);
386+
tree.add_point(1.0, 0.0);
387+
tree.add_point(0.0, 1.0);
388+
tree.add_point(1.0, 1.0);
389+
tree.add_point(0.5, 0.5);
390+
391+
tree.build();
392+
393+
// Verify all points are stored correctly
394+
assert_eq!(tree.get_point(0), Some((0.0, 0.0)));
395+
assert_eq!(tree.get_point(1), Some((1.0, 0.0)));
396+
assert_eq!(tree.get_point(2), Some((0.0, 1.0)));
397+
assert_eq!(tree.get_point(3), Some((1.0, 1.0)));
398+
assert_eq!(tree.get_point(4), Some((0.5, 0.5)));
399+
}
400+
401+
/// Test that add_point stores correct coordinates (not swapped)
402+
#[test]
403+
fn test_add_point_coordinate_order() {
404+
let mut tree = AABB::with_capacity(3);
405+
406+
tree.add_point(1.5, 2.5);
407+
tree.add_point(3.0, 4.0);
408+
tree.add_point(-1.0, -2.0);
409+
410+
tree.build();
411+
412+
// Verify X and Y are not swapped
413+
assert_eq!(tree.get_point(0), Some((1.5, 2.5)));
414+
assert_eq!(tree.get_point(1), Some((3.0, 4.0)));
415+
assert_eq!(tree.get_point(2), Some((-1.0, -2.0)));
416+
417+
// Verify via get() as well
418+
assert_eq!(tree.get(0).unwrap(), (1.5, 2.5, 1.5, 2.5));
419+
assert_eq!(tree.get(1).unwrap(), (3.0, 4.0, 3.0, 4.0));
420+
assert_eq!(tree.get(2).unwrap(), (-1.0, -2.0, -1.0, -2.0));
421+
}
422+
423+
/// Test consistency between query_circle_points and get_point
424+
#[test]
425+
fn test_query_circle_points_consistency_with_get_point() {
426+
let mut tree = AABB::with_capacity(10);
427+
428+
// Create grid of points
429+
for i in 0..3 {
430+
for j in 0..3 {
431+
tree.add_point(i as f64, j as f64);
432+
}
433+
}
434+
tree.build();
435+
436+
// Query for points near (1.0, 1.0) with radius 1.5
437+
let mut results = Vec::new();
438+
tree.query_circle_points(1.0, 1.0, 1.5, &mut results);
439+
440+
// For each result, verify get_point returns the correct coordinates
441+
for &idx in &results {
442+
if let Some((x, y)) = tree.get_point(idx) {
443+
let distance = ((x - 1.0).powi(2) + (y - 1.0).powi(2)).sqrt();
444+
assert!(distance <= 1.5, "Point {} at ({}, {}) has distance {}, exceeds radius 1.5", idx, x, y, distance);
445+
}
446+
}
447+
}
325448
}

0 commit comments

Comments
 (0)