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>
1650 lines
54 KiB
Markdown
1650 lines
54 KiB
Markdown
# 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*
|