feat: Pre-migration checkpoint - updated docs and utilities
Updates before optimization_engine migration: - Updated migration plan to v2.1 with complete file inventory - Added OP_07 disk optimization protocol - Added SYS_16 self-aware turbo protocol - Added study archiver and cleanup utilities - Added ensemble surrogate module - Updated NX solver and session manager - Updated zernike HTML generator - Added context engineering plan - LAC session insights updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
32
tools/archive_study.bat
Normal file
32
tools/archive_study.bat
Normal file
@@ -0,0 +1,32 @@
|
||||
@echo off
|
||||
REM Atomizer Study Archiver - Convenience Script
|
||||
REM Usage: archive_study.bat <command> [study_path]
|
||||
REM
|
||||
REM Commands:
|
||||
REM analyze - Show disk usage analysis
|
||||
REM cleanup - Remove regenerable files (dry run by default)
|
||||
REM archive - Archive to dalidou server
|
||||
REM list - List archived studies on server
|
||||
REM
|
||||
REM Examples:
|
||||
REM archive_study.bat analyze studies\M1_Mirror
|
||||
REM archive_study.bat cleanup studies\M1_Mirror\m1_mirror_V12 --execute
|
||||
REM archive_study.bat archive studies\M1_Mirror\m1_mirror_V12 --execute
|
||||
|
||||
cd /d C:\Users\antoi\Atomizer
|
||||
|
||||
if "%1"=="" (
|
||||
echo Usage: archive_study.bat ^<command^> [path] [options]
|
||||
echo.
|
||||
echo Commands:
|
||||
echo analyze ^<path^> - Analyze disk usage
|
||||
echo cleanup ^<study^> [--execute] - Remove regenerable files
|
||||
echo archive ^<study^> [--execute] - Archive to dalidou
|
||||
echo restore ^<name^> - Restore from dalidou
|
||||
echo list - List remote archives
|
||||
echo.
|
||||
echo Add --tailscale for remote access via Tailscale
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
C:\Users\antoi\anaconda3\envs\atomizer\python.exe -m optimization_engine.utils.study_archiver %*
|
||||
@@ -8,6 +8,11 @@ Generates 3 interactive HTML reports for Zernike wavefront analysis:
|
||||
2. 60° vs 20° (relative) - Operational angle comparison
|
||||
3. 90° (Manufacturing) - Absolute with manufacturing metrics
|
||||
|
||||
Uses the rigorous OPD method from extract_zernike_figure.py which:
|
||||
- Accounts for lateral (X, Y) displacement via interpolation
|
||||
- Uses actual mesh geometry as reference (no shape assumptions)
|
||||
- Provides more accurate WFE for mirror optimization
|
||||
|
||||
Usage:
|
||||
conda activate atomizer
|
||||
python zernike_html_generator.py "path/to/solution.op2"
|
||||
@@ -23,6 +28,7 @@ Output:
|
||||
|
||||
Author: Atomizer
|
||||
Created: 2025-12-19
|
||||
Updated: 2025-12-28 - Upgraded to use rigorous OPD method
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -49,6 +55,15 @@ except ImportError as e:
|
||||
print("Run: conda activate atomizer")
|
||||
sys.exit(1)
|
||||
|
||||
# Import the rigorous OPD extractor
|
||||
try:
|
||||
from optimization_engine.extractors.extract_zernike_figure import ZernikeOPDExtractor
|
||||
USE_OPD_METHOD = True
|
||||
print("[INFO] Using rigorous OPD method (accounts for lateral displacement)")
|
||||
except ImportError:
|
||||
USE_OPD_METHOD = False
|
||||
print("[WARN] OPD extractor not available, falling back to simple Z-only method")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
@@ -278,13 +293,31 @@ def compute_rms_metrics(X, Y, W_nm):
|
||||
|
||||
|
||||
def compute_mfg_metrics(coeffs):
|
||||
"""Manufacturing aberration magnitudes."""
|
||||
"""Manufacturing aberration magnitudes from Zernike coefficients.
|
||||
|
||||
Noll indexing (1-based): J1=Piston, J2=TiltX, J3=TiltY, J4=Defocus,
|
||||
J5=Astig45, J6=Astig0, J7=ComaX, J8=ComaY, J9=TrefoilX, J10=TrefoilY, J11=Spherical
|
||||
|
||||
Python 0-indexed: coeffs[0]=J1, coeffs[3]=J4, etc.
|
||||
"""
|
||||
# Individual mode magnitudes (RSS for paired modes)
|
||||
defocus = float(abs(coeffs[3])) # J4
|
||||
astigmatism = float(np.sqrt(coeffs[4]**2 + coeffs[5]**2)) # RSS(J5, J6)
|
||||
coma = float(np.sqrt(coeffs[6]**2 + coeffs[7]**2)) # RSS(J7, J8)
|
||||
trefoil = float(np.sqrt(coeffs[8]**2 + coeffs[9]**2)) # RSS(J9, J10)
|
||||
spherical = float(abs(coeffs[10])) if len(coeffs) > 10 else 0.0 # J11
|
||||
|
||||
# RMS of higher-order terms (J4+): sqrt(sum of squares of coefficients)
|
||||
# This is the proper Zernike-coefficient-based RMS excluding piston/tip/tilt
|
||||
higher_order_rms = float(np.sqrt(np.sum(coeffs[3:]**2)))
|
||||
|
||||
return {
|
||||
'defocus_nm': float(abs(coeffs[3])),
|
||||
'astigmatism_rms': float(np.sqrt(coeffs[4]**2 + coeffs[5]**2)),
|
||||
'coma_rms': float(np.sqrt(coeffs[6]**2 + coeffs[7]**2)),
|
||||
'trefoil_rms': float(np.sqrt(coeffs[8]**2 + coeffs[9]**2)),
|
||||
'spherical_nm': float(abs(coeffs[10])) if len(coeffs) > 10 else 0.0,
|
||||
'defocus_nm': defocus,
|
||||
'astigmatism_rms': astigmatism,
|
||||
'coma_rms': coma,
|
||||
'trefoil_rms': trefoil,
|
||||
'spherical_nm': spherical,
|
||||
'higher_order_rms': higher_order_rms, # RMS of all J4+ coefficients
|
||||
}
|
||||
|
||||
|
||||
@@ -502,19 +535,22 @@ def generate_html(
|
||||
], align="left", fill_color='#374151', font=dict(color='white'))
|
||||
), row=3, col=1)
|
||||
|
||||
# Pre-correction (row 4)
|
||||
# Pre-correction (row 4) - Aberrations to polish out (90° - 20°)
|
||||
# Shows what correction is needed when manufacturing at 90° to achieve 20° figure
|
||||
fig.add_trace(go.Table(
|
||||
header=dict(values=["<b>Mode</b>", "<b>Correction (nm)</b>"],
|
||||
header=dict(values=["<b>Aberration</b>", "<b>Magnitude (nm)</b>"],
|
||||
align="left", fill_color='#1f2937', font=dict(color='white')),
|
||||
cells=dict(values=[
|
||||
["Total RMS (J1-J3 filter)",
|
||||
"Defocus (J4)",
|
||||
["Defocus (J4)",
|
||||
"Astigmatism (J5+J6)",
|
||||
"Coma (J7+J8)"],
|
||||
[f"{correction_metrics['rms_filter_j1to3']:.2f}",
|
||||
f"{correction_metrics['defocus_nm']:.2f}",
|
||||
"Coma (J7+J8)",
|
||||
"Trefoil (J9+J10)",
|
||||
"Spherical (J11)"],
|
||||
[f"{correction_metrics['defocus_nm']:.2f}",
|
||||
f"{correction_metrics['astigmatism_rms']:.2f}",
|
||||
f"{correction_metrics['coma_rms']:.2f}"]
|
||||
f"{correction_metrics['coma_rms']:.2f}",
|
||||
f"{correction_metrics['trefoil_rms']:.2f}",
|
||||
f"{correction_metrics['spherical_nm']:.2f}"]
|
||||
], align="left", fill_color='#374151', font=dict(color='white'))
|
||||
), row=4, col=1)
|
||||
else:
|
||||
@@ -595,8 +631,248 @@ def find_op2_file(working_dir=None):
|
||||
return max(op2_files, key=lambda p: p.stat().st_mtime)
|
||||
|
||||
|
||||
def main_opd(op2_path: Path):
|
||||
"""Generate all 3 HTML files using rigorous OPD method."""
|
||||
print("=" * 70)
|
||||
print(" ATOMIZER ZERNIKE HTML GENERATOR (OPD METHOD)")
|
||||
print("=" * 70)
|
||||
print(f"\nOP2 File: {op2_path.name}")
|
||||
print(f"Directory: {op2_path.parent}")
|
||||
print("\n[INFO] Using OPD method: accounts for lateral (X,Y) displacement")
|
||||
|
||||
# Initialize extractor
|
||||
extractor = ZernikeOPDExtractor(
|
||||
op2_path,
|
||||
displacement_unit='mm',
|
||||
n_modes=N_MODES,
|
||||
filter_orders=FILTER_LOW_ORDERS
|
||||
)
|
||||
|
||||
print(f"\nAvailable subcases: {list(extractor.displacements.keys())}")
|
||||
|
||||
# Map subcases (try common patterns)
|
||||
displacements = extractor.displacements
|
||||
subcase_map = {}
|
||||
|
||||
if '1' in displacements and '2' in displacements:
|
||||
subcase_map = {'90': '1', '20': '2', '40': '3', '60': '4'}
|
||||
elif '90' in displacements and '20' in displacements:
|
||||
subcase_map = {'90': '90', '20': '20', '40': '40', '60': '60'}
|
||||
else:
|
||||
available = sorted(displacements.keys(), key=lambda x: int(x) if x.isdigit() else 0)
|
||||
if len(available) >= 4:
|
||||
subcase_map = {'90': available[0], '20': available[1], '40': available[2], '60': available[3]}
|
||||
print(f"[WARN] Using mapped subcases: {subcase_map}")
|
||||
else:
|
||||
print(f"[ERROR] Need 4 subcases, found: {available}")
|
||||
return
|
||||
|
||||
output_dir = op2_path.parent
|
||||
base = op2_path.stem
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
html_files = []
|
||||
|
||||
# ========================================================================
|
||||
# Extract absolute metrics for each subcase
|
||||
# ========================================================================
|
||||
print("\nExtracting absolute metrics (OPD method)...")
|
||||
|
||||
results_abs = {}
|
||||
for angle, label in subcase_map.items():
|
||||
result = extractor.extract_subcase(label, include_coefficients=True)
|
||||
results_abs[angle] = result
|
||||
lat_disp = result.get('max_lateral_displacement_um', 0)
|
||||
print(f" {angle} deg: Filtered RMS = {result['filtered_rms_nm']:.2f} nm, "
|
||||
f"Lateral disp max = {lat_disp:.3f} um")
|
||||
|
||||
# ========================================================================
|
||||
# Extract relative metrics (40-20, 60-20, 90-20)
|
||||
# ========================================================================
|
||||
print("\nExtracting relative metrics (OPD method)...")
|
||||
|
||||
# 40 vs 20
|
||||
result_40_rel = extractor.extract_relative(subcase_map['40'], subcase_map['20'], include_coefficients=True)
|
||||
print(f" 40-20: Relative Filtered RMS = {result_40_rel['relative_filtered_rms_nm']:.2f} nm")
|
||||
|
||||
# 60 vs 20
|
||||
result_60_rel = extractor.extract_relative(subcase_map['60'], subcase_map['20'], include_coefficients=True)
|
||||
print(f" 60-20: Relative Filtered RMS = {result_60_rel['relative_filtered_rms_nm']:.2f} nm")
|
||||
|
||||
# 90 vs 20 (for correction metrics)
|
||||
result_90_rel = extractor.extract_relative(subcase_map['90'], subcase_map['20'], include_coefficients=True)
|
||||
print(f" 90-20: Relative Filtered RMS = {result_90_rel['relative_filtered_rms_nm']:.2f} nm")
|
||||
|
||||
# ========================================================================
|
||||
# Generate HTML files
|
||||
# ========================================================================
|
||||
|
||||
# Helper to convert OPD results to the format expected by generate_html
|
||||
def opd_to_rms_data(result, is_relative=False):
|
||||
"""Convert OPD extractor result to rms_data dict for generate_html."""
|
||||
coeffs = np.array(result.get('coefficients', [0] * N_MODES))
|
||||
|
||||
# Recompute filtered residuals for visualization
|
||||
# For now, use simplified approach - the metrics are correct
|
||||
filtered_rms = result.get('relative_filtered_rms_nm' if is_relative else 'filtered_rms_nm', 0)
|
||||
global_rms = result.get('relative_global_rms_nm' if is_relative else 'global_rms_nm', 0)
|
||||
rms_j1to3 = result.get('relative_rms_filter_j1to3' if is_relative else 'rms_filter_j1to3_nm', 0)
|
||||
|
||||
# We need W_res_filt for visualization - extract from diagnostic data
|
||||
# For now, create a placeholder that will be updated
|
||||
return {
|
||||
'coefficients': coeffs,
|
||||
'R': 1.0, # Will be updated
|
||||
'global_rms': global_rms,
|
||||
'filtered_rms': filtered_rms,
|
||||
'rms_filter_j1to3': rms_j1to3,
|
||||
'W_res_filt': None, # Will compute separately for visualization
|
||||
}
|
||||
|
||||
# For visualization, we need the actual WFE arrays
|
||||
# Get diagnostic data from extractor
|
||||
print("\nGenerating HTML reports...")
|
||||
|
||||
# 40 vs 20
|
||||
print(" Generating 40 deg vs 20 deg...")
|
||||
opd_40 = extractor._build_figure_opd_data(subcase_map['40'])
|
||||
opd_20 = extractor._build_figure_opd_data(subcase_map['20'])
|
||||
|
||||
# Build relative WFE arrays
|
||||
ref_wfe_map = {int(nid): wfe for nid, wfe in zip(opd_20['node_ids'], opd_20['wfe_nm'])}
|
||||
X_40_rel, Y_40_rel, WFE_40_rel = [], [], []
|
||||
for i, nid in enumerate(opd_40['node_ids']):
|
||||
nid = int(nid)
|
||||
if nid in ref_wfe_map:
|
||||
X_40_rel.append(opd_40['x_deformed'][i])
|
||||
Y_40_rel.append(opd_40['y_deformed'][i])
|
||||
WFE_40_rel.append(opd_40['wfe_nm'][i] - ref_wfe_map[nid])
|
||||
X_40_rel = np.array(X_40_rel)
|
||||
Y_40_rel = np.array(Y_40_rel)
|
||||
WFE_40_rel = np.array(WFE_40_rel)
|
||||
|
||||
rms_40_rel = compute_rms_metrics(X_40_rel, Y_40_rel, WFE_40_rel)
|
||||
rms_40_abs = compute_rms_metrics(opd_40['x_deformed'], opd_40['y_deformed'], opd_40['wfe_nm'])
|
||||
|
||||
html_40 = generate_html(
|
||||
title="40 deg (OPD)",
|
||||
X=X_40_rel, Y=Y_40_rel, W_nm=WFE_40_rel,
|
||||
rms_data=rms_40_rel,
|
||||
is_relative=True,
|
||||
ref_title="20 deg",
|
||||
abs_pair=(rms_40_abs['global_rms'], rms_40_abs['filtered_rms'])
|
||||
)
|
||||
path_40 = output_dir / f"{base}_{timestamp}_40_vs_20.html"
|
||||
path_40.write_text(html_40, encoding='utf-8')
|
||||
html_files.append(path_40)
|
||||
print(f" Created: {path_40.name}")
|
||||
|
||||
# 60 vs 20
|
||||
print(" Generating 60 deg vs 20 deg...")
|
||||
opd_60 = extractor._build_figure_opd_data(subcase_map['60'])
|
||||
|
||||
X_60_rel, Y_60_rel, WFE_60_rel = [], [], []
|
||||
for i, nid in enumerate(opd_60['node_ids']):
|
||||
nid = int(nid)
|
||||
if nid in ref_wfe_map:
|
||||
X_60_rel.append(opd_60['x_deformed'][i])
|
||||
Y_60_rel.append(opd_60['y_deformed'][i])
|
||||
WFE_60_rel.append(opd_60['wfe_nm'][i] - ref_wfe_map[nid])
|
||||
X_60_rel = np.array(X_60_rel)
|
||||
Y_60_rel = np.array(Y_60_rel)
|
||||
WFE_60_rel = np.array(WFE_60_rel)
|
||||
|
||||
rms_60_rel = compute_rms_metrics(X_60_rel, Y_60_rel, WFE_60_rel)
|
||||
rms_60_abs = compute_rms_metrics(opd_60['x_deformed'], opd_60['y_deformed'], opd_60['wfe_nm'])
|
||||
|
||||
html_60 = generate_html(
|
||||
title="60 deg (OPD)",
|
||||
X=X_60_rel, Y=Y_60_rel, W_nm=WFE_60_rel,
|
||||
rms_data=rms_60_rel,
|
||||
is_relative=True,
|
||||
ref_title="20 deg",
|
||||
abs_pair=(rms_60_abs['global_rms'], rms_60_abs['filtered_rms'])
|
||||
)
|
||||
path_60 = output_dir / f"{base}_{timestamp}_60_vs_20.html"
|
||||
path_60.write_text(html_60, encoding='utf-8')
|
||||
html_files.append(path_60)
|
||||
print(f" Created: {path_60.name}")
|
||||
|
||||
# 90 deg Manufacturing
|
||||
print(" Generating 90 deg Manufacturing...")
|
||||
opd_90 = extractor._build_figure_opd_data(subcase_map['90'])
|
||||
rms_90 = compute_rms_metrics(opd_90['x_deformed'], opd_90['y_deformed'], opd_90['wfe_nm'])
|
||||
mfg_metrics = compute_mfg_metrics(rms_90['coefficients'])
|
||||
|
||||
# 90-20 relative for correction metrics
|
||||
X_90_rel, Y_90_rel, WFE_90_rel = [], [], []
|
||||
for i, nid in enumerate(opd_90['node_ids']):
|
||||
nid = int(nid)
|
||||
if nid in ref_wfe_map:
|
||||
X_90_rel.append(opd_90['x_deformed'][i])
|
||||
Y_90_rel.append(opd_90['y_deformed'][i])
|
||||
WFE_90_rel.append(opd_90['wfe_nm'][i] - ref_wfe_map[nid])
|
||||
X_90_rel = np.array(X_90_rel)
|
||||
Y_90_rel = np.array(Y_90_rel)
|
||||
WFE_90_rel = np.array(WFE_90_rel)
|
||||
rms_90_rel = compute_rms_metrics(X_90_rel, Y_90_rel, WFE_90_rel)
|
||||
|
||||
# Get all correction metrics from Zernike coefficients (90° - 20°)
|
||||
correction_metrics = compute_mfg_metrics(rms_90_rel['coefficients'])
|
||||
|
||||
html_90 = generate_html(
|
||||
title="90 deg Manufacturing (OPD)",
|
||||
X=opd_90['x_deformed'], Y=opd_90['y_deformed'], W_nm=opd_90['wfe_nm'],
|
||||
rms_data=rms_90,
|
||||
is_relative=False,
|
||||
is_manufacturing=True,
|
||||
mfg_metrics=mfg_metrics,
|
||||
correction_metrics=correction_metrics
|
||||
)
|
||||
path_90 = output_dir / f"{base}_{timestamp}_90_mfg.html"
|
||||
path_90.write_text(html_90, encoding='utf-8')
|
||||
html_files.append(path_90)
|
||||
print(f" Created: {path_90.name}")
|
||||
|
||||
# ========================================================================
|
||||
# Summary
|
||||
# ========================================================================
|
||||
print("\n" + "=" * 70)
|
||||
print("SUMMARY (OPD Method)")
|
||||
print("=" * 70)
|
||||
print(f"\nGenerated {len(html_files)} HTML files:")
|
||||
for f in html_files:
|
||||
print(f" - {f.name}")
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
print("OPTIMIZATION OBJECTIVES (OPD Method)")
|
||||
print("-" * 70)
|
||||
print(f" 40-20 Filtered RMS: {rms_40_rel['filtered_rms']:.2f} nm")
|
||||
print(f" 60-20 Filtered RMS: {rms_60_rel['filtered_rms']:.2f} nm")
|
||||
print(f" MFG 90 (J1-J3): {rms_90_rel['rms_filter_j1to3']:.2f} nm")
|
||||
|
||||
# Weighted sums
|
||||
ws_v4 = 5*rms_40_rel['filtered_rms'] + 5*rms_60_rel['filtered_rms'] + 2*rms_90_rel['rms_filter_j1to3']
|
||||
ws_v5 = 5*rms_40_rel['filtered_rms'] + 5*rms_60_rel['filtered_rms'] + 3*rms_90_rel['rms_filter_j1to3']
|
||||
print(f"\n V4 Weighted Sum (5/5/2): {ws_v4:.2f}")
|
||||
print(f" V5 Weighted Sum (5/5/3): {ws_v5:.2f}")
|
||||
|
||||
# Lateral displacement summary
|
||||
print("\n" + "-" * 70)
|
||||
print("LATERAL DISPLACEMENT SUMMARY")
|
||||
print("-" * 70)
|
||||
for angle in ['20', '40', '60', '90']:
|
||||
lat = results_abs[angle].get('max_lateral_displacement_um', 0)
|
||||
print(f" {angle} deg: max {lat:.3f} um")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("DONE")
|
||||
print("=" * 70)
|
||||
|
||||
return html_files
|
||||
|
||||
|
||||
def main(op2_path: Path):
|
||||
"""Generate all 3 HTML files."""
|
||||
"""Generate all 3 HTML files (legacy Z-only method)."""
|
||||
print("=" * 70)
|
||||
print(" ATOMIZER ZERNIKE HTML GENERATOR")
|
||||
print("=" * 70)
|
||||
@@ -753,12 +1029,8 @@ def main(op2_path: Path):
|
||||
X_ref, Y_ref, WFE_ref, ref_data['node_ids']
|
||||
)
|
||||
rms_90_rel = compute_rms_metrics(X_90_rel, Y_90_rel, WFE_90_rel)
|
||||
correction_metrics = {
|
||||
'rms_filter_j1to3': rms_90_rel['rms_filter_j1to3'],
|
||||
'defocus_nm': compute_mfg_metrics(rms_90_rel['coefficients'])['defocus_nm'],
|
||||
'astigmatism_rms': compute_mfg_metrics(rms_90_rel['coefficients'])['astigmatism_rms'],
|
||||
'coma_rms': compute_mfg_metrics(rms_90_rel['coefficients'])['coma_rms'],
|
||||
}
|
||||
# Get all correction metrics from Zernike coefficients (90° - 20°)
|
||||
correction_metrics = compute_mfg_metrics(rms_90_rel['coefficients'])
|
||||
|
||||
html_90 = generate_html(
|
||||
title="90 deg (Manufacturing)",
|
||||
@@ -822,8 +1094,16 @@ if __name__ == '__main__':
|
||||
sys.exit(1)
|
||||
print(f"Found: {op2_path}")
|
||||
|
||||
# Check for --legacy flag to use old Z-only method
|
||||
use_legacy = '--legacy' in sys.argv or '--z-only' in sys.argv
|
||||
|
||||
try:
|
||||
main(op2_path)
|
||||
if USE_OPD_METHOD and not use_legacy:
|
||||
main_opd(op2_path)
|
||||
else:
|
||||
if use_legacy:
|
||||
print("[INFO] Using legacy Z-only method (--legacy flag)")
|
||||
main(op2_path)
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
import traceback
|
||||
|
||||
Reference in New Issue
Block a user