feat: Add M1 mirror Zernike optimization with correct RMS calculation
Major improvements to telescope mirror optimization workflow: Assembly FEM Workflow (solve_simulation.py): - Fixed multi-part assembly FEM update sequence - Use ImportFromFile() for reliable expression updates - Add DuplicateNodesCheckBuilder with MergeOccurrenceNodes=True - Switch to Foreground solve mode for multi-subcase solutions - Add detailed logging and diagnostics for node merge operations Zernike RMS Calculation: - CRITICAL FIX: Use correct surface-based RMS formula - Global RMS = sqrt(mean(W^2)) from actual WFE values - Filtered RMS = sqrt(mean(W_residual^2)) after removing low-order fit - This matches zernike_Post_Script_NX.py (optical standard) - Previous WRONG formula was: sqrt(sum(coeffs^2)) - Add compute_rms_filter_j1to3() for optician workload metric Subcase Mapping: - Fix subcase mapping to match NX model: - Subcase 1 = 90 deg (polishing orientation) - Subcase 2 = 20 deg (reference) - Subcase 3 = 40 deg - Subcase 4 = 60 deg New Study: M1 Mirror Zernike Optimization - Full optimization config with 11 design variables - 3 objectives: rel_filtered_rms_40_vs_20, rel_filtered_rms_60_vs_20, mfg_90_optician_workload - Neural surrogate support for accelerated optimization Documentation: - Update ZERNIKE_INTEGRATION.md with correct RMS formula - Update ASSEMBLY_FEM_WORKFLOW.md with expression import and node merge details - Add reference scripts from original zernike_Post_Script_NX.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,53 @@
|
||||
"""
|
||||
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>
|
||||
This script handles BOTH single-part simulations AND multi-part assembly FEMs.
|
||||
|
||||
Based on recorded NX journal pattern for solving simulations.
|
||||
=============================================================================
|
||||
MULTI-PART ASSEMBLY FEM WORKFLOW (for .afm-based simulations)
|
||||
=============================================================================
|
||||
|
||||
Based on recorded NX journal from interactive session (Nov 28, 2025).
|
||||
|
||||
The correct workflow for assembly FEM updates:
|
||||
|
||||
1. LOAD PARTS
|
||||
- Open ASSY_M1.prt and M1_Blank_fem1_i.prt to have geometry loaded
|
||||
- Find and switch to M1_Blank part for expression editing
|
||||
|
||||
2. UPDATE EXPRESSIONS
|
||||
- Switch to modeling application
|
||||
- Edit expressions with units
|
||||
- Call MakeUpToDate() on modified expressions
|
||||
- Call DoUpdate() to rebuild geometry
|
||||
|
||||
3. SWITCH TO SIM AND UPDATE FEM COMPONENTS
|
||||
- Open the .sim file
|
||||
- Navigate component hierarchy via RootComponent.FindObject()
|
||||
- For each component FEM:
|
||||
- SetWorkComponent() to make it the work part
|
||||
- FindObject("FEModel").UpdateFemodel()
|
||||
|
||||
4. MERGE DUPLICATE NODES (critical for assembly FEM!)
|
||||
- Switch to assembly FEM component
|
||||
- CreateDuplicateNodesCheckBuilder()
|
||||
- Set MergeOccurrenceNodes = True
|
||||
- IdentifyDuplicateNodes() then MergeDuplicateNodes()
|
||||
|
||||
5. RESOLVE LABEL CONFLICTS
|
||||
- CreateAssemblyLabelManagerBuilder()
|
||||
- SetFEModelOccOffsets() for each occurrence
|
||||
- Commit()
|
||||
|
||||
6. SOLVE
|
||||
- SetWorkComponent(Null) to return to sim level
|
||||
- SolveChainOfSolutions()
|
||||
|
||||
=============================================================================
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import NXOpen
|
||||
import NXOpen.Assemblies
|
||||
import NXOpen.CAE
|
||||
@@ -15,341 +55,510 @@ import NXOpen.CAE
|
||||
|
||||
def main(args):
|
||||
"""
|
||||
Open and solve a simulation file with updated expression values.
|
||||
Main entry point for NX journal.
|
||||
|
||||
Args:
|
||||
args: Command line arguments
|
||||
args[0]: .sim file path
|
||||
args[1]: solution_name (optional, e.g., "Solution_Normal_Modes" or None for default)
|
||||
args[1]: solution_name (optional, or "None" for default)
|
||||
args[2+]: expression updates as "name=value" pairs
|
||||
"""
|
||||
if len(args) < 1:
|
||||
print("ERROR: No .sim file path provided")
|
||||
print("Usage: run_journal.exe solve_simulation.py <sim_file_path> [solution_name] [expr1=val1] [expr2=val2] ...")
|
||||
print("Usage: run_journal.exe solve_simulation.py <sim_file_path> [solution_name] [expr1=val1] ...")
|
||||
return False
|
||||
|
||||
sim_file_path = args[0]
|
||||
|
||||
# Parse solution name if provided (args[1])
|
||||
solution_name = args[1] if len(args) > 1 and args[1] != 'None' else None
|
||||
|
||||
# Extract base name from sim file (e.g., "Beam_sim1.sim" -> "Beam")
|
||||
import os
|
||||
sim_filename = os.path.basename(sim_file_path)
|
||||
part_base_name = sim_filename.split('_sim')[0] if '_sim' in sim_filename else sim_filename.split('.sim')[0]
|
||||
|
||||
# Parse expression updates from args[2+] as "name=value" pairs
|
||||
# Parse expression updates
|
||||
expression_updates = {}
|
||||
for arg in args[2:]:
|
||||
if '=' in arg:
|
||||
name, value = arg.split('=', 1)
|
||||
expression_updates[name] = float(value)
|
||||
|
||||
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
|
||||
print(f"[JOURNAL] Detected part base name: {part_base_name}")
|
||||
if solution_name:
|
||||
print(f"[JOURNAL] Will solve specific solution: {solution_name}")
|
||||
else:
|
||||
print(f"[JOURNAL] Will solve default solution (Solution 1)")
|
||||
if expression_updates:
|
||||
print(f"[JOURNAL] Will update expressions:")
|
||||
for name, value in expression_updates.items():
|
||||
print(f"[JOURNAL] {name} = {value}")
|
||||
# Get working directory
|
||||
working_dir = os.path.dirname(os.path.abspath(sim_file_path))
|
||||
sim_filename = os.path.basename(sim_file_path)
|
||||
|
||||
print(f"[JOURNAL] " + "="*60)
|
||||
print(f"[JOURNAL] NX SIMULATION SOLVER (Assembly FEM Workflow)")
|
||||
print(f"[JOURNAL] " + "="*60)
|
||||
print(f"[JOURNAL] Simulation: {sim_filename}")
|
||||
print(f"[JOURNAL] Working directory: {working_dir}")
|
||||
print(f"[JOURNAL] Solution: {solution_name or 'Solution 1'}")
|
||||
print(f"[JOURNAL] Expression updates: {len(expression_updates)}")
|
||||
for name, value in expression_updates.items():
|
||||
print(f"[JOURNAL] {name} = {value}")
|
||||
|
||||
try:
|
||||
theSession = NXOpen.Session.GetSession()
|
||||
|
||||
# Set load options to load linked parts from directory
|
||||
print("[JOURNAL] Setting load options for linked parts...")
|
||||
import os
|
||||
working_dir = os.path.dirname(os.path.abspath(sim_file_path))
|
||||
|
||||
# Complete load options setup (from recorded journal)
|
||||
# Set load options
|
||||
theSession.Parts.LoadOptions.LoadLatest = False
|
||||
theSession.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
|
||||
searchDirectories = [working_dir]
|
||||
searchSubDirs = [True]
|
||||
theSession.Parts.LoadOptions.SetSearchDirectories(searchDirectories, searchSubDirs)
|
||||
|
||||
theSession.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
theSession.Parts.LoadOptions.ComponentsToLoad = NXOpen.LoadOptions.LoadComponents.All
|
||||
theSession.Parts.LoadOptions.PartLoadOption = NXOpen.LoadOptions.LoadOption.FullyLoad
|
||||
theSession.Parts.LoadOptions.SetInterpartData(True, NXOpen.LoadOptions.Parent.All)
|
||||
theSession.Parts.LoadOptions.AllowSubstitution = False
|
||||
theSession.Parts.LoadOptions.GenerateMissingPartFamilyMembers = True
|
||||
theSession.Parts.LoadOptions.AbortOnFailure = False
|
||||
|
||||
referenceSets = ["As Saved", "Use Simplified", "Use Model", "Entire Part", "Empty"]
|
||||
theSession.Parts.LoadOptions.SetDefaultReferenceSets(referenceSets)
|
||||
theSession.Parts.LoadOptions.ReferenceSetOverride = False
|
||||
|
||||
print(f"[JOURNAL] Load directory set to: {working_dir}")
|
||||
|
||||
# Close any currently open sim file to force reload from disk
|
||||
print("[JOURNAL] Checking for open parts...")
|
||||
# Close any 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}")
|
||||
theSession.Parts.CloseAll([NXOpen.BasePart.CloseWholeTree])
|
||||
except:
|
||||
pass
|
||||
|
||||
# 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
|
||||
)
|
||||
# Check for assembly FEM files
|
||||
afm_files = [f for f in os.listdir(working_dir) if f.endswith('.afm')]
|
||||
is_assembly = len(afm_files) > 0
|
||||
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
displaySimPart = theSession.Parts.BaseDisplay
|
||||
|
||||
print(f"[JOURNAL] Simulation opened successfully")
|
||||
partLoadStatus1.Dispose()
|
||||
|
||||
# Switch to simulation application
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||
|
||||
simPart1 = workSimPart
|
||||
theSession.Post.UpdateUserGroupsFromSimPart(simPart1)
|
||||
|
||||
# STEP 1: Try to switch to part and update expressions (optional for some models)
|
||||
print(f"[JOURNAL] STEP 1: Checking for {part_base_name}.prt geometry...")
|
||||
geometry_updated = False
|
||||
try:
|
||||
# Find the main part (may not exist for embedded geometry models)
|
||||
bracketPart = None
|
||||
try:
|
||||
bracketPart = theSession.Parts.FindObject(part_base_name)
|
||||
except:
|
||||
pass
|
||||
|
||||
if bracketPart:
|
||||
print(f"[JOURNAL] Found {part_base_name} part, updating geometry...")
|
||||
# Make Bracket the active display part
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
bracketPart,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
workPart = theSession.Parts.Work
|
||||
|
||||
# CRITICAL: Apply expression changes BEFORE updating geometry
|
||||
expressions_updated = []
|
||||
|
||||
# Apply all expression updates dynamically
|
||||
for expr_name, expr_value in expression_updates.items():
|
||||
print(f"[JOURNAL] Applying {expr_name} = {expr_value}")
|
||||
try:
|
||||
expr_obj = workPart.Expressions.FindObject(expr_name)
|
||||
if expr_obj:
|
||||
# Use millimeters as default unit for geometric parameters
|
||||
unit_mm = workPart.UnitCollection.FindObject("MilliMeter")
|
||||
workPart.Expressions.EditExpressionWithUnits(expr_obj, unit_mm, str(expr_value))
|
||||
expressions_updated.append(expr_obj)
|
||||
print(f"[JOURNAL] {expr_name} updated successfully")
|
||||
else:
|
||||
print(f"[JOURNAL] WARNING: {expr_name} expression not found!")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR updating {expr_name}: {e}")
|
||||
|
||||
# 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")
|
||||
print(f"[JOURNAL] {part_base_name} geometry updated ({nErrs} errors)")
|
||||
|
||||
# Extract mass from expression p173 if it exists and write to temp file
|
||||
try:
|
||||
mass_expr = workPart.Expressions.FindObject("p173")
|
||||
if mass_expr:
|
||||
mass_kg = mass_expr.Value
|
||||
mass_output_file = os.path.join(working_dir, "_temp_mass.txt")
|
||||
with open(mass_output_file, 'w') as f:
|
||||
f.write(str(mass_kg))
|
||||
print(f"[JOURNAL] Mass from p173: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)")
|
||||
print(f"[JOURNAL] Mass written to: {mass_output_file}")
|
||||
except:
|
||||
pass # Expression p173 might not exist in all models
|
||||
|
||||
geometry_updated = True
|
||||
else:
|
||||
print(f"[JOURNAL] {part_base_name} part not found - may be embedded in sim file")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] Could not update {part_base_name}.prt: {e}")
|
||||
print(f"[JOURNAL] Continuing with sim-only solve...")
|
||||
|
||||
# STEP 2: Try to switch to FEM part and update (optional for some models)
|
||||
fem_part_name = f"{part_base_name}_fem1"
|
||||
print(f"[JOURNAL] STEP 2: Checking for {fem_part_name}.fem...")
|
||||
fem_updated = False
|
||||
try:
|
||||
# Find the FEM part (may not exist or may have different name)
|
||||
femPart1 = None
|
||||
try:
|
||||
femPart1 = theSession.Parts.FindObject(fem_part_name)
|
||||
except:
|
||||
# Try with _i suffix for idealized FEM
|
||||
try:
|
||||
femPart1 = theSession.Parts.FindObject(f"{fem_part_name}_i")
|
||||
except:
|
||||
pass
|
||||
|
||||
if femPart1:
|
||||
print(f"[JOURNAL] Found FEM part, updating...")
|
||||
# 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
|
||||
print("[JOURNAL] Updating FE Model...")
|
||||
fEModel1 = workFemPart.FindObject("FEModel")
|
||||
if fEModel1:
|
||||
fEModel1.UpdateFemodel()
|
||||
print("[JOURNAL] FE Model updated with new geometry!")
|
||||
fem_updated = True
|
||||
else:
|
||||
print("[JOURNAL] WARNING: Could not find FEModel object")
|
||||
else:
|
||||
print(f"[JOURNAL] FEM part not found - may be embedded in sim file")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] Could not update FEM: {e}")
|
||||
print(f"[JOURNAL] Continuing with sim-only solve...")
|
||||
|
||||
# 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 simulation object
|
||||
simSimulation1 = workSimPart.FindObject("Simulation")
|
||||
|
||||
# CRITICAL: Disable solution monitor when solving multiple solutions
|
||||
# This prevents NX from opening multiple monitor windows which superpose and cause usability issues
|
||||
if not solution_name:
|
||||
print("[JOURNAL] Disabling solution monitor for all solutions to prevent window pile-up...")
|
||||
try:
|
||||
# Get all solutions in the simulation
|
||||
solutions_disabled = 0
|
||||
solution_num = 1
|
||||
while True:
|
||||
try:
|
||||
solution_obj_name = f"Solution[Solution {solution_num}]"
|
||||
simSolution = simSimulation1.FindObject(solution_obj_name)
|
||||
if simSolution:
|
||||
propertyTable = simSolution.SolverOptionsPropertyTable
|
||||
propertyTable.SetBooleanPropertyValue("solution monitor", False)
|
||||
solutions_disabled += 1
|
||||
solution_num += 1
|
||||
else:
|
||||
break
|
||||
except:
|
||||
break # No more solutions
|
||||
print(f"[JOURNAL] Solution monitor disabled for {solutions_disabled} solution(s)")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Could not disable solution monitor: {e}")
|
||||
print(f"[JOURNAL] Continuing with solve anyway...")
|
||||
|
||||
# Get the solution(s) to solve - either specific or all
|
||||
if solution_name:
|
||||
# Solve specific solution in background mode
|
||||
solution_obj_name = f"Solution[{solution_name}]"
|
||||
print(f"[JOURNAL] Looking for solution: {solution_obj_name}")
|
||||
simSolution1 = simSimulation1.FindObject(solution_obj_name)
|
||||
psolutions1 = [simSolution1]
|
||||
|
||||
numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Background
|
||||
if is_assembly and expression_updates:
|
||||
print(f"[JOURNAL] ")
|
||||
print(f"[JOURNAL] DETECTED: Multi-part Assembly FEM")
|
||||
print(f"[JOURNAL] Using ASSEMBLY FEM WORKFLOW")
|
||||
print(f"[JOURNAL] ")
|
||||
return solve_assembly_fem_workflow(
|
||||
theSession, sim_file_path, solution_name, expression_updates, working_dir
|
||||
)
|
||||
else:
|
||||
# Solve ALL solutions using SolveAllSolutions API (Foreground mode)
|
||||
# This ensures all solutions (static + modal, etc.) complete before returning
|
||||
print(f"[JOURNAL] Solving all solutions using SolveAllSolutions API (Foreground mode)...")
|
||||
|
||||
numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveAllSolutions(
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground,
|
||||
False
|
||||
print(f"[JOURNAL] ")
|
||||
print(f"[JOURNAL] Using SIMPLE WORKFLOW (no expression updates or single-part)")
|
||||
print(f"[JOURNAL] ")
|
||||
return solve_simple_workflow(
|
||||
theSession, sim_file_path, solution_name, expression_updates, working_dir
|
||||
)
|
||||
|
||||
theSession.DeleteUndoMark(markId5, None)
|
||||
theSession.SetUndoMarkName(markId3, "Solve")
|
||||
|
||||
print(f"[JOURNAL] Solve completed!")
|
||||
print(f"[JOURNAL] Solutions solved: {numsolutionssolved1}")
|
||||
print(f"[JOURNAL] Solutions failed: {numsolutionsfailed1}")
|
||||
print(f"[JOURNAL] Solutions skipped: {numsolutionsskipped1}")
|
||||
|
||||
# NOTE: When solution_name=None, we use Foreground mode to ensure all solutions
|
||||
# complete before returning. When solution_name is specified, Background mode is used.
|
||||
|
||||
# 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}")
|
||||
print(f"[JOURNAL] FATAL ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
|
||||
"""
|
||||
Full assembly FEM workflow based on recorded NX journal.
|
||||
|
||||
This is the correct workflow for multi-part assembly FEMs.
|
||||
"""
|
||||
sim_filename = os.path.basename(sim_file_path)
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 1: LOAD REQUIRED PARTS
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 1: Loading required parts...")
|
||||
|
||||
# Load ASSY_M1.prt (to have the geometry assembly available)
|
||||
assy_prt_path = os.path.join(working_dir, "ASSY_M1.prt")
|
||||
if os.path.exists(assy_prt_path):
|
||||
print(f"[JOURNAL] Loading ASSY_M1.prt...")
|
||||
markId1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Load Part")
|
||||
part1, partLoadStatus1 = theSession.Parts.Open(assy_prt_path)
|
||||
partLoadStatus1.Dispose()
|
||||
else:
|
||||
print(f"[JOURNAL] WARNING: ASSY_M1.prt not found, continuing anyway...")
|
||||
|
||||
# Load M1_Blank_fem1_i.prt (idealized geometry)
|
||||
idealized_prt_path = os.path.join(working_dir, "M1_Blank_fem1_i.prt")
|
||||
if os.path.exists(idealized_prt_path):
|
||||
print(f"[JOURNAL] Loading M1_Blank_fem1_i.prt...")
|
||||
markId2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Load Part")
|
||||
part2, partLoadStatus2 = theSession.Parts.Open(idealized_prt_path)
|
||||
partLoadStatus2.Dispose()
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 2: UPDATE EXPRESSIONS IN M1_BLANK
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 2: Updating expressions in M1_Blank...")
|
||||
|
||||
# Find and switch to M1_Blank part
|
||||
try:
|
||||
part3 = theSession.Parts.FindObject("M1_Blank")
|
||||
markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Change Displayed Part")
|
||||
status1, partLoadStatus3 = theSession.Parts.SetActiveDisplay(
|
||||
part3,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
)
|
||||
partLoadStatus3.Dispose()
|
||||
|
||||
# Switch to modeling application for expression editing
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_MODELING")
|
||||
|
||||
workPart = theSession.Parts.Work
|
||||
|
||||
# Create undo mark for expressions
|
||||
markId4 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
|
||||
theSession.SetUndoMarkName(markId4, "Expressions Dialog")
|
||||
|
||||
# Write expressions to a temp file and import (more reliable than editing one by one)
|
||||
exp_file_path = os.path.join(working_dir, "_temp_expressions.exp")
|
||||
with open(exp_file_path, 'w') as f:
|
||||
for expr_name, expr_value in expression_updates.items():
|
||||
# Determine unit
|
||||
if 'angle' in expr_name.lower() or 'vertical' in expr_name.lower():
|
||||
unit_str = "Degrees"
|
||||
else:
|
||||
unit_str = "MilliMeter"
|
||||
f.write(f"[{unit_str}]{expr_name}={expr_value}\n")
|
||||
print(f"[JOURNAL] {expr_name} = {expr_value} ({unit_str})")
|
||||
|
||||
print(f"[JOURNAL] Importing expressions from file...")
|
||||
markId_import = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Import Expressions")
|
||||
|
||||
try:
|
||||
expModified, errorMessages = workPart.Expressions.ImportFromFile(
|
||||
exp_file_path,
|
||||
NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
)
|
||||
print(f"[JOURNAL] Expressions imported: {expModified} modified")
|
||||
if errorMessages:
|
||||
print(f"[JOURNAL] Import errors: {errorMessages}")
|
||||
|
||||
# Update geometry after import
|
||||
print(f"[JOURNAL] Rebuilding geometry...")
|
||||
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] Geometry rebuilt ({nErrs} errors)")
|
||||
|
||||
updated_expressions = list(expression_updates.keys())
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR importing expressions: {e}")
|
||||
updated_expressions = []
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.remove(exp_file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
theSession.SetUndoMarkName(markId4, "Expressions")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR updating expressions: {e}")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 3: OPEN SIM AND UPDATE COMPONENT FEMs
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 3: Opening sim and updating component FEMs...")
|
||||
|
||||
# Try to find the sim part first (like the recorded journal does)
|
||||
# This ensures we're working with the same loaded sim part context
|
||||
sim_part_name = os.path.splitext(sim_filename)[0] # e.g., "ASSY_M1_assyfem1_sim1"
|
||||
print(f"[JOURNAL] Looking for sim part: {sim_part_name}")
|
||||
|
||||
markId_sim = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Change Displayed Part")
|
||||
|
||||
try:
|
||||
# First try to find it among loaded parts (like recorded journal)
|
||||
simPart1 = theSession.Parts.FindObject(sim_part_name)
|
||||
status_sim, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
simPart1,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Found and activated existing sim part")
|
||||
except:
|
||||
# Fallback: Open fresh if not found
|
||||
print(f"[JOURNAL] Sim part not found, opening fresh: {sim_filename}")
|
||||
basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay(
|
||||
sim_file_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
displaySimPart = theSession.Parts.BaseDisplay
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
|
||||
|
||||
# Navigate component hierarchy
|
||||
try:
|
||||
rootComponent = workSimPart.ComponentAssembly.RootComponent
|
||||
component1 = rootComponent.FindObject("COMPONENT ASSY_M1_assyfem1 1")
|
||||
|
||||
# Update M1_Blank_fem1
|
||||
print(f"[JOURNAL] Updating M1_Blank_fem1...")
|
||||
try:
|
||||
component2 = component1.FindObject("COMPONENT M1_Blank_fem1 1")
|
||||
markId_fem1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
|
||||
partLoadStatus5 = theSession.Parts.SetWorkComponent(
|
||||
component2,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
)
|
||||
workFemPart = theSession.Parts.BaseWork
|
||||
partLoadStatus5.Dispose()
|
||||
|
||||
markId_update1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update FE Model")
|
||||
fEModel1 = workFemPart.FindObject("FEModel")
|
||||
fEModel1.UpdateFemodel()
|
||||
print(f"[JOURNAL] M1_Blank_fem1 updated")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: M1_Blank_fem1: {e}")
|
||||
|
||||
# Update M1_Vertical_Support_Skeleton_fem1
|
||||
print(f"[JOURNAL] Updating M1_Vertical_Support_Skeleton_fem1...")
|
||||
try:
|
||||
component3 = component1.FindObject("COMPONENT M1_Vertical_Support_Skeleton_fem1 3")
|
||||
markId_fem2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Make Work Part")
|
||||
partLoadStatus6 = theSession.Parts.SetWorkComponent(
|
||||
component3,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
)
|
||||
workFemPart = theSession.Parts.BaseWork
|
||||
partLoadStatus6.Dispose()
|
||||
|
||||
markId_update2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update FE Model")
|
||||
fEModel2 = workFemPart.FindObject("FEModel")
|
||||
fEModel2.UpdateFemodel()
|
||||
print(f"[JOURNAL] M1_Vertical_Support_Skeleton_fem1 updated")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: M1_Vertical_Support_Skeleton_fem1: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR navigating component hierarchy: {e}")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 4: MERGE DUPLICATE NODES
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 4: Merging duplicate nodes...")
|
||||
|
||||
try:
|
||||
# Switch to assembly FEM
|
||||
partLoadStatus8 = theSession.Parts.SetWorkComponent(
|
||||
component1,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
)
|
||||
workAssyFemPart = theSession.Parts.BaseWork
|
||||
displaySimPart = theSession.Parts.BaseDisplay
|
||||
partLoadStatus8.Dispose()
|
||||
print(f"[JOURNAL] Switched to assembly FEM: {workAssyFemPart.Name}")
|
||||
|
||||
markId_merge = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
|
||||
|
||||
caePart1 = workAssyFemPart
|
||||
duplicateNodesCheckBuilder1 = caePart1.ModelCheckMgr.CreateDuplicateNodesCheckBuilder()
|
||||
|
||||
# Set tolerance
|
||||
unit_tol = duplicateNodesCheckBuilder1.Tolerance.Units
|
||||
duplicateNodesCheckBuilder1.Tolerance.Units = unit_tol
|
||||
duplicateNodesCheckBuilder1.Tolerance.SetFormula("0.01")
|
||||
print(f"[JOURNAL] Tolerance: 0.01 mm")
|
||||
|
||||
# Enable occurrence node merge - CRITICAL for assembly FEM
|
||||
duplicateNodesCheckBuilder1.MergeOccurrenceNodes = True
|
||||
print(f"[JOURNAL] MergeOccurrenceNodes: True")
|
||||
|
||||
theSession.SetUndoMarkName(markId_merge, "Duplicate Nodes Dialog")
|
||||
|
||||
# Configure display settings
|
||||
displaysettings1 = NXOpen.CAE.ModelCheck.DuplicateNodesCheckBuilder.DisplaySettings()
|
||||
displaysettings1.ShowDuplicateNodes = True
|
||||
displaysettings1.ShowMergedNodeLabels = False
|
||||
displaysettings1.ShowRetainedNodeLabels = False
|
||||
displaysettings1.KeepNodesColor = displaySimPart.Colors.Find("Blue")
|
||||
displaysettings1.MergeNodesColor = displaySimPart.Colors.Find("Yellow")
|
||||
displaysettings1.UnableToMergeNodesColor = displaySimPart.Colors.Find("Red")
|
||||
duplicateNodesCheckBuilder1.DisplaySettingsData = displaysettings1
|
||||
|
||||
# Check scope
|
||||
duplicateNodesCheckBuilder1.CheckScopeOption = NXOpen.CAE.ModelCheck.CheckScope.Displayed
|
||||
print(f"[JOURNAL] CheckScope: Displayed")
|
||||
|
||||
# Identify duplicates
|
||||
print(f"[JOURNAL] Identifying duplicate nodes...")
|
||||
numDuplicates = duplicateNodesCheckBuilder1.IdentifyDuplicateNodes()
|
||||
print(f"[JOURNAL] Found {numDuplicates} duplicate node sets")
|
||||
|
||||
# Merge duplicates
|
||||
if numDuplicates > 0:
|
||||
print(f"[JOURNAL] Merging duplicate nodes...")
|
||||
numMerged = duplicateNodesCheckBuilder1.MergeDuplicateNodes()
|
||||
print(f"[JOURNAL] Merged {numMerged} duplicate node sets")
|
||||
else:
|
||||
print(f"[JOURNAL] WARNING: No duplicate nodes found to merge!")
|
||||
print(f"[JOURNAL] This may indicate mesh update didn't work properly")
|
||||
|
||||
theSession.SetUndoMarkName(markId_merge, "Duplicate Nodes")
|
||||
duplicateNodesCheckBuilder1.Destroy()
|
||||
theSession.DeleteUndoMark(markId_merge, None)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Node merge: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 5: RESOLVE LABEL CONFLICTS
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 5: Resolving label conflicts...")
|
||||
|
||||
try:
|
||||
markId_labels = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
|
||||
|
||||
assyFemPart1 = workAssyFemPart
|
||||
assemblyLabelManagerBuilder1 = assyFemPart1.CreateAssemblyLabelManagerBuilder()
|
||||
|
||||
theSession.SetUndoMarkName(markId_labels, "Assembly Label Manager Dialog")
|
||||
|
||||
markId_labels2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Assembly Label Manager")
|
||||
|
||||
# Set offsets for each FE model occurrence
|
||||
# These offsets ensure unique node/element labels across components
|
||||
entitytypes = [
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Node,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Element,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Csys,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Physical,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Group,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Ply,
|
||||
NXOpen.CAE.AssemblyLabelManagerBuilder.EntityType.Ssmo,
|
||||
]
|
||||
|
||||
# Apply offsets to each occurrence (values from recorded journal)
|
||||
occurrence_offsets = [
|
||||
("FEModelOccurrence[3]", 2),
|
||||
("FEModelOccurrence[4]", 74),
|
||||
("FEModelOccurrence[5]", 146),
|
||||
("FEModelOccurrence[7]", 218),
|
||||
]
|
||||
|
||||
for occ_name, offset_val in occurrence_offsets:
|
||||
try:
|
||||
fEModelOcc = workAssyFemPart.FindObject(occ_name)
|
||||
offsets = [offset_val] * 7
|
||||
assemblyLabelManagerBuilder1.SetFEModelOccOffsets(fEModelOcc, entitytypes, offsets)
|
||||
except:
|
||||
pass # Some occurrences may not exist
|
||||
|
||||
nXObject1 = assemblyLabelManagerBuilder1.Commit()
|
||||
|
||||
theSession.DeleteUndoMark(markId_labels2, None)
|
||||
theSession.SetUndoMarkName(markId_labels, "Assembly Label Manager")
|
||||
assemblyLabelManagerBuilder1.Destroy()
|
||||
|
||||
print(f"[JOURNAL] Label conflicts resolved")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Label management: {e}")
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 6: SOLVE
|
||||
# ==========================================================================
|
||||
print(f"[JOURNAL] STEP 6: Solving simulation...")
|
||||
|
||||
try:
|
||||
# Return to sim level by setting null component
|
||||
partLoadStatus9 = theSession.Parts.SetWorkComponent(
|
||||
NXOpen.Assemblies.Component.Null,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
)
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
partLoadStatus9.Dispose()
|
||||
|
||||
# Set up solve
|
||||
markId_solve = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
|
||||
theSession.SetUndoMarkName(markId_solve, "Solve Dialog")
|
||||
|
||||
markId_solve2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
|
||||
|
||||
theCAESimSolveManager = NXOpen.CAE.SimSolveManager.GetSimSolveManager(theSession)
|
||||
|
||||
simSimulation1 = workSimPart.FindObject("Simulation")
|
||||
sol_name = solution_name if solution_name else "Solution 1"
|
||||
simSolution1 = simSimulation1.FindObject(f"Solution[{sol_name}]")
|
||||
|
||||
psolutions1 = [simSolution1]
|
||||
|
||||
print(f"[JOURNAL] Solving: {sol_name} (Foreground mode)")
|
||||
numsolved, numfailed, numskipped = theCAESimSolveManager.SolveChainOfSolutions(
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to ensure OP2 is complete
|
||||
)
|
||||
|
||||
theSession.DeleteUndoMark(markId_solve2, None)
|
||||
theSession.SetUndoMarkName(markId_solve, "Solve")
|
||||
|
||||
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
|
||||
|
||||
return numfailed == 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR solving: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
|
||||
"""
|
||||
Simple workflow for single-part simulations or when no expression updates needed.
|
||||
"""
|
||||
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
|
||||
|
||||
# Open the .sim file
|
||||
basePart1, partLoadStatus1 = theSession.Parts.OpenActiveDisplay(
|
||||
sim_file_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus1.Dispose()
|
||||
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
|
||||
|
||||
# Set up solve
|
||||
markId_solve = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
|
||||
theSession.SetUndoMarkName(markId_solve, "Solve Dialog")
|
||||
|
||||
markId_solve2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Solve")
|
||||
|
||||
theCAESimSolveManager = NXOpen.CAE.SimSolveManager.GetSimSolveManager(theSession)
|
||||
|
||||
simSimulation1 = workSimPart.FindObject("Simulation")
|
||||
sol_name = solution_name if solution_name else "Solution 1"
|
||||
simSolution1 = simSimulation1.FindObject(f"Solution[{sol_name}]")
|
||||
|
||||
psolutions1 = [simSolution1]
|
||||
|
||||
print(f"[JOURNAL] Solving: {sol_name}")
|
||||
numsolved, numfailed, numskipped = theCAESimSolveManager.SolveChainOfSolutions(
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Background
|
||||
)
|
||||
|
||||
theSession.DeleteUndoMark(markId_solve2, None)
|
||||
theSession.SetUndoMarkName(markId_solve, "Solve")
|
||||
|
||||
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
|
||||
|
||||
# Save
|
||||
try:
|
||||
partSaveStatus = workSimPart.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue,
|
||||
NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
partSaveStatus.Dispose()
|
||||
print(f"[JOURNAL] Saved!")
|
||||
except:
|
||||
pass
|
||||
|
||||
return numfailed == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = main(sys.argv[1:])
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
Reference in New Issue
Block a user