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}