Skip to main content

holographic_memory/core/
intersection.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Sparse intersection primitives.
5//!
6//! Galloping intersection for skewed sizes, sorted merge for balanced.
7
8/// Count common elements in two sorted u32 slices.
9///
10/// Adaptively picks the best algorithm:
11/// - When one slice is much smaller (|a| * 8 < |b|), uses galloping search
12///   which is O(|small| * log(|large|)) and beats O(|a| + |b|) merge.
13/// - Otherwise uses the standard sorted merge at O(|a| + |b|).
14#[inline]
15pub fn sparse_intersection_count(a: &[u32], b: &[u32]) -> usize {
16    if a.is_empty() || b.is_empty() {
17        return 0;
18    }
19
20    // Ensure a is the smaller slice for the skew check.
21    let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
22    // Skewed sizes gallop before any block kernel: a block kernel walks the
23    // large side linearly.
24    if small.len() * 8 < large.len() {
25        return galloping_intersection_count(small, large);
26    }
27
28    #[cfg(target_arch = "x86_64")]
29    if let Some(count) = crate::core::simd_math::simd_intersection_count(small, large) {
30        return count;
31    }
32
33    merge_intersection_count(small, large)
34}
35
36/// Sorted merge intersection count: O(|a| + |b|).
37#[inline]
38fn merge_intersection_count(a: &[u32], b: &[u32]) -> usize {
39    let mut count = 0;
40    let mut i = 0;
41    let mut j = 0;
42    while i < a.len() && j < b.len() {
43        match a[i].cmp(&b[j]) {
44            std::cmp::Ordering::Less => i += 1,
45            std::cmp::Ordering::Greater => j += 1,
46            std::cmp::Ordering::Equal => {
47                count += 1;
48                i += 1;
49                j += 1;
50            }
51        }
52    }
53    count
54}
55
56/// Galloping (exponential search) intersection: O(|small| * log(|large|)).
57///
58/// For each element in `small`, does an exponential search in `large` to find
59/// the matching position. Maintains a cursor into `large` so total work on
60/// the large array is bounded by O(|small| * log(|large|/|small|)).
61#[inline]
62fn galloping_intersection_count(small: &[u32], large: &[u32]) -> usize {
63    let mut count = 0;
64    let mut lo = 0; // cursor into large; advances monotonically
65    for &val in small {
66        // Skip elements in large that are less than val using exponential search.
67        // First, find a bound by doubling the step.
68        let mut step = 1;
69        while lo + step < large.len() && large[lo + step] < val {
70            step *= 2;
71        }
72        // Binary search within [lo, min(lo+step, len))
73        let hi = (lo + step).min(large.len());
74        // Find first position >= val
75        lo = binary_search_left(&large[lo..hi], val) + lo;
76        if lo < large.len() && large[lo] == val {
77            count += 1;
78            lo += 1; // move past this match
79        }
80    }
81    count
82}
83
84/// Binary search for the leftmost position where `slice[pos] >= target`.
85#[inline]
86fn binary_search_left(slice: &[u32], target: u32) -> usize {
87    let mut lo = 0;
88    let mut hi = slice.len();
89    while lo < hi {
90        let mid = lo + (hi - lo) / 2;
91        if slice[mid] < target {
92            lo = mid + 1;
93        } else {
94            hi = mid;
95        }
96    }
97    lo
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_sparse_intersection() {
106        let a = vec![1, 3, 5, 7, 9];
107        let b = vec![2, 3, 6, 7, 10];
108        assert_eq!(sparse_intersection_count(&a, &b), 2);
109    }
110
111    #[test]
112    fn test_sparse_intersection_empty() {
113        let a: Vec<u32> = vec![];
114        let b = vec![1, 2, 3];
115        assert_eq!(sparse_intersection_count(&a, &b), 0);
116    }
117
118    #[test]
119    fn test_galloping_skewed() {
120        // Small vs large: should trigger galloping path
121        let small = vec![5, 50, 500];
122        let large: Vec<u32> = (0..1000).collect();
123        assert_eq!(sparse_intersection_count(&small, &large), 3);
124    }
125
126    #[test]
127    fn test_galloping_no_overlap() {
128        let small = vec![1001, 1002, 1003];
129        let large: Vec<u32> = (0..1000).collect();
130        assert_eq!(sparse_intersection_count(&small, &large), 0);
131    }
132
133    #[test]
134    fn test_galloping_all_overlap() {
135        let small = vec![0, 1, 2, 3, 4];
136        let large: Vec<u32> = (0..1000).collect();
137        assert_eq!(sparse_intersection_count(&small, &large), 5);
138    }
139
140    #[test]
141    fn test_merge_balanced() {
142        let a: Vec<u32> = (0..100).step_by(2).collect(); // evens
143        let b: Vec<u32> = (0..100).step_by(3).collect(); // multiples of 3
144        let expected = (0..100u32).filter(|x| x % 2 == 0 && x % 3 == 0).count();
145        assert_eq!(sparse_intersection_count(&a, &b), expected);
146    }
147
148    #[test]
149    fn test_identical() {
150        let a: Vec<u32> = (0..50).collect();
151        assert_eq!(sparse_intersection_count(&a, &a), 50);
152    }
153
154    fn naive(a: &[u32], b: &[u32]) -> usize {
155        let set: std::collections::HashSet<u32> = b.iter().copied().collect();
156        a.iter().filter(|x| set.contains(x)).count()
157    }
158
159    struct Lcg(u64);
160    impl Lcg {
161        fn next(&mut self) -> u64 {
162            self.0 = self
163                .0
164                .wrapping_mul(6364136223846793005)
165                .wrapping_add(1442695040888963407);
166            self.0 >> 33
167        }
168    }
169
170    /// Sorted, deduplicated sample of `n` values below `universe` (`universe >= n`).
171    fn sample(rng: &mut Lcg, n: usize, universe: u64) -> Vec<u32> {
172        let mut set = std::collections::BTreeSet::new();
173        while set.len() < n {
174            set.insert((rng.next() % universe) as u32);
175        }
176        set.into_iter().collect()
177    }
178
179    fn check_all(a: &[u32], b: &[u32]) {
180        let want = naive(a, b);
181        assert_eq!(sparse_intersection_count(a, b), want, "dispatch a,b");
182        assert_eq!(sparse_intersection_count(b, a), want, "dispatch b,a");
183        assert_eq!(merge_intersection_count(a, b), want, "merge");
184        let (small, large) = if a.len() <= b.len() { (a, b) } else { (b, a) };
185        assert_eq!(galloping_intersection_count(small, large), want, "gallop");
186        #[cfg(target_arch = "x86_64")]
187        if let Some(c) = crate::core::simd_math::simd_intersection_count(a, b) {
188            assert_eq!(c, want, "avx2");
189        }
190    }
191
192    #[test]
193    fn all_kernels_match_naive_count() {
194        for seed in 0..300u64 {
195            let mut rng = Lcg(seed);
196            let na = (rng.next() % 2001) as usize;
197            let nb = (rng.next() % 2001) as usize;
198            // A universe near max(na, nb) forces heavy overlap; a wide one forces little.
199            let universe = (na.max(nb) as u64 + 1) * (1 + rng.next() % 8);
200            check_all(
201                &sample(&mut rng, na, universe),
202                &sample(&mut rng, nb, universe),
203            );
204        }
205    }
206
207    #[test]
208    fn kernels_match_on_edge_shapes_and_tails() {
209        let mut rng = Lcg(7);
210        for na in 0..=17usize {
211            for nb in (0..=17usize).chain([64, 65, 600]) {
212                for universe in [40u64, 2000, 1 << 20] {
213                    let universe = universe.max(na.max(nb) as u64);
214                    check_all(
215                        &sample(&mut rng, na, universe),
216                        &sample(&mut rng, nb, universe),
217                    );
218                }
219            }
220        }
221        let ident = sample(&mut rng, 777, 5000);
222        check_all(&ident, &ident);
223        let evens: Vec<u32> = (0..1000).map(|x| x * 2).collect();
224        let odds: Vec<u32> = (0..1000).map(|x| x * 2 + 1).collect();
225        check_all(&evens, &odds);
226        check_all(&[], &odds);
227        check_all(&[], &[]);
228    }
229
230    #[test]
231    fn test_reversed_args() {
232        // Ensure order does not matter
233        let a = vec![1, 5, 10];
234        let b: Vec<u32> = (0..1000).collect();
235        assert_eq!(
236            sparse_intersection_count(&a, &b),
237            sparse_intersection_count(&b, &a)
238        );
239    }
240}