MVP / profile_engine.py
cigawa's picture
Upload 11 files
52a2a94 verified
Raw
History Blame Contribute Delete
5.49 kB
"""
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]