feat: Add substudy system with live history tracking and workflow fixes

Major Features:
- Hierarchical substudy system (like NX Solutions/Subcases)
  * Shared model files across all substudies
  * Independent configuration per substudy
  * Continuation support from previous substudies
  * Real-time incremental history updates
- Live history tracking with optimization_history_incremental.json
- Complete bracket_displacement_maximizing study with substudy examples

Core Fixes:
- Fixed expression update workflow to pass design_vars through simulation_runner
  * Restored working NX journal expression update mechanism
  * OP2 timestamp verification instead of file deletion
  * Resolved issue where all trials returned identical objective values
- Fixed LLMOptimizationRunner to pass design variables to simulation runner
- Enhanced NXSolver with timestamp-based file regeneration verification

New Components:
- optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner
- optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard
- studies/bracket_displacement_maximizing/ - Complete substudy example
  * run_substudy.py - Substudy runner with continuation
  * run_optimization.py - Standalone optimization runner
  * config/substudy_template.json - Template for new substudies
  * substudies/coarse_exploration/ - 20-trial coarse search
  * substudies/fine_tuning/ - 50-trial refinement (continuation example)
  * SUBSTUDIES_README.md - Complete substudy documentation

Technical Improvements:
- Incremental history saving after each trial (optimization_history_incremental.json)
- Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve
- Trial indexing fix in substudy result saving
- Updated README with substudy system documentation

Testing:
- Successfully ran 20-trial coarse_exploration substudy
- Verified different objective values across trials (workflow fix validated)
- Confirmed live history updates in real-time
- Tested shared model file usage across substudies

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-16 21:29:54 -05:00
parent 90a9e020d8
commit 2f3afc3813
126 changed files with 15592 additions and 97 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/Bracket_fem1_i.prt Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -3,15 +3,15 @@ $*
$* Simcenter v2412.0.0.3001 Translator
$* for Simcenter Nastran version 2412.0
$*
$* FEM FILE: C:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_fem1.fem
$* SIM FILE: C:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_sim1.sim
$* FEM FILE: c:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_fem1.fem
$* SIM FILE: c:\Users\antoi\Documents\Atomaste\Atomizer\tests\Bracket_sim1.sim
$* ANALYSIS TYPE: Structural
$* SOLUTION NAME: Solution 1
$* SOLUTION TYPE: SOL 101 Linear Statics
$*
$* SOLVER INPUT FILE: bracket_sim1-solution_1.dat
$* CREATION DATE: 15-Nov-2025
$* CREATION TIME: 12:17:41
$* CREATION DATE: 16-Nov-2025
$* CREATION TIME: 20:44:27
$* HOSTNAME: AntoineThinkpad
$* NASTRAN LICENSE: Desktop Bundle
$*

View File

@@ -0,0 +1,33 @@
Nastran BUFFSIZE=32769 $(c:/program files/siemens/simcenter3d_2412/nxnastran/conf/nastran.rcf[1])
Nastran BUFFPOOL=20.0X $(c:/program files/siemens/simcenter3d_2412/nxnastran/conf/nastran.rcf[4])
Nastran DIAGA=128 DIAGB=0 $(c:/program files/siemens/simcenter3d_2412/nxnastran/conf/nastran.rcf[7])
Nastran SYSTEM(442)=2 $(command line[5])
Nastran REAL=8545370112 $(Memory limit for MPI and other specialized modules)
JID='c:\Users\antoi\Documents\Atomaste\Atomizer\tests\bracket_sim1-solution_1.dat'
OUT='./bracket_sim1-solution_1'
MEM=3846123520
MACH='Intel64 Family 6 Model 183 Stepping 1'
OPER='Windows 10'
OSV=' '
MODEL='Intel(R) Core(TM) i7-14700HX (AntoineThinkpad)'
CONFIG=8666
NPROC=28
symbol=DELDIR='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/nast/del' $(program default)
symbol=DEMODIR='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/nast/demo' $(program default)
symbol=SSSALTERDIR='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/nast/misc/sssalter' $(program default)
symbol=TPLDIR='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/nast/tpl' $(program default)
SDIR='c:/users/antoi/appdata/local/temp/bracket_sim1-solution_1.T13980_32'
DBS='c:/users/antoi/appdata/local/temp/bracket_sim1-solution_1.T13980_32'
SCR=yes
SMEM=20.0X
NEWDEL='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/em64tntl/SSS'
DEL='NXNDEF'
AUTH='29000@AntoineThinkpad'
AUTHQUE=0
MSGCAT='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/em64tntl/analysis.msg'
MSGDEST='f06'
PROG=bundle
NEWS='c:/program files/siemens/simcenter3d_2412/nxnastran/scnas/nast/news.txt'
UMATLIB='libnxumat.dll'
UCRPLIB='libucreep.dll'
USOLLIB='libusol.dll'

