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>
29 KiB
SYS_12: Extractor Library
Overview
The Extractor Library provides centralized, reusable functions for extracting physics results from FEA output files. Always use these extractors instead of writing custom extraction code in studies.
Key Principle: If you're writing >20 lines of extraction code in run_optimization.py, stop and check this library first.
When to Use
| Trigger | Action |
|---|---|
| Need to extract displacement | Use E1 extract_displacement |
| Need to extract frequency | Use E2 extract_frequency |
| Need to extract stress | Use E3 extract_solid_stress |
| Need to extract mass | Use E4 or E5 |
| Need Zernike/wavefront | Use E8, E9, or E10 |
| Need custom physics | Check library first, then EXT_01 |
Quick Reference
| ID | Physics | Function | Input | Output |
|---|---|---|---|---|
| E1 | Displacement | extract_displacement() |
.op2 | mm |
| E2 | Frequency | extract_frequency() |
.op2 | Hz |
| E3 | Von Mises Stress | extract_solid_stress() |
.op2 | MPa |
| E4 | BDF Mass | extract_mass_from_bdf() |
.bdf/.dat | kg |
| E5 | CAD Expression Mass | extract_mass_from_expression() |
.prt | kg |
| E6 | Field Data | FieldDataExtractor() |
.fld/.csv | varies |
| E7 | Stiffness | StiffnessCalculator() |
.fld + .op2 | N/mm |
| E8 | Zernike WFE | extract_zernike_from_op2() |
.op2 + .bdf | nm |
| E9 | Zernike Relative | extract_zernike_relative_rms() |
.op2 + .bdf | nm |
| E10 | Zernike Builder | ZernikeObjectiveBuilder() |
.op2 | nm |
| E11 | Part Mass & Material | extract_part_mass_material() |
.prt | kg + dict |
| Phase 2 (2025-12-06) | ||||
| E12 | Principal Stress | extract_principal_stress() |
.op2 | MPa |
| E13 | Strain Energy | extract_strain_energy() |
.op2 | J |
| E14 | SPC Forces | extract_spc_forces() |
.op2 | N |
| Phase 3 (2025-12-06) | ||||
| E15 | Temperature | extract_temperature() |
.op2 | K/°C |
| E16 | Thermal Gradient | extract_temperature_gradient() |
.op2 | K/mm |
| E17 | Heat Flux | extract_heat_flux() |
.op2 | W/mm² |
| E18 | Modal Mass | extract_modal_mass() |
.f06 | kg |
| Phase 4 (2025-12-19) | ||||
| E19 | Part Introspection | introspect_part() |
.prt | dict |
| Phase 5 (2025-12-22) | ||||
| E20 | Zernike Analytic (Parabola) | extract_zernike_analytic() |
.op2 + .bdf | nm |
| E21 | Zernike Method Comparison | compare_zernike_methods() |
.op2 + .bdf | dict |
| E22 | Zernike OPD (RECOMMENDED) | extract_zernike_opd() |
.op2 + .bdf | nm |
Extractor Details
E1: Displacement Extraction
Module: optimization_engine.extractors.extract_displacement
from optimization_engine.extractors.extract_displacement import extract_displacement
result = extract_displacement(op2_file, subcase=1)
# Returns: {
# 'max_displacement': float, # mm
# 'max_disp_node': int,
# 'max_disp_x': float,
# 'max_disp_y': float,
# 'max_disp_z': float
# }
max_displacement = result['max_displacement'] # mm
E2: Frequency Extraction
Module: optimization_engine.extractors.extract_frequency
from optimization_engine.extractors.extract_frequency import extract_frequency
result = extract_frequency(op2_file, subcase=1, mode_number=1)
# Returns: {
# 'frequency': float, # Hz
# 'mode_number': int,
# 'eigenvalue': float,
# 'all_frequencies': list # All modes
# }
frequency = result['frequency'] # Hz
E3: Von Mises Stress Extraction
Module: optimization_engine.extractors.extract_von_mises_stress
from optimization_engine.extractors.extract_von_mises_stress import extract_solid_stress
# RECOMMENDED: Check ALL solid element types (returns max across all)
result = extract_solid_stress(op2_file, subcase=1)
# Or specify single element type
result = extract_solid_stress(op2_file, subcase=1, element_type='chexa')
# Returns: {
# 'max_von_mises': float, # MPa (auto-converted from kPa)
# 'max_stress_element': int,
# 'element_type': str, # e.g., 'CHEXA', 'CTETRA'
# 'units': 'MPa'
# }
max_stress = result['max_von_mises'] # MPa
IMPORTANT (Updated 2026-01-22):
- By default, checks ALL solid types: CTETRA, CHEXA, CPENTA, CPYRAM
- CHEXA elements often have highest stress (not CTETRA!)
- Auto-converts from kPa to MPa (NX kg-mm-s unit system outputs kPa)
- Returns Elemental Nodal stress (peak), not Elemental Centroid (averaged)
E4: BDF Mass Extraction
Module: optimization_engine.extractors.extract_mass_from_bdf
from optimization_engine.extractors import extract_mass_from_bdf
result = extract_mass_from_bdf(bdf_file)
# Returns: {
# 'total_mass': float, # kg (primary key)
# 'mass_kg': float, # kg
# 'mass_g': float, # grams
# 'cg': [x, y, z], # center of gravity
# 'num_elements': int
# }
mass_kg = result['mass_kg'] # kg
Note: Uses BDFMassExtractor internally. Reads mass from element geometry and material density in BDF/DAT file. NX kg-mm-s unit system - mass is directly in kg.
E5: CAD Expression Mass
Module: optimization_engine.extractors.extract_mass_from_expression
from optimization_engine.extractors.extract_mass_from_expression import extract_mass_from_expression
mass_kg = extract_mass_from_expression(model_file, expression_name="p173") # kg
Note: Requires _temp_mass.txt to be written by solve journal. Uses NX expression system.
E11: Part Mass & Material Extraction
Module: optimization_engine.extractors.extract_part_mass_material
Extracts mass, volume, surface area, center of gravity, and material properties directly from NX .prt files using NXOpen.MeasureManager.
Prerequisites: Run the NX journal first to create the temp file:
run_journal.exe nx_journals/extract_part_mass_material.py model.prt
from optimization_engine.extractors import extract_part_mass_material, extract_part_mass
# Full extraction with all properties
result = extract_part_mass_material(prt_file)
# Returns: {
# 'mass_kg': float, # Mass in kg
# 'mass_g': float, # Mass in grams
# 'volume_mm3': float, # Volume in mm^3
# 'surface_area_mm2': float, # Surface area in mm^2
# 'center_of_gravity_mm': [x, y, z], # CoG in mm
# 'moments_of_inertia': {'Ixx', 'Iyy', 'Izz', 'unit'}, # or None
# 'material': {
# 'name': str or None, # Material name if assigned
# 'density': float or None, # Density in kg/mm^3
# 'density_unit': str
# },
# 'num_bodies': int
# }
mass = result['mass_kg'] # kg
material_name = result['material']['name'] # e.g., "Aluminum_6061"
# Simple mass-only extraction
mass_kg = extract_part_mass(prt_file) # kg
Class-based version for caching:
from optimization_engine.extractors import PartMassExtractor
extractor = PartMassExtractor(prt_file)
mass = extractor.mass_kg # Extracts and caches
material = extractor.material_name
NX Open APIs Used (by journal):
NXOpen.MeasureManager.NewMassProperties()NXOpen.MeasureBodiesNXOpen.Body.GetBodies()NXOpen.PhysicalMaterial
IMPORTANT - Mass Accuracy Note:
Always prefer E11 (geometry-based) over E4 (BDF-based) for mass extraction.
Testing on hex-dominant meshes with tet/pyramid fill elements revealed that:
- E11 from .prt: 97.66 kg (accurate - matches NX GUI)
- E4 pyNastran get_mass_breakdown(): 90.73 kg (~7% under-reported)
- *E4 pyNastran sum(elem.Volume())rho: 100.16 kg (~2.5% over-reported)
The
get_mass_breakdown()function in pyNastran has known issues with mixed-element meshes (CHEXA + CPENTA + CPYRAM + CTETRA). Use E11 with the NX journal for reliable mass values. Only use E4 if material properties are overridden at FEM level.
E6: Field Data Extraction
Module: optimization_engine.extractors.field_data_extractor
from optimization_engine.extractors.field_data_extractor import FieldDataExtractor
extractor = FieldDataExtractor(
field_file="results.fld",
result_column="Temperature",
aggregation="max" # or "min", "mean", "std"
)
result = extractor.extract()
# Returns: {
# 'value': float,
# 'stats': dict
# }
E7: Stiffness Calculation
Module: optimization_engine.extractors.stiffness_calculator
from optimization_engine.extractors.stiffness_calculator import StiffnessCalculator
calculator = StiffnessCalculator(
field_file=field_file,
op2_file=op2_file,
force_component="FZ",
displacement_component="UZ"
)
result = calculator.calculate()
# Returns: {
# 'stiffness': float, # N/mm
# 'displacement': float,
# 'force': float
# }
Simple Alternative (when force is known):
applied_force = 1000.0 # N - MUST MATCH MODEL'S APPLIED LOAD
stiffness = applied_force / max(abs(max_displacement), 1e-6) # N/mm
E8: Zernike Wavefront Error (Single Subcase)
Module: optimization_engine.extractors.extract_zernike
from optimization_engine.extractors.extract_zernike import extract_zernike_from_op2
result = extract_zernike_from_op2(
op2_file,
bdf_file=None, # Auto-detect from op2 location
subcase="20", # Subcase label (e.g., "20" = 20 deg elevation)
displacement_unit="mm"
)
# Returns: {
# 'global_rms_nm': float, # Total surface RMS in nm
# 'filtered_rms_nm': float, # RMS with low orders removed
# 'coefficients': list, # 50 Zernike coefficients
# 'r_squared': float,
# 'subcase': str
# }
filtered_rms = result['filtered_rms_nm'] # nm
E9: Zernike Relative RMS (Between Subcases)
Module: optimization_engine.extractors.extract_zernike
from optimization_engine.extractors.extract_zernike import extract_zernike_relative_rms
result = extract_zernike_relative_rms(
op2_file,
bdf_file=None,
target_subcase="40", # Target orientation
reference_subcase="20", # Reference (usually polishing orientation)
displacement_unit="mm"
)
# Returns: {
# 'relative_filtered_rms_nm': float, # Differential WFE in nm
# 'delta_coefficients': list, # Coefficient differences
# 'target_subcase': str,
# 'reference_subcase': str
# }
relative_rms = result['relative_filtered_rms_nm'] # nm
E10: Zernike Objective Builder (Multi-Subcase)
Module: optimization_engine.extractors.zernike_helpers
from optimization_engine.extractors.zernike_helpers import ZernikeObjectiveBuilder
builder = ZernikeObjectiveBuilder(
op2_finder=lambda: model_dir / "ASSY_M1-solution_1.op2"
)
# Add relative objectives (target vs reference)
builder.add_relative_objective("40", "20", metric="relative_filtered_rms_nm", weight=5.0)
builder.add_relative_objective("60", "20", metric="relative_filtered_rms_nm", weight=5.0)
# Add absolute objective for polishing orientation
builder.add_subcase_objective("90", metric="rms_filter_j1to3", weight=1.0)
# Evaluate all at once (efficient - parses OP2 only once)
results = builder.evaluate_all()
# Returns: {'rel_40_vs_20': 4.2, 'rel_60_vs_20': 8.7, 'rms_90': 15.3}
E20: Zernike Analytic (Parabola-Based with Lateral Correction)
Module: optimization_engine.extractors.extract_zernike_opd
Uses an analytical parabola formula to account for lateral (X, Y) displacements. Requires knowing the focal length.
Use when: You know the optical prescription and want to compare against theoretical parabola.
from optimization_engine.extractors import extract_zernike_analytic, ZernikeAnalyticExtractor
# Full extraction with lateral displacement diagnostics
result = extract_zernike_analytic(
op2_file,
subcase="20",
focal_length=5000.0, # Required for analytic method
)
# Class-based usage
extractor = ZernikeAnalyticExtractor(op2_file, focal_length=5000.0)
result = extractor.extract_subcase('20')
E21: Zernike Method Comparison
Module: optimization_engine.extractors.extract_zernike_opd
Compare standard (Z-only) vs analytic (parabola) methods.
from optimization_engine.extractors import compare_zernike_methods
comparison = compare_zernike_methods(op2_file, subcase="20", focal_length=5000.0)
print(comparison['recommendation'])
E22: Zernike OPD (RECOMMENDED - Most Rigorous)
Module: optimization_engine.extractors.extract_zernike_figure
MOST RIGOROUS METHOD for computing WFE. Uses the actual BDF geometry (filtered to OP2 nodes) as the reference surface instead of assuming a parabolic shape.
Advantages over E20 (Analytic):
- No need to know focal length or optical prescription
- Works with any surface shape: parabola, hyperbola, asphere, freeform
- Uses the actual mesh geometry as the "ideal" surface reference
- Interpolates
z_figureat deformed(x+dx, y+dy)position for true OPD
How it works:
- Load BDF geometry for nodes present in OP2 (figure surface nodes)
- Build 2D interpolator
z_figure(x, y)from undeformed coordinates - For each deformed node at
(x0+dx, y0+dy, z0+dz):- Interpolate
z_figureat the deformed (x,y) position - Surface error =
(z0 + dz) - z_interpolated
- Interpolate
- Fit Zernike polynomials to the surface error map
from optimization_engine.extractors import (
ZernikeOPDExtractor,
extract_zernike_opd,
extract_zernike_opd_filtered_rms,
)
# Full extraction with diagnostics
result = extract_zernike_opd(op2_file, subcase="20")
# Returns: {
# 'global_rms_nm': float,
# 'filtered_rms_nm': float,
# 'max_lateral_displacement_um': float,
# 'rms_lateral_displacement_um': float,
# 'coefficients': list, # 50 Zernike coefficients
# 'method': 'opd',
# 'figure_file': 'BDF (filtered to OP2)',
# ...
# }
# Simple usage for optimization objective
rms = extract_zernike_opd_filtered_rms(op2_file, subcase="20")
# Class-based for multi-subcase analysis
extractor = ZernikeOPDExtractor(op2_file)
results = extractor.extract_all_subcases()
Relative WFE (CRITICAL for Optimization)
Use extract_relative() for computing relative WFE between subcases!
BUG WARNING (V10 Fix - 2025-12-22): The WRONG way to compute relative WFE is:
# ❌ WRONG: Difference of RMS values result_40 = extractor.extract_subcase("3") result_ref = extractor.extract_subcase("2") rel_40 = abs(result_40['filtered_rms_nm'] - result_ref['filtered_rms_nm']) # WRONG!This computes
|RMS(WFE_40) - RMS(WFE_20)|, which is NOT the same asRMS(WFE_40 - WFE_20). The difference can be 3-4x lower than the correct value, leading to false "too good to be true" results.
The CORRECT approach uses extract_relative():
# ✅ CORRECT: Computes node-by-node WFE difference, then fits Zernike, then RMS
extractor = ZernikeOPDExtractor(op2_file)
rel_40 = extractor.extract_relative("3", "2") # 40 deg vs 20 deg
rel_60 = extractor.extract_relative("4", "2") # 60 deg vs 20 deg
rel_90 = extractor.extract_relative("1", "2") # 90 deg vs 20 deg
# Returns: {
# 'target_subcase': '3',
# 'reference_subcase': '2',
# 'method': 'figure_opd_relative',
# 'relative_global_rms_nm': float, # RMS of the difference field
# 'relative_filtered_rms_nm': float, # Use this for optimization!
# 'relative_rms_filter_j1to3': float, # For manufacturing/optician workload
# 'max_lateral_displacement_um': float,
# 'rms_lateral_displacement_um': float,
# 'delta_coefficients': list, # Zernike coeffs of difference
# }
# Use in optimization objectives:
objectives = {
'rel_filtered_rms_40_vs_20': rel_40['relative_filtered_rms_nm'],
'rel_filtered_rms_60_vs_20': rel_60['relative_filtered_rms_nm'],
'mfg_90_optician_workload': rel_90['relative_rms_filter_j1to3'],
}
Mathematical Difference:
WRONG: |RMS(WFE_40) - RMS(WFE_20)| = |6.14 - 8.13| = 1.99 nm ← FALSE!
CORRECT: RMS(WFE_40 - WFE_20) = RMS(diff_field) = 6.59 nm ← TRUE!
The Standard ZernikeExtractor also has extract_relative() if you don't need the OPD method:
from optimization_engine.extractors import ZernikeExtractor
extractor = ZernikeExtractor(op2_file, n_modes=50, filter_orders=4)
rel_40 = extractor.extract_relative("3", "2") # Z-only method
Backwards compatibility: The old names (ZernikeFigureExtractor, extract_zernike_figure, extract_zernike_figure_rms) still work but are deprecated.
When to use which Zernike method:
| Method | Class | When to Use | Assumptions |
|---|---|---|---|
| Standard (E8) | ZernikeExtractor |
Quick analysis, negligible lateral displacement | Z-only at original (x,y) |
| Analytic (E20) | ZernikeAnalyticExtractor |
Known focal length, parabolic surface | Parabola shape |
| OPD (E22) | ZernikeOPDExtractor |
Any surface, most rigorous | None - uses actual geometry |
IMPORTANT: Do NOT provide a figure.dat file unless you're certain it matches your BDF geometry exactly. The default behavior (using BDF geometry filtered to OP2 nodes) is the safest option.
Code Reuse Protocol
The 20-Line Rule
If you're writing a function longer than ~20 lines in run_optimization.py:
- STOP - This is a code smell
- SEARCH - Check this library
- IMPORT - Use existing extractor
- Only if truly new - Create via EXT_01
Correct Pattern
# ✅ CORRECT: Import and use
from optimization_engine.extractors import extract_displacement, extract_frequency
def objective(trial):
# ... run simulation ...
disp_result = extract_displacement(op2_file)
freq_result = extract_frequency(op2_file)
return disp_result['max_displacement']
# ❌ WRONG: Duplicate code in study
def objective(trial):
# ... run simulation ...
# Don't write 50 lines of OP2 parsing here
from pyNastran.op2.op2 import OP2
op2 = OP2()
op2.read_op2(str(op2_file))
# ... 40 more lines ...
Adding New Extractors
If needed physics isn't in library:
- Check EXT_01_CREATE_EXTRACTOR
- Create in
optimization_engine/extractors/new_extractor.py - Add to
optimization_engine/extractors/__init__.py - Update this document
Do NOT add extraction code directly to run_optimization.py.
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
| "No displacement data found" | Wrong subcase number | Check subcase in OP2 |
| "OP2 file not found" | Solve failed | Check NX logs |
| "Unknown element type: auto" | Element type not specified | Specify element_type='cquad4' or 'ctetra' |
| "No stress results in OP2" | Wrong element type specified | Use correct type for your mesh |
| Import error | Module not exported | Check __init__.py exports |
Element Type Selection Guide
Critical: You must specify the correct element type for stress extraction based on your mesh:
| Mesh Type | Elements | element_type= |
|---|---|---|
| Shell (thin structures) | CQUAD4, CTRIA3 | 'cquad4' or 'ctria3' |
| Solid (3D volumes) | CTETRA, CHEXA | 'ctetra' or 'chexa' |
How to check your mesh type:
- Open .dat/.bdf file
- Search for element cards (CQUAD4, CTETRA, etc.)
- Use the dominant element type
Common models:
- Bracket (solid): Uses CTETRA →
element_type='ctetra' - Beam (shell): Uses CQUAD4 →
element_type='cquad4' - Mirror (shell): Uses CQUAD4 →
element_type='cquad4'
Von Mises column mapping (handled automatically):
- Shell elements (8 columns): von Mises at column 7
- Solid elements (10 columns): von Mises at column 9
Cross-References
- Depends On: pyNastran for OP2 parsing
- Used By: All optimization studies
- Extended By: EXT_01_CREATE_EXTRACTOR
- See Also: modules/extractors-catalog.md
Phase 2 Extractors (2025-12-06)
E12: Principal Stress Extraction
Module: optimization_engine.extractors.extract_principal_stress
from optimization_engine.extractors import extract_principal_stress
result = extract_principal_stress(op2_file, subcase=1, element_type='ctetra')
# Returns: {
# 'success': bool,
# 'sigma1_max': float, # Maximum principal stress (MPa)
# 'sigma2_max': float, # Intermediate principal stress
# 'sigma3_min': float, # Minimum principal stress
# 'element_count': int
# }
E13: Strain Energy Extraction
Module: optimization_engine.extractors.extract_strain_energy
from optimization_engine.extractors import extract_strain_energy, extract_total_strain_energy
result = extract_strain_energy(op2_file, subcase=1)
# Returns: {
# 'success': bool,
# 'total_strain_energy': float, # J
# 'max_element_energy': float,
# 'max_element_id': int
# }
# Convenience function
total_energy = extract_total_strain_energy(op2_file) # J
E14: SPC Forces (Reaction Forces)
Module: optimization_engine.extractors.extract_spc_forces
from optimization_engine.extractors import extract_spc_forces, extract_total_reaction_force
result = extract_spc_forces(op2_file, subcase=1)
# Returns: {
# 'success': bool,
# 'total_force_magnitude': float, # N
# 'total_force_x': float,
# 'total_force_y': float,
# 'total_force_z': float,
# 'node_count': int
# }
# Convenience function
total_reaction = extract_total_reaction_force(op2_file) # N
Phase 3 Extractors (2025-12-06)
E15: Temperature Extraction
Module: optimization_engine.extractors.extract_temperature
For SOL 153 (Steady-State) and SOL 159 (Transient) thermal analyses.
from optimization_engine.extractors import extract_temperature, get_max_temperature
result = extract_temperature(op2_file, subcase=1, return_field=False)
# Returns: {
# 'success': bool,
# 'max_temperature': float, # K or °C
# 'min_temperature': float,
# 'avg_temperature': float,
# 'max_node_id': int,
# 'node_count': int,
# 'unit': str
# }
# Convenience function for constraints
max_temp = get_max_temperature(op2_file) # Returns inf on failure
E16: Thermal Gradient Extraction
Module: optimization_engine.extractors.extract_temperature
from optimization_engine.extractors import extract_temperature_gradient
result = extract_temperature_gradient(op2_file, subcase=1)
# Returns: {
# 'success': bool,
# 'max_gradient': float, # K/mm (approximation)
# 'temperature_range': float, # Max - Min temperature
# 'gradient_location': tuple # (max_node, min_node)
# }
E17: Heat Flux Extraction
Module: optimization_engine.extractors.extract_temperature
from optimization_engine.extractors import extract_heat_flux
result = extract_heat_flux(op2_file, subcase=1)
# Returns: {
# 'success': bool,
# 'max_heat_flux': float, # W/mm²
# 'avg_heat_flux': float,
# 'element_count': int
# }
E18: Modal Mass Extraction
Module: optimization_engine.extractors.extract_modal_mass
For SOL 103 (Normal Modes) F06 files with MEFFMASS output.
from optimization_engine.extractors import (
extract_modal_mass,
extract_frequencies,
get_first_frequency,
get_modal_mass_ratio
)
# Get all modes
result = extract_modal_mass(f06_file, mode=None)
# Returns: {
# 'success': bool,
# 'mode_count': int,
# 'frequencies': list, # Hz
# 'modes': list of mode dicts
# }
# Get specific mode
result = extract_modal_mass(f06_file, mode=1)
# Returns: {
# 'success': bool,
# 'frequency': float, # Hz
# 'modal_mass_x': float, # kg
# 'modal_mass_y': float,
# 'modal_mass_z': float,
# 'participation_x': float # 0-1
# }
# Convenience functions
freq = get_first_frequency(f06_file) # Hz
ratio = get_modal_mass_ratio(f06_file, direction='z', n_modes=10) # 0-1
Phase 4 Extractors (2025-12-19)
E19: Part Introspection (Comprehensive)
Module: optimization_engine.extractors.introspect_part
Comprehensive introspection of NX .prt files. Extracts everything available from a part in a single call.
Prerequisites: Uses PowerShell with proper license server setup (see LAC workaround).
from optimization_engine.extractors import (
introspect_part,
get_expressions_dict,
get_expression_value,
print_introspection_summary
)
# Full introspection
result = introspect_part("path/to/model.prt")
# Returns: {
# 'success': bool,
# 'part_file': str,
# 'expressions': {
# 'user': [{'name', 'value', 'rhs', 'units', 'type'}, ...],
# 'internal': [...],
# 'user_count': int,
# 'total_count': int
# },
# 'mass_properties': {
# 'mass_kg': float,
# 'mass_g': float,
# 'volume_mm3': float,
# 'surface_area_mm2': float,
# 'center_of_gravity_mm': [x, y, z]
# },
# 'materials': {
# 'assigned': [{'name', 'body', 'properties': {...}}],
# 'available': [...]
# },
# 'bodies': {
# 'solid_bodies': [{'name', 'is_solid', 'attributes': [...]}],
# 'sheet_bodies': [...],
# 'counts': {'solid', 'sheet', 'total'}
# },
# 'attributes': [{'title', 'type', 'value'}, ...],
# 'groups': [{'name', 'member_count', 'members': [...]}],
# 'features': {
# 'total_count': int,
# 'by_type': {'Extrude': 5, 'Revolve': 2, ...}
# },
# 'datums': {
# 'planes': [...],
# 'csys': [...],
# 'axes': [...]
# },
# 'units': {
# 'base_units': {'Length': 'MilliMeter', ...},
# 'system': 'Metric (mm)'
# },
# 'linked_parts': {
# 'loaded_parts': [...],
# 'fem_parts': [...],
# 'sim_parts': [...],
# 'idealized_parts': [...]
# }
# }
# Convenience functions
expr_dict = get_expressions_dict(result) # {'name': value, ...}
pocket_radius = get_expression_value(result, 'Pocket_Radius') # float
# Print formatted summary
print_introspection_summary(result)
What It Extracts:
- Expressions: All user and internal expressions with values, RHS formulas, units
- Mass Properties: Mass, volume, surface area, center of gravity
- Materials: Material names and properties (density, Young's modulus, etc.)
- Bodies: Solid and sheet bodies with their attributes
- Part Attributes: All NX_* system attributes plus user attributes
- Groups: Named groups and their members
- Features: Feature tree summary by type
- Datums: Datum planes, coordinate systems, axes
- Units: Base units and unit system
- Linked Parts: FEM, SIM, idealized parts loaded in session
Use Cases:
- Study setup: Extract actual expression values for baseline
- Debugging: Verify model state before optimization
- Documentation: Generate part specifications
- Validation: Compare expected vs actual parameter values
NX Journal Execution (LAC Workaround):
# CRITICAL: Use PowerShell with [Environment]::SetEnvironmentVariable()
# NOT cmd /c SET or $env: syntax (these fail)
powershell -Command "[Environment]::SetEnvironmentVariable('SPLM_LICENSE_SERVER', '28000@server', 'Process'); & 'run_journal.exe' 'introspect_part.py' -args 'model.prt' 'output_dir'"
Implementation Files
optimization_engine/extractors/
├── __init__.py # Exports all extractors
├── extract_displacement.py # E1
├── extract_frequency.py # E2
├── extract_von_mises_stress.py # E3
├── bdf_mass_extractor.py # E4
├── extract_mass_from_expression.py # E5
├── field_data_extractor.py # E6
├── stiffness_calculator.py # E7
├── extract_zernike.py # E8, E9 (Standard Z-only)
├── extract_zernike_opd.py # E20, E21 (Parabola OPD)
├── extract_zernike_figure.py # E22 (Figure OPD - most rigorous)
├── zernike_helpers.py # E10
├── extract_part_mass_material.py # E11 (Part mass & material)
├── extract_zernike_surface.py # Surface utilities
├── op2_extractor.py # Low-level OP2 access
├── extract_principal_stress.py # E12 (Phase 2)
├── extract_strain_energy.py # E13 (Phase 2)
├── extract_spc_forces.py # E14 (Phase 2)
├── extract_temperature.py # E15, E16, E17 (Phase 3)
├── extract_modal_mass.py # E18 (Phase 3)
├── introspect_part.py # E19 (Phase 4)
├── test_phase2_extractors.py # Phase 2 tests
└── test_phase3_extractors.py # Phase 3 tests
nx_journals/
├── extract_part_mass_material.py # E11 NX journal (prereq)
└── introspect_part.py # E19 NX journal (comprehensive introspection)
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-12-05 | Initial consolidation from scattered docs |
| 1.1 | 2025-12-06 | Added Phase 2: E12 (principal stress), E13 (strain energy), E14 (SPC forces) |
| 1.2 | 2025-12-06 | Added Phase 3: E15-E17 (thermal), E18 (modal mass) |
| 1.3 | 2025-12-07 | Added Element Type Selection Guide; documented shell vs solid stress columns |
| 1.4 | 2025-12-19 | Added Phase 4: E19 (comprehensive part introspection) |
| 1.5 | 2025-12-22 | Added Phase 5: E20 (Parabola OPD), E21 (comparison), E22 (Figure OPD - most rigorous) |