# Dynamic Response Processor - Master Implementation Plan

## Atomizer Feature: Modal Superposition Engine for Dynamic Optimization

**Document Version**: 1.0  
**Created**: 2025-12-22  
**Author**: Antoine + Claude  
**Status**: Implementation Ready  
**Priority**: High - Differentiating Feature

---

## Executive Summary

This document provides the complete implementation roadmap for adding **analytical dynamic response processing** to Atomizer. This feature enables optimization of structures under random vibration, sine sweep, and shock environments — **without running expensive frequency response FEA solutions for each design iteration**.

### Value Proposition

| Traditional Workflow | Atomizer Dynamic Workflow |
|---------------------|---------------------------|
| Run SOL 103 (modal) | Run SOL 103 (modal) |
| Run SOL 111 (frequency response) | **Skip** - compute analytically |
| Run SOL 112 (random) | **Skip** - compute analytically |
| ~15-30 min per iteration | ~30 sec per iteration |
| 100 iterations = 25-50 hours | 100 iterations = **50 minutes** |

**Key Differentiator**: SATK is a post-processor. Atomizer becomes an **optimization engine with built-in dynamic response intelligence**.

---

## Table of Contents

1. [Technical Foundation](#1-technical-foundation)
2. [Architecture Design](#2-architecture-design)
3. [Implementation Phases](#3-implementation-phases)
4. [Module Specifications](#4-module-specifications)
5. [Integration Points](#5-integration-points)
6. [Validation Strategy](#6-validation-strategy)
7. [Performance Targets](#7-performance-targets)
8. [Risk Mitigation](#8-risk-mitigation)
9. [Future Extensions](#9-future-extensions)

---

## 1. Technical Foundation

### 1.1 The Physics: Why This Works

The fundamental insight is that **dynamic response can be computed from modal data alone**.

For a structure with N degrees of freedom, the equation of motion is:

```
[M]{ü} + [C]{u̇} + [K]{u} = {F(t)}
```

Modal analysis (SOL 103) decomposes this into N independent single-DOF equations:

```
q̈ᵢ + 2ζᵢωᵢq̇ᵢ + ωᵢ²qᵢ = Lᵢ·a_base / mᵢ
```

Where:
- `qᵢ` = modal coordinate (how much mode i is excited)
- `ωᵢ` = natural frequency of mode i (rad/s)
- `ζᵢ` = damping ratio of mode i
- `Lᵢ` = modal participation factor (how mode i couples to base motion)
- `mᵢ` = generalized (modal) mass

**The key**: Once we extract (ωᵢ, ζᵢ, Lᵢ, φᵢ) from SOL 103, we can compute the response to ANY excitation profile analytically — no more FEA needed.

### 1.2 Transfer Function Mathematics

#### Single Mode Transfer Function

For base acceleration input, the displacement transfer function of mode i is:

```python
def H_displacement(f, f_n, zeta):
    """
    Displacement per unit base acceleration.
    
    H(f) = 1 / [(ωₙ² - ω²) + j·2·ζ·ωₙ·ω]
    
    In dimensionless form with r = f/fₙ:
    H(r) = 1 / [(1 - r²) + j·2·ζ·r]
    """
    r = f / f_n
    return 1 / ((1 - r**2) + 2j * zeta * r)
```

#### Acceleration Transmissibility

For base excitation, the ratio of response acceleration to input acceleration:

```python
def transmissibility(f, f_n, zeta):
    """
    T(f) = (1 + j·2·ζ·r) / [(1 - r²) + j·2·ζ·r]
    
    |T| = 1 at low frequency (rigid body)
    |T| = Q at resonance (amplification)
    |T| → 0 at high frequency (isolation)
    """
    r = f / f_n
    return (1 + 2j * zeta * r) / ((1 - r**2) + 2j * zeta * r)
```

#### Multi-Mode Combination

Total response sums all modal contributions:

```python
def total_response(frequencies, modes, participation_factors):
    """
    H_total(f) = Σᵢ Lᵢ · φᵢ · Hᵢ(f) / mᵢ
    
    Where:
    - Lᵢ = participation factor
    - φᵢ = mode shape at point of interest
    - Hᵢ = modal transfer function
    - mᵢ = generalized mass
    """
    H_total = np.zeros(len(frequencies), dtype=complex)
    for mode in modes:
        H_mode = H_displacement(frequencies, mode.frequency, mode.damping)
        H_total += mode.participation * mode.shape * H_mode / mode.gen_mass
    return H_total
```

### 1.3 Random Vibration Response

Random vibration is characterized by Power Spectral Density (PSD), typically in G²/Hz.

#### Response PSD

```python
S_response(f) = |H(f)|² × S_input(f)
```

#### RMS (Root Mean Square)

```python
σ² = ∫ S_response(f) df  # Mean square
RMS = √σ²                 # Root mean square
```

#### Peak Response

For Gaussian random processes:
```python
Peak = k × RMS
```

Where k (crest factor) depends on:
- **k = 3.0**: Standard 3-sigma (99.7% probability not exceeded)
- **k = 3.5-4.0**: Von Mises stress (non-Gaussian distribution)
- **k = 4.0-4.5**: Fatigue-critical applications

#### Miles' Equation (SDOF Approximation)

For quick estimates with white noise PSD:

```python
Grms = √(π/2 × fₙ × Q × PSD_level)
```

Where Q = 1/(2ζ) is the quality factor.

### 1.4 Stress Recovery from Modal Data

Element stresses can also be computed via modal superposition:

```python
σ(t) = Σᵢ qᵢ(t) × σᵢ_modal
```

Where `σᵢ_modal` is the stress pattern when mode i has unit amplitude.

For random vibration, stress PSD:
```python
S_σ(f) = |Σᵢ Lᵢ × σᵢ_modal × Hᵢ(f)|² × S_input(f)
```

### 1.5 What We Extract from SOL 103

| Data | Source | Used For |
|------|--------|----------|
| Natural frequencies (fₙ) | OP2: eigenvalues | Transfer function poles |
| Mode shapes (φ) | OP2: eigenvectors | Response at specific nodes |
| Generalized mass (m) | OP2/F06: eigenvalues | Modal scaling |
| Modal effective mass | F06: MEFFMASS | Participation estimation |
| Participation factors (L) | F06: MEFFMASS | Base excitation coupling |
| Modal stress shapes | OP2: modal stress | Stress recovery |

---

## 2. Architecture Design

### 2.1 Module Structure

```
optimization_engine/
├── processors/
│   └── dynamic_response/
│       ├── __init__.py                 # Public API exports
│       ├── modal_database.py           # ModalDatabase, ModeShape classes
│       ├── transfer_functions.py       # FRF computation engine
│       ├── random_vibration.py         # Random response processor
│       ├── sine_sweep.py               # Harmonic response processor
│       ├── shock_response.py           # SRS computation (Phase 5)
│       ├── stress_recovery.py          # Modal stress combination
│       ├── psd_profiles.py             # Standard PSD definitions
│       └── utils/
│           ├── __init__.py
│           ├── frequency_utils.py      # Frequency array generation
│           ├── integration.py          # Spectral integration methods
│           └── statistics.py           # Peak factors, distributions
│
├── extractors/
│   ├── extract_modal_database.py       # Full modal data extraction
│   ├── extract_random_response.py      # Random vibration objectives
│   ├── extract_sine_response.py        # Sine sweep objectives
│   └── extract_dynamic_stress.py       # Dynamic stress extraction
│
├── hooks/
│   └── dynamic_response/
│       ├── pre_dynamic_analysis.py     # Validate modal data
│       └── post_dynamic_analysis.py    # Cache results, logging
│
└── templates/
    ├── random_vibration_optimization.json
    ├── sine_sweep_optimization.json
    └── multi_environment_optimization.json
```

### 2.2 Class Hierarchy

```
┌─────────────────────────────────────────────────────────────┐
│                    DynamicResponseProcessor                  │
│                        (Abstract Base)                       │
├─────────────────────────────────────────────────────────────┤
│ + modal_db: ModalDatabase                                   │
│ + frf_engine: TransferFunctionEngine                        │
│ + compute_response() → ResponseResult                       │
│ + compute_at_node(node_id) → float                         │
│ + compute_stress(element_ids) → Dict                        │
└─────────────────────────────────────────────────────────────┘
                              │
          ┌───────────────────┼───────────────────┐
          ▼                   ▼                   ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│ RandomVibration │  │   SineSweep     │  │ ShockResponse   │
│   Processor     │  │   Processor     │  │   Processor     │
├─────────────────┤  ├─────────────────┤  ├─────────────────┤
│ + psd_profile   │  │ + sweep_rate    │  │ + srs_damping   │
│ + compute_grms()│  │ + dwell_freqs   │  │ + compute_srs() │
│ + compute_peak()│  │ + compute_peak()│  │ + compute_pseudo│
└─────────────────┘  └─────────────────┘  └─────────────────┘
```

### 2.3 Data Flow

```
┌──────────────────────────────────────────────────────────────────┐
│                      OPTIMIZATION LOOP                            │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  1. UPDATE DESIGN PARAMETERS                                      │
│     NX Expression Manager → thickness=2.5mm, rib_height=12mm     │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  2. RUN SOL 103 (Modal Analysis)                                  │
│     NX Solver → model.op2, model.f06                             │
│     Time: ~20-60 seconds                                          │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  3. EXTRACT MODAL DATABASE                                        │
│     pyNastran → ModalDatabase object                              │
│     - Frequencies, mode shapes, participation factors             │
│     - Modal stress shapes (if needed)                             │
│     Time: ~0.5-2 seconds                                          │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  4. COMPUTE DYNAMIC RESPONSE (Analytical)                         │
│     RandomVibrationProcessor.compute_response()                   │
│     - Build transfer functions: ~5ms                              │
│     - Compute response PSD: ~10ms                                 │
│     - Integrate for RMS: ~5ms                                     │
│     - Stress recovery: ~50ms                                      │
│     Time: ~50-100 milliseconds                                    │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  5. RETURN OBJECTIVES & CONSTRAINTS                               │
│     {                                                             │
│       'grms': 8.4,                                               │
│       'peak_acceleration': 25.2,                                  │
│       'peak_stress': 145.6,                                       │
│       'first_frequency': 127.3                                    │
│     }                                                             │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  6. OPTUNA SUGGESTS NEXT DESIGN                                   │
│     TPE/CMA-ES/NSGA-II → next trial parameters                   │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                        [REPEAT 1-6]
```

### 2.4 Memory Management

For large models with many modes/elements, we need smart memory handling:

```python
class ModalDatabase:
    """
    Memory-efficient modal data storage.
    
    Strategies:
    1. Lazy loading - only load mode shapes when needed
    2. Sparse storage - only store non-zero eigenvector components
    3. Node filtering - only extract nodes of interest
    4. Disk caching - cache to HDF5 for reuse across trials
    """
    
    def __init__(self, op2_file, lazy_load=True):
        self._op2_path = op2_file
        self._modes_loaded = False
        self._mode_shapes = None  # Load on demand
        
    @property
    def mode_shapes(self):
        if not self._modes_loaded:
            self._load_mode_shapes()
        return self._mode_shapes
    
    def get_modes_for_nodes(self, node_ids: List[int]) -> np.ndarray:
        """Extract mode shapes only for specified nodes."""
        # Efficient partial loading
        pass
    
    def cache_to_hdf5(self, cache_path: Path):
        """Cache modal database for fast reload."""
        pass
    
    @classmethod
    def from_cache(cls, cache_path: Path) -> 'ModalDatabase':
        """Load from HDF5 cache (10x faster than OP2)."""
        pass
```

---

## 3. Implementation Phases

### Phase 1: Core Infrastructure (Week 1)

**Goal**: Build the foundational classes and basic functionality.

#### Tasks

| Task | Description | Effort | Deliverable |
|------|-------------|--------|-------------|
| 1.1 | Create `ModeShape` dataclass | 2h | `modal_database.py` |
| 1.2 | Create `ModalDatabase` class | 4h | `modal_database.py` |
| 1.3 | Implement OP2 extraction | 4h | `ModalDatabase.from_op2_f06()` |
| 1.4 | Implement F06 MEFFMASS parsing | 3h | Modal effective mass extraction |
| 1.5 | Create `TransferFunctionEngine` | 4h | `transfer_functions.py` |
| 1.6 | Single mode FRF | 2h | `single_mode_frf()` |
| 1.7 | Multi-mode combination | 2h | `base_excitation_frf()` |
| 1.8 | Unit tests for FRF | 3h | `tests/test_transfer_functions.py` |

**Validation Checkpoint**: 
- FRF matches closed-form SDOF solution
- Modal database correctly extracts from sample OP2

#### Code: ModeShape Dataclass

```python
# optimization_engine/processors/dynamic_response/modal_database.py

from dataclasses import dataclass, field
from typing import Dict, Optional, List
import numpy as np

@dataclass
class ModeShape:
    """
    Single mode shape data container.
    
    Stores all information needed for modal superposition
    of one natural mode.
    """
    # Identification
    mode_number: int
    
    # Dynamic properties
    frequency: float  # Hz
    damping_ratio: float = 0.02  # Dimensionless (2% default)
    
    # Modal mass/stiffness
    generalized_mass: float = 1.0  # kg (normalized)
    generalized_stiffness: float = 0.0  # N/m
    
    # Participation factors (for base excitation)
    participation_x: float = 0.0
    participation_y: float = 0.0
    participation_z: float = 0.0
    
    # Modal effective mass (absolute contribution)
    effective_mass_x: float = 0.0
    effective_mass_y: float = 0.0
    effective_mass_z: float = 0.0
    
    # Eigenvector at specific nodes
    # Dict[node_id, np.array([tx, ty, tz, rx, ry, rz])]
    eigenvector: Dict[int, np.ndarray] = field(default_factory=dict)
    
    # Modal stress shapes for stress recovery
    # Dict[element_id, np.array([sxx, syy, szz, sxy, syz, sxz, vm])]
    stress_shapes: Dict[int, np.ndarray] = field(default_factory=dict)
    
    # Derived properties
    @property
    def omega(self) -> float:
        """Angular frequency (rad/s)."""
        return 2 * np.pi * self.frequency
    
    @property
    def omega_squared(self) -> float:
        """ω² for efficiency."""
        return self.omega ** 2
    
    @property
    def quality_factor(self) -> float:
        """Q = 1/(2ζ) - amplification at resonance."""
        if self.damping_ratio > 0:
            return 1 / (2 * self.damping_ratio)
        return np.inf
    
    @property
    def half_power_bandwidth(self) -> float:
        """Δf = fₙ/Q - bandwidth at -3dB."""
        return self.frequency / self.quality_factor
    
    def get_participation(self, direction: str) -> float:
        """Get participation factor for direction ('x', 'y', 'z')."""
        return getattr(self, f'participation_{direction}', 0.0)
    
    def get_effective_mass(self, direction: str) -> float:
        """Get effective mass for direction."""
        return getattr(self, f'effective_mass_{direction}', 0.0)
    
    def get_eigenvector_component(
        self, 
        node_id: int, 
        direction: str
    ) -> float:
        """Get eigenvector component at node in direction."""
        if node_id not in self.eigenvector:
            return 0.0
        dir_map = {'x': 0, 'y': 1, 'z': 2, 'rx': 3, 'ry': 4, 'rz': 5}
        idx = dir_map.get(direction, 0)
        return self.eigenvector[node_id][idx]
```

### Phase 2: Random Vibration Processor (Week 2)

**Goal**: Full random vibration response calculation with stress recovery.

#### Tasks

| Task | Description | Effort | Deliverable |
|------|-------------|--------|-------------|
| 2.1 | Create `PSDProfile` class | 2h | `psd_profiles.py` |
| 2.2 | Standard profiles (GEVS, MIL-STD) | 2h | `STANDARD_PROFILES` dict |
| 2.3 | PSD interpolation (log-log) | 2h | `PSDProfile.interpolate()` |
| 2.4 | Create `RandomVibrationProcessor` | 4h | `random_vibration.py` |
| 2.5 | Response PSD calculation | 3h | `compute_response_psd()` |
| 2.6 | Spectral integration | 2h | `compute_rms()` |
| 2.7 | Peak factor calculation | 2h | Gaussian + non-Gaussian |
| 2.8 | Modal contribution analysis | 2h | Identify dominant modes |
| 2.9 | Stress recovery | 4h | `compute_stress_response()` |
| 2.10 | Result dataclass | 2h | `RandomVibrationResult` |
| 2.11 | Integration tests | 4h | Miles' equation validation |

**Validation Checkpoint**:
- Grms matches Miles' equation for SDOF
- Multi-mode response reasonable for sample model
- Stress recovery gives correct distribution

#### Code: PSD Profiles

```python
# optimization_engine/processors/dynamic_response/psd_profiles.py

import numpy as np
from scipy import interpolate
from dataclasses import dataclass
from typing import List, Tuple, Optional
from pathlib import Path


@dataclass
class PSDProfile:
    """
    Power Spectral Density profile for random vibration.
    
    Stores PSD as G²/Hz vs frequency, with log-log interpolation.
    """
    name: str
    frequencies: np.ndarray  # Hz
    psd_values: np.ndarray   # G²/Hz
    description: str = ""
    source: str = ""  # Standard reference
    
    @property
    def freq_min(self) -> float:
        return float(self.frequencies.min())
    
    @property
    def freq_max(self) -> float:
        return float(self.frequencies.max())
    
    @property
    def grms(self) -> float:
        """Overall Grms level."""
        from scipy.integrate import trapezoid
        return np.sqrt(trapezoid(self.psd_values, self.frequencies))
    
    def interpolate(self, frequencies: np.ndarray) -> np.ndarray:
        """
        Interpolate PSD to arbitrary frequencies.
        
        Uses log-log interpolation (standard for PSD).
        Clamps to edge values outside defined range.
        """
        # Safety: ensure positive frequencies
        frequencies = np.maximum(frequencies, 1e-6)
        
        log_f = np.log10(self.frequencies)
        log_psd = np.log10(np.maximum(self.psd_values, 1e-20))
        
        interp_func = interpolate.interp1d(
            log_f, log_psd,
            kind='linear',
            bounds_error=False,
            fill_value=(log_psd[0], log_psd[-1])
        )
        
        return 10 ** interp_func(np.log10(frequencies))
    
    def plot(self, ax=None, **kwargs):
        """Plot PSD profile."""
        import matplotlib.pyplot as plt
        if ax is None:
            fig, ax = plt.subplots()
        ax.loglog(self.frequencies, self.psd_values, **kwargs)
        ax.set_xlabel('Frequency (Hz)')
        ax.set_ylabel('PSD (G²/Hz)')
        ax.set_title(f'{self.name} - {self.grms:.2f} Grms')
        ax.grid(True, which='both', alpha=0.3)
        return ax
    
    @classmethod
    def from_breakpoints(
        cls, 
        breakpoints: List[Tuple[float, float]],
        name: str = "Custom",
        description: str = ""
    ) -> 'PSDProfile':
        """
        Create PSD from breakpoint specification.
        
        Args:
            breakpoints: List of (frequency_hz, psd_g2_hz) tuples
            name: Profile name
            description: Description text
        
        Example:
            psd = PSDProfile.from_breakpoints([
                (20, 0.01),    # 20 Hz: 0.01 G²/Hz
                (80, 0.04),    # 80 Hz: 0.04 G²/Hz (ramp up)
                (500, 0.04),   # 500 Hz: 0.04 G²/Hz (flat)
                (2000, 0.007)  # 2000 Hz: 0.007 G²/Hz (ramp down)
            ], name='MIL-STD-810')
        """
        breakpoints = sorted(breakpoints, key=lambda x: x[0])
        frequencies = np.array([bp[0] for bp in breakpoints])
        psd_values = np.array([bp[1] for bp in breakpoints])
        return cls(
            name=name,
            frequencies=frequencies,
            psd_values=psd_values,
            description=description
        )
    
    @classmethod
    def from_csv(cls, filepath: Path, name: str = None) -> 'PSDProfile':
        """Load PSD from CSV file with freq,psd columns."""
        data = np.loadtxt(filepath, delimiter=',', skiprows=1)
        return cls(
            name=name or filepath.stem,
            frequencies=data[:, 0],
            psd_values=data[:, 1]
        )
    
    @classmethod
    def flat(
        cls,
        level: float,
        freq_min: float = 20,
        freq_max: float = 2000,
        name: str = "Flat"
    ) -> 'PSDProfile':
        """Create flat (white noise) PSD profile."""
        return cls(
            name=name,
            frequencies=np.array([freq_min, freq_max]),
            psd_values=np.array([level, level]),
            description=f"Flat PSD at {level} G²/Hz"
        )


# ============================================================================
# STANDARD PSD PROFILES
# ============================================================================

STANDARD_PROFILES = {
    # NASA GEVS (General Environmental Verification Standard)
    'GEVS': PSDProfile.from_breakpoints([
        (20, 0.026),
        (50, 0.16),
        (800, 0.16),
        (2000, 0.026),
    ], name='NASA GEVS', 
       description='NASA General Environmental Verification Standard'),
    
    # MIL-STD-810G Category 1 (Basic)
    'MIL-STD-810G_CAT1': PSDProfile.from_breakpoints([
        (10, 0.04),
        (40, 0.04),
        (500, 0.04),
        (2000, 0.01),
    ], name='MIL-STD-810G Cat 1',
       description='Basic transportation vibration'),
    
    # MIL-STD-810G Category 4 (Truck)
    'MIL-STD-810G_CAT4': PSDProfile.from_breakpoints([
        (5, 0.001),
        (10, 0.015),
        (40, 0.015),
        (500, 0.015),
        (2000, 0.003),
    ], name='MIL-STD-810G Cat 4',
       description='Truck transportation'),
    
    # MIL-STD-810G Category 7 (Jet Aircraft)
    'MIL-STD-810G_CAT7': PSDProfile.from_breakpoints([
        (15, 0.015),
        (80, 0.015),
        (350, 0.015),
        (2000, 0.006),
    ], name='MIL-STD-810G Cat 7',
       description='Jet aircraft vibration'),
    
    # MIL-STD-810G Category 24 (Helicopter)
    'MIL-STD-810G_CAT24': PSDProfile.from_breakpoints([
        (5, 0.001),
        (20, 0.01),
        (80, 0.04),
        (350, 0.04),
        (2000, 0.007),
    ], name='MIL-STD-810G Cat 24',
       description='Helicopter vibration'),
    
    # NAVMAT P-9492
    'NAVMAT': PSDProfile.from_breakpoints([
        (20, 0.04),
        (80, 0.04),
        (350, 0.04),
        (2000, 0.007),
    ], name='NAVMAT P-9492',
       description='Navy shipboard equipment'),
    
    # ESA PSS-01-401 Qualification
    'ESA_QUAL': PSDProfile.from_breakpoints([
        (20, 0.01),
        (100, 0.08),
        (300, 0.08),
        (2000, 0.008),
    ], name='ESA PSS-01-401',
       description='ESA qualification level'),
    
    # ESA PSS-01-401 Acceptance
    'ESA_ACCEPT': PSDProfile.from_breakpoints([
        (20, 0.005),
        (100, 0.04),
        (300, 0.04),
        (2000, 0.004),
    ], name='ESA PSS-01-401 Accept',
       description='ESA acceptance level'),
    
    # SpaceX Falcon 9 (approximate public data)
    'FALCON9': PSDProfile.from_breakpoints([
        (20, 0.01),
        (50, 0.08),
        (800, 0.08),
        (2000, 0.02),
    ], name='Falcon 9 (approx)',
       description='SpaceX Falcon 9 approximate envelope'),
    
    # Ariane 5 (approximate)
    'ARIANE5': PSDProfile.from_breakpoints([
        (20, 0.015),
        (100, 0.09),
        (400, 0.09),
        (2000, 0.01),
    ], name='Ariane 5 (approx)',
       description='Ariane 5 approximate envelope'),
}


def get_psd_profile(name: str) -> PSDProfile:
    """
    Get a standard PSD profile by name.
    
    Args:
        name: Profile name (case-insensitive, underscores/hyphens flexible)
    
    Returns:
        PSDProfile instance
    
    Raises:
        ValueError: If profile not found
    
    Example:
        >>> psd = get_psd_profile('GEVS')
        >>> psd = get_psd_profile('mil-std-810g-cat7')
    """
    # Normalize name
    normalized = name.upper().replace('-', '_').replace(' ', '_')
    
    if normalized in STANDARD_PROFILES:
        return STANDARD_PROFILES[normalized]
    
    # Try partial match
    for key, profile in STANDARD_PROFILES.items():
        if normalized in key or key in normalized:
            return profile
    
    available = ', '.join(STANDARD_PROFILES.keys())
    raise ValueError(f"Unknown PSD profile: '{name}'. Available: {available}")


def list_available_profiles() -> List[str]:
    """Return list of available standard PSD profile names."""
    return list(STANDARD_PROFILES.keys())
```

### Phase 3: Atomizer Integration (Week 3)

**Goal**: Connect dynamic response processor to Atomizer's extractor and optimization systems.

#### Tasks

| Task | Description | Effort | Deliverable |
|------|-------------|--------|-------------|
| 3.1 | Create `extract_modal_database` | 3h | New extractor |
| 3.2 | Create `extract_random_response` | 3h | New extractor |
| 3.3 | Create `extract_dynamic_stress` | 3h | New extractor |
| 3.4 | Optimization template | 2h | `random_vibration_optimization.json` |
| 3.5 | Runner integration | 4h | Hook into `OptimizationRunner` |
| 3.6 | Dashboard widgets | 4h | Response PSD plot, Grms history |
| 3.7 | Result caching | 3h | Cache modal DB between similar designs |
| 3.8 | Documentation | 3h | User guide, API docs |
| 3.9 | End-to-end test | 4h | Full optimization run |

**Validation Checkpoint**:
- Complete optimization runs successfully
- Dashboard shows dynamic response metrics
- Results match manual SATK/Nastran verification

#### Code: Random Response Extractor

```python
# optimization_engine/extractors/extract_random_response.py

"""
Random Vibration Response Extractor for Atomizer Optimization
=============================================================

Extracts random vibration metrics (Grms, peak acceleration, peak stress)
from SOL 103 modal analysis results using analytical modal superposition.

This extractor enables dynamic optimization without running SOL 111.

Example:
    >>> result = extract_random_response(
    ...     op2_file='model.op2',
    ...     psd_profile='GEVS',
    ...     direction='z'
    ... )
    >>> print(f"Grms: {result['grms']:.2f} G")
    >>> print(f"Peak: {result['peak_acceleration']:.2f} G")
"""

from pathlib import Path
from typing import Dict, Any, Optional, Union, List
import logging

logger = logging.getLogger(__name__)


def extract_random_response(
    op2_file: Union[str, Path],
    psd_profile: str = 'GEVS',
    direction: str = 'z',
    damping: float = 0.02,
    node_id: Optional[int] = None,
    peak_sigma: float = 3.0,
    f06_file: Optional[Union[str, Path]] = None,
    n_modes: Optional[int] = None,
    frequency_resolution: int = 500,
) -> Dict[str, Any]:
    """
    Extract random vibration response for optimization objective/constraint.
    
    This is the main interface for dynamic optimization. It reads SOL 103
    results and computes random vibration response analytically.
    
    Args:
        op2_file: Path to OP2 file from SOL 103 analysis
        psd_profile: PSD specification - name ('GEVS', 'MIL-STD-810G_CAT7', etc.)
                     or path to CSV file
        direction: Excitation direction ('x', 'y', 'z')
        damping: Modal damping ratio (default 2%)
        node_id: Specific node for response (None = use effective mass)
        peak_sigma: Sigma level for peak (3.0 = 3-sigma)
        f06_file: Optional F06 for modal effective mass table
        n_modes: Number of modes to include (None = auto 95% mass)
        frequency_resolution: Points in frequency array
    
    Returns:
        Dict containing:
        - 'grms': RMS acceleration (G)
        - 'peak_acceleration': Peak acceleration (G)
        - 'rms_displacement': RMS displacement (mm)
        - 'peak_displacement': Peak displacement (mm)
        - 'dominant_mode': Mode number with highest contribution
        - 'dominant_frequency': Frequency of dominant mode (Hz)
        - 'n_modes_used': Number of modes in calculation
        - 'mass_fraction': Cumulative mass fraction of included modes
        - 'psd_profile': Name of PSD profile used
        - 'direction': Excitation direction
        - 'success': True if extraction succeeded
        - 'error': Error message if failed
    
    Example:
        # For optimization objective
        >>> result = extract_random_response('bracket.op2', psd_profile='GEVS')
        >>> objective = result['peak_acceleration']
        
        # For constraint checking
        >>> result = extract_random_response('bracket.op2', psd_profile='GEVS')
        >>> if result['peak_acceleration'] > 25.0:
        ...     print("Constraint violated!")
    """
    from optimization_engine.processors.dynamic_response import (
        ModalDatabase,
        RandomVibrationProcessor,
        get_psd_profile
    )
    
    op2_path = Path(op2_file)
    f06_path = Path(f06_file) if f06_file else None
    
    # Auto-detect F06 if not provided
    if f06_path is None:
        possible_f06 = op2_path.with_suffix('.f06')
        if possible_f06.exists():
            f06_path = possible_f06
            logger.info(f"Auto-detected F06: {f06_path}")
    
    try:
        # Build modal database
        logger.info(f"Extracting modal database from {op2_path}")
        modal_db = ModalDatabase.from_op2_f06(
            op2_file=op2_path,
            f06_file=f06_path,
            damping=damping,
            node_ids=[node_id] if node_id else None,
        )
        
        logger.info(f"Extracted {modal_db.n_modes} modes, "
                   f"freq range: {modal_db.frequency_range[0]:.1f}-"
                   f"{modal_db.frequency_range[1]:.1f} Hz")
        
        # Load PSD profile
        if Path(psd_profile).exists():
            from optimization_engine.processors.dynamic_response import PSDProfile
            psd = PSDProfile.from_csv(Path(psd_profile))
        else:
            psd = get_psd_profile(psd_profile)
        
        logger.info(f"Using PSD profile: {psd.name} ({psd.grms:.2f} Grms)")
        
        # Create processor
        processor = RandomVibrationProcessor(
            modal_db=modal_db,
            default_damping=damping
        )
        
        # Compute response
        result = processor.compute_response(
            psd_profile=psd,
            direction=direction,
            node_id=node_id,
            n_modes=n_modes,
            frequency_resolution=frequency_resolution,
            peak_sigma=peak_sigma,
            output_type='acceleration'
        )
        
        # Get mass fraction for included modes
        n_modes_used = n_modes or modal_db.modes_for_mass_fraction(0.95, direction)
        mass_fraction = modal_db.get_cumulative_mass_fraction(direction)[n_modes_used - 1]
        
        # Dominant mode info
        dominant_freq = modal_db.modes[result.dominant_mode - 1].frequency
        
        return {
            'grms': result.rms_acceleration,
            'peak_acceleration': result.peak_acceleration,
            'rms_displacement': result.rms_displacement,
            'peak_displacement': result.peak_displacement,
            'dominant_mode': result.dominant_mode,
            'dominant_frequency': dominant_freq,
            'n_modes_used': n_modes_used,
            'mass_fraction': mass_fraction,
            'modal_contributions': result.modal_contributions,
            'psd_profile': psd.name,
            'psd_grms': psd.grms,
            'direction': direction,
            'peak_sigma': peak_sigma,
            'success': True,
            'error': None,
        }
        
    except Exception as e:
        logger.exception(f"Random response extraction failed: {e}")
        return {
            'grms': None,
            'peak_acceleration': None,
            'rms_displacement': None,
            'peak_displacement': None,
            'dominant_mode': None,
            'dominant_frequency': None,
            'success': False,
            'error': str(e),
        }


def extract_random_stress(
    op2_file: Union[str, Path],
    psd_profile: str = 'GEVS',
    direction: str = 'z',
    damping: float = 0.02,
    peak_sigma: float = 3.0,
    element_ids: Optional[List[int]] = None,
    non_gaussian_correction: bool = True,
    f06_file: Optional[Union[str, Path]] = None,
) -> Dict[str, Any]:
    """
    Extract peak Von Mises stress from random vibration.
    
    Args:
        op2_file: Path to OP2 with modal stress results
        psd_profile: PSD specification
        direction: Excitation direction
        damping: Modal damping ratio
        peak_sigma: Sigma level
        element_ids: Specific elements (None = all)
        non_gaussian_correction: Apply VM stress distribution correction
        f06_file: Optional F06 for effective mass
    
    Returns:
        Dict with:
        - 'max_rms_von_mises': Maximum RMS VM stress (MPa)
        - 'max_peak_von_mises': Maximum peak VM stress (MPa)
        - 'critical_element': Element ID with max stress
        - 'margin_of_safety': (allowable - peak) / peak
        - 'n_elements': Number of elements analyzed
    """
    from optimization_engine.processors.dynamic_response import (
        ModalDatabase,
        RandomVibrationProcessor,
        get_psd_profile
    )
    
    op2_path = Path(op2_file)
    f06_path = Path(f06_file) if f06_file else None
    
    try:
        # Build modal database with stress shapes
        modal_db = ModalDatabase.from_op2_f06(
            op2_file=op2_path,
            f06_file=f06_path,
            damping=damping,
            element_ids=element_ids,  # Request stress shapes
        )
        
        # Load PSD
        psd = get_psd_profile(psd_profile) if not Path(psd_profile).exists() \
              else PSDProfile.from_csv(Path(psd_profile))
        
        # Process
        processor = RandomVibrationProcessor(modal_db, damping)
        result = processor.compute_stress_response(
            psd_profile=psd,
            direction=direction,
            element_ids=element_ids,
            peak_sigma=peak_sigma,
            non_gaussian_correction=non_gaussian_correction,
        )
        
        return {
            'max_rms_von_mises': result['max_rms_von_mises'],
            'max_peak_von_mises': result['max_peak_von_mises'],
            'critical_element': result['critical_element'],
            'n_elements': result['n_elements'],
            'success': True,
            'error': None,
        }
        
    except Exception as e:
        logger.exception(f"Stress extraction failed: {e}")
        return {
            'max_rms_von_mises': None,
            'max_peak_von_mises': None,
            'critical_element': None,
            'success': False,
            'error': str(e),
        }


# Convenience functions for optimization objectives

def get_grms(op2_file, psd='GEVS', direction='z', damping=0.02) -> float:
    """Quick Grms extraction for objective function."""
    result = extract_random_response(op2_file, psd, direction, damping)
    return result['grms'] if result['success'] else float('inf')


def get_peak_g(op2_file, psd='GEVS', direction='z', damping=0.02, sigma=3.0) -> float:
    """Quick peak acceleration extraction for constraint."""
    result = extract_random_response(op2_file, psd, direction, damping, peak_sigma=sigma)
    return result['peak_acceleration'] if result['success'] else float('inf')


def get_peak_stress(op2_file, psd='GEVS', direction='z', damping=0.02, sigma=3.0) -> float:
    """Quick peak stress extraction for constraint."""
    result = extract_random_stress(op2_file, psd, direction, damping, sigma)
    return result['max_peak_von_mises'] if result['success'] else float('inf')
```

### Phase 4: Sine Sweep Processor (Week 4)

**Goal**: Add harmonic/sine sweep response capability.

#### Tasks

| Task | Description | Effort | Deliverable |
|------|-------------|--------|-------------|
| 4.1 | Sine sweep transfer function | 3h | Magnitude/phase at frequencies |
| 4.2 | Resonance search | 2h | Find peak frequencies |
| 4.3 | Dwell analysis | 3h | Response at specific frequencies |
| 4.4 | Sweep rate effects | 2h | Transient buildup correction |
| 4.5 | Create `SineSweepProcessor` | 4h | `sine_sweep.py` |
| 4.6 | Create extractor | 2h | `extract_sine_response.py` |
| 4.7 | Optimization template | 2h | `sine_sweep_optimization.json` |
| 4.8 | Tests | 3h | Validate against analytical |

### Phase 5: Advanced Features (Future)

**Goal**: Extended capabilities for complete dynamic qualification.

#### Shock Response Spectrum (SRS)
- Compute SRS from transient pulse or base motion
- Pseudo-velocity and pseudo-acceleration
- Compare against equipment SRS limits

#### Multi-Axis Excitation
- SRSS (Square Root Sum of Squares) combination
- CQC (Complete Quadratic Combination) for correlated inputs
- Full 6-DOF base motion

#### Fatigue from Random Vibration
- Narrow-band fatigue using Miner's rule
- Dirlik method for wide-band random
- Cycle counting from PSD

#### Neural Surrogate for Modal Database
- Train GNN to predict (frequencies, mode shapes) from geometry
- Skip SOL 103 entirely for similar designs
- 1000x additional speedup potential

---

## 4. Module Specifications

### 4.1 ModalDatabase

```python
class ModalDatabase:
    """
    Central storage for all modal analysis data.
    
    Responsibilities:
    - Extract data from OP2/F06 files
    - Store mode shapes, frequencies, participation factors
    - Provide efficient access patterns for dynamic calculations
    - Support caching and serialization
    
    Thread Safety: Read-only after construction
    Memory: O(n_modes × n_nodes × 6) for full eigenvectors
    """
    
    # Class attributes
    SUPPORTED_ELEMENTS = ['CQUAD4', 'CTRIA3', 'CHEXA', 'CPENTA', 'CTETRA']
    
    # Required data
    model_name: str
    total_mass: float
    center_of_gravity: np.ndarray
    modes: List[ModeShape]
    
    # Optional data
    node_coordinates: Dict[int, np.ndarray]
    element_connectivity: Dict[int, List[int]]
    
    # Metadata
    source_op2: Path
    source_f06: Path
    extraction_timestamp: datetime
    nastran_version: str
    
    # Methods
    @classmethod
    def from_op2_f06(cls, op2_file, f06_file, **options) -> 'ModalDatabase'
    
    def get_mode(self, mode_number: int) -> ModeShape
    def get_frequencies(self) -> np.ndarray
    def get_participation_factors(self, direction: str) -> np.ndarray
    def get_cumulative_mass_fraction(self, direction: str) -> np.ndarray
    def modes_for_mass_fraction(self, fraction: float, direction: str) -> int
    
    def to_hdf5(self, filepath: Path)
    @classmethod
    def from_hdf5(cls, filepath: Path) -> 'ModalDatabase'
    
    def to_dict(self) -> Dict
    def summary(self) -> str
```

### 4.2 TransferFunctionEngine

```python
class TransferFunctionEngine:
    """
    Computes frequency response functions from modal data.
    
    Responsibilities:
    - Single mode FRF calculation
    - Multi-mode combination
    - Base excitation transmissibility
    - Point excitation mobility (future)
    
    Performance: Vectorized numpy operations
    Accuracy: Double precision complex arithmetic
    """
    
    def __init__(self, modal_db: ModalDatabase)
    
    # Core FRF methods
    def single_mode_frf(
        self,
        frequencies: np.ndarray,
        mode: ModeShape,
        output_type: str = 'displacement'
    ) -> np.ndarray  # Complex FRF
    
    def base_excitation_frf(
        self,
        frequencies: np.ndarray,
        direction: str,
        output_type: str = 'acceleration',
        node_id: int = None,
        n_modes: int = None
    ) -> np.ndarray  # Complex transmissibility
    
    def point_excitation_frf(
        self,
        frequencies: np.ndarray,
        input_node: int,
        input_direction: str,
        output_node: int,
        output_direction: str
    ) -> np.ndarray  # Complex FRF
    
    # Stress FRF
    def stress_frf(
        self,
        frequencies: np.ndarray,
        element_id: int,
        direction: str
    ) -> np.ndarray  # Complex stress transfer function
```

### 4.3 RandomVibrationProcessor

```python
class RandomVibrationProcessor:
    """
    Computes random vibration response using modal superposition.
    
    This is the SATK-equivalent processor.
    
    Capabilities:
    - Grms and peak response calculation
    - Response PSD computation
    - Modal contribution analysis
    - Stress recovery with non-Gaussian correction
    - Multiple response locations
    
    Performance Target: <100ms for typical aerospace model
    """
    
    def __init__(self, modal_db: ModalDatabase, default_damping: float = 0.02)
    
    # Main computation methods
    def compute_response(
        self,
        psd_profile: PSDProfile,
        direction: str = 'z',
        node_id: int = None,
        n_modes: int = None,
        frequency_resolution: int = 500,
        peak_sigma: float = 3.0,
        output_type: str = 'acceleration'
    ) -> RandomVibrationResult
    
    def compute_stress_response(
        self,
        psd_profile: PSDProfile,
        direction: str = 'z',
        element_ids: List[int] = None,
        peak_sigma: float = 3.0,
        non_gaussian_correction: bool = True
    ) -> Dict[str, Any]
    
    # Quick objective/constraint methods
    def compute_grms(self, psd_profile, direction, node_id=None) -> float
    def compute_peak_g(self, psd_profile, direction, sigma=3.0) -> float
    def compute_peak_stress(self, psd_profile, direction, sigma=3.0) -> float
    
    # Analysis methods
    def get_modal_contributions(self, psd_profile, direction) -> Dict[int, float]
    def get_response_psd(self, psd_profile, direction) -> Tuple[np.ndarray, np.ndarray]
    def identify_critical_modes(self, psd_profile, direction, threshold=0.1) -> List[int]
```

---

## 5. Integration Points

### 5.1 Extractor Registry

Add to `optimization_engine/extractors/__init__.py`:

```python
# Dynamic response extractors
from .extract_modal_database import extract_modal_database
from .extract_random_response import (
    extract_random_response,
    extract_random_stress,
    get_grms,
    get_peak_g,
    get_peak_stress
)
from .extract_sine_response import extract_sine_response

__all__ = [
    # ... existing extractors ...
    
    # Dynamic response
    'extract_modal_database',
    'extract_random_response',
    'extract_random_stress',
    'get_grms',
    'get_peak_g', 
    'get_peak_stress',
    'extract_sine_response',
]
```

### 5.2 Feature Registry

Add to `optimization_engine/feature_registry.json`:

```json
{
  "dynamic_response": {
    "random_vibration_processor": {
      "feature_id": "random_vibration_processor",
      "name": "Random Vibration Processor",
      "description": "Modal superposition-based random vibration response calculation",
      "category": "dynamics",
      "implementation": {
        "file_path": "optimization_engine/processors/dynamic_response/random_vibration.py",
        "class_name": "RandomVibrationProcessor"
      },
      "interface": {
        "inputs": [
          {"name": "modal_database", "type": "ModalDatabase"},
          {"name": "psd_profile", "type": "PSDProfile"},
          {"name": "direction", "type": "str"}
        ],
        "outputs": [
          {"name": "grms", "type": "float"},
          {"name": "peak_acceleration", "type": "float"},
          {"name": "response_psd", "type": "np.ndarray"}
        ]
      }
    }
  }
}
```

### 5.3 Dashboard Integration

New dashboard components for dynamic response:

```typescript
// atomizer-dashboard/src/components/DynamicResponse/
├── ResponsePSDPlot.tsx      // Log-log PSD plot with input/output
├── TransmissibilityPlot.tsx // FRF magnitude vs frequency
├── ModalContributionBar.tsx // Bar chart of mode contributions
├── GrmsHistoryChart.tsx     // Grms vs trial number
├── CampbellDiagram.tsx      // For rotating machinery (future)
└── DynamicResponseCard.tsx  // Summary card for dashboard
```

### 5.4 CLI Integration

```bash
# New CLI commands
atomizer dynamic extract model.op2 --psd GEVS --direction z
atomizer dynamic psd-list
atomizer dynamic psd-plot GEVS --output psd.png
atomizer optimize --template random_vibration_optimization.json
```

---

## 6. Validation Strategy

### 6.1 Unit Tests

```python
# tests/test_dynamic_response/

test_modal_database.py
├── test_mode_shape_creation
├── test_op2_extraction
├── test_f06_parsing
├── test_mass_fraction_calculation
└── test_hdf5_caching

test_transfer_functions.py
├── test_sdof_frf_analytical
├── test_resonance_amplification
├── test_phase_at_resonance
├── test_multi_mode_combination
└── test_base_excitation_transmissibility

test_random_vibration.py
├── test_miles_equation_sdof
├── test_grms_calculation
├── test_peak_factor
├── test_non_gaussian_correction
├── test_modal_contributions
└── test_stress_recovery

test_psd_profiles.py
├── test_interpolation
├── test_grms_integration
├── test_standard_profiles
└── test_csv_loading
```

### 6.2 Integration Tests

```python
# tests/integration/

test_dynamic_optimization.py
├── test_full_random_vib_optimization
├── test_multi_objective_dynamic
├── test_constraint_handling
└── test_dashboard_updates

test_nastran_comparison.py
├── test_vs_sol111_response
├── test_vs_sol112_random
└── test_stress_recovery_accuracy
```

### 6.3 Validation Cases

| Case | Model | Validation Against | Acceptance |
|------|-------|-------------------|------------|
| SDOF | Single mass-spring | Miles' equation | <1% error |
| Cantilever beam | Simple beam | SOL 111 FRF | <5% error |
| Plate | Rectangular plate | SOL 112 random | <5% error |
| Bracket | Real component | SATK results | <10% error |

### 6.4 Benchmark Against SATK

If possible, run same model through both Atomizer and SATK:

```
Model: CubeSat bracket
PSD: GEVS
Direction: Z-axis
Damping: 2%

Compare:
- Grms
- Peak acceleration
- Dominant mode
- Response PSD shape
- Peak stress location
```

---

## 7. Performance Targets

### 7.1 Timing Budgets

| Operation | Target | Notes |
|-----------|--------|-------|
| OP2 read (50 modes) | <2s | pyNastran |
| F06 parse (MEFFMASS) | <0.5s | Regex parsing |
| Modal DB construction | <1s | Data structuring |
| FRF calculation (1000 freq) | <20ms | Vectorized |
| Response PSD | <20ms | Element-wise |
| Spectral integration | <10ms | Trapezoidal |
| Stress recovery (1000 elements) | <200ms | Parallel possible |
| **Total per trial** | **<3s** | Excluding SOL 103 |

### 7.2 Memory Budgets

| Data | Size Estimate | Notes |
|------|---------------|-------|
| Modal DB (100 modes, 10k nodes) | ~50 MB | Full eigenvectors |
| Modal DB (100 modes, 100 nodes) | ~5 MB | Sparse storage |
| Response PSD (1000 freq) | <100 KB | Just arrays |
| Stress shapes (100 modes, 1k elements) | ~50 MB | If needed |

### 7.3 Scalability

Tested configurations:

| Model Size | Modes | Nodes | Elements | Expected Time |
|------------|-------|-------|----------|---------------|
| Small | 50 | 5,000 | 10,000 | <2s |
| Medium | 100 | 50,000 | 100,000 | <5s |
| Large | 200 | 200,000 | 500,000 | <15s |
| Very Large | 500 | 1,000,000 | 2,000,000 | <60s |

---

## 8. Risk Mitigation

### 8.1 Technical Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| pyNastran can't read NX OP2 | Low | High | Test early, have F06 fallback |
| Modal stress not in OP2 | Medium | Medium | Request STRESS(MODAL) output |
| Poor accuracy vs SOL 111 | Low | High | Validate thoroughly, document assumptions |
| Memory issues large models | Medium | Medium | Lazy loading, sparse storage |
| Complex modes (damped) | Low | Low | Detect and warn, use magnitude |

### 8.2 Mitigation Actions

1. **Early OP2 Testing**: Test pyNastran with NX 2412 OP2 files in week 1
2. **F06 Fallback**: Implement F06 parsing for critical data
3. **Validation Suite**: Build comprehensive test cases against SOL 111
4. **Memory Profiling**: Profile with large models, implement streaming if needed
5. **Documentation**: Clear documentation of assumptions and limitations

---

## 9. Future Extensions

### 9.1 Short-Term (3-6 months)

- Sine sweep processor
- Shock response spectrum
- Multi-axis excitation
- Response at multiple nodes
- Automatic mode selection

### 9.2 Medium-Term (6-12 months)

- Fatigue from random vibration
- Acoustic loading (diffuse field)
- Coupled structural-acoustic
- Time-domain transient from PSD
- Design sensitivity (∂response/∂parameter)

### 9.3 Long-Term (12+ months)

- Neural surrogate for modal database
- Real-time response visualization
- Uncertainty quantification
- Multi-physics coupling (thermal + dynamic)
- Foundation model for structural dynamics

---

## 10. Success Metrics

### 10.1 Technical Metrics

- [ ] Grms accuracy <5% vs SOL 111
- [ ] Peak stress accuracy <10% vs SOL 112
- [ ] Processing time <5s per trial
- [ ] Memory <500MB for large models
- [ ] 100% test coverage for core modules

### 10.2 User Metrics

- [ ] Complete optimization in <1 hour (100 trials)
- [ ] Dashboard shows real-time dynamic metrics
- [ ] Documentation enables self-service usage
- [ ] Template covers 80% of use cases

### 10.3 Business Metrics

- [ ] Feature demo ready for marketing video
- [ ] 3+ real-world optimization case studies
- [ ] Positive feedback from beta users
- [ ] Competitive differentiation documented

---

## Appendix A: Reference Equations

### A.1 Modal Superposition

```
Physical response:
u(x,t) = Σᵢ φᵢ(x) · qᵢ(t)

Modal equation of motion:
q̈ᵢ + 2ζᵢωᵢq̇ᵢ + ωᵢ²qᵢ = Lᵢ·a_base(t) / mᵢ

Frequency response function:
Hᵢ(ω) = 1 / (ωᵢ² - ω² + j·2ζᵢωᵢω)
```

### A.2 Random Vibration

```
Response PSD:
Sᵧ(f) = |H(f)|² · Sₓ(f)

Mean square:
σ² = ∫₀^∞ Sᵧ(f) df

Peak (Gaussian):
peak = k · σ,  k ≈ 3 for 3-sigma
```

### A.3 Miles' Equation

```
For SDOF with white noise input:
Grms = √(π/2 · fₙ · Q · PSD)

Where:
- fₙ = natural frequency (Hz)
- Q = 1/(2ζ) = quality factor
- PSD = input PSD level (G²/Hz)
```

---

## Appendix B: File Formats

### B.1 OP2 Data Blocks

| Table | Contains | Used For |
|-------|----------|----------|
| OUGV1 | Eigenvectors | Mode shapes |
| LAMA | Eigenvalues | Frequencies |
| OES1 | Element stress | Modal stress shapes |
| OGPFB1 | Grid point forces | Participation |
| OQMG1 | Modal participation | Effective mass |

### B.2 F06 Tables

| Section | Contains | Parsing Pattern |
|---------|----------|-----------------|
| REAL EIGENVALUES | Frequencies, gen mass | Regex table |
| MODAL EFFECTIVE MASS | Participation | Regex table |
| MODAL PARTICIPATION FACTORS | L factors | Regex table |

### B.3 PSD CSV Format

```csv
frequency_hz,psd_g2_hz
20,0.026
50,0.16
800,0.16
2000,0.026
```

---

*Document End - Dynamic Response Processor Master Plan*
