Skip to main content

feagi_structures/genomic/cortical_area/
cortical_id.rs

1use crate::genomic::cortical_area::cortical_area_type::{
2    CoreCorticalType, CorticalAreaType, CustomCorticalType, MemoryCorticalType,
3};
4use crate::genomic::cortical_area::io_cortical_area_configuration_flag::IOCorticalAreaConfigurationFlag;
5use crate::FeagiDataError;
6use base64::{engine::general_purpose, Engine as _};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::fmt::Display;
9
10macro_rules! match_bytes_by_cortical_type {
11    ($cortical_id_bytes: expr,
12        custom => $custom:block,
13        memory => $memory:block,
14        core => $core:block,
15
16        brain_input => $brain_input:block,
17        brain_output => $brain_output:block,
18        invalid => $invalid:block,
19    ) => {
20        match $cortical_id_bytes[0] {
21            b'c' => $custom,
22            b'm' => $memory,
23            b'_' => $core,
24            b'i' => $brain_input,
25            b'o' => $brain_output,
26            _ => $invalid,
27        }
28    };
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct CorticalID {
33    pub(crate) bytes: [u8; CorticalID::CORTICAL_ID_LENGTH],
34}
35
36impl CorticalID {
37    pub const CORTICAL_ID_LENGTH: usize = 8; // 8 bytes -> 64 bit
38    pub const CORTICAL_ID_LENGTH_BASE_64: usize = 4 * (Self::CORTICAL_ID_LENGTH + 3); // enforces rounding up
39
40    pub const NUMBER_OF_BYTES: usize = Self::CORTICAL_ID_LENGTH;
41
42    //region Constructors
43
44    pub fn try_from_bytes(
45        bytes: &[u8; CorticalID::CORTICAL_ID_LENGTH],
46    ) -> Result<Self, FeagiDataError> {
47        match_bytes_by_cortical_type!(bytes,
48            custom => {
49                Ok(CorticalID {bytes: *bytes})
50            },
51            memory => {
52                Ok(CorticalID {bytes: *bytes})
53            },
54            core => {
55                Ok(CorticalID {bytes: *bytes})
56            },
57            brain_input => {
58                // TODO more checks
59                Ok(CorticalID {bytes: *bytes})
60            },
61            brain_output => {
62                // TODO more checks
63                Ok(CorticalID {bytes: *bytes})
64            },
65            invalid => {
66                Err(FeagiDataError::DeserializationError("Unable to deserialize cortical ID bytes as any possible type!".into()))
67            },
68        )
69    }
70
71    pub fn try_from_u64(u: u64) -> Result<Self, FeagiDataError> {
72        let bytes = u.to_be_bytes();
73        Self::try_from_bytes(&bytes)
74    }
75
76    pub fn try_from_base_64(str: &str) -> Result<Self, FeagiDataError> {
77        let decoded = general_purpose::STANDARD.decode(str).map_err(|e| {
78            FeagiDataError::DeserializationError(format!(
79                "Failed to decode base64 string to cortical ID: {}",
80                e
81            ))
82        })?;
83
84        if decoded.len() != Self::CORTICAL_ID_LENGTH {
85            return Err(FeagiDataError::DeserializationError(format!(
86                "Invalid base64 cortical ID length: expected {} bytes, got {}",
87                Self::CORTICAL_ID_LENGTH,
88                decoded.len()
89            )));
90        }
91
92        let mut bytes = [0u8; Self::CORTICAL_ID_LENGTH];
93        bytes.copy_from_slice(&decoded);
94        Self::try_from_bytes(&bytes)
95    }
96
97    /// Parse legacy 6-char or 8-char ASCII cortical ID strings.
98    /// Normalizes uppercase first byte (C/M/I/O) to lowercase for compatibility with legacy genomes.
99    /// Any other invalid first byte (e.g. v, 0) is treated as custom cortical area ('c').
100    pub fn try_from_legacy_ascii(id_str: &str) -> Result<Self, FeagiDataError> {
101        let mut bytes = [b'_'; Self::CORTICAL_ID_LENGTH];
102        let len = id_str.len().min(8);
103        bytes[..len].copy_from_slice(&id_str.as_bytes()[..len]);
104        bytes[0] = match bytes[0] {
105            b'C' => b'c',
106            b'M' => b'm',
107            b'I' => b'i',
108            b'O' => b'o',
109            b'c' | b'm' | b'_' | b'i' | b'o' => bytes[0],
110            _ => b'c',
111        };
112        Self::try_from_bytes(&bytes)
113    }
114    //endregion
115
116    //region export
117
118    pub fn write_id_to_bytes(&self, bytes: &mut [u8; Self::NUMBER_OF_BYTES]) {
119        bytes.copy_from_slice(&self.bytes)
120    }
121
122    /// Extract IO data type configuration from cortical ID bytes
123    ///
124    /// Extracts the data type configuration flag from bytes 4-5 (u16, little-endian)
125    /// and converts it to an IOCorticalAreaDataFlag.
126    ///
127    /// This is used for both BrainInput and BrainOutput cortical areas.
128    #[inline]
129    pub fn extract_io_data_flag(&self) -> Result<IOCorticalAreaConfigurationFlag, FeagiDataError> {
130        let data_type_config = u16::from_le_bytes([self.bytes[4], self.bytes[5]]);
131        IOCorticalAreaConfigurationFlag::try_from_data_type_configuration_flag(data_type_config)
132    }
133
134    pub fn as_cortical_type(&self) -> Result<CorticalAreaType, FeagiDataError> {
135        match_bytes_by_cortical_type!(self.bytes,
136            custom => {
137                // NOTE: Only 1 custom type currently
138                Ok(CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire))
139            },
140            memory => {
141                // NOTE: Only 1 memory type currently
142                Ok(CorticalAreaType::Memory(MemoryCorticalType::Memory))
143            },
144            core => {
145                Ok(CorticalAreaType::Core(CoreCorticalType::try_from_cortical_id_bytes_type_unchecked(&self.bytes)?))
146            },
147            brain_input => {
148                Ok(CorticalAreaType::BrainInput(self.extract_io_data_flag()?))
149            },
150            brain_output => {
151                Ok(CorticalAreaType::BrainOutput(self.extract_io_data_flag()?))
152            },
153            invalid => {
154                Err(FeagiDataError::InternalError("Attempted to convert an invalid cortical ID instantiated object to cortical type!".into()))
155            },
156        )
157    }
158
159    pub fn as_bytes(&self) -> &[u8; CorticalID::CORTICAL_ID_LENGTH] {
160        &self.bytes
161    }
162
163    pub fn as_u64(&self) -> u64 {
164        u64::from_be_bytes(self.bytes)
165    }
166
167    pub fn as_base_64(&self) -> String {
168        general_purpose::STANDARD.encode(self.bytes)
169    }
170
171    /// Extract subtype from cortical ID (e.g., "isvi0___" → "svi")
172    /// Returns None for CORE areas or if bytes are invalid UTF-8
173    pub fn extract_subtype(&self) -> Option<String> {
174        // For IPU/OPU areas, bytes 1-3 contain the subtype
175        if self.bytes[0] == b'i' || self.bytes[0] == b'o' {
176            // Extract bytes 1-3, trim trailing underscores/nulls
177            let subtype_bytes = &self.bytes[1..4];
178            String::from_utf8(subtype_bytes.to_vec())
179                .ok()
180                .map(|s| {
181                    s.trim_end_matches('_')
182                        .trim_end_matches('\0')
183                        .to_lowercase()
184                })
185                .filter(|s| !s.is_empty())
186        } else {
187            None
188        }
189    }
190
191    /// Extract unit ID from cortical ID (typically byte 4)
192    /// Returns None for CORE/CUSTOM/MEMORY areas
193    pub fn extract_unit_id(&self) -> Option<u8> {
194        if self.bytes[0] == b'i' || self.bytes[0] == b'o' {
195            // Byte 4 typically contains unit ID (0-9 as ASCII)
196            let byte = self.bytes[4];
197            if byte.is_ascii_digit() {
198                Some(byte - b'0')
199            } else if byte == b'_' || byte == 0 {
200                Some(0)
201            } else {
202                None
203            }
204        } else {
205            None
206        }
207    }
208
209    /// Extract group ID from cortical ID (similar to unit ID, but may be in different byte)
210    /// For now, returns the same as unit_id
211    pub fn extract_group_id(&self) -> Option<u8> {
212        self.extract_unit_id()
213    }
214
215    //endregion
216
217    //region internal
218
219    //endregion
220}
221
222impl Display for CorticalID {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        // Use base64 encoding for display instead of UTF-8 to avoid control characters
225        write!(f, "{}", self.as_base_64())
226    }
227}
228
229// Implement Serialize for CorticalID - uses base64 format for JSON compatibility
230impl Serialize for CorticalID {
231    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
232    where
233        S: Serializer,
234    {
235        // Serialize as base64 string for JSON compatibility
236        serializer.serialize_str(&self.as_base_64())
237    }
238}
239
240// Implement Deserialize for CorticalID - accepts base64 format
241impl<'de> Deserialize<'de> for CorticalID {
242    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243    where
244        D: Deserializer<'de>,
245    {
246        let s = String::deserialize(deserializer)?;
247        CorticalID::try_from_base_64(&s)
248            .map_err(|e| serde::de::Error::custom(format!("Invalid CorticalID: {}", e)))
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::genomic::cortical_area::cortical_area_type::CoreCorticalType;
256
257    #[test]
258    fn test_u64_round_trip() {
259        // Create a cortical ID from a core type
260        let original_id = CoreCorticalType::Power.to_cortical_id();
261
262        // Convert to u64
263        let as_u64 = original_id.as_u64();
264
265        // Convert back from u64
266        let restored_id = CorticalID::try_from_u64(as_u64).unwrap();
267
268        // Verify they're equal
269        assert_eq!(original_id, restored_id);
270        assert_eq!(original_id.as_bytes(), restored_id.as_bytes());
271    }
272
273    #[test]
274    fn test_base64_round_trip() {
275        // Create a cortical ID from a core type
276        let original_id = CoreCorticalType::Death.to_cortical_id();
277
278        // Convert to base64
279        let as_base64 = original_id.as_base_64();
280
281        // Convert back from base64
282        let restored_id = CorticalID::try_from_base_64(&as_base64).unwrap();
283
284        // Verify they're equal
285        assert_eq!(original_id, restored_id);
286        assert_eq!(original_id.as_bytes(), restored_id.as_bytes());
287    }
288
289    #[test]
290    fn test_base64_length() {
291        let id = CoreCorticalType::Power.to_cortical_id();
292        let base64_str = id.as_base_64();
293
294        // Base64 of 8 bytes should be 12 characters (with potential padding)
295        // 8 bytes = 64 bits, base64 uses 6 bits per character
296        // 64 / 6 = 10.67, rounded up to 11, but base64 padding rounds to multiple of 4 = 12
297        assert!(base64_str.len() >= 11 && base64_str.len() <= 12);
298    }
299
300    #[test]
301    fn test_invalid_base64() {
302        // Test with invalid base64 string
303        let result = CorticalID::try_from_base_64("not valid base64!");
304        assert!(result.is_err());
305    }
306
307    #[test]
308    fn test_base64_wrong_length() {
309        // Test with valid base64 but wrong length (only 4 bytes encoded)
310        let short_base64 = general_purpose::STANDARD.encode([1u8, 2, 3, 4]);
311        let result = CorticalID::try_from_base_64(&short_base64);
312        assert!(result.is_err());
313    }
314
315    #[test]
316    fn test_try_from_legacy_ascii_uppercase() {
317        // Legacy genomes may use uppercase C/M/I/O for custom/memory/IPU/OPU
318        let id = CorticalID::try_from_legacy_ascii("C03bbb").unwrap();
319        assert_eq!(id.bytes[0], b'c');
320        assert_eq!(&id.bytes[1..6], b"03bbb");
321    }
322
323    #[test]
324    fn test_try_from_legacy_ascii_invalid_as_custom() {
325        // visioA, visioB, 0_45de etc. treated as custom cortical areas
326        for s in ["visioA", "visioB", "0_45de"] {
327            let id = CorticalID::try_from_legacy_ascii(s).unwrap();
328            assert_eq!(id.bytes[0], b'c');
329        }
330    }
331
332    #[test]
333    fn test_u64_with_various_core_types() {
334        let core_types = [
335            CoreCorticalType::Power,
336            CoreCorticalType::Death,
337            CoreCorticalType::Fatigue,
338            CoreCorticalType::Pain,
339            CoreCorticalType::Pleasure,
340            CoreCorticalType::Fear,
341            CoreCorticalType::Hope,
342        ];
343
344        for core_type in &core_types {
345            let id = core_type.to_cortical_id();
346            let as_u64 = id.as_u64();
347            let restored = CorticalID::try_from_u64(as_u64).unwrap();
348            assert_eq!(id, restored, "Failed round-trip for {:?}", core_type);
349        }
350    }
351}