molframe_spatial/planning/
options.rs1use crate::{SpatialBackend, SpatialError, SpatialOption};
4
5#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7pub struct AutoBackendProfile {
8 pub brute_force_pair_limit: usize,
10 pub kd_target_minimum: usize,
12 pub kd_query_ratio: usize,
14}
15
16impl AutoBackendProfile {
17 pub const BALANCED: Self = Self {
19 brute_force_pair_limit: 250_000,
20 kd_target_minimum: 20_000,
21 kd_query_ratio: 8,
22 };
23
24 pub fn validate(self) -> Result<(), SpatialError> {
30 if self.brute_force_pair_limit == 0 {
31 return Err(SpatialError::InvalidOption(
32 SpatialOption::BruteForcePairLimit,
33 ));
34 }
35 if self.kd_target_minimum == 0 {
36 return Err(SpatialError::InvalidOption(SpatialOption::KdTargetMinimum));
37 }
38 if self.kd_query_ratio == 0 {
39 return Err(SpatialError::InvalidOption(SpatialOption::KdQueryRatio));
40 }
41 Ok(())
42 }
43
44 pub fn resolve(
55 self,
56 left_count: usize,
57 right_count: usize,
58 ) -> Result<SpatialBackend, SpatialError> {
59 self.validate()?;
60 let pair_count = left_count
61 .checked_mul(right_count)
62 .ok_or(SpatialError::NumericRangeExceeded)?;
63 if pair_count <= self.brute_force_pair_limit {
64 return Ok(SpatialBackend::BruteForce);
65 }
66 if right_count >= self.kd_target_minimum && left_count < right_count / self.kd_query_ratio {
67 return Ok(SpatialBackend::KdTree);
68 }
69 Ok(SpatialBackend::CellList)
70 }
71}
72
73impl Default for AutoBackendProfile {
74 fn default() -> Self {
75 Self::BALANCED
76 }
77}
78
79#[derive(Clone, Copy, PartialEq, Debug)]
81pub struct NeighborSkinProfile {
82 pub cutoff_ratio: f32,
84 pub minimum: f32,
86}
87
88impl NeighborSkinProfile {
89 pub const BALANCED: Self = Self {
91 cutoff_ratio: 0.2,
92 minimum: 0.5,
93 };
94
95 pub fn validate(self) -> Result<(), SpatialError> {
101 if !self.cutoff_ratio.is_finite() || self.cutoff_ratio < 0.0 {
102 return Err(SpatialError::InvalidOption(
103 SpatialOption::NeighborSkinRatio,
104 ));
105 }
106 if !self.minimum.is_finite() || self.minimum < 0.0 {
107 return Err(SpatialError::InvalidOption(
108 SpatialOption::NeighborSkinMinimum,
109 ));
110 }
111 Ok(())
112 }
113
114 pub fn skin(self, cutoff: f32) -> Result<f32, SpatialError> {
120 self.validate()?;
121 let skin = (cutoff * self.cutoff_ratio).max(self.minimum);
122 if skin.is_finite() {
123 Ok(skin)
124 } else {
125 Err(SpatialError::InvalidOption(
126 SpatialOption::NeighborSkinRatio,
127 ))
128 }
129 }
130}
131
132impl Default for NeighborSkinProfile {
133 fn default() -> Self {
134 Self::BALANCED
135 }
136}
137
138#[derive(Clone, Copy, PartialEq, Debug)]
140pub struct CellGridOptions {
141 pub maximum_cell_count: usize,
143 pub edge_growth_factor: f32,
145}
146
147impl CellGridOptions {
148 pub const MEMORY_BALANCED: Self = Self {
150 maximum_cell_count: 1_000_000,
151 edge_growth_factor: 2.0,
152 };
153
154 pub fn validate(self) -> Result<(), SpatialError> {
160 if self.maximum_cell_count == 0 {
161 return Err(SpatialError::InvalidOption(SpatialOption::MaximumCellCount));
162 }
163 if !self.edge_growth_factor.is_finite() || self.edge_growth_factor <= 1.0 {
164 return Err(SpatialError::InvalidOption(SpatialOption::CellGrowthFactor));
165 }
166 Ok(())
167 }
168}
169
170impl Default for CellGridOptions {
171 fn default() -> Self {
172 Self::MEMORY_BALANCED
173 }
174}
175
176#[derive(Clone, Copy, PartialEq, Eq, Debug)]
178pub struct KdPeriodicOptions {
179 pub maximum_image_count: usize,
181}
182
183impl KdPeriodicOptions {
184 pub const BALANCED: Self = Self {
186 maximum_image_count: 4_096,
187 };
188
189 pub fn validate(self) -> Result<(), SpatialError> {
195 if self.maximum_image_count == 0 {
196 Err(SpatialError::InvalidOption(
197 SpatialOption::KdPeriodicImageLimit,
198 ))
199 } else {
200 Ok(())
201 }
202 }
203}
204
205impl Default for KdPeriodicOptions {
206 fn default() -> Self {
207 Self::BALANCED
208 }
209}
210
211#[derive(Clone, Copy, PartialEq, Debug)]
213pub struct NeighborListOptions {
214 pub skin: f32,
216 pub cell_grid: CellGridOptions,
218}
219
220impl NeighborListOptions {
221 #[must_use]
223 pub const fn with_skin(skin: f32) -> Self {
224 Self {
225 skin,
226 cell_grid: CellGridOptions::MEMORY_BALANCED,
227 }
228 }
229
230 pub fn validate(self) -> Result<(), SpatialError> {
236 if !self.skin.is_finite() || self.skin < 0.0 {
237 return Err(SpatialError::InvalidOption(
238 SpatialOption::NeighborSkinMinimum,
239 ));
240 }
241 self.cell_grid.validate()
242 }
243}
244
245#[derive(Clone, Copy, PartialEq, Debug)]
247pub struct SpatialSearchOptions {
248 pub backend: SpatialBackend,
250 pub automatic: AutoBackendProfile,
252 pub neighbor_skin: NeighborSkinProfile,
254 pub cell_grid: CellGridOptions,
256 pub kd_periodic: KdPeriodicOptions,
258}
259
260impl SpatialSearchOptions {
261 pub const BALANCED: Self = Self {
263 backend: SpatialBackend::Auto,
264 automatic: AutoBackendProfile::BALANCED,
265 neighbor_skin: NeighborSkinProfile::BALANCED,
266 cell_grid: CellGridOptions::MEMORY_BALANCED,
267 kd_periodic: KdPeriodicOptions::BALANCED,
268 };
269
270 #[must_use]
272 pub const fn with_backend(backend: SpatialBackend) -> Self {
273 Self {
274 backend,
275 ..Self::BALANCED
276 }
277 }
278
279 pub fn validate(self) -> Result<(), SpatialError> {
285 self.automatic.validate()?;
286 self.neighbor_skin.validate()?;
287 self.cell_grid.validate()?;
288 self.kd_periodic.validate()
289 }
290
291 pub fn plan(
297 self,
298 left_count: usize,
299 right_count: usize,
300 cutoff: f32,
301 ) -> Result<SpatialPlan, SpatialError> {
302 if !cutoff.is_finite() || cutoff < 0.0 {
303 return Err(SpatialError::InvalidCutoff);
304 }
305 self.validate()?;
306 let backend = match self.backend {
307 SpatialBackend::Auto => self.automatic.resolve(left_count, right_count)?,
308 backend => backend,
309 };
310 let neighbor_skin = if backend == SpatialBackend::NeighborList {
311 Some(self.neighbor_skin.skin(cutoff)?)
312 } else {
313 None
314 };
315 Ok(SpatialPlan {
316 requested_backend: self.backend,
317 backend,
318 neighbor_skin,
319 options: self,
320 })
321 }
322}
323
324impl Default for SpatialSearchOptions {
325 fn default() -> Self {
326 Self::BALANCED
327 }
328}
329
330#[derive(Clone, Copy, PartialEq, Debug)]
332pub struct SpatialPlan {
333 pub requested_backend: SpatialBackend,
335 pub backend: SpatialBackend,
337 pub neighbor_skin: Option<f32>,
339 pub options: SpatialSearchOptions,
341}
342
343#[cfg(test)]
344#[path = "options_tests.rs"]
345mod tests;