Files
Atomizer/.claude/skills/core/study-creation-core.md
Anto01 d19fc39a2a feat: Add OPD method support to Zernike visualization with Standard/OPD toggle
Major improvements to Zernike WFE visualization:

- Add ZernikeDashboardInsight: Unified dashboard with all orientations (40°, 60°, 90°)
  on one page with light theme and executive summary
- Add OPD method toggle: Switch between Standard (Z-only) and OPD (X,Y,Z) methods
  in ZernikeWFEInsight with interactive buttons
- Add lateral displacement maps: Visualize X,Y displacement for each orientation
- Add displacement component views: Toggle between WFE, ΔX, ΔY, ΔZ in relative views
- Add metrics comparison table showing both methods side-by-side

New extractors:
- extract_zernike_figure.py: ZernikeOPDExtractor using BDF geometry interpolation
- extract_zernike_opd.py: Parabola-based OPD with focal length

Key finding: OPD method gives 8-11% higher WFE values than Standard method
(more conservative/accurate for surfaces with lateral displacement under gravity)

Documentation updates:
- SYS_12: Added E22 ZernikeOPD as recommended method
- SYS_16: Added ZernikeDashboard, updated ZernikeWFE with OPD features
- Cheatsheet: Added Zernike method comparison table

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 21:03:19 -05:00

23 KiB

skill_id, version, last_updated, type, code_dependencies, requires_skills, replaces
skill_id version last_updated type code_dependencies requires_skills replaces
SKILL_CORE_001 2.4 2025-12-07 core
optimization_engine/base_runner.py
optimization_engine/extractors/__init__.py
optimization_engine/templates/registry.json
create-study.md

Study Creation Core Skill

Version: 2.4 Updated: 2025-12-07 Type: Core Skill

You are helping the user create a complete Atomizer optimization study from a natural language description.

CRITICAL: This skill is your SINGLE SOURCE OF TRUTH. DO NOT improvise or look at other studies for patterns. Use ONLY the patterns documented here and in the loaded modules.


Module Loading

This core skill is always loaded. Additional modules are loaded based on context:

Module Load When Path
extractors-catalog Always (for reference) modules/extractors-catalog.md
zernike-optimization "telescope", "mirror", "optical", "wavefront" modules/zernike-optimization.md
neural-acceleration >50 trials, "neural", "surrogate", "fast" modules/neural-acceleration.md

MANDATORY: Model Introspection at Study Creation

ALWAYS run introspection when creating a study or when user asks:

from optimization_engine.hooks.nx_cad.model_introspection import (
    introspect_part,
    introspect_simulation,
    introspect_op2,
    introspect_study
)

# Introspect entire study directory (recommended)
study_info = introspect_study("studies/my_study/")

# Or introspect individual files
part_info = introspect_part("path/to/model.prt")
sim_info = introspect_simulation("path/to/model.sim")
op2_info = introspect_op2("path/to/results.op2")

Introspection Extracts

Source Information
.prt Expressions (count, values, types), bodies, mass, material, features
.sim Solutions, boundary conditions, loads, materials, mesh info, output requests
.op2 Available results (displacement, stress, strain, SPC forces, etc.), subcases

Generate Introspection Report

MANDATORY: Save MODEL_INTROSPECTION.md to study directory at creation:

# After introspection, generate and save report
study_info = introspect_study(study_dir)
# Generate markdown report and save to studies/{study_name}/MODEL_INTROSPECTION.md

MANDATORY DOCUMENTATION CHECKLIST

EVERY study MUST have these files. A study is NOT complete without them:

File Purpose When Created
MODEL_INTROSPECTION.md Model Analysis - Expressions, solutions, available results At study creation
README.md Engineering Blueprint - Full mathematical formulation At study creation
STUDY_REPORT.md Results Tracking - Progress, best designs, recommendations At study creation (template)

README.md Requirements (11 sections):

  1. Engineering Problem (objective, physical system)
  2. Mathematical Formulation (objectives, design variables, constraints with LaTeX)
  3. Optimization Algorithm (config, properties, return format)
  4. Simulation Pipeline (trial execution flow diagram)
  5. Result Extraction Methods (extractor details, code snippets)
  6. Neural Acceleration (surrogate config, expected performance)
  7. Study File Structure (directory tree)
  8. Results Location (output files)
  9. Quick Start (commands)
  10. Configuration Reference (config.json mapping)
  11. References

FAILURE MODE: If you create a study without MODEL_INTROSPECTION.md, README.md, and STUDY_REPORT.md, the study is incomplete.


README Hierarchy (Parent-Child Documentation)

