use crate::{SpatialBackend, SpatialError, SpatialOption};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AutoBackendProfile {
pub brute_force_pair_limit: usize,
pub kd_target_minimum: usize,
pub kd_query_ratio: usize,
}
impl AutoBackendProfile {
pub const BALANCED: Self = Self {
brute_force_pair_limit: 250_000,
kd_target_minimum: 20_000,
kd_query_ratio: 8,
};
pub fn validate(self) -> Result<(), SpatialError> {
if self.brute_force_pair_limit == 0 {
return Err(SpatialError::InvalidOption(
SpatialOption::BruteForcePairLimit,
));
}
if self.kd_target_minimum == 0 {
return Err(SpatialError::InvalidOption(SpatialOption::KdTargetMinimum));
}
if self.kd_query_ratio == 0 {
return Err(SpatialError::InvalidOption(SpatialOption::KdQueryRatio));
}
Ok(())
}
pub fn resolve(
self,
left_count: usize,
right_count: usize,
) -> Result<SpatialBackend, SpatialError> {
self.validate()?;
let pair_count = left_count
.checked_mul(right_count)
.ok_or(SpatialError::NumericRangeExceeded)?;
if pair_count <= self.brute_force_pair_limit {
return Ok(SpatialBackend::BruteForce);
}
if right_count >= self.kd_target_minimum && left_count < right_count / self.kd_query_ratio {
return Ok(SpatialBackend::KdTree);
}
Ok(SpatialBackend::CellList)
}
}
impl Default for AutoBackendProfile {
fn default() -> Self {
Self::BALANCED
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct NeighborSkinProfile {
pub cutoff_ratio: f32,
pub minimum: f32,
}
impl NeighborSkinProfile {
pub const BALANCED: Self = Self {
cutoff_ratio: 0.2,
minimum: 0.5,
};
pub fn validate(self) -> Result<(), SpatialError> {
if !self.cutoff_ratio.is_finite() || self.cutoff_ratio < 0.0 {
return Err(SpatialError::InvalidOption(
SpatialOption::NeighborSkinRatio,
));
}
if !self.minimum.is_finite() || self.minimum < 0.0 {
return Err(SpatialError::InvalidOption(
SpatialOption::NeighborSkinMinimum,
));
}
Ok(())
}
pub fn skin(self, cutoff: f32) -> Result<f32, SpatialError> {
self.validate()?;
let skin = (cutoff * self.cutoff_ratio).max(self.minimum);
if skin.is_finite() {
Ok(skin)
} else {
Err(SpatialError::InvalidOption(
SpatialOption::NeighborSkinRatio,
))
}
}
}
impl Default for NeighborSkinProfile {
fn default() -> Self {
Self::BALANCED
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct CellGridOptions {
pub maximum_cell_count: usize,
pub edge_growth_factor: f32,
}
impl CellGridOptions {
pub const MEMORY_BALANCED: Self = Self {
maximum_cell_count: 1_000_000,
edge_growth_factor: 2.0,
};
pub fn validate(self) -> Result<(), SpatialError> {
if self.maximum_cell_count == 0 {
return Err(SpatialError::InvalidOption(SpatialOption::MaximumCellCount));
}
if !self.edge_growth_factor.is_finite() || self.edge_growth_factor <= 1.0 {
return Err(SpatialError::InvalidOption(SpatialOption::CellGrowthFactor));
}
Ok(())
}
}
impl Default for CellGridOptions {
fn default() -> Self {
Self::MEMORY_BALANCED
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct KdPeriodicOptions {
pub maximum_image_count: usize,
}
impl KdPeriodicOptions {
pub const BALANCED: Self = Self {
maximum_image_count: 4_096,
};
pub fn validate(self) -> Result<(), SpatialError> {
if self.maximum_image_count == 0 {
Err(SpatialError::InvalidOption(
SpatialOption::KdPeriodicImageLimit,
))
} else {
Ok(())
}
}
}
impl Default for KdPeriodicOptions {
fn default() -> Self {
Self::BALANCED
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct NeighborListOptions {
pub skin: f32,
pub cell_grid: CellGridOptions,
}
impl NeighborListOptions {
#[must_use]
pub const fn with_skin(skin: f32) -> Self {
Self {
skin,
cell_grid: CellGridOptions::MEMORY_BALANCED,
}
}
pub fn validate(self) -> Result<(), SpatialError> {
if !self.skin.is_finite() || self.skin < 0.0 {
return Err(SpatialError::InvalidOption(
SpatialOption::NeighborSkinMinimum,
));
}
self.cell_grid.validate()
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SpatialSearchOptions {
pub backend: SpatialBackend,
pub automatic: AutoBackendProfile,
pub neighbor_skin: NeighborSkinProfile,
pub cell_grid: CellGridOptions,
pub kd_periodic: KdPeriodicOptions,
}
impl SpatialSearchOptions {
pub const BALANCED: Self = Self {
backend: SpatialBackend::Auto,
automatic: AutoBackendProfile::BALANCED,
neighbor_skin: NeighborSkinProfile::BALANCED,
cell_grid: CellGridOptions::MEMORY_BALANCED,
kd_periodic: KdPeriodicOptions::BALANCED,
};
#[must_use]
pub const fn with_backend(backend: SpatialBackend) -> Self {
Self {
backend,
..Self::BALANCED
}
}
pub fn validate(self) -> Result<(), SpatialError> {
self.automatic.validate()?;
self.neighbor_skin.validate()?;
self.cell_grid.validate()?;
self.kd_periodic.validate()
}
pub fn plan(
self,
left_count: usize,
right_count: usize,
cutoff: f32,
) -> Result<SpatialPlan, SpatialError> {
if !cutoff.is_finite() || cutoff < 0.0 {
return Err(SpatialError::InvalidCutoff);
}
self.validate()?;
let backend = match self.backend {
SpatialBackend::Auto => self.automatic.resolve(left_count, right_count)?,
backend => backend,
};
let neighbor_skin = if backend == SpatialBackend::NeighborList {
Some(self.neighbor_skin.skin(cutoff)?)
} else {
None
};
Ok(SpatialPlan {
requested_backend: self.backend,
backend,
neighbor_skin,
options: self,
})
}
}
impl Default for SpatialSearchOptions {
fn default() -> Self {
Self::BALANCED
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SpatialPlan {
pub requested_backend: SpatialBackend,
pub backend: SpatialBackend,
pub neighbor_skin: Option<f32>,
pub options: SpatialSearchOptions,
}
#[cfg(test)]
#[path = "options_tests.rs"]
mod tests;