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