diff --git a/tools/generate_optical_report.py b/tools/generate_optical_report.py
index d0bffd4f..9dda8f2f 100644
--- a/tools/generate_optical_report.py
+++ b/tools/generate_optical_report.py
@@ -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'✅ {value:.2f} {unit} ≤ {target:.1f}'
+ return 'color: #16a34a; font-weight: 700;' # green
ratio = value / target
if ratio < 1.5:
- return f'⚠️ {value:.2f} {unit} ({ratio:.1f}× target)'
- return f'❌ {value:.2f} {unit} ({ratio:.1f}× target)'
+ 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}
Y: %{y:.1f}
Residual: %{z:.2f} nm"
))
@@ -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}
|c| = %{x:.3f} nm",
))
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}
%{{x}}°: %{{y:.2f}} nm"
+ name=f"{display_name} (RSS)",
+ line=dict(color=color, width=3, dash=dash),
+ marker=dict(size=10, symbol='circle'),
+ hovertemplate=f"{display_name} RSS
%{{x:.1f}}°: %{{y:.3f}} nm",
+ 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}
%{{x:.1f}}°: %{{y:.3f}} nm",
+ 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}
Axial: %{x:.3f} nm/unit"
))
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}
Lateral: %{x:.3f} nm/unit"
))
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"
))
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"
| {k} | {v:.4f} {unit} |
\n"
params_html = f"""
-
🔧 Design Parameters (Trial #{study_params.get('trial_id', '?')})
+
7. Design Parameters (Trial #{study_params.get('trial_id', '?')})
@@ -740,7 +814,7 @@ def generate_report(
abs_glob = r['rms_abs']['global_rms']
ab = r['aberrations_abs']
angle_detail_rows += f"""
- | {ang}° |
+ {ang}\u00b0 |
{abs_glob:.2f} | {abs_filt:.2f} |
{rel_filt:.2f} |
{ab['astigmatism']:.2f} | {ab['coma']:.2f} |
@@ -773,29 +847,42 @@ def generate_report(
{traj_result['total_filtered_rms_nm']:.2f} nm
-
Linear Fit R²
+
Linear Fit R\u00b2
{traj_result['linear_fit_r2']:.4f}
Dominant aberration mode: {MODE_NAMES.get(traj_result['dominant_mode'], traj_result['dominant_mode'])}
- Mode ranking: {' → '.join(traj_result['mode_ranking'][:5])}
+ Mode ranking: {' \u2192 '.join(traj_result['mode_ranking'][:5])}
"""
# Manufacturing details
mfg_html = f"""
- | Metric | Absolute 90° | Correction (90°−20°) |
+ | Metric | Absolute 90\u00b0 | Correction (90\u00b0\u221220\u00b0) |
| Defocus (J4) | {mfg_abs_aberr['defocus']:.2f} nm | {mfg_correction['defocus']:.2f} nm |
| Astigmatism (J5+J6) | {mfg_abs_aberr['astigmatism']:.2f} nm | {mfg_correction['astigmatism']:.2f} nm |
| Coma (J7+J8) | {mfg_abs_aberr['coma']:.2f} nm | {mfg_correction['coma']:.2f} nm |
| Trefoil (J9+J10) | {mfg_abs_aberr['trefoil']:.2f} nm | {mfg_correction['trefoil']:.2f} nm |
| Spherical (J11) | {mfg_abs_aberr['spherical']:.2f} nm | {mfg_correction['spherical']:.2f} nm |
- | J1−J3 Filtered RMS | {r90['rms_abs']['rms_j1to3']:.2f} nm | {mfg_rms_j1to3:.2f} nm |
+ | J1\u2212J3 Filtered RMS | {r90['rms_abs']['rms_j1to3']:.2f} nm | {mfg_rms_j1to3:.2f} nm |
"""
+ # 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"""
@@ -806,22 +893,23 @@ def generate_report(
@@ -981,48 +1092,53 @@ def generate_report(
-
+
-
📋 Executive Summary
+
1. Executive Summary
-
WFE 40° vs 20° (Tracking)
-
{wfe_40_20:.2f} nm
-
{status_badge(wfe_40_20, targets['wfe_40_20'])}
+
WFE 40\u00b0 vs 20\u00b0 (Tracking)
+
{wfe_40_20:.2f} nm
-
WFE 60° vs 20° (Tracking)
-
{wfe_60_20:.2f} nm
-
{status_badge(wfe_60_20, targets['wfe_60_20'])}
+
WFE 60\u00b0 vs 20\u00b0 (Tracking)
+
{wfe_60_20:.2f} nm
-
MFG 90° (J1−J3 Filtered)
-
{mfg_90:.2f} nm
-
{status_badge(mfg_90, targets['mfg_90'])}
+
MFG 90\u00b0 (J1\u2212J3 Filtered)
+
{mfg_90:.2f} nm
-
Weighted Sum (6·W40 + 5·W60 + 3·MFG)
-
{ws:.1f}
-
Lower is better
+
Weighted Sum (6\u00b7W40 + 5\u00b7W60 + 3\u00b7MFG)
+
{ws:.1f}
- {'
Annular aperture: inner radius = ' + f'{inner_radius:.1f} mm (ø{2*inner_radius:.1f} mm central hole)' + '
' if inner_radius else ''}
+
-
+
-
📊 Per-Angle RMS Summary
+
2. Per-Angle RMS Summary
{per_angle_plot}
@@ -1034,33 +1150,33 @@ def generate_report(
{angle_detail_rows}
-
All values in nm. Filtered = J1−J4 removed. Relative = vs 20° reference. Aberrations are absolute.
+
All values in nm. Filtered = J1\u2212J4 removed. Relative = vs 20\u00b0 reference. Aberrations are absolute.
-
+
-
🌊 Wavefront Error Surface Maps
-
3D residual surfaces after removing piston, tip, tilt, and defocus (J1−J4). Interactive — drag to rotate.
+
{sec_surface}. Wavefront Error Surface Maps
+
3D residual surfaces after removing piston, tip, tilt, and defocus (J1\u2212J4). Interactive \u2014 drag to rotate.
-
40° vs 20° (Relative)
+ 40\u00b0 vs 20\u00b0 (Relative)
{surf_40}
-
60° vs 20° (Relative)
+ 60\u00b0 vs 20\u00b0 (Relative)
{surf_60}
-
90° Manufacturing (Absolute)
+ 90\u00b0 Manufacturing (Absolute)
{surf_90}
-{'📈 Zernike Trajectory Analysis
' +
+{'
' + str(sec_traj) + '. Zernike Trajectory Analysis
' +
'
Mode-specific integrated RMS across the operating elevation range. ' +
- 'The linear model cj(θ) = aj·Δsinθ + bj·Δcosθ decomposes gravity into axial and lateral components.
' +
+ 'The linear model c
j(\u03b8) = a
j\u00b7\u0394sin\u03b8 + b
j\u00b7\u0394cos\u03b8 decomposes gravity into axial and lateral components.' +
traj_metrics_html +
'
' +
'
Mode RMS vs Elevation Angle
' + traj_plot_html + '' +
@@ -1069,10 +1185,10 @@ def generate_report(
-
🏭 Manufacturing Analysis (90° Orientation)
+
{sec_mfg}. Manufacturing Analysis (90\u00b0 Orientation)
- 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.
{mfg_html}
@@ -1082,43 +1198,43 @@ def generate_report(
-
🔬 Zernike Coefficient Details
+
{sec_zernike}. Zernike Coefficient Details
- 40° vs 20° — Relative Coefficients
+ 40\u00b0 vs 20\u00b0 \u2014 Relative Coefficients
{bar_40}
- 60° vs 20° — Relative Coefficients
+ 60\u00b0 vs 20\u00b0 \u2014 Relative Coefficients
{bar_60}
- 90° — Absolute Coefficients
+ 90\u00b0 \u2014 Absolute Coefficients
{bar_90}
-
-
📝 Methodology
+
+
{sec_method}. Methodology
| Zernike Modes | {N_MODES} (Noll convention) |
- | Filtered Modes | J1−J4 (Piston, Tip, Tilt, Defocus) |
- | WFE Calculation | WFE = 2 × Surface Error (reflective) |
- | Displacement Unit | {DISP_UNIT} → nm ({NM_SCALE:.0e}×) |
+ | Filtered Modes | J1\u2212J4 (Piston, Tip, Tilt, Defocus) |
+ | WFE Calculation | WFE = 2 \u00d7 Surface Error (reflective) |
+ | Displacement Unit | {DISP_UNIT} \u2192 nm ({NM_SCALE:.0e}\u00d7) |
| Aperture | {'Annular (inner R = ' + f'{inner_radius:.1f} mm)' if inner_radius else 'Full disk'} |
- | Reference Angle | 20° (polishing/measurement orientation) |
- | MFG Objective | 90°−20° relative, J1−J3 filtered (optician workload) |
- | Weighted Sum | 6×WFE(40−20) + 5×WFE(60−20) + 3×MFG(90) |
- {'| Trajectory R² | ' + f'{traj_result["linear_fit_r2"]:.6f}' + ' |
' if traj_result else ''}
+ | Reference Angle | 20\u00b0 (polishing/measurement orientation) |
+ | MFG Objective | 90\u00b0\u221220\u00b0 relative, J1\u2212J3 filtered (optician workload) |
+ | Weighted Sum | 6\u00d7WFE(40\u221220) + 5\u00d7WFE(60\u221220) + 3\u00d7MFG(90) |
+ {'| Trajectory R\u00b2 | ' + f'{traj_result["linear_fit_r2"]:.6f}' + ' |
' if traj_result else ''}
-
+