Skip to main content

feagi_structures/genomic/
sensory_cortical_unit.rs

1use crate::genomic::cortical_area::descriptors::CorticalSubUnitIndex;
2use crate::genomic::cortical_area::descriptors::CorticalUnitIndex;
3use crate::genomic::cortical_area::io_cortical_area_configuration_flag::{
4    FrameChangeHandling, PercentageNeuronPositioning,
5};
6use crate::genomic::cortical_area::{
7    CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
8};
9use crate::sensor_cortical_units;
10use paste;
11use serde_json::{Map, Value};
12use std::collections::HashMap;
13use std::fmt::{Display, Formatter};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] // TODO move me!
16pub struct UnitTopology {
17    pub relative_position: [i32; 3],
18    pub channel_dimensions_default: [u32; 3],
19    pub channel_dimensions_min: [u32; 3],
20    pub channel_dimensions_max: [u32; 3],
21}
22
23macro_rules! default_firing_threshold_impl {
24    () => {
25        None
26    };
27    ($value:expr) => {
28        Some($value)
29    };
30}
31
32macro_rules! default_mp_charge_accumulation_impl {
33    () => {
34        None
35    };
36    ($value:expr) => {
37        Some($value)
38    };
39}
40
41macro_rules! default_sensory_hold_last_frame_impl {
42    () => {
43        None
44    };
45    ($value:expr) => {
46        Some($value)
47    };
48}
49
50macro_rules! default_firing_threshold_increment_impl {
51    () => {
52        None
53    };
54    ([$x:expr, $y:expr, $z:expr]) => {
55        Some([$x, $y, $z])
56    };
57}
58
59macro_rules! define_sensory_cortical_units_enum {
60    (
61        SensoryCorticalUnit {
62            $(
63                $(#[doc = $doc:expr])?
64                $variant_name:ident => {
65                    friendly_name: $friendly_name:expr,
66                    accepted_wrapped_io_data_type: $accepted_wrapped_io_data_type:expr,
67                    cortical_id_unit_reference: $cortical_id_unit_reference:expr,
68                    number_cortical_areas: $number_cortical_areas:expr,
69                    $(default_firing_threshold: $default_firing_threshold:expr,)?
70                    $(default_firing_threshold_increment: [$default_firing_threshold_increment_x:expr, $default_firing_threshold_increment_y:expr, $default_firing_threshold_increment_z:expr],)?
71                    $(default_mp_charge_accumulation: $default_mp_charge_accumulation:expr,)?
72                    $(default_sensory_hold_last_frame: $default_sensory_hold_last_frame:expr,)?
73                    cortical_type_parameters: {
74                        $($param_name:ident: $param_type:ty),* $(,)?
75                    },
76                    $(allowed_frame_change_handling: [$($allowed_frame:ident),* $(,)?],)? // TODO delete this!
77                    cortical_area_properties: {
78                        $($cortical_sub_unit_index:tt => ($io_cortical_area_configuration_flag_expr:expr, relative_position: [$rel_x:expr, $rel_y:expr, $rel_z:expr], channel_dimensions_default: [$dim_default_x:expr, $dim_default_y:expr, $dim_default_z:expr], channel_dimensions_min: [$dim_min_x:expr, $dim_min_y:expr, $dim_min_z:expr], channel_dimensions_max: [$dim_max_x:expr, $dim_max_y:expr, $dim_max_z:expr])),* $(,)?
79                    }
80                }
81            ),* $(,)?
82        }
83    ) => {
84        #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
85        pub enum SensoryCorticalUnit {
86            $(
87                $(#[doc = $doc])?
88                $variant_name,
89            )*
90        }
91
92        impl SensoryCorticalUnit {
93            $(
94                paste::paste! {
95                    #[doc = "Get cortical area types array for " $friendly_name " using individual parameters."]
96                    pub const fn [<get_cortical_area_types_array_for_ $variant_name:snake _with_parameters >](
97                        $($param_name: $param_type),*) -> [CorticalAreaType; $number_cortical_areas] {
98                        [
99                            $(CorticalAreaType::BrainInput($io_cortical_area_configuration_flag_expr)),*
100                        ]
101                    }
102
103                    #[doc = "Get cortical IDs array for " $friendly_name " using individual parameters."]
104                    pub const fn [<get_cortical_ids_array_for_ $variant_name:snake _with_parameters >](
105                        $($param_name: $param_type,)* cortical_unit_index: CorticalUnitIndex) -> [CorticalID; $number_cortical_areas] {
106                        let cortical_unit_identifier: [u8; 3] = $cortical_id_unit_reference;
107                        [
108                            $(
109                                $io_cortical_area_configuration_flag_expr .as_io_cortical_id(true, cortical_unit_identifier, cortical_unit_index, CorticalSubUnitIndex::from($cortical_sub_unit_index))
110                            ),*
111                        ]
112                    }
113                }
114            )*
115
116            pub const fn get_snake_case_name(&self) -> &'static str {
117                match self {
118                    $(
119                        SensoryCorticalUnit::$variant_name => paste::paste!{ stringify!([<$variant_name:snake>]) },
120                    )*
121                }
122            }
123
124            /// Parse a sensory cortical unit from its snake_case name
125            ///
126            /// # Arguments
127            /// * `name` - The snake_case name (e.g., "simple_vision", "raw_i_m_u")
128            ///
129            /// # Returns
130            /// * `Some(SensoryCorticalUnit)` - If name matches a known type
131            /// * `None` - If name is not recognized
132            pub fn from_snake_case_name(name: &str) -> Option<SensoryCorticalUnit> {
133                match name {
134                    $(
135                        paste::paste!{ stringify!([<$variant_name:snake>]) } => Some(SensoryCorticalUnit::$variant_name),
136                    )*
137                    _ => None,
138                }
139            }
140
141            /// Returns all available sensory cortical unit types.
142            /// This is useful for enumerating all possible sensor types in the system.
143            pub const fn list_all() -> &'static [SensoryCorticalUnit] {
144                &[
145                    $(
146                        SensoryCorticalUnit::$variant_name,
147                    )*
148                ]
149            }
150
151            /// Returns the friendly (human-readable) name for this sensory cortical unit type.
152            pub const fn get_friendly_name(&self) -> &'static str {
153                match self {
154                    $(
155                        SensoryCorticalUnit::$variant_name => $friendly_name,
156                    )*
157                }
158            }
159
160            /// Returns the 3-byte cortical ID unit reference for this type. // TODO delete me!
161            pub const fn get_cortical_id_unit_reference(&self) -> [u8; 3] {
162                match self {
163                    $(
164                        SensoryCorticalUnit::$variant_name => $cortical_id_unit_reference,
165                    )*
166                }
167            }
168
169            /// Returns the number of cortical areas this type creates.
170            pub const fn get_number_cortical_areas(&self) -> usize {
171                match self {
172                    $(
173                        SensoryCorticalUnit::$variant_name => $number_cortical_areas,
174                    )*
175                }
176            }
177
178            /// Returns the template-defined default firing threshold for auto-created areas.
179            /// `None` means no template override is defined.
180            pub const fn get_default_firing_threshold(&self) -> Option<f64> {
181                match self {
182                    $(
183                        SensoryCorticalUnit::$variant_name => {
184                            default_firing_threshold_impl!($($default_firing_threshold)?)
185                        }
186                    )*
187                }
188            }
189
190            /// Returns template-defined default per-axis firing threshold increment [x, y, z].
191            /// `None` means no template override is defined.
192            pub const fn get_default_firing_threshold_increment(&self) -> Option<[f64; 3]> {
193                match self {
194                    $(
195                        SensoryCorticalUnit::$variant_name => {
196                            default_firing_threshold_increment_impl!(
197                                $([$default_firing_threshold_increment_x, $default_firing_threshold_increment_y, $default_firing_threshold_increment_z])?
198                            )
199                        }
200                    )*
201                }
202            }
203
204            /// Returns the template-defined default mp_charge_accumulation for auto-created areas.
205            /// `None` means no template override is defined.
206            pub const fn get_default_mp_charge_accumulation(&self) -> Option<bool> {
207                match self {
208                    $(
209                        SensoryCorticalUnit::$variant_name => {
210                            default_mp_charge_accumulation_impl!($($default_mp_charge_accumulation)?)
211                        }
212                    )*
213                }
214            }
215
216            /// Returns the template-defined sensor-cadence hold default for this unit.
217            /// `None` means the wrapped-IO-type rule of
218            /// [`SensoryCorticalUnit::get_default_sensory_hold_last_frame`] applies.
219            pub const fn get_template_sensory_hold_last_frame(&self) -> Option<bool> {
220                match self {
221                    $(
222                        SensoryCorticalUnit::$variant_name => {
223                            default_sensory_hold_last_frame_impl!($($default_sensory_hold_last_frame)?)
224                        }
225                    )*
226                }
227            }
228
229            /// Returns the accepted wrapped IO data type name for this sensory unit type.
230            pub const fn get_accepted_wrapped_io_data_type(&self) -> &'static str { // TODO delete me!
231                match self {
232                    $(
233                        SensoryCorticalUnit::$variant_name => stringify!($accepted_wrapped_io_data_type),
234                    )*
235                }
236            }
237
238            /// Returns the default topology for all units of this cortical type.
239            pub fn get_unit_default_topology(&self) -> HashMap<CorticalSubUnitIndex, UnitTopology> {
240                match self {
241                    $(
242                        SensoryCorticalUnit::$variant_name => {
243                            let mut topology = HashMap::new();
244                            $(
245                                topology.insert(
246                                    CorticalSubUnitIndex::from($cortical_sub_unit_index),
247                                    UnitTopology {
248                                        relative_position: [$rel_x, $rel_y, $rel_z],
249                                        channel_dimensions_default: [$dim_default_x, $dim_default_y, $dim_default_z],
250                                        channel_dimensions_min: [$dim_min_x, $dim_min_y, $dim_min_z],
251                                        channel_dimensions_max: [$dim_max_x, $dim_max_y, $dim_max_z],
252                                    }
253                                );
254                            )*
255                            topology
256                        }
257                    )*
258                }
259            }
260
261
262            /// Returns the allowed frame change handling modes from the template, if restricted.
263            /// If None is returned, all frame change handling modes are allowed.
264            /// If Some is returned, only the specified modes are valid.
265            pub fn get_allowed_frame_change_handling(&self) -> Option<&'static [FrameChangeHandling]> { // TODO delete me!
266                match self {
267                    $(
268                        SensoryCorticalUnit::$variant_name => {
269                            $crate::get_allowed_frame_change_handling_impl!($($($allowed_frame),*)?)
270                        }
271                    )*
272                }
273            }
274
275            pub fn get_cortical_id_vector_from_index_and_serde_io_configuration_flags(&self, cortical_unit_index: CorticalUnitIndex, map: Map<String, Value>) -> Result<Vec<CorticalID>, crate::FeagiDataError> {
276                match self {
277                    $(
278                        SensoryCorticalUnit::$variant_name => {
279                            paste::paste! {
280                                let array = SensoryCorticalUnit::[<get_cortical_ids_array_for_ $variant_name:snake _with_parameters >](
281                                    $($param_type::try_from_serde_map(&map)?,)*
282                                    cortical_unit_index);
283                                return Ok(array.to_vec());
284                            }
285                        }
286                    )*
287                }
288            }
289
290
291
292        }
293
294        impl Display for SensoryCorticalUnit {
295            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
296                match self {
297                    $(
298                        SensoryCorticalUnit::$variant_name => write!(f, $friendly_name),
299                    )*
300                }
301    }
302        }
303    };
304
305}
306// Generate the SensoryCorticalUnit enum and all helper methods from the template
307sensor_cortical_units!(define_sensory_cortical_units_enum);
308
309/// Cortical area property key for the per-IPU sensor-cadence hold option.
310///
311/// When `true`, the burst engine re-injects the previous burst's frame for this input
312/// area on a burst where no new frame arrived (exactly one extra burst). Areas without
313/// the property use [`SensoryCorticalUnit::get_default_sensory_hold_last_frame`].
314pub const SENSORY_HOLD_LAST_FRAME_PROPERTY: &str = "sensory_hold_last_frame";
315
316/// Effective `sensory_hold_last_frame` for a cortical area: the explicit property when
317/// present and boolean, otherwise the template default of its sensory unit. Non-input
318/// areas and unknown input units resolve to `false`.
319pub fn resolve_sensory_hold_last_frame(
320    cortical_id: &CorticalID,
321    properties: &HashMap<String, Value>,
322) -> bool {
323    if let Some(value) = properties
324        .get(SENSORY_HOLD_LAST_FRAME_PROPERTY)
325        .and_then(Value::as_bool)
326    {
327        return value;
328    }
329    SensoryCorticalUnit::try_from_cortical_id(cortical_id)
330        .map(|unit| unit.get_default_sensory_hold_last_frame())
331        .unwrap_or(false)
332}
333
334impl SensoryCorticalUnit {
335    /// Template default for the `sensory_hold_last_frame` cortical area property:
336    /// `true` for scalar sensor units (IMU, encoder, proximity, battery, analog, ...) and for the
337    /// miscellaneous scalar-array input (`MiscData`); `false` for frame-based (vision, audio),
338    /// boolean/digital and event-like inputs (text tokens share the `MiscData` wire type but
339    /// must not repeat). A unit's own template value wins; otherwise it is derived from the
340    /// unit's accepted wrapped IO data type.
341    pub fn get_default_sensory_hold_last_frame(&self) -> bool {
342        if let Some(declared) = self.get_template_sensory_hold_last_frame() {
343            return declared;
344        }
345        matches!(
346            self.get_accepted_wrapped_io_data_type(),
347            "Percentage" | "Percentage_3D" | "SignedPercentage_4D" | "RawIMU"
348        )
349    }
350
351    /// Resolve the sensory unit of an input (`i...`) cortical ID from its 3-byte unit reference.
352    /// Returns `None` for non-input IDs and unknown unit references.
353    pub fn try_from_cortical_id(cortical_id: &CorticalID) -> Option<SensoryCorticalUnit> {
354        let bytes = cortical_id.as_bytes();
355        if bytes[0] != b'i' {
356            return None;
357        }
358        let unit_ref = [bytes[1], bytes[2], bytes[3]];
359        Self::list_all()
360            .iter()
361            .copied()
362            .find(|unit| unit.get_cortical_id_unit_reference() == unit_ref)
363    }
364
365    /// Check if a legacy 3-char subtype matches a supported IPU type and return its default CorticalID.
366    /// Used by genome migrator to avoid converting unsupported legacy IPU areas to MiscData.
367    pub fn try_from_legacy_subtype(subtype: &str) -> Option<CorticalID> {
368        let subtype_bytes = subtype.as_bytes();
369        if subtype_bytes.len() != 3 {
370            return None;
371        }
372        let subtype_arr = [subtype_bytes[0], subtype_bytes[1], subtype_bytes[2]];
373        for unit in Self::list_all() {
374            if unit.get_cortical_id_unit_reference() == subtype_arr {
375                return Some(unit.get_default_cortical_id_for_group(CorticalUnitIndex::from(0u16)));
376            }
377        }
378        None
379    }
380
381    /// Get the default CorticalID for this unit with group index 0 (Absolute frame handling, Linear positioning).
382    pub fn get_default_cortical_id_for_group(&self, group_index: CorticalUnitIndex) -> CorticalID {
383        use crate::genomic::cortical_area::io_cortical_area_configuration_flag::{
384            FrameChangeHandling, PercentageNeuronPositioning,
385        };
386        let fh = FrameChangeHandling::Absolute;
387        let pos = PercentageNeuronPositioning::Linear;
388        match self {
389            SensoryCorticalUnit::Infrared => {
390                Self::get_cortical_ids_array_for_infrared_with_parameters(fh, pos, group_index)[0]
391            }
392            SensoryCorticalUnit::Proximity => {
393                Self::get_cortical_ids_array_for_proximity_with_parameters(fh, pos, group_index)[0]
394            }
395            SensoryCorticalUnit::Shock => {
396                Self::get_cortical_ids_array_for_shock_with_parameters(fh, pos, group_index)[0]
397            }
398            SensoryCorticalUnit::Battery => {
399                Self::get_cortical_ids_array_for_battery_with_parameters(fh, pos, group_index)[0]
400            }
401            SensoryCorticalUnit::Servo => {
402                Self::get_cortical_ids_array_for_servo_with_parameters(fh, pos, group_index)[0]
403            }
404            SensoryCorticalUnit::AnalogGPIO => {
405                Self::get_cortical_ids_array_for_analog_g_p_i_o_with_parameters(
406                    fh,
407                    pos,
408                    group_index,
409                )[0]
410            }
411            SensoryCorticalUnit::DigitalGPIO => {
412                Self::get_cortical_ids_array_for_digital_g_p_i_o_with_parameters(group_index)[0]
413            }
414            SensoryCorticalUnit::MiscData => {
415                Self::get_cortical_ids_array_for_misc_data_with_parameters(fh, group_index)[0]
416            }
417            SensoryCorticalUnit::TextEnglishInput => {
418                Self::get_cortical_ids_array_for_text_english_input_with_parameters(fh, group_index)
419                    [0]
420            }
421            SensoryCorticalUnit::CountInput => {
422                Self::get_cortical_ids_array_for_count_input_with_parameters(fh, pos, group_index)
423                    [0]
424            }
425            SensoryCorticalUnit::Vision => {
426                Self::get_cortical_ids_array_for_vision_with_parameters(fh, group_index)[0]
427            }
428            SensoryCorticalUnit::DepthMap => {
429                Self::get_cortical_ids_array_for_depth_map_with_parameters(fh, group_index)[0]
430            }
431            SensoryCorticalUnit::ObjectSegmentationInput => {
432                Self::get_cortical_ids_array_for_object_segmentation_input_with_parameters(
433                    fh,
434                    group_index,
435                )[0]
436            }
437            SensoryCorticalUnit::SegmentedVision => {
438                Self::get_cortical_ids_array_for_segmented_vision_with_parameters(fh, group_index)
439                    [0]
440            }
441            SensoryCorticalUnit::RawIMU => {
442                // Default group index returns the accelerometer (sub-area 0) as
443                // the canonical "primary" cortical id of a Raw IMU unit.
444                Self::get_cortical_ids_array_for_raw_i_m_u_with_parameters(fh, pos, group_index)[0]
445            }
446            SensoryCorticalUnit::SmartIMU => {
447                Self::get_cortical_ids_array_for_smart_i_m_u_with_parameters(fh, pos, group_index)
448                    [0]
449            }
450            SensoryCorticalUnit::CartesianPosition => {
451                Self::get_cortical_ids_array_for_cartesian_position_with_parameters(
452                    fh,
453                    pos,
454                    group_index,
455                )[0]
456            }
457            SensoryCorticalUnit::AudioInput => {
458                Self::get_cortical_ids_array_for_audio_input_with_parameters(fh, group_index)[0]
459            }
460        }
461    }
462}
463
464#[cfg(test)]
465mod sensory_hold_default_tests {
466    use super::*;
467
468    #[test]
469    fn scalar_units_hold_by_default_frame_units_do_not() {
470        for unit in [
471            SensoryCorticalUnit::RawIMU,
472            SensoryCorticalUnit::SmartIMU,
473            SensoryCorticalUnit::Servo,
474            SensoryCorticalUnit::Proximity,
475            SensoryCorticalUnit::Battery,
476            SensoryCorticalUnit::AnalogGPIO,
477            SensoryCorticalUnit::CartesianPosition,
478            SensoryCorticalUnit::MiscData,
479        ] {
480            assert!(unit.get_default_sensory_hold_last_frame(), "{unit:?}");
481        }
482        for unit in [
483            SensoryCorticalUnit::Vision,
484            SensoryCorticalUnit::SegmentedVision,
485            SensoryCorticalUnit::AudioInput,
486            SensoryCorticalUnit::DigitalGPIO,
487            SensoryCorticalUnit::TextEnglishInput,
488            SensoryCorticalUnit::DepthMap,
489        ] {
490            assert!(!unit.get_default_sensory_hold_last_frame(), "{unit:?}");
491        }
492    }
493
494    #[test]
495    fn resolve_uses_property_then_template_default() {
496        let imu = SensoryCorticalUnit::RawIMU
497            .get_default_cortical_id_for_group(CorticalUnitIndex::from(0u16));
498        let vision = SensoryCorticalUnit::Vision
499            .get_default_cortical_id_for_group(CorticalUnitIndex::from(0u16));
500        assert_eq!(
501            SensoryCorticalUnit::try_from_cortical_id(&imu),
502            Some(SensoryCorticalUnit::RawIMU)
503        );
504        let empty = HashMap::new();
505        assert!(resolve_sensory_hold_last_frame(&imu, &empty));
506        assert!(!resolve_sensory_hold_last_frame(&vision, &empty));
507        let off = HashMap::from([(
508            SENSORY_HOLD_LAST_FRAME_PROPERTY.to_string(),
509            Value::Bool(false),
510        )]);
511        assert!(!resolve_sensory_hold_last_frame(&imu, &off));
512        let on = HashMap::from([(
513            SENSORY_HOLD_LAST_FRAME_PROPERTY.to_string(),
514            Value::Bool(true),
515        )]);
516        assert!(resolve_sensory_hold_last_frame(&vision, &on));
517        let custom = CorticalID::try_from_bytes(b"cHOLD001").unwrap();
518        assert!(!resolve_sensory_hold_last_frame(&custom, &empty));
519    }
520}