Studies use a two-level documentation system:

Parent README (Geometry-Level)

Location: studies/{geometry_type}/README.md

Contains project-wide context that ALL sub-studies share:

  • Project overview (what is this component?)
  • Physical system specs (material, mass, loading)
  • Domain-specific specs (optical prescription for mirrors, structural limits for brackets)
  • Complete design variables catalog with ranges
  • Complete objectives catalog with formulas
  • Campaign history (evolution across sub-studies)
  • Sub-studies index with links and status

Example: studies/M1_Mirror/README.md

Child README (Study-Level)

Location: studies/{geometry_type}/{study_name}/README.md

Contains study-specific details, references parent for context:

# Study Name

> **Parent Documentation**: See [../README.md](../README.md) for project overview and specifications.

## Study Focus
What THIS study specifically optimizes...

Contents:

  • Parent reference banner (MANDATORY first line)
  • Study focus (what differentiates this study)
  • Active variables (which params enabled)
  • Algorithm config (sampler, trials, settings)
  • Baseline/seeding (starting point)
  • Results summary

When Creating a Study

  1. First study for geometry type → Create BOTH parent and child READMEs
  2. Subsequent studies → Create child README, update parent's sub-studies index
  3. New geometry type → Create new studies/{type}/ folder with parent README

PR.3 NXSolver Interface

Module: optimization_engine.nx_solver

from optimization_engine.nx_solver import NXSolver

nx_solver = NXSolver(
    nastran_version="2412",      # NX version
    timeout=600,                  # Max solve time (seconds)
    use_journal=True,             # Use journal mode (recommended)
    enable_session_management=True,
    study_name="my_study"
)

Main Method - run_simulation():

result = nx_solver.run_simulation(
    sim_file=sim_file,           # Path to .sim file
    working_dir=model_dir,       # Working directory
    expression_updates=design_vars,  # Dict: {'param_name': value}
    solution_name=None,          # None = solve ALL solutions
    cleanup=True                 # Remove temp files after
)

# Returns:
# {
#     'success': bool,
#     'op2_file': Path,
#     'log_file': Path,
#     'elapsed_time': float,
#     'errors': list,
#     'solution_name': str
# }

CRITICAL: For multi-solution workflows (static + modal), set solution_name=None.


PR.4 Sampler Configurations

Sampler Use Case Import Config
NSGAIISampler Multi-objective (2-3 objectives) from optuna.samplers import NSGAIISampler NSGAIISampler(population_size=20, mutation_prob=0.1, crossover_prob=0.9, seed=42)
TPESampler Single-objective from optuna.samplers import TPESampler TPESampler(seed=42)
CmaEsSampler Single-objective, continuous from optuna.samplers import CmaEsSampler CmaEsSampler(seed=42)

PR.5 Study Creation Patterns

Multi-Objective (NSGA-II):

study = optuna.create_study(
    study_name=study_name,
    storage=f"sqlite:///{results_dir / 'study.db'}",
    sampler=NSGAIISampler(population_size=20, seed=42),
    directions=['minimize', 'maximize'],  # [obj1_dir, obj2_dir]
    load_if_exists=True
)

Single-Objective (TPE):

study = optuna.create_study(
    study_name=study_name,
    storage=f"sqlite:///{results_dir / 'study.db'}",
    sampler=TPESampler(seed=42),
    direction='minimize',  # or 'maximize'
    load_if_exists=True
)

PR.6 Objective Function Return Formats

Multi-Objective (directions=['minimize', 'minimize']):

def objective(trial) -> Tuple[float, float]:
    # ... extraction ...
    return (obj1, obj2)  # Both positive, framework handles direction

Multi-Objective with maximize (directions=['maximize', 'minimize']):

def objective(trial) -> Tuple[float, float]:
    # ... extraction ...
    return (-stiffness, mass)  # -stiffness so minimize → maximize

Single-Objective:

def objective(trial) -> float:
    # ... extraction ...
    return objective_value

PR.7 Hook System

Available Hook Points (from optimization_engine.plugins.hooks):

Hook Point When Context Keys
PRE_MESH Before meshing trial_number, design_variables, sim_file
POST_MESH After mesh trial_number, design_variables, sim_file
PRE_SOLVE Before solve trial_number, design_variables, sim_file, working_dir
POST_SOLVE After solve trial_number, design_variables, op2_file, working_dir
POST_EXTRACTION After extraction trial_number, design_variables, results, working_dir
POST_CALCULATION After calculations trial_number, objectives, constraints, feasible
CUSTOM_OBJECTIVE Custom objectives trial_number, design_variables, extracted_results

