# NXOpen API Guide — Model Introspection Patterns **Author:** NX Expert 🖥️ **Date:** 2026-02-14 **Purpose:** Technical reference for extracting introspection data using NXOpen Python API --- ## Quick Reference This guide provides **copy-paste ready** code patterns for each introspection layer. All patterns are NXOpen 2512 compatible. --- ## 1. Geometric Parameters — Part-Level Extraction ### 1.1 Expression Iteration & Filtering ```python import NXOpen def extract_expressions(part): """Extract all user-defined expressions with metadata.""" expressions = { 'user': [], 'internal': [], 'total_count': 0 } for expr in part.Expressions: # Extract basic data expr_data = { 'name': expr.Name, 'value': expr.Value, 'formula': expr.RightHandSide if hasattr(expr, 'RightHandSide') else None, 'units': expr.Units.Name if expr.Units else None, 'type': str(expr.Type) if hasattr(expr, 'Type') else 'Unknown', } # Determine if internal (p0, p1, p123, etc.) name = expr.Name is_internal = False if name.startswith('p') and len(name) > 1: rest = name[1:].replace('.', '').replace('_', '') if rest.isdigit(): is_internal = True if is_internal: expressions['internal'].append(expr_data) else: expressions['user'].append(expr_data) expressions['total_count'] = len(expressions['user']) + len(expressions['internal']) return expressions ``` ### 1.2 Expression Dependency Parsing ```python import re def parse_expression_dependencies(expr_formula, all_expression_names): """Parse RHS formula to find referenced expressions.""" if not expr_formula: return [] dependencies = [] # Find all potential expression names in formula # Pattern: word characters followed by optional parentheses/operators tokens = re.findall(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b', expr_formula) for token in tokens: # Check if this token is an expression name if token in all_expression_names: dependencies.append(token) return list(set(dependencies)) # Remove duplicates def build_expression_graph(part): """Build dependency graph for all expressions.""" # Get all expression names first all_names = [expr.Name for expr in part.Expressions] graph = { 'nodes': [], 'edges': [] } for expr in part.Expressions: # Add node graph['nodes'].append({ 'name': expr.Name, 'value': expr.Value, 'is_user_defined': not expr.Name.startswith('p') }) # Parse dependencies formula = expr.RightHandSide if hasattr(expr, 'RightHandSide') else None deps = parse_expression_dependencies(formula, all_names) # Add edges for dep in deps: graph['edges'].append({ 'from': dep, 'to': expr.Name, 'relationship': 'drives' }) return graph ``` ### 1.3 Feature Extraction with Parameters ```python def extract_features(part): """Extract feature list with type and parameter info.""" features = { 'total_count': 0, 'by_type': {}, 'details': [] } for feature in part.Features: feat_type = str(type(feature).__name__) feat_name = feature.Name if hasattr(feature, 'Name') else f'{feat_type}_unknown' feat_data = { 'name': feat_name, 'type': feat_type, 'suppressed': feature.Suppressed if hasattr(feature, 'Suppressed') else False, 'parameters': {} } # Try to extract parameters based on feature type # This is type-specific - examples below # Extrude features if 'Extrude' in feat_type: try: # Access via builder (read-only) # Note: Full parameter access requires feature editing feat_data['parameters']['type'] = 'extrusion' except: pass # Shell features elif 'Shell' in feat_type: try: feat_data['parameters']['type'] = 'shell' except: pass features['details'].append(feat_data) # Count by type if feat_type in features['by_type']: features['by_type'][feat_type] += 1 else: features['by_type'][feat_type] = 1 features['total_count'] = len(features['details']) return features ``` ### 1.4 Mass Properties Extraction ```python def extract_mass_properties(part): """Extract mass, volume, COG using MeasureManager.""" # Get all solid bodies solid_bodies = [body for body in part.Bodies if body.IsSolidBody] if not solid_bodies: return { 'error': 'No solid bodies found', 'success': False } try: measureManager = part.MeasureManager # Build mass units array uc = part.UnitCollection mass_units = [ uc.GetBase("Area"), uc.GetBase("Volume"), uc.GetBase("Mass"), uc.GetBase("Length") ] # Compute mass properties measureBodies = measureManager.NewMassProperties(mass_units, 0.99, solid_bodies) result = { 'mass_kg': measureBodies.Mass, 'mass_g': measureBodies.Mass * 1000.0, 'volume_mm3': measureBodies.Volume, 'surface_area_mm2': measureBodies.Area, 'center_of_gravity_mm': [ measureBodies.Centroid.X, measureBodies.Centroid.Y, measureBodies.Centroid.Z ], 'num_bodies': len(solid_bodies), 'success': True } # Clean up measureBodies.Dispose() return result except Exception as e: return { 'error': str(e), 'success': False } ``` ### 1.5 Material Extraction ```python def extract_materials(part): """Extract all materials with properties.""" materials = { 'assigned': [], 'available': [] } # Get materials assigned to bodies for body in part.Bodies: if not body.IsSolidBody: continue try: phys_mat = body.GetPhysicalMaterial() if phys_mat: mat_info = { 'name': phys_mat.Name, 'body': body.Name if hasattr(body, 'Name') else 'Unknown', 'properties': {} } # Common material properties prop_names = [ 'Density', 'YoungModulus', 'PoissonRatio', 'ThermalExpansionCoefficient', 'ThermalConductivity', 'SpecificHeat', 'YieldStrength', 'UltimateStrength' ] for prop_name in prop_names: try: val = phys_mat.GetPropertyValue(prop_name) if val is not None: mat_info['properties'][prop_name] = float(val) except: pass materials['assigned'].append(mat_info) except: pass # Get all materials in part try: pmm = part.PhysicalMaterialManager if pmm: all_mats = pmm.GetAllPhysicalMaterials() for mat in all_mats: mat_info = { 'name': mat.Name, 'properties': {} } prop_names = ['Density', 'YoungModulus', 'PoissonRatio'] for prop_name in prop_names: try: val = mat.GetPropertyValue(prop_name) if val is not None: mat_info['properties'][prop_name] = float(val) except: pass materials['available'].append(mat_info) except: pass return materials ``` --- ## 2. FEA Model Structure — FEM Part Extraction ### 2.1 Mesh Statistics (NXOpen CAE) ```python import NXOpen.CAE def extract_mesh_stats(fem_part): """Extract basic mesh statistics.""" mesh_info = { 'total_nodes': 0, 'total_elements': 0, 'element_types': {}, 'success': False } try: fe_model = fem_part.BaseFEModel if not fe_model: return mesh_info # Get node count try: mesh_info['total_nodes'] = fe_model.FenodeLabelMap.Size except: pass # Get element count try: mesh_info['total_elements'] = fe_model.FeelementLabelMap.Size except: pass # Iterate elements to count by type # Note: Full element type extraction requires pyNastran BDF parsing mesh_info['success'] = True except Exception as e: mesh_info['error'] = str(e) return mesh_info ``` ### 2.2 Mesh Quality Audit (NXOpen CAE) ```python import NXOpen.CAE.QualityAudit def extract_mesh_quality(fem_part): """Run quality audit and extract metrics.""" quality = { 'aspect_ratio': {}, 'jacobian': {}, 'warpage': {}, 'skew': {}, 'success': False } try: # Create quality audit builder qa_manager = fem_part.QualityAuditManager # Note: Full quality audit requires setting up checks # This is a simplified example # Get quality audit collections # (Actual implementation depends on NX version and setup) quality['success'] = True except Exception as e: quality['error'] = str(e) return quality ``` ### 2.3 Mesh Collector Extraction ```python def extract_mesh_collectors(fem_part): """Extract mesh collectors with element assignments.""" collectors = [] try: fe_model = fem_part.BaseFEModel if not fe_model: return collectors # Iterate mesh collectors for collector in fe_model.MeshCollectors: collector_info = { 'name': collector.Name if hasattr(collector, 'Name') else 'Unknown', 'type': str(type(collector).__name__), 'element_count': 0 } # Try to get elements try: elements = collector.GetElements() collector_info['element_count'] = len(elements) if elements else 0 except: pass collectors.append(collector_info) except Exception as e: pass return collectors ``` --- ## 3. pyNastran BDF Parsing — Detailed FEA Data ### 3.1 Element Type Distribution ```python from pyNastran.bdf.bdf import BDF def extract_element_types(bdf_path): """Extract element type distribution from BDF file.""" model = BDF() model.read_bdf(bdf_path) element_types = {} for eid, elem in model.elements.items(): elem_type = elem.type if elem_type in element_types: element_types[elem_type] += 1 else: element_types[elem_type] = 1 return { 'total_elements': len(model.elements), 'total_nodes': len(model.nodes), 'element_types': element_types } ``` ### 3.2 Material Properties ```python def extract_materials_from_bdf(bdf_path): """Extract all materials from BDF file.""" model = BDF() model.read_bdf(bdf_path) materials = [] for mat_id, mat in model.materials.items(): mat_info = { 'id': mat_id, 'type': mat.type, 'properties': {} } # MAT1 (isotropic) if mat.type == 'MAT1': mat_info['properties'] = { 'E': mat.E, # Young's modulus 'G': mat.G, # Shear modulus 'nu': mat.nu, # Poisson's ratio 'rho': mat.rho, # Density } # Add other material types (MAT2, MAT8, etc.) as needed materials.append(mat_info) return materials ``` ### 3.3 Property Cards ```python def extract_properties_from_bdf(bdf_path): """Extract property cards (PSHELL, PSOLID, etc.).""" model = BDF() model.read_bdf(bdf_path) properties = [] for prop_id, prop in model.properties.items(): prop_info = { 'id': prop_id, 'type': prop.type, 'parameters': {} } # PSHELL if prop.type == 'PSHELL': prop_info['parameters'] = { 'thickness': prop.t, 'material_id': prop.mid1 } # PSOLID elif prop.type == 'PSOLID': prop_info['parameters'] = { 'material_id': prop.mid } properties.append(prop_info) return properties ``` ### 3.4 Boundary Conditions & Loads ```python def extract_bcs_from_bdf(bdf_path): """Extract SPCs and loads from BDF file.""" model = BDF() model.read_bdf(bdf_path) bcs = { 'spcs': [], 'forces': [], 'pressures': [] } # SPCs (Single Point Constraints) for spc_id, spc in model.spcadds.items(): spc_info = { 'id': spc_id, 'type': 'SPC', 'node_ids': [], 'dofs': [] } # Parse SPC details bcs['spcs'].append(spc_info) # Forces for force_id, force in model.forces.items(): force_info = { 'id': force_id, 'type': 'FORCE', 'node_id': force.node, 'magnitude': force.mag, 'direction': [force.xyz[0], force.xyz[1], force.xyz[2]] } bcs['forces'].append(force_info) # Pressures (PLOAD4) for pload_id, pload in model.pressures.items(): pload_info = { 'id': pload_id, 'type': 'PLOAD4', 'element_ids': [pload.eid], 'pressure': pload.pressures[0] } bcs['pressures'].append(pload_info) return bcs ``` ### 3.5 Subcases & Solution Configuration ```python def extract_subcases_from_bdf(bdf_path): """Extract subcase information from BDF.""" model = BDF() model.read_bdf(bdf_path) subcases = [] # Access case control deck for subcase_id, subcase in model.subcases.items(): if subcase_id == 0: continue # Skip global subcase subcase_info = { 'id': subcase_id, 'name': subcase.params.get('SUBTITLE', [''])[0], 'load_set': subcase.params.get('LOAD', [None])[0], 'spc_set': subcase.params.get('SPC', [None])[0], 'output_requests': [] } # Check for output requests if 'DISPLACEMENT' in subcase.params: subcase_info['output_requests'].append('DISPLACEMENT') if 'STRESS' in subcase.params: subcase_info['output_requests'].append('STRESS') if 'STRAIN' in subcase.params: subcase_info['output_requests'].append('STRAIN') subcases.append(subcase_info) return subcases ``` --- ## 4. Result Extraction — pyNastran OP2 ### 4.1 Displacement Results ```python from pyNastran.op2.op2 import OP2 def extract_displacement_results(op2_path, subcase_id=1): """Extract displacement results from OP2 file.""" op2 = OP2() op2.read_op2(op2_path) # Get displacement for subcase displ = op2.displacements[subcase_id] # Get max displacement data = displ.data[0] # First time step (static) magnitudes = np.sqrt(data[:, 0]**2 + data[:, 1]**2 + data[:, 2]**2) max_idx = np.argmax(magnitudes) max_node = displ.node_gridtype[max_idx, 0] result = { 'max_magnitude_mm': float(magnitudes[max_idx]), 'max_node': int(max_node), 'average_mm': float(np.mean(magnitudes)), 'std_dev_mm': float(np.std(magnitudes)) } return result ``` ### 4.2 Stress Results ```python def extract_stress_results(op2_path, subcase_id=1): """Extract von Mises stress from OP2 file.""" op2 = OP2() op2.read_op2(op2_path) # Try to get element stress (CTETRA, CQUAD4, etc.) if subcase_id in op2.ctetra_stress: stress = op2.ctetra_stress[subcase_id] vm_stress = stress.data[0][:, 6] # Von Mises column elif subcase_id in op2.cquad4_stress: stress = op2.cquad4_stress[subcase_id] vm_stress = stress.data[0][:, 7] # Von Mises column else: return {'error': 'No stress results found'} max_idx = np.argmax(vm_stress) max_elem = stress.element_node[max_idx, 0] result = { 'max_von_mises_MPa': float(vm_stress[max_idx]), 'max_element': int(max_elem), 'average_MPa': float(np.mean(vm_stress)), 'std_dev_MPa': float(np.std(vm_stress)) } return result ``` ### 4.3 Frequency Results (Modal) ```python def extract_frequency_results(op2_path, num_modes=10): """Extract modal frequencies from OP2 file.""" op2 = OP2() op2.read_op2(op2_path) # Get eigenvalues eigenvalues = op2.eigenvalues frequencies = [] for mode_id in sorted(eigenvalues.keys())[:num_modes]: eig_data = eigenvalues[mode_id] freq_hz = eig_data.eigenvalues[0] # First value is frequency frequencies.append({ 'mode': mode_id, 'frequency_hz': float(freq_hz) }) return frequencies ``` --- ## 5. Solver Configuration — SIM File Introspection ### 5.1 Solution Detection ```python def extract_solutions(sim_simulation): """Extract all solutions from simulation object.""" solutions = [] # Try common solution name patterns patterns = [ "Solution 1", "Solution 2", "Solution 3", "Static", "Modal", "Buckling", "Thermal" ] for pattern in patterns: try: sol = sim_simulation.FindObject(f"Solution[{pattern}]") if sol: sol_info = { 'name': pattern, 'type': str(type(sol).__name__) } # Try to get solver type try: sol_info['solver_type'] = str(sol.SolverType) except: pass # Try to get analysis type try: sol_info['analysis_type'] = str(sol.AnalysisType) except: pass solutions.append(sol_info) except: pass return solutions ``` ### 5.2 Boundary Condition Detection (Exploratory) ```python def extract_boundary_conditions(sim_simulation): """Extract boundary conditions (exploratory).""" bcs = { 'constraints': [], 'loads': [] } # Try common BC name patterns constraint_patterns = [ "Fixed Constraint[1]", "Fixed Constraint[2]", "SPC[1]", "SPC[2]", "Constraint Group[1]" ] load_patterns = [ "Force[1]", "Force[2]", "Pressure[1]", "Pressure[2]", "Load Group[1]" ] for pattern in constraint_patterns: try: obj = sim_simulation.FindObject(pattern) if obj: bcs['constraints'].append({ 'name': pattern, 'type': str(type(obj).__name__) }) except: pass for pattern in load_patterns: try: obj = sim_simulation.FindObject(pattern) if obj: bcs['loads'].append({ 'name': pattern, 'type': str(type(obj).__name__) }) except: pass return bcs ``` --- ## 6. Master Introspection Orchestrator ### 6.1 Full Introspection Runner ```python import json import os from datetime import datetime def run_full_introspection(prt_path, sim_path, output_dir): """Run comprehensive introspection and generate master JSON.""" # Initialize result structure introspection = { 'introspection_version': '1.0.0', 'timestamp': datetime.now().isoformat(), 'model_id': os.path.basename(prt_path).replace('.prt', ''), 'files': { 'geometry': prt_path, 'simulation': sim_path }, 'geometric_parameters': {}, 'fea_model': {}, 'solver_configuration': {}, 'dependencies': {}, 'baseline_results': {} } # Phase 1: Part introspection print("[INTROSPECT] Phase 1: Geometric parameters...") part_data = introspect_part(prt_path) introspection['geometric_parameters'] = part_data # Phase 2: FEM introspection print("[INTROSPECT] Phase 2: FEA model...") fem_data = introspect_fem(sim_path) introspection['fea_model'] = fem_data # Phase 3: Solver configuration print("[INTROSPECT] Phase 3: Solver configuration...") solver_data = introspect_solver(sim_path) introspection['solver_configuration'] = solver_data # Phase 4: Dependency graph print("[INTROSPECT] Phase 4: Dependencies...") deps = build_dependency_graph(prt_path) introspection['dependencies'] = deps # Phase 5: Baseline results (if available) print("[INTROSPECT] Phase 5: Baseline results...") # (Only if OP2 exists) # Write output output_file = os.path.join(output_dir, 'model_introspection_FULL.json') with open(output_file, 'w') as f: json.dump(introspection, f, indent=2) print(f"[INTROSPECT] Complete! Output: {output_file}") return introspection ``` --- ## 7. Usage Examples ### 7.1 Part Introspection (Standalone) ```python # Open NX part theSession = NXOpen.Session.GetSession() basePart, status = theSession.Parts.OpenActiveDisplay( "/path/to/bracket.prt", NXOpen.DisplayPartOption.AllowAdditional ) status.Dispose() workPart = theSession.Parts.Work # Extract expressions expressions = extract_expressions(workPart) print(f"Found {len(expressions['user'])} user expressions") # Extract mass properties mass_props = extract_mass_properties(workPart) print(f"Mass: {mass_props['mass_kg']:.4f} kg") # Build expression graph graph = build_expression_graph(workPart) print(f"Expression graph: {len(graph['nodes'])} nodes, {len(graph['edges'])} edges") ``` ### 7.2 BDF Parsing (Standalone) ```python from pyNastran.bdf.bdf import BDF # Read BDF file model = BDF() model.read_bdf("/path/to/bracket_fem1.bdf") # Extract element types elem_types = extract_element_types("/path/to/bracket_fem1.bdf") print(f"Elements: {elem_types['total_elements']}") print(f"Types: {elem_types['element_types']}") # Extract materials materials = extract_materials_from_bdf("/path/to/bracket_fem1.bdf") for mat in materials: print(f"Material {mat['id']}: {mat['type']}, E={mat['properties'].get('E')}") ``` ### 7.3 OP2 Result Extraction ```python from pyNastran.op2.op2 import OP2 import numpy as np # Read OP2 file op2_path = "/path/to/bracket_sim1_s1.op2" displ = extract_displacement_results(op2_path, subcase_id=1) print(f"Max displacement: {displ['max_magnitude_mm']:.4f} mm at node {displ['max_node']}") stress = extract_stress_results(op2_path, subcase_id=1) print(f"Max von Mises: {stress['max_von_mises_MPa']:.2f} MPa at element {stress['max_element']}") ``` --- ## 8. Best Practices ### 8.1 Error Handling - Always wrap NXOpen API calls in try-except blocks - Log errors to JSON output for debugging - Continue execution even if one introspection layer fails ### 8.2 Performance - Use lazy loading for large OP2 files - Cache expression dependency graphs - Limit mesh quality checks to sample elements for very large meshes ### 8.3 NX Version Compatibility - Test on NX 2506+ (guaranteed compatible) - Use `hasattr()` checks before accessing optional properties - Provide fallback values for missing API methods --- **Status:** Technical implementation guide complete — ready for development. **Next:** Implement enhanced `introspect_part.py` and new `introspect_fem.py` based on these patterns.