# EXT_01: Create New Extractor ## 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 |