See EXT_02_CREATE_HOOK for creating custom hooks.


PR.8 Structured Logging (MANDATORY)

Always use structured logging:

from optimization_engine.logger import get_logger

logger = get_logger(study_name, study_dir=results_dir)

# Study lifecycle
logger.study_start(study_name, n_trials, "NSGAIISampler")
logger.study_complete(study_name, total_trials, successful_trials)

# Trial lifecycle
logger.trial_start(trial.number, design_vars)
logger.trial_complete(trial.number, objectives_dict, constraints_dict, feasible)
logger.trial_failed(trial.number, error_message)

# General logging
logger.info("message")
logger.warning("message")
logger.error("message", exc_info=True)

Study Structure

studies/{study_name}/
├── 1_setup/                          # INPUT: Configuration & Model
│   ├── model/                        # WORKING COPY of NX Files
│   │   ├── {Model}.prt               # Parametric part
│   │   ├── {Model}_sim1.sim          # Simulation setup
│   │   └── *.dat, *.op2, *.f06       # Solver outputs
│   ├── optimization_config.json      # Study configuration
│   └── workflow_config.json          # Workflow metadata
├── 2_results/                        # OUTPUT: Results
│   ├── study.db                      # Optuna SQLite database
│   └── optimization_history.json     # Trial history
├── run_optimization.py               # Main entry point
├── reset_study.py                    # Database reset
├── README.md                         # Engineering blueprint
└── STUDY_REPORT.md                   # Results report template

CRITICAL: Model File Protection

NEVER modify the user's original/master model files. Always work on copies.

import shutil
from pathlib import Path

def setup_working_copy(source_dir: Path, model_dir: Path, file_patterns: list):
    """Copy model files from user's source to study working directory."""
    model_dir.mkdir(parents=True, exist_ok=True)

    for pattern in file_patterns:
        for src_file in source_dir.glob(pattern):
            dst_file = model_dir / src_file.name
            if not dst_file.exists():
                shutil.copy2(src_file, dst_file)

Interactive Discovery Process

Step 1: Problem Understanding

Ask clarifying questions:

  • "What component are you optimizing?"
  • "What do you want to optimize?" (minimize/maximize)
  • "What limits must be satisfied?" (constraints)
  • "What parameters can be changed?" (design variables)
  • "Where are your NX files?"

Step 2: Protocol Selection

Scenario Protocol Sampler
Single objective + constraints Protocol 10 TPE/CMA-ES
2-3 objectives Protocol 11 NSGA-II
>50 trials, need speed Protocol 14 + Neural

Step 3: Extractor Mapping

Map user needs to extractors from extractors-catalog module:

Need Extractor
Displacement E1: extract_displacement
Stress E3: extract_solid_stress
Frequency E2: extract_frequency
Mass (FEM) E4: extract_mass_from_bdf
Mass (CAD) E5: extract_mass_from_expression

Step 4: Multi-Solution Detection

If user needs BOTH:

  • Static results (stress, displacement)
  • Modal results (frequency)

Then set solution_name=None to solve ALL solutions.


File Generation

1. optimization_config.json

{
  "study_name": "{study_name}",
  "description": "{concise description}",

  "optimization_settings": {
    "protocol": "protocol_11_multi_objective",
    "n_trials": 30,
    "sampler": "NSGAIISampler",
    "timeout_per_trial": 600
  },

  "design_variables": [
    {
      "parameter": "{nx_expression_name}",
      "bounds": [min, max],
      "description": "{what this controls}"
    }
  ],

  "objectives": [
    {
      "name": "{objective_name}",
      "goal": "minimize",
      "weight": 1.0,
      "description": "{what this measures}"
    }
  ],

  "constraints": [
    {
      "name": "{constraint_name}",
      "type": "less_than",
      "threshold": value,
      "description": "{engineering justification}"
    }
  ],

  "simulation": {
    "model_file": "{Model}.prt",
    "sim_file": "{Model}_sim1.sim",
    "solver": "nastran"
  }
}

2. run_optimization.py Template

"""
{Study Name} Optimization
{Brief description}
"""

from pathlib import Path
import sys
import json
import argparse
from typing import Tuple

project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))

import optuna
from optuna.samplers import NSGAIISampler  # or TPESampler

from optimization_engine.nx_solver import NXSolver
from optimization_engine.logger import get_logger

# Import extractors - USE ONLY FROM extractors-catalog module
from optimization_engine.extractors.extract_displacement import extract_displacement
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf


