Skip to main content

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 firing_threshold_limit from properties (defaults to 0.0 = no limit)
57    fn firing_threshold_limit(&self) -> f32;
58
59    /// Get property as u32 with default
60    fn get_u32_property(&self, key: &str, default: u32) -> u32;
61
62    /// Get property as u16 with default
63    fn get_u16_property(&self, key: &str, default: u16) -> u16;
64
65    /// Get property as f32 with default
66    fn get_f32_property(&self, key: &str, default: f32) -> f32;
67
68    /// Get property as bool with default
69    fn get_bool_property(&self, key: &str, default: bool) -> bool;
70
71    /// Check if this is an input area
72    fn is_input_area(&self) -> bool;
73
74    /// Check if this is an output area
75    fn is_output_area(&self) -> bool;
76
77    /// Get cortical group classification
78    fn get_cortical_group(&self) -> Option<String>;
79
80    /// Get visible flag from properties (defaults to true)
81    fn visible(&self) -> bool;
82
83    /// Get sub_group from properties
84    fn sub_group(&self) -> Option<String>;
85
86    /// Get plasticity_constant from properties
87    fn plasticity_constant(&self) -> f32;
88
89    /// Get postsynaptic_current from properties
90    fn postsynaptic_current(&self) -> f32;
91
92    /// Get psp_uniform_distribution from properties (defaults to true for memory cortical areas,
93    /// false for other types when the key is absent)
94    fn psp_uniform_distribution(&self) -> bool;
95
96    /// Get degeneration from properties
97    fn degeneration(&self) -> f32;
98
99    /// Get burst_engine_active from properties
100    fn burst_engine_active(&self) -> bool;
101
102    /// Get firing_threshold_increment from properties
103    fn firing_threshold_increment(&self) -> f32;
104
105    /// Get firing_threshold_increment_x from properties
106    fn firing_threshold_increment_x(&self) -> f32;
107
108    /// Get firing_threshold_increment_y from properties
109    fn firing_threshold_increment_y(&self) -> f32;
110
111    /// Get firing_threshold_increment_z from properties
112    fn firing_threshold_increment_z(&self) -> f32;
113
114    /// Get consecutive_fire_count from properties
115    fn consecutive_fire_count(&self) -> u32;
116
117    /// Get leak_variability from properties
118    fn leak_variability(&self) -> f32;
119
120    /// Get neuron_excitability from properties
121    fn neuron_excitability(&self) -> f32;
122
123    /// Get postsynaptic_current_max from properties
124    fn postsynaptic_current_max(&self) -> f32;
125
126    /// Get mp_charge_accumulation from properties
127    fn mp_charge_accumulation(&self) -> bool;
128
129    /// Get mp_driven_psp from properties
130    fn mp_driven_psp(&self) -> bool;
131
132    /// Get init_lifespan from properties (memory parameter)
133    fn init_lifespan(&self) -> u32;
134
135    /// Get lifespan_growth_rate from properties (memory parameter)
136    fn lifespan_growth_rate(&self) -> f32;
137
138    /// Get longterm_mem_threshold from properties (memory parameter)
139    fn longterm_mem_threshold(&self) -> u32;
140}
141
142impl CorticalAreaExt for CorticalArea {
143    fn with_properties(mut self, properties: HashMap<String, serde_json::Value>) -> Self {
144        self.properties = properties;
145        self
146    }
147
148    fn add_property(mut self, key: String, value: serde_json::Value) -> Self {
149        self.properties.insert(key, value);
150        self
151    }
152
153    fn add_property_mut(&mut self, key: String, value: serde_json::Value) {
154        self.properties.insert(key, value);
155    }
156
157    fn contains_position(&self, pos: (i32, i32, i32)) -> bool {
158        let (x, y, z) = pos;
159        let ox = self.position.x;
160        let oy = self.position.y;
161        let oz = self.position.z;
162
163        x >= ox
164            && y >= oy
165            && z >= oz
166            && x < ox + self.dimensions.width as i32
167            && y < oy + self.dimensions.height as i32
168            && z < oz + self.dimensions.depth as i32
169    }
170
171    fn to_relative_position(&self, pos: (i32, i32, i32)) -> BduResult<Position> {
172        if !self.contains_position(pos) {
173            return Err(BduError::OutOfBounds {
174                pos: (pos.0 as u32, pos.1 as u32, pos.2 as u32),
175                dims: (
176                    self.dimensions.width as usize,
177                    self.dimensions.height as usize,
178                    self.dimensions.depth as usize,
179                ),
180            });
181        }
182
183        let ox = self.position.x;
184        let oy = self.position.y;
185        let oz = self.position.z;
186        Ok((
187            (pos.0 - ox) as u32,
188            (pos.1 - oy) as u32,
189            (pos.2 - oz) as u32,
190        ))
191    }
192
193    fn to_absolute_position(&self, rel_pos: Position) -> BduResult<(i32, i32, i32)> {
194        if !self.dimensions.contains(rel_pos) {
195            return Err(BduError::OutOfBounds {
196                pos: rel_pos,
197                dims: (
198                    self.dimensions.width as usize,
199                    self.dimensions.height as usize,
200                    self.dimensions.depth as usize,
201                ),
202            });
203        }
204
205        let ox = self.position.x;
206        let oy = self.position.y;
207        let oz = self.position.z;
208        Ok((
209            ox + rel_pos.0 as i32,
210            oy + rel_pos.1 as i32,
211            oz + rel_pos.2 as i32,
212        ))
213    }
214
215    fn neurons_per_voxel(&self) -> u32 {
216        self.get_u32_property("neurons_per_voxel", 1)
217    }
218
219    fn refractory_period(&self) -> u16 {
220        self.get_u16_property("refractory_period", 0)
221    }
222
223    fn snooze_period(&self) -> u16 {
224        self.get_u16_property("snooze_period", 0)
225    }
226
227    fn leak_coefficient(&self) -> f32 {
228        self.get_f32_property("leak_coefficient", 0.0)
229    }
230
231    fn firing_threshold(&self) -> f32 {
232        self.get_f32_property("firing_threshold", 1.0)
233    }
234
235    fn get_u32_property(&self, key: &str, default: u32) -> u32 {
236        self.properties
237            .get(key)
238            .and_then(|v| v.as_u64())
239            .map(|v| v as u32)
240            .unwrap_or(default)
241    }
242
243    fn get_u16_property(&self, key: &str, default: u16) -> u16 {
244        self.properties
245            .get(key)
246            .and_then(|v| v.as_u64())
247            .map(|v| v as u16)
248            .unwrap_or(default)
249    }
250
251    fn get_f32_property(&self, key: &str, default: f32) -> f32 {
252        self.properties
253            .get(key)
254            .and_then(|v| v.as_f64())
255            .map(|v| v as f32)
256            .unwrap_or(default)
257    }
258
259    fn get_bool_property(&self, key: &str, default: bool) -> bool {
260        self.properties
261            .get(key)
262            .and_then(|v| v.as_bool())
263            .unwrap_or(default)
264    }
265
266    fn is_input_area(&self) -> bool {
267        matches!(
268            self.cortical_type,
269            feagi_structures::genomic::cortical_area::CorticalAreaType::BrainInput(_)
270        )
271    }
272
273    fn is_output_area(&self) -> bool {
274        matches!(
275            self.cortical_type,
276            feagi_structures::genomic::cortical_area::CorticalAreaType::BrainOutput(_)
277        )
278    }
279
280    fn get_cortical_group(&self) -> Option<String> {
281        self.properties
282            .get("cortical_group")
283            .and_then(|v| v.as_str())
284            .map(|s| s.to_string())
285            .or_else(|| {
286                // Derive from cortical_type if not in properties
287                use feagi_structures::genomic::cortical_area::CorticalAreaType;
288                match self.cortical_type {
289                    CorticalAreaType::BrainInput(_) => Some("IPU".to_string()),
290                    CorticalAreaType::BrainOutput(_) => Some("OPU".to_string()),
291                    CorticalAreaType::Memory(_) => Some("MEMORY".to_string()),
292                    CorticalAreaType::Custom(_) => Some("CUSTOM".to_string()),
293                    CorticalAreaType::Core(_) => Some("CORE".to_string()),
294                }
295            })
296    }
297
298    fn visible(&self) -> bool {
299        self.get_bool_property("visible", true)
300    }
301
302    fn sub_group(&self) -> Option<String> {
303        self.properties
304            .get("sub_group")
305            .and_then(|v| v.as_str())
306            .map(|s| s.to_string())
307    }
308
309    fn plasticity_constant(&self) -> f32 {
310        self.get_f32_property("plasticity_constant", 0.0)
311    }
312
313    fn postsynaptic_current(&self) -> f32 {
314        self.get_f32_property("postsynaptic_current", 1.0)
315    }
316
317    fn psp_uniform_distribution(&self) -> bool {
318        let default = matches!(
319            self.cortical_type,
320            feagi_structures::genomic::cortical_area::CorticalAreaType::Memory(_)
321        );
322        self.get_bool_property("psp_uniform_distribution", default)
323    }
324
325    fn degeneration(&self) -> f32 {
326        self.get_f32_property("degeneration", 0.0)
327    }
328
329    fn burst_engine_active(&self) -> bool {
330        self.get_bool_property("burst_engine_active", false)
331    }
332
333    fn firing_threshold_increment(&self) -> f32 {
334        self.get_f32_property("firing_threshold_increment", 0.0)
335    }
336
337    fn firing_threshold_increment_x(&self) -> f32 {
338        self.get_f32_property("firing_threshold_increment_x", 0.0)
339    }
340
341    fn firing_threshold_increment_y(&self) -> f32 {
342        self.get_f32_property("firing_threshold_increment_y", 0.0)
343    }
344
345    fn firing_threshold_increment_z(&self) -> f32 {
346        self.get_f32_property("firing_threshold_increment_z", 0.0)
347    }
348
349    fn firing_threshold_limit(&self) -> f32 {
350        self.get_f32_property("firing_threshold_limit", 0.0)
351    }
352
353    fn consecutive_fire_count(&self) -> u32 {
354        self.get_u32_property("consecutive_fire_limit", 0)
355    }
356
357    fn leak_variability(&self) -> f32 {
358        self.get_f32_property("leak_variability", 0.0)
359    }
360
361    fn neuron_excitability(&self) -> f32 {
362        self.get_f32_property("neuron_excitability", 100.0)
363    }
364
365    fn postsynaptic_current_max(&self) -> f32 {
366        self.get_f32_property("postsynaptic_current_max", 0.0)
367    }
368
369    fn mp_charge_accumulation(&self) -> bool {
370        self.get_bool_property("mp_charge_accumulation", false)
371    }
372
373    fn mp_driven_psp(&self) -> bool {
374        self.get_bool_property("mp_driven_psp", false)
375    }
376
377    fn init_lifespan(&self) -> u32 {
378        self.get_u32_property("init_lifespan", 0)
379    }
380
381    fn lifespan_growth_rate(&self) -> f32 {
382        self.get_f32_property("lifespan_growth_rate", 0.0)
383    }
384
385    fn longterm_mem_threshold(&self) -> u32 {
386        self.get_u32_property("longterm_mem_threshold", 0)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_contains_position() {
396        let cortical_id = CoreCorticalType::Power.to_cortical_id();
397        let cortical_type = cortical_id
398            .as_cortical_type()
399            .expect("Failed to get cortical type");
400        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
401        let area = CorticalArea::new(
402            cortical_id,
403            0,
404            "Test Area".to_string(),
405            dims,
406            (5, 5, 5).into(),
407            cortical_type,
408        )
409        .unwrap();
410
411        assert!(area.contains_position((5, 5, 5))); // Min corner
412        assert!(area.contains_position((14, 14, 14))); // Max corner
413        assert!(!area.contains_position((4, 5, 5))); // Outside (x too small)
414        assert!(!area.contains_position((15, 5, 5))); // Outside (x too large)
415    }
416
417    #[test]
418    fn test_position_conversion() {
419        let cortical_id = CoreCorticalType::Power.to_cortical_id();
420        let cortical_type = cortical_id
421            .as_cortical_type()
422            .expect("Failed to get cortical type");
423        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
424        let area = CorticalArea::new(
425            cortical_id,
426            0,
427            "Test Area".to_string(),
428            dims,
429            (100, 200, 300).into(),
430            cortical_type,
431        )
432        .unwrap();
433
434        // Area spans from (100,200,300) to (109,209,309)
435        // Absolute (105, 207, 308) should map to relative (5, 7, 8)
436        let rel_pos = area.to_relative_position((105, 207, 308)).unwrap();
437        assert_eq!(rel_pos, (5, 7, 8));
438
439        // Convert back
440        let abs_pos = area.to_absolute_position(rel_pos).unwrap();
441        assert_eq!(abs_pos, (105, 207, 308));
442
443        // Test out of bounds
444        let result = area.to_relative_position((99, 200, 300));
445        assert!(result.is_err());
446    }
447
448    #[test]
449    fn test_properties() {
450        let cortical_id = CoreCorticalType::Power.to_cortical_id();
451        let cortical_type = cortical_id
452            .as_cortical_type()
453            .expect("Failed to get cortical type");
454        let dims = CorticalAreaDimensions::new(10, 10, 10).unwrap();
455        let area = CorticalArea::new(
456            cortical_id,
457            0,
458            "Test".to_string(),
459            dims,
460            (0, 0, 0).into(),
461            cortical_type,
462        )
463        .unwrap()
464        .add_property("resolution".to_string(), serde_json::json!(128))
465        .add_property("modality".to_string(), serde_json::json!("visual"));
466
467        assert_eq!(
468            area.get_property("resolution"),
469            Some(&serde_json::json!(128))
470        );
471        assert_eq!(
472            area.get_property("modality"),
473            Some(&serde_json::json!("visual"))
474        );
475        assert_eq!(area.get_property("nonexistent"), None);
476    }
477}