Documentation: - Add docs/06_PHYSICS/ with Zernike fundamentals and OPD method docs - Add docs/guides/CMA-ES_EXPLAINED.md optimization guide - Update CLAUDE.md and ATOMIZER_CONTEXT.md with current architecture - Update OP_01_CREATE_STUDY protocol Planning: - Add DYNAMIC_RESPONSE plans for random vibration/PSD support - Add OPTIMIZATION_ENGINE_MIGRATION_PLAN for code reorganization Insights System: - Update design_space, modal_analysis, stress_field, thermal_field insights - Improve error handling and data validation NX Journals: - Add analyze_wfe_zernike.py for Zernike WFE analysis - Add capture_study_images.py for automated screenshots - Add extract_expressions.py and introspect_part.py utilities - Add user_generated_journals/journal_top_view_image_taking.py Tests & Tools: - Add comprehensive Zernike OPD test suite - Add audit_v10 tests for WFE validation - Add tools for Pareto graphs and mirror data extraction - Add migrate_studies_to_topics.py utility Knowledge Base: - Initialize LAC (Learning Atomizer Core) with failure/success patterns Dashboard: - Update Setup.tsx and launch_dashboard.py - Add restart-dev.bat helper script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
389 lines
15 KiB
Python
389 lines
15 KiB
Python
"""
|
|
Create Pareto front visualizations for M1 Mirror optimization data.
|
|
Shows relationship between geometric parameters and 60/20 WFE performance.
|
|
"""
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.colors import LinearSegmentedColormap
|
|
import matplotlib.patches as mpatches
|
|
|
|
# Set style for publication-quality plots
|
|
plt.rcParams.update({
|
|
'font.family': 'sans-serif',
|
|
'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'],
|
|
'font.size': 11,
|
|
'axes.titlesize': 16,
|
|
'axes.labelsize': 13,
|
|
'xtick.labelsize': 11,
|
|
'ytick.labelsize': 11,
|
|
'legend.fontsize': 10,
|
|
'figure.dpi': 150,
|
|
'savefig.dpi': 150,
|
|
'axes.spines.top': False,
|
|
'axes.spines.right': False,
|
|
})
|
|
|
|
# Load data
|
|
df = pd.read_csv(r'c:\Users\antoi\Atomizer\studies\m1_mirror_all_trials_export.csv')
|
|
|
|
print("=== Data Overview ===")
|
|
print(f"Total rows: {len(df)}")
|
|
print(f"\nColumn data availability:")
|
|
for col in df.columns:
|
|
non_null = df[col].notna().sum()
|
|
if non_null > 0:
|
|
print(f" {col}: {non_null} ({100*non_null/len(df):.1f}%)")
|
|
|
|
print(f"\nStudies: {df['study'].unique()}")
|
|
|
|
# Filter for rows with the key parameters
|
|
thickness_col = 'center_thickness'
|
|
angle_col = 'blank_backface_angle'
|
|
wfe_col = 'rel_filtered_rms_60_vs_20'
|
|
|
|
print(f"\n=== Key columns ===")
|
|
print(f"center_thickness non-null: {df[thickness_col].notna().sum()}")
|
|
print(f"blank_backface_angle non-null: {df[angle_col].notna().sum()}")
|
|
print(f"rel_filtered_rms_60_vs_20 non-null: {df[wfe_col].notna().sum()}")
|
|
|
|
# Create filtered dataset with valid WFE values
|
|
df_valid = df[df[wfe_col].notna()].copy()
|
|
print(f"\nRows with valid WFE data (before outlier removal): {len(df_valid)}")
|
|
|
|
if len(df_valid) == 0:
|
|
print("No valid WFE data found!")
|
|
exit()
|
|
|
|
# Remove outliers - WFE values above 1000 are clearly failed simulations
|
|
WFE_THRESHOLD = 100 # Reasonable upper bound for WFE ratio
|
|
df_valid = df_valid[df_valid[wfe_col] < WFE_THRESHOLD].copy()
|
|
print(f"Rows with valid WFE data (after outlier removal, WFE < {WFE_THRESHOLD}): {len(df_valid)}")
|
|
|
|
# Show ranges
|
|
print(f"\n=== Value ranges (clean data) ===")
|
|
if df_valid[thickness_col].notna().any():
|
|
print(f"center_thickness: {df_valid[thickness_col].min():.2f} - {df_valid[thickness_col].max():.2f} mm")
|
|
if df_valid[angle_col].notna().any():
|
|
print(f"blank_backface_angle: {df_valid[angle_col].min():.2f} - {df_valid[angle_col].max():.2f}°")
|
|
print(f"rel_filtered_rms_60_vs_20: {df_valid[wfe_col].min():.4f} - {df_valid[wfe_col].max():.4f}")
|
|
|
|
# Also check mass
|
|
if 'mass_kg' in df_valid.columns and df_valid['mass_kg'].notna().any():
|
|
print(f"mass_kg: {df_valid['mass_kg'].min():.2f} - {df_valid['mass_kg'].max():.2f} kg")
|
|
|
|
|
|
def compute_pareto_front(x, y, minimize_x=True, minimize_y=True):
|
|
"""
|
|
Compute Pareto front indices.
|
|
Returns indices of points on the Pareto front.
|
|
"""
|
|
# Create array of points
|
|
points = np.column_stack([x, y])
|
|
n_points = len(points)
|
|
|
|
# Adjust for minimization/maximization
|
|
if not minimize_x:
|
|
points[:, 0] = -points[:, 0]
|
|
if not minimize_y:
|
|
points[:, 1] = -points[:, 1]
|
|
|
|
# Find Pareto front
|
|
pareto_mask = np.ones(n_points, dtype=bool)
|
|
|
|
for i in range(n_points):
|
|
if pareto_mask[i]:
|
|
# Check if any other point dominates point i
|
|
for j in range(n_points):
|
|
if i != j and pareto_mask[j]:
|
|
# j dominates i if j is <= in all objectives and < in at least one
|
|
if (points[j, 0] <= points[i, 0] and points[j, 1] <= points[i, 1] and
|
|
(points[j, 0] < points[i, 0] or points[j, 1] < points[i, 1])):
|
|
pareto_mask[i] = False
|
|
break
|
|
|
|
return np.where(pareto_mask)[0]
|
|
|
|
|
|
def create_pareto_plot(df_plot, x_col, y_col, x_label, y_label, title, filename,
|
|
minimize_x=True, minimize_y=True, color_by=None, color_label=None):
|
|
"""Create a publication-quality Pareto front plot."""
|
|
|
|
# Filter valid data
|
|
mask = df_plot[x_col].notna() & df_plot[y_col].notna()
|
|
df_clean = df_plot[mask].copy()
|
|
|
|
if len(df_clean) < 2:
|
|
print(f"Not enough data for {title}")
|
|
return
|
|
|
|
x = df_clean[x_col].values
|
|
y = df_clean[y_col].values
|
|
|
|
# Compute Pareto front
|
|
pareto_idx = compute_pareto_front(x, y, minimize_x, minimize_y)
|
|
|
|
# Sort Pareto points by x for line drawing
|
|
pareto_points = np.column_stack([x[pareto_idx], y[pareto_idx]])
|
|
sort_idx = np.argsort(pareto_points[:, 0])
|
|
pareto_sorted = pareto_points[sort_idx]
|
|
|
|
# Create figure with professional styling
|
|
fig, ax = plt.subplots(figsize=(12, 8))
|
|
fig.patch.set_facecolor('white')
|
|
|
|
# Professional color palette
|
|
bg_color = '#f8f9fa'
|
|
grid_color = '#dee2e6'
|
|
point_color = '#6c757d'
|
|
pareto_color = '#dc3545'
|
|
pareto_fill = '#ffc107'
|
|
|
|
ax.set_facecolor(bg_color)
|
|
|
|
# Color scheme - use mass as color if available
|
|
if color_by is not None and color_by in df_clean.columns and df_clean[color_by].notna().sum() > 10:
|
|
# Only use color if we have enough colored points
|
|
color_mask = df_clean[color_by].notna()
|
|
colors = df_clean.loc[color_mask, color_by].values
|
|
|
|
# Plot non-colored points in gray
|
|
ax.scatter(x[~color_mask.values], y[~color_mask.values],
|
|
c=point_color, alpha=0.3, s=40,
|
|
edgecolors='white', linewidth=0.3, zorder=2)
|
|
|
|
# Plot colored points
|
|
scatter = ax.scatter(x[color_mask.values], y[color_mask.values],
|
|
c=colors, cmap='plasma', alpha=0.7, s=60,
|
|
edgecolors='white', linewidth=0.5, zorder=2)
|
|
cbar = plt.colorbar(scatter, ax=ax, pad=0.02, shrink=0.8)
|
|
cbar.set_label(color_label or color_by, fontsize=12, fontweight='bold')
|
|
cbar.ax.tick_params(labelsize=10)
|
|
else:
|
|
ax.scatter(x, y, c=point_color, alpha=0.4, s=50,
|
|
edgecolors='white', linewidth=0.3, zorder=2, label='Design candidates')
|
|
|
|
# Draw Pareto front fill area (visual emphasis)
|
|
if len(pareto_sorted) > 1:
|
|
# Smooth interpolation for the Pareto front line
|
|
from scipy.interpolate import interp1d
|
|
if len(pareto_sorted) >= 4:
|
|
# Use cubic interpolation for smooth curve
|
|
try:
|
|
f = interp1d(pareto_sorted[:, 0], pareto_sorted[:, 1], kind='cubic')
|
|
x_smooth = np.linspace(pareto_sorted[:, 0].min(), pareto_sorted[:, 0].max(), 100)
|
|
y_smooth = f(x_smooth)
|
|
ax.plot(x_smooth, y_smooth, color=pareto_color, linewidth=3, alpha=0.9, zorder=3)
|
|
except:
|
|
ax.plot(pareto_sorted[:, 0], pareto_sorted[:, 1], color=pareto_color,
|
|
linewidth=3, alpha=0.9, zorder=3)
|
|
else:
|
|
ax.plot(pareto_sorted[:, 0], pareto_sorted[:, 1], color=pareto_color,
|
|
linewidth=3, alpha=0.9, zorder=3)
|
|
|
|
# Plot Pareto front points with emphasis
|
|
ax.scatter(x[pareto_idx], y[pareto_idx], c=pareto_fill, s=180,
|
|
edgecolors=pareto_color, linewidth=2.5, zorder=5,
|
|
label=f'Pareto optimal ({len(pareto_idx)} designs)')
|
|
|
|
# Styling
|
|
ax.set_xlabel(x_label, fontsize=14, fontweight='bold', labelpad=12)
|
|
ax.set_ylabel(y_label, fontsize=14, fontweight='bold', labelpad=12)
|
|
ax.set_title(title, fontsize=18, fontweight='bold', pad=20, color='#212529')
|
|
|
|
# Refined grid
|
|
ax.grid(True, alpha=0.5, linestyle='-', linewidth=0.5, color=grid_color)
|
|
ax.set_axisbelow(True)
|
|
|
|
# Add minor grid
|
|
ax.minorticks_on()
|
|
ax.grid(True, which='minor', alpha=0.2, linestyle=':', linewidth=0.3, color=grid_color)
|
|
|
|
# Legend with professional styling
|
|
legend = ax.legend(loc='upper right', fontsize=11, framealpha=0.95,
|
|
edgecolor=grid_color, fancybox=True, shadow=True)
|
|
|
|
# Add annotation for best point
|
|
if minimize_y:
|
|
best_idx = pareto_idx[np.argmin(y[pareto_idx])]
|
|
else:
|
|
best_idx = pareto_idx[np.argmax(y[pareto_idx])]
|
|
|
|
# Professional annotation box - position dynamically based on data location
|
|
# Determine best quadrant for annotation
|
|
x_range = x.max() - x.min()
|
|
y_range = y.max() - y.min()
|
|
x_mid = x.min() + x_range / 2
|
|
y_mid = y.min() + y_range / 2
|
|
|
|
# Place annotation away from the best point
|
|
if x[best_idx] < x_mid:
|
|
x_offset = 50
|
|
else:
|
|
x_offset = -120
|
|
if y[best_idx] < y_mid:
|
|
y_offset = 50
|
|
else:
|
|
y_offset = -60
|
|
|
|
ax.annotate(f'Best WFE: {y[best_idx]:.2f}\n{x_label.split()[0]}: {x[best_idx]:.1f}',
|
|
xy=(x[best_idx], y[best_idx]),
|
|
xytext=(x_offset, y_offset), textcoords='offset points',
|
|
fontsize=11, fontweight='bold',
|
|
bbox=dict(boxstyle='round,pad=0.6', facecolor='white',
|
|
edgecolor=pareto_color, linewidth=2, alpha=0.95),
|
|
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.2',
|
|
color=pareto_color, lw=2))
|
|
|
|
# Statistics box in bottom left
|
|
stats_text = f'Total designs explored: {len(df_clean):,}\nPareto optimal: {len(pareto_idx)}'
|
|
ax.text(0.02, 0.02, stats_text, transform=ax.transAxes, fontsize=10,
|
|
verticalalignment='bottom',
|
|
bbox=dict(boxstyle='round,pad=0.5', facecolor='white',
|
|
edgecolor=grid_color, alpha=0.9))
|
|
|
|
# Adjust spines
|
|
for spine in ax.spines.values():
|
|
spine.set_color(grid_color)
|
|
spine.set_linewidth(1.5)
|
|
|
|
plt.tight_layout()
|
|
plt.savefig(filename, dpi=200, bbox_inches='tight', facecolor='white',
|
|
edgecolor='none', pad_inches=0.2)
|
|
plt.close()
|
|
print(f"Saved: {filename}")
|
|
|
|
return pareto_sorted, pareto_idx
|
|
|
|
|
|
# Create plots
|
|
output_dir = r'c:\Users\antoi\Atomizer\studies'
|
|
|
|
# 1. Blank Thickness vs 60/20 WFE
|
|
print("\n--- Creating Blank Thickness vs WFE plot ---")
|
|
if df_valid[thickness_col].notna().any():
|
|
result = create_pareto_plot(
|
|
df_valid,
|
|
x_col=thickness_col,
|
|
y_col=wfe_col,
|
|
x_label='Blank Thickness (mm)',
|
|
y_label='60/20 WFE (Relative RMS)',
|
|
title='M1 Mirror Optimization\nBlank Thickness vs Wavefront Error',
|
|
filename=f'{output_dir}\\pareto_thickness_vs_wfe.png',
|
|
minimize_x=False, # Thinner may be desirable
|
|
minimize_y=True, # Lower WFE is better
|
|
color_by='mass_kg' if 'mass_kg' in df_valid.columns else None,
|
|
color_label='Mass (kg)'
|
|
)
|
|
else:
|
|
print("No thickness data available")
|
|
|
|
# 2. Blank Backface Angle vs 60/20 WFE
|
|
print("\n--- Creating Backface Angle vs WFE plot ---")
|
|
if df_valid[angle_col].notna().any():
|
|
result = create_pareto_plot(
|
|
df_valid,
|
|
x_col=angle_col,
|
|
y_col=wfe_col,
|
|
x_label='Blank Backface Angle (degrees)',
|
|
y_label='60/20 WFE (Relative RMS)',
|
|
title='M1 Mirror Optimization\nBackface Angle vs Wavefront Error',
|
|
filename=f'{output_dir}\\pareto_angle_vs_wfe.png',
|
|
minimize_x=False,
|
|
minimize_y=True,
|
|
color_by='mass_kg' if 'mass_kg' in df_valid.columns else None,
|
|
color_label='Mass (kg)'
|
|
)
|
|
else:
|
|
print("No backface angle data available")
|
|
|
|
# 3. Combined 2D Design Space plot
|
|
print("\n--- Creating Design Space plot ---")
|
|
if df_valid[thickness_col].notna().any() and df_valid[angle_col].notna().any():
|
|
mask = df_valid[thickness_col].notna() & df_valid[angle_col].notna()
|
|
df_both = df_valid[mask].copy()
|
|
|
|
if len(df_both) > 0:
|
|
fig, ax = plt.subplots(figsize=(12, 9))
|
|
fig.patch.set_facecolor('white')
|
|
|
|
bg_color = '#f8f9fa'
|
|
grid_color = '#dee2e6'
|
|
ax.set_facecolor(bg_color)
|
|
|
|
# Use a perceptually uniform colormap
|
|
scatter = ax.scatter(
|
|
df_both[thickness_col],
|
|
df_both[angle_col],
|
|
c=df_both[wfe_col],
|
|
cmap='RdYlGn_r', # Red=bad (high WFE), Green=good (low WFE)
|
|
s=100,
|
|
alpha=0.8,
|
|
edgecolors='white',
|
|
linewidth=0.5,
|
|
vmin=df_both[wfe_col].quantile(0.05),
|
|
vmax=df_both[wfe_col].quantile(0.95)
|
|
)
|
|
|
|
cbar = plt.colorbar(scatter, ax=ax, pad=0.02, shrink=0.85)
|
|
cbar.set_label('60/20 WFE (Relative RMS)\nLower = Better Performance',
|
|
fontsize=12, fontweight='bold')
|
|
cbar.ax.tick_params(labelsize=10)
|
|
|
|
ax.set_xlabel('Blank Thickness (mm)', fontsize=14, fontweight='bold', labelpad=12)
|
|
ax.set_ylabel('Blank Backface Angle (degrees)', fontsize=14, fontweight='bold', labelpad=12)
|
|
ax.set_title('M1 Mirror Design Space Exploration\nGeometric Parameters vs Optical Performance',
|
|
fontsize=18, fontweight='bold', pad=20)
|
|
|
|
ax.grid(True, alpha=0.5, color=grid_color)
|
|
ax.minorticks_on()
|
|
ax.grid(True, which='minor', alpha=0.2, linestyle=':', color=grid_color)
|
|
|
|
# Mark best point with star
|
|
best_idx = df_both[wfe_col].idxmin()
|
|
best_row = df_both.loc[best_idx]
|
|
ax.scatter(best_row[thickness_col], best_row[angle_col],
|
|
c='#ffc107', s=400, marker='*', edgecolors='#dc3545', linewidth=3,
|
|
zorder=5, label=f'Best Design (WFE={best_row[wfe_col]:.2f})')
|
|
|
|
# Add annotation for best point - position in upper left to avoid overlap
|
|
ax.annotate(f'Best Design\nThickness: {best_row[thickness_col]:.1f}mm\nAngle: {best_row[angle_col]:.2f}°\nWFE: {best_row[wfe_col]:.2f}',
|
|
xy=(best_row[thickness_col], best_row[angle_col]),
|
|
xytext=(-100, 60), textcoords='offset points',
|
|
fontsize=10, fontweight='bold',
|
|
bbox=dict(boxstyle='round,pad=0.6', facecolor='white',
|
|
edgecolor='#dc3545', linewidth=2, alpha=0.95),
|
|
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.3',
|
|
color='#dc3545', lw=2))
|
|
|
|
ax.legend(loc='upper right', fontsize=11, framealpha=0.95, fancybox=True, shadow=True)
|
|
|
|
# Stats
|
|
stats_text = f'Designs evaluated: {len(df_both):,}'
|
|
ax.text(0.02, 0.02, stats_text, transform=ax.transAxes, fontsize=10,
|
|
verticalalignment='bottom',
|
|
bbox=dict(boxstyle='round,pad=0.5', facecolor='white',
|
|
edgecolor=grid_color, alpha=0.9))
|
|
|
|
for spine in ax.spines.values():
|
|
spine.set_color(grid_color)
|
|
spine.set_linewidth(1.5)
|
|
|
|
plt.tight_layout()
|
|
plt.savefig(f'{output_dir}\\design_space_wfe.png',
|
|
dpi=200, bbox_inches='tight', facecolor='white', pad_inches=0.2)
|
|
plt.close()
|
|
print(f"Saved: design_space_wfe.png")
|
|
else:
|
|
print("Not enough data for combined design space plot")
|
|
|
|
print("\n" + "="*60)
|
|
print("PARETO VISUALIZATION COMPLETE")
|
|
print("="*60)
|
|
print(f"\nOutput files saved to: {output_dir}")
|
|
print("\nFiles created:")
|
|
print(" 1. pareto_thickness_vs_wfe.png - Thickness vs WFE Pareto front")
|
|
print(" 2. pareto_angle_vs_wfe.png - Backface Angle vs WFE Pareto front")
|
|
print(" 3. design_space_wfe.png - Combined design space heatmap")
|