feat: Add Studio UI, intake system, and extractor improvements
Dashboard: - Add Studio page with drag-drop model upload and Claude chat - Add intake system for study creation workflow - Improve session manager and context builder - Add intake API routes and frontend components Optimization Engine: - Add CLI module for command-line operations - Add intake module for study preprocessing - Add validation module with gate checks - Improve Zernike extractor documentation - Update spec models with better validation - Enhance solve_simulation robustness Documentation: - Add ATOMIZER_STUDIO.md planning doc - Add ATOMIZER_UX_SYSTEM.md for UX patterns - Update extractor library docs - Add study-readme-generator skill Tools: - Add test scripts for extraction validation - Add Zernike recentering test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,15 +70,15 @@ def extract_part_mass(theSession, part, output_dir):
|
||||
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
|
||||
"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:
|
||||
@@ -88,10 +88,10 @@ def extract_part_mass(theSession, part, output_dir):
|
||||
if body.IsSolidBody:
|
||||
bodies.append(body)
|
||||
|
||||
results['num_bodies'] = len(bodies)
|
||||
results["num_bodies"] = len(bodies)
|
||||
|
||||
if not bodies:
|
||||
results['error'] = "No solid bodies found"
|
||||
results["error"] = "No solid bodies found"
|
||||
raise ValueError("No solid bodies found in part")
|
||||
|
||||
# Get the measure manager
|
||||
@@ -104,30 +104,30 @@ def extract_part_mass(theSession, part, output_dir):
|
||||
uc.GetBase("Area"),
|
||||
uc.GetBase("Volume"),
|
||||
uc.GetBase("Mass"),
|
||||
uc.GetBase("Length")
|
||||
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
|
||||
results["mass_kg"] = measureBodies.Mass
|
||||
results["mass_g"] = results["mass_kg"] * 1000.0
|
||||
|
||||
try:
|
||||
results['volume_mm3'] = measureBodies.Volume
|
||||
results["volume_mm3"] = measureBodies.Volume
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
results['surface_area_mm2'] = measureBodies.Area
|
||||
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]
|
||||
results["center_of_gravity_mm"] = [cog.X, cog.Y, cog.Z]
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -136,26 +136,26 @@ def extract_part_mass(theSession, part, output_dir):
|
||||
except:
|
||||
pass
|
||||
|
||||
results['success'] = True
|
||||
results["success"] = True
|
||||
|
||||
except Exception as e:
|
||||
results['error'] = str(e)
|
||||
results['success'] = False
|
||||
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:
|
||||
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']))
|
||||
with open(mass_file, "w") as f:
|
||||
f.write(str(results["mass_kg"]))
|
||||
|
||||
if not results['success']:
|
||||
raise ValueError(results['error'])
|
||||
if not results["success"]:
|
||||
raise ValueError(results["error"])
|
||||
|
||||
return results['mass_kg']
|
||||
return results["mass_kg"]
|
||||
|
||||
|
||||
def find_or_open_part(theSession, part_path):
|
||||
@@ -164,7 +164,7 @@ def find_or_open_part(theSession, part_path):
|
||||
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:
|
||||
@@ -174,9 +174,9 @@ def find_or_open_part(theSession, part_path):
|
||||
return part, True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Not found, open it
|
||||
markId = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, f'Load {part_name}')
|
||||
markId = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, f"Load {part_name}")
|
||||
part, partLoadStatus = theSession.Parts.Open(part_path)
|
||||
partLoadStatus.Dispose()
|
||||
return part, False
|
||||
@@ -194,26 +194,28 @@ def main(args):
|
||||
"""
|
||||
if len(args) < 1:
|
||||
print("ERROR: No .sim file path provided")
|
||||
print("Usage: run_journal.exe solve_simulation.py <sim_file_path> [solution_name] [expr1=val1] ...")
|
||||
print(
|
||||
"Usage: run_journal.exe solve_simulation.py <sim_file_path> [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
|
||||
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)
|
||||
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] " + "=" * 60)
|
||||
print(f"[JOURNAL] NX SIMULATION SOLVER (Assembly FEM Workflow)")
|
||||
print(f"[JOURNAL] " + "="*60)
|
||||
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'}")
|
||||
@@ -226,7 +228,9 @@ def main(args):
|
||||
|
||||
# Set load options
|
||||
theSession.Parts.LoadOptions.LoadLatest = False
|
||||
theSession.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
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
|
||||
@@ -240,7 +244,7 @@ def main(args):
|
||||
pass
|
||||
|
||||
# Check for assembly FEM files
|
||||
afm_files = [f for f in os.listdir(working_dir) if f.endswith('.afm')]
|
||||
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:
|
||||
@@ -262,11 +266,14 @@ def main(args):
|
||||
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):
|
||||
def solve_assembly_fem_workflow(
|
||||
theSession, sim_file_path, solution_name, expression_updates, working_dir
|
||||
):
|
||||
"""
|
||||
Full assembly FEM workflow based on recorded NX journal.
|
||||
|
||||
@@ -285,8 +292,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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
|
||||
sim_file_full_path, NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
@@ -330,7 +336,9 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
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)
|
||||
@@ -347,11 +355,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
# Find and switch to M1_Blank part
|
||||
try:
|
||||
part3 = theSession.Parts.FindObject("M1_Blank")
|
||||
markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Change Displayed Part")
|
||||
markId3 = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Change Displayed Part"
|
||||
)
|
||||
status1, partLoadStatus3 = theSession.Parts.SetActiveDisplay(
|
||||
part3,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast,
|
||||
)
|
||||
partLoadStatus3.Dispose()
|
||||
|
||||
@@ -366,10 +376,10 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
|
||||
# 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:
|
||||
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():
|
||||
if "angle" in expr_name.lower() or "vertical" in expr_name.lower():
|
||||
unit_str = "Degrees"
|
||||
else:
|
||||
unit_str = "MilliMeter"
|
||||
@@ -377,12 +387,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
markId_import = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Import Expressions"
|
||||
)
|
||||
|
||||
try:
|
||||
expModified, errorMessages = workPart.Expressions.ImportFromFile(
|
||||
exp_file_path,
|
||||
NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
exp_file_path, NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
)
|
||||
print(f"[JOURNAL] Expressions imported: {expModified} modified")
|
||||
if errorMessages:
|
||||
@@ -390,14 +401,18 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
|
||||
# Update geometry after import
|
||||
print(f"[JOURNAL] Rebuilding M1_Blank geometry...")
|
||||
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
||||
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 = workPart.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
partSaveStatus_blank.Dispose()
|
||||
print(f"[JOURNAL] M1_Blank saved")
|
||||
|
||||
@@ -445,11 +460,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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}")
|
||||
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
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast,
|
||||
)
|
||||
partLoadStatus_linked.Dispose()
|
||||
|
||||
@@ -457,14 +474,18 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_MODELING")
|
||||
|
||||
# Update to propagate linked expression changes
|
||||
markId_linked_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
||||
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 = linked_part.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
partSaveStatus_linked.Dispose()
|
||||
print(f"[JOURNAL] {part_name} saved")
|
||||
|
||||
@@ -482,7 +503,9 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
markId_sim = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Change Displayed Part"
|
||||
)
|
||||
|
||||
try:
|
||||
# First try to find it among loaded parts (like recorded journal)
|
||||
@@ -490,7 +513,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
status_sim, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
simPart1,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast,
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Found and activated existing sim part")
|
||||
@@ -498,8 +521,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
# 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
|
||||
sim_file_path, NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
@@ -517,23 +539,29 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
markId_fem1 = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Make Work Part"
|
||||
)
|
||||
partLoadStatus5 = theSession.Parts.SetWorkComponent(
|
||||
component2,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible,
|
||||
)
|
||||
workFemPart = theSession.Parts.BaseWork
|
||||
partLoadStatus5.Dispose()
|
||||
|
||||
markId_update1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update FE Model")
|
||||
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 = workFemPart.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
partSaveStatus_fem1.Dispose()
|
||||
print(f"[JOURNAL] M1_Blank_fem1 saved")
|
||||
except Exception as e:
|
||||
@@ -543,23 +571,29 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
markId_fem2 = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Make Work Part"
|
||||
)
|
||||
partLoadStatus6 = theSession.Parts.SetWorkComponent(
|
||||
component3,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible,
|
||||
)
|
||||
workFemPart = theSession.Parts.BaseWork
|
||||
partLoadStatus6.Dispose()
|
||||
|
||||
markId_update2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update FE Model")
|
||||
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 = 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:
|
||||
@@ -578,7 +612,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
partLoadStatus8 = theSession.Parts.SetWorkComponent(
|
||||
component1,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible,
|
||||
)
|
||||
workAssyFemPart = theSession.Parts.BaseWork
|
||||
displaySimPart = theSession.Parts.BaseDisplay
|
||||
@@ -643,13 +677,17 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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")
|
||||
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")
|
||||
print(
|
||||
f"[JOURNAL] This combined with IdentifyDuplicateNodes=None suggests display issue"
|
||||
)
|
||||
|
||||
theSession.SetUndoMarkName(markId_merge, "Duplicate Nodes")
|
||||
duplicateNodesCheckBuilder1.Destroy()
|
||||
@@ -658,6 +696,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Node merge: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
# ==========================================================================
|
||||
@@ -673,7 +712,9 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
|
||||
theSession.SetUndoMarkName(markId_labels, "Assembly Label Manager Dialog")
|
||||
|
||||
markId_labels2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "Assembly Label Manager")
|
||||
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
|
||||
@@ -720,7 +761,9 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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 = 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:
|
||||
@@ -736,7 +779,7 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
partLoadStatus9 = theSession.Parts.SetWorkComponent(
|
||||
NXOpen.Assemblies.Component.Null,
|
||||
NXOpen.PartCollection.RefsetOption.Entire,
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible
|
||||
NXOpen.PartCollection.WorkComponentOption.Visible,
|
||||
)
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
partLoadStatus9.Dispose()
|
||||
@@ -760,13 +803,15 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to ensure OP2 is complete
|
||||
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")
|
||||
print(
|
||||
f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped"
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# STEP 7: SAVE ALL - Save all modified parts (FEM, SIM, PRT)
|
||||
@@ -784,11 +829,14 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
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):
|
||||
def solve_simple_workflow(
|
||||
theSession, sim_file_path, solution_name, expression_updates, working_dir
|
||||
):
|
||||
"""
|
||||
Workflow for single-part simulations with optional expression updates.
|
||||
|
||||
@@ -802,8 +850,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
|
||||
# Open the .sim file
|
||||
basePart1, partLoadStatus1 = theSession.Parts.OpenActiveDisplay(
|
||||
sim_file_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
sim_file_path, NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus1.Dispose()
|
||||
|
||||
@@ -830,11 +877,11 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
part_type = type(part).__name__
|
||||
|
||||
# Skip FEM and SIM parts by type
|
||||
if 'fem' in part_type.lower() or 'sim' in part_type.lower():
|
||||
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:
|
||||
if "_fem" in part_name or "_sim" in part_name:
|
||||
continue
|
||||
|
||||
geom_part = part
|
||||
@@ -845,25 +892,38 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
if geom_part is None:
|
||||
print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...")
|
||||
for filename in os.listdir(working_dir):
|
||||
if filename.endswith('.prt') and '_fem' not in filename.lower() and '_sim' not in filename.lower():
|
||||
# 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:
|
||||
geom_part, partLoadStatus = theSession.Parts.Open(prt_path)
|
||||
loaded_part, partLoadStatus = theSession.Parts.Open(prt_path)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}")
|
||||
break
|
||||
# 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")
|
||||
markId_expr = theSession.SetUndoMark(
|
||||
NXOpen.Session.MarkVisibility.Visible, "Update Expressions"
|
||||
)
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
geom_part,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast,
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
@@ -874,10 +934,10 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
|
||||
# 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:
|
||||
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():
|
||||
if "angle" in expr_name.lower():
|
||||
unit_str = "Degrees"
|
||||
else:
|
||||
unit_str = "MilliMeter"
|
||||
@@ -886,8 +946,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
|
||||
print(f"[JOURNAL] Importing expressions...")
|
||||
expModified, errorMessages = workPart.Expressions.ImportFromFile(
|
||||
exp_file_path,
|
||||
NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
exp_file_path, NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
)
|
||||
print(f"[JOURNAL] Expressions modified: {expModified}")
|
||||
if errorMessages:
|
||||
@@ -895,14 +954,19 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
|
||||
# Update geometry
|
||||
print(f"[JOURNAL] Rebuilding geometry...")
|
||||
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
||||
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 = workPart.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue,
|
||||
NXOpen.BasePart.CloseAfterSave.FalseValue,
|
||||
)
|
||||
partSaveStatus_geom.Dispose()
|
||||
|
||||
# Clean up temp file
|
||||
@@ -914,6 +978,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
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!")
|
||||
@@ -928,13 +993,18 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
# 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():
|
||||
if "_i.prt" in filename.lower():
|
||||
idealized_path = os.path.join(working_dir, filename)
|
||||
print(f"[JOURNAL] Loading idealized part: {filename}")
|
||||
try:
|
||||
idealized_part, partLoadStatus = theSession.Parts.Open(idealized_path)
|
||||
loaded_part, partLoadStatus = theSession.Parts.Open(idealized_path)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}")
|
||||
# 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
|
||||
@@ -942,7 +1012,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
# Find the FEM part
|
||||
fem_part = None
|
||||
for part in theSession.Parts:
|
||||
if '_fem' in part.Name.lower() or part.Name.lower().endswith('.fem'):
|
||||
if "_fem" in part.Name.lower() or part.Name.lower().endswith(".fem"):
|
||||
fem_part = part
|
||||
print(f"[JOURNAL] Found FEM part: {part.Name}")
|
||||
break
|
||||
@@ -956,7 +1026,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
fem_part,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay # Critical fix!
|
||||
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay, # Critical fix!
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
@@ -972,13 +1042,17 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
print(f"[JOURNAL] FE model updated")
|
||||
|
||||
# Save FEM
|
||||
partSaveStatus_fem = workFemPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
|
||||
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()
|
||||
|
||||
# =========================================================================
|
||||
@@ -990,7 +1064,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
workSimPart,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast,
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
@@ -1016,13 +1090,15 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to wait for completion
|
||||
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")
|
||||
print(
|
||||
f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped"
|
||||
)
|
||||
|
||||
# Save all
|
||||
try:
|
||||
@@ -1035,6 +1111,6 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
return numfailed == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
success = main(sys.argv[1:])
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
Reference in New Issue
Block a user