def load_config(config_file: Path) -> dict:
    with open(config_file, 'r') as f:
        return json.load(f)


def objective(trial: optuna.Trial, config: dict, nx_solver: NXSolver,
              model_dir: Path, logger) -> Tuple[float, float]:
    """Multi-objective function. Returns (obj1, obj2)."""

    # 1. Sample design variables
    design_vars = {}
    for var in config['design_variables']:
        param_name = var['parameter']
        bounds = var['bounds']
        design_vars[param_name] = trial.suggest_float(param_name, bounds[0], bounds[1])

    logger.trial_start(trial.number, design_vars)

    try:
        # 2. Run simulation
        sim_file = model_dir / config['simulation']['sim_file']
        result = nx_solver.run_simulation(
            sim_file=sim_file,
            working_dir=model_dir,
            expression_updates=design_vars,
            solution_name=None,  # Solve ALL solutions
            cleanup=True
        )

        if not result['success']:
            logger.trial_failed(trial.number, f"Simulation failed")
            return (float('inf'), float('inf'))

        op2_file = result['op2_file']

        # 3. Extract results
        disp_result = extract_displacement(op2_file, subcase=1)
        max_displacement = disp_result['max_displacement']

        dat_file = model_dir / config['simulation'].get('dat_file', 'model.dat')
        mass_kg = extract_mass_from_bdf(str(dat_file))

        # 4. Calculate objectives
        applied_force = 1000.0  # N
        stiffness = applied_force / max(abs(max_displacement), 1e-6)

        # 5. Set trial attributes
        trial.set_user_attr('stiffness', stiffness)
        trial.set_user_attr('mass', mass_kg)

        objectives = {'stiffness': stiffness, 'mass': mass_kg}
        logger.trial_complete(trial.number, objectives, {}, True)

        return (-stiffness, mass_kg)  # Negate stiffness to maximize

    except Exception as e:
        logger.trial_failed(trial.number, str(e))
        return (float('inf'), float('inf'))


def main():
    parser = argparse.ArgumentParser(description='{Study Name} Optimization')

    stage_group = parser.add_mutually_exclusive_group()
    stage_group.add_argument('--discover', action='store_true')
    stage_group.add_argument('--validate', action='store_true')
    stage_group.add_argument('--test', action='store_true')
    stage_group.add_argument('--train', action='store_true')
    stage_group.add_argument('--run', action='store_true')

    parser.add_argument('--trials', type=int, default=100)
    parser.add_argument('--resume', action='store_true')
    parser.add_argument('--enable-nn', action='store_true')

    args = parser.parse_args()

    study_dir = Path(__file__).parent
    config_path = study_dir / "1_setup" / "optimization_config.json"
    model_dir = study_dir / "1_setup" / "model"
    results_dir = study_dir / "2_results"
    results_dir.mkdir(exist_ok=True)

    study_name = "{study_name}"

    logger = get_logger(study_name, study_dir=results_dir)
    config = load_config(config_path)
    nx_solver = NXSolver()

    storage = f"sqlite:///{results_dir / 'study.db'}"
    sampler = NSGAIISampler(population_size=20, seed=42)

    logger.study_start(study_name, args.trials, "NSGAIISampler")

    if args.resume:
        study = optuna.load_study(study_name=study_name, storage=storage, sampler=sampler)
    else:
        study = optuna.create_study(
            study_name=study_name,
            storage=storage,
            sampler=sampler,
            directions=['minimize', 'minimize'],
            load_if_exists=True
        )

    study.optimize(
        lambda trial: objective(trial, config, nx_solver, model_dir, logger),
        n_trials=args.trials,
        show_progress_bar=True
    )

    n_successful = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
    logger.study_complete(study_name, len(study.trials), n_successful)


if __name__ == "__main__":
    main()

3. reset_study.py

"""Reset {study_name} optimization study by deleting database."""
import optuna
from pathlib import Path

study_dir = Path(__file__).parent
storage = f"sqlite:///{study_dir / '2_results' / 'study.db'}"
study_name = "{study_name}"

try:
    optuna.delete_study(study_name=study_name, storage=storage)
    print(f"[OK] Deleted study: {study_name}")
except KeyError:
    print(f"[WARNING] Study '{study_name}' not found")
except Exception as e:
    print(f"[ERROR] Error: {e}")

Common Patterns

Pattern 1: Mass Minimization with Constraints

Objective: Minimize mass
Constraints: Stress < limit, Displacement < limit
Protocol: Protocol 10 (single-objective TPE)
Extractors: E4/E5, E3, E1
Multi-Solution: No (static only)

