- 8-agent OpenClaw cluster (Manager, Tech-Lead, Secretary, Auditor, Optimizer, Study-Builder, NX-Expert, Webster) - Orchestration engine: orchestrate.py (sync delegation + handoffs) - Workflow engine: YAML-defined multi-step pipelines - Agent workspaces: SOUL.md, AGENTS.md, MEMORY.md per agent - Shared skills: delegate, orchestrate, atomizer-protocols - Capability registry (AGENTS_REGISTRY.json) - Cluster management: cluster.sh, systemd template - All secrets replaced with env var references
927 lines
24 KiB
Markdown
927 lines
24 KiB
Markdown
# 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.
|