1use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26pub const CLASSIFIER_SAMPLE_MORPHOLOGY: &str = "classifier_sample";
28pub const CLASSIFIER_CLASS_MORPHOLOGY: &str = "classifier_class";
30pub const CLASSIFIER_ASSOCIATIVE_MORPHOLOGY: &str = "associative_memory";
32
33pub fn morphology_is_episodic_input(morphology_id: &str) -> bool {
39 matches!(
40 morphology_id,
41 "episodic_memory" | CLASSIFIER_SAMPLE_MORPHOLOGY | CLASSIFIER_CLASS_MORPHOLOGY
42 )
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ClassifierMappingEffect {
48 pub applied: bool,
49 pub removed_output_id: Option<String>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ClassifierMapping {
55 pub src_area_id: String,
56 pub dst_area_id: String,
57 pub morphology_id: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct ClassifierClassBinding {
66 pub class_area_id: String,
67 pub class_output_area_id: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72pub struct Classifier {
73 pub classifier_id: String,
75 pub name: String,
76 pub parent_region_id: String,
82 pub coordinates_3d: [i32; 3],
83 #[serde(default)]
85 pub sample_area_ids: Vec<String>,
86 #[serde(default)]
88 pub class_bindings: Vec<ClassifierClassBinding>,
89 pub sample_memory_id: String,
91 pub class_memory_id: String,
93 pub error_area_id: String,
96 #[serde(default)]
98 pub reward_training: bool,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub answer_feedback_area_id: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub pain_area_id: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub pleasure_area_id: Option<String>,
108 #[serde(default)]
110 pub answer_latency_bursts: u32,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub learn_area_id: Option<String>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub confidence_area_id: Option<String>,
117 #[serde(default)]
118 pub properties: HashMap<String, serde_json::Value>,
119}
120
121impl Classifier {
122 pub fn assembly_core_ids(&self) -> Vec<String> {
124 vec![self.sample_memory_id.clone(), self.class_memory_id.clone()]
125 }
126
127 pub fn owned_area_ids(&self) -> Vec<String> {
129 let mut owned = self.assembly_core_ids();
130 if !self.error_area_id.is_empty() {
131 owned.push(self.error_area_id.clone());
132 }
133 for binding in &self.class_bindings {
134 if !binding.class_output_area_id.is_empty() {
135 owned.push(binding.class_output_area_id.clone());
136 }
137 }
138 if let Some(pain_area_id) = &self.pain_area_id {
139 if !pain_area_id.is_empty() {
140 owned.push(pain_area_id.clone());
141 }
142 }
143 if let Some(pleasure_area_id) = &self.pleasure_area_id {
144 if !pleasure_area_id.is_empty() {
145 owned.push(pleasure_area_id.clone());
146 }
147 }
148 owned
149 }
150
151 pub fn input_area_ids(&self) -> Vec<String> {
153 let mut inputs = self.sample_area_ids.clone();
154 for binding in &self.class_bindings {
155 inputs.push(binding.class_area_id.clone());
156 }
157 inputs
158 }
159
160 pub fn owns_assembly_core(&self, area_id: &str) -> bool {
161 self.sample_memory_id == area_id || self.class_memory_id == area_id
162 }
163
164 pub fn owns_area(&self, area_id: &str) -> bool {
166 self.owns_assembly_core(area_id)
167 || self.error_area_id == area_id
168 || self.binding_for_output(area_id).is_some()
169 }
170
171 pub fn binding_for_class(&self, class_area_id: &str) -> Option<&ClassifierClassBinding> {
172 self.class_bindings
173 .iter()
174 .find(|binding| binding.class_area_id == class_area_id)
175 }
176
177 pub fn binding_for_output(&self, output_area_id: &str) -> Option<&ClassifierClassBinding> {
178 self.class_bindings
179 .iter()
180 .find(|binding| binding.class_output_area_id == output_area_id)
181 }
182
183 pub fn references_input(&self, area_id: &str) -> bool {
185 self.sample_area_ids.iter().any(|id| id == area_id)
186 || self.binding_for_class(area_id).is_some()
187 }
188
189 pub fn apply_metadata_update(
191 &mut self,
192 name: Option<String>,
193 coordinates_3d: Option<[i32; 3]>,
194 ) -> Result<(), String> {
195 self.apply_assembly_update(name, coordinates_3d, None)
196 }
197
198 pub fn apply_assembly_update(
200 &mut self,
201 name: Option<String>,
202 coordinates_3d: Option<[i32; 3]>,
203 parent_region_id: Option<String>,
204 ) -> Result<(), String> {
205 if let Some(name) = name {
206 let trimmed = name.trim();
207 if trimmed.is_empty() {
208 return Err("Classifier name cannot be blank".to_string());
209 }
210 self.name = trimmed.to_string();
211 }
212 if let Some(coordinates_3d) = coordinates_3d {
213 self.coordinates_3d = coordinates_3d;
214 }
215 if let Some(parent_region_id) = parent_region_id {
216 let trimmed = parent_region_id.trim();
217 if trimmed.is_empty() {
218 return Err("parent_region_id cannot be blank".to_string());
219 }
220 self.parent_region_id = trimmed.to_string();
221 }
222 Ok(())
223 }
224
225 pub fn required_mappings(&self) -> Vec<ClassifierMapping> {
227 let mut mappings = Vec::new();
228 for sample_area_id in &self.sample_area_ids {
229 mappings.push(ClassifierMapping {
230 src_area_id: sample_area_id.clone(),
231 dst_area_id: self.sample_memory_id.clone(),
232 morphology_id: CLASSIFIER_SAMPLE_MORPHOLOGY.to_string(),
233 });
234 }
235 for binding in &self.class_bindings {
236 mappings.push(ClassifierMapping {
237 src_area_id: binding.class_area_id.clone(),
238 dst_area_id: self.class_memory_id.clone(),
239 morphology_id: CLASSIFIER_CLASS_MORPHOLOGY.to_string(),
240 });
241 }
242 mappings.push(ClassifierMapping {
243 src_area_id: self.sample_memory_id.clone(),
244 dst_area_id: self.class_memory_id.clone(),
245 morphology_id: CLASSIFIER_ASSOCIATIVE_MORPHOLOGY.to_string(),
246 });
247 mappings
248 }
249
250 pub fn clear_input(&mut self, area_id: &str) -> Option<String> {
254 self.sample_area_ids.retain(|id| id != area_id);
255 if self.answer_feedback_area_id.as_deref() == Some(area_id) {
256 self.answer_feedback_area_id = None;
257 }
258 if self.learn_area_id.as_deref() == Some(area_id) {
259 self.learn_area_id = None;
260 }
261 if self.confidence_area_id.as_deref() == Some(area_id) {
262 self.confidence_area_id = None;
263 }
264 self.detach_class(area_id)
265 }
266
267 pub fn attach_sample(&mut self, sample_area_id: String) -> Result<(), String> {
269 let sample_area_id = required_area_id(sample_area_id, "sample_area_id")?;
270 if !self.sample_area_ids.iter().any(|id| id == &sample_area_id) {
271 self.sample_area_ids.push(sample_area_id);
272 }
273 Ok(())
274 }
275
276 pub fn detach_sample(&mut self, sample_area_id: &str) -> bool {
278 let before = self.sample_area_ids.len();
279 self.sample_area_ids.retain(|id| id != sample_area_id);
280 self.sample_area_ids.len() != before
281 }
282
283 pub fn attach_class(
285 &mut self,
286 class_area_id: String,
287 class_output_area_id: String,
288 ) -> Result<(), String> {
289 let class_area_id = required_area_id(class_area_id, "class_area_id")?;
290 if self.binding_for_class(&class_area_id).is_some() {
291 return Err(format!(
292 "class_area_id {class_area_id} is already mapped to this classifier"
293 ));
294 }
295 self.class_bindings.push(ClassifierClassBinding {
296 class_area_id,
297 class_output_area_id,
298 });
299 Ok(())
300 }
301
302 pub fn set_class_output(
304 &mut self,
305 class_area_id: &str,
306 class_output_area_id: String,
307 ) -> Result<(), String> {
308 let class_output_area_id = required_area_id(class_output_area_id, "class_output_area_id")?;
309 let binding = self
310 .class_bindings
311 .iter_mut()
312 .find(|binding| binding.class_area_id == class_area_id)
313 .ok_or_else(|| format!("class_area_id {class_area_id} is not mapped"))?;
314 binding.class_output_area_id = class_output_area_id;
315 Ok(())
316 }
317
318 pub fn detach_class(&mut self, class_area_id: &str) -> Option<String> {
320 let position = self
321 .class_bindings
322 .iter()
323 .position(|binding| binding.class_area_id == class_area_id)?;
324 let removed = self.class_bindings.remove(position);
325 if removed.class_output_area_id.is_empty() {
326 None
327 } else {
328 Some(removed.class_output_area_id)
329 }
330 }
331
332 pub fn apply_mapping_change(
337 &mut self,
338 src_area_id: &str,
339 dst_area_id: &str,
340 morphology_id: &str,
341 removed: bool,
342 ) -> Result<ClassifierMappingEffect, String> {
343 if dst_area_id == self.sample_memory_id && morphology_id == CLASSIFIER_SAMPLE_MORPHOLOGY {
344 if removed {
345 self.detach_sample(src_area_id);
346 } else {
347 self.attach_sample(src_area_id.to_string())?;
348 }
349 return Ok(ClassifierMappingEffect {
350 applied: true,
351 removed_output_id: None,
352 });
353 }
354 if dst_area_id == self.class_memory_id && morphology_id == CLASSIFIER_CLASS_MORPHOLOGY {
355 if removed {
356 return Ok(ClassifierMappingEffect {
357 applied: true,
358 removed_output_id: self.detach_class(src_area_id),
359 });
360 }
361 if self.binding_for_class(src_area_id).is_none() {
362 self.attach_class(src_area_id.to_string(), String::new())?;
363 }
364 return Ok(ClassifierMappingEffect {
365 applied: true,
366 removed_output_id: None,
367 });
368 }
369 Ok(ClassifierMappingEffect {
370 applied: false,
371 removed_output_id: None,
372 })
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct ClassPairFiring {
379 pub input_coords: Vec<(u32, u32, u32)>,
380 pub output_coords: Vec<(u32, u32, u32)>,
381}
382
383pub fn class_output_reports_error(pair: &ClassPairFiring) -> bool {
390 if pair.output_coords.len() > 1 {
391 return true;
392 }
393 if pair.input_coords.is_empty() {
394 return false;
395 }
396 let mut input = pair.input_coords.clone();
397 let mut output = pair.output_coords.clone();
398 input.sort_unstable();
399 output.sort_unstable();
400 input.dedup();
401 output.dedup();
402 input != output
403}
404
405pub fn classifier_reports_error(pairs: &[ClassPairFiring]) -> bool {
407 pairs.iter().any(class_output_reports_error)
408}
409
410pub fn class_output_area_name(classifier_name: &str, class_area_name: &str) -> String {
412 format!("{classifier_name} {class_area_name} class output")
413}
414
415pub fn classifier_mapping_rule(morphology_id: &str, associative_window: u32) -> serde_json::Value {
420 let is_associative = morphology_id == CLASSIFIER_ASSOCIATIVE_MORPHOLOGY;
421 let plasticity_value = if is_associative { 1 } else { 0 };
422 serde_json::json!({
423 "morphology_id": morphology_id,
424 "morphology_scalar": [1, 1, 1],
425 "postSynapticCurrent_multiplier": 1,
426 "plasticity_flag": is_associative,
427 "plasticity_constant": plasticity_value,
428 "ltp_multiplier": plasticity_value,
429 "ltd_multiplier": plasticity_value,
430 "plasticity_window": if is_associative { associative_window } else { 0 },
431 })
432}
433
434fn required_area_id(area_id: String, field: &str) -> Result<String, String> {
435 let trimmed = area_id.trim();
436 if trimmed.is_empty() {
437 return Err(format!("{field} cannot be blank"));
438 }
439 Ok(trimmed.to_string())
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 fn sample() -> Classifier {
447 let mut classifier = Classifier {
448 classifier_id: "clf-1".to_string(),
449 name: "demo".to_string(),
450 parent_region_id: "region".to_string(),
451 coordinates_3d: [1, 2, 3],
452 sample_area_ids: vec!["sample-a".to_string()],
453 class_bindings: Vec::new(),
454 sample_memory_id: "smem".to_string(),
455 class_memory_id: "cmem".to_string(),
456 error_area_id: "err".to_string(),
457 reward_training: false,
458 answer_feedback_area_id: None,
459 pain_area_id: None,
460 pleasure_area_id: None,
461 answer_latency_bursts: 0,
462 learn_area_id: None,
463 confidence_area_id: None,
464 properties: HashMap::new(),
465 };
466 classifier
467 .attach_class("class-a".to_string(), "out-a".to_string())
468 .expect("class");
469 classifier
470 }
471
472 #[test]
473 fn required_mappings_cover_each_sample_class_and_the_associative_edge() {
474 let mut classifier = sample();
475 classifier
476 .attach_sample("sample-b".to_string())
477 .expect("second sample");
478 classifier
479 .attach_class("class-b".to_string(), "out-b".to_string())
480 .expect("second class");
481 let mappings = classifier.required_mappings();
482 assert_eq!(mappings.len(), 5);
483 assert!(mappings.iter().any(|mapping| {
484 mapping.src_area_id == "sample-b"
485 && mapping.dst_area_id == "smem"
486 && mapping.morphology_id == CLASSIFIER_SAMPLE_MORPHOLOGY
487 }));
488 assert!(mappings.iter().any(|mapping| {
489 mapping.src_area_id == "class-b"
490 && mapping.dst_area_id == "cmem"
491 && mapping.morphology_id == CLASSIFIER_CLASS_MORPHOLOGY
492 }));
493 assert!(mappings
494 .iter()
495 .any(|mapping| { mapping.morphology_id == CLASSIFIER_ASSOCIATIVE_MORPHOLOGY }));
496 }
497
498 #[test]
499 fn removing_one_class_keeps_the_other_output() {
500 let mut classifier = sample();
501 classifier
502 .attach_class("class-b".to_string(), "out-b".to_string())
503 .expect("second class");
504 assert_eq!(
505 classifier.detach_class("class-a"),
506 Some("out-a".to_string())
507 );
508 assert!(classifier.binding_for_class("class-a").is_none());
509 assert_eq!(
510 classifier
511 .binding_for_class("class-b")
512 .map(|binding| binding.class_output_area_id.as_str()),
513 Some("out-b")
514 );
515 assert!(classifier.owns_area("out-b"));
516 assert!(!classifier.owns_area("out-a"));
517 }
518
519 #[test]
520 fn duplicate_class_mapping_is_rejected() {
521 let mut classifier = sample();
522 let result = classifier.attach_class("class-a".to_string(), "other".to_string());
523 assert!(result.is_err());
524 assert_eq!(classifier.class_bindings.len(), 1);
525 }
526
527 #[test]
528 fn combo_class_with_one_neuron_per_area_is_not_an_error() {
529 let pairs = vec![
530 ClassPairFiring {
531 input_coords: vec![(0, 0, 1)],
532 output_coords: vec![(0, 0, 1)],
533 },
534 ClassPairFiring {
535 input_coords: vec![(0, 0, 0)],
536 output_coords: vec![(0, 0, 0)],
537 },
538 ClassPairFiring {
539 input_coords: vec![(0, 0, 3)],
540 output_coords: vec![(0, 0, 3)],
541 },
542 ClassPairFiring {
543 input_coords: vec![(0, 0, 2)],
544 output_coords: vec![(0, 0, 2)],
545 },
546 ];
547 assert!(!classifier_reports_error(&pairs));
548 }
549
550 #[test]
551 fn two_neurons_in_one_class_output_is_an_error() {
552 let pairs = vec![ClassPairFiring {
553 input_coords: vec![(0, 0, 1)],
554 output_coords: vec![(0, 0, 1), (0, 0, 4)],
555 }];
556 assert!(classifier_reports_error(&pairs));
557 }
558
559 #[test]
560 fn mismatch_is_per_class_area_and_silent_input_is_not_mismatch() {
561 assert!(class_output_reports_error(&ClassPairFiring {
562 input_coords: vec![(0, 0, 1)],
563 output_coords: vec![(0, 0, 2)],
564 }));
565 assert!(!class_output_reports_error(&ClassPairFiring {
566 input_coords: Vec::new(),
567 output_coords: vec![(0, 0, 2)],
568 }));
569 }
570}