feat: improve optical report with embedded Plotly and 4x PNG export

- Embed Plotly.js inline for offline viewing (fixes CDN loading issues)
- Add 4x resolution PNG export for all charts via toImageButtonOptions
- Add SAT3_Trajectory_V7 study (TPE warm-start from V5, 86 trials, WS=277.37)
- Include V7 optimization report and configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 19:27:56 -05:00
parent 65711cdbf1
commit ca4101dcb0
15 changed files with 1386 additions and 8 deletions

View File

@@ -129,6 +129,24 @@ _BLUE_PALETTE = ['#2563eb', '#3b82f6', '#60a5fa', '#93c5fd', '#1d4ed8', '#1e40af
_MODE_COLORS = ['#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', '#4f46e5']
_MODE_DASHES = ['solid', 'dash', 'dot', 'dashdot', 'longdash', 'longdashdot', 'solid']
# High-resolution PNG export settings
PNG_EXPORT_SCALE = 4 # 4x resolution (e.g., 700px width -> 2800px export)
PNG_EXPORT_FORMAT = 'png'
def get_plotly_config(filename_prefix="plot"):
"""Get Plotly config with high-resolution PNG export settings."""
return {
'toImageButtonOptions': {
'format': PNG_EXPORT_FORMAT,
'filename': filename_prefix,
'height': None, # Use current height
'width': None, # Use current width
'scale': PNG_EXPORT_SCALE, # 4x resolution multiplier
},
'displaylogo': False,
'modeBarButtonsToAdd': ['hoverClosest3d'],
}
# ============================================================================
# Data Extraction Helpers
@@ -483,7 +501,7 @@ def _metric_color(value, target):
return 'color: #dc2626; font-weight: 700;' # red
def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, downsample=PLOT_DOWNSAMPLE):
def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, downsample=PLOT_DOWNSAMPLE, include_plotlyjs=False):
"""Create a 3D surface plot of residual WFE."""
Xm, Ym, Wm = X[mask], Y[mask], W_res[mask]
@@ -578,7 +596,9 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d
height=650,
width=1200,
)
return fig.to_html(include_plotlyjs=False, full_html=False, div_id=f"surface_{title.replace(' ','_')}")
div_id = f"surface_{title.replace(' ','_')}"
config = get_plotly_config(f"WFE_Surface_{title.replace(' ','_')}")
return fig.to_html(include_plotlyjs=include_plotlyjs, full_html=False, div_id=div_id, config=config)
def make_bar_chart(coeffs, title="Zernike Coefficients", max_modes=50):
@@ -606,7 +626,9 @@ def make_bar_chart(coeffs, title="Zernike Coefficients", max_modes=50):
tickfont=dict(color='#475569')),
yaxis=dict(autorange='reversed', tickfont=dict(size=10, color='#1e293b')),
)
return fig.to_html(include_plotlyjs=False, full_html=False, div_id=f"bar_{title.replace(' ','_')}")
div_id = f"bar_{title.replace(' ','_')}"
config = get_plotly_config(f"Zernike_Coefficients_{title.replace(' ','_')}")
return fig.to_html(include_plotlyjs=False, full_html=False, div_id=div_id, config=config)
def make_trajectory_plot(angles, coefficients_relative, mode_groups, sensitivity, title=""):
@@ -693,7 +715,8 @@ def make_trajectory_plot(angles, coefficients_relative, mode_groups, sensitivity
bordercolor='#e2e8f0', borderwidth=1,
font=dict(size=10, color='#1e293b')),
)
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="trajectory_plot")
config = get_plotly_config("Zernike_Trajectory")
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="trajectory_plot", config=config)
def make_sensitivity_bar(sensitivity_dict):
@@ -732,7 +755,8 @@ def make_sensitivity_bar(sensitivity_dict):
bordercolor='#e2e8f0', borderwidth=1,
font=dict(color='#1e293b')),
)
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="sensitivity_bar")
config = get_plotly_config("Sensitivity_Axial_vs_Lateral")
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="sensitivity_bar", config=config)
def make_per_angle_rms_plot(angle_rms_data, ref_angle=20):
@@ -759,7 +783,8 @@ def make_per_angle_rms_plot(angle_rms_data, ref_angle=20):
tickfont=dict(color='#475569')),
xaxis=dict(tickfont=dict(color='#1e293b', size=12)),
)
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="per_angle_rms")
config = get_plotly_config("Per_Angle_RMS_WFE")
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="per_angle_rms", config=config)
# ============================================================================
@@ -943,10 +968,11 @@ def generate_report(
print("\nGenerating HTML report...")
# Surface plots
# First plot embeds the full Plotly library (~3.5MB) for offline viewing
surf_40 = make_surface_plot(
angle_results[40]['X_rel'], angle_results[40]['Y_rel'],
angle_results[40]['rms_rel']['W_res_filt'], angle_results[40]['rms_rel']['mask'],
inner_radius=inner_radius, title="40 vs 20"
inner_radius=inner_radius, title="40 vs 20", include_plotlyjs=True
)
surf_60 = make_surface_plot(
angle_results[60]['X_rel'], angle_results[60]['Y_rel'],
@@ -1123,7 +1149,7 @@ def generate_report(
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<script src="https://cdn.plot.ly/plotly-3.3.1.min.js"></script>
<!-- Plotly.js is embedded inline in the first surface plot for offline viewing -->
<style>
:root {{
--bg-primary: #ffffff;