feat: Add configuration validation system for MVP stability (Phase 1.2)
Implements JSON Schema validation for optimization configurations to ensure
consistency across all studies and prevent configuration errors.
Added:
- optimization_engine/schemas/optimization_config_schema.json
- Comprehensive schema for Protocol 10 & 11 configurations
- Validates objectives, constraints, design variables, simulation settings
- Enforces standard field names (goal, bounds, parameter, threshold)
- optimization_engine/config_manager.py
- ConfigManager class with schema validation
- CLI tool: python config_manager.py <config.json>
- Type-safe accessor methods for config elements
- Custom validations: bounds check, multi-objective consistency, location check
- optimization_engine/schemas/README.md
- Complete documentation of standard configuration format
- Validation examples and common error fixes
- Migration guidance for legacy configs
- docs/07_DEVELOPMENT/Phase_1_2_Implementation_Plan.md
- Detailed implementation plan for remaining Phase 1.2 tasks
- Migration tool design, integration guide, testing plan
Testing:
- Validated drone_gimbal_arm_optimization config successfully
- ConfigManager works with drone_gimbal format (new standard)
- Identifies legacy format issues in bracket studies
Standards Established:
- Configuration location: studies/{name}/1_setup/
- Objective direction: "goal" not "type"
- Design var bounds: "bounds": [min, max] not "min"/"max"
- Design var name: "parameter" not "name"
- Constraint threshold: "threshold" not "value"
Next Steps (Phase 1.2.1+):
- Config migration tool for legacy studies
- Integration with run_optimization.py
- Update create-study Claude skill with schema reference
- Migrate bracket studies to new format
Relates to: Phase 1.2 MVP Development Plan
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 09:21:55 -05:00
|
|
|
"""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
|
refactor: Major reorganization of optimization_engine module structure
BREAKING CHANGE: Module paths have been reorganized for better maintainability.
Backwards compatibility aliases with deprecation warnings are provided.
New Structure:
- core/ - Optimization runners (runner, intelligent_optimizer, etc.)
- processors/ - Data processing
- surrogates/ - Neural network surrogates
- nx/ - NX/Nastran integration (solver, updater, session_manager)
- study/ - Study management (creator, wizard, state, reset)
- reporting/ - Reports and analysis (visualizer, report_generator)
- config/ - Configuration management (manager, builder)
- utils/ - Utilities (logger, auto_doc, etc.)
- future/ - Research/experimental code
Migration:
- ~200 import changes across 125 files
- All __init__.py files use lazy loading to avoid circular imports
- Backwards compatibility layer supports old import paths with warnings
- All existing functionality preserved
To migrate existing code:
OLD: from optimization_engine.nx_solver import NXSolver
NEW: from optimization_engine.nx.solver import NXSolver
OLD: from optimization_engine.runner import OptimizationRunner
NEW: from optimization_engine.core.runner import OptimizationRunner
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 12:30:59 -05:00
|
|
|
from optimization_engine.config.manager import ConfigManager
|
feat: Add configuration validation system for MVP stability (Phase 1.2)
Implements JSON Schema validation for optimization configurations to ensure
consistency across all studies and prevent configuration errors.
Added:
- optimization_engine/schemas/optimization_config_schema.json
- Comprehensive schema for Protocol 10 & 11 configurations
- Validates objectives, constraints, design variables, simulation settings
- Enforces standard field names (goal, bounds, parameter, threshold)
- optimization_engine/config_manager.py
- ConfigManager class with schema validation
- CLI tool: python config_manager.py <config.json>
- Type-safe accessor methods for config elements
- Custom validations: bounds check, multi-objective consistency, location check
- optimization_engine/schemas/README.md
- Complete documentation of standard configuration format
- Validation examples and common error fixes
- Migration guidance for legacy configs
- docs/07_DEVELOPMENT/Phase_1_2_Implementation_Plan.md
- Detailed implementation plan for remaining Phase 1.2 tasks
- Migration tool design, integration guide, testing plan
Testing:
- Validated drone_gimbal_arm_optimization config successfully
- ConfigManager works with drone_gimbal format (new standard)
- Identifies legacy format issues in bracket studies
Standards Established:
- Configuration location: studies/{name}/1_setup/
- Objective direction: "goal" not "type"
- Design var bounds: "bounds": [min, max] not "min"/"max"
- Design var name: "parameter" not "name"
- Constraint threshold: "threshold" not "value"
Next Steps (Phase 1.2.1+):
- Config migration tool for legacy studies
- Integration with run_optimization.py
- Update create-study Claude skill with schema reference
- Migrate bracket studies to new format
Relates to: Phase 1.2 MVP Development Plan
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 09:21:55 -05:00
|
|
|
|
|
|
|
|
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 <path_to_optimization_config.json>")
|
|
|
|
|
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)
|