fix: Apply expression updates directly in NX journal

Critical fix - the expressions were not being applied during optimization!
The journal now receives expression values and applies them using
EditExpressionWithUnits() BEFORE rebuilding geometry and regenerating FEM.

## Key Changes

### Expression Application in Journal (solve_simulation.py)
- Journal now accepts expression values as arguments (tip_thickness, support_angle)
- Applies expressions using EditExpressionWithUnits() on active Bracket part
- Calls MakeUpToDate() on each modified expression
- Then calls UpdateManager.DoUpdate() to rebuild geometry with new values
- Follows the exact pattern from the user's working journal

### NX Solver Updates (nx_solver.py)
- Added expression_updates parameter to run_simulation() and run_nx_simulation()
- Passes expression values to journal via sys.argv
- For bracket: passes tip_thickness and support_angle as separate args

### Test Script Updates (test_journal_optimization.py)
- Removed nx_updater step (no longer needed - expressions applied in journal)
- model_updater now just stores design vars in global variable
- simulation_runner passes expression_updates to nx_solver
- Sequential workflow: update vars -> run journal (apply expressions) -> extract results

## Results - OPTIMIZATION NOW WORKS!

Before (all trials same stress):
- Trial 0: tip=23.48, angle=37.21 → stress=197.89 MPa
- Trial 1: tip=20.08, angle=20.32 → stress=197.89 MPa (SAME!)
- Trial 2: tip=18.19, angle=35.23 → stress=197.89 MPa (SAME!)

After (varying stress values):
- Trial 0: tip=21.62, angle=30.15 → stress=192.71 MPa 
- Trial 1: tip=17.17, angle=33.52 → stress=167.96 MPa  BEST!
- Trial 2: tip=15.06, angle=21.81 → stress=242.50 MPa 

Mesh also changes: 1027 → 951 CTETRA elements with different parameters.

The optimization loop is now fully functional with expressions being properly
applied and the FEM regenerating with correct geometry!

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-15 12:47:55 -05:00
parent 718c72bea2
commit 96e88fe714
14 changed files with 5547 additions and 6253 deletions

View File

