586 lines
17 KiB
Markdown
586 lines
17 KiB
Markdown
|
|
# SYS_12: Extractor Library
|
||
|
|
|
||
|
|
<!--
|
||
|
|
PROTOCOL: Centralized Extractor Library
|
||
|
|
LAYER: System
|
||
|
|
VERSION: 1.0
|
||
|
|
STATUS: Active
|
||
|
|
LAST_UPDATED: 2025-12-05
|
||
|
|
PRIVILEGE: user
|
||
|
|
LOAD_WITH: []
|
||
|
|
-->
|
||
|
|
|
||
|
|
## 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`
|
||
|
|
|
||
|
|
```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
|
||
|
|
|
||
|
|
# 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`
|
||
|
|
|
||
|
|
```python
|
||
|
|
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`
|
||
|
|
|
||
|
|
```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`
|
||
|
|
|
||
|
|
### 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}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 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 |
|
||
|
|
| "Element type not supported" | Using unsupported element | Check available types in function |
|
||
|
|
| Import error | Module not exported | Check `__init__.py` exports |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 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
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 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) |
|