aero-agent-roles 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +18 -0
- package/README.md +49 -0
- package/bin/aero-agent-roles.js +127 -0
- package/lib/catalog.js +97 -0
- package/lib/install.js +48 -0
- package/lib/mcp.js +199 -0
- package/manifest.json +516 -0
- package/package.json +44 -0
- package/roles/aerodynamics-engineer/ROLE.md +153 -0
- package/roles/aerodynamics-engineer/SOURCES.md +12 -0
- package/roles/aerodynamics-engineer/cli.py +143 -0
- package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
- package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
- package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
- package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
- package/roles/aircraft-design-engineer/ROLE.md +138 -0
- package/roles/aircraft-design-engineer/SOURCES.md +12 -0
- package/roles/aircraft-design-engineer/cli.py +152 -0
- package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
- package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
- package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
- package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
- package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
- package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
- package/roles/airworthiness-compliance-engineer/cli.py +146 -0
- package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
- package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
- package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
- package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
- package/roles/as9100-quality-auditor/ROLE.md +128 -0
- package/roles/as9100-quality-auditor/SOURCES.md +16 -0
- package/roles/as9100-quality-auditor/cli.py +136 -0
- package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
- package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
- package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
- package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
- package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
- package/roles/data-bus-avionics-engineer/cli.py +311 -0
- package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
- package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
- package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
- package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
- package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
- package/roles/do160-environmental-engineer/ROLE.md +136 -0
- package/roles/do160-environmental-engineer/SOURCES.md +30 -0
- package/roles/do160-environmental-engineer/cli.py +147 -0
- package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
- package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
- package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
- package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
- package/roles/do178c-cert-engineer/ROLE.md +120 -0
- package/roles/do178c-cert-engineer/SOURCES.md +16 -0
- package/roles/do178c-cert-engineer/cli.py +129 -0
- package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
- package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
- package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
- package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
- package/roles/do254-hardware-engineer/ROLE.md +127 -0
- package/roles/do254-hardware-engineer/SOURCES.md +19 -0
- package/roles/do254-hardware-engineer/cli.py +308 -0
- package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
- package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
- package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
- package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
- package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
- package/roles/engineering-analysis-engineer/ROLE.md +154 -0
- package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
- package/roles/engineering-analysis-engineer/cli.py +151 -0
- package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
- package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
- package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
- package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
- package/roles/flight-management-engineer/ROLE.md +160 -0
- package/roles/flight-management-engineer/SOURCES.md +30 -0
- package/roles/flight-management-engineer/cli.py +258 -0
- package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
- package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
- package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
- package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
- package/roles/flight-mechanics-engineer/ROLE.md +162 -0
- package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
- package/roles/flight-mechanics-engineer/cli.py +149 -0
- package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
- package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
- package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
- package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
- package/roles/flight-test-engineer/ROLE.md +137 -0
- package/roles/flight-test-engineer/SOURCES.md +24 -0
- package/roles/flight-test-engineer/cli.py +135 -0
- package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
- package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
- package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
- package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
- package/roles/flight-test-performance-engineer/ROLE.md +165 -0
- package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
- package/roles/flight-test-performance-engineer/cli.py +319 -0
- package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
- package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
- package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
- package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
- package/roles/gnc-engineer/ROLE.md +141 -0
- package/roles/gnc-engineer/SOURCES.md +35 -0
- package/roles/gnc-engineer/cli.py +141 -0
- package/roles/gnc-engineer/core/gnc_core.py +1074 -0
- package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
- package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
- package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
- package/roles/guidance-engineer/ROLE.md +156 -0
- package/roles/guidance-engineer/SOURCES.md +43 -0
- package/roles/guidance-engineer/cli.py +280 -0
- package/roles/guidance-engineer/core/guidance_core.py +820 -0
- package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
- package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
- package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
- package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
- package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
- package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
- package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
- package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
- package/roles/ndt-engineer/ROLE.md +157 -0
- package/roles/ndt-engineer/SOURCES.md +18 -0
- package/roles/ndt-engineer/cli.py +316 -0
- package/roles/ndt-engineer/core/ndt_core.py +1590 -0
- package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
- package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
- package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
- package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
- package/roles/numerical-analysis-engineer/ROLE.md +158 -0
- package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
- package/roles/numerical-analysis-engineer/cli.py +414 -0
- package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
- package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
- package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
- package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
- package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
- package/roles/propulsion-engineer/ROLE.md +163 -0
- package/roles/propulsion-engineer/SOURCES.md +15 -0
- package/roles/propulsion-engineer/cli.py +168 -0
- package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
- package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
- package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
- package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
- package/roles/space-systems-engineer/ROLE.md +161 -0
- package/roles/space-systems-engineer/SOURCES.md +49 -0
- package/roles/space-systems-engineer/cli.py +173 -0
- package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
- package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
- package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
- package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
- package/roles/state-estimation-engineer/ROLE.md +154 -0
- package/roles/state-estimation-engineer/SOURCES.md +27 -0
- package/roles/state-estimation-engineer/cli.py +326 -0
- package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
- package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
- package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
- package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
- package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
- package/roles/structures-loads-engineer/ROLE.md +171 -0
- package/roles/structures-loads-engineer/SOURCES.md +44 -0
- package/roles/structures-loads-engineer/cli.py +219 -0
- package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
- package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
- package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
- package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
- package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""engineering_analysis_engineer_core.py - Engineering Analysis and Data
|
|
3
|
+
Engineer executable core.
|
|
4
|
+
|
|
5
|
+
This is the role's ENGINE: given a measurement/calculation chain's facts
|
|
6
|
+
it computes the ISA atmosphere reference values, propagates measurement
|
|
7
|
+
uncertainty (GUM first-order law), builds the t-distribution confidence
|
|
8
|
+
interval for repeated-measurement bias, runs the tolerance stack-up
|
|
9
|
+
(worst case and RSS), verifies numerical convergence (Richardson
|
|
10
|
+
extrapolation + grid convergence index), states margins per the
|
|
11
|
+
engineering-margins convention, and BUILDS the Analysis Verification
|
|
12
|
+
Memo. It also gate-checks deliverables. Standalone: stdlib only, no
|
|
13
|
+
external repo needed.
|
|
14
|
+
|
|
15
|
+
Every formula mirrors the REAL logic in the bound AeroSkills leaves
|
|
16
|
+
(paraphrased common engineering methodology):
|
|
17
|
+
- isa-atmosphere: ICAO/ISO 2533 standard atmosphere model
|
|
18
|
+
- uncertainty-propagation: GUM (JCGM 100) first order law
|
|
19
|
+
- confidence-interval-estimation: Student t interval
|
|
20
|
+
- tolerance-stackup: worst case + RSS dimension chain
|
|
21
|
+
- convergence-verification: Richardson extrapolation + GCI (Fs=1.25)
|
|
22
|
+
- engineering-margins: MS = allowable/applied - 1
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Domain tables (public standards / common engineering methodology)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
# ISA standard atmosphere constants (ISO 2533 / ICAO Doc 7488; also the
|
|
35
|
+
# values encoded in the bound isa-atmosphere leaf).
|
|
36
|
+
ISA_G = 9.80665 # m/s2 gravitational acceleration
|
|
37
|
+
ISA_R = 287.05 # J/(kg K) specific gas constant of air
|
|
38
|
+
ISA_LAPSE = 0.0065 # K/m tropospheric lapse rate
|
|
39
|
+
ISA_T0 = 288.15 # K sea-level temperature
|
|
40
|
+
ISA_P0 = 101325.0 # Pa sea-level pressure
|
|
41
|
+
ISA_TROPOPAUSE = 11000.0 # m
|
|
42
|
+
ISA_TOP = 20000.0 # m model top
|
|
43
|
+
ISA_T_TROP = ISA_T0 - ISA_LAPSE * ISA_TROPOPAUSE # 216.65 K
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# ISA standard atmosphere (0-20 km)
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def isa_temperature_k(h: float) -> float:
|
|
51
|
+
"""ISA temperature (K) at altitude h (m), 0-20 km."""
|
|
52
|
+
if h < 0 or h > ISA_TOP:
|
|
53
|
+
raise ValueError(f"altitude outside model 0-20 km: {h!r}")
|
|
54
|
+
return ISA_T0 - ISA_LAPSE * h if h <= ISA_TROPOPAUSE else ISA_T_TROP
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def isa_pressure_pa(h: float) -> float:
|
|
58
|
+
"""ISA pressure (Pa) at altitude h (m), 0-20 km."""
|
|
59
|
+
if h < 0 or h > ISA_TOP:
|
|
60
|
+
raise ValueError(f"altitude outside model 0-20 km: {h!r}")
|
|
61
|
+
if h <= ISA_TROPOPAUSE:
|
|
62
|
+
t = ISA_T0 - ISA_LAPSE * h
|
|
63
|
+
return ISA_P0 * (t / ISA_T0) ** (ISA_G / (ISA_R * ISA_LAPSE))
|
|
64
|
+
p_trop = isa_pressure_pa(ISA_TROPOPAUSE)
|
|
65
|
+
return p_trop * math.exp(-ISA_G * (h - ISA_TROPOPAUSE)
|
|
66
|
+
/ (ISA_R * ISA_T_TROP))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def isa_density_kgm3(h: float) -> float:
|
|
70
|
+
"""ISA density (kg/m3) at altitude h (m), 0-20 km."""
|
|
71
|
+
return isa_pressure_pa(h) / (ISA_R * isa_temperature_k(h))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def isa_reference_values(h: float) -> dict:
|
|
75
|
+
"""ISA reference (temperature K, pressure Pa, density kg/m3) at h."""
|
|
76
|
+
t = isa_temperature_k(h)
|
|
77
|
+
p = isa_pressure_pa(h)
|
|
78
|
+
return {"altitude_m": h, "temperature_k": t,
|
|
79
|
+
"pressure_pa": p, "density_kgm3": p / (ISA_R * t)}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def density_from_measured(p_pa: float, t_k: float) -> float:
|
|
83
|
+
"""Air density from measured pressure and temperature (ideal gas EOS)."""
|
|
84
|
+
if p_pa <= 0 or t_k <= 0:
|
|
85
|
+
raise ValueError("pressure and temperature must be positive")
|
|
86
|
+
return p_pa / (ISA_R * t_k)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Uncertainty propagation (GUM / JCGM 100 first-order law)
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def combined_standard_uncertainty(sensitivities, uncertainties):
|
|
94
|
+
"""u_c = sqrt(sum((s_i * u_i)^2)), GUM first-order law.
|
|
95
|
+
|
|
96
|
+
sensitivities[i] = df/dx_i at the operating point, uncertainties[i]
|
|
97
|
+
= standard uncertainty (1 sigma) of input i; independent inputs.
|
|
98
|
+
"""
|
|
99
|
+
if not sensitivities or not uncertainties:
|
|
100
|
+
raise ValueError("sensitivities and uncertainties must be non-empty")
|
|
101
|
+
if len(sensitivities) != len(uncertainties):
|
|
102
|
+
raise ValueError("sensitivities and uncertainties length mismatch")
|
|
103
|
+
for u in uncertainties:
|
|
104
|
+
if u < 0.0:
|
|
105
|
+
raise ValueError(f"uncertainty must be non-negative: {u!r}")
|
|
106
|
+
return math.sqrt(sum((s * u) ** 2
|
|
107
|
+
for s, u in zip(sensitivities, uncertainties)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def expanded_uncertainty(combined, k=2.0):
|
|
111
|
+
"""U = k * u_c; coverage factor k = 2.0 (~95%) by convention."""
|
|
112
|
+
if k <= 0.0:
|
|
113
|
+
raise ValueError(f"coverage factor k must be > 0: {k!r}")
|
|
114
|
+
return k * combined
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def uncertainty_contributions(sensitivities, uncertainties):
|
|
118
|
+
"""Per-input variance contributions sorted descending.
|
|
119
|
+
|
|
120
|
+
Each dict: {index, name, sensitivity, uncertainty, contribution,
|
|
121
|
+
percent}. Percent shares sum to ~100.
|
|
122
|
+
"""
|
|
123
|
+
combined = combined_standard_uncertainty(sensitivities, uncertainties)
|
|
124
|
+
total = combined ** 2
|
|
125
|
+
contribs = []
|
|
126
|
+
for i, (s, u) in enumerate(zip(sensitivities, uncertainties)):
|
|
127
|
+
c = (s * u) ** 2
|
|
128
|
+
contribs.append({"index": i, "sensitivity": s, "uncertainty": u,
|
|
129
|
+
"contribution": c,
|
|
130
|
+
"percent": 0.0 if total == 0.0 else 100.0 * c / total})
|
|
131
|
+
contribs.sort(key=lambda e: e["contribution"], reverse=True)
|
|
132
|
+
return contribs
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Confidence interval (Student t distribution, small-sample)
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
# Regularized incomplete beta machinery for the t quantile (pure math,
|
|
140
|
+
# same method as the bound confidence-interval-estimation leaf).
|
|
141
|
+
_CF_MAX_ITER = 200
|
|
142
|
+
_CF_EPS = 3e-12
|
|
143
|
+
_FPMIN = 1e-300
|
|
144
|
+
_BIS_TOL = 1e-9
|
|
145
|
+
_BIS_MAX = 300
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _betacf(a, b, x):
|
|
149
|
+
qab, qap, qam = a + b, a + 1.0, a - 1.0
|
|
150
|
+
c = 1.0
|
|
151
|
+
d = 1.0 - qab * x / qap
|
|
152
|
+
if abs(d) < _FPMIN:
|
|
153
|
+
d = _FPMIN
|
|
154
|
+
d = 1.0 / d
|
|
155
|
+
h = d
|
|
156
|
+
for m in range(1, _CF_MAX_ITER + 1):
|
|
157
|
+
m2 = 2 * m
|
|
158
|
+
aa = m * (b - m) * x / ((qam + m2) * (a + m2))
|
|
159
|
+
d = 1.0 + aa * d
|
|
160
|
+
if abs(d) < _FPMIN:
|
|
161
|
+
d = _FPMIN
|
|
162
|
+
c = 1.0 + aa / c
|
|
163
|
+
if abs(c) < _FPMIN:
|
|
164
|
+
c = _FPMIN
|
|
165
|
+
d = 1.0 / d
|
|
166
|
+
h *= d * c
|
|
167
|
+
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
|
168
|
+
d = 1.0 + aa * d
|
|
169
|
+
if abs(d) < _FPMIN:
|
|
170
|
+
d = _FPMIN
|
|
171
|
+
c = 1.0 + aa / c
|
|
172
|
+
if abs(c) < _FPMIN:
|
|
173
|
+
c = _FPMIN
|
|
174
|
+
d = 1.0 / d
|
|
175
|
+
delta = d * c
|
|
176
|
+
h *= delta
|
|
177
|
+
if abs(delta - 1.0) < _CF_EPS:
|
|
178
|
+
break
|
|
179
|
+
return h
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _betai(a, b, x):
|
|
183
|
+
if x <= 0.0:
|
|
184
|
+
return 0.0
|
|
185
|
+
if x >= 1.0:
|
|
186
|
+
return 1.0
|
|
187
|
+
bt = math.exp(math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b)
|
|
188
|
+
+ a * math.log(x) + b * math.log1p(-x))
|
|
189
|
+
if x < (a + 1.0) / (a + b + 2.0):
|
|
190
|
+
return bt * _betacf(a, b, x) / a
|
|
191
|
+
return 1.0 - bt * _betacf(b, a, 1.0 - x) / b
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _t_cdf(t, df):
|
|
195
|
+
x = df / (df + t * t)
|
|
196
|
+
return 1.0 - 0.5 * _betai(df / 2.0, 0.5, x)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def t_ppf_two_sided(level: float, df: float) -> float:
|
|
200
|
+
"""Two-sided t quantile t_{1-alpha/2, df} for alpha = 1 - level."""
|
|
201
|
+
if not 0.0 < level < 1.0:
|
|
202
|
+
raise ValueError(f"level must be in (0, 1): {level!r}")
|
|
203
|
+
if df < 1.0:
|
|
204
|
+
raise ValueError(f"df must be >= 1: {df!r}")
|
|
205
|
+
target = 0.5 * (1.0 + float(level))
|
|
206
|
+
hi = 1.0
|
|
207
|
+
while _t_cdf(hi, df) < target and hi < 1e12:
|
|
208
|
+
hi *= 2.0
|
|
209
|
+
lo, flo = 0.0, _t_cdf(0.0, df)
|
|
210
|
+
for _ in range(_BIS_MAX):
|
|
211
|
+
mid = 0.5 * (lo + hi)
|
|
212
|
+
if hi - lo <= _BIS_TOL:
|
|
213
|
+
return mid
|
|
214
|
+
fmid = _t_cdf(mid, df)
|
|
215
|
+
if (fmid - target) * (flo - target) <= 0.0:
|
|
216
|
+
hi = mid
|
|
217
|
+
else:
|
|
218
|
+
lo, flo = mid, fmid
|
|
219
|
+
return 0.5 * (lo + hi)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def confidence_interval_mean(x, level: float = 0.95) -> dict:
|
|
223
|
+
"""t interval for the mean: xbar +/- t_{1-a/2, n-1} * s / sqrt(n)."""
|
|
224
|
+
if not isinstance(x, (list, tuple)) or len(x) < 2:
|
|
225
|
+
raise ValueError("sample must have at least 2 observations")
|
|
226
|
+
n = len(x)
|
|
227
|
+
mean = sum(float(v) for v in x) / n
|
|
228
|
+
var = sum((float(v) - mean) ** 2 for v in x) / (n - 1)
|
|
229
|
+
s = math.sqrt(var)
|
|
230
|
+
df = n - 1
|
|
231
|
+
t_q = t_ppf_two_sided(level, df)
|
|
232
|
+
se = s / math.sqrt(n)
|
|
233
|
+
half = t_q * se
|
|
234
|
+
return {"mean": mean, "sample_std": s, "se": se, "n": n, "df": float(df),
|
|
235
|
+
"level": float(level), "t_quantile": t_q,
|
|
236
|
+
"lower": mean - half, "upper": mean + half}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Tolerance stack-up (worst case + RSS dimension chain)
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class StackMember:
|
|
245
|
+
"""One dimension in the tolerance chain (signed direction)."""
|
|
246
|
+
name: str
|
|
247
|
+
nominal_mm: float
|
|
248
|
+
tolerance_mm: float # symmetric bilateral half-width
|
|
249
|
+
direction: int = 1 # +1 adds to chain, -1 subtracts
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def nominal_total(members) -> float:
|
|
253
|
+
if not members:
|
|
254
|
+
raise ValueError("stack must be non-empty")
|
|
255
|
+
return sum(m.direction * m.nominal_mm for m in members)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def worst_case_total(members) -> float:
|
|
259
|
+
if not members:
|
|
260
|
+
raise ValueError("stack must be non-empty")
|
|
261
|
+
for m in members:
|
|
262
|
+
if m.tolerance_mm < 0:
|
|
263
|
+
raise ValueError(f"tolerance must be >= 0: {m.tolerance_mm!r}")
|
|
264
|
+
return sum(abs(m.tolerance_mm) for m in members)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def rss_total(members) -> float:
|
|
268
|
+
if not members:
|
|
269
|
+
raise ValueError("stack must be non-empty")
|
|
270
|
+
for m in members:
|
|
271
|
+
if m.tolerance_mm < 0:
|
|
272
|
+
raise ValueError(f"tolerance must be >= 0: {m.tolerance_mm!r}")
|
|
273
|
+
return math.sqrt(sum(m.tolerance_mm ** 2 for m in members))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def rss_shares(members) -> list:
|
|
277
|
+
"""Percent RSS variance share per member (100 * t_i^2 / sum t_j^2)."""
|
|
278
|
+
denom = sum(m.tolerance_mm ** 2 for m in members)
|
|
279
|
+
if denom == 0:
|
|
280
|
+
raise ValueError("all tolerances zero: no stack spread")
|
|
281
|
+
return [100.0 * m.tolerance_mm ** 2 / denom for m in members]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Convergence verification (Richardson extrapolation + GCI)
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def observed_order(f1, f2, f3, r: float) -> float:
|
|
289
|
+
"""p = ln((f3-f2)/(f2-f1)) / ln(r); r > 1, monotone sequence."""
|
|
290
|
+
if r <= 1.0:
|
|
291
|
+
raise ValueError(f"refinement ratio must be > 1: {r!r}")
|
|
292
|
+
if f2 == f1:
|
|
293
|
+
raise ValueError("degenerate: f2 == f1")
|
|
294
|
+
ratio = (f3 - f2) / (f2 - f1)
|
|
295
|
+
if ratio <= 0.0:
|
|
296
|
+
raise ValueError("non-monotone sequence for observed order")
|
|
297
|
+
return math.log(ratio) / math.log(r)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def richardson_extrapolation(f1, f2, r: float, p: float) -> float:
|
|
301
|
+
return f1 + (f1 - f2) / (r ** p - 1.0)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def grid_convergence_index(f1, f2, r: float, p: float, fs: float = 1.25) -> float:
|
|
305
|
+
"""GCI = Fs * |(f1-f2)/f1| / (r^p - 1); fraction not percent."""
|
|
306
|
+
return fs * abs((f1 - f2) / f1) / (r ** p - 1.0)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def convergence_verdict(f1, f2, f3, r: float) -> dict:
|
|
310
|
+
"""Classify a three-solution grid study (f1 finest ... f3 coarsest)."""
|
|
311
|
+
if r <= 1.0:
|
|
312
|
+
raise ValueError(f"refinement ratio must be > 1: {r!r}")
|
|
313
|
+
if f2 == f1:
|
|
314
|
+
raise ValueError("degenerate: f2 == f1")
|
|
315
|
+
ratio = (f3 - f2) / (f2 - f1)
|
|
316
|
+
if ratio > 0.0:
|
|
317
|
+
p = math.log(ratio) / math.log(r)
|
|
318
|
+
denom = r ** p - 1.0
|
|
319
|
+
if abs(denom) < 1e-12:
|
|
320
|
+
return {"order": p, "extrapolated": None, "gci": None,
|
|
321
|
+
"verdict": "monotone converged"}
|
|
322
|
+
f_exact = f1 + (f1 - f2) / denom
|
|
323
|
+
gci = 1.25 * abs((f1 - f2) / f1) / denom
|
|
324
|
+
return {"order": p, "extrapolated": f_exact, "gci": gci,
|
|
325
|
+
"verdict": "monotone converged"}
|
|
326
|
+
if ratio < 0.0:
|
|
327
|
+
verdict = "diverging" if abs(ratio) > 1.0 else "oscillatory"
|
|
328
|
+
else:
|
|
329
|
+
verdict = "oscillatory"
|
|
330
|
+
return {"order": None, "extrapolated": None, "gci": None,
|
|
331
|
+
"verdict": verdict}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# Numerical integration of the hydrostatic balance (verification study)
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def trapezoid_hydrostatic_pressure(h_top: float, n_steps: int) -> float:
|
|
339
|
+
"""Integrate dp/dz = -rho(z)*g from 0 to h_top by the trapezoid rule.
|
|
340
|
+
|
|
341
|
+
The integrand is evaluated from the closed-form ISA model; the study
|
|
342
|
+
verifies that the integrator used for the atmosphere model reproduces
|
|
343
|
+
the analytic ISA pressure at the top altitude.
|
|
344
|
+
"""
|
|
345
|
+
if h_top <= 0 or n_steps < 1:
|
|
346
|
+
raise ValueError("h_top > 0 and n_steps >= 1 required")
|
|
347
|
+
dz = h_top / n_steps
|
|
348
|
+
total = 0.0
|
|
349
|
+
for i in range(n_steps + 1):
|
|
350
|
+
z = i * dz
|
|
351
|
+
rho = isa_density_kgm3(z)
|
|
352
|
+
w = 0.5 if i in (0, n_steps) else 1.0
|
|
353
|
+
total += w * (-rho * ISA_G)
|
|
354
|
+
return ISA_P0 + total * dz
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Root finding: density-altitude inversion (Newton iteration)
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def density_altitude_m(rho_target: float, h0: float = 3000.0,
|
|
362
|
+
tol: float = 1e-9, max_iter: int = 50) -> dict:
|
|
363
|
+
"""Invert rho_ISA(h) = rho_target by Newton iteration (troposphere).
|
|
364
|
+
|
|
365
|
+
rho(h) = rho0 * (T/T0)^(g/(R L) - 1); analytic derivative
|
|
366
|
+
d rho/dh = -rho * (L/T) * (g/(R L) - 1).
|
|
367
|
+
"""
|
|
368
|
+
if rho_target <= 0:
|
|
369
|
+
raise ValueError(f"target density must be positive: {rho_target!r}")
|
|
370
|
+
rho0 = ISA_P0 / (ISA_R * ISA_T0)
|
|
371
|
+
expo = ISA_G / (ISA_R * ISA_LAPSE) - 1.0
|
|
372
|
+
h = h0
|
|
373
|
+
for _ in range(max_iter):
|
|
374
|
+
t = isa_temperature_k(h)
|
|
375
|
+
rho = rho0 * (t / ISA_T0) ** expo
|
|
376
|
+
resid = rho - rho_target
|
|
377
|
+
drho_dh = -rho * (ISA_LAPSE / t) * expo
|
|
378
|
+
if abs(resid) < tol:
|
|
379
|
+
return {"altitude_m": h, "residual": abs(resid),
|
|
380
|
+
"iterations": _ + 1, "converged": True}
|
|
381
|
+
h -= resid / drho_dh
|
|
382
|
+
raise ValueError("Newton iteration did not converge")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# Margins (engineering-margins convention: MS = allowable/applied - 1)
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def margin_of_safety(allowable: float, applied: float) -> float:
|
|
390
|
+
"""MS = allowable/applied - 1 (unitless; same units for both inputs)."""
|
|
391
|
+
if allowable < 0 or applied <= 0:
|
|
392
|
+
raise ValueError("allowable >= 0 and applied > 0 required")
|
|
393
|
+
return allowable / applied - 1.0
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def margin_verdict(ms: float) -> str:
|
|
397
|
+
return "PASS" if ms >= 0.0 else "FAIL"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# Example measurement chain (the reference item for the memo)
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
@dataclass
|
|
405
|
+
class MeasurementChain:
|
|
406
|
+
"""Project facts the role needs to build the analysis memo."""
|
|
407
|
+
chain_name: str
|
|
408
|
+
description: str = ""
|
|
409
|
+
altitude_m: float = 3000.0 # test point geometric altitude
|
|
410
|
+
pressure_pa: float = 70050.0 # measured static pressure
|
|
411
|
+
pressure_unc_pa: float = 35.0 # u_p, 1 sigma (calibrated sensor)
|
|
412
|
+
temperature_k: float = 271.0 # measured temperature
|
|
413
|
+
temperature_unc_k: float = 1.2 # u_T, 1 sigma
|
|
414
|
+
coverage_factor: float = 2.0 # k ~ 95%
|
|
415
|
+
confidence_level: float = 0.95
|
|
416
|
+
bias_samples_pa: list = field(default_factory=list) # repeat readings
|
|
417
|
+
bias_reference_note: str = ""
|
|
418
|
+
stack_members: list = field(default_factory=list) # list[StackMember]
|
|
419
|
+
stack_allowable_mm: float = 0.8 # spec limit on probe position
|
|
420
|
+
density_allowable_pct: float = 1.5 # spec limit on density accuracy
|
|
421
|
+
export_classification: str = "uncontrolled technical data (example)"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def example_chain() -> MeasurementChain:
|
|
425
|
+
"""Flight-test air-data chain used for the worked example memo."""
|
|
426
|
+
return MeasurementChain(
|
|
427
|
+
chain_name="Flight-test air-data chain at 3000 m",
|
|
428
|
+
description="Static pressure and temperature are measured at the "
|
|
429
|
+
"flight-test point and reduced to air density and "
|
|
430
|
+
"density altitude; repeated static-source readings are "
|
|
431
|
+
"checked for bias; the probe mounting stack and the "
|
|
432
|
+
"atmosphere-model integrator are verified.",
|
|
433
|
+
altitude_m=3000.0,
|
|
434
|
+
pressure_pa=70050.0,
|
|
435
|
+
pressure_unc_pa=35.0,
|
|
436
|
+
temperature_k=271.0,
|
|
437
|
+
temperature_unc_k=1.2,
|
|
438
|
+
coverage_factor=2.0,
|
|
439
|
+
confidence_level=0.95,
|
|
440
|
+
bias_samples_pa=[1.8, -2.2, 3.1, -0.7, 2.4, -1.5,
|
|
441
|
+
0.9, 2.8, -1.1, 0.4],
|
|
442
|
+
bias_reference_note="n=10 repeat readings of the static source "
|
|
443
|
+
"error vs the reference pressure standard",
|
|
444
|
+
stack_members=[
|
|
445
|
+
StackMember("mounting bracket", 120.0, 0.50, +1),
|
|
446
|
+
StackMember("adapter", 45.0, 0.25, +1),
|
|
447
|
+
StackMember("probe inset", 60.0, 0.40, -1),
|
|
448
|
+
],
|
|
449
|
+
stack_allowable_mm=0.8,
|
|
450
|
+
density_allowable_pct=1.5,
|
|
451
|
+
export_classification="uncontrolled technical data (example)",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Memo builder: produces the deliverable content model (REAL numbers)
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def _fmt(v, digits: int = 5) -> str:
|
|
460
|
+
return f"{v:.{digits}g}"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def build_memo(chain: MeasurementChain) -> dict:
|
|
464
|
+
"""Compute every result in the chain and assemble the memo model."""
|
|
465
|
+
alt = chain.altitude_m
|
|
466
|
+
if not (0 < alt <= ISA_TROPOPAUSE):
|
|
467
|
+
raise ValueError("example chain altitude must be tropospheric (0-11 km)")
|
|
468
|
+
|
|
469
|
+
# --- Stage 2: ISA reference values at the test point ----------------
|
|
470
|
+
isa = isa_reference_values(alt)
|
|
471
|
+
|
|
472
|
+
# --- Stage 4: derived density + uncertainty propagation --------------
|
|
473
|
+
rho_m = density_from_measured(chain.pressure_pa, chain.temperature_k)
|
|
474
|
+
t = chain.temperature_k
|
|
475
|
+
sens_p = 1.0 / (ISA_R * t) # d rho / d p
|
|
476
|
+
sens_t = -chain.pressure_pa / (ISA_R * t * t) # d rho / d T
|
|
477
|
+
u_c = combined_standard_uncertainty([sens_p, sens_t],
|
|
478
|
+
[chain.pressure_unc_pa,
|
|
479
|
+
chain.temperature_unc_k])
|
|
480
|
+
U = expanded_uncertainty(u_c, chain.coverage_factor)
|
|
481
|
+
contribs = uncertainty_contributions([sens_p, sens_t],
|
|
482
|
+
[chain.pressure_unc_pa,
|
|
483
|
+
chain.temperature_unc_k])
|
|
484
|
+
for c in contribs:
|
|
485
|
+
c["name"] = "static pressure" if c["index"] == 0 else "temperature"
|
|
486
|
+
rel_unc_pct = 100.0 * u_c / rho_m
|
|
487
|
+
delta_isa_pct = 100.0 * (rho_m - isa["density_kgm3"]) / isa["density_kgm3"]
|
|
488
|
+
|
|
489
|
+
# --- Density-altitude inversion (Newton, convergence checked) --------
|
|
490
|
+
h_rho = density_altitude_m(rho_m, h0=alt)
|
|
491
|
+
alt_delta_m = h_rho["altitude_m"] - alt
|
|
492
|
+
|
|
493
|
+
# --- Stage 7: confidence interval on repeated-measurement bias -------
|
|
494
|
+
ci = confidence_interval_mean(chain.bias_samples_pa,
|
|
495
|
+
chain.confidence_level)
|
|
496
|
+
|
|
497
|
+
# --- Stage 9: tolerance stack-up --------------------------------------
|
|
498
|
+
members = chain.stack_members
|
|
499
|
+
nom = nominal_total(members)
|
|
500
|
+
wc = worst_case_total(members)
|
|
501
|
+
rss = rss_total(members)
|
|
502
|
+
shares = rss_shares(members)
|
|
503
|
+
wc_limits = (nom - wc, nom + wc)
|
|
504
|
+
rss_limits = (nom - rss, nom + rss)
|
|
505
|
+
dominant_i = max(range(len(shares)), key=lambda i: shares[i])
|
|
506
|
+
# Margins against the position spec (engineering-margins convention).
|
|
507
|
+
ms_rss = margin_of_safety(chain.stack_allowable_mm, rss)
|
|
508
|
+
ms_wc = margin_of_safety(chain.stack_allowable_mm, wc)
|
|
509
|
+
|
|
510
|
+
# --- Stage 8: grid-convergence verification of the integrator --------
|
|
511
|
+
# Three-grid study of trapezoid hydrostatic integration 0..altitude,
|
|
512
|
+
# coarse N/4, medium N/2, fine N steps (refinement ratio r = 2).
|
|
513
|
+
n_fine = 24
|
|
514
|
+
f3 = trapezoid_hydrostatic_pressure(alt, n_fine // 4)
|
|
515
|
+
f2 = trapezoid_hydrostatic_pressure(alt, n_fine // 2)
|
|
516
|
+
f1 = trapezoid_hydrostatic_pressure(alt, n_fine)
|
|
517
|
+
r = 2.0
|
|
518
|
+
conv = convergence_verdict(f1, f2, f3, r)
|
|
519
|
+
closed_p = isa["pressure_pa"]
|
|
520
|
+
conv["closed_form_pa"] = closed_p
|
|
521
|
+
conv["fine_steps"] = n_fine
|
|
522
|
+
conv["refinement_ratio"] = r
|
|
523
|
+
conv["f1_pa"], conv["f2_pa"], conv["f3_pa"] = f1, f2, f3
|
|
524
|
+
conv["extrap_error_pct"] = (None if conv["extrapolated"] is None else
|
|
525
|
+
100.0 * abs(conv["extrapolated"] - closed_p)
|
|
526
|
+
/ closed_p)
|
|
527
|
+
|
|
528
|
+
# --- Margins (Stage 12) ----------------------------------------------
|
|
529
|
+
ms_density = margin_of_safety(chain.density_allowable_pct,
|
|
530
|
+
100.0 * U / rho_m)
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
"document_type": "Analysis Verification Memo",
|
|
534
|
+
"status": "draft-for-review",
|
|
535
|
+
"chain": chain.chain_name,
|
|
536
|
+
"chain_description": chain.description,
|
|
537
|
+
"altitude_m": alt,
|
|
538
|
+
"isa": isa,
|
|
539
|
+
"pressure_pa": chain.pressure_pa,
|
|
540
|
+
"pressure_unc_pa": chain.pressure_unc_pa,
|
|
541
|
+
"temperature_k": chain.temperature_k,
|
|
542
|
+
"temperature_unc_k": chain.temperature_unc_k,
|
|
543
|
+
"coverage_factor": chain.coverage_factor,
|
|
544
|
+
"density_kgm3": rho_m,
|
|
545
|
+
"sens_p": sens_p,
|
|
546
|
+
"sens_t": sens_t,
|
|
547
|
+
"u_c": u_c,
|
|
548
|
+
"expanded_U": U,
|
|
549
|
+
"contributions": contribs,
|
|
550
|
+
"rel_unc_pct": rel_unc_pct,
|
|
551
|
+
"delta_isa_pct": delta_isa_pct,
|
|
552
|
+
"density_altitude_m": h_rho["altitude_m"],
|
|
553
|
+
"density_altitude_iterations": h_rho["iterations"],
|
|
554
|
+
"density_altitude_residual": h_rho["residual"],
|
|
555
|
+
"alt_delta_m": alt_delta_m,
|
|
556
|
+
"bias_ci": ci,
|
|
557
|
+
"bias_reference_note": chain.bias_reference_note,
|
|
558
|
+
"stack": {"members": [{"name": m.name, "nominal_mm": m.nominal_mm,
|
|
559
|
+
"tolerance_mm": m.tolerance_mm,
|
|
560
|
+
"direction": m.direction,
|
|
561
|
+
"share_pct": s}
|
|
562
|
+
for m, s in zip(members, shares)],
|
|
563
|
+
"nominal_mm": nom, "worst_case_mm": wc, "rss_mm": rss,
|
|
564
|
+
"wc_limits_mm": wc_limits, "rss_limits_mm": rss_limits,
|
|
565
|
+
"dominant": members[dominant_i].name,
|
|
566
|
+
"dominant_share_pct": shares[dominant_i],
|
|
567
|
+
"allowable_mm": chain.stack_allowable_mm,
|
|
568
|
+
"ms_rss": ms_rss, "ms_wc": ms_wc,
|
|
569
|
+
"verdict_rss": margin_verdict(ms_rss),
|
|
570
|
+
"verdict_wc": margin_verdict(ms_wc)},
|
|
571
|
+
"convergence": conv,
|
|
572
|
+
"density_allowable_pct": chain.density_allowable_pct,
|
|
573
|
+
"ms_density": ms_density,
|
|
574
|
+
"density_verdict": margin_verdict(ms_density),
|
|
575
|
+
"export_classification": chain.export_classification,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
# Memo renderer: markdown deliverable (DRAFT, human review, no approval)
|
|
581
|
+
# ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
def _fmt_contrib(contribs) -> str:
|
|
584
|
+
return "; ".join(
|
|
585
|
+
f"{c['percent']:.1f}% from {c['name']} "
|
|
586
|
+
f"(s = {c['sensitivity']:.3e}, u = {c['uncertainty']:.3g})"
|
|
587
|
+
for c in contribs)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def render_memo_markdown(model: dict) -> str:
|
|
591
|
+
isa = model["isa"]
|
|
592
|
+
ci = model["bias_ci"]
|
|
593
|
+
st = model["stack"]
|
|
594
|
+
cv = model["convergence"]
|
|
595
|
+
lines = [
|
|
596
|
+
"# Analysis Verification Memo",
|
|
597
|
+
"",
|
|
598
|
+
f"**Chain:** {model['chain']}",
|
|
599
|
+
f"**Status:** {model['status']}",
|
|
600
|
+
"",
|
|
601
|
+
"## 1. Scope and inputs",
|
|
602
|
+
"",
|
|
603
|
+
"Analysis question: verify the measurement/calculation chain "
|
|
604
|
+
f"{model['chain']} - reduce the measured static pressure and "
|
|
605
|
+
"temperature to air density and density altitude, check the "
|
|
606
|
+
"repeated-measurement bias, verify the probe mounting tolerance "
|
|
607
|
+
"stack and the atmosphere-model integrator, and state margins.",
|
|
608
|
+
f"- Analysis item: {model['chain']} at geometric altitude "
|
|
609
|
+
f"{_fmt(model['altitude_m'])} m.",
|
|
610
|
+
f"- Inputs (measured, with 1-sigma standard uncertainties): static "
|
|
611
|
+
f"pressure p = {_fmt(model['pressure_pa'])} Pa +/- "
|
|
612
|
+
f"{_fmt(model['pressure_unc_pa'])} Pa; temperature T = "
|
|
613
|
+
f"{_fmt(model['temperature_k'])} K +/- {_fmt(model['temperature_unc_k'])} K.",
|
|
614
|
+
f"- Inputs (geometry): {len(st['members'])}-member probe mounting "
|
|
615
|
+
f"stack (see Section 5); position spec +/- {_fmt(st['allowable_mm'])} mm.",
|
|
616
|
+
f"- Inputs (acceptance): density accuracy spec +/- "
|
|
617
|
+
f"{_fmt(model['density_allowable_pct'])} %.",
|
|
618
|
+
f"- {model['bias_reference_note']}.",
|
|
619
|
+
"",
|
|
620
|
+
"## 2. Unit/atmosphere basis",
|
|
621
|
+
"",
|
|
622
|
+
"- Unit system: SI (Pa, K, kg/m3, m, mm). No unit conversions "
|
|
623
|
+
"required in the chain; conversions applied for reporting: "
|
|
624
|
+
f"altitude {_fmt(model['altitude_m'])} m = {_fmt(model['altitude_m'] * 3.28084)} ft; "
|
|
625
|
+
f"T {_fmt(isa['temperature_k'])} K = {_fmt(isa['temperature_k'] - 273.15)} degC.",
|
|
626
|
+
"- Atmosphere basis: ISA (ISO 2533 / ICAO Doc 7488) reference "
|
|
627
|
+
f"model, sea level {ISA_T0} K / {ISA_P0} Pa / "
|
|
628
|
+
f"{_fmt(ISA_P0 / (ISA_R * ISA_T0))} kg/m3, lapse rate "
|
|
629
|
+
f"{ISA_LAPSE * 1000} K/km to 11 km.",
|
|
630
|
+
f"- ISA reference values at {_fmt(model['altitude_m'])} m: "
|
|
631
|
+
f"T_ISA = {_fmt(isa['temperature_k'])} K, "
|
|
632
|
+
f"p_ISA = {_fmt(isa['pressure_pa'])} Pa, "
|
|
633
|
+
f"rho_ISA = {_fmt(isa['density_kgm3'])} kg/m3.",
|
|
634
|
+
"",
|
|
635
|
+
"## 3. Method and verification",
|
|
636
|
+
"",
|
|
637
|
+
"- Density from measured state: ideal gas EOS "
|
|
638
|
+
f"rho = p / (R T), R = {ISA_R} J/(kg K) (bound leaf: isa-atmosphere).",
|
|
639
|
+
"- Uncertainty: GUM (JCGM 100) first-order law with analytic "
|
|
640
|
+
"sensitivities, combined standard uncertainty "
|
|
641
|
+
"u_c = sqrt(sum((df/dx_i * u_i)^2)) and expanded U = k * u_c with "
|
|
642
|
+
f"k = {_fmt(model['coverage_factor'])} (~95 %) "
|
|
643
|
+
"(bound leaf: uncertainty-propagation).",
|
|
644
|
+
"- Bias statistics: Student t interval for the mean of the "
|
|
645
|
+
"repeat readings (bound leaf: confidence-interval-estimation).",
|
|
646
|
+
"- Density altitude: Newton inversion of rho_ISA(h) = rho_measured, "
|
|
647
|
+
f"converged in {model['density_altitude_iterations']} iterations to "
|
|
648
|
+
f"residual {model['density_altitude_residual']:.2e} "
|
|
649
|
+
"(bound leaf: root-finding; convergence evidence per leaf "
|
|
650
|
+
"convergence-verification).",
|
|
651
|
+
"- Numerical verification: three-grid trapezoid integration of the "
|
|
652
|
+
"hydrostatic balance dp/dz = -rho g over 0.."
|
|
653
|
+
f"{_fmt(model['altitude_m'])} m at {cv['fine_steps'] // 4}, "
|
|
654
|
+
f"{cv['fine_steps'] // 2} and {cv['fine_steps']} steps "
|
|
655
|
+
f"(refinement ratio r = {_fmt(cv['refinement_ratio'])}); Richardson "
|
|
656
|
+
"extrapolation and grid convergence index (GCI) versus the "
|
|
657
|
+
"closed-form ISA pressure.",
|
|
658
|
+
"- Numerical accuracy statement: see grid study below; the "
|
|
659
|
+
"integrator is grid-converged and agrees with the closed-form ISA "
|
|
660
|
+
"pressure at the test point.",
|
|
661
|
+
"",
|
|
662
|
+
"## 4. Results",
|
|
663
|
+
"",
|
|
664
|
+
f"- Air density (measured chain): rho = {_fmt(model['density_kgm3'])} kg/m3.",
|
|
665
|
+
f"- Sensitivities: d rho/dp = {_fmt(model['sens_p'])} (kg/m3)/Pa, "
|
|
666
|
+
f"d rho/dT = {_fmt(model['sens_t'])} (kg/m3)/K.",
|
|
667
|
+
f"- Combined standard uncertainty: u_c = {_fmt(model['u_c'])} kg/m3 "
|
|
668
|
+
f"({_fmt(model['rel_unc_pct'])} % of rho).",
|
|
669
|
+
f"- Expanded uncertainty: U = {_fmt(model['expanded_U'])} kg/m3 "
|
|
670
|
+
f"(k = {_fmt(model['coverage_factor'])}). Result: rho = "
|
|
671
|
+
f"{_fmt(model['density_kgm3'])} +/- {_fmt(model['expanded_U'])} kg/m3.",
|
|
672
|
+
f"- Uncertainty budget: {_fmt_contrib(model['contributions'])}.",
|
|
673
|
+
f"- Versus ISA at {_fmt(model['altitude_m'])} m: delta = "
|
|
674
|
+
f"{_fmt(model['delta_isa_pct'])} % (non-standard day).",
|
|
675
|
+
f"- Density altitude: h_rho = {_fmt(model['density_altitude_m'])} m "
|
|
676
|
+
f"(+{_fmt(model['alt_delta_m'])} m above the geometric test point).",
|
|
677
|
+
f"- Bias statistics ({model['bias_reference_note']}): mean bias "
|
|
678
|
+
f"{_fmt(ci['mean'])} Pa, sample s = {_fmt(ci['sample_std'])} Pa, "
|
|
679
|
+
f"95 % t-interval [{_fmt(ci['lower'])}, {_fmt(ci['upper'])}] Pa "
|
|
680
|
+
f"(t = {_fmt(ci['t_quantile'])}, df = {_fmt(ci['df'])}). Zero lies "
|
|
681
|
+
"inside the interval: no statistically significant static-source "
|
|
682
|
+
"bias at the 95 % level.",
|
|
683
|
+
"",
|
|
684
|
+
"## 5. Tolerances",
|
|
685
|
+
"",
|
|
686
|
+
"- GD&T basis: ASME Y14.5 (TIER-2 reference-only). The stack is a "
|
|
687
|
+
"1-D linear dimension chain with equal bilateral tolerances and "
|
|
688
|
+
"independent, centered parts (bound leaf: tolerance-stackup).",
|
|
689
|
+
"- Chain members (signed nominal direction, tolerance, RSS share):",
|
|
690
|
+
*[f" - {m['name']}: {m['nominal_mm']:g} mm "
|
|
691
|
+
f"(dir {'+' if m['direction'] > 0 else '-'}), +/- {m['tolerance_mm']:g} mm, "
|
|
692
|
+
f"{m['share_pct']:.1f} % share" for m in st["members"]],
|
|
693
|
+
f"- Nominal total: {_fmt(st['nominal_mm'])} mm.",
|
|
694
|
+
f"- Worst case: +/- {_fmt(st['worst_case_mm'])} mm -> limits "
|
|
695
|
+
f"[{_fmt(st['wc_limits_mm'][0])}, {_fmt(st['wc_limits_mm'][1])}] mm.",
|
|
696
|
+
f"- RSS: +/- {_fmt(st['rss_mm'])} mm -> limits "
|
|
697
|
+
f"[{_fmt(st['rss_limits_mm'][0])}, {_fmt(st['rss_limits_mm'][1])}] mm.",
|
|
698
|
+
f"- Dominant contributor: {st['dominant']} "
|
|
699
|
+
f"({st['dominant_share_pct']:.1f} % of RSS variance); tightening it "
|
|
700
|
+
"reduces the stack most.",
|
|
701
|
+
"",
|
|
702
|
+
"## 6. Data sources",
|
|
703
|
+
"",
|
|
704
|
+
"- ISA model and constants: ISO 2533 / ICAO Doc 7488 standard "
|
|
705
|
+
"atmosphere (public standard; summary-only use).",
|
|
706
|
+
"- Uncertainty method: GUM, JCGM 100:2008 (public guide; "
|
|
707
|
+
"summary-only use).",
|
|
708
|
+
"- GD&T conventions: ASME Y14.5 (TIER-2 reference-only; "
|
|
709
|
+
"summary-not-copy per SOURCES.md).",
|
|
710
|
+
f"- Export-control classification: {model['export_classification']}. "
|
|
711
|
+
"No controlled data is included in this memo.",
|
|
712
|
+
"",
|
|
713
|
+
"## 7. Margins and conclusions",
|
|
714
|
+
"",
|
|
715
|
+
"- Margins per the engineering-margins convention "
|
|
716
|
+
"MS = allowable / applied - 1 (same units both sides).",
|
|
717
|
+
f"- Density accuracy: applied (expanded) = "
|
|
718
|
+
f"{_fmt(100.0 * model['expanded_U'] / model['density_kgm3'])} %, "
|
|
719
|
+
f"allowable +/- {_fmt(model['density_allowable_pct'])} % -> "
|
|
720
|
+
f"MS = {_fmt(model['ms_density'])} ({model['density_verdict']}).",
|
|
721
|
+
f"- Probe position stack, RSS basis: applied +/- {_fmt(st['rss_mm'])} mm, "
|
|
722
|
+
f"allowable +/- {_fmt(st['allowable_mm'])} mm -> "
|
|
723
|
+
f"MS = {_fmt(st['ms_rss'])} ({st['verdict_rss']}).",
|
|
724
|
+
f"- Probe position stack, worst-case basis: applied +/- {_fmt(st['worst_case_mm'])} mm, "
|
|
725
|
+
f"allowable +/- {_fmt(st['allowable_mm'])} mm -> "
|
|
726
|
+
f"MS = {_fmt(st['ms_wc'])} ({st['verdict_wc']}).",
|
|
727
|
+
"- Conclusions: the chain is unit-consistent with the stated ISA "
|
|
728
|
+
"basis; the expanded density uncertainty is driven by the "
|
|
729
|
+
f"temperature input ({model['contributions'][0]['percent']:.0f} % of "
|
|
730
|
+
"variance); repeated-measurement bias is not significant; the "
|
|
731
|
+
f"atmosphere integrator is monotone-converged (order "
|
|
732
|
+
f"{_fmt(cv['order'])}), GCI {_fmt(cv['gci'] * 100)} % at the test "
|
|
733
|
+
"point, and agrees with the closed-form ISA pressure. The stack "
|
|
734
|
+
"meets the position spec on the RSS basis but not on the "
|
|
735
|
+
"worst-case basis: the RSS basis is valid only while the parts "
|
|
736
|
+
"remain independent and centered (process-capability evidence "
|
|
737
|
+
"required).",
|
|
738
|
+
"",
|
|
739
|
+
"## 8. Open items",
|
|
740
|
+
"",
|
|
741
|
+
"- For the design owner / human approver: confirm the density "
|
|
742
|
+
"accuracy spec and the position spec limits used as allowables; "
|
|
743
|
+
"decide worst-case vs RSS basis for the probe stack (tighten the "
|
|
744
|
+
f"dominant contributor {st['dominant']} if worst case must be met); "
|
|
745
|
+
"confirm export-control classification before external release.",
|
|
746
|
+
"",
|
|
747
|
+
"---",
|
|
748
|
+
"*Generated by Aero Agent Roles engineering-analysis-engineer core. "
|
|
749
|
+
"DRAFT for human engineering review. Not an approval document.*",
|
|
750
|
+
]
|
|
751
|
+
return "\n".join(lines)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# Evidence gate checkers
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
GATE_CHECKS = {
|
|
759
|
+
"title_present": "memo carries the Analysis Verification Memo title",
|
|
760
|
+
"isa_values_real": "ISA temperature/pressure/density match the model",
|
|
761
|
+
"uncertainty_valid": "u_c, U and budget contributions are real and consistent",
|
|
762
|
+
"stack_valid": "worst case >= RSS >= 0 and limits ordered",
|
|
763
|
+
"ci_valid": "t-interval is ordered and contains the mean",
|
|
764
|
+
"convergence_verified": "grid study is monotone-converged with GCI",
|
|
765
|
+
"margins_stated": "margins use MS = allowable/applied - 1 per basis",
|
|
766
|
+
"sign_off_honest": "document is draft-for-review, not approval",
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def check_memo(model: dict) -> dict:
|
|
771
|
+
"""Run the evidence gates against the memo content model."""
|
|
772
|
+
isa = model["isa"]
|
|
773
|
+
t_exp = isa_temperature_k(model["altitude_m"])
|
|
774
|
+
p_exp = isa_pressure_pa(model["altitude_m"])
|
|
775
|
+
rho_exp = p_exp / (ISA_R * t_exp)
|
|
776
|
+
st = model["stack"]
|
|
777
|
+
ci = model["bias_ci"]
|
|
778
|
+
cv = model["convergence"]
|
|
779
|
+
uc_model = math.sqrt((model["sens_p"] * model["pressure_unc_pa"]) ** 2
|
|
780
|
+
+ (model["sens_t"] * model["temperature_unc_k"]) ** 2)
|
|
781
|
+
results = {
|
|
782
|
+
"title_present": model.get("document_type") == "Analysis Verification Memo",
|
|
783
|
+
"isa_values_real": (math.isclose(isa["temperature_k"], t_exp, rel_tol=1e-9)
|
|
784
|
+
and math.isclose(isa["pressure_pa"], p_exp, rel_tol=1e-9)
|
|
785
|
+
and math.isclose(isa["density_kgm3"], rho_exp, rel_tol=1e-9)),
|
|
786
|
+
"uncertainty_valid": (math.isclose(model["u_c"], uc_model, rel_tol=1e-9)
|
|
787
|
+
and math.isclose(model["expanded_U"],
|
|
788
|
+
model["coverage_factor"] * model["u_c"])
|
|
789
|
+
and abs(sum(c["percent"] for c in model["contributions"])
|
|
790
|
+
- 100.0) < 1e-6
|
|
791
|
+
and model["u_c"] > 0),
|
|
792
|
+
"stack_valid": (st["worst_case_mm"] >= st["rss_mm"] >= 0
|
|
793
|
+
and st["wc_limits_mm"][0] < st["wc_limits_mm"][1]
|
|
794
|
+
and st["rss_limits_mm"][0] < st["rss_limits_mm"][1]),
|
|
795
|
+
"ci_valid": (ci["lower"] < ci["mean"] < ci["upper"]
|
|
796
|
+
and ci["t_quantile"] > 1.5),
|
|
797
|
+
"convergence_verified": (cv["verdict"] == "monotone converged"
|
|
798
|
+
and cv["gci"] is not None and cv["gci"] < 0.01
|
|
799
|
+
and cv["order"] is not None
|
|
800
|
+
and 1.5 <= cv["order"] <= 2.5),
|
|
801
|
+
"margins_stated": (model["ms_density"] == margin_of_safety(
|
|
802
|
+
model["density_allowable_pct"],
|
|
803
|
+
100.0 * model["expanded_U"] / model["density_kgm3"])
|
|
804
|
+
and st["ms_rss"] == margin_of_safety(st["allowable_mm"], st["rss_mm"])
|
|
805
|
+
and st["ms_wc"] == margin_of_safety(st["allowable_mm"], st["worst_case_mm"])),
|
|
806
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
807
|
+
}
|
|
808
|
+
results["all_pass"] = all(results.values())
|
|
809
|
+
return results
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def check_memo_markdown(md_text: str) -> dict:
|
|
813
|
+
"""Gate-check the rendered markdown deliverable (text markers)."""
|
|
814
|
+
low = md_text.lower()
|
|
815
|
+
checks = {
|
|
816
|
+
"has_title": "analysis verification memo" in low,
|
|
817
|
+
"has_isa_numbers": "rho_isa" in low and "p_isa" in low
|
|
818
|
+
and "t_isa" in low,
|
|
819
|
+
"has_uncertainty": "combined standard uncertainty" in low
|
|
820
|
+
and "u_c" in low and "kg/m3" in low,
|
|
821
|
+
"has_ci": "t-interval" in low or "confidence interval" in low,
|
|
822
|
+
"has_stack": "worst case" in low and "rss" in low and "mm" in low,
|
|
823
|
+
"has_convergence": "monotone-converged" in low
|
|
824
|
+
or "monotone converged" in low,
|
|
825
|
+
"has_margins": "ms = allowable / applied - 1" in low,
|
|
826
|
+
"has_draft_marker": "draft" in low,
|
|
827
|
+
"has_not_approval": "not an approval" in low,
|
|
828
|
+
}
|
|
829
|
+
checks["all_pass"] = all(checks.values())
|
|
830
|
+
return checks
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
834
|
+
"""Public entry point used by gate tooling: check a memo document."""
|
|
835
|
+
return check_memo_markdown(md_text)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
# ---------------------------------------------------------------------------
|
|
839
|
+
# Example deliverables (tests / demo / filled template source)
|
|
840
|
+
# ---------------------------------------------------------------------------
|
|
841
|
+
|
|
842
|
+
def example_memo_model() -> dict:
|
|
843
|
+
return build_memo(example_chain())
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def example_memo_markdown() -> str:
|
|
847
|
+
return render_memo_markdown(example_memo_model())
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
if __name__ == "__main__":
|
|
851
|
+
model = example_memo_model()
|
|
852
|
+
md = example_memo_markdown()
|
|
853
|
+
print(f"CHAIN: {model['chain']}")
|
|
854
|
+
print(f"DENSITY: {model['density_kgm3']:.6g} +/- {model['expanded_U']:.6g} kg/m3 (k={model['coverage_factor']:g})")
|
|
855
|
+
print(f"u_c: {model['u_c']:.6g} kg/m3; budget: "
|
|
856
|
+
+ "; ".join(f"{c['percent']:.1f}%" for c in model['contributions']))
|
|
857
|
+
print(f"BIAS CI: [{model['bias_ci']['lower']:.4g}, {model['bias_ci']['upper']:.4g}] Pa")
|
|
858
|
+
print(f"STACK: nominal {model['stack']['nominal_mm']} mm, "
|
|
859
|
+
f"WC +/-{model['stack']['worst_case_mm']} mm, "
|
|
860
|
+
f"RSS +/-{model['stack']['rss_mm']} mm")
|
|
861
|
+
print(f"CONVERGENCE: {model['convergence']['verdict']}, "
|
|
862
|
+
f"order {model['convergence']['order']:.3f}, "
|
|
863
|
+
f"GCI {model['convergence']['gci'] * 100:.4g}%")
|
|
864
|
+
print(f"MODEL GATES: {check_memo(model)}")
|
|
865
|
+
print(f"MARKDOWN GATES: {check_memo_markdown(md)}")
|
|
866
|
+
print(f"RENDERED: {len(md)} chars")
|