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
11const OFFSETS_8: [(i32, i32); 8] = [
13 (-1, 0),
14 (1, 0),
15 (0, -1),
16 (0, 1),
17 (-1, -1),
18 (-1, 1),
19 (1, -1),
20 (1, 1),
21];
22
23#[derive(Debug, Clone)]
34pub struct Square8 {
35 rows: u32,
36 cols: u32,
37 edge: EdgeBehavior,
38 instance_id: SpaceInstanceId,
39}
40
41impl Square8 {
42 pub const MAX_DIM: u32 = i32::MAX as u32;
46
47 pub fn new(rows: u32, cols: u32, edge: EdgeBehavior) -> Result<Self, SpaceError> {
50 if rows == 0 || cols == 0 {
51 return Err(SpaceError::EmptySpace);
52 }
53 if rows > Self::MAX_DIM {
54 return Err(SpaceError::DimensionTooLarge {
55 name: "rows",
56 value: rows,
57 max: Self::MAX_DIM,
58 });
59 }
60 if cols > Self::MAX_DIM {
61 return Err(SpaceError::DimensionTooLarge {
62 name: "cols",
63 value: cols,
64 max: Self::MAX_DIM,
65 });
66 }
67 Ok(Self {
68 rows,
69 cols,
70 edge,
71 instance_id: SpaceInstanceId::next(),
72 })
73 }
74
75 pub fn rows(&self) -> u32 {
77 self.rows
78 }
79
80 pub fn cols(&self) -> u32 {
82 self.cols
83 }
84
85 pub fn is_empty(&self) -> bool {
87 false
88 }
89
90 pub fn edge_behavior(&self) -> EdgeBehavior {
92 self.edge
93 }
94
95 fn neighbours_rc(&self, r: i32, c: i32) -> Vec<(i32, i32)> {
97 let mut result = Vec::with_capacity(8);
98 for (dr, dc) in OFFSETS_8 {
99 let nr = grid2d::resolve_axis(r + dr, self.rows, self.edge);
100 let nc = grid2d::resolve_axis(c + dc, self.cols, self.edge);
101 if let (Some(nr), Some(nc)) = (nr, nc) {
102 result.push((nr, nc));
103 }
104 }
105 result
106 }
107}
108
109impl Space for Square8 {
110 fn ndim(&self) -> usize {
111 2
112 }
113
114 fn cell_count(&self) -> usize {
115 (self.rows as usize) * (self.cols as usize)
116 }
117
118 fn neighbours(&self, coord: &Coord) -> SmallVec<[Coord; 8]> {
119 let r = coord[0];
120 let c = coord[1];
121 self.neighbours_rc(r, c)
122 .into_iter()
123 .map(|(nr, nc)| smallvec![nr, nc])
124 .collect()
125 }
126
127 fn max_neighbour_degree(&self) -> usize {
128 match self.edge {
129 EdgeBehavior::Clamp | EdgeBehavior::Wrap => 8,
130 EdgeBehavior::Absorb => {
131 let axis_max = |len: u32| -> usize {
132 match len {
133 0 | 1 => 0,
134 2 => 1,
135 _ => 2,
136 }
137 };
138 let v = axis_max(self.rows);
139 let h = axis_max(self.cols);
140 (v + h) + (v * h)
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.max(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 mut result = Vec::with_capacity(8);
159 for (dr, dc) in OFFSETS_8 {
160 let nr = grid2d::resolve_axis(r + dr, rows, edge);
161 let nc = grid2d::resolve_axis(c + dc, cols, edge);
162 if let (Some(nr), Some(nc)) = (nr, nc) {
163 result.push((nr, nc));
164 }
165 }
166 result
167 })
168 }
169
170 fn canonical_ordering(&self) -> Vec<Coord> {
171 grid2d::canonical_ordering_2d(self.rows, self.cols)
172 }
173
174 fn canonical_rank(&self, coord: &Coord) -> Option<usize> {
175 grid2d::canonical_rank_2d(coord, self.rows, self.cols)
176 }
177
178 fn canonical_rank_slice(&self, coord: &[i32]) -> Option<usize> {
179 if coord.len() != 2 {
180 return None;
181 }
182 let r = coord[0];
183 let c = coord[1];
184 if r >= 0 && r < self.rows as i32 && c >= 0 && c < self.cols as i32 {
185 Some(r as usize * self.cols as usize + c as usize)
186 } else {
187 None
188 }
189 }
190
191 fn instance_id(&self) -> SpaceInstanceId {
192 self.instance_id
193 }
194
195 fn topology_eq(&self, other: &dyn Space) -> bool {
196 (other as &dyn std::any::Any)
197 .downcast_ref::<Self>()
198 .is_some_and(|o| self.rows == o.rows && self.cols == o.cols && self.edge == o.edge)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::compliance;
206 use murk_core::Coord;
207 use proptest::prelude::*;
208
209 fn c(r: i32, col: i32) -> Coord {
210 smallvec![r, col]
211 }
212
213 #[test]
216 fn neighbours_absorb_interior() {
217 let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
218 let n = s.neighbours(&c(2, 2));
219 assert_eq!(n.len(), 8);
220 }
221
222 #[test]
223 fn neighbours_absorb_corner() {
224 let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
225 let n = s.neighbours(&c(0, 0));
226 assert_eq!(n.len(), 3);
227 assert!(n.contains(&c(1, 0)));
228 assert!(n.contains(&c(0, 1)));
229 assert!(n.contains(&c(1, 1)));
230 }
231
232 #[test]
233 fn neighbours_absorb_edge() {
234 let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
235 let n = s.neighbours(&c(0, 2));
236 assert_eq!(n.len(), 5);
237 }
238
239 #[test]
240 fn neighbours_wrap_corner() {
241 let s = Square8::new(5, 5, EdgeBehavior::Wrap).unwrap();
242 let n = s.neighbours(&c(0, 0));
243 assert_eq!(n.len(), 8);
244 assert!(n.contains(&c(4, 4))); assert!(n.contains(&c(4, 0))); assert!(n.contains(&c(0, 4))); }
248
249 #[test]
252 fn distance_chebyshev_absorb() {
253 let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
254 assert_eq!(s.distance(&c(0, 0), &c(1, 1)), 1.0);
256 assert_eq!(s.distance(&c(0, 0), &c(3, 4)), 4.0);
258 assert_eq!(s.distance(&c(2, 3), &c(5, 7)), 4.0);
260 }
261
262 #[test]
263 fn distance_chebyshev_wrap() {
264 let s = Square8::new(10, 10, EdgeBehavior::Wrap).unwrap();
265 assert_eq!(s.distance(&c(0, 0), &c(9, 9)), 1.0);
267 }
268
269 #[test]
272 fn compile_region_all() {
273 let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
274 let plan = s.compile_region(&RegionSpec::All).unwrap();
275 assert_eq!(plan.cell_count(), 25);
276 assert_eq!(plan.valid_ratio(), 1.0);
277 }
278
279 #[test]
280 fn compile_region_disk_square_shape() {
281 let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
283 let plan = s
284 .compile_region(&RegionSpec::Disk {
285 center: c(5, 5),
286 radius: 2,
287 })
288 .unwrap();
289 assert_eq!(plan.cell_count(), 25);
291 }
292
293 #[test]
294 fn compile_region_rect() {
295 let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
296 let plan = s
297 .compile_region(&RegionSpec::Rect {
298 min: c(2, 3),
299 max: c(4, 6),
300 })
301 .unwrap();
302 assert_eq!(plan.cell_count(), 12); }
304
305 #[test]
308 fn new_zero_rows_returns_error() {
309 assert!(matches!(
310 Square8::new(0, 5, EdgeBehavior::Absorb),
311 Err(SpaceError::EmptySpace)
312 ));
313 }
314
315 #[test]
316 fn new_rejects_dims_exceeding_i32_max() {
317 let big = i32::MAX as u32 + 1;
318 assert!(matches!(
319 Square8::new(big, 5, EdgeBehavior::Absorb),
320 Err(SpaceError::DimensionTooLarge { name: "rows", .. })
321 ));
322 assert!(matches!(
323 Square8::new(5, big, EdgeBehavior::Absorb),
324 Err(SpaceError::DimensionTooLarge { name: "cols", .. })
325 ));
326 }
327
328 #[test]
331 fn single_cell_absorb() {
332 let s = Square8::new(1, 1, EdgeBehavior::Absorb).unwrap();
333 assert!(s.neighbours(&c(0, 0)).is_empty());
334 }
335
336 #[test]
337 fn single_cell_wrap() {
338 let s = Square8::new(1, 1, EdgeBehavior::Wrap).unwrap();
339 let n = s.neighbours(&c(0, 0));
340 assert_eq!(n.len(), 8);
342 assert!(n.iter().all(|nb| nb == &c(0, 0)));
343 }
344
345 #[test]
348 fn compliance_absorb() {
349 let s = Square8::new(8, 8, EdgeBehavior::Absorb).unwrap();
350 compliance::run_full_compliance(&s);
351 }
352
353 #[test]
354 fn compliance_clamp() {
355 let s = Square8::new(8, 8, EdgeBehavior::Clamp).unwrap();
356 compliance::run_full_compliance(&s);
357 }
358
359 #[test]
360 fn compliance_wrap() {
361 let s = Square8::new(8, 8, EdgeBehavior::Wrap).unwrap();
362 compliance::run_full_compliance(&s);
363 }
364
365 #[test]
368 fn downcast_ref_square8() {
369 let s: Box<dyn Space> = Box::new(Square8::new(3, 3, EdgeBehavior::Absorb).unwrap());
370 assert!(s.downcast_ref::<Square8>().is_some());
371 assert!(s.downcast_ref::<crate::Square4>().is_none());
372 }
373
374 fn arb_edge() -> impl Strategy<Value = EdgeBehavior> {
377 prop_oneof![
378 Just(EdgeBehavior::Absorb),
379 Just(EdgeBehavior::Clamp),
380 Just(EdgeBehavior::Wrap),
381 ]
382 }
383
384 proptest! {
385 #[test]
386 fn distance_is_metric(
387 rows in 2u32..10,
388 cols in 2u32..10,
389 edge in arb_edge(),
390 ar in 0i32..10, ac in 0i32..10,
391 br in 0i32..10, bc in 0i32..10,
392 cr in 0i32..10, cc_val in 0i32..10,
393 ) {
394 let ar = ar % rows as i32;
395 let ac = ac % cols as i32;
396 let br = br % rows as i32;
397 let bc = bc % cols as i32;
398 let cr = cr % rows as i32;
399 let cc_val = cc_val % cols as i32;
400 let s = Square8::new(rows, cols, edge).unwrap();
401 let a: Coord = smallvec![ar, ac];
402 let b: Coord = smallvec![br, bc];
403 let cv: Coord = smallvec![cr, cc_val];
404
405 prop_assert!((s.distance(&a, &a) - 0.0).abs() < f64::EPSILON);
406 prop_assert!((s.distance(&a, &b) - s.distance(&b, &a)).abs() < f64::EPSILON);
407 prop_assert!(s.distance(&a, &cv) <= s.distance(&a, &b) + s.distance(&b, &cv) + f64::EPSILON);
408 }
409
410 #[test]
411 fn neighbours_symmetric(
412 rows in 2u32..10,
413 cols in 2u32..10,
414 edge in arb_edge(),
415 r in 0i32..10, col in 0i32..10,
416 ) {
417 let r = r % rows as i32;
418 let col = col % cols as i32;
419 let s = Square8::new(rows, cols, edge).unwrap();
420 let coord: Coord = smallvec![r, col];
421 for nb in s.neighbours(&coord) {
422 let nb_neighbours = s.neighbours(&nb);
423 prop_assert!(
424 nb_neighbours.contains(&coord),
425 "neighbour symmetry violated: {:?} in N({:?}) but {:?} not in N({:?})",
426 nb, coord, coord, nb,
427 );
428 }
429 }
430 }
431}