From 8028c92a4d0d2eac9397c16861348dfb8f144ee1 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 15:00:40 +0000 Subject: [PATCH 1/6] add empty tree test also remove irrelevant node id test --- rtree-capi/tests/test.c | 88 ++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/rtree-capi/tests/test.c b/rtree-capi/tests/test.c index 5e28876..e2d1fdc 100644 --- a/rtree-capi/tests/test.c +++ b/rtree-capi/tests/test.c @@ -208,29 +208,6 @@ bool test_nodes(void) { } -bool test_root_node_id(void) { - const size_t N = 2; - const uint32_t dim = 2; - double mins[4] = {0.0, 0.0, 1.0, 1.0}; - double maxs[4] = {2.0, 2.0, 3.0, 3.0}; - size_t ids[2] = {1, 2}; - RTreeH *tree = NULL; - rtree_bulk_load(&tree, mins, maxs, ids, N, dim); - if (tree == NULL) { - return false; - } - - RTreeNodeH *root = NULL; - rtree_root_node(tree, &root); - if (root == NULL) { - rtree_free(tree); - return false; - } - rtree_node_free(root); - rtree_free(tree); - return true; -} - bool test_rtree_1d(void) { const size_t N = 4; const uint32_t dim = 1; @@ -383,6 +360,69 @@ bool test_rtree_node_1d(void) { return true; } +bool test_rtree_empty(void) { + const size_t N = 0; + const uint32_t dim = 2; + double *mins = NULL; + double *maxs = NULL; + size_t *ids = NULL; + RTreeH *tree = NULL; + rtree_bulk_load(&tree, mins, maxs, ids, N, dim); + if (tree == NULL) { + fprintf(stderr, "Expected to create empty tree, got null pointer\n"); + return false; + } + + // Query empty tree + double point[2] = {0.0, 0.0}; + size_t *ids_out = NULL; + size_t nids_out = 0; + rtree_locate_all_at_point(tree, point, &ids_out, &nids_out); + if (nids_out != 0) { + fprintf(stderr, "Expected to find no ids at point in empty tree"); + rtree_free_ids(ids_out, nids_out); + rtree_free(tree); + return false; + } else { + rtree_free_ids(ids_out, nids_out); + } + + // Check root node of empty tree + RTreeNodeH *root = NULL; + rtree_root_node(tree, &root); + if (root == NULL) { + rtree_free(tree); + return false; + } + double root_min[2]; + double root_max[2]; + rtree_node_envelope(root, root_min, root_max); + if (root_min[0] != 0.0 || root_min[1] != 0.0 || root_max[0] != 0.0 || root_max[1] != 0.0) { + fprintf(stderr, "Expected root envelope of empty tree to be [0, 0], [0, 0], got [%f, %f], [%f, %f]\n", + root_min[0], root_min[1], root_max[0], root_max[1]); + rtree_node_free(root); + rtree_free(tree); + return false; + } + + // Get children of root node of empty tree + RTreeNodeH **children = NULL; + size_t nchildren = 0; + rtree_node_children(root, &children, &nchildren); + if (nchildren != 0) { + fprintf(stderr, "Expected root of empty tree to have 0 children, got %zu\n", nchildren); + rtree_node_children_free(children, nchildren); + rtree_node_free(root); + rtree_free(tree); + return false; + } + + rtree_node_children_free(children, nchildren); + rtree_node_free(root); + rtree_free(tree); + return true; +} + void run_test( bool (test)(void), const char *test_name, @@ -404,9 +444,9 @@ int main(void) { run_test(test_get_dimension, "test_get_dimension", &passed); run_test(test_bulk_load, "test_bulk_load", &passed); run_test(test_nodes, "test_nodes", &passed); - run_test(test_root_node_id, "test_root_node_id", &passed); run_test(test_rtree_1d, "test_rtree_1d", &passed); run_test(test_rtree_node_1d, "test_rtree_node_1d", &passed); + run_test(test_rtree_empty, "test_rtree_empty", &passed); if (passed) { fprintf(stdout, "All tests passed\n"); From 4cefc59468ac66be4dbe9604ddd919a7eb4a6e36 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 15:30:38 +0000 Subject: [PATCH 2/6] add new method to interval tree --- interval-tree/src/lib.rs | 14 ++++++++++---- interval-tree/src/tests.rs | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/interval-tree/src/lib.rs b/interval-tree/src/lib.rs index 70b3332..fa509ec 100644 --- a/interval-tree/src/lib.rs +++ b/interval-tree/src/lib.rs @@ -101,10 +101,7 @@ impl IntervalTree { "Inputs must have the same length" ); if n == 0 { - return Self { - root: None, - size: 0, - }; + return Self::new(); } let elements: Vec<(f64, f64, usize)> = (0..n).map(|i| (mins[i], maxs[i], ids[i])).collect(); Self { @@ -160,6 +157,15 @@ impl IntervalTree { pub fn root(&self) -> Option<&IntervalTreeNode> { self.root.as_ref() } + + /// Returns an empty interval tree. + #[must_use] + pub fn new() -> Self { + Self { + root: None, + size: 0, + } + } } #[cfg(test)] diff --git a/interval-tree/src/tests.rs b/interval-tree/src/tests.rs index a712a32..f07b973 100644 --- a/interval-tree/src/tests.rs +++ b/interval-tree/src/tests.rs @@ -67,6 +67,13 @@ fn test_interval_tree_bulk_load() { assert_eq!(right_node.right, None); } +#[test] +fn test_interval_tree_new() { + let tree = IntervalTree::new(); + assert_eq!(tree.size(), 0); + assert_eq!(tree.root(), None); +} + #[test] fn test_interval_tree_empty_bulk_load() { let mins = vec![]; From 7ddcf1dad561349c41b826fa3a53470a787d3c5f Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 15:30:59 +0000 Subject: [PATCH 3/6] create empty tree if n==0 --- rtree-capi/src/rtree.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rtree-capi/src/rtree.rs b/rtree-capi/src/rtree.rs index da0d21b..d646eb3 100644 --- a/rtree-capi/src/rtree.rs +++ b/rtree-capi/src/rtree.rs @@ -101,7 +101,22 @@ pub extern "C" fn rtree_bulk_load( n: usize, dim: u32, ) -> RTreeError { - if tree.is_null() || mins.is_null() || maxs.is_null() || ids.is_null() { + if tree.is_null() { + return RTreeError::NullPointer; + } + + if n == 0 { + let rtree = match dim { + 1 => RTreeDim::D1(IntervalTree::new()), + 2 => RTreeDim::D2(RTree::new()), + 3 => RTreeDim::D3(RTree::new()), + _ => return RTreeError::InvalidDimension, + }; + unsafe { *tree = Box::into_raw(Box::new(rtree)) as *mut RTreeH }; + return RTreeError::Success; + } + + if mins.is_null() || maxs.is_null() || ids.is_null() { return RTreeError::NullPointer; } From 7fbe5cf7dc89f0f007ae9ef4ce068010b553e9a7 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 15:34:40 +0000 Subject: [PATCH 4/6] add rtree_size function --- rtree-capi/include/rtree-capi.h | 2 ++ rtree-capi/src/rtree.rs | 15 ++++++++++ rtree-capi/tests/test.c | 49 ++++++++++++++++++++++++++------- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/rtree-capi/include/rtree-capi.h b/rtree-capi/include/rtree-capi.h index fea4860..bd67ca6 100644 --- a/rtree-capi/include/rtree-capi.h +++ b/rtree-capi/include/rtree-capi.h @@ -47,3 +47,5 @@ RTreeError rtree_node_envelope(const struct RTreeNodeH *node, double *min_out, d RTreeError rtree_node_free(struct RTreeNodeH *node); RTreeError rtree_root_node(const struct RTreeH *tree, struct RTreeNodeH **node); + +RTreeError rtree_size(const struct RTreeH *tree, size_t *size_out); diff --git a/rtree-capi/src/rtree.rs b/rtree-capi/src/rtree.rs index d646eb3..d09265d 100644 --- a/rtree-capi/src/rtree.rs +++ b/rtree-capi/src/rtree.rs @@ -168,6 +168,21 @@ pub extern "C" fn rtree_locate_all_at_point( RTreeError::Success } +#[no_mangle] +pub extern "C" fn rtree_size(tree: *const RTreeH, size_out: *mut usize) -> RTreeError { + if tree.is_null() || size_out.is_null() { + return RTreeError::NullPointer; + } + let rtree = unsafe { &*(tree as *const RTreeDim) }; + let size = match rtree { + RTreeDim::D1(tree) => tree.size(), + RTreeDim::D2(tree) => tree.size(), + RTreeDim::D3(tree) => tree.size(), + }; + unsafe { *size_out = size }; + RTreeError::Success +} + #[no_mangle] pub extern "C" fn rtree_free_ids(ids: *mut usize, n: usize) -> RTreeError { if ids.is_null() { diff --git a/rtree-capi/tests/test.c b/rtree-capi/tests/test.c index e2d1fdc..5a89261 100644 --- a/rtree-capi/tests/test.c +++ b/rtree-capi/tests/test.c @@ -79,6 +79,15 @@ bool test_bulk_load(void) { return false; } + // test rtree_size + size_t size = 0; + rtree_size(tree, &size); + if (size != N) { + fprintf(stderr, "Expected tree size %zu, got %zu\n", N, size); + rtree_free(tree); + return false; + } + double point1[2] = {1.5, 1.5}; double point2[2] = {0.0, 0.0}; double point3[2] = {-1.0, 0.0}; @@ -219,6 +228,15 @@ bool test_rtree_1d(void) { if (tree == NULL) { return false; } + + // test rtree_size + size_t size = 0; + rtree_size(tree, &size); + if (size != N) { + fprintf(stderr, "Expected tree size %zu, got %zu\n", N, size); + rtree_free(tree); + return false; + } double point1[1] = {0.5}; double point2[1] = {1.5}; @@ -373,6 +391,15 @@ bool test_rtree_empty(void) { return false; } + // test rtree_size + size_t size = 0; + rtree_size(tree, &size); + if (size != N) { + fprintf(stderr, "Expected tree size %zu, got %zu\n", N, size); + rtree_free(tree); + return false; + } + // Query empty tree double point[2] = {0.0, 0.0}; size_t *ids_out = NULL; @@ -391,19 +418,21 @@ bool test_rtree_empty(void) { RTreeNodeH *root = NULL; rtree_root_node(tree, &root); if (root == NULL) { + fprintf(stderr, "Expected root node of empty tree to be non-null\n"); rtree_free(tree); return false; } - double root_min[2]; - double root_max[2]; - rtree_node_envelope(root, root_min, root_max); - if (root_min[0] != 0.0 || root_min[1] != 0.0 || root_max[0] != 0.0 || root_max[1] != 0.0) { - fprintf(stderr, "Expected root envelope of empty tree to be [0, 0], [0, 0], got [%f, %f], [%f, %f]\n", - root_min[0], root_min[1], root_max[0], root_max[1]); - rtree_node_free(root); - rtree_free(tree); - return false; - } + + // double root_min[2]; + // double root_max[2]; + // rtree_node_envelope(root, root_min, root_max); + // if (root_min[0] != 0.0 || root_min[1] != 0.0 || root_max[0] != 0.0 || root_max[1] != 0.0) { + // fprintf(stderr, "Expected root envelope of empty tree to be [0, 0], [0, 0], got [%f, %f], [%f, %f]\n", + // root_min[0], root_min[1], root_max[0], root_max[1]); + // rtree_node_free(root); + // rtree_free(tree); + // return false; + // } // Get children of root node of empty tree RTreeNodeH **children = NULL; From f82674f6fd4091f149970dafb4fb930abe72fb4f Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 16:00:56 +0000 Subject: [PATCH 5/6] handle empty nodes correctly Create EmptyNode type. If the tree's size is zero, then we return EmptyNode from `rtree_root_node` `rtree_node_children` returns the same as before `rtree_node_envelope` returns an EmptyNodeEnvelope error if given an EmptyNode --- rtree-capi/include/rtree-capi.h | 1 + rtree-capi/src/error.rs | 1 + rtree-capi/src/node.rs | 13 +++++++++++++ rtree-capi/tests/test.c | 25 +++++++++++++++---------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/rtree-capi/include/rtree-capi.h b/rtree-capi/include/rtree-capi.h index bd67ca6..a16d96e 100644 --- a/rtree-capi/include/rtree-capi.h +++ b/rtree-capi/include/rtree-capi.h @@ -9,6 +9,7 @@ enum RTreeError { Success = 0, NullPointer = 1, InvalidDimension = 2, + EmptyNodeEnvelope = 3, }; typedef uint16_t RTreeError; diff --git a/rtree-capi/src/error.rs b/rtree-capi/src/error.rs index bd5687f..ba98dc7 100644 --- a/rtree-capi/src/error.rs +++ b/rtree-capi/src/error.rs @@ -3,4 +3,5 @@ pub enum RTreeError { Success = 0, NullPointer = 1, InvalidDimension = 2, + EmptyNodeEnvelope = 3, } diff --git a/rtree-capi/src/node.rs b/rtree-capi/src/node.rs index d366c98..11ffce7 100644 --- a/rtree-capi/src/node.rs +++ b/rtree-capi/src/node.rs @@ -10,6 +10,7 @@ enum NodeRef { Parent3D(*const ParentNode), Node2D(*const RTreeNode), Node3D(*const RTreeNode), + EmptyNode, // root node of empty tree } pub enum RTreeNodeH {} @@ -20,6 +21,16 @@ pub extern "C" fn rtree_root_node(tree: *const RTreeH, node: *mut *mut RTreeNode return RTreeError::NullPointer; } let rtree = unsafe { &*(tree as *const RTreeDim) }; + let size = match rtree { + RTreeDim::D1(tree) => tree.size(), + RTreeDim::D2(tree) => tree.size(), + RTreeDim::D3(tree) => tree.size(), + }; + if size == 0 { + let node_ref = NodeRef::EmptyNode; + unsafe { *node = Box::into_raw(Box::new(node_ref)) as *mut RTreeNodeH }; + return RTreeError::Success; + } let node_ref = match rtree { RTreeDim::D1(tree) => NodeRef::ITreeNode(tree.root().unwrap()), RTreeDim::D2(tree) => NodeRef::Parent2D(tree.root() as *const _), @@ -41,6 +52,7 @@ pub extern "C" fn rtree_node_children( let node_ref = unsafe { &*(node as *const NodeRef) }; let child_node_refs: Vec = match node_ref { + NodeRef::EmptyNode => Vec::new(), NodeRef::ITreeNode(ptr) => { let node = unsafe { &**ptr }; let mut children = Vec::new(); @@ -115,6 +127,7 @@ pub extern "C" fn rtree_node_envelope( let node_ref = unsafe { &*(node as *const NodeRef) }; match node_ref { + NodeRef::EmptyNode => return RTreeError::EmptyNodeEnvelope, NodeRef::ITreeNode(ptr) => { let node = unsafe { &**ptr }; let min = node.min; diff --git a/rtree-capi/tests/test.c b/rtree-capi/tests/test.c index 5a89261..2f54bb5 100644 --- a/rtree-capi/tests/test.c +++ b/rtree-capi/tests/test.c @@ -306,6 +306,8 @@ bool test_rtree_node_1d(void) { if (tree == NULL) { return false; } + + // Get root node RTreeNodeH *root = NULL; rtree_root_node(tree, &root); if (root == NULL) { @@ -313,6 +315,7 @@ bool test_rtree_node_1d(void) { return false; } + // Get envelope of root node double root_min[1]; double root_max[1]; rtree_node_envelope(root, root_min, root_max); @@ -324,6 +327,7 @@ bool test_rtree_node_1d(void) { return false; } + // Get children of root node struct RTreeNodeH **children = NULL; size_t nchildren = 0; rtree_node_children(root, &children, &nchildren); @@ -335,6 +339,7 @@ bool test_rtree_node_1d(void) { return false; } + // Get envelopes of child nodes double child1_min[1]; double child1_max[1]; rtree_node_envelope(children[0], child1_min, child1_max); @@ -359,6 +364,7 @@ bool test_rtree_node_1d(void) { return false; } + // Get children of child1 node RTreeNodeH **child1children = NULL; size_t nchild1children = 0; rtree_node_children(children[0], &child1children, &nchild1children); @@ -423,16 +429,15 @@ bool test_rtree_empty(void) { return false; } - // double root_min[2]; - // double root_max[2]; - // rtree_node_envelope(root, root_min, root_max); - // if (root_min[0] != 0.0 || root_min[1] != 0.0 || root_max[0] != 0.0 || root_max[1] != 0.0) { - // fprintf(stderr, "Expected root envelope of empty tree to be [0, 0], [0, 0], got [%f, %f], [%f, %f]\n", - // root_min[0], root_min[1], root_max[0], root_max[1]); - // rtree_node_free(root); - // rtree_free(tree); - // return false; - // } + double root_min[2]; + double root_max[2]; + RTreeError err = rtree_node_envelope(root, root_min, root_max); + if (err != EmptyNodeEnvelope) { + fprintf(stderr, "Expected EmptyNodeEnvelope error for envelope of root node of empty tree\n"); + rtree_node_free(root); + rtree_free(tree); + return false; + } // Get children of root node of empty tree RTreeNodeH **children = NULL; From 14828305f2d1a39de0a9c2dae66a43cdd2a37724 Mon Sep 17 00:00:00 2001 From: Leo Collins Date: Mon, 23 Mar 2026 16:34:59 +0000 Subject: [PATCH 6/6] add invalid dimension test and 1D empty tree tests more --- rtree-capi/tests/test.c | 111 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/rtree-capi/tests/test.c b/rtree-capi/tests/test.c index 2f54bb5..bd54d87 100644 --- a/rtree-capi/tests/test.c +++ b/rtree-capi/tests/test.c @@ -34,6 +34,21 @@ bool test_null(void) { return true; } +bool test_invalid_dimension(void) { + RTreeH *tree = NULL; + RTreeError err = rtree_create(&tree, 0); + if (err != InvalidDimension) { + fprintf(stderr, "Expected InvalidDimension error for rtree_create with dimension 0\n"); + return false; + } + err = rtree_create(&tree, 4); + if (err != InvalidDimension) { + fprintf(stderr, "Expected InvalidDimension error for rtree_create with dimension 4\n"); + return false; + } + return true; +} + bool test_get_dimension(void) { RTreeH *tree3d = NULL; @@ -429,8 +444,91 @@ bool test_rtree_empty(void) { return false; } - double root_min[2]; - double root_max[2]; + // Check envelope of root node of empty tree + double root_min[2] = {1.0, 1.0}; // initialize to something + double root_max[2] = {1.0, 1.0}; + RTreeError err = rtree_node_envelope(root, root_min, root_max); + if (err != EmptyNodeEnvelope) { + fprintf(stderr, "Expected EmptyNodeEnvelope error for envelope of root node of empty tree\n"); + rtree_node_free(root); + rtree_free(tree); + return false; + } + for (size_t i = 0; i < dim; i++) { + if (root_min[i] != 1.0 || root_max[i] != 1.0) { + fprintf(stderr, "Expected root_min and root_max to be unchanged for empty node, got root_min[%zu] = %f, root_max[%zu] = %f\n", + i, root_min[i], i, root_max[i]); + rtree_node_free(root); + rtree_free(tree); + return false; + } + } + + // Get children of root node of empty tree + RTreeNodeH **children = NULL; + size_t nchildren = 0; + rtree_node_children(root, &children, &nchildren); + if (nchildren != 0) { + fprintf(stderr, "Expected root of empty tree to have 0 children, got %zu\n", nchildren); + rtree_node_children_free(children, nchildren); + rtree_node_free(root); + rtree_free(tree); + return false; + } + + rtree_node_children_free(children, nchildren); + rtree_node_free(root); + rtree_free(tree); + return true; +} + +bool test_rtree_empty_1d(void) { + const size_t N = 0; + const uint32_t dim = 1; + double *mins = NULL; + double *maxs = NULL; + size_t *ids = NULL; + RTreeH *tree = NULL; + rtree_bulk_load(&tree, mins, maxs, ids, N, dim); + if (tree == NULL) { + fprintf(stderr, "Expected to create empty tree, got null pointer\n"); + return false; + } + // test rtree_size + size_t size = 0; + rtree_size(tree, &size); + if (size != N) { + fprintf(stderr, "Expected tree size %zu, got %zu\n", N, size); + rtree_free(tree); + return false; + } + + // Query empty tree + double point[1] = {0.0}; + size_t *ids_out = NULL; + size_t nids_out = 0; + rtree_locate_all_at_point(tree, point, &ids_out, &nids_out); + if (nids_out != 0) { + fprintf(stderr, "Expected to find no ids at point in empty tree"); + rtree_free_ids(ids_out, nids_out); + rtree_free(tree); + return false; + } else { + rtree_free_ids(ids_out, nids_out); + } + + // Check root node of empty tree + RTreeNodeH *root = NULL; + rtree_root_node(tree, &root); + if (root == NULL) { + fprintf(stderr, "Expected root node of empty tree to be non-null\n"); + rtree_free(tree); + return false; + } + + // Check envelope of root node of empty tree + double root_min[1] = {1.0}; // initialize to something + double root_max[1] = {1.0}; RTreeError err = rtree_node_envelope(root, root_min, root_max); if (err != EmptyNodeEnvelope) { fprintf(stderr, "Expected EmptyNodeEnvelope error for envelope of root node of empty tree\n"); @@ -438,6 +536,13 @@ bool test_rtree_empty(void) { rtree_free(tree); return false; } + if (root_min[0] != 1.0 || root_max[0] != 1.0) { + fprintf(stderr, "Expected root_min and root_max to be unchanged for empty node, got root_min[0] = %f, root_max[0] = %f\n", + root_min[0], root_max[0]); + rtree_node_free(root); + rtree_free(tree); + return false; + } // Get children of root node of empty tree RTreeNodeH **children = NULL; @@ -481,6 +586,8 @@ int main(void) { run_test(test_rtree_1d, "test_rtree_1d", &passed); run_test(test_rtree_node_1d, "test_rtree_node_1d", &passed); run_test(test_rtree_empty, "test_rtree_empty", &passed); + run_test(test_rtree_empty_1d, "test_rtree_empty_1d", &passed); + run_test(test_invalid_dimension, "test_invalid_dimension", &passed); if (passed) { fprintf(stdout, "All tests passed\n");