Skip to main content

murk_space/
line1d.rs

1//! 1D line lattice with configurable edge behavior.
2
3use crate::edge::EdgeBehavior;
4use crate::error::SpaceError;
5use crate::region::{BoundingShape, RegionPlan, RegionSpec};
6use crate::space::Space;
7use murk_core::{Coord, SpaceInstanceId};
8use smallvec::{smallvec, SmallVec};
9use std::collections::VecDeque;
10
11/// A one-dimensional line lattice.
12///
13/// Each cell has coordinate `[i]` where `0 <= i < len`.
14/// Boundary handling is controlled by [`EdgeBehavior`]:
15/// - **Absorb**: edge cells have fewer neighbors
16/// - **Clamp**: edge cells self-loop
17/// - **Wrap**: periodic boundary (equivalent to [`Ring1D`](crate::Ring1D))
18///
19/// # Examples
20///
21/// ```
22/// use murk_space::{Line1D, EdgeBehavior, Space};
23///
24/// let line = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
25/// assert_eq!(line.len(), 5);
26/// assert_eq!(line.cell_count(), 5);
27/// assert_eq!(line.ndim(), 1);
28///
29/// // Interior cell has two neighbours.
30/// let coord: murk_core::Coord = vec![2i32].into();
31/// assert_eq!(line.neighbours(&coord).len(), 2);
32///
33/// // Edge cell (absorb) has only one neighbour.
34/// let edge: murk_core::Coord = vec![0i32].into();
35/// assert_eq!(line.neighbours(&edge).len(), 1);
36/// ```
37#[derive(Debug, Clone)]
38pub struct Line1D {
39    len: u32,
40    edge: EdgeBehavior,
41    instance_id: SpaceInstanceId,
42}
43
44impl Line1D {
45    /// Maximum length: coordinates use `i32`, so `len` must fit.
46    pub const MAX_LEN: u32 = i32::MAX as u32;
47
48    /// Create a new 1D line with `len` cells and the given edge behavior.
49    ///
50    /// Returns `Err(SpaceError::EmptySpace)` if `len == 0`, or
51    /// `Err(SpaceError::DimensionTooLarge)` if `len > i32::MAX`.
52    pub fn new(len: u32, edge: EdgeBehavior) -> Result<Self, SpaceError> {
53        if len == 0 {
54            return Err(SpaceError::EmptySpace);
55        }
56        if len > Self::MAX_LEN {
57            return Err(SpaceError::DimensionTooLarge {
58                name: "len",
59                value: len,
60                max: Self::MAX_LEN,
61            });
62        }
63        Ok(Self {
64            len,
65            edge,
66            instance_id: SpaceInstanceId::next(),
67        })
68    }
69
70    /// Number of cells.
71    pub fn len(&self) -> u32 {
72        self.len
73    }
74
75    /// Always returns `false` — construction rejects `len == 0`.
76    pub fn is_empty(&self) -> bool {
77        false
78    }
79
80    /// Edge behavior.
81    pub fn edge_behavior(&self) -> EdgeBehavior {
82        self.edge
83    }
84}
85
86// ── pub(crate) helpers shared with Ring1D ────────────────────────────
87
88/// Wrap-aware neighbor computation for a 1D lattice of length `len`.
89pub(crate) fn wrap_neighbours_1d(i: i32, len: u32) -> SmallVec<[Coord; 8]> {
90    let n = len as i32;
91    let left = ((i - 1) + n) % n;
92    let right = (i + 1) % n;
93    smallvec![smallvec![left], smallvec![right]]
94}
95
96/// Wrap-aware distance for a 1D lattice of length `len`.
97pub(crate) fn wrap_distance_1d(a: i32, b: i32, len: u32) -> f64 {
98    let diff = (a - b).unsigned_abs();
99    let wrap = len - diff;
100    diff.min(wrap) as f64
101}
102
103/// Canonical ordering for a 1D lattice: `[0], [1], ..., [len-1]`.
104pub(crate) fn canonical_ordering_1d(len: u32) -> Vec<Coord> {
105    (0..len as i32).map(|i| smallvec![i]).collect()
106}
107
108/// Check that a 1D coordinate is in bounds.
109pub(crate) fn check_1d_bounds(coord: &Coord, len: u32) -> Result<i32, SpaceError> {
110    if coord.len() != 1 {
111        return Err(SpaceError::CoordOutOfBounds {
112            coord: coord.clone(),
113            bounds: format!("expected 1D coordinate, got {}D", coord.len()),
114        });
115    }
116    let i = coord[0];
117    if i < 0 || i >= len as i32 {
118        return Err(SpaceError::CoordOutOfBounds {
119            coord: coord.clone(),
120            bounds: format!("[0, {})", len),
121        });
122    }
123    Ok(i)
124}
125
126/// Compile a region for a 1D space. `wrap` controls whether disk/BFS wraps around.
127pub(crate) fn compile_region_1d(
128    spec: &RegionSpec,
129    len: u32,
130    wrap: bool,
131) -> Result<RegionPlan, SpaceError> {
132    match spec {
133        RegionSpec::All => {
134            let coords = canonical_ordering_1d(len);
135            let cell_count = coords.len();
136            let tensor_indices: Vec<usize> = (0..cell_count).collect();
137            let valid_mask = vec![1u8; cell_count];
138            Ok(RegionPlan {
139                coords,
140                tensor_indices,
141                valid_mask,
142                bounding_shape: BoundingShape::Rect(vec![cell_count]),
143            })
144        }
145
146        RegionSpec::Disk { center, radius } => {
147            let c = check_1d_bounds(center, len)?;
148            compile_disk_1d(c, *radius, len, wrap)
149        }
150
151        RegionSpec::Neighbours { center, depth } => {
152            let c = check_1d_bounds(center, len)?;
153            compile_disk_1d(c, *depth, len, wrap)
154        }
155
156        RegionSpec::Rect { min, max } => {
157            let lo = check_1d_bounds(min, len)?;
158            let hi = check_1d_bounds(max, len)?;
159            if lo > hi {
160                return Err(SpaceError::InvalidRegion {
161                    reason: format!("Rect min ({lo}) > max ({hi})"),
162                });
163            }
164            let coords: Vec<Coord> = (lo..=hi).map(|i| smallvec![i]).collect();
165            let cell_count = coords.len();
166            let tensor_indices: Vec<usize> = (0..cell_count).collect();
167            let valid_mask = vec![1u8; cell_count];
168            Ok(RegionPlan {
169                coords,
170                tensor_indices,
171                valid_mask,
172                bounding_shape: BoundingShape::Rect(vec![cell_count]),
173            })
174        }
175
176        RegionSpec::Coords(coords) => {
177            for coord in coords {
178                check_1d_bounds(coord, len)?;
179            }
180            // Deduplicate and sort in canonical order.
181            let mut sorted: Vec<Coord> = coords.clone();
182            sorted.sort();
183            sorted.dedup();
184            let cell_count = sorted.len();
185            let tensor_indices: Vec<usize> = (0..cell_count).collect();
186            let valid_mask = vec![1u8; cell_count];
187            Ok(RegionPlan {
188                coords: sorted,
189                tensor_indices,
190                valid_mask,
191                bounding_shape: BoundingShape::Rect(vec![cell_count]),
192            })
193        }
194    }
195}
196
197/// BFS-based disk compilation for 1D. Returns cells within `radius` graph-distance
198/// of `center`, handling wrap if enabled.
199fn compile_disk_1d(
200    center: i32,
201    radius: u32,
202    len: u32,
203    wrap: bool,
204) -> Result<RegionPlan, SpaceError> {
205    let n = len as i32;
206    let mut visited = vec![false; len as usize];
207    let mut queue = VecDeque::new();
208    let mut result_indices: Vec<i32> = Vec::new();
209
210    visited[center as usize] = true;
211    queue.push_back((center, 0u32));
212    result_indices.push(center);
213
214    while let Some((pos, dist)) = queue.pop_front() {
215        if dist >= radius {
216            continue;
217        }
218        let candidates = if wrap {
219            vec![((pos - 1 + n) % n), ((pos + 1) % n)]
220        } else {
221            let mut c = Vec::new();
222            if pos > 0 {
223                c.push(pos - 1);
224            }
225            if pos < n - 1 {
226                c.push(pos + 1);
227            }
228            c
229        };
230        for nb in candidates {
231            if !visited[nb as usize] {
232                visited[nb as usize] = true;
233                queue.push_back((nb, dist + 1));
234                result_indices.push(nb);
235            }
236        }
237    }
238
239    // Sort in canonical order.
240    result_indices.sort();
241    let coords: Vec<Coord> = result_indices.iter().map(|&i| smallvec![i]).collect();
242    let cell_count = coords.len();
243    let tensor_indices: Vec<usize> = (0..cell_count).collect();
244    let valid_mask = vec![1u8; cell_count];
245
246    Ok(RegionPlan {
247        coords,
248        tensor_indices,
249        valid_mask,
250        bounding_shape: BoundingShape::Rect(vec![cell_count]),
251    })
252}
253
254impl Space for Line1D {
255    fn ndim(&self) -> usize {
256        1
257    }
258
259    fn cell_count(&self) -> usize {
260        self.len as usize
261    }
262
263    fn neighbours(&self, coord: &Coord) -> SmallVec<[Coord; 8]> {
264        let i = coord[0];
265        let n = self.len as i32;
266        match self.edge {
267            EdgeBehavior::Absorb => {
268                let mut result = SmallVec::new();
269                if i > 0 {
270                    result.push(smallvec![i - 1]);
271                }
272                if i < n - 1 {
273                    result.push(smallvec![i + 1]);
274                }
275                result
276            }
277            EdgeBehavior::Clamp => {
278                let left = (i - 1).max(0);
279                let right = (i + 1).min(n - 1);
280                smallvec![smallvec![left], smallvec![right]]
281            }
282            EdgeBehavior::Wrap => wrap_neighbours_1d(i, self.len),
283        }
284    }
285
286    fn max_neighbour_degree(&self) -> usize {
287        match self.edge {
288            EdgeBehavior::Clamp | EdgeBehavior::Wrap => 2,
289            EdgeBehavior::Absorb => match self.len {
290                1 => 0,
291                2 => 1,
292                _ => 2,
293            },
294        }
295    }
296
297    fn distance(&self, a: &Coord, b: &Coord) -> f64 {
298        let ai = a[0];
299        let bi = b[0];
300        match self.edge {
301            EdgeBehavior::Wrap => wrap_distance_1d(ai, bi, self.len),
302            EdgeBehavior::Absorb | EdgeBehavior::Clamp => (ai - bi).abs() as f64,
303        }
304    }
305
306    fn compile_region(&self, spec: &RegionSpec) -> Result<RegionPlan, SpaceError> {
307        let wrap = self.edge == EdgeBehavior::Wrap;
308        compile_region_1d(spec, self.len, wrap)
309    }
310
311    fn canonical_ordering(&self) -> Vec<Coord> {
312        canonical_ordering_1d(self.len)
313    }
314
315    fn canonical_rank(&self, coord: &Coord) -> Option<usize> {
316        if coord.len() != 1 {
317            return None;
318        }
319        let i = coord[0];
320        if i >= 0 && i < self.len as i32 {
321            Some(i as usize)
322        } else {
323            None
324        }
325    }
326
327    fn canonical_rank_slice(&self, coord: &[i32]) -> Option<usize> {
328        if coord.len() != 1 {
329            return None;
330        }
331        let i = coord[0];
332        if i >= 0 && i < self.len as i32 {
333            Some(i as usize)
334        } else {
335            None
336        }
337    }
338
339    fn instance_id(&self) -> SpaceInstanceId {
340        self.instance_id
341    }
342
343    fn topology_eq(&self, other: &dyn Space) -> bool {
344        (other as &dyn std::any::Any)
345            .downcast_ref::<Self>()
346            .is_some_and(|o| self.len == o.len && self.edge == o.edge)
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::compliance;
354    use murk_core::Coord;
355    use proptest::prelude::*;
356
357    /// Helper to build a Coord from a single i32 (avoids type-inference issues
358    /// when `smallvec!` is used inside array literals).
359    fn c(i: i32) -> Coord {
360        smallvec![i]
361    }
362
363    // ── Neighbour tests ─────────────────────────────────────────
364
365    #[test]
366    fn neighbours_absorb_interior() {
367        let s = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
368        let n = s.neighbours(&c(2));
369        assert_eq!(n.as_slice(), &[c(1), c(3)]);
370    }
371
372    #[test]
373    fn neighbours_absorb_left_edge() {
374        let s = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
375        let n = s.neighbours(&c(0));
376        assert_eq!(n.as_slice(), &[c(1)]);
377    }
378
379    #[test]
380    fn neighbours_absorb_right_edge() {
381        let s = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
382        let n = s.neighbours(&c(4));
383        assert_eq!(n.as_slice(), &[c(3)]);
384    }
385
386    #[test]
387    fn neighbours_clamp_interior() {
388        let s = Line1D::new(5, EdgeBehavior::Clamp).unwrap();
389        let n = s.neighbours(&c(2));
390        assert_eq!(n.as_slice(), &[c(1), c(3)]);
391    }
392
393    #[test]
394    fn neighbours_clamp_left_edge() {
395        let s = Line1D::new(5, EdgeBehavior::Clamp).unwrap();
396        let n = s.neighbours(&c(0));
397        assert_eq!(n.as_slice(), &[c(0), c(1)]);
398    }
399
400    #[test]
401    fn neighbours_clamp_right_edge() {
402        let s = Line1D::new(5, EdgeBehavior::Clamp).unwrap();
403        let n = s.neighbours(&c(4));
404        assert_eq!(n.as_slice(), &[c(3), c(4)]);
405    }
406
407    #[test]
408    fn neighbours_wrap_interior() {
409        let s = Line1D::new(5, EdgeBehavior::Wrap).unwrap();
410        let n = s.neighbours(&c(2));
411        assert_eq!(n.as_slice(), &[c(1), c(3)]);
412    }
413
414    #[test]
415    fn neighbours_wrap_left_edge() {
416        let s = Line1D::new(5, EdgeBehavior::Wrap).unwrap();
417        let n = s.neighbours(&c(0));
418        assert_eq!(n.as_slice(), &[c(4), c(1)]);
419    }
420
421    #[test]
422    fn neighbours_wrap_right_edge() {
423        let s = Line1D::new(5, EdgeBehavior::Wrap).unwrap();
424        let n = s.neighbours(&c(4));
425        assert_eq!(n.as_slice(), &[c(3), c(0)]);
426    }
427
428    // ── Single-cell edge cases ──────────────────────────────────
429
430    #[test]
431    fn neighbours_absorb_single_cell() {
432        let s = Line1D::new(1, EdgeBehavior::Absorb).unwrap();
433        let n = s.neighbours(&c(0));
434        assert!(n.is_empty());
435    }
436
437    #[test]
438    fn neighbours_clamp_single_cell() {
439        let s = Line1D::new(1, EdgeBehavior::Clamp).unwrap();
440        let n = s.neighbours(&c(0));
441        assert_eq!(n.as_slice(), &[c(0), c(0)]);
442    }
443
444    #[test]
445    fn neighbours_wrap_single_cell() {
446        let s = Line1D::new(1, EdgeBehavior::Wrap).unwrap();
447        let n = s.neighbours(&c(0));
448        assert_eq!(n.as_slice(), &[c(0), c(0)]);
449    }
450
451    // ── Distance tests ──────────────────────────────────────────
452
453    #[test]
454    fn distance_absorb_worked() {
455        let s = Line1D::new(10, EdgeBehavior::Absorb).unwrap();
456        assert_eq!(s.distance(&c(2), &c(7)), 5.0);
457        assert_eq!(s.distance(&c(0), &c(9)), 9.0);
458    }
459
460    #[test]
461    fn distance_wrap_worked() {
462        let s = Line1D::new(10, EdgeBehavior::Wrap).unwrap();
463        // Direct: |2-7| = 5, Wrap: 10-5 = 5 → min = 5
464        assert_eq!(s.distance(&c(2), &c(7)), 5.0);
465        // Direct: |0-9| = 9, Wrap: 10-9 = 1 → min = 1
466        assert_eq!(s.distance(&c(0), &c(9)), 1.0);
467    }
468
469    // ── Region compilation tests ────────────────────────────────
470
471    #[test]
472    fn compile_region_all() {
473        let s = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
474        let plan = s.compile_region(&RegionSpec::All).unwrap();
475        assert_eq!(plan.cell_count(), 5);
476        assert_eq!(plan.coords.len(), 5);
477        assert_eq!(plan.valid_ratio(), 1.0);
478    }
479
480    #[test]
481    fn compile_region_disk_interior() {
482        let s = Line1D::new(10, EdgeBehavior::Absorb).unwrap();
483        let plan = s
484            .compile_region(&RegionSpec::Disk {
485                center: c(5),
486                radius: 2,
487            })
488            .unwrap();
489        assert_eq!(plan.cell_count(), 5); // cells 3,4,5,6,7
490        assert_eq!(plan.coords, vec![c(3), c(4), c(5), c(6), c(7)]);
491    }
492
493    #[test]
494    fn compile_region_disk_wrap_edge() {
495        let s = Line1D::new(10, EdgeBehavior::Wrap).unwrap();
496        let plan = s
497            .compile_region(&RegionSpec::Disk {
498                center: c(0),
499                radius: 2,
500            })
501            .unwrap();
502        // Should include 8, 9, 0, 1, 2 (wrapping around)
503        assert_eq!(plan.cell_count(), 5);
504        assert!(plan.coords.contains(&c(8)));
505        assert!(plan.coords.contains(&c(9)));
506        assert!(plan.coords.contains(&c(0)));
507        assert!(plan.coords.contains(&c(1)));
508        assert!(plan.coords.contains(&c(2)));
509    }
510
511    #[test]
512    fn compile_region_rect() {
513        let s = Line1D::new(10, EdgeBehavior::Absorb).unwrap();
514        let plan = s
515            .compile_region(&RegionSpec::Rect {
516                min: c(2),
517                max: c(5),
518            })
519            .unwrap();
520        assert_eq!(plan.cell_count(), 4);
521        assert_eq!(plan.coords, vec![c(2), c(3), c(4), c(5)]);
522    }
523
524    #[test]
525    fn compile_region_rect_invalid() {
526        let s = Line1D::new(10, EdgeBehavior::Absorb).unwrap();
527        let result = s.compile_region(&RegionSpec::Rect {
528            min: c(5),
529            max: c(2),
530        });
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn compile_region_coords_valid() {
536        let s = Line1D::new(10, EdgeBehavior::Absorb).unwrap();
537        let plan = s
538            .compile_region(&RegionSpec::Coords(vec![c(3), c(7), c(1)]))
539            .unwrap();
540        // Sorted and deduplicated
541        assert_eq!(plan.coords, vec![c(1), c(3), c(7)]);
542    }
543
544    #[test]
545    fn compile_region_coords_oob() {
546        let s = Line1D::new(5, EdgeBehavior::Absorb).unwrap();
547        let result = s.compile_region(&RegionSpec::Coords(vec![c(10)]));
548        assert!(result.is_err());
549    }
550
551    // ── Constructor tests ───────────────────────────────────────
552
553    #[test]
554    fn new_zero_len_returns_error() {
555        let result = Line1D::new(0, EdgeBehavior::Absorb);
556        assert!(matches!(result, Err(SpaceError::EmptySpace)));
557    }
558
559    #[test]
560    fn new_rejects_len_exceeding_i32_max() {
561        let result = Line1D::new(i32::MAX as u32 + 1, EdgeBehavior::Absorb);
562        assert!(matches!(result, Err(SpaceError::DimensionTooLarge { .. })));
563        // i32::MAX itself should be accepted.
564        assert!(Line1D::new(i32::MAX as u32, EdgeBehavior::Absorb).is_ok());
565    }
566
567    // ── Compliance suites ───────────────────────────────────────
568
569    #[test]
570    fn compliance_absorb() {
571        let s = Line1D::new(20, EdgeBehavior::Absorb).unwrap();
572        compliance::run_full_compliance(&s);
573    }
574
575    #[test]
576    fn compliance_clamp() {
577        let s = Line1D::new(20, EdgeBehavior::Clamp).unwrap();
578        compliance::run_full_compliance(&s);
579    }
580
581    #[test]
582    fn compliance_wrap() {
583        let s = Line1D::new(20, EdgeBehavior::Wrap).unwrap();
584        compliance::run_full_compliance(&s);
585    }
586
587    // ── Property tests ──────────────────────────────────────────
588
589    fn arb_edge() -> impl Strategy<Value = EdgeBehavior> {
590        prop_oneof![
591            Just(EdgeBehavior::Absorb),
592            Just(EdgeBehavior::Clamp),
593            Just(EdgeBehavior::Wrap),
594        ]
595    }
596
597    proptest! {
598        #[test]
599        fn distance_is_metric(
600            len in 2u32..50,
601            edge in arb_edge(),
602            a in 0i32..50,
603            b in 0i32..50,
604            c in 0i32..50,
605        ) {
606            let a = a % len as i32;
607            let b = b % len as i32;
608            let c = c % len as i32;
609            let s = Line1D::new(len, edge).unwrap();
610            let ca: Coord = smallvec![a];
611            let cb: Coord = smallvec![b];
612            let cc: Coord = smallvec![c];
613
614            // Reflexive
615            prop_assert!((s.distance(&ca, &ca) - 0.0).abs() < f64::EPSILON);
616            // Symmetric
617            prop_assert!((s.distance(&ca, &cb) - s.distance(&cb, &ca)).abs() < f64::EPSILON);
618            // Triangle inequality
619            prop_assert!(s.distance(&ca, &cc) <= s.distance(&ca, &cb) + s.distance(&cb, &cc) + f64::EPSILON);
620        }
621
622        #[test]
623        fn neighbours_symmetric(
624            len in 2u32..50,
625            edge in arb_edge(),
626            i in 0i32..50,
627        ) {
628            let i = i % len as i32;
629            let s = Line1D::new(len, edge).unwrap();
630            let coord: Coord = smallvec![i];
631            for nb in s.neighbours(&coord) {
632                let nb_neighbours = s.neighbours(&nb);
633                prop_assert!(
634                    nb_neighbours.contains(&coord),
635                    "neighbour symmetry violated: {:?} in N({:?}) but {:?} not in N({:?})",
636                    nb, coord, coord, nb,
637                );
638            }
639        }
640    }
641}