Skip to main content

feagi_brain_development/spatial/
hash.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Morton spatial hash implementation using Roaring bitmaps.
6
7High-performance spatial indexing for neuron positions.
8*/
9
10use ahash::AHashMap;
11use roaring::RoaringBitmap;
12use std::sync::{Arc, RwLock};
13
14use super::morton::{morton_encode_3d, morton_encode_region_3d};
15
16/// Type alias for neuron map key: (cortical_area, morton_code)
17type NeuronMapKey = (String, u64);
18/// Type alias for coordinate map value: (area, x, y, z)
19type CoordinateMapValue = (String, u32, u32, u32);
20
21/// Spatial hash system using Morton encoding + Roaring bitmaps
22pub struct MortonSpatialHash {
23    /// Per-cortical-area bitmaps of occupied positions
24    cortical_bitmaps: Arc<RwLock<AHashMap<String, RoaringBitmap>>>,
25
26    /// Map Morton code -> list of neuron IDs at that position
27    /// Key: (cortical_area, morton_code)
28    neuron_map: Arc<RwLock<AHashMap<NeuronMapKey, Vec<u64>>>>,
29
30    /// Reverse map: neuron_id -> (area, x, y, z)
31    coordinate_map: Arc<RwLock<AHashMap<u64, CoordinateMapValue>>>,
32}
33
34impl MortonSpatialHash {
35    /// Create a new spatial hash system
36    pub fn new() -> Self {
37        Self {
38            cortical_bitmaps: Arc::new(RwLock::new(AHashMap::new())),
39            neuron_map: Arc::new(RwLock::new(AHashMap::new())),
40            coordinate_map: Arc::new(RwLock::new(AHashMap::new())),
41        }
42    }
43
44    /// Add a neuron to the spatial hash
45    pub fn add_neuron(
46        &self,
47        cortical_area: String,
48        x: u32,
49        y: u32,
50        z: u32,
51        neuron_id: u64,
52    ) -> bool {
53        // Validate coordinates
54        if x >= (1 << 21) || y >= (1 << 21) || z >= (1 << 21) {
55            return false;
56        }
57
58        let morton_code = morton_encode_3d(x, y, z);
59
60        // Add to cortical bitmap
61        {
62            let mut bitmaps = self.cortical_bitmaps.write().unwrap();
63            bitmaps
64                .entry(cortical_area.clone())
65                .or_default()
66                .insert(morton_code as u32);
67        }
68
69        // Add to neuron map
70        {
71            let mut neuron_map = self.neuron_map.write().unwrap();
72            let key = (cortical_area.clone(), morton_code);
73            neuron_map.entry(key).or_default().push(neuron_id);
74        }
75
76        // Add to coordinate map
77        {
78            let mut coord_map = self.coordinate_map.write().unwrap();
79            coord_map.insert(neuron_id, (cortical_area, x, y, z));
80        }
81
82        true
83    }
84
85    /// Get first neuron at coordinate (or None)
86    pub fn get_neuron_at_coordinate(
87        &self,
88        cortical_area: &str,
89        x: u32,
90        y: u32,
91        z: u32,
92    ) -> Option<u64> {
93        if x >= (1 << 21) || y >= (1 << 21) || z >= (1 << 21) {
94            return None;
95        }
96
97        let morton_code = morton_encode_3d(x, y, z);
98
99        {
100            let bitmaps = self.cortical_bitmaps.read().unwrap();
101            let bitmap = bitmaps.get(cortical_area)?;
102            if !bitmap.contains(morton_code as u32) {
103                return None;
104            }
105        }
106
107        // Get neuron IDs
108        let neuron_map = self.neuron_map.read().unwrap();
109        let key = (cortical_area.to_string(), morton_code);
110        neuron_map
111            .get(&key)
112            .and_then(|neurons| neurons.first().copied())
113    }
114
115    /// Get all neurons at coordinate
116    pub fn get_neurons_at_coordinate(
117        &self,
118        cortical_area: &str,
119        x: u32,
120        y: u32,
121        z: u32,
122    ) -> Vec<u64> {
123        if x >= (1 << 21) || y >= (1 << 21) || z >= (1 << 21) {
124            return Vec::new();
125        }
126
127        let morton_code = morton_encode_3d(x, y, z);
128
129        // Check bitmap first (fast)
130        {
131            let bitmaps = self.cortical_bitmaps.read().unwrap();
132            if let Some(bitmap) = bitmaps.get(cortical_area) {
133                if !bitmap.contains(morton_code as u32) {
134                    return Vec::new();
135                }
136            } else {
137                return Vec::new();
138            }
139        }
140
141        // Get neurons
142        let neuron_map = self.neuron_map.read().unwrap();
143        let key = (cortical_area.to_string(), morton_code);
144        neuron_map.get(&key).cloned().unwrap_or_default()
145    }
146
147    /// Get all neurons in a 3D region
148    #[allow(clippy::too_many_arguments)]
149    pub fn get_neurons_in_region(
150        &self,
151        cortical_area: &str,
152        x1: u32,
153        y1: u32,
154        z1: u32,
155        x2: u32,
156        y2: u32,
157        z2: u32,
158    ) -> Vec<u64> {
159        // Get area bitmap
160        let area_bitmap = {
161            let bitmaps = self.cortical_bitmaps.read().unwrap();
162            match bitmaps.get(cortical_area) {
163                Some(bitmap) => bitmap.clone(),
164                None => return Vec::new(),
165            }
166        };
167
168        // Create region bitmap
169        let region_codes = morton_encode_region_3d(x1, y1, z1, x2, y2, z2);
170        let mut region_bitmap = RoaringBitmap::new();
171        for code in region_codes {
172            region_bitmap.insert(code as u32);
173        }
174
175        // Fast intersection
176        let intersection = &area_bitmap & &region_bitmap;
177
178        // Collect neurons
179        let neuron_map = self.neuron_map.read().unwrap();
180        let mut result = Vec::new();
181
182        for morton_code in intersection {
183            let key = (cortical_area.to_string(), morton_code as u64);
184            if let Some(neurons) = neuron_map.get(&key) {
185                result.extend(neurons);
186            }
187        }
188
189        result
190    }
191
192    /// Get neuron's position
193    pub fn get_neuron_position(&self, neuron_id: u64) -> Option<(String, u32, u32, u32)> {
194        let coord_map = self.coordinate_map.read().unwrap();
195        coord_map.get(&neuron_id).cloned()
196    }
197
198    /// Remove a neuron from the spatial hash
199    pub fn remove_neuron(&self, neuron_id: u64) -> bool {
200        // Get position
201        let position = {
202            let mut coord_map = self.coordinate_map.write().unwrap();
203            coord_map.remove(&neuron_id)
204        };
205
206        if let Some((area, x, y, z)) = position {
207            let morton_code = morton_encode_3d(x, y, z);
208
209            // Remove from neuron map
210            {
211                let mut neuron_map = self.neuron_map.write().unwrap();
212                let key = (area.clone(), morton_code);
213                if let Some(neurons) = neuron_map.get_mut(&key) {
214                    neurons.retain(|&id| id != neuron_id);
215                    if neurons.is_empty() {
216                        neuron_map.remove(&key);
217                    }
218                }
219            }
220
221            // If no more neurons at this position, remove from bitmap
222            {
223                let neuron_map = self.neuron_map.read().unwrap();
224                let key = (area.clone(), morton_code);
225                if !neuron_map.contains_key(&key) {
226                    let mut bitmaps = self.cortical_bitmaps.write().unwrap();
227                    if let Some(bitmap) = bitmaps.get_mut(&area) {
228                        bitmap.remove(morton_code as u32);
229                    }
230                }
231            }
232
233            true
234        } else {
235            false
236        }
237    }
238
239    /// Clear all data
240    pub fn clear(&self) {
241        self.cortical_bitmaps.write().unwrap().clear();
242        self.neuron_map.write().unwrap().clear();
243        self.coordinate_map.write().unwrap().clear();
244    }
245
246    /// Get statistics
247    pub fn get_stats(&self) -> SpatialHashStats {
248        let bitmaps = self.cortical_bitmaps.read().unwrap();
249        let coord_map = self.coordinate_map.read().unwrap();
250
251        SpatialHashStats {
252            total_areas: bitmaps.len(),
253            total_neurons: coord_map.len(),
254            total_occupied_positions: bitmaps.values().map(|b| b.len() as usize).sum(),
255        }
256    }
257}
258
259impl Default for MortonSpatialHash {
260    fn default() -> Self {
261        Self::new()
262    }
263}
264
265/// Statistics about the spatial hash
266#[derive(Debug, Clone)]
267pub struct SpatialHashStats {
268    pub total_areas: usize,
269    pub total_neurons: usize,
270    pub total_occupied_positions: usize,
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_add_and_get_neuron() {
279        let hash = MortonSpatialHash::new();
280
281        assert!(hash.add_neuron("v1".to_string(), 10, 20, 30, 1001));
282
283        let neuron = hash.get_neuron_at_coordinate("v1", 10, 20, 30);
284        assert_eq!(neuron, Some(1001));
285
286        let neurons = hash.get_neurons_at_coordinate("v1", 10, 20, 30);
287        assert_eq!(neurons, vec![1001]);
288    }
289
290    #[test]
291    fn test_multiple_neurons_same_position() {
292        let hash = MortonSpatialHash::new();
293
294        hash.add_neuron("v1".to_string(), 5, 5, 5, 100);
295        hash.add_neuron("v1".to_string(), 5, 5, 5, 101);
296        hash.add_neuron("v1".to_string(), 5, 5, 5, 102);
297
298        let neurons = hash.get_neurons_at_coordinate("v1", 5, 5, 5);
299        assert_eq!(neurons.len(), 3);
300        assert!(neurons.contains(&100));
301        assert!(neurons.contains(&101));
302        assert!(neurons.contains(&102));
303    }
304
305    #[test]
306    fn test_region_query() {
307        let hash = MortonSpatialHash::new();
308
309        // Add neurons in a 10x10x10 grid
310        for x in 0..10 {
311            for y in 0..10 {
312                for z in 0..10 {
313                    let neuron_id = (x * 100 + y * 10 + z) as u64;
314                    hash.add_neuron("v1".to_string(), x, y, z, neuron_id);
315                }
316            }
317        }
318
319        // Query a 2x2x2 subregion
320        let neurons = hash.get_neurons_in_region("v1", 0, 0, 0, 1, 1, 1);
321        assert_eq!(neurons.len(), 8);
322    }
323
324    #[test]
325    fn test_get_neuron_position() {
326        let hash = MortonSpatialHash::new();
327
328        hash.add_neuron("v1".to_string(), 42, 84, 126, 999);
329
330        let pos = hash.get_neuron_position(999);
331        assert_eq!(pos, Some(("v1".to_string(), 42, 84, 126)));
332    }
333
334    #[test]
335    fn test_remove_neuron() {
336        let hash = MortonSpatialHash::new();
337
338        hash.add_neuron("v1".to_string(), 10, 20, 30, 1001);
339        assert!(hash.remove_neuron(1001));
340
341        let neuron = hash.get_neuron_at_coordinate("v1", 10, 20, 30);
342        assert_eq!(neuron, None);
343    }
344}