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,73 @@
|
|
|
1
|
+
# Analysis Verification Memo
|
|
2
|
+
|
|
3
|
+
**Chain:** Flight-test air-data chain at 3000 m
|
|
4
|
+
**Status:** draft-for-review
|
|
5
|
+
|
|
6
|
+
## 1. Scope and inputs
|
|
7
|
+
|
|
8
|
+
Analysis question: verify the measurement/calculation chain Flight-test air-data chain at 3000 m - reduce the measured static pressure and temperature to air density and density altitude, check the repeated-measurement bias, verify the probe mounting tolerance stack and the atmosphere-model integrator, and state margins.
|
|
9
|
+
- Analysis item: Flight-test air-data chain at 3000 m at geometric altitude 3000 m.
|
|
10
|
+
- Inputs (measured, with 1-sigma standard uncertainties): static pressure p = 70050 Pa +/- 35 Pa; temperature T = 271 K +/- 1.2 K.
|
|
11
|
+
- Inputs (geometry): 3-member probe mounting stack (see Section 5); position spec +/- 0.8 mm.
|
|
12
|
+
- Inputs (acceptance): density accuracy spec +/- 1.5 %.
|
|
13
|
+
- n=10 repeat readings of the static source error vs the reference pressure standard.
|
|
14
|
+
|
|
15
|
+
## 2. Unit/atmosphere basis
|
|
16
|
+
|
|
17
|
+
- Unit system: SI (Pa, K, kg/m3, m, mm). No unit conversions required in the chain; conversions applied for reporting: altitude 3000 m = 9842.5 ft; T 268.65 K = -4.5 degC.
|
|
18
|
+
- Atmosphere basis: ISA (ISO 2533 / ICAO Doc 7488) reference model, sea level 288.15 K / 101325.0 Pa / 1.225 kg/m3, lapse rate 6.5 K/km to 11 km.
|
|
19
|
+
- ISA reference values at 3000 m: T_ISA = 268.65 K, p_ISA = 70108 Pa, rho_ISA = 0.90913 kg/m3.
|
|
20
|
+
|
|
21
|
+
## 3. Method and verification
|
|
22
|
+
|
|
23
|
+
- Density from measured state: ideal gas EOS rho = p / (R T), R = 287.05 J/(kg K) (bound leaf: isa-atmosphere).
|
|
24
|
+
- Uncertainty: GUM (JCGM 100) first-order law with analytic sensitivities, combined standard uncertainty u_c = sqrt(sum((df/dx_i * u_i)^2)) and expanded U = k * u_c with k = 2 (~95 %) (bound leaf: uncertainty-propagation).
|
|
25
|
+
- Bias statistics: Student t interval for the mean of the repeat readings (bound leaf: confidence-interval-estimation).
|
|
26
|
+
- Density altitude: Newton inversion of rho_ISA(h) = rho_measured, converged in 3 iterations to residual 4.16e-10 (bound leaf: root-finding; convergence evidence per leaf convergence-verification).
|
|
27
|
+
- Numerical verification: three-grid trapezoid integration of the hydrostatic balance dp/dz = -rho g over 0..3000 m at 6, 12 and 24 steps (refinement ratio r = 2); Richardson extrapolation and grid convergence index (GCI) versus the closed-form ISA pressure.
|
|
28
|
+
- Numerical accuracy statement: see grid study below; the integrator is grid-converged and agrees with the closed-form ISA pressure at the test point.
|
|
29
|
+
|
|
30
|
+
## 4. Results
|
|
31
|
+
|
|
32
|
+
- Air density (measured chain): rho = 0.90049 kg/m3.
|
|
33
|
+
- Sensitivities: d rho/dp = 1.2855e-05 (kg/m3)/Pa, d rho/dT = -0.0033229 (kg/m3)/K.
|
|
34
|
+
- Combined standard uncertainty: u_c = 0.0040127 kg/m3 (0.44561 % of rho).
|
|
35
|
+
- Expanded uncertainty: U = 0.0080255 kg/m3 (k = 2). Result: rho = 0.90049 +/- 0.0080255 kg/m3.
|
|
36
|
+
- Uncertainty budget: 98.7% from temperature (s = -3.323e-03, u = 1.2); 1.3% from static pressure (s = 1.286e-05, u = 35).
|
|
37
|
+
- Versus ISA at 3000 m: delta = -0.94955 % (non-standard day).
|
|
38
|
+
- Density altitude: h_rho = 3092.6 m (+92.551 m above the geometric test point).
|
|
39
|
+
- Bias statistics (n=10 repeat readings of the static source error vs the reference pressure standard): mean bias 0.59 Pa, sample s = 1.907 Pa, 95 % t-interval [-0.77417, 1.9542] Pa (t = 2.2622, df = 9). Zero lies inside the interval: no statistically significant static-source bias at the 95 % level.
|
|
40
|
+
|
|
41
|
+
## 5. Tolerances
|
|
42
|
+
|
|
43
|
+
- GD&T basis: ASME Y14.5 (TIER-2 reference-only). The stack is a 1-D linear dimension chain with equal bilateral tolerances and independent, centered parts (bound leaf: tolerance-stackup).
|
|
44
|
+
- Chain members (signed nominal direction, tolerance, RSS share):
|
|
45
|
+
- mounting bracket: 120 mm (dir +), +/- 0.5 mm, 52.9 % share
|
|
46
|
+
- adapter: 45 mm (dir +), +/- 0.25 mm, 13.2 % share
|
|
47
|
+
- probe inset: 60 mm (dir -), +/- 0.4 mm, 33.9 % share
|
|
48
|
+
- Nominal total: 105 mm.
|
|
49
|
+
- Worst case: +/- 1.15 mm -> limits [103.85, 106.15] mm.
|
|
50
|
+
- RSS: +/- 0.68739 mm -> limits [104.31, 105.69] mm.
|
|
51
|
+
- Dominant contributor: mounting bracket (52.9 % of RSS variance); tightening it reduces the stack most.
|
|
52
|
+
|
|
53
|
+
## 6. Data sources
|
|
54
|
+
|
|
55
|
+
- ISA model and constants: ISO 2533 / ICAO Doc 7488 standard atmosphere (public standard; summary-only use).
|
|
56
|
+
- Uncertainty method: GUM, JCGM 100:2008 (public guide; summary-only use).
|
|
57
|
+
- GD&T conventions: ASME Y14.5 (TIER-2 reference-only; summary-not-copy per SOURCES.md).
|
|
58
|
+
- Export-control classification: uncontrolled technical data (example). No controlled data is included in this memo.
|
|
59
|
+
|
|
60
|
+
## 7. Margins and conclusions
|
|
61
|
+
|
|
62
|
+
- Margins per the engineering-margins convention MS = allowable / applied - 1 (same units both sides).
|
|
63
|
+
- Density accuracy: applied (expanded) = 0.89123 %, allowable +/- 1.5 % -> MS = 0.68307 (PASS).
|
|
64
|
+
- Probe position stack, RSS basis: applied +/- 0.68739 mm, allowable +/- 0.8 mm -> MS = 0.16383 (PASS).
|
|
65
|
+
- Probe position stack, worst-case basis: applied +/- 1.15 mm, allowable +/- 0.8 mm -> MS = -0.30435 (FAIL).
|
|
66
|
+
- Conclusions: the chain is unit-consistent with the stated ISA basis; the expanded density uncertainty is driven by the temperature input (99 % of variance); repeated-measurement bias is not significant; the atmosphere integrator is monotone-converged (order 2), GCI 0.0005462 % at the test point, and agrees with the closed-form ISA pressure. The stack meets the position spec on the RSS basis but not on the worst-case basis: the RSS basis is valid only while the parts remain independent and centered (process-capability evidence required).
|
|
67
|
+
|
|
68
|
+
## 8. Open items
|
|
69
|
+
|
|
70
|
+
- For the design owner / human approver: confirm the density accuracy spec and the position spec limits used as allowables; decide worst-case vs RSS basis for the probe stack (tighten the dominant contributor mounting bracket if worst case must be met); confirm export-control classification before external release.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
*Generated by Aero Agent Roles engineering-analysis-engineer core. DRAFT for human engineering review. Not an approval document.*
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test engineering_analysis_engineer_core: the executable engine of the
|
|
3
|
+
Engineering Analysis and Data Engineer role.
|
|
4
|
+
|
|
5
|
+
Proves the role can DO its job standalone (no AeroSkills needed): ISA
|
|
6
|
+
reference values, GUM uncertainty propagation, t confidence intervals,
|
|
7
|
+
tolerance stack-up (worst case + RSS), Richardson/GCI convergence
|
|
8
|
+
verification, engineering margins, memo generation, evidence gates, and
|
|
9
|
+
the filled deliverable template.
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
16
|
+
"..", "core"))
|
|
17
|
+
from engineering_analysis_engineer_core import ( # noqa: E402
|
|
18
|
+
ISA_G, ISA_LAPSE, ISA_P0, ISA_R, ISA_T0, StackMember,
|
|
19
|
+
check_memo, check_memo_markdown, combined_standard_uncertainty,
|
|
20
|
+
confidence_interval_mean, convergence_verdict, density_altitude_m,
|
|
21
|
+
density_from_measured, example_chain, example_memo_markdown,
|
|
22
|
+
expanded_uncertainty, grid_convergence_index, isa_density_kgm3,
|
|
23
|
+
isa_pressure_pa, isa_reference_values, isa_temperature_k,
|
|
24
|
+
margin_of_safety, margin_verdict, nominal_total, observed_order,
|
|
25
|
+
render_memo_markdown, richardson_extrapolation, rss_shares, rss_total,
|
|
26
|
+
t_ppf_two_sided, uncertainty_contributions, worst_case_total,
|
|
27
|
+
build_memo, trapezoid_hydrostatic_pressure,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Deterministic across midnight: pin fresh renders to the date the
|
|
31
|
+
# committed template carries (CI override wins).
|
|
32
|
+
os.environ.setdefault("ROLE_GEN_DATE", "2026-09-05")
|
|
33
|
+
|
|
34
|
+
ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
35
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates", "analysis-memo-template.md")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestIsaAtmosphere(unittest.TestCase):
|
|
39
|
+
|
|
40
|
+
def test_sea_level_values(self):
|
|
41
|
+
self.assertAlmostEqual(isa_temperature_k(0), ISA_T0, places=9)
|
|
42
|
+
self.assertAlmostEqual(isa_pressure_pa(0), ISA_P0, places=6)
|
|
43
|
+
rho0 = ISA_P0 / (ISA_R * ISA_T0)
|
|
44
|
+
self.assertAlmostEqual(isa_density_kgm3(0), rho0, places=9)
|
|
45
|
+
self.assertAlmostEqual(rho0, 1.225, places=3)
|
|
46
|
+
|
|
47
|
+
def test_tropopause(self):
|
|
48
|
+
self.assertAlmostEqual(isa_temperature_k(11000), 216.65, places=9)
|
|
49
|
+
self.assertAlmostEqual(isa_temperature_k(15000), 216.65, places=9)
|
|
50
|
+
|
|
51
|
+
def test_known_troposphere_point(self):
|
|
52
|
+
# 3000 m: T = 288.15 - 0.0065*3000 = 268.65 K
|
|
53
|
+
self.assertAlmostEqual(isa_temperature_k(3000), 268.65, places=9)
|
|
54
|
+
p3 = isa_pressure_pa(3000)
|
|
55
|
+
t3 = isa_temperature_k(3000)
|
|
56
|
+
self.assertAlmostEqual(isa_density_kgm3(3000),
|
|
57
|
+
p3 / (ISA_R * t3), places=12)
|
|
58
|
+
self.assertGreater(isa_pressure_pa(1000), isa_pressure_pa(3000))
|
|
59
|
+
|
|
60
|
+
def test_pressure_monotonic_and_reference_dict(self):
|
|
61
|
+
vals = [isa_reference_values(h) for h in (0, 3000, 11000)]
|
|
62
|
+
ps = [v["pressure_pa"] for v in vals]
|
|
63
|
+
self.assertEqual(ps, sorted(ps, reverse=True))
|
|
64
|
+
self.assertEqual(set(vals[0]), {"altitude_m", "temperature_k",
|
|
65
|
+
"pressure_pa", "density_kgm3"})
|
|
66
|
+
|
|
67
|
+
def test_out_of_range_raises(self):
|
|
68
|
+
for h in (-1, 21000):
|
|
69
|
+
with self.assertRaises(ValueError):
|
|
70
|
+
isa_temperature_k(h)
|
|
71
|
+
with self.assertRaises(ValueError):
|
|
72
|
+
isa_pressure_pa(h)
|
|
73
|
+
|
|
74
|
+
def test_density_from_measured(self):
|
|
75
|
+
# Ideal gas EOS rho = p / (R T)
|
|
76
|
+
self.assertAlmostEqual(density_from_measured(101325, 288.15),
|
|
77
|
+
101325 / (ISA_R * 288.15), places=12)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestUncertaintyPropagation(unittest.TestCase):
|
|
81
|
+
|
|
82
|
+
def test_combined_standard(self):
|
|
83
|
+
# u_c = sqrt((2*0.5)^2 + (3*1)^2) = sqrt(1 + 9) = sqrt(10)
|
|
84
|
+
self.assertAlmostEqual(combined_standard_uncertainty([2, 3],
|
|
85
|
+
[0.5, 1.0]),
|
|
86
|
+
(10.0) ** 0.5, places=12)
|
|
87
|
+
# single input: u_c = s*u
|
|
88
|
+
self.assertAlmostEqual(combined_standard_uncertainty([4], [0.25]),
|
|
89
|
+
1.0, places=12)
|
|
90
|
+
|
|
91
|
+
def test_expanded(self):
|
|
92
|
+
self.assertAlmostEqual(expanded_uncertainty(2.0), 4.0, places=12)
|
|
93
|
+
self.assertAlmostEqual(expanded_uncertainty(2.0, k=3), 6.0, places=12)
|
|
94
|
+
with self.assertRaises(ValueError):
|
|
95
|
+
expanded_uncertainty(1.0, k=0)
|
|
96
|
+
|
|
97
|
+
def test_contributions_and_shares(self):
|
|
98
|
+
contribs = uncertainty_contributions([2, 3], [0.5, 1.0])
|
|
99
|
+
self.assertAlmostEqual(sum(c["percent"] for c in contribs), 100.0,
|
|
100
|
+
places=9)
|
|
101
|
+
self.assertEqual(contribs[0]["index"], 1) # dominant first
|
|
102
|
+
# invalid inputs
|
|
103
|
+
with self.assertRaises(ValueError):
|
|
104
|
+
combined_standard_uncertainty([1], [1, 2])
|
|
105
|
+
with self.assertRaises(ValueError):
|
|
106
|
+
combined_standard_uncertainty([1], [-1])
|
|
107
|
+
|
|
108
|
+
def test_example_density_chain_uncertainty(self):
|
|
109
|
+
# Example: p = 70050 +/- 35 Pa, T = 271 +/- 1.2 K, rho = p/(R T)
|
|
110
|
+
chain = example_chain()
|
|
111
|
+
t = chain.temperature_k
|
|
112
|
+
sens_p = 1.0 / (ISA_R * t)
|
|
113
|
+
sens_t = -chain.pressure_pa / (ISA_R * t * t)
|
|
114
|
+
uc = combined_standard_uncertainty([sens_p, sens_t],
|
|
115
|
+
[chain.pressure_unc_pa,
|
|
116
|
+
chain.temperature_unc_k])
|
|
117
|
+
U = expanded_uncertainty(uc, chain.coverage_factor)
|
|
118
|
+
rho = density_from_measured(chain.pressure_pa, t)
|
|
119
|
+
# temperature dominates the budget (~99%)
|
|
120
|
+
contribs = uncertainty_contributions([sens_p, sens_t],
|
|
121
|
+
[chain.pressure_unc_pa,
|
|
122
|
+
chain.temperature_unc_k])
|
|
123
|
+
self.assertAlmostEqual(uc, 0.0040127, places=6)
|
|
124
|
+
self.assertAlmostEqual(U, 2 * uc, places=12)
|
|
125
|
+
self.assertGreater(contribs[0]["percent"], 90.0)
|
|
126
|
+
self.assertAlmostEqual(rho, 0.900495, places=5)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestConfidenceInterval(unittest.TestCase):
|
|
130
|
+
|
|
131
|
+
def test_t_quantile_known_values(self):
|
|
132
|
+
self.assertAlmostEqual(t_ppf_two_sided(0.95, 9), 2.262157, places=4)
|
|
133
|
+
self.assertAlmostEqual(t_ppf_two_sided(0.95, 4), 2.776445, places=4)
|
|
134
|
+
self.assertAlmostEqual(t_ppf_two_sided(0.99, 10), 3.169273, places=4)
|
|
135
|
+
with self.assertRaises(ValueError):
|
|
136
|
+
t_ppf_two_sided(0.0, 5)
|
|
137
|
+
with self.assertRaises(ValueError):
|
|
138
|
+
t_ppf_two_sided(0.95, 0.5)
|
|
139
|
+
|
|
140
|
+
def test_confidence_interval_mean(self):
|
|
141
|
+
x = [1.8, -2.2, 3.1, -0.7, 2.4, -1.5, 0.9, 2.8, -1.1, 0.4]
|
|
142
|
+
ci = confidence_interval_mean(x)
|
|
143
|
+
mean = sum(x) / len(x)
|
|
144
|
+
self.assertAlmostEqual(ci["mean"], mean, places=12)
|
|
145
|
+
self.assertEqual(ci["n"], 10)
|
|
146
|
+
self.assertEqual(ci["df"], 9.0)
|
|
147
|
+
self.assertLess(ci["lower"], mean)
|
|
148
|
+
self.assertGreater(ci["upper"], mean)
|
|
149
|
+
# 95% of the interval width comes from t * s / sqrt(n)
|
|
150
|
+
import math
|
|
151
|
+
s = math.sqrt(sum((v - mean) ** 2 for v in x) / 9.0)
|
|
152
|
+
self.assertAlmostEqual(ci["se"], s / math.sqrt(10), places=12)
|
|
153
|
+
self.assertAlmostEqual(ci["t_quantile"], 2.262157, places=4)
|
|
154
|
+
|
|
155
|
+
def test_degenerate_constant_sample(self):
|
|
156
|
+
ci = confidence_interval_mean([5.0, 5.0, 5.0])
|
|
157
|
+
self.assertEqual(ci["lower"], ci["upper"])
|
|
158
|
+
|
|
159
|
+
def test_short_sample_raises(self):
|
|
160
|
+
with self.assertRaises(ValueError):
|
|
161
|
+
confidence_interval_mean([1.0])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestToleranceStackup(unittest.TestCase):
|
|
165
|
+
|
|
166
|
+
def _stack(self):
|
|
167
|
+
return [StackMember("bracket", 120.0, 0.50, +1),
|
|
168
|
+
StackMember("adapter", 45.0, 0.25, +1),
|
|
169
|
+
StackMember("probe inset", 60.0, 0.40, -1)]
|
|
170
|
+
|
|
171
|
+
def test_nominal_total(self):
|
|
172
|
+
self.assertAlmostEqual(nominal_total(self._stack()), 105.0, places=9)
|
|
173
|
+
|
|
174
|
+
def test_worst_case_total(self):
|
|
175
|
+
self.assertAlmostEqual(worst_case_total(self._stack()), 1.15,
|
|
176
|
+
places=9)
|
|
177
|
+
|
|
178
|
+
def test_rss_total(self):
|
|
179
|
+
# sqrt(0.5^2 + 0.25^2 + 0.4^2)
|
|
180
|
+
self.assertAlmostEqual(rss_total(self._stack()),
|
|
181
|
+
(0.25 + 0.0625 + 0.16) ** 0.5, places=9)
|
|
182
|
+
# RSS never exceeds worst case
|
|
183
|
+
self.assertLessEqual(rss_total(self._stack()),
|
|
184
|
+
worst_case_total(self._stack()))
|
|
185
|
+
|
|
186
|
+
def test_rss_shares(self):
|
|
187
|
+
shares = rss_shares(self._stack())
|
|
188
|
+
self.assertAlmostEqual(sum(shares), 100.0, places=9)
|
|
189
|
+
self.assertGreater(shares[0], shares[1]) # bracket dominant
|
|
190
|
+
self.assertAlmostEqual(shares[0], 52.910, places=2)
|
|
191
|
+
|
|
192
|
+
def test_empty_raises(self):
|
|
193
|
+
with self.assertRaises(ValueError):
|
|
194
|
+
nominal_total([])
|
|
195
|
+
with self.assertRaises(ValueError):
|
|
196
|
+
worst_case_total([])
|
|
197
|
+
with self.assertRaises(ValueError):
|
|
198
|
+
rss_total([])
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class TestConvergenceVerification(unittest.TestCase):
|
|
202
|
+
|
|
203
|
+
def test_observed_order_monotone(self):
|
|
204
|
+
# r = 2, ratio (f3-f2)/(f2-f1) = 4 -> order exactly 2
|
|
205
|
+
self.assertAlmostEqual(observed_order(1.000, 1.010, 1.050, 2.0),
|
|
206
|
+
2.0, places=9)
|
|
207
|
+
|
|
208
|
+
def test_richardson_and_gci(self):
|
|
209
|
+
f1, f2, r, p = 70107.96199725207, 70107.04297140652, 2.0, 2.0
|
|
210
|
+
fex = richardson_extrapolation(f1, f2, r, p)
|
|
211
|
+
gci = grid_convergence_index(f1, f2, r, p)
|
|
212
|
+
# extrapolated value recovers the closed-form ISA pressure at 3000 m
|
|
213
|
+
closed = isa_pressure_pa(3000)
|
|
214
|
+
self.assertAlmostEqual(fex, closed, delta=1e-4)
|
|
215
|
+
self.assertAlmostEqual(gci, 5.462e-6, places=8)
|
|
216
|
+
|
|
217
|
+
def test_verdict_monotone(self):
|
|
218
|
+
v = convergence_verdict(1.000, 1.010, 1.050, 2.0)
|
|
219
|
+
self.assertEqual(v["verdict"], "monotone converged")
|
|
220
|
+
self.assertAlmostEqual(v["order"], 2.0, places=9)
|
|
221
|
+
self.assertIsNotNone(v["extrapolated"])
|
|
222
|
+
self.assertIsNotNone(v["gci"])
|
|
223
|
+
|
|
224
|
+
def test_verdict_oscillatory_and_diverging(self):
|
|
225
|
+
# oscillatory: (f3-f2)/(f2-f1) in (-1, 0)
|
|
226
|
+
v = convergence_verdict(1.0, 1.1, 1.05, 2.0)
|
|
227
|
+
self.assertEqual(v["verdict"], "oscillatory")
|
|
228
|
+
self.assertIsNone(v["order"])
|
|
229
|
+
# diverging: |ratio| > 1 on the negative branch
|
|
230
|
+
v = convergence_verdict(1.0, 1.1, 0.6, 2.0)
|
|
231
|
+
self.assertEqual(v["verdict"], "diverging")
|
|
232
|
+
|
|
233
|
+
def test_trapezoid_hydrostatic_agrees_with_closed_form(self):
|
|
234
|
+
# fine-grid integration must approach the closed-form ISA pressure
|
|
235
|
+
exact = isa_pressure_pa(3000)
|
|
236
|
+
fine = trapezoid_hydrostatic_pressure(3000, 24)
|
|
237
|
+
coarse = trapezoid_hydrostatic_pressure(3000, 6)
|
|
238
|
+
self.assertLess(abs(fine - exact), abs(coarse - exact))
|
|
239
|
+
self.assertLess(abs(fine - exact) / exact, 1e-4)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestDensityAltitudeRootFinding(unittest.TestCase):
|
|
243
|
+
|
|
244
|
+
def test_inversion_round_trip(self):
|
|
245
|
+
# rho_ISA at 3000 m must invert back to ~3000 m
|
|
246
|
+
rho3 = isa_density_kgm3(3000)
|
|
247
|
+
res = density_altitude_m(rho3, h0=2800)
|
|
248
|
+
self.assertTrue(res["converged"])
|
|
249
|
+
self.assertAlmostEqual(res["altitude_m"], 3000.0, delta=1e-3)
|
|
250
|
+
|
|
251
|
+
def test_warm_day_density_altitude(self):
|
|
252
|
+
# measured density below ISA at 3000 m -> density altitude above it
|
|
253
|
+
chain = example_chain()
|
|
254
|
+
rho_m = density_from_measured(chain.pressure_pa, chain.temperature_k)
|
|
255
|
+
res = density_altitude_m(rho_m, h0=3000)
|
|
256
|
+
self.assertGreater(res["altitude_m"], 3000.0)
|
|
257
|
+
self.assertAlmostEqual(res["altitude_m"], 3092.55, delta=0.5)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestMargins(unittest.TestCase):
|
|
261
|
+
|
|
262
|
+
def test_margin_of_safety(self):
|
|
263
|
+
self.assertAlmostEqual(margin_of_safety(125000, 100000), 0.25,
|
|
264
|
+
places=12)
|
|
265
|
+
self.assertAlmostEqual(margin_of_safety(90000, 100000), -0.1,
|
|
266
|
+
places=12)
|
|
267
|
+
|
|
268
|
+
def test_margin_verdict(self):
|
|
269
|
+
self.assertEqual(margin_verdict(0.25), "PASS")
|
|
270
|
+
self.assertEqual(margin_verdict(-0.1), "FAIL")
|
|
271
|
+
self.assertEqual(margin_verdict(0.0), "PASS")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestMemoBuilder(unittest.TestCase):
|
|
275
|
+
|
|
276
|
+
def test_example_model_numbers(self):
|
|
277
|
+
model = build_memo(example_chain())
|
|
278
|
+
self.assertEqual(model["document_type"], "Analysis Verification Memo")
|
|
279
|
+
self.assertEqual(model["status"], "draft-for-review")
|
|
280
|
+
self.assertAlmostEqual(model["density_kgm3"], 0.900495, places=5)
|
|
281
|
+
self.assertAlmostEqual(model["u_c"], 0.0040127, places=6)
|
|
282
|
+
self.assertAlmostEqual(model["expanded_U"], 0.0080255, places=6)
|
|
283
|
+
# ISA values at the test altitude
|
|
284
|
+
self.assertAlmostEqual(model["isa"]["temperature_k"], 268.65,
|
|
285
|
+
places=9)
|
|
286
|
+
self.assertAlmostEqual(model["isa"]["pressure_pa"],
|
|
287
|
+
isa_pressure_pa(3000), places=6)
|
|
288
|
+
# stack numbers
|
|
289
|
+
st = model["stack"]
|
|
290
|
+
self.assertAlmostEqual(st["nominal_mm"], 105.0, places=6)
|
|
291
|
+
self.assertAlmostEqual(st["worst_case_mm"], 1.15, places=6)
|
|
292
|
+
self.assertAlmostEqual(st["rss_mm"], 0.687386, places=5)
|
|
293
|
+
self.assertEqual(st["dominant"], "mounting bracket")
|
|
294
|
+
# margins
|
|
295
|
+
self.assertAlmostEqual(model["ms_density"], 0.68307, places=4)
|
|
296
|
+
self.assertAlmostEqual(st["ms_rss"], 0.16383, places=4)
|
|
297
|
+
self.assertAlmostEqual(st["ms_wc"], -0.30435, places=4)
|
|
298
|
+
# convergence
|
|
299
|
+
cv = model["convergence"]
|
|
300
|
+
self.assertEqual(cv["verdict"], "monotone converged")
|
|
301
|
+
self.assertAlmostEqual(cv["order"], 2.0, delta=0.01)
|
|
302
|
+
self.assertLess(cv["gci"], 0.001)
|
|
303
|
+
# bias CI contains the mean and zero
|
|
304
|
+
ci = model["bias_ci"]
|
|
305
|
+
self.assertAlmostEqual(ci["mean"], 0.59, places=9)
|
|
306
|
+
self.assertLess(ci["lower"], 0.0)
|
|
307
|
+
self.assertGreater(ci["upper"], 0.0)
|
|
308
|
+
|
|
309
|
+
def test_custom_altitude(self):
|
|
310
|
+
chain = example_chain()
|
|
311
|
+
chain.altitude_m = 6000.0
|
|
312
|
+
model = build_memo(chain)
|
|
313
|
+
self.assertAlmostEqual(model["isa"]["temperature_k"],
|
|
314
|
+
isa_temperature_k(6000), places=9)
|
|
315
|
+
self.assertAlmostEqual(model["altitude_m"], 6000.0, places=9)
|
|
316
|
+
|
|
317
|
+
def test_model_gates_all_pass(self):
|
|
318
|
+
model = build_memo(example_chain())
|
|
319
|
+
gates = check_memo(model)
|
|
320
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
321
|
+
|
|
322
|
+
def test_markdown_gates_all_pass(self):
|
|
323
|
+
md = example_memo_markdown()
|
|
324
|
+
gates = check_memo_markdown(md)
|
|
325
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
326
|
+
|
|
327
|
+
def test_render_contains_required_sections(self):
|
|
328
|
+
md = example_memo_markdown()
|
|
329
|
+
for sec in ["## 1. Scope and inputs", "## 2. Unit/atmosphere basis",
|
|
330
|
+
"## 3. Method and verification", "## 4. Results",
|
|
331
|
+
"## 5. Tolerances", "## 6. Data sources",
|
|
332
|
+
"## 7. Margins and conclusions", "## 8. Open items"]:
|
|
333
|
+
self.assertIn(sec, md)
|
|
334
|
+
self.assertIn("not an approval", md.lower())
|
|
335
|
+
|
|
336
|
+
def test_gate_failures_are_caught(self):
|
|
337
|
+
model = build_memo(example_chain())
|
|
338
|
+
model["status"] = "approved" # dishonest sign-off
|
|
339
|
+
self.assertFalse(check_memo(model)["sign_off_honest"])
|
|
340
|
+
model2 = build_memo(example_chain())
|
|
341
|
+
model2["isa"]["pressure_pa"] = 1.0 # ISA values not real
|
|
342
|
+
self.assertFalse(check_memo(model2)["isa_values_real"])
|
|
343
|
+
model3 = build_memo(example_chain())
|
|
344
|
+
model3["u_c"] = 0.0 # uncertainty not real
|
|
345
|
+
self.assertFalse(check_memo(model3)["uncertainty_valid"])
|
|
346
|
+
bad_md = "# nothing here"
|
|
347
|
+
self.assertFalse(check_memo_markdown(bad_md)["all_pass"])
|
|
348
|
+
|
|
349
|
+
def test_standalone_no_skills_repo(self):
|
|
350
|
+
# Core must work with no AeroSkills present.
|
|
351
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
352
|
+
model = build_memo(example_chain())
|
|
353
|
+
self.assertGreater(model["density_kgm3"], 0.1)
|
|
354
|
+
md = render_memo_markdown(model)
|
|
355
|
+
self.assertGreater(len(md), 3000)
|
|
356
|
+
|
|
357
|
+
def test_template_is_the_filled_example(self):
|
|
358
|
+
# ROLE-STANDARD: the template IS the example deliverable generated
|
|
359
|
+
# by the core - byte-identical, zero blanks.
|
|
360
|
+
self.assertTrue(os.path.exists(TEMPLATE))
|
|
361
|
+
template = open(TEMPLATE).read()
|
|
362
|
+
self.assertNotIn("___", template)
|
|
363
|
+
self.assertEqual(template, example_memo_markdown())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
unittest.main()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Role test: engineering-analysis-engineer."""
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
ROLES_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
|
|
8
|
+
os.path.abspath(__file__)))))
|
|
9
|
+
AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
|
|
10
|
+
HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
|
|
11
|
+
ROLE_DIR = os.path.join(ROLES_REPO, "roles", "engineering-analysis-engineer")
|
|
12
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates", "analysis-memo-template.md")
|
|
13
|
+
|
|
14
|
+
EXPECTED_BOUND = [
|
|
15
|
+
"cross-cutting/units-atmos/isa-atmosphere",
|
|
16
|
+
"cross-cutting/units-atmos/density-altitude",
|
|
17
|
+
"cross-cutting/units-atmos/airspeed-conversion",
|
|
18
|
+
"cross-cutting/units-atmos/unit-conversion",
|
|
19
|
+
"cross-cutting/units-atmos/dimensional-analysis",
|
|
20
|
+
"cross-cutting/units-atmos/temperature-conversion",
|
|
21
|
+
"cross-cutting/numerics/finite-difference-derivatives",
|
|
22
|
+
"cross-cutting/numerics/numerical-integration",
|
|
23
|
+
"cross-cutting/numerics/ode-solvers",
|
|
24
|
+
"cross-cutting/numerics/root-finding",
|
|
25
|
+
"cross-cutting/numerics/interpolation",
|
|
26
|
+
"cross-cutting/numerics/least-squares-regression",
|
|
27
|
+
"cross-cutting/numerics/optimization-algorithms",
|
|
28
|
+
"cross-cutting/numerics/eigenvalue-decomposition",
|
|
29
|
+
"cross-cutting/numerics/singular-value-decomposition",
|
|
30
|
+
"cross-cutting/numerics/matrix-operations",
|
|
31
|
+
"cross-cutting/numerics/complex-number-algebra",
|
|
32
|
+
"cross-cutting/numerics/quaternion-algebra",
|
|
33
|
+
"cross-cutting/numerics/fast-fourier-transform",
|
|
34
|
+
"cross-cutting/numerics/fir-filter-design",
|
|
35
|
+
"cross-cutting/numerics/digital-filter-design",
|
|
36
|
+
"cross-cutting/numerics/power-spectral-density",
|
|
37
|
+
"cross-cutting/numerics/uncertainty-propagation",
|
|
38
|
+
"cross-cutting/numerics/monte-carlo-sampling",
|
|
39
|
+
"cross-cutting/numerics/probability-distributions",
|
|
40
|
+
"cross-cutting/numerics/confidence-interval-estimation",
|
|
41
|
+
"cross-cutting/numerics/hypothesis-testing",
|
|
42
|
+
"cross-cutting/numerics/descriptive-statistics",
|
|
43
|
+
"cross-cutting/numerics/cross-correlation-analysis",
|
|
44
|
+
"cross-cutting/numerics/information-entropy",
|
|
45
|
+
"cross-cutting/numerics/convergence-verification",
|
|
46
|
+
"cross-cutting/numerics/grubbs-outlier-test",
|
|
47
|
+
"cross-cutting/numerics/runs-test",
|
|
48
|
+
"cross-cutting/numerics/rank-based-hypothesis-testing",
|
|
49
|
+
"cross-cutting/tolerancing/gdandt-basics",
|
|
50
|
+
"cross-cutting/tolerancing/datum-reference-frames",
|
|
51
|
+
"cross-cutting/tolerancing/position-tolerance-calc",
|
|
52
|
+
"cross-cutting/tolerancing/tolerance-stackup",
|
|
53
|
+
"cross-cutting/documentation/engineering-report",
|
|
54
|
+
"cross-cutting/documentation/engineering-margins",
|
|
55
|
+
"cross-cutting/data-sources/aeronautical-data-sources",
|
|
56
|
+
"cross-cutting/export-control/export-control-awareness"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
STAGES = [
|
|
60
|
+
"1. Unit/atmosphere",
|
|
61
|
+
"2. Numerics",
|
|
62
|
+
"3. Regression/opt",
|
|
63
|
+
"4. Linear algebra",
|
|
64
|
+
"5. Signal",
|
|
65
|
+
"6. Uncertainty",
|
|
66
|
+
"7. Statistics",
|
|
67
|
+
"8. Convergence",
|
|
68
|
+
"9. Tolerancing",
|
|
69
|
+
"10. Data sourcing",
|
|
70
|
+
"11. Export control",
|
|
71
|
+
"12. Report"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def role_text():
|
|
76
|
+
p = os.path.join(ROLE_DIR, "ROLE.md")
|
|
77
|
+
return open(p).read() if os.path.exists(p) else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestEngineeringAnalysisEngineerRole(unittest.TestCase):
|
|
81
|
+
|
|
82
|
+
def test_bound_skills_resolve(self):
|
|
83
|
+
if not HAS_SKILLS:
|
|
84
|
+
self.skipTest("skills checkout absent (cross-repo dev check)")
|
|
85
|
+
for leaf in EXPECTED_BOUND:
|
|
86
|
+
self.assertTrue(
|
|
87
|
+
os.path.exists(os.path.join(AEROSKILLS, "skills", leaf, "SKILL.md")),
|
|
88
|
+
f"bound skill missing: {leaf}")
|
|
89
|
+
|
|
90
|
+
def test_stages_ordered(self):
|
|
91
|
+
role = role_text()
|
|
92
|
+
self.assertIsNotNone(role)
|
|
93
|
+
wf = role[role.find("## Workflow"):role.find("## Evidence gates")]
|
|
94
|
+
idx = [wf.find(s) for s in STAGES]
|
|
95
|
+
self.assertTrue(all(i >= 0 for i in idx), "stage missing in workflow")
|
|
96
|
+
self.assertEqual(idx, sorted(idx), "stage order not deterministic")
|
|
97
|
+
|
|
98
|
+
def test_template_present(self):
|
|
99
|
+
self.assertTrue(os.path.exists(TEMPLATE), "deliverable template missing")
|
|
100
|
+
|
|
101
|
+
def test_boundaries(self):
|
|
102
|
+
role = role_text()
|
|
103
|
+
self.assertIsNotNone(role)
|
|
104
|
+
for phrase in ["sign_off_required: true", "forbidden"]:
|
|
105
|
+
self.assertIn(phrase, role)
|
|
106
|
+
|
|
107
|
+
def test_sources(self):
|
|
108
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, "SOURCES.md")),
|
|
109
|
+
"SOURCES.md missing")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
unittest.main()
|