feat: Add Phase 2 & 3 physics extractors for multi-physics optimization

Phase 2 - Structural Analysis:
- extract_principal_stress: σ1, σ2, σ3 principal stresses from OP2
- extract_strain_energy: Element and total strain energy
- extract_spc_forces: Reaction forces at boundary conditions

Phase 3 - Multi-Physics:
- extract_temperature: Nodal temperatures from thermal OP2 (SOL 153/159)
- extract_temperature_gradient: Thermal gradient approximation
- extract_heat_flux: Element heat flux from thermal analysis
- extract_modal_mass: Modal effective mass from F06 (SOL 103)
- get_first_frequency: Convenience function for first natural frequency

Documentation:
- Updated SYS_12_EXTRACTOR_LIBRARY.md with E12-E18 specifications
- Updated NX_OPEN_AUTOMATION_ROADMAP.md marking Phase 3 complete
- Added test_phase3_extractors.py for validation

All extractors follow consistent API pattern returning Dict with
success, data, and error fields for robust error handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-06 13:40:14 -05:00
parent 5fb94fdf01
commit 0cb2808c44
9 changed files with 3395 additions and 2 deletions

View File

@@ -0,0 +1,717 @@
# Atomizer NX Open Automation Roadmap
## Plan de Match: Hooks, Extractors & Manipulators pour Optimisation FEA
**Date**: 2025-12-06
**Version**: 2.0 (merged with ATOMIZER_NXOPEN_MASTER_PLAN.md)
**Objectif**: Définir l'ensemble des fonctionnalités NX Open à implémenter pour un framework d'optimisation structurelle/thermique complet, basé sur la règle 80/20.
---
## Quick Reference: NX Open API Index
### Core Classes (verified via MCP)
| Class | Page ID | Primary Use |
|-------|---------|-------------|
| `NXOpen.Session` | a03318.html | Session singleton, part access |
| `NXOpen.Part` | a02434.html | Part operations, expressions |
| `NXOpen.BasePart` | a00266.html | Base class, common methods |
| `NXOpen.CAE.CaeSession` | a10510.html | CAE session, utilities |
| `NXOpen.CAE.FemPart` | - | FEM part, mesh access |
| `NXOpen.CAE.SimPart` | - | Simulation part, solutions |
### Key Collections & Managers (from NXOpen.Part)
| Manager | Access | Purpose |
|---------|--------|---------|
| `Expressions` | `part.Expressions` | Expression management |
| `MeasureManager` | `part.MeasureManager()` | Mass properties |
| `Bodies` | `part.Bodies()` | Body collection |
| `Features` | `part.Features()` | Feature collection |
| `MaterialManager` | `part.MaterialManager()` | Material assignment |
### CAE Managers (from NXOpen.CAE.CaeSession)
| Manager | Access | Purpose |
|---------|--------|---------|
| `MaterialUtils` | `cae_session.MaterialUtils()` | CAE material utilities |
| `AssociationUtils` | `cae_session.AssociationUtils()` | Geometry-FEM association |
| `PenetrationCheckManager` | `cae_session.PenetrationCheckManager()` | Contact check |
---
## 1. Analyse de l'Industrie MDO
### Fonctionnalités Standard (ce que font les concurrents)
| Fonctionnalité | HyperStudy | modeFRONTIER | HEEDS | OpenMDAO | Atomizer (actuel) |
|----------------|------------|--------------|-------|----------|-------------------|
| DOE (LHS, Sobol, etc.) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Optimisation mono-objectif | ✓ | ✓ | ✓ | ✓ | ✓ |
| Multi-objectif (Pareto) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Surrogate Models | ✓ | ✓ | ✓ | ✓ | ✓ (NN) |
| Kriging/Gaussian Process | ✓ | ✓ | ✓ | ✓ | ❌ |
| Robustesse/Fiabilité (RBDO) | ✓ | ✓ | ❌ | ✓ | ❌ |
| Sensibilité paramétrique | ✓ | ✓ | ✓ | ✓ | Partiel |
| Topology Optimization | ✓ | ❌ | ❌ | ❌ | ❌ |
| Workflow visuel | ✓ | ✓ | ✓ | ❌ | ❌ |
| Interface NX native | ❌ | ❌ | ✓ | ❌ | ✓ |
### Sources
- [Altair HyperStudy](https://altair.com/hyperstudy/)
- [modeFRONTIER](https://engineering.esteco.com/modefrontier/)
- [OpenMDAO](https://openmdao.org/)
- [M4 Engineering NXOpen Example](https://www.m4-engineering.com/automating-load-case-combination-and-enveloping-in-simcenter-3d-using-nxopen-python/)
---
## 2. Capacités Simcenter 13500
### Modules Disponibles avec ta Licence
| Module | Description | Utilisable pour Optimisation |
|--------|-------------|------------------------------|
| **NX Nastran Basic** | Static, Modal, Buckling | ✓ Priorité haute |
| **NX Nastran Dynamic** | Frequency/Transient Response | ✓ Priorité moyenne |
| **NX Nastran Thermal** | Heat Transfer (steady/transient) | ✓ Priorité haute |
| **NX Nastran Optimization** | SOL 200 (taille, forme) | ✓ Priorité moyenne |
| **Simcenter 3D Pre/Post** | Meshing, Results | ✓ Indispensable |
### Types d'Analyses Supportées
1. **Structurel Linéaire** (SOL 101)
- Static stress/displacement
- Reaction forces
2. **Modal** (SOL 103)
- Natural frequencies
- Mode shapes
- Modal effective mass
3. **Buckling** (SOL 105)
- Critical load factors
- Buckling mode shapes
4. **Thermique** (SOL 153/159)
- Steady-state heat transfer
- Transient thermal
- Thermal stress coupling
5. **Dynamique** (SOL 108/109/111/112)
- Frequency response
- Transient response
- Random response
---
## 3. Architecture des Hooks NX Open
### 3.1 Hooks de Manipulation CAD (Priorité 1)
```
optimization_engine/
└── hooks/
└── nx_cad/
├── __init__.py
├── part_manager.py # Open/Save/Close parts
├── expression_manager.py # Get/Set expressions
├── feature_manager.py # Suppress/Unsuppress features
├── geometry_query.py # Query geometry (mass, volume, area)
└── assembly_manager.py # Component positioning
```
| Hook | Description | API NX Open | Priorité |
|------|-------------|-------------|----------|
| `open_part(path)` | Ouvrir une pièce | `Session.Parts.OpenBase()` | P1 |
| `close_part(save=False)` | Fermer une pièce | `Part.Close()` | P1 |
| `set_expression(name, value)` | Modifier expression | `Expression.SetValue()` | P1 |
| `get_expression(name)` | Lire expression | `Expression.Value` | P1 |
| `update_model()` | Mettre à jour le modèle | `Session.UpdateManager.DoUpdate()` | P1 |
| `suppress_feature(name)` | Supprimer feature | `Feature.Suppress()` | P2 |
| `get_mass_properties()` | Masse, CG, inertie | `MeasureManager.NewMassProperties()` | P1 |
| `export_parasolid(path)` | Exporter géométrie | `Part.SaveAs()` | P2 |
### 3.2 Hooks FEM/Meshing (Priorité 2)
```
optimization_engine/
└── hooks/
└── nx_fem/
├── __init__.py
├── mesh_manager.py # Create/Update mesh
├── material_manager.py # Assign materials
├── property_manager.py # Shell/Solid properties
├── boundary_conditions.py # Loads & constraints
└── connection_manager.py # Connectors, contacts
```
| Hook | Description | API NX Open | Priorité |
|------|-------------|-------------|----------|
| `create_tet_mesh(body, size)` | Mailler en tétra | `MeshManager.CreateMesh3d()` | P1 |
| `update_mesh()` | Régénérer maillage | `FEModel.UpdateMesh()` | P1 |
| `set_material(mesh, mat_name)` | Assigner matériau | `PhysicalProperty.SetMaterial()` | P1 |
| `create_shell_property(t)` | Propriété shell | `PhysicalPropertyCollection.CreateShellProperty()` | P2 |
| `apply_force(nodes, vector)` | Appliquer force | `LoadCollection.CreateForce()` | P1 |
| `apply_constraint(nodes, dof)` | Appliquer contrainte | `ConstraintCollection.CreateConstraint()` | P1 |
| `create_contact(faces1, faces2)` | Contact surfaces | `ConnectionCollection.CreateSurfaceContact()` | P2 |
### 3.3 Hooks Simulation/Solve (Priorité 1)
```
optimization_engine/
└── hooks/
└── nx_sim/
├── __init__.py
├── solution_manager.py # Create/Run solutions
├── solve_manager.py # Submit solver jobs
└── result_manager.py # Access results
```
| Hook | Description | API NX Open | Priorité |
|------|-------------|-------------|----------|
| `create_solution(type, name)` | Créer solution | `SimSolutionCollection.CreateSolution()` | P1 |
| `solve(solution)` | Lancer solveur | `SimSolution.Solve()` | P1 |
| `solve_batch(bdf_path)` | Nastran en batch | `subprocess` + run_nastran | P1 |
| `get_solve_status()` | Statut du solve | `SimSolution.SolveStatus` | P1 |
| `export_bdf(path)` | Exporter deck Nastran | `SimSolution.ExportSolver()` | P1 |
---
## 4. Architecture des Extractors
### 4.0 Current Implementation Status (as of 2025-12-06)
```
optimization_engine/extractors/
├── __init__.py
├── extract_displacement.py # ✓ extract_displacement()
├── extract_von_mises_stress.py # ✓ extract_solid_stress()
├── extract_frequency.py # ✓ extract_frequency()
├── extract_mass.py # ✓ extract_generic()
├── extract_mass_from_bdf.py # ✓ extract_mass_from_bdf()
├── extract_mass_from_expression.py # ✓ extract_mass_from_expression()
├── extract_part_mass_material.py # ✓ PartMassExtractor (NX Open via journal)
├── bdf_mass_extractor.py # ✓ BDFMassExtractor class
├── op2_extractor.py # ✓ OP2Extractor class (mass, grid forces, loads)
├── field_data_extractor.py # ✓ FieldDataExtractor class
├── extract_zernike.py # ✓ ZernikeExtractor class (advanced)
├── extract_zernike_surface.py # ✓ SurfaceZernikeExtractor class
└── zernike_helpers.py # ✓ Helper functions
```
### 4.1 Extractors Structurels (Priorité 1)
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `extract_displacement(op2, subcase)` | mm | OP2 | P1 | ✓ | extract_displacement.py |
| `extract_solid_stress(op2, subcase, elem_type)` | MPa | OP2 | P1 | ✓ | extract_von_mises_stress.py |
| `extract_mass_from_bdf(bdf)` | kg | BDF | P1 | ✓ | bdf_mass_extractor.py |
| `extract_mass_from_op2(op2)` | kg | OP2 | P1 | ✓ | op2_extractor.py |
| `extract_grid_point_forces(op2)` | N | OP2 | P1 | ✓ | op2_extractor.py |
| `extract_displacement_field(op2)` | [mm] | OP2 | P1 | ✓ | field_data_extractor.py |
| `extract_principal_stress(elem)` | MPa | OP2 | P2 | ❌ | - |
| `extract_strain(elem)` | - | OP2 | P2 | ❌ | - |
| `extract_strain_energy(elem)` | J | OP2 | P2 | ❌ | - |
### 4.2 Extractors Modaux (Priorité 2)
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `extract_frequency(op2, subcase, mode)` | Hz | OP2 | P1 | ✓ | extract_frequency.py |
| `extract_modal_mass(mode)` | kg | F06 | P2 | ❌ | - |
| `extract_mode_shape(mode, nodes)` | [mm] | OP2 | P3 | ❌ | - |
| `extract_mac_matrix(modes)` | [0-1] | Calc | P3 | ❌ | - |
### 4.3 Extractors Thermiques (Priorité 2)
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `extract_temperature(node)` | °C/K | OP2/F06 | P2 | ❌ | - |
| `extract_max_temperature()` | °C/K | OP2/F06 | P2 | ❌ | - |
| `extract_heat_flux(elem)` | W/m² | OP2/F06 | P2 | ❌ | - |
| `extract_thermal_stress(elem)` | MPa | OP2/F06 | P2 | ❌ | - |
### 4.4 Extractors Géométriques (CAD) (Priorité 1) - NX Open
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `extract_part_mass_material(prt)` | kg, material | NX Open | P1 | ✓ | extract_part_mass_material.py |
| `extract_part_mass(prt)` | kg | NX Open | P1 | ✓ | extract_part_mass_material.py |
| `extract_part_material(prt)` | string | NX Open | P1 | ✓ | extract_part_mass_material.py |
| `extract_mass_from_expression(prt)` | kg | NX Open | P1 | ✓ | extract_mass_from_expression.py |
| `extract_volume()` | mm³ | NX Open | P2 | ❌ | - |
| `extract_surface_area()` | mm² | NX Open | P2 | ❌ | - |
| `extract_center_of_gravity()` | [mm] | NX Open | P2 | ❌ | - |
| `extract_inertia_tensor()` | kg·mm² | NX Open | P3 | ❌ | - |
**NX Open APIs for CAD Extraction**:
- `part.MeasureManager()` - Main entry point for mass properties
- `MeasureManager.NewMassProperties()` - Create mass measurement
- `MasProperties.Mass`, `.CenterOfGravity`, `.MomentsOfInertia`
### 4.5 Extractors Buckling (Priorité 3)
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `extract_buckling_factor(mode)` | - | F06 | P3 | ❌ | - |
| `extract_critical_load()` | N | F06 | P3 | ❌ | - |
### 4.6 Extractors Zernike (Spécialisé Optique) ✓
| Extractor | Output | Source | Priorité | Status | File |
|-----------|--------|--------|----------|--------|------|
| `ZernikeExtractor.extract_subcase()` | coeffs | OP2 | P1 | ✓ | extract_zernike.py |
| `ZernikeExtractor.extract_relative()` | delta_coeffs | OP2 | P1 | ✓ | extract_zernike.py |
| `extract_zernike_from_op2()` | coeffs | OP2 | P1 | ✓ | extract_zernike.py |
| `extract_zernike_filtered_rms()` | RMS(nm) | OP2 | P1 | ✓ | extract_zernike.py |
| `SurfaceZernikeExtractor.extract_from_op2()` | coeffs | OP2 | P1 | ✓ | extract_zernike_surface.py |
**Usage**: Mirror/lens deformation optimization using Zernike polynomial decomposition
---
## 5. Manipulateurs Avancés
### 5.1 Manipulateurs de Forme (Shape Optimization)
```
optimization_engine/
└── manipulators/
├── morpher.py # Morphing mesh/géométrie
├── ffd.py # Free-Form Deformation
└── surface_offset.py # Offset surfaces
```
| Manipulator | Description | Priorité |
|-------------|-------------|----------|
| `morph_nodes(nodes, displacements)` | Morphing direct | P3 |
| `ffd_box(control_points)` | Déformation FFD | P3 |
| `offset_faces(faces, distance)` | Offset paramétrique | P2 |
### 5.2 Manipulateurs Topologiques (Future)
| Manipulator | Description | Priorité |
|-------------|-------------|----------|
| `apply_density_filter(elements)` | SIMP filtering | P4 |
| `extract_iso_surface(density)` | Topology to geometry | P4 |
| `create_lattice_infill(region)` | Lattice génération | P4 |
---
## 6. Plan d'Implémentation 80/20
### Phase 1: Fondations ✓ COMPLETED
**Objectif**: Stabiliser le workflow de base
| # | Tâche | Fichier | Status |
|---|-------|---------|--------|
| 1.1 | Expression manipulation via .exp file | `nx_updater.py` | ✓ |
| 1.2 | Mass extraction from BDF | `bdf_mass_extractor.py` | ✓ |
| 1.3 | Mass extraction from NX Open | `extract_part_mass_material.py` | ✓ |
| 1.4 | Displacement extraction | `extract_displacement.py` | ✓ |
| 1.5 | Von Mises stress extraction | `extract_von_mises_stress.py` | ✓ |
| 1.6 | Frequency extraction | `extract_frequency.py` | ✓ |
### Phase 1b: NX Open Hooks ✓ COMPLETED (2025-12-06)
**Objectif**: Create direct NX Open Python hooks for CAD/FEM operations
**Location**: `optimization_engine/hooks/nx_cad/`
| # | Tâche | API (verified via MCP) | Status | File |
|---|-------|------------------------|--------|------|
| 1b.1 | Hook: `open_part` / `close_part` / `save_part` | `Session.Parts.OpenBase()`, `Part.Close()`, `Part.Save()` | ✓ | part_manager.py |
| 1b.2 | Hook: `get_expression` / `set_expression` / `set_expressions` | `part.Expressions`, `Expressions.Edit()` | ✓ | expression_manager.py |
| 1b.3 | Hook: `update_model` (integrated) | `Session.UpdateManager.DoUpdate()` | ✓ | expression_manager.py |
| 1b.4 | Hook: `get_mass_properties` / `get_bodies` / `get_volume` | `MeasureManager.NewMassProperties()` | ✓ | geometry_query.py |
| 1b.5 | Hook: `suppress_feature` / `unsuppress_feature` | `Feature.Suppress()`, `Feature.Unsuppress()` | ✓ | feature_manager.py |
| 1b.6 | Hook: `save_part_as` (export) | `Part.SaveAs()` | ✓ | part_manager.py |
### Phase 2: Workflow Complet ✓ COMPLETED
**Objectif**: Automatiser le cycle CAD → FEM → Results
| # | Tâche | API / Fichier | Status |
|---|-------|---------------|--------|
| 2.1 | BDF export via NX Open | `solver_manager.export_bdf()` | ✓ |
| 2.2 | Batch solver launch | `subprocess` + NX run_solver | ✓ (external) |
| 2.3 | Principal stress extraction | `extract_principal_stress()` | ✓ |
| 2.4 | Strain energy extraction | `extract_strain_energy()` | ✓ |
| 2.5 | Reaction force extraction | `extract_spc_forces()` | ✓ |
| 2.6 | **Model Introspection** | `model_introspection.py` | ✓ |
**Files Created (2025-12-06):**
- `optimization_engine/hooks/nx_cae/solver_manager.py` - BDF export & solve hooks
- `optimization_engine/extractors/extract_principal_stress.py` - Principal stress (σ1, σ2, σ3)
- `optimization_engine/extractors/extract_strain_energy.py` - Element strain energy
- `optimization_engine/extractors/extract_spc_forces.py` - Reaction forces at BCs
- `optimization_engine/hooks/nx_cad/model_introspection.py` - **Comprehensive model introspection**
**Phase 2 Introspection Feature (2025-12-06):**
The model introspection module provides comprehensive extraction of:
- **Part (.prt)**: Expressions, bodies, mass properties, features, materials
- **Simulation (.sim)**: Solutions, boundary conditions, loads, materials, mesh info, output requests
- **Results (.op2)**: Available results (displacement, stress, strain, SPC forces, frequencies), subcases
Usage:
```python
from optimization_engine.hooks.nx_cad.model_introspection import (
introspect_part,
introspect_simulation,
introspect_op2,
introspect_study
)
# Introspect entire study
study_info = introspect_study("studies/my_study/")
```
### Phase 3: Multi-Physique ✓ COMPLETED (Core Extractors)
**Objectif**: Support thermique et dynamique
**Priority**: P1 (High) - Extends optimization to thermal/dynamic domains
| # | Tâche | API / Fichier | Status | Priority |
|---|-------|---------------|--------|----------|
| 3.1 | Temperature extraction | `extract_temperature.py` | ✓ | P1 |
| 3.2 | Thermal gradient extraction | `extract_temperature_gradient()` | ✓ | P1 |
| 3.3 | Thermal stress extraction | OP2 + thermal subcase | ✓ (via E3) | P1 |
| 3.4 | Modal mass extraction | `extract_modal_mass.py` | ✓ | P1 |
| 3.5 | Heat flux extraction | `extract_heat_flux()` | ✓ | P2 |
| 3.6 | Thermal BC setup hook | `NXOpen.CAE.LoadCollection` | ❌ | P2 |
| 3.7 | Thermo-mechanical coupling | Multi-step solve | ❌ | P3 |
**Files Created (2025-12-06):**
- `optimization_engine/extractors/extract_temperature.py` - Temperature, gradient, heat flux (E15-E17)
- `optimization_engine/extractors/extract_modal_mass.py` - Modal effective mass from F06 (E18)
- `optimization_engine/extractors/test_phase3_extractors.py` - Phase 3 test suite
**Phase 3 Implementation Guide:**
#### 3.1 Temperature Extraction
```python
# Target API (pyNastran)
from pyNastran.op2.op2 import read_op2
op2 = read_op2(op2_file)
temperatures = op2.temperatures # TEMP subcase results
def extract_temperature(op2_file, subcase=1, nodes=None):
"""Extract nodal temperatures from thermal analysis.
Returns:
dict: {
'max_temperature': float (°C or K),
'min_temperature': float,
'avg_temperature': float,
'temperatures': {node_id: temp, ...}
}
"""
```
#### 3.2 Thermal Gradient Extraction
```python
def extract_thermal_gradient(op2_file, subcase=1):
"""Extract temperature gradients from thermal analysis.
Returns:
dict: {
'max_gradient': float (K/mm),
'avg_gradient': float,
'gradient_location': int (element_id)
}
"""
```
#### 3.3 Thermal Stress Extraction
```python
def extract_thermal_stress(op2_file, subcase=1, element_type='ctetra'):
"""Extract stress from thermal-mechanical analysis.
Notes:
- Requires coupled thermal-structural solution
- Uses temperature field as load
Returns:
dict: Similar to extract_solid_stress but from thermal loading
"""
```
#### 3.4 Modal Mass Extraction
```python
def extract_modal_mass(f06_file, mode_number=1):
"""Extract modal effective mass from F06 file.
Returns:
dict: {
'modal_mass_x': float (kg),
'modal_mass_y': float,
'modal_mass_z': float,
'participation_factor': float
}
"""
```
**Expected Outputs After Phase 3:**
- New extractors: `extract_temperature.py`, `extract_thermal_gradient.py`, `extract_modal_mass.py`
- Updated protocol SYS_12: Add E15-E18 for thermal extractors
- New study templates: Thermal optimization, thermo-mechanical optimization
### Phase 4: AtomizerField Integration (from Master Plan)
**Objectif**: Neural network surrogate for field prediction
| # | Tâche | Description | Status |
|---|-------|-------------|--------|
| 4.1 | Mesh Graph Builder | GNN graph from FEM mesh | ❌ |
| 4.2 | Training Data Exporter | Mesh + BC + results package | ❌ |
| 4.3 | Field Mapper | GNN predictions → NX format | ❌ |
| 4.4 | Sample Validation | Check convergence/quality | ❌ |
### Phase 5: Surrogates & Advanced
**Objectif**: Kriging, topology, lattice
| # | Tâche | Status |
|---|-------|--------|
| 5.1 | Gaussian Process / Kriging surrogate | ❌ |
| 5.2 | SOL 200 interface (native topo) | ❌ |
| 5.3 | Lattice infill generation | ❌ |
| 5.4 | FFD morphing | ❌ |
---
## 7. Classes NX Open Clés (Verified via MCP)
### Session & Part Access
```python
import NXOpen
# Session singleton
session = NXOpen.Session.GetSession()
# Part access
work_part = session.Parts.Work # Current work part (Part object)
display_part = session.Parts.Display # Display part
all_parts = session.Parts # PartCollection
# Key Part methods (from a02434.html):
expressions = work_part.Expressions # ExpressionCollection
bodies = work_part.Bodies() # BodyCollection
features = work_part.Features() # FeatureCollection
measure_mgr = work_part.MeasureManager() # For mass properties
material_mgr = work_part.MaterialManager() # Material assignment
```
### Expression Manipulation
```python
# Find expression by name
expr = work_part.Expressions.FindObject("width")
# Get value
value = expr.Value # float
units = expr.Units # Unit object
# Set value (with units)
unit = work_part.UnitCollection.FindObject("MilliMeter")
work_part.Expressions.EditWithUnits(expr, unit, "50.0")
# Import from .exp file (batch update - robust method)
work_part.Expressions.ImportFromFile(
exp_path,
NXOpen.ExpressionCollection.ExportMode.Replace
)
# Update model after changes
session.UpdateManager.DoUpdate(session.SetUndoMark(...))
```
### Mass Properties
```python
# Create mass measurement
mass_props = work_part.MeasureManager().NewMassProperties(
accuracy, # int: 0.97-0.99 typical
infoUnits, # MassPropertiesInfo for units
bodies # Array of Body objects
)
# Get properties
mass = mass_props.Mass # float (kg)
cog = mass_props.CenterOfGravity # Point3d (x,y,z)
inertia = mass_props.MomentsOfInertia # Matrix3x3
volume = mass_props.Volume # float (mm³)
area = mass_props.SurfaceArea # float (mm²)
```
### CAE/FEM Access
```python
from NXOpen.CAE import CaeSession
# CAE session utilities (from a10510.html)
cae_session = session.CaeSession() # Returns CaeSession
# Key CaeSession methods:
material_utils = cae_session.MaterialUtils() # MaterialUtilities
association_utils = cae_session.AssociationUtils() # AssociationUtilities
mesh_mapping = cae_session.MeshMappingUtils() # MeshMapping.Utils
penetration_mgr = cae_session.PenetrationCheckManager() # PenetrationCheck.Manager
# FEM/SIM parts
fem_part = work_part # When .fem is work part
sim_part = work_part # When .sim is work part
# Access mesh (from FemPart)
mesh_manager = fem_part.MeshManager
for mesh in mesh_manager.GetMeshes():
nodes = mesh.GetNodes()
elements = mesh.GetElements()
# Access solutions (from SimPart)
sim_simulation = sim_part.FindObject("Simulation")
for solution in sim_simulation.Solutions:
name = solution.Name
sol_type = solution.SolutionType
solution.Solve() # Run solver
```
### Feature Manipulation
```python
# Get feature by name
feature = work_part.Features.FindObject("FEATURE_NAME")
# Suppress/unsuppress
feature.Suppress()
feature.Unsuppress()
# Get expression-linked features
for expr in feature.GetExpressions():
print(expr.Name, expr.Value)
```
---
## 8. Structure de Fichiers Finale
```
optimization_engine/
├── hooks/
│ ├── __init__.py
│ ├── nx_cad/
│ │ ├── __init__.py
│ │ ├── part_manager.py
│ │ ├── expression_manager.py
│ │ ├── feature_manager.py
│ │ ├── geometry_query.py
│ │ └── assembly_manager.py
│ ├── nx_fem/
│ │ ├── __init__.py
│ │ ├── mesh_manager.py
│ │ ├── material_manager.py
│ │ ├── property_manager.py
│ │ ├── boundary_conditions.py
│ │ └── connection_manager.py
│ └── nx_sim/
│ ├── __init__.py
│ ├── solution_manager.py
│ ├── solve_manager.py
│ └── result_manager.py
├── extractors/
│ ├── __init__.py
│ ├── displacement.py # ✓
│ ├── stress.py # À améliorer
│ ├── strain.py # Nouveau
│ ├── frequency.py # ✓
│ ├── modal_mass.py # Nouveau
│ ├── temperature.py # Nouveau
│ ├── reaction_force.py # Nouveau
│ ├── cad_mass.py # Nouveau (NX Open)
│ ├── cad_volume.py # Nouveau
│ └── zernike/ # ✓
└── manipulators/
├── __init__.py
├── morpher.py # Future
└── ffd.py # Future
```
---
## 9. Métriques de Succès
### Phase 1 Complete When: ✓ ACHIEVED
- [x] Peut ouvrir/fermer pièce NX via Python ✓ (part_manager.py)
- [x] Peut modifier expressions et update ✓ (expression_manager.py)
- [x] Peut extraire masse depuis CAD (pas BDF) ✓ (geometry_query.py)
- [x] Extrait stress max fiable de tous les éléments ✓ (extract_von_mises_stress.py)
### Phase 2 Complete When: ✓ ACHIEVED (2025-12-06)
- [x] Workflow complet: expression → BDF → solve → extract ✓
- [x] Support de toutes les contraintes mécaniques ✓ (principal stress, SPC forces)
- [x] Extraction de strain energy fonctionnelle ✓ (extract_strain_energy.py)
- [x] **Model Introspection**: Full model analysis capability ✓ (model_introspection.py)
### Phase 3 Complete When: ✓ CORE ACHIEVED (2025-12-06)
- [x] Temperature extraction from OP2 functional ✓ (extract_temperature.py)
- [x] Thermal gradient extraction functional ✓ (extract_temperature_gradient)
- [x] Heat flux extraction functional ✓ (extract_heat_flux)
- [x] Modal mass extraction from F06 functional ✓ (extract_modal_mass.py)
- [ ] At least one thermal optimization study runs successfully
- [ ] Thermo-mechanical coupling documented
- [ ] Thermal BC setup hook (NXOpen.CAE)
---
## 10. Références
### Documentation NX Open (via MCP)
**MCP Tools for NX Open Documentation**:
```
mcp__siemens-docs__nxopen_get_class(className) # Get class doc
mcp__siemens-docs__nxopen_get_index(indexType) # Browse class index
mcp__siemens-docs__nxopen_fetch_page(pagePath) # Fetch any page
mcp__siemens-docs__siemens_docs_list() # List available resources
```
**Key Page IDs**:
| Class | Page ID | URL Path |
|-------|---------|----------|
| Session | a03318.html | `/nxopen_python_ref/a03318.html` |
| Part | a02434.html | `/nxopen_python_ref/a02434.html` |
| BasePart | a00266.html | `/nxopen_python_ref/a00266.html` |
| CaeSession | a10510.html | `/nxopen_python_ref/a10510.html` |
| Class Index | classes.html | `/nxopen_python_ref/classes.html` |
| Function Index | functions_*.html | `/nxopen_python_ref/functions_m.html` (etc.) |
### Ressources Externes
- [NXJournaling.com](https://nxjournaling.com/) - NX Open examples and tutorials
- [NXOpen TSE](https://nxopentsedocumentation.thescriptingengineer.com/) - The Scripting Engineer docs
- [PyNastran Documentation](https://pynastran-git.readthedocs.io/) - OP2/BDF parsing
- [NAFEMS Python FEA Course](https://www.nafems.org/training/e-learning/python-for-fea-automation-and-optimization/)
### Papers & Academic
- Kriging for FEA Optimization: [Springer](https://link.springer.com/article/10.1007/s00158-019-02211-z)
- OpenMDAO Framework: [NASA Technical Report](https://openmdao.org/)
- SIMP Topology Optimization: Bendsøe & Sigmund methods
### Related Atomizer Documentation
- [ATOMIZER_NXOPEN_MASTER_PLAN.md](../07_DEVELOPMENT/ATOMIZER_NXOPEN_MASTER_PLAN.md) - Detailed API specs
- [SYS_12_EXTRACTOR_LIBRARY.md](../protocols/system/SYS_12_EXTRACTOR_LIBRARY.md) - Extractor catalog
- [MCP Server README](../../mcp-server/README.md) - Siemens docs proxy setup
---
## 11. Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-06 | Initial roadmap from industry research |
| 2.0 | 2025-12-06 | Merged with ATOMIZER_NXOPEN_MASTER_PLAN.md, added verified MCP API mappings, updated extractor status |
| 2.1 | 2025-12-06 | **Phase 1b Complete**: NX Open hooks implemented (part_manager, expression_manager, geometry_query, feature_manager) |
| 2.2 | 2025-12-06 | **Phase 2 Complete**: Principal stress, strain energy, SPC forces extractors + solver_manager |
| 2.3 | 2025-12-06 | **Model Introspection**: Added comprehensive model_introspection.py for full part/sim/op2 analysis |
| 2.4 | 2025-12-06 | **Phase 3 Prepared**: Detailed roadmap for thermal/dynamic extractors with implementation guide |
| 2.5 | 2025-12-06 | **Phase 3 Core Complete**: Temperature (E15-E17) and modal mass (E18) extractors implemented |
---
*Generated with assistance from Claude Code using MCP Siemens Documentation tools*

View File

@@ -0,0 +1,585 @@
# 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) |