feagi_structures/genomic/classifiers/
mod.rs1use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20pub const CLASSIFIER_KERNEL_MORPHOLOGY: &str = "episodic_memory";
22pub const CLASSIFIER_CLASS_MORPHOLOGY: &str = "episodic_memory";
24pub const CLASSIFIER_ASSOCIATIVE_MORPHOLOGY: &str = "associative_memory";
26pub const CLASSIFIER_SCAN_MORPHOLOGY: &str = "episodic_scan";
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ClassifierMapping {
32 pub src_area_id: String,
33 pub dst_area_id: String,
34 pub morphology_id: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct ClassifierField {
40 pub field_area_id: String,
41 pub scan_twin_id: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46pub struct Classifier {
47 pub classifier_id: String,
49 pub name: String,
50 pub parent_region_id: String,
52 pub coordinates_3d: [i32; 3],
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub kernel_area_id: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub class_area_id: Option<String>,
58 #[serde(default)]
60 pub fields: Vec<ClassifierField>,
61 pub kernel_memory_id: String,
63 pub class_memory_id: String,
64 #[serde(default)]
65 pub properties: HashMap<String, serde_json::Value>,
66}
67
68impl Classifier {
69 pub fn assembly_core_ids(&self) -> Vec<String> {
71 vec![self.kernel_memory_id.clone(), self.class_memory_id.clone()]
72 }
73
74 pub fn owned_area_ids(&self) -> Vec<String> {
76 let mut owned = self.assembly_core_ids();
77 for field in &self.fields {
78 if !field.scan_twin_id.is_empty() {
79 owned.push(field.scan_twin_id.clone());
80 }
81 }
82 owned
83 }
84
85 pub fn input_area_ids(&self) -> Vec<String> {
87 let mut inputs = Vec::new();
88 if let Some(kernel) = &self.kernel_area_id {
89 inputs.push(kernel.clone());
90 }
91 if let Some(class) = &self.class_area_id {
92 inputs.push(class.clone());
93 }
94 for field in &self.fields {
95 inputs.push(field.field_area_id.clone());
96 }
97 inputs
98 }
99
100 pub fn owns_assembly_core(&self, area_id: &str) -> bool {
101 self.kernel_memory_id == area_id || self.class_memory_id == area_id
102 }
103
104 pub fn owns_area(&self, area_id: &str) -> bool {
106 self.owns_assembly_core(area_id) || self.field_for_twin(area_id).is_some()
107 }
108
109 pub fn field_for_twin(&self, twin_id: &str) -> Option<&ClassifierField> {
110 self.fields
111 .iter()
112 .find(|field| field.scan_twin_id == twin_id)
113 }
114
115 pub fn binding_for_field(&self, field_area_id: &str) -> Option<&ClassifierField> {
116 self.fields
117 .iter()
118 .find(|field| field.field_area_id == field_area_id)
119 }
120
121 pub fn references_input(&self, area_id: &str) -> bool {
123 self.kernel_area_id.as_deref() == Some(area_id)
124 || self.class_area_id.as_deref() == Some(area_id)
125 || self.binding_for_field(area_id).is_some()
126 }
127
128 pub fn apply_assembly_update(
130 &mut self,
131 name: Option<String>,
132 coordinates_3d: Option<[i32; 3]>,
133 parent_region_id: Option<String>,
134 kernel_area_id: Option<String>,
135 class_area_id: Option<String>,
136 ) -> Result<(), String> {
137 if let Some(name) = name {
138 let trimmed = name.trim();
139 if trimmed.is_empty() {
140 return Err("Classifier name cannot be blank".to_string());
141 }
142 self.name = trimmed.to_string();
143 }
144 if let Some(coordinates_3d) = coordinates_3d {
145 self.coordinates_3d = coordinates_3d;
146 }
147 if let Some(parent_region_id) = parent_region_id {
148 let trimmed = parent_region_id.trim();
149 if trimmed.is_empty() {
150 return Err("parent_region_id cannot be blank".to_string());
151 }
152 self.parent_region_id = trimmed.to_string();
153 }
154 if let Some(kernel_area_id) = kernel_area_id {
155 self.kernel_area_id = Some(required_area_id(kernel_area_id, "kernel_area_id")?);
156 }
157 if let Some(class_area_id) = class_area_id {
158 self.class_area_id = Some(required_area_id(class_area_id, "class_area_id")?);
159 }
160 Ok(())
161 }
162
163 pub fn apply_metadata_update(
165 &mut self,
166 name: Option<String>,
167 coordinates_3d: Option<[i32; 3]>,
168 ) -> Result<(), String> {
169 self.apply_assembly_update(name, coordinates_3d, None, None, None)
170 }
171
172 pub fn required_mappings(&self) -> Vec<ClassifierMapping> {
174 let mut mappings = Vec::new();
175 if let Some(kernel) = &self.kernel_area_id {
176 mappings.push(ClassifierMapping {
177 src_area_id: kernel.clone(),
178 dst_area_id: self.kernel_memory_id.clone(),
179 morphology_id: CLASSIFIER_KERNEL_MORPHOLOGY.to_string(),
180 });
181 }
182 if let Some(class) = &self.class_area_id {
183 mappings.push(ClassifierMapping {
184 src_area_id: class.clone(),
185 dst_area_id: self.class_memory_id.clone(),
186 morphology_id: CLASSIFIER_CLASS_MORPHOLOGY.to_string(),
187 });
188 }
189 mappings.push(ClassifierMapping {
190 src_area_id: self.kernel_memory_id.clone(),
191 dst_area_id: self.class_memory_id.clone(),
192 morphology_id: CLASSIFIER_ASSOCIATIVE_MORPHOLOGY.to_string(),
193 });
194 for field in &self.fields {
195 mappings.push(ClassifierMapping {
196 src_area_id: field.field_area_id.clone(),
197 dst_area_id: self.kernel_memory_id.clone(),
198 morphology_id: CLASSIFIER_SCAN_MORPHOLOGY.to_string(),
199 });
200 }
201 mappings
202 }
203
204 pub fn clear_input(&mut self, area_id: &str) {
206 if self.kernel_area_id.as_deref() == Some(area_id) {
207 self.kernel_area_id = None;
208 }
209 if self.class_area_id.as_deref() == Some(area_id) {
210 self.class_area_id = None;
211 }
212 self.fields.retain(|field| field.field_area_id != area_id);
213 }
214
215 pub fn detach_field(&mut self, field_area_id: &str) -> Option<String> {
217 let position = self
218 .fields
219 .iter()
220 .position(|field| field.field_area_id == field_area_id)?;
221 Some(self.fields.remove(position).scan_twin_id)
222 }
223
224 pub fn detach_twin(&mut self, twin_id: &str) -> Option<String> {
226 let position = self
227 .fields
228 .iter()
229 .position(|field| field.scan_twin_id == twin_id)?;
230 Some(self.fields.remove(position).field_area_id)
231 }
232
233 pub fn attach_field(
234 &mut self,
235 field_area_id: String,
236 scan_twin_id: String,
237 ) -> Result<(), String> {
238 let field_area_id = required_area_id(field_area_id, "field_area_id")?;
239 let scan_twin_id = required_area_id(scan_twin_id, "scan_twin_id")?;
240 if self.binding_for_field(&field_area_id).is_some() {
241 return Err(format!(
242 "field_area_id {field_area_id} is already mapped to this classifier"
243 ));
244 }
245 self.fields.push(ClassifierField {
246 field_area_id,
247 scan_twin_id,
248 });
249 Ok(())
250 }
251
252 pub fn apply_mapping_change(
254 &mut self,
255 src_area_id: &str,
256 dst_area_id: &str,
257 morphology_id: &str,
258 removed: bool,
259 ) -> bool {
260 if dst_area_id == self.kernel_memory_id && morphology_id == CLASSIFIER_KERNEL_MORPHOLOGY {
261 self.kernel_area_id = if removed {
262 None
263 } else {
264 Some(src_area_id.to_string())
265 };
266 return true;
267 }
268 if dst_area_id == self.class_memory_id && morphology_id == CLASSIFIER_CLASS_MORPHOLOGY {
269 self.class_area_id = if removed {
270 None
271 } else {
272 Some(src_area_id.to_string())
273 };
274 return true;
275 }
276 false
277 }
278}
279
280fn required_area_id(area_id: String, field: &str) -> Result<String, String> {
281 let trimmed = area_id.trim();
282 if trimmed.is_empty() {
283 return Err(format!("{field} cannot be blank"));
284 }
285 Ok(trimmed.to_string())
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn sample() -> Classifier {
293 let mut classifier = Classifier {
294 classifier_id: "clf-1".to_string(),
295 name: "demo".to_string(),
296 parent_region_id: "region".to_string(),
297 coordinates_3d: [1, 2, 3],
298 kernel_area_id: Some("kernel".to_string()),
299 class_area_id: Some("class".to_string()),
300 fields: Vec::new(),
301 kernel_memory_id: "kmem".to_string(),
302 class_memory_id: "cmem".to_string(),
303 properties: HashMap::new(),
304 };
305 classifier
306 .attach_field("field".to_string(), "twin".to_string())
307 .expect("first field");
308 classifier
309 }
310
311 #[test]
312 fn required_mappings_cover_shared_edges_and_each_field() {
313 let mut classifier = sample();
314 classifier
315 .attach_field("field-b".to_string(), "twin-b".to_string())
316 .expect("second field");
317 let mappings = classifier.required_mappings();
318 assert_eq!(mappings.len(), 5);
319 assert!(mappings.iter().any(|m| {
320 m.src_area_id == "field"
321 && m.dst_area_id == "kmem"
322 && m.morphology_id == CLASSIFIER_SCAN_MORPHOLOGY
323 }));
324 assert!(mappings.iter().any(|m| m.src_area_id == "field-b"));
325 }
326
327 #[test]
328 fn deleting_one_field_keeps_the_other_eye() {
329 let mut classifier = sample();
330 classifier
331 .attach_field("field-b".to_string(), "twin-b".to_string())
332 .expect("second field");
333 assert_eq!(classifier.detach_field("field"), Some("twin".to_string()));
334 assert!(classifier.binding_for_field("field").is_none());
335 assert_eq!(
336 classifier
337 .binding_for_field("field-b")
338 .map(|f| f.scan_twin_id.as_str()),
339 Some("twin-b")
340 );
341 assert!(classifier.owns_assembly_core("kmem"));
342 assert!(!classifier.owns_area("twin"));
343 assert!(classifier.owns_area("twin-b"));
344 }
345
346 #[test]
347 fn duplicate_field_mapping_is_rejected() {
348 let mut classifier = sample();
349 let result = classifier.attach_field("field".to_string(), "other-twin".to_string());
350 assert!(result.is_err());
351 assert_eq!(classifier.fields.len(), 1);
352 }
353
354 #[test]
355 fn metadata_update_renames_without_touching_areas() {
356 let mut classifier = sample();
357 classifier
358 .apply_metadata_update(Some(" renamed ".to_string()), Some([9, 8, 7]))
359 .expect("valid metadata");
360 assert_eq!(classifier.name, "renamed");
361 assert_eq!(classifier.coordinates_3d, [9, 8, 7]);
362 assert_eq!(classifier.kernel_memory_id, "kmem");
363 assert_eq!(classifier.fields[0].scan_twin_id, "twin");
364 assert_eq!(classifier.kernel_area_id.as_deref(), Some("kernel"));
365 }
366
367 #[test]
368 fn assembly_update_retargets_kernel_and_class_only() {
369 let mut classifier = sample();
370 classifier
371 .apply_assembly_update(
372 None,
373 None,
374 Some("other-region".to_string()),
375 Some("kernel2".to_string()),
376 Some("class2".to_string()),
377 )
378 .expect("valid assembly update");
379 assert_eq!(classifier.parent_region_id, "other-region");
380 assert_eq!(classifier.kernel_area_id.as_deref(), Some("kernel2"));
381 assert_eq!(classifier.class_area_id.as_deref(), Some("class2"));
382 assert_eq!(classifier.fields[0].field_area_id, "field");
383 }
384}