feat(report): wider surface maps + spatial frequency band metrics
- CSS .plot-grid: minmax(650px) → minmax(1100px) for full-width surface maps - Add compute_spatial_freq_metrics(): LSF/MSF band RMS, per-radial-order breakdown - Add styled metrics cards in Zernike Coefficient Details (section 6) showing LSF (J4-J15), MSF (J16-J50), ratio, and per-order RMS n=2..9
This commit is contained in:
@@ -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'<div class="sf-order-item">'
|
||||
f'<span class="sf-order-n">n={n}:</span>'
|
||||
f'<span class="sf-order-val">{val:.2f}</span>'
|
||||
f'</div>\n'
|
||||
)
|
||||
|
||||
ratio_str = (
|
||||
f"{m['lsf_msf_ratio']:.1f}\u00d7"
|
||||
if m['lsf_msf_ratio'] < 1000
|
||||
else "\u221e"
|
||||
)
|
||||
|
||||
return f"""
|
||||
<div class="sf-breakdown">
|
||||
<h4>Spatial Frequency Breakdown</h4>
|
||||
<div class="sf-band-grid">
|
||||
<div class="sf-band-card sf-lsf">
|
||||
<div class="sf-band-label">LSF (Form)</div>
|
||||
<div class="sf-band-range">J4\u2013J15 n\u22644</div>
|
||||
<div class="sf-band-value">{m['lsf_rms']:.2f} <span class="sf-unit">nm</span></div>
|
||||
</div>
|
||||
<div class="sf-band-card sf-msf">
|
||||
<div class="sf-band-label">MSF (Ripple)</div>
|
||||
<div class="sf-band-range">J16\u2013J50 n\u22655</div>
|
||||
<div class="sf-band-value">{m['msf_rms']:.2f} <span class="sf-unit">nm</span></div>
|
||||
</div>
|
||||
<div class="sf-band-card sf-ratio">
|
||||
<div class="sf-band-label">LSF / MSF</div>
|
||||
<div class="sf-band-range">Ratio</div>
|
||||
<div class="sf-band-value">{ratio_str}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sf-order-grid">
|
||||
<div class="sf-order-title">Per-Order RMS (nm)</div>
|
||||
<div class="sf-order-items">{order_items}</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 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(
|
||||
<h2>{sec_zernike}. Zernike Coefficient Details</h2>
|
||||
<details>
|
||||
<summary>40\u00b0 vs 20\u00b0 \u2014 Relative Coefficients</summary>
|
||||
<div>{bar_40}</div>
|
||||
<div>{sfm_40}{bar_40}</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>60\u00b0 vs 20\u00b0 \u2014 Relative Coefficients</summary>
|
||||
<div>{bar_60}</div>
|
||||
<div>{sfm_60}{bar_60}</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>90\u00b0 \u2014 Absolute Coefficients</summary>
|
||||
<div>{bar_90}</div>
|
||||
<div>{sfm_90}{bar_90}</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user