---
skill_id: SKILL_CORE_001
version: 2.4
last_updated: 2025-12-07
type: core
code_dependencies:
  - optimization_engine/base_runner.py
  - optimization_engine/extractors/__init__.py
  - optimization_engine/templates/registry.json
requires_skills: []
replaces: 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:**

```python
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:

```python
# 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:
```markdown
# 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`

```python
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()`**:
```python
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)**:
```python
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)**:
```python
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']):
```python
def objective(trial) -> Tuple[float, float]:
    # ... extraction ...
    return (obj1, obj2)  # Both positive, framework handles direction
```

**Multi-Objective with maximize** (directions=['maximize', 'minimize']):
```python
def objective(trial) -> Tuple[float, float]:
    # ... extraction ...
    return (-stiffness, mass)  # -stiffness so minimize → maximize
```

**Single-Objective**:
```python
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](../../docs/protocols/extensions/EXT_02_CREATE_HOOK.md) for creating custom hooks.

---

## PR.8 Structured Logging (MANDATORY)

**Always use structured logging**:
```python
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.

```python
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](../modules/extractors-catalog.md):

| 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

```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

```python
"""
{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

```python
"""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

```python
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

```json
{
  "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

```python
# 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:

```python
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

- **Operations Protocol**: [OP_01_CREATE_STUDY](../../docs/protocols/operations/OP_01_CREATE_STUDY.md)
- **Extractors Module**: [extractors-catalog](../modules/extractors-catalog.md)
- **Zernike Module**: [zernike-optimization](../modules/zernike-optimization.md)
- **Neural Module**: [neural-acceleration](../modules/neural-acceleration.md)
- **System Protocols**: [SYS_10_IMSO](../../docs/protocols/system/SYS_10_IMSO.md), [SYS_11_MULTI_OBJECTIVE](../../docs/protocols/system/SYS_11_MULTI_OBJECTIVE.md)
