Skip to main content

feagi_brain_development/connectivity/rules/
patterns.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Pattern-based connectivity - wildcard matching and transformations.
6
7Supports absolute patterns (*, ?, !, int) and source-relative directional
8patterns (?+, ?-, ?+=, ?-=, ?+N, ?-N, ?-N:?+M) for spatial connectivity.
9*/
10
11use crate::types::Position;
12
13type Dimensions = (usize, usize, usize);
14
15/// Direction along an axis relative to the source coordinate.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Direction {
18    Positive,
19    Negative,
20}
21
22/// Pattern element types for specifying connectivity rules per axis.
23#[derive(Debug, Clone, PartialEq)]
24pub enum PatternElement {
25    /// `"*"` - matches any coordinate on this axis
26    Wildcard,
27    /// `"?"` - pass through source coordinate (dst = src)
28    Skip,
29    /// `"!"` - exclude source coordinate (all except src)
30    Exclude,
31    /// Absolute coordinate value
32    Exact(i32),
33    /// `"?+"` or `"?-"` - all coordinates strictly above/below src
34    DirectionExclusive(Direction),
35    /// `"?+="` or `"?-="` - all coordinates at or above/below src
36    DirectionInclusive(Direction),
37    /// `"?+N"` or `"?-N"` - single coordinate offset from src
38    Offset(i32),
39    /// `"?-A:?+B"` - inclusive range [src + lo, src + hi]
40    Range(i32, i32),
41}
42
43impl PatternElement {
44    /// Parse a pattern element from a string value.
45    pub fn from_value(value: &str) -> Self {
46        match value {
47            "*" => PatternElement::Wildcard,
48            "?" => PatternElement::Skip,
49            "!" => PatternElement::Exclude,
50            "?+" => PatternElement::DirectionExclusive(Direction::Positive),
51            "?-" => PatternElement::DirectionExclusive(Direction::Negative),
52            "?+=" => PatternElement::DirectionInclusive(Direction::Positive),
53            "?-=" => PatternElement::DirectionInclusive(Direction::Negative),
54            _ => {
55                if let Some(range_str) = Self::try_parse_range(value) {
56                    return range_str;
57                }
58                if let Some(offset) = Self::try_parse_offset(value) {
59                    return offset;
60                }
61                if let Ok(num) = value.parse::<i32>() {
62                    PatternElement::Exact(num)
63                } else {
64                    PatternElement::Wildcard
65                }
66            }
67        }
68    }
69
70    /// Parse FFI integer encoding into a PatternElement.
71    pub fn from_int(value: i32) -> Self {
72        match value {
73            -1 => PatternElement::Wildcard,
74            -2 => PatternElement::Skip,
75            -3 => PatternElement::Exclude,
76            -10 => PatternElement::DirectionExclusive(Direction::Positive),
77            -11 => PatternElement::DirectionExclusive(Direction::Negative),
78            -12 => PatternElement::DirectionInclusive(Direction::Positive),
79            -13 => PatternElement::DirectionInclusive(Direction::Negative),
80            _ => PatternElement::Exact(value),
81        }
82    }
83
84    /// Attempt to parse a relative range pattern like "?-1:?+1" or "?+2:?+5".
85    /// Format: "?<sign><int>:?<sign><int>" where both bounds are relative to src.
86    fn try_parse_range(value: &str) -> Option<PatternElement> {
87        let parts: Vec<&str> = value.split(':').collect();
88        if parts.len() != 2 {
89            return None;
90        }
91        let lo = Self::extract_relative_offset(parts[0])?;
92        let hi = Self::extract_relative_offset(parts[1])?;
93        Some(PatternElement::Range(lo, hi))
94    }
95
96    /// Attempt to parse a single offset pattern like "?+3" or "?-2".
97    fn try_parse_offset(value: &str) -> Option<PatternElement> {
98        let offset = Self::extract_relative_offset(value)?;
99        Some(PatternElement::Offset(offset))
100    }
101
102    /// Extract a numeric offset from a "?+N" or "?-N" string.
103    fn extract_relative_offset(s: &str) -> Option<i32> {
104        if !s.starts_with('?') {
105            return None;
106        }
107        let rest = &s[1..];
108        if rest.is_empty() {
109            return None;
110        }
111        if rest == "+" || rest == "-" || rest == "+=" || rest == "-=" {
112            return None;
113        }
114        rest.parse::<i32>().ok()
115    }
116}
117
118/// 3D pattern (x, y, z)
119pub type Pattern3D = (PatternElement, PatternElement, PatternElement);
120
121/// Match a coordinate against a pattern element (point-wise check).
122pub fn match_pattern_element(element: &PatternElement, coordinate: i32, src_coord: i32) -> bool {
123    match element {
124        PatternElement::Wildcard => true,
125        PatternElement::Skip => coordinate == src_coord,
126        PatternElement::Exclude => coordinate != src_coord,
127        PatternElement::Exact(val) => coordinate == *val,
128        PatternElement::DirectionExclusive(Direction::Positive) => coordinate > src_coord,
129        PatternElement::DirectionExclusive(Direction::Negative) => coordinate < src_coord,
130        PatternElement::DirectionInclusive(Direction::Positive) => coordinate >= src_coord,
131        PatternElement::DirectionInclusive(Direction::Negative) => coordinate <= src_coord,
132        PatternElement::Offset(off) => coordinate == src_coord + off,
133        PatternElement::Range(lo, hi) => {
134            coordinate >= src_coord + lo && coordinate <= src_coord + hi
135        }
136    }
137}
138
139/// Expand a single axis pattern element into a set of destination coordinates.
140fn expand_axis(element: &PatternElement, src_coord: u32, dim: usize) -> Vec<u32> {
141    match element {
142        PatternElement::Wildcard => (0..dim as u32).collect(),
143        PatternElement::Skip => {
144            if (src_coord as usize) < dim {
145                vec![src_coord]
146            } else {
147                vec![]
148            }
149        }
150        PatternElement::Exclude => (0..dim as u32).filter(|&c| c != src_coord).collect(),
151        PatternElement::Exact(val) => {
152            if *val >= 0 && (*val as usize) < dim {
153                vec![*val as u32]
154            } else {
155                vec![]
156            }
157        }
158        PatternElement::DirectionExclusive(Direction::Positive) => {
159            ((src_coord + 1)..dim as u32).collect()
160        }
161        PatternElement::DirectionExclusive(Direction::Negative) => (0..src_coord).collect(),
162        PatternElement::DirectionInclusive(Direction::Positive) => {
163            (src_coord..dim as u32).collect()
164        }
165        PatternElement::DirectionInclusive(Direction::Negative) => {
166            (0..=src_coord).filter(|&c| (c as usize) < dim).collect()
167        }
168        PatternElement::Offset(off) => {
169            let target = src_coord as i32 + off;
170            if target >= 0 && (target as usize) < dim {
171                vec![target as u32]
172            } else {
173                vec![]
174            }
175        }
176        PatternElement::Range(lo, hi) => {
177            let start = (src_coord as i32 + lo).max(0) as u32;
178            let end_exclusive = ((src_coord as i32 + hi) + 1).min(dim as i32) as u32;
179            if start >= end_exclusive {
180                vec![]
181            } else {
182                (start..end_exclusive).collect()
183            }
184        }
185    }
186}
187
188/// Generate destination coordinates from pattern matching.
189pub fn find_destination_coordinates(
190    dst_dimensions: Dimensions,
191    src_coordinate: Position,
192    _src_pattern: &Pattern3D,
193    dst_pattern: &Pattern3D,
194) -> Vec<Position> {
195    let (dst_width, dst_height, dst_depth) = dst_dimensions;
196    let (src_x, src_y, src_z) = src_coordinate;
197
198    let x_range = expand_axis(&dst_pattern.0, src_x, dst_width);
199    let y_range = expand_axis(&dst_pattern.1, src_y, dst_height);
200    let z_range = expand_axis(&dst_pattern.2, src_z, dst_depth);
201
202    let mut results = Vec::with_capacity(x_range.len() * y_range.len() * z_range.len());
203    for x in &x_range {
204        for y in &y_range {
205            for z in &z_range {
206                results.push((*x, *y, *z));
207            }
208        }
209    }
210
211    results
212}
213
214/// Find source coordinates that match a pattern.
215/// Directional/relative patterns are treated as wildcard on source side since
216/// they require a specific source coordinate to resolve against.
217pub fn find_source_coordinates(
218    src_pattern: &Pattern3D,
219    src_dimensions: Dimensions,
220) -> Vec<Position> {
221    let (src_width, src_height, src_depth) = src_dimensions;
222
223    let x_range: Vec<u32> = match &src_pattern.0 {
224        PatternElement::Wildcard => (0..src_width as u32).collect(),
225        PatternElement::Exact(val) => {
226            if *val >= 0 && (*val as usize) < src_width {
227                vec![*val as u32]
228            } else {
229                vec![]
230            }
231        }
232        _ => (0..src_width as u32).collect(),
233    };
234
235    let y_range: Vec<u32> = match &src_pattern.1 {
236        PatternElement::Wildcard => (0..src_height as u32).collect(),
237        PatternElement::Exact(val) => {
238            if *val >= 0 && (*val as usize) < src_height {
239                vec![*val as u32]
240            } else {
241                vec![]
242            }
243        }
244        _ => (0..src_height as u32).collect(),
245    };
246
247    let z_range: Vec<u32> = match &src_pattern.2 {
248        PatternElement::Wildcard => (0..src_depth as u32).collect(),
249        PatternElement::Exact(val) => {
250            if *val >= 0 && (*val as usize) < src_depth {
251                vec![*val as u32]
252            } else {
253                vec![]
254            }
255        }
256        _ => (0..src_depth as u32).collect(),
257    };
258
259    let mut results = Vec::with_capacity(x_range.len() * y_range.len() * z_range.len());
260    for x in &x_range {
261        for y in &y_range {
262            for z in &z_range {
263                results.push((*x, *y, *z));
264            }
265        }
266    }
267
268    results
269}
270
271/// Batch process pattern matching for multiple patterns.
272pub fn match_patterns_batch(
273    src_coordinate: Position,
274    patterns: &[(Pattern3D, Pattern3D)],
275    _src_dimensions: Dimensions,
276    dst_dimensions: Dimensions,
277) -> Vec<Position> {
278    let mut all_results = Vec::new();
279
280    for (src_pattern, dst_pattern) in patterns {
281        let (src_x, src_y, src_z) = src_coordinate;
282
283        let x_match = match &src_pattern.0 {
284            PatternElement::Wildcard => true,
285            PatternElement::Exact(val) => src_x == (*val as u32),
286            _ => true,
287        };
288
289        let y_match = match &src_pattern.1 {
290            PatternElement::Wildcard => true,
291            PatternElement::Exact(val) => src_y == (*val as u32),
292            _ => true,
293        };
294
295        let z_match = match &src_pattern.2 {
296            PatternElement::Wildcard => true,
297            PatternElement::Exact(val) => src_z == (*val as u32),
298            _ => true,
299        };
300
301        if x_match && y_match && z_match {
302            let mut results = find_destination_coordinates(
303                dst_dimensions,
304                src_coordinate,
305                src_pattern,
306                dst_pattern,
307            );
308            all_results.append(&mut results);
309        }
310    }
311
312    all_results.sort_unstable();
313    all_results.dedup();
314
315    all_results
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_wildcard_pattern() {
324        let src_pattern = (
325            PatternElement::Wildcard,
326            PatternElement::Wildcard,
327            PatternElement::Exact(0),
328        );
329        let dst_pattern = (
330            PatternElement::Skip,
331            PatternElement::Skip,
332            PatternElement::Exact(1),
333        );
334
335        let results =
336            find_destination_coordinates((10, 10, 10), (5, 5, 0), &src_pattern, &dst_pattern);
337
338        assert_eq!(results.len(), 1);
339        assert_eq!(results[0], (5, 5, 1));
340    }
341
342    #[test]
343    fn test_exact_pattern() {
344        let src_pattern = (
345            PatternElement::Exact(0),
346            PatternElement::Exact(0),
347            PatternElement::Exact(0),
348        );
349        let dst_pattern = (
350            PatternElement::Exact(1),
351            PatternElement::Exact(2),
352            PatternElement::Exact(3),
353        );
354
355        let results =
356            find_destination_coordinates((10, 10, 10), (0, 0, 0), &src_pattern, &dst_pattern);
357
358        assert_eq!(results.len(), 1);
359        assert_eq!(results[0], (1, 2, 3));
360    }
361
362    #[test]
363    fn test_exclude_pattern() {
364        let src_pattern = (
365            PatternElement::Wildcard,
366            PatternElement::Wildcard,
367            PatternElement::Wildcard,
368        );
369        let dst_pattern = (
370            PatternElement::Exclude,
371            PatternElement::Exact(0),
372            PatternElement::Exact(0),
373        );
374
375        let results =
376            find_destination_coordinates((3, 1, 1), (1, 0, 0), &src_pattern, &dst_pattern);
377
378        assert_eq!(results.len(), 2);
379        assert!(results.contains(&(0, 0, 0)));
380        assert!(results.contains(&(2, 0, 0)));
381    }
382
383    #[test]
384    fn test_direction_positive_exclusive() {
385        let src_pattern = (
386            PatternElement::Wildcard,
387            PatternElement::Wildcard,
388            PatternElement::Wildcard,
389        );
390        let dst_pattern = (
391            PatternElement::DirectionExclusive(Direction::Positive),
392            PatternElement::Skip,
393            PatternElement::Skip,
394        );
395
396        let results =
397            find_destination_coordinates((8, 4, 2), (3, 1, 0), &src_pattern, &dst_pattern);
398
399        assert_eq!(results, vec![(4, 1, 0), (5, 1, 0), (6, 1, 0), (7, 1, 0)]);
400    }
401
402    #[test]
403    fn test_direction_negative_exclusive() {
404        let src_pattern = (
405            PatternElement::Wildcard,
406            PatternElement::Wildcard,
407            PatternElement::Wildcard,
408        );
409        let dst_pattern = (
410            PatternElement::DirectionExclusive(Direction::Negative),
411            PatternElement::Skip,
412            PatternElement::Skip,
413        );
414
415        let results =
416            find_destination_coordinates((8, 4, 2), (3, 1, 0), &src_pattern, &dst_pattern);
417
418        assert_eq!(results, vec![(0, 1, 0), (1, 1, 0), (2, 1, 0)]);
419    }
420
421    #[test]
422    fn test_direction_positive_inclusive() {
423        let dst_pattern = (
424            PatternElement::DirectionInclusive(Direction::Positive),
425            PatternElement::Skip,
426            PatternElement::Skip,
427        );
428        let src_pattern = (
429            PatternElement::Wildcard,
430            PatternElement::Wildcard,
431            PatternElement::Wildcard,
432        );
433
434        let results =
435            find_destination_coordinates((6, 3, 1), (2, 1, 0), &src_pattern, &dst_pattern);
436
437        assert_eq!(results, vec![(2, 1, 0), (3, 1, 0), (4, 1, 0), (5, 1, 0)]);
438    }
439
440    #[test]
441    fn test_direction_negative_inclusive() {
442        let dst_pattern = (
443            PatternElement::DirectionInclusive(Direction::Negative),
444            PatternElement::Skip,
445            PatternElement::Skip,
446        );
447        let src_pattern = (
448            PatternElement::Wildcard,
449            PatternElement::Wildcard,
450            PatternElement::Wildcard,
451        );
452
453        let results =
454            find_destination_coordinates((6, 3, 1), (2, 1, 0), &src_pattern, &dst_pattern);
455
456        assert_eq!(results, vec![(0, 1, 0), (1, 1, 0), (2, 1, 0)]);
457    }
458
459    #[test]
460    fn test_offset_positive() {
461        let dst_pattern = (
462            PatternElement::Offset(2),
463            PatternElement::Skip,
464            PatternElement::Skip,
465        );
466        let src_pattern = (
467            PatternElement::Wildcard,
468            PatternElement::Wildcard,
469            PatternElement::Wildcard,
470        );
471
472        let results =
473            find_destination_coordinates((10, 10, 10), (3, 5, 7), &src_pattern, &dst_pattern);
474
475        assert_eq!(results, vec![(5, 5, 7)]);
476    }
477
478    #[test]
479    fn test_offset_negative() {
480        let dst_pattern = (
481            PatternElement::Offset(-2),
482            PatternElement::Skip,
483            PatternElement::Skip,
484        );
485        let src_pattern = (
486            PatternElement::Wildcard,
487            PatternElement::Wildcard,
488            PatternElement::Wildcard,
489        );
490
491        let results =
492            find_destination_coordinates((10, 10, 10), (3, 5, 7), &src_pattern, &dst_pattern);
493
494        assert_eq!(results, vec![(1, 5, 7)]);
495    }
496
497    #[test]
498    fn test_offset_out_of_bounds() {
499        let dst_pattern = (
500            PatternElement::Offset(5),
501            PatternElement::Skip,
502            PatternElement::Skip,
503        );
504        let src_pattern = (
505            PatternElement::Wildcard,
506            PatternElement::Wildcard,
507            PatternElement::Wildcard,
508        );
509
510        let results =
511            find_destination_coordinates((6, 5, 5), (4, 2, 2), &src_pattern, &dst_pattern);
512
513        assert!(results.is_empty());
514    }
515
516    #[test]
517    fn test_range_symmetric() {
518        let dst_pattern = (
519            PatternElement::Range(-1, 1),
520            PatternElement::Range(-1, 1),
521            PatternElement::Skip,
522        );
523        let src_pattern = (
524            PatternElement::Wildcard,
525            PatternElement::Wildcard,
526            PatternElement::Wildcard,
527        );
528
529        let results =
530            find_destination_coordinates((10, 10, 5), (5, 5, 2), &src_pattern, &dst_pattern);
531
532        assert_eq!(results.len(), 9); // 3x3 grid
533        assert!(results.contains(&(4, 4, 2)));
534        assert!(results.contains(&(5, 5, 2)));
535        assert!(results.contains(&(6, 6, 2)));
536    }
537
538    #[test]
539    fn test_range_clamped_at_boundary() {
540        let dst_pattern = (
541            PatternElement::Range(-3, 3),
542            PatternElement::Skip,
543            PatternElement::Skip,
544        );
545        let src_pattern = (
546            PatternElement::Wildcard,
547            PatternElement::Wildcard,
548            PatternElement::Wildcard,
549        );
550
551        // src_x=1, range would be -2..4, clamped to 0..4
552        let results =
553            find_destination_coordinates((8, 1, 1), (1, 0, 0), &src_pattern, &dst_pattern);
554
555        assert_eq!(
556            results,
557            vec![(0, 0, 0), (1, 0, 0), (2, 0, 0), (3, 0, 0), (4, 0, 0)]
558        );
559    }
560
561    #[test]
562    fn test_range_forward_only() {
563        let dst_pattern = (
564            PatternElement::Range(1, 3),
565            PatternElement::Skip,
566            PatternElement::Skip,
567        );
568        let src_pattern = (
569            PatternElement::Wildcard,
570            PatternElement::Wildcard,
571            PatternElement::Wildcard,
572        );
573
574        let results =
575            find_destination_coordinates((10, 1, 1), (2, 0, 0), &src_pattern, &dst_pattern);
576
577        assert_eq!(results, vec![(3, 0, 0), (4, 0, 0), (5, 0, 0)]);
578    }
579
580    #[test]
581    fn test_direction_at_edge() {
582        let dst_pattern = (
583            PatternElement::DirectionExclusive(Direction::Negative),
584            PatternElement::Skip,
585            PatternElement::Skip,
586        );
587        let src_pattern = (
588            PatternElement::Wildcard,
589            PatternElement::Wildcard,
590            PatternElement::Wildcard,
591        );
592
593        // src_x=0: nothing to the left
594        let results =
595            find_destination_coordinates((10, 1, 1), (0, 0, 0), &src_pattern, &dst_pattern);
596
597        assert!(results.is_empty());
598    }
599
600    #[test]
601    fn test_from_value_new_patterns() {
602        assert_eq!(
603            PatternElement::from_value("?+"),
604            PatternElement::DirectionExclusive(Direction::Positive)
605        );
606        assert_eq!(
607            PatternElement::from_value("?-"),
608            PatternElement::DirectionExclusive(Direction::Negative)
609        );
610        assert_eq!(
611            PatternElement::from_value("?+="),
612            PatternElement::DirectionInclusive(Direction::Positive)
613        );
614        assert_eq!(
615            PatternElement::from_value("?-="),
616            PatternElement::DirectionInclusive(Direction::Negative)
617        );
618        assert_eq!(PatternElement::from_value("?+3"), PatternElement::Offset(3));
619        assert_eq!(
620            PatternElement::from_value("?-2"),
621            PatternElement::Offset(-2)
622        );
623        assert_eq!(
624            PatternElement::from_value("?-1:?+1"),
625            PatternElement::Range(-1, 1)
626        );
627        assert_eq!(
628            PatternElement::from_value("?+2:?+5"),
629            PatternElement::Range(2, 5)
630        );
631    }
632
633    #[test]
634    fn test_from_value_backward_compat() {
635        assert_eq!(PatternElement::from_value("*"), PatternElement::Wildcard);
636        assert_eq!(PatternElement::from_value("?"), PatternElement::Skip);
637        assert_eq!(PatternElement::from_value("!"), PatternElement::Exclude);
638        assert_eq!(PatternElement::from_value("7"), PatternElement::Exact(7));
639    }
640
641    #[test]
642    fn test_from_int_new_encodings() {
643        assert_eq!(
644            PatternElement::from_int(-10),
645            PatternElement::DirectionExclusive(Direction::Positive)
646        );
647        assert_eq!(
648            PatternElement::from_int(-11),
649            PatternElement::DirectionExclusive(Direction::Negative)
650        );
651        assert_eq!(
652            PatternElement::from_int(-12),
653            PatternElement::DirectionInclusive(Direction::Positive)
654        );
655        assert_eq!(
656            PatternElement::from_int(-13),
657            PatternElement::DirectionInclusive(Direction::Negative)
658        );
659    }
660
661    #[test]
662    fn test_batch_with_directional() {
663        let patterns = vec![(
664            (
665                PatternElement::Wildcard,
666                PatternElement::Wildcard,
667                PatternElement::Wildcard,
668            ),
669            (
670                PatternElement::DirectionExclusive(Direction::Positive),
671                PatternElement::Skip,
672                PatternElement::Skip,
673            ),
674        )];
675
676        let results = match_patterns_batch((3, 0, 0), &patterns, (8, 1, 1), (8, 1, 1));
677
678        assert_eq!(results, vec![(4, 0, 0), (5, 0, 0), (6, 0, 0), (7, 0, 0)]);
679    }
680}