Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| class ProfilePoint: | |
| freq_hz: float | |
| asd_g2_hz: float | |
| 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] | |