# 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` ```python 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` ```python 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` ```python 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` ```python 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` ```python 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: ```bash run_journal.exe nx_journals/extract_part_mass_material.py model.prt ``` ```python 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: ```python 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.MeasureBodies` - `NXOpen.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` ```python 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` ```python 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): ```python 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` ```python 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` ```python 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` ```python 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. ```python 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. ```python 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_figure` at deformed `(x+dx, y+dy)` position for true OPD **How it works**: 1. Load BDF geometry for nodes present in OP2 (figure surface nodes) 2. Build 2D interpolator `z_figure(x, y)` from undeformed coordinates 3. For each deformed node at `(x0+dx, y0+dy, z0+dz)`: - Interpolate `z_figure` at the deformed (x,y) position - Surface error = `(z0 + dz) - z_interpolated` 4. Fit Zernike polynomials to the surface error map ```python 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: > ```python > # ❌ 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 as `RMS(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()`:** ```python # ✅ 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: ```python 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`: 1. **STOP** - This is a code smell 2. **SEARCH** - Check this library 3. **IMPORT** - Use existing extractor 4. **Only if truly new** - Create via EXT_01 ### Correct Pattern ```python # ✅ 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'] ``` ```python # ❌ 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: 1. Check [EXT_01_CREATE_EXTRACTOR](../extensions/EXT_01_CREATE_EXTRACTOR.md) 2. Create in `optimization_engine/extractors/new_extractor.py` 3. Add to `optimization_engine/extractors/__init__.py` 4. 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:** 1. Open .dat/.bdf file 2. Search for element cards (CQUAD4, CTETRA, etc.) 3. 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](../extensions/EXT_01_CREATE_EXTRACTOR.md) - **See Also**: [modules/extractors-catalog.md](../../.claude/skills/modules/extractors-catalog.md) --- ## Phase 2 Extractors (2025-12-06) ### E12: Principal Stress Extraction **Module**: `optimization_engine.extractors.extract_principal_stress` ```python 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` ```python 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` ```python 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. ```python 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` ```python 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` ```python 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. ```python 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). ```python 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): ```python # 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) |