#!/usr/bin/env python """ Test ZernikeOPDExtractor with validated M1 Mirror optical prescription. Compares: 1. Standard Zernike (Z-displacement only at original x,y) 2. OPD Zernike with auto-estimated focal length 3. OPD Zernike with correct focal length (1445 mm from prescription) M1 Mirror Optical Prescription: - Radius of Curvature: 2890 ± 3 mm - Conic Constant: -0.987 ± 0.001 (near-parabolic) - Clear Aperture: 1202 mm - Central Bore: 271.56 mm - Focal Length: 1445 mm (R/2) """ from pathlib import Path import sys # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) import numpy as np def run_comparison(op2_path: Path): """Run comparison between standard and OPD Zernike methods.""" from optimization_engine.extractors.extract_zernike_opd import ZernikeOPDExtractor from optimization_engine.extractors.extract_zernike_wfe import ZernikeExtractor print("=" * 70) print("ZERNIKE METHOD COMPARISON WITH VALIDATED PRESCRIPTION") print("=" * 70) print(f"\nOP2 file: {op2_path.name}") print(f"Optical prescription focal length: 1445 mm") print() # 1. Standard Zernike (Z-displacement only) print("1. STANDARD ZERNIKE (Z-displacement at original x,y)") print("-" * 50) try: std_extractor = ZernikeExtractor(op2_path) std_results = std_extractor.extract_all_subcases() for sc, data in std_results.items(): coeffs = data['coefficients'] rms = data['rms_wfe_nm'] print(f" Subcase {sc}: RMS WFE = {rms:.2f} nm") except Exception as e: print(f" Error: {e}") std_results = None print() # 2. OPD Zernike with auto-estimated focal length print("2. OPD ZERNIKE (auto-estimated focal length)") print("-" * 50) try: opd_auto = ZernikeOPDExtractor(op2_path, concave=True) auto_focal = opd_auto.focal_length print(f" Auto-estimated focal length: {auto_focal:.1f} mm") opd_auto_results = opd_auto.extract_all_subcases() for sc, data in opd_auto_results.items(): rms = data['rms_wfe_nm'] lat = data.get('max_lateral_displacement_um', 0) print(f" Subcase {sc}: RMS WFE = {rms:.2f} nm, Max lateral = {lat:.2f} µm") except Exception as e: print(f" Error: {e}") opd_auto_results = None print() # 3. OPD Zernike with correct prescription focal length print("3. OPD ZERNIKE (prescription focal length = 1445 mm)") print("-" * 50) try: opd_correct = ZernikeOPDExtractor(op2_path, focal_length=1445.0, concave=True) print(f" Using focal length: {opd_correct.focal_length:.1f} mm") opd_correct_results = opd_correct.extract_all_subcases() for sc, data in opd_correct_results.items(): rms = data['rms_wfe_nm'] lat = data.get('max_lateral_displacement_um', 0) print(f" Subcase {sc}: RMS WFE = {rms:.2f} nm, Max lateral = {lat:.2f} µm") except Exception as e: print(f" Error: {e}") opd_correct_results = None print() # 4. Comparison summary if std_results and opd_correct_results: print("=" * 70) print("COMPARISON SUMMARY") print("=" * 70) print() print(f"{'Subcase':<10} {'Standard':<15} {'OPD (auto)':<15} {'OPD (1445mm)':<15} {'Diff %':<10}") print("-" * 65) for sc in std_results.keys(): std_rms = std_results[sc]['rms_wfe_nm'] auto_rms = opd_auto_results[sc]['rms_wfe_nm'] if opd_auto_results else 0 corr_rms = opd_correct_results[sc]['rms_wfe_nm'] diff_pct = ((corr_rms - std_rms) / std_rms * 100) if std_rms > 0 else 0 print(f"{sc:<10} {std_rms:<15.2f} {auto_rms:<15.2f} {corr_rms:<15.2f} {diff_pct:>+8.1f}%") print() print("LATERAL DISPLACEMENT ANALYSIS") print("-" * 50) for sc, data in opd_correct_results.items(): lat = data.get('max_lateral_displacement_um', 0) severity = "CRITICAL - OPD method required" if lat > 10 else "Low - standard OK" if lat < 1 else "Moderate" print(f" Subcase {sc}: Max lateral = {lat:.2f} µm ({severity})") print() # Tracking WFE comparison (40-20 and 60-20) if 2 in opd_correct_results and 3 in opd_correct_results and 4 in opd_correct_results: print("TRACKING WFE (differential between elevations)") print("-" * 50) # Get coefficients for differential analysis z20 = np.array(opd_correct_results[2]['coefficients']) z40 = np.array(opd_correct_results[3]['coefficients']) z60 = np.array(opd_correct_results[4]['coefficients']) # Differential (remove J1-J4: piston, tip, tilt, defocus) diff_40_20 = z40 - z20 diff_60_20 = z60 - z20 # RMS of filtered differential (J5+) rms_40_20 = np.sqrt(np.sum(diff_40_20[4:]**2)) # Skip J1-J4 rms_60_20 = np.sqrt(np.sum(diff_60_20[4:]**2)) print(f" 40°-20° tracking WFE: {rms_40_20:.2f} nm RMS (filtered)") print(f" 60°-20° tracking WFE: {rms_60_20:.2f} nm RMS (filtered)") print() print(" Standard method comparison:") z20_std = np.array(std_results[2]['coefficients']) z40_std = np.array(std_results[3]['coefficients']) z60_std = np.array(std_results[4]['coefficients']) diff_40_20_std = z40_std - z20_std diff_60_20_std = z60_std - z20_std rms_40_20_std = np.sqrt(np.sum(diff_40_20_std[4:]**2)) rms_60_20_std = np.sqrt(np.sum(diff_60_20_std[4:]**2)) print(f" 40°-20° tracking WFE (std): {rms_40_20_std:.2f} nm RMS") print(f" 60°-20° tracking WFE (std): {rms_60_20_std:.2f} nm RMS") print() print(f" Difference (OPD vs Standard):") print(f" 40°-20°: {rms_40_20 - rms_40_20_std:+.2f} nm ({(rms_40_20/rms_40_20_std - 1)*100:+.1f}%)") print(f" 60°-20°: {rms_60_20 - rms_60_20_std:+.2f} nm ({(rms_60_20/rms_60_20_std - 1)*100:+.1f}%)") print() print("=" * 70) def main(): import argparse parser = argparse.ArgumentParser(description='Test ZernikeOPD with M1 prescription') parser.add_argument('path', nargs='?', default='.', help='Path to OP2 file or study directory') args = parser.parse_args() path = Path(args.path).resolve() # Find OP2 file if path.is_file() and path.suffix.lower() == '.op2': op2_path = path elif path.is_dir(): # Look for best design or recent iteration patterns = [ '3_results/best_design_archive/**/*.op2', '2_iterations/iter1/*.op2', '**/*.op2' ] for pattern in patterns: files = list(path.glob(pattern)) if files: op2_path = max(files, key=lambda p: p.stat().st_mtime) break else: print(f"No OP2 file found in {path}") sys.exit(1) else: print(f"Invalid path: {path}") sys.exit(1) run_comparison(op2_path) if __name__ == '__main__': main()