Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ jobs:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run lints
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --verbose
28 changes: 27 additions & 1 deletion quadtree/src/interval.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use eyre::{Result, ensure};
use rand::distr::uniform::SampleRange;

/// Represents an interval with a start and end value.
/// The interval is inclusive of start and exclusive of end.
Expand Down Expand Up @@ -32,7 +33,7 @@ impl Interval {
pub fn subdivide(&self) -> Vec<Self> {
let midpoint = self.start.midpoint(self.end);
if self.start == midpoint {
vec![self.clone()]
vec![*self]
} else {
vec![
Interval {
Expand All @@ -52,6 +53,21 @@ impl Interval {
}
}

impl SampleRange<f64> for Interval {
fn sample_single<R: rand::RngCore + ?Sized>(
self,
rng: &mut R,
) -> std::result::Result<f64, rand::distr::uniform::Error> {
// Generate a random value within the interval
(self.start..=self.end).sample_single(rng)
}

fn is_empty(&self) -> bool {
// By construction, an Interval cannot be empty
false
}
}

#[cfg(test)]
mod tests {
use core::f64;
Expand Down Expand Up @@ -113,4 +129,14 @@ mod tests {
assert!(!interval_b.intersects(&interval_c));
assert!(!interval_d.intersects(&interval_a));
}

#[test]
fn test_interval_sample_range() {
let interval = Interval::try_new(1.0, 5.0).unwrap();
let mut rng = rand::rng();
for _ in 0..100 {
let sample = interval.sample_single(&mut rng).unwrap();
assert!(interval.contains(&sample));
}
}
}
10 changes: 5 additions & 5 deletions quadtree/src/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl<const N: usize> Point<N> {
)
}

pub fn try_new<T: Copy + Into<f64>>(vec: &Vec<T>) -> Result<Point<N>> {
pub fn try_new<T: Copy + Into<f64>>(vec: &[T]) -> Result<Point<N>> {
ensure!(
vec.len() == N,
"cannot create point of size {} from Vec of size {}",
Expand Down Expand Up @@ -127,19 +127,19 @@ mod tests {
#[test]
fn test_point_creation() {
// 3D using integers
let point = Point::try_new(&vec![1, 2, 3]).unwrap();
let point = Point::try_new(&[1, 2, 3]).unwrap();
assert_eq!(point.0, [1.0, 2.0, 3.0]);

// 3D using floats
let point_float = Point::try_new(&vec![1.0, 2.0, 3.0]).unwrap();
let point_float = Point::try_new(&[1.0, 2.0, 3.0]).unwrap();
assert_eq!(point_float.0, [1.0, 2.0, 3.0]);

// 2D using integers
let point_2d = Point::try_new(&vec![4, 5]).unwrap();
let point_2d = Point::try_new(&[4, 5]).unwrap();
assert_eq!(point_2d.0, [4.0, 5.0]);

// Try to force an 3D slice into a 2D Point
let result = Point::<2>::try_new(&vec![4, 5, 6]);
let result = Point::<2>::try_new(&[4, 5, 6]);
assert!(result.is_err());
}

Expand Down
15 changes: 7 additions & 8 deletions quadtree/src/quadtree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl<const N: usize, V: Storable<V, N>> QuadTree<N, V> {
/// All points must be [Storable] and of the type set in the [QuadTree].
pub fn insert(&mut self, point: V) -> Result<()> {
if self.points.len() < self.points.capacity() {
if !self.region.contains(&point.point()) {
if !self.region.contains(point.point()) {
bail!("Point is outside the region");
}
self.points.push(point);
Expand All @@ -86,7 +86,7 @@ impl<const N: usize, V: Storable<V, N>> QuadTree<N, V> {
.as_mut()
.ok_or_eyre("subtrees not created, this is a bug")?
{
if subtree.region.contains(&point.point()) {
if subtree.region.contains(point.point()) {
return subtree.insert(point);
}
}
Expand Down Expand Up @@ -118,7 +118,7 @@ impl<const N: usize, V: Storable<V, N>> QuadTree<N, V> {
let my_iter = self
.points
.iter()
.filter_map(move |point| query.contains(&point.point()).then_some(point.item()));
.filter_map(move |point| query.contains(point.point()).then_some(point.item()));

let subtree_iter = self.subtrees.iter().flat_map(|subtrees| {
subtrees
Expand Down Expand Up @@ -226,8 +226,7 @@ mod tests {
subtrees
.iter()
.flat_map(|subtree| subtree.points.iter())
.map(|p| p.item().1 == "data_subdivided")
.all(|x| x)
.all(|p| p.item().1 == "data_subdivided")
);
}

Expand Down Expand Up @@ -361,7 +360,7 @@ mod tests {
]);
let mut quadtree = QuadTree::new(&region, NonZero::new(10).unwrap());
for point in &points {
quadtree.insert(point.clone()).unwrap();
quadtree.insert(*point).unwrap();
}

// Loop over the points and count how many points have a neighbour within a distance of 10.0
Expand Down Expand Up @@ -424,7 +423,7 @@ mod tests {
]);
let mut quadtree = QuadTree::new(&region, NonZero::new(10).unwrap());
for point in &points {
quadtree.insert(point.clone()).unwrap();
quadtree.insert(*point).unwrap();
}

let search_region = Region::new(&[
Expand All @@ -437,7 +436,7 @@ mod tests {
let start = std::time::Instant::now();
let count_non_quadtree = points
.iter()
.filter(|point| search_region.contains(&point.point()))
.filter(|point| search_region.contains(point.point()))
.count();
let elapsed_non_quadtree = start.elapsed();
assert_ne!(
Expand Down
2 changes: 1 addition & 1 deletion quadtree/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl<const N: usize> DistanceQuery<N> {
.expect("same sized array");
let region = Region::new(&intervals);
DistanceQuery {
center: center.clone(),
center: *center,
radius,
region,
}
Expand Down
50 changes: 47 additions & 3 deletions quadtree/src/region.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use crate::{interval::Interval, point::Point, query::Query};
use eyre::{Result, ensure};
use itertools::Itertools;
use rand::{Rng, distr::uniform::SampleRange};

/// A region in n-dimensional space defined by a Vec of intervals.
#[derive(Debug, Clone, PartialEq)]
pub struct Region<const N: usize>([Interval; N]);

impl<const N: usize> Region<N> {
pub fn new(intervals: &[Interval; N]) -> Self {
Region((*intervals).clone())
Region(*intervals)
}

pub fn try_new(intervals: &Vec<Interval>) -> Result<Self> {
pub fn try_new(intervals: &[Interval]) -> Result<Self> {
ensure!(
intervals.len() == N,
"cannot create region of size {} from Vec of size {}",
Expand All @@ -35,7 +36,7 @@ impl<const N: usize> Region<N> {
self.intervals()
.iter()
.zip(point.dimension_values())
.all(|(interval, value)| interval.contains(&value))
.all(|(interval, value)| interval.contains(value))
}

pub fn subdivide(&self) -> Vec<[Interval; N]> {
Expand Down Expand Up @@ -64,6 +65,15 @@ impl<const N: usize> Region<N> {
.zip(other.intervals().iter())
.all(|(a, b)| a.intersects(b))
}

pub fn sample_point(&self, rng: &mut impl Rng) -> Point<N> {
let values: Vec<f64> = self
.intervals()
.iter()
.map(|interval| interval.sample_single(rng).unwrap())
.collect();
Point::try_new(&values).expect("should be same size as N")
}
}

/// We can trivially implement [Query] for [Region]
Expand Down Expand Up @@ -102,6 +112,8 @@ fn test_compile_fail_different_dimensions() {}

#[cfg(test)]
mod tests {
use std::collections::HashSet;

use itertools::Itertools;

use crate::{interval::Interval, point::Point, region::Region};
Expand Down Expand Up @@ -171,4 +183,36 @@ mod tests {
.collect();
assert_eq!(unique_intervals.len(), 8);
}

#[test]
fn test_intersects() {
let x_axis = Interval::try_new(1.0, 5.0).unwrap();
let y_axis = Interval::try_new(20.0, 60.0).unwrap();
let region_a = Region::new(&[x_axis, y_axis]);
let region_b = Region::new(&[Interval::try_new(4.0, 6.0).unwrap(), y_axis]);

assert!(region_a.intersects(&region_b));

let region_c = Region::new(&[Interval::try_new(6.0, 8.0).unwrap(), y_axis]);
assert!(!region_a.intersects(&region_c));
}

#[test]
fn test_sample_point() {
let x_axis = Interval::try_new(1.0, 5.0).unwrap();
let y_axis = Interval::try_new(20.0, 60.0).unwrap();
let region = Region::new(&[x_axis, y_axis]);

use rand::{SeedableRng, rngs::StdRng};
let mut rng = StdRng::seed_from_u64(42);

let mut points_seen = HashSet::new();
for _ in 0..100 {
let point = region.sample_point(&mut rng);
points_seen.insert(point);
assert!(region.contains(&point));
}
// Ensure we sampled multiple unique points
assert_eq!(points_seen.len(), 100);
}
}
7 changes: 2 additions & 5 deletions visualize/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ fn main() -> Result<()> {
let results: Vec<_> = quadtree.query(&query_region).collect();

println!(
"{}",
format!(
"There are {} points within the distance query",
results.len()
)
"There are {} points within the distance query",
results.len()
);
Ok(())
}