Skip to main content

murk_space/
space.rs

1//! The core `Space` trait and `dyn Space` downcast support.
2
3use crate::error::SpaceError;
4use crate::region::{RegionPlan, RegionSpec};
5use murk_core::{Coord, SpaceInstanceId};
6use smallvec::SmallVec;
7use std::any::Any;
8
9/// Central spatial abstraction for Murk simulations.
10///
11/// All propagators, observations, and region queries flow through this trait.
12/// Concrete backends (Line1D, Ring1D, Square4, Hex2D, Fcc12, ProductSpace) implement
13/// it to define their topology.
14///
15/// # Object Safety
16///
17/// This trait is designed for use as `dyn Space`. Use
18/// `downcast_ref` for opt-in specialization
19/// on concrete types (Decision M).
20///
21/// # Thread Safety
22///
23/// `Sync` is required because `StepContext` holds `&'a dyn Space` and must
24/// be `Send` for RealtimeAsync mode (`&T: Send` requires `T: Sync`).
25pub trait Space: Any + Send + Sync + 'static {
26    /// Number of spatial dimensions.
27    fn ndim(&self) -> usize;
28
29    /// Total number of cells in the space.
30    fn cell_count(&self) -> usize;
31
32    /// Enumerate the neighbors of a cell.
33    ///
34    /// Returns coordinates in a deterministic, backend-defined order.
35    /// The `SmallVec<[Coord; 8]>` avoids heap allocation for common
36    /// topologies (up to 8 neighbors covers Hex2D and Square8).
37    fn neighbours(&self, coord: &Coord) -> SmallVec<[Coord; 8]>;
38
39    /// Maximum neighbor-list length over all cells in this space.
40    ///
41    /// Implementations may return a conservative upper bound, but must
42    /// never under-estimate the true maximum. This value is used by
43    /// timestep stability checks (for example CFL validation), where
44    /// under-estimation can admit unstable configurations.
45    ///
46    /// The default implementation performs an exact full scan over
47    /// `canonical_ordering()`. Backends should override with a cheap
48    /// closed form when available.
49    fn max_neighbour_degree(&self) -> usize {
50        self.canonical_ordering()
51            .iter()
52            .map(|coord| self.neighbours(coord).len())
53            .max()
54            .unwrap_or(0)
55    }
56
57    /// Graph-geodesic distance between two cells.
58    fn distance(&self, a: &Coord, b: &Coord) -> f64;
59
60    /// Compile a region specification into a plan for O(1) lookups.
61    fn compile_region(&self, spec: &RegionSpec) -> Result<RegionPlan, SpaceError>;
62
63    /// Iterate over the cells in a compiled region.
64    ///
65    /// Default implementation iterates over `plan.coords`. Backends may
66    /// override for performance.
67    fn iter_region<'a>(&'a self, plan: &'a RegionPlan) -> Box<dyn Iterator<Item = Coord> + 'a> {
68        Box::new(plan.coords.iter().cloned())
69    }
70
71    /// Map a coordinate to its flat tensor index within a compiled region.
72    ///
73    /// Default implementation performs a linear search in `plan.coords`.
74    /// Backends may override for O(1) index arithmetic.
75    fn map_coord_to_tensor_index(&self, coord: &Coord, plan: &RegionPlan) -> Option<usize> {
76        plan.coords
77            .iter()
78            .position(|c| c == coord)
79            .map(|i| plan.tensor_indices[i])
80    }
81
82    /// All cells in deterministic canonical order.
83    ///
84    /// Two calls on the same space instance must return the same sequence.
85    /// Used for observation export and replay reproducibility.
86    fn canonical_ordering(&self) -> Vec<Coord>;
87
88    /// Position of a coordinate in the canonical ordering.
89    ///
90    /// Returns the index such that `canonical_ordering()[index] == coord`.
91    /// Default implementation performs a linear search; backends should
92    /// override with O(1) arithmetic when possible.
93    fn canonical_rank(&self, coord: &Coord) -> Option<usize> {
94        self.canonical_ordering().iter().position(|c| c == coord)
95    }
96
97    /// Position of a coordinate slice in the canonical ordering.
98    ///
99    /// The default implementation delegates to [`canonical_rank`](Self::canonical_rank)
100    /// for backwards compatibility. Backends can override this to avoid
101    /// temporary `Coord` allocations when callers already have a slice.
102    fn canonical_rank_slice(&self, coord: &[i32]) -> Option<usize> {
103        let coord: Coord = SmallVec::from_slice(coord);
104        self.canonical_rank(&coord)
105    }
106
107    /// Unique instance identifier for this space object.
108    ///
109    /// Allocated from a monotonic counter at construction time. Used by
110    /// observation plan caching to detect when a different space instance
111    /// is passed, avoiding stale plan reuse.
112    fn instance_id(&self) -> SpaceInstanceId;
113
114    /// Returns `true` if `self` and `other` are topologically equivalent:
115    /// same concrete type and identical behavioral parameters.
116    ///
117    /// Used by `BatchedEngine` to verify all worlds share the same
118    /// topology before compiling a shared observation plan.
119    ///
120    /// Implementors should downcast `other` to `Self` and compare all
121    /// behavior-relevant fields (dimensions, edge behavior, etc.).
122    /// Return `false` if the downcast fails (different concrete type).
123    fn topology_eq(&self, other: &dyn Space) -> bool;
124}
125
126impl dyn Space {
127    /// Attempt to downcast a trait object to a concrete Space type.
128    ///
129    /// This enables opt-in specialization (Decision M): code that works
130    /// with `&dyn Space` can check for a known backend and use
131    /// type-specific fast paths.
132    pub fn downcast_ref<T: Space>(&self) -> Option<&T> {
133        (self as &dyn Any).downcast_ref::<T>()
134    }
135}