use crate::{MorphologyParameters, RuntimeGenome};
#[allow(unused_imports)]
use feagi_structures::genomic::cortical_area::CorticalID;
use serde_json::Value;
use std::collections::HashSet;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, error: String) {
self.valid = false;
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: String) {
self.warnings.push(warning);
}
pub fn merge(&mut self, other: ValidationResult) {
if !other.valid {
self.valid = false;
}
self.errors.extend(other.errors);
self.warnings.extend(other.warnings);
}
}
impl Default for ValidationResult {
fn default() -> Self {
Self::new()
}
}
pub fn validate_genome(genome: &RuntimeGenome) -> ValidationResult {
let mut result = ValidationResult::new();
validate_metadata(genome, &mut result);
validate_cortical_areas(genome, &mut result);
validate_morphologies(genome, &mut result);
validate_physiology(genome, &mut result);
cross_validate(genome, &mut result);
result
}
pub fn auto_fix_genome(genome: &mut RuntimeGenome) -> usize {
use tracing::info;
let mut fixes_applied = 0;
if genome.physiology.simulation_timestep <= 0.0 {
let default_timestep = crate::runtime::PhysiologyConfig::default().simulation_timestep;
info!(
"🔧 AUTO-FIX: Invalid simulation_timestep {} → {} (default)",
genome.physiology.simulation_timestep, default_timestep
);
genome.physiology.simulation_timestep = default_timestep;
fixes_applied += 1;
}
if genome.physiology.max_age == 0 {
let default_age = crate::runtime::PhysiologyConfig::default().max_age;
info!("🔧 AUTO-FIX: max_age 0 → {} (default)", default_age);
genome.physiology.max_age = default_age;
fixes_applied += 1;
}
if genome.physiology.quantization_precision.is_empty() {
let default_precision = crate::runtime::default_quantization_precision();
info!(
"🔧 AUTO-FIX: Missing quantization_precision → '{}' (default)",
default_precision
);
genome.physiology.quantization_precision = default_precision;
fixes_applied += 1;
} else {
use feagi_npu_neural::types::Precision;
match Precision::from_str(&genome.physiology.quantization_precision) {
Ok(precision) => {
let canonical = precision.as_str().to_string();
if genome.physiology.quantization_precision != canonical {
info!(
"🔧 AUTO-FIX: Quantization precision '{}' → '{}' (normalized)",
genome.physiology.quantization_precision, canonical
);
genome.physiology.quantization_precision = canonical;
fixes_applied += 1;
}
}
Err(_) => {
let default_precision = crate::runtime::default_quantization_precision();
info!(
"🔧 AUTO-FIX: Invalid quantization_precision '{}' → '{}' (default)",
genome.physiology.quantization_precision, default_precision
);
genome.physiology.quantization_precision = default_precision;
fixes_applied += 1;
}
}
}
for (cortical_id, area) in &mut genome.cortical_areas {
let cortical_id_display = cortical_id.to_string();
if area.dimensions.width == 0 {
info!(
"🔧 AUTO-FIX: Cortical area '{}' width 0 → 1",
cortical_id_display
);
area.dimensions.width = 1;
fixes_applied += 1;
}
if area.dimensions.height == 0 {
info!(
"🔧 AUTO-FIX: Cortical area '{}' height 0 → 1",
cortical_id_display
);
area.dimensions.height = 1;
fixes_applied += 1;
}
if area.dimensions.depth == 0 {
info!(
"🔧 AUTO-FIX: Cortical area '{}' depth 0 → 1",
cortical_id_display
);
area.dimensions.depth = 1;
fixes_applied += 1;
}
let neurons_per_voxel = area
.properties
.get("neurons_per_voxel")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
if neurons_per_voxel == 0 {
info!(
"🔧 AUTO-FIX: Cortical area '{}' neurons_per_voxel 0 → 1",
cortical_id_display
);
area.properties
.insert("neurons_per_voxel".to_string(), serde_json::json!(1));
fixes_applied += 1;
}
}
if fixes_applied > 0 {
info!(
"🔧 AUTO-FIX: Applied {} automatic corrections to genome",
fixes_applied
);
}
fixes_applied
}
fn validate_metadata(genome: &RuntimeGenome, result: &mut ValidationResult) {
if genome.metadata.genome_id.is_empty() {
result.add_error("Genome ID is empty".to_string());
}
if genome.metadata.version.is_empty() {
result.add_error("Genome version is empty".to_string());
}
if genome.metadata.version != "2.0" {
result.add_warning(format!(
"Genome version '{}' may not be fully supported (expected '2.0')",
genome.metadata.version
));
}
}
fn validate_cortical_areas(genome: &RuntimeGenome, result: &mut ValidationResult) {
if genome.cortical_areas.is_empty() {
result.add_warning("Genome has no cortical areas defined".to_string());
return;
}
for (cortical_id, area) in &genome.cortical_areas {
let cortical_id_display = cortical_id.to_string();
validate_cortical_id_format(cortical_id, &cortical_id_display, result);
if area.dimensions.width == 0 || area.dimensions.height == 0 || area.dimensions.depth == 0 {
result.add_warning(format!(
"AUTO-FIX: Cortical area '{}' has zero dimension(s): {}x{}x{} - will be corrected to minimum (1,1,1)",
cortical_id_display, area.dimensions.width, area.dimensions.height, area.dimensions.depth
));
}
let neurons_per_voxel = area
.properties
.get("neurons_per_voxel")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
if neurons_per_voxel == 0 {
result.add_warning(format!(
"AUTO-FIX: Cortical area '{}' has neurons_per_voxel=0 - will be corrected to 1",
cortical_id_display
));
}
let total_voxels = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
if total_voxels > 1_000_000 {
result.add_warning(format!(
"Cortical area '{}' has very large dimensions: {} total voxels",
cortical_id_display, total_voxels
));
}
if area.name.is_empty() {
result.add_warning(format!(
"Cortical area '{}' has empty name",
cortical_id_display
));
}
}
}
fn validate_cortical_id_format(
_cortical_id: &CorticalID,
display: &str,
result: &mut ValidationResult,
) {
if display.len() != 8 && display.len() != 12 {
result.add_error(format!(
"Invalid cortical ID length: '{}' is {} characters (must be 8 or 12)",
display,
display.len()
));
return;
}
if display.starts_with('_') {
validate_core_area_id(display, result);
return;
}
if display.starts_with('c') {
if !display.chars().all(|c| c.is_alphanumeric() || c == '_') {
result.add_warning(format!(
"Custom cortical ID '{}' contains non-alphanumeric characters",
display
));
}
return;
}
validate_io_area_id(display, result);
}
fn validate_core_area_id(display: &str, result: &mut ValidationResult) {
use feagi_structures::genomic::cortical_area::CoreCorticalType;
let valid_core_ids: Vec<String> = vec![
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(), ];
if !valid_core_ids.contains(&display.to_string()) {
result.add_error(format!(
"Invalid CORE cortical ID: '{}' - must be one of: {:?}",
display, valid_core_ids
));
}
}
fn validate_io_area_id(display: &str, result: &mut ValidationResult) {
let first_char = display.chars().next().unwrap_or('_');
let unit_prefix = &display[1..4];
const VALID_IPU_PREFIXES: &[&str] = &[
"svi", "aud", "tac", "olf", "vis", ];
const VALID_OPU_PREFIXES: &[&str] = &[
"mot", "voc", "gaz", "pse", "mis", ];
let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
if !is_valid_ipu && !is_valid_opu {
if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
{
result.add_error(format!(
"INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
Valid IPU units: {:?}, Valid OPU units: {:?}. \
This genome needs migration to the new format.",
display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
));
} else {
result.add_warning(format!(
"Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
));
}
return;
}
let suffix = &display[4..];
if first_char == 'i' && unit_prefix == "svi" {
if let Some(index_char) = display.chars().nth(4) {
if index_char.is_ascii_digit() {
let digit = index_char as u8 - b'0';
if digit > 8 {
result.add_error(format!(
"Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
digit, display
));
}
}
}
}
if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
result.add_warning(format!(
"Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
display
));
}
}
fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
if genome.morphologies.count() == 0 {
result.add_warning("Genome has no morphologies defined".to_string());
return;
}
let required_core = vec!["block_to_block", "projector"];
for morph_id in required_core {
if !genome.morphologies.contains(morph_id) {
result.add_warning(format!(
"Missing recommended core morphology: '{}'",
morph_id
));
}
}
for (morphology_id, morphology) in genome.morphologies.iter() {
validate_single_morphology(morphology_id, morphology, result);
}
}
fn validate_single_morphology(
morphology_id: &str,
morphology: &crate::Morphology,
result: &mut ValidationResult,
) {
match &morphology.parameters {
MorphologyParameters::Vectors { vectors } => {
if vectors.is_empty() {
result.add_error(format!(
"Morphology '{}' (vectors) has no vectors defined",
morphology_id
));
}
for (i, vec) in vectors.iter().enumerate() {
if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
result.add_warning(format!(
"Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
morphology_id, i, vec[0], vec[1], vec[2]
));
}
}
}
MorphologyParameters::Patterns { patterns } => {
if patterns.is_empty() {
result.add_error(format!(
"Morphology '{}' (patterns) has no patterns defined",
morphology_id
));
}
for (i, pattern) in patterns.iter().enumerate() {
if pattern[0].len() != 3 || pattern[1].len() != 3 {
result.add_error(format!(
"Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
morphology_id, i
));
}
}
}
MorphologyParameters::Functions {} => {
}
MorphologyParameters::Composite {
src_seed,
src_pattern,
mapper_morphology,
} => {
if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
result.add_warning(format!(
"Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
morphology_id, src_seed[0], src_seed[1], src_seed[2]
));
}
if src_pattern.is_empty() {
result.add_error(format!(
"Morphology '{}' (composite) has empty src_pattern",
morphology_id
));
}
if mapper_morphology.is_empty() {
result.add_error(format!(
"Morphology '{}' (composite) has empty mapper_morphology reference",
morphology_id
));
}
}
}
}
fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
let phys = &genome.physiology;
if phys.simulation_timestep <= 0.0 {
result.add_error(format!(
"Invalid simulation_timestep: {} (must be > 0.0)",
phys.simulation_timestep
));
}
if phys.simulation_timestep > 1.0 {
result.add_warning(format!(
"Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
phys.simulation_timestep
));
}
if phys.max_age == 0 {
result.add_warning("max_age is 0 (neurons will never age)".to_string());
}
if phys.plasticity_queue_depth == 0 {
result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
}
validate_quantization_precision(&phys.quantization_precision, result);
}
fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
use feagi_npu_neural::types::Precision;
match Precision::from_str(precision) {
Ok(parsed_precision) => {
if precision != parsed_precision.as_str() {
result.add_warning(format!(
"Quantization precision '{}' normalized to '{}'",
precision,
parsed_precision.as_str()
));
}
}
Err(_) => {
result.add_error(format!(
"Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
precision
));
}
}
}
fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
let morphology_ids: HashSet<String> =
genome.morphologies.morphology_ids().into_iter().collect();
for (cortical_id, area) in &genome.cortical_areas {
let cortical_id_display = cortical_id.to_string();
if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
for (dest_area, rules) in dstmap {
if let Ok(dest_cortical_id) =
crate::genome::parser::string_to_cortical_id(dest_area)
{
if !genome.cortical_areas.contains_key(&dest_cortical_id) {
result.add_error(format!(
"Cortical area '{}' references non-existent destination area '{}' in dstmap",
cortical_id_display, dest_area
));
}
} else {
result.add_error(format!(
"Cortical area '{}' has invalid destination area ID '{}' in dstmap",
cortical_id_display, dest_area
));
}
if let Value::Array(rules_array) = rules {
for rule in rules_array {
if let Value::Array(rule_array) = rule {
if let Some(Value::String(morph_id)) = rule_array.first() {
if !morphology_ids.contains(morph_id) {
result.add_error(format!(
"Cortical area '{}' references undefined morphology '{}' in dstmap rule",
cortical_id_display, morph_id
));
}
}
}
}
}
}
}
}
for (region_id, region) in &genome.brain_regions {
for cortical_id in ®ion.cortical_areas {
if !genome.cortical_areas.contains_key(cortical_id) {
result.add_error(format!(
"Brain region '{}' references non-existent cortical area '{}'",
region_id, cortical_id
));
}
}
}
for (morphology_id, morphology) in genome.morphologies.iter() {
if let MorphologyParameters::Composite {
mapper_morphology, ..
} = &morphology.parameters
{
if !morphology_ids.contains(mapper_morphology) {
result.add_error(format!(
"Composite morphology '{}' references undefined mapper morphology '{}'",
morphology_id, mapper_morphology
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
};
use std::collections::HashMap;
#[test]
fn test_validate_empty_genome() {
let genome = RuntimeGenome {
metadata: GenomeMetadata {
genome_id: "test".to_string(),
genome_title: "Test".to_string(),
genome_description: "".to_string(),
version: "2.0".to_string(),
timestamp: 0.0,
brain_regions_root: None,
},
cortical_areas: HashMap::new(),
brain_regions: HashMap::new(),
morphologies: MorphologyRegistry::new(),
physiology: PhysiologyConfig::default(),
signatures: GenomeSignatures {
genome: "0".to_string(),
blueprint: "0".to_string(),
physiology: "0".to_string(),
morphologies: None,
},
stats: GenomeStats::default(),
};
let result = validate_genome(&genome);
assert!(!result.warnings.is_empty());
println!("Warnings: {:?}", result.warnings);
}
#[test]
fn test_validate_valid_genome() {
let mut genome = RuntimeGenome {
metadata: GenomeMetadata {
genome_id: "test_genome".to_string(),
genome_title: "Test Genome".to_string(),
genome_description: "Valid test genome".to_string(),
version: "2.0".to_string(),
timestamp: 1234567890.0,
brain_regions_root: None,
},
cortical_areas: HashMap::new(),
brain_regions: HashMap::new(),
morphologies: MorphologyRegistry::new(),
physiology: PhysiologyConfig::default(),
signatures: GenomeSignatures {
genome: "abc123".to_string(),
blueprint: "def456".to_string(),
physiology: "ghi789".to_string(),
morphologies: None,
},
stats: GenomeStats::default(),
};
use feagi_structures::genomic::cortical_area::CustomCorticalType;
use feagi_structures::genomic::cortical_area::{
CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
};
let test_id = CoreCorticalType::Power.to_cortical_id();
let area = CorticalArea::new(
test_id,
0,
"Test Area".to_string(),
CorticalAreaDimensions::new(10, 10, 10).unwrap(),
(0, 0, 0).into(),
CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
)
.expect("Failed to create cortical area");
genome.cortical_areas.insert(test_id, area);
let result = validate_genome(&genome);
println!("Errors: {:?}", result.errors);
println!("Warnings: {:?}", result.warnings);
assert!(result.errors.is_empty());
assert!(!result.warnings.is_empty()); }
#[test]
fn test_validate_quantization_precision() {
let mut genome = create_minimal_genome();
genome.physiology.quantization_precision = "fp32".to_string();
let result = validate_genome(&genome);
assert!(result.errors.is_empty(), "fp32 should be valid");
genome.physiology.quantization_precision = "int8".to_string();
let result = validate_genome(&genome);
assert!(result.errors.is_empty(), "int8 should be valid");
genome.physiology.quantization_precision = "i8".to_string();
let result = validate_genome(&genome);
assert!(result.errors.is_empty(), "i8 should be valid");
assert!(
result.warnings.iter().any(|w| w.contains("normalized")),
"Should warn about normalization"
);
genome.physiology.quantization_precision = "invalid".to_string();
let result = validate_genome(&genome);
assert!(!result.errors.is_empty(), "invalid should produce error");
assert!(
result
.errors
.iter()
.any(|e| e.contains("Invalid quantization_precision")),
"Should have quantization error"
);
}
#[test]
fn test_auto_fix_quantization_precision() {
let mut genome = create_minimal_genome();
genome.physiology.quantization_precision = "".to_string();
let fixes = auto_fix_genome(&mut genome);
assert!(fixes > 0, "Should apply at least one fix");
assert_eq!(
genome.physiology.quantization_precision, "int8",
"Should default to int8"
);
genome.physiology.quantization_precision = "i8".to_string();
let _fixes = auto_fix_genome(&mut genome);
assert_eq!(
genome.physiology.quantization_precision, "int8",
"Should normalize i8 to int8"
);
genome.physiology.quantization_precision = "invalid".to_string();
let _fixes = auto_fix_genome(&mut genome);
assert_eq!(
genome.physiology.quantization_precision, "int8",
"Invalid should default to int8"
);
}
fn create_minimal_genome() -> RuntimeGenome {
RuntimeGenome {
metadata: GenomeMetadata {
genome_id: "test".to_string(),
genome_title: "Test".to_string(),
genome_description: "".to_string(),
version: "2.0".to_string(),
timestamp: 0.0,
brain_regions_root: None,
},
cortical_areas: HashMap::new(),
brain_regions: HashMap::new(),
morphologies: MorphologyRegistry::new(),
physiology: PhysiologyConfig::default(),
signatures: GenomeSignatures {
genome: "0".to_string(),
blueprint: "0".to_string(),
physiology: "0".to_string(),
morphologies: None,
},
stats: GenomeStats::default(),
}
}
}