Files
Atomizer/docs/protocols/extensions/EXT_01_CREATE_EXTRACTOR.md

396 lines
9.7 KiB
Markdown
Raw Permalink Normal View History

# EXT_01: Create New Extractor
<!--
PROTOCOL: Create New Physics Extractor
LAYER: Extensions
VERSION: 1.0
STATUS: Active
LAST_UPDATED: 2025-12-05
PRIVILEGE: power_user
LOAD_WITH: [SYS_12_EXTRACTOR_LIBRARY]
-->
## Overview
This protocol guides you through creating a new physics extractor for the centralized extractor library. Follow this when you need to extract results not covered by existing extractors.
**Privilege Required**: power_user or admin
---
## When to Use
| Trigger | Action |
|---------|--------|
| Need physics not in library | Follow this protocol |
| "create extractor", "new extractor" | Follow this protocol |
| Custom result extraction needed | Follow this protocol |
**First**: Check [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md) - the functionality may already exist!
---
## Quick Reference
**Create in**: `optimization_engine/extractors/`
**Export from**: `optimization_engine/extractors/__init__.py`
**Document in**: Update SYS_12 and this protocol
**Template location**: `docs/protocols/extensions/templates/extractor_template.py`
---
## Step-by-Step Guide
### Step 1: Verify Need
Before creating:
1. Check existing extractors in [SYS_12](../system/SYS_12_EXTRACTOR_LIBRARY.md)
2. Search codebase: `grep -r "your_physics" optimization_engine/`
3. Confirm no existing solution
### Step 1.5: Research NX Open APIs (REQUIRED for NX extractors)
**If the extractor needs NX Open APIs** (not just pyNastran OP2 parsing):
```
# 1. Search for relevant NX Open APIs
siemens_docs_search("inertia properties NXOpen")
siemens_docs_search("mass properties body NXOpen.CAE")
# 2. Fetch detailed documentation for promising classes
siemens_docs_fetch("NXOpen.MeasureManager")
siemens_docs_fetch("NXOpen.UF.UFWeight")
# 3. Get method signatures
siemens_docs_search("AskMassProperties NXOpen")
```
**When to use NX Open vs pyNastran:**
| Data Source | Tool | Example |
|-------------|------|---------|
| OP2 results (stress, disp, freq) | pyNastran | `extract_displacement()` |
| CAD properties (mass, inertia) | NX Open | New extractor with NXOpen API |
| BDF data (mesh, properties) | pyNastran | `extract_mass_from_bdf()` |
| NX expressions | NX Open | `extract_mass_from_expression()` |
| FEM model data | NX Open CAE | Needs `NXOpen.CAE.*` APIs |
**Document the APIs used** in the extractor docstring:
```python
def extract_inertia(part_file: Path) -> Dict[str, Any]:
"""
Extract mass and inertia properties from NX part.
NX Open APIs Used:
- NXOpen.MeasureManager.NewMassProperties()
- NXOpen.MeasureBodies.InformationUnit
- NXOpen.UF.UFWeight.AskProps()
See: docs.sw.siemens.com for full API reference
"""
```
### Step 2: Create Extractor File
Create `optimization_engine/extractors/extract_{physics}.py`:
```python
"""
Extract {Physics Name} from FEA results.
Author: {Your Name}
Created: {Date}
Version: 1.0
"""
from pathlib import Path
from typing import Dict, Any, Optional, Union
from pyNastran.op2.op2 import OP2
def extract_{physics}(
op2_file: Union[str, Path],
subcase: int = 1,
# Add other parameters as needed
) -> Dict[str, Any]:
"""
Extract {physics description} from OP2 file.
Args:
op2_file: Path to the OP2 results file
subcase: Subcase number to extract (default: 1)
Returns:
Dictionary containing:
- '{main_result}': The primary result value
- '{secondary}': Additional result info
- 'subcase': The subcase extracted
Raises:
FileNotFoundError: If OP2 file doesn't exist
KeyError: If subcase not found in results
ValueError: If result data is invalid
Example:
>>> result = extract_{physics}('model.op2', subcase=1)
>>> print(result['{main_result}'])
123.45
"""
op2_file = Path(op2_file)
if not op2_file.exists():
raise FileNotFoundError(f"OP2 file not found: {op2_file}")
# Read OP2 file
op2 = OP2()
op2.read_op2(str(op2_file))
# Extract your physics
# TODO: Implement extraction logic
# Example for displacement-like result:
if subcase not in op2.displacements:
raise KeyError(f"Subcase {subcase} not found in results")
data = op2.displacements[subcase]
# Process data...
return {
'{main_result}': computed_value,
'{secondary}': secondary_value,
'subcase': subcase,
}
# Optional: Class-based extractor for complex cases
class {Physics}Extractor:
"""
Class-based extractor for {physics} with state management.
Use when extraction requires multiple steps or configuration.
"""
def __init__(self, op2_file: Union[str, Path], **config):
self.op2_file = Path(op2_file)
self.config = config
self._op2 = None
def _load_op2(self):
"""Lazy load OP2 file."""
if self._op2 is None:
self._op2 = OP2()
self._op2.read_op2(str(self.op2_file))
return self._op2
def extract(self, subcase: int = 1) -> Dict[str, Any]:
"""Extract results for given subcase."""
op2 = self._load_op2()
# Implementation here
pass
```
### Step 3: Add to __init__.py
Edit `optimization_engine/extractors/__init__.py`:
```python
# Add import
from .extract_{physics} import extract_{physics}
# Or for class
from .extract_{physics} import {Physics}Extractor
# Add to __all__
__all__ = [
# ... existing exports ...
'extract_{physics}',
'{Physics}Extractor',
]
```
### Step 4: Write Tests
Create `tests/test_extract_{physics}.py`:
```python
"""Tests for {physics} extractor."""
import pytest
from pathlib import Path
from optimization_engine.extractors import extract_{physics}
class TestExtract{Physics}:
"""Test suite for {physics} extraction."""
@pytest.fixture
def sample_op2(self, tmp_path):
"""Create or copy sample OP2 for testing."""
# Either copy existing test file or create mock
pass
def test_basic_extraction(self, sample_op2):
"""Test basic extraction works."""
result = extract_{physics}(sample_op2)
assert '{main_result}' in result
assert isinstance(result['{main_result}'], float)
def test_file_not_found(self):
"""Test error handling for missing file."""
with pytest.raises(FileNotFoundError):
extract_{physics}('nonexistent.op2')
def test_invalid_subcase(self, sample_op2):
"""Test error handling for invalid subcase."""
with pytest.raises(KeyError):
extract_{physics}(sample_op2, subcase=999)
```
### Step 5: Document
#### Update SYS_12_EXTRACTOR_LIBRARY.md
Add to Quick Reference table:
```markdown
| E{N} | {Physics} | `extract_{physics}()` | .op2 | {unit} |
```
Add detailed section:
```markdown
### E{N}: {Physics} Extraction
**Module**: `optimization_engine.extractors.extract_{physics}`
\`\`\`python
from optimization_engine.extractors import extract_{physics}
result = extract_{physics}(op2_file, subcase=1)
{main_result} = result['{main_result}']
\`\`\`
```
#### Update skills/modules/extractors-catalog.md
Add entry following existing pattern.
### Step 6: Validate
```bash
# Run tests
pytest tests/test_extract_{physics}.py -v
# Test import
python -c "from optimization_engine.extractors import extract_{physics}; print('OK')"
# Test with real file
python -c "
from optimization_engine.extractors import extract_{physics}
result = extract_{physics}('path/to/test.op2')
print(result)
"
```
---
## Extractor Design Guidelines
### Do's
- Return dictionaries with clear keys
- Include metadata (subcase, units, etc.)
- Handle edge cases gracefully
- Provide clear error messages
- Document all parameters and returns
- Write tests
### Don'ts
- Don't re-parse OP2 multiple times in one call
- Don't hardcode paths
- Don't swallow exceptions silently
- Don't return raw pyNastran objects
- Don't modify input files
### Naming Conventions
| Type | Convention | Example |
|------|------------|---------|
| File | `extract_{physics}.py` | `extract_thermal.py` |
| Function | `extract_{physics}` | `extract_thermal` |
| Class | `{Physics}Extractor` | `ThermalExtractor` |
| Return key | lowercase_with_underscores | `max_temperature` |
---
## Examples
### Example: Thermal Gradient Extractor
```python
"""Extract thermal gradients from temperature results."""
from pathlib import Path
from typing import Dict, Any
from pyNastran.op2.op2 import OP2
import numpy as np
def extract_thermal_gradient(
op2_file: Path,
subcase: int = 1,
direction: str = 'magnitude'
) -> Dict[str, Any]:
"""
Extract thermal gradient from temperature field.
Args:
op2_file: Path to OP2 file
subcase: Subcase number
direction: 'magnitude', 'x', 'y', or 'z'
Returns:
Dictionary with gradient results
"""
op2 = OP2()
op2.read_op2(str(op2_file))
temps = op2.temperatures[subcase]
# Calculate gradient...
return {
'max_gradient': max_grad,
'mean_gradient': mean_grad,
'max_gradient_location': location,
'direction': direction,
'subcase': subcase,
'unit': 'K/mm'
}
```
---
## Troubleshooting
| Issue | Cause | Solution |
|-------|-------|----------|
| Import error | Not added to __init__.py | Add export |
| "No module" | Wrong file location | Check path |
| KeyError | Wrong OP2 data structure | Debug OP2 contents |
| Tests fail | Missing test data | Create fixtures |
---
## Cross-References
- **Reference**: [SYS_12_EXTRACTOR_LIBRARY](../system/SYS_12_EXTRACTOR_LIBRARY.md)
- **Template**: `templates/extractor_template.py`
- **Related**: [EXT_02_CREATE_HOOK](./EXT_02_CREATE_HOOK.md)
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-05 | Initial release |