#!/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([])
