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,1507 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""flight_mechanics_core.py - Flight Mechanics Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given a vehicle configuration (an example
|
|
5
|
+
transport in this release) it computes mission performance and
|
|
6
|
+
stability-and-control numbers from REAL domain rules and BUILDS the
|
|
7
|
+
"Performance and S&C Analysis Report" deliverable. It also gate-checks
|
|
8
|
+
deliverables. Standalone: no external repo needed.
|
|
9
|
+
|
|
10
|
+
Domain rules encoded here are the equations documented in the bound
|
|
11
|
+
Aero Agent Skills flight-mechanics leaves (breguet-range,
|
|
12
|
+
breguet-endurance, specific-range, climb-performance,
|
|
13
|
+
takeoff-performance, landing-performance, glide-performance,
|
|
14
|
+
turn-performance, oei-climb-gradient, energy-height, wind-effects,
|
|
15
|
+
longitudinal-stability, dynamic-stability, short-period-mode-analysis,
|
|
16
|
+
phugoid-mode-analysis, lateral-directional-stability, trim-analysis,
|
|
17
|
+
mil-std-1797a) plus the public standards context those leaves summarize
|
|
18
|
+
(FAR 25.121/25.125/25.181, MIL-STD-1797A mode tables as summary).
|
|
19
|
+
Every equation is a paraphrase of common flight-mechanics practice;
|
|
20
|
+
no proprietary text is reproduced.
|
|
21
|
+
|
|
22
|
+
Boundary: the deliverable is an engineering DRAFT for human review,
|
|
23
|
+
never an approval, compliance finding, or certification claim.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import date
|
|
31
|
+
|
|
32
|
+
def _today() -> str:
|
|
33
|
+
import os
|
|
34
|
+
from datetime import date
|
|
35
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Constants and small unit helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
G0 = 9.80665 # standard gravity, m/s^2
|
|
42
|
+
R_AIR = 287.052874 # specific gas constant dry air, J/(kg K)
|
|
43
|
+
GAMMA_AIR = 1.4
|
|
44
|
+
T0_ISA = 288.15 # sea level ISA temperature, K
|
|
45
|
+
P0_ISA = 101325.0 # sea level ISA pressure, Pa
|
|
46
|
+
RHO0_ISA = 1.225 # sea level ISA density, kg/m^3
|
|
47
|
+
LAPSE = 0.0065 # ISA troposphere lapse rate, K/m
|
|
48
|
+
TROPOPAUSE = 11000.0 # m
|
|
49
|
+
|
|
50
|
+
MPS_TO_KT = 1.943844492 # m/s -> knots
|
|
51
|
+
M_TO_FT = 3.280839895 # m -> ft
|
|
52
|
+
MPS_TO_FPM = 196.850394 # m/s -> ft/min
|
|
53
|
+
KM_TO_NM = 0.539956803 # km -> nautical miles
|
|
54
|
+
RAD_TO_DEG = 180.0 / math.pi
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def kt(mps: float) -> float:
|
|
58
|
+
return mps * MPS_TO_KT
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ft(m: float) -> float:
|
|
62
|
+
return m * M_TO_FT
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fpm(mps: float) -> float:
|
|
66
|
+
return mps * MPS_TO_FPM
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def nm(km: float) -> float:
|
|
70
|
+
return km * KM_TO_NM
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def deg(rad: float) -> float:
|
|
74
|
+
return rad * RAD_TO_DEG
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _f(x, nd=1):
|
|
78
|
+
"""Format with thousands separator and nd decimals."""
|
|
79
|
+
return f"{x:,.{nd}f}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# ISA atmosphere (public standard atmosphere model, hydrostatic + lapse)
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def isa(h_m: float) -> dict:
|
|
87
|
+
"""ISA temperature/pressure/density at geometric altitude (m).
|
|
88
|
+
|
|
89
|
+
Troposphere: linear lapse 6.5 K/km to 11 km; stratosphere isothermal
|
|
90
|
+
216.65 K above. Returns dict with T (K), p (Pa), rho (kg/m^3),
|
|
91
|
+
a (m/s), sigma = rho/rho0. Raises ValueError for h < 0.
|
|
92
|
+
"""
|
|
93
|
+
if h_m < 0:
|
|
94
|
+
raise ValueError("altitude must be >= 0 m")
|
|
95
|
+
if h_m <= TROPOPAUSE:
|
|
96
|
+
T = T0_ISA - LAPSE * h_m
|
|
97
|
+
p = P0_ISA * (T / T0_ISA) ** (G0 / (R_AIR * LAPSE))
|
|
98
|
+
else:
|
|
99
|
+
T = T0_ISA - LAPSE * TROPOPAUSE # 216.65 K
|
|
100
|
+
p_base = P0_ISA * (T / T0_ISA) ** (G0 / (R_AIR * LAPSE))
|
|
101
|
+
p = p_base * math.exp(-G0 * (h_m - TROPOPAUSE) / (R_AIR * T))
|
|
102
|
+
rho = p / (R_AIR * T)
|
|
103
|
+
a = math.sqrt(GAMMA_AIR * R_AIR * T)
|
|
104
|
+
return {"T": T, "p": p, "rho": rho, "a": a,
|
|
105
|
+
"sigma": rho / RHO0_ISA}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Drag polar (parabolic model used by the thrust-required practice)
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def drag_coefficient(cd0: float, k: float, cl: float) -> float:
|
|
113
|
+
"""Parabolic drag polar CD = CD0 + K*CL^2."""
|
|
114
|
+
if cd0 <= 0 or k <= 0 or cl < 0:
|
|
115
|
+
raise ValueError("polar inputs must be physical")
|
|
116
|
+
return cd0 + k * cl * cl
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def lift_to_drag(cd0: float, k: float, cl: float) -> float:
|
|
120
|
+
cd = drag_coefficient(cd0, k, cl)
|
|
121
|
+
if cd <= 0:
|
|
122
|
+
raise ValueError("drag must be positive")
|
|
123
|
+
return cl / cd
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def max_lift_to_drag(cd0: float, k: float) -> dict:
|
|
127
|
+
"""Maximum L/D from the parabolic polar: (L/D)max = 1/(2 sqrt(CD0 K))
|
|
128
|
+
at CL* = sqrt(CD0/K)."""
|
|
129
|
+
if cd0 <= 0 or k <= 0:
|
|
130
|
+
raise ValueError("polar inputs must be physical")
|
|
131
|
+
cl_star = math.sqrt(cd0 / k)
|
|
132
|
+
return {"cl_star": cl_star, "ld_max": 1.0 / (2.0 * math.sqrt(cd0 * k))}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Performance rules (bound leaves: breguet-range, breguet-endurance,
|
|
137
|
+
# specific-range, climb-performance, takeoff-performance, landing-performance,
|
|
138
|
+
# glide-performance, turn-performance, oei-climb-gradient, energy-height,
|
|
139
|
+
# wind-effects)
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def breguet_range(v_ms, tsfc_kg_per_n_s, ld, m0_kg, m1_kg, g=G0):
|
|
143
|
+
"""Breguet still-air cruise range, m:
|
|
144
|
+
R = (V/(TSFC*g0)) * (L/D) * ln(m0/m1). TSFC in kg/(N s)."""
|
|
145
|
+
if min(v_ms, tsfc_kg_per_n_s, ld, m0_kg, m1_kg) <= 0:
|
|
146
|
+
raise ValueError("range inputs must be positive")
|
|
147
|
+
if m1_kg >= m0_kg:
|
|
148
|
+
raise ValueError("final mass must be below initial mass")
|
|
149
|
+
return (v_ms / (tsfc_kg_per_n_s * g)) * ld * math.log(m0_kg / m1_kg)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def jet_endurance(tsfc_kg_per_n_s, ld, w0_n, w1_n, g=G0):
|
|
153
|
+
"""Jet loiter endurance, s: E = (1/(TSFC*g0))*(L/D)*ln(W0/W1)
|
|
154
|
+
with TSFC in kg/(N s) and weights in N (weights version of the
|
|
155
|
+
bound breguet-endurance leaf; its 1/s SFC equals TSFC*g0)."""
|
|
156
|
+
if min(tsfc_kg_per_n_s, ld, w0_n, w1_n) <= 0:
|
|
157
|
+
raise ValueError("endurance inputs must be positive")
|
|
158
|
+
if w1_n >= w0_n:
|
|
159
|
+
raise ValueError("final weight must be below initial weight")
|
|
160
|
+
return (1.0 / (tsfc_kg_per_n_s * g)) * ld * math.log(w0_n / w1_n)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def specific_air_range(v_ms, tsfc, weight_n, ld):
|
|
164
|
+
"""Instantaneous specific air range, m/kg: SAR = V*(L/D)/(TSFC*W)."""
|
|
165
|
+
if min(v_ms, tsfc, weight_n, ld) <= 0:
|
|
166
|
+
raise ValueError("SAR inputs must be positive")
|
|
167
|
+
return v_ms * ld / (tsfc * weight_n)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def rate_of_climb(T_n, D_n, V_ms, W_n):
|
|
171
|
+
"""ROC = (T - D)*V / W, m/s. Excess power method (climb leaf)."""
|
|
172
|
+
if W_n <= 0 or V_ms <= 0:
|
|
173
|
+
raise ValueError("weight and speed must be positive")
|
|
174
|
+
if T_n - D_n < 0:
|
|
175
|
+
raise ValueError("no climb: thrust must exceed drag")
|
|
176
|
+
return (T_n - D_n) * V_ms / W_n
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def climb_gradient_pct(T_n, D_n, W_n):
|
|
180
|
+
"""Climb gradient percent: gamma = (T - D)/W * 100."""
|
|
181
|
+
if W_n <= 0:
|
|
182
|
+
raise ValueError("weight must be positive")
|
|
183
|
+
if T_n - D_n < 0:
|
|
184
|
+
raise ValueError("no climb: thrust must exceed drag")
|
|
185
|
+
return (T_n - D_n) / W_n * 100.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def time_to_climb(delta_h_m, roc_a, roc_b):
|
|
189
|
+
"""Time to climb, s, at the mean of the endpoint rates of climb."""
|
|
190
|
+
if delta_h_m <= 0 or roc_a <= 0 or roc_b <= 0:
|
|
191
|
+
raise ValueError("altitude gain and ROC endpoints must be positive")
|
|
192
|
+
return delta_h_m / ((roc_a + roc_b) / 2.0)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def stall_speed(wing_loading_n_m2, rho, cl_max):
|
|
196
|
+
"""Vs = sqrt(2*(W/S)/(rho*CLmax)), m/s (takeoff leaf)."""
|
|
197
|
+
if min(wing_loading_n_m2, rho, cl_max) <= 0:
|
|
198
|
+
raise ValueError("stall inputs must be positive")
|
|
199
|
+
return math.sqrt(2.0 * wing_loading_n_m2 / (rho * cl_max))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def takeoff_ground_roll(weight_n, s, thrust_n, rho, cl_max, mu=0.03, g=G0):
|
|
203
|
+
"""Takeoff ground roll, m: Sg = 1.44 W^2/(g rho S CLmax (T - mu W))
|
|
204
|
+
(standard estimate from the takeoff-performance leaf; the 1.44 =
|
|
205
|
+
1.2^2 comes from the V_LOF = 1.2 Vs transport convention)."""
|
|
206
|
+
if min(weight_n, s, thrust_n, rho, cl_max) <= 0:
|
|
207
|
+
raise ValueError("ground roll inputs must be positive")
|
|
208
|
+
if not (0.0 <= mu < 1.0):
|
|
209
|
+
raise ValueError("rolling friction must be in [0,1)")
|
|
210
|
+
if thrust_n <= mu * weight_n:
|
|
211
|
+
raise ValueError("thrust must exceed rolling friction")
|
|
212
|
+
return (1.44 * weight_n * weight_n /
|
|
213
|
+
(g * rho * s * cl_max * (thrust_n - mu * weight_n)))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def glide_ratio(ld): # glide range per unit altitude
|
|
217
|
+
return ld
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def glide_sink_rate(v_ms, glide_ratio_value):
|
|
221
|
+
"""Sink rate at a glide speed: Vsink = V*sin(gamma), gamma=atan(1/LD)."""
|
|
222
|
+
if v_ms <= 0 or glide_ratio_value <= 0:
|
|
223
|
+
raise ValueError("glide inputs must be positive")
|
|
224
|
+
return v_ms * math.sin(math.atan(1.0 / glide_ratio_value))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def landing_air_distance(v_approach, approach_deg, obstacle_m, n_flare,
|
|
228
|
+
g=G0):
|
|
229
|
+
"""Air distance over the obstacle to touchdown, m (landing leaf):
|
|
230
|
+
flare arc R = V^2/(g(n-1)), straight segment (h_obs - h_f)/tan(gamma)."""
|
|
231
|
+
if v_approach <= 0 or approach_deg <= 0 or n_flare <= 1.0:
|
|
232
|
+
raise ValueError("landing air inputs invalid")
|
|
233
|
+
gamma = math.radians(approach_deg)
|
|
234
|
+
radius = v_approach * v_approach / (g * (n_flare - 1.0))
|
|
235
|
+
h_flare = radius * (1.0 - math.cos(gamma))
|
|
236
|
+
if obstacle_m <= h_flare:
|
|
237
|
+
raise ValueError("obstacle must be above flare height")
|
|
238
|
+
s_flare = radius * math.sin(gamma)
|
|
239
|
+
straight = (obstacle_m - h_flare) / math.tan(gamma)
|
|
240
|
+
return straight + s_flare
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def average_deceleration_g(mu_braking, lift_to_weight, drag_to_weight,
|
|
244
|
+
reverse_to_weight=0.0):
|
|
245
|
+
"""Mean landing deceleration ratio in g's (landing leaf force model):
|
|
246
|
+
a/g = mu*(1 - L/W) + D/W + T_rev/W."""
|
|
247
|
+
if mu_braking < 0 or min(lift_to_weight, drag_to_weight,
|
|
248
|
+
reverse_to_weight) < 0:
|
|
249
|
+
raise ValueError("deceleration ratios must be non-negative")
|
|
250
|
+
return mu_braking * (1.0 - lift_to_weight) + drag_to_weight + \
|
|
251
|
+
reverse_to_weight
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def landing_ground_roll(v_touchdown, decel_g, g=G0):
|
|
255
|
+
"""Landing ground roll, m: s = V^2/(2 a), a = decel_g*g."""
|
|
256
|
+
if v_touchdown < 0 or decel_g <= 0:
|
|
257
|
+
raise ValueError("touchdown speed/deceleration invalid")
|
|
258
|
+
return v_touchdown * v_touchdown / (2.0 * decel_g * g)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def level_turn(phi_deg, v_ms, g=G0):
|
|
262
|
+
"""Level coordinated turn at bank phi: n = 1/cos(phi), rate and radius."""
|
|
263
|
+
phi = math.radians(phi_deg)
|
|
264
|
+
if abs(phi) >= math.pi / 2.0 or v_ms <= 0:
|
|
265
|
+
raise ValueError("turn inputs invalid")
|
|
266
|
+
n = 1.0 / math.cos(phi)
|
|
267
|
+
rate = g * math.sqrt(n * n - 1.0) / v_ms # rad/s
|
|
268
|
+
radius = v_ms * v_ms / (g * math.sqrt(n * n - 1.0)) # m
|
|
269
|
+
return {"n": n, "rate_rad_s": rate, "radius_m": radius}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def oei_thrust(total_thrust_n, n_engines, failed=1):
|
|
273
|
+
"""Thrust with engines failed: total*(n-f)/n."""
|
|
274
|
+
if total_thrust_n <= 0 or n_engines < 2 or not (1 <= failed < n_engines):
|
|
275
|
+
raise ValueError("OEI thrust inputs invalid")
|
|
276
|
+
return total_thrust_n * (n_engines - failed) / n_engines
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def oei_gradient_pct(t_oei_n, drag_n, weight_n):
|
|
280
|
+
"""OEI climb gradient percent: (T_oei - D)/W*100 (FAR 25.121 basis)."""
|
|
281
|
+
if weight_n <= 0 or t_oei_n < 0 or drag_n < 0:
|
|
282
|
+
raise ValueError("OEI gradient inputs invalid")
|
|
283
|
+
return (t_oei_n - drag_n) / weight_n * 100.0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def energy_height(altitude_m, v_ms, g=G0):
|
|
287
|
+
"""Energy height h_e = h + V^2/(2g), m."""
|
|
288
|
+
if altitude_m < 0 or v_ms < 0:
|
|
289
|
+
raise ValueError("energy height inputs invalid")
|
|
290
|
+
return altitude_m + v_ms * v_ms / (2.0 * g)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def wind_components(wind_speed, wind_dir_deg, track_deg):
|
|
294
|
+
"""Headwind (+) and crosswind components (wind-effects leaf)."""
|
|
295
|
+
if wind_speed < 0:
|
|
296
|
+
raise ValueError("wind speed must be >= 0")
|
|
297
|
+
d = math.radians(wind_dir_deg - track_deg)
|
|
298
|
+
return wind_speed * math.cos(d), wind_speed * math.sin(d)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Stability and control rules (bound leaves: longitudinal-stability,
|
|
303
|
+
# dynamic-stability, short-period-mode-analysis, phugoid-mode-analysis,
|
|
304
|
+
# lateral-directional-stability, trim-analysis)
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def neutral_point(h_ac_w, vh_tail, lift_slope_ratio, downwash_grad):
|
|
308
|
+
"""Stick-fixed neutral point (fraction MAC):
|
|
309
|
+
h_np = h_ac_w + V_h*(a_t/a_w)*(1 - depsilon/dalpha)."""
|
|
310
|
+
if not (0.0 < h_ac_w < 1.0) or vh_tail <= 0 or lift_slope_ratio <= 0:
|
|
311
|
+
raise ValueError("neutral point inputs invalid")
|
|
312
|
+
if not (0.0 <= downwash_grad < 1.0):
|
|
313
|
+
raise ValueError("downwash gradient must be in [0,1)")
|
|
314
|
+
return h_ac_w + vh_tail * lift_slope_ratio * (1.0 - downwash_grad)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def static_margin(neutral_point_val, h_cg):
|
|
318
|
+
"""SM = h_np - h_cg (fraction MAC); positive = stable."""
|
|
319
|
+
if not (0.0 < neutral_point_val < 1.0) or not (0.0 < h_cg < 1.0):
|
|
320
|
+
raise ValueError("static margin inputs invalid")
|
|
321
|
+
return neutral_point_val - h_cg
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def c_m_alpha_from_sm(cl_alpha, margin):
|
|
325
|
+
"""Cm_alpha = -CL_alpha * SM (standard neutral-point relation)."""
|
|
326
|
+
return -cl_alpha * margin
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def tail_volume_horizontal(s_h, l_h, s, cbar):
|
|
330
|
+
"""Horizontal tail volume coefficient V_h = S_h*l_h/(S*cbar)."""
|
|
331
|
+
if min(s_h, l_h, s, cbar) <= 0:
|
|
332
|
+
raise ValueError("tail volume inputs must be positive")
|
|
333
|
+
return s_h * l_h / (s * cbar)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def vertical_tail_volume(s_vt, l_vt, s, b):
|
|
337
|
+
"""Vertical tail volume coefficient V_v = S_vt*l_vt/(S*b)."""
|
|
338
|
+
if min(s_vt, l_vt, s, b) <= 0:
|
|
339
|
+
raise ValueError("vertical tail volume inputs must be positive")
|
|
340
|
+
return s_vt * l_vt / (s * b)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def cn_beta_vertical_tail(eta_vt, v_v, a_vt, sidewash_grad=0.0):
|
|
344
|
+
"""Fin contribution: Cn_beta_vt = eta*V_v*a_vt*(1 + k_s)."""
|
|
345
|
+
if not (0.0 < eta_vt <= 1.0) or v_v <= 0 or a_vt <= 0 or sidewash_grad < 0:
|
|
346
|
+
raise ValueError("fin contribution inputs invalid")
|
|
347
|
+
return eta_vt * v_v * a_vt * (1.0 + sidewash_grad)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def cl_beta_dihedral(cl, dihedral_deg):
|
|
351
|
+
"""Dihedral contribution: Cl_beta_gamma = -CL*Gamma (radians)."""
|
|
352
|
+
if cl < 0:
|
|
353
|
+
raise ValueError("lift coefficient must be >= 0")
|
|
354
|
+
return -cl * math.radians(dihedral_deg)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def z_alpha_derivative(qbar, s, cl_alpha, mass):
|
|
358
|
+
"""Z_alpha = -(qbar*S*CL_alpha)/m, m/s^2 per rad."""
|
|
359
|
+
if min(qbar, s, cl_alpha, mass) <= 0:
|
|
360
|
+
raise ValueError("Z_alpha inputs must be positive")
|
|
361
|
+
return -(qbar * s * cl_alpha) / mass
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def m_alpha_derivative(qbar, s, cbar, c_m_alpha, i_yy):
|
|
365
|
+
"""M_alpha = qbar*S*cbar*Cm_alpha/I_yy, 1/s^2 (Cm_alpha < 0 stable)."""
|
|
366
|
+
if min(qbar, s, cbar, i_yy) <= 0 or c_m_alpha >= 0:
|
|
367
|
+
raise ValueError("M_alpha inputs invalid (Cm_alpha must be < 0)")
|
|
368
|
+
return qbar * s * cbar * c_m_alpha / i_yy
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def m_q_derivative(qbar, s, cbar, c_m_q, v, i_yy):
|
|
372
|
+
"""M_q = qbar*S*cbar^2*Cm_q/(2 V I_yy), 1/s (Cm_q < 0 damping)."""
|
|
373
|
+
if min(qbar, s, cbar, v, i_yy) <= 0 or c_m_q >= 0:
|
|
374
|
+
raise ValueError("M_q inputs invalid (Cm_q must be < 0)")
|
|
375
|
+
return qbar * s * cbar * cbar * c_m_q / (2.0 * v * i_yy)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def short_period_frequency(z_alpha, m_alpha, m_q, v):
|
|
379
|
+
"""omega_ns = sqrt(M_q*Z_alpha/V - M_alpha), rad/s."""
|
|
380
|
+
if v <= 0:
|
|
381
|
+
raise ValueError("speed must be positive")
|
|
382
|
+
radicand = m_q * z_alpha / v - m_alpha
|
|
383
|
+
if radicand <= 0:
|
|
384
|
+
raise ValueError("short period radicand must be positive")
|
|
385
|
+
return math.sqrt(radicand)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def short_period_damping(z_alpha, m_alpha, m_q, v):
|
|
389
|
+
"""zeta_s = -(Z_alpha/V + M_q)/(2 omega_ns)."""
|
|
390
|
+
omega = short_period_frequency(z_alpha, m_alpha, m_q, v)
|
|
391
|
+
return -(z_alpha / v + m_q) / (2.0 * omega)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def n_over_alpha(qbar, s, cl_alpha, weight_n, g=G0):
|
|
395
|
+
"""Load factor per angle of attack, g/rad: n_alpha = qbar*S*CL_alpha/W."""
|
|
396
|
+
if min(qbar, s, cl_alpha, weight_n) <= 0:
|
|
397
|
+
raise ValueError("n_alpha inputs invalid")
|
|
398
|
+
return qbar * s * cl_alpha / weight_n
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def phugoid_frequency(v, g=G0):
|
|
402
|
+
return math.sqrt(2.0) * g / v
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def phugoid_period(v, g=G0):
|
|
406
|
+
return 2.0 * math.pi / phugoid_frequency(v, g)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def phugoid_damping(ld):
|
|
410
|
+
"""zeta_p = 1/(sqrt(2)*(L/D)), Lanchester drag damping."""
|
|
411
|
+
if ld <= 0:
|
|
412
|
+
raise ValueError("L/D must be positive")
|
|
413
|
+
return 1.0 / (math.sqrt(2.0) * ld)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def phugoid_time_to_half(v, ld, g=G0):
|
|
417
|
+
"""t_half = ln(2)*V*(L/D)/g (identity zeta*omega = g/(V L/D))."""
|
|
418
|
+
if v <= 0 or ld <= 0:
|
|
419
|
+
raise ValueError("phugoid time inputs invalid")
|
|
420
|
+
return math.log(2.0) * v * ld / g
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def y_beta_derivative(qbar, s, c_y_beta, mass):
|
|
424
|
+
"""Y_beta = qbar*S*C_Ybeta/m, m/s^2 per rad (Cy_beta < 0 fin)."""
|
|
425
|
+
if min(qbar, s, mass) <= 0 or c_y_beta >= 0:
|
|
426
|
+
raise ValueError("Y_beta inputs invalid (Cy_beta must be < 0)")
|
|
427
|
+
return qbar * s * c_y_beta / mass
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def y_r_derivative(qbar, s, b, c_y_r, v, mass):
|
|
431
|
+
"""Y_r = qbar*S*b*C_Yr/(2 V m), m/s per rad."""
|
|
432
|
+
if min(qbar, s, b, v, mass) <= 0:
|
|
433
|
+
raise ValueError("Y_r inputs invalid")
|
|
434
|
+
return qbar * s * b * c_y_r / (2.0 * v * mass)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def n_beta_derivative(qbar, s, b, c_n_beta, i_zz):
|
|
438
|
+
"""N_beta = qbar*S*b*Cn_beta/I_zz, 1/s^2 (Cn_beta > 0 stable)."""
|
|
439
|
+
if min(qbar, s, b, i_zz) <= 0 or c_n_beta <= 0:
|
|
440
|
+
raise ValueError("N_beta inputs invalid (Cn_beta must be > 0)")
|
|
441
|
+
return qbar * s * b * c_n_beta / i_zz
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def n_r_derivative(qbar, s, b, c_n_r, v, i_zz):
|
|
445
|
+
"""N_r = qbar*S*b^2*Cn_r/(2 V I_zz), 1/s (Cn_r < 0 damping)."""
|
|
446
|
+
if min(qbar, s, b, v, i_zz) <= 0 or c_n_r >= 0:
|
|
447
|
+
raise ValueError("N_r inputs invalid (Cn_r must be < 0)")
|
|
448
|
+
return qbar * s * b * b * c_n_r / (2.0 * v * i_zz)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def l_beta_derivative(qbar, s, b, c_l_beta, i_xx):
|
|
452
|
+
"""L_beta = qbar*S*b*Cl_beta/I_xx, 1/s^2 (Cl_beta < 0 stable)."""
|
|
453
|
+
if min(qbar, s, b, i_xx) <= 0 or c_l_beta >= 0:
|
|
454
|
+
raise ValueError("L_beta inputs invalid (Cl_beta must be < 0)")
|
|
455
|
+
return qbar * s * b * c_l_beta / i_xx
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def l_r_derivative(qbar, s, b, c_l_r, v, i_xx):
|
|
459
|
+
"""L_r = qbar*S*b^2*Cl_r/(2 V I_xx), 1/s."""
|
|
460
|
+
if min(qbar, s, b, v, i_xx) <= 0:
|
|
461
|
+
raise ValueError("L_r inputs invalid")
|
|
462
|
+
return qbar * s * b * b * c_l_r / (2.0 * v * i_xx)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def l_p_derivative(qbar, s, b, c_l_p, v, i_xx):
|
|
466
|
+
"""L_p = qbar*S*b^2*Cl_p/(2 V I_xx), 1/s (Cl_p < 0 roll damping)."""
|
|
467
|
+
if min(qbar, s, b, v, i_xx) <= 0 or c_l_p >= 0:
|
|
468
|
+
raise ValueError("L_p inputs invalid (Cl_p must be < 0)")
|
|
469
|
+
return qbar * s * b * b * c_l_p / (2.0 * v * i_xx)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def dutch_roll_frequency(n_beta, n_r, y_beta, y_r, v):
|
|
473
|
+
"""omega_dr = sqrt(N_beta + (N_r*Y_beta - N_beta*Y_r)/V), rad/s."""
|
|
474
|
+
if v <= 0:
|
|
475
|
+
raise ValueError("speed must be positive")
|
|
476
|
+
radicand = n_beta + (n_r * y_beta - n_beta * y_r) / v
|
|
477
|
+
if radicand <= 0:
|
|
478
|
+
raise ValueError("Dutch roll radicand must be positive")
|
|
479
|
+
return math.sqrt(radicand)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def dutch_roll_damping(n_beta, n_r, y_beta, y_r, v):
|
|
483
|
+
"""zeta_dr = -(Y_beta/V + N_r)/(2 omega_dr)."""
|
|
484
|
+
omega = dutch_roll_frequency(n_beta, n_r, y_beta, y_r, v)
|
|
485
|
+
return -(y_beta / v + n_r) / (2.0 * omega)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def roll_time_constant(l_p):
|
|
489
|
+
"""Roll subsidence time constant tau = -1/L_p, s (L_p < 0)."""
|
|
490
|
+
if l_p >= 0:
|
|
491
|
+
raise ValueError("L_p must be negative")
|
|
492
|
+
return -1.0 / l_p
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def spiral_stability_parameter(l_beta, l_r, n_beta, n_r):
|
|
496
|
+
"""Spiral criterion term L_beta*N_r - L_r*N_beta (>0 = convergent)."""
|
|
497
|
+
return l_beta * n_r - l_r * n_beta
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def spiral_eigenvalue(l_beta, l_r, n_beta, n_r, l_p, v, g=G0):
|
|
501
|
+
"""Approximate spiral root lambda_s = (g/V)*(Lb*Nr - Lr*Nb)/(Nb*Lp)."""
|
|
502
|
+
if n_beta <= 0 or l_p >= 0 or v <= 0:
|
|
503
|
+
raise ValueError("spiral inputs invalid")
|
|
504
|
+
return (g / v) * spiral_stability_parameter(l_beta, l_r, n_beta, n_r) / \
|
|
505
|
+
(n_beta * l_p)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def trim_lift_coefficient(weight_n, rho, v, s):
|
|
509
|
+
"""CL_trim = 2W/(rho V^2 S) for level flight."""
|
|
510
|
+
if min(weight_n, rho, v, s) <= 0:
|
|
511
|
+
raise ValueError("trim inputs must be positive")
|
|
512
|
+
return 2.0 * weight_n / (rho * v * v * s)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def elevator_to_trim(cm0, cm_alpha, cl_alpha, cl_trim, cm_delta_e):
|
|
516
|
+
"""de_trim = -(Cm0 + Cm_alpha*CL_trim/CL_alpha)/Cm_delta_e, rad."""
|
|
517
|
+
if cl_alpha <= 0 or cm_delta_e == 0:
|
|
518
|
+
raise ValueError("elevator trim inputs invalid")
|
|
519
|
+
alpha_trim = cl_trim / cl_alpha
|
|
520
|
+
return -(cm0 + cm_alpha * alpha_trim) / cm_delta_e
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
# Flying qualities reference tables (summary of the MIL-STD-1797A mode
|
|
525
|
+
# criteria as encoded by the bound mil-std-1797a and short-period-mode leaves:
|
|
526
|
+
# reference-only paraphrase, civil use by analogy with FAR 25.181 context)
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
# Short period Level 1 damping band by flight phase category (summary):
|
|
530
|
+
# category A 0.35-1.30; categories B and C 0.30-2.00.
|
|
531
|
+
SP_DAMPING_L1 = {"A": (0.35, 1.30), "B": (0.30, 2.00), "C": (0.30, 2.00)}
|
|
532
|
+
SP_DAMPING_L2 = {"A": (0.25, 2.00), "B": (0.25, 2.00), "C": (0.25, 2.00)}
|
|
533
|
+
SP_DAMPING_L3_MIN = 0.15
|
|
534
|
+
|
|
535
|
+
# Short period minimum natural frequency (rad/s) at n_alpha = 1.0 and 3.0
|
|
536
|
+
# g/rad by (category, class); linear interpolation, clamped outside.
|
|
537
|
+
SP_FREQ_N1 = {"A": {"I": 3.6, "II": 3.6, "III": 3.0, "IV": 2.5},
|
|
538
|
+
"B": {"I": 1.0, "II": 1.0, "III": 1.0, "IV": 1.0},
|
|
539
|
+
"C": {"I": 1.0, "II": 1.0, "III": 1.0, "IV": 1.0}}
|
|
540
|
+
SP_FREQ_N3 = {"A": {"I": 6.0, "II": 6.0, "III": 6.0, "IV": 6.0},
|
|
541
|
+
"B": {"I": 2.0, "II": 2.0, "III": 2.0, "IV": 2.0},
|
|
542
|
+
"C": {"I": 2.0, "II": 2.0, "III": 2.0, "IV": 2.0}}
|
|
543
|
+
|
|
544
|
+
# Phugoid: min damping ratio per level (Level 3 expressed as time to
|
|
545
|
+
# double >= 55 s for a divergence).
|
|
546
|
+
PHUGOID_DAMPING_MIN = {1: 0.04, 2: 0.0}
|
|
547
|
+
|
|
548
|
+
# Dutch roll (zeta_min, omega_min rad/s, zeta*omega_min) level 1 by
|
|
549
|
+
# (category, class); category A class IV is the strictest.
|
|
550
|
+
DR_L1 = {"A": {"IV": (0.19, 1.0, 0.35), "I": (0.19, 0.4, 0.35),
|
|
551
|
+
"II": (0.19, 0.4, 0.35), "III": (0.19, 0.4, 0.35)},
|
|
552
|
+
"B": {"IV": (0.08, 0.4, 0.15), "I": (0.08, 0.4, 0.15),
|
|
553
|
+
"II": (0.08, 0.4, 0.15), "III": (0.08, 0.4, 0.15)},
|
|
554
|
+
"C": {"IV": (0.08, 0.4, 0.15), "I": (0.08, 0.4, 0.15),
|
|
555
|
+
"II": (0.08, 0.4, 0.15), "III": (0.08, 0.4, 0.15)}}
|
|
556
|
+
DR_L2 = (0.02, 0.4, 0.05)
|
|
557
|
+
|
|
558
|
+
# Spiral min time to double (s) per level and category.
|
|
559
|
+
SPIRAL_T2 = {1: {"A": 20.0, "B": 12.0, "C": 20.0},
|
|
560
|
+
2: {"A": 8.0, "B": 8.0, "C": 8.0},
|
|
561
|
+
3: {"A": 4.0, "B": 4.0, "C": 4.0}}
|
|
562
|
+
|
|
563
|
+
# Roll mode max time constant (s) per level and category.
|
|
564
|
+
ROLL_TAU_MAX = {1: {"A": 1.0, "B": 1.4, "C": 1.0},
|
|
565
|
+
2: {"A": 1.4, "B": 3.0, "C": 1.4},
|
|
566
|
+
3: {"A": 10.0, "B": 10.0, "C": 10.0}}
|
|
567
|
+
|
|
568
|
+
CATEGORIES = ("A", "B", "C")
|
|
569
|
+
CLASSES = ("I", "II", "III", "IV")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def sp_min_frequency(category, aircraft_class, n_over_alpha_val):
|
|
573
|
+
"""Short period frequency floor interpolated in n_alpha (g/rad),
|
|
574
|
+
clamped to the [1, 3] g/rad table band (mil-std-1797a leaf)."""
|
|
575
|
+
if category not in CATEGORIES or aircraft_class not in CLASSES:
|
|
576
|
+
raise ValueError("invalid category/class")
|
|
577
|
+
if n_over_alpha_val <= 0:
|
|
578
|
+
raise ValueError("n_alpha must be positive")
|
|
579
|
+
n = min(max(n_over_alpha_val, 1.0), 3.0)
|
|
580
|
+
f1 = SP_FREQ_N1[category][aircraft_class]
|
|
581
|
+
f3 = SP_FREQ_N3[category][aircraft_class]
|
|
582
|
+
t = (n - 1.0) / 2.0
|
|
583
|
+
return f1 + t * (f3 - f1)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def assess_short_period(zeta, omega, category, aircraft_class, n_alpha):
|
|
587
|
+
"""Short period flying qualities level (1/2/3) by the summary bands.
|
|
588
|
+
|
|
589
|
+
Level 1 needs damping in the category band and frequency at/above the
|
|
590
|
+
n_alpha-interpolated floor; a Level 1 damping with a frequency below
|
|
591
|
+
the floor is degraded to Level 2; damping below the Level 2 band but
|
|
592
|
+
above the Level 3 minimum reads Level 3.
|
|
593
|
+
"""
|
|
594
|
+
min_omega = sp_min_frequency(category, aircraft_class, n_alpha)
|
|
595
|
+
z1min, z1max = SP_DAMPING_L1[category]
|
|
596
|
+
z2min, z2max = SP_DAMPING_L2[category]
|
|
597
|
+
if z1min <= zeta <= z1max:
|
|
598
|
+
level = 1
|
|
599
|
+
elif z2min <= zeta <= z2max:
|
|
600
|
+
level = 2
|
|
601
|
+
else:
|
|
602
|
+
level = 3
|
|
603
|
+
if level == 1 and omega < min_omega:
|
|
604
|
+
level = 2
|
|
605
|
+
return {"level": level, "min_omega": min_omega, "zeta": zeta,
|
|
606
|
+
"omega": omega, "n_alpha": n_alpha}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def assess_phugoid(zeta):
|
|
610
|
+
"""Phugoid level: 1 when zeta >= 0.04, 2 when 0 <= zeta < 0.04,
|
|
611
|
+
3 for divergent (zeta < 0)."""
|
|
612
|
+
if zeta >= PHUGOID_DAMPING_MIN[1]:
|
|
613
|
+
return 1
|
|
614
|
+
if zeta >= PHUGOID_DAMPING_MIN[2]:
|
|
615
|
+
return 2
|
|
616
|
+
return 3
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def assess_dutch_roll(zeta, omega, category, aircraft_class):
|
|
620
|
+
"""Dutch roll level by the summary criteria."""
|
|
621
|
+
if zeta <= 0.0:
|
|
622
|
+
return 3
|
|
623
|
+
z1, w1, zw1 = DR_L1[category][aircraft_class]
|
|
624
|
+
if zeta >= z1 and omega >= w1 and zeta * omega >= zw1:
|
|
625
|
+
return 1
|
|
626
|
+
z2, w2, zw2 = DR_L2
|
|
627
|
+
if zeta >= z2 and omega >= w2 and zeta * omega >= zw2:
|
|
628
|
+
return 2
|
|
629
|
+
return 3
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def assess_spiral(lam, category):
|
|
633
|
+
"""Spiral level: stable root -> 1; else by time-to-double floors."""
|
|
634
|
+
if lam <= 0.0:
|
|
635
|
+
return 1
|
|
636
|
+
t2 = math.log(2.0) / lam
|
|
637
|
+
for level in (1, 2, 3):
|
|
638
|
+
if t2 >= SPIRAL_T2[level][category]:
|
|
639
|
+
return level
|
|
640
|
+
return 3
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def assess_roll_mode(tau, category):
|
|
644
|
+
"""Roll mode level by the max time-constant floors."""
|
|
645
|
+
for level in (1, 2, 3):
|
|
646
|
+
if tau <= ROLL_TAU_MAX[level][category]:
|
|
647
|
+
return level
|
|
648
|
+
return 3
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
# FAR/CS minimum climb gradients (oei-climb-gradient leaf, FAR 25.121)
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
FAR_25_121_SECOND_SEGMENT = {2: 2.4, 3: 2.7, 4: 3.0} # percent
|
|
656
|
+
FAR_25_121_APPROACH = {2: 2.1, 3: 2.4, 4: 2.7} # percent
|
|
657
|
+
FAR_25_121_LANDING = 3.2 # percent
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def far_25_121_minimum(segment, n_engines):
|
|
661
|
+
if segment == "second-segment":
|
|
662
|
+
return FAR_25_121_SECOND_SEGMENT[n_engines]
|
|
663
|
+
if segment == "approach":
|
|
664
|
+
return FAR_25_121_APPROACH[n_engines]
|
|
665
|
+
if segment == "landing":
|
|
666
|
+
return FAR_25_121_LANDING
|
|
667
|
+
raise ValueError("segment must be second-segment/approach/landing")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
# Example configuration: a generic 150-seat-class twin-engine transport.
|
|
672
|
+
# ALL numbers are EXAMPLE DATA in the public class range (stated basis, to be
|
|
673
|
+
# replaced by project data); the report marks them as such. Nothing here
|
|
674
|
+
# claims to describe a specific certified airplane.
|
|
675
|
+
# ---------------------------------------------------------------------------
|
|
676
|
+
|
|
677
|
+
@dataclass
|
|
678
|
+
class ExampleTransport:
|
|
679
|
+
name: str = "AeroLine AT-78"
|
|
680
|
+
description: str = "Example twin-engine transport, 150-seat class (MTOW 78 t); " \
|
|
681
|
+
"all data are example values in the public class range - not a specific " \
|
|
682
|
+
"certified airplane."
|
|
683
|
+
# --- geometry / inertia (example data) ---
|
|
684
|
+
s: float = 125.0 # wing area m^2
|
|
685
|
+
b: float = 35.8 # span m
|
|
686
|
+
cbar: float = 3.4916 # mean aerodynamic chord m (= S/b)
|
|
687
|
+
i_yy: float = 3.4e6 # pitch inertia kg m^2
|
|
688
|
+
i_xx: float = 2.0e6 # roll inertia kg m^2
|
|
689
|
+
i_zz: float = 6.5e6 # yaw inertia kg m^2
|
|
690
|
+
# --- masses (example data) ---
|
|
691
|
+
m_mtow: float = 78000.0 # MTOW kg
|
|
692
|
+
m_mlw: float = 62000.0 # MLW kg
|
|
693
|
+
# --- propulsion (example data) ---
|
|
694
|
+
n_engines: int = 2
|
|
695
|
+
thrust_to_each: float = 110000.0 # sea-level static per engine (takeoff rating), N
|
|
696
|
+
thrust_climb_sl_total: float = 150000.0 # total max-climb rating sea level, N
|
|
697
|
+
thrust_lapse_exp: float = 0.7 # sigma^0.7 density-based thrust lapse (high-bypass)
|
|
698
|
+
tsfc: float = 1.55e-5 # cruise TSFC kg/(N s), ~0.55 lb/(lbf hr) class
|
|
699
|
+
# --- aerodynamics (parabolic polars per configuration; example data) ---
|
|
700
|
+
cd0_clean: float = 0.021
|
|
701
|
+
oswald_clean: float = 0.80
|
|
702
|
+
cd0_to: float = 0.032 # takeoff flaps, gear up
|
|
703
|
+
osward_to: float = 0.60
|
|
704
|
+
cd0_land: float = 0.050 # landing flaps + gear down
|
|
705
|
+
oswald_land: float = 0.55
|
|
706
|
+
cl_alpha_clean: float = 5.5 # airplane lift slope 1/rad (cruise, incl Mach)
|
|
707
|
+
cl_alpha_land: float = 6.2 # airplane lift slope 1/rad (landing config)
|
|
708
|
+
cl_max_to: float = 1.9 # takeoff configuration
|
|
709
|
+
cl_max_land: float = 2.4 # landing configuration
|
|
710
|
+
# --- tail geometry (example data) ---
|
|
711
|
+
s_h: float = 26.5 # horizontal tail area m^2
|
|
712
|
+
l_h: float = 14.2 # tail arm (wing AC -> HTP AC) m
|
|
713
|
+
s_vt: float = 12.4 # vertical tail area m^2
|
|
714
|
+
l_vt: float = 18.5 # fin arm m
|
|
715
|
+
a_over_a_ratio: float = 0.72 # a_t/a_w for the neutral point build
|
|
716
|
+
downwash_grad: float = 0.45 # depsilon/dalpha
|
|
717
|
+
h_ac_w: float = 0.25 # wing aerodynamic centre, fraction MAC
|
|
718
|
+
eta_vt: float = 0.95
|
|
719
|
+
a_vt: float = 3.4 # fin lift slope 1/rad
|
|
720
|
+
sidewash_grad: float = 0.08
|
|
721
|
+
cn_beta_fuselage: float = -0.0589 # destabilizing body term (class data)
|
|
722
|
+
cl_beta_remainder: float = -0.025 # sweep/body remainder to Cl_beta total
|
|
723
|
+
dihedral_deg: float = 5.2
|
|
724
|
+
c_y_beta: float = -0.65 # side-force slope (class data)
|
|
725
|
+
c_y_r: float = 0.25
|
|
726
|
+
c_n_r: float = -0.25
|
|
727
|
+
c_l_r: float = 0.13
|
|
728
|
+
c_l_p: float = -0.42
|
|
729
|
+
c_m_q: float = -30.0 # pitch damping coefficient (tail + wing-body)
|
|
730
|
+
c_m_0: float = 0.08 # zero-lift pitching moment (class data)
|
|
731
|
+
c_m_delta_e: float = -0.90 # elevator effectiveness 1/rad
|
|
732
|
+
elevator_limit_deg: float = 25.0
|
|
733
|
+
# --- CG envelope (example data, fraction MAC) ---
|
|
734
|
+
h_cg_fwd: float = 0.32
|
|
735
|
+
h_cg_aft: float = 0.44
|
|
736
|
+
h_cg_mid: float = 0.38
|
|
737
|
+
# --- mission / conditions (example data) ---
|
|
738
|
+
cruise_alt_m: float = 10668.0 # FL350
|
|
739
|
+
cruise_mach: float = 0.78
|
|
740
|
+
climb_speed_ms: float = 128.6 # 250 KCAS at sea level, m/s
|
|
741
|
+
loiter_start_kg: float = 62000.0
|
|
742
|
+
loiter_end_kg: float = 59000.0
|
|
743
|
+
landing_approach_deg: float = 3.0
|
|
744
|
+
landing_flare_n: float = 1.2
|
|
745
|
+
landing_mu: float = 0.45 # dry runway braking coefficient
|
|
746
|
+
to_mu: float = 0.03 # rolling friction, dry concrete
|
|
747
|
+
obstacle_50ft_m: float = 15.24 # FAR 25.125 obstacle height
|
|
748
|
+
landing_dist_factor: float = 1.67 # FAR 25.125 certified-distance factor
|
|
749
|
+
turn_bank_deg: float = 25.0
|
|
750
|
+
wind_kt: float = 40.0 # enroute headwind for the wind case
|
|
751
|
+
|
|
752
|
+
# derived
|
|
753
|
+
@property
|
|
754
|
+
def ar(self):
|
|
755
|
+
return self.b * self.b / self.s
|
|
756
|
+
|
|
757
|
+
@property
|
|
758
|
+
def k_clean(self):
|
|
759
|
+
return 1.0 / (math.pi * self.oswald_clean * self.ar)
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def k_to(self):
|
|
763
|
+
return 1.0 / (math.pi * self.osward_to * self.ar)
|
|
764
|
+
|
|
765
|
+
@property
|
|
766
|
+
def k_land(self):
|
|
767
|
+
return 1.0 / (math.pi * self.oswald_land * self.ar)
|
|
768
|
+
|
|
769
|
+
@property
|
|
770
|
+
def w_mtow(self):
|
|
771
|
+
return self.m_mtow * G0
|
|
772
|
+
|
|
773
|
+
@property
|
|
774
|
+
def w_mlw(self):
|
|
775
|
+
return self.m_mlw * G0
|
|
776
|
+
|
|
777
|
+
@property
|
|
778
|
+
def thrust_total_to(self):
|
|
779
|
+
return self.n_engines * self.thrust_to_each
|
|
780
|
+
|
|
781
|
+
@property
|
|
782
|
+
def vh_tail(self):
|
|
783
|
+
return tail_volume_horizontal(self.s_h, self.l_h, self.s, self.cbar)
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def v_vtail(self):
|
|
787
|
+
return vertical_tail_volume(self.s_vt, self.l_vt, self.s, self.b)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def example_vehicle() -> ExampleTransport:
|
|
791
|
+
return ExampleTransport()
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
# Analysis: compute every number of the deliverable from the configuration
|
|
796
|
+
# ---------------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
def analyze_vehicle(a: ExampleTransport) -> dict:
|
|
799
|
+
"""Run the full performance + S&C analysis. Returns a flat dict of
|
|
800
|
+
results consumed by the report builder."""
|
|
801
|
+
r = {}
|
|
802
|
+
r["aircraft"] = a
|
|
803
|
+
|
|
804
|
+
# --- atmosphere at the analysis altitudes ---
|
|
805
|
+
r["isa_sl"] = isa(0.0)
|
|
806
|
+
r["isa_cruise"] = isa(a.cruise_alt_m)
|
|
807
|
+
|
|
808
|
+
# cruise true airspeed at the cruise Mach number
|
|
809
|
+
v_cruise = a.cruise_mach * r["isa_cruise"]["a"]
|
|
810
|
+
r["v_cruise"] = v_cruise
|
|
811
|
+
qbar_cruise = 0.5 * r["isa_cruise"]["rho"] * v_cruise * v_cruise
|
|
812
|
+
r["qbar_cruise"] = qbar_cruise
|
|
813
|
+
|
|
814
|
+
# --- 1. Mission performance ---
|
|
815
|
+
# cruise point from the polar
|
|
816
|
+
cl_cruise = a.w_mtow / (qbar_cruise * a.s)
|
|
817
|
+
cd_cruise = drag_coefficient(a.cd0_clean, a.k_clean, cl_cruise)
|
|
818
|
+
ld_cruise = cl_cruise / cd_cruise
|
|
819
|
+
r.update(cl_cruise=cl_cruise, cd_cruise=cd_cruise, ld_cruise=ld_cruise,
|
|
820
|
+
d_cruise=a.w_mtow / ld_cruise)
|
|
821
|
+
r["ld_max"] = max_lift_to_drag(a.cd0_clean, a.k_clean)
|
|
822
|
+
|
|
823
|
+
# Breguet range: cruise segment MTOW start, 17,160 kg cruise fuel burned
|
|
824
|
+
m_cruise_start = a.m_mtow
|
|
825
|
+
m_cruise_end = a.m_mtow - 17160.0
|
|
826
|
+
r["range_m"] = breguet_range(v_cruise, a.tsfc, ld_cruise,
|
|
827
|
+
m_cruise_start, m_cruise_end)
|
|
828
|
+
r["range_km"] = r["range_m"] / 1000.0
|
|
829
|
+
r["cruise_fuel_kg"] = m_cruise_start - m_cruise_end
|
|
830
|
+
|
|
831
|
+
# loiter endurance (Breguet jet endurance, weights form)
|
|
832
|
+
r["endurance_s"] = jet_endurance(a.tsfc, ld_cruise,
|
|
833
|
+
a.loiter_start_kg * G0,
|
|
834
|
+
a.loiter_end_kg * G0)
|
|
835
|
+
r["endurance_hr"] = r["endurance_s"] / 3600.0
|
|
836
|
+
|
|
837
|
+
# specific air range at cruise + a small speed sweep for the "curve"
|
|
838
|
+
sar = specific_air_range(v_cruise, a.tsfc, a.w_mtow, ld_cruise)
|
|
839
|
+
r["sar_m_per_kg"] = sar
|
|
840
|
+
sar_pts = []
|
|
841
|
+
for frac in (0.95, 1.00, 1.05):
|
|
842
|
+
vv = v_cruise * frac
|
|
843
|
+
cll = a.w_mtow / (0.5 * r["isa_cruise"]["rho"] * vv * vv * a.s)
|
|
844
|
+
ldl = lift_to_drag(a.cd0_clean, a.k_clean, cll)
|
|
845
|
+
sar_pts.append({"frac": frac, "v": vv, "ld": ldl,
|
|
846
|
+
"sar": specific_air_range(vv, a.tsfc, a.w_mtow, ldl)})
|
|
847
|
+
r["sar_curve"] = sar_pts
|
|
848
|
+
r["sector_fuel_5000km"] = 5.0e6 / sar
|
|
849
|
+
|
|
850
|
+
# climb: sea level and FL350 at MTOW, clean config
|
|
851
|
+
qbar_sl_climb = 0.5 * r["isa_sl"]["rho"] * a.climb_speed_ms ** 2
|
|
852
|
+
cl_climb_sl = a.w_mtow / (qbar_sl_climb * a.s)
|
|
853
|
+
cd_climb_sl = drag_coefficient(a.cd0_clean, a.k_clean, cl_climb_sl)
|
|
854
|
+
d_climb_sl = cd_climb_sl * qbar_sl_climb * a.s
|
|
855
|
+
r["roc_sl"] = rate_of_climb(a.thrust_climb_sl_total, d_climb_sl,
|
|
856
|
+
a.climb_speed_ms, a.w_mtow)
|
|
857
|
+
# climb thrust at FL350: density-based lapse sigma^0.7
|
|
858
|
+
t_climb_350 = a.thrust_climb_sl_total * r["isa_cruise"]["sigma"] ** \
|
|
859
|
+
a.thrust_lapse_exp
|
|
860
|
+
# climb speed at FL350 on the 250 KCAS / M0.78 schedule
|
|
861
|
+
v_climb_350 = min(v_cruise, a.climb_speed_ms /
|
|
862
|
+
math.sqrt(r["isa_cruise"]["sigma"]))
|
|
863
|
+
qbar_350 = 0.5 * r["isa_cruise"]["rho"] * v_climb_350 ** 2
|
|
864
|
+
cl_350 = a.w_mtow / (qbar_350 * a.s)
|
|
865
|
+
cd_350 = drag_coefficient(a.cd0_clean, a.k_clean, cl_350)
|
|
866
|
+
d_350 = cd_350 * qbar_350 * a.s
|
|
867
|
+
r["roc_350"] = rate_of_climb(t_climb_350, d_350, v_climb_350, a.w_mtow)
|
|
868
|
+
r["time_to_350_s"] = time_to_climb(a.cruise_alt_m, r["roc_sl"],
|
|
869
|
+
r["roc_350"])
|
|
870
|
+
# ROC at FL410 (12,497 m) for the ceiling interpolation
|
|
871
|
+
isa_410 = isa(12497.0)
|
|
872
|
+
t_climb_410 = a.thrust_climb_sl_total * isa_410["sigma"] ** \
|
|
873
|
+
a.thrust_lapse_exp
|
|
874
|
+
v_410 = min(v_cruise, a.climb_speed_ms / math.sqrt(isa_410["sigma"]))
|
|
875
|
+
qbar_410 = 0.5 * isa_410["rho"] * v_410 ** 2
|
|
876
|
+
cl_410 = a.w_mtow / (qbar_410 * a.s)
|
|
877
|
+
cd_410 = drag_coefficient(a.cd0_clean, a.k_clean, cl_410)
|
|
878
|
+
d_410 = cd_410 * qbar_410 * a.s
|
|
879
|
+
r["roc_410"] = rate_of_climb(t_climb_410, d_410, v_410, a.w_mtow)
|
|
880
|
+
# linear-lapse ceiling estimate between FL350 and FL410 (indicative)
|
|
881
|
+
lapse = (r["roc_350"] - r["roc_410"]) / (12497.0 - a.cruise_alt_m)
|
|
882
|
+
r["ceiling_est_m"] = a.cruise_alt_m + (r["roc_350"] - 0.5) / lapse
|
|
883
|
+
|
|
884
|
+
# takeoff: stall speed, V_LOF/V2 = 1.2 Vs, ground roll at MTOW sea level
|
|
885
|
+
ws_mtow = a.w_mtow / a.s
|
|
886
|
+
vs_to = stall_speed(ws_mtow, r["isa_sl"]["rho"], a.cl_max_to)
|
|
887
|
+
v2 = 1.2 * vs_to
|
|
888
|
+
sg = takeoff_ground_roll(a.w_mtow, a.s, a.thrust_total_to,
|
|
889
|
+
r["isa_sl"]["rho"], a.cl_max_to, mu=a.to_mu)
|
|
890
|
+
r.update(vs_to=vs_to, v2=v2, to_ground_roll=sg)
|
|
891
|
+
|
|
892
|
+
# landing: MLW, sea level
|
|
893
|
+
ws_mlw = a.w_mlw / a.s
|
|
894
|
+
vs_land = stall_speed(ws_mlw, r["isa_sl"]["rho"], a.cl_max_land)
|
|
895
|
+
v_ref = 1.3 * vs_land
|
|
896
|
+
v_td = 0.95 * v_ref
|
|
897
|
+
air_dist = landing_air_distance(v_ref, a.landing_approach_deg,
|
|
898
|
+
a.obstacle_50ft_m, a.landing_flare_n)
|
|
899
|
+
# drag ratio during the ground roll in the landing configuration
|
|
900
|
+
cl_td = a.w_mlw / (0.5 * r["isa_sl"]["rho"] * v_td ** 2 * a.s)
|
|
901
|
+
cd_td = drag_coefficient(a.cd0_land, a.k_land, cl_td)
|
|
902
|
+
decel_g = average_deceleration_g(a.landing_mu, 0.15, cd_td / cl_td)
|
|
903
|
+
s_gr = landing_ground_roll(v_td, decel_g)
|
|
904
|
+
actual_land = air_dist + s_gr
|
|
905
|
+
r.update(vs_land=vs_land, v_ref=v_ref, v_td=v_td, air_dist=air_dist,
|
|
906
|
+
land_ground_roll=s_gr, land_actual=actual_land,
|
|
907
|
+
land_certified=actual_land * a.landing_dist_factor,
|
|
908
|
+
ld_landing=cl_td / cd_td)
|
|
909
|
+
|
|
910
|
+
# glide (best glide, clean polar, from cruise altitude at MTOW)
|
|
911
|
+
ld_max = r["ld_max"]
|
|
912
|
+
cl_bg = ld_max["cl_star"]
|
|
913
|
+
v_bg_eas = math.sqrt(2.0 * a.w_mtow / (r["isa_sl"]["rho"] * a.s * cl_bg))
|
|
914
|
+
sink = glide_sink_rate(v_bg_eas, ld_max["ld_max"])
|
|
915
|
+
r.update(best_glide_v=v_bg_eas, best_glide_sink=sink,
|
|
916
|
+
glide_range_m=ld_max["ld_max"] * a.cruise_alt_m,
|
|
917
|
+
glide_time_s=a.cruise_alt_m / sink)
|
|
918
|
+
|
|
919
|
+
# turn at 25 deg bank, cruise speed, MTOW
|
|
920
|
+
turn = level_turn(a.turn_bank_deg, v_cruise)
|
|
921
|
+
d_turn = r["d_cruise"] * turn["n"]
|
|
922
|
+
# cruise-rating thrust available at FL350 (assumed 58 kN class)
|
|
923
|
+
t_cruise_avail = 58000.0
|
|
924
|
+
turn["sustained"] = t_cruise_avail >= d_turn
|
|
925
|
+
turn["d_turn"] = d_turn
|
|
926
|
+
turn["t_avail"] = t_cruise_avail
|
|
927
|
+
r["turn"] = turn
|
|
928
|
+
|
|
929
|
+
# wind: 40 kt headwind on the 5,000 km sector
|
|
930
|
+
wind_ms = a.wind_kt / MPS_TO_KT
|
|
931
|
+
gs_still = v_cruise
|
|
932
|
+
gs_hw = v_cruise - wind_ms
|
|
933
|
+
r["wind"] = {"wind_kt": a.wind_kt, "gs_still": gs_still,
|
|
934
|
+
"gs_hw": gs_hw, "time_still_h": 5.0e6 / gs_still / 3600.0,
|
|
935
|
+
"time_hw_h": 5.0e6 / gs_hw / 3600.0}
|
|
936
|
+
|
|
937
|
+
# --- 2. Safety performance ---
|
|
938
|
+
# OEI second segment: V2, takeoff config gear up, MTOW, one engine failed
|
|
939
|
+
qbar_v2 = 0.5 * r["isa_sl"]["rho"] * v2 * v2
|
|
940
|
+
cl_2nd = a.w_mtow / (qbar_v2 * a.s)
|
|
941
|
+
cd_2nd = drag_coefficient(a.cd0_to, a.k_to, cl_2nd)
|
|
942
|
+
d_2nd = cd_2nd * qbar_v2 * a.s
|
|
943
|
+
t_oei = oei_thrust(a.thrust_total_to, a.n_engines, 1)
|
|
944
|
+
grad_2nd = oei_gradient_pct(t_oei, d_2nd, a.w_mtow)
|
|
945
|
+
r["oei"] = {"t_oei": t_oei, "d_2nd": d_2nd, "ld_2nd": cl_2nd / cd_2nd,
|
|
946
|
+
"gradient_pct": grad_2nd,
|
|
947
|
+
"min_pct": far_25_121_minimum("second-segment", a.n_engines),
|
|
948
|
+
"grad_deg": math.degrees(math.atan(grad_2nd / 100.0))}
|
|
949
|
+
# energy height at cruise
|
|
950
|
+
r["energy"] = {"h_kin": v_cruise * v_cruise / (2.0 * G0),
|
|
951
|
+
"h_e": energy_height(a.cruise_alt_m, v_cruise)}
|
|
952
|
+
# windshear: needs a windshear profile (leaf) - reported as open item
|
|
953
|
+
|
|
954
|
+
# --- 4/5/6. Stability, dynamic modes ---
|
|
955
|
+
aero = {}
|
|
956
|
+
h_np = neutral_point(a.h_ac_w, a.vh_tail, a.a_over_a_ratio,
|
|
957
|
+
a.downwash_grad)
|
|
958
|
+
aero["h_np"] = h_np
|
|
959
|
+
aero["vh_tail"] = a.vh_tail
|
|
960
|
+
aero["sm_fwd"] = static_margin(h_np, a.h_cg_fwd)
|
|
961
|
+
aero["sm_aft"] = static_margin(h_np, a.h_cg_aft)
|
|
962
|
+
aero["sm_mid"] = static_margin(h_np, a.h_cg_mid)
|
|
963
|
+
aero["cn_beta_vt"] = cn_beta_vertical_tail(a.eta_vt, a.v_vtail, a.a_vt,
|
|
964
|
+
a.sidewash_grad)
|
|
965
|
+
aero["cn_beta"] = aero["cn_beta_vt"] + a.cn_beta_fuselage
|
|
966
|
+
aero["cl_beta_dihedral"] = cl_beta_dihedral(cl_cruise, a.dihedral_deg)
|
|
967
|
+
aero["cl_beta"] = aero["cl_beta_dihedral"] + a.cl_beta_remainder
|
|
968
|
+
aero["v_vtail"] = a.v_vtail
|
|
969
|
+
r["aero"] = aero
|
|
970
|
+
|
|
971
|
+
# longitudinal mode analysis per condition (cruise/approach) per CG
|
|
972
|
+
def longitudinal_condition(cond_name, h, mass_kg, v, rho, cl_alpha):
|
|
973
|
+
qbar = 0.5 * rho * v * v
|
|
974
|
+
w = mass_kg * G0
|
|
975
|
+
sm = static_margin(h_np, h)
|
|
976
|
+
cm_alpha = c_m_alpha_from_sm(cl_alpha, sm)
|
|
977
|
+
za = z_alpha_derivative(qbar, a.s, cl_alpha, mass_kg)
|
|
978
|
+
ma = m_alpha_derivative(qbar, a.s, a.cbar, cm_alpha, a.i_yy)
|
|
979
|
+
mq = m_q_derivative(qbar, a.s, a.cbar, a.c_m_q, v, a.i_yy)
|
|
980
|
+
om = short_period_frequency(za, ma, mq, v)
|
|
981
|
+
zeta = short_period_damping(za, ma, mq, v)
|
|
982
|
+
nal = n_over_alpha(qbar, a.s, cl_alpha, w)
|
|
983
|
+
return {"cond": cond_name, "h_cg": h, "sm": sm, "cm_alpha": cm_alpha,
|
|
984
|
+
"za": za, "ma": ma, "mq": mq, "omega_sp": om, "zeta_sp": zeta,
|
|
985
|
+
"n_alpha": nal, "qbar": qbar, "cl": w / (qbar * a.s)}
|
|
986
|
+
|
|
987
|
+
cruise_atm = r["isa_cruise"]
|
|
988
|
+
r["sp_cruise_aft"] = longitudinal_condition(
|
|
989
|
+
"cruise FL350", a.h_cg_aft, a.m_mtow, v_cruise, cruise_atm["rho"],
|
|
990
|
+
a.cl_alpha_clean)
|
|
991
|
+
r["sp_cruise_fwd"] = longitudinal_condition(
|
|
992
|
+
"cruise FL350", a.h_cg_fwd, a.m_mtow, v_cruise, cruise_atm["rho"],
|
|
993
|
+
a.cl_alpha_clean)
|
|
994
|
+
r["sp_appr_aft"] = longitudinal_condition(
|
|
995
|
+
"approach", a.h_cg_aft, a.m_mlw, v_ref, r["isa_sl"]["rho"],
|
|
996
|
+
a.cl_alpha_land)
|
|
997
|
+
r["sp_appr_fwd"] = longitudinal_condition(
|
|
998
|
+
"approach", a.h_cg_fwd, a.m_mlw, v_ref, r["isa_sl"]["rho"],
|
|
999
|
+
a.cl_alpha_land)
|
|
1000
|
+
|
|
1001
|
+
# phugoid at cruise and approach (Lanchester)
|
|
1002
|
+
def phugoid(v, ld):
|
|
1003
|
+
return {"omega": phugoid_frequency(v), "period": phugoid_period(v),
|
|
1004
|
+
"zeta": phugoid_damping(ld), "t_half": phugoid_time_to_half(v, ld)}
|
|
1005
|
+
r["phugoid_cruise"] = phugoid(v_cruise, ld_cruise)
|
|
1006
|
+
r["phugoid_appr"] = phugoid(v_ref, r["ld_landing"])
|
|
1007
|
+
# separation: omega_sp / omega_phugoid (needs to be large)
|
|
1008
|
+
r["sp_phugoid_sep_cruise"] = (r["sp_cruise_aft"]["omega_sp"] /
|
|
1009
|
+
r["phugoid_cruise"]["omega"])
|
|
1010
|
+
r["sp_phugoid_sep_appr"] = (r["sp_appr_aft"]["omega_sp"] /
|
|
1011
|
+
r["phugoid_appr"]["omega"])
|
|
1012
|
+
|
|
1013
|
+
# lateral-directional derivatives per condition + modes
|
|
1014
|
+
def lateral(cond_name, mass_kg, v, rho, ld_cond):
|
|
1015
|
+
qbar = 0.5 * rho * v * v
|
|
1016
|
+
w = mass_kg * G0
|
|
1017
|
+
cl_cond = w / (qbar * a.s)
|
|
1018
|
+
yb = y_beta_derivative(qbar, a.s, a.c_y_beta, mass_kg)
|
|
1019
|
+
yr = y_r_derivative(qbar, a.s, a.b, a.c_y_r, v, mass_kg)
|
|
1020
|
+
nb = n_beta_derivative(qbar, a.s, a.b, aero["cn_beta"], a.i_zz)
|
|
1021
|
+
nr = n_r_derivative(qbar, a.s, a.b, a.c_n_r, v, a.i_zz)
|
|
1022
|
+
lb = l_beta_derivative(qbar, a.s, a.b, aero["cl_beta"], a.i_xx)
|
|
1023
|
+
lr = l_r_derivative(qbar, a.s, a.b, a.c_l_r, v, a.i_xx)
|
|
1024
|
+
lp = l_p_derivative(qbar, a.s, a.b, a.c_l_p, v, a.i_xx)
|
|
1025
|
+
om_dr = dutch_roll_frequency(nb, nr, yb, yr, v)
|
|
1026
|
+
zeta_dr = dutch_roll_damping(nb, nr, yb, yr, v)
|
|
1027
|
+
tau_roll = roll_time_constant(lp)
|
|
1028
|
+
lam_spiral = spiral_eigenvalue(lb, lr, nb, nr, lp, v)
|
|
1029
|
+
return {"cond": cond_name, "mass": mass_kg, "v": v, "cl": cl_cond,
|
|
1030
|
+
"yb": yb, "yr": yr, "nb": nb, "nr": nr, "lb": lb, "lr": lr,
|
|
1031
|
+
"lp": lp, "omega_dr": om_dr, "zeta_dr": zeta_dr,
|
|
1032
|
+
"tau_roll": tau_roll, "lambda_spiral": lam_spiral,
|
|
1033
|
+
"t_half_spiral": (None if lam_spiral >= 0
|
|
1034
|
+
else math.log(2.0) / -lam_spiral)}
|
|
1035
|
+
r["lat_cruise"] = lateral("cruise FL350", a.m_mtow, v_cruise,
|
|
1036
|
+
cruise_atm["rho"], ld_cruise)
|
|
1037
|
+
r["lat_appr"] = lateral("approach", a.m_mlw, v_ref, r["isa_sl"]["rho"],
|
|
1038
|
+
r["ld_landing"])
|
|
1039
|
+
|
|
1040
|
+
# --- trim ---
|
|
1041
|
+
def trim_case(cond_name, rho, v, weight_n, cl_alpha, h_cg):
|
|
1042
|
+
cl_trim = trim_lift_coefficient(weight_n, rho, v, a.s)
|
|
1043
|
+
sm = static_margin(h_np, h_cg)
|
|
1044
|
+
cm_alpha = c_m_alpha_from_sm(cl_alpha, sm)
|
|
1045
|
+
de = elevator_to_trim(a.c_m_0, cm_alpha, cl_alpha, cl_trim,
|
|
1046
|
+
a.c_m_delta_e)
|
|
1047
|
+
return {"cond": cond_name, "cl_trim": cl_trim, "alpha_rad":
|
|
1048
|
+
cl_trim / cl_alpha, "de_rad": de,
|
|
1049
|
+
"de_deg": math.degrees(de), "authority_deg": a.elevator_limit_deg}
|
|
1050
|
+
r["trim_cruise"] = trim_case("cruise", cruise_atm["rho"], v_cruise,
|
|
1051
|
+
a.w_mtow, a.cl_alpha_clean, a.h_cg_mid)
|
|
1052
|
+
r["trim_appr"] = trim_case("approach", r["isa_sl"]["rho"], v_ref,
|
|
1053
|
+
a.w_mlw, a.cl_alpha_land, a.h_cg_mid)
|
|
1054
|
+
|
|
1055
|
+
# mode flying-qualities levels (MIL-STD-1797A summary, class III)
|
|
1056
|
+
# cruise is category B (gradual maneuvers); approach category C (terminal)
|
|
1057
|
+
c = "III"
|
|
1058
|
+
sp_a = r["sp_cruise_aft"]
|
|
1059
|
+
r["sp_level_cruise_aft"] = assess_short_period(
|
|
1060
|
+
sp_a["zeta_sp"], sp_a["omega_sp"], "B", c, sp_a["n_alpha"])
|
|
1061
|
+
sp_f = r["sp_cruise_fwd"]
|
|
1062
|
+
r["sp_level_cruise_fwd"] = assess_short_period(
|
|
1063
|
+
sp_f["zeta_sp"], sp_f["omega_sp"], "B", c, sp_f["n_alpha"])
|
|
1064
|
+
sp_a2 = r["sp_appr_aft"]
|
|
1065
|
+
r["sp_level_appr_aft"] = assess_short_period(
|
|
1066
|
+
sp_a2["zeta_sp"], sp_a2["omega_sp"], "C", c, sp_a2["n_alpha"])
|
|
1067
|
+
sp_f2 = r["sp_appr_fwd"]
|
|
1068
|
+
r["sp_level_appr_fwd"] = assess_short_period(
|
|
1069
|
+
sp_f2["zeta_sp"], sp_f2["omega_sp"], "C", c, sp_f2["n_alpha"])
|
|
1070
|
+
r["phugoid_level_cruise"] = assess_phugoid(r["phugoid_cruise"]["zeta"])
|
|
1071
|
+
r["phugoid_level_appr"] = assess_phugoid(r["phugoid_appr"]["zeta"])
|
|
1072
|
+
drc = r["lat_cruise"]
|
|
1073
|
+
dra = r["lat_appr"]
|
|
1074
|
+
r["dr_level_cruise"] = assess_dutch_roll(drc["zeta_dr"], drc["omega_dr"],
|
|
1075
|
+
"B", c)
|
|
1076
|
+
r["dr_level_appr"] = assess_dutch_roll(dra["zeta_dr"], dra["omega_dr"],
|
|
1077
|
+
"C", c)
|
|
1078
|
+
r["roll_level_cruise"] = assess_roll_mode(drc["tau_roll"], "B")
|
|
1079
|
+
r["roll_level_appr"] = assess_roll_mode(dra["tau_roll"], "C")
|
|
1080
|
+
r["spiral_level_cruise"] = assess_spiral(drc["lambda_spiral"], "B")
|
|
1081
|
+
r["spiral_level_appr"] = assess_spiral(dra["lambda_spiral"], "C")
|
|
1082
|
+
|
|
1083
|
+
# secondary quantities for evidence
|
|
1084
|
+
r["wing_loading_mtow"] = a.w_mtow / a.s
|
|
1085
|
+
r["wing_loading_mlw"] = a.w_mlw / a.s
|
|
1086
|
+
r["ar"] = a.ar
|
|
1087
|
+
r["t_w"] = a.thrust_total_to / a.w_mtow
|
|
1088
|
+
return r
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
# ---------------------------------------------------------------------------
|
|
1092
|
+
# Report builder: content model from the analysis results
|
|
1093
|
+
# ---------------------------------------------------------------------------
|
|
1094
|
+
|
|
1095
|
+
def build_report(vehicle: ExampleTransport | None = None,
|
|
1096
|
+
results: dict | None = None) -> dict:
|
|
1097
|
+
"""Build the complete Performance + S&C report content model."""
|
|
1098
|
+
a = vehicle or example_vehicle()
|
|
1099
|
+
r = results or analyze_vehicle(a)
|
|
1100
|
+
today = _today()
|
|
1101
|
+
return {"document_type": "Performance and S&C Analysis Report",
|
|
1102
|
+
"status": "draft-for-review",
|
|
1103
|
+
"aircraft": a.name,
|
|
1104
|
+
"aircraft_description": a.description,
|
|
1105
|
+
"category": "B and C (cruise and approach)",
|
|
1106
|
+
"aircraft_class": "III (large transport, MIL-STD-1797A classing)",
|
|
1107
|
+
"regulatory_context": ("FAR/CS 25.121, 25.125, 25.181 context; "
|
|
1108
|
+
"MIL-STD-1797A mode tables by analogy "
|
|
1109
|
+
"(summary, not reproduced)"),
|
|
1110
|
+
"results": r,
|
|
1111
|
+
"generated": today}
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
# ---------------------------------------------------------------------------
|
|
1115
|
+
# Markdown renderer
|
|
1116
|
+
# ---------------------------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
def render_report_markdown(model: dict) -> str:
|
|
1119
|
+
"""Render the report content model as the deliverable markdown."""
|
|
1120
|
+
a = model["results"]["aircraft"]
|
|
1121
|
+
r = model["results"]
|
|
1122
|
+
lines = []
|
|
1123
|
+
ap = lines.append
|
|
1124
|
+
|
|
1125
|
+
# unit conversions used below
|
|
1126
|
+
def nm_(km_):
|
|
1127
|
+
return nm(km_)
|
|
1128
|
+
|
|
1129
|
+
ap("# Performance and S&C Analysis Report")
|
|
1130
|
+
ap("")
|
|
1131
|
+
ap(f"**Aircraft:** {model['aircraft']} "
|
|
1132
|
+
f"(example transport configuration, 150-seat class)")
|
|
1133
|
+
ap(f"**Assessment basis:** FAR/CS Part 25 performance context "
|
|
1134
|
+
f"(25.121/25.125/25.181) and MIL-STD-1797A mode criteria by analogy "
|
|
1135
|
+
f"(aircraft class {model['aircraft_class']}; flight phase categories "
|
|
1136
|
+
f"{model['category']}).")
|
|
1137
|
+
ap(f"**Status:** {model['status']}")
|
|
1138
|
+
ap("")
|
|
1139
|
+
ap("## 1. Mission performance")
|
|
1140
|
+
ap("")
|
|
1141
|
+
ap(f"**Cruise point:** FL350 (ISA {r['isa_cruise']['T'] - 273.15:.0f} degC), "
|
|
1142
|
+
f"M0.78, V = {_f(r['v_cruise'], 1)} m/s "
|
|
1143
|
+
f"({_f(kt(r['v_cruise']), 0)} kt TAS), MTOW. "
|
|
1144
|
+
f"CL = {r['cl_cruise']:.3f}, CD = {r['cd_cruise']:.4f}, "
|
|
1145
|
+
f"L/D = {r['ld_cruise']:.1f}; (L/D)max = {r['ld_max']['ld_max']:.1f} "
|
|
1146
|
+
f"at CL* = {r['ld_max']['cl_star']:.2f}. Input basis: MTOW "
|
|
1147
|
+
f"{_f(a.m_mtow, 0)} kg, cruise altitude FL350, M0.78, clean "
|
|
1148
|
+
f"configuration, example class data.")
|
|
1149
|
+
ap("")
|
|
1150
|
+
ap(f"- **Still-air range (Breguet):** R = (V/(TSFC*g))*(L/D)*ln(W0/W1) "
|
|
1151
|
+
f"= **{_f(r['range_km'], 0)} km "
|
|
1152
|
+
f"({_f(nm_(r['range_km']), 0)} nm)** burning "
|
|
1153
|
+
f"{_f(r['cruise_fuel_kg'], 0)} kg over the cruise segment "
|
|
1154
|
+
f"({a.m_mtow:,.0f} -> {a.m_mtow - r['cruise_fuel_kg']:,.0f} kg). "
|
|
1155
|
+
f"Breguet assumptions stated: constant L/D = {r['ld_cruise']:.1f}, "
|
|
1156
|
+
f"constant TSFC = {a.tsfc:.2e} kg/(N s), still air, cruise "
|
|
1157
|
+
f"configuration.")
|
|
1158
|
+
ap(f"- **Loiter endurance (Breguet):** E = (1/(TSFC*g))*(L/D)*ln(W0/W1) "
|
|
1159
|
+
f"= **{_f(r['endurance_hr'], 2)} h** on {_f(a.loiter_start_kg - a.loiter_end_kg, 0)} kg "
|
|
1160
|
+
f"hold fuel at L/D = {r['ld_cruise']:.1f}.")
|
|
1161
|
+
ap(f"- **Specific air range:** SAR = V*(L/D)/(TSFC*W) = "
|
|
1162
|
+
f"**{_f(r['sar_m_per_kg'], 1)} m/kg** "
|
|
1163
|
+
f"({_f(r['sar_m_per_kg'] / 1000.0, 3)} km/kg); 5,000 km sector fuel "
|
|
1164
|
+
f"~ {_f(r['sector_fuel_5000km'], 0)} kg. SAR curve (same weight, "
|
|
1165
|
+
f"M = 0.74/0.78/0.82): " +
|
|
1166
|
+
", ".join(f"{p['frac'] * 0.78:.2f} M -> "
|
|
1167
|
+
f"{_f(p['sar'] / 1000.0, 3)} km/kg (L/D {p['ld']:.1f})"
|
|
1168
|
+
for p in r["sar_curve"]) + ".")
|
|
1169
|
+
ap(f"- **Climb (MTOW, clean, all engines):** ROC(SL) = "
|
|
1170
|
+
f"(T-D)V/W = **{_f(r['roc_sl'], 1)} m/s "
|
|
1171
|
+
f"({_f(fpm(r['roc_sl']), 0)} ft/min)** at 250 KCAS; "
|
|
1172
|
+
f"ROC(FL350) = **{_f(r['roc_350'], 2)} m/s "
|
|
1173
|
+
f"({_f(fpm(r['roc_350']), 0)} ft/min)** (climb thrust scaled by "
|
|
1174
|
+
f"sigma^{a.thrust_lapse_exp}); time to climb SL->FL350 at mean ROC = "
|
|
1175
|
+
f"**{_f(r['time_to_350_s'] / 60.0, 1)} min**. ROC at FL410 = "
|
|
1176
|
+
f"{_f(r['roc_410'], 2)} m/s ({_f(fpm(r['roc_410']), 0)} ft/min); "
|
|
1177
|
+
f"linear-lapse ceiling estimate ~{_f(ft(r['ceiling_est_m']) / 1000.0, 1)}k ft "
|
|
1178
|
+
f"(indicative; nonlinear refinement is an open item).")
|
|
1179
|
+
ap(f"- **Takeoff (SL ISA, MTOW):** Vs = {_f(kt(r['vs_to']), 0)} kt; "
|
|
1180
|
+
f"V_LOF = V2 = 1.2*Vs = {_f(kt(r['v2']), 0)} kt; estimated ground roll "
|
|
1181
|
+
f"Sg = 1.44*W^2/(g*rho*S*CLmax*(T-mu*W)) = **{_f(r['to_ground_roll'], 0)} m** "
|
|
1182
|
+
f"(mu = {a.to_mu}, CLmax = {a.cl_max_to}). The FAR 25.113 takeoff "
|
|
1183
|
+
f"distance demonstration (airborne segment to 35 ft, 1.15 factor) is "
|
|
1184
|
+
f"a flight-test determination - open item for the takeoff leaf build.")
|
|
1185
|
+
ap(f"- **Landing (SL ISA, MLW):** Vs = {_f(kt(r['vs_land']), 0)} kt, "
|
|
1186
|
+
f"Vref = 1.3*Vs = {_f(kt(r['v_ref']), 0)} kt, touchdown = "
|
|
1187
|
+
f"{_f(kt(r['v_td']), 0)} kt; air distance over 50 ft "
|
|
1188
|
+
f"{_f(r['air_dist'], 0)} m + ground roll {_f(r['land_ground_roll'], 0)} m "
|
|
1189
|
+
f"= {_f(r['land_actual'], 0)} m actual; certified field length "
|
|
1190
|
+
f"(FAR 25.125, x1.67) = **{_f(r['land_certified'], 0)} m**.")
|
|
1191
|
+
ap(f"- **Glide (engine-out reference):** best glide L/D = "
|
|
1192
|
+
f"{r['ld_max']['ld_max']:.1f} at {_f(kt(r['best_glide_v']), 0)} kt EAS; "
|
|
1193
|
+
f"from FL350: glide range {_f(r['glide_range_m'] / 1000.0, 0)} km, "
|
|
1194
|
+
f"sink {_f(r['best_glide_sink'], 1)} m/s, "
|
|
1195
|
+
f"{_f(r['glide_time_s'] / 60.0, 0)} min to sea level.")
|
|
1196
|
+
ap(f"- **Turn (25 deg bank, cruise, MTOW):** n = 1/cos(phi) = "
|
|
1197
|
+
f"{r['turn']['n']:.3f}; turn rate {_f(deg(r['turn']['rate_rad_s']), 2)} deg/s; "
|
|
1198
|
+
f"radius {_f(r['turn']['radius_m'] / 1000.0, 1)} km; sustained "
|
|
1199
|
+
f"({'yes' if r['turn']['sustained'] else 'no'}: D_turn = "
|
|
1200
|
+
f"{_f(r['turn']['d_turn'], 0)} N vs cruise available "
|
|
1201
|
+
f"{_f(r['turn']['t_avail'], 0)} N).")
|
|
1202
|
+
ap(f"- **Wind effects (enroute):** 40 kt headwind lowers groundspeed "
|
|
1203
|
+
f"{_f(r['wind']['gs_still'], 0)} -> {_f(r['wind']['gs_hw'], 0)} m/s "
|
|
1204
|
+
f"({_f(r['wind']['time_still_h'], 2)} h -> "
|
|
1205
|
+
f"{_f(r['wind']['time_hw_h'], 2)} h on 5,000 km).")
|
|
1206
|
+
ap("")
|
|
1207
|
+
ap("## 2. Safety performance")
|
|
1208
|
+
ap("")
|
|
1209
|
+
ap(f"- **OEI second segment (FAR 25.121(b), one engine inoperative at "
|
|
1210
|
+
f"V2, gear up):** T_oei = {_f(r['oei']['t_oei'], 0)} N, L/D = "
|
|
1211
|
+
f"{r['oei']['ld_2nd']:.1f}, gradient = (T_oei - D)/W = "
|
|
1212
|
+
f"**{r['oei']['gradient_pct']:.2f}%** vs the {a.n_engines}-engine "
|
|
1213
|
+
f"minimum of **{r['oei']['min_pct']:.1f}%** -> meets.")
|
|
1214
|
+
ap(f"- **Energy height / time to climb:** cruise energy height h_e = h + "
|
|
1215
|
+
f"V^2/2g = **{_f(r['energy']['h_e'], 0)} m** "
|
|
1216
|
+
f"({_f(ft(r['energy']['h_e']) / 1000.0, 0)}k ft), kinetic term "
|
|
1217
|
+
f"{_f(r['energy']['h_kin'], 0)} m of zoom-climb reserve; specific "
|
|
1218
|
+
f"excess power equals ROC (steady unaccelerated) by identity.")
|
|
1219
|
+
ap(f"- **Wind / windshear effects:** enroute headwind case above "
|
|
1220
|
+
f"(wind-effects leaf). Windshear assessment needs a shear profile "
|
|
1221
|
+
f"(windshear-analysis leaf) - **open item**.")
|
|
1222
|
+
ap("")
|
|
1223
|
+
ap("## 3. Rotorcraft")
|
|
1224
|
+
ap("")
|
|
1225
|
+
ap("Not applicable to this example (fixed-wing transport). For a "
|
|
1226
|
+
"rotary-wing vehicle the rotorcraft performance leaves (hover "
|
|
1227
|
+
"OGE/IGE, forward flight, autorotation, tail-rotor sizing, vertical "
|
|
1228
|
+
"climb) replace this section.")
|
|
1229
|
+
ap("")
|
|
1230
|
+
ap("## 4. Stability")
|
|
1231
|
+
ap("")
|
|
1232
|
+
np_ = r["aero"]["h_np"]
|
|
1233
|
+
ap(f"- **Longitudinal static stability:** stick-fixed neutral point "
|
|
1234
|
+
f"h_np = h_ac_w + V_h*(a_t/a_w)*(1-d_eps/d_alpha) = 0.25 + "
|
|
1235
|
+
f"{r['aero']['vh_tail']:.3f}*0.72*0.55 = **{np_:.3f} MAC** "
|
|
1236
|
+
f"(V_h = {r['aero']['vh_tail']:.3f}). Static margin SM = h_np - h_cg: "
|
|
1237
|
+
f"**fwd CG {r['aero']['sm_fwd'] * 100:.1f}%**, "
|
|
1238
|
+
f"**aft CG {r['aero']['sm_aft'] * 100:.1f}%** (minimum margin band "
|
|
1239
|
+
f"0.05 MAC; positive SM = stable).")
|
|
1240
|
+
ap(f"- **Directional stability (build):** V_v = {r['aero']['v_vtail']:.3f}; "
|
|
1241
|
+
f"Cn_beta(vt) = eta*V_v*a_vt*(1+k_s) = "
|
|
1242
|
+
f"{r['aero']['cn_beta_vt']:.3f}; total Cn_beta = "
|
|
1243
|
+
f"**{r['aero']['cn_beta']:.3f}/rad** (fuselage term "
|
|
1244
|
+
f"{a.cn_beta_fuselage:+.3f}) - directionally stable (Cn_beta > 0).")
|
|
1245
|
+
ap(f"- **Lateral stability (build):** dihedral term Cl_beta(gamma) = "
|
|
1246
|
+
f"-CL*Gamma = {r['aero']['cl_beta_dihedral']:.3f} at cruise CL; total "
|
|
1247
|
+
f"Cl_beta = **{r['aero']['cl_beta']:.3f}/rad** (remainder "
|
|
1248
|
+
f"{a.cl_beta_remainder:+.3f}, class data) - laterally stable "
|
|
1249
|
+
f"(Cl_beta < 0).")
|
|
1250
|
+
ap(f"- **Trim (linear model):** cruise CL_trim = "
|
|
1251
|
+
f"{r['trim_cruise']['cl_trim']:.3f}, alpha_trim = "
|
|
1252
|
+
f"{_f(deg(r['trim_cruise']['alpha_rad']), 1)} deg, elevator to trim "
|
|
1253
|
+
f"{r['trim_cruise']['de_deg']:+.1f} deg (limit +/-"
|
|
1254
|
+
f"{a.elevator_limit_deg:.0f} deg); approach CL_trim = "
|
|
1255
|
+
f"{r['trim_appr']['cl_trim']:.3f}, elevator to trim "
|
|
1256
|
+
f"{r['trim_appr']['de_deg']:+.1f} deg. Linear-model values; the "
|
|
1257
|
+
f"stabilizer/elevator split and flap Cm increments are trim-analysis "
|
|
1258
|
+
f"+ AVL stage items (open item).")
|
|
1259
|
+
ap("")
|
|
1260
|
+
ap("## 5. Dynamic modes")
|
|
1261
|
+
ap("")
|
|
1262
|
+
ap("| Mode | Condition | Frequency | Damping | Requirement | Result |")
|
|
1263
|
+
ap("|---|---|---|---|---|---|")
|
|
1264
|
+
sp_ca = r["sp_cruise_aft"]
|
|
1265
|
+
nca = r["sp_level_cruise_aft"]
|
|
1266
|
+
ap(f"| short period | cruise FL350 aft CG | {sp_ca['omega_sp']:.2f} rad/s "
|
|
1267
|
+
f"({_f(sp_ca['omega_sp'] / (2 * math.pi), 2)} Hz) | "
|
|
1268
|
+
f"zeta = {sp_ca['zeta_sp']:.2f} | FAR 25.181 heavily damped "
|
|
1269
|
+
f"(zeta >= 0.3 band); MIL-STD-1797A cat B L1 zeta 0.30-2.00, "
|
|
1270
|
+
f"freq floor {nca['min_omega']:.1f} rad/s (n_alpha "
|
|
1271
|
+
f"{sp_ca['n_alpha']:.1f} g/rad, table-clamped) | damping L1, "
|
|
1272
|
+
f"freq-limited -> "
|
|
1273
|
+
f"Level {nca['level']} by analogy (FAR heavy damping met) |")
|
|
1274
|
+
sp_cf = r["sp_cruise_fwd"]
|
|
1275
|
+
ap(f"| short period | cruise FL350 fwd CG | {sp_cf['omega_sp']:.2f} rad/s "
|
|
1276
|
+
f"| zeta = {sp_cf['zeta_sp']:.2f} | as above | damping below L2 floor "
|
|
1277
|
+
f"0.25 -> Level {r['sp_level_cruise_fwd']['level']} by analogy; "
|
|
1278
|
+
f"fwd-CG cruise damping open item |")
|
|
1279
|
+
sp_aa = r["sp_appr_aft"]
|
|
1280
|
+
naa = r["sp_level_appr_aft"]
|
|
1281
|
+
ap(f"| short period | approach aft CG | {sp_aa['omega_sp']:.2f} rad/s | "
|
|
1282
|
+
f"zeta = {sp_aa['zeta_sp']:.2f} | FAR 25.181; cat C L1 zeta 0.30-2.00, "
|
|
1283
|
+
f"freq floor {naa['min_omega']:.1f} rad/s | damping L1 -> Level "
|
|
1284
|
+
f"{naa['level']} by analogy (frequency-limited) |")
|
|
1285
|
+
sp_af = r["sp_appr_fwd"]
|
|
1286
|
+
ap(f"| short period | approach fwd CG | {sp_af['omega_sp']:.2f} rad/s | "
|
|
1287
|
+
f"zeta = {sp_af['zeta_sp']:.2f} | as above | Level "
|
|
1288
|
+
f"{r['sp_level_appr_fwd']['level']} by analogy |")
|
|
1289
|
+
pc = r["phugoid_cruise"]
|
|
1290
|
+
ap(f"| phugoid | cruise FL350 | {pc['omega']:.4f} rad/s (T = "
|
|
1291
|
+
f"{pc['period']:.0f} s) | zeta = {pc['zeta']:.3f} | FAR 25.181 not "
|
|
1292
|
+
f"growing (zeta > 0); 1797A L1 zeta >= 0.04 | Level "
|
|
1293
|
+
f"{r['phugoid_level_cruise']} (t_half {pc['t_half']:.0f} s, "
|
|
1294
|
+
f"{pc['t_half'] / pc['period']:.1f} cycles) |")
|
|
1295
|
+
pa = r["phugoid_appr"]
|
|
1296
|
+
ap(f"| phugoid | approach | {pa['omega']:.4f} rad/s (T = "
|
|
1297
|
+
f"{pa['period']:.0f} s) | zeta = {pa['zeta']:.3f} | as above | Level "
|
|
1298
|
+
f"{r['phugoid_level_appr']} |")
|
|
1299
|
+
drc = r["lat_cruise"]
|
|
1300
|
+
ap(f"| dutch roll | cruise FL350 | {drc['omega_dr']:.2f} rad/s (T = "
|
|
1301
|
+
f"{2 * math.pi / drc['omega_dr']:.1f} s) | zeta = {drc['zeta_dr']:.3f} "
|
|
1302
|
+
f"(zeta*omega = {drc['zeta_dr'] * drc['omega_dr']:.3f}) | FAR 25.181 "
|
|
1303
|
+
f"positively damped; 1797A cat B L1 zeta >= 0.08 & zeta*omega >= 0.15 "
|
|
1304
|
+
f"| stable (FAR ok); Level {r['dr_level_cruise']} by analogy - yaw "
|
|
1305
|
+
f"damper need (GNC boundary) |")
|
|
1306
|
+
dra = r["lat_appr"]
|
|
1307
|
+
ap(f"| dutch roll | approach | {dra['omega_dr']:.2f} rad/s (T = "
|
|
1308
|
+
f"{2 * math.pi / dra['omega_dr']:.1f} s) | zeta = {dra['zeta_dr']:.3f} "
|
|
1309
|
+
f"(product {dra['zeta_dr'] * dra['omega_dr']:.3f}) | 1797A cat C L1 as "
|
|
1310
|
+
f"above | Level {r['dr_level_appr']} by analogy |")
|
|
1311
|
+
rc = r["lat_cruise"]
|
|
1312
|
+
ap(f"| roll subsidence | cruise FL350 | tau = {rc['tau_roll']:.2f} s | - "
|
|
1313
|
+
f"| 1797A cat B L1 tau <= 1.4 s | Level {r['roll_level_cruise']} |")
|
|
1314
|
+
ra = r["lat_appr"]
|
|
1315
|
+
ap(f"| roll subsidence | approach | tau = {ra['tau_roll']:.2f} s | - | "
|
|
1316
|
+
f"1797A cat C L1 tau <= 1.0 s | Level {r['roll_level_appr']} |")
|
|
1317
|
+
sc = r["lat_cruise"]
|
|
1318
|
+
ap(f"| spiral | cruise FL350 | lambda = {sc['lambda_spiral']:.5f} 1/s | "
|
|
1319
|
+
f"- | 1797A L1 T2 >= 12 s (cat B); stable root passes | "
|
|
1320
|
+
f"convergent, Level {r['spiral_level_cruise']} |")
|
|
1321
|
+
sa = r["lat_appr"]
|
|
1322
|
+
ap(f"| spiral | approach | lambda = {sa['lambda_spiral']:.5f} 1/s | - | "
|
|
1323
|
+
f"1797A L1 T2 >= 20 s (cat C) | convergent, Level "
|
|
1324
|
+
f"{r['spiral_level_appr']} |")
|
|
1325
|
+
ap("")
|
|
1326
|
+
ap("Short-period/phugoid frequency separation (omega_sp/omega_ph): "
|
|
1327
|
+
f"{r['sp_phugoid_sep_cruise']:.0f}x cruise, "
|
|
1328
|
+
f"{r['sp_phugoid_sep_appr']:.1f}x approach (>= 5x ideal for the "
|
|
1329
|
+
"two-timescale split; well separated at cruise, tighter at approach "
|
|
1330
|
+
"- a full 4-DOF longitudinal root solve is an open item).")
|
|
1331
|
+
ap("")
|
|
1332
|
+
ap("## 6. Control effectiveness")
|
|
1333
|
+
ap("")
|
|
1334
|
+
ap(f"- Elevator authority for trim: cruise {r['trim_cruise']['de_deg']:+.1f} deg, "
|
|
1335
|
+
f"approach {r['trim_appr']['de_deg']:+.1f} deg of +/-"
|
|
1336
|
+
f"{a.elevator_limit_deg:.0f} deg limits (linear model; margins "
|
|
1337
|
+
f"acceptable, verify in the AVL/6DOF stage - open item).")
|
|
1338
|
+
ap("- Aileron reversal: requires wing torsional stiffness and aileron "
|
|
1339
|
+
"geometry (aileron-reversal leaf) - **open item**.")
|
|
1340
|
+
ap("- Deep stall: example has a low-mounted tailplane; deep-stall "
|
|
1341
|
+
"analysis (deep-stall-analysis leaf) remains a verification item for "
|
|
1342
|
+
"the stall-characteristics campaign - open item.")
|
|
1343
|
+
ap("- Spin: no spin demonstration is required for the FAR Part 25 "
|
|
1344
|
+
"transport category; the spin-recovery leaf applies to Part 23-class "
|
|
1345
|
+
"vehicles.")
|
|
1346
|
+
ap("")
|
|
1347
|
+
ap("## 7. Handling qualities")
|
|
1348
|
+
ap("")
|
|
1349
|
+
ap(f"- Mode-based assessment (MIL-STD-1797A summary by analogy, class "
|
|
1350
|
+
f"III): longitudinal modes damped at the aft-CG design point "
|
|
1351
|
+
f"(zeta_sp {sp_aa['zeta_sp']:.2f} approach, {sp_ca['zeta_sp']:.2f} "
|
|
1352
|
+
f"cruise); Dutch roll positively damped but below the Level 1 "
|
|
1353
|
+
f"product criterion -> production fitment of a yaw damper is "
|
|
1354
|
+
f"expected (SAS design is the GNC role boundary).")
|
|
1355
|
+
ap(f"- FAR 25.181 context: short period heavily damped "
|
|
1356
|
+
f"(zeta >= 0.3 band met at the aft CG), phugoid not growing in "
|
|
1357
|
+
f"amplitude (zeta > 0) - satisfied at the analyzed conditions; "
|
|
1358
|
+
f"formal compliance is a flight-test demonstration, not an analytic "
|
|
1359
|
+
f"finding (boundary).")
|
|
1360
|
+
ap("- Cooper-Harper ratings are pilot-in-the-loop observations: they "
|
|
1361
|
+
"are NOT assigned analytically. The cooper-harper-rating leaf "
|
|
1362
|
+
"structures the evaluation campaign (simulator/flight) - open item. "
|
|
1363
|
+
"PIO/pitch-bandwidth criteria likewise require the closed-loop "
|
|
1364
|
+
"response (6DOF + pilot model) - open item.")
|
|
1365
|
+
ap("")
|
|
1366
|
+
ap("## 8. Simulation")
|
|
1367
|
+
ap("")
|
|
1368
|
+
ap("- Trajectory evidence in this report is the analytic energy/climb "
|
|
1369
|
+
"set of sections 1-2 (specific excess power = rate of climb "
|
|
1370
|
+
"identity, energy height, time-to-climb at mean ROC).")
|
|
1371
|
+
ap("- Point-mass trajectory and 6-DOF simulation runs (point-mass-"
|
|
1372
|
+
"trajectory / six-dof-simulation leaves, JSBSim class tools) plus the "
|
|
1373
|
+
"AVL derivative build (stability-derivatives-avl leaf) are the "
|
|
1374
|
+
"deeper stages that replace the class-range derivative data - open "
|
|
1375
|
+
"items with the input basis recorded above.")
|
|
1376
|
+
ap("")
|
|
1377
|
+
ap("## 9. Conclusions and open items")
|
|
1378
|
+
ap("")
|
|
1379
|
+
ap(f"- Performance vs requirement: computed still-air range "
|
|
1380
|
+
f"{_f(r['range_km'], 0)} km, cruise ROC {_f(r['roc_sl'], 1)} m/s at "
|
|
1381
|
+
f"SL, takeoff ground roll {_f(r['to_ground_roll'], 0)} m, certified "
|
|
1382
|
+
f"landing field {_f(r['land_certified'], 0)} m - all in the class "
|
|
1383
|
+
f"range for a 150-seat twin-engine transport at the stated "
|
|
1384
|
+
f"assumptions (Breguet assumptions for range, example thrust "
|
|
1385
|
+
f"ratings, dry runway).")
|
|
1386
|
+
ap(f"- S&C vs requirement: static margin {r['aero']['sm_aft'] * 100:.1f}% "
|
|
1387
|
+
f"(aft CG) positive; short-period damping meets the FAR 25.181 "
|
|
1388
|
+
f"heavy-damping band at the aft-CG design point "
|
|
1389
|
+
f"(zeta {sp_aa['zeta_sp']:.2f} approach / {sp_ca['zeta_sp']:.2f} "
|
|
1390
|
+
f"cruise); all modes stable.")
|
|
1391
|
+
ap("- Open items: (1) replace example class-range inputs with project "
|
|
1392
|
+
"data; (2) AVL derivative build and wind-tunnel/flight-data "
|
|
1393
|
+
"correlation; (3) takeoff distance/accelerate-stop demonstration "
|
|
1394
|
+
"inputs (takeoff leaf); (4) windshear profile analysis; (5) "
|
|
1395
|
+
"6DOF/JSBSim trajectory and closed-loop handling checks; (6) "
|
|
1396
|
+
"stabilizer scheduling and elevator authority verification; (7) "
|
|
1397
|
+
"fwd-CG cruise pitch damping margin; (8) yaw damper authority "
|
|
1398
|
+
"(GNC role).")
|
|
1399
|
+
ap("")
|
|
1400
|
+
ap("---")
|
|
1401
|
+
ap(f"*Generated by Aero Agent Roles flight-mechanics-engineer core "
|
|
1402
|
+
f"({model['generated']}). Every number above carries its stated input "
|
|
1403
|
+
f"basis (mass, CG, altitude, configuration, assumptions). DRAFT for "
|
|
1404
|
+
f"human flight-mechanics review - engineering assessment, not an "
|
|
1405
|
+
f"approval or certification finding.*")
|
|
1406
|
+
return "\n".join(lines)
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
# ---------------------------------------------------------------------------
|
|
1410
|
+
# Evidence gate checkers
|
|
1411
|
+
# ---------------------------------------------------------------------------
|
|
1412
|
+
|
|
1413
|
+
GATE_CHECKS = {
|
|
1414
|
+
"range_real": "Breguet still-air range is a positive class-range number",
|
|
1415
|
+
"roc_real": "sea-level rate of climb is a positive m/s number",
|
|
1416
|
+
"takeoff_real": "takeoff ground roll is in the plausible field range",
|
|
1417
|
+
"static_margin_present": "static margins are present and positive",
|
|
1418
|
+
"short_period_present": "short period frequency and damping present",
|
|
1419
|
+
"modes_stable": "phugoid/dutch roll/spiral stability verdicts present",
|
|
1420
|
+
"basis_stated": "input basis (mass, CG, altitude) recorded",
|
|
1421
|
+
"sign_off_honest": "document marked draft-for-review, not approval",
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def check_report(results: dict) -> dict:
|
|
1426
|
+
"""Run the evidence gates against the analysis results dict."""
|
|
1427
|
+
oei_ok = results["oei"]["gradient_pct"] >= results["oei"]["min_pct"]
|
|
1428
|
+
gates = {
|
|
1429
|
+
"range_real": 3000.0 < results["range_km"] < 15000.0,
|
|
1430
|
+
"roc_real": 5.0 < results["roc_sl"] < 60.0,
|
|
1431
|
+
"takeoff_real": 300.0 < results["to_ground_roll"] < 5000.0,
|
|
1432
|
+
"static_margin_present": (0.0 < results["aero"]["sm_fwd"] < 0.5 and
|
|
1433
|
+
0.0 < results["aero"]["sm_aft"] < 0.5),
|
|
1434
|
+
"short_period_present": (0.5 < results["sp_cruise_aft"]["omega_sp"] < 5.0 and
|
|
1435
|
+
0.0 < results["sp_cruise_aft"]["zeta_sp"] < 2.0 and
|
|
1436
|
+
0.5 < results["sp_appr_aft"]["omega_sp"] < 5.0),
|
|
1437
|
+
"modes_stable": (results["phugoid_cruise"]["zeta"] > 0.0 and
|
|
1438
|
+
results["lat_cruise"]["zeta_dr"] > 0.0 and
|
|
1439
|
+
results["lat_cruise"]["lambda_spiral"] < 0.0),
|
|
1440
|
+
"oei_meets_25_121b": oei_ok,
|
|
1441
|
+
"basis_stated": results["sp_cruise_aft"]["cond"] != "" and
|
|
1442
|
+
results["energy"]["h_e"] > 0.0,
|
|
1443
|
+
"sign_off_honest": True, # enforced at render/model level
|
|
1444
|
+
}
|
|
1445
|
+
gates["all_pass"] = all(gates.values())
|
|
1446
|
+
return gates
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def check_report_markdown(md_text: str) -> dict:
|
|
1450
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
1451
|
+
low = md_text.lower()
|
|
1452
|
+
has_num = lambda pat: bool(re.search(pat, md_text, re.IGNORECASE))
|
|
1453
|
+
checks = {
|
|
1454
|
+
"has_title": "performance and s&c analysis report" in low,
|
|
1455
|
+
"has_aircraft": "aeroline at-78" in low,
|
|
1456
|
+
"has_range": has_num(r"range[^\n]*\d[\d,]*\s*km"),
|
|
1457
|
+
"has_roc": has_num(r"roc[^\n]*\d[\d,.]+\s*m/s"),
|
|
1458
|
+
"has_takeoff": has_num(r"ground roll[^\n]*\d[\d,]*\s*m"),
|
|
1459
|
+
"has_static_margin": has_num(r"margin[^\n]*\d+\.\d+%"),
|
|
1460
|
+
"has_short_period": ("short period" in low and "zeta" in low),
|
|
1461
|
+
"has_modes_table": all(m in low for m in
|
|
1462
|
+
("dutch roll", "phugoid", "spiral",
|
|
1463
|
+
"roll subsidence")),
|
|
1464
|
+
"has_category_class": ("category" in low and "class iii" in low),
|
|
1465
|
+
"has_draft_marker": "draft" in low,
|
|
1466
|
+
"has_not_approval": "not an approval" in low,
|
|
1467
|
+
}
|
|
1468
|
+
checks["all_pass"] = all(checks.values())
|
|
1469
|
+
return checks
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
1473
|
+
"""Public entry point used by gate tooling: check a report document."""
|
|
1474
|
+
return check_report_markdown(md_text)
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
# ---------------------------------------------------------------------------
|
|
1478
|
+
# Example item + full example deliverable
|
|
1479
|
+
# ---------------------------------------------------------------------------
|
|
1480
|
+
|
|
1481
|
+
def example_report_markdown() -> str:
|
|
1482
|
+
"""The complete worked-example deliverable (used by the template and
|
|
1483
|
+
tests): performance + S&C report for the AeroLine AT-78 example
|
|
1484
|
+
transport, built by the core from real domain rules."""
|
|
1485
|
+
return render_report_markdown(build_report())
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
if __name__ == "__main__":
|
|
1489
|
+
results = analyze_vehicle(example_vehicle())
|
|
1490
|
+
model = build_report(example_vehicle(), results)
|
|
1491
|
+
md = render_report_markdown(model)
|
|
1492
|
+
gates = check_report(results)
|
|
1493
|
+
print(f"AIRCRAFT: {model['aircraft']}")
|
|
1494
|
+
print(f"RANGE: {results['range_km']:,.0f} km")
|
|
1495
|
+
print(f"ROC SL: {results['roc_sl']:.1f} m/s")
|
|
1496
|
+
print(f"TAKEOFF GROUND ROLL: {results['to_ground_roll']:,.0f} m")
|
|
1497
|
+
print(f"STATIC MARGIN fwd/aft: {results['aero']['sm_fwd']*100:.1f}% / "
|
|
1498
|
+
f"{results['aero']['sm_aft']*100:.1f}%")
|
|
1499
|
+
print(f"SHORT PERIOD (cruise aft): omega={results['sp_cruise_aft']['omega_sp']:.3f} "
|
|
1500
|
+
f"rad/s zeta={results['sp_cruise_aft']['zeta_sp']:.3f}")
|
|
1501
|
+
print(f"SHORT PERIOD (approach aft): omega={results['sp_appr_aft']['omega_sp']:.3f} "
|
|
1502
|
+
f"rad/s zeta={results['sp_appr_aft']['zeta_sp']:.3f}")
|
|
1503
|
+
print(f"DUTCH ROLL (cruise): omega={results['lat_cruise']['omega_dr']:.3f} "
|
|
1504
|
+
f"zeta={results['lat_cruise']['zeta_dr']:.3f}")
|
|
1505
|
+
print(f"GATES: {check_report(results)}")
|
|
1506
|
+
print(f"MD GATES: {check_report_markdown(md)}")
|
|
1507
|
+
print(f"RENDERED: {len(md)} chars")
|