Files
Atomizer/nx_journals/analyze_wfe_zernike.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

234 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Atomizer Zernike WFE Analyzer
=============================
Analyze Zernike wavefront error from NX Nastran OP2 results.
IMPORTANT: This script requires numpy/scipy. Run from command line with
the atomizer conda environment, NOT from within NX.
Usage:
conda activate atomizer
python analyze_wfe_zernike.py "path/to/solution.op2"
# Or without argument - searches current directory for OP2 files:
python analyze_wfe_zernike.py
Output:
- Zernike coefficients for each subcase
- Relative WFE metrics (filtered RMS)
- Manufacturing workload (J1-J3 filtered)
- Weighted sum calculation
Author: Atomizer
Created: 2025-12-18
"""
import sys
import os
from pathlib import Path
def log(msg):
"""Print to console."""
print(msg)
def find_op2_file(working_dir=None):
"""Find the most recent OP2 file in the working directory."""
if working_dir is None:
working_dir = Path.cwd()
else:
working_dir = Path(working_dir)
# Look for OP2 files
op2_files = list(working_dir.glob("*solution*.op2")) + list(working_dir.glob("*.op2"))
if not op2_files:
# Check subdirectories
op2_files = list(working_dir.glob("**/*solution*.op2"))
if not op2_files:
return None
# Return most recently modified
return max(op2_files, key=lambda p: p.stat().st_mtime)
def analyze_zernike(op2_path):
"""Run Zernike analysis on OP2 file."""
# Add Atomizer to path
atomizer_root = Path(__file__).parent.parent
if str(atomizer_root) not in sys.path:
sys.path.insert(0, str(atomizer_root))
try:
from optimization_engine.extractors import ZernikeExtractor
except ImportError as e:
log(f"ERROR: Could not import ZernikeExtractor: {e}")
log(f"Make sure Atomizer is properly installed.")
log(f"Atomizer root: {atomizer_root}")
return None
log("=" * 70)
log("ZERNIKE WAVEFRONT ERROR ANALYSIS")
log("=" * 70)
log(f"OP2 File: {op2_path.name}")
log(f"Directory: {op2_path.parent}")
log("")
# Create extractor
try:
extractor = ZernikeExtractor(
op2_path,
bdf_path=None,
displacement_unit='mm',
n_modes=50,
filter_orders=4
)
except Exception as e:
log(f"ERROR creating extractor: {e}")
return None
# Get available subcases from the extractor's displacement data
subcases = list(extractor.displacements.keys())
log(f"Available subcases: {subcases}")
log("")
# Standard subcase mapping for M1 mirror
subcase_labels = {
'1': '90 deg (Manufacturing/Polishing)',
'2': '20 deg (Reference)',
'3': '40 deg (Operational)',
'4': '60 deg (Operational)'
}
# Extract absolute Zernike for each subcase
log("-" * 70)
log("ABSOLUTE ZERNIKE ANALYSIS (per subcase)")
log("-" * 70)
results = {}
for sc in subcases:
try:
result = extractor.extract_subcase(sc)
results[sc] = result
label = subcase_labels.get(sc, f'Subcase {sc}')
log(f"\n{label}:")
log(f" Global RMS: {result['global_rms_nm']:.2f} nm")
log(f" Filtered RMS: {result['filtered_rms_nm']:.2f} nm (J4+ only)")
except Exception as e:
log(f" ERROR extracting subcase {sc}: {e}")
# Relative analysis (using subcase 2 as reference)
ref_subcase = '2'
if ref_subcase in subcases:
log("")
log("-" * 70)
log(f"RELATIVE ANALYSIS (vs {subcase_labels.get(ref_subcase, ref_subcase)})")
log("-" * 70)
relative_results = {}
for sc in subcases:
if sc == ref_subcase:
continue
try:
rel = extractor.extract_relative(sc, ref_subcase)
relative_results[sc] = rel
label = subcase_labels.get(sc, f'Subcase {sc}')
log(f"\n{label} vs Reference:")
log(f" Relative Filtered RMS: {rel['relative_filtered_rms_nm']:.2f} nm")
if 'relative_rms_filter_j1to3' in rel:
log(f" J1-J3 Filtered RMS: {rel['relative_rms_filter_j1to3']:.2f} nm")
except Exception as e:
log(f" ERROR: {e}")
# Calculate weighted sum (M1 mirror optimization objectives)
log("")
log("-" * 70)
log("OPTIMIZATION OBJECTIVES")
log("-" * 70)
obj_40_20 = relative_results.get('3', {}).get('relative_filtered_rms_nm', 0)
obj_60_20 = relative_results.get('4', {}).get('relative_filtered_rms_nm', 0)
obj_mfg = relative_results.get('1', {}).get('relative_rms_filter_j1to3', 0)
log(f"\n 40-20 Filtered RMS: {obj_40_20:.2f} nm")
log(f" 60-20 Filtered RMS: {obj_60_20:.2f} nm")
log(f" MFG 90 (J1-J3): {obj_mfg:.2f} nm")
# Weighted sums for different weight configurations
log("")
log("Weighted Sum Calculations:")
# V4 weights: 5*40 + 5*60 + 2*mfg + mass
ws_v4 = 5*obj_40_20 + 5*obj_60_20 + 2*obj_mfg
log(f" V4 weights (5/5/2): {ws_v4:.2f} (+ mass)")
# V5 weights: 5*40 + 5*60 + 3*mfg + mass
ws_v5 = 5*obj_40_20 + 5*obj_60_20 + 3*obj_mfg
log(f" V5 weights (5/5/3): {ws_v5:.2f} (+ mass)")
return {
'absolute': results,
'relative': relative_results,
'objectives': {
'40_20': obj_40_20,
'60_20': obj_60_20,
'mfg_90': obj_mfg,
'ws_v4': ws_v4,
'ws_v5': ws_v5
}
}
return {'absolute': results}
def main(args):
"""Main entry point."""
log("")
log("=" * 70)
log(" ATOMIZER ZERNIKE WFE ANALYZER")
log("=" * 70)
log("")
# Determine OP2 file
op2_path = None
if args and len(args) > 0 and args[0]:
# OP2 path provided as argument
op2_path = Path(args[0])
if not op2_path.exists():
log(f"ERROR: OP2 file not found: {op2_path}")
return
else:
# Try to find OP2 in current directory
log("No OP2 file specified, searching...")
op2_path = find_op2_file()
if op2_path is None:
log("ERROR: No OP2 file found in current directory.")
log("Usage: Run after solving, or provide OP2 path as argument.")
return
log(f"Found: {op2_path}")
# Run analysis
results = analyze_zernike(op2_path)
if results:
log("")
log("=" * 70)
log("ANALYSIS COMPLETE")
log("=" * 70)
else:
log("")
log("Analysis failed. Check errors above.")
if __name__ == '__main__':
# Get arguments (works both in NX and command line)
if len(sys.argv) > 1:
main(sys.argv[1:])
else:
main([])