feat: Add Study Insights module (SYS_16) for physics visualizations
Introduces a new plugin architecture for study-specific physics visualizations, separating "optimizer perspective" (Analysis) from "engineer perspective" (Insights). New module: optimization_engine/insights/ - base.py: StudyInsight base class, InsightConfig, InsightResult, registry - zernike_wfe.py: Mirror WFE with 3D surface and Zernike decomposition - stress_field.py: Von Mises stress contours with safety factors - modal_analysis.py: Natural frequencies and mode shapes - thermal_field.py: Temperature distribution visualization - design_space.py: Parameter-objective landscape exploration Features: - 5 insight types: zernike_wfe, stress_field, modal, thermal, design_space - CLI: python -m optimization_engine.insights generate <study> - Standalone HTML generation with Plotly - Enhanced Zernike viz: Turbo colorscale, smooth shading, 0.5x AMP - Dashboard API fix: Added include_coefficients param to extract_relative() Documentation: - docs/protocols/system/SYS_16_STUDY_INSIGHTS.md - Updated ATOMIZER_CONTEXT.md (v1.7) - Updated 01_CHEATSHEET.md with insights section Tools: - tools/zernike_html_generator.py: Standalone WFE HTML generator - tools/analyze_wfe.bat: Double-click to analyze OP2 files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
336
optimization_engine/insights/base.py
Normal file
336
optimization_engine/insights/base.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""
|
||||
Study Insights - Base Classes and Infrastructure
|
||||
|
||||
Study Insights provide physics-focused visualizations for optimization results.
|
||||
Unlike Analysis (optimizer-centric), Insights show the engineering reality
|
||||
of specific designs.
|
||||
|
||||
Architecture:
|
||||
- StudyInsight: Abstract base class for all insight types
|
||||
- InsightRegistry: Central registry for available insight types
|
||||
- Each insight can generate standalone HTML or Plotly data for dashboard
|
||||
|
||||
Usage:
|
||||
from optimization_engine.insights import get_insight, list_insights
|
||||
|
||||
# Get specific insight
|
||||
insight = get_insight('zernike_wfe')
|
||||
if insight.can_generate(study_path):
|
||||
html_path = insight.generate_html(study_path, trial_id=47)
|
||||
plotly_data = insight.get_plotly_data(study_path, trial_id=47)
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
import json
|
||||
|
||||
|
||||
@dataclass
|
||||
class InsightConfig:
|
||||
"""Configuration for an insight instance."""
|
||||
trial_id: Optional[int] = None # Specific trial to visualize (None = best)
|
||||
colorscale: str = 'Turbo'
|
||||
output_dir: Optional[Path] = None # Where to save HTML (None = study/3_insights/)
|
||||
|
||||
# Visual settings
|
||||
amplification: float = 1.0 # Deformation scale factor
|
||||
lighting: bool = True # 3D lighting effects
|
||||
|
||||
# Type-specific config (passed through)
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InsightResult:
|
||||
"""Result from generating an insight."""
|
||||
success: bool
|
||||
html_path: Optional[Path] = None
|
||||
plotly_figure: Optional[Dict[str, Any]] = None # Plotly figure as dict
|
||||
summary: Optional[Dict[str, Any]] = None # Key metrics
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class StudyInsight(ABC):
|
||||
"""
|
||||
Abstract base class for study-specific physics visualizations.
|
||||
|
||||
Each insight type provides:
|
||||
- Detection: Can this insight be generated for this study?
|
||||
- HTML generation: Standalone interactive report
|
||||
- Plotly data: For embedding in dashboard
|
||||
- Summary: Key metrics extracted
|
||||
|
||||
Subclasses must implement:
|
||||
- insight_type: Unique identifier (e.g., 'zernike_wfe')
|
||||
- name: Human-readable name
|
||||
- description: What this insight shows
|
||||
- applicable_to: List of study types this applies to
|
||||
- can_generate(): Check if study has required data
|
||||
- _generate(): Core generation logic
|
||||
"""
|
||||
|
||||
# Class-level metadata (override in subclasses)
|
||||
insight_type: str = "base"
|
||||
name: str = "Base Insight"
|
||||
description: str = "Abstract base insight"
|
||||
applicable_to: List[str] = [] # e.g., ['mirror', 'structural', 'all']
|
||||
|
||||
# Required files/data patterns
|
||||
required_files: List[str] = [] # e.g., ['*.op2', '*.bdf']
|
||||
|
||||
def __init__(self, study_path: Path):
|
||||
"""
|
||||
Initialize insight for a specific study.
|
||||
|
||||
Args:
|
||||
study_path: Path to study directory (studies/{name}/)
|
||||
"""
|
||||
self.study_path = Path(study_path)
|
||||
self.setup_path = self.study_path / "1_setup"
|
||||
self.results_path = self.study_path / "2_results"
|
||||
self.insights_path = self.study_path / "3_insights"
|
||||
|
||||
# Load study config if available
|
||||
self.config = self._load_study_config()
|
||||
|
||||
def _load_study_config(self) -> Dict[str, Any]:
|
||||
"""Load optimization_config.json if it exists."""
|
||||
config_path = self.setup_path / "optimization_config.json"
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def can_generate(self) -> bool:
|
||||
"""
|
||||
Check if this insight can be generated for the study.
|
||||
|
||||
Returns:
|
||||
True if all required data is available
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _generate(self, config: InsightConfig) -> InsightResult:
|
||||
"""
|
||||
Core generation logic. Implemented by subclasses.
|
||||
|
||||
Args:
|
||||
config: Insight configuration
|
||||
|
||||
Returns:
|
||||
InsightResult with HTML path and/or Plotly data
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate(self, config: Optional[InsightConfig] = None) -> InsightResult:
|
||||
"""
|
||||
Generate the insight visualization.
|
||||
|
||||
Args:
|
||||
config: Optional configuration (uses defaults if None)
|
||||
|
||||
Returns:
|
||||
InsightResult with generated content
|
||||
"""
|
||||
if config is None:
|
||||
config = InsightConfig()
|
||||
|
||||
# Ensure output directory exists
|
||||
if config.output_dir is None:
|
||||
config.output_dir = self.insights_path
|
||||
config.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check prerequisites
|
||||
if not self.can_generate():
|
||||
return InsightResult(
|
||||
success=False,
|
||||
error=f"Cannot generate {self.name}: required data not found"
|
||||
)
|
||||
|
||||
try:
|
||||
return self._generate(config)
|
||||
except Exception as e:
|
||||
return InsightResult(
|
||||
success=False,
|
||||
error=f"Error generating {self.name}: {str(e)}"
|
||||
)
|
||||
|
||||
def generate_html(
|
||||
self,
|
||||
trial_id: Optional[int] = None,
|
||||
**kwargs
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
Convenience method to generate standalone HTML.
|
||||
|
||||
Args:
|
||||
trial_id: Specific trial to visualize (None = best)
|
||||
**kwargs: Additional config options
|
||||
|
||||
Returns:
|
||||
Path to generated HTML file, or None on failure
|
||||
"""
|
||||
config = InsightConfig(trial_id=trial_id, extra=kwargs)
|
||||
result = self.generate(config)
|
||||
return result.html_path if result.success else None
|
||||
|
||||
def get_plotly_data(
|
||||
self,
|
||||
trial_id: Optional[int] = None,
|
||||
**kwargs
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get Plotly figure data for dashboard embedding.
|
||||
|
||||
Args:
|
||||
trial_id: Specific trial to visualize (None = best)
|
||||
**kwargs: Additional config options
|
||||
|
||||
Returns:
|
||||
Plotly figure as dictionary, or None on failure
|
||||
"""
|
||||
config = InsightConfig(trial_id=trial_id, extra=kwargs)
|
||||
result = self.generate(config)
|
||||
return result.plotly_figure if result.success else None
|
||||
|
||||
def get_summary(self, trial_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get key metrics summary without full visualization.
|
||||
|
||||
Args:
|
||||
trial_id: Specific trial (None = best)
|
||||
|
||||
Returns:
|
||||
Dictionary of key metrics
|
||||
"""
|
||||
config = InsightConfig(trial_id=trial_id)
|
||||
result = self.generate(config)
|
||||
return result.summary if result.success else None
|
||||
|
||||
|
||||
class InsightRegistry:
|
||||
"""
|
||||
Central registry for available insight types.
|
||||
|
||||
Usage:
|
||||
registry = InsightRegistry()
|
||||
registry.register(ZernikeWFEInsight)
|
||||
|
||||
# Get insight for a study
|
||||
insight = registry.get('zernike_wfe', study_path)
|
||||
|
||||
# List available insights for a study
|
||||
available = registry.list_available(study_path)
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_insights: Dict[str, Type[StudyInsight]] = {}
|
||||
|
||||
def __new__(cls):
|
||||
"""Singleton pattern."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._insights = {}
|
||||
return cls._instance
|
||||
|
||||
def register(self, insight_class: Type[StudyInsight]) -> None:
|
||||
"""
|
||||
Register an insight type.
|
||||
|
||||
Args:
|
||||
insight_class: StudyInsight subclass to register
|
||||
"""
|
||||
self._insights[insight_class.insight_type] = insight_class
|
||||
|
||||
def get(self, insight_type: str, study_path: Path) -> Optional[StudyInsight]:
|
||||
"""
|
||||
Get an insight instance for a study.
|
||||
|
||||
Args:
|
||||
insight_type: Registered insight type ID
|
||||
study_path: Path to study directory
|
||||
|
||||
Returns:
|
||||
Configured insight instance, or None if not found
|
||||
"""
|
||||
if insight_type not in self._insights:
|
||||
return None
|
||||
return self._insights[insight_type](study_path)
|
||||
|
||||
def list_all(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all registered insight types.
|
||||
|
||||
Returns:
|
||||
List of insight metadata dictionaries
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'type': cls.insight_type,
|
||||
'name': cls.name,
|
||||
'description': cls.description,
|
||||
'applicable_to': cls.applicable_to
|
||||
}
|
||||
for cls in self._insights.values()
|
||||
]
|
||||
|
||||
def list_available(self, study_path: Path) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List insights that can be generated for a specific study.
|
||||
|
||||
Args:
|
||||
study_path: Path to study directory
|
||||
|
||||
Returns:
|
||||
List of available insight metadata
|
||||
"""
|
||||
available = []
|
||||
for insight_type, cls in self._insights.items():
|
||||
try:
|
||||
insight = cls(study_path)
|
||||
if insight.can_generate():
|
||||
available.append({
|
||||
'type': insight_type,
|
||||
'name': cls.name,
|
||||
'description': cls.description
|
||||
})
|
||||
except Exception:
|
||||
pass # Skip insights that fail to initialize
|
||||
return available
|
||||
|
||||
|
||||
# Global registry instance
|
||||
_registry = InsightRegistry()
|
||||
|
||||
|
||||
def register_insight(insight_class: Type[StudyInsight]) -> Type[StudyInsight]:
|
||||
"""
|
||||
Decorator to register an insight class.
|
||||
|
||||
Usage:
|
||||
@register_insight
|
||||
class MyInsight(StudyInsight):
|
||||
insight_type = 'my_insight'
|
||||
...
|
||||
"""
|
||||
_registry.register(insight_class)
|
||||
return insight_class
|
||||
|
||||
|
||||
def get_insight(insight_type: str, study_path: Path) -> Optional[StudyInsight]:
|
||||
"""Get an insight instance by type."""
|
||||
return _registry.get(insight_type, study_path)
|
||||
|
||||
|
||||
def list_insights() -> List[Dict[str, Any]]:
|
||||
"""List all registered insight types."""
|
||||
return _registry.list_all()
|
||||
|
||||
|
||||
def list_available_insights(study_path: Path) -> List[Dict[str, Any]]:
|
||||
"""List insights available for a specific study."""
|
||||
return _registry.list_available(study_path)
|
||||
Reference in New Issue
Block a user