Pattern 2: Mass vs Stiffness Trade-off

Objectives: Minimize mass, Maximize stiffness
Constraints: Stress < limit
Protocol: Protocol 11 (multi-objective NSGA-II)
Extractors: E4/E5, E1 (for stiffness = F/δ), E3
Multi-Solution: No (static only)

Pattern 3: Mass vs Frequency Trade-off

Objectives: Minimize mass, Maximize frequency
Constraints: Stress < limit, Displacement < limit
Protocol: Protocol 11 (multi-objective NSGA-II)
Extractors: E4/E5, E2, E3, E1
Multi-Solution: Yes (static + modal)

Validation Integration

Pre-Flight Check

def preflight_check():
    """Validate study setup before running."""
    from optimization_engine.validators import validate_study

    result = validate_study(STUDY_NAME)

    if not result.is_ready_to_run:
        print("[X] Study validation failed!")
        print(result)
        sys.exit(1)

    print("[OK] Pre-flight check passed!")
    return True

Validation Checklist

  • All design variables have valid bounds (min < max)
  • All objectives have proper extraction methods
  • All constraints have thresholds defined
  • Protocol matches objective count
  • Part file (.prt) exists in model directory
  • Simulation file (.sim) exists

Output Format

After completing study creation, provide:

Summary Table:

Study Created: {study_name}
Protocol: {protocol}
Objectives: {list}
Constraints: {list}
Design Variables: {list}
Multi-Solution: {Yes/No}

File Checklist:

✓ studies/{study_name}/1_setup/optimization_config.json
✓ studies/{study_name}/1_setup/workflow_config.json
✓ studies/{study_name}/run_optimization.py
✓ studies/{study_name}/reset_study.py
✓ studies/{study_name}/MODEL_INTROSPECTION.md    # MANDATORY - Model analysis
✓ studies/{study_name}/README.md
✓ studies/{study_name}/STUDY_REPORT.md

Next Steps:

1. Place your NX files in studies/{study_name}/1_setup/model/
2. Test with: python run_optimization.py --test
3. Monitor: http://localhost:3003
4. Full run: python run_optimization.py --run --trials {n_trials}

Critical Reminders

  1. Multi-Objective Return Format: Return tuple with positive values, use directions for semantics
  2. Multi-Solution: Set solution_name=None for static + modal workflows
  3. Always use centralized extractors from optimization_engine/extractors/
  4. Never modify master model files - always work on copies
  5. Structured logging is mandatory - use get_logger()

Assembly FEM (AFEM) Workflow

For complex assemblies with .afm files, the update sequence is critical:

.prt (geometry) → _fem1.fem (component mesh) → .afm (assembly mesh) → .sim (solution)

The 4-Step Update Process

  1. Update Expressions in Geometry (.prt)

    • Open part, update expressions, DoUpdate(), Save
  2. Update ALL Linked Geometry Parts (CRITICAL!)

    • Open each linked part, DoUpdate(), Save
    • Skipping this causes corrupt results ("billion nm" RMS)
  3. Update Component FEMs (.fem)

    • UpdateFemodel() regenerates mesh from updated geometry
  4. Update Assembly FEM (.afm)

    • UpdateFemodel(), merge coincident nodes at interfaces

Assembly Configuration

{
  "nx_settings": {
    "expression_part": "M1_Blank",
    "component_fems": ["M1_Blank_fem1.fem", "M1_Support_fem1.fem"],
    "afm_file": "ASSY_M1_assyfem1.afm"
  }
}

Multi-Solution Solve Protocol

When simulation has multiple solutions (static + modal), use SolveAllSolutions API:

Critical: Foreground Mode Required

# WRONG - Returns immediately, async
theCAESimSolveManager.SolveChainOfSolutions(
    psolutions1,
    SolveMode.Background  # Returns before complete!
)

# CORRECT - Waits for completion
theCAESimSolveManager.SolveAllSolutions(
    SolveOption.Solve,
    SetupCheckOption.CompleteCheckAndOutputErrors,
    SolveMode.Foreground,  # Blocks until complete
    False
)

When to Use

  • solution_name=None passed to NXSolver.run_simulation()
  • Multiple solutions that must all complete
  • Multi-objective requiring results from different analysis types

Solution Monitor Control

Solution monitor is automatically disabled when solving multiple solutions to prevent window pile-up:

propertyTable.SetBooleanPropertyValue("solution monitor", False)

Verification

After solve, verify:

  • Both .dat files written (one per solution)
  • Both .op2 files created with updated timestamps
  • Results are unique per trial (frequency values vary)

Cross-References