Files
Atomizer/.claude/skills/core/study-creation-core.md

752 lines
22 KiB
Markdown
Raw Normal View History

---
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.
---
## 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)