fix: replace deprecated titlefont with title.font for Plotly compat
This commit is contained in:
@@ -115,6 +115,18 @@ SUBCASE_ANGLE_MAP = {
|
||||
'90': 90, '20': 20, '40': 40, '60': 60,
|
||||
}
|
||||
|
||||
# ── Professional light-theme Plotly layout defaults ──
|
||||
_PLOTLY_LIGHT_LAYOUT = dict(
|
||||
paper_bgcolor='#ffffff',
|
||||
plot_bgcolor='#f8fafc',
|
||||
font=dict(family="Inter, system-ui, sans-serif", color='#1e293b', size=12),
|
||||
)
|
||||
|
||||
# Professional blue palette for charts
|
||||
_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']
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Extraction Helpers
|
||||
@@ -269,14 +281,14 @@ def aberration_magnitudes(coeffs):
|
||||
# HTML Report Generation
|
||||
# ============================================================================
|
||||
|
||||
def status_badge(value, target, unit='nm'):
|
||||
"""Return pass/fail badge HTML."""
|
||||
def _metric_color(value, target):
|
||||
"""Return a CSS color class based on value vs target."""
|
||||
if value <= target:
|
||||
return f'<span class="badge pass">✅ {value:.2f} {unit} ≤ {target:.1f}</span>'
|
||||
return 'color: #16a34a; font-weight: 700;' # green
|
||||
ratio = value / target
|
||||
if ratio < 1.5:
|
||||
return f'<span class="badge warn">⚠️ {value:.2f} {unit} ({ratio:.1f}× target)</span>'
|
||||
return f'<span class="badge fail">❌ {value:.2f} {unit} ({ratio:.1f}× target)</span>'
|
||||
return 'color: #d97706; font-weight: 700;' # amber
|
||||
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):
|
||||
@@ -330,7 +342,8 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d
|
||||
lighting=dict(ambient=0.4, diffuse=0.8, specular=0.3, roughness=0.5, fresnel=0.2),
|
||||
lightposition=dict(x=100, y=200, z=300),
|
||||
showscale=True,
|
||||
colorbar=dict(title=dict(text="nm", side="right"), thickness=12, len=0.5, tickformat=".1f"),
|
||||
colorbar=dict(title=dict(text="nm", side="right", font=dict(color='#1e293b')), thickness=12, len=0.5, tickformat=".1f",
|
||||
tickfont=dict(color='#1e293b')),
|
||||
hovertemplate="X: %{x:.1f}<br>Y: %{y:.1f}<br>Residual: %{z:.2f} nm<extra></extra>"
|
||||
))
|
||||
|
||||
@@ -341,7 +354,7 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d
|
||||
x=cx + inner_radius*np.cos(theta_c),
|
||||
y=cy + inner_radius*np.sin(theta_c),
|
||||
z=np.zeros(80),
|
||||
mode='lines', line=dict(color='white', width=2),
|
||||
mode='lines', line=dict(color='#64748b', width=2),
|
||||
name='Central Hole', showlegend=False, hoverinfo='name'
|
||||
))
|
||||
except Exception:
|
||||
@@ -355,20 +368,21 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d
|
||||
fig.update_layout(
|
||||
scene=dict(
|
||||
camera=dict(eye=dict(x=1.2, y=1.2, z=0.8), up=dict(x=0, y=0, z=1)),
|
||||
xaxis=dict(title="X (mm)", showgrid=True, gridcolor='rgba(128,128,128,0.3)',
|
||||
showbackground=True, backgroundcolor='rgba(240,240,240,0.9)'),
|
||||
yaxis=dict(title="Y (mm)", showgrid=True, gridcolor='rgba(128,128,128,0.3)',
|
||||
showbackground=True, backgroundcolor='rgba(240,240,240,0.9)'),
|
||||
zaxis=dict(title="Residual (nm)", range=[-max_amp*3.0, max_amp*3.0],
|
||||
showgrid=True, gridcolor='rgba(128,128,128,0.3)',
|
||||
showbackground=True, backgroundcolor='rgba(230,230,250,0.9)'),
|
||||
xaxis=dict(title=dict(text="X (mm)", font=dict(color='#1e293b')), showgrid=True, gridcolor='#e2e8f0',
|
||||
showbackground=True, backgroundcolor='#f1f5f9',
|
||||
tickfont=dict(color='#475569')),
|
||||
yaxis=dict(title=dict(text="Y (mm)", font=dict(color='#1e293b')), showgrid=True, gridcolor='#e2e8f0',
|
||||
showbackground=True, backgroundcolor='#f1f5f9',
|
||||
tickfont=dict(color='#475569')),
|
||||
zaxis=dict(title=dict(text="Residual (nm)", font=dict(color='#1e293b')), range=[-max_amp*3.0, max_amp*3.0],
|
||||
showgrid=True, gridcolor='#e2e8f0',
|
||||
showbackground=True, backgroundcolor='#eff6ff',
|
||||
tickfont=dict(color='#475569')),
|
||||
aspectmode='manual',
|
||||
aspectratio=dict(x=1, y=1, z=0.4),
|
||||
),
|
||||
margin=dict(t=30, b=10, l=10, r=10),
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(0,0,0,0)',
|
||||
font=dict(color='#e0e0e0'),
|
||||
**_PLOTLY_LIGHT_LAYOUT,
|
||||
height=500,
|
||||
width=700,
|
||||
)
|
||||
@@ -378,22 +392,27 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d
|
||||
def make_bar_chart(coeffs, title="Zernike Coefficients", max_modes=30):
|
||||
"""Create horizontal bar chart of Zernike coefficient magnitudes."""
|
||||
n = min(len(coeffs), max_modes)
|
||||
labels = [zernike_label(j) for j in range(1, n+1)]
|
||||
labels = [str(zernike_label(j))[:40] for j in range(1, n+1)]
|
||||
vals = np.abs(coeffs[:n])
|
||||
text_vals = [f"{v:.3f}" for v in vals]
|
||||
|
||||
fig = go.Figure(go.Bar(
|
||||
x=vals, y=labels, orientation='h',
|
||||
marker_color='#6366f1',
|
||||
marker_color=_BLUE_PALETTE[0],
|
||||
text=text_vals,
|
||||
textposition='outside',
|
||||
textfont=dict(size=9, color='#475569'),
|
||||
hovertemplate="%{y}<br>|c| = %{x:.3f} nm<extra></extra>",
|
||||
))
|
||||
fig.update_layout(
|
||||
height=max(400, n*22),
|
||||
margin=dict(t=30, b=10, l=200, r=20),
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(17,24,39,0.8)',
|
||||
font=dict(color='#e0e0e0', size=10),
|
||||
xaxis=dict(title="|Coefficient| (nm)", gridcolor='rgba(128,128,128,0.2)'),
|
||||
yaxis=dict(autorange='reversed'),
|
||||
margin=dict(t=30, b=10, l=220, r=60),
|
||||
**_PLOTLY_LIGHT_LAYOUT,
|
||||
xaxis=dict(title=dict(text="|Coefficient| (nm)", font=dict(color='#1e293b')),
|
||||
gridcolor='#e2e8f0', zeroline=True,
|
||||
zerolinecolor='#cbd5e1',
|
||||
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(' ','_')}")
|
||||
|
||||
@@ -402,8 +421,6 @@ def make_trajectory_plot(angles, coefficients_relative, mode_groups, sensitivity
|
||||
"""Create trajectory visualization: Zernike modes vs elevation angle."""
|
||||
fig = go.Figure()
|
||||
|
||||
# Plot each mode group
|
||||
colors = ['#f59e0b', '#ef4444', '#10b981', '#6366f1', '#ec4899', '#14b8a6', '#f97316']
|
||||
color_idx = 0
|
||||
|
||||
for group_name, noll_indices in mode_groups.items():
|
||||
@@ -411,30 +428,77 @@ def make_trajectory_plot(angles, coefficients_relative, mode_groups, sensitivity
|
||||
if not indices:
|
||||
continue
|
||||
|
||||
color = _MODE_COLORS[color_idx % len(_MODE_COLORS)]
|
||||
dash = _MODE_DASHES[color_idx % len(_MODE_DASHES)]
|
||||
|
||||
# RSS of modes in this group at each angle
|
||||
rss = np.sqrt(np.sum(coefficients_relative[:, indices]**2, axis=1))
|
||||
color = colors[color_idx % len(colors)]
|
||||
display_name = MODE_NAMES.get(group_name, group_name)
|
||||
|
||||
# Group RSS trace (thick)
|
||||
fig.add_trace(go.Scatter(
|
||||
x=angles, y=rss,
|
||||
mode='lines+markers',
|
||||
name=MODE_NAMES.get(group_name, group_name),
|
||||
line=dict(color=color, width=2),
|
||||
marker=dict(size=8),
|
||||
hovertemplate=f"{group_name}<br>%{{x}}°: %{{y:.2f}} nm<extra></extra>"
|
||||
name=f"{display_name} (RSS)",
|
||||
line=dict(color=color, width=3, dash=dash),
|
||||
marker=dict(size=10, symbol='circle'),
|
||||
hovertemplate=f"{display_name} RSS<br>%{{x:.1f}}°: %{{y:.3f}} nm<extra></extra>",
|
||||
legendgroup=group_name,
|
||||
))
|
||||
|
||||
# Individual mode traces (thin, same color, lighter)
|
||||
for idx_i, noll_idx in enumerate(noll_indices):
|
||||
col_idx = noll_idx - 5
|
||||
if col_idx < 0 or col_idx >= coefficients_relative.shape[1]:
|
||||
continue
|
||||
mode_vals = np.abs(coefficients_relative[:, col_idx])
|
||||
mode_label = f"J{noll_idx}"
|
||||
fig.add_trace(go.Scatter(
|
||||
x=angles, y=mode_vals,
|
||||
mode='lines+markers',
|
||||
name=f" {mode_label}",
|
||||
line=dict(color=color, width=1, dash='dot'),
|
||||
marker=dict(size=5, symbol='diamond'),
|
||||
opacity=0.5,
|
||||
hovertemplate=f"{mode_label}<br>%{{x:.1f}}°: %{{y:.3f}} nm<extra></extra>",
|
||||
legendgroup=group_name,
|
||||
showlegend=True,
|
||||
))
|
||||
|
||||
color_idx += 1
|
||||
|
||||
# Check if log scale would help (values span >2 orders of magnitude)
|
||||
all_y = []
|
||||
for trace in fig.data:
|
||||
all_y.extend([v for v in trace.y if v > 0])
|
||||
use_log = False
|
||||
if all_y:
|
||||
y_min, y_max = min(all_y), max(all_y)
|
||||
if y_max > 0 and y_min > 0 and (y_max / y_min) > 100:
|
||||
use_log = True
|
||||
|
||||
yaxis_cfg = dict(
|
||||
title=dict(text="RMS (nm)", font=dict(color='#1e293b')),
|
||||
gridcolor='#e2e8f0',
|
||||
zeroline=True, zerolinecolor='#cbd5e1',
|
||||
tickfont=dict(color='#475569'),
|
||||
)
|
||||
if use_log:
|
||||
yaxis_cfg['type'] = 'log'
|
||||
yaxis_cfg['title'] = dict(text="RMS (nm) — log scale", font=dict(color='#1e293b'))
|
||||
|
||||
fig.update_layout(
|
||||
height=400,
|
||||
margin=dict(t=30, b=40, l=60, r=20),
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(17,24,39,0.8)',
|
||||
font=dict(color='#e0e0e0'),
|
||||
xaxis=dict(title="Elevation Angle (°)", gridcolor='rgba(128,128,128,0.2)',
|
||||
tickvals=angles, dtick=10),
|
||||
yaxis=dict(title="RMS (nm)", gridcolor='rgba(128,128,128,0.2)'),
|
||||
legend=dict(x=0.02, y=0.98, bgcolor='rgba(17,24,39,0.7)'),
|
||||
height=450,
|
||||
margin=dict(t=30, b=50, l=70, r=20),
|
||||
**_PLOTLY_LIGHT_LAYOUT,
|
||||
xaxis=dict(title=dict(text="Elevation Angle (°)", font=dict(color='#1e293b')),
|
||||
gridcolor='#e2e8f0',
|
||||
tickvals=list(angles), dtick=10,
|
||||
tickfont=dict(color='#475569')),
|
||||
yaxis=yaxis_cfg,
|
||||
legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.9)',
|
||||
bordercolor='#e2e8f0', borderwidth=1,
|
||||
font=dict(size=10, color='#1e293b')),
|
||||
)
|
||||
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="trajectory_plot")
|
||||
|
||||
@@ -444,29 +508,36 @@ def make_sensitivity_bar(sensitivity_dict):
|
||||
modes = list(sensitivity_dict.keys())
|
||||
axial = [sensitivity_dict[m]['axial'] for m in modes]
|
||||
lateral = [sensitivity_dict[m]['lateral'] for m in modes]
|
||||
labels = [MODE_NAMES.get(m, m) for m in modes]
|
||||
labels = [str(MODE_NAMES.get(m, m)) for m in modes]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Bar(
|
||||
y=labels, x=axial, orientation='h',
|
||||
name='Axial (sin θ)', marker_color='#f59e0b',
|
||||
name='Axial (sin \u03b8)', marker_color='#2563eb',
|
||||
text=[f"{v:.3f}" for v in axial], textposition='outside',
|
||||
textfont=dict(size=9, color='#475569'),
|
||||
hovertemplate="%{y}<br>Axial: %{x:.3f} nm/unit<extra></extra>"
|
||||
))
|
||||
fig.add_trace(go.Bar(
|
||||
y=labels, x=lateral, orientation='h',
|
||||
name='Lateral (cos θ)', marker_color='#6366f1',
|
||||
name='Lateral (cos \u03b8)', marker_color='#7c3aed',
|
||||
text=[f"{v:.3f}" for v in lateral], textposition='outside',
|
||||
textfont=dict(size=9, color='#475569'),
|
||||
hovertemplate="%{y}<br>Lateral: %{x:.3f} nm/unit<extra></extra>"
|
||||
))
|
||||
fig.update_layout(
|
||||
barmode='group',
|
||||
height=max(300, len(modes)*40),
|
||||
margin=dict(t=30, b=40, l=200, r=20),
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(17,24,39,0.8)',
|
||||
font=dict(color='#e0e0e0', size=11),
|
||||
xaxis=dict(title="Sensitivity (nm per load fraction)", gridcolor='rgba(128,128,128,0.2)'),
|
||||
yaxis=dict(autorange='reversed'),
|
||||
legend=dict(x=0.6, y=0.98, bgcolor='rgba(17,24,39,0.7)'),
|
||||
height=max(300, len(modes)*45),
|
||||
margin=dict(t=30, b=40, l=180, r=60),
|
||||
**_PLOTLY_LIGHT_LAYOUT,
|
||||
xaxis=dict(title=dict(text="Sensitivity (nm per load fraction)", font=dict(color='#1e293b')),
|
||||
gridcolor='#e2e8f0',
|
||||
zeroline=True, zerolinecolor='#cbd5e1',
|
||||
tickfont=dict(color='#475569')),
|
||||
yaxis=dict(autorange='reversed', tickfont=dict(size=11, color='#1e293b')),
|
||||
legend=dict(x=0.55, y=0.99, bgcolor='rgba(255,255,255,0.9)',
|
||||
bordercolor='#e2e8f0', borderwidth=1,
|
||||
font=dict(color='#1e293b')),
|
||||
)
|
||||
return fig.to_html(include_plotlyjs=False, full_html=False, div_id="sensitivity_bar")
|
||||
|
||||
@@ -475,22 +546,25 @@ def make_per_angle_rms_plot(angle_rms_data, ref_angle=20):
|
||||
"""Create bar chart of per-angle RMS relative to reference."""
|
||||
angles = sorted(angle_rms_data.keys())
|
||||
rms_vals = [angle_rms_data[a] for a in angles]
|
||||
labels = [f"{a}° vs {ref_angle}°" for a in angles]
|
||||
labels = [f"{a}\u00b0 vs {ref_angle}\u00b0" for a in angles]
|
||||
|
||||
fig = go.Figure(go.Bar(
|
||||
x=labels, y=rms_vals,
|
||||
marker_color=['#10b981' if v < 10 else '#f59e0b' if v < 20 else '#ef4444' for v in rms_vals],
|
||||
marker_color=_BLUE_PALETTE[0],
|
||||
text=[f"{v:.2f} nm" for v in rms_vals],
|
||||
textposition='outside',
|
||||
textfont=dict(size=11, color='#1e293b'),
|
||||
hovertemplate="%{x}: %{y:.2f} nm<extra></extra>"
|
||||
))
|
||||
fig.update_layout(
|
||||
height=350,
|
||||
margin=dict(t=30, b=40, l=60, r=20),
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(17,24,39,0.8)',
|
||||
font=dict(color='#e0e0e0'),
|
||||
yaxis=dict(title="Filtered RMS WFE (nm)", gridcolor='rgba(128,128,128,0.2)'),
|
||||
**_PLOTLY_LIGHT_LAYOUT,
|
||||
yaxis=dict(title=dict(text="Filtered RMS WFE (nm)", font=dict(color='#1e293b')),
|
||||
gridcolor='#e2e8f0',
|
||||
zeroline=True, zerolinecolor='#cbd5e1',
|
||||
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")
|
||||
|
||||
@@ -721,11 +795,11 @@ def generate_report(
|
||||
params = study_params['parameters']
|
||||
rows = ""
|
||||
for k, v in sorted(params.items()):
|
||||
unit = "°" if "angle" in k else "mm"
|
||||
unit = "\u00b0" if "angle" in k else "mm"
|
||||
rows += f"<tr><td>{k}</td><td>{v:.4f} {unit}</td></tr>\n"
|
||||
params_html = f"""
|
||||
<div class="section">
|
||||
<h2>🔧 Design Parameters (Trial #{study_params.get('trial_id', '?')})</h2>
|
||||
<h2>7. Design Parameters (Trial #{study_params.get('trial_id', '?')})</h2>
|
||||
<table class="data-table"><thead><tr><th>Parameter</th><th>Value</th></tr></thead>
|
||||
<tbody>{rows}</tbody></table>
|
||||
</div>
|
||||
@@ -740,7 +814,7 @@ def generate_report(
|
||||
abs_glob = r['rms_abs']['global_rms']
|
||||
ab = r['aberrations_abs']
|
||||
angle_detail_rows += f"""<tr>
|
||||
<td><b>{ang}°</b></td>
|
||||
<td><b>{ang}\u00b0</b></td>
|
||||
<td>{abs_glob:.2f}</td><td>{abs_filt:.2f}</td>
|
||||
<td>{rel_filt:.2f}</td>
|
||||
<td>{ab['astigmatism']:.2f}</td><td>{ab['coma']:.2f}</td>
|
||||
@@ -773,29 +847,42 @@ def generate_report(
|
||||
<div class="metric-value">{traj_result['total_filtered_rms_nm']:.2f} nm</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Linear Fit R²</div>
|
||||
<div class="metric-label">Linear Fit R\u00b2</div>
|
||||
<div class="metric-value">{traj_result['linear_fit_r2']:.4f}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note">Dominant aberration mode: <b>{MODE_NAMES.get(traj_result['dominant_mode'], traj_result['dominant_mode'])}</b></p>
|
||||
<p class="note">Mode ranking: {' → '.join(traj_result['mode_ranking'][:5])}</p>
|
||||
<p class="note">Mode ranking: {' \u2192 '.join(traj_result['mode_ranking'][:5])}</p>
|
||||
"""
|
||||
|
||||
# Manufacturing details
|
||||
mfg_html = f"""
|
||||
<table class="data-table">
|
||||
<thead><tr><th>Metric</th><th>Absolute 90°</th><th>Correction (90°−20°)</th></tr></thead>
|
||||
<thead><tr><th>Metric</th><th>Absolute 90\u00b0</th><th>Correction (90\u00b0\u221220\u00b0)</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Defocus (J4)</td><td>{mfg_abs_aberr['defocus']:.2f} nm</td><td>{mfg_correction['defocus']:.2f} nm</td></tr>
|
||||
<tr><td>Astigmatism (J5+J6)</td><td>{mfg_abs_aberr['astigmatism']:.2f} nm</td><td>{mfg_correction['astigmatism']:.2f} nm</td></tr>
|
||||
<tr><td>Coma (J7+J8)</td><td>{mfg_abs_aberr['coma']:.2f} nm</td><td>{mfg_correction['coma']:.2f} nm</td></tr>
|
||||
<tr><td>Trefoil (J9+J10)</td><td>{mfg_abs_aberr['trefoil']:.2f} nm</td><td>{mfg_correction['trefoil']:.2f} nm</td></tr>
|
||||
<tr><td>Spherical (J11)</td><td>{mfg_abs_aberr['spherical']:.2f} nm</td><td>{mfg_correction['spherical']:.2f} nm</td></tr>
|
||||
<tr class="highlight"><td><b>J1−J3 Filtered RMS</b></td><td>{r90['rms_abs']['rms_j1to3']:.2f} nm</td><td><b>{mfg_rms_j1to3:.2f} nm</b></td></tr>
|
||||
<tr class="highlight"><td><b>J1\u2212J3 Filtered RMS</b></td><td>{r90['rms_abs']['rms_j1to3']:.2f} nm</td><td><b>{mfg_rms_j1to3:.2f} nm</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
|
||||
# Executive summary metric styling
|
||||
style_40 = _metric_color(wfe_40_20, targets['wfe_40_20'])
|
||||
style_60 = _metric_color(wfe_60_20, targets['wfe_60_20'])
|
||||
style_mfg = _metric_color(mfg_90, targets['mfg_90'])
|
||||
|
||||
# Section numbering: adjust if trajectory present
|
||||
sec_surface = 3
|
||||
sec_traj = 4
|
||||
sec_mfg = 5 if traj_result else 4
|
||||
sec_params = sec_mfg + 1
|
||||
sec_zernike = sec_params + 1 if (study_params and study_params.get('parameters')) else sec_mfg + 1
|
||||
sec_method = sec_zernike + 1
|
||||
|
||||
# Assemble full HTML
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -806,22 +893,23 @@ def generate_report(
|
||||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||
<style>
|
||||
:root {{
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-card: #1e293b;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--border: #334155;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-card: #ffffff;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--accent: #2563eb;
|
||||
--accent-light: #dbeafe;
|
||||
--success: #16a34a;
|
||||
--warning: #d97706;
|
||||
--danger: #dc2626;
|
||||
--border: #e2e8f0;
|
||||
--border-strong: #cbd5e1;
|
||||
}}
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}}
|
||||
@@ -829,34 +917,37 @@ def generate_report(
|
||||
|
||||
/* Header */
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 2rem 3rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
.header h1 {{ font-size: 1.8rem; font-weight: 700; }}
|
||||
.header .subtitle {{ color: var(--text-secondary); font-size: 0.95rem; margin-top: 0.3rem; }}
|
||||
.header h1 {{ font-size: 1.6rem; font-weight: 700; color: var(--text-primary); }}
|
||||
.header .subtitle {{ color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.3rem; }}
|
||||
.header .branding {{
|
||||
text-align: right;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}}
|
||||
.header .branding .logo {{ font-size: 1.4rem; font-weight: 700; color: var(--accent); }}
|
||||
.header .branding .by-line {{ color: #94a3b8; font-size: 0.8rem; margin-top: 0.2rem; }}
|
||||
.header .branding .tagline {{ color: var(--text-secondary); font-size: 0.8rem; margin-top: 0.15rem; }}
|
||||
|
||||
/* Sections */
|
||||
.section {{
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}}
|
||||
.section h2 {{
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -865,31 +956,28 @@ def generate_report(
|
||||
/* Executive Summary */
|
||||
.exec-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}}
|
||||
.exec-card {{
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 1.2rem;
|
||||
}}
|
||||
.exec-card .label {{ font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.3rem; }}
|
||||
.exec-card .value {{ font-size: 1.8rem; font-weight: 700; }}
|
||||
.exec-card .target {{ font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.3rem; }}
|
||||
|
||||
/* Badges */
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
.exec-card .unit {{ font-size: 0.9rem; font-weight: 400; color: var(--text-secondary); }}
|
||||
.exec-footnote {{
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
}}
|
||||
.badge.pass {{ background: rgba(16,185,129,0.15); color: var(--success); }}
|
||||
.badge.warn {{ background: rgba(245,158,11,0.15); color: var(--warning); }}
|
||||
.badge.fail {{ background: rgba(239,68,68,0.15); color: var(--danger); }}
|
||||
|
||||
/* Tables */
|
||||
.data-table {{
|
||||
@@ -903,34 +991,35 @@ def generate_report(
|
||||
border-bottom: 1px solid var(--border);
|
||||
}}
|
||||
.data-table th {{
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary);
|
||||
}}
|
||||
.data-table tr:hover {{ background: #f1f5f9; }}
|
||||
.data-table tr.highlight td {{
|
||||
background: rgba(99,102,241,0.08);
|
||||
background: var(--accent-light);
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
/* Metrics Grid */
|
||||
.metrics-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 0.8rem;
|
||||
margin: 1rem 0;
|
||||
}}
|
||||
.metric-card {{
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}}
|
||||
.metric-label {{ font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.3rem; }}
|
||||
.metric-value {{ font-size: 1.3rem; font-weight: 700; color: var(--accent); }}
|
||||
.metric-value {{ font-size: 1.2rem; font-weight: 700; color: var(--accent); }}
|
||||
|
||||
/* Plots */
|
||||
.plot-grid {{
|
||||
@@ -939,13 +1028,15 @@ def generate_report(
|
||||
gap: 1rem;
|
||||
}}
|
||||
.plot-container {{
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
min-height: 400px;
|
||||
}}
|
||||
.plot-container h3 {{
|
||||
font-size: 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}}
|
||||
@@ -955,24 +1046,44 @@ def generate_report(
|
||||
summary {{
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-primary);
|
||||
padding: 0.6rem 0.8rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
}}
|
||||
summary:hover {{ background: rgba(99,102,241,0.1); }}
|
||||
summary:hover {{ background: var(--accent-light); }}
|
||||
details > div {{ padding: 1rem; }}
|
||||
|
||||
.note {{ color: var(--text-secondary); font-size: 0.9rem; margin: 0.5rem 0; }}
|
||||
|
||||
/* Print styles */
|
||||
@media print {{
|
||||
body {{ background: white; color: black; }}
|
||||
.section {{ border: 1px solid #ccc; page-break-inside: avoid; }}
|
||||
}}
|
||||
.note {{ color: var(--text-secondary); font-size: 0.88rem; margin: 0.5rem 0; }}
|
||||
|
||||
.two-col {{ display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }}
|
||||
@media (max-width: 900px) {{ .two-col {{ grid-template-columns: 1fr; }} }}
|
||||
|
||||
/* Print styles */
|
||||
@media print {{
|
||||
body {{ background: white; color: black; font-size: 10pt; }}
|
||||
.container {{ max-width: 100%; padding: 0.5cm; }}
|
||||
.header {{ border: 1px solid #999; }}
|
||||
.section {{ border: 1px solid #ccc; page-break-inside: avoid; margin-bottom: 0.5cm; }}
|
||||
.plot-container {{ page-break-inside: avoid; }}
|
||||
details {{ display: block; }}
|
||||
details > summary {{ display: none; }}
|
||||
details > div {{ padding: 0; }}
|
||||
.exec-grid {{ grid-template-columns: repeat(2, 1fr); }}
|
||||
.no-print {{ display: none; }}
|
||||
}}
|
||||
|
||||
/* Footer */
|
||||
.footer {{
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 1rem;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -981,48 +1092,53 @@ def generate_report(
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>🔭 {title}</h1>
|
||||
<h1>{title}</h1>
|
||||
<div class="subtitle">Generated {timestamp} | OP2: {op2_path.name}</div>
|
||||
{'<div class="subtitle">Study: ' + study_name + '</div>' if study_name else ''}
|
||||
</div>
|
||||
<div class="branding">
|
||||
<div class="logo">ATOMIZER</div>
|
||||
<div>by Atomaste</div>
|
||||
<div style="margin-top:0.3rem">FEA Optimization Platform</div>
|
||||
<svg width="120" height="32" viewBox="0 0 120 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="0" y="24" font-family="Inter, system-ui, sans-serif" font-size="22" font-weight="700" fill="#2563eb">ATOMIZER</text>
|
||||
</svg>
|
||||
<div class="by-line">by Atomaste</div>
|
||||
<div class="tagline">FEA Optimization Platform</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<!-- 1. Executive Summary -->
|
||||
<div class="section">
|
||||
<h2>📋 Executive Summary</h2>
|
||||
<h2>1. Executive Summary</h2>
|
||||
<div class="exec-grid">
|
||||
<div class="exec-card">
|
||||
<div class="label">WFE 40° vs 20° (Tracking)</div>
|
||||
<div class="value">{wfe_40_20:.2f} <small>nm</small></div>
|
||||
<div class="target">{status_badge(wfe_40_20, targets['wfe_40_20'])}</div>
|
||||
<div class="label">WFE 40\u00b0 vs 20\u00b0 (Tracking)</div>
|
||||
<div class="value" style="{style_40}">{wfe_40_20:.2f} <span class="unit">nm</span></div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="label">WFE 60° vs 20° (Tracking)</div>
|
||||
<div class="value">{wfe_60_20:.2f} <small>nm</small></div>
|
||||
<div class="target">{status_badge(wfe_60_20, targets['wfe_60_20'])}</div>
|
||||
<div class="label">WFE 60\u00b0 vs 20\u00b0 (Tracking)</div>
|
||||
<div class="value" style="{style_60}">{wfe_60_20:.2f} <span class="unit">nm</span></div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="label">MFG 90° (J1−J3 Filtered)</div>
|
||||
<div class="value">{mfg_90:.2f} <small>nm</small></div>
|
||||
<div class="target">{status_badge(mfg_90, targets['mfg_90'])}</div>
|
||||
<div class="label">MFG 90\u00b0 (J1\u2212J3 Filtered)</div>
|
||||
<div class="value" style="{style_mfg}">{mfg_90:.2f} <span class="unit">nm</span></div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="label">Weighted Sum (6·W40 + 5·W60 + 3·MFG)</div>
|
||||
<div class="value" style="color: var(--accent)">{ws:.1f}</div>
|
||||
<div class="target">Lower is better</div>
|
||||
<div class="label">Weighted Sum (6\u00b7W40 + 5\u00b7W60 + 3\u00b7MFG)</div>
|
||||
<div class="value" style="color: var(--accent); font-weight: 700;">{ws:.1f}</div>
|
||||
</div>
|
||||
</div>
|
||||
{'<p class="note">Annular aperture: inner radius = ' + f'{inner_radius:.1f} mm (ø{2*inner_radius:.1f} mm central hole)' + '</p>' if inner_radius else ''}
|
||||
<div class="exec-footnote">
|
||||
Design targets —
|
||||
WFE 40\u00b0\u221220\u00b0 \u2264 {targets['wfe_40_20']:.1f} nm |
|
||||
WFE 60\u00b0\u221220\u00b0 \u2264 {targets['wfe_60_20']:.1f} nm |
|
||||
MFG 90\u00b0 \u2264 {targets['mfg_90']:.1f} nm |
|
||||
Weighted sum: lower is better.
|
||||
{'<br>Annular aperture: inner radius = ' + f'{inner_radius:.1f} mm (\u00f8{2*inner_radius:.1f} mm central hole)' if inner_radius else ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-Angle Summary -->
|
||||
<!-- 2. Per-Angle Summary -->
|
||||
<div class="section">
|
||||
<h2>📊 Per-Angle RMS Summary</h2>
|
||||
<h2>2. Per-Angle RMS Summary</h2>
|
||||
{per_angle_plot}
|
||||
<table class="data-table" style="margin-top:1rem">
|
||||
<thead>
|
||||
@@ -1034,33 +1150,33 @@ def generate_report(
|
||||
</thead>
|
||||
<tbody>{angle_detail_rows}</tbody>
|
||||
</table>
|
||||
<p class="note">All values in nm. Filtered = J1−J4 removed. Relative = vs 20° reference. Aberrations are absolute.</p>
|
||||
<p class="note">All values in nm. Filtered = J1\u2212J4 removed. Relative = vs 20\u00b0 reference. Aberrations are absolute.</p>
|
||||
</div>
|
||||
|
||||
<!-- Surface Plots -->
|
||||
<!-- 3. Surface Plots -->
|
||||
<div class="section">
|
||||
<h2>🌊 Wavefront Error Surface Maps</h2>
|
||||
<p class="note">3D residual surfaces after removing piston, tip, tilt, and defocus (J1−J4). Interactive — drag to rotate.</p>
|
||||
<h2>{sec_surface}. Wavefront Error Surface Maps</h2>
|
||||
<p class="note">3D residual surfaces after removing piston, tip, tilt, and defocus (J1\u2212J4). Interactive \u2014 drag to rotate.</p>
|
||||
<div class="plot-grid">
|
||||
<div class="plot-container">
|
||||
<h3>40° vs 20° (Relative)</h3>
|
||||
<h3>40\u00b0 vs 20\u00b0 (Relative)</h3>
|
||||
{surf_40}
|
||||
</div>
|
||||
<div class="plot-container">
|
||||
<h3>60° vs 20° (Relative)</h3>
|
||||
<h3>60\u00b0 vs 20\u00b0 (Relative)</h3>
|
||||
{surf_60}
|
||||
</div>
|
||||
</div>
|
||||
<div class="plot-container" style="margin-top:1rem">
|
||||
<h3>90° Manufacturing (Absolute)</h3>
|
||||
<h3>90\u00b0 Manufacturing (Absolute)</h3>
|
||||
{surf_90}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trajectory Analysis -->
|
||||
{'<div class="section"><h2>📈 Zernike Trajectory Analysis</h2>' +
|
||||
{'<div class="section"><h2>' + str(sec_traj) + '. Zernike Trajectory Analysis</h2>' +
|
||||
'<p class="note">Mode-specific integrated RMS across the operating elevation range. ' +
|
||||
'The linear model c<sub>j</sub>(θ) = a<sub>j</sub>·Δsinθ + b<sub>j</sub>·Δcosθ decomposes gravity into axial and lateral components.</p>' +
|
||||
'The linear model c<sub>j</sub>(\u03b8) = a<sub>j</sub>\u00b7\u0394sin\u03b8 + b<sub>j</sub>\u00b7\u0394cos\u03b8 decomposes gravity into axial and lateral components.</p>' +
|
||||
traj_metrics_html +
|
||||
'<div class="two-col" style="margin-top:1rem">' +
|
||||
'<div class="plot-container"><h3>Mode RMS vs Elevation Angle</h3>' + traj_plot_html + '</div>' +
|
||||
@@ -1069,10 +1185,10 @@ def generate_report(
|
||||
|
||||
<!-- Manufacturing Analysis -->
|
||||
<div class="section">
|
||||
<h2>🏭 Manufacturing Analysis (90° Orientation)</h2>
|
||||
<h2>{sec_mfg}. Manufacturing Analysis (90\u00b0 Orientation)</h2>
|
||||
<p class="note">
|
||||
The mirror is manufactured (polished) at 90° orientation. The "Correction" column shows the
|
||||
aberrations that must be polished out to achieve the 20° operational figure.
|
||||
The mirror is manufactured (polished) at 90\u00b0 orientation. The "Correction" column shows the
|
||||
aberrations that must be polished out to achieve the 20\u00b0 operational figure.
|
||||
</p>
|
||||
{mfg_html}
|
||||
</div>
|
||||
@@ -1082,43 +1198,43 @@ def generate_report(
|
||||
|
||||
<!-- Zernike Coefficient Details -->
|
||||
<div class="section">
|
||||
<h2>🔬 Zernike Coefficient Details</h2>
|
||||
<h2>{sec_zernike}. Zernike Coefficient Details</h2>
|
||||
<details>
|
||||
<summary>40° vs 20° — Relative Coefficients</summary>
|
||||
<summary>40\u00b0 vs 20\u00b0 \u2014 Relative Coefficients</summary>
|
||||
<div>{bar_40}</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>60° vs 20° — Relative Coefficients</summary>
|
||||
<summary>60\u00b0 vs 20\u00b0 \u2014 Relative Coefficients</summary>
|
||||
<div>{bar_60}</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>90° — Absolute Coefficients</summary>
|
||||
<summary>90\u00b0 \u2014 Absolute Coefficients</summary>
|
||||
<div>{bar_90}</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Methodology -->
|
||||
<div class="section" style="opacity:0.85">
|
||||
<h2>📝 Methodology</h2>
|
||||
<div class="section">
|
||||
<h2>{sec_method}. Methodology</h2>
|
||||
<table class="data-table">
|
||||
<tbody>
|
||||
<tr><td><b>Zernike Modes</b></td><td>{N_MODES} (Noll convention)</td></tr>
|
||||
<tr><td><b>Filtered Modes</b></td><td>J1−J4 (Piston, Tip, Tilt, Defocus)</td></tr>
|
||||
<tr><td><b>WFE Calculation</b></td><td>WFE = 2 × Surface Error (reflective)</td></tr>
|
||||
<tr><td><b>Displacement Unit</b></td><td>{DISP_UNIT} → nm ({NM_SCALE:.0e}×)</td></tr>
|
||||
<tr><td><b>Filtered Modes</b></td><td>J1\u2212J4 (Piston, Tip, Tilt, Defocus)</td></tr>
|
||||
<tr><td><b>WFE Calculation</b></td><td>WFE = 2 \u00d7 Surface Error (reflective)</td></tr>
|
||||
<tr><td><b>Displacement Unit</b></td><td>{DISP_UNIT} \u2192 nm ({NM_SCALE:.0e}\u00d7)</td></tr>
|
||||
<tr><td><b>Aperture</b></td><td>{'Annular (inner R = ' + f'{inner_radius:.1f} mm)' if inner_radius else 'Full disk'}</td></tr>
|
||||
<tr><td><b>Reference Angle</b></td><td>20° (polishing/measurement orientation)</td></tr>
|
||||
<tr><td><b>MFG Objective</b></td><td>90°−20° relative, J1−J3 filtered (optician workload)</td></tr>
|
||||
<tr><td><b>Weighted Sum</b></td><td>6×WFE(40−20) + 5×WFE(60−20) + 3×MFG(90)</td></tr>
|
||||
{'<tr><td><b>Trajectory R²</b></td><td>' + f'{traj_result["linear_fit_r2"]:.6f}' + '</td></tr>' if traj_result else ''}
|
||||
<tr><td><b>Reference Angle</b></td><td>20\u00b0 (polishing/measurement orientation)</td></tr>
|
||||
<tr><td><b>MFG Objective</b></td><td>90\u00b0\u221220\u00b0 relative, J1\u2212J3 filtered (optician workload)</td></tr>
|
||||
<tr><td><b>Weighted Sum</b></td><td>6\u00d7WFE(40\u221220) + 5\u00d7WFE(60\u221220) + 3\u00d7MFG(90)</td></tr>
|
||||
{'<tr><td><b>Trajectory R\u00b2</b></td><td>' + f'{traj_result["linear_fit_r2"]:.6f}' + '</td></tr>' if traj_result else ''}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="text-align:center; padding:2rem; color:var(--text-secondary); font-size:0.8rem;">
|
||||
<div class="footer">
|
||||
Generated by <b>Atomizer</b> Optical Report Generator | {timestamp}<br>
|
||||
© Atomaste | atomaste.ca
|
||||
\u00a9 Atomaste | atomaste.ca
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user