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,931 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""flight_test_core.py - Flight Test Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given an aircraft's project facts and the
|
|
5
|
+
flight-recorded (or worked-example simulated) data it plans a safe
|
|
6
|
+
build-up flight test campaign and produces the flight test plan plus
|
|
7
|
+
envelope expansion report: build-up blocks at fractions of the design
|
|
8
|
+
limits, V-speed margins, stall/VS1g checks, flutter point progression
|
|
9
|
+
with the damping requirement, load factor checks, an altitude/Mach
|
|
10
|
+
grid, and per-block PASS/FAIL criteria. It also gate-checks
|
|
11
|
+
deliverables. Standalone: no external repo needed.
|
|
12
|
+
|
|
13
|
+
Domain rules encoded here are public process knowledge as bound in the
|
|
14
|
+
Aero Agent Skills flight-test-operations leaves and in the public
|
|
15
|
+
FAR-25 / CS-25 context those leaves reference (vref/v2/vr factors, the
|
|
16
|
+
VA = VS*sqrt(n_max) maneuvering speed, the FAR 25.629 flutter margin
|
|
17
|
+
factor of 1.2 and the 0.03 minimum damping at the maximum test speed,
|
|
18
|
+
the FAR 25.207/25.203 stall warning and recovery criteria, equal-step
|
|
19
|
+
envelope expansion with the load factor checked at every point, and
|
|
20
|
+
ISA atmosphere physics for the altitude/Mach grid). No proprietary text
|
|
21
|
+
is reproduced; regulations are paraphrased context only.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import date
|
|
29
|
+
|
|
30
|
+
def _today() -> str:
|
|
31
|
+
import os
|
|
32
|
+
from datetime import date
|
|
33
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Domain tables (public practice / FAR-25 & CS-25 context, summary-only)
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
# Certification V-speed factors (FAR-25.107/25.125 context as encoded in the
|
|
40
|
+
# v-speeds AeroSkills leaf): vref = 1.3*vs0, v2 = 1.2*vs1, vr = 1.1*vs1.
|
|
41
|
+
VREF_FACTOR = 1.3
|
|
42
|
+
V2_FACTOR = 1.2
|
|
43
|
+
VR_FACTOR = 1.1
|
|
44
|
+
|
|
45
|
+
# Maneuvering speed VA = VS * sqrt(n_max) (FAR-25.335 context).
|
|
46
|
+
# Stall warning must begin at 1.05 * VS1g, or VS1g + 3 kt, whichever is
|
|
47
|
+
# greater (FAR-25.207 / CS-25.207 context, as in the stall leaf).
|
|
48
|
+
STALL_WARNING_MARGIN = 0.05
|
|
49
|
+
STALL_WARNING_KNOT_FLOOR = 3.0
|
|
50
|
+
|
|
51
|
+
# Flutter: required demonstration speed = 1.2 * V_D and minimum damping
|
|
52
|
+
# 0.03 at the maximum test speed (FAR-25.629 context, flutter-testing leaf).
|
|
53
|
+
FLUTTER_MARGIN_FACTOR = 1.2
|
|
54
|
+
MIN_DAMPING = 0.03
|
|
55
|
+
MIN_FREQ_SEPARATION = 0.10
|
|
56
|
+
|
|
57
|
+
# Load factor / safety: severity x likelihood risk index; >= 15 is high
|
|
58
|
+
# risk and must be mitigated before the flight (flight-test-safety leaf).
|
|
59
|
+
HIGH_RISK_INDEX = 15
|
|
60
|
+
|
|
61
|
+
# ISA constants (public physics) for the altitude/Mach grid.
|
|
62
|
+
GAMMA = 1.4
|
|
63
|
+
R_AIR = 287.05287 # J/(kg K)
|
|
64
|
+
T0_K = 288.15 # ISA sea level temperature
|
|
65
|
+
LAPSE_K_PER_M = 0.0065 # troposphere lapse
|
|
66
|
+
H_TROPO_M = 11000.0
|
|
67
|
+
ISA_EXP = 9.80665 / (R_AIR * LAPSE_K_PER_M) - 1.0 # 4.25588 (density)
|
|
68
|
+
|
|
69
|
+
MPS_TO_KTS = 1.0 / 0.514444
|
|
70
|
+
FT_PER_M = 3.280839895
|
|
71
|
+
|
|
72
|
+
# Default build-up speed blocks as fractions of the design limit V_D:
|
|
73
|
+
# equal increments from 0.85 V_D to 1.00 V_D (dive build-up convention
|
|
74
|
+
# in the FAR-25.629 context - clearance is progressive, point by point).
|
|
75
|
+
DEFAULT_SPEED_BLOCK_FRACTIONS = (0.85, 0.90, 0.95, 1.00)
|
|
76
|
+
|
|
77
|
+
# Load factor build-up for the maneuver envelope: fractions of n_max.
|
|
78
|
+
DEFAULT_LOAD_FACTOR_BLOCKS = (0.5, 0.75, 1.0)
|
|
79
|
+
|
|
80
|
+
# Planned test altitudes for the altitude/Mach grid, meters pressure alt.
|
|
81
|
+
DEFAULT_ALTITUDES_M = (0.0, 7620.0, 10668.0) # SL / FL250 / FL350
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Project facts
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class TestAircraft:
|
|
90
|
+
"""Aircraft project facts the role needs to plan and reduce the test."""
|
|
91
|
+
name: str
|
|
92
|
+
description: str = ""
|
|
93
|
+
gross_weight_kg: float = 0.0
|
|
94
|
+
n_max: float = 2.5 # positive limit load factor
|
|
95
|
+
vs_eas_ms: float = 0.0 # clean 1g stalling speed VS1g, m/s EAS
|
|
96
|
+
vs1_eas_ms: float = 0.0 # takeoff-config stalling speed, m/s EAS
|
|
97
|
+
vs0_eas_ms: float = 0.0 # landing-config stalling speed, m/s EAS
|
|
98
|
+
vfe_eas_ms: float = 0.0 # flaps-extended limit speed
|
|
99
|
+
vno_eas_ms: float = 0.0 # normal operating limit speed
|
|
100
|
+
vne_eas_ms: float = 0.0 # never-exceed speed
|
|
101
|
+
vd_eas_ms: float = 0.0 # design dive speed
|
|
102
|
+
mmo: float = 0.78 # maximum operating Mach
|
|
103
|
+
certification_basis: str = "FAR/CS-25 (context only)"
|
|
104
|
+
recovery_alt_loss_limit_m: float = 30.0
|
|
105
|
+
recovery_pitch_up_limit_deg: float = 8.0
|
|
106
|
+
recovery_roll_off_limit_deg: float = 20.0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Core computations (real rules, real numbers)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _require_positive(value, name):
|
|
114
|
+
if value <= 0:
|
|
115
|
+
raise ValueError("%s must be positive, got %r" % (name, value))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _require_ordered_placards(ac: TestAircraft) -> None:
|
|
119
|
+
"""Placard ordering 0 < vfe < va < vno < vne < vd (design context)."""
|
|
120
|
+
va = corner_speed(ac.vs_eas_ms, ac.n_max)
|
|
121
|
+
order = (ac.vfe_eas_ms, va, ac.vno_eas_ms, ac.vne_eas_ms, ac.vd_eas_ms)
|
|
122
|
+
if not (0 < order[0] < order[1] < order[2] < order[3] < order[4]):
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"placard order must be 0 < vfe < va < vno < vne < vd, got %r"
|
|
125
|
+
% (order,))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def corner_speed(vs_ms, n_max):
|
|
129
|
+
"""Maneuvering speed VA = VS * sqrt(n_max), speeds in m/s.
|
|
130
|
+
|
|
131
|
+
Raises ValueError when vs_ms <= 0 or n_max <= 1."""
|
|
132
|
+
if vs_ms <= 0:
|
|
133
|
+
raise ValueError("stall speed must be > 0, got %r" % (vs_ms,))
|
|
134
|
+
if n_max <= 1:
|
|
135
|
+
raise ValueError("limit load factor must be > 1, got %r" % (n_max,))
|
|
136
|
+
return vs_ms * math.sqrt(n_max)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def v_speeds(ac: TestAircraft) -> dict:
|
|
140
|
+
"""Assemble the validated V-speed set from the stall speeds (m/s EAS).
|
|
141
|
+
|
|
142
|
+
vref = 1.3*vs0, v2 = 1.2*vs1, vr = 1.1*vs1 plus VA = vs*sqrt(n_max)."""
|
|
143
|
+
for name, value in (("vs", ac.vs_eas_ms), ("vs1", ac.vs1_eas_ms),
|
|
144
|
+
("vs0", ac.vs0_eas_ms)):
|
|
145
|
+
_require_positive(value, name)
|
|
146
|
+
if not (ac.vs0_eas_ms <= ac.vs1_eas_ms <= ac.vs_eas_ms):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"stall speeds must satisfy vs0 <= vs1 <= vs (clean), got "
|
|
149
|
+
"vs0=%r vs1=%r vs=%r" % (ac.vs0_eas_ms, ac.vs1_eas_ms,
|
|
150
|
+
ac.vs_eas_ms))
|
|
151
|
+
va = corner_speed(ac.vs_eas_ms, ac.n_max)
|
|
152
|
+
return {
|
|
153
|
+
"vs": ac.vs_eas_ms,
|
|
154
|
+
"vs1": ac.vs1_eas_ms,
|
|
155
|
+
"vs0": ac.vs0_eas_ms,
|
|
156
|
+
"vref": VREF_FACTOR * ac.vs0_eas_ms,
|
|
157
|
+
"v2": V2_FACTOR * ac.vs1_eas_ms,
|
|
158
|
+
"vr": VR_FACTOR * ac.vs1_eas_ms,
|
|
159
|
+
"va": va,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def classify_airspeed(v_ms, vfe, va, vno, vne):
|
|
164
|
+
"""Classify a test speed into a flight test category.
|
|
165
|
+
|
|
166
|
+
Half-open boundaries: v < vfe -> below-vfe; vfe <= v < va ->
|
|
167
|
+
vfe-to-va; va <= v < vno -> va-to-vno; vno <= v < vne ->
|
|
168
|
+
vno-to-vne; v >= vne -> at-or-above-vne. Raises ValueError when
|
|
169
|
+
v_ms < 0 or the bound order 0 < vfe < va < vno < vne is violated."""
|
|
170
|
+
if v_ms < 0:
|
|
171
|
+
raise ValueError("airspeed must be >= 0, got %r" % (v_ms,))
|
|
172
|
+
if not (0 < vfe < va < vno < vne):
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"bound order must be 0 < vfe < va < vno < vne, got %r"
|
|
175
|
+
% ((vfe, va, vno, vne),))
|
|
176
|
+
if v_ms < vfe:
|
|
177
|
+
return "below-vfe"
|
|
178
|
+
if v_ms < va:
|
|
179
|
+
return "vfe-to-va"
|
|
180
|
+
if v_ms < vno:
|
|
181
|
+
return "va-to-vno"
|
|
182
|
+
if v_ms < vne:
|
|
183
|
+
return "vno-to-vne"
|
|
184
|
+
return "at-or-above-vne"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def speed_block_targets(limit_ms, fractions=DEFAULT_SPEED_BLOCK_FRACTIONS):
|
|
188
|
+
"""Build-up block speeds = fraction * limit (m/s EAS).
|
|
189
|
+
|
|
190
|
+
Validates equal fraction increments and that the top block reaches
|
|
191
|
+
the limit (>= 0.99*limit). Returns a list of block dicts."""
|
|
192
|
+
if len(fractions) < 2:
|
|
193
|
+
raise ValueError("need at least two block fractions")
|
|
194
|
+
steps = [round(b - a, 6) for a, b in zip(fractions, fractions[1:])]
|
|
195
|
+
if len(set(steps)) != 1 or steps[0] <= 0:
|
|
196
|
+
raise ValueError("fractions must increase in equal steps, got %r"
|
|
197
|
+
% (fractions,))
|
|
198
|
+
if fractions[-1] < 0.99:
|
|
199
|
+
raise ValueError("top block must reach the limit, got %r"
|
|
200
|
+
% (fractions[-1],))
|
|
201
|
+
blocks = []
|
|
202
|
+
for i, frac in enumerate(fractions, start=1):
|
|
203
|
+
blocks.append({
|
|
204
|
+
"id": "S%d" % i,
|
|
205
|
+
"fraction": frac,
|
|
206
|
+
"target_eas_ms": limit_ms * frac,
|
|
207
|
+
})
|
|
208
|
+
return blocks
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def flutter_required_speed(vd_ms, margin_factor=FLUTTER_MARGIN_FACTOR):
|
|
212
|
+
"""Required flutter demonstration speed V_F = margin_factor * V_D."""
|
|
213
|
+
_require_positive(vd_ms, "vd")
|
|
214
|
+
_require_positive(margin_factor, "margin_factor")
|
|
215
|
+
return margin_factor * vd_ms
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def damping_margin(damping_at_v_test, min_damping=MIN_DAMPING):
|
|
219
|
+
"""Damping margin verdict at the maximum test speed."""
|
|
220
|
+
_require_positive(min_damping, "min_damping")
|
|
221
|
+
return {"damping": damping_at_v_test, "min_required": min_damping,
|
|
222
|
+
"pass": damping_at_v_test >= min_damping}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def frequency_separation(f1, f2, min_frac=MIN_FREQ_SEPARATION):
|
|
226
|
+
"""Mode frequency separation |f1-f2| / mean(f1,f2) vs min_frac."""
|
|
227
|
+
_require_positive(f1, "f1")
|
|
228
|
+
_require_positive(f2, "f2")
|
|
229
|
+
_require_positive(min_frac, "min_frac")
|
|
230
|
+
separation = abs(f1 - f2) / ((f1 + f2) / 2.0)
|
|
231
|
+
return {"f1": f1, "f2": f2, "separation": separation,
|
|
232
|
+
"min_frac": min_frac, "pass": separation >= min_frac}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def flutter_speed_from_damping(speeds, dampings):
|
|
236
|
+
"""Extrapolated flutter speed from the damping vs test speed trend.
|
|
237
|
+
|
|
238
|
+
Least squares line damping = m*speed + b; V_F = -b/m at zero
|
|
239
|
+
crossing. Speeds strictly increasing, >= 2 points, decreasing
|
|
240
|
+
trend (slope < 0). Raises ValueError otherwise."""
|
|
241
|
+
n = len(speeds)
|
|
242
|
+
if n == 0 or len(dampings) == 0:
|
|
243
|
+
raise ValueError("speeds and dampings must not be empty")
|
|
244
|
+
if n != len(dampings):
|
|
245
|
+
raise ValueError("speeds and dampings lengths differ")
|
|
246
|
+
if n < 2:
|
|
247
|
+
raise ValueError("at least two damping points are required")
|
|
248
|
+
if any(speeds[i] >= speeds[i + 1] for i in range(n - 1)):
|
|
249
|
+
raise ValueError("speeds must be strictly increasing")
|
|
250
|
+
sx = sum(speeds)
|
|
251
|
+
sy = sum(dampings)
|
|
252
|
+
sxx = sum(x * x for x in speeds)
|
|
253
|
+
sxy = sum(x * y for x, y in zip(speeds, dampings))
|
|
254
|
+
denom = n * sxx - sx * sx
|
|
255
|
+
if denom == 0:
|
|
256
|
+
raise ValueError("degenerate speed set")
|
|
257
|
+
m = (n * sxy - sx * sy) / denom
|
|
258
|
+
b = (sy - m * sx) / n
|
|
259
|
+
if m >= 0:
|
|
260
|
+
raise ValueError("damping trend is not decreasing; no zero crossing")
|
|
261
|
+
return -b / m
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def flutter_margin_ratio(vf_measured, vd, required_ratio=FLUTTER_MARGIN_FACTOR):
|
|
265
|
+
"""Flutter margin verdict ratio = V_F_measured / V_D >= 1.2."""
|
|
266
|
+
_require_positive(vf_measured, "vf_measured")
|
|
267
|
+
_require_positive(vd, "vd")
|
|
268
|
+
_require_positive(required_ratio, "required_ratio")
|
|
269
|
+
ratio = vf_measured / vd
|
|
270
|
+
return {"ratio": ratio, "required_ratio": required_ratio,
|
|
271
|
+
"pass": ratio >= required_ratio}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def stall_warning_required(vs1g_ms):
|
|
275
|
+
"""Warning onset speed required: 1.05*VS1g or VS1g + 3 kt, greater."""
|
|
276
|
+
_require_positive(vs1g_ms, "vs1g")
|
|
277
|
+
knot_floor_ms = STALL_WARNING_KNOT_FLOOR * 0.514444
|
|
278
|
+
return max(vs1g_ms * (1.0 + STALL_WARNING_MARGIN), vs1g_ms + knot_floor_ms)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def stall_warning_verdict(vs1g_ms, observed_warning_ms):
|
|
282
|
+
"""Verdict that the observed stall warning onset meets the margin."""
|
|
283
|
+
required = stall_warning_required(vs1g_ms)
|
|
284
|
+
ok = observed_warning_ms >= required
|
|
285
|
+
return {"required_ms": required, "observed_ms": observed_warning_ms,
|
|
286
|
+
"ok": ok}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def accelerated_stall_speed(vs1g_ms, load_factor):
|
|
290
|
+
"""Stall speed at an elevated entry load factor V = vs1g*sqrt(n)."""
|
|
291
|
+
_require_positive(vs1g_ms, "vs1g")
|
|
292
|
+
if load_factor < 1:
|
|
293
|
+
raise ValueError("entry load factor must be >= 1")
|
|
294
|
+
return vs1g_ms * math.sqrt(load_factor)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def level_turn_load_factor(bank_deg):
|
|
298
|
+
"""Load factor n = 1/cos(bank) of a coordinated level turn."""
|
|
299
|
+
if abs(bank_deg) >= 90.0:
|
|
300
|
+
raise ValueError("bank must be strictly between -90 and 90 deg")
|
|
301
|
+
return 1.0 / math.cos(math.radians(bank_deg))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def stall_recovery_verdict(ac: TestAircraft, altitude_loss_m, pitch_up_deg,
|
|
305
|
+
roll_off_deg):
|
|
306
|
+
"""Recovery characteristics verdict against the program limits."""
|
|
307
|
+
if altitude_loss_m < 0 or pitch_up_deg < 0 or roll_off_deg < 0:
|
|
308
|
+
raise ValueError("observed recovery values must be >= 0")
|
|
309
|
+
alt_ok = altitude_loss_m <= ac.recovery_alt_loss_limit_m
|
|
310
|
+
pitch_ok = pitch_up_deg <= ac.recovery_pitch_up_limit_deg
|
|
311
|
+
roll_ok = roll_off_deg <= ac.recovery_roll_off_limit_deg
|
|
312
|
+
return {"altitude_loss_ok": alt_ok, "pitch_up_ok": pitch_ok,
|
|
313
|
+
"roll_off_ok": roll_ok,
|
|
314
|
+
"ok": alt_ok and pitch_ok and roll_ok}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def risk_index(severity, likelihood):
|
|
318
|
+
"""Risk index = severity * likelihood (both 1..5, ints)."""
|
|
319
|
+
if not (1 <= severity <= 5 and 1 <= likelihood <= 5):
|
|
320
|
+
raise ValueError("severity and likelihood must be in 1..5")
|
|
321
|
+
return severity * likelihood
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def load_factor_within_limit(n, n_max):
|
|
325
|
+
"""True when the applied load factor is within the positive limit."""
|
|
326
|
+
if n < 0:
|
|
327
|
+
raise ValueError("load factor must be >= 0")
|
|
328
|
+
_require_positive(n_max, "n_max")
|
|
329
|
+
return n <= n_max
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
# ISA atmosphere for the altitude / Mach grid (public physics)
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
def isa_temperature_k(h_m):
|
|
337
|
+
"""ISA static temperature at geopotential altitude h_m."""
|
|
338
|
+
if h_m < 0 or h_m > 20000.0:
|
|
339
|
+
raise ValueError("altitude out of ISA model range")
|
|
340
|
+
if h_m <= H_TROPO_M:
|
|
341
|
+
return T0_K - LAPSE_K_PER_M * h_m
|
|
342
|
+
return T0_K - LAPSE_K_PER_M * H_TROPO_M
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def isa_density_ratio(h_m):
|
|
346
|
+
"""ISA density ratio sigma at geopotential altitude h_m."""
|
|
347
|
+
if h_m <= H_TROPO_M:
|
|
348
|
+
return (1.0 - LAPSE_K_PER_M * h_m / T0_K) ** ISA_EXP
|
|
349
|
+
sigma_t = (1.0 - LAPSE_K_PER_M * H_TROPO_M / T0_K) ** ISA_EXP
|
|
350
|
+
t_tropo = isa_temperature_k(H_TROPO_M)
|
|
351
|
+
h_iso = R_AIR * t_tropo / 9.80665
|
|
352
|
+
return sigma_t * math.exp(-(h_m - H_TROPO_M) / h_iso)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def mach_grid_row(ac: TestAircraft, h_m, eas_ms):
|
|
356
|
+
"""Reduce a test EAS at altitude to TAS and Mach (ISA).
|
|
357
|
+
|
|
358
|
+
Returns dict with sigma, temperature, speed of sound, TAS, Mach,
|
|
359
|
+
and the limiting factor against MMO."""
|
|
360
|
+
if h_m < 0:
|
|
361
|
+
raise ValueError("altitude must be >= 0")
|
|
362
|
+
_require_positive(eas_ms, "eas_ms")
|
|
363
|
+
sigma = isa_density_ratio(h_m)
|
|
364
|
+
t_k = isa_temperature_k(h_m)
|
|
365
|
+
a_ms = math.sqrt(GAMMA * R_AIR * t_k)
|
|
366
|
+
tas_ms = eas_ms / math.sqrt(sigma)
|
|
367
|
+
mach = tas_ms / a_ms
|
|
368
|
+
return {"h_m": h_m, "h_ft": h_m * FT_PER_M, "sigma": sigma,
|
|
369
|
+
"t_k": t_k, "a_ms": a_ms, "tas_ms": tas_ms, "mach": mach,
|
|
370
|
+
"mmo": ac.mmo,
|
|
371
|
+
"limiting": ("mach-limited" if mach >= ac.mmo else "eas-limited")}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Deliverable builder: plan + envelope expansion report content model
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
def build_flight_test_deliverable(ac: TestAircraft, data: dict | None = None) -> dict:
|
|
379
|
+
"""Build the flight test plan + envelope report content model.
|
|
380
|
+
|
|
381
|
+
data is the flight-recorded (or worked-example simulated) data dict;
|
|
382
|
+
when None the example simulated data is used. The model carries every
|
|
383
|
+
computed number and verdict; nothing is asserted without a check."""
|
|
384
|
+
if data is None:
|
|
385
|
+
data = example_flight_data()
|
|
386
|
+
speeds = v_speeds(ac)
|
|
387
|
+
_require_ordered_placards(ac)
|
|
388
|
+
va = speeds["va"]
|
|
389
|
+
|
|
390
|
+
# --- Build-up speed blocks (fractions of V_D) ---------------------------
|
|
391
|
+
speed_blocks = speed_block_targets(ac.vd_eas_ms)
|
|
392
|
+
for blk in speed_blocks:
|
|
393
|
+
blk["category"] = classify_airspeed(
|
|
394
|
+
blk["target_eas_ms"], ac.vfe_eas_ms, va, ac.vno_eas_ms,
|
|
395
|
+
ac.vne_eas_ms)
|
|
396
|
+
blk["abort"] = ("immediate pull-up + throttle reduction if buffet, "
|
|
397
|
+
"LCO, or control anomaly")
|
|
398
|
+
blk["criteria"] = ("PASS: damping >= 0.03 at point, no LCO, load "
|
|
399
|
+
"factor within %.1f g; FAIL otherwise" % ac.n_max)
|
|
400
|
+
|
|
401
|
+
# --- Flutter build-up / point progression -------------------------------
|
|
402
|
+
flutter_pts = data["flutter_points"]
|
|
403
|
+
if len(flutter_pts["speeds"]) != len(speed_blocks):
|
|
404
|
+
raise ValueError("flutter data must cover every speed block")
|
|
405
|
+
vf_req = flutter_required_speed(ac.vd_eas_ms)
|
|
406
|
+
damp_verdicts = []
|
|
407
|
+
for blk, dmp in zip(speed_blocks, flutter_pts["dampings"]):
|
|
408
|
+
damp_verdicts.append(damping_margin(dmp))
|
|
409
|
+
vf_meas = flutter_speed_from_damping(list(flutter_pts["speeds"]),
|
|
410
|
+
list(flutter_pts["dampings"]))
|
|
411
|
+
margin = flutter_margin_ratio(vf_meas, ac.vd_eas_ms)
|
|
412
|
+
mode_pairs = [frequency_separation(f1, f2) for f1, f2 in
|
|
413
|
+
data["gvt_modes"]]
|
|
414
|
+
flutter = {
|
|
415
|
+
"required_speed_ms": vf_req,
|
|
416
|
+
"points": [{"block": blk["id"], "speed_ms": sp, "damping": dmp,
|
|
417
|
+
"damping_pass": dv["pass"]}
|
|
418
|
+
for blk, sp, dmp, dv in zip(speed_blocks,
|
|
419
|
+
flutter_pts["speeds"],
|
|
420
|
+
flutter_pts["dampings"],
|
|
421
|
+
damp_verdicts)],
|
|
422
|
+
"vf_extrapolated_ms": vf_meas,
|
|
423
|
+
"margin": margin,
|
|
424
|
+
"mode_pairs": mode_pairs,
|
|
425
|
+
"min_separation": min(p["separation"] for p in mode_pairs),
|
|
426
|
+
"pass": margin["pass"] and all(dv["pass"] for dv in damp_verdicts)
|
|
427
|
+
and min(p["separation"] for p in mode_pairs)
|
|
428
|
+
>= MIN_FREQ_SEPARATION,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# --- Altitude / Mach grid ------------------------------------------------
|
|
432
|
+
mach_grid = [mach_grid_row(ac, h, ac.vd_eas_ms)
|
|
433
|
+
for h in DEFAULT_ALTITUDES_M]
|
|
434
|
+
|
|
435
|
+
# --- Stall / VS1g matrix --------------------------------------------------
|
|
436
|
+
stall_rows = []
|
|
437
|
+
stall_all_ok = True
|
|
438
|
+
for row in data["stall_rows"]:
|
|
439
|
+
req = stall_warning_required(row["vs_measured"])
|
|
440
|
+
wv = stall_warning_verdict(row["vs_measured"], row["warning_onset"])
|
|
441
|
+
rec = None
|
|
442
|
+
if row.get("recovery"):
|
|
443
|
+
rec = stall_recovery_verdict(ac, row["recovery"]["alt_loss_m"],
|
|
444
|
+
row["recovery"]["pitch_up_deg"],
|
|
445
|
+
row["recovery"]["roll_off_deg"])
|
|
446
|
+
rec["altitude_loss_m"] = row["recovery"]["alt_loss_m"]
|
|
447
|
+
rec["pitch_up_deg"] = row["recovery"]["pitch_up_deg"]
|
|
448
|
+
rec["roll_off_deg"] = row["recovery"]["roll_off_deg"]
|
|
449
|
+
row_ok = wv["ok"] and rec["ok"]
|
|
450
|
+
else:
|
|
451
|
+
row_ok = wv["ok"]
|
|
452
|
+
stall_all_ok = stall_all_ok and row_ok
|
|
453
|
+
stall_rows.append({
|
|
454
|
+
"config": row["config"],
|
|
455
|
+
"vs1g_measured": row["vs_measured"],
|
|
456
|
+
"warning_required": req,
|
|
457
|
+
"warning_onset": row["warning_onset"],
|
|
458
|
+
"warning_ok": wv["ok"],
|
|
459
|
+
"recovery": rec,
|
|
460
|
+
"ok": row_ok,
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
# --- Envelope / load factor blocks ----------------------------------------
|
|
464
|
+
n_blocks = []
|
|
465
|
+
for i, frac in enumerate(DEFAULT_LOAD_FACTOR_BLOCKS, start=1):
|
|
466
|
+
target_n = frac * ac.n_max
|
|
467
|
+
achieved = data["maneuver_n"][i - 1]
|
|
468
|
+
n_blocks.append({
|
|
469
|
+
"id": "L%d" % i, "fraction": frac, "target_n": target_n,
|
|
470
|
+
"achieved_n": achieved, "ok": load_factor_within_limit(
|
|
471
|
+
achieved, ac.n_max),
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
# --- Climb performance check ----------------------------------------------
|
|
475
|
+
climb = {
|
|
476
|
+
"roc_fpm": data["climb_roc_fpm"],
|
|
477
|
+
"gradient_pct": data["climb_gradient_pct"],
|
|
478
|
+
"required_gradient_pct": 2.4, # FAR-25.121 context, two-engine
|
|
479
|
+
"margin_pct": data["climb_gradient_pct"] - 2.4,
|
|
480
|
+
"pass": data["climb_gradient_pct"] >= 2.4,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# --- Safety register --------------------------------------------------------
|
|
484
|
+
safety_rows = []
|
|
485
|
+
for r in data["risk_rows"]:
|
|
486
|
+
idx = risk_index(r["severity"], r["likelihood"])
|
|
487
|
+
safety_rows.append({
|
|
488
|
+
"hazard": r["hazard"],
|
|
489
|
+
"severity": r["severity"],
|
|
490
|
+
"likelihood": r["likelihood"],
|
|
491
|
+
"index": idx,
|
|
492
|
+
"high": idx >= HIGH_RISK_INDEX,
|
|
493
|
+
"mitigation": r["mitigation"],
|
|
494
|
+
})
|
|
495
|
+
go_no_go = {"checks": data["go_no_go"],
|
|
496
|
+
"go": all(data["go_no_go"].values())}
|
|
497
|
+
|
|
498
|
+
# --- Limit changes traced to data points -------------------------------------
|
|
499
|
+
limit_changes = data["limit_changes"]
|
|
500
|
+
|
|
501
|
+
objectives = [
|
|
502
|
+
"Determine VS1g and the V-speed set (vref/v2/vr/VA) with "
|
|
503
|
+
"warning-margin evidence in every configuration",
|
|
504
|
+
"Demonstrate freedom from flutter to V_D with damping >= 0.03 at "
|
|
505
|
+
"the maximum test speed and an extrapolated margin >= 1.2*V_D",
|
|
506
|
+
"Expand the speed and load factor envelope in build-up blocks to "
|
|
507
|
+
"V_D / n_max with a load factor check at every point",
|
|
508
|
+
"Verify stall recovery characteristics within the program limits",
|
|
509
|
+
"Measure climb performance and check the gradient requirement",
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
model = {
|
|
513
|
+
"document_type": "Flight Test Plan and Envelope Expansion Report",
|
|
514
|
+
"status": "draft-for-review",
|
|
515
|
+
"aircraft": ac.name,
|
|
516
|
+
"aircraft_description": ac.description,
|
|
517
|
+
"certification_basis": ac.certification_basis,
|
|
518
|
+
"generated": _today(),
|
|
519
|
+
"objectives": objectives,
|
|
520
|
+
"v_speeds": speeds,
|
|
521
|
+
"n_max": ac.n_max,
|
|
522
|
+
"placards": {"vfe": ac.vfe_eas_ms, "vno": ac.vno_eas_ms,
|
|
523
|
+
"vne": ac.vne_eas_ms, "vd": ac.vd_eas_ms,
|
|
524
|
+
"mmo": ac.mmo},
|
|
525
|
+
"recovery_limits": {"alt_loss_m": ac.recovery_alt_loss_limit_m,
|
|
526
|
+
"pitch_up_deg": ac.recovery_pitch_up_limit_deg,
|
|
527
|
+
"roll_off_deg": ac.recovery_roll_off_limit_deg},
|
|
528
|
+
"speed_blocks": speed_blocks,
|
|
529
|
+
"flutter": flutter,
|
|
530
|
+
"mach_grid": mach_grid,
|
|
531
|
+
"stall_rows": stall_rows,
|
|
532
|
+
"stall_all_ok": stall_all_ok,
|
|
533
|
+
"n_blocks": n_blocks,
|
|
534
|
+
"climb": climb,
|
|
535
|
+
"safety_rows": safety_rows,
|
|
536
|
+
"safety_no_unmitigated": not any(r["high"] for r in safety_rows),
|
|
537
|
+
"go_no_go": go_no_go,
|
|
538
|
+
"limit_changes": limit_changes,
|
|
539
|
+
"example_data_note": data.get("note", ""),
|
|
540
|
+
}
|
|
541
|
+
return model
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# Renderer: the actual deliverable markdown document
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
def _fmt_ms(v):
|
|
549
|
+
return "%.1f" % v
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _kt(v):
|
|
553
|
+
return "%.0f" % (v * MPS_TO_KTS)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _verdict(ok):
|
|
557
|
+
return "PASS" if ok else "FAIL"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def render_flight_test_markdown(model: dict) -> str:
|
|
561
|
+
"""Render the content model as the deliverable markdown document."""
|
|
562
|
+
vs = model["v_speeds"]
|
|
563
|
+
pl = model["placards"]
|
|
564
|
+
L = []
|
|
565
|
+
L += [
|
|
566
|
+
"# Flight Test Plan and Envelope Expansion Report",
|
|
567
|
+
"",
|
|
568
|
+
f"**Aircraft:** {model['aircraft']}",
|
|
569
|
+
f"**Basis:** {model['certification_basis']}",
|
|
570
|
+
f"**Status:** {model['status']}",
|
|
571
|
+
"",
|
|
572
|
+
]
|
|
573
|
+
if model.get("example_data_note"):
|
|
574
|
+
L += ["_%s_" % model["example_data_note"], ""]
|
|
575
|
+
|
|
576
|
+
# 1. Objectives and configuration
|
|
577
|
+
L += ["## 1. Objectives and configuration", "",
|
|
578
|
+
*[f"- {o}" for o in model["objectives"]], "",
|
|
579
|
+
f"- Limit load factor: n_max = {model['n_max']:.1f} g positive. "
|
|
580
|
+
f"Stall speeds (EAS): VS1g = {_fmt_ms(vs['vs'])} m/s "
|
|
581
|
+
f"({_kt(vs['vs'])} kt), VS1 = {_fmt_ms(vs['vs1'])} m/s "
|
|
582
|
+
f"({_kt(vs['vs1'])} kt), VS0 = {_fmt_ms(vs['vs0'])} m/s "
|
|
583
|
+
f"({_kt(vs['vs0'])} kt).",
|
|
584
|
+
f"- Placards (EAS): VFE = {_fmt_ms(pl['vfe'])} m/s, "
|
|
585
|
+
f"VNO = {_fmt_ms(pl['vno'])} m/s, VNE = {_fmt_ms(pl['vne'])} m/s, "
|
|
586
|
+
f"V_D = {_fmt_ms(pl['vd'])} m/s, MMO = {pl['mmo']:.2f}.",
|
|
587
|
+
f"- Derived: VA = {_fmt_ms(vs['va'])} m/s "
|
|
588
|
+
f"({_kt(vs['va'])} kt), vref = {_fmt_ms(vs['vref'])} m/s "
|
|
589
|
+
f"({_kt(vs['vref'])} kt), v2 = {_fmt_ms(vs['v2'])} m/s "
|
|
590
|
+
f"({_kt(vs['v2'])} kt), vr = {_fmt_ms(vs['vr'])} m/s "
|
|
591
|
+
f"({_kt(vs['vr'])} kt).",
|
|
592
|
+
f"- Crew: test pilot + flight test engineer console; "
|
|
593
|
+
f"airspace: restricted area; instrumentation: calibrated "
|
|
594
|
+
f"pitot-static, accelerometers, strain gauges on wing/elevator, "
|
|
595
|
+
f"flutter excitation vanes, telemetry with real-time damping.",
|
|
596
|
+
"",
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
# 2. Safety
|
|
600
|
+
L += ["## 2. Safety", "",
|
|
601
|
+
"| Hazard | Sev | Lik | Risk | High (>=15) | Mitigation |",
|
|
602
|
+
"|---|---|---|---|---|---|"]
|
|
603
|
+
for r in model["safety_rows"]:
|
|
604
|
+
L.append("| %s | %d | %d | %d | %s | %s |" % (
|
|
605
|
+
r["hazard"], r["severity"], r["likelihood"], r["index"],
|
|
606
|
+
"yes" if r["high"] else "no", r["mitigation"]))
|
|
607
|
+
unmit = "true" if model["safety_no_unmitigated"] else "FALSE - blocked"
|
|
608
|
+
L += ["", "Every high-risk item is mitigated before the flight: %s."
|
|
609
|
+
% unmit,
|
|
610
|
+
"",
|
|
611
|
+
"| Go/no-go criterion | Value |",
|
|
612
|
+
"|---|---|"]
|
|
613
|
+
for name, ok in model["go_no_go"]["checks"].items():
|
|
614
|
+
L.append("| %s | %s |" % (name, "GO" if ok else "NO-GO"))
|
|
615
|
+
L += ["", "Flight release: %s." % (
|
|
616
|
+
"GO" if model["go_no_go"]["go"] else "NO-GO - blocked"), ""]
|
|
617
|
+
|
|
618
|
+
# 3. Build-up plan
|
|
619
|
+
L += ["## 3. Build-up plan", "",
|
|
620
|
+
"Build-up order: GVT -> flutter build-up (blocks S1..S4) -> "
|
|
621
|
+
"stall matrix -> load factor envelope -> climb performance. "
|
|
622
|
+
"Each block clears before the next is flown; every block states "
|
|
623
|
+
"its limit, abort criteria, and PASS/FAIL criteria.",
|
|
624
|
+
"",
|
|
625
|
+
"### 3.1 Flutter build-up (speed blocks as fractions of V_D)", ""]
|
|
626
|
+
L += ["| Block | V_D fraction | Target (EAS) | Category | Abort | "
|
|
627
|
+
"PASS/FAIL criteria |",
|
|
628
|
+
"|---|---|---|---|---|---|"]
|
|
629
|
+
for blk in model["speed_blocks"]:
|
|
630
|
+
L.append("| %s | %.2f | %s m/s (%s kt) | %s | %s | %s |" % (
|
|
631
|
+
blk["id"], blk["fraction"], _fmt_ms(blk["target_eas_ms"]),
|
|
632
|
+
_kt(blk["target_eas_ms"]), blk["category"], blk["abort"],
|
|
633
|
+
blk["criteria"]))
|
|
634
|
+
fl = model["flutter"]
|
|
635
|
+
L += ["",
|
|
636
|
+
f"Required flutter demonstration speed: V_F = "
|
|
637
|
+
f"{_fmt_ms(fl['required_speed_ms'])} m/s "
|
|
638
|
+
f"({FLUTTER_MARGIN_FACTOR:.1f} x V_D). Damping >= 0.03 required "
|
|
639
|
+
f"at the maximum test speed; structural mode frequency "
|
|
640
|
+
f"separation >= {MIN_FREQ_SEPARATION:.2f} at every point.",
|
|
641
|
+
"",
|
|
642
|
+
"### 3.2 Altitude / Mach grid (V_D tested per altitude, ISA)", ""]
|
|
643
|
+
L += ["| Altitude | Density ratio | Speed of sound | TAS at V_D | "
|
|
644
|
+
"Mach at V_D | MMO | Limit |",
|
|
645
|
+
"|---|---|---|---|---|---|---|"]
|
|
646
|
+
for g in model["mach_grid"]:
|
|
647
|
+
L.append("| %d ft | %.3f | %.1f m/s | %.1f m/s | %.3f | %.2f | %s |"
|
|
648
|
+
% (round(g["h_ft"]), g["sigma"], g["a_ms"], g["tas_ms"],
|
|
649
|
+
g["mach"], g["mmo"], g["limiting"]))
|
|
650
|
+
L += ["",
|
|
651
|
+
"### 3.3 Stall matrix (VS1g and warning onset per configuration)",
|
|
652
|
+
""]
|
|
653
|
+
L += ["| Config | Measured VS1g (EAS) | Warning required | Warning "
|
|
654
|
+
"onset | Recovery limits |",
|
|
655
|
+
"|---|---|---|---|---|"]
|
|
656
|
+
for row in model["stall_rows"]:
|
|
657
|
+
L.append("| %s | %s m/s | %s m/s | %s m/s | alt <= %.0f m, pitch "
|
|
658
|
+
"<= %.0f deg, roll <= %.0f deg |" % (
|
|
659
|
+
row["config"], _fmt_ms(row["vs1g_measured"]),
|
|
660
|
+
_fmt_ms(row["warning_required"]),
|
|
661
|
+
_fmt_ms(row["warning_onset"]),
|
|
662
|
+
model["recovery_limits"]["alt_loss_m"],
|
|
663
|
+
model["recovery_limits"]["pitch_up_deg"],
|
|
664
|
+
model["recovery_limits"]["roll_off_deg"]))
|
|
665
|
+
L += ["",
|
|
666
|
+
"### 3.4 Load factor envelope (pull blocks at VA or above)", ""]
|
|
667
|
+
L += ["| Block | n_max fraction | Target n | PASS/FAIL |",
|
|
668
|
+
"|---|---|---|---|"]
|
|
669
|
+
for b in model["n_blocks"]:
|
|
670
|
+
L.append("| %s | %.2f | %.2f g | PASS if n <= %.1f g at point, "
|
|
671
|
+
"with no buffet/LCO |" % (b["id"], b["fraction"],
|
|
672
|
+
b["target_n"], model["n_max"]))
|
|
673
|
+
L += ["",
|
|
674
|
+
"### 3.5 Climb performance",
|
|
675
|
+
"",
|
|
676
|
+
"Sawtooth climb segments at constant indicated airspeed; "
|
|
677
|
+
f"gradient check against {model['climb']['required_gradient_pct']}"
|
|
678
|
+
" % (two-engine context).",
|
|
679
|
+
"",
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
# 4. Results
|
|
683
|
+
L += ["## 4. Results", "", "### 4.1 Flutter build-up points", ""]
|
|
684
|
+
L += ["| Block | Speed (EAS) | Damping | Min damping | Verdict |",
|
|
685
|
+
"|---|---|---|---|---|"]
|
|
686
|
+
for p in fl["points"]:
|
|
687
|
+
L.append("| %s | %s m/s | %.3f | %.3f | %s |" % (
|
|
688
|
+
p["block"], _fmt_ms(p["speed_ms"]), p["damping"],
|
|
689
|
+
MIN_DAMPING, _verdict(p["damping_pass"])))
|
|
690
|
+
L += ["",
|
|
691
|
+
f"Extrapolated flutter speed from the damping trend: "
|
|
692
|
+
f"{_fmt_ms(fl['vf_extrapolated_ms'])} m/s; margin ratio "
|
|
693
|
+
f"{fl['margin']['ratio']:.2f} vs required "
|
|
694
|
+
f"{fl['margin']['required_ratio']:.1f} -> "
|
|
695
|
+
f"{_verdict(fl['margin']['pass'])}.",
|
|
696
|
+
"",
|
|
697
|
+
"Structural mode frequency separation (GVT pairs):"]
|
|
698
|
+
for p in fl["mode_pairs"]:
|
|
699
|
+
L.append(f"- mode pair {p['f1']:.1f} Hz / {p['f2']:.1f} Hz: "
|
|
700
|
+
f"separation {p['separation']:.2f} >= "
|
|
701
|
+
f"{p['min_frac']:.2f} -> {_verdict(p['pass'])}")
|
|
702
|
+
L += ["", "### 4.2 Stall results", ""]
|
|
703
|
+
L += ["| Config | VS1g (EAS) | Warning onset | Warning margin | "
|
|
704
|
+
"Recovery | Verdict |",
|
|
705
|
+
"|---|---|---|---|---|---|"]
|
|
706
|
+
for row in model["stall_rows"]:
|
|
707
|
+
if row["recovery"]:
|
|
708
|
+
rec = "alt %.0f m / pitch %.0f deg / roll %.0f deg" % (
|
|
709
|
+
row["recovery"]["altitude_loss_m"],
|
|
710
|
+
row["recovery"]["pitch_up_deg"],
|
|
711
|
+
row["recovery"]["roll_off_deg"])
|
|
712
|
+
else:
|
|
713
|
+
rec = "-"
|
|
714
|
+
L.append("| %s | %s m/s | %s m/s | %+.2f m/s | %s | %s |" % (
|
|
715
|
+
row["config"], _fmt_ms(row["vs1g_measured"]),
|
|
716
|
+
_fmt_ms(row["warning_onset"]),
|
|
717
|
+
row["warning_onset"] - row["vs1g_measured"], rec,
|
|
718
|
+
_verdict(row["ok"])))
|
|
719
|
+
L += ["", "### 4.3 Load factor envelope results", ""]
|
|
720
|
+
L += ["| Block | Target n | Achieved n | Limit | Verdict |",
|
|
721
|
+
"|---|---|---|---|---|"]
|
|
722
|
+
for b in model["n_blocks"]:
|
|
723
|
+
L.append("| %s | %.2f g | %.2f g | %.1f g | %s |" % (
|
|
724
|
+
b["id"], b["target_n"], b["achieved_n"], model["n_max"],
|
|
725
|
+
_verdict(b["ok"])))
|
|
726
|
+
L += ["", "### 4.4 Climb performance results", "",
|
|
727
|
+
f"- Rate of climb: {model['climb']['roc_fpm']:.0f} ft/min "
|
|
728
|
+
f"(segment reduction, corrected to reference weight).",
|
|
729
|
+
f"- Gradient: {model['climb']['gradient_pct']:.2f} % vs "
|
|
730
|
+
f"{model['climb']['required_gradient_pct']:.1f} % required -> "
|
|
731
|
+
f"margin {model['climb']['margin_pct']:+.2f} % "
|
|
732
|
+
f"({_verdict(model['climb']['pass'])}).",
|
|
733
|
+
"",
|
|
734
|
+
]
|
|
735
|
+
|
|
736
|
+
# 5. Limit changes and findings
|
|
737
|
+
L += ["## 5. Findings, limit changes, and recommendations", ""]
|
|
738
|
+
L += ["| Envelope limit change | Traced to data point | Verdict |",
|
|
739
|
+
"|---|---|---|"]
|
|
740
|
+
for ch in model["limit_changes"]:
|
|
741
|
+
L.append("| %s | %s | %s |" % (ch["limit"], ch["data_point"],
|
|
742
|
+
_verdict(ch["ok"])))
|
|
743
|
+
L += ["",
|
|
744
|
+
"Recommendations: (1) expand to the top block only after the "
|
|
745
|
+
"damping and separation verdicts above; (2) repeat the "
|
|
746
|
+
"configuration whose stall warning onset is marginal before "
|
|
747
|
+
"envelope use; (3) confirm flutter clearance per point with the "
|
|
748
|
+
"flight test conductor.",
|
|
749
|
+
"",
|
|
750
|
+
]
|
|
751
|
+
|
|
752
|
+
# 6. Sign-off
|
|
753
|
+
L += ["## 6. Sign-off", "",
|
|
754
|
+
"This plan/report states that the recorded data support "
|
|
755
|
+
"expanding to the listed limits PENDING human flight test "
|
|
756
|
+
"conductor review. It is a draft deliverable, not a clearance.",
|
|
757
|
+
"",
|
|
758
|
+
"---",
|
|
759
|
+
f"*Generated by Aero Agent Roles flight-test-engineer core "
|
|
760
|
+
f"({model['generated']}). DRAFT for human flight test conductor "
|
|
761
|
+
f"review. Not a clearance and not an approval of the envelope "
|
|
762
|
+
f"or of flight.*",
|
|
763
|
+
]
|
|
764
|
+
return "\n".join(L)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# ---------------------------------------------------------------------------
|
|
768
|
+
# Evidence gate checkers
|
|
769
|
+
# ---------------------------------------------------------------------------
|
|
770
|
+
|
|
771
|
+
GATE_CHECKS = {
|
|
772
|
+
"objectives_stated": "test objectives are present",
|
|
773
|
+
"blocks_buildup": "speed blocks rise to >= 0.99 of V_D in equal steps",
|
|
774
|
+
"limits_stated": "VNO/VNE/V_D/n_max and flutter margin numbers present",
|
|
775
|
+
"criteria_stated": "every block carries PASS/FAIL criteria",
|
|
776
|
+
"flutter_damping_gate": "damping >= 0.03 verdict present at top speed",
|
|
777
|
+
"sign_off_honest": "deliverable is draft, not a clearance",
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def check_flight_test_deliverable(model: dict) -> dict:
|
|
782
|
+
"""Run the evidence gates against the content model."""
|
|
783
|
+
blocks = model.get("speed_blocks", [])
|
|
784
|
+
fl = model.get("flutter", {})
|
|
785
|
+
results = {
|
|
786
|
+
"objectives_stated": bool(model.get("objectives")),
|
|
787
|
+
"blocks_buildup": bool(blocks) and blocks[-1]["fraction"] >= 0.99,
|
|
788
|
+
"limits_stated": ("vd" in model.get("placards", {})
|
|
789
|
+
and model.get("n_max") is not None),
|
|
790
|
+
"criteria_stated": all(b.get("criteria") for b in blocks),
|
|
791
|
+
"flutter_damping_gate": bool(fl.get("points")) and all(
|
|
792
|
+
p["damping_pass"] for p in fl.get("points", [])),
|
|
793
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
794
|
+
}
|
|
795
|
+
results["all_pass"] = all(results.values())
|
|
796
|
+
return results
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def check_flight_test_markdown(md_text: str) -> dict:
|
|
800
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
801
|
+
low = md_text.lower()
|
|
802
|
+
checks = {
|
|
803
|
+
"has_title": ("flight test plan" in low
|
|
804
|
+
and "envelope expansion report" in low),
|
|
805
|
+
"has_limits": bool(re.search(r"vne|v_d|vno", low)),
|
|
806
|
+
"has_blocks": bool(re.search(r"\bs[1-4]\b|\bs%d\b", low))
|
|
807
|
+
or "0.85" in low,
|
|
808
|
+
"has_pass_fail": "pass" in low and "fail" in low,
|
|
809
|
+
"has_draft_marker": "draft" in low,
|
|
810
|
+
"has_not_clearance": "not a clearance" in low,
|
|
811
|
+
"has_numbers": bool(re.search(r"\d+\.\d+", low)),
|
|
812
|
+
"no_blank_blanks": "___" not in md_text,
|
|
813
|
+
}
|
|
814
|
+
checks["all_pass"] = all(checks.values())
|
|
815
|
+
return checks
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
819
|
+
"""Public entry point used by gate tooling: check a plan/report doc."""
|
|
820
|
+
return check_flight_test_markdown(md_text)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ---------------------------------------------------------------------------
|
|
824
|
+
# Example aircraft and simulated flight data (worked example for tests)
|
|
825
|
+
# ---------------------------------------------------------------------------
|
|
826
|
+
|
|
827
|
+
def example_aircraft() -> TestAircraft:
|
|
828
|
+
"""The worked-example reference aircraft (a synthetic light jet)."""
|
|
829
|
+
return TestAircraft(
|
|
830
|
+
name="FT-1 Worked-Example Light Jet",
|
|
831
|
+
description="Synthetic light jet used to demonstrate the flight "
|
|
832
|
+
"test engineer deliverable. Numbers are for planning "
|
|
833
|
+
"practice only, not a real vehicle.",
|
|
834
|
+
gross_weight_kg=5900.0,
|
|
835
|
+
n_max=2.5,
|
|
836
|
+
vs_eas_ms=44.0,
|
|
837
|
+
vs1_eas_ms=40.0,
|
|
838
|
+
vs0_eas_ms=36.0,
|
|
839
|
+
vfe_eas_ms=62.0,
|
|
840
|
+
vno_eas_ms=85.0,
|
|
841
|
+
vne_eas_ms=94.0,
|
|
842
|
+
vd_eas_ms=106.0,
|
|
843
|
+
mmo=0.78,
|
|
844
|
+
certification_basis="FAR/CS-25 context (worked example)",
|
|
845
|
+
recovery_alt_loss_limit_m=30.0,
|
|
846
|
+
recovery_pitch_up_limit_deg=8.0,
|
|
847
|
+
recovery_roll_off_limit_deg=20.0,
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def example_flight_data() -> dict:
|
|
852
|
+
"""Worked-example simulated flight data (clearly labeled; not real)."""
|
|
853
|
+
return {
|
|
854
|
+
"note": "Flight data below are SIMULATED for the worked example "
|
|
855
|
+
"and must be replaced by recorded flight data.",
|
|
856
|
+
"gvt_modes": [(4.2, 11.6), (9.1, 14.8), (11.6, 14.8), (4.2, 9.1)],
|
|
857
|
+
"flutter_points": {
|
|
858
|
+
"speeds": [90.1, 95.4, 100.7, 106.0],
|
|
859
|
+
"dampings": [0.120, 0.115, 0.108, 0.100],
|
|
860
|
+
},
|
|
861
|
+
"stall_rows": [
|
|
862
|
+
{"config": "clean", "vs_measured": 44.1, "warning_onset": 46.8,
|
|
863
|
+
"recovery": {"alt_loss_m": 18.0, "pitch_up_deg": 3.0,
|
|
864
|
+
"roll_off_deg": 6.0}},
|
|
865
|
+
{"config": "takeoff (flaps)", "vs_measured": 40.4,
|
|
866
|
+
"warning_onset": 42.5,
|
|
867
|
+
"recovery": {"alt_loss_m": 21.0, "pitch_up_deg": 4.0,
|
|
868
|
+
"roll_off_deg": 8.0}},
|
|
869
|
+
{"config": "landing (flaps/gear)", "vs_measured": 36.3,
|
|
870
|
+
"warning_onset": 38.2,
|
|
871
|
+
"recovery": {"alt_loss_m": 24.0, "pitch_up_deg": 5.0,
|
|
872
|
+
"roll_off_deg": 9.0}},
|
|
873
|
+
],
|
|
874
|
+
"maneuver_n": [1.26, 1.88, 2.48],
|
|
875
|
+
"climb_roc_fpm": 1450.0,
|
|
876
|
+
"climb_gradient_pct": 2.70,
|
|
877
|
+
"risk_rows": [
|
|
878
|
+
{"hazard": "Flutter onset at high speed", "severity": 5,
|
|
879
|
+
"likelihood": 2,
|
|
880
|
+
"mitigation": "GVT + build-up blocks, damping gate 0.03, "
|
|
881
|
+
"excitation vanes, real-time monitoring"},
|
|
882
|
+
{"hazard": "Stall departure at aft CG", "severity": 4,
|
|
883
|
+
"likelihood": 3,
|
|
884
|
+
"mitigation": "entry technique, altitude floor, recovery "
|
|
885
|
+
"demonstration, spin chute installed"},
|
|
886
|
+
{"hazard": "Overspeed control upset", "severity": 4,
|
|
887
|
+
"likelihood": 2,
|
|
888
|
+
"mitigation": "abort criteria at every block, test pilot "
|
|
889
|
+
"briefing, VMO/VNE marking"},
|
|
890
|
+
{"hazard": "Engine failure during climb segment", "severity": 3,
|
|
891
|
+
"likelihood": 2,
|
|
892
|
+
"mitigation": "single-engine drills, field/landing options, "
|
|
893
|
+
"engine instruments monitored"},
|
|
894
|
+
{"hazard": "Telemetry data loss", "severity": 2,
|
|
895
|
+
"likelihood": 3,
|
|
896
|
+
"mitigation": "onboard recording backup, link checked "
|
|
897
|
+
"pre-flight"},
|
|
898
|
+
],
|
|
899
|
+
"go_no_go": {"weather": True, "aircraft_ready": True,
|
|
900
|
+
"instrumentation_ok": True, "safety_review_ok": True,
|
|
901
|
+
"airspace_ok": True},
|
|
902
|
+
"limit_changes": [
|
|
903
|
+
{"limit": "Speed envelope to 0.85 V_D", "data_point": "S1",
|
|
904
|
+
"ok": True},
|
|
905
|
+
{"limit": "Speed envelope to 0.90 V_D", "data_point": "S2",
|
|
906
|
+
"ok": True},
|
|
907
|
+
{"limit": "Speed envelope to 0.95 V_D", "data_point": "S3",
|
|
908
|
+
"ok": True},
|
|
909
|
+
{"limit": "Speed envelope to 1.00 V_D", "data_point": "S4",
|
|
910
|
+
"ok": True},
|
|
911
|
+
],
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def example_deliverable_markdown() -> str:
|
|
916
|
+
return render_flight_test_markdown(
|
|
917
|
+
build_flight_test_deliverable(example_aircraft()))
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
if __name__ == "__main__":
|
|
921
|
+
ac = example_aircraft()
|
|
922
|
+
model = build_flight_test_deliverable(ac)
|
|
923
|
+
md = render_flight_test_markdown(model)
|
|
924
|
+
print("AIRCRAFT:", model["aircraft"])
|
|
925
|
+
print("V-SPEEDS:", {k: round(v, 2) for k, v in model["v_speeds"].items()})
|
|
926
|
+
print("FLUTTER REQUIRED: %.1f m/s" % model["flutter"]["required_speed_ms"])
|
|
927
|
+
print("FLUTTER EXTRAPOLATED: %.1f m/s" %
|
|
928
|
+
model["flutter"]["vf_extrapolated_ms"])
|
|
929
|
+
print("FLUTTER MARGIN RATIO: %.3f" % model["flutter"]["margin"]["ratio"])
|
|
930
|
+
print("GATES:", check_flight_test_deliverable(model))
|
|
931
|
+
print("RENDERED:", len(md), "chars")
|