diff --git a/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-152342.py b/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-152342.py deleted file mode 100755 index 60421634..00000000 --- a/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-152342.py +++ /dev/null @@ -1,1126 +0,0 @@ -# Auto-generated journal for solving Beam_sim1.sim -import sys -sys.argv = ['', r'C:\Users\antoi\Atomizer\projects\hydrotech-beam\studies\01_doe_landscape\..\..\models\Beam_sim1.sim', None, 'beam_half_core_thickness=16.7813185433211', 'beam_face_thickness=26.83364680743843', 'holes_diameter=192.42062402658527', 'hole_count=8.0'] # Set argv for the main function -""" -NX Journal Script to Solve Simulation in Batch Mode - -This script handles BOTH single-part simulations AND multi-part assembly FEMs. - -============================================================================= -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 - - -def extract_part_mass(theSession, part, output_dir): - """ - Extract mass from a part using NX MeasureManager. - - Writes mass to _temp_mass.txt and _temp_part_properties.json in output_dir. - - Args: - theSession: NXOpen.Session - part: NXOpen.Part to extract mass from - output_dir: Directory to write temp files - - Returns: - Mass in kg (float) - """ - import json - - results = { - "part_file": part.Name, - "mass_kg": 0.0, - "mass_g": 0.0, - "volume_mm3": 0.0, - "surface_area_mm2": 0.0, - "center_of_gravity_mm": [0.0, 0.0, 0.0], - "num_bodies": 0, - "success": False, - "error": None, - } - - try: - # Get all solid bodies - bodies = [] - for body in part.Bodies: - if body.IsSolidBody: - bodies.append(body) - - results["num_bodies"] = len(bodies) - - if not bodies: - results["error"] = "No solid bodies found" - raise ValueError("No solid bodies found in part") - - # Get the measure manager - measureManager = part.MeasureManager - - # Get unit collection and build mass_units array - # API requires: [Area, Volume, Mass, Length] base units - uc = part.UnitCollection - mass_units = [ - uc.GetBase("Area"), - uc.GetBase("Volume"), - uc.GetBase("Mass"), - uc.GetBase("Length"), - ] - - # Create mass properties measurement - measureBodies = measureManager.NewMassProperties(mass_units, 0.99, bodies) - - if measureBodies: - results["mass_kg"] = measureBodies.Mass - results["mass_g"] = results["mass_kg"] * 1000.0 - - try: - results["volume_mm3"] = measureBodies.Volume - except: - pass - - try: - results["surface_area_mm2"] = measureBodies.Area - except: - pass - - try: - cog = measureBodies.Centroid - if cog: - results["center_of_gravity_mm"] = [cog.X, cog.Y, cog.Z] - except: - pass - - try: - measureBodies.Dispose() - except: - pass - - results["success"] = True - - except Exception as e: - results["error"] = str(e) - results["success"] = False - - # Write results to JSON file - output_file = os.path.join(output_dir, "_temp_part_properties.json") - with open(output_file, "w") as f: - json.dump(results, f, indent=2) - - # Write simple mass value for backward compatibility - mass_file = os.path.join(output_dir, "_temp_mass.txt") - with open(mass_file, "w") as f: - f.write(str(results["mass_kg"])) - - if not results["success"]: - raise ValueError(results["error"]) - - return results["mass_kg"] - - -def find_or_open_part(theSession, part_path): - """ - Find a part if already loaded, otherwise open it. - In NX, calling Parts.Open() on an already-loaded part raises 'File already exists'. - """ - part_name = os.path.splitext(os.path.basename(part_path))[0] - - # Try to find in already-loaded parts - for part in theSession.Parts: - if part.Name == part_name: - return part, True - try: - if part.FullPath and part.FullPath.lower() == part_path.lower(): - return part, True - except: - pass - - # Not found, open it - markId = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, f"Load {part_name}") - part, partLoadStatus = theSession.Parts.Open(part_path) - partLoadStatus.Dispose() - return part, False - - -def main(args): - """ - Main entry point for NX journal. - - Args: - args: Command line arguments - args[0]: .sim file path - 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 [solution_name] [expr1=val1] ..." - ) - return False - - sim_file_path = args[0] - solution_name = args[1] if len(args) > 1 and args[1] != "None" else None - - # Parse expression updates - expression_updates = {} - for arg in args[2:]: - if "=" in arg: - name, value = arg.split("=", 1) - expression_updates[name] = float(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 - theSession.Parts.LoadOptions.LoadLatest = False - theSession.Parts.LoadOptions.ComponentLoadMethod = ( - NXOpen.LoadOptions.LoadMethod.FromDirectory - ) - 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.AbortOnFailure = False - - # Close any open parts - try: - theSession.Parts.CloseAll([NXOpen.BasePart.CloseWholeTree]) - except: - pass - - # 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 - - 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: - 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 - ) - - except Exception as 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 SIM FILE FIRST (loads entire assembly hierarchy) - # ========================================================================== - print(f"[JOURNAL] STEP 1: Loading SIM file and all components...") - - # CRITICAL: Open the SIM file FIRST using OpenActiveDisplay - # This loads the entire assembly FEM hierarchy (.afm, .fem, associated .prt files) - # The sim file knows its component structure and will load everything it needs - sim_file_full_path = os.path.join(working_dir, sim_filename) - print(f"[JOURNAL] Opening SIM file: {sim_filename}") - basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay( - sim_file_full_path, NXOpen.DisplayPartOption.AllowAdditional - ) - partLoadStatus.Dispose() - - workSimPart = theSession.Parts.BaseWork - displaySimPart = theSession.Parts.BaseDisplay - print(f"[JOURNAL] SIM loaded: {workSimPart.Name}") - - # List loaded parts - print(f"[JOURNAL] Currently loaded parts:") - for part in theSession.Parts: - print(f"[JOURNAL] - {part.Name}") - - # ========================================================================== - # STEP 1b: LOAD GEOMETRY PARTS FOR EXPRESSION EDITING - # ========================================================================== - print(f"[JOURNAL] STEP 1b: Loading geometry parts for expression editing...") - - # The recorded journal loads these geometry parts explicitly: - # 1. ASSY_M1.prt - the main geometry assembly - # 2. M1_Blank_fem1_i.prt - idealized geometry for M1_Blank FEM - # 3. M1_Vertical_Support_Skeleton_fem1_i.prt - idealized geometry for support FEM - - # 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...") - part1, was_loaded = find_or_open_part(theSession, assy_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: ASSY_M1.prt not found!") - - # Load M1_Blank_fem1_i.prt (idealized geometry for M1_Blank) - 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...") - part2, was_loaded = find_or_open_part(theSession, idealized_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: M1_Blank_fem1_i.prt not found!") - - # Load M1_Vertical_Support_Skeleton_fem1_i.prt (CRITICAL: idealized geometry for support) - skeleton_idealized_prt_path = os.path.join( - working_dir, "M1_Vertical_Support_Skeleton_fem1_i.prt" - ) - if os.path.exists(skeleton_idealized_prt_path): - print(f"[JOURNAL] Loading M1_Vertical_Support_Skeleton_fem1_i.prt...") - part3_skel, was_loaded = find_or_open_part(theSession, skeleton_idealized_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: M1_Vertical_Support_Skeleton_fem1_i.prt not found!") - - # ========================================================================== - # STEP 2: UPDATE EXPRESSIONS IN M1_BLANK AND REBUILD ALL GEOMETRY - # ========================================================================== - 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 M1_Blank 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] M1_Blank geometry rebuilt ({nErrs} errors)") - - # CRITICAL: Save M1_Blank after geometry update so FEM can read updated geometry - print(f"[JOURNAL] Saving M1_Blank...") - partSaveStatus_blank = workPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_blank.Dispose() - print(f"[JOURNAL] M1_Blank saved") - - # STEP 2a: EXTRACT MASS FROM M1_BLANK - # Extract mass using MeasureManager after geometry is updated - print(f"[JOURNAL] Extracting mass from M1_Blank...") - try: - mass_kg = extract_part_mass(theSession, workPart, working_dir) - print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)") - except Exception as mass_err: - print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}") - - 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 2b: UPDATE ALL LINKED GEOMETRY PARTS - # ========================================================================== - # CRITICAL: Must update ALL geometry parts that have linked expressions - # before updating FEMs, otherwise interface nodes won't be coincident! - print(f"[JOURNAL] STEP 2b: Updating all linked geometry parts...") - - # List of geometry parts that may have linked expressions from M1_Blank - linked_geometry_parts = [ - "M1_Vertical_Support_Skeleton", - # Add more parts here if the assembly has additional linked geometry - ] - - for part_name in linked_geometry_parts: - try: - print(f"[JOURNAL] Updating {part_name}...") - linked_part = theSession.Parts.FindObject(part_name) - - markId_linked = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Visible, f"Update {part_name}" - ) - status_linked, partLoadStatus_linked = theSession.Parts.SetActiveDisplay( - linked_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus_linked.Dispose() - - # Switch to modeling application - theSession.ApplicationSwitchImmediate("UG_APP_MODELING") - - # Update to propagate linked expression changes - markId_linked_update = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Invisible, "NX update" - ) - nErrs_linked = theSession.UpdateManager.DoUpdate(markId_linked_update) - theSession.DeleteUndoMark(markId_linked_update, "NX update") - print(f"[JOURNAL] {part_name} geometry rebuilt ({nErrs_linked} errors)") - - # CRITICAL: Save part after geometry update so FEM can read updated geometry - print(f"[JOURNAL] Saving {part_name}...") - partSaveStatus_linked = linked_part.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_linked.Dispose() - print(f"[JOURNAL] {part_name} saved") - - except Exception as e: - print(f"[JOURNAL] WARNING: Could not update {part_name}: {e}") - print(f"[JOURNAL] (Part may not exist in this assembly)") - - # ========================================================================== - # 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") - - # CRITICAL: Save FEM file after update to persist mesh changes - print(f"[JOURNAL] Saving M1_Blank_fem1...") - partSaveStatus_fem1 = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_fem1.Dispose() - print(f"[JOURNAL] M1_Blank_fem1 saved") - 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") - - # CRITICAL: Save FEM file after update to persist mesh changes - print(f"[JOURNAL] Saving M1_Vertical_Support_Skeleton_fem1...") - partSaveStatus_fem2 = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_fem2.Dispose() - print(f"[JOURNAL] M1_Vertical_Support_Skeleton_fem1 saved") - 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") - - # WORKAROUND: Force display refresh before duplicate node check - # The recorded journal does zoom operations before checking - this may - # be needed to refresh the internal mesh representation - try: - displaySimPart.ModelingViews.WorkView.Fit() - print(f"[JOURNAL] Forced view Fit() to refresh display") - except Exception as fit_err: - print(f"[JOURNAL] View Fit() failed (non-critical): {fit_err}") - - 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] IdentifyDuplicateNodes returned: {numDuplicates}") - - # WORKAROUND: In batch mode, IdentifyDuplicateNodes() often returns None - # even when duplicates exist. The recorded NX journal doesn't check the - # return value - it just calls MergeDuplicateNodes unconditionally. - # So we do the same: always attempt to merge. - print(f"[JOURNAL] Attempting to merge duplicate nodes...") - try: - numMerged = duplicateNodesCheckBuilder1.MergeDuplicateNodes() - print(f"[JOURNAL] MergeDuplicateNodes returned: {numMerged}") - if numMerged is not None and numMerged > 0: - print(f"[JOURNAL] Successfully merged {numMerged} duplicate node sets") - elif numMerged == 0: - print(f"[JOURNAL] No nodes were merged (0 returned)") - if numDuplicates is None: - print( - f"[JOURNAL] WARNING: IdentifyDuplicateNodes returned None - mesh may need display refresh" - ) - else: - print(f"[JOURNAL] MergeDuplicateNodes returned None - batch mode limitation") - except Exception as merge_error: - print(f"[JOURNAL] MergeDuplicateNodes failed: {merge_error}") - if numDuplicates is None: - print( - f"[JOURNAL] This combined with IdentifyDuplicateNodes=None suggests display issue" - ) - - 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 5b: SAVE ASSEMBLY FEM - # ========================================================================== - print(f"[JOURNAL] STEP 5b: Saving assembly FEM after all updates...") - try: - # Save the assembly FEM to persist all mesh updates and node merges - partSaveStatus_afem = workAssyFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_afem.Dispose() - print(f"[JOURNAL] Assembly FEM saved: {workAssyFemPart.Name}") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not save assembly FEM: {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" - ) - - # ========================================================================== - # STEP 7: SAVE ALL - Save all modified parts (FEM, SIM, PRT) - # ========================================================================== - print(f"[JOURNAL] STEP 7: Saving all modified parts...") - try: - anyPartsModified, partSaveStatus_all = theSession.Parts.SaveAll() - partSaveStatus_all.Dispose() - print(f"[JOURNAL] SaveAll completed (parts modified: {anyPartsModified})") - except Exception as e: - print(f"[JOURNAL] WARNING: SaveAll failed: {e}") - - 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 -): - """ - Workflow for single-part simulations with optional expression updates. - - For single-part FEMs (Bracket.prt -> Bracket_fem1.fem -> Bracket_sim1.sim): - 1. Open the .sim file (this loads .fem and .prt) - 2. If expression_updates: find the geometry .prt, update expressions, rebuild - 3. Update the FEM mesh - 4. Solve - """ - 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 - - # ========================================================================= - # STEP 1: UPDATE EXPRESSIONS IN GEOMETRY PART (if any) - # ========================================================================= - if expression_updates: - print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...") - - # List all loaded parts for debugging - print(f"[JOURNAL] Currently loaded parts:") - for part in theSession.Parts: - print(f"[JOURNAL] - {part.Name} (type: {type(part).__name__})") - - # NX doesn't automatically load the geometry .prt when opening a SIM file - # We need to find and load it explicitly from the working directory - geom_part = None - - # First, try to find an already loaded geometry part - for part in theSession.Parts: - part_name = part.Name.lower() - part_type = type(part).__name__ - - # Skip FEM and SIM parts by type - if "fem" in part_type.lower() or "sim" in part_type.lower(): - continue - - # Skip parts with _fem or _sim in name - if "_fem" in part_name or "_sim" in part_name: - continue - - geom_part = part - print(f"[JOURNAL] Found geometry part (already loaded): {part.Name}") - break - - # If not found, try to load the geometry .prt file from working directory - if geom_part is None: - print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...") - for filename in os.listdir(working_dir): - # Skip idealized parts (_i.prt), FEM parts, and SIM parts - if ( - filename.endswith(".prt") - and "_fem" not in filename.lower() - and "_sim" not in filename.lower() - and "_i.prt" not in filename.lower() - ): - prt_path = os.path.join(working_dir, filename) - print(f"[JOURNAL] Loading geometry part: {filename}") - try: - loaded_part, partLoadStatus = theSession.Parts.Open(prt_path) - partLoadStatus.Dispose() - # Check if load actually succeeded (Parts.Open can return None) - if loaded_part is not None: - geom_part = loaded_part - print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}") - break - else: - print(f"[JOURNAL] WARNING: Parts.Open returned None for {filename}") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not load {filename}: {e}") - - if geom_part: - try: - # Switch to the geometry part for expression editing - markId_expr = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Visible, "Update Expressions" - ) - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - geom_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus.Dispose() - - # Switch to modeling application for expression editing - theSession.ApplicationSwitchImmediate("UG_APP_MODELING") - - workPart = theSession.Parts.Work - - # Write expressions to temp file and import - 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 based on name - if "angle" 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...") - expModified, errorMessages = workPart.Expressions.ImportFromFile( - exp_file_path, NXOpen.ExpressionCollection.ImportMode.Replace - ) - print(f"[JOURNAL] Expressions modified: {expModified}") - if errorMessages: - print(f"[JOURNAL] Import messages: {errorMessages}") - - # Update geometry - 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)") - - # Save geometry part - print(f"[JOURNAL] Saving geometry part...") - partSaveStatus_geom = workPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, - NXOpen.BasePart.CloseAfterSave.FalseValue, - ) - partSaveStatus_geom.Dispose() - - # Clean up temp file - try: - os.remove(exp_file_path) - except: - pass - - except Exception as e: - print(f"[JOURNAL] ERROR updating expressions: {e}") - import traceback - - traceback.print_exc() - else: - print(f"[JOURNAL] WARNING: Could not find geometry part for expression updates!") - - # ========================================================================= - # STEP 2: UPDATE FEM MESH (if expressions were updated) - # ========================================================================= - if expression_updates: - print(f"[JOURNAL] STEP 2: Updating FEM mesh...") - - # First, load the idealized part if it exists (required for mesh update chain) - # The chain is: .prt (geometry) -> _i.prt (idealized) -> .fem (mesh) - idealized_part = None - for filename in os.listdir(working_dir): - if "_i.prt" in filename.lower(): - idealized_path = os.path.join(working_dir, filename) - print(f"[JOURNAL] Loading idealized part: {filename}") - try: - loaded_part, partLoadStatus = theSession.Parts.Open(idealized_path) - partLoadStatus.Dispose() - # Check if load actually succeeded (Parts.Open can return None) - if loaded_part is not None: - idealized_part = loaded_part - print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}") - else: - print(f"[JOURNAL] WARNING: Parts.Open returned None for idealized part") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not load idealized part: {e}") - break - - # Find the FEM part - fem_part = None - for part in theSession.Parts: - if "_fem" in part.Name.lower() or part.Name.lower().endswith(".fem"): - fem_part = part - print(f"[JOURNAL] Found FEM part: {part.Name}") - break - - if fem_part: - try: - # Switch to FEM part - CRITICAL: Use SameAsDisplay to make FEM the work part - # This is required for UpdateFemodel() to properly regenerate the mesh - # Reference: tests/journal_with_regenerate.py line 76 - print(f"[JOURNAL] Switching to FEM part: {fem_part.Name}") - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - fem_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay, # Critical fix! - ) - partLoadStatus.Dispose() - - # Switch to FEM application - theSession.ApplicationSwitchImmediate("UG_APP_SFEM") - - # Update the FE model - workFemPart = theSession.Parts.BaseWork - feModel = workFemPart.FindObject("FEModel") - - print(f"[JOURNAL] Updating FE model...") - feModel.UpdateFemodel() - print(f"[JOURNAL] FE model updated") - - # Save FEM - partSaveStatus_fem = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, - NXOpen.BasePart.CloseAfterSave.FalseValue, - ) - partSaveStatus_fem.Dispose() - print(f"[JOURNAL] FEM saved") - - except Exception as e: - print(f"[JOURNAL] ERROR updating FEM: {e}") - import traceback - - traceback.print_exc() - - # ========================================================================= - # STEP 3: SWITCH BACK TO SIM AND SOLVE - # ========================================================================= - print(f"[JOURNAL] STEP 3: Solving simulation...") - - # Switch back to sim part - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - workSimPart, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus.Dispose() - - 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.Foreground, # Use Foreground to wait for completion - ) - - theSession.DeleteUndoMark(markId_solve2, None) - theSession.SetUndoMarkName(markId_solve, "Solve") - - print( - f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped" - ) - - # Save all - try: - anyPartsModified, partSaveStatus = theSession.Parts.SaveAll() - partSaveStatus.Dispose() - print(f"[JOURNAL] Saved all parts!") - except: - pass - - return numfailed == 0 - - -if __name__ == "__main__": - success = main(sys.argv[1:]) - # NOTE: Do NOT use sys.exit() here! - # run_journal.exe treats SystemExit (even code 0) as "Syntax errors" - # and returns a non-zero exit code, which makes the solver think - # the journal crashed. Instead, we just let the script end naturally. - # The solver checks for output files (.op2, .f06) to determine success. - if not success: - print("[JOURNAL] FAILED - solve did not complete successfully") - diff --git a/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-154152.py b/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-154152.py deleted file mode 100755 index 8f3bae0d..00000000 --- a/projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-154152.py +++ /dev/null @@ -1,1126 +0,0 @@ -# Auto-generated journal for solving Beam_sim1.sim -import sys -sys.argv = ['', r'C:\Users\antoi\Atomizer\projects\hydrotech-beam\studies\01_doe_landscape\..\..\models\Beam_sim1.sim', None, 'beam_half_core_thickness=25.162', 'beam_face_thickness=21.504', 'holes_diameter=300.0', 'hole_count=10.0'] # Set argv for the main function -""" -NX Journal Script to Solve Simulation in Batch Mode - -This script handles BOTH single-part simulations AND multi-part assembly FEMs. - -============================================================================= -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 - - -def extract_part_mass(theSession, part, output_dir): - """ - Extract mass from a part using NX MeasureManager. - - Writes mass to _temp_mass.txt and _temp_part_properties.json in output_dir. - - Args: - theSession: NXOpen.Session - part: NXOpen.Part to extract mass from - output_dir: Directory to write temp files - - Returns: - Mass in kg (float) - """ - import json - - results = { - "part_file": part.Name, - "mass_kg": 0.0, - "mass_g": 0.0, - "volume_mm3": 0.0, - "surface_area_mm2": 0.0, - "center_of_gravity_mm": [0.0, 0.0, 0.0], - "num_bodies": 0, - "success": False, - "error": None, - } - - try: - # Get all solid bodies - bodies = [] - for body in part.Bodies: - if body.IsSolidBody: - bodies.append(body) - - results["num_bodies"] = len(bodies) - - if not bodies: - results["error"] = "No solid bodies found" - raise ValueError("No solid bodies found in part") - - # Get the measure manager - measureManager = part.MeasureManager - - # Get unit collection and build mass_units array - # API requires: [Area, Volume, Mass, Length] base units - uc = part.UnitCollection - mass_units = [ - uc.GetBase("Area"), - uc.GetBase("Volume"), - uc.GetBase("Mass"), - uc.GetBase("Length"), - ] - - # Create mass properties measurement - measureBodies = measureManager.NewMassProperties(mass_units, 0.99, bodies) - - if measureBodies: - results["mass_kg"] = measureBodies.Mass - results["mass_g"] = results["mass_kg"] * 1000.0 - - try: - results["volume_mm3"] = measureBodies.Volume - except: - pass - - try: - results["surface_area_mm2"] = measureBodies.Area - except: - pass - - try: - cog = measureBodies.Centroid - if cog: - results["center_of_gravity_mm"] = [cog.X, cog.Y, cog.Z] - except: - pass - - try: - measureBodies.Dispose() - except: - pass - - results["success"] = True - - except Exception as e: - results["error"] = str(e) - results["success"] = False - - # Write results to JSON file - output_file = os.path.join(output_dir, "_temp_part_properties.json") - with open(output_file, "w") as f: - json.dump(results, f, indent=2) - - # Write simple mass value for backward compatibility - mass_file = os.path.join(output_dir, "_temp_mass.txt") - with open(mass_file, "w") as f: - f.write(str(results["mass_kg"])) - - if not results["success"]: - raise ValueError(results["error"]) - - return results["mass_kg"] - - -def find_or_open_part(theSession, part_path): - """ - Find a part if already loaded, otherwise open it. - In NX, calling Parts.Open() on an already-loaded part raises 'File already exists'. - """ - part_name = os.path.splitext(os.path.basename(part_path))[0] - - # Try to find in already-loaded parts - for part in theSession.Parts: - if part.Name == part_name: - return part, True - try: - if part.FullPath and part.FullPath.lower() == part_path.lower(): - return part, True - except: - pass - - # Not found, open it - markId = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, f"Load {part_name}") - part, partLoadStatus = theSession.Parts.Open(part_path) - partLoadStatus.Dispose() - return part, False - - -def main(args): - """ - Main entry point for NX journal. - - Args: - args: Command line arguments - args[0]: .sim file path - 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 [solution_name] [expr1=val1] ..." - ) - return False - - sim_file_path = args[0] - solution_name = args[1] if len(args) > 1 and args[1] != "None" else None - - # Parse expression updates - expression_updates = {} - for arg in args[2:]: - if "=" in arg: - name, value = arg.split("=", 1) - expression_updates[name] = float(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 - theSession.Parts.LoadOptions.LoadLatest = False - theSession.Parts.LoadOptions.ComponentLoadMethod = ( - NXOpen.LoadOptions.LoadMethod.FromDirectory - ) - 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.AbortOnFailure = False - - # Close any open parts - try: - theSession.Parts.CloseAll([NXOpen.BasePart.CloseWholeTree]) - except: - pass - - # 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 - - 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: - 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 - ) - - except Exception as 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 SIM FILE FIRST (loads entire assembly hierarchy) - # ========================================================================== - print(f"[JOURNAL] STEP 1: Loading SIM file and all components...") - - # CRITICAL: Open the SIM file FIRST using OpenActiveDisplay - # This loads the entire assembly FEM hierarchy (.afm, .fem, associated .prt files) - # The sim file knows its component structure and will load everything it needs - sim_file_full_path = os.path.join(working_dir, sim_filename) - print(f"[JOURNAL] Opening SIM file: {sim_filename}") - basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay( - sim_file_full_path, NXOpen.DisplayPartOption.AllowAdditional - ) - partLoadStatus.Dispose() - - workSimPart = theSession.Parts.BaseWork - displaySimPart = theSession.Parts.BaseDisplay - print(f"[JOURNAL] SIM loaded: {workSimPart.Name}") - - # List loaded parts - print(f"[JOURNAL] Currently loaded parts:") - for part in theSession.Parts: - print(f"[JOURNAL] - {part.Name}") - - # ========================================================================== - # STEP 1b: LOAD GEOMETRY PARTS FOR EXPRESSION EDITING - # ========================================================================== - print(f"[JOURNAL] STEP 1b: Loading geometry parts for expression editing...") - - # The recorded journal loads these geometry parts explicitly: - # 1. ASSY_M1.prt - the main geometry assembly - # 2. M1_Blank_fem1_i.prt - idealized geometry for M1_Blank FEM - # 3. M1_Vertical_Support_Skeleton_fem1_i.prt - idealized geometry for support FEM - - # 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...") - part1, was_loaded = find_or_open_part(theSession, assy_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: ASSY_M1.prt not found!") - - # Load M1_Blank_fem1_i.prt (idealized geometry for M1_Blank) - 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...") - part2, was_loaded = find_or_open_part(theSession, idealized_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: M1_Blank_fem1_i.prt not found!") - - # Load M1_Vertical_Support_Skeleton_fem1_i.prt (CRITICAL: idealized geometry for support) - skeleton_idealized_prt_path = os.path.join( - working_dir, "M1_Vertical_Support_Skeleton_fem1_i.prt" - ) - if os.path.exists(skeleton_idealized_prt_path): - print(f"[JOURNAL] Loading M1_Vertical_Support_Skeleton_fem1_i.prt...") - part3_skel, was_loaded = find_or_open_part(theSession, skeleton_idealized_prt_path) - if was_loaded: - print(f"[JOURNAL] (already loaded)") - else: - print(f"[JOURNAL] WARNING: M1_Vertical_Support_Skeleton_fem1_i.prt not found!") - - # ========================================================================== - # STEP 2: UPDATE EXPRESSIONS IN M1_BLANK AND REBUILD ALL GEOMETRY - # ========================================================================== - 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 M1_Blank 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] M1_Blank geometry rebuilt ({nErrs} errors)") - - # CRITICAL: Save M1_Blank after geometry update so FEM can read updated geometry - print(f"[JOURNAL] Saving M1_Blank...") - partSaveStatus_blank = workPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_blank.Dispose() - print(f"[JOURNAL] M1_Blank saved") - - # STEP 2a: EXTRACT MASS FROM M1_BLANK - # Extract mass using MeasureManager after geometry is updated - print(f"[JOURNAL] Extracting mass from M1_Blank...") - try: - mass_kg = extract_part_mass(theSession, workPart, working_dir) - print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)") - except Exception as mass_err: - print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}") - - 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 2b: UPDATE ALL LINKED GEOMETRY PARTS - # ========================================================================== - # CRITICAL: Must update ALL geometry parts that have linked expressions - # before updating FEMs, otherwise interface nodes won't be coincident! - print(f"[JOURNAL] STEP 2b: Updating all linked geometry parts...") - - # List of geometry parts that may have linked expressions from M1_Blank - linked_geometry_parts = [ - "M1_Vertical_Support_Skeleton", - # Add more parts here if the assembly has additional linked geometry - ] - - for part_name in linked_geometry_parts: - try: - print(f"[JOURNAL] Updating {part_name}...") - linked_part = theSession.Parts.FindObject(part_name) - - markId_linked = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Visible, f"Update {part_name}" - ) - status_linked, partLoadStatus_linked = theSession.Parts.SetActiveDisplay( - linked_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus_linked.Dispose() - - # Switch to modeling application - theSession.ApplicationSwitchImmediate("UG_APP_MODELING") - - # Update to propagate linked expression changes - markId_linked_update = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Invisible, "NX update" - ) - nErrs_linked = theSession.UpdateManager.DoUpdate(markId_linked_update) - theSession.DeleteUndoMark(markId_linked_update, "NX update") - print(f"[JOURNAL] {part_name} geometry rebuilt ({nErrs_linked} errors)") - - # CRITICAL: Save part after geometry update so FEM can read updated geometry - print(f"[JOURNAL] Saving {part_name}...") - partSaveStatus_linked = linked_part.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_linked.Dispose() - print(f"[JOURNAL] {part_name} saved") - - except Exception as e: - print(f"[JOURNAL] WARNING: Could not update {part_name}: {e}") - print(f"[JOURNAL] (Part may not exist in this assembly)") - - # ========================================================================== - # 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") - - # CRITICAL: Save FEM file after update to persist mesh changes - print(f"[JOURNAL] Saving M1_Blank_fem1...") - partSaveStatus_fem1 = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_fem1.Dispose() - print(f"[JOURNAL] M1_Blank_fem1 saved") - 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") - - # CRITICAL: Save FEM file after update to persist mesh changes - print(f"[JOURNAL] Saving M1_Vertical_Support_Skeleton_fem1...") - partSaveStatus_fem2 = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_fem2.Dispose() - print(f"[JOURNAL] M1_Vertical_Support_Skeleton_fem1 saved") - 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") - - # WORKAROUND: Force display refresh before duplicate node check - # The recorded journal does zoom operations before checking - this may - # be needed to refresh the internal mesh representation - try: - displaySimPart.ModelingViews.WorkView.Fit() - print(f"[JOURNAL] Forced view Fit() to refresh display") - except Exception as fit_err: - print(f"[JOURNAL] View Fit() failed (non-critical): {fit_err}") - - 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] IdentifyDuplicateNodes returned: {numDuplicates}") - - # WORKAROUND: In batch mode, IdentifyDuplicateNodes() often returns None - # even when duplicates exist. The recorded NX journal doesn't check the - # return value - it just calls MergeDuplicateNodes unconditionally. - # So we do the same: always attempt to merge. - print(f"[JOURNAL] Attempting to merge duplicate nodes...") - try: - numMerged = duplicateNodesCheckBuilder1.MergeDuplicateNodes() - print(f"[JOURNAL] MergeDuplicateNodes returned: {numMerged}") - if numMerged is not None and numMerged > 0: - print(f"[JOURNAL] Successfully merged {numMerged} duplicate node sets") - elif numMerged == 0: - print(f"[JOURNAL] No nodes were merged (0 returned)") - if numDuplicates is None: - print( - f"[JOURNAL] WARNING: IdentifyDuplicateNodes returned None - mesh may need display refresh" - ) - else: - print(f"[JOURNAL] MergeDuplicateNodes returned None - batch mode limitation") - except Exception as merge_error: - print(f"[JOURNAL] MergeDuplicateNodes failed: {merge_error}") - if numDuplicates is None: - print( - f"[JOURNAL] This combined with IdentifyDuplicateNodes=None suggests display issue" - ) - - 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 5b: SAVE ASSEMBLY FEM - # ========================================================================== - print(f"[JOURNAL] STEP 5b: Saving assembly FEM after all updates...") - try: - # Save the assembly FEM to persist all mesh updates and node merges - partSaveStatus_afem = workAssyFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue - ) - partSaveStatus_afem.Dispose() - print(f"[JOURNAL] Assembly FEM saved: {workAssyFemPart.Name}") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not save assembly FEM: {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" - ) - - # ========================================================================== - # STEP 7: SAVE ALL - Save all modified parts (FEM, SIM, PRT) - # ========================================================================== - print(f"[JOURNAL] STEP 7: Saving all modified parts...") - try: - anyPartsModified, partSaveStatus_all = theSession.Parts.SaveAll() - partSaveStatus_all.Dispose() - print(f"[JOURNAL] SaveAll completed (parts modified: {anyPartsModified})") - except Exception as e: - print(f"[JOURNAL] WARNING: SaveAll failed: {e}") - - 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 -): - """ - Workflow for single-part simulations with optional expression updates. - - For single-part FEMs (Bracket.prt -> Bracket_fem1.fem -> Bracket_sim1.sim): - 1. Open the .sim file (this loads .fem and .prt) - 2. If expression_updates: find the geometry .prt, update expressions, rebuild - 3. Update the FEM mesh - 4. Solve - """ - 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 - - # ========================================================================= - # STEP 1: UPDATE EXPRESSIONS IN GEOMETRY PART (if any) - # ========================================================================= - if expression_updates: - print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...") - - # List all loaded parts for debugging - print(f"[JOURNAL] Currently loaded parts:") - for part in theSession.Parts: - print(f"[JOURNAL] - {part.Name} (type: {type(part).__name__})") - - # NX doesn't automatically load the geometry .prt when opening a SIM file - # We need to find and load it explicitly from the working directory - geom_part = None - - # First, try to find an already loaded geometry part - for part in theSession.Parts: - part_name = part.Name.lower() - part_type = type(part).__name__ - - # Skip FEM and SIM parts by type - if "fem" in part_type.lower() or "sim" in part_type.lower(): - continue - - # Skip parts with _fem or _sim in name - if "_fem" in part_name or "_sim" in part_name: - continue - - geom_part = part - print(f"[JOURNAL] Found geometry part (already loaded): {part.Name}") - break - - # If not found, try to load the geometry .prt file from working directory - if geom_part is None: - print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...") - for filename in os.listdir(working_dir): - # Skip idealized parts (_i.prt), FEM parts, and SIM parts - if ( - filename.endswith(".prt") - and "_fem" not in filename.lower() - and "_sim" not in filename.lower() - and "_i.prt" not in filename.lower() - ): - prt_path = os.path.join(working_dir, filename) - print(f"[JOURNAL] Loading geometry part: {filename}") - try: - loaded_part, partLoadStatus = theSession.Parts.Open(prt_path) - partLoadStatus.Dispose() - # Check if load actually succeeded (Parts.Open can return None) - if loaded_part is not None: - geom_part = loaded_part - print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}") - break - else: - print(f"[JOURNAL] WARNING: Parts.Open returned None for {filename}") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not load {filename}: {e}") - - if geom_part: - try: - # Switch to the geometry part for expression editing - markId_expr = theSession.SetUndoMark( - NXOpen.Session.MarkVisibility.Visible, "Update Expressions" - ) - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - geom_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus.Dispose() - - # Switch to modeling application for expression editing - theSession.ApplicationSwitchImmediate("UG_APP_MODELING") - - workPart = theSession.Parts.Work - - # Write expressions to temp file and import - 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 based on name - if "angle" 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...") - expModified, errorMessages = workPart.Expressions.ImportFromFile( - exp_file_path, NXOpen.ExpressionCollection.ImportMode.Replace - ) - print(f"[JOURNAL] Expressions modified: {expModified}") - if errorMessages: - print(f"[JOURNAL] Import messages: {errorMessages}") - - # Update geometry - 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)") - - # Save geometry part - print(f"[JOURNAL] Saving geometry part...") - partSaveStatus_geom = workPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, - NXOpen.BasePart.CloseAfterSave.FalseValue, - ) - partSaveStatus_geom.Dispose() - - # Clean up temp file - try: - os.remove(exp_file_path) - except: - pass - - except Exception as e: - print(f"[JOURNAL] ERROR updating expressions: {e}") - import traceback - - traceback.print_exc() - else: - print(f"[JOURNAL] WARNING: Could not find geometry part for expression updates!") - - # ========================================================================= - # STEP 2: UPDATE FEM MESH (if expressions were updated) - # ========================================================================= - if expression_updates: - print(f"[JOURNAL] STEP 2: Updating FEM mesh...") - - # First, load the idealized part if it exists (required for mesh update chain) - # The chain is: .prt (geometry) -> _i.prt (idealized) -> .fem (mesh) - idealized_part = None - for filename in os.listdir(working_dir): - if "_i.prt" in filename.lower(): - idealized_path = os.path.join(working_dir, filename) - print(f"[JOURNAL] Loading idealized part: {filename}") - try: - loaded_part, partLoadStatus = theSession.Parts.Open(idealized_path) - partLoadStatus.Dispose() - # Check if load actually succeeded (Parts.Open can return None) - if loaded_part is not None: - idealized_part = loaded_part - print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}") - else: - print(f"[JOURNAL] WARNING: Parts.Open returned None for idealized part") - except Exception as e: - print(f"[JOURNAL] WARNING: Could not load idealized part: {e}") - break - - # Find the FEM part - fem_part = None - for part in theSession.Parts: - if "_fem" in part.Name.lower() or part.Name.lower().endswith(".fem"): - fem_part = part - print(f"[JOURNAL] Found FEM part: {part.Name}") - break - - if fem_part: - try: - # Switch to FEM part - CRITICAL: Use SameAsDisplay to make FEM the work part - # This is required for UpdateFemodel() to properly regenerate the mesh - # Reference: tests/journal_with_regenerate.py line 76 - print(f"[JOURNAL] Switching to FEM part: {fem_part.Name}") - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - fem_part, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay, # Critical fix! - ) - partLoadStatus.Dispose() - - # Switch to FEM application - theSession.ApplicationSwitchImmediate("UG_APP_SFEM") - - # Update the FE model - workFemPart = theSession.Parts.BaseWork - feModel = workFemPart.FindObject("FEModel") - - print(f"[JOURNAL] Updating FE model...") - feModel.UpdateFemodel() - print(f"[JOURNAL] FE model updated") - - # Save FEM - partSaveStatus_fem = workFemPart.Save( - NXOpen.BasePart.SaveComponents.TrueValue, - NXOpen.BasePart.CloseAfterSave.FalseValue, - ) - partSaveStatus_fem.Dispose() - print(f"[JOURNAL] FEM saved") - - except Exception as e: - print(f"[JOURNAL] ERROR updating FEM: {e}") - import traceback - - traceback.print_exc() - - # ========================================================================= - # STEP 3: SWITCH BACK TO SIM AND SOLVE - # ========================================================================= - print(f"[JOURNAL] STEP 3: Solving simulation...") - - # Switch back to sim part - status, partLoadStatus = theSession.Parts.SetActiveDisplay( - workSimPart, - NXOpen.DisplayPartOption.AllowAdditional, - NXOpen.PartDisplayPartWorkPartOption.UseLast, - ) - partLoadStatus.Dispose() - - 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.Foreground, # Use Foreground to wait for completion - ) - - theSession.DeleteUndoMark(markId_solve2, None) - theSession.SetUndoMarkName(markId_solve, "Solve") - - print( - f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped" - ) - - # Save all - try: - anyPartsModified, partSaveStatus = theSession.Parts.SaveAll() - partSaveStatus.Dispose() - print(f"[JOURNAL] Saved all parts!") - except: - pass - - return numfailed == 0 - - -if __name__ == "__main__": - success = main(sys.argv[1:]) - # NOTE: Do NOT use sys.exit() here! - # run_journal.exe treats SystemExit (even code 0) as "Syntax errors" - # and returns a non-zero exit code, which makes the solver think - # the journal crashed. Instead, we just let the script end naturally. - # The solver checks for output files (.op2, .f06) to determine success. - if not success: - print("[JOURNAL] FAILED - solve did not complete successfully") - diff --git a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-151402.db b/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-151402.db deleted file mode 100644 index 94ced0fa..00000000 Binary files a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-151402.db and /dev/null differ diff --git a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152342.db b/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152342.db deleted file mode 100644 index 61c004d7..00000000 Binary files a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152342.db and /dev/null differ diff --git a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152502.db b/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152502.db deleted file mode 100644 index 3496112a..00000000 Binary files a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152502.db and /dev/null differ diff --git a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154152.db b/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154152.db deleted file mode 100644 index 00bdccac..00000000 Binary files a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154152.db and /dev/null differ diff --git a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154302.db b/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154302.db deleted file mode 100644 index 979fce9e..00000000 Binary files a/projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154302.db and /dev/null differ