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