New E11 Part Mass Extractor: - Add nx_journals/extract_part_mass_material.py - NX journal using NXOpen.MeasureManager.NewMassProperties() for accurate geometry-based mass - Add optimization_engine/extractors/extract_part_mass_material.py - Python wrapper that reads JSON output from journal - Add E11 entry to extractors/catalog.json Documentation Updates: - SYS_12_EXTRACTOR_LIBRARY.md: Add mass accuracy warning noting pyNastran get_mass_breakdown() under-reports ~7% on hex-dominant meshes with tet/pyramid fill elements. E11 (geometry .prt) should be preferred over E4 (BDF) unless material is overridden at FEM level. - 01_CHEATSHEET.md: Add mass extraction tip V14 Config: - Expand design variable bounds (blank_backface_angle max 4.5°, whiffle_triangle_closeness max 80mm, whiffle_min max 60mm) Testing showed: - E11 from .prt: 97.66 kg (accurate - matches NX GUI) - E4 pyNastran get_mass_breakdown(): 90.73 kg (~7% under-reported) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 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 |
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
# For shell elements (CQUAD4, CTRIA3)
result = extract_solid_stress(op2_file, subcase=1, element_type='cquad4')
# For solid elements (CTETRA, CHEXA)
result = extract_solid_stress(op2_file, subcase=1, element_type='ctetra')
# Returns: {
# 'max_von_mises': float, # MPa
# 'max_stress_element': int
# }
max_stress = result['max_von_mises'] # MPa
E4: BDF Mass Extraction
Module: optimization_engine.extractors.bdf_mass_extractor
from optimization_engine.extractors.bdf_mass_extractor import extract_mass_from_bdf
mass_kg = extract_mass_from_bdf(str(bdf_file)) # kg
Note: Reads mass directly from BDF/DAT file material and element definitions.
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}
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
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
├── 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)
├── 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)
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 |