Files
Atomizer/tools/create_pareto_graphs.py
Anto01 f13563d7ab feat: Major update - Physics docs, Zernike OPD, insights, NX journals, tools
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>
2025-12-23 19:47:37 -05:00

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")