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:
2026-01-27 12:02:30 -05:00
parent 3193831340
commit a26914bbe8
56 changed files with 14173 additions and 646 deletions

View File

@@ -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)