#![forbid(unsafe_code)]
use super::conversions::safe_usize_to_scalar;
use super::norms::hypot;
use crate::geometry::coordinate_range::{
CoordinateRange, CoordinateRangeError, InvalidCoordinateValue,
};
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::{CoordinateConversionError, CoordinateValidationError};
use core::array;
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::{any::type_name, env, num::NonZeroUsize};
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum InvalidPositiveScalar<T = f64> {
#[error("non-finite value {value}")]
NonFinite {
value: InvalidCoordinateValue,
},
#[error("non-positive value {value:?}")]
NonPositive {
value: T,
},
}
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum RandomPointGenerationError<T = f64> {
#[error("Invalid coordinate range for random point generation: {source}")]
InvalidCoordinateRange {
#[from]
source: CoordinateRangeError<T>,
},
#[error("Coordinate range width overflows for random point generation: {min:?}..{max:?}")]
CoordinateRangeWidthOverflow {
min: T,
max: T,
},
#[error("Invalid minimum distance for Poisson point generation: {distance} must be finite")]
InvalidMinimumDistance {
distance: InvalidCoordinateValue,
},
#[error(
"Invalid periodic domain period at axis {axis}: {reason}; period must be finite and positive"
)]
InvalidPeriodicDomain {
axis: usize,
reason: InvalidPositiveScalar<T>,
},
#[error("Invalid ball radius: {reason}; radius must be finite and positive")]
InvalidBallRadius {
reason: InvalidPositiveScalar<T>,
},
#[error("Invalid squared ball radius: {value}; squared radius must be finite")]
InvalidBallRadiusSquared {
value: InvalidCoordinateValue,
},
#[error(
"Could not generate {requested_points} ball points in dimension {dimension} with radius {radius:?} after {attempts} attempts; generated {generated_points}"
)]
BallSamplingFailed {
requested_points: usize,
generated_points: usize,
dimension: usize,
radius: T,
attempts: usize,
},
#[error("Failed to convert {value} to {target_type}: {source}")]
CoordinateConversionFailed {
value: usize,
target_type: &'static str,
source: CoordinateConversionError,
},
#[error("Generated point coordinates were invalid: {source}")]
GeneratedPointCoordinateRejected {
source: CoordinateValidationError,
},
#[error("Requested grid size {points_per_dim}^{dimension} overflows usize point count")]
GridSizeOverflow {
points_per_dim: usize,
dimension: usize,
},
#[error(
"Requested grid allocation {required_bytes} bytes exceeds safety cap {cap_bytes} bytes"
)]
GridAllocationTooLarge {
required_bytes: usize,
cap_bytes: usize,
},
#[error("Invalid grid spacing: {reason}; spacing must be finite and positive")]
InvalidGridSpacing {
reason: InvalidPositiveScalar<T>,
},
#[error("Invalid grid offset at axis {axis}: {value}; offset coordinates must be finite")]
InvalidGridOffset {
axis: usize,
value: InvalidCoordinateValue,
},
#[error("Invalid generated grid coordinate at axis {axis}: {value}")]
InvalidGeneratedGridCoordinate {
axis: usize,
value: InvalidCoordinateValue,
},
#[error(
"Could not generate {requested_points} Poisson points in {bounds} with minimum distance {min_distance:?} after {attempts} attempts; generated {generated_points}"
)]
PoissonSamplingFailed {
requested_points: usize,
generated_points: usize,
min_distance: T,
bounds: CoordinateRange<T>,
attempts: usize,
},
}
const MAX_GRID_BYTES_SAFETY_CAP_DEFAULT: usize = 4_294_967_296;
const POISSON_ATTEMPTS_PER_POINT: usize = 30;
const BALL_ATTEMPTS_PER_POINT: usize = 1_024;
fn max_grid_bytes_safety_cap() -> usize {
if let Ok(v) = env::var("MAX_GRID_BYTES_SAFETY_CAP")
&& let Ok(n) = v.parse::<usize>()
{
return n;
}
MAX_GRID_BYTES_SAFETY_CAP_DEFAULT
}
pub fn scaled_bounds_by_point_count(
n_points: usize,
) -> Result<CoordinateRange<f64>, RandomPointGenerationError> {
let side_len = n_points.max(1);
let side = safe_usize_to_scalar(side_len).map_err(|source| {
RandomPointGenerationError::CoordinateConversionFailed {
value: side_len,
target_type: type_name::<f64>(),
source,
}
})?;
let half = side / 2.0;
Ok(CoordinateRange::from_validated_bounds(-half, half))
}
fn point_from_generated_coords<const D: usize>(
coords: [f64; D],
) -> Result<Point<D>, RandomPointGenerationError> {
Point::try_new(coords)
.map_err(|source| RandomPointGenerationError::GeneratedPointCoordinateRejected { source })
}
#[derive(Clone, Copy, Debug)]
struct SamplerRange {
range: CoordinateRange<f64>,
}
impl SamplerRange {
fn try_new(range: CoordinateRange<f64>) -> Result<Self, RandomPointGenerationError> {
let width = range.max() - range.min();
if width.is_finite() {
Ok(Self { range })
} else {
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow {
min: range.min(),
max: range.max(),
})
}
}
fn min(self) -> f64 {
self.range.min()
}
fn max(self) -> f64 {
self.range.max()
}
}
fn sample_point_in_range<R, const D: usize>(
range: SamplerRange,
rng: &mut R,
) -> Result<Point<D>, RandomPointGenerationError>
where
R: rand::Rng + ?Sized,
{
let coords = [0.0; D].map(|_| rng.random_range(range.min()..range.max()));
point_from_generated_coords(coords)
}
fn is_poisson_candidate_spaced<const D: usize>(
candidate: &Point<D>,
points: &[Point<D>],
min_distance: f64,
) -> bool {
let candidate_coords = *candidate.coords();
points.iter().all(|existing_point| {
let existing_coords = *existing_point.coords();
let diff_coords: [f64; D] = array::from_fn(|i| candidate_coords[i] - existing_coords[i]);
let distance = hypot(&diff_coords);
distance >= min_distance
})
}
fn generate_random_points_in_range_with_rng<R, const D: usize>(
n_points: usize,
range: CoordinateRange<f64>,
rng: &mut R,
) -> Result<Vec<Point<D>>, RandomPointGenerationError>
where
R: rand::Rng + ?Sized,
{
let range = SamplerRange::try_new(range)?;
(0..n_points).try_fold(Vec::with_capacity(n_points), |mut points, _| {
points.push(sample_point_in_range(range, rng)?);
Ok(points)
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PoissonSpacing {
Disabled,
Minimum(PositiveScalar),
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct PositiveScalar(f64);
impl PositiveScalar {
fn try_new(value: f64) -> Result<Self, InvalidPositiveScalar> {
if !value.is_finite() {
return Err(InvalidPositiveScalar::NonFinite {
value: InvalidCoordinateValue::from_debug(&value),
});
}
if value <= 0.0 {
Err(InvalidPositiveScalar::NonPositive { value })
} else {
Ok(Self(value))
}
}
const fn get(self) -> f64 {
self.0
}
fn symmetric_range(self) -> CoordinateRange<f64> {
let radius = self.get();
CoordinateRange::from_validated_bounds(-radius, radius)
}
}
impl PoissonSpacing {
fn try_new(min_distance: f64) -> Result<Self, RandomPointGenerationError> {
if !min_distance.is_finite() {
return Err(RandomPointGenerationError::InvalidMinimumDistance {
distance: InvalidCoordinateValue::from_debug(&min_distance),
});
}
if min_distance <= 0.0 {
Ok(Self::Disabled)
} else {
Ok(Self::Minimum(PositiveScalar(min_distance)))
}
}
}
const fn poisson_dimension_attempt_scaling(dimension: usize) -> usize {
match dimension {
0..=2 => 1,
3..=4 => 2,
5..=6 => 4,
_ => 8,
}
}
const fn poisson_max_attempts(n_points: usize, dimension: usize) -> usize {
n_points
.saturating_mul(POISSON_ATTEMPTS_PER_POINT)
.saturating_mul(poisson_dimension_attempt_scaling(dimension))
}
const fn ball_max_attempts(n_points: usize, dimension: usize) -> usize {
n_points
.saturating_mul(BALL_ATTEMPTS_PER_POINT)
.saturating_mul(poisson_dimension_attempt_scaling(dimension))
}
pub fn try_generate_random_points<const D: usize>(
n_points: usize,
range: (f64, f64),
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
#[cfg(debug_assertions)]
if env::var_os("DELAUNAY_DEBUG_UNUSED_IMPORTS").is_some() {
tracing::debug!(
n_points,
dimension = D,
"point_generation::try_generate_random_points called"
);
}
let range = CoordinateRange::try_from(range)?;
let mut rng = rand::rng();
let points = generate_random_points_in_range_with_rng(n_points, range, &mut rng)?;
Ok(points)
}
pub fn generate_random_points_in_range<const D: usize>(
n_points: usize,
range: CoordinateRange<f64>,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let mut rng = rand::rng();
generate_random_points_in_range_with_rng(n_points, range, &mut rng)
}
pub fn try_generate_random_points_seeded<const D: usize>(
n_points: usize,
range: (f64, f64),
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
#[cfg(debug_assertions)]
if env::var_os("DELAUNAY_DEBUG_UNUSED_IMPORTS").is_some() {
tracing::debug!(
n_points,
dimension = D,
seed,
"point_generation::try_generate_random_points_seeded called"
);
}
let range = CoordinateRange::try_from(range)?;
let mut rng = StdRng::seed_from_u64(seed);
let points = generate_random_points_in_range_with_rng(n_points, range, &mut rng)?;
Ok(points)
}
pub fn generate_random_points_in_range_seeded<const D: usize>(
n_points: usize,
range: CoordinateRange<f64>,
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let mut rng = StdRng::seed_from_u64(seed);
generate_random_points_in_range_with_rng(n_points, range, &mut rng)
}
pub fn generate_random_points_periodic<const D: usize>(
n_points: usize,
domain: [f64; D],
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let mut periods = [PositiveScalar(1.0); D];
for (axis, period) in domain.into_iter().enumerate() {
periods[axis] = PositiveScalar::try_new(period)
.map_err(|reason| RandomPointGenerationError::InvalidPeriodicDomain { axis, reason })?;
}
let mut rng = StdRng::seed_from_u64(seed);
(0..n_points).try_fold(Vec::with_capacity(n_points), |mut points, _| {
let coords = array::from_fn(|axis| rng.random_range(0.0..periods[axis].get()));
points.push(point_from_generated_coords(coords)?);
Ok(points)
})
}
fn generate_random_points_in_ball_with_rng<R, const D: usize>(
n_points: usize,
radius: f64,
rng: &mut R,
) -> Result<Vec<Point<D>>, RandomPointGenerationError>
where
R: rand::Rng + ?Sized,
{
generate_random_points_in_ball_with_rng_and_budget(
n_points,
radius,
rng,
ball_max_attempts(n_points, D),
)
}
fn generate_random_points_in_ball_with_rng_and_budget<R, const D: usize>(
n_points: usize,
radius: f64,
rng: &mut R,
max_attempts: usize,
) -> Result<Vec<Point<D>>, RandomPointGenerationError>
where
R: rand::Rng + ?Sized,
{
let radius = PositiveScalar::try_new(radius)
.map_err(|reason| RandomPointGenerationError::InvalidBallRadius { reason })?;
if n_points == 0 {
return Ok(Vec::new());
}
let bounds = radius.symmetric_range();
let radius = radius.get();
let radius_sq = radius * radius;
if !radius_sq.is_finite() {
return Err(RandomPointGenerationError::InvalidBallRadiusSquared {
value: InvalidCoordinateValue::from_debug(&radius_sq),
});
}
let bounds = SamplerRange::try_new(bounds)?;
let mut points = Vec::with_capacity(n_points);
let mut attempts = 0;
while points.len() < n_points && attempts < max_attempts {
attempts += 1;
let coords = [0.0; D].map(|_| rng.random_range(bounds.min()..bounds.max()));
let norm_sq = coords.iter().fold(0.0, |acc, &c| c.mul_add(c, acc));
if norm_sq <= radius_sq {
points.push(point_from_generated_coords(coords)?);
}
}
if points.len() < n_points {
return Err(RandomPointGenerationError::BallSamplingFailed {
requested_points: n_points,
generated_points: points.len(),
dimension: D,
radius,
attempts,
});
}
Ok(points)
}
pub fn generate_random_points_in_ball<const D: usize>(
n_points: usize,
radius: f64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let mut rng = rand::rng();
generate_random_points_in_ball_with_rng(n_points, radius, &mut rng)
}
pub fn generate_random_points_in_ball_seeded<const D: usize>(
n_points: usize,
radius: f64,
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let mut rng = StdRng::seed_from_u64(seed);
generate_random_points_in_ball_with_rng(n_points, radius, &mut rng)
}
pub fn generate_grid_points<const D: usize>(
points_per_dim: NonZeroUsize,
spacing: f64,
offset: [f64; D],
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let points_per_dim = points_per_dim.get();
let spacing = PositiveScalar::try_new(spacing)
.map_err(|reason| RandomPointGenerationError::InvalidGridSpacing { reason })?
.get();
for (axis, coordinate) in offset.iter().enumerate() {
if !coordinate.is_finite() {
return Err(RandomPointGenerationError::InvalidGridOffset {
axis,
value: InvalidCoordinateValue::from_debug(coordinate),
});
}
}
let mut total_points: usize = 1;
for _ in 0..D {
total_points = total_points.checked_mul(points_per_dim).ok_or({
RandomPointGenerationError::GridSizeOverflow {
points_per_dim,
dimension: D,
}
})?;
}
let per_point_bytes = D.saturating_mul(core::mem::size_of::<f64>());
let total_bytes = total_points.saturating_mul(per_point_bytes);
let cap = max_grid_bytes_safety_cap();
if total_bytes > cap {
return Err(RandomPointGenerationError::GridAllocationTooLarge {
required_bytes: total_bytes,
cap_bytes: cap,
});
}
let mut points = Vec::with_capacity(total_points);
let mut idx = [0usize; D];
for _ in 0..total_points {
let mut coords = [0.0; D];
for d in 0..D {
let index_as_scalar = grid_index_as_scalar::<D>(&idx, d, points_per_dim)?;
coords[d] = index_as_scalar.mul_add(spacing, offset[d]);
if !coords[d].is_finite() {
return Err(RandomPointGenerationError::InvalidGeneratedGridCoordinate {
axis: d,
value: InvalidCoordinateValue::from_debug(&coords[d]),
});
}
}
points.push(point_from_generated_coords(coords)?);
for d in (0..D).rev() {
idx[d] += 1;
if idx[d] < points_per_dim {
break;
}
idx[d] = 0;
}
}
Ok(points)
}
fn grid_index_as_scalar<const D: usize>(
idx: &[usize; D],
coordinate_index: usize,
_points_per_dim: usize,
) -> Result<f64, RandomPointGenerationError> {
safe_usize_to_scalar(idx[coordinate_index]).map_err(|source| {
RandomPointGenerationError::CoordinateConversionFailed {
value: idx[coordinate_index],
target_type: type_name::<f64>(),
source,
}
})
}
pub fn try_generate_poisson_points<const D: usize>(
n_points: usize,
bounds: (f64, f64),
min_distance: f64,
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let bounds = CoordinateRange::try_from(bounds)?;
generate_poisson_points_in_range(n_points, bounds, min_distance, seed)
}
pub fn generate_poisson_points_in_range<const D: usize>(
n_points: usize,
bounds: CoordinateRange<f64>,
min_distance: f64,
seed: u64,
) -> Result<Vec<Point<D>>, RandomPointGenerationError> {
let spacing = PoissonSpacing::try_new(min_distance)?;
if n_points == 0 {
return Ok(Vec::new());
}
let mut rng = StdRng::seed_from_u64(seed);
let min_distance = match spacing {
PoissonSpacing::Disabled => {
return generate_random_points_in_range_with_rng(n_points, bounds, &mut rng);
}
PoissonSpacing::Minimum(min_distance) => min_distance,
};
let min_distance = min_distance.get();
let raw_bounds = bounds;
let bounds = SamplerRange::try_new(bounds)?;
let mut points: Vec<Point<D>> = Vec::new();
let max_attempts = poisson_max_attempts(n_points, D);
let mut attempts = 0;
while points.len() < n_points && attempts < max_attempts {
attempts += 1;
let candidate = sample_point_in_range(bounds, &mut rng)?;
if is_poisson_candidate_spaced(&candidate, &points, min_distance) {
points.push(candidate);
}
}
if points.is_empty() {
return Err(RandomPointGenerationError::PoissonSamplingFailed {
requested_points: n_points,
generated_points: points.len(),
min_distance,
bounds: raw_bounds,
attempts,
});
}
Ok(points)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::coordinate_range::{
CoordinateRangeBound, CoordinateRangeError, CoordinateRangeOrdering, InvalidCoordinateValue,
};
use crate::geometry::traits::coordinate::CoordinateConversionError;
use approx::assert_relative_eq;
use std::assert_matches;
const fn nonzero(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).expect("test point count must be non-zero")
}
fn assert_invalid_coordinate_range<const D: usize>(
result: &Result<Vec<Point<D>>, RandomPointGenerationError<f64>>,
expected_ordering: CoordinateRangeOrdering,
expected_min: f64,
expected_max: f64,
) {
let Err(RandomPointGenerationError::InvalidCoordinateRange {
source: CoordinateRangeError::NonIncreasing { ordering, min, max },
}) = result
else {
panic!("expected non-increasing coordinate range");
};
assert_eq!(*ordering, expected_ordering);
assert_relative_eq!(*min, expected_min, epsilon = f64::EPSILON);
assert_relative_eq!(*max, expected_max, epsilon = f64::EPSILON);
}
fn assert_generated_point_coordinate_rejected<const D: usize>(
coords: [f64; D],
expected_index: usize,
expected_value: &InvalidCoordinateValue,
) {
let Err(RandomPointGenerationError::GeneratedPointCoordinateRejected {
source:
CoordinateValidationError::InvalidCoordinate {
coordinate_index,
coordinate_value,
dimension,
},
}) = point_from_generated_coords(coords)
else {
panic!("expected generated point coordinate rejection");
};
assert_eq!(coordinate_index, expected_index);
assert_eq!(&coordinate_value, expected_value);
assert_eq!(dimension, D);
}
macro_rules! gen_generated_point_coordinate_rejected_tests {
($dim:literal) => {
pastey::paste! {
#[test]
fn [<test_generated_point_coordinate_rejected_nan_ $dim d>]() {
let mut coords = [0.0; $dim];
coords[0] = f64::NAN;
assert_generated_point_coordinate_rejected::<$dim>(
coords,
0,
&InvalidCoordinateValue::Nan,
);
}
#[test]
fn [<test_generated_point_coordinate_rejected_infinity_ $dim d>]() {
let mut coords = [0.0; $dim];
coords[$dim - 1] = f64::INFINITY;
assert_generated_point_coordinate_rejected::<$dim>(
coords,
$dim - 1,
&InvalidCoordinateValue::PositiveInfinity,
);
}
}
};
}
gen_generated_point_coordinate_rejected_tests!(2);
gen_generated_point_coordinate_rejected_tests!(3);
gen_generated_point_coordinate_rejected_tests!(4);
gen_generated_point_coordinate_rejected_tests!(5);
#[test]
fn random_point_generation_error_display_names_variants() {
let range_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::InvalidCoordinateRange {
source: CoordinateRangeError::NonIncreasing {
ordering: CoordinateRangeOrdering::Decreasing,
min: 10.0,
max: 5.0,
},
};
let display = format!("{range_error}");
assert!(display.contains("Invalid coordinate range"));
let range_width_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::CoordinateRangeWidthOverflow {
min: -f64::MAX,
max: f64::MAX,
};
let display = format!("{range_width_error}");
assert!(display.contains("Coordinate range width overflows"));
let generated_point_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::GeneratedPointCoordinateRejected {
source: CoordinateValidationError::InvalidCoordinate {
coordinate_index: 1,
coordinate_value: InvalidCoordinateValue::PositiveInfinity,
dimension: 3,
},
};
let display = format!("{generated_point_error}");
assert!(display.contains("Generated point coordinates were invalid"));
assert!(display.contains("inf"));
let grid_allocation_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::GridAllocationTooLarge {
required_bytes: 8_589_934_592,
cap_bytes: 4_294_967_296,
};
let display = format!("{grid_allocation_error}");
assert!(display.contains("exceeds safety cap"));
let grid_spacing_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::InvalidGridSpacing {
reason: InvalidPositiveScalar::NonPositive { value: 0.0 },
};
let display = format!("{grid_spacing_error}");
assert!(display.contains("Invalid grid spacing"));
assert!(display.contains("non-positive"));
let radius_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::InvalidBallRadius {
reason: InvalidPositiveScalar::NonFinite {
value: InvalidCoordinateValue::Nan,
},
};
let display = format!("{radius_error}");
assert!(display.contains("Invalid ball radius"));
assert!(display.contains("NaN"));
let squared_radius_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::InvalidBallRadiusSquared {
value: InvalidCoordinateValue::PositiveInfinity,
};
let display = format!("{squared_radius_error}");
assert!(display.contains("Invalid squared ball radius"));
assert!(display.contains("inf"));
let ball_error = RandomPointGenerationError::BallSamplingFailed {
requested_points: 4,
generated_points: 1,
dimension: 12,
radius: 2.0,
attempts: 8_192,
};
let display = format!("{ball_error}");
assert!(display.contains("Could not generate 4 ball points"));
assert!(display.contains("dimension 12"));
assert!(display.contains("generated 1"));
let generated_grid_coordinate_error: RandomPointGenerationError<f64> =
RandomPointGenerationError::InvalidGeneratedGridCoordinate {
axis: 2,
value: InvalidCoordinateValue::PositiveInfinity,
};
let display = format!("{generated_grid_coordinate_error}");
assert!(display.contains("Invalid generated grid coordinate at axis 2"));
assert!(display.contains("inf"));
let poisson_error = RandomPointGenerationError::PoissonSamplingFailed {
requested_points: 10,
generated_points: 0,
min_distance: 2.5,
bounds: CoordinateRange::try_new(-1.0, 1.0).unwrap(),
attempts: 300,
};
let RandomPointGenerationError::PoissonSamplingFailed {
min_distance,
bounds,
..
} = poisson_error
else {
panic!("expected PoissonSamplingFailed");
};
assert_relative_eq!(min_distance, 2.5, epsilon = f64::EPSILON);
assert_relative_eq!(bounds.min(), -1.0, epsilon = f64::EPSILON);
assert_relative_eq!(bounds.max(), 1.0, epsilon = f64::EPSILON);
}
#[test]
fn test_generate_random_points_rejects_nonfinite_tuple_bounds() {
assert_matches!(
try_generate_random_points::<2>(4, (f64::NAN, 1.0)),
Err(RandomPointGenerationError::InvalidCoordinateRange {
source: CoordinateRangeError::NonFiniteBound { bound, value }
}) if bound == CoordinateRangeBound::Minimum && value == InvalidCoordinateValue::Nan
);
assert_matches!(
try_generate_random_points_seeded::<2>(4, (0.0, f64::INFINITY), 42),
Err(RandomPointGenerationError::InvalidCoordinateRange {
source: CoordinateRangeError::NonFiniteBound { bound, value }
}) if bound == CoordinateRangeBound::Maximum
&& value == InvalidCoordinateValue::PositiveInfinity
);
assert_matches!(
try_generate_poisson_points::<2>(4, (f64::NEG_INFINITY, 1.0), 0.1, 42),
Err(RandomPointGenerationError::InvalidCoordinateRange {
source: CoordinateRangeError::NonFiniteBound { bound, value }
}) if bound == CoordinateRangeBound::Minimum
&& value == InvalidCoordinateValue::NegativeInfinity
);
}
#[test]
fn test_range_based_generators_use_validated_bounds() {
let range = CoordinateRange::try_new(-1.0_f64, 1.0).unwrap();
let points = generate_random_points_in_range::<3>(8, range)
.expect("validated range should generate finite points");
assert_eq!(points.len(), 8);
for point in points {
for &coord in point.coords() {
assert!((-1.0..1.0).contains(&coord));
}
}
let seeded_a = generate_random_points_in_range_seeded::<3>(8, range, 42)
.expect("validated range should generate finite points");
let seeded_b = generate_random_points_in_range_seeded::<3>(8, range, 42)
.expect("validated range should generate finite points");
assert_eq!(seeded_a, seeded_b);
for point in seeded_a {
for &coord in point.coords() {
assert!((-1.0..1.0).contains(&coord));
}
}
let empty = generate_random_points_in_range::<3>(0, range)
.expect("validated range should generate finite points");
assert!(empty.is_empty());
let seeded_empty = generate_random_points_in_range_seeded::<3>(0, range, 42)
.expect("validated range should generate finite points");
assert!(seeded_empty.is_empty());
let poisson = generate_poisson_points_in_range::<2>(8, range, 0.1, 42).unwrap();
assert!(!poisson.is_empty());
let unconstrained_poisson =
generate_poisson_points_in_range::<2>(8, range, 0.0, 42).unwrap();
assert_eq!(unconstrained_poisson.len(), 8);
let empty_poisson = generate_poisson_points_in_range::<2>(0, range, 0.1, 42).unwrap();
assert!(empty_poisson.is_empty());
let invalid_empty_poisson = generate_poisson_points_in_range::<2>(0, range, f64::NAN, 42);
assert_matches!(
invalid_empty_poisson,
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::Nan
);
}
#[test]
fn test_generate_random_points_in_range_rejects_overflowing_width() {
let range = CoordinateRange::try_new(-f64::MAX, f64::MAX).unwrap();
assert_matches!(
try_generate_random_points::<2>(1, (-f64::MAX, f64::MAX)),
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow { min, max })
if min.to_bits() == (-f64::MAX).to_bits() && max.to_bits() == f64::MAX.to_bits()
);
assert_matches!(
try_generate_random_points_seeded::<2>(1, (-f64::MAX, f64::MAX), 42),
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow { min, max })
if min.to_bits() == (-f64::MAX).to_bits() && max.to_bits() == f64::MAX.to_bits()
);
assert_matches!(
generate_random_points_in_range::<2>(1, range),
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow { min, max })
if min.to_bits() == (-f64::MAX).to_bits() && max.to_bits() == f64::MAX.to_bits()
);
assert_matches!(
generate_random_points_in_range_seeded::<2>(1, range, 42),
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow { min, max })
if min.to_bits() == (-f64::MAX).to_bits() && max.to_bits() == f64::MAX.to_bits()
);
}
#[test]
fn test_unconstrained_poisson_rejects_overflowing_width() {
let range = CoordinateRange::try_new(-f64::MAX, f64::MAX).unwrap();
assert_matches!(
generate_poisson_points_in_range::<2>(1, range, 0.0, 42),
Err(RandomPointGenerationError::CoordinateRangeWidthOverflow { min, max })
if min.to_bits() == (-f64::MAX).to_bits() && max.to_bits() == f64::MAX.to_bits()
);
}
#[test]
fn test_scaled_bounds_by_point_count_returns_validated_range() {
let hundred = scaled_bounds_by_point_count(100).unwrap();
assert_relative_eq!(hundred.min(), -50.0, epsilon = f64::EPSILON);
assert_relative_eq!(hundred.max(), 50.0, epsilon = f64::EPSILON);
let empty = scaled_bounds_by_point_count(0).unwrap();
assert_relative_eq!(empty.min(), -0.5, epsilon = f64::EPSILON);
assert_relative_eq!(empty.max(), 0.5, epsilon = f64::EPSILON);
}
#[test]
fn test_poisson_spacing_parses_raw_distance_once() {
assert_eq!(
PoissonSpacing::try_new(0.0_f64),
Ok(PoissonSpacing::Disabled)
);
assert_eq!(
PoissonSpacing::try_new(-1.0_f64),
Ok(PoissonSpacing::Disabled)
);
assert_eq!(
PoissonSpacing::try_new(0.25_f64),
Ok(PoissonSpacing::Minimum(PositiveScalar(0.25)))
);
assert_matches!(
PoissonSpacing::try_new(f64::NAN),
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::Nan
);
assert_matches!(
PoissonSpacing::try_new(f64::INFINITY),
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::PositiveInfinity
);
assert_matches!(
PoissonSpacing::try_new(f64::NEG_INFINITY),
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::NegativeInfinity
);
}
#[test]
fn test_positive_scalar_preserves_finite_positive_proof() {
assert_eq!(PositiveScalar::try_new(2.5_f64), Ok(PositiveScalar(2.5)));
assert_eq!(
PositiveScalar::try_new(0.0_f64),
Err(InvalidPositiveScalar::NonPositive { value: 0.0 })
);
assert_eq!(
PositiveScalar::try_new(-1.0_f64),
Err(InvalidPositiveScalar::NonPositive { value: -1.0 })
);
assert_eq!(
PositiveScalar::try_new(f64::NAN),
Err(InvalidPositiveScalar::NonFinite {
value: InvalidCoordinateValue::Nan
})
);
assert_eq!(
PositiveScalar::try_new(f64::INFINITY),
Err(InvalidPositiveScalar::NonFinite {
value: InvalidCoordinateValue::PositiveInfinity
})
);
}
#[test]
fn test_poisson_attempt_budget_saturates_for_large_point_counts() {
assert_eq!(poisson_max_attempts(10, 2), 300);
assert_eq!(poisson_max_attempts(10, 4), 600);
assert_eq!(poisson_max_attempts(10, 6), 1_200);
assert_eq!(poisson_max_attempts(10, 7), 2_400);
assert_eq!(poisson_max_attempts(usize::MAX, 7), usize::MAX);
}
#[test]
fn test_ball_attempt_budget_saturates_for_large_point_counts() {
assert_eq!(ball_max_attempts(10, 2), 10_240);
assert_eq!(ball_max_attempts(10, 4), 20_480);
assert_eq!(ball_max_attempts(10, 6), 40_960);
assert_eq!(ball_max_attempts(10, 7), 81_920);
assert_eq!(ball_max_attempts(usize::MAX, 7), usize::MAX);
}
#[test]
fn test_generate_random_points_2d() {
let points = try_generate_random_points::<2>(100, (-10.0, 10.0)).unwrap();
assert_eq!(points.len(), 100);
for point in &points {
let coords = *point.coords();
assert!(coords[0] >= -10.0 && coords[0] < 10.0);
assert!(coords[1] >= -10.0 && coords[1] < 10.0);
}
}
#[test]
fn test_generate_random_points_3d() {
let points = try_generate_random_points::<3>(75, (0.0, 5.0)).unwrap();
assert_eq!(points.len(), 75);
for point in &points {
let coords = *point.coords();
assert!(coords[0] >= 0.0 && coords[0] < 5.0);
assert!(coords[1] >= 0.0 && coords[1] < 5.0);
assert!(coords[2] >= 0.0 && coords[2] < 5.0);
}
}
#[test]
fn test_generate_random_points_4d() {
let points = try_generate_random_points::<4>(50, (-2.0, 2.0)).unwrap();
assert_eq!(points.len(), 50);
for point in &points {
let coords = *point.coords();
for &coord in &coords {
assert!((-2.0..2.0).contains(&coord));
}
}
}
#[test]
fn test_generate_random_points_5d() {
let points = try_generate_random_points::<5>(25, (-1.0, 1.0)).unwrap();
assert_eq!(points.len(), 25);
for point in &points {
let coords = *point.coords();
for &coord in &coords {
assert!((-1.0..1.0).contains(&coord));
}
}
}
#[test]
fn test_generate_random_points_error_handling() {
let result = try_generate_random_points::<2>(100, (10.0, -10.0));
assert_invalid_coordinate_range(&result, CoordinateRangeOrdering::Decreasing, 10.0, -10.0);
let result = try_generate_random_points::<3>(50, (5.0, 5.0));
assert_invalid_coordinate_range(&result, CoordinateRangeOrdering::Equal, 5.0, 5.0);
let result = try_generate_random_points::<4>(25, (1.0, 0.5));
assert_invalid_coordinate_range(&result, CoordinateRangeOrdering::Decreasing, 1.0, 0.5);
let result = try_generate_random_points::<5>(10, (2.0, 2.0));
assert_invalid_coordinate_range(&result, CoordinateRangeOrdering::Equal, 2.0, 2.0);
let points = try_generate_random_points::<2>(10, (0.0, 0.001)).unwrap();
assert_eq!(points.len(), 10);
for point in points {
for &coord in point.coords() {
assert!((0.0..0.001).contains(&coord));
}
}
}
#[test]
fn test_generate_random_points_zero_points() {
let points_2d = try_generate_random_points::<2>(0, (-1.0, 1.0)).unwrap();
assert_eq!(points_2d.len(), 0);
let points_3d = try_generate_random_points::<3>(0, (-1.0, 1.0)).unwrap();
assert_eq!(points_3d.len(), 0);
let points_4d = try_generate_random_points::<4>(0, (-1.0, 1.0)).unwrap();
assert_eq!(points_4d.len(), 0);
let points_5d = try_generate_random_points::<5>(0, (-1.0, 1.0)).unwrap();
assert_eq!(points_5d.len(), 0);
}
fn assert_seeded_random_points_reproducible<const D: usize>(
count: usize,
range: (f64, f64),
seed: u64,
) {
let points1 = try_generate_random_points_seeded::<D>(count, range, seed).unwrap();
let points2 = try_generate_random_points_seeded::<D>(count, range, seed).unwrap();
assert_eq!(points1.len(), points2.len());
for (p1, p2) in points1.iter().zip(points2.iter()) {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
for (c1, c2) in coords1.iter().zip(coords2.iter()) {
assert_relative_eq!(c1, c2, epsilon = 1e-15);
}
}
}
macro_rules! gen_seeded_random_points_tests {
($($name:ident: $dim:literal, $count:literal, $range:expr, $seed:literal;)*) => {
$(
#[test]
fn $name() {
assert_seeded_random_points_reproducible::<$dim>($count, $range, $seed);
}
)*
};
}
gen_seeded_random_points_tests! {
test_generate_random_points_seeded_2d: 2, 50, (-5.0, 5.0), 42;
test_generate_random_points_seeded_3d: 3, 40, (0.0, 10.0), 123;
test_generate_random_points_seeded_4d: 4, 30, (-2.5, 2.5), 789;
test_generate_random_points_seeded_5d: 5, 20, (-1.0, 3.0), 456;
}
#[test]
fn test_generate_random_points_seeded_different_seeds() {
let points1_2d = try_generate_random_points_seeded::<2>(50, (0.0, 1.0), 42).unwrap();
let points2_2d = try_generate_random_points_seeded::<2>(50, (0.0, 1.0), 123).unwrap();
assert_ne!(points1_2d, points2_2d);
let points1_3d = try_generate_random_points_seeded::<3>(30, (-5.0, 5.0), 42).unwrap();
let points2_3d = try_generate_random_points_seeded::<3>(30, (-5.0, 5.0), 999).unwrap();
assert_ne!(points1_3d, points2_3d);
let points1_4d = try_generate_random_points_seeded::<4>(25, (-1.0, 1.0), 1337).unwrap();
let points2_4d = try_generate_random_points_seeded::<4>(25, (-1.0, 1.0), 7331).unwrap();
assert_ne!(points1_4d, points2_4d);
let points1_5d = try_generate_random_points_seeded::<5>(15, (0.0, 10.0), 2021).unwrap();
let points2_5d = try_generate_random_points_seeded::<5>(15, (0.0, 10.0), 2024).unwrap();
assert_ne!(points1_5d, points2_5d);
}
#[test]
fn test_generate_random_points_periodic_2d_in_domain() {
let points = generate_random_points_periodic::<2>(100, [1.0, 2.0], 42).unwrap();
assert_eq!(points.len(), 100);
for point in &points {
let coords = *point.coords();
assert!((0.0..1.0).contains(&coords[0]));
assert!((0.0..2.0).contains(&coords[1]));
}
}
#[test]
fn test_generate_random_points_periodic_seeded_reproducible() {
let points1 = generate_random_points_periodic::<3>(50, [1.0, 1.0, 1.0], 123).unwrap();
let points2 = generate_random_points_periodic::<3>(50, [1.0, 1.0, 1.0], 123).unwrap();
assert_eq!(points1, points2);
}
#[test]
fn test_generate_random_points_periodic_invalid_domain() {
let zero_period = generate_random_points_periodic::<2>(10, [1.0, 0.0], 7);
let Err(RandomPointGenerationError::InvalidPeriodicDomain {
axis,
reason: InvalidPositiveScalar::NonPositive { value },
}) = zero_period
else {
panic!("expected non-positive periodic domain error");
};
assert_eq!(axis, 1);
assert_relative_eq!(value, 0.0, epsilon = f64::EPSILON);
let negative_period = generate_random_points_periodic::<2>(10, [1.0, -2.0], 7);
let Err(RandomPointGenerationError::InvalidPeriodicDomain {
axis,
reason: InvalidPositiveScalar::NonPositive { value },
}) = negative_period
else {
panic!("expected non-positive periodic domain error");
};
assert_eq!(axis, 1);
assert_relative_eq!(value, -2.0, epsilon = f64::EPSILON);
let nan_period = generate_random_points_periodic::<2>(10, [1.0, f64::NAN], 7);
assert_matches!(
nan_period,
Err(RandomPointGenerationError::InvalidPeriodicDomain {
axis,
reason: InvalidPositiveScalar::NonFinite { value }
}) if axis == 1 && value == InvalidCoordinateValue::Nan
);
let infinite_period = generate_random_points_periodic::<2>(10, [1.0, f64::INFINITY], 7);
assert_matches!(
infinite_period,
Err(RandomPointGenerationError::InvalidPeriodicDomain {
axis,
reason: InvalidPositiveScalar::NonFinite { value }
}) if axis == 1 && value == InvalidCoordinateValue::PositiveInfinity
);
}
#[test]
fn test_generate_random_points_periodic_zero_points() {
let points = generate_random_points_periodic::<4>(0, [1.0, 2.0, 3.0, 4.0], 9).unwrap();
assert!(points.is_empty());
}
#[test]
fn test_generate_random_points_in_ball_4d() {
let radius = 3.0_f64;
let points = generate_random_points_in_ball::<4>(200, radius).unwrap();
assert_eq!(points.len(), 200);
let radius_sq = radius * radius;
for point in &points {
let coords = *point.coords();
let mut norm_sq = 0.0_f64;
for &c in &coords {
assert!(c >= -radius && c <= radius);
norm_sq = c.mul_add(c, norm_sq);
}
assert!(norm_sq <= radius_sq + 1e-12);
}
}
#[test]
fn test_generate_random_points_in_ball_seeded_reproducible_4d() {
let points1 = generate_random_points_in_ball_seeded::<4>(50, 2.5, 42).unwrap();
let points2 = generate_random_points_in_ball_seeded::<4>(50, 2.5, 42).unwrap();
assert_eq!(points1, points2);
}
#[test]
fn test_generate_random_points_in_ball_seeded_different_seeds_4d() {
let points1 = generate_random_points_in_ball_seeded::<4>(50, 2.5, 42).unwrap();
let points2 = generate_random_points_in_ball_seeded::<4>(50, 2.5, 123).unwrap();
assert_ne!(points1, points2);
}
#[test]
fn test_generate_random_points_in_ball_rejects_zero_radius() {
let result = generate_random_points_in_ball::<4>(10, 0.0);
let Err(RandomPointGenerationError::InvalidBallRadius {
reason: InvalidPositiveScalar::NonPositive { value },
}) = result
else {
panic!("expected non-positive ball radius error");
};
assert_relative_eq!(value, 0.0, epsilon = f64::EPSILON);
}
#[test]
fn test_generate_random_points_in_ball_rejects_negative_radius() {
let result = generate_random_points_in_ball::<4>(10, -1.0);
let Err(RandomPointGenerationError::InvalidBallRadius {
reason: InvalidPositiveScalar::NonPositive { value },
}) = result
else {
panic!("expected non-positive ball radius error");
};
assert_relative_eq!(value, -1.0, epsilon = f64::EPSILON);
}
#[test]
fn test_generate_random_points_in_ball_zero_points() {
let points = generate_random_points_in_ball::<4>(0, 1.0).unwrap();
assert!(points.is_empty());
}
#[test]
fn test_generate_random_points_in_ball_seeded_zero_points() {
let points = generate_random_points_in_ball_seeded::<4>(0, 1.0, 7).unwrap();
assert!(points.is_empty());
}
#[test]
fn test_generate_random_points_in_ball_rejects_nan_radius() {
let result = generate_random_points_in_ball::<4>(10, f64::NAN);
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidBallRadius {
reason: InvalidPositiveScalar::NonFinite { value }
}) if value == InvalidCoordinateValue::Nan
);
}
#[test]
fn test_generate_random_points_in_ball_rejects_infinite_radius() {
let result = generate_random_points_in_ball::<4>(10, f64::INFINITY);
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidBallRadius {
reason: InvalidPositiveScalar::NonFinite { value }
}) if value == InvalidCoordinateValue::PositiveInfinity
);
}
#[test]
fn test_generate_random_points_in_ball_rejects_overflowing_radius_squared() {
let result = generate_random_points_in_ball::<2>(1, f64::MAX);
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidBallRadiusSquared { value })
if value == InvalidCoordinateValue::PositiveInfinity
);
}
#[test]
fn test_generate_random_points_in_ball_seeded_in_ball_constraints_4d() {
let radius = 1.25;
let points = generate_random_points_in_ball_seeded::<4>(100, radius, 99).unwrap();
assert_eq!(points.len(), 100);
let radius_sq = radius * radius;
for point in &points {
let coords = *point.coords();
let mut norm_sq = 0.0;
for &c in &coords {
assert!(c >= -radius && c <= radius);
norm_sq = c.mul_add(c, norm_sq);
}
assert!(norm_sq <= radius_sq + 1e-12);
}
}
#[test]
fn test_generate_random_points_in_ball_returns_typed_error_when_budget_exhausts() {
let mut rng = StdRng::seed_from_u64(42);
let result =
generate_random_points_in_ball_with_rng_and_budget::<_, 2>(1, 1.0, &mut rng, 0);
let Err(RandomPointGenerationError::BallSamplingFailed {
requested_points,
generated_points,
dimension,
radius,
attempts,
}) = result
else {
panic!("expected ball sampling budget exhaustion");
};
assert_eq!(requested_points, 1);
assert_eq!(generated_points, 0);
assert_eq!(dimension, 2);
assert_relative_eq!(radius, 1.0, epsilon = f64::EPSILON);
assert_eq!(attempts, 0);
}
#[test]
fn test_generate_random_points_in_ball_seeded_same_seed_is_deterministic_4d() {
let points1 = generate_random_points_in_ball_seeded::<4>(10, 1.0, 0xBEEF).unwrap();
let points2 = generate_random_points_in_ball_seeded::<4>(10, 1.0, 0xBEEF).unwrap();
assert_eq!(points1, points2);
}
#[test]
fn test_generate_random_points_distribution_coverage_all_dimensions() {
let points_2d = try_generate_random_points::<2>(500, (0.0, 10.0)).unwrap();
let mut min_2d = [f64::INFINITY; 2];
let mut max_2d = [f64::NEG_INFINITY; 2];
for point in &points_2d {
let coords = *point.coords();
for (i, &coord) in coords.iter().enumerate() {
min_2d[i] = min_2d[i].min(coord);
max_2d[i] = max_2d[i].max(coord);
}
}
for i in 0..2 {
assert!(
min_2d[i] < 2.0,
"Min in dimension {i} should be close to lower bound"
);
assert!(
max_2d[i] > 8.0,
"Max in dimension {i} should be close to upper bound"
);
}
let points_5d = try_generate_random_points::<5>(200, (-5.0, 5.0)).unwrap();
let mut min_5d = [f64::INFINITY; 5];
let mut max_5d = [f64::NEG_INFINITY; 5];
for point in &points_5d {
let coords = *point.coords();
for (i, &coord) in coords.iter().enumerate() {
min_5d[i] = min_5d[i].min(coord);
max_5d[i] = max_5d[i].max(coord);
}
}
for i in 0..5 {
assert!(
min_5d[i] < -2.0,
"Min in 5D dimension {i} should be reasonably low"
);
assert!(
max_5d[i] > 2.0,
"Max in 5D dimension {i} should be reasonably high"
);
}
}
#[test]
fn test_generate_random_points_common_ranges() {
let unit_2d = try_generate_random_points::<2>(50, (0.0, 1.0)).unwrap();
let unit_3d = try_generate_random_points::<3>(50, (0.0, 1.0)).unwrap();
let unit_4d = try_generate_random_points::<4>(50, (0.0, 1.0)).unwrap();
let unit_5d = try_generate_random_points::<5>(50, (0.0, 1.0)).unwrap();
assert_eq!(unit_2d.len(), 50);
assert_eq!(unit_3d.len(), 50);
assert_eq!(unit_4d.len(), 50);
assert_eq!(unit_5d.len(), 50);
let centered_2d = try_generate_random_points::<2>(30, (-1.0, 1.0)).unwrap();
let centered_3d = try_generate_random_points::<3>(30, (-1.0, 1.0)).unwrap();
let centered_4d = try_generate_random_points::<4>(30, (-1.0, 1.0)).unwrap();
let centered_5d = try_generate_random_points::<5>(30, (-1.0, 1.0)).unwrap();
assert_eq!(centered_2d.len(), 30);
assert_eq!(centered_3d.len(), 30);
assert_eq!(centered_4d.len(), 30);
assert_eq!(centered_5d.len(), 30);
for point in ¢ered_5d {
let coords = *point.coords();
for &coord in &coords {
assert!((-1.0..1.0).contains(&coord));
}
}
}
#[test]
fn test_generate_grid_points_2d() {
let grid = generate_grid_points::<2>(nonzero(3), 1.0, [0.0, 0.0]).unwrap();
assert_eq!(grid.len(), 9);
let expected_coords = [
[0.0, 0.0],
[1.0, 0.0],
[2.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[2.0, 1.0],
[0.0, 2.0],
[1.0, 2.0],
[2.0, 2.0],
];
for point in &grid {
let coords = *point.coords();
assert!(
expected_coords.iter().any(|&expected| {
(coords[0] - expected[0]).abs() < 1e-10
&& (coords[1] - expected[1]).abs() < 1e-10
}),
"Point {coords:?} not found in expected coordinates"
);
}
}
#[test]
fn test_generate_grid_points_3d() {
let grid = generate_grid_points::<3>(nonzero(2), 2.0, [1.0, 1.0, 1.0]).unwrap();
assert_eq!(grid.len(), 8);
for point in &grid {
let coords = *point.coords();
for &coord in &coords {
assert!((1.0..=3.0).contains(&coord)); }
}
}
#[test]
fn test_generate_grid_points_4d() {
let grid = generate_grid_points::<4>(nonzero(2), 0.5, [-0.5, -0.5, -0.5, -0.5]).unwrap();
assert_eq!(grid.len(), 16);
for point in &grid {
let coords = *point.coords();
for &coord in &coords {
assert!((-0.5..=0.0).contains(&coord)); }
}
}
#[test]
fn test_generate_grid_points_5d() {
let grid = generate_grid_points::<5>(nonzero(2), 1.0, [0.0, 0.0, 0.0, 0.0, 0.0]).unwrap();
assert_eq!(grid.len(), 32);
for point in &grid {
let coords = *point.coords();
for &coord in &coords {
assert!((0.0..=1.0).contains(&coord)); }
}
}
#[test]
fn test_generate_grid_points_edge_cases() {
let grid = generate_grid_points::<3>(nonzero(1), 1.0, [0.0, 0.0, 0.0]).unwrap();
assert_eq!(grid.len(), 1);
let coords = *grid[0].coords();
for (actual, expected) in coords.iter().zip([0.0, 0.0, 0.0].iter()) {
assert!((actual - expected).abs() < 1e-15);
}
let zero_spacing = generate_grid_points::<2>(nonzero(2), 0.0, [5.0, 5.0]);
let Err(RandomPointGenerationError::InvalidGridSpacing {
reason: InvalidPositiveScalar::NonPositive { value },
}) = zero_spacing
else {
panic!("expected non-positive grid spacing error for zero spacing");
};
assert_relative_eq!(value, 0.0, epsilon = f64::EPSILON);
let negative_spacing = generate_grid_points::<2>(nonzero(2), -1.0, [5.0, 5.0]);
let Err(RandomPointGenerationError::InvalidGridSpacing {
reason: InvalidPositiveScalar::NonPositive { value },
}) = negative_spacing
else {
panic!("expected non-positive grid spacing error for negative spacing");
};
assert_relative_eq!(value, -1.0, epsilon = f64::EPSILON);
}
#[test]
fn test_generate_grid_points_error_handling() {
assert_eq!(NonZeroUsize::new(0), None);
let nan_spacing = generate_grid_points::<2>(nonzero(2), f64::NAN, [0.0, 0.0]);
assert_matches!(
nan_spacing,
Err(RandomPointGenerationError::InvalidGridSpacing {
reason: InvalidPositiveScalar::NonFinite { value }
}) if value == InvalidCoordinateValue::Nan
);
let infinite_offset = generate_grid_points::<3>(nonzero(2), 1.0, [0.0, f64::INFINITY, 0.0]);
assert_matches!(
infinite_offset,
Err(RandomPointGenerationError::InvalidGridOffset { axis, value })
if axis == 1 && value == InvalidCoordinateValue::PositiveInfinity
);
let result = generate_grid_points::<3>(nonzero(1000), 1.0, [0.0, 0.0, 0.0]);
assert_matches!(
result,
Err(RandomPointGenerationError::GridAllocationTooLarge {
required_bytes,
cap_bytes,
}) if required_bytes > cap_bytes
);
}
#[test]
fn test_generate_grid_points_rejects_non_finite_generated_coordinate() {
let result = generate_grid_points::<1>(nonzero(2), f64::MAX, [f64::MAX]);
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidGeneratedGridCoordinate { axis, value })
if axis == 0 && value == InvalidCoordinateValue::PositiveInfinity
);
}
#[test]
fn test_generate_grid_points_overflow_detection() {
const LARGE_D: usize = 64; let offset = [0.0; LARGE_D];
let spacing = 0.1; let points_per_dim = 10;
let result = generate_grid_points::<LARGE_D>(nonzero(points_per_dim), spacing, offset);
assert_matches!(
result,
Err(RandomPointGenerationError::GridSizeOverflow {
points_per_dim: actual_points_per_dim,
dimension,
}) if actual_points_per_dim == points_per_dim && dimension == LARGE_D
);
}
#[test]
fn test_generate_grid_points_index_conversion_failure() {
let first_inexact_f64_index = (1_usize << f64::MANTISSA_DIGITS) + 1;
let idx = [first_inexact_f64_index];
let result = grid_index_as_scalar::<1>(&idx, 0, first_inexact_f64_index.saturating_add(1));
assert_matches!(
result,
Err(RandomPointGenerationError::CoordinateConversionFailed {
value,
target_type,
ref source,
}) if value == first_inexact_f64_index
&& target_type == "f64"
&& matches!(
source,
CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
from_type: "usize",
to_type: "f64",
..
}
)
);
}
#[test]
fn test_generate_poisson_points_2d() {
let points = try_generate_poisson_points::<2>(50, (0.0, 10.0), 0.5, 42).unwrap();
assert!(!points.is_empty());
assert!(points.len() <= 50);
for point in &points {
let coords = *point.coords();
assert!((0.0..10.0).contains(&coords[0]));
assert!((0.0..10.0).contains(&coords[1]));
}
for (i, p1) in points.iter().enumerate() {
for (j, p2) in points.iter().enumerate() {
if i != j {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
let diff = [coords1[0] - coords2[0], coords1[1] - coords2[1]];
let distance = hypot(&diff);
assert!(
distance >= 0.5 - 1e-10,
"Distance {distance} violates minimum distance constraint"
);
}
}
}
}
#[test]
fn test_generate_poisson_points_3d() {
let points = try_generate_poisson_points::<3>(30, (-1.0, 1.0), 0.2, 123).unwrap();
assert!(!points.is_empty());
for point in &points {
let coords = *point.coords();
for &coord in &coords {
assert!((-1.0..1.0).contains(&coord));
}
}
for (i, p1) in points.iter().enumerate() {
for (j, p2) in points.iter().enumerate() {
if i != j {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
let diff = [
coords1[0] - coords2[0],
coords1[1] - coords2[1],
coords1[2] - coords2[2],
];
let distance = hypot(&diff);
assert!(distance >= 0.2 - 1e-10);
}
}
}
}
#[test]
fn test_generate_poisson_points_4d() {
let points = try_generate_poisson_points::<4>(15, (0.0, 5.0), 0.5, 333).unwrap();
assert!(!points.is_empty());
for point in &points {
let coords = *point.coords();
for &coord in &coords {
assert!((0.0..5.0).contains(&coord));
}
}
for (i, p1) in points.iter().enumerate() {
for (j, p2) in points.iter().enumerate() {
if i != j {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
let diff = [
coords1[0] - coords2[0],
coords1[1] - coords2[1],
coords1[2] - coords2[2],
coords1[3] - coords2[3],
];
let distance = hypot(&diff);
assert!(distance >= 0.5 - 1e-12);
}
}
}
}
#[test]
fn test_generate_poisson_points_5d() {
let points = try_generate_poisson_points::<5>(10, (-2.0, 2.0), 0.4, 777).unwrap();
assert!(!points.is_empty());
for point in &points {
let coords = *point.coords();
for &coord in &coords {
assert!((-2.0..2.0).contains(&coord));
}
}
for (i, p1) in points.iter().enumerate() {
for (j, p2) in points.iter().enumerate() {
if i != j {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
let diff = [
coords1[0] - coords2[0],
coords1[1] - coords2[1],
coords1[2] - coords2[2],
coords1[3] - coords2[3],
coords1[4] - coords2[4],
];
let distance = hypot(&diff);
assert!(distance >= 0.4 - 1e-10);
}
}
}
}
#[test]
fn test_generate_poisson_points_reproducible() {
let points1 = try_generate_poisson_points::<2>(25, (0.0, 5.0), 0.3, 456).unwrap();
let points2 = try_generate_poisson_points::<2>(25, (0.0, 5.0), 0.3, 456).unwrap();
assert_eq!(points1.len(), points2.len());
for (p1, p2) in points1.iter().zip(points2.iter()) {
let coords1 = *p1.coords();
let coords2 = *p2.coords();
for (c1, c2) in coords1.iter().zip(coords2.iter()) {
assert_relative_eq!(c1, c2, epsilon = 1e-15);
}
}
let points3 = try_generate_poisson_points::<2>(25, (0.0, 5.0), 0.3, 789).unwrap();
assert_ne!(points1, points3);
}
#[test]
fn test_generate_poisson_points_error_handling() {
let result = try_generate_poisson_points::<2>(50, (10.0, 5.0), 0.1, 42);
assert_invalid_coordinate_range(&result, CoordinateRangeOrdering::Decreasing, 10.0, 5.0);
let nan_distance = try_generate_poisson_points::<2>(0, (0.0, 1.0), f64::NAN, 42);
assert_matches!(
nan_distance,
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::Nan
);
let infinite_distance = try_generate_poisson_points::<2>(50, (0.0, 1.0), f64::INFINITY, 42);
assert_matches!(
infinite_distance,
Err(RandomPointGenerationError::InvalidMinimumDistance { distance })
if distance == InvalidCoordinateValue::PositiveInfinity
);
let result = try_generate_poisson_points::<2>(100, (0.0, 1.0), 10.0, 42);
match result {
Ok(points) => {
assert!(points.len() < 5);
}
Err(RandomPointGenerationError::PoissonSamplingFailed { .. }) => {
}
_ => panic!("Unexpected error type"),
}
let points = try_generate_poisson_points::<2>(100, (0.0, 10.0), 0.0, 42).unwrap();
assert_eq!(points.len(), 100);
let points = try_generate_poisson_points::<2>(50, (0.0, 10.0), -1.0, 42).unwrap();
assert_eq!(points.len(), 50); }
#[test]
fn test_generate_random_points_invalid_range() {
let result = try_generate_random_points::<2>(100, (10.0, 5.0));
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidCoordinateRange { .. })
);
let result = try_generate_random_points::<2>(100, (5.0, 5.0));
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidCoordinateRange { .. })
);
let points = try_generate_random_points::<2>(10, (0.0, 1.0)).unwrap();
assert_eq!(points.len(), 10);
}
#[test]
fn test_generate_random_points_seeded_invalid_range() {
let result = try_generate_random_points_seeded::<3>(50, (100.0, 10.0), 42);
assert_matches!(
result,
Err(RandomPointGenerationError::InvalidCoordinateRange { .. })
);
let points1 = try_generate_random_points_seeded::<3>(5, (0.0, 1.0), 42).unwrap();
let points2 = try_generate_random_points_seeded::<3>(5, (0.0, 1.0), 42).unwrap();
assert_eq!(points1, points2);
let points3 = try_generate_random_points_seeded::<3>(5, (0.0, 1.0), 123).unwrap();
assert_ne!(points1, points3);
}
#[test]
fn test_generate_grid_points_overflow_detection_edge_cases() {
let points = generate_grid_points::<2>(nonzero(10), 0.1, [0.0, 0.0]).unwrap();
assert!(!points.is_empty());
let result = generate_grid_points::<2>(nonzero(1000), 0.0001, [0.0, 0.0]);
if let Ok(points) = result {
assert!(!points.is_empty());
}
}
#[test]
fn test_generate_poisson_points_edge_cases() {
let result = try_generate_poisson_points::<2>(100, (0.0, 1.0), 0.001, 42);
if let Ok(points) = result {
assert!(!points.is_empty());
}
let result = try_generate_poisson_points::<2>(0, (0.0, 1.0), 0.1, 42);
if let Ok(points) = result {
assert!(points.is_empty());
}
let result = try_generate_poisson_points::<2>(10, (0.0, 1.0), 2.0, 42);
if let Ok(points) = result {
assert!(points.len() <= 10);
}
}
#[test]
fn test_max_grid_bytes_safety_cap() {
let expected = env::var("MAX_GRID_BYTES_SAFETY_CAP")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(MAX_GRID_BYTES_SAFETY_CAP_DEFAULT);
let cap = max_grid_bytes_safety_cap();
assert!(cap > 0);
assert_eq!(cap, expected);
assert_eq!(MAX_GRID_BYTES_SAFETY_CAP_DEFAULT, 4_294_967_296);
}
}