This commit completes the optimization loop infrastructure by implementing the full FEM regeneration workflow based on the user's working journal. ## Changes ### FEM Regeneration Workflow (solve_simulation.py) - Added STEP 1: Switch to Bracket.prt and update geometry - Uses SetActiveDisplay() to make Bracket.prt active - Calls UpdateManager.DoUpdate() to rebuild CAD geometry with new expressions - Added STEP 2: Switch to Bracket_fem1 and update FE model - Uses SetActiveDisplay() to make FEM active - Calls fEModel1.UpdateFemodel() to regenerate FEM with updated geometry - Added STEP 3: Switch back to sim part before solving - Close and reopen .sim file to force reload from disk ### Enhanced Journal Output (nx_solver.py) - Display journal stdout output for debugging - Shows all journal steps: geometry update, FEM regeneration, solve, save - Helps verify workflow execution ### Verification Tools - Added verify_parametric_link.py journal to check expression dependencies - Added FEM_REGENERATION_STATUS.md documenting the complete status ## Status ### ✅ Fully Functional Components 1. Parameter updates - nx_updater.py modifies .prt expressions 2. NX solver - ~4s per solve via journal 3. Result extraction - pyNastran reads .op2 files 4. History tracking - saves to JSON/CSV 5. Optimization loop - Optuna explores parameter space 6. **FEM regeneration workflow** - Journal executes all steps successfully ### ❌ Remaining Issue: Expressions Not Linked to Geometry The optimization returns identical stress values (197.89 MPa) for all trials because the Bracket.prt expressions are not referenced by any geometry features. Evidence: - Journal verification shows FEM update steps execute successfully - Feature dependency check shows no features reference the expressions - All optimization infrastructure is working correctly The code is ready - waiting for Bracket.prt to have its expressions properly linked to the geometry features in NX. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
7.7 KiB
Python
194 lines
7.7 KiB
Python
"""
|
|
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 <sim_file_path>
|
|
|
|
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 <sim_file_path>")
|
|
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)
|
|
|
|
# STEP 1: Switch to Bracket.prt and update geometry with new expression values
|
|
print("[JOURNAL] STEP 1: Updating Bracket.prt geometry...")
|
|
try:
|
|
# Find the Bracket part
|
|
bracketPart = theSession.Parts.FindObject("Bracket")
|
|
if bracketPart:
|
|
# Make Bracket the active display part
|
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
|
bracketPart,
|
|
NXOpen.DisplayPartOption.AllowAdditional,
|
|
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
|
)
|
|
partLoadStatus.Dispose()
|
|
|
|
# CRITICAL: Update the geometry model - rebuilds features with new expressions
|
|
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
|
nErrs = theSession.UpdateManager.DoUpdate(markId_update)
|
|
theSession.DeleteUndoMark(markId_update, "NX update")
|
|
print(f"[JOURNAL] Bracket geometry updated ({nErrs} errors)")
|
|
else:
|
|
print("[JOURNAL] WARNING: Could not find Bracket part")
|
|
except Exception as e:
|
|
print(f"[JOURNAL] ERROR updating Bracket.prt: {e}")
|
|
|
|
# STEP 2: Switch to Bracket_fem1 and update FE model
|
|
print("[JOURNAL] STEP 2: Opening Bracket_fem1.fem...")
|
|
try:
|
|
# Find the FEM part
|
|
femPart1 = theSession.Parts.FindObject("Bracket_fem1")
|
|
if femPart1:
|
|
# Make FEM the active display part
|
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
|
femPart1,
|
|
NXOpen.DisplayPartOption.AllowAdditional,
|
|
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay
|
|
)
|
|
partLoadStatus.Dispose()
|
|
|
|
workFemPart = theSession.Parts.BaseWork
|
|
|
|
# CRITICAL: Update FE Model - regenerates FEM with new geometry from Bracket.prt
|
|
print("[JOURNAL] Updating FE Model...")
|
|
fEModel1 = workFemPart.FindObject("FEModel")
|
|
if fEModel1:
|
|
fEModel1.UpdateFemodel()
|
|
print("[JOURNAL] FE Model updated with new geometry!")
|
|
else:
|
|
print("[JOURNAL] WARNING: Could not find FEModel object")
|
|
else:
|
|
print("[JOURNAL] WARNING: Could not find Bracket_fem1 part")
|
|
except Exception as e:
|
|
print(f"[JOURNAL] ERROR updating FEM: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# STEP 3: Switch back to sim part
|
|
print("[JOURNAL] STEP 3: Switching back to sim part...")
|
|
try:
|
|
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
|
simPart1,
|
|
NXOpen.DisplayPartOption.AllowAdditional,
|
|
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
|
)
|
|
partLoadStatus.Dispose()
|
|
workSimPart = theSession.Parts.BaseWork
|
|
print("[JOURNAL] Switched back to sim part")
|
|
except Exception as e:
|
|
print(f"[JOURNAL] WARNING: Error switching to sim part: {e}")
|
|
|
|
# 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)
|