"""Configuration validation and management for Atomizer studies. This module provides schema-based validation for optimization configuration files, ensuring consistency across all studies. Usage: # In run_optimization.py from optimization_engine.config.manager import ConfigManager config_manager = ConfigManager(Path(__file__).parent / "1_setup" / "optimization_config.json") config_manager.load_config() if not config_manager.validate(): print(config_manager.get_validation_report()) sys.exit(1) # Access validated configuration design_vars = config_manager.get_design_variables() objectives = config_manager.get_objectives() """ import json from pathlib import Path from typing import Dict, List, Any, Optional try: import jsonschema JSONSCHEMA_AVAILABLE = True except ImportError: JSONSCHEMA_AVAILABLE = False print("Warning: jsonschema not installed. Install with: pip install jsonschema>=4.17.0") class ConfigValidationError(Exception): """Raised when configuration validation fails.""" pass class ConfigManager: """Manages and validates optimization configuration files.""" def __init__(self, config_path: Path): """ Initialize ConfigManager with path to optimization_config.json. Args: config_path: Path to optimization_config.json file """ self.config_path = Path(config_path) self.schema_path = Path(__file__).parent / "schemas" / "optimization_config_schema.json" self.config: Optional[Dict[str, Any]] = None self.validation_errors: List[str] = [] def load_schema(self) -> Dict[str, Any]: """Load JSON schema for validation.""" if not self.schema_path.exists(): raise FileNotFoundError(f"Schema file not found: {self.schema_path}") with open(self.schema_path, 'r') as f: return json.load(f) def load_config(self) -> Dict[str, Any]: """Load configuration file.""" if not self.config_path.exists(): raise FileNotFoundError(f"Config file not found: {self.config_path}") with open(self.config_path, 'r') as f: self.config = json.load(f) return self.config def validate(self, strict: bool = True) -> bool: """ Validate configuration against schema. Args: strict: If True, enforce all validations. If False, only warn on non-critical issues. Returns: True if valid, False otherwise """ if self.config is None: self.load_config() self.validation_errors = [] # JSON Schema validation if JSONSCHEMA_AVAILABLE: schema = self.load_schema() try: jsonschema.validate(instance=self.config, schema=schema) except jsonschema.ValidationError as e: self.validation_errors.append(f"Schema validation failed: {e.message}") if strict: return False else: self.validation_errors.append("jsonschema not installed - schema validation skipped") # Custom validations self._validate_design_variable_bounds() self._validate_multi_objective_consistency() self._validate_file_locations() self._validate_extraction_consistency() return len(self.validation_errors) == 0 def _validate_design_variable_bounds(self): """Ensure bounds are valid (min < max).""" for dv in self.config.get("design_variables", []): bounds = dv.get("bounds", []) if len(bounds) == 2 and bounds[0] >= bounds[1]: self.validation_errors.append( f"Design variable '{dv.get('parameter', 'unknown')}': " f"min ({bounds[0]}) must be < max ({bounds[1]})" ) def _validate_multi_objective_consistency(self): """Validate multi-objective settings consistency.""" n_objectives = len(self.config.get("objectives", [])) protocol = self.config.get("optimization_settings", {}).get("protocol") sampler = self.config.get("optimization_settings", {}).get("sampler") if n_objectives > 1: # Multi-objective should use protocol_11 and NSGA-II if protocol and protocol != "protocol_11_multi_objective": self.validation_errors.append( f"Multi-objective optimization ({n_objectives} objectives) " f"should use protocol_11_multi_objective (got {protocol})" ) if sampler and sampler != "NSGAIISampler": self.validation_errors.append( f"Multi-objective optimization should use NSGAIISampler (got {sampler})" ) elif n_objectives == 1: # Single-objective should not use NSGA-II if sampler == "NSGAIISampler": self.validation_errors.append( "Single-objective optimization should not use NSGAIISampler " "(use TPESampler or CmaEsSampler)" ) def _validate_file_locations(self): """Check if config is in correct location (1_setup/).""" if "1_setup" not in str(self.config_path.parent): self.validation_errors.append( f"Warning: Config should be in '1_setup/' directory, " f"found in {self.config_path.parent}" ) def _validate_extraction_consistency(self): """Validate extraction specifications.""" # Check objectives have extraction specs for obj in self.config.get("objectives", []): if "extraction" not in obj: self.validation_errors.append( f"Objective '{obj.get('name', 'unknown')}' missing extraction specification" ) # Check constraints have extraction specs for constraint in self.config.get("constraints", []): if "extraction" not in constraint: self.validation_errors.append( f"Constraint '{constraint.get('name', 'unknown')}' missing extraction specification" ) def get_validation_report(self) -> str: """Get human-readable validation report.""" if not self.validation_errors: return "[OK] Configuration is valid" report = "[FAIL] Configuration validation failed:\n" for i, error in enumerate(self.validation_errors, 1): report += f" {i}. {error}\n" return report # Type-safe accessor methods def get_design_variables(self) -> List[Dict[str, Any]]: """Get design variables with validated structure.""" if self.config is None: self.load_config() return self.config.get("design_variables", []) def get_objectives(self) -> List[Dict[str, Any]]: """Get objectives with validated structure.""" if self.config is None: self.load_config() return self.config.get("objectives", []) def get_constraints(self) -> List[Dict[str, Any]]: """Get constraints with validated structure.""" if self.config is None: self.load_config() return self.config.get("constraints", []) def get_simulation_settings(self) -> Dict[str, Any]: """Get simulation settings.""" if self.config is None: self.load_config() return self.config.get("simulation", {}) def get_optimization_settings(self) -> Dict[str, Any]: """Get optimization settings.""" if self.config is None: self.load_config() return self.config.get("optimization_settings", {}) # CLI tool for validation if __name__ == "__main__": import sys if len(sys.argv) < 2: print("Usage: python config_manager.py ") print("\nExample:") print(" python config_manager.py studies/drone_gimbal_arm_optimization/1_setup/optimization_config.json") sys.exit(1) config_path = Path(sys.argv[1]) print(f"Validating configuration: {config_path}") print("=" * 60) manager = ConfigManager(config_path) try: manager.load_config() print("[OK] Config loaded successfully") is_valid = manager.validate() print(manager.get_validation_report()) if is_valid: print("\n" + "=" * 60) print("Configuration Summary:") print(f" Study: {manager.config.get('study_name')}") print(f" Protocol: {manager.get_optimization_settings().get('protocol')}") print(f" Design Variables: {len(manager.get_design_variables())}") print(f" Objectives: {len(manager.get_objectives())}") print(f" Constraints: {len(manager.get_constraints())}") sys.exit(0 if is_valid else 1) except Exception as e: print(f"[ERROR] {e}") sys.exit(1)