Major improvements to Zernike WFE visualization: - Add ZernikeDashboardInsight: Unified dashboard with all orientations (40°, 60°, 90°) on one page with light theme and executive summary - Add OPD method toggle: Switch between Standard (Z-only) and OPD (X,Y,Z) methods in ZernikeWFEInsight with interactive buttons - Add lateral displacement maps: Visualize X,Y displacement for each orientation - Add displacement component views: Toggle between WFE, ΔX, ΔY, ΔZ in relative views - Add metrics comparison table showing both methods side-by-side New extractors: - extract_zernike_figure.py: ZernikeOPDExtractor using BDF geometry interpolation - extract_zernike_opd.py: Parabola-based OPD with focal length Key finding: OPD method gives 8-11% higher WFE values than Standard method (more conservative/accurate for surfaces with lateral displacement under gravity) Documentation updates: - SYS_12: Added E22 ZernikeOPD as recommended method - SYS_16: Added ZernikeDashboard, updated ZernikeWFE with OPD features - Cheatsheet: Added Zernike method comparison table 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
357 lines
11 KiB
Python
357 lines
11 KiB
Python
"""
|
|
Atomizer Study Insights Module
|
|
|
|
Provides physics-focused visualizations for FEA optimization results.
|
|
Unlike the Analysis page (optimizer-centric), Insights show the engineering
|
|
reality of specific designs through interactive 3D visualizations.
|
|
|
|
Architecture:
|
|
- StudyInsight: Abstract base class for all insight types
|
|
- InsightRegistry: Central registry for available insight types
|
|
- InsightSpec: Config-driven insight specification (from optimization_config.json)
|
|
- InsightReport: Aggregates insights into HTML/PDF report with appendix
|
|
- Each insight generates standalone HTML or Plotly data for dashboard
|
|
|
|
Available Insight Types:
|
|
-----------------------
|
|
| Type ID | Name | Description |
|
|
|------------------|------------------------|------------------------------------------|
|
|
| zernike_dashboard| Zernike Dashboard | RECOMMENDED: Unified WFE dashboard |
|
|
| zernike_wfe | Zernike WFE Analysis | 3D wavefront error with Zernike decomp |
|
|
| msf_zernike | MSF Zernike Analysis | Mid-spatial frequency band decomposition |
|
|
| stress_field | Stress Distribution | Von Mises stress contours |
|
|
| modal | Modal Analysis | Natural frequencies and mode shapes |
|
|
| thermal | Thermal Analysis | Temperature distribution |
|
|
| design_space | Design Space Explorer | Parameter-objective relationships |
|
|
|
|
Config Schema (in optimization_config.json):
|
|
-------------------------------------------
|
|
```json
|
|
"insights": [
|
|
{
|
|
"type": "zernike_wfe",
|
|
"name": "WFE at 40 vs 20 deg",
|
|
"enabled": true,
|
|
"linked_objective": "rel_filtered_rms_40_vs_20",
|
|
"config": { "target_subcase": "3", "reference_subcase": "2" },
|
|
"include_in_report": true
|
|
}
|
|
]
|
|
```
|
|
|
|
Quick Start:
|
|
-----------
|
|
```python
|
|
from optimization_engine.insights import (
|
|
get_insight, list_available_insights,
|
|
get_configured_insights, recommend_insights_for_study, InsightReport
|
|
)
|
|
from pathlib import Path
|
|
|
|
study_path = Path("studies/my_study")
|
|
|
|
# Get recommended insights based on objectives
|
|
recommendations = recommend_insights_for_study(study_path)
|
|
for rec in recommendations:
|
|
print(f"{rec['type']}: {rec['reason']}")
|
|
|
|
# Generate report from configured insights
|
|
report = InsightReport(study_path)
|
|
results = report.generate_all() # Uses specs from optimization_config.json
|
|
report_path = report.generate_report_html() # Creates STUDY_INSIGHTS_REPORT.html
|
|
```
|
|
|
|
CLI Usage:
|
|
---------
|
|
```bash
|
|
# Generate all available insights for a study
|
|
python -m optimization_engine.insights generate studies/my_study
|
|
|
|
# Generate specific insight type
|
|
python -m optimization_engine.insights generate studies/my_study --type zernike_wfe
|
|
|
|
# Generate full report from config
|
|
python -m optimization_engine.insights report studies/my_study
|
|
|
|
# Get recommendations for a study
|
|
python -m optimization_engine.insights recommend studies/my_study
|
|
|
|
# List available insight types
|
|
python -m optimization_engine.insights list
|
|
```
|
|
"""
|
|
|
|
# Import base classes first
|
|
from .base import (
|
|
StudyInsight,
|
|
InsightConfig,
|
|
InsightResult,
|
|
InsightSpec,
|
|
InsightReport,
|
|
InsightRegistry,
|
|
register_insight,
|
|
get_insight,
|
|
list_insights,
|
|
list_available_insights,
|
|
get_configured_insights,
|
|
recommend_insights_for_study,
|
|
)
|
|
|
|
# Import insight implementations (triggers @register_insight decorators)
|
|
from .zernike_wfe import ZernikeWFEInsight
|
|
from .zernike_opd_comparison import ZernikeOPDComparisonInsight
|
|
from .msf_zernike import MSFZernikeInsight
|
|
from .zernike_dashboard import ZernikeDashboardInsight # NEW: Unified dashboard
|
|
from .stress_field import StressFieldInsight
|
|
from .modal_analysis import ModalInsight
|
|
from .thermal_field import ThermalInsight
|
|
from .design_space import DesignSpaceInsight
|
|
|
|
# Public API
|
|
__all__ = [
|
|
# Base classes
|
|
'StudyInsight',
|
|
'InsightConfig',
|
|
'InsightResult',
|
|
'InsightSpec',
|
|
'InsightReport',
|
|
'InsightRegistry',
|
|
'register_insight',
|
|
|
|
# API functions
|
|
'get_insight',
|
|
'list_insights',
|
|
'list_available_insights',
|
|
'get_configured_insights',
|
|
'recommend_insights_for_study',
|
|
|
|
# Insight implementations
|
|
'ZernikeWFEInsight',
|
|
'ZernikeOPDComparisonInsight',
|
|
'MSFZernikeInsight',
|
|
'ZernikeDashboardInsight', # NEW: Unified dashboard
|
|
'StressFieldInsight',
|
|
'ModalInsight',
|
|
'ThermalInsight',
|
|
'DesignSpaceInsight',
|
|
]
|
|
|
|
|
|
def generate_all_insights(study_path, output_dir=None):
|
|
"""
|
|
Generate all available insights for a study.
|
|
|
|
Args:
|
|
study_path: Path to study directory
|
|
output_dir: Optional output directory (defaults to study/3_insights/)
|
|
|
|
Returns:
|
|
List of InsightResult objects
|
|
"""
|
|
from pathlib import Path
|
|
study_path = Path(study_path)
|
|
|
|
results = []
|
|
available = list_available_insights(study_path)
|
|
|
|
for info in available:
|
|
insight = get_insight(info['type'], study_path)
|
|
if insight:
|
|
config = InsightConfig()
|
|
if output_dir:
|
|
config.output_dir = Path(output_dir)
|
|
|
|
result = insight.generate(config)
|
|
results.append({
|
|
'type': info['type'],
|
|
'name': info['name'],
|
|
'result': result
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
# CLI entry point
|
|
if __name__ == '__main__':
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
def print_usage():
|
|
print("Atomizer Study Insights")
|
|
print("=" * 50)
|
|
print()
|
|
print("Usage:")
|
|
print(" python -m optimization_engine.insights list")
|
|
print(" python -m optimization_engine.insights generate <study_path> [--type TYPE]")
|
|
print(" python -m optimization_engine.insights report <study_path>")
|
|
print(" python -m optimization_engine.insights recommend <study_path>")
|
|
print()
|
|
print("Commands:")
|
|
print(" list - List all registered insight types")
|
|
print(" generate - Generate insights for a study")
|
|
print(" report - Generate full report from config")
|
|
print(" recommend - Get AI recommendations for insights")
|
|
print()
|
|
print("Options:")
|
|
print(" --type TYPE Generate only the specified insight type")
|
|
print()
|
|
|
|
if len(sys.argv) < 2:
|
|
print_usage()
|
|
sys.exit(0)
|
|
|
|
command = sys.argv[1]
|
|
|
|
if command == 'list':
|
|
print("\nRegistered Insight Types:")
|
|
print("-" * 60)
|
|
for info in list_insights():
|
|
print(f" {info['type']:15} - {info['name']}")
|
|
print(f" {info['description']}")
|
|
print(f" Applies to: {', '.join(info['applicable_to'])}")
|
|
print()
|
|
|
|
elif command == 'recommend':
|
|
if len(sys.argv) < 3:
|
|
print("Error: Missing study path")
|
|
print_usage()
|
|
sys.exit(1)
|
|
|
|
study_path = Path(sys.argv[2])
|
|
if not study_path.exists():
|
|
print(f"Error: Study path does not exist: {study_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"\nRecommended Insights for: {study_path.name}")
|
|
print("-" * 60)
|
|
|
|
recommendations = recommend_insights_for_study(study_path)
|
|
if not recommendations:
|
|
print("No recommendations available (no objectives found)")
|
|
sys.exit(0)
|
|
|
|
for rec in recommendations:
|
|
linked = f" -> {rec['linked_objective']}" if rec.get('linked_objective') else ""
|
|
print(f"\n {rec['type']}{linked}")
|
|
print(f" Name: {rec['name']}")
|
|
print(f" Reason: {rec['reason']}")
|
|
|
|
print()
|
|
print("Add these to optimization_config.json under 'insights' key")
|
|
|
|
elif command == 'report':
|
|
if len(sys.argv) < 3:
|
|
print("Error: Missing study path")
|
|
print_usage()
|
|
sys.exit(1)
|
|
|
|
study_path = Path(sys.argv[2])
|
|
if not study_path.exists():
|
|
print(f"Error: Study path does not exist: {study_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"\nGenerating Insight Report for: {study_path.name}")
|
|
print("-" * 60)
|
|
|
|
# Check for configured insights
|
|
specs = get_configured_insights(study_path)
|
|
if not specs:
|
|
print("No insights configured in optimization_config.json")
|
|
print("Using recommendations based on objectives...")
|
|
recommendations = recommend_insights_for_study(study_path)
|
|
specs = [
|
|
InsightSpec(
|
|
type=rec['type'],
|
|
name=rec['name'],
|
|
linked_objective=rec.get('linked_objective'),
|
|
config=rec.get('config', {})
|
|
)
|
|
for rec in recommendations
|
|
]
|
|
|
|
if not specs:
|
|
print("No insights to generate")
|
|
sys.exit(0)
|
|
|
|
print(f"Generating {len(specs)} insight(s)...")
|
|
|
|
report = InsightReport(study_path)
|
|
results = report.generate_all(specs)
|
|
|
|
for result in results:
|
|
status = "OK" if result.success else f"FAIL: {result.error}"
|
|
print(f" {result.insight_name}: {status}")
|
|
|
|
if any(r.success for r in results):
|
|
report_path = report.generate_report_html()
|
|
print(f"\nReport generated: {report_path}")
|
|
print("Open in browser and use 'Save as PDF' button to export")
|
|
else:
|
|
print("\nNo insights generated successfully")
|
|
|
|
elif command == 'generate':
|
|
if len(sys.argv) < 3:
|
|
print("Error: Missing study path")
|
|
print_usage()
|
|
sys.exit(1)
|
|
|
|
study_path = Path(sys.argv[2])
|
|
if not study_path.exists():
|
|
print(f"Error: Study path does not exist: {study_path}")
|
|
sys.exit(1)
|
|
|
|
# Parse options
|
|
insight_type = None
|
|
for i, arg in enumerate(sys.argv[3:], 3):
|
|
if arg == '--type' and i + 1 < len(sys.argv):
|
|
insight_type = sys.argv[i + 1]
|
|
|
|
print(f"\nGenerating insights for: {study_path}")
|
|
print("-" * 60)
|
|
|
|
if insight_type:
|
|
# Generate specific type
|
|
insight = get_insight(insight_type, study_path)
|
|
if insight is None:
|
|
print(f"Error: Unknown insight type: {insight_type}")
|
|
sys.exit(1)
|
|
|
|
if not insight.can_generate():
|
|
print(f"Cannot generate {insight_type}: required data not found")
|
|
sys.exit(1)
|
|
|
|
result = insight.generate()
|
|
if result.success:
|
|
print(f"Generated: {result.html_path}")
|
|
if result.summary:
|
|
print(f"Summary: {result.summary}")
|
|
else:
|
|
print(f"Error: {result.error}")
|
|
else:
|
|
# Generate all available
|
|
available = list_available_insights(study_path)
|
|
if not available:
|
|
print("No insights available for this study")
|
|
sys.exit(0)
|
|
|
|
print(f"Found {len(available)} available insight(s)")
|
|
print()
|
|
|
|
for info in available:
|
|
print(f"Generating {info['name']}...")
|
|
insight = get_insight(info['type'], study_path)
|
|
result = insight.generate()
|
|
|
|
if result.success:
|
|
print(f" Created: {result.html_path}")
|
|
else:
|
|
print(f" Error: {result.error}")
|
|
|
|
print()
|
|
print("Done!")
|
|
|
|
else:
|
|
print(f"Unknown command: {command}")
|
|
print_usage()
|
|
sys.exit(1)
|