# 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 |
