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}
288
289fn validate_cortical_id_format(
291 _cortical_id: &CorticalID,
292 display: &str,
293 result: &mut ValidationResult,
294) {
295 if display.len() != 8 && display.len() != 12 {
299 result.add_error(format!(
300 "Invalid cortical ID length: '{}' is {} characters (must be 8 or 12)",
301 display,
302 display.len()
303 ));
304 return;
305 }
306
307 if display.starts_with('_') {
309 validate_core_area_id(display, result);
310 return;
311 }
312
313 if display.starts_with('c') {
315 if !display.chars().all(|c| c.is_alphanumeric() || c == '_') {
318 result.add_warning(format!(
319 "Custom cortical ID '{}' contains non-alphanumeric characters",
320 display
321 ));
322 }
323 return;
324 }
325
326 validate_io_area_id(display, result);
328}
329
330fn validate_core_area_id(display: &str, result: &mut ValidationResult) {
332 use feagi_structures::genomic::cortical_area::CoreCorticalType;
333
334 let valid_core_ids: Vec<String> = vec![
336 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(), ];
344
345 if !valid_core_ids.contains(&display.to_string()) {
346 result.add_error(format!(
347 "Invalid CORE cortical ID: '{}' - must be one of: {:?}",
348 display, valid_core_ids
349 ));
350 }
351}
352
353fn validate_io_area_id(display: &str, result: &mut ValidationResult) {
355 let first_char = display.chars().next().unwrap_or('_');
359 let unit_prefix = &display[1..4]; const VALID_IPU_PREFIXES: &[&str] = &[
363 "svi", "aud", "tac", "olf", "vis", ];
369
370 const VALID_OPU_PREFIXES: &[&str] = &[
372 "mot", "voc", "gaz", "pse", "mis", ];
378
379 let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
380 let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
381
382 if !is_valid_ipu && !is_valid_opu {
383 if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
385 {
386 result.add_error(format!(
387 "INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
388 Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
389 Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
390 Valid IPU units: {:?}, Valid OPU units: {:?}. \
391 This genome needs migration to the new format.",
392 display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
393 ));
394 } else {
395 result.add_warning(format!(
396 "Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
397 Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
398 display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
399 ));
400 }
401 return;
402 }
403
404 let suffix = &display[4..];
406
407 if first_char == 'i' && unit_prefix == "svi" {
409 if let Some(index_char) = display.chars().nth(4) {
410 if index_char.is_ascii_digit() {
411 let digit = index_char as u8 - b'0';
412 if digit > 8 {
413 result.add_error(format!(
414 "Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
415 digit, display
416 ));
417 }
418 }
419 }
420 }
421
422 if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
424 result.add_warning(format!(
425 "Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
426 display
427 ));
428 }
429}
430
431fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
433 if genome.morphologies.count() == 0 {
434 result.add_warning("Genome has no morphologies defined".to_string());
435 return;
436 }
437
438 let required_core = vec!["block_to_block", "projector"];
440 for morph_id in required_core {
441 if !genome.morphologies.contains(morph_id) {
442 result.add_warning(format!(
443 "Missing recommended core morphology: '{}'",
444 morph_id
445 ));
446 }
447 }
448
449 for (morphology_id, morphology) in genome.morphologies.iter() {
450 validate_single_morphology(morphology_id, morphology, result);
451 }
452}
453
454fn validate_single_morphology(
456 morphology_id: &str,
457 morphology: &crate::Morphology,
458 result: &mut ValidationResult,
459) {
460 match &morphology.parameters {
461 MorphologyParameters::Vectors { vectors } => {
462 if vectors.is_empty() {
463 result.add_error(format!(
464 "Morphology '{}' (vectors) has no vectors defined",
465 morphology_id
466 ));
467 }
468
469 for (i, vec) in vectors.iter().enumerate() {
471 if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
472 result.add_warning(format!(
473 "Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
474 morphology_id, i, vec[0], vec[1], vec[2]
475 ));
476 }
477 }
478 }
479
480 MorphologyParameters::Patterns { patterns } => {
481 if patterns.is_empty() {
482 result.add_error(format!(
483 "Morphology '{}' (patterns) has no patterns defined",
484 morphology_id
485 ));
486 }
487
488 for (i, pattern) in patterns.iter().enumerate() {
489 if pattern[0].len() != 3 || pattern[1].len() != 3 {
490 result.add_error(format!(
491 "Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
492 morphology_id, i
493 ));
494 }
495 }
496 }
497
498 MorphologyParameters::Functions {} => {
499 }
501
502 MorphologyParameters::Composite {
503 src_seed,
504 src_pattern,
505 mapper_morphology,
506 } => {
507 if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
509 result.add_warning(format!(
510 "Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
511 morphology_id, src_seed[0], src_seed[1], src_seed[2]
512 ));
513 }
514
515 if src_pattern.is_empty() {
517 result.add_error(format!(
518 "Morphology '{}' (composite) has empty src_pattern",
519 morphology_id
520 ));
521 }
522
523 if mapper_morphology.is_empty() {
525 result.add_error(format!(
526 "Morphology '{}' (composite) has empty mapper_morphology reference",
527 morphology_id
528 ));
529 }
530 }
531 }
532}
533
534fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
536 let phys = &genome.physiology;
537
538 if phys.simulation_timestep <= 0.0 {
539 result.add_error(format!(
540 "Invalid simulation_timestep: {} (must be > 0.0)",
541 phys.simulation_timestep
542 ));
543 }
544
545 if phys.simulation_timestep > 1.0 {
546 result.add_warning(format!(
547 "Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
548 phys.simulation_timestep
549 ));
550 }
551
552 if phys.max_age == 0 {
553 result.add_warning("max_age is 0 (neurons will never age)".to_string());
554 }
555
556 if phys.plasticity_queue_depth == 0 {
557 result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
558 }
559
560 validate_quantization_precision(&phys.quantization_precision, result);
562}
563
564fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
566 use feagi_npu_neural::types::Precision;
567
568 match Precision::from_str(precision) {
570 Ok(parsed_precision) => {
571 if precision != parsed_precision.as_str() {
573 result.add_warning(format!(
574 "Quantization precision '{}' normalized to '{}'",
575 precision,
576 parsed_precision.as_str()
577 ));
578 }
579 }
580 Err(_) => {
581 result.add_error(format!(
582 "Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
583 precision
584 ));
585 }
586 }
587}
588
589fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
591 let morphology_ids: HashSet<String> =
593 genome.morphologies.morphology_ids().into_iter().collect();
594
595 for (cortical_id, area) in &genome.cortical_areas {
597 let cortical_id_display = cortical_id.to_string();
598 if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
599 for (dest_area, rules) in dstmap {
600 if let Ok(dest_cortical_id) =
602 crate::genome::parser::string_to_cortical_id(dest_area)
603 {
604 if !genome.cortical_areas.contains_key(&dest_cortical_id) {
605 result.add_error(format!(
606 "Cortical area '{}' references non-existent destination area '{}' in dstmap",
607 cortical_id_display, dest_area
608 ));
609 }
610 } else {
611 result.add_error(format!(
612 "Cortical area '{}' has invalid destination area ID '{}' in dstmap",
613 cortical_id_display, dest_area
614 ));
615 }
616
617 if let Value::Array(rules_array) = rules {
619 for rule in rules_array {
620 if let Value::Array(rule_array) = rule {
621 if let Some(Value::String(morph_id)) = rule_array.first() {
622 if !morphology_ids.contains(morph_id) {
623 result.add_error(format!(
624 "Cortical area '{}' references undefined morphology '{}' in dstmap rule",
625 cortical_id_display, morph_id
626 ));
627 }
628 }
629 }
630 }
631 }
632 }
633 }
634 }
635
636 for (region_id, region) in &genome.brain_regions {
638 for cortical_id in ®ion.cortical_areas {
640 if !genome.cortical_areas.contains_key(cortical_id) {
641 result.add_error(format!(
642 "Brain region '{}' references non-existent cortical area '{}'",
643 region_id, cortical_id
644 ));
645 }
646 }
647 }
648
649 for (morphology_id, morphology) in genome.morphologies.iter() {
651 if let MorphologyParameters::Composite {
652 mapper_morphology, ..
653 } = &morphology.parameters
654 {
655 if !morphology_ids.contains(mapper_morphology) {
656 result.add_error(format!(
657 "Composite morphology '{}' references undefined mapper morphology '{}'",
658 morphology_id, mapper_morphology
659 ));
660 }
661 }
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::{
669 GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
670 };
671 use std::collections::HashMap;
672
673 #[test]
674 fn test_validate_empty_genome() {
675 let genome = RuntimeGenome {
676 metadata: GenomeMetadata {
677 genome_id: "test".to_string(),
678 genome_title: "Test".to_string(),
679 genome_description: "".to_string(),
680 version: "2.0".to_string(),
681 timestamp: 0.0,
682 brain_regions_root: None,
683 },
684 cortical_areas: HashMap::new(),
685 brain_regions: HashMap::new(),
686 morphologies: MorphologyRegistry::new(),
687 physiology: PhysiologyConfig::default(),
688 signatures: GenomeSignatures {
689 genome: "0".to_string(),
690 blueprint: "0".to_string(),
691 physiology: "0".to_string(),
692 morphologies: None,
693 },
694 stats: GenomeStats::default(),
695 };
696
697 let result = validate_genome(&genome);
698
699 assert!(!result.warnings.is_empty());
701 println!("Warnings: {:?}", result.warnings);
702 }
703
704 #[test]
705 fn test_validate_valid_genome() {
706 let mut genome = RuntimeGenome {
707 metadata: GenomeMetadata {
708 genome_id: "test_genome".to_string(),
709 genome_title: "Test Genome".to_string(),
710 genome_description: "Valid test genome".to_string(),
711 version: "2.0".to_string(),
712 timestamp: 1234567890.0,
713 brain_regions_root: None,
714 },
715 cortical_areas: HashMap::new(),
716 brain_regions: HashMap::new(),
717 morphologies: MorphologyRegistry::new(),
718 physiology: PhysiologyConfig::default(),
719 signatures: GenomeSignatures {
720 genome: "abc123".to_string(),
721 blueprint: "def456".to_string(),
722 physiology: "ghi789".to_string(),
723 morphologies: None,
724 },
725 stats: GenomeStats::default(),
726 };
727
728 use feagi_structures::genomic::cortical_area::CustomCorticalType;
730 use feagi_structures::genomic::cortical_area::{
731 CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
732 };
733 let test_id = CoreCorticalType::Power.to_cortical_id();
734 let area = CorticalArea::new(
735 test_id,
736 0,
737 "Test Area".to_string(),
738 CorticalAreaDimensions::new(10, 10, 10).unwrap(),
739 (0, 0, 0).into(),
740 CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
741 )
742 .expect("Failed to create cortical area");
743
744 genome.cortical_areas.insert(test_id, area);
745
746 let result = validate_genome(&genome);
747
748 println!("Errors: {:?}", result.errors);
750 println!("Warnings: {:?}", result.warnings);
751
752 assert!(result.errors.is_empty());
754 assert!(!result.warnings.is_empty()); }
756
757 #[test]
758 fn test_validate_quantization_precision() {
759 let mut genome = create_minimal_genome();
760
761 genome.physiology.quantization_precision = "fp32".to_string();
763 let result = validate_genome(&genome);
764 assert!(result.errors.is_empty(), "fp32 should be valid");
765
766 genome.physiology.quantization_precision = "int8".to_string();
768 let result = validate_genome(&genome);
769 assert!(result.errors.is_empty(), "int8 should be valid");
770
771 genome.physiology.quantization_precision = "i8".to_string();
773 let result = validate_genome(&genome);
774 assert!(result.errors.is_empty(), "i8 should be valid");
775 assert!(
776 result.warnings.iter().any(|w| w.contains("normalized")),
777 "Should warn about normalization"
778 );
779
780 genome.physiology.quantization_precision = "invalid".to_string();
782 let result = validate_genome(&genome);
783 assert!(!result.errors.is_empty(), "invalid should produce error");
784 assert!(
785 result
786 .errors
787 .iter()
788 .any(|e| e.contains("Invalid quantization_precision")),
789 "Should have quantization error"
790 );
791 }
792
793 #[test]
794 fn test_auto_fix_quantization_precision() {
795 let mut genome = create_minimal_genome();
797 genome.physiology.quantization_precision = "".to_string();
798
799 let fixes = auto_fix_genome(&mut genome);
800 assert!(fixes > 0, "Should apply at least one fix");
801 assert_eq!(
802 genome.physiology.quantization_precision, "int8",
803 "Should default to int8"
804 );
805
806 genome.physiology.quantization_precision = "i8".to_string();
808 let _fixes = auto_fix_genome(&mut genome);
809 assert_eq!(
810 genome.physiology.quantization_precision, "int8",
811 "Should normalize i8 to int8"
812 );
813
814 genome.physiology.quantization_precision = "invalid".to_string();
816 let _fixes = auto_fix_genome(&mut genome);
817 assert_eq!(
818 genome.physiology.quantization_precision, "int8",
819 "Invalid should default to int8"
820 );
821 }
822
823 fn create_minimal_genome() -> RuntimeGenome {
824 RuntimeGenome {
825 metadata: GenomeMetadata {
826 genome_id: "test".to_string(),
827 genome_title: "Test".to_string(),
828 genome_description: "".to_string(),
829 version: "2.0".to_string(),
830 timestamp: 0.0,
831 brain_regions_root: None,
832 },
833 cortical_areas: HashMap::new(),
834 brain_regions: HashMap::new(),
835 morphologies: MorphologyRegistry::new(),
836 physiology: PhysiologyConfig::default(),
837 signatures: GenomeSignatures {
838 genome: "0".to_string(),
839 blueprint: "0".to_string(),
840 physiology: "0".to_string(),
841 morphologies: None,
842 },
843 stats: GenomeStats::default(),
844 }
845 }
846}