Files
Atomizer/hq/workspaces/nx-expert/deliverables/mass_extraction_fix.py
Antoine 3289a76e19 feat: add Atomizer HQ multi-agent cluster infrastructure
- 8-agent OpenClaw cluster (Manager, Tech-Lead, Secretary, Auditor,
  Optimizer, Study-Builder, NX-Expert, Webster)
- Orchestration engine: orchestrate.py (sync delegation + handoffs)
- Workflow engine: YAML-defined multi-step pipelines
- Agent workspaces: SOUL.md, AGENTS.md, MEMORY.md per agent
- Shared skills: delegate, orchestrate, atomizer-protocols
- Capability registry (AGENTS_REGISTRY.json)
- Cluster management: cluster.sh, systemd template
- All secrets replaced with env var references
2026-02-15 21:18:18 +00:00

338 lines
15 KiB
Python

"""
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.