Files
Atomizer/.claude/skills/modules/DYNAMIC_RESPONSE_MASTER_PLAN.md
Anto01 f13563d7ab feat: Major update - Physics docs, Zernike OPD, insights, NX journals, tools
Documentation:
- Add docs/06_PHYSICS/ with Zernike fundamentals and OPD method docs
- Add docs/guides/CMA-ES_EXPLAINED.md optimization guide
- Update CLAUDE.md and ATOMIZER_CONTEXT.md with current architecture
- Update OP_01_CREATE_STUDY protocol

Planning:
- Add DYNAMIC_RESPONSE plans for random vibration/PSD support
- Add OPTIMIZATION_ENGINE_MIGRATION_PLAN for code reorganization

Insights System:
- Update design_space, modal_analysis, stress_field, thermal_field insights
- Improve error handling and data validation

NX Journals:
- Add analyze_wfe_zernike.py for Zernike WFE analysis
- Add capture_study_images.py for automated screenshots
- Add extract_expressions.py and introspect_part.py utilities
- Add user_generated_journals/journal_top_view_image_taking.py

Tests & Tools:
- Add comprehensive Zernike OPD test suite
- Add audit_v10 tests for WFE validation
- Add tools for Pareto graphs and mirror data extraction
- Add migrate_studies_to_topics.py utility

Knowledge Base:
- Initialize LAC (Learning Atomizer Core) with failure/success patterns

Dashboard:
- Update Setup.tsx and launch_dashboard.py
- Add restart-dev.bat helper script

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 19:47:37 -05:00

1650 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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*