- 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
338 lines
15 KiB
Python
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.
|