""" Mass Extraction Fix for Generic Projects (Bracket, Beam, etc.) ============================================================== PROBLEM: - solve_assembly_fem_workflow() hardcodes "M1_Blank" for part lookup and mass extraction - solve_simple_workflow() mass extraction after solve has two issues: 1. Expression p173 (MeasureBody) may NOT auto-update after expression import + solve because MeasureBody expressions are "on-demand" — they reflect geometry state at last update, not post-solve state. 2. MeasureManager fallback works correctly (computes fresh from solid bodies) but the geometry part discovery could fail if the part wasn't loaded. ANALYSIS: - For the Beam project (single-part, no .afm), solve_simple_workflow() is used ✓ - The geometry part discovery logic (lines ~488-530) already works generically ✓ - MeasureManager.NewMassProperties() computes fresh mass — CORRECT approach ✓ - Expression p173 may be STALE after expression import — should NOT be trusted after parameter changes without explicit geometry update + expression refresh FIX SUMMARY: The actual fix needed is small. Two changes to solve_simple_workflow(): 1. After geometry rebuild (DoUpdate), extract mass IMMEDIATELY (before switching to FEM/solve) — this is when geometry is current and MeasureManager will give correct results 2. Remove the post-solve mass extraction attempt via p173 expression (unreliable) 3. For assembly workflow: parameterize part names (but that's a bigger refactor) Below is the patched solve_simple_workflow mass extraction section. """ # ============================================================================= # PATCH 1: Add mass extraction RIGHT AFTER geometry rebuild in solve_simple_workflow # ============================================================================= # # In solve_simple_workflow(), after the geometry rebuild block (around line 510): # # nErrs = theSession.UpdateManager.DoUpdate(markId_update) # theSession.DeleteUndoMark(markId_update, "NX update") # print(f"[JOURNAL] Geometry rebuilt ({nErrs} errors)") # # ADD THIS (before saving geometry part): # # # Extract mass NOW while geometry part is work part and freshly rebuilt # print(f"[JOURNAL] Extracting mass from {workPart.Name}...") # try: # mass_kg = extract_part_mass(theSession, workPart, working_dir) # print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg") # except Exception as mass_err: # print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}") # # ============================================================================= # PATCH 2: Simplify post-solve mass extraction (remove unreliable p173 lookup) # ============================================================================= # # Replace the entire post-solve mass extraction block (lines ~1178-1220) with: # POST_SOLVE_MASS_EXTRACTION = ''' # Extract mass after solve # Strategy: Use MeasureManager on geometry part (most reliable) # Note: Expression p173 (MeasureBody) may be stale — don't trust it after param changes try: geom_part = None for part in theSession.Parts: part_name = part.Name.lower() part_type = type(part).__name__ if "fem" not in part_type.lower() and "sim" not in part_type.lower(): if "_fem" not in part_name and "_sim" not in part_name and "_i" not in part_name: geom_part = part break if geom_part is not None: # Switch to geometry part briefly for mass measurement status, pls = theSession.Parts.SetActiveDisplay( geom_part, NXOpen.DisplayPartOption.AllowAdditional, NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay, ) pls.Dispose() theSession.ApplicationSwitchImmediate("UG_APP_MODELING") # Force geometry update to ensure expressions are current markId_mass = theSession.SetUndoMark( NXOpen.Session.MarkVisibility.Invisible, "Mass update" ) theSession.UpdateManager.DoUpdate(markId_mass) theSession.DeleteUndoMark(markId_mass, "Mass update") mass_value = extract_part_mass(theSession, geom_part, working_dir) print(f"[JOURNAL] Mass = {mass_value:.6f} kg") # Also write in p173= format for backward compat mass_file = os.path.join(working_dir, "_temp_mass.txt") with open(mass_file, "w") as f: f.write(f"p173={mass_value}\\n") # Switch back to sim status, pls = theSession.Parts.SetActiveDisplay( workSimPart, NXOpen.DisplayPartOption.AllowAdditional, NXOpen.PartDisplayPartWorkPartOption.UseLast, ) pls.Dispose() else: print(f"[JOURNAL] WARNING: No geometry part found for mass extraction") except Exception as e: print(f"[JOURNAL] WARNING: Mass extraction failed: {e}") ''' # ============================================================================= # FULL PATCHED solve_simple_workflow (drop-in replacement) # ============================================================================= # To apply: replace the solve_simple_workflow function in solve_simulation.py # with this version. Only the mass extraction logic changes. def solve_simple_workflow_PATCHED( theSession, sim_file_path, solution_name, expression_updates, working_dir ): """ Patched workflow for single-part simulations. Changes from original: 1. Mass extraction happens RIGHT AFTER geometry rebuild (most reliable timing) 2. Post-solve mass extraction uses MeasureManager with forced geometry update 3. Removed unreliable p173 expression lookup """ import os import NXOpen import NXOpen.CAE 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 # ========================================================================= geom_part_ref = None # Keep reference for post-solve mass extraction if expression_updates: print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...") # Find geometry part (generic: any non-FEM, non-SIM, non-idealized part) geom_part = None for part in theSession.Parts: part_name = part.Name.lower() part_type = type(part).__name__ if "fem" not in part_type.lower() and "sim" not in part_type.lower(): if "_fem" not in part_name and "_sim" not in part_name: geom_part = part print(f"[JOURNAL] Found geometry part: {part.Name}") break # If not loaded, search working directory if geom_part is None: for filename in os.listdir(working_dir): 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, pls = theSession.Parts.Open(prt_path) pls.Dispose() if loaded_part is not None: geom_part = loaded_part break except Exception as e: print(f"[JOURNAL] WARNING: Could not load {filename}: {e}") if geom_part: geom_part_ref = geom_part try: # Switch to geometry part status, pls = theSession.Parts.SetActiveDisplay( geom_part, NXOpen.DisplayPartOption.AllowAdditional, NXOpen.PartDisplayPartWorkPartOption.UseLast, ) pls.Dispose() theSession.ApplicationSwitchImmediate("UG_APP_MODELING") workPart = theSession.Parts.Work # Import expressions exp_file_path = os.path.join(working_dir, "_temp_expressions.exp") CONSTANT_EXPRESSIONS = {"hole_count"} with open(exp_file_path, "w") as f: for expr_name, expr_value in expression_updates.items(): if expr_name in CONSTANT_EXPRESSIONS: unit_str = "Constant" if expr_value == int(expr_value): expr_value = int(expr_value) elif "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})") expModified, errorMessages = workPart.Expressions.ImportFromFile( exp_file_path, NXOpen.ExpressionCollection.ImportMode.Replace ) print(f"[JOURNAL] Expressions modified: {expModified}") # Rebuild 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)") # >>> FIX: Extract mass NOW while geometry is fresh <<< print(f"[JOURNAL] Extracting mass from {workPart.Name}...") try: mass_kg = extract_part_mass(theSession, workPart, working_dir) print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg") except Exception as mass_err: print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}") # Save geometry part pss = workPart.Save( NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue, ) pss.Dispose() try: os.remove(exp_file_path) except: pass except Exception as e: print(f"[JOURNAL] ERROR updating expressions: {e}") import traceback traceback.print_exc() # ========================================================================= # STEP 2: UPDATE FEM MESH # ========================================================================= if expression_updates: print(f"[JOURNAL] STEP 2: Updating FEM mesh...") # (Same as original — find FEM part, switch to it, UpdateFemodel, save) # ... [unchanged from original] ... # ========================================================================= # STEP 3: SOLVE # ========================================================================= print(f"[JOURNAL] STEP 3: Solving simulation...") status, pls = theSession.Parts.SetActiveDisplay( workSimPart, NXOpen.DisplayPartOption.AllowAdditional, NXOpen.PartDisplayPartWorkPartOption.UseLast, ) pls.Dispose() theSession.ApplicationSwitchImmediate("UG_APP_SFEM") theSession.Post.UpdateUserGroupsFromSimPart(workSimPart) 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}]") numsolved, numfailed, numskipped = theCAESimSolveManager.SolveChainOfSolutions( [simSolution1], NXOpen.CAE.SimSolution.SolveOption.Solve, NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors, NXOpen.CAE.SimSolution.SolveMode.Foreground, ) print(f"[JOURNAL] Solve: {numsolved} solved, {numfailed} failed, {numskipped} skipped") # Mass was already extracted after geometry rebuild (most reliable). # No need for post-solve p173 expression lookup. # Save all try: anyPartsModified, pss = theSession.Parts.SaveAll() pss.Dispose() except: pass return numfailed == 0 # ============================================================================= # APPLYING THE PATCH # ============================================================================= # # Option A (recommended): Apply these two surgical edits to solve_simulation.py: # # EDIT 1: After geometry rebuild in solve_simple_workflow (~line 510), add mass extraction: # After: print(f"[JOURNAL] Geometry rebuilt ({nErrs} errors)") # Add: # # Extract mass while geometry is fresh # print(f"[JOURNAL] Extracting mass from {workPart.Name}...") # try: # mass_kg = extract_part_mass(theSession, workPart, working_dir) # print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg") # except Exception as mass_err: # print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}") # # EDIT 2: Replace post-solve mass extraction block (~lines 1178-1220). # The current code tries expression p173 first then MeasureManager fallback. # Since mass was already extracted in EDIT 1, simplify to just a log message: # print(f"[JOURNAL] Mass already extracted during geometry rebuild phase") # # Option B: Replace entire solve_simple_workflow with solve_simple_workflow_PATCHED above. # # ============================================================================= # KEY FINDINGS # ============================================================================= # # Q: Does expression p173 (MeasureBody) auto-update after expression import? # A: NO — MeasureBody expressions update when DoUpdate() is called on the geometry. # After DoUpdate(), the expression VALUE in memory should be current. However, # when switching between parts (geom -> FEM -> SIM -> solve -> back), the # expression may not be accessible or may reflect a cached state. # MeasureManager.NewMassProperties() is the RELIABLE approach — it computes # fresh from the current solid body geometry regardless of expression state. # # Q: Should we pass .prt filename as argument? # A: Not needed for solve_simple_workflow — the generic discovery logic works. # For solve_assembly_fem_workflow, YES — that needs a bigger refactor to # parameterize M1_Blank, ASSY_M1, etc. But that's not needed for Beam. # # Q: Best timing for mass extraction? # A: RIGHT AFTER geometry rebuild (DoUpdate) while geometry part is still the # work part. This is when solid bodies reflect the updated parameters.