From 0795cccc97d97ab3d7f334a89ca6173ab0091f13 Mon Sep 17 00:00:00 2001 From: Antoine Date: Fri, 13 Feb 2026 08:00:19 +0000 Subject: [PATCH] auto: daily sync --- .../_temp_solve_journal~20260211-152342.py | 1126 ----------------- .../_temp_solve_journal~20260211-154152.py | 1126 ----------------- .../results/optuna_study~20260211-151402.db | Bin 114688 -> 0 bytes .../results/optuna_study~20260211-152342.db | Bin 114688 -> 0 bytes .../results/optuna_study~20260211-152502.db | Bin 114688 -> 0 bytes .../results/optuna_study~20260211-154152.db | Bin 114688 -> 0 bytes .../results/optuna_study~20260211-154302.db | Bin 114688 -> 0 bytes 7 files changed, 2252 deletions(-) delete mode 100755 projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-152342.py delete mode 100755 projects/.stversions/hydrotech-beam/models/_temp_solve_journal~20260211-154152.py delete mode 100644 projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-151402.db delete mode 100644 projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152342.db delete mode 100644 projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-152502.db delete mode 100644 projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154152.db delete mode 100644 projects/.stversions/hydrotech-beam/studies/01_doe_landscape/results/optuna_study~20260211-154302.db 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 94ced0fa625cc0af518074103b40767e74f3919d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114688 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 Opd-)|J&`o8|NjdQ72;F? 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 00bdccac1276889fc3e9ba74eeecb956d87a7124..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114688 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 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 979fce9ec0f4e1d1367ea7aeb55040d5a3510d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114688 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 Q0ubm5^hHl4&HMlV0?u;cPyhe`