1use 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#[derive(Debug, Clone)]
38pub struct Line1D {
39 len: u32,
40 edge: EdgeBehavior,
41 instance_id: SpaceInstanceId,
42}
43
44impl Line1D {
45 pub const MAX_LEN: u32 = i32::MAX as u32;
47
48 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 pub fn len(&self) -> u32 {
72 self.len
73 }
74
75 pub fn is_empty(&self) -> bool {
77 false
78 }
79
80 pub fn edge_behavior(&self) -> EdgeBehavior {
82 self.edge
83 }
84}
85
86pub(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
96pub(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
103pub(crate) fn canonical_ordering_1d(len: u32) -> Vec<Coord> {
105 (0..len as i32).map(|i| smallvec![i]).collect()
106}
107
108pub(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
126pub(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 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
197fn 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 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 fn c(i: i32) -> Coord {
360 smallvec![i]
361 }
362
363 #[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 #[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 #[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 assert_eq!(s.distance(&c(2), &c(7)), 5.0);
465 assert_eq!(s.distance(&c(0), &c(9)), 1.0);
467 }
468
469 #[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); 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 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 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 #[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 assert!(Line1D::new(i32::MAX as u32, EdgeBehavior::Absorb).is_ok());
565 }
566
567 #[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 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 prop_assert!((s.distance(&ca, &ca) - 0.0).abs() < f64::EPSILON);
616 prop_assert!((s.distance(&ca, &cb) - s.distance(&cb, &ca)).abs() < f64::EPSILON);
618 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}