Files
Anto01 d19fc39a2a feat: Add OPD method support to Zernike visualization with Standard/OPD toggle
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>
2025-12-22 21:03:19 -05:00

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)