diff --git a/NX_SOLVER_INTEGRATION.md b/NX_SOLVER_INTEGRATION.md
new file mode 100644
index 00000000..5710bf17
--- /dev/null
+++ b/NX_SOLVER_INTEGRATION.md
@@ -0,0 +1,294 @@
+# NX Solver Integration Guide
+
+## Overview
+
+The NX solver integration allows Atomizer to automatically run Siemens NX Nastran simulations in batch mode during optimization loops.
+
+## Architecture
+
+```
+Optimization Loop:
+1. Update parameters in .prt file → nx_updater.py
+2. Run NX solver in batch mode → nx_solver.py ← NEW!
+3. Extract results from OP2 → op2_extractor_example.py
+4. Evaluate objectives/constraints → runner.py
+5. Optuna suggests next parameters → repeat
+```
+
+## Quick Start
+
+### Test 1: Verify Solver Integration
+
+```bash
+conda activate test_env
+python examples/test_nx_solver.py
+```
+
+This tests:
+- NX installation detection
+- Batch solver execution
+- OP2 file generation
+- Error handling
+
+**Expected**: Solver runs and produces .op2 file in ~1-2 minutes
+
+### Test 2: Run Optimization with Real Solver
+
+```bash
+conda activate test_env
+python examples/test_optimization_with_solver.py
+```
+
+This runs 3 optimization trials with REAL simulations!
+
+**Expected time**: ~5-10 minutes (depends on model complexity)
+
+## Usage in Your Code
+
+### Simple Usage (Convenience Function)
+
+```python
+from optimization_engine.nx_solver import run_nx_simulation
+from pathlib import Path
+
+sim_file = Path("path/to/model.sim")
+op2_file = run_nx_simulation(
+ sim_file=sim_file,
+ nastran_version="2412",
+ timeout=600, # 10 minutes
+ cleanup=True # Remove temp files
+)
+
+# op2_file now contains path to results
+```
+
+### Advanced Usage (Full Control)
+
+```python
+from optimization_engine.nx_solver import NXSolver
+
+solver = NXSolver(
+ nastran_version="2412",
+ timeout=600
+)
+
+result = solver.run_simulation(
+ sim_file=sim_file,
+ working_dir=None, # Defaults to sim file directory
+ cleanup=True
+)
+
+if result['success']:
+ print(f"OP2: {result['op2_file']}")
+ print(f"Time: {result['elapsed_time']:.1f}s")
+else:
+ print(f"Errors: {result['errors']}")
+```
+
+### Integration with Optimization Runner
+
+```python
+from optimization_engine.nx_solver import run_nx_simulation
+
+def my_simulation_runner() -> Path:
+ """Simulation runner for optimization."""
+ sim_file = Path("my_model.sim")
+
+ # Run solver
+ op2_file = run_nx_simulation(
+ sim_file=sim_file,
+ nastran_version="2412",
+ timeout=600,
+ cleanup=True
+ )
+
+ return op2_file
+
+# Use in OptimizationRunner
+runner = OptimizationRunner(
+ config_path=config_path,
+ model_updater=my_model_updater,
+ simulation_runner=my_simulation_runner, # Uses real solver!
+ result_extractors=extractors
+)
+```
+
+## Configuration
+
+### Auto-Detection
+
+By default, NXSolver auto-detects NX installation:
+
+```python
+solver = NXSolver(nastran_version="2412")
+# Searches:
+# - C:/Program Files/Siemens/NX2412
+# - C:/Program Files/Siemens/Simcenter3D_2412
+# - C:/Program Files (x86)/Siemens/NX2412
+```
+
+### Manual Configuration
+
+```python
+from pathlib import Path
+
+solver = NXSolver(
+ nx_install_dir=Path("C:/Program Files/Siemens/NX2412"),
+ nastran_version="2412",
+ timeout=1200 # 20 minutes
+)
+```
+
+## Solver Output Files
+
+### Files Created
+- `model.op2` - Binary results (kept)
+- `model.f06` - Text output (kept)
+- `model.log` - Solver log (kept)
+- `model.f04` - Intermediate (cleaned up)
+- `model.dat` - Intermediate (cleaned up)
+- `model.diag` - Diagnostic (cleaned up)
+
+### Cleanup Behavior
+
+With `cleanup=True` (recommended):
+- Keeps: .op2, .f06, .log
+- Removes: .f04, .dat, .diag, .master, .dball, plots
+
+With `cleanup=False`:
+- Keeps all files for debugging
+
+## Error Handling
+
+### Common Issues
+
+**Issue**: `FileNotFoundError: NX Nastran solver not found`
+
+**Solution**:
+- Check NX is installed at standard location
+- Specify `nx_install_dir` manually
+- Verify nastran.exe exists in NXNASTRAN/bin/
+
+**Issue**: `RuntimeError: NX simulation failed`
+
+**Solution**:
+- Check .f06 file for error messages
+- Verify .sim file is valid
+- Check NX license is available
+- Ensure model can solve in NX GUI first
+
+**Issue**: `TimeoutExpired`
+
+**Solution**:
+- Increase `timeout` parameter
+- Simplify model (fewer elements, linear analysis)
+- Check solver isn't stuck (memory issues)
+
+### Checking Solver Success
+
+The solver checks for completion by:
+1. Looking for "NORMAL TERMINATION" in .f06
+2. Checking for "FATAL MESSAGE" errors
+3. Verifying .op2 file was created recently
+
+## Performance Tips
+
+### Speed Up Optimization
+
+1. **Reduce Model Complexity**
+ - Use coarser mesh for initial exploration
+ - Simplify geometry in non-critical areas
+ - Use linear analysis if possible
+
+2. **Parallel Trials (Future)**
+ - Run multiple trials simultaneously
+ - Requires separate working directories
+ - Use Optuna's parallelization features
+
+3. **Smart Sampling**
+ - Use TPE sampler (default) for efficiency
+ - Increase `n_startup_trials` for better initial sampling
+ - Use constraints to avoid infeasible regions
+
+4. **Cleanup Strategy**
+ - Use `cleanup=True` to save disk space
+ - Only keep .op2 and .log files
+ - Archive results after optimization
+
+### Typical Solve Times
+
+| Model Size | Analysis Type | Time per Trial |
+|------------|---------------|----------------|
+| Small (<10k nodes) | Linear Static | 30-60s |
+| Medium (10-50k) | Linear Static | 1-3 min |
+| Large (>50k) | Linear Static | 3-10 min |
+| Any | Nonlinear | 5-30 min |
+
+## Batch Processing
+
+For running many optimizations:
+
+```python
+# Save solver instance to reuse
+solver = NXSolver(nastran_version="2412", timeout=600)
+
+for trial_params in parameter_sets:
+ # Update model
+ update_nx_model(prt_file, trial_params)
+
+ # Solve
+ result = solver.run_simulation(sim_file, cleanup=True)
+
+ if result['success']:
+ # Extract and analyze
+ results = extract_all_results(result['op2_file'])
+```
+
+## Troubleshooting
+
+### Enable Debug Output
+
+```python
+solver = NXSolver(nastran_version="2412")
+
+result = solver.run_simulation(
+ sim_file=sim_file,
+ cleanup=False # Keep all files
+)
+
+# Check detailed output
+print(result['errors'])
+
+# Manually inspect files
+# - Check .f06 for solver messages
+# - Check .log for execution details
+# - Check .f04 for input deck
+```
+
+### Verify NX Installation
+
+```python
+from optimization_engine.nx_solver import NXSolver
+
+solver = NXSolver(nastran_version="2412")
+print(f"NX Dir: {solver.nx_install_dir}")
+print(f"Solver: {solver.solver_exe}")
+print(f"Exists: {solver.solver_exe.exists()}")
+```
+
+## Next Steps
+
+1. **Test solver integration**: Run `test_nx_solver.py`
+2. **Test optimization loop**: Run `test_optimization_with_solver.py`
+3. **Customize for your model**: Modify simulation_runner function
+4. **Run real optimization**: Increase n_trials to 50-150
+5. **Analyze results**: Use history.csv to understand parameter sensitivity
+
+## Support
+
+For issues:
+1. Check this guide
+2. Verify NX installation
+3. Test .sim file in NX GUI first
+4. Check solver logs (.f06, .log files)
+5. Review error messages in result['errors']
diff --git a/examples/check_nx_installation.py b/examples/check_nx_installation.py
new file mode 100644
index 00000000..509cb5e5
--- /dev/null
+++ b/examples/check_nx_installation.py
@@ -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()
diff --git a/examples/check_nx_license.py b/examples/check_nx_license.py
new file mode 100644
index 00000000..c9a99b5d
--- /dev/null
+++ b/examples/check_nx_license.py
@@ -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
+""")
diff --git a/examples/test_journal_optimization.py b/examples/test_journal_optimization.py
new file mode 100644
index 00000000..9ce25471
--- /dev/null
+++ b/examples/test_journal_optimization.py
@@ -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")
diff --git a/examples/test_nx_solver.py b/examples/test_nx_solver.py
new file mode 100644
index 00000000..d0f44a12
--- /dev/null
+++ b/examples/test_nx_solver.py
@@ -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")
diff --git a/examples/test_optimization_with_solver.py b/examples/test_optimization_with_solver.py
new file mode 100644
index 00000000..84e1a605
--- /dev/null
+++ b/examples/test_optimization_with_solver.py
@@ -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")
diff --git a/optimization_engine/nx_solver.py b/optimization_engine/nx_solver.py
new file mode 100644
index 00000000..71d24512
--- /dev/null
+++ b/optimization_engine/nx_solver.py
@@ -0,0 +1,466 @@
+"""
+NX Nastran Solver Integration
+
+Executes NX Nastran solver in batch mode for optimization loops.
+"""
+
+from pathlib import Path
+from typing import Optional, Dict, Any
+import subprocess
+import time
+import shutil
+import os
+
+
+class NXSolver:
+ """
+ Wrapper for NX Nastran batch solver execution.
+
+ Supports:
+ - Running .sim files through NX Nastran
+ - Monitoring solver progress
+ - Detecting completion and errors
+ - Cleaning up temporary files
+ """
+
+ def __init__(
+ self,
+ nx_install_dir: Optional[Path] = None,
+ nastran_version: str = "2412",
+ timeout: int = 600,
+ use_journal: bool = True
+ ):
+ """
+ Initialize NX Solver.
+
+ Args:
+ nx_install_dir: Path to NX installation (auto-detected if None)
+ nastran_version: NX version (e.g., "2412", "2506")
+ timeout: Maximum solver time in seconds (default: 10 minutes)
+ use_journal: Use NX journal for solving (recommended for licensing)
+ """
+ self.nastran_version = nastran_version
+ self.timeout = timeout
+ self.use_journal = use_journal
+
+ # Auto-detect NX installation
+ if nx_install_dir is None:
+ nx_install_dir = self._find_nx_installation()
+
+ self.nx_install_dir = Path(nx_install_dir)
+
+ # Set up solver executable
+ if use_journal:
+ self.solver_exe = self._find_journal_runner()
+ else:
+ self.solver_exe = self._find_solver_executable()
+
+ if not self.solver_exe.exists():
+ raise FileNotFoundError(
+ f"NX solver/runner not found at: {self.solver_exe}\n"
+ f"Please check NX installation at: {self.nx_install_dir}"
+ )
+
+ def _find_nx_installation(self) -> Path:
+ """Auto-detect NX installation directory."""
+ # Common installation paths
+ possible_paths = [
+ Path(f"C:/Program Files/Siemens/NX{self.nastran_version}"),
+ Path(f"C:/Program Files/Siemens/Simcenter3D_{self.nastran_version}"),
+ Path(f"C:/Program Files (x86)/Siemens/NX{self.nastran_version}"),
+ ]
+
+ for path in possible_paths:
+ if path.exists():
+ return path
+
+ raise FileNotFoundError(
+ f"Could not auto-detect NX {self.nastran_version} installation.\n"
+ f"Checked: {[str(p) for p in possible_paths]}\n"
+ f"Please specify nx_install_dir manually."
+ )
+
+ def _find_journal_runner(self) -> Path:
+ """Find the NX journal runner executable."""
+ # Simcenter3D has run_journal.exe for batch execution
+ possible_exes = [
+ Path(f"C:/Program Files/Siemens/Simcenter3D_{self.nastran_version}/NXBIN/run_journal.exe"),
+ Path(f"C:/Program Files/Siemens/NX{self.nastran_version}/NXBIN/run_journal.exe"),
+ ]
+
+ for exe in possible_exes:
+ if exe.exists():
+ return exe
+
+ # Return first guess (will error in __init__ if doesn't exist)
+ return possible_exes[0]
+
+ def _find_solver_executable(self) -> Path:
+ """Find the Nastran solver executable."""
+ # Use NX Nastran (not Simcenter) - has different licensing
+ # Priority: Use NX installation, not Simcenter
+ possible_exes = [
+ self.nx_install_dir / "NXNASTRAN" / "bin" / "nastran.exe",
+ self.nx_install_dir / "NXNASTRAN" / "nastran.exe",
+ self.nx_install_dir / "bin" / "nastran.exe",
+ ]
+
+ for exe in possible_exes:
+ if exe.exists():
+ return exe
+
+ # If not found in NX, try Simcenter as fallback
+ simcenter_paths = [
+ Path(f"C:/Program Files/Siemens/Simcenter3D_{self.nastran_version}"),
+ ]
+
+ for simcenter_dir in simcenter_paths:
+ if simcenter_dir.exists():
+ solve_exe = simcenter_dir / "NXNASTRAN" / "bin" / "nastran.exe"
+ if solve_exe.exists():
+ return solve_exe
+
+ # Return first guess (will error in __init__ if doesn't exist)
+ return possible_exes[0]
+
+ def run_simulation(
+ self,
+ sim_file: Path,
+ working_dir: Optional[Path] = None,
+ cleanup: bool = True
+ ) -> Dict[str, Any]:
+ """
+ Run NX Nastran simulation.
+
+ Args:
+ sim_file: Path to .sim file
+ working_dir: Working directory for solver (defaults to sim file dir)
+ cleanup: Remove intermediate files after solving
+
+ Returns:
+ Dictionary with:
+ - success: bool
+ - op2_file: Path to output .op2 file
+ - log_file: Path to .log file
+ - elapsed_time: Solve time in seconds
+ - errors: List of error messages (if any)
+ """
+ sim_file = Path(sim_file)
+ if not sim_file.exists():
+ raise FileNotFoundError(f"Simulation file not found: {sim_file}")
+
+ if working_dir is None:
+ working_dir = sim_file.parent
+ else:
+ working_dir = Path(working_dir)
+
+ # Check if we need to find/use .dat file (only in direct mode, not journal mode)
+ # .sim files require NX GUI, but .dat files can be run directly with Nastran
+ dat_file = None
+ if not self.use_journal and sim_file.suffix == '.sim':
+ # Look for corresponding .dat file (created by NX when solving)
+ # Pattern: Bracket_sim1.sim -> bracket_sim1-solution_1.dat
+ base = sim_file.stem.lower()
+ possible_dats = list(working_dir.glob(f"{base}-solution_*.dat"))
+ if possible_dats:
+ # Use the most recent .dat file
+ dat_file = max(possible_dats, key=lambda p: p.stat().st_mtime)
+ print(f"\n[NX SOLVER] Found .dat file: {dat_file.name}")
+ print(f" Using .dat instead of .sim for better compatibility")
+ sim_file = dat_file
+
+ # Prepare output file names
+ # When using journal mode with .sim files, output is named: -solution_1.op2
+ # When using direct mode with .dat files, output is named: .op2
+ base_name = sim_file.stem
+
+ if self.use_journal and sim_file.suffix == '.sim':
+ # Journal mode: look for -solution_1 pattern
+ output_base = f"{base_name.lower()}-solution_1"
+ else:
+ # Direct mode or .dat file
+ output_base = base_name
+
+ op2_file = working_dir / f"{output_base}.op2"
+ log_file = working_dir / f"{output_base}.log"
+ f06_file = working_dir / f"{output_base}.f06"
+
+ print(f"\n[NX SOLVER] Starting simulation...")
+ print(f" Input file: {sim_file.name}")
+ print(f" Working dir: {working_dir}")
+ print(f" Mode: {'Journal' if self.use_journal else 'Direct'}")
+
+ # Delete old result files (.op2, .log, .f06) to force fresh solve
+ # (.dat file is needed by NX, don't delete it!)
+ # (Otherwise NX may reuse cached results)
+ files_to_delete = [op2_file, log_file, f06_file]
+
+ deleted_count = 0
+ for old_file in files_to_delete:
+ if old_file.exists():
+ try:
+ old_file.unlink()
+ deleted_count += 1
+ except Exception as e:
+ print(f" Warning: Could not delete {old_file.name}: {e}")
+
+ if deleted_count > 0:
+ print(f" Deleted {deleted_count} old result file(s) to force fresh solve")
+
+ # Build command based on mode
+ if self.use_journal and sim_file.suffix == '.sim':
+ # Use NX journal for .sim files (handles licensing properly)
+ # Generate a temporary journal file with the correct sim file path
+ journal_template = Path(__file__).parent / "solve_simulation.py"
+ temp_journal = working_dir / "_temp_solve_journal.py"
+
+ # Read template and replace placeholder with actual path
+ with open(journal_template, 'r') as f:
+ journal_content = f.read()
+
+ # Create a custom journal that passes the sim file path
+ custom_journal = f'''# Auto-generated journal for solving {sim_file.name}
+import sys
+sys.argv = ['', r'{sim_file.absolute()}'] # Set argv for the main function
+{journal_content}
+'''
+ with open(temp_journal, 'w') as f:
+ f.write(custom_journal)
+
+ cmd = [
+ str(self.solver_exe), # run_journal.exe
+ str(temp_journal.absolute()) # Use absolute path to avoid path issues
+ ]
+ else:
+ # Direct Nastran batch command for .dat files or direct mode
+ # IMPORTANT: prog=bundle enables bundle licensing (required for desktop licenses)
+ cmd = [
+ str(self.solver_exe),
+ str(sim_file),
+ "prog=bundle",
+ "old=no",
+ "scratch=yes"
+ ]
+
+ # Set up environment for Simcenter/NX
+ env = os.environ.copy()
+
+ # Set license server (use 29000 for Simcenter)
+ # Override any incorrect license server settings
+ env['SPLM_LICENSE_SERVER'] = '29000@AntoineThinkpad'
+
+ # Force desktop licensing instead of enterprise
+ # User has nx_nas_bn_basic_dsk (desktop) not nx_nas_basic_ent (enterprise)
+ env['NXNA_LICENSE_FILE'] = '29000@AntoineThinkpad'
+ env['NXNASTRAN_LICENSE_FILE'] = '29000@AntoineThinkpad'
+
+ # Add NX/Simcenter paths to environment
+ nx_bin = self.nx_install_dir / "NXBIN"
+ if nx_bin.exists():
+ env['PATH'] = f"{nx_bin};{env.get('PATH', '')}"
+
+ nastran_bin = self.solver_exe.parent
+ if nastran_bin.exists():
+ env['PATH'] = f"{nastran_bin};{env.get('PATH', '')}"
+
+ # Run solver
+ start_time = time.time()
+
+ try:
+ result = subprocess.run(
+ cmd,
+ cwd=str(working_dir),
+ capture_output=True,
+ text=True,
+ timeout=self.timeout,
+ env=env # Use modified environment
+ )
+
+ elapsed_time = time.time() - start_time
+
+ # Check for journal errors
+ if self.use_journal and result.stderr and "error" in result.stderr.lower():
+ print("[JOURNAL ERRORS]")
+ for line in result.stderr.strip().split('\n')[:5]:
+ print(f" {line}")
+
+ # Wait for output files to appear (journal mode runs solve in background)
+ if self.use_journal:
+ max_wait = 30 # seconds - background solves can take time
+ wait_start = time.time()
+ print("[NX SOLVER] Waiting for solve to complete...")
+ while not (f06_file.exists() and op2_file.exists()) and (time.time() - wait_start) < max_wait:
+ time.sleep(0.5)
+ if (time.time() - wait_start) % 2 < 0.5: # Print every 2 seconds
+ elapsed = time.time() - wait_start
+ print(f" Waiting... ({elapsed:.0f}s)")
+
+ if f06_file.exists() and op2_file.exists():
+ print(f"[NX SOLVER] Output files detected after {time.time() - wait_start:.1f}s")
+
+ # Check for completion
+ success = self._check_solution_success(f06_file, log_file)
+
+ errors = []
+ if not success:
+ errors = self._extract_errors(f06_file, log_file)
+
+ # Clean up intermediate files if requested
+ if cleanup and success:
+ self._cleanup_temp_files(working_dir, base_name)
+
+ # Clean up temporary journal file if it was created
+ temp_journal_path = working_dir / "_temp_solve_journal.py"
+ if temp_journal_path.exists():
+ try:
+ temp_journal_path.unlink()
+ except Exception:
+ pass
+
+ print(f"[NX SOLVER] Complete in {elapsed_time:.1f}s")
+ if success:
+ print(f"[NX SOLVER] Results: {op2_file.name}")
+ else:
+ print(f"[NX SOLVER] FAILED - check {f06_file.name}")
+ for error in errors:
+ print(f" ERROR: {error}")
+
+ return {
+ 'success': success,
+ 'op2_file': op2_file if op2_file.exists() else None,
+ 'log_file': log_file if log_file.exists() else None,
+ 'f06_file': f06_file if f06_file.exists() else None,
+ 'elapsed_time': elapsed_time,
+ 'errors': errors,
+ 'return_code': result.returncode
+ }
+
+ except subprocess.TimeoutExpired:
+ elapsed_time = time.time() - start_time
+ print(f"[NX SOLVER] TIMEOUT after {elapsed_time:.1f}s")
+ return {
+ 'success': False,
+ 'op2_file': None,
+ 'log_file': log_file if log_file.exists() else None,
+ 'elapsed_time': elapsed_time,
+ 'errors': [f'Solver timeout after {self.timeout}s'],
+ 'return_code': -1
+ }
+
+ except Exception as e:
+ elapsed_time = time.time() - start_time
+ print(f"[NX SOLVER] ERROR: {e}")
+ return {
+ 'success': False,
+ 'op2_file': None,
+ 'log_file': None,
+ 'elapsed_time': elapsed_time,
+ 'errors': [str(e)],
+ 'return_code': -1
+ }
+
+ def _check_solution_success(self, f06_file: Path, log_file: Path) -> bool:
+ """
+ Check if solution completed successfully.
+
+ Looks for completion markers in .f06 and .log files.
+ """
+ # Check .f06 file for completion
+ if f06_file.exists():
+ try:
+ with open(f06_file, 'r', encoding='latin-1', errors='ignore') as f:
+ content = f.read()
+ # Look for successful completion markers
+ if 'NORMAL TERMINATION' in content or 'USER INFORMATION MESSAGE' in content:
+ return True
+ # Check for fatal errors
+ if 'FATAL MESSAGE' in content or 'EXECUTION TERMINATED' in content:
+ return False
+ except Exception:
+ pass
+
+ # Fallback: check if OP2 was created recently
+ op2_file = f06_file.with_suffix('.op2')
+ if op2_file.exists():
+ # If OP2 was modified within last minute, assume success
+ if (time.time() - op2_file.stat().st_mtime) < 60:
+ return True
+
+ return False
+
+ def _extract_errors(self, f06_file: Path, log_file: Path) -> list:
+ """Extract error messages from output files."""
+ errors = []
+
+ if f06_file.exists():
+ try:
+ with open(f06_file, 'r', encoding='latin-1', errors='ignore') as f:
+ for line in f:
+ if 'FATAL' in line or 'ERROR' in line:
+ errors.append(line.strip())
+ except Exception:
+ pass
+
+ return errors[:10] # Limit to first 10 errors
+
+ def _cleanup_temp_files(self, working_dir: Path, base_name: str):
+ """Remove temporary solver files."""
+ # Files to keep
+ keep_extensions = {'.op2', '.f06', '.log'}
+
+ # Files to remove
+ remove_patterns = [
+ f"{base_name}.f04",
+ f"{base_name}.dat",
+ f"{base_name}.diag",
+ f"{base_name}.master",
+ f"{base_name}.dball",
+ f"{base_name}.MASTER",
+ f"{base_name}.DBALL",
+ f"{base_name}_*.png",
+ f"{base_name}_*.html",
+ ]
+
+ for pattern in remove_patterns:
+ for file in working_dir.glob(pattern):
+ try:
+ file.unlink()
+ except Exception:
+ pass
+
+
+# Convenience function for optimization loops
+def run_nx_simulation(
+ sim_file: Path,
+ nastran_version: str = "2412",
+ timeout: int = 600,
+ cleanup: bool = True,
+ use_journal: bool = True
+) -> Path:
+ """
+ Convenience function to run NX simulation and return OP2 file path.
+
+ Args:
+ sim_file: Path to .sim file
+ nastran_version: NX version
+ timeout: Solver timeout in seconds
+ cleanup: Remove temp files
+ use_journal: Use NX journal for solving (recommended for licensing)
+
+ Returns:
+ Path to output .op2 file
+
+ Raises:
+ RuntimeError: If simulation fails
+ """
+ solver = NXSolver(nastran_version=nastran_version, timeout=timeout, use_journal=use_journal)
+ result = solver.run_simulation(sim_file, cleanup=cleanup)
+
+ if not result['success']:
+ error_msg = '\n'.join(result['errors']) if result['errors'] else 'Unknown error'
+ raise RuntimeError(f"NX simulation failed:\n{error_msg}")
+
+ if not result['op2_file'] or not result['op2_file'].exists():
+ raise RuntimeError("Simulation completed but OP2 file not found")
+
+ return result['op2_file']
diff --git a/optimization_engine/solve_simulation.py b/optimization_engine/solve_simulation.py
new file mode 100644
index 00000000..934b7d85
--- /dev/null
+++ b/optimization_engine/solve_simulation.py
@@ -0,0 +1,182 @@
+"""
+NX Journal Script to Solve Simulation in Batch Mode
+
+This script opens a .sim file, updates the FEM, and solves it through the NX API.
+Usage: run_journal.exe solve_simulation.py
+
+Based on recorded NX journal pattern for solving simulations.
+"""
+
+import sys
+import NXOpen
+import NXOpen.Assemblies
+import NXOpen.CAE
+
+
+def main(args):
+ """
+ Open and solve a simulation file.
+
+ Args:
+ args: Command line arguments, args[0] should be the .sim file path
+ """
+ if len(args) < 1:
+ print("ERROR: No .sim file path provided")
+ print("Usage: run_journal.exe solve_simulation.py ")
+ return False
+
+ sim_file_path = args[0]
+ print(f"[JOURNAL] Opening simulation: {sim_file_path}")
+
+ try:
+ theSession = NXOpen.Session.GetSession()
+
+ # Close any currently open sim file to force reload from disk
+ print("[JOURNAL] Checking for open parts...")
+ try:
+ current_work = theSession.Parts.BaseWork
+ if current_work and hasattr(current_work, 'FullPath'):
+ current_path = current_work.FullPath
+ print(f"[JOURNAL] Closing currently open part: {current_path}")
+ # Close without saving (we want to reload from disk)
+ partCloseResponses1 = [NXOpen.BasePart.CloseWholeTree]
+ theSession.Parts.CloseAll(partCloseResponses1)
+ print("[JOURNAL] Parts closed")
+ except Exception as e:
+ print(f"[JOURNAL] No parts to close or error closing: {e}")
+
+ # Open the .sim file (now will load fresh from disk with updated .prt files)
+ print(f"[JOURNAL] Opening simulation fresh from disk...")
+ basePart1, partLoadStatus1 = theSession.Parts.OpenActiveDisplay(
+ sim_file_path,
+ NXOpen.DisplayPartOption.AllowAdditional
+ )
+
+ workSimPart = theSession.Parts.BaseWork
+ displaySimPart = theSession.Parts.BaseDisplay
+ partLoadStatus1.Dispose()
+
+ # Switch to simulation application
+ theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
+
+ simPart1 = workSimPart
+ theSession.Post.UpdateUserGroupsFromSimPart(simPart1)
+
+ # Reload all components to pick up parameter changes from .prt files
+ print("[JOURNAL] Reloading components to pick up .prt parameter changes...")
+ try:
+ workSimPart.ComponentAssembly.ReloadComponents(
+ NXOpen.Assemblies.ComponentAssembly.ReloadOption.AllLoaded
+ )
+ print("[JOURNAL] Components reloaded")
+ except Exception as e:
+ print(f"[JOURNAL] Warning: Could not reload components: {e}")
+
+ # Make FEM work component (to ensure it's updated)
+ # Find the FEM component - pattern: "COMPONENT _fem1 1"
+ markId1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+
+ # Get all components and find the FEM one
+ rootComponent = workSimPart.ComponentAssembly.RootComponent
+ femComponent = None
+
+ for component in rootComponent.GetChildren():
+ if "_fem" in component.DisplayName.lower():
+ femComponent = component
+ break
+
+ if femComponent:
+ print(f"[JOURNAL] Switching to FEM component: {femComponent.DisplayName}")
+
+ # Make FEM the work component (this is what your recorded journal does)
+ partLoadStatus2 = theSession.Parts.SetWorkComponent(
+ femComponent,
+ NXOpen.PartCollection.RefsetOption.Entire,
+ NXOpen.PartCollection.WorkComponentOption.Visible
+ )
+ partLoadStatus2.Dispose()
+
+ # Get the FEM part and try to update it
+ workFemPart = theSession.Parts.BaseWork
+ try:
+ # Try to update the FEM to pick up geometry/parameter changes
+ print("[JOURNAL] Updating FEM to recognize parameter changes...")
+ if hasattr(workFemPart, 'FemPart'):
+ workFemPart.FemPart.UpdateFeModel()
+ print("[JOURNAL] FEM updated")
+ except Exception as e:
+ print(f"[JOURNAL] Note: Could not update FEM (may not be necessary): {e}")
+
+ # Switch back to sim part (this forces NX to recognize updates)
+ markId2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+ partLoadStatus3 = theSession.Parts.SetWorkComponent(
+ NXOpen.Assemblies.Component.Null,
+ NXOpen.PartCollection.RefsetOption.Entire,
+ NXOpen.PartCollection.WorkComponentOption.Visible
+ )
+ workSimPart = theSession.Parts.BaseWork
+ partLoadStatus3.Dispose()
+ print("[JOURNAL] Switched back to sim part")
+ else:
+ print("[JOURNAL] WARNING: No FEM component found, proceeding with solve anyway")
+
+ # Note: Old output files are deleted by nx_solver.py before calling this journal
+ # This ensures NX performs a fresh solve
+
+ # Solve the simulation
+ print("[JOURNAL] Starting solve...")
+ markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
+ theSession.SetUndoMarkName(markId3, "Solve Dialog")
+
+ markId5 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
+
+ theCAESimSolveManager = NXOpen.CAE.SimSolveManager.GetSimSolveManager(theSession)
+
+ # Get the first solution from the simulation
+ simSimulation1 = workSimPart.FindObject("Simulation")
+ simSolution1 = simSimulation1.FindObject("Solution[Solution 1]")
+
+ psolutions1 = [simSolution1]
+
+ # Solve in background mode
+ numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(
+ psolutions1,
+ NXOpen.CAE.SimSolution.SolveOption.Solve,
+ NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
+ NXOpen.CAE.SimSolution.SolveMode.Background
+ )
+
+ theSession.DeleteUndoMark(markId5, None)
+ theSession.SetUndoMarkName(markId3, "Solve")
+
+ print(f"[JOURNAL] Solve submitted!")
+ print(f"[JOURNAL] Solutions solved: {numsolutionssolved1}")
+ print(f"[JOURNAL] Solutions failed: {numsolutionsfailed1}")
+ print(f"[JOURNAL] Solutions skipped: {numsolutionsskipped1}")
+
+ # NOTE: In Background mode, these values may not be accurate since the solve
+ # runs asynchronously. The solve will continue after this journal finishes.
+ # We rely on the Save operation and file existence checks to verify success.
+
+ # Save the simulation to write all output files
+ print("[JOURNAL] Saving simulation to ensure output files are written...")
+ simPart2 = workSimPart
+ partSaveStatus1 = simPart2.Save(
+ NXOpen.BasePart.SaveComponents.TrueValue,
+ NXOpen.BasePart.CloseAfterSave.FalseValue
+ )
+ partSaveStatus1.Dispose()
+ print("[JOURNAL] Save complete!")
+
+ return True
+
+ except Exception as e:
+ print(f"[JOURNAL] ERROR: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+if __name__ == '__main__':
+ success = main(sys.argv[1:])
+ sys.exit(0 if success else 1)
diff --git a/tests/journal_open_run_sim.py b/tests/journal_open_run_sim.py
new file mode 100644
index 00000000..2752b499
--- /dev/null
+++ b/tests/journal_open_run_sim.py
@@ -0,0 +1,75 @@
+# NX 2412
+# Journal created by antoi on Sat Nov 15 11:49:43 2025 Eastern Standard Time
+#
+import math
+import NXOpen
+import NXOpen.Assemblies
+import NXOpen.CAE
+import NXOpen.MenuBar
+import NXOpen.UserDefinedObjects
+def main(args) :
+
+ theSession = NXOpen.Session.GetSession() #type: NXOpen.Session
+ # ----------------------------------------------
+ # Menu: File->Open...
+ # ----------------------------------------------
+ basePart1, partLoadStatus1 = theSession.Parts.OpenActiveDisplay("C:\\Users\\antoi\\Documents\\Atomaste\\Atomizer\\tests\\Bracket_sim1.sim", NXOpen.DisplayPartOption.AllowAdditional)
+
+ workSimPart = theSession.Parts.BaseWork
+ displaySimPart = theSession.Parts.BaseDisplay
+ partLoadStatus1.Dispose()
+ theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
+
+ # User Function call - UF_translate_variable
+
+ # User Function call - UF_TEXT_ask_text_mode
+
+ baseTemplateManager1 = theSession.XYPlotManager.TemplateManager
+
+ simPart1 = workSimPart
+ theSession.Post.UpdateUserGroupsFromSimPart(simPart1)
+
+ markId1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+
+ component1 = workSimPart.ComponentAssembly.RootComponent.FindObject("COMPONENT Bracket_fem1 1")
+ partLoadStatus2 = theSession.Parts.SetWorkComponent(component1, NXOpen.PartCollection.RefsetOption.Entire, NXOpen.PartCollection.WorkComponentOption.Visible)
+
+ workFemPart = theSession.Parts.BaseWork
+ partLoadStatus2.Dispose()
+ markId2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+
+ partLoadStatus3 = theSession.Parts.SetWorkComponent(NXOpen.Assemblies.Component.Null, NXOpen.PartCollection.RefsetOption.Entire, NXOpen.PartCollection.WorkComponentOption.Visible)
+
+ workSimPart = theSession.Parts.BaseWork # Bracket_sim1
+ partLoadStatus3.Dispose()
+ # ----------------------------------------------
+ # Menu: Analysis->Solve...
+ # ----------------------------------------------
+ markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
+
+ theSession.SetUndoMarkName(markId3, "Solve Dialog")
+
+ markId4 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
+
+ theSession.DeleteUndoMark(markId4, None)
+
+ markId5 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
+
+ theCAESimSolveManager = NXOpen.CAE.SimSolveManager.GetSimSolveManager(theSession)
+
+ psolutions1 = [NXOpen.CAE.SimSolution.Null] * 1
+ simSimulation1 = workSimPart.FindObject("Simulation")
+ simSolution1 = simSimulation1.FindObject("Solution[Solution 1]")
+ psolutions1[0] = simSolution1
+ numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(psolutions1, NXOpen.CAE.SimSolution.SolveOption.Solve, NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors, NXOpen.CAE.SimSolution.SolveMode.Background)
+
+ theSession.DeleteUndoMark(markId5, None)
+
+ theSession.SetUndoMarkName(markId3, "Solve")
+
+ # ----------------------------------------------
+ # Menu: Tools->Automation->Journal->Stop Recording
+ # ----------------------------------------------
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
\ No newline at end of file
diff --git a/tests/journal_open_run_sim_and_save.py b/tests/journal_open_run_sim_and_save.py
new file mode 100644
index 00000000..b7ac8485
--- /dev/null
+++ b/tests/journal_open_run_sim_and_save.py
@@ -0,0 +1,82 @@
+# NX 2412
+# Journal created by antoi on Sat Nov 15 12:01:55 2025 Eastern Standard Time
+#
+import math
+import NXOpen
+import NXOpen.Assemblies
+import NXOpen.CAE
+import NXOpen.MenuBar
+import NXOpen.UserDefinedObjects
+def main(args) :
+
+ theSession = NXOpen.Session.GetSession() #type: NXOpen.Session
+ # ----------------------------------------------
+ # Menu: File->Open...
+ # ----------------------------------------------
+ basePart1, partLoadStatus1 = theSession.Parts.OpenActiveDisplay("C:\\Users\\antoi\\Documents\\Atomaste\\Atomizer\\tests\\Bracket_sim1.sim", NXOpen.DisplayPartOption.AllowAdditional)
+
+ workSimPart = theSession.Parts.BaseWork
+ displaySimPart = theSession.Parts.BaseDisplay
+ partLoadStatus1.Dispose()
+ theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
+
+ # User Function call - UF_translate_variable
+
+ # User Function call - UF_TEXT_ask_text_mode
+
+ baseTemplateManager1 = theSession.XYPlotManager.TemplateManager
+
+ simPart1 = workSimPart
+ theSession.Post.UpdateUserGroupsFromSimPart(simPart1)
+
+ markId1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+
+ component1 = workSimPart.ComponentAssembly.RootComponent.FindObject("COMPONENT Bracket_fem1 1")
+ partLoadStatus2 = theSession.Parts.SetWorkComponent(component1, NXOpen.PartCollection.RefsetOption.Entire, NXOpen.PartCollection.WorkComponentOption.Visible)
+
+ workFemPart = theSession.Parts.BaseWork
+ partLoadStatus2.Dispose()
+ markId2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
+
+ partLoadStatus3 = theSession.Parts.SetWorkComponent(NXOpen.Assemblies.Component.Null, NXOpen.PartCollection.RefsetOption.Entire, NXOpen.PartCollection.WorkComponentOption.Visible)
+
+ workSimPart = theSession.Parts.BaseWork # Bracket_sim1
+ partLoadStatus3.Dispose()
+ # ----------------------------------------------
+ # Menu: Analysis->Solve...
+ # ----------------------------------------------
+ markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
+
+ theSession.SetUndoMarkName(markId3, "Solve Dialog")
+
+ markId4 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
+
+ theSession.DeleteUndoMark(markId4, None)
+
+ markId5 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
+
+ theCAESimSolveManager = NXOpen.CAE.SimSolveManager.GetSimSolveManager(theSession)
+
+ psolutions1 = [NXOpen.CAE.SimSolution.Null] * 1
+ simSimulation1 = workSimPart.FindObject("Simulation")
+ simSolution1 = simSimulation1.FindObject("Solution[Solution 1]")
+ psolutions1[0] = simSolution1
+ numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(psolutions1, NXOpen.CAE.SimSolution.SolveOption.Solve, NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors, NXOpen.CAE.SimSolution.SolveMode.Background)
+
+ theSession.DeleteUndoMark(markId5, None)
+
+ theSession.SetUndoMarkName(markId3, "Solve")
+
+ # ----------------------------------------------
+ # Menu: File->Save
+ # ----------------------------------------------
+ simPart2 = workSimPart
+ partSaveStatus1 = simPart2.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
+
+ partSaveStatus1.Dispose()
+ # ----------------------------------------------
+ # Menu: Tools->Automation->Journal->Stop Recording
+ # ----------------------------------------------
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
\ No newline at end of file
diff --git a/tests/journal_reload_master.py b/tests/journal_reload_master.py
new file mode 100644
index 00000000..c25c78f3
--- /dev/null
+++ b/tests/journal_reload_master.py
@@ -0,0 +1,23 @@
+# NX 2412
+# Journal created by antoi on Sat Nov 15 12:12:09 2025 Eastern Standard Time
+#
+import math
+import NXOpen
+import NXOpen.CAE
+def main(args) :
+
+ theSession = NXOpen.Session.GetSession() #type: NXOpen.Session
+ workSimPart = theSession.Parts.BaseWork
+ displaySimPart = theSession.Parts.BaseDisplay
+ # ----------------------------------------------
+ # Menu: File->Close->Close and Reopen All Modified Parts
+ # ----------------------------------------------
+ partReopenReport1 = theSession.Parts.ReopenAll(NXOpen.BasePart.CloseModified.CloseModified, None)
+
+ partReopenReport1.Dispose()
+ # ----------------------------------------------
+ # Menu: Tools->Automation->Journal->Stop Recording
+ # ----------------------------------------------
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
\ No newline at end of file