File size: 5,491 Bytes
52a2a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""
Random vibration test profile engine.

Computes a real, defensible random vibration PSD (the GEVS generalized component
workmanship spectrum), applies:
  - level selection (qualification / acceptance / protoflight) per GEVS,
  - mass attenuation for components heavier than 22.7 kg,
  - optional notch suggestions at supplied resonant frequencies,
and computes overall Grms analytically from the breakpoint table.

This is deterministic math grounded in NASA-STD-7001 / GEVS, NOT an LLM guess.
The LLM's job is to interpret inputs and explain; the numbers come from here.
All output still requires test engineer review.
"""

from dataclasses import dataclass
from typing import List, Tuple, Optional
import math


# GEVS generalized component minimum workmanship QUALIFICATION breakpoints.
# (freq Hz, ASD g^2/Hz). Flat 0.16 from 50-800, +/-3 dB/oct ramps, 14.1 Grms.
_GEVS_QUAL_BREAKPOINTS: List[Tuple[float, float]] = [
    (20.0, 0.026),
    (50.0, 0.160),
    (800.0, 0.160),
    (2000.0, 0.026),
]

# Level adjustments relative to qualification, in dB.
_LEVEL_DB = {
    "qualification": 0.0,
    "protoflight": 0.0,    # same level as qual, shorter duration
    "acceptance": -6.0,    # 6 dB below qual
}

_LEVEL_DURATION_S = {
    "qualification": 120,
    "protoflight": 60,
    "acceptance": 60,
}

_REFERENCE_MASS_KG = 22.7  # 50 lb GEVS reference


@dataclass
class ProfilePoint:
    freq_hz: float
    asd_g2_hz: float


@dataclass
class VibrationProfile:
    level: str
    duration_s_per_axis: int
    points: List[ProfilePoint]
    overall_grms: float
    mass_attenuation_db: float
    notches: List[dict]
    notes: List[str]


def _db_scale(asd: float, db: float) -> float:
    """Scale an ASD value by a dB amount (10*log10 power ratio)."""
    return asd * (10.0 ** (db / 10.0))


def _grms_from_breakpoints(points: List[Tuple[float, float]]) -> float:
    """
    Analytic Grms = sqrt(area under ASD curve), summing per-segment areas.
    Flat segment area = ASD * df. Sloped segment (linear on log-log) handled
    with the standard constant-dB/oct integration formula.
    """
    area = 0.0
    for (f1, a1), (f2, a2) in zip(points, points[1:]):
        if f2 <= f1:
            continue
        if abs(a2 - a1) < 1e-12:
            area += a1 * (f2 - f1)  # flat
        else:
            # slope in dB/oct on log-log axes
            m = math.log10(a2 / a1) / math.log10(f2 / f1)
            if abs(m + 1.0) < 1e-9:
                # special case: integral of A*f^-1 = A*ln(f2/f1)
                area += a1 * f1 * math.log(f2 / f1)
            else:
                area += (a2 * f2 - a1 * f1) / (m + 1.0)
    return math.sqrt(area)


def compute_profile(level: str = "qualification",
                    mass_kg: Optional[float] = None,
                    resonances_hz: Optional[List[float]] = None,
                    notch_depth_db: float = 6.0) -> VibrationProfile:
    """Build a random vibration profile from inputs."""
    level = level.lower().strip()
    if level not in _LEVEL_DB:
        level = "qualification"

    notes: List[str] = []

    # 1) Level adjustment
    level_db = _LEVEL_DB[level]

    # 2) Mass attenuation (only for heavier-than-reference components)
    mass_att_db = 0.0
    if mass_kg and mass_kg > _REFERENCE_MASS_KG:
        mass_att_db = 10.0 * math.log10(_REFERENCE_MASS_KG / mass_kg)
        notes.append(
            f"Mass attenuation applied: {mass_att_db:.1f} dB for "
            f"{mass_kg:.1f} kg (> {_REFERENCE_MASS_KG} kg reference)."
        )
    elif mass_kg:
        notes.append(
            f"No mass attenuation: {mass_kg:.1f} kg is at or below the "
            f"{_REFERENCE_MASS_KG} kg reference; workmanship minimum applies."
        )

    total_db = level_db + mass_att_db

    # 3) Build adjusted breakpoints
    adj = [(f, _db_scale(a, total_db)) for (f, a) in _GEVS_QUAL_BREAKPOINTS]
    points = [ProfilePoint(f, round(a, 5)) for (f, a) in adj]

    # 4) Grms
    grms = _grms_from_breakpoints([(p.freq_hz, p.asd_g2_hz) for p in points])

    # 5) Notch suggestions at resonances
    notches = []
    if resonances_hz:
        for rf in resonances_hz:
            if 20.0 <= rf <= 2000.0:
                # interpolate baseline ASD at rf for context
                notches.append({
                    "freq_hz": rf,
                    "suggested_notch_db": notch_depth_db,
                    "basis": "response/force-limited; verify with CLA or measured data",
                })
        if notches:
            notes.append(
                f"{len(notches)} notch(es) suggested at supplied resonances "
                f"(-{notch_depth_db:.0f} dB). MUST be justified per NASA-HDBK-7004 "
                f"and approved by a test engineer; over-notching causes undertesting."
            )

    notes.append(
        "Spectrum is the GEVS generalized component workmanship envelope. If a "
        "launch vehicle PSD is provided, the final spec must envelope both and "
        "re-derive Grms."
    )

    return VibrationProfile(
        level=level,
        duration_s_per_axis=_LEVEL_DURATION_S[level],
        points=points,
        overall_grms=round(grms, 2),
        mass_attenuation_db=round(mass_att_db, 2),
        notches=notches,
        notes=notes,
    )


def profile_as_table(profile: VibrationProfile) -> List[dict]:
    """Breakpoint table for display/plot."""
    return [{"Frequency (Hz)": p.freq_hz, "ASD (g^2/Hz)": p.asd_g2_hz}
            for p in profile.points]