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