Files
Atomizer/.claude/skills/core/study-creation-core.md
Antoine 602560c46a feat: Add MLP surrogate with Turbo Mode for 100x faster optimization
Neural Acceleration (MLP Surrogate):
- Add run_nn_optimization.py with hybrid FEA/NN workflow
- MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout
- Three workflow modes:
  - --all: Sequential export->train->optimize->validate
  - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle
  - --turbo: Aggressive single-best validation (RECOMMENDED)
- Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes
- Separate nn_study.db to avoid overloading dashboard

Performance Results (bracket_pareto_3obj study):
- NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15%
- Found minimum mass designs at boundary (angle~30deg, thick~30mm)
- 100x speedup vs pure FEA exploration

Protocol Operating System:
- Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader
- Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14)
- Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs

NX Automation:
- Add optimization_engine/hooks/ for NX CAD/CAE automation
- Add study_wizard.py for guided study creation
- Fix FEM mesh update: load idealized part before UpdateFemodel()

New Study:
- bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness)
- 167 FEA trials + 5000 NN trials completed
- Demonstrates full hybrid workflow

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 20:01:59 -05:00

21 KiB

Study Creation Core Skill

Last Updated: December 6, 2025 Version: 2.3 - Added Model Introspection 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.


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