1use crate::{MorphologyParameters, RuntimeGenome};
15#[allow(unused_imports)]
17use feagi_structures::genomic::cortical_area::CorticalID;
18use serde_json::Value;
19use std::collections::HashSet;
20use std::str::FromStr;
21
22#[derive(Debug, Clone)]
24pub struct ValidationResult {
25 pub valid: bool,
27 pub errors: Vec<String>,
29 pub warnings: Vec<String>,
31}
32
33impl ValidationResult {
34 pub fn new() -> Self {
36 Self {
37 valid: true,
38 errors: Vec::new(),
39 warnings: Vec::new(),
40 }
41 }
42
43 pub fn add_error(&mut self, error: String) {
45 self.valid = false;
46 self.errors.push(error);
47 }
48
49 pub fn add_warning(&mut self, warning: String) {
51 self.warnings.push(warning);
52 }
53
54 pub fn merge(&mut self, other: ValidationResult) {
56 if !other.valid {
57 self.valid = false;
58 }
59 self.errors.extend(other.errors);
60 self.warnings.extend(other.warnings);
61 }
62}
63
64impl Default for ValidationResult {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70pub fn validate_genome(genome: &RuntimeGenome) -> ValidationResult {
72 let mut result = ValidationResult::new();
73
74 validate_metadata(genome, &mut result);
76
77 validate_cortical_areas(genome, &mut result);
79
80 validate_morphologies(genome, &mut result);
82
83 validate_physiology(genome, &mut result);
85
86 cross_validate(genome, &mut result);
88
89 result
90}
91
92pub fn auto_fix_genome(genome: &mut RuntimeGenome) -> usize {
103 use tracing::info;
104
105 let mut fixes_applied = 0;
106
107 if genome.physiology.simulation_timestep <= 0.0 {
109 let default_timestep = crate::runtime::PhysiologyConfig::default().simulation_timestep;
110 info!(
111 "🔧 AUTO-FIX: Invalid simulation_timestep {} → {} (default)",
112 genome.physiology.simulation_timestep, default_timestep
113 );
114 genome.physiology.simulation_timestep = default_timestep;
115 fixes_applied += 1;
116 }
117
118 if genome.physiology.max_age == 0 {
119 let default_age = crate::runtime::PhysiologyConfig::default().max_age;
120 info!("🔧 AUTO-FIX: max_age 0 → {} (default)", default_age);
121 genome.physiology.max_age = default_age;
122 fixes_applied += 1;
123 }
124
125 if genome.physiology.quantization_precision.is_empty() {
127 let default_precision = crate::runtime::default_quantization_precision();
128 info!(
129 "🔧 AUTO-FIX: Missing quantization_precision → '{}' (default)",
130 default_precision
131 );
132 genome.physiology.quantization_precision = default_precision;
133 fixes_applied += 1;
134 } else {
135 use feagi_npu_neural::types::Precision;
137 match Precision::from_str(&genome.physiology.quantization_precision) {
138 Ok(precision) => {
139 let canonical = precision.as_str().to_string();
140 if genome.physiology.quantization_precision != canonical {
141 info!(
142 "🔧 AUTO-FIX: Quantization precision '{}' → '{}' (normalized)",
143 genome.physiology.quantization_precision, canonical
144 );
145 genome.physiology.quantization_precision = canonical;
146 fixes_applied += 1;
147 }
148 }
149 Err(_) => {
150 let default_precision = crate::runtime::default_quantization_precision();
152 info!(
153 "🔧 AUTO-FIX: Invalid quantization_precision '{}' → '{}' (default)",
154 genome.physiology.quantization_precision, default_precision
155 );
156 genome.physiology.quantization_precision = default_precision;
157 fixes_applied += 1;
158 }
159 }
160 }
161
162 for (cortical_id, area) in &mut genome.cortical_areas {
163 let cortical_id_display = cortical_id.to_string();
164 if area.dimensions.width == 0 {
166 info!(
167 "🔧 AUTO-FIX: Cortical area '{}' width 0 → 1",
168 cortical_id_display
169 );
170 area.dimensions.width = 1;
171 fixes_applied += 1;
172 }
173 if area.dimensions.height == 0 {
174 info!(
175 "🔧 AUTO-FIX: Cortical area '{}' height 0 → 1",
176 cortical_id_display
177 );
178 area.dimensions.height = 1;
179 fixes_applied += 1;
180 }
181 if area.dimensions.depth == 0 {
182 info!(
183 "🔧 AUTO-FIX: Cortical area '{}' depth 0 → 1",
184 cortical_id_display
185 );
186 area.dimensions.depth = 1;
187 fixes_applied += 1;
188 }
189
190 let neurons_per_voxel = area
192 .properties
193 .get("neurons_per_voxel")
194 .and_then(|v| v.as_u64())
195 .unwrap_or(0) as u32;
196 if neurons_per_voxel == 0 {
197 info!(
198 "🔧 AUTO-FIX: Cortical area '{}' neurons_per_voxel 0 → 1",
199 cortical_id_display
200 );
201 area.properties
202 .insert("neurons_per_voxel".to_string(), serde_json::json!(1));
203 fixes_applied += 1;
204 }
205 }
206
207 if fixes_applied > 0 {
208 info!(
209 "🔧 AUTO-FIX: Applied {} automatic corrections to genome",
210 fixes_applied
211 );
212 }
213
214 fixes_applied
215}
216
217fn validate_metadata(genome: &RuntimeGenome, result: &mut ValidationResult) {
219 if genome.metadata.genome_id.is_empty() {
220 result.add_error("Genome ID is empty".to_string());
221 }
222
223 if genome.metadata.version.is_empty() {
224 result.add_error("Genome version is empty".to_string());
225 }
226
227 if genome.metadata.version != "2.0" {
228 result.add_warning(format!(
229 "Genome version '{}' may not be fully supported (expected '2.0')",
230 genome.metadata.version
231 ));
232 }
233}
234
235fn validate_cortical_areas(genome: &RuntimeGenome, result: &mut ValidationResult) {
237 if genome.cortical_areas.is_empty() {
238 result.add_warning("Genome has no cortical areas defined".to_string());
239 return;
240 }
241
242 for (cortical_id, area) in &genome.cortical_areas {
243 let cortical_id_display = cortical_id.to_string();
244
245 validate_cortical_id_format(cortical_id, &cortical_id_display, result);
247
248 if area.dimensions.width == 0 || area.dimensions.height == 0 || area.dimensions.depth == 0 {
250 result.add_warning(format!(
251 "AUTO-FIX: Cortical area '{}' has zero dimension(s): {}x{}x{} - will be corrected to minimum (1,1,1)",
252 cortical_id_display, area.dimensions.width, area.dimensions.height, area.dimensions.depth
253 ));
254 }
256
257 let neurons_per_voxel = area
259 .properties
260 .get("neurons_per_voxel")
261 .and_then(|v| v.as_u64())
262 .unwrap_or(0) as u32;
263 if neurons_per_voxel == 0 {
264 result.add_warning(format!(
265 "AUTO-FIX: Cortical area '{}' has neurons_per_voxel=0 - will be corrected to 1",
266 cortical_id_display
267 ));
268 }
269
270 let total_voxels = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
272 if total_voxels > 1_000_000 {
273 result.add_warning(format!(
274 "Cortical area '{}' has very large dimensions: {} total voxels",
275 cortical_id_display, total_voxels
276 ));
277 }
278
279 if area.name.is_empty() {
281 result.add_warning(format!(
282 "Cortical area '{}' has empty name",
283 cortical_id_display
284 ));
285 }
286
287 validate_memory_mp_encoding(&area.properties, &cortical_id_display, result);
288 }
289}
290
291fn validate_memory_mp_encoding(
296 properties: &std::collections::HashMap<String, Value>,
297 cortical_id_display: &str,
298 result: &mut ValidationResult,
299) {
300 let Some(mem_props) = crate::extract_memory_properties(properties) else {
301 return;
302 };
303 if let Err(e) = crate::validate_memory_mp_properties(properties) {
304 result.add_error(format!(
305 "Memory area '{}' has invalid MP encoding: {}",
306 cortical_id_display, e
307 ));
308 return;
309 }
310 if mem_props.mp_change_mode != crate::MpChangeMode::None && mem_props.temporal_depth < 2 {
311 result.add_warning(format!(
312 "Memory area '{}' uses {}='{}' with temporal_depth={}; change encoding needs temporal_depth >= 2 and will be disabled",
313 cortical_id_display,
314 crate::MP_CHANGE_MODE_KEY,
315 mem_props.mp_change_mode.as_str(),
316 mem_props.temporal_depth
317 ));
318 }
319}
320
321fn validate_cortical_id_format(
323 _cortical_id: &CorticalID,
324 display: &str,
325 result: &mut ValidationResult,
326) {
327 if display.len() != 8 && display.len() != 12 {
331 result.add_error(format!(
332 "Invalid cortical ID length: '{}' is {} characters (must be 8 or 12)",
333 display,
334 display.len()
335 ));
336 return;
337 }
338
339 if display.starts_with('_') {
341 validate_core_area_id(display, result);
342 return;
343 }
344
345 if display.starts_with('c') {
347 if !display.chars().all(|c| c.is_alphanumeric() || c == '_') {
350 result.add_warning(format!(
351 "Custom cortical ID '{}' contains non-alphanumeric characters",
352 display
353 ));
354 }
355 return;
356 }
357
358 validate_io_area_id(display, result);
360}
361
362fn validate_core_area_id(display: &str, result: &mut ValidationResult) {
364 use feagi_structures::genomic::cortical_area::CoreCorticalType;
365
366 let valid_core_ids: Vec<String> = vec![
368 CoreCorticalType::Power.to_cortical_id().to_string(), CoreCorticalType::Death.to_cortical_id().to_string(), CoreCorticalType::Fatigue.to_cortical_id().to_string(), CoreCorticalType::Pain.to_cortical_id().to_string(), CoreCorticalType::Pleasure.to_cortical_id().to_string(), CoreCorticalType::Fear.to_cortical_id().to_string(), CoreCorticalType::Hope.to_cortical_id().to_string(), ];
376
377 if !valid_core_ids.contains(&display.to_string()) {
378 result.add_error(format!(
379 "Invalid CORE cortical ID: '{}' - must be one of: {:?}",
380 display, valid_core_ids
381 ));
382 }
383}
384
385fn validate_io_area_id(display: &str, result: &mut ValidationResult) {
387 let first_char = display.chars().next().unwrap_or('_');
391 let unit_prefix = &display[1..4]; const VALID_IPU_PREFIXES: &[&str] = &[
395 "svi", "aud", "tac", "olf", "vis", "dpt", ];
402
403 const VALID_OPU_PREFIXES: &[&str] = &[
405 "mot", "voc", "gaz", "pse", "mis", ];
411
412 let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
413 let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
414
415 if !is_valid_ipu && !is_valid_opu {
416 if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
418 {
419 result.add_error(format!(
420 "INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
421 Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
422 Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
423 Valid IPU units: {:?}, Valid OPU units: {:?}. \
424 This genome needs migration to the new format.",
425 display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
426 ));
427 } else {
428 result.add_warning(format!(
429 "Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
430 Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
431 display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
432 ));
433 }
434 return;
435 }
436
437 let suffix = &display[4..];
439
440 if first_char == 'i' && unit_prefix == "svi" {
442 if let Some(index_char) = display.chars().nth(4) {
443 if index_char.is_ascii_digit() {
444 let digit = index_char as u8 - b'0';
445 if digit > 8 {
446 result.add_error(format!(
447 "Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
448 digit, display
449 ));
450 }
451 }
452 }
453 }
454
455 if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
457 result.add_warning(format!(
458 "Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
459 display
460 ));
461 }
462}
463
464fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
466 if genome.morphologies.count() == 0 {
467 result.add_warning("Genome has no morphologies defined".to_string());
468 return;
469 }
470
471 let required_core = vec!["block_to_block", "projector"];
473 for morph_id in required_core {
474 if !genome.morphologies.contains(morph_id) {
475 result.add_warning(format!(
476 "Missing recommended core morphology: '{}'",
477 morph_id
478 ));
479 }
480 }
481
482 for (morphology_id, morphology) in genome.morphologies.iter() {
483 validate_single_morphology(morphology_id, morphology, result);
484 }
485}
486
487fn validate_single_morphology(
489 morphology_id: &str,
490 morphology: &crate::Morphology,
491 result: &mut ValidationResult,
492) {
493 match &morphology.parameters {
494 MorphologyParameters::Vectors { vectors } => {
495 if vectors.is_empty() {
496 result.add_error(format!(
497 "Morphology '{}' (vectors) has no vectors defined",
498 morphology_id
499 ));
500 }
501
502 for (i, vec) in vectors.iter().enumerate() {
504 if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
505 result.add_warning(format!(
506 "Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
507 morphology_id, i, vec[0], vec[1], vec[2]
508 ));
509 }
510 }
511 }
512
513 MorphologyParameters::Patterns { patterns } => {
514 if patterns.is_empty() {
515 result.add_error(format!(
516 "Morphology '{}' (patterns) has no patterns defined",
517 morphology_id
518 ));
519 }
520
521 for (i, pattern) in patterns.iter().enumerate() {
522 if pattern[0].len() != 3 || pattern[1].len() != 3 {
523 result.add_error(format!(
524 "Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
525 morphology_id, i
526 ));
527 }
528 }
529 }
530
531 MorphologyParameters::Functions {} => {
532 }
534
535 MorphologyParameters::Composite {
536 src_seed,
537 src_pattern,
538 mapper_morphology,
539 } => {
540 if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
542 result.add_warning(format!(
543 "Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
544 morphology_id, src_seed[0], src_seed[1], src_seed[2]
545 ));
546 }
547
548 if src_pattern.is_empty() {
550 result.add_error(format!(
551 "Morphology '{}' (composite) has empty src_pattern",
552 morphology_id
553 ));
554 }
555
556 if mapper_morphology.is_empty() {
558 result.add_error(format!(
559 "Morphology '{}' (composite) has empty mapper_morphology reference",
560 morphology_id
561 ));
562 }
563 }
564 }
565}
566
567fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
569 let phys = &genome.physiology;
570
571 if phys.simulation_timestep <= 0.0 {
572 result.add_error(format!(
573 "Invalid simulation_timestep: {} (must be > 0.0)",
574 phys.simulation_timestep
575 ));
576 }
577
578 if phys.simulation_timestep > 1.0 {
579 result.add_warning(format!(
580 "Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
581 phys.simulation_timestep
582 ));
583 }
584
585 if phys.max_age == 0 {
586 result.add_warning("max_age is 0 (neurons will never age)".to_string());
587 }
588
589 if phys.plasticity_queue_depth == 0 {
590 result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
591 }
592
593 validate_quantization_precision(&phys.quantization_precision, result);
595}
596
597fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
599 use feagi_npu_neural::types::Precision;
600
601 match Precision::from_str(precision) {
603 Ok(parsed_precision) => {
604 if precision != parsed_precision.as_str() {
606 result.add_warning(format!(
607 "Quantization precision '{}' normalized to '{}'",
608 precision,
609 parsed_precision.as_str()
610 ));
611 }
612 }
613 Err(_) => {
614 result.add_error(format!(
615 "Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
616 precision
617 ));
618 }
619 }
620}
621
622fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
624 let morphology_ids: HashSet<String> =
626 genome.morphologies.morphology_ids().into_iter().collect();
627
628 for (cortical_id, area) in &genome.cortical_areas {
630 let cortical_id_display = cortical_id.to_string();
631 if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
632 for (dest_area, rules) in dstmap {
633 if let Ok(dest_cortical_id) =
635 crate::genome::parser::string_to_cortical_id(dest_area)
636 {
637 if !genome.cortical_areas.contains_key(&dest_cortical_id) {
638 result.add_error(format!(
639 "Cortical area '{}' references non-existent destination area '{}' in dstmap",
640 cortical_id_display, dest_area
641 ));
642 }
643 } else {
644 result.add_error(format!(
645 "Cortical area '{}' has invalid destination area ID '{}' in dstmap",
646 cortical_id_display, dest_area
647 ));
648 }
649
650 if let Value::Array(rules_array) = rules {
652 for rule in rules_array {
653 if let Value::Array(rule_array) = rule {
654 if let Some(Value::String(morph_id)) = rule_array.first() {
655 if !morphology_ids.contains(morph_id) {
656 result.add_error(format!(
657 "Cortical area '{}' references undefined morphology '{}' in dstmap rule",
658 cortical_id_display, morph_id
659 ));
660 }
661 }
662 }
663 }
664 }
665 }
666 }
667 }
668
669 for (region_id, region) in &genome.brain_regions {
671 for cortical_id in ®ion.cortical_areas {
673 if !genome.cortical_areas.contains_key(cortical_id) {
674 result.add_error(format!(
675 "Brain region '{}' references non-existent cortical area '{}'",
676 region_id, cortical_id
677 ));
678 }
679 }
680 }
681
682 for (classifier_id, classifier) in &genome.classifiers {
683 if classifier.name.trim().is_empty() {
684 result.add_error(format!("Classifier '{}' has an empty name", classifier_id));
685 }
686 if !genome
687 .brain_regions
688 .contains_key(&classifier.parent_region_id)
689 {
690 result.add_error(format!(
691 "Classifier '{}' references unknown parent_region_id '{}'",
692 classifier_id, classifier.parent_region_id
693 ));
694 }
695 for area_id in classifier.owned_area_ids() {
696 if crate::genome::parser::string_to_cortical_id(&area_id)
697 .ok()
698 .and_then(|id| genome.cortical_areas.get(&id).map(|_| ()))
699 .is_none()
700 {
701 result.add_error(format!(
702 "Classifier '{}' references missing owned area '{}'",
703 classifier_id, area_id
704 ));
705 }
706 }
707 for area_id in classifier.input_area_ids() {
708 if crate::genome::parser::string_to_cortical_id(&area_id)
709 .ok()
710 .and_then(|id| genome.cortical_areas.get(&id).map(|_| ()))
711 .is_none()
712 {
713 result.add_error(format!(
714 "Classifier '{}' references missing input area '{}'",
715 classifier_id, area_id
716 ));
717 }
718 }
719 }
720
721 for (morphology_id, morphology) in genome.morphologies.iter() {
723 if let MorphologyParameters::Composite {
724 mapper_morphology, ..
725 } = &morphology.parameters
726 {
727 if !morphology_ids.contains(mapper_morphology) {
728 result.add_error(format!(
729 "Composite morphology '{}' references undefined mapper morphology '{}'",
730 morphology_id, mapper_morphology
731 ));
732 }
733 }
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::{
741 GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
742 };
743 use std::collections::HashMap;
744
745 #[test]
746 fn test_validate_empty_genome() {
747 let genome = RuntimeGenome {
748 metadata: GenomeMetadata {
749 genome_id: "test".to_string(),
750 genome_title: "Test".to_string(),
751 genome_description: "".to_string(),
752 version: "2.0".to_string(),
753 timestamp: 0.0,
754 brain_regions_root: None,
755 },
756 cortical_areas: HashMap::new(),
757 brain_regions: HashMap::new(),
758 classifiers: HashMap::new(),
759 morphologies: MorphologyRegistry::new(),
760 physiology: PhysiologyConfig::default(),
761 signatures: GenomeSignatures {
762 genome: "0".to_string(),
763 blueprint: "0".to_string(),
764 physiology: "0".to_string(),
765 morphologies: None,
766 },
767 stats: GenomeStats::default(),
768 };
769
770 let result = validate_genome(&genome);
771
772 assert!(!result.warnings.is_empty());
774 println!("Warnings: {:?}", result.warnings);
775 }
776
777 #[test]
778 fn test_validate_valid_genome() {
779 let mut genome = RuntimeGenome {
780 metadata: GenomeMetadata {
781 genome_id: "test_genome".to_string(),
782 genome_title: "Test Genome".to_string(),
783 genome_description: "Valid test genome".to_string(),
784 version: "2.0".to_string(),
785 timestamp: 1234567890.0,
786 brain_regions_root: None,
787 },
788 cortical_areas: HashMap::new(),
789 brain_regions: HashMap::new(),
790 classifiers: HashMap::new(),
791 morphologies: MorphologyRegistry::new(),
792 physiology: PhysiologyConfig::default(),
793 signatures: GenomeSignatures {
794 genome: "abc123".to_string(),
795 blueprint: "def456".to_string(),
796 physiology: "ghi789".to_string(),
797 morphologies: None,
798 },
799 stats: GenomeStats::default(),
800 };
801
802 use feagi_structures::genomic::cortical_area::CustomCorticalType;
804 use feagi_structures::genomic::cortical_area::{
805 CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
806 };
807 let test_id = CoreCorticalType::Power.to_cortical_id();
808 let area = CorticalArea::new(
809 test_id,
810 0,
811 "Test Area".to_string(),
812 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
813 (0, 0, 0).into(),
814 CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
815 )
816 .expect("Failed to create cortical area");
817
818 genome.cortical_areas.insert(test_id, area);
819
820 let result = validate_genome(&genome);
821
822 println!("Errors: {:?}", result.errors);
824 println!("Warnings: {:?}", result.warnings);
825
826 assert!(result.errors.is_empty());
828 assert!(!result.warnings.is_empty()); }
830
831 #[test]
832 fn test_validate_quantization_precision() {
833 let mut genome = create_minimal_genome();
834
835 genome.physiology.quantization_precision = "fp32".to_string();
837 let result = validate_genome(&genome);
838 assert!(result.errors.is_empty(), "fp32 should be valid");
839
840 genome.physiology.quantization_precision = "int8".to_string();
842 let result = validate_genome(&genome);
843 assert!(result.errors.is_empty(), "int8 should be valid");
844
845 genome.physiology.quantization_precision = "i8".to_string();
847 let result = validate_genome(&genome);
848 assert!(result.errors.is_empty(), "i8 should be valid");
849 assert!(
850 result.warnings.iter().any(|w| w.contains("normalized")),
851 "Should warn about normalization"
852 );
853
854 genome.physiology.quantization_precision = "invalid".to_string();
856 let result = validate_genome(&genome);
857 assert!(!result.errors.is_empty(), "invalid should produce error");
858 assert!(
859 result
860 .errors
861 .iter()
862 .any(|e| e.contains("Invalid quantization_precision")),
863 "Should have quantization error"
864 );
865 }
866
867 #[test]
868 fn test_auto_fix_quantization_precision() {
869 let mut genome = create_minimal_genome();
871 genome.physiology.quantization_precision = "".to_string();
872
873 let fixes = auto_fix_genome(&mut genome);
874 assert!(fixes > 0, "Should apply at least one fix");
875 assert_eq!(
876 genome.physiology.quantization_precision, "int8",
877 "Should default to int8"
878 );
879
880 genome.physiology.quantization_precision = "i8".to_string();
882 let _fixes = auto_fix_genome(&mut genome);
883 assert_eq!(
884 genome.physiology.quantization_precision, "int8",
885 "Should normalize i8 to int8"
886 );
887
888 genome.physiology.quantization_precision = "invalid".to_string();
890 let _fixes = auto_fix_genome(&mut genome);
891 assert_eq!(
892 genome.physiology.quantization_precision, "int8",
893 "Invalid should default to int8"
894 );
895 }
896
897 #[test]
898 fn test_validate_classifier_requires_parent_region() {
899 let mut genome = create_minimal_genome();
900 genome.classifiers.insert(
901 "clf-1".to_string(),
902 feagi_structures::genomic::classifiers::Classifier {
903 classifier_id: "clf-1".to_string(),
904 name: "demo".to_string(),
905 parent_region_id: "missing".to_string(),
906 coordinates_3d: [0, 0, 0],
907 training_mode:
908 feagi_structures::genomic::classifiers::ClassifierTrainingMode::Kernel,
909 kernel_area_id: None,
910 class_area_id: None,
911 mask_area_id: None,
912 class_count: None,
913 kernel_size: None,
914 fields: vec![feagi_structures::genomic::classifiers::ClassifierField {
915 field_area_id: "cfield".to_string(),
916 scan_twin_id: "cscan1".to_string(),
917 }],
918 kernel_memory_id: "mkmem1".to_string(),
919 class_memory_id: "mcmem1".to_string(),
920 reward_training: false,
921 answer_feedback_area_id: None,
922 pain_area_id: None,
923 pleasure_area_id: None,
924 answer_latency_bursts: 0,
925 learn_area_id: None,
926 confidence_area_id: None,
927 properties: HashMap::new(),
928 },
929 );
930 let result = validate_genome(&genome);
931 assert!(result
932 .errors
933 .iter()
934 .any(|error| error.contains("unknown parent_region_id")));
935 assert!(result
936 .errors
937 .iter()
938 .any(|error| error.contains("missing owned area")));
939 }
940
941 fn create_minimal_genome() -> RuntimeGenome {
942 RuntimeGenome {
943 metadata: GenomeMetadata {
944 genome_id: "test".to_string(),
945 genome_title: "Test".to_string(),
946 genome_description: "".to_string(),
947 version: "2.0".to_string(),
948 timestamp: 0.0,
949 brain_regions_root: None,
950 },
951 cortical_areas: HashMap::new(),
952 brain_regions: HashMap::new(),
953 classifiers: HashMap::new(),
954 morphologies: MorphologyRegistry::new(),
955 physiology: PhysiologyConfig::default(),
956 signatures: GenomeSignatures {
957 genome: "0".to_string(),
958 blueprint: "0".to_string(),
959 physiology: "0".to_string(),
960 morphologies: None,
961 },
962 stats: GenomeStats::default(),
963 }
964 }
965}