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