diff --git a/tools/generate_optical_report.py b/tools/generate_optical_report.py index 457433ed..957fbce6 100644 --- a/tools/generate_optical_report.py +++ b/tools/generate_optical_report.py @@ -277,6 +277,105 @@ def aberration_magnitudes(coeffs): } +def compute_spatial_freq_metrics(coefficients): + """Compute spatial frequency band metrics from Zernike coefficients. + + Decomposes the Zernike spectrum into Low Spatial Frequency (form/figure) + and Mid Spatial Frequency (ripple) bands based on radial order. + + Args: + coefficients: array of Zernike coefficients (50 modes, Noll convention) + + Returns: + dict with band RMS values and per-radial-order breakdown + """ + n_modes = min(len(coefficients), 50) + coeffs = np.array(coefficients[:n_modes]) + + # Build radial order mapping using noll_indices + order_map = {} # n -> list of coeff values + for j in range(1, n_modes + 1): + n, m = noll_indices(j) + if n not in order_map: + order_map[n] = [] + order_map[n].append(coeffs[j - 1]) + + # Band definitions (by Noll index, 1-based) + # LSF: J4-J15 (n=2..4) — Low Spatial Frequency (form/figure) + # MSF: J16-J50 (n=5..9) — Mid Spatial Frequency (ripple) + lsf_coeffs = coeffs[3:15] # indices 3..14 → J4..J15 + msf_coeffs = coeffs[15:n_modes] # indices 15..49 → J16..J50 + total_coeffs = coeffs[3:n_modes] # J4..J50 (excluding piston/tilt) + + lsf_rms = float(np.sqrt(np.sum(lsf_coeffs**2))) + msf_rms = float(np.sqrt(np.sum(msf_coeffs**2))) + total_filtered_rms = float(np.sqrt(np.sum(total_coeffs**2))) + lsf_msf_ratio = lsf_rms / msf_rms if msf_rms > 0 else float('inf') + + # Per-radial-order RMS + per_order = {} + for n in sorted(order_map.keys()): + order_coeffs = np.array(order_map[n]) + per_order[n] = float(np.sqrt(np.sum(order_coeffs**2))) + + return { + 'lsf_rms': lsf_rms, + 'msf_rms': msf_rms, + 'total_filtered_rms': total_filtered_rms, + 'lsf_msf_ratio': lsf_msf_ratio, + 'per_order': per_order, + } + + +def _spatial_freq_html(metrics): + """Generate HTML card for spatial frequency band metrics.""" + m = metrics + + # Per-order breakdown for n=2..9 + order_items = "" + for n in range(2, 10): + val = m['per_order'].get(n, 0.0) + order_items += ( + f'
' + f'n={n}:' + f'{val:.2f}' + f'
\n' + ) + + ratio_str = ( + f"{m['lsf_msf_ratio']:.1f}\u00d7" + if m['lsf_msf_ratio'] < 1000 + else "\u221e" + ) + + return f""" +
+

Spatial Frequency Breakdown

+
+
+
LSF (Form)
+
J4\u2013J15   n\u22644
+
{m['lsf_rms']:.2f} nm
+
+
+
MSF (Ripple)
+
J16\u2013J50   n\u22655
+
{m['msf_rms']:.2f} nm
+
+
+
LSF / MSF
+
Ratio
+
{ratio_str}
+
+
+
+
Per-Order RMS (nm)
+
{order_items}
+
+
+ """ + + # ============================================================================ # HTML Report Generation # ============================================================================ @@ -383,8 +482,8 @@ def make_surface_plot(X, Y, W_res, mask, inner_radius=None, title="", amp=0.5, d ), margin=dict(t=30, b=10, l=10, r=10), **_PLOTLY_LIGHT_LAYOUT, - height=500, - width=700, + height=650, + width=1200, ) return fig.to_html(include_plotlyjs=False, full_html=False, div_id=f"surface_{title.replace(' ','_')}") @@ -771,6 +870,11 @@ def generate_report( bar_60 = make_bar_chart(angle_results[60]['rms_rel']['coefficients'], title="60v20 coeffs") bar_90 = make_bar_chart(angle_results[90]['rms_abs']['coefficients'], title="90abs coeffs") + # Spatial frequency band metrics + sfm_40 = _spatial_freq_html(compute_spatial_freq_metrics(angle_results[40]['rms_rel']['coefficients'])) + sfm_60 = _spatial_freq_html(compute_spatial_freq_metrics(angle_results[60]['rms_rel']['coefficients'])) + sfm_90 = _spatial_freq_html(compute_spatial_freq_metrics(angle_results[90]['rms_abs']['coefficients'])) + # Per-angle RMS plot angle_rms_data = {} for ang in sorted(angle_results.keys()): @@ -1024,7 +1128,7 @@ def generate_report( /* Plots */ .plot-grid {{ display: grid; - grid-template-columns: repeat(auto-fit, minmax(650px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(1100px, 1fr)); gap: 1rem; }} .plot-container {{ @@ -1041,6 +1145,89 @@ def generate_report( color: var(--text-secondary); }} + /* Spatial Frequency Breakdown */ + .sf-breakdown {{ + margin: 1rem 0 1.5rem 0; + padding: 1.2rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + }} + .sf-breakdown h4 {{ + font-size: 0.9rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.8rem; + }} + .sf-band-grid {{ + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 0.8rem; + margin-bottom: 1rem; + }} + .sf-band-card {{ + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.8rem 1rem; + text-align: center; + }} + .sf-band-label {{ + font-size: 0.82rem; + font-weight: 600; + color: var(--text-primary); + }} + .sf-band-range {{ + font-size: 0.75rem; + color: var(--text-secondary); + margin: 0.15rem 0 0.4rem 0; + }} + .sf-band-value {{ + font-size: 1.3rem; + font-weight: 700; + color: var(--accent); + }} + .sf-unit {{ + font-size: 0.8rem; + font-weight: 400; + color: var(--text-secondary); + }} + .sf-lsf {{ border-top: 3px solid #2563eb; }} + .sf-msf {{ border-top: 3px solid #7c3aed; }} + .sf-ratio {{ border-top: 3px solid #16a34a; min-width: 100px; }} + .sf-order-grid {{ + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.8rem 1rem; + }} + .sf-order-title {{ + font-size: 0.8rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 0.5rem; + }} + .sf-order-items {{ + display: flex; + flex-wrap: wrap; + gap: 0.3rem 1.2rem; + }} + .sf-order-item {{ + display: flex; + gap: 0.3rem; + font-size: 0.85rem; + }} + .sf-order-n {{ + color: var(--text-secondary); + font-weight: 500; + }} + .sf-order-val {{ + color: var(--text-primary); + font-weight: 600; + }} + /* Collapsible */ details {{ margin: 0.5rem 0; }} summary {{ @@ -1201,15 +1388,15 @@ def generate_report(

{sec_zernike}. Zernike Coefficient Details

40\u00b0 vs 20\u00b0 \u2014 Relative Coefficients -
{bar_40}
+
{sfm_40}{bar_40}
60\u00b0 vs 20\u00b0 \u2014 Relative Coefficients -
{bar_60}
+
{sfm_60}{bar_60}
90\u00b0 \u2014 Absolute Coefficients -
{bar_90}
+
{sfm_90}{bar_90}