From f725e751644a945f537ea608def5ff9c643bed0e Mon Sep 17 00:00:00 2001 From: Anto01 Date: Tue, 20 Jan 2026 21:20:14 -0500 Subject: [PATCH] feat: Add SIM file introspection journal and enhanced file-type specific UI - Create introspect_sim.py NX journal to extract solutions, BCs from SIM files - Update introspect_sim_file() to optionally call NX journal for full introspection - Add FEM mesh section (nodes, elements, materials, properties) to IntrospectionPanel - Add SIM solutions and boundary conditions sections to IntrospectionPanel - Show introspection method and NX errors in panel --- .../backend/api/routes/optimization.py | 84 +++- .../canvas/panels/IntrospectionPanel.tsx | 272 ++++++++++++- nx_journals/introspect_sim.py | 380 ++++++++++++++++++ 3 files changed, 730 insertions(+), 6 deletions(-) create mode 100644 nx_journals/introspect_sim.py diff --git a/atomizer-dashboard/backend/api/routes/optimization.py b/atomizer-dashboard/backend/api/routes/optimization.py index 400ccd1f..0dd85cca 100644 --- a/atomizer-dashboard/backend/api/routes/optimization.py +++ b/atomizer-dashboard/backend/api/routes/optimization.py @@ -5357,19 +5357,89 @@ def introspect_fem_file(fem_path: Path) -> dict: return result -def introspect_sim_file(sim_path: Path) -> dict: +def introspect_sim_file(sim_path: Path, use_nx: bool = True) -> dict: """ Introspect a .sim file - extract solution info. - Note: .sim is a binary NX format, so we extract what we can from associated files. + Args: + sim_path: Path to the .sim file + use_nx: If True, use NX journal for full introspection (requires NX license) + If False, use file-based heuristics only + + Note: .sim is a binary NX format. Full introspection requires NX Open. """ result = { "file_type": "sim", "file_name": sim_path.name, "success": True, - "note": "SIM files are binary NX format. Use NX introspection for full details.", } + # Try NX journal introspection if requested + if use_nx: + try: + import subprocess + import json + + # Find NX installation + nx_paths = [ + Path("C:/Program Files/Siemens/DesigncenterNX2512/NXBIN/run_journal.exe"), + Path("C:/Program Files/Siemens/NX2412/NXBIN/run_journal.exe"), + Path("C:/Program Files/Siemens/Simcenter3D_2412/NXBIN/run_journal.exe"), + ] + journal_runner = None + for p in nx_paths: + if p.exists(): + journal_runner = p + break + + if journal_runner: + journal_path = ( + Path(__file__).parent.parent.parent.parent.parent + / "nx_journals" + / "introspect_sim.py" + ) + if not journal_path.exists(): + # Try alternate path + journal_path = Path("C:/Users/antoi/Atomizer/nx_journals/introspect_sim.py") + + if journal_path.exists(): + cmd = [ + str(journal_runner), + str(journal_path), + str(sim_path), + str(sim_path.parent), + ] + + # Run NX journal with timeout + proc = subprocess.run( + cmd, capture_output=True, text=True, timeout=120, cwd=str(sim_path.parent) + ) + + # Check for output file + output_file = sim_path.parent / "_introspection_sim.json" + if output_file.exists(): + with open(output_file, "r") as f: + nx_result = json.load(f) + result.update(nx_result) + result["introspection_method"] = "nx_journal" + return result + else: + result["nx_error"] = "Journal completed but no output file" + result["nx_stdout"] = proc.stdout[-2000:] if proc.stdout else None + result["nx_stderr"] = proc.stderr[-2000:] if proc.stderr else None + else: + result["nx_error"] = f"Journal not found: {journal_path}" + else: + result["nx_error"] = "NX installation not found" + except subprocess.TimeoutExpired: + result["nx_error"] = "NX journal timed out (120s)" + except Exception as e: + result["nx_error"] = f"NX introspection failed: {str(e)}" + + # Fallback: file-based heuristics + result["introspection_method"] = "file_heuristics" + result["note"] = "SIM files are binary NX format. NX introspection unavailable." + # Check for associated .dat file that might have been exported dat_file = sim_path.parent / (sim_path.stem + ".dat") if not dat_file.exists(): @@ -5380,7 +5450,13 @@ def introspect_sim_file(sim_path: Path) -> dict: if dat_file.exists(): result["associated_dat"] = dat_file.name - # Could parse the DAT file for solution info if needed + + # Try to infer solver type from related files + parent_dir = sim_path.parent + if any(f.suffix.lower() == ".afm" for f in parent_dir.iterdir() if f.is_file()): + result["inferred"] = {"assembly_fem": True, "solver": "nastran"} + elif any(f.suffix.lower() == ".fem" for f in parent_dir.iterdir() if f.is_file()): + result["inferred"] = {"assembly_fem": False, "solver": "nastran"} return result diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx index 67c2cf69..d9f3458d 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx @@ -23,6 +23,10 @@ import { File, Database, CheckCircle2, + Layers, + Grid3x3, + Target, + Zap, } from 'lucide-react'; import { useCanvasStore } from '../../../hooks/useCanvasStore'; @@ -82,6 +86,45 @@ interface ExpressionsResult { user_count: number; } +// FEM file introspection result (from PyNastran) +interface FemIntrospection { + node_count?: number; + element_count?: number; + element_types?: Record; + materials?: Array<{ id: number; type: string; name?: string }>; + properties?: Array<{ id: number; type: string; material_id?: number }>; + coordinate_systems?: Array<{ id: number; type: string }>; + load_sets?: number[]; + spc_sets?: number[]; +} + +// SIM file introspection result (from NX journal) +interface SimIntrospection { + solutions?: Array<{ + name: string; + type?: string; + properties?: Record; + }>; + boundary_conditions?: { + constraints?: Array<{ name: string; type: string }>; + loads?: Array<{ name: string; type: string }>; + total_count?: number; + }; + tree_structure?: { + simulation_objects?: Array<{ pattern: string; type: string; found: boolean }>; + found_types?: string[]; + }; + loaded_parts?: Array<{ name: string; type: string; leaf?: string }>; + part_info?: { + name?: string; + is_assembly?: boolean; + component_count?: number; + components?: Array<{ name: string; type: string }>; + }; + introspection_method?: string; + nx_error?: string; +} + interface IntrospectionResult { part_file?: string; part_path?: string; @@ -95,7 +138,7 @@ interface IntrospectionResult { dependent_files?: DependentFile[]; extractors_available?: Extractor[]; warnings?: string[]; - // Additional fields from NX introspection + // Additional fields from NX introspection (PRT files) mass_properties?: Record; materials?: Record; bodies?: Record; @@ -119,6 +162,18 @@ interface IntrospectionResult { op2: string[]; }; }; + // FEM file introspection (from PyNastran) + fem?: FemIntrospection; + // SIM file introspection (from NX journal) + sim?: SimIntrospection; + // Additional SIM fields that may be at top level + solutions?: SimIntrospection['solutions']; + boundary_conditions?: SimIntrospection['boundary_conditions']; + tree_structure?: SimIntrospection['tree_structure']; + loaded_parts?: SimIntrospection['loaded_parts']; + part_info?: SimIntrospection['part_info']; + introspection_method?: string; + nx_error?: string; } // Baseline run result interface @@ -165,7 +220,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [expandedSections, setExpandedSections] = useState>( - new Set(['expressions', 'extractors', 'file_deps', 'fea_results']) + new Set(['expressions', 'extractors', 'file_deps', 'fea_results', 'fem_mesh', 'sim_solutions', 'sim_bcs']) ); const [searchTerm, setSearchTerm] = useState(''); @@ -804,6 +859,219 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection )} + {/* FEM Mesh Info Section (for .fem/.afm files) */} + {result.fem && ( +
+ + + {expandedSections.has('fem_mesh') && ( +
+
+ Nodes + {result.fem.node_count?.toLocaleString() || 0} +
+
+ Elements + {result.fem.element_count?.toLocaleString() || 0} +
+ + {/* Element types breakdown */} + {result.fem.element_types && Object.keys(result.fem.element_types).length > 0 && ( +
+

Element Types:

+ {Object.entries(result.fem.element_types).map(([type, count]) => ( +
+ {type} + {count} +
+ ))} +
+ )} + + {/* Materials */} + {result.fem.materials && result.fem.materials.length > 0 && ( +
+

Materials ({result.fem.materials.length}):

+ {result.fem.materials.slice(0, 5).map((mat) => ( +
+ ID {mat.id} + {mat.type} +
+ ))} + {result.fem.materials.length > 5 && ( +

+{result.fem.materials.length - 5} more

+ )} +
+ )} + + {/* Properties */} + {result.fem.properties && result.fem.properties.length > 0 && ( +
+

Properties ({result.fem.properties.length}):

+ {result.fem.properties.slice(0, 5).map((prop) => ( +
+ ID {prop.id} + {prop.type} +
+ ))} + {result.fem.properties.length > 5 && ( +

+{result.fem.properties.length - 5} more

+ )} +
+ )} + + {/* Load/SPC sets */} + {(result.fem.load_sets?.length || result.fem.spc_sets?.length) && ( +
+ {result.fem.load_sets && result.fem.load_sets.length > 0 && ( +
+ Load Sets: + {result.fem.load_sets.join(', ')} +
+ )} + {result.fem.spc_sets && result.fem.spc_sets.length > 0 && ( +
+ SPC Sets: + {result.fem.spc_sets.join(', ')} +
+ )} +
+ )} +
+ )} +
+ )} + + {/* SIM Solutions Section (for .sim files) */} + {(result.solutions || result.sim?.solutions) && ( +
+ + + {expandedSections.has('sim_solutions') && ( +
+ {((result.solutions || result.sim?.solutions) || []).map((sol, idx) => ( +
+
+ + {sol.name} +
+ {sol.type && ( +

Type: {sol.type}

+ )} +
+ ))} + {(result.solutions || result.sim?.solutions)?.length === 0 && ( +

No solutions found

+ )} +
+ )} +
+ )} + + {/* SIM Boundary Conditions Section */} + {(result.boundary_conditions || result.sim?.boundary_conditions) && ( +
+ + + {expandedSections.has('sim_bcs') && ( +
+ {/* Constraints */} + {(result.boundary_conditions || result.sim?.boundary_conditions)?.constraints && + (result.boundary_conditions || result.sim?.boundary_conditions)!.constraints!.length > 0 && ( +
+

Constraints:

+ {(result.boundary_conditions || result.sim?.boundary_conditions)!.constraints!.map((bc, idx) => ( +
+ {bc.name} + {bc.type} +
+ ))} +
+ )} + + {/* Loads */} + {(result.boundary_conditions || result.sim?.boundary_conditions)?.loads && + (result.boundary_conditions || result.sim?.boundary_conditions)!.loads!.length > 0 && ( +
+

Loads:

+ {(result.boundary_conditions || result.sim?.boundary_conditions)!.loads!.map((load, idx) => ( +
+ {load.name} + {load.type} +
+ ))} +
+ )} + + {(result.boundary_conditions || result.sim?.boundary_conditions)?.total_count === 0 && ( +

No boundary conditions found

+ )} +
+ )} +
+ )} + + {/* SIM Introspection Method Info */} + {(result.introspection_method || result.nx_error) && ( +
+ {result.introspection_method && ( +

+ Method: {result.introspection_method} +

+ )} + {result.nx_error && ( +

+ NX Error: {result.nx_error} +

+ )} +
+ )} + {/* File Dependencies Section (NX file chain) */} {result.file_dependencies && (
diff --git a/nx_journals/introspect_sim.py b/nx_journals/introspect_sim.py new file mode 100644 index 00000000..885273c9 --- /dev/null +++ b/nx_journals/introspect_sim.py @@ -0,0 +1,380 @@ +""" +NX Journal: SIM File Introspection Tool +========================================= + +This journal performs deep introspection of an NX .sim file and extracts: +- Solutions (name, type, solver) +- Boundary conditions (SPCs, loads, etc.) +- Subcases +- Linked FEM files +- Solution properties + +Usage: + run_journal.exe introspect_sim.py [output_dir] + +Output: + _introspection_sim.json - JSON with all extracted data + +Author: Atomizer +Created: 2026-01-20 +Version: 1.0 +""" + +import sys +import os +import json +import NXOpen +import NXOpen.CAE + + +def get_solutions(simSimulation): + """Extract all solutions from the simulation.""" + solutions = [] + + try: + # Iterate through all solutions in the simulation + # Solutions are accessed via FindObject with pattern "Solution[name]" + # But we can also iterate if the simulation has a solutions collection + + # Try to get solution info by iterating through known solution names + # Common patterns: "Solution 1", "Solution 2", etc. + for i in range(1, 20): # Check up to 20 solutions + sol_name = f"Solution {i}" + try: + sol = simSimulation.FindObject(f"Solution[{sol_name}]") + if sol: + sol_info = {"name": sol_name, "type": str(type(sol).__name__), "properties": {}} + + # Try to get common properties + try: + sol_info["properties"]["solver_type"] = ( + str(sol.SolverType) if hasattr(sol, "SolverType") else None + ) + except: + pass + + try: + sol_info["properties"]["analysis_type"] = ( + str(sol.AnalysisType) if hasattr(sol, "AnalysisType") else None + ) + except: + pass + + solutions.append(sol_info) + except: + # Solution not found, stop looking + if i > 5: # Give a few tries in case there are gaps + break + continue + + except Exception as e: + solutions.append({"error": str(e)}) + + return solutions + + +def get_boundary_conditions(simSimulation, workPart): + """Extract boundary conditions from the simulation.""" + bcs = {"constraints": [], "loads": [], "total_count": 0} + + try: + # Try to access BC collections through the simulation object + # BCs are typically stored in the simulation's children + + # Look for constraint groups + constraint_names = [ + "Constraint Group[1]", + "Constraint Group[2]", + "Constraint Group[3]", + "SPC[1]", + "SPC[2]", + "SPC[3]", + "Fixed Constraint[1]", + "Fixed Constraint[2]", + ] + + for name in constraint_names: + try: + obj = simSimulation.FindObject(name) + if obj: + bc_info = { + "name": name, + "type": str(type(obj).__name__), + } + bcs["constraints"].append(bc_info) + except: + pass + + # Look for load groups + load_names = [ + "Load Group[1]", + "Load Group[2]", + "Load Group[3]", + "Force[1]", + "Force[2]", + "Pressure[1]", + "Pressure[2]", + "Enforced Displacement[1]", + "Enforced Displacement[2]", + ] + + for name in load_names: + try: + obj = simSimulation.FindObject(name) + if obj: + load_info = { + "name": name, + "type": str(type(obj).__name__), + } + bcs["loads"].append(load_info) + except: + pass + + bcs["total_count"] = len(bcs["constraints"]) + len(bcs["loads"]) + + except Exception as e: + bcs["error"] = str(e) + + return bcs + + +def get_sim_part_info(workPart): + """Extract SIM part-level information.""" + info = {"name": None, "full_path": None, "type": None, "fem_parts": [], "component_count": 0} + + try: + info["name"] = workPart.Name + info["full_path"] = workPart.FullPath if hasattr(workPart, "FullPath") else None + info["type"] = str(type(workPart).__name__) + + # Check for component assembly (assembly FEM) + try: + root = workPart.ComponentAssembly.RootComponent + if root: + info["is_assembly"] = True + # Count components + try: + children = root.GetChildren() + info["component_count"] = len(children) if children else 0 + + # Get component names + components = [] + for child in children[:10]: # Limit to first 10 + try: + comp_info = { + "name": child.Name if hasattr(child, "Name") else str(child), + "type": str(type(child).__name__), + } + components.append(comp_info) + except: + pass + info["components"] = components + except: + pass + except: + info["is_assembly"] = False + + except Exception as e: + info["error"] = str(e) + + return info + + +def get_cae_session_info(theSession): + """Get CAE session information.""" + cae_info = {"active_sim_part": None, "active_fem_part": None, "solver_types": []} + + try: + # Get CAE session + caeSession = theSession.GetExportedObject("NXOpen.CAE.CaeSession") + if caeSession: + cae_info["cae_session_exists"] = True + except: + cae_info["cae_session_exists"] = False + + return cae_info + + +def explore_simulation_tree(simSimulation, workPart): + """Explore the simulation tree structure.""" + tree_info = {"simulation_objects": [], "found_types": set()} + + # Try to enumerate objects in the simulation + # This is exploratory - we don't know the exact API + + try: + # Try common child object patterns + patterns = [ + # Solutions + "Solution[Solution 1]", + "Solution[Solution 2]", + "Solution[SOLUTION 1]", + # Subcases + "Subcase[Subcase 1]", + "Subcase[Subcase - Static 1]", + # Loads/BCs + "LoadSet[LoadSet 1]", + "ConstraintSet[ConstraintSet 1]", + "BoundaryCondition[1]", + # FEM reference + "FemPart", + "AssyFemPart", + ] + + for pattern in patterns: + try: + obj = simSimulation.FindObject(pattern) + if obj: + obj_info = {"pattern": pattern, "type": str(type(obj).__name__), "found": True} + tree_info["simulation_objects"].append(obj_info) + tree_info["found_types"].add(str(type(obj).__name__)) + except: + pass + + tree_info["found_types"] = list(tree_info["found_types"]) + + except Exception as e: + tree_info["error"] = str(e) + + return tree_info + + +def main(args): + """Main entry point for NX journal.""" + + if len(args) < 1: + print("ERROR: No .sim file path provided") + print("Usage: run_journal.exe introspect_sim.py [output_dir]") + return False + + sim_file_path = args[0] + output_dir = args[1] if len(args) > 1 else os.path.dirname(sim_file_path) + sim_filename = os.path.basename(sim_file_path) + + print(f"[INTROSPECT-SIM] " + "=" * 60) + print(f"[INTROSPECT-SIM] NX SIMULATION INTROSPECTION") + print(f"[INTROSPECT-SIM] " + "=" * 60) + print(f"[INTROSPECT-SIM] SIM File: {sim_filename}") + print(f"[INTROSPECT-SIM] Output: {output_dir}") + + results = { + "sim_file": sim_filename, + "sim_path": sim_file_path, + "success": False, + "error": None, + "part_info": {}, + "solutions": [], + "boundary_conditions": {}, + "tree_structure": {}, + "cae_info": {}, + } + + try: + theSession = NXOpen.Session.GetSession() + + # Set load options + working_dir = os.path.dirname(sim_file_path) + 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 + + # Open the SIM file + print(f"[INTROSPECT-SIM] Opening SIM file...") + basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay( + sim_file_path, NXOpen.DisplayPartOption.AllowAdditional + ) + partLoadStatus.Dispose() + + workPart = theSession.Parts.Work + print(f"[INTROSPECT-SIM] Loaded: {workPart.Name}") + + # Switch to SFEM application + try: + theSession.ApplicationSwitchImmediate("UG_APP_SFEM") + print(f"[INTROSPECT-SIM] Switched to SFEM application") + except Exception as e: + print(f"[INTROSPECT-SIM] Note: Could not switch to SFEM: {e}") + + # Get part info + print(f"[INTROSPECT-SIM] Extracting part info...") + results["part_info"] = get_sim_part_info(workPart) + print(f"[INTROSPECT-SIM] Part: {results['part_info'].get('name')}") + print(f"[INTROSPECT-SIM] Is Assembly: {results['part_info'].get('is_assembly', False)}") + + # Get simulation object + print(f"[INTROSPECT-SIM] Finding Simulation object...") + try: + simSimulation = workPart.FindObject("Simulation") + print(f"[INTROSPECT-SIM] Found Simulation object: {type(simSimulation).__name__}") + + # Get solutions + print(f"[INTROSPECT-SIM] Extracting solutions...") + results["solutions"] = get_solutions(simSimulation) + print(f"[INTROSPECT-SIM] Found {len(results['solutions'])} solutions") + + # Get boundary conditions + print(f"[INTROSPECT-SIM] Extracting boundary conditions...") + results["boundary_conditions"] = get_boundary_conditions(simSimulation, workPart) + print( + f"[INTROSPECT-SIM] Found {results['boundary_conditions'].get('total_count', 0)} BCs" + ) + + # Explore tree structure + print(f"[INTROSPECT-SIM] Exploring simulation tree...") + results["tree_structure"] = explore_simulation_tree(simSimulation, workPart) + print( + f"[INTROSPECT-SIM] Found types: {results['tree_structure'].get('found_types', [])}" + ) + + except Exception as e: + print(f"[INTROSPECT-SIM] WARNING: Could not find Simulation object: {e}") + results["simulation_object_error"] = str(e) + + # Get CAE session info + print(f"[INTROSPECT-SIM] Getting CAE session info...") + results["cae_info"] = get_cae_session_info(theSession) + + # List all loaded parts + print(f"[INTROSPECT-SIM] Listing loaded parts...") + loaded_parts = [] + for part in theSession.Parts: + try: + loaded_parts.append( + { + "name": part.Name, + "type": str(type(part).__name__), + "leaf": part.Leaf if hasattr(part, "Leaf") else None, + } + ) + except: + pass + results["loaded_parts"] = loaded_parts + print(f"[INTROSPECT-SIM] {len(loaded_parts)} parts loaded") + + results["success"] = True + print(f"[INTROSPECT-SIM] ") + print(f"[INTROSPECT-SIM] INTROSPECTION COMPLETE!") + print(f"[INTROSPECT-SIM] " + "=" * 60) + + except Exception as e: + results["error"] = str(e) + results["success"] = False + print(f"[INTROSPECT-SIM] FATAL ERROR: {e}") + import traceback + + traceback.print_exc() + + # Write results + output_file = os.path.join(output_dir, "_introspection_sim.json") + with open(output_file, "w") as f: + json.dump(results, f, indent=2) + print(f"[INTROSPECT-SIM] Results written to: {output_file}") + + return results["success"] + + +if __name__ == "__main__": + main(sys.argv[1:])