feat: Add journal-based NX solver integration for optimization
Implements NX solver integration that connects to running Simcenter3D GUI to solve simulations using the journal API. This approach handles licensing properly and ensures fresh output files are generated for each iteration. **New Components:** - optimization_engine/nx_solver.py: Main solver wrapper with auto-detection - optimization_engine/solve_simulation.py: NX journal script for batch solving - examples/test_journal_optimization.py: Complete optimization workflow test - examples/test_nx_solver.py: Solver integration tests - tests/journal_*.py: Reference journal files for NX automation **Key Features:** - Auto-detects NX installation and version - Connects to running NX GUI session (uses existing license) - Closes/reopens .sim files to force reload of updated .prt files - Deletes old output files to force fresh solves - Waits for background solve completion - Saves simulation to ensure all outputs are written - ~4 second solve time per iteration **Workflow:** 1. Update parameters in .prt file (nx_updater.py) 2. Close any open parts in NX session 3. Open .sim file fresh from disk (loads updated .prt) 4. Reload components and switch to FEM component 5. Solve in background mode 6. Save .sim file 7. Wait for .op2/.f06 to appear 8. Extract results from fresh .op2 **Tested:** - Multiple iteration loop (3+ iterations) - Files regenerated fresh each time (verified by timestamps) - Complete parameter update -> solve -> extract workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
48
examples/check_nx_installation.py
Normal file
48
examples/check_nx_installation.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Quick check: Verify NX installation can be found
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.nx_solver import NXSolver
|
||||
|
||||
print("="*60)
|
||||
print("NX INSTALLATION CHECK")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
solver = NXSolver(nastran_version="2412")
|
||||
|
||||
print("\n✓ NX Solver found!")
|
||||
print(f"\nInstallation:")
|
||||
print(f" Directory: {solver.nx_install_dir}")
|
||||
print(f" Solver: {solver.solver_exe}")
|
||||
print(f"\nSolver executable exists: {solver.solver_exe.exists()}")
|
||||
|
||||
if solver.solver_exe.exists():
|
||||
print(f"Solver size: {solver.solver_exe.stat().st_size / (1024*1024):.1f} MB")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("READY TO USE!")
|
||||
print("="*60)
|
||||
print("\nNext step: Run test_nx_solver.py to verify solver execution")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"\n✗ Error: {e}")
|
||||
print("\nPlease check:")
|
||||
print(" - NX 2412 is installed")
|
||||
print(" - Installation is at standard location")
|
||||
print("\nTry specifying path manually:")
|
||||
print(" solver = NXSolver(")
|
||||
print(" nx_install_dir=Path('C:/your/path/to/NX2412'),")
|
||||
print(" nastran_version='2412'")
|
||||
print(" )")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
89
examples/check_nx_license.py
Normal file
89
examples/check_nx_license.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Check NX License Configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
print("="*60)
|
||||
print("NX LICENSE CONFIGURATION CHECK")
|
||||
print("="*60)
|
||||
|
||||
# Check environment variables
|
||||
print("\n--- Environment Variables ---")
|
||||
|
||||
license_vars = [
|
||||
'SPLM_LICENSE_SERVER',
|
||||
'UGII_LICENSE_BUNDLE',
|
||||
'LM_LICENSE_FILE',
|
||||
'NX_LICENSE_FILE',
|
||||
]
|
||||
|
||||
for var in license_vars:
|
||||
value = os.environ.get(var)
|
||||
if value:
|
||||
print(f" ✓ {var} = {value}")
|
||||
else:
|
||||
print(f" ✗ {var} = (not set)")
|
||||
|
||||
# Check license server files
|
||||
print("\n--- License Server Files ---")
|
||||
|
||||
possible_license_files = [
|
||||
Path("C:/Program Files/Siemens/License Server/ugslmd.opt"),
|
||||
Path("C:/Program Files/Siemens/License Server/server.lic"),
|
||||
Path("C:/Program Files (x86)/Siemens/License Server/ugslmd.opt"),
|
||||
]
|
||||
|
||||
for lic_file in possible_license_files:
|
||||
if lic_file.exists():
|
||||
print(f" ✓ Found: {lic_file}")
|
||||
else:
|
||||
print(f" ✗ Not found: {lic_file}")
|
||||
|
||||
# Check NX installation licensing
|
||||
print("\n--- NX Installation License Info ---")
|
||||
|
||||
nx_dirs = [
|
||||
Path("C:/Program Files/Siemens/NX2412"),
|
||||
Path("C:/Program Files/Siemens/Simcenter3D_2412"),
|
||||
]
|
||||
|
||||
for nx_dir in nx_dirs:
|
||||
if nx_dir.exists():
|
||||
print(f"\n{nx_dir.name}:")
|
||||
license_file = nx_dir / "ugslmd.lic"
|
||||
if license_file.exists():
|
||||
print(f" ✓ License file: {license_file}")
|
||||
else:
|
||||
print(f" ✗ No ugslmd.lic found")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("RECOMMENDATIONS:")
|
||||
print("="*60)
|
||||
|
||||
print("""
|
||||
1. If you see SPLM_LICENSE_SERVER:
|
||||
- License server is configured ✓
|
||||
|
||||
2. If no environment variables are set:
|
||||
- You may need to set SPLM_LICENSE_SERVER
|
||||
- Format: port@hostname (e.g., 28000@localhost)
|
||||
- Or: path to license file
|
||||
|
||||
3. Common fixes:
|
||||
- Set environment variable in Windows:
|
||||
setx SPLM_LICENSE_SERVER "28000@your-license-server"
|
||||
|
||||
- Or use license file:
|
||||
setx SPLM_LICENSE_FILE "C:\\path\\to\\license.dat"
|
||||
|
||||
4. For local/node-locked license:
|
||||
- Check License Server is running
|
||||
- Services → Siemens License Server should be running
|
||||
|
||||
5. For network license:
|
||||
- Verify license server hostname/IP
|
||||
- Check port (usually 28000)
|
||||
- Verify firewall allows connection
|
||||
""")
|
||||
133
examples/test_journal_optimization.py
Normal file
133
examples/test_journal_optimization.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Test: Complete Optimization with Journal-Based NX Solver
|
||||
|
||||
This tests the complete workflow:
|
||||
1. Update model parameters in .prt
|
||||
2. Solve via journal (using running NX GUI)
|
||||
3. Extract results from OP2
|
||||
4. Run optimization loop
|
||||
|
||||
REQUIREMENTS:
|
||||
- Simcenter3D must be open (but no files need to be loaded)
|
||||
- test_env conda environment activated
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.runner import OptimizationRunner
|
||||
from optimization_engine.nx_updater import update_nx_model
|
||||
from optimization_engine.nx_solver import run_nx_simulation
|
||||
from optimization_engine.result_extractors.extractors import (
|
||||
stress_extractor,
|
||||
displacement_extractor
|
||||
)
|
||||
|
||||
|
||||
def bracket_model_updater(design_vars: dict):
|
||||
"""Update bracket model parameters."""
|
||||
prt_file = project_root / "examples/bracket/Bracket.prt"
|
||||
print(f"\n[MODEL UPDATE] {prt_file.name}")
|
||||
for name, value in design_vars.items():
|
||||
print(f" {name} = {value:.4f}")
|
||||
update_nx_model(prt_file, design_vars, backup=False)
|
||||
|
||||
|
||||
def bracket_simulation_runner() -> Path:
|
||||
"""
|
||||
Run NX solver via journal on running NX GUI session.
|
||||
|
||||
This connects to the running Simcenter3D GUI and:
|
||||
1. Opens the .sim file
|
||||
2. Updates the FEM component
|
||||
3. Solves the simulation
|
||||
4. Returns path to .op2 file
|
||||
"""
|
||||
sim_file = project_root / "examples/bracket/Bracket_sim1.sim"
|
||||
|
||||
print("\n[SIMULATION] Running via journal on NX GUI...")
|
||||
print(f" SIM file: {sim_file.name}")
|
||||
|
||||
try:
|
||||
# Run solver via journal (connects to running NX GUI)
|
||||
op2_file = run_nx_simulation(
|
||||
sim_file=sim_file,
|
||||
nastran_version="2412",
|
||||
timeout=300, # 5 minute timeout
|
||||
cleanup=True, # Clean up temp files
|
||||
use_journal=True # Use journal mode (requires NX GUI open)
|
||||
)
|
||||
|
||||
print(f"[SIMULATION] Complete! Results: {op2_file.name}")
|
||||
return op2_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SIMULATION] FAILED: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*60)
|
||||
print("JOURNAL-BASED OPTIMIZATION TEST")
|
||||
print("="*60)
|
||||
print("\n⚠️ REQUIREMENTS ⚠️")
|
||||
print("- Simcenter3D must be OPEN (no files need to be loaded)")
|
||||
print("- Will run 3 optimization trials")
|
||||
print("- Each trial: update params -> solve via journal -> extract results")
|
||||
print("="*60)
|
||||
|
||||
response = input("\nIs Simcenter3D open? (yes/no): ")
|
||||
if response.lower() not in ['yes', 'y']:
|
||||
print("Please open Simcenter3D and try again.")
|
||||
sys.exit(0)
|
||||
|
||||
config_path = project_root / "examples/bracket/optimization_config_stress_displacement.json"
|
||||
|
||||
runner = OptimizationRunner(
|
||||
config_path=config_path,
|
||||
model_updater=bracket_model_updater,
|
||||
simulation_runner=bracket_simulation_runner, # Journal-based solver!
|
||||
result_extractors={
|
||||
'stress_extractor': stress_extractor,
|
||||
'displacement_extractor': displacement_extractor
|
||||
}
|
||||
)
|
||||
|
||||
# Run just 3 trials for testing
|
||||
runner.config['optimization_settings']['n_trials'] = 3
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Starting optimization with 3 trials")
|
||||
print("Objective: Minimize max von Mises stress")
|
||||
print("Constraint: Max displacement <= 1.0 mm")
|
||||
print("Solver: Journal-based (using running NX GUI)")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
study = runner.run(study_name="journal_solver_test")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("OPTIMIZATION COMPLETE!")
|
||||
print("="*60)
|
||||
print(f"\nBest stress: {study.best_value:.2f} MPa")
|
||||
print(f"\nBest parameters:")
|
||||
for param, value in study.best_params.items():
|
||||
print(f" {param}: {value:.4f}")
|
||||
|
||||
print(f"\nResults saved to: {runner.output_dir}")
|
||||
print("\nCheck history.csv to see optimization progress!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n{'='*60}")
|
||||
print("ERROR DURING OPTIMIZATION")
|
||||
print("="*60)
|
||||
print(f"{e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("\nMake sure:")
|
||||
print(" - Simcenter3D is open and running")
|
||||
print(" - .sim file is valid and solvable")
|
||||
print(" - No other processes are locking the files")
|
||||
130
examples/test_nx_solver.py
Normal file
130
examples/test_nx_solver.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Test NX Solver Integration
|
||||
|
||||
Tests running NX Nastran in batch mode.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.nx_solver import NXSolver, run_nx_simulation
|
||||
|
||||
|
||||
def test_solver_basic():
|
||||
"""Test basic solver execution."""
|
||||
print("="*60)
|
||||
print("TEST 1: Basic Solver Execution")
|
||||
print("="*60)
|
||||
|
||||
sim_file = project_root / "examples/bracket/Bracket_sim1.sim"
|
||||
|
||||
if not sim_file.exists():
|
||||
print(f"ERROR: Simulation file not found: {sim_file}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Initialize solver
|
||||
solver = NXSolver(nastran_version="2412", timeout=300)
|
||||
print(f"\nSolver initialized:")
|
||||
print(f" NX Directory: {solver.nx_install_dir}")
|
||||
print(f" Solver Exe: {solver.solver_exe}")
|
||||
|
||||
# Run simulation
|
||||
result = solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
cleanup=False # Keep all files for inspection
|
||||
)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("SOLVER RESULT:")
|
||||
print(f"{'='*60}")
|
||||
print(f" Success: {result['success']}")
|
||||
print(f" Time: {result['elapsed_time']:.1f}s")
|
||||
print(f" OP2 file: {result['op2_file']}")
|
||||
print(f" Return code: {result['return_code']}")
|
||||
|
||||
if result['errors']:
|
||||
print(f"\n Errors:")
|
||||
for error in result['errors']:
|
||||
print(f" {error}")
|
||||
|
||||
return result['success']
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_convenience_function():
|
||||
"""Test convenience function."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 2: Convenience Function")
|
||||
print("="*60)
|
||||
|
||||
sim_file = project_root / "examples/bracket/Bracket_sim1.sim"
|
||||
|
||||
try:
|
||||
op2_file = run_nx_simulation(
|
||||
sim_file=sim_file,
|
||||
nastran_version="2412",
|
||||
timeout=300,
|
||||
cleanup=True
|
||||
)
|
||||
|
||||
print(f"\nSUCCESS!")
|
||||
print(f" OP2 file: {op2_file}")
|
||||
print(f" File exists: {op2_file.exists()}")
|
||||
print(f" File size: {op2_file.stat().st_size / 1024:.1f} KB")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nFAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*60)
|
||||
print("NX SOLVER INTEGRATION TEST")
|
||||
print("="*60)
|
||||
print("\nThis will run NX Nastran solver in batch mode.")
|
||||
print("Make sure:")
|
||||
print(" 1. NX 2412 is installed")
|
||||
print(" 2. No NX GUI sessions are using the .sim file")
|
||||
print(" 3. You have write permissions in the bracket folder")
|
||||
print("\n" + "="*60)
|
||||
|
||||
input("\nPress ENTER to continue or Ctrl+C to cancel...")
|
||||
|
||||
# Test 1: Basic execution
|
||||
test1_result = test_solver_basic()
|
||||
|
||||
if test1_result:
|
||||
# Test 2: Convenience function
|
||||
test2_result = test_convenience_function()
|
||||
|
||||
if test2_result:
|
||||
print("\n" + "="*60)
|
||||
print("ALL TESTS PASSED ✓")
|
||||
print("="*60)
|
||||
print("\nNX solver integration is working!")
|
||||
print("You can now use it in optimization loops.")
|
||||
else:
|
||||
print("\n" + "="*60)
|
||||
print("TEST 2 FAILED")
|
||||
print("="*60)
|
||||
else:
|
||||
print("\n" + "="*60)
|
||||
print("TEST 1 FAILED - Skipping Test 2")
|
||||
print("="*60)
|
||||
print("\nCheck:")
|
||||
print(" - NX installation path")
|
||||
print(" - .sim file is valid")
|
||||
print(" - NX license is available")
|
||||
130
examples/test_optimization_with_solver.py
Normal file
130
examples/test_optimization_with_solver.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Test: Complete Optimization with Real NX Solver
|
||||
|
||||
This runs the complete optimization loop:
|
||||
1. Update model parameters
|
||||
2. Run NX solver (REAL simulation)
|
||||
3. Extract results from OP2
|
||||
4. Optimize with Optuna
|
||||
|
||||
WARNING: This will run NX solver for each trial!
|
||||
For 5 trials, expect ~5-10 minutes depending on solver speed.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.runner import OptimizationRunner
|
||||
from optimization_engine.nx_updater import update_nx_model
|
||||
from optimization_engine.nx_solver import run_nx_simulation
|
||||
from optimization_engine.result_extractors.extractors import (
|
||||
stress_extractor,
|
||||
displacement_extractor
|
||||
)
|
||||
|
||||
|
||||
def bracket_model_updater(design_vars: dict):
|
||||
"""Update bracket model parameters."""
|
||||
prt_file = project_root / "examples/bracket/Bracket.prt"
|
||||
print(f"\n[MODEL UPDATE] {prt_file.name}")
|
||||
for name, value in design_vars.items():
|
||||
print(f" {name} = {value:.4f}")
|
||||
update_nx_model(prt_file, design_vars, backup=False)
|
||||
|
||||
|
||||
def bracket_simulation_runner() -> Path:
|
||||
"""
|
||||
Run NX Nastran solver and return path to OP2 file.
|
||||
|
||||
This is the key difference from the test version -
|
||||
it actually runs the solver for each trial!
|
||||
"""
|
||||
sim_file = project_root / "examples/bracket/Bracket_sim1.sim"
|
||||
|
||||
print("\n[SIMULATION] Running NX Nastran solver...")
|
||||
print(f" SIM file: {sim_file.name}")
|
||||
|
||||
try:
|
||||
# Run solver (this will take ~1-2 minutes per trial)
|
||||
op2_file = run_nx_simulation(
|
||||
sim_file=sim_file,
|
||||
nastran_version="2412",
|
||||
timeout=600, # 10 minute timeout
|
||||
cleanup=True # Clean up temp files
|
||||
)
|
||||
|
||||
print(f"[SIMULATION] Complete! Results: {op2_file.name}")
|
||||
return op2_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SIMULATION] FAILED: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("="*60)
|
||||
print("REAL OPTIMIZATION WITH NX SOLVER")
|
||||
print("="*60)
|
||||
print("\n⚠️ WARNING ⚠️")
|
||||
print("This will run NX Nastran solver for each trial!")
|
||||
print("For 3 trials, expect ~5-10 minutes total.")
|
||||
print("\nMake sure:")
|
||||
print(" - NX 2412 is installed and licensed")
|
||||
print(" - No NX GUI sessions are open")
|
||||
print(" - Bracket.prt and Bracket_sim1.sim are accessible")
|
||||
print("="*60)
|
||||
|
||||
response = input("\nContinue? (yes/no): ")
|
||||
if response.lower() not in ['yes', 'y']:
|
||||
print("Cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
config_path = project_root / "examples/bracket/optimization_config_stress_displacement.json"
|
||||
|
||||
runner = OptimizationRunner(
|
||||
config_path=config_path,
|
||||
model_updater=bracket_model_updater,
|
||||
simulation_runner=bracket_simulation_runner, # REAL SOLVER!
|
||||
result_extractors={
|
||||
'stress_extractor': stress_extractor,
|
||||
'displacement_extractor': displacement_extractor
|
||||
}
|
||||
)
|
||||
|
||||
# Run just 3 trials for testing (change to 20-50 for real optimization)
|
||||
runner.config['optimization_settings']['n_trials'] = 3
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Starting optimization with 3 trials")
|
||||
print("Objective: Minimize max von Mises stress")
|
||||
print("Constraint: Max displacement <= 1.0 mm")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
study = runner.run(study_name="real_solver_test")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("OPTIMIZATION COMPLETE!")
|
||||
print("="*60)
|
||||
print(f"\nBest stress: {study.best_value:.2f} MPa")
|
||||
print(f"\nBest parameters:")
|
||||
for param, value in study.best_params.items():
|
||||
print(f" {param}: {value:.4f}")
|
||||
|
||||
print(f"\nResults saved to: {runner.output_dir}")
|
||||
print("\nCheck history.csv to see how stress changed with parameters!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n{'='*60}")
|
||||
print("ERROR DURING OPTIMIZATION")
|
||||
print("="*60)
|
||||
print(f"{e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("\nMake sure:")
|
||||
print(" - NX Nastran is properly installed")
|
||||
print(" - License is available")
|
||||
print(" - .sim file is valid and solvable")
|
||||
Reference in New Issue
Block a user