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

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

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

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

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

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

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

473 lines
14 KiB
Python

"""
NX Solver Manager Hook
======================
Provides Python functions to export BDF decks and solve simulations.
API Reference (NX Open):
- SimSolution.ExportSolver() -> Export Nastran deck (.dat/.bdf)
- SimSolution.Solve() -> Solve a single solution
- SimSolveManager.SolveChainOfSolutions() -> Solve solution chain
Phase 2 Task 2.1 - NX Open Automation Roadmap
Usage:
from optimization_engine.hooks.nx_cae import solver_manager
# Export BDF without solving
result = solver_manager.export_bdf(
"C:/model.sim",
"Solution 1",
"C:/output/model.dat"
)
# Solve simulation
result = solver_manager.solve_simulation("C:/model.sim", "Solution 1")
"""
import os
import json
import subprocess
import tempfile
from pathlib import Path
from typing import Optional, Dict, Any
# NX installation path (configurable)
NX_BIN_PATH = os.environ.get(
"NX_BIN_PATH",
r"C:\Program Files\Siemens\NX2506\NXBIN"
)
# Journal template for BDF export
BDF_EXPORT_JOURNAL = '''
# NX Open Python Journal - BDF Export
# Auto-generated by Atomizer hooks
# Phase 2 Task 2.1 - NX Open Automation Roadmap
import NXOpen
import NXOpen.CAE
import json
import sys
import os
def main():
"""Export BDF/DAT file from a simulation solution."""
args = sys.argv[1:] if len(sys.argv) > 1 else []
if len(args) < 3:
raise ValueError("Usage: script.py <sim_path> <solution_name> <output_bdf> [output_json]")
sim_path = args[0]
solution_name = args[1]
output_bdf = args[2]
output_json = args[3] if len(args) > 3 else None
result = {"success": False, "error": None, "data": {}}
try:
session = NXOpen.Session.GetSession()
# Set load options
working_dir = os.path.dirname(sim_path)
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
# Open the simulation file
print(f"[JOURNAL] Opening simulation: {sim_path}")
basePart, loadStatus = session.Parts.OpenActiveDisplay(
sim_path,
NXOpen.DisplayPartOption.AllowAdditional
)
loadStatus.Dispose()
# Get the sim part
simPart = session.Parts.Work
if not isinstance(simPart, NXOpen.CAE.SimPart):
raise ValueError(f"Part is not a SimPart: {type(simPart)}")
simSimulation = simPart.Simulation
print(f"[JOURNAL] Simulation: {simSimulation.Name}")
# Find the solution
solution = None
for sol in simSimulation.Solutions:
if sol.Name == solution_name:
solution = sol
break
if solution is None:
# Try to find by index or use first solution
solutions = list(simSimulation.Solutions)
if solutions:
solution = solutions[0]
print(f"[JOURNAL] Solution '{solution_name}' not found, using '{solution.Name}'")
else:
raise ValueError(f"No solutions found in simulation")
print(f"[JOURNAL] Solution: {solution.Name}")
# Export the solver deck
# The ExportSolver method exports the Nastran input deck
print(f"[JOURNAL] Exporting BDF to: {output_bdf}")
# Create export builder
# NX API: SimSolution has methods for exporting
# Method 1: Try ExportSolver if available
try:
# Some NX versions use NastranSolverExportBuilder
exportBuilder = solution.CreateNastranSolverExportBuilder()
exportBuilder.NastranInputFile = output_bdf
exportBuilder.Commit()
exportBuilder.Destroy()
print("[JOURNAL] Exported via NastranSolverExportBuilder")
except AttributeError:
# Method 2: Alternative - solve and copy output
# When solving, NX creates the deck in SXXXXX folder
print("[JOURNAL] NastranSolverExportBuilder not available")
print("[JOURNAL] BDF export requires solving - use solve_simulation instead")
raise ValueError("Direct BDF export not available in this NX version. "
"Use solve_simulation() and find BDF in solution folder.")
result["success"] = True
result["data"] = {
"output_file": output_bdf,
"solution_name": solution.Name,
"simulation": simSimulation.Name,
}
print(f"[JOURNAL] Export completed successfully")
except Exception as e:
result["error"] = str(e)
print(f"[JOURNAL] ERROR: {e}")
import traceback
traceback.print_exc()
# Write result
if output_json:
with open(output_json, 'w') as f:
json.dump(result, f, indent=2)
return result
if __name__ == '__main__':
main()
'''
def _run_journal(journal_content: str, *args) -> Dict[str, Any]:
"""Execute an NX journal script and return the result."""
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
if not run_journal_exe.exists():
return {
"success": False,
"error": f"run_journal.exe not found at {run_journal_exe}",
"data": {}
}
# Create temporary files
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as journal_file:
journal_file.write(journal_content)
journal_path = journal_file.name
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as output_file:
output_path = output_file.name
try:
# Build command
cmd = [str(run_journal_exe), journal_path, "-args"]
cmd.extend(str(a) for a in args)
cmd.append(output_path)
# Execute
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120 # 2 minute timeout
)
# Read result
if os.path.exists(output_path):
with open(output_path, 'r') as f:
return json.load(f)
else:
return {
"success": False,
"error": f"No output file generated. stdout: {result.stdout}, stderr: {result.stderr}",
"data": {}
}
except subprocess.TimeoutExpired:
return {
"success": False,
"error": "Journal execution timed out after 120 seconds",
"data": {}
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": {}
}
finally:
# Cleanup
try:
os.unlink(journal_path)
except:
pass
try:
os.unlink(output_path)
except:
pass
def export_bdf(
sim_path: str,
solution_name: str = "Solution 1",
output_bdf: Optional[str] = None
) -> Dict[str, Any]:
"""
Export Nastran deck (BDF/DAT) from a simulation without solving.
Note: This functionality depends on NX version. Some versions require
solving to generate the BDF. Use solve_simulation() and locate the BDF
in the solution folder (SXXXXX/*.dat) as an alternative.
Args:
sim_path: Path to .sim file
solution_name: Name of solution to export (default "Solution 1")
output_bdf: Output path for BDF file (default: same dir as sim)
Returns:
dict: {
'success': bool,
'error': str or None,
'data': {
'output_file': Path to exported BDF,
'solution_name': Solution name used,
'simulation': Simulation name
}
}
Example:
>>> result = export_bdf("C:/model.sim", "Solution 1", "C:/output/model.dat")
>>> if result["success"]:
... print(f"BDF exported to: {result['data']['output_file']}")
"""
sim_path = str(Path(sim_path).resolve())
if not Path(sim_path).exists():
return {
"success": False,
"error": f"Simulation file not found: {sim_path}",
"data": {}
}
if output_bdf is None:
sim_dir = Path(sim_path).parent
sim_name = Path(sim_path).stem
output_bdf = str(sim_dir / f"{sim_name}.dat")
return _run_journal(BDF_EXPORT_JOURNAL, sim_path, solution_name, output_bdf)
def get_bdf_from_solution_folder(
sim_path: str,
solution_name: str = "Solution 1"
) -> Dict[str, Any]:
"""
Locate BDF file in the solution output folder.
After solving, NX creates a folder structure like:
- model_sim1_fem1_SXXXXX/
- model_sim1_fem1.dat (BDF file)
- model_sim1_fem1.op2 (results)
This function finds the BDF without running export.
Args:
sim_path: Path to .sim file
solution_name: Name of solution
Returns:
dict: {
'success': bool,
'error': str or None,
'data': {
'bdf_file': Path to BDF if found,
'solution_folders': List of found solution folders
}
}
"""
sim_path = Path(sim_path)
if not sim_path.exists():
return {
"success": False,
"error": f"Simulation file not found: {sim_path}",
"data": {}
}
sim_dir = sim_path.parent
sim_stem = sim_path.stem
# Search for solution folders (pattern: *_SXXXXX)
solution_folders = list(sim_dir.glob(f"{sim_stem}*_S[0-9]*"))
if not solution_folders:
# Also try simpler patterns
solution_folders = list(sim_dir.glob("*_S[0-9]*"))
bdf_files = []
for folder in solution_folders:
if folder.is_dir():
# Look for .dat or .bdf files
dat_files = list(folder.glob("*.dat"))
bdf_files.extend(dat_files)
if bdf_files:
# Return the most recent one
bdf_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
return {
"success": True,
"error": None,
"data": {
"bdf_file": str(bdf_files[0]),
"all_bdf_files": [str(f) for f in bdf_files],
"solution_folders": [str(f) for f in solution_folders]
}
}
else:
return {
"success": False,
"error": "No BDF files found. Ensure the simulation has been solved.",
"data": {
"solution_folders": [str(f) for f in solution_folders]
}
}
def solve_simulation(
sim_path: str,
solution_name: str = "Solution 1",
expression_updates: Optional[Dict[str, float]] = None
) -> Dict[str, Any]:
"""
Solve a simulation solution.
This uses the existing solve_simulation.py journal which handles both
single-part and assembly FEM workflows.
Args:
sim_path: Path to .sim file
solution_name: Name of solution to solve (default "Solution 1")
expression_updates: Optional dict of {expression_name: value} to update
Returns:
dict: {
'success': bool,
'error': str or None,
'data': {
'solution_folder': Path to solution output folder,
'op2_file': Path to OP2 results file,
'bdf_file': Path to BDF input file
}
}
Note:
For full solve functionality, use the NXSolver class in
optimization_engine/nx_solver.py which provides more features
like iteration folders and batch processing.
"""
# This is a simplified wrapper - for full functionality use NXSolver
solve_journal = Path(__file__).parent.parent.parent / "solve_simulation.py"
if not solve_journal.exists():
return {
"success": False,
"error": f"Solve journal not found: {solve_journal}",
"data": {}
}
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
if not run_journal_exe.exists():
return {
"success": False,
"error": f"run_journal.exe not found at {run_journal_exe}",
"data": {}
}
# Build command
cmd = [str(run_journal_exe), str(solve_journal), "-args", sim_path, solution_name]
# Add expression updates
if expression_updates:
for name, value in expression_updates.items():
cmd.append(f"{name}={value}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600 # 10 minute timeout for solving
)
# Check for success in output
if "Solve completed successfully" in result.stdout or result.returncode == 0:
# Find output files
bdf_result = get_bdf_from_solution_folder(sim_path, solution_name)
return {
"success": True,
"error": None,
"data": {
"stdout": result.stdout[-2000:], # Last 2000 chars
"bdf_file": bdf_result["data"].get("bdf_file") if bdf_result["success"] else None,
"solution_folders": bdf_result["data"].get("solution_folders", [])
}
}
else:
return {
"success": False,
"error": f"Solve may have failed. Check output.",
"data": {
"stdout": result.stdout[-2000:],
"stderr": result.stderr[-1000:]
}
}
except subprocess.TimeoutExpired:
return {
"success": False,
"error": "Solve timed out after 600 seconds",
"data": {}
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": {}
}
if __name__ == "__main__":
# Example usage
import sys
if len(sys.argv) > 1:
sim_path = sys.argv[1]
solution = sys.argv[2] if len(sys.argv) > 2 else "Solution 1"
print(f"Looking for BDF in solution folder...")
result = get_bdf_from_solution_folder(sim_path, solution)
if result["success"]:
print(f"Found BDF: {result['data']['bdf_file']}")
else:
print(f"Error: {result['error']}")
print(f"Trying to export...")
result = export_bdf(sim_path, solution)
print(f"Export result: {result}")
else:
print("Usage: python solver_manager.py <sim_path> [solution_name]")