feagi_brain_development/models/
cortical_area.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5CorticalArea business logic and extension methods.
6
7The core CorticalArea data structure is defined in feagi_data_structures.
8This module provides business logic methods for coordinate transformations
9and builder patterns.
10*/
11
12use std::collections::HashMap;
13
14use crate::types::{BduError, BduResult, Position};
15
16// Import core types from feagi_data_structures
17pub use feagi_structures::genomic::cortical_area::{
18    CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalID,
19};
20
21/// Extension trait providing business logic methods for CorticalArea
22pub trait CorticalAreaExt {
23    /// Create a cortical area with custom properties
24    fn with_properties(self, properties: HashMap<String, serde_json::Value>) -> Self;
25
26    /// Add a single property (builder pattern)
27    fn add_property(self, key: String, value: serde_json::Value) -> Self;
28
29    /// Add a single property in-place
30    fn add_property_mut(&mut self, key: String, value: serde_json::Value);
31
32    /// Check if a 3D position is within this area's bounds
33    fn contains_position(&self, pos: (i32, i32, i32)) -> bool;
34
35    /// Convert absolute brain position to relative position within this area
36    fn to_relative_position(&self, pos: (i32, i32, i32)) -> BduResult<Position>;
37
38    /// Convert relative position within area to absolute brain position
39    fn to_absolute_position(&self, rel_pos: Position) -> BduResult<(i32, i32, i32)>;
40
41    /// Get neurons_per_voxel from properties (defaults to 1)
42    fn neurons_per_voxel(&self) -> u32;
43
44    /// Get refractory_period from properties (defaults to 0)
45    fn refractory_period(&self) -> u16;
46
47    /// Get snooze_period from properties (defaults to 0)
48    fn snooze_period(&self) -> u16;
49
50    /// Get leak_coefficient from properties (defaults to 0.0)
51    fn leak_coefficient(&self) -> f32;
52
53    /// Get firing_threshold from properties (defaults to 1.0)
54    fn firing_threshold(&self) -> f32;
55
56    /// Get property as u32 with default
57    fn get_u32_property(&self, key: &str, default: u32) -> u32;
58
59    /// Get property as u16 with default
60    fn get_u16_property(&self, key: &str, default: u16) -> u16;
61
62    /// Get property as f32 with default
63    fn get_f32_property(&self, key: &str, default: f32) -> f32;
64
65    /// Get property as bool with default
66    fn get_bool_property(&self, key: &str, default: bool) -> bool;
67
68    /// Check if this is an input area
69    fn is_input_area(&self) -> bool;
70
71    /// Check if this is an output area
72    fn is_output_area(&self) -> bool;
73
74    /// Get cortical group classification
75    fn get_cortical_group(&self) -> Option<String>;
76
77    /// Get visible flag from properties (defaults to true)
78    fn visible(&self) -> bool;
79
80    /// Get sub_group from properties
81    fn sub_group(&self) -> Option<String>;
82
83    /// Get plasticity_constant from properties
84    fn plasticity_constant(&self) -> f32;
85
86    /// Get postsynaptic_current from properties
87    fn postsynaptic_current(&self) -> f32;
88
89    /// Get psp_uniform_distribution from properties
90    fn psp_uniform_distribution(&self) -> f32;
91
92    /// Get degeneration from properties
93    fn degeneration(&self) -> f32;
94
95    /// Get burst_engine_active from properties
96    fn burst_engine_active(&self) -> bool;
97
98    /// Get firing_threshold_increment from properties
99    fn firing_threshold_increment(&self) -> f32;
100
101    /// Get firing_threshold_limit from properties
102    fn firing_threshold_limit(&self) -> f32;
103
104    /// Get consecutive_fire_count from properties
105    fn consecutive_fire_count(&self) -> u32;
106
107    /// Get leak_variability from properties
108    fn leak_variability(&self) -> f32;
109}
110
111impl CorticalAreaExt for CorticalArea {
112    fn with_properties(mut self, properties: HashMap<String, serde_json::Value>) -> Self {
113        self.properties = properties;
114        self
115    }
116
117    fn add_property(mut self, key: String, value: serde_json::Value) -> Self {
118        self.properties.insert(key, value);
119        self
120    }
121
122    fn add_property_mut(&mut self, key: String, value: serde_json::Value) {
123        self.properties.insert(key, value);
124    }
125
126    fn contains_position(&self, pos: (i32, i32, i32)) -> bool {
127        let (x, y, z) = pos;
128        let ox = self.position.x;
129        let oy = self.position.y;
130        let oz = self.position.z;
131
132        x >= ox
133            && y >= oy
134            && z >= oz
135            && x < ox + self.dimensions.width as i32
136            && y < oy + self.dimensions.height as i32
137            && z < oz + self.dimensions.depth as i32
138    }
139
140    fn to_relative_position(&self, pos: (i32, i32, i32)) -> BduResult<Position> {
141        if !self.contains_position(pos) {
142            return Err(BduError::OutOfBounds {
143                pos: (pos.0 as u32, pos.1 as u32, pos.2 as u32),
144                dims: (
145                    self.dimensions.width as usize,
146                    self.dimensions.height as usize,
147                    self.dimensions.depth as usize,
148                ),
149            });
150        }
151
152        let ox = self.position.x;
153        let oy = self.position.y;
154        let oz = self.position.z;
155        Ok((
156            (pos.0 - ox) as u32,
157            (pos.1 - oy) as u32,
158            (pos.2 - oz) as u32,
159        ))
160    }
161
162    fn to_absolute_position(&self, rel_pos: Position) -> BduResult<(i32, i32, i32)> {
163        if !self.dimensions.contains(rel_pos) {
164            return Err(BduError::OutOfBounds {
165                pos: rel_pos,
166                dims: (
167                    self.dimensions.width as usize,
168                    self.dimensions.height as usize,
169                    self.dimensions.depth as usize,
170                ),
171            });
172        }
173
174        let ox = self.position.x;
175        let oy = self.position.y;
176        let oz = self.position.z;
177        Ok((
178            ox + rel_pos.0 as i32,
179            oy + rel_pos.1 as i32,
180            oz + rel_pos.2 as i32,
181        ))
182    }
183
184    fn neurons_per_voxel(&self) -> u32 {
185        self.get_u32_property("neurons_per_voxel", 1)
186    }
187
188    fn refractory_period(&self) -> u16 {
189        self.get_u16_property("refractory_period", 0)
190    }
191
192    fn snooze_period(&self) -> u16 {
193        self.get_u16_property("snooze_period", 0)
194    }
195
196    fn leak_coefficient(&self) -> f32 {
197        self.get_f32_property("leak_coefficient", 0.0)
198    }
199
200    fn firing_threshold(&self) -> f32 {
201        self.get_f32_property("firing_threshold", 1.0)
202    }
203
204    fn get_u32_property(&self, key: &str, default: u32) -> u32 {
205        self.properties
206            .get(key)
207            .and_then(|v| v.as_u64())
208            .map(|v| v as u32)
209            .unwrap_or(default)
210    }
211
212    fn get_u16_property(&self, key: &str, default: u16) -> u16 {
213        self.properties
214            .get(key)
215            .and_then(|v| v.as_u64())
216            .map(|v| v as u16)
217            .unwrap_or(default)
218    }
219
220    fn get_f32_property(&self, key: &str, default: f32) -> f32 {
221        self.properties
222            .get(key)
223            .and_then(|v| v.as_f64())
224            .map(|v| v as f32)
225            .unwrap_or(default)
226    }
227
228    fn get_bool_property(&self, key: &str, default: bool) -> bool {
229        self.properties
230            .get(key)
231            .and_then(|v| v.as_bool())
232            .unwrap_or(default)
233    }
234
235    fn is_input_area(&self) -> bool {
236        matches!(
237            self.cortical_type,
238            feagi_structures::genomic::cortical_area::CorticalAreaType::BrainInput(_)
239        )
240    }
241
242    fn is_output_area(&self) -> bool {
243        matches!(
244            self.cortical_type,
245            feagi_structures::genomic::cortical_area::CorticalAreaType::BrainOutput(_)
246        )
247    }
248
249    fn get_cortical_group(&self) -> Option<String> {
250        self.properties
251            .get("cortical_group")
252            .and_then(|v| v.as_str())
253            .map(|s| s.to_string())
254            .or_else(|| {
255                // Derive from cortical_type if not in properties
256                use feagi_structures::genomic::cortical_area::CorticalAreaType;
257                match self.cortical_type {
258                    CorticalAreaType::BrainInput(_) => Some("IPU".to_string()),
259                    CorticalAreaType::BrainOutput(_) => Some("OPU".to_string()),
260                    CorticalAreaType::Memory(_) => Some("MEMORY".to_string()),
261                    CorticalAreaType::Custom(_) => Some("CUSTOM".to_string()),
262                    CorticalAreaType::Core(_) => Some("CORE".to_string()),
263                }
264            })
265    }
266
267    fn visible(&self) -> bool {
268        self.get_bool_property("visible", true)
269    }
270
271    fn sub_group(&self) -> Option<String> {
272        self.properties
273            .get("sub_group")
274            .and_then(|v| v.as_str())
275            .map(|s| s.to_string())
276    }
277
278    fn plasticity_constant(&self) -> f32 {
279        self.get_f32_property("plasticity_constant", 0.0)
280    }
281
282    fn postsynaptic_current(&self) -> f32 {
283        self.get_f32_property("postsynaptic_current", 0.0)
284    }
285
286    fn psp_uniform_distribution(&self) -> f32 {
287        self.get_f32_property("psp_uniform_distribution", 0.0)
288    }
289
290    fn degeneration(&self) -> f32 {
291        self.get_f32_property("degeneration", 0.0)
292    }
293
294    fn burst_engine_active(&self) -> bool {
295        self.get_bool_property("burst_engine_active", false)
296    }
297
298    fn firing_threshold_increment(&self) -> f32 {
299        self.get_f32_property("firing_threshold_increment", 0.0)
300    }
301
302    fn firing_threshold_limit(&self) -> f32 {
303        self.get_f32_property("firing_threshold_limit", 1.0)
304    }
305
306    fn consecutive_fire_count(&self) -> u32 {
307        self.get_u32_property("consecutive_fire_limit", 0)
308    }
309
310    fn leak_variability(&self) -> f32 {
311        self.get_f32_property("leak_variability", 0.0)
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_contains_position() {
321        let cortical_id = CoreCorticalType::Power.to_cortical_id();
322        let cortical_type = cortical_id
323            .as_cortical_type()
324            .expect("Failed to get cortical type");
325        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
326        let area = CorticalArea::new(
327            cortical_id,
328            0,
329            "Test Area".to_string(),
330            dims,
331            (5, 5, 5).into(),
332            cortical_type,
333        )
334        .unwrap();
335
336        assert!(area.contains_position((5, 5, 5))); // Min corner
337        assert!(area.contains_position((14, 14, 14))); // Max corner
338        assert!(!area.contains_position((4, 5, 5))); // Outside (x too small)
339        assert!(!area.contains_position((15, 5, 5))); // Outside (x too large)
340    }
341
342    #[test]
343    fn test_position_conversion() {
344        let cortical_id = CoreCorticalType::Power.to_cortical_id();
345        let cortical_type = cortical_id
346            .as_cortical_type()
347            .expect("Failed to get cortical type");
348        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
349        let area = CorticalArea::new(
350            cortical_id,
351            0,
352            "Test Area".to_string(),
353            dims,
354            (100, 200, 300).into(),
355            cortical_type,
356        )
357        .unwrap();
358
359        // Area spans from (100,200,300) to (109,209,309)
360        // Absolute (105, 207, 308) should map to relative (5, 7, 8)
361        let rel_pos = area.to_relative_position((105, 207, 308)).unwrap();
362        assert_eq!(rel_pos, (5, 7, 8));
363
364        // Convert back
365        let abs_pos = area.to_absolute_position(rel_pos).unwrap();
366        assert_eq!(abs_pos, (105, 207, 308));
367
368        // Test out of bounds
369        let result = area.to_relative_position((99, 200, 300));
370        assert!(result.is_err());
371    }
372
373    #[test]
374    fn test_properties() {
375        let cortical_id = CoreCorticalType::Power.to_cortical_id();
376        let cortical_type = cortical_id
377            .as_cortical_type()
378            .expect("Failed to get cortical type");
379        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
380        let area = CorticalArea::new(
381            cortical_id,
382            0,
383            "Test".to_string(),
384            dims,
385            (0, 0, 0).into(),
386            cortical_type,
387        )
388        .unwrap()
389        .add_property("resolution".to_string(), serde_json::json!(128))
390        .add_property("modality".to_string(), serde_json::json!("visual"));
391
392        assert_eq!(
393            area.get_property("resolution"),
394            Some(&serde_json::json!(128))
395        );
396        assert_eq!(
397            area.get_property("modality"),
398            Some(&serde_json::json!("visual"))
399        );
400        assert_eq!(area.get_property("nonexistent"), None);
401    }
402}