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>
This commit is contained in:
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
NX CAE Hooks
|
||||
============
|
||||
|
||||
Python hooks for NX CAE (FEM/Simulation) operations via NX Open API.
|
||||
|
||||
Modules
|
||||
-------
|
||||
solver_manager : Solution export and solve operations
|
||||
- export_bdf: Export Nastran deck without solving
|
||||
- solve_simulation: Solve a simulation solution
|
||||
|
||||
Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||
"""
|
||||
|
||||
from . import solver_manager
|
||||
|
||||
__all__ = ['solver_manager']
|
||||
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""
|
||||
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]")
|
||||
Reference in New Issue
Block a user