Files
Atomizer/optimization_engine/study/benchmarking.py

473 lines
17 KiB
Python
Raw Normal View History

feat: Add centralized configuration system and Phase 3.2 enhancements Major Features Added: 1. Centralized Configuration System (config.py) - Single source of truth for all NX and environment paths - Change NX version in ONE place: NX_VERSION = "2412" - Change Python environment in ONE place: PYTHON_ENV_NAME = "atomizer" - Automatic path derivation and validation - Helper functions: get_nx_journal_command() - Future-proof: Easy to upgrade when NX 2506+ released 2. NX Path Corrections (Critical Fix) - Fixed all incorrect Simcenter3D_2412 references to NX2412 - Updated nx_updater.py to use config.NX_RUN_JOURNAL - Updated dashboard/api/app.py to use config.NX_RUN_JOURNAL - Corrected material library path to NX2412/UGII/materials - All files now use correct NX2412 installation 3. NX Expression Import System - Dual-method expression gathering (.exp export + binary parsing) - Robust handling of all NX expression types - Support for formulas, units, and dependencies - Documented in docs/NX_EXPRESSION_IMPORT_SYSTEM.md 4. Study Management & Analysis Tools - StudyCreator: Unified interface for study/substudy creation - BenchmarkingSubstudy: Automated baseline analysis - ComprehensiveResultsAnalyzer: Multi-result extraction from .op2 - Expression extractor generator (LLM-powered) 5. 50-Trial Beam Optimization Complete - Full optimization results documented - Best design: 23.1% improvement over baseline - Comprehensive analysis with plots and insights - Results in studies/simple_beam_optimization/ Documentation Updates: - docs/SYSTEM_CONFIGURATION.md - System paths and validation - docs/QUICK_CONFIG_REFERENCE.md - Quick config change guide - docs/NX_EXPRESSION_IMPORT_SYSTEM.md - Expression import details - docs/OPTIMIZATION_WORKFLOW.md - Complete workflow guide - Updated README.md with NX2412 paths Files Modified: - config.py (NEW) - Central configuration system - optimization_engine/nx_updater.py - Now uses config - dashboard/api/app.py - Now uses config - optimization_engine/study_creator.py - Enhanced features - optimization_engine/benchmarking_substudy.py - New analyzer - optimization_engine/comprehensive_results_analyzer.py - Multi-result extraction - optimization_engine/result_extractors/generated/extract_expression.py - Generated extractor Cleanup: - Removed all temporary test files - Removed migration scripts (no longer needed) - Clean production-ready codebase Strategic Impact: - Configuration maintenance time: reduced from hours to seconds - Path consistency: 100% enforced across codebase - Future NX upgrades: Edit ONE variable in config.py - Foundation for Phase 3.2 Integration completion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 14:36:00 -05:00
"""
Benchmarking Substudy - Mandatory Discovery & Validation System
The benchmarking substudy is a mandatory first step for all optimization studies.
It performs model introspection, validation, and configuration proposal before
any optimization trials are run.
Purpose:
- Discover available expressions, OP2 contents, baseline performance
- Validate that model can be simulated and results extracted
- Propose initial optimization configuration
- Act as gatekeeper before full optimization
This substudy ALWAYS runs before any other substudy and auto-updates when
new substudies are created.
Author: Antoine Letarte
Date: 2025-11-17
Version: 1.0.0
"""
import json
import logging
from pathlib import Path
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
from optimization_engine.config.setup_wizard import OptimizationSetupWizard, ModelIntrospection, OP2Introspection
feat: Add centralized configuration system and Phase 3.2 enhancements Major Features Added: 1. Centralized Configuration System (config.py) - Single source of truth for all NX and environment paths - Change NX version in ONE place: NX_VERSION = "2412" - Change Python environment in ONE place: PYTHON_ENV_NAME = "atomizer" - Automatic path derivation and validation - Helper functions: get_nx_journal_command() - Future-proof: Easy to upgrade when NX 2506+ released 2. NX Path Corrections (Critical Fix) - Fixed all incorrect Simcenter3D_2412 references to NX2412 - Updated nx_updater.py to use config.NX_RUN_JOURNAL - Updated dashboard/api/app.py to use config.NX_RUN_JOURNAL - Corrected material library path to NX2412/UGII/materials - All files now use correct NX2412 installation 3. NX Expression Import System - Dual-method expression gathering (.exp export + binary parsing) - Robust handling of all NX expression types - Support for formulas, units, and dependencies - Documented in docs/NX_EXPRESSION_IMPORT_SYSTEM.md 4. Study Management & Analysis Tools - StudyCreator: Unified interface for study/substudy creation - BenchmarkingSubstudy: Automated baseline analysis - ComprehensiveResultsAnalyzer: Multi-result extraction from .op2 - Expression extractor generator (LLM-powered) 5. 50-Trial Beam Optimization Complete - Full optimization results documented - Best design: 23.1% improvement over baseline - Comprehensive analysis with plots and insights - Results in studies/simple_beam_optimization/ Documentation Updates: - docs/SYSTEM_CONFIGURATION.md - System paths and validation - docs/QUICK_CONFIG_REFERENCE.md - Quick config change guide - docs/NX_EXPRESSION_IMPORT_SYSTEM.md - Expression import details - docs/OPTIMIZATION_WORKFLOW.md - Complete workflow guide - Updated README.md with NX2412 paths Files Modified: - config.py (NEW) - Central configuration system - optimization_engine/nx_updater.py - Now uses config - dashboard/api/app.py - Now uses config - optimization_engine/study_creator.py - Enhanced features - optimization_engine/benchmarking_substudy.py - New analyzer - optimization_engine/comprehensive_results_analyzer.py - Multi-result extraction - optimization_engine/result_extractors/generated/extract_expression.py - Generated extractor Cleanup: - Removed all temporary test files - Removed migration scripts (no longer needed) - Clean production-ready codebase Strategic Impact: - Configuration maintenance time: reduced from hours to seconds - Path consistency: 100% enforced across codebase - Future NX upgrades: Edit ONE variable in config.py - Foundation for Phase 3.2 Integration completion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 14:36:00 -05:00
logger = logging.getLogger(__name__)
@dataclass
class BenchmarkResults:
"""Results from benchmarking analysis."""
timestamp: str
# Model introspection
expressions: Dict[str, Dict[str, Any]] # name -> {value, units, formula}
expression_count: int
# OP2 introspection
element_types: List[str]
result_types: List[str]
subcases: List[int]
node_count: int
element_count: int
# Baseline simulation results
baseline_op2_path: str
baseline_results: Dict[str, float] # e.g., max_stress, max_displacement, mass
# Validation status
simulation_works: bool
extraction_works: bool
validation_passed: bool
# Proposals
proposed_design_variables: List[Dict[str, Any]]
proposed_extractors: List[Dict[str, Any]]
proposed_objectives: List[str]
# Issues found
warnings: List[str]
errors: List[str]
class BenchmarkingSubstudy:
"""
Mandatory benchmarking substudy for discovery and validation.
This runs before any optimization to:
1. Discover what's in the model
2. Validate the pipeline works
3. Propose configuration
4. Gate-keep before optimization
"""
def __init__(self, study_dir: Path, prt_file: Path, sim_file: Path):
"""
Initialize benchmarking substudy.
Args:
study_dir: Root study directory
prt_file: Path to NX part file
sim_file: Path to NX simulation file
"""
self.study_dir = Path(study_dir)
self.prt_file = Path(prt_file)
self.sim_file = Path(sim_file)
# Benchmarking substudy directory
self.benchmark_dir = self.study_dir / "substudies" / "benchmarking"
self.benchmark_dir.mkdir(parents=True, exist_ok=True)
# Results file
self.results_file = self.benchmark_dir / "benchmark_results.json"
# Use Phase 3.3 wizard for introspection
self.wizard = OptimizationSetupWizard(prt_file, sim_file)
logger.info(f"Benchmarking substudy initialized for: {study_dir.name}")
def run_discovery(self) -> BenchmarkResults:
"""
Run complete discovery and validation.
Returns:
BenchmarkResults with all discovery information
"""
logger.info("=" * 80)
logger.info("BENCHMARKING SUBSTUDY - Discovery & Validation")
logger.info("=" * 80)
logger.info("")
results = BenchmarkResults(
timestamp=datetime.now().isoformat(),
expressions={},
expression_count=0,
element_types=[],
result_types=[],
subcases=[],
node_count=0,
element_count=0,
baseline_op2_path="",
baseline_results={},
simulation_works=False,
extraction_works=False,
validation_passed=False,
proposed_design_variables=[],
proposed_extractors=[],
proposed_objectives=[],
warnings=[],
errors=[]
)
# Step 1: Model Introspection
logger.info("Step 1: Model Introspection")
logger.info("-" * 40)
try:
model_info = self.wizard.introspect_model()
results.expressions = model_info.expressions
results.expression_count = len(model_info.expressions)
logger.info(f"Found {results.expression_count} expressions:")
for name, info in model_info.expressions.items():
logger.info(f" - {name}: {info['value']} {info['units']}")
logger.info("")
except Exception as e:
error_msg = f"Model introspection failed: {e}"
logger.error(error_msg)
results.errors.append(error_msg)
results.validation_passed = False
return results
# Step 2: Baseline Simulation
logger.info("Step 2: Baseline Simulation")
logger.info("-" * 40)
try:
baseline_op2 = self.wizard.run_baseline_simulation()
if baseline_op2:
results.baseline_op2_path = str(baseline_op2)
results.simulation_works = True
logger.info(f"Baseline simulation complete: {baseline_op2.name}")
logger.info("")
else:
warning_msg = "Baseline simulation returned no OP2 file"
logger.warning(warning_msg)
results.warnings.append(warning_msg)
logger.info("")
except Exception as e:
error_msg = f"Baseline simulation failed: {e}"
logger.error(error_msg)
results.errors.append(error_msg)
logger.info("Continuing with available information...")
logger.info("")
# Step 3: OP2 Introspection
logger.info("Step 3: OP2 Introspection")
logger.info("-" * 40)
try:
op2_info = self.wizard.introspect_op2()
results.element_types = op2_info.element_types
results.result_types = op2_info.result_types
results.subcases = op2_info.subcases
results.node_count = op2_info.node_count
results.element_count = op2_info.element_count
logger.info(f"OP2 Analysis:")
logger.info(f" - Element types: {', '.join(results.element_types)}")
logger.info(f" - Result types: {', '.join(results.result_types)}")
logger.info(f" - Subcases: {results.subcases}")
logger.info(f" - Nodes: {results.node_count}")
logger.info(f" - Elements: {results.element_count}")
logger.info("")
except Exception as e:
error_msg = f"OP2 introspection failed: {e}"
logger.error(error_msg)
results.errors.append(error_msg)
results.validation_passed = False
return results
# Step 4: Extract Baseline Results
logger.info("Step 4: Extract Baseline Results")
logger.info("-" * 40)
try:
# Try to extract common results
baseline_results = self._extract_baseline_results(Path(results.baseline_op2_path))
results.baseline_results = baseline_results
results.extraction_works = True
logger.info("Baseline performance:")
for key, value in baseline_results.items():
logger.info(f" - {key}: {value}")
logger.info("")
except Exception as e:
warning_msg = f"Baseline extraction partially failed: {e}"
logger.warning(warning_msg)
results.warnings.append(warning_msg)
# Not a hard failure - continue
# Step 5: Generate Proposals
logger.info("Step 5: Generate Configuration Proposals")
logger.info("-" * 40)
proposals = self._generate_proposals(model_info, op2_info, results.baseline_results)
results.proposed_design_variables = proposals['design_variables']
results.proposed_extractors = proposals['extractors']
results.proposed_objectives = proposals['objectives']
logger.info(f"Proposed design variables ({len(results.proposed_design_variables)}):")
for var in results.proposed_design_variables:
logger.info(f" - {var['parameter']}: {var.get('suggested_range', 'range needed')}")
logger.info(f"\nProposed extractors ({len(results.proposed_extractors)}):")
for ext in results.proposed_extractors:
logger.info(f" - {ext['action']}: {ext['description']}")
logger.info(f"\nProposed objectives ({len(results.proposed_objectives)}):")
for obj in results.proposed_objectives:
logger.info(f" - {obj}")
logger.info("")
# Validation passed if simulation and basic extraction work
results.validation_passed = results.simulation_works and len(results.element_types) > 0
# Save results
self._save_results(results)
logger.info("=" * 80)
if results.validation_passed:
logger.info("BENCHMARKING COMPLETE - Validation PASSED")
else:
logger.info("BENCHMARKING COMPLETE - Validation FAILED")
logger.info("=" * 80)
logger.info("")
return results
def _extract_baseline_results(self, op2_file: Path) -> Dict[str, float]:
"""Extract baseline results from OP2 file."""
from pyNastran.op2.op2 import OP2
results = {}
try:
op2 = OP2()
op2.read_op2(str(op2_file), load_geometry=False)
# Try to extract displacement
if hasattr(op2, 'displacements') and op2.displacements:
disp_data = list(op2.displacements.values())[0]
if hasattr(disp_data, 'data'):
max_disp = float(abs(disp_data.data).max())
results['max_displacement'] = round(max_disp, 6)
# Try to extract stress
if hasattr(op2, 'ctetra_stress') and op2.ctetra_stress:
stress_data = list(op2.ctetra_stress.values())[0]
if hasattr(stress_data, 'data'):
max_stress = float(abs(stress_data.data).max())
results['max_von_mises'] = round(max_stress, 3)
elif hasattr(op2, 'chexa_stress') and op2.chexa_stress:
stress_data = list(op2.chexa_stress.values())[0]
if hasattr(stress_data, 'data'):
max_stress = float(abs(stress_data.data).max())
results['max_von_mises'] = round(max_stress, 3)
except Exception as e:
logger.warning(f"Could not extract all baseline results: {e}")
return results
def _generate_proposals(self, model_info: ModelIntrospection, op2_info: OP2Introspection,
baseline_results: Dict[str, float]) -> Dict[str, Any]:
"""Generate configuration proposals based on discovery."""
proposals = {
'design_variables': [],
'extractors': [],
'objectives': []
}
# Propose design variables from expressions
# Filter out likely constants (e.g., material properties, loads)
constant_keywords = ['modulus', 'poisson', 'density', 'load', 'force', 'pressure']
for name, info in model_info.expressions.items():
# Skip if likely a constant
if any(keyword in name.lower() for keyword in constant_keywords):
continue
# Propose as design variable
proposals['design_variables'].append({
'parameter': name,
'current_value': info['value'],
'units': info['units'],
'suggested_range': f"±20% of {info['value']} {info['units']}"
})
# Propose extractors based on OP2 contents
if 'displacement' in op2_info.result_types or 'DISPLACEMENT' in op2_info.result_types:
proposals['extractors'].append({
'action': 'extract_displacement',
'description': 'Extract displacement results from OP2 file',
'params': {'result_type': 'displacement'}
})
proposals['objectives'].append('max_displacement (minimize or maximize)')
if op2_info.element_types:
element_type = op2_info.element_types[0].lower()
proposals['extractors'].append({
'action': 'extract_solid_stress',
'description': f'Extract stress from {element_type.upper()} elements',
'params': {
'result_type': 'stress',
'element_type': element_type
}
})
proposals['objectives'].append('max_von_mises (minimize for safety)')
return proposals
def _save_results(self, results: BenchmarkResults):
"""Save benchmark results to JSON file."""
import numpy as np
results_dict = asdict(results)
# Convert numpy types to native Python types for JSON serialization
def convert_numpy(obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, dict):
return {k: convert_numpy(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_numpy(item) for item in obj]
return obj
results_dict = convert_numpy(results_dict)
with open(self.results_file, 'w') as f:
json.dump(results_dict, f, indent=2)
logger.info(f"Benchmark results saved to: {self.results_file}")
def load_results(self) -> Optional[BenchmarkResults]:
"""Load previous benchmark results if they exist."""
if not self.results_file.exists():
return None
with open(self.results_file, 'r') as f:
data = json.load(f)
return BenchmarkResults(**data)
def generate_report(self, results: BenchmarkResults) -> str:
"""
Generate human-readable benchmark report.
Returns:
Markdown formatted report
"""
report = []
report.append("# Benchmarking Report")
report.append("")
report.append(f"**Study**: {self.study_dir.name}")
report.append(f"**Date**: {results.timestamp}")
report.append(f"**Validation**: {'✅ PASSED' if results.validation_passed else '❌ FAILED'}")
report.append("")
report.append("## Model Introspection")
report.append("")
report.append(f"**Expressions Found**: {results.expression_count}")
report.append("")
report.append("| Expression | Value | Units |")
report.append("|------------|-------|-------|")
for name, info in results.expressions.items():
report.append(f"| {name} | {info['value']} | {info['units']} |")
report.append("")
report.append("## OP2 Analysis")
report.append("")
report.append(f"- **Element Types**: {', '.join(results.element_types)}")
report.append(f"- **Result Types**: {', '.join(results.result_types)}")
report.append(f"- **Subcases**: {results.subcases}")
report.append(f"- **Nodes**: {results.node_count}")
report.append(f"- **Elements**: {results.element_count}")
report.append("")
report.append("## Baseline Performance")
report.append("")
if results.baseline_results:
for key, value in results.baseline_results.items():
report.append(f"- **{key}**: {value}")
else:
report.append("*No baseline results extracted*")
report.append("")
report.append("## Configuration Proposals")
report.append("")
report.append("### Proposed Design Variables")
report.append("")
for var in results.proposed_design_variables:
report.append(f"- **{var['parameter']}**: {var['suggested_range']}")
report.append("")
report.append("### Proposed Extractors")
report.append("")
for ext in results.proposed_extractors:
report.append(f"- **{ext['action']}**: {ext['description']}")
report.append("")
report.append("### Proposed Objectives")
report.append("")
for obj in results.proposed_objectives:
report.append(f"- {obj}")
report.append("")
if results.warnings:
report.append("## Warnings")
report.append("")
for warning in results.warnings:
report.append(f"⚠️ {warning}")
report.append("")
if results.errors:
report.append("## Errors")
report.append("")
for error in results.errors:
report.append(f"{error}")
report.append("")
return "\n".join(report)
def main():
"""Test benchmarking substudy."""
print("Benchmarking Substudy Test")
print("=" * 80)
print()
print("This module provides mandatory discovery and validation for all studies.")
print("Use it via the study setup workflow.")
print()
if __name__ == '__main__':
main()