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