1use rand::Rng;
10use rustsim_core::{
11 interaction::{PositionedAgent, SpaceInteraction},
12 space::Space,
13 types::AgentId,
14};
15use std::collections::HashMap;
16use thiserror::Error;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct ContinuousPos3D {
21 pub x: f64,
23 pub y: f64,
25 pub z: f64,
27}
28
29impl ContinuousPos3D {
30 pub fn new(x: f64, y: f64, z: f64) -> Self {
32 Self { x, y, z }
33 }
34
35 pub fn zero() -> Self {
37 Self {
38 x: 0.0,
39 y: 0.0,
40 z: 0.0,
41 }
42 }
43
44 pub fn distance_to(&self, other: &Self) -> f64 {
46 let dx = self.x - other.x;
47 let dy = self.y - other.y;
48 let dz = self.z - other.z;
49 (dx * dx + dy * dy + dz * dz).sqrt()
50 }
51
52 pub fn length(&self) -> f64 {
54 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
55 }
56
57 pub fn normalized(&self) -> Self {
59 let len = self.length();
60 if len < 1e-12 {
61 Self::zero()
62 } else {
63 Self::new(self.x / len, self.y / len, self.z / len)
64 }
65 }
66}
67
68impl std::ops::Add for ContinuousPos3D {
69 type Output = Self;
70 fn add(self, rhs: Self) -> Self {
71 Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
72 }
73}
74
75impl std::ops::Sub for ContinuousPos3D {
76 type Output = Self;
77 fn sub(self, rhs: Self) -> Self {
78 Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
79 }
80}
81
82impl std::ops::Mul<f64> for ContinuousPos3D {
83 type Output = Self;
84 fn mul(self, rhs: f64) -> Self {
85 Self::new(self.x * rhs, self.y * rhs, self.z * rhs)
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Error)]
91pub enum ContinuousSpace3DConfigError {
92 #[error("space extents must be positive")]
94 InvalidExtent,
95 #[error("spacing must be positive")]
97 InvalidSpacing,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
102pub enum ContinuousSpace3DError {
103 #[error("position is out of bounds")]
105 OutOfBounds,
106}
107
108#[derive(Debug, Clone)]
115pub struct ContinuousSpace3D {
116 extent_x: f64,
117 extent_y: f64,
118 extent_z: f64,
119 periodic: bool,
120 spacing: f64,
121 grid_w: usize,
122 grid_h: usize,
123 grid_d: usize,
124 cells: Vec<Vec<AgentId>>,
125 agent_positions: HashMap<AgentId, ContinuousPos3D>,
126}
127
128impl ContinuousSpace3D {
129 pub fn new(
137 extent_x: f64,
138 extent_y: f64,
139 extent_z: f64,
140 periodic: bool,
141 spacing: f64,
142 ) -> Result<Self, ContinuousSpace3DConfigError> {
143 if extent_x <= 0.0 || extent_y <= 0.0 || extent_z <= 0.0 {
144 return Err(ContinuousSpace3DConfigError::InvalidExtent);
145 }
146 if spacing <= 0.0 {
147 return Err(ContinuousSpace3DConfigError::InvalidSpacing);
148 }
149 let grid_w = (extent_x / spacing).ceil() as usize;
150 let grid_h = (extent_y / spacing).ceil() as usize;
151 let grid_d = (extent_z / spacing).ceil() as usize;
152 let cells = vec![Vec::new(); grid_w * grid_h * grid_d];
153 Ok(Self {
154 extent_x,
155 extent_y,
156 extent_z,
157 periodic,
158 spacing,
159 grid_w,
160 grid_h,
161 grid_d,
162 cells,
163 agent_positions: HashMap::new(),
164 })
165 }
166
167 pub fn extent(&self) -> (f64, f64, f64) {
169 (self.extent_x, self.extent_y, self.extent_z)
170 }
171
172 pub fn periodic(&self) -> bool {
174 self.periodic
175 }
176
177 pub fn spacing(&self) -> f64 {
179 self.spacing
180 }
181
182 pub fn agent_position(&self, id: AgentId) -> Option<&ContinuousPos3D> {
184 self.agent_positions.get(&id)
185 }
186
187 fn normalize_pos(&self, pos: ContinuousPos3D) -> Option<ContinuousPos3D> {
188 if self.periodic {
189 let x = ((pos.x % self.extent_x) + self.extent_x) % self.extent_x;
190 let y = ((pos.y % self.extent_y) + self.extent_y) % self.extent_y;
191 let z = ((pos.z % self.extent_z) + self.extent_z) % self.extent_z;
192 Some(ContinuousPos3D::new(x, y, z))
193 } else if pos.x >= 0.0
194 && pos.x < self.extent_x
195 && pos.y >= 0.0
196 && pos.y < self.extent_y
197 && pos.z >= 0.0
198 && pos.z < self.extent_z
199 {
200 Some(pos)
201 } else {
202 None
203 }
204 }
205
206 fn pos_to_cell(&self, pos: &ContinuousPos3D) -> (usize, usize, usize) {
207 let cx = ((pos.x / self.spacing).floor() as usize).min(self.grid_w - 1);
208 let cy = ((pos.y / self.spacing).floor() as usize).min(self.grid_h - 1);
209 let cz = ((pos.z / self.spacing).floor() as usize).min(self.grid_d - 1);
210 (cx, cy, cz)
211 }
212
213 fn cell_index(&self, cx: usize, cy: usize, cz: usize) -> usize {
214 (cz * self.grid_h + cy) * self.grid_w + cx
215 }
216
217 fn add_to_grid(&mut self, id: AgentId, pos: &ContinuousPos3D) {
218 let (cx, cy, cz) = self.pos_to_cell(pos);
219 let idx = self.cell_index(cx, cy, cz);
220 self.cells[idx].push(id);
221 }
222
223 fn remove_from_grid(&mut self, id: AgentId, pos: &ContinuousPos3D) {
224 let (cx, cy, cz) = self.pos_to_cell(pos);
225 let idx = self.cell_index(cx, cy, cz);
226 if let Some(i) = self.cells[idx].iter().position(|v| *v == id) {
227 self.cells[idx].swap_remove(i);
228 }
229 }
230
231 pub fn move_agent_pos(
236 &mut self,
237 id: AgentId,
238 new_pos: ContinuousPos3D,
239 ) -> Result<ContinuousPos3D, ContinuousSpace3DError> {
240 let new_pos = self
241 .normalize_pos(new_pos)
242 .ok_or(ContinuousSpace3DError::OutOfBounds)?;
243
244 if let Some(old_pos) = self.agent_positions.get(&id).copied() {
245 let old_cell = self.pos_to_cell(&old_pos);
246 let new_cell = self.pos_to_cell(&new_pos);
247 if old_cell != new_cell {
248 self.remove_from_grid(id, &old_pos);
249 self.add_to_grid(id, &new_pos);
250 }
251 self.agent_positions.insert(id, new_pos);
252 }
253 Ok(new_pos)
254 }
255
256 fn euclidean_distance_wrapped(&self, a: &ContinuousPos3D, b: &ContinuousPos3D) -> f64 {
257 let mut dx = (a.x - b.x).abs();
258 let mut dy = (a.y - b.y).abs();
259 let mut dz = (a.z - b.z).abs();
260 if self.periodic {
261 if dx > self.extent_x / 2.0 {
262 dx = self.extent_x - dx;
263 }
264 if dy > self.extent_y / 2.0 {
265 dy = self.extent_y - dy;
266 }
267 if dz > self.extent_z / 2.0 {
268 dz = self.extent_z - dz;
269 }
270 }
271 (dx * dx + dy * dy + dz * dz).sqrt()
272 }
273
274 pub fn nearby_ids_euclidean(&self, pos: &ContinuousPos3D, radius: f64) -> Vec<AgentId> {
278 let grid_r = ((radius / self.spacing).ceil() as i32) + 1;
279 let (focal_cx, focal_cy, focal_cz) = self.pos_to_cell(pos);
280 let mut ids = Vec::new();
281
282 for dz in -grid_r..=grid_r {
283 for dy in -grid_r..=grid_r {
284 for dx in -grid_r..=grid_r {
285 let nx = focal_cx as i32 + dx;
286 let ny = focal_cy as i32 + dy;
287 let nz = focal_cz as i32 + dz;
288
289 let cell = if self.periodic {
290 let px =
291 ((nx % self.grid_w as i32) + self.grid_w as i32) % self.grid_w as i32;
292 let py =
293 ((ny % self.grid_h as i32) + self.grid_h as i32) % self.grid_h as i32;
294 let pz =
295 ((nz % self.grid_d as i32) + self.grid_d as i32) % self.grid_d as i32;
296 Some((px as usize, py as usize, pz as usize))
297 } else if nx >= 0
298 && ny >= 0
299 && nz >= 0
300 && (nx as usize) < self.grid_w
301 && (ny as usize) < self.grid_h
302 && (nz as usize) < self.grid_d
303 {
304 Some((nx as usize, ny as usize, nz as usize))
305 } else {
306 None
307 };
308
309 if let Some((cx, cy, cz)) = cell {
310 let idx = self.cell_index(cx, cy, cz);
311 for &id in &self.cells[idx] {
312 if let Some(agent_pos) = self.agent_positions.get(&id) {
313 if self.euclidean_distance_wrapped(pos, agent_pos) <= radius {
314 ids.push(id);
315 }
316 }
317 }
318 }
319 }
320 }
321 }
322 ids
323 }
324}
325
326impl Space for ContinuousSpace3D {}
327
328impl<A> SpaceInteraction<A> for ContinuousSpace3D
329where
330 A: PositionedAgent<Position = ContinuousPos3D>,
331{
332 type Error = ContinuousSpace3DError;
333
334 fn random_position<R: rand::RngCore>(&self, rng: &mut R) -> A::Position {
335 ContinuousPos3D::new(
336 rng.gen_range(0.0..self.extent_x),
337 rng.gen_range(0.0..self.extent_y),
338 rng.gen_range(0.0..self.extent_z),
339 )
340 }
341
342 fn add_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
343 let pos = self
344 .normalize_pos(*agent.position())
345 .ok_or(ContinuousSpace3DError::OutOfBounds)?;
346 let id = agent.id();
347 self.agent_positions.insert(id, pos);
348 self.add_to_grid(id, &pos);
349 Ok(())
350 }
351
352 fn remove_agent(&mut self, agent: &A) -> Result<(), Self::Error> {
353 let id = agent.id();
354 if let Some(pos) = self.agent_positions.remove(&id) {
355 self.remove_from_grid(id, &pos);
356 }
357 Ok(())
358 }
359
360 fn nearby_ids(&self, position: &A::Position, radius: usize) -> Vec<AgentId> {
361 self.nearby_ids_euclidean(position, radius as f64)
362 }
363}