use crate::edge::EdgeBehavior;
use crate::error::SpaceError;
use crate::grid2d;
use crate::region::{RegionPlan, RegionSpec};
use crate::space::Space;
use murk_core::{Coord, SpaceInstanceId};
use smallvec::{smallvec, SmallVec};
const OFFSETS_8: [(i32, i32); 8] = [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
];
#[derive(Debug, Clone)]
pub struct Square8 {
rows: u32,
cols: u32,
edge: EdgeBehavior,
instance_id: SpaceInstanceId,
}
impl Square8 {
pub const MAX_DIM: u32 = i32::MAX as u32;
pub fn new(rows: u32, cols: u32, edge: EdgeBehavior) -> Result<Self, SpaceError> {
if rows == 0 || cols == 0 {
return Err(SpaceError::EmptySpace);
}
if rows > Self::MAX_DIM {
return Err(SpaceError::DimensionTooLarge {
name: "rows",
value: rows,
max: Self::MAX_DIM,
});
}
if cols > Self::MAX_DIM {
return Err(SpaceError::DimensionTooLarge {
name: "cols",
value: cols,
max: Self::MAX_DIM,
});
}
Ok(Self {
rows,
cols,
edge,
instance_id: SpaceInstanceId::next(),
})
}
pub fn rows(&self) -> u32 {
self.rows
}
pub fn cols(&self) -> u32 {
self.cols
}
pub fn is_empty(&self) -> bool {
false
}
pub fn edge_behavior(&self) -> EdgeBehavior {
self.edge
}
fn neighbours_rc(&self, r: i32, c: i32) -> Vec<(i32, i32)> {
let mut result = Vec::with_capacity(8);
for (dr, dc) in OFFSETS_8 {
let nr = grid2d::resolve_axis(r + dr, self.rows, self.edge);
let nc = grid2d::resolve_axis(c + dc, self.cols, self.edge);
if let (Some(nr), Some(nc)) = (nr, nc) {
result.push((nr, nc));
}
}
result
}
}
impl Space for Square8 {
fn ndim(&self) -> usize {
2
}
fn cell_count(&self) -> usize {
(self.rows as usize) * (self.cols as usize)
}
fn neighbours(&self, coord: &Coord) -> SmallVec<[Coord; 8]> {
let r = coord[0];
let c = coord[1];
self.neighbours_rc(r, c)
.into_iter()
.map(|(nr, nc)| smallvec![nr, nc])
.collect()
}
fn distance(&self, a: &Coord, b: &Coord) -> f64 {
let dr = grid2d::axis_distance(a[0], b[0], self.rows, self.edge);
let dc = grid2d::axis_distance(a[1], b[1], self.cols, self.edge);
dr.max(dc)
}
fn compile_region(&self, spec: &RegionSpec) -> Result<RegionPlan, SpaceError> {
let edge = self.edge;
let rows = self.rows;
let cols = self.cols;
grid2d::compile_region_2d(spec, rows, cols, self, |r, c| {
let mut result = Vec::with_capacity(8);
for (dr, dc) in OFFSETS_8 {
let nr = grid2d::resolve_axis(r + dr, rows, edge);
let nc = grid2d::resolve_axis(c + dc, cols, edge);
if let (Some(nr), Some(nc)) = (nr, nc) {
result.push((nr, nc));
}
}
result
})
}
fn canonical_ordering(&self) -> Vec<Coord> {
grid2d::canonical_ordering_2d(self.rows, self.cols)
}
fn canonical_rank(&self, coord: &Coord) -> Option<usize> {
grid2d::canonical_rank_2d(coord, self.rows, self.cols)
}
fn instance_id(&self) -> SpaceInstanceId {
self.instance_id
}
fn topology_eq(&self, other: &dyn Space) -> bool {
(other as &dyn std::any::Any)
.downcast_ref::<Self>()
.is_some_and(|o| self.rows == o.rows && self.cols == o.cols && self.edge == o.edge)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compliance;
use murk_core::Coord;
use proptest::prelude::*;
fn c(r: i32, col: i32) -> Coord {
smallvec![r, col]
}
#[test]
fn neighbours_absorb_interior() {
let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
let n = s.neighbours(&c(2, 2));
assert_eq!(n.len(), 8);
}
#[test]
fn neighbours_absorb_corner() {
let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
let n = s.neighbours(&c(0, 0));
assert_eq!(n.len(), 3);
assert!(n.contains(&c(1, 0)));
assert!(n.contains(&c(0, 1)));
assert!(n.contains(&c(1, 1)));
}
#[test]
fn neighbours_absorb_edge() {
let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
let n = s.neighbours(&c(0, 2));
assert_eq!(n.len(), 5);
}
#[test]
fn neighbours_wrap_corner() {
let s = Square8::new(5, 5, EdgeBehavior::Wrap).unwrap();
let n = s.neighbours(&c(0, 0));
assert_eq!(n.len(), 8);
assert!(n.contains(&c(4, 4))); assert!(n.contains(&c(4, 0))); assert!(n.contains(&c(0, 4))); }
#[test]
fn distance_chebyshev_absorb() {
let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
assert_eq!(s.distance(&c(0, 0), &c(1, 1)), 1.0);
assert_eq!(s.distance(&c(0, 0), &c(3, 4)), 4.0);
assert_eq!(s.distance(&c(2, 3), &c(5, 7)), 4.0);
}
#[test]
fn distance_chebyshev_wrap() {
let s = Square8::new(10, 10, EdgeBehavior::Wrap).unwrap();
assert_eq!(s.distance(&c(0, 0), &c(9, 9)), 1.0);
}
#[test]
fn compile_region_all() {
let s = Square8::new(5, 5, EdgeBehavior::Absorb).unwrap();
let plan = s.compile_region(&RegionSpec::All).unwrap();
assert_eq!(plan.cell_count(), 25);
assert_eq!(plan.valid_ratio(), 1.0);
}
#[test]
fn compile_region_disk_square_shape() {
let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
let plan = s
.compile_region(&RegionSpec::Disk {
center: c(5, 5),
radius: 2,
})
.unwrap();
assert_eq!(plan.cell_count(), 25);
}
#[test]
fn compile_region_rect() {
let s = Square8::new(10, 10, EdgeBehavior::Absorb).unwrap();
let plan = s
.compile_region(&RegionSpec::Rect {
min: c(2, 3),
max: c(4, 6),
})
.unwrap();
assert_eq!(plan.cell_count(), 12); }
#[test]
fn new_zero_rows_returns_error() {
assert!(matches!(
Square8::new(0, 5, EdgeBehavior::Absorb),
Err(SpaceError::EmptySpace)
));
}
#[test]
fn new_rejects_dims_exceeding_i32_max() {
let big = i32::MAX as u32 + 1;
assert!(matches!(
Square8::new(big, 5, EdgeBehavior::Absorb),
Err(SpaceError::DimensionTooLarge { name: "rows", .. })
));
assert!(matches!(
Square8::new(5, big, EdgeBehavior::Absorb),
Err(SpaceError::DimensionTooLarge { name: "cols", .. })
));
}
#[test]
fn single_cell_absorb() {
let s = Square8::new(1, 1, EdgeBehavior::Absorb).unwrap();
assert!(s.neighbours(&c(0, 0)).is_empty());
}
#[test]
fn single_cell_wrap() {
let s = Square8::new(1, 1, EdgeBehavior::Wrap).unwrap();
let n = s.neighbours(&c(0, 0));
assert_eq!(n.len(), 8);
assert!(n.iter().all(|nb| nb == &c(0, 0)));
}
#[test]
fn compliance_absorb() {
let s = Square8::new(8, 8, EdgeBehavior::Absorb).unwrap();
compliance::run_full_compliance(&s);
}
#[test]
fn compliance_clamp() {
let s = Square8::new(8, 8, EdgeBehavior::Clamp).unwrap();
compliance::run_full_compliance(&s);
}
#[test]
fn compliance_wrap() {
let s = Square8::new(8, 8, EdgeBehavior::Wrap).unwrap();
compliance::run_full_compliance(&s);
}
#[test]
fn downcast_ref_square8() {
let s: Box<dyn Space> = Box::new(Square8::new(3, 3, EdgeBehavior::Absorb).unwrap());
assert!(s.downcast_ref::<Square8>().is_some());
assert!(s.downcast_ref::<crate::Square4>().is_none());
}
fn arb_edge() -> impl Strategy<Value = EdgeBehavior> {
prop_oneof![
Just(EdgeBehavior::Absorb),
Just(EdgeBehavior::Clamp),
Just(EdgeBehavior::Wrap),
]
}
proptest! {
#[test]
fn distance_is_metric(
rows in 2u32..10,
cols in 2u32..10,
edge in arb_edge(),
ar in 0i32..10, ac in 0i32..10,
br in 0i32..10, bc in 0i32..10,
cr in 0i32..10, cc_val in 0i32..10,
) {
let ar = ar % rows as i32;
let ac = ac % cols as i32;
let br = br % rows as i32;
let bc = bc % cols as i32;
let cr = cr % rows as i32;
let cc_val = cc_val % cols as i32;
let s = Square8::new(rows, cols, edge).unwrap();
let a: Coord = smallvec![ar, ac];
let b: Coord = smallvec![br, bc];
let cv: Coord = smallvec![cr, cc_val];
prop_assert!((s.distance(&a, &a) - 0.0).abs() < f64::EPSILON);
prop_assert!((s.distance(&a, &b) - s.distance(&b, &a)).abs() < f64::EPSILON);
prop_assert!(s.distance(&a, &cv) <= s.distance(&a, &b) + s.distance(&b, &cv) + f64::EPSILON);
}
#[test]
fn neighbours_symmetric(
rows in 2u32..10,
cols in 2u32..10,
edge in arb_edge(),
r in 0i32..10, col in 0i32..10,
) {
let r = r % rows as i32;
let col = col % cols as i32;
let s = Square8::new(rows, cols, edge).unwrap();
let coord: Coord = smallvec![r, col];
for nb in s.neighbours(&coord) {
let nb_neighbours = s.neighbours(&nb);
prop_assert!(
nb_neighbours.contains(&coord),
"neighbour symmetry violated: {:?} in N({:?}) but {:?} not in N({:?})",
nb, coord, coord, nb,
);
}
}
}
}