1use crate::edge::EdgeBehavior;
4use crate::error::SpaceError;
5use crate::grid2d;
6use crate::region::{RegionPlan, RegionSpec};
7use crate::space::Space;
8use murk_core::{Coord, SpaceInstanceId};
9use smallvec::{smallvec, SmallVec};
10
11#[derive(Debug, Clone)]
22pub struct Square4 {
23 rows: u32,
24 cols: u32,
25 edge: EdgeBehavior,
26 instance_id: SpaceInstanceId,
27}
28
29impl Square4 {
30 pub const MAX_DIM: u32 = i32::MAX as u32;
32
33 pub fn new(rows: u32, cols: u32, edge: EdgeBehavior) -> Result<Self, SpaceError> {
52 if rows == 0 || cols == 0 {
53 return Err(SpaceError::EmptySpace);
54 }
55 if rows > Self::MAX_DIM {
56 return Err(SpaceError::DimensionTooLarge {
57 name: "rows",
58 value: rows,
59 max: Self::MAX_DIM,
60 });
61 }
62 if cols > Self::MAX_DIM {
63 return Err(SpaceError::DimensionTooLarge {
64 name: "cols",
65 value: cols,
66 max: Self::MAX_DIM,
67 });
68 }
69 Ok(Self {
70 rows,
71 cols,
72 edge,
73 instance_id: SpaceInstanceId::next(),
74 })
75 }
76
77 pub fn rows(&self) -> u32 {
79 self.rows
80 }
81
82 pub fn cols(&self) -> u32 {
84 self.cols
85 }
86
87 pub fn is_empty(&self) -> bool {
89 false
90 }
91
92 pub fn edge_behavior(&self) -> EdgeBehavior {
94 self.edge
95 }
96
97 fn neighbours_rc(&self, r: i32, c: i32) -> Vec<(i32, i32)> {
99 let offsets: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
100 let mut result = Vec::with_capacity(4);
101 for (dr, dc) in offsets {
102 let nr = grid2d::resolve_axis(r + dr, self.rows, self.edge);
103 let nc = grid2d::resolve_axis(c + dc, self.cols, self.edge);
104 if let (Some(nr), Some(nc)) = (nr, nc) {
105 result.push((nr, nc));
106 }
107 }
108 result
109 }
110}
111
112impl Space for Square4 {
113 fn ndim(&self) -> usize {
114 2
115 }
116
117 fn cell_count(&self) -> usize {
118 (self.rows as usize) * (self.cols as usize)
119 }
120
121 fn neighbours(&self, coord: &Coord) -> SmallVec<[Coord; 8]> {
122 let r = coord[0];
123 let c = coord[1];
124 self.neighbours_rc(r, c)
125 .into_iter()
126 .map(|(nr, nc)| smallvec![nr, nc])
127 .collect()
128 }
129
130 fn max_neighbour_degree(&self) -> usize {
131 match self.edge {
132 EdgeBehavior::Clamp | EdgeBehavior::Wrap => 4,
133 EdgeBehavior::Absorb => {
134 let axis_max = |len: u32| -> usize {
135 match len {
136 0 | 1 => 0,
137 2 => 1,
138 _ => 2,
139 }
140 };
141 axis_max(self.rows) + axis_max(self.cols)
142 }
143 }
144 }
145
146 fn distance(&self, a: &Coord, b: &Coord) -> f64 {
147 let dr = grid2d::axis_distance(a[0], b[0], self.rows, self.edge);
149 let dc = grid2d::axis_distance(a[1], b[1], self.cols, self.edge);
150 dr + dc
151 }
152
153 fn compile_region(&self, spec: &RegionSpec) -> Result<RegionPlan, SpaceError> {
154 let edge = self.edge;
155 let rows = self.rows;
156 let cols = self.cols;
157 grid2d::compile_region_2d(spec, rows, cols, self, |r, c| {
158 let offsets: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
159 let mut result = Vec::with_capacity(4);
160 for (dr, dc) in offsets {
161 let nr = grid2d::resolve_axis(r + dr, rows, edge);
162 let nc = grid2d::resolve_axis(c + dc, cols, edge);
163 if let (Some(nr), Some(nc)) = (nr, nc) {
164 result.push((nr, nc));
165 }
166 }
167 result
168 })
169 }
170
171 fn canonical_ordering(&self) -> Vec<Coord> {
172 grid2d::canonical_ordering_2d(self.rows, self.cols)
173 }
174
175 fn canonical_rank(&self, coord: &Coord) -> Option<usize> {
176 grid2d::canonical_rank_2d(coord, self.rows, self.cols)
177 }
178
179 fn canonical_rank_slice(&self, coord: &[i32]) -> Option<usize> {
180 if coord.len() != 2 {
181 return None;
182 }
183 let r = coord[0];
184 let c = coord[1];
185 if r >= 0 && r < self.rows as i32 && c >= 0 && c < self.cols as i32 {
186 Some(r as usize * self.cols as usize + c as usize)
187 } else {
188 None
189 }
190 }
191
192 fn instance_id(&self) -> SpaceInstanceId {
193 self.instance_id
194 }
195
196 fn topology_eq(&self, other: &dyn Space) -> bool {
197 (other as &dyn std::any::Any)
198 .downcast_ref::<Self>()
199 .is_some_and(|o| self.rows == o.rows && self.cols == o.cols && self.edge == o.edge)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::compliance;
207 use murk_core::Coord;
208 use proptest::prelude::*;
209
210 fn c(r: i32, col: i32) -> Coord {
211 smallvec![r, col]
212 }
213
214 #[test]
217 fn neighbours_absorb_interior() {
218 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
219 let n = s.neighbours(&c(2, 2));
220 assert_eq!(n.len(), 4);
221 assert!(n.contains(&c(1, 2))); assert!(n.contains(&c(3, 2))); assert!(n.contains(&c(2, 1))); assert!(n.contains(&c(2, 3))); }
226
227 #[test]
228 fn neighbours_absorb_corner() {
229 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
230 let n = s.neighbours(&c(0, 0));
231 assert_eq!(n.len(), 2);
232 assert!(n.contains(&c(1, 0)));
233 assert!(n.contains(&c(0, 1)));
234 }
235
236 #[test]
237 fn neighbours_absorb_edge() {
238 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
239 let n = s.neighbours(&c(0, 2));
240 assert_eq!(n.len(), 3);
241 assert!(n.contains(&c(1, 2)));
242 assert!(n.contains(&c(0, 1)));
243 assert!(n.contains(&c(0, 3)));
244 }
245
246 #[test]
247 fn neighbours_clamp_corner() {
248 let s = Square4::new(5, 5, EdgeBehavior::Clamp).unwrap();
249 let n = s.neighbours(&c(0, 0));
250 assert_eq!(n.len(), 4);
251 assert!(n.contains(&c(0, 0))); assert!(n.contains(&c(1, 0)));
254 assert!(n.contains(&c(0, 0))); assert!(n.contains(&c(0, 1)));
256 }
257
258 #[test]
259 fn neighbours_wrap_corner() {
260 let s = Square4::new(5, 5, EdgeBehavior::Wrap).unwrap();
261 let n = s.neighbours(&c(0, 0));
262 assert_eq!(n.len(), 4);
263 assert!(n.contains(&c(4, 0))); assert!(n.contains(&c(1, 0))); assert!(n.contains(&c(0, 4))); assert!(n.contains(&c(0, 1))); }
268
269 #[test]
270 fn neighbours_wrap_opposite_corner() {
271 let s = Square4::new(5, 5, EdgeBehavior::Wrap).unwrap();
272 let n = s.neighbours(&c(4, 4));
273 assert_eq!(n.len(), 4);
274 assert!(n.contains(&c(3, 4))); assert!(n.contains(&c(0, 4))); assert!(n.contains(&c(4, 3))); assert!(n.contains(&c(4, 0))); }
279
280 #[test]
283 fn distance_manhattan_absorb() {
284 let s = Square4::new(10, 10, EdgeBehavior::Absorb).unwrap();
285 assert_eq!(s.distance(&c(0, 0), &c(3, 4)), 7.0);
286 assert_eq!(s.distance(&c(2, 3), &c(5, 7)), 7.0);
287 }
288
289 #[test]
290 fn distance_manhattan_wrap() {
291 let s = Square4::new(10, 10, EdgeBehavior::Wrap).unwrap();
292 assert_eq!(s.distance(&c(0, 0), &c(9, 9)), 2.0);
294 assert_eq!(s.distance(&c(0, 0), &c(3, 4)), 7.0);
296 }
297
298 #[test]
301 fn compile_region_all() {
302 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
303 let plan = s.compile_region(&RegionSpec::All).unwrap();
304 assert_eq!(plan.cell_count(), 25);
305 assert_eq!(plan.valid_ratio(), 1.0);
306 }
307
308 #[test]
309 fn compile_region_disk_diamond_shape() {
310 let s = Square4::new(10, 10, EdgeBehavior::Absorb).unwrap();
312 let plan = s
313 .compile_region(&RegionSpec::Disk {
314 center: c(5, 5),
315 radius: 2,
316 })
317 .unwrap();
318 assert_eq!(plan.cell_count(), 13);
320 }
321
322 #[test]
323 fn compile_region_rect() {
324 let s = Square4::new(10, 10, EdgeBehavior::Absorb).unwrap();
325 let plan = s
326 .compile_region(&RegionSpec::Rect {
327 min: c(2, 3),
328 max: c(4, 6),
329 })
330 .unwrap();
331 assert_eq!(plan.cell_count(), 12); }
333
334 #[test]
335 fn compile_region_rect_invalid() {
336 let s = Square4::new(10, 10, EdgeBehavior::Absorb).unwrap();
337 assert!(s
338 .compile_region(&RegionSpec::Rect {
339 min: c(5, 0),
340 max: c(2, 3),
341 })
342 .is_err());
343 }
344
345 #[test]
346 fn compile_region_coords_valid() {
347 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
348 let plan = s
349 .compile_region(&RegionSpec::Coords(vec![c(1, 2), c(3, 4), c(0, 0)]))
350 .unwrap();
351 assert_eq!(plan.coords, vec![c(0, 0), c(1, 2), c(3, 4)]);
352 }
353
354 #[test]
355 fn compile_region_coords_oob() {
356 let s = Square4::new(5, 5, EdgeBehavior::Absorb).unwrap();
357 assert!(s
358 .compile_region(&RegionSpec::Coords(vec![c(10, 0)]))
359 .is_err());
360 }
361
362 #[test]
365 fn new_zero_rows_returns_error() {
366 assert!(matches!(
367 Square4::new(0, 5, EdgeBehavior::Absorb),
368 Err(SpaceError::EmptySpace)
369 ));
370 }
371
372 #[test]
373 fn new_zero_cols_returns_error() {
374 assert!(matches!(
375 Square4::new(5, 0, EdgeBehavior::Absorb),
376 Err(SpaceError::EmptySpace)
377 ));
378 }
379
380 #[test]
381 fn new_rejects_dims_exceeding_i32_max() {
382 let big = i32::MAX as u32 + 1;
383 assert!(matches!(
384 Square4::new(big, 5, EdgeBehavior::Absorb),
385 Err(SpaceError::DimensionTooLarge { name: "rows", .. })
386 ));
387 assert!(matches!(
388 Square4::new(5, big, EdgeBehavior::Absorb),
389 Err(SpaceError::DimensionTooLarge { name: "cols", .. })
390 ));
391 assert!(Square4::new(i32::MAX as u32, 1, EdgeBehavior::Absorb).is_ok());
392 }
393
394 #[test]
397 fn single_cell_absorb() {
398 let s = Square4::new(1, 1, EdgeBehavior::Absorb).unwrap();
399 assert!(s.neighbours(&c(0, 0)).is_empty());
400 }
401
402 #[test]
403 fn single_cell_wrap() {
404 let s = Square4::new(1, 1, EdgeBehavior::Wrap).unwrap();
405 let n = s.neighbours(&c(0, 0));
406 assert_eq!(n.len(), 4);
408 assert!(n.iter().all(|nb| nb == &c(0, 0)));
409 }
410
411 #[test]
414 fn compliance_absorb() {
415 let s = Square4::new(8, 8, EdgeBehavior::Absorb).unwrap();
416 compliance::run_full_compliance(&s);
417 }
418
419 #[test]
420 fn compliance_clamp() {
421 let s = Square4::new(8, 8, EdgeBehavior::Clamp).unwrap();
422 compliance::run_full_compliance(&s);
423 }
424
425 #[test]
426 fn compliance_wrap() {
427 let s = Square4::new(8, 8, EdgeBehavior::Wrap).unwrap();
428 compliance::run_full_compliance(&s);
429 }
430
431 #[test]
434 fn downcast_ref_square4() {
435 let s: Box<dyn Space> = Box::new(Square4::new(3, 3, EdgeBehavior::Absorb).unwrap());
436 assert!(s.downcast_ref::<Square4>().is_some());
437 assert!(s.downcast_ref::<crate::Ring1D>().is_none());
438 }
439
440 fn arb_edge() -> impl Strategy<Value = EdgeBehavior> {
443 prop_oneof![
444 Just(EdgeBehavior::Absorb),
445 Just(EdgeBehavior::Clamp),
446 Just(EdgeBehavior::Wrap),
447 ]
448 }
449
450 proptest! {
451 #[test]
452 fn distance_is_metric(
453 rows in 2u32..10,
454 cols in 2u32..10,
455 edge in arb_edge(),
456 ar in 0i32..10, ac in 0i32..10,
457 br in 0i32..10, bc in 0i32..10,
458 cr in 0i32..10, cc in 0i32..10,
459 ) {
460 let ar = ar % rows as i32;
461 let ac = ac % cols as i32;
462 let br = br % rows as i32;
463 let bc = bc % cols as i32;
464 let cr = cr % rows as i32;
465 let cc = cc % cols as i32;
466 let s = Square4::new(rows, cols, edge).unwrap();
467 let a: Coord = smallvec![ar, ac];
468 let b: Coord = smallvec![br, bc];
469 let cv: Coord = smallvec![cr, cc];
470
471 prop_assert!((s.distance(&a, &a) - 0.0).abs() < f64::EPSILON);
472 prop_assert!((s.distance(&a, &b) - s.distance(&b, &a)).abs() < f64::EPSILON);
473 prop_assert!(s.distance(&a, &cv) <= s.distance(&a, &b) + s.distance(&b, &cv) + f64::EPSILON);
474 }
475
476 #[test]
477 fn neighbours_symmetric(
478 rows in 2u32..10,
479 cols in 2u32..10,
480 edge in arb_edge(),
481 r in 0i32..10, col in 0i32..10,
482 ) {
483 let r = r % rows as i32;
484 let col = col % cols as i32;
485 let s = Square4::new(rows, cols, edge).unwrap();
486 let coord: Coord = smallvec![r, col];
487 for nb in s.neighbours(&coord) {
488 let nb_neighbours = s.neighbours(&nb);
489 prop_assert!(
490 nb_neighbours.contains(&coord),
491 "neighbour symmetry violated: {:?} in N({:?}) but {:?} not in N({:?})",
492 nb, coord, coord, nb,
493 );
494 }
495 }
496 }
497}