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>
This commit is contained in:
@@ -8,36 +8,57 @@ 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_wfe | Zernike WFE Analysis | 3D wavefront error with Zernike decomp |
|
||||
| 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 |
|
||||
| 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
|
||||
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")
|
||||
|
||||
# List available insights for a study
|
||||
available = list_available_insights(study_path)
|
||||
print(available)
|
||||
# Get recommended insights based on objectives
|
||||
recommendations = recommend_insights_for_study(study_path)
|
||||
for rec in recommendations:
|
||||
print(f"{rec['type']}: {rec['reason']}")
|
||||
|
||||
# Generate a specific insight
|
||||
insight = get_insight('zernike_wfe', study_path)
|
||||
if insight and insight.can_generate():
|
||||
result = insight.generate()
|
||||
print(f"Generated: {result.html_path}")
|
||||
print(f"Summary: {result.summary}")
|
||||
# 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:
|
||||
@@ -49,6 +70,12 @@ 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
|
||||
```
|
||||
@@ -59,15 +86,22 @@ 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
|
||||
@@ -79,6 +113,8 @@ __all__ = [
|
||||
'StudyInsight',
|
||||
'InsightConfig',
|
||||
'InsightResult',
|
||||
'InsightSpec',
|
||||
'InsightReport',
|
||||
'InsightRegistry',
|
||||
'register_insight',
|
||||
|
||||
@@ -86,9 +122,14 @@ __all__ = [
|
||||
'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',
|
||||
@@ -142,10 +183,14 @@ if __name__ == '__main__':
|
||||
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(" 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")
|
||||
@@ -166,6 +211,84 @@ if __name__ == '__main__':
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user