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>
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 |
|
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):
- Engineering Problem (objective, physical system)
- Mathematical Formulation (objectives, design variables, constraints with LaTeX)
- Optimization Algorithm (config, properties, return format)
- Simulation Pipeline (trial execution flow diagram)
- Result Extraction Methods (extractor details, code snippets)
- Neural Acceleration (surrogate config, expected performance)
- Study File Structure (directory tree)
- Results Location (output files)
- Quick Start (commands)
- Configuration Reference (config.json mapping)
- 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
- First study for geometry type → Create BOTH parent and child READMEs
- Subsequent studies → Create child README, update parent's sub-studies index
- 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
- Multi-Objective Return Format: Return tuple with positive values, use
directionsfor semantics - Multi-Solution: Set
solution_name=Nonefor static + modal workflows - Always use centralized extractors from
optimization_engine/extractors/ - Never modify master model files - always work on copies
- 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
-
Update Expressions in Geometry (.prt)
- Open part, update expressions, DoUpdate(), Save
-
Update ALL Linked Geometry Parts (CRITICAL!)
- Open each linked part, DoUpdate(), Save
- Skipping this causes corrupt results ("billion nm" RMS)
-
Update Component FEMs (.fem)
- UpdateFemodel() regenerates mesh from updated geometry
-
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=Nonepassed toNXSolver.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
.datfiles written (one per solution) - Both
.op2files created with updated timestamps - Results are unique per trial (frequency values vary)
Cross-References
- Operations Protocol: OP_01_CREATE_STUDY
- Extractors Module: extractors-catalog
- Zernike Module: zernike-optimization
- Neural Module: neural-acceleration
- System Protocols: SYS_10_IMSO, SYS_11_MULTI_OBJECTIVE