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