@@ -127,7 +127,8 @@ class NXSolver:
self,
sim_file: Path,
working_dir: Optional[Path] = None,
cleanup: bool = True
cleanup: bool = True,
expression_updates: Optional[Dict[str, float]] = None
) -> Dict[str, Any]:
"""
Run NX Nastran simulation.
@@ -136,6 +137,9 @@ class NXSolver:
sim_file: Path to .sim file
working_dir: Working directory for solver (defaults to sim file dir)
cleanup: Remove intermediate files after solving
expression_updates: Dict of expression name -> value to update
(only used in journal mode)
e.g., {'tip_thickness': 22.5, 'support_angle': 35.0}
Returns:
Dictionary with:
@@ -218,10 +222,23 @@ class NXSolver:
with open(journal_template, 'r') as f:
journal_content = f.read()
# Create a custom journal that passes the sim file path
# Create a custom journal that passes the sim file path and expression values
# Build argv list with expression updates
argv_list = [f"r'{sim_file.absolute()}'"]
# Add expression values if provided
if expression_updates:
# For bracket example, we expect: tip_thickness, support_angle
if 'tip_thickness' in expression_updates:
argv_list.append(str(expression_updates['tip_thickness']))
if 'support_angle' in expression_updates:
argv_list.append(str(expression_updates['support_angle']))
argv_str = ', '.join(argv_list)
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
sys.argv = ['', {argv_str}] # Set argv for the main function
{journal_content}
'''
with open(temp_journal, 'w') as f:
@@ -442,7 +459,8 @@ def run_nx_simulation(
nastran_version: str = "2412",
timeout: int = 600,
cleanup: bool = True,
use_journal: bool = True
use_journal: bool = True,
expression_updates: Optional[Dict[str, float]] = None
) -> Path:
"""
Convenience function to run NX simulation and return OP2 file path.
@@ -453,6 +471,7 @@ def run_nx_simulation(
timeout: Solver timeout in seconds
cleanup: Remove temp files
use_journal: Use NX journal for solving (recommended for licensing)
expression_updates: Dict of expression name -> value to update in journal
Returns:
Path to output .op2 file
@@ -461,7 +480,7 @@ def run_nx_simulation(
RuntimeError: If simulation fails
"""
solver = NXSolver(nastran_version=nastran_version, timeout=timeout, use_journal=use_journal)
result = solver.run_simulation(sim_file, cleanup=cleanup)
result = solver.run_simulation(sim_file, cleanup=cleanup, expression_updates=expression_updates)
if not result['success']:
error_msg = '\n'.join(result['errors']) if result['errors'] else 'Unknown error'

View File

@@ -15,18 +15,30 @@ import NXOpen.CAE
def main(args):
"""
Open and solve a simulation file.
Open and solve a simulation file with updated expression values.
Args:
args: Command line arguments, args[0] should be the .sim file path
args: Command line arguments
args[0]: .sim file path
args[1]: tip_thickness value (optional)
args[2]: support_angle value (optional)
"""
if len(args) < 1:
print("ERROR: No .sim file path provided")
print("Usage: run_journal.exe solve_simulation.py <sim_file_path>")
print("Usage: run_journal.exe solve_simulation.py <sim_file_path> [tip_thickness] [support_angle]")
return False
sim_file_path = args[0]
# Parse expression values if provided
tip_thickness = float(args[1]) if len(args) > 1 else None
support_angle = float(args[2]) if len(args) > 2 else None
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
if tip_thickness is not None:
print(f"[JOURNAL] Will update tip_thickness = {tip_thickness}")
if support_angle is not None:
print(f"[JOURNAL] Will update support_angle = {support_angle}")
try:
theSession = NXOpen.Session.GetSession()
@@ -62,7 +74,7 @@ def main(args):
simPart1 = workSimPart
theSession.Post.UpdateUserGroupsFromSimPart(simPart1)
# STEP 1: Switch to Bracket.prt and update geometry with new expression values
# STEP 1: Switch to Bracket.prt and update expressions, then update geometry
print("[JOURNAL] STEP 1: Updating Bracket.prt geometry...")
try:
# Find the Bracket part
@@ -76,7 +88,44 @@ def main(args):
)
partLoadStatus.Dispose()
workPart = theSession.Parts.Work
# CRITICAL: Apply expression changes BEFORE updating geometry
expressions_updated = []
if tip_thickness is not None:
print(f"[JOURNAL] Applying tip_thickness = {tip_thickness}")
expr_tip = workPart.Expressions.FindObject("tip_thickness")
if expr_tip:
unit_mm = workPart.UnitCollection.FindObject("MilliMeter")
workPart.Expressions.EditExpressionWithUnits(expr_tip, unit_mm, str(tip_thickness))
expressions_updated.append(expr_tip)
print(f"[JOURNAL] tip_thickness updated")
else:
print(f"[JOURNAL] WARNING: tip_thickness expression not found!")
if support_angle is not None:
print(f"[JOURNAL] Applying support_angle = {support_angle}")
expr_angle = workPart.Expressions.FindObject("support_angle")
if expr_angle:
unit_deg = workPart.UnitCollection.FindObject("Degrees")
workPart.Expressions.EditExpressionWithUnits(expr_angle, unit_deg, str(support_angle))
expressions_updated.append(expr_angle)
print(f"[JOURNAL] support_angle updated")
else:
print(f"[JOURNAL] WARNING: support_angle expression not found!")
# Make expressions up to date
if expressions_updated:
print(f"[JOURNAL] Making {len(expressions_updated)} expression(s) up to date...")
for expr in expressions_updated:
markId_expr = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Make Up to Date")
objects1 = [expr]
theSession.UpdateManager.MakeUpToDate(objects1, markId_expr)
theSession.DeleteUndoMark(markId_expr, None)
# CRITICAL: Update the geometry model - rebuilds features with new expressions
print(f"[JOURNAL] Rebuilding geometry with new expression values...")
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
nErrs = theSession.UpdateManager.DoUpdate(markId_update)
theSession.DeleteUndoMark(markId_update, "NX update")
@@ -85,6 +134,8 @@ def main(args):
print("[JOURNAL] WARNING: Could not find Bracket part")
except Exception as e:
print(f"[JOURNAL] ERROR updating Bracket.prt: {e}")
import traceback
traceback.print_exc()
# STEP 2: Switch to Bracket_fem1 and update FE model
print("[JOURNAL] STEP 2: Opening Bracket_fem1.fem...")