View File

@@ -0,0 +1,324 @@
"""
Interactive Optimization Setup - Phase 3.3
This script simulates a real Atomizer setup session where:
- User provides their optimization goal in natural language
- Atomizer LLM Assistant helps configure the optimization
- Wizard validates the setup before running
- User confirms and starts optimization
This is how Atomizer SHOULD work in production!
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.optimization_setup_wizard import OptimizationSetupWizard
from optimization_engine.llm_optimization_runner import LLMOptimizationRunner
from optimization_engine.nx_solver import NXSolver
from optimization_engine.nx_updater import NXParameterUpdater
def print_section(title: str):
"""Print a section header."""
print()
print("=" * 80)
print(f" {title}")
print("=" * 80)
print()
def print_assistant(message: str):
"""Print Atomizer LLM Assistant message."""
print(f"\n[Atomizer] {message}\n")
def print_user(message: str):
"""Print user message."""
print(f"\n[User] {message}\n")
def print_wizard(message: str):
"""Print wizard message."""
print(f"[Wizard] {message}")
def main():
print_section("ATOMIZER - INTERACTIVE OPTIMIZATION SETUP")
print_assistant("Welcome to Atomizer! I'll help you set up your optimization.")
print_assistant("First, I need to know about your model files.")
# File paths
prt_file = Path("tests/Bracket.prt")
sim_file = Path("tests/Bracket_sim1.sim")
if not prt_file.exists() or not sim_file.exists():
print("[ERROR] Test files not found!")
sys.exit(1)
print_user(f"I have a bracket model:")
print(f" - Part file: {prt_file}")
print(f" - Simulation file: {sim_file}")
print_assistant("Great! Let me initialize the Setup Wizard to analyze your model...")
# Initialize wizard
wizard = OptimizationSetupWizard(prt_file, sim_file)
print_section("STEP 1: MODEL INTROSPECTION")
print_assistant("I'm reading your NX model to find available design parameters...")
# Introspect model
model_info = wizard.introspect_model()
print_assistant(f"Found {len(model_info.expressions)} expressions in your model:")
for name, info in model_info.expressions.items():
print(f" - {name}: {info['value']} {info['units']}")
print_assistant("Which parameters would you like to use as design variables?")
print_user("I want to optimize tip_thickness and support_angle")
print_assistant("Perfect! Now, what's your optimization goal?")
print_user("I want to maximize displacement while keeping stress below")
print(" a safety factor of 4. The material is Aluminum 6061-T6.")
user_goal = "Maximize displacement while keeping stress below safety factor of 4 (Aluminum 6061-T6, yield=276 MPa)"
print_section("STEP 2: BASELINE SIMULATION")
print_assistant("To validate your setup, I need to run ONE baseline simulation.")
print_assistant("This will generate an OP2 file that I can analyze to ensure")
print_assistant("the extraction pipeline will work correctly.")
print_assistant("")
print_assistant("Running baseline simulation with current parameter values...")
# Run baseline
baseline_op2 = wizard.run_baseline_simulation()
print_assistant(f"Baseline simulation complete! OP2 file: {baseline_op2.name}")
print_section("STEP 3: OP2 INTROSPECTION")
print_assistant("Now I'll analyze the OP2 file to see what's actually in there...")
# Introspect OP2
op2_info = wizard.introspect_op2()
print_assistant("Here's what I found in your OP2 file:")
print(f" - Element types with stress: {', '.join(op2_info.element_types)}")
print(f" - Available results: {', '.join(op2_info.result_types)}")
print(f" - Number of elements: {op2_info.element_count}")
print(f" - Number of nodes: {op2_info.node_count}")
print_section("STEP 4: LLM-GUIDED CONFIGURATION")
print_assistant("Based on your goal and the OP2 contents, here's what I recommend:")
print_assistant("")
print_assistant("OBJECTIVE:")
print_assistant(" - Maximize displacement (minimize negative displacement)")
print_assistant("")
print_assistant("EXTRACTIONS:")
print_assistant(" - Extract displacement from OP2")
print_assistant(f" - Extract stress from {op2_info.element_types[0]} elements")
print_assistant(" (I detected these element types in your model)")
print_assistant("")
print_assistant("CALCULATIONS:")
print_assistant(" - Calculate safety factor: SF = 276 MPa / max_stress")
print_assistant(" - Negate displacement for minimization")
print_assistant("")
print_assistant("CONSTRAINT:")
print_assistant(" - Enforce SF >= 4.0 with penalty")
print_assistant("")
print_assistant("DESIGN VARIABLES:")
print_assistant(f" - tip_thickness: {model_info.expressions['tip_thickness']['value']} mm (suggest range: 15-25 mm)")
print_assistant(f" - support_angle: {model_info.expressions['support_angle']['value']} degrees (suggest range: 20-40 deg)")
print_user("That looks good! Let's use those ranges.")
# Build configuration
llm_workflow = {
'engineering_features': [
{
'action': 'extract_displacement',
'domain': 'result_extraction',
'description': 'Extract displacement results from OP2 file',
'params': {'result_type': 'displacement'}
},
{
'action': 'extract_solid_stress',
'domain': 'result_extraction',
'description': f'Extract von Mises stress from {op2_info.element_types[0]} elements',
'params': {
'result_type': 'stress',
'element_type': op2_info.element_types[0].lower() # Use detected element type!
}
}
],
'inline_calculations': [
{
'action': 'calculate_safety_factor',
'params': {
'input': 'max_von_mises',
'yield_strength': 276.0,
'operation': 'divide'
},
'code_hint': 'safety_factor = 276.0 / max_von_mises'
},
{
'action': 'negate_displacement',
'params': {
'input': 'max_displacement',
'operation': 'negate'
},
'code_hint': 'neg_displacement = -max_displacement'
}
],
'post_processing_hooks': [], # Using manual safety_factor_constraint hook
'optimization': {
'algorithm': 'TPE',
'direction': 'minimize',
'design_variables': [
{
'parameter': 'tip_thickness',
'min': 15.0,
'max': 25.0,
'units': 'mm'
},
{
'parameter': 'support_angle',
'min': 20.0,
'max': 40.0,
'units': 'degrees'
}
]
}
}
print_section("STEP 5: PIPELINE VALIDATION (DRY RUN)")
print_assistant("Before running 20-30 optimization trials, let me validate that")
print_assistant("EVERYTHING works correctly with your baseline OP2 file...")
print_assistant("")
print_assistant("Running dry-run validation...")
# Validate pipeline
validation_results = wizard.validate_pipeline(llm_workflow)
# Check results
all_passed = all(r.success for r in validation_results)
print()
print_wizard("VALIDATION RESULTS:")
for result in validation_results:
status = "[OK]" if result.success else "[FAIL]"
print_wizard(f" {status} {result.component}: {result.message.split(':')[-1].strip()}")
print()
if all_passed:
print_section("VALIDATION PASSED!")
print_assistant("Excellent! All pipeline components validated successfully.")
print_assistant("Your optimization is ready to run!")
print_assistant("")
print_assistant("Summary:")
print(f" - Design variables: tip_thickness (15-25mm), support_angle (20-40deg)")
print(f" - Objective: Maximize displacement")
print(f" - Constraint: Safety factor >= 4.0")
print(f" - Material: Aluminum 6061-T6 (Yield = 276 MPa)")
print()
print_user("Great! Let's run 10 trials to test it.")
print_section("STEP 6: RUNNING OPTIMIZATION")
print_assistant("Initializing optimization runner...")
# Create updater and runner
def create_model_updater(prt_file: Path):
updater = NXParameterUpdater(prt_file_path=prt_file)
def update_model(design_vars: dict):
updater.update_expressions(design_vars)
updater.save()
return update_model
def create_simulation_runner(sim_file: Path):
solver = NXSolver(nastran_version='2412', use_journal=True)
def run_simulation() -> Path:
result = solver.run_simulation(sim_file)
return result['op2_file']
return run_simulation
model_updater = create_model_updater(prt_file)
simulation_runner = create_simulation_runner(sim_file)
# Initialize runner
runner = LLMOptimizationRunner(
llm_workflow=llm_workflow,
model_updater=model_updater,
simulation_runner=simulation_runner,
study_name='bracket_interactive_setup'
)
print_assistant("Starting optimization with 10 trials...")
print_assistant("(This will take a few minutes...)")
print()
# Run optimization
results = runner.run_optimization(n_trials=10)
print()
print_section("OPTIMIZATION COMPLETE!")
print_assistant("Optimization finished! Here are the results:")
print()
print(f" Best tip_thickness: {results['best_params']['tip_thickness']:.3f} mm")
print(f" Best support_angle: {results['best_params']['support_angle']:.3f} degrees")
print(f" Best objective value: {results['best_value']:.6f}")
print()
print(f" Results saved to: {runner.output_dir}")
# Show some trial history
print()
print_assistant("Trial history (last 5 trials):")
print()
print(" Trial | Tip(mm) | Angle(°) | Disp(mm) | Stress(MPa) | Objective")
print(" ------|---------|----------|----------|-------------|----------")
for trial in results['history'][-5:]:
trial_num = trial['trial_number']
tip = trial['design_variables']['tip_thickness']
ang = trial['design_variables']['support_angle']
disp = trial['results'].get('max_displacement', 0)
stress = trial['results'].get('max_von_mises', 0)
obj = trial['objective']
print(f" {trial_num:5d} | {tip:7.2f} | {ang:8.2f} | {disp:8.5f} | {stress:11.2f} | {obj:9.6f}")
print()
print_assistant("Success! Your optimization completed without errors.")
print_assistant("This is exactly how Atomizer should work - validate first,")
print_assistant("then optimize with confidence!")
else:
print_section("VALIDATION FAILED!")
print_assistant("The validation found issues that need to be fixed:")
print()
for result in validation_results:
if not result.success:
print(f" [ERROR] {result.message}")
print()
print_assistant("Please fix these issues before starting the optimization.")
print_assistant("This saved you from wasting time on 20-30 failed trials!")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,278 @@
"""
Full End-to-End Bracket Optimization Test - Phase 3.2
This test demonstrates the complete LLM-enhanced optimization workflow:
1. LLM workflow configuration
2. Automatic extractor generation (displacement + stress)
3. Inline calculation generation (safety factor)
4. Manual safety factor constraint hook
5. Real NX simulation execution
6. OP2 result extraction
7. Constraint checking
8. Optimization with Optuna
9. Report generation
Objective: Maximize displacement
Constraint: Safety factor >= 4.0 (stress < yield/4)
Material: Aluminum 6061-T6 (Yield = 276 MPa)
Design Variable: wall_thickness (3-8mm)
"""
import sys
from pathlib import Path
from datetime import datetime
import json
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.llm_optimization_runner import LLMOptimizationRunner
from optimization_engine.nx_solver import NXSolver
from optimization_engine.nx_updater import NXParameterUpdater
# LLM workflow for bracket optimization
llm_workflow = {
'engineering_features': [
{
'action': 'extract_displacement',
'domain': 'result_extraction',
'description': 'Extract displacement results from OP2 file',
'params': {'result_type': 'displacement'}
},
{
'action': 'extract_solid_stress',
'domain': 'result_extraction',
'description': 'Extract von Mises stress from solid elements',
'params': {
'result_type': 'stress',
'element_type': 'chexa' # Bracket uses CHEXA elements, not CTETRA
}
}
],
'inline_calculations': [
{
'action': 'calculate_safety_factor',
'params': {
'input': 'max_von_mises',
'yield_strength': 276.0, # MPa for Aluminum 6061-T6
'operation': 'divide'
},
'code_hint': 'safety_factor = 276.0 / max_von_mises'
},
{
'action': 'negate_displacement',
'params': {
'input': 'max_displacement',
'operation': 'negate'
},
'code_hint': 'neg_displacement = -max_displacement'
}
],
'post_processing_hooks': [], # Using manual safety_factor_constraint hook
'optimization': {
'algorithm': 'TPE',
'direction': 'minimize', # Minimize neg_displacement = maximize displacement
'design_variables': [
{
'parameter': 'tip_thickness',
'min': 15.0,
'max': 25.0,
'units': 'mm'
},
{
'parameter': 'support_angle',
'min': 20.0,
'max': 40.0,
'units': 'degrees'
}
]
}
}
def create_model_updater(prt_file: Path):
"""Create model updater for NX."""
updater = NXParameterUpdater(prt_file_path=prt_file)
def update_model(design_vars: dict):
"""Update NX model with design variables."""
updater.update_expressions(design_vars)
updater.save() # Save changes to disk
return update_model
def create_simulation_runner(sim_file: Path):
"""Create simulation runner for NX."""
solver = NXSolver(nastran_version='2412', use_journal=True)
def run_simulation() -> Path:
"""Run NX simulation and return OP2 file path."""
result = solver.run_simulation(sim_file)
return result['op2_file'] # Extract Path from result dict
return run_simulation
def generate_report(results: dict, output_dir: Path):
"""Generate optimization report with history."""
report_file = output_dir / "optimization_report.md"
best_params = results['best_params']
best_value = results['best_value']
history = results['history']
with open(report_file, 'w') as f:
f.write("# Bracket Optimization Report - Phase 3.2\n\n")
f.write(f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("## Problem Definition\n\n")
f.write("- **Objective**: Maximize displacement\n")
f.write("- **Constraint**: Safety factor >= 4.0\n")
f.write("- **Material**: Aluminum 6061-T6 (Yield = 276 MPa)\n")
f.write("- **Allowable Stress**: 69 MPa (276/4)\n")
f.write("- **Design Variables**:\n")
f.write(" - tip_thickness (15-25 mm)\n")
f.write(" - support_angle (20-40 degrees)\n\n")
f.write("## Optimization Results\n\n")
f.write(f"- **Best tip_thickness**: {best_params['tip_thickness']:.3f} mm\n")
f.write(f"- **Best support_angle**: {best_params['support_angle']:.3f} degrees\n")
f.write(f"- **Best objective value**: {best_value:.6f}\n")
f.write(f"- **Total trials**: {len(history)}\n\n")
# Find best trial details
best_trial = history[results['best_trial_number']]
best_results = best_trial['results']
best_calcs = best_trial['calculations']
f.write("## Best Design Details\n\n")
f.write(f"- **Max displacement**: {best_results.get('max_displacement', 0):.6f} mm\n")
f.write(f"- **Max stress**: {best_results.get('max_von_mises', 0):.3f} MPa\n")
f.write(f"- **Safety factor**: {best_calcs.get('safety_factor', 0):.3f}\n")
f.write(f"- **Constraint status**: {'SATISFIED' if best_calcs.get('safety_factor', 0) >= 4.0 else 'VIOLATED'}\n\n")
f.write("## Optimization History\n\n")
f.write("| Trial | Tip Thick (mm) | Support Angle (°) | Displacement (mm) | Stress (MPa) | Safety Factor | Constraint | Objective |\n")
f.write("|-------|----------------|-------------------|-------------------|--------------|---------------|------------|-----------|\n")
for trial in history:
trial_num = trial['trial_number']
tip_thick = trial['design_variables']['tip_thickness']
support_ang = trial['design_variables']['support_angle']
disp = trial['results'].get('max_displacement', 0)
stress = trial['results'].get('max_von_mises', 0)
sf = trial['calculations'].get('safety_factor', 0)
constraint = 'OK' if sf >= 4.0 else 'FAIL'
obj = trial['objective']
f.write(f"| {trial_num} | {tip_thick:.3f} | {support_ang:.3f} | {disp:.6f} | {stress:.3f} | {sf:.3f} | {constraint} | {obj:.6f} |\n")
f.write("\n## LLM-Enhanced Workflow\n\n")
f.write("This optimization was run using the Phase 3.2 LLM-enhanced runner with:\n\n")
f.write("- **Automatic extractor generation**: Displacement + Stress (Phase 3.1)\n")
f.write("- **Inline calculations**: Safety factor + Objective negation (Phase 2.8)\n")
f.write("- **Manual constraint hook**: Safety factor constraint (demonstrates flexibility)\n")
f.write("- **Real NX simulation**: Journal-based solver execution\n")
f.write("- **Optuna optimization**: TPE sampler\n\n")
f.write("---\n\n")
f.write("*Generated by Atomizer Phase 3.2 - LLM-Enhanced Optimization Framework*\n")
print(f"\nReport saved to: {report_file}")
return report_file
if __name__ == '__main__':
print("=" * 80)
print("Phase 3.2: Full End-to-End Bracket Optimization Test")
print("=" * 80)
print()
print("Problem:")
print(" Maximize displacement while maintaining safety factor >= 4.0")
print(" Material: Aluminum 6061-T6 (Yield = 276 MPa)")
print(" Design Variables:")
print(" - tip_thickness (15-25 mm)")
print(" - support_angle (20-40 degrees)")
print()
# Configuration
sim_file = Path("tests/Bracket_sim1.sim")
prt_file = Path("tests/Bracket.prt")
n_trials = 5 # Start with 5 trials for testing
if not sim_file.exists():
print(f"ERROR: Simulation file not found: {sim_file}")
print("Please ensure the bracket .sim file is available")
sys.exit(1)
if not prt_file.exists():
print(f"ERROR: Part file not found: {prt_file}")
print("Please ensure the bracket .prt file is available")
sys.exit(1)
print(f"Simulation file: {sim_file}")
print(f"Part file: {prt_file}")
print(f"Number of trials: {n_trials}")
print()
try:
# Create model updater and simulation runner
print("Setting up NX integration...")
model_updater = create_model_updater(prt_file)
simulation_runner = create_simulation_runner(sim_file)
print(" NX updater: OK")
print(" NX solver: OK")
print()
# Initialize LLM optimization runner
print("Initializing LLM-enhanced optimization runner...")
runner = LLMOptimizationRunner(
llm_workflow=llm_workflow,
model_updater=model_updater,
simulation_runner=simulation_runner,
study_name='bracket_maximize_disp_sf4'
)
print(f" Extractors generated: {len(runner.extractors)}")
for ext in runner.extractors:
print(f" - {ext}")
print(f" Inline calculations: {len(runner.inline_code)}")
hook_summary = runner.hook_manager.get_summary()
print(f" Hooks loaded: {hook_summary['enabled_hooks']}")
print(" (Including manual safety_factor_constraint hook)")
print()
# Run optimization
print("=" * 80)
print("Starting Optimization...")
print("=" * 80)
print()
results = runner.run_optimization(n_trials=n_trials)
print()
print("=" * 80)
print("Optimization Complete!")
print("=" * 80)
print()
print(f"Best tip_thickness: {results['best_params']['tip_thickness']:.3f} mm")
print(f"Best support_angle: {results['best_params']['support_angle']:.3f} degrees")
print(f"Best objective value: {results['best_value']:.6f}")
print()
# Generate report
print("Generating optimization report...")
report_file = generate_report(results, runner.output_dir)
print()
print("=" * 80)
print("Test Complete!")
print("=" * 80)
print(f"Results directory: {runner.output_dir}")
print(f"Report: {report_file}")
except Exception as e:
print(f"\nERROR during optimization: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,144 @@
"""
Test LLM Optimization Runner with Bracket Problem
Objective: Maximize displacement
Constraint: Stress must remain under safety factor of 4
Assuming Aluminum 6061-T6:
- Yield Strength: 276 MPa
- Safety Factor: 4
- Allowable Stress: 276 / 4 = 69 MPa
This test validates Phase 3.2 LLM-enhanced optimization with stress constraints.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.llm_optimization_runner import LLMOptimizationRunner
# LLM workflow for bracket optimization
# Goal: Maximize displacement while keeping stress below safety factor
llm_workflow = {
'engineering_features': [
{
'action': 'extract_displacement',
'domain': 'result_extraction',
'description': 'Extract displacement results from OP2 file',
'params': {'result_type': 'displacement'}
},
{
'action': 'extract_solid_stress',
'domain': 'result_extraction',
'description': 'Extract von Mises stress from solid elements',
'params': {
'result_type': 'stress',
'element_type': 'ctetra'
}
}
],
'inline_calculations': [
{
'action': 'calculate_safety_factor',
'params': {
'input': 'max_von_mises',
'yield_strength': 276.0, # MPa for Aluminum 6061-T6
'operation': 'divide'
},
'code_hint': 'safety_factor = 276.0 / max_von_mises'
},
{
'action': 'negate_displacement',
'params': {
'input': 'max_displacement',
'operation': 'negate'
},
'code_hint': 'neg_displacement = -max_displacement'
}
],
'post_processing_hooks': [
{
'action': 'weighted_objective',
'params': {
'inputs': ['neg_displacement'],
'weights': [1.0],
'objective': 'minimize'
},
'description': 'Minimize negative displacement (maximize actual displacement)'
}
],
'optimization': {
'algorithm': 'TPE',
'direction': 'minimize', # Minimize negative displacement = maximize displacement
'design_variables': [
{
'parameter': 'wall_thickness',
'min': 3.0,
'max': 8.0,
'units': 'mm'
}
]
}
}
def dummy_updater(design_vars):
"""Dummy model updater for testing."""
print(f" [Test] Would update model with: {design_vars}")
def dummy_runner():
"""Dummy simulation runner for testing."""
print(" [Test] Would run NX simulation...")
# Return path to existing test OP2
return Path('tests/bracket_sim1-solution_1.op2')
if __name__ == '__main__':
print("=" * 80)
print("Test: LLM Runner with Bracket Problem")
print("=" * 80)
print()
print("Problem Definition:")
print(" Objective: MAXIMIZE displacement")
print(" Constraint: Stress must remain below yield/SF")
print(" Material: Aluminum 6061-T6 (Yield = 276 MPa)")
print(" Safety Factor: 4.0")
print(" Allowable Stress: 69 MPa")
print()
print("LLM Workflow:")
print(f" Engineering features: {len(llm_workflow['engineering_features'])}")
print(f" Inline calculations: {len(llm_workflow['inline_calculations'])}")
print(f" Post-processing hooks: {len(llm_workflow['post_processing_hooks'])}")
print(f" Design variables: {len(llm_workflow['optimization']['design_variables'])}")
print()
# Initialize LLM runner
print("Initializing LLM optimization runner...")
try:
runner = LLMOptimizationRunner(
llm_workflow=llm_workflow,
model_updater=dummy_updater,
simulation_runner=dummy_runner,
study_name='bracket_maximize_disp_constrained'
)
print()
print("=" * 80)
print("Runner Initialized Successfully!")
print("=" * 80)
print(f" Extractors generated: {len(runner.extractors)}")
print(f" Inline calculations: {len(runner.inline_code)}")
hook_summary = runner.hook_manager.get_summary()
print(f" Hooks loaded: {hook_summary['enabled_hooks']}")
print()
print("To run full optimization, call runner.run_optimization(n_trials=50)")
except Exception as e:
print(f"\nERROR during initialization: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,70 @@
"""Quick test of LLM runner initialization"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.llm_optimization_runner import LLMOptimizationRunner
# Example LLM workflow
llm_workflow = {
'engineering_features': [
{
'action': 'extract_displacement',
'domain': 'result_extraction',
'description': 'Extract displacement from OP2',
'params': {'result_type': 'displacement'}
}
],
'inline_calculations': [
{
'action': 'normalize',
'params': {
'input': 'max_displacement',
'reference': 'max_allowed_disp',
'value': 5.0
},
'code_hint': 'norm_disp = max_displacement / 5.0'
}
],
'post_processing_hooks': [
{
'action': 'weighted_objective',
'params': {
'inputs': ['norm_disp'],
'weights': [1.0],
'objective': 'minimize'
}
}
],
'optimization': {
'algorithm': 'TPE',
'direction': 'minimize',
'design_variables': [
{
'parameter': 'wall_thickness',
'min': 3.0,
'max': 8.0
}
]
}
}
def dummy_updater(dv):
pass
def dummy_runner():
return Path('tests/bracket_sim1-solution_1.op2')
print("Initializing LLM runner...")
runner = LLMOptimizationRunner(
llm_workflow=llm_workflow,
model_updater=dummy_updater,
simulation_runner=dummy_runner,
study_name='test'
)
print(f"\nSuccess!")
print(f"Extractors: {len(runner.extractors)}")
print(f"Inline code: {len(runner.inline_code)}")
print(f"Hooks: {runner.hook_manager.get_summary()['enabled_hooks']}")

View File

@@ -0,0 +1,168 @@
"""
Test Optimization Setup Wizard - Phase 3.3
This test demonstrates the complete validation workflow:
1. Introspect model for expressions
2. Run baseline simulation
3. Introspect OP2 for element types
4. Validate complete pipeline
5. Report readiness for optimization
This prevents wasted time on optimizations that will fail!
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.optimization_setup_wizard import OptimizationSetupWizard
if __name__ == '__main__':
print("=" * 80)
print("Phase 3.3: Optimization Setup Wizard Test")
print("=" * 80)
print()
print("This wizard validates the optimization pipeline BEFORE running trials.")
print("It will:")
print(" 1. Introspect NX model for available expressions")
print(" 2. Run baseline simulation to generate OP2")
print(" 3. Introspect OP2 to detect element types and results")
print(" 4. Validate extraction/calculation/hook pipeline")
print(" 5. Report readiness for optimization")
print()
# Configuration
prt_file = Path("tests/Bracket.prt")
sim_file = Path("tests/Bracket_sim1.sim")
if not prt_file.exists():
print(f"ERROR: Part file not found: {prt_file}")
sys.exit(1)
if not sim_file.exists():
print(f"ERROR: Simulation file not found: {sim_file}")
sys.exit(1)
print(f"Part file: {prt_file}")
print(f"Sim file: {sim_file}")
print()
try:
# Initialize wizard
print("Initializing Optimization Setup Wizard...")
wizard = OptimizationSetupWizard(prt_file, sim_file)
print(" Wizard initialized!")
print()
# Define optimization goal
user_goal = "Maximize displacement while keeping stress below yield/4 (safety factor >= 4.0)"
# Custom workflow with safety factor constraint
llm_workflow = {
'engineering_features': [
{
'action': 'extract_displacement',
'domain': 'result_extraction',
'description': 'Extract displacement results from OP2 file',
'params': {'result_type': 'displacement'}
},
{
'action': 'extract_solid_stress',
'domain': 'result_extraction',
'description': 'Extract von Mises stress from solid elements',
'params': {
'result_type': 'stress',
'element_type': 'chexa' # Will be auto-detected
}
}
],
'inline_calculations': [
{
'action': 'calculate_safety_factor',
'params': {
'input': 'max_von_mises',
'yield_strength': 276.0, # MPa for Aluminum 6061-T6
'operation': 'divide'
},
'code_hint': 'safety_factor = 276.0 / max_von_mises'
},
{
'action': 'negate_displacement',
'params': {
'input': 'max_displacement',
'operation': 'negate'
},
'code_hint': 'neg_displacement = -max_displacement'
}
],
'post_processing_hooks': [] # Using manual safety_factor_constraint hook
}
# Run complete validation
print("=" * 80)
print("RUNNING COMPLETE VALIDATION WORKFLOW")
print("=" * 80)
print()
success, validation_results = wizard.run_complete_validation(
user_goal=user_goal,
llm_workflow=llm_workflow
)
print()
print("=" * 80)
print("VALIDATION COMPLETE")
print("=" * 80)
print()
if success:
print("[SUCCESS] ALL VALIDATIONS PASSED!")
print()
print("Pipeline is ready for optimization. Key findings:")
print()
# Show model expressions
if wizard.model_info:
print(f"Model Expressions ({len(wizard.model_info.expressions)}):")
for name, info in wizard.model_info.expressions.items():
print(f" - {name}: {info['value']} {info['units']}")
print()
# Show OP2 contents
if wizard.op2_info:
print(f"OP2 Contents:")
print(f" - Element types: {', '.join(wizard.op2_info.element_types)}")
print(f" - Result types: {', '.join(wizard.op2_info.result_types)}")
print(f" - Subcases: {wizard.op2_info.subcases}")
print(f" - Nodes: {wizard.op2_info.node_count}")
print(f" - Elements: {wizard.op2_info.element_count}")
print()
# Show validation summary
print(f"Validation Summary:")
for result in validation_results:
if result.success:
print(f" {result.message}")
print()
print("[OK] You can now start the optimization with confidence!")
print(" The system has verified that all pipeline components work correctly.")
else:
print("[FAILED] VALIDATION FAILED!")
print()
print("Issues found:")
for result in validation_results:
if not result.success:
print(f" [ERROR] {result.message}")
print()
print("Please fix these issues before starting optimization.")
sys.exit(1)
except Exception as e:
print(f"\nERROR during validation: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,70 @@
"""
Test: Verify NX regenerates .dat and .op2 files with new timestamps
This test verifies the fix for OP2 caching by checking file timestamps.
"""
from pathlib import Path
import sys
import time
sys.path.insert(0, str(Path(__file__).parent.parent))
from optimization_engine.nx_solver import NXSolver
print("=" * 60)
print("Testing timestamp verification fix")
print("=" * 60)
sim_file = Path('studies/bracket_displacement_maximizing/model/Bracket_sim1.sim')
if not sim_file.exists():
print(f"ERROR: Simulation file not found: {sim_file}")
sys.exit(1)
# Check current timestamps
dat_file = sim_file.parent / "bracket_sim1-solution_1.dat"
op2_file = sim_file.parent / "bracket_sim1-solution_1.op2"
print(f"\nSimulation file: {sim_file}")
print(f"Working directory: {sim_file.parent}")
if dat_file.exists():
old_dat_time = dat_file.stat().st_mtime
print(f"\nBefore solve:")
print(f" .dat modified: {time.ctime(old_dat_time)}")
if op2_file.exists():
old_op2_time = op2_file.stat().st_mtime
print(f" .op2 modified: {time.ctime(old_op2_time)}")
# Run solve with timestamp verification
print(f"\nRunning solve with timestamp verification...")
solver = NXSolver(nastran_version='2412', use_journal=True)
result = solver.run_simulation(sim_file)
print(f"\nSolve result: {'SUCCESS' if result['success'] else 'FAILED'}")
# Check new timestamps
if op2_file.exists():
new_op2_time = op2_file.stat().st_mtime
print(f"\nAfter solve:")
print(f" .op2 modified: {time.ctime(new_op2_time)}")
if 'old_op2_time' in locals():
if new_op2_time > old_op2_time:
print(f"\n ✓ SUCCESS! .op2 file was regenerated!")
print(f" Time difference: {new_op2_time - old_op2_time:.1f} seconds")
else:
print(f"\n ✗ FAILED! .op2 file was NOT updated")
print(f" This means NX did not regenerate the file")
else:
print(f"\n ✓ New .op2 file created")
if dat_file.exists():
new_dat_time = dat_file.stat().st_mtime
print(f" .dat modified: {time.ctime(new_dat_time)}")
if 'old_dat_time' in locals():
if new_dat_time > old_dat_time:
print(f"\n ✓ .dat file was also regenerated")