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,1218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""aerodynamics_core.py - Aerodynamics Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given an aircraft configuration's project
|
|
5
|
+
facts it computes the aerodynamic design numbers with REAL domain
|
|
6
|
+
formulas and BUILDS the aerodynamic design report. Standalone: no
|
|
7
|
+
external repo needed (stdlib math only, offline).
|
|
8
|
+
|
|
9
|
+
Every formula below mirrors the logic of the bound Aero Agent Skills
|
|
10
|
+
leaves (which encode public-domain aero practice; summaries only, never
|
|
11
|
+
reproduced text):
|
|
12
|
+
|
|
13
|
+
- Parasite drag buildup (aerodynamics/drag-polars/parasite-drag):
|
|
14
|
+
Re = rho*V*L/mu
|
|
15
|
+
Cf_laminar = 1.328/sqrt(Re) (Blasius)
|
|
16
|
+
Cf_turbulent = 0.455/(log10(Re))^2.58 (Schlichting)
|
|
17
|
+
Cf_mixed = Cf_t(Re) - (Re_tr/Re)*(Cf_t(Re_tr) - Cf_l(Re_tr))
|
|
18
|
+
FF wing/tail = 1 + 2(t/c) + 100(t/c)^4
|
|
19
|
+
FF fuselage = 1 + 60/(l/d)^3 + 0.0025*(l/d)
|
|
20
|
+
FF nacelle = 1 + 0.35/(l/d)
|
|
21
|
+
CD_i = Cf * FF * Q * S_wet_i / S_ref; CD_parasite = sum(CD_i)
|
|
22
|
+
S_wet_wing = 2 * S_exposed * (1 + 0.2 t/c)
|
|
23
|
+
Cf_e = CD0 * S_ref / S_wet_total
|
|
24
|
+
- Drag polar (aerodynamics/drag-polars/drag-polar):
|
|
25
|
+
k = 1/(pi*e*AR); CD = CD0 + k*CL^2; cl_opt = sqrt(CD0/k)
|
|
26
|
+
L/D_max = 1/(2*sqrt(CD0*k))
|
|
27
|
+
- Lift curve slope (aerodynamics/drag-polars/lift-curve-slope):
|
|
28
|
+
a0 = 2*pi (thin airfoil); a = a0/(1 + a0/(pi*e*AR)) (lifting line)
|
|
29
|
+
a_swept = a*cos(sweep); a_mach = a/sqrt(1-M^2) (P-G, M < 0.7)
|
|
30
|
+
- Sweep effects (aerodynamics/high-speed/swept-wing-aerodynamics):
|
|
31
|
+
M_eff = M*cos(Lambda)
|
|
32
|
+
- High-lift (aerodynamics/high-lift/high-lift-systems):
|
|
33
|
+
Delta_clmax = ref * K_delta * K_chord * K_span,
|
|
34
|
+
K_delta = sin(delta)/sin(delta_max); wing CLmax = 0.9*clmax*cos(sweep)
|
|
35
|
+
V_stall = sqrt(2W/(rho*S*CLmax))
|
|
36
|
+
- Transonic (aerodynamics/high-speed/supercritical-airfoil):
|
|
37
|
+
M_DD = 0.95 - t/c - C_L/10 (supercritical), 0.90 - t/c - C_L/10 (conv.)
|
|
38
|
+
wave-drag penalty ~ (M - M_DD)^3 above divergence, 0 below
|
|
39
|
+
- Flutter margin (aerodynamics/aeroelasticity/flutter-speed-prediction):
|
|
40
|
+
margin = V_F/V_D, clearance practice threshold 1.15
|
|
41
|
+
- Static divergence (aerodynamics/aeroelasticity/divergence-speed):
|
|
42
|
+
q_div = k_theta/(S*c*C_Lalpha*e); V_div = sqrt(2 q_div / rho)
|
|
43
|
+
- CFD validation metrics (aerodynamics/cfd/cfd-validation):
|
|
44
|
+
relative_error, Richardson extrapolation with Roache GCI (F=1.25),
|
|
45
|
+
PASS/FAIL verdict vs tolerance band
|
|
46
|
+
- Discrete-gust screening (aerodynamics/aeroelasticity/
|
|
47
|
+
aeroelastic-gust-response): quasi-steady section gust lift
|
|
48
|
+
Delta_CL = a_wing*(U_de/V), Delta n = (0.5*rho*V*a*U_de)/(W/S)
|
|
49
|
+
- ISA atmosphere: standard lapse-rate model (public domain), Sutherland
|
|
50
|
+
viscosity.
|
|
51
|
+
|
|
52
|
+
Deliverable: the aerodynamic design report (render_*_markdown), with
|
|
53
|
+
evidence gates (check_report / check_report_markdown) that verify the
|
|
54
|
+
document before it leaves the role. Every margin row in the report
|
|
55
|
+
traces to a stage output of the bound workflow.
|
|
56
|
+
"""
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import math
|
|
60
|
+
import re
|
|
61
|
+
from dataclasses import dataclass, field
|
|
62
|
+
from datetime import date
|
|
63
|
+
from typing import Dict, List, Optional
|
|
64
|
+
|
|
65
|
+
def _today() -> str:
|
|
66
|
+
import os
|
|
67
|
+
from datetime import date
|
|
68
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Physical / ISA constants (public domain)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
ISA_T0 = 288.15 # K, sea-level temperature
|
|
74
|
+
ISA_P0 = 101325.0 # Pa, sea-level pressure
|
|
75
|
+
ISA_RHO0 = 1.225 # kg/m^3, sea-level density
|
|
76
|
+
ISA_LAPSE = 0.0065 # K/m, troposphere lapse rate
|
|
77
|
+
ISA_TROPOPAUSE = 11000.0 # m
|
|
78
|
+
GAMMA = 1.4 # specific-heat ratio of air
|
|
79
|
+
R_GAS = 287.052874 # J/(kg K), gas constant of air
|
|
80
|
+
G0 = 9.80665 # m/s^2
|
|
81
|
+
SUTHERLAND_MU0 = 1.458e-6 # Sutherland law constant (kg/(m s K^1.5))
|
|
82
|
+
SUTHERLAND_S = 110.4 # Sutherland reference temperature (K)
|
|
83
|
+
|
|
84
|
+
# AeroSkills leaf constants mirrored here (see module docstring).
|
|
85
|
+
SECTION_SLOPE = 2.0 * math.pi # thin-airfoil section slope, per rad
|
|
86
|
+
MAX_MACH_PG = 0.7 # Prandtl-Glauert subsonic validity limit
|
|
87
|
+
FLUTTER_MARGIN_REQUIRED = 1.15 # clearance practice, flutter leaf
|
|
88
|
+
MIN_DIVERGENCE_MARGIN = 1.15 # design practice, divergence leaf
|
|
89
|
+
WING_CLMAX_FACTOR = 0.9 # 3D/sweep reduction, high-lift leaf
|
|
90
|
+
GCI_SAFETY_FACTOR = 1.25 # Roache GCI safety factor, cfd-validation
|
|
91
|
+
DEFAULT_VALIDATION_TOL = 0.05 # 5% default band, cfd-validation leaf
|
|
92
|
+
|
|
93
|
+
# High-lift reference flap data (high-lift-systems leaf).
|
|
94
|
+
FLAP_TYPES = {
|
|
95
|
+
"plain": {"delta_clmax_ref": 0.9, "deflection_max_deg": 60.0,
|
|
96
|
+
"chord_frac_ref": 0.20, "cd0_ref": 0.05, "cp_frac": 0.50},
|
|
97
|
+
"split": {"delta_clmax_ref": 0.9, "deflection_max_deg": 60.0,
|
|
98
|
+
"chord_frac_ref": 0.20, "cd0_ref": 0.06, "cp_frac": 0.50},
|
|
99
|
+
"slotted": {"delta_clmax_ref": 1.3, "deflection_max_deg": 40.0,
|
|
100
|
+
"chord_frac_ref": 0.25, "cd0_ref": 0.08, "cp_frac": 0.52},
|
|
101
|
+
"fowler": {"delta_clmax_ref": 1.7, "deflection_max_deg": 40.0,
|
|
102
|
+
"chord_frac_ref": 0.30, "cd0_ref": 0.09, "cp_frac": 0.58},
|
|
103
|
+
}
|
|
104
|
+
LEADING_EDGE_DEVICES = {"slat": 0.5, "krueger": 0.4}
|
|
105
|
+
|
|
106
|
+
# CFD validation case catalog (cfd-validation leaf, summary data).
|
|
107
|
+
VALIDATION_CASES = {
|
|
108
|
+
"onera-m6": {
|
|
109
|
+
"name": "ONERA M6 wing",
|
|
110
|
+
"regime": "transonic", "application": "wing",
|
|
111
|
+
"conditions": {"mach": 0.84, "reynolds": 11.72e6, "alpha_deg": 3.06},
|
|
112
|
+
"reference": {"cd": 0.0163, "cl": 0.266},
|
|
113
|
+
"data_source": "AGARD-AR-138 (Schmitt and Charpin 1979), summary",
|
|
114
|
+
},
|
|
115
|
+
"dlr-f6": {
|
|
116
|
+
"name": "DLR-F6 wing-body transport",
|
|
117
|
+
"regime": "transonic", "application": "wing-body",
|
|
118
|
+
"conditions": {"mach": 0.75, "reynolds": 3.0e6, "cl_target": 0.5},
|
|
119
|
+
"reference": {"cd": 0.0299, "cl": 0.5},
|
|
120
|
+
"data_source": "AIAA Drag Prediction Workshop series, summary",
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Validation helpers
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _require_positive(name: str, value: float) -> None:
|
|
129
|
+
if not (isinstance(value, (int, float)) and value > 0.0):
|
|
130
|
+
raise ValueError("%s must be positive, got %r" % (name, value))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _require_nonnegative(name: str, value: float) -> None:
|
|
134
|
+
if not (isinstance(value, (int, float)) and value >= 0.0):
|
|
135
|
+
raise ValueError("%s must be >= 0, got %r" % (name, value))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# ISA standard atmosphere (public-domain lapse-rate model)
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def isa_temperature(altitude_m: float) -> float:
|
|
143
|
+
"""ISA temperature (K) at geometric altitude (m), troposphere+stratosphere."""
|
|
144
|
+
_require_nonnegative("altitude", altitude_m)
|
|
145
|
+
if altitude_m <= ISA_TROPOPAUSE:
|
|
146
|
+
return ISA_T0 - ISA_LAPSE * altitude_m
|
|
147
|
+
return ISA_T0 - ISA_LAPSE * ISA_TROPOPAUSE # 216.65 K isothermal
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def isa_pressure(altitude_m: float) -> float:
|
|
151
|
+
"""ISA static pressure (Pa) at altitude (m)."""
|
|
152
|
+
_require_nonnegative("altitude", altitude_m)
|
|
153
|
+
t = isa_temperature(altitude_m)
|
|
154
|
+
if altitude_m <= ISA_TROPOPAUSE:
|
|
155
|
+
return ISA_P0 * (t / ISA_T0) ** (G0 / (ISA_LAPSE * R_GAS))
|
|
156
|
+
p_tropo = ISA_P0 * (216.65 / ISA_T0) ** (G0 / (ISA_LAPSE * R_GAS))
|
|
157
|
+
return p_tropo * math.exp(-G0 * (altitude_m - ISA_TROPOPAUSE) / (R_GAS * 216.65))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def isa_density(altitude_m: float) -> float:
|
|
161
|
+
"""ISA air density (kg/m^3) from the perfect-gas law."""
|
|
162
|
+
_require_nonnegative("altitude", altitude_m)
|
|
163
|
+
return isa_pressure(altitude_m) / (R_GAS * isa_temperature(altitude_m))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def speed_of_sound(temperature_k: float) -> float:
|
|
167
|
+
"""Speed of sound a = sqrt(gamma*R*T) in m/s."""
|
|
168
|
+
_require_positive("temperature", temperature_k)
|
|
169
|
+
return math.sqrt(GAMMA * R_GAS * temperature_k)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def air_viscosity(temperature_k: float) -> float:
|
|
173
|
+
"""Dynamic viscosity mu (kg/(m s)) by Sutherland's law."""
|
|
174
|
+
_require_positive("temperature", temperature_k)
|
|
175
|
+
return (SUTHERLAND_MU0 * temperature_k ** 1.5) / (temperature_k + SUTHERLAND_S)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def atmosphere(altitude_m: float) -> Dict[str, float]:
|
|
179
|
+
"""ISA atmosphere state at altitude: T, p, rho, a, mu (SI units)."""
|
|
180
|
+
_require_nonnegative("altitude", altitude_m)
|
|
181
|
+
t = isa_temperature(altitude_m)
|
|
182
|
+
p = isa_pressure(altitude_m)
|
|
183
|
+
rho = isa_density(altitude_m)
|
|
184
|
+
a = speed_of_sound(t)
|
|
185
|
+
mu = air_viscosity(t)
|
|
186
|
+
return {"temperature": t, "pressure": p, "density": rho,
|
|
187
|
+
"speed_of_sound": a, "viscosity": mu}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Reynolds number and skin friction (parasite-drag leaf)
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def reynolds(rho: float, v: float, l: float, mu: float) -> float:
|
|
195
|
+
"""Reynolds number Re = rho * V * L / mu for a component of length L."""
|
|
196
|
+
_require_positive("rho", rho)
|
|
197
|
+
_require_positive("v", v)
|
|
198
|
+
_require_positive("l", l)
|
|
199
|
+
_require_positive("mu", mu)
|
|
200
|
+
return rho * v * l / mu
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cf_laminar(re: float) -> float:
|
|
204
|
+
"""Laminar flat-plate skin friction Cf = 1.328/sqrt(Re) (Blasius)."""
|
|
205
|
+
_require_positive("Reynolds number", re)
|
|
206
|
+
return 1.328 / math.sqrt(re)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cf_turbulent(re: float) -> float:
|
|
210
|
+
"""Turbulent flat-plate skin friction Cf = 0.455/(log10 Re)^2.58."""
|
|
211
|
+
if re <= 10.0:
|
|
212
|
+
raise ValueError("Reynolds number must be > 10 for turbulent Cf, got %r" % (re,))
|
|
213
|
+
return 0.455 / (math.log10(re) ** 2.58)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cf_mixed(re: float, re_transition: float) -> float:
|
|
217
|
+
"""Mixed laminar-turbulent skin friction with transition at Re_tr.
|
|
218
|
+
|
|
219
|
+
Cf = Cf_turb(Re) - (Re_tr/Re) * (Cf_turb(Re_tr) - Cf_lam(Re_tr)).
|
|
220
|
+
"""
|
|
221
|
+
if re <= 10.0:
|
|
222
|
+
raise ValueError("Reynolds number must be > 10, got %r" % (re,))
|
|
223
|
+
if re_transition <= 10.0 or re_transition >= re:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"transition Reynolds number must satisfy 10 < Re_tr < Re, "
|
|
226
|
+
"got Re_tr=%r, Re=%r" % (re_transition, re)
|
|
227
|
+
)
|
|
228
|
+
return (cf_turbulent(re)
|
|
229
|
+
- (re_transition / re) * (cf_turbulent(re_transition) - cf_laminar(re_transition)))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def form_factor(kind: str, t_over_c: Optional[float] = None,
|
|
233
|
+
l_over_d: Optional[float] = None) -> float:
|
|
234
|
+
"""Component form factor FF (parasite-drag leaf formulas)."""
|
|
235
|
+
kind = str(kind).lower()
|
|
236
|
+
if kind in ("wing", "tail"):
|
|
237
|
+
if t_over_c is None or t_over_c <= 0.0 or t_over_c >= 0.5:
|
|
238
|
+
raise ValueError("t_over_c must be in (0, 0.5) for wing/tail, got %r" % (t_over_c,))
|
|
239
|
+
return 1.0 + 2.0 * t_over_c + 100.0 * t_over_c ** 4
|
|
240
|
+
if kind in ("fuselage", "nacelle"):
|
|
241
|
+
if l_over_d is None or l_over_d <= 1.0:
|
|
242
|
+
raise ValueError("l_over_d must be > 1.0 for fuselage/nacelle, got %r" % (l_over_d,))
|
|
243
|
+
if kind == "fuselage":
|
|
244
|
+
return 1.0 + 60.0 / (l_over_d ** 3) + 0.0025 * l_over_d
|
|
245
|
+
return 1.0 + 0.35 / l_over_d
|
|
246
|
+
raise ValueError("unknown component kind %r (use wing, tail, fuselage, nacelle)" % (kind,))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def component_drag(cf: float, ff: float, q: float, s_wet: float, s_ref: float) -> float:
|
|
250
|
+
"""Component parasite drag CD_i = Cf * FF * Q * S_wet / S_ref."""
|
|
251
|
+
_require_positive("skin-friction coefficient", cf)
|
|
252
|
+
if ff < 1.0:
|
|
253
|
+
raise ValueError("form factor must be >= 1, got %r" % (ff,))
|
|
254
|
+
if q < 1.0:
|
|
255
|
+
raise ValueError("interference factor must be >= 1, got %r" % (q,))
|
|
256
|
+
_require_positive("wetted area", s_wet)
|
|
257
|
+
_require_positive("reference area", s_ref)
|
|
258
|
+
return cf * ff * q * s_wet / s_ref
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def wing_wetted_area(s_exposed: float, t_over_c: float) -> float:
|
|
262
|
+
"""Wing wetted area S_wet = 2 * S_exposed * (1 + 0.2*t/c)."""
|
|
263
|
+
_require_positive("exposed area", s_exposed)
|
|
264
|
+
if t_over_c <= 0.0 or t_over_c >= 0.5:
|
|
265
|
+
raise ValueError("t_over_c must be in (0, 0.5), got %r" % (t_over_c,))
|
|
266
|
+
return 2.0 * s_exposed * (1.0 + 0.2 * t_over_c)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def equivalent_skin_friction(cd0: float, s_ref: float, s_wet_total: float) -> float:
|
|
270
|
+
"""Equivalent skin friction Cf_e = CD0 * S_ref / S_wet_total."""
|
|
271
|
+
_require_nonnegative("total parasite drag", cd0)
|
|
272
|
+
_require_positive("reference area", s_ref)
|
|
273
|
+
_require_positive("total wetted area", s_wet_total)
|
|
274
|
+
return cd0 * s_ref / s_wet_total
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def parasite_drag(components: List[dict], s_ref: float, rho: float, v: float,
|
|
278
|
+
mu: float, re_transition: float = 5e5) -> Dict:
|
|
279
|
+
"""Assemble the parasite-drag buildup from component geometry.
|
|
280
|
+
|
|
281
|
+
Each component dict: {name, kind ('wing'|'tail'|'fuselage'|'nacelle'),
|
|
282
|
+
length (m, for Re), s_wet (m^2), t_over_c or l_over_d, q (interference),
|
|
283
|
+
count (optional, default 1)}. All sub-formulas are the parasite-drag
|
|
284
|
+
leaf formulas; total = sum of CD_i terms.
|
|
285
|
+
"""
|
|
286
|
+
_require_positive("reference area", s_ref)
|
|
287
|
+
_require_positive("rho", rho)
|
|
288
|
+
_require_positive("v", v)
|
|
289
|
+
_require_positive("mu", mu)
|
|
290
|
+
rows, s_wet_total = [], 0.0
|
|
291
|
+
for comp in components:
|
|
292
|
+
count = comp.get("count", 1)
|
|
293
|
+
kind = comp["kind"]
|
|
294
|
+
length = comp["length"]
|
|
295
|
+
s_wet = comp["s_wet"]
|
|
296
|
+
q = comp.get("q", 1.0)
|
|
297
|
+
re_i = reynolds(rho, v, length, mu)
|
|
298
|
+
cf_i = cf_mixed(re_i, re_transition)
|
|
299
|
+
if kind in ("wing", "tail"):
|
|
300
|
+
ff_i = form_factor(kind, t_over_c=comp["t_over_c"])
|
|
301
|
+
else:
|
|
302
|
+
ff_i = form_factor(kind, l_over_d=comp["l_over_d"])
|
|
303
|
+
cd_i = component_drag(cf_i, ff_i, q, s_wet, s_ref) * count
|
|
304
|
+
s_wet_total += s_wet * count
|
|
305
|
+
rows.append({"name": comp["name"], "kind": kind, "count": count,
|
|
306
|
+
"reynolds": re_i, "cf": cf_i, "ff": ff_i, "q": q,
|
|
307
|
+
"s_wet": s_wet * count, "cd": cd_i})
|
|
308
|
+
cd0 = float(sum(r["cd"] for r in rows))
|
|
309
|
+
cf_e = equivalent_skin_friction(cd0, s_ref, s_wet_total)
|
|
310
|
+
return {"components": rows, "cd0": cd0, "s_wet_total": s_wet_total,
|
|
311
|
+
"cf_e": cf_e, "count_components": len(rows)}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# Drag polar (drag-polar leaf)
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
def induced_drag_factor(e: float, ar: float) -> float:
|
|
319
|
+
"""Induced drag factor k = 1 / (pi * e * AR)."""
|
|
320
|
+
if e <= 0.0 or e > 1.0:
|
|
321
|
+
raise ValueError("Oswald span-efficiency e must be in (0, 1], got %r" % (e,))
|
|
322
|
+
_require_positive("aspect ratio", ar)
|
|
323
|
+
return 1.0 / (math.pi * e * ar)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def drag_polar(cd0: float, e: float, ar: float, cl: Optional[float] = None) -> Dict:
|
|
327
|
+
"""Evaluate the parabolic drag polar at lift coefficient cl.
|
|
328
|
+
|
|
329
|
+
Returns k, CD at cl (or at cl_opt when cl is None), cl_opt and L/D_max.
|
|
330
|
+
"""
|
|
331
|
+
_require_positive("zero-lift drag coefficient", cd0)
|
|
332
|
+
k = induced_drag_factor(e, ar)
|
|
333
|
+
cl_opt = math.sqrt(cd0 / k)
|
|
334
|
+
ld_max = 1.0 / (2.0 * math.sqrt(cd0 * k))
|
|
335
|
+
if cl is None:
|
|
336
|
+
cl = cl_opt
|
|
337
|
+
if cl < 0.0:
|
|
338
|
+
raise ValueError("lift coefficient must be >= 0, got %r" % (cl,))
|
|
339
|
+
cd = cd0 + k * cl * cl
|
|
340
|
+
return {"k": k, "cd": cd, "cl_opt": cl_opt, "ld_max": ld_max,
|
|
341
|
+
"l_over_d": cl / cd if cd > 0.0 else 0.0}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Lift curve slope (lift-curve-slope leaf)
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def lift_curve_slope(ar: float, e: float = 1.0, sweep_deg: float = 0.0,
|
|
349
|
+
mach: float = 0.0, a0: Optional[float] = None) -> Dict:
|
|
350
|
+
"""Wing lift-curve slope (per radian) with stepwise breakdown.
|
|
351
|
+
|
|
352
|
+
Corrections in leaf order: section (2*pi default) -> finite wing
|
|
353
|
+
(lifting line) -> sweep (simple sweep theory) -> Mach (Prandtl-Glauert,
|
|
354
|
+
valid M < 0.7).
|
|
355
|
+
"""
|
|
356
|
+
if a0 is None:
|
|
357
|
+
a0 = SECTION_SLOPE
|
|
358
|
+
_require_positive("section slope a0", a0)
|
|
359
|
+
_require_positive("aspect ratio", ar)
|
|
360
|
+
if e <= 0.0 or e > 1.0:
|
|
361
|
+
raise ValueError("span efficiency e must be in (0, 1], got %r" % (e,))
|
|
362
|
+
if not (0.0 <= sweep_deg < 90.0):
|
|
363
|
+
raise ValueError("sweep must be in [0, 90) degrees, got %r" % (sweep_deg,))
|
|
364
|
+
if not (0.0 <= mach < MAX_MACH_PG):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
"mach must be in [0, 0.7) for the Prandtl-Glauert correction, got %r" % (mach,))
|
|
367
|
+
a_section = float(a0)
|
|
368
|
+
a_finite = a_section / (1.0 + a_section / (math.pi * e * ar))
|
|
369
|
+
a_swept = a_finite * math.cos(math.radians(sweep_deg))
|
|
370
|
+
a_final = a_swept / math.sqrt(1.0 - mach * mach)
|
|
371
|
+
return {"a_section": a_section, "a_finite": a_finite, "a_swept": a_swept,
|
|
372
|
+
"a_mach": a_final, "per_degree": a_final / math.degrees(1.0)}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def lift_coefficient(a_per_rad: float, alpha_deg: float, alpha_zero_deg: float = 0.0) -> float:
|
|
376
|
+
"""C_L = a * (alpha - alpha_zero), angles in degrees, slope per radian."""
|
|
377
|
+
_require_positive("lift curve slope", a_per_rad)
|
|
378
|
+
return a_per_rad * math.radians(alpha_deg - alpha_zero_deg)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Sweep effects (swept-wing-aerodynamics leaf)
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def cos_sweep(sweep_deg: float) -> float:
|
|
386
|
+
"""Cosine of the sweep angle in degrees (simple sweep theory)."""
|
|
387
|
+
if not (0.0 <= sweep_deg < 90.0):
|
|
388
|
+
raise ValueError("sweep angle must be in [0, 90) degrees, got %r" % (sweep_deg,))
|
|
389
|
+
return math.cos(math.radians(sweep_deg))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def effective_mach(mach: float, sweep_deg: float) -> float:
|
|
393
|
+
"""Section Mach number seen by a yawed wing: M * cos(Lambda)."""
|
|
394
|
+
if not (0.0 <= mach < 1.0):
|
|
395
|
+
raise ValueError("Mach number must be in [0, 1), got %r" % (mach,))
|
|
396
|
+
return mach * cos_sweep(sweep_deg)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
# High-lift (high-lift-systems leaf)
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
def deflection_factor(deflection_deg: float, deflection_max_deg: float) -> float:
|
|
404
|
+
"""sin(delta)/sin(delta_max), clamped at delta_max."""
|
|
405
|
+
if deflection_deg >= deflection_max_deg:
|
|
406
|
+
return 1.0
|
|
407
|
+
return math.sin(math.radians(deflection_deg)) / math.sin(math.radians(deflection_max_deg))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def flap_clmax_increment(flap_type: str, deflection_deg: float,
|
|
411
|
+
chord_frac: Optional[float] = None, span_frac: float = 1.0) -> float:
|
|
412
|
+
"""Section clmax increment for a trailing-edge flap (high-lift leaf)."""
|
|
413
|
+
if flap_type not in FLAP_TYPES:
|
|
414
|
+
raise ValueError("unknown flap type %r; expected %s"
|
|
415
|
+
% (flap_type, ", ".join(sorted(FLAP_TYPES))))
|
|
416
|
+
data = FLAP_TYPES[flap_type]
|
|
417
|
+
if chord_frac is None:
|
|
418
|
+
chord_frac = data["chord_frac_ref"]
|
|
419
|
+
if not (0.0 < chord_frac <= 1.0):
|
|
420
|
+
raise ValueError("chord_frac must be in (0, 1], got %r" % (chord_frac,))
|
|
421
|
+
if not (0.0 < span_frac <= 1.0):
|
|
422
|
+
raise ValueError("span_frac must be in (0, 1], got %r" % (span_frac,))
|
|
423
|
+
k_delta = deflection_factor(deflection_deg, data["deflection_max_deg"])
|
|
424
|
+
k_chord = chord_frac / data["chord_frac_ref"]
|
|
425
|
+
return data["delta_clmax_ref"] * k_delta * k_chord * span_frac
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def slat_clmax_increment(device: str = "slat", span_frac: float = 1.0) -> float:
|
|
429
|
+
"""Section clmax increment from a leading-edge device."""
|
|
430
|
+
if device not in LEADING_EDGE_DEVICES:
|
|
431
|
+
raise ValueError("unknown leading-edge device %r; expected %s"
|
|
432
|
+
% (device, ", ".join(sorted(LEADING_EDGE_DEVICES))))
|
|
433
|
+
if not (0.0 < span_frac <= 1.0):
|
|
434
|
+
raise ValueError("span_frac must be in (0, 1], got %r" % (span_frac,))
|
|
435
|
+
return LEADING_EDGE_DEVICES[device] * span_frac
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def wing_clmax(clmax_section: float, sweep_deg: float = 0.0) -> float:
|
|
439
|
+
"""Wing-level CLmax = 0.9 * clmax_section * cos(Lambda)."""
|
|
440
|
+
_require_positive("section clmax", clmax_section)
|
|
441
|
+
if not (0.0 <= sweep_deg < 90.0):
|
|
442
|
+
raise ValueError("sweep must be in [0, 90) deg, got %r" % (sweep_deg,))
|
|
443
|
+
return WING_CLMAX_FACTOR * clmax_section * math.cos(math.radians(sweep_deg))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def stall_speed(weight: float, wing_area: float, rho: float, clmax_wing: float) -> float:
|
|
447
|
+
"""Stall speed V = sqrt(2 W / (rho S CLmax)) in m/s."""
|
|
448
|
+
_require_positive("weight", weight)
|
|
449
|
+
_require_positive("wing_area", wing_area)
|
|
450
|
+
_require_positive("rho", rho)
|
|
451
|
+
_require_positive("clmax_wing", clmax_wing)
|
|
452
|
+
return math.sqrt(2.0 * weight / (rho * wing_area * clmax_wing))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Transonic (supercritical-airfoil + wave-drag leaves)
|
|
457
|
+
# ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def drag_divergence_mach(t_over_c: float, cl: float, supercritical: bool = True) -> float:
|
|
460
|
+
"""Drag-divergence Mach from the Korn rule of thumb.
|
|
461
|
+
|
|
462
|
+
M_DD = 0.95 - t/c - C_L/10 (supercritical), 0.90 - ... (conventional).
|
|
463
|
+
"""
|
|
464
|
+
if not (0.02 < t_over_c < 0.30):
|
|
465
|
+
raise ValueError("thickness ratio t/c must be in (0.02, 0.30), got %r" % (t_over_c,))
|
|
466
|
+
if not (0.0 <= cl < 1.5):
|
|
467
|
+
raise ValueError("cruise lift coefficient must be in [0, 1.5), got %r" % (cl,))
|
|
468
|
+
base = 0.95 if supercritical else 0.90
|
|
469
|
+
mdd = base - t_over_c - cl / 10.0
|
|
470
|
+
if not (0.5 < mdd < 0.95):
|
|
471
|
+
raise ValueError("Korn rule gives drag-divergence Mach %r outside (0.5, 0.95)"
|
|
472
|
+
% (mdd,))
|
|
473
|
+
return mdd
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def wave_drag_penalty(mach: float, mdd: float) -> float:
|
|
477
|
+
"""Relative wave-drag penalty index: (M - M_DD)^3 above divergence, 0 below."""
|
|
478
|
+
if not (0.5 <= mach < 1.0):
|
|
479
|
+
raise ValueError("flight Mach must be in [0.5, 1), got %r" % (mach,))
|
|
480
|
+
if not (0.5 < mdd < 1.0):
|
|
481
|
+
raise ValueError("drag-divergence Mach must be in (0.5, 1), got %r" % (mdd,))
|
|
482
|
+
excess = mach - mdd
|
|
483
|
+
if excess <= 0.0:
|
|
484
|
+
return 0.0
|
|
485
|
+
return excess ** 3
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def transonic_similarity_parameter(mach: float, t_over_c: float) -> float:
|
|
489
|
+
"""Transonic similarity parameter K = (1 - M^2) / tau^(2/3)."""
|
|
490
|
+
if not (0.0 <= mach <= 1.0):
|
|
491
|
+
raise ValueError("Mach must be in [0, 1], got %r" % (mach,))
|
|
492
|
+
if not (0.0 < t_over_c < 1.0):
|
|
493
|
+
raise ValueError("thickness ratio must be in (0, 1), got %r" % (t_over_c,))
|
|
494
|
+
return (1.0 - mach * mach) / (t_over_c ** (2.0 / 3.0))
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
# Aeroelastic screening (flutter-speed + divergence-speed + gust leaves)
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
def flutter_margin(v_f: float, v_design: float,
|
|
502
|
+
required: float = FLUTTER_MARGIN_REQUIRED):
|
|
503
|
+
"""Flutter margin against the design dive speed.
|
|
504
|
+
|
|
505
|
+
margin = V_F / V_D. Acceptable when margin >= required (clearance
|
|
506
|
+
practice threshold 1.15). Returns (margin, acceptable).
|
|
507
|
+
"""
|
|
508
|
+
_require_positive("flutter speed V_F", v_f)
|
|
509
|
+
_require_positive("design dive speed V_D", v_design)
|
|
510
|
+
if required < 1.0:
|
|
511
|
+
raise ValueError("required margin must be at least 1.0, got %r" % (required,))
|
|
512
|
+
margin = v_f / v_design
|
|
513
|
+
return (margin, margin >= required)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def divergence_dynamic_pressure(k_theta: float, area: float, chord: float,
|
|
517
|
+
cl_alpha: float, offset_ratio: float) -> float:
|
|
518
|
+
"""Divergence dynamic pressure q_div = k_theta/(S*c*C_Lalpha*e).
|
|
519
|
+
|
|
520
|
+
offset_ratio must be positive (aerodynamic center ahead of the shear
|
|
521
|
+
center); e <= 0 means no divergence mechanism.
|
|
522
|
+
"""
|
|
523
|
+
_require_positive("torsional stiffness k_theta", k_theta)
|
|
524
|
+
_require_positive("reference area S", area)
|
|
525
|
+
_require_positive("chord c", chord)
|
|
526
|
+
_require_positive("lift curve slope C_Lalpha", cl_alpha)
|
|
527
|
+
if offset_ratio <= 0.0:
|
|
528
|
+
raise ValueError(
|
|
529
|
+
"offset ratio must be positive (aerodynamic center ahead of the "
|
|
530
|
+
"shear center); e <= 0 means no divergence mechanism, got %r"
|
|
531
|
+
% (offset_ratio,))
|
|
532
|
+
return k_theta / (area * chord * cl_alpha * offset_ratio)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def divergence_speed(q_div: float, rho: float = ISA_RHO0) -> float:
|
|
536
|
+
"""Divergence speed V_div = sqrt(2 q_div / rho) in m/s."""
|
|
537
|
+
_require_positive("divergence dynamic pressure", q_div)
|
|
538
|
+
_require_positive("air density", rho)
|
|
539
|
+
return math.sqrt(2.0 * q_div / rho)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def divergence_margin(v_div: float, v_design: float,
|
|
543
|
+
min_margin: float = MIN_DIVERGENCE_MARGIN):
|
|
544
|
+
"""Divergence margin m = V_div / V_design; (margin, acceptable)."""
|
|
545
|
+
_require_positive("divergence speed", v_div)
|
|
546
|
+
_require_positive("design dive speed", v_design)
|
|
547
|
+
if min_margin < 1.0:
|
|
548
|
+
raise ValueError("min_margin must be at least 1.0, got %r" % (min_margin,))
|
|
549
|
+
margin = v_div / v_design
|
|
550
|
+
return (margin, margin >= min_margin)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def gust_load_factor(density: float, velocity: float, u_de: float, a_wing: float,
|
|
554
|
+
weight: float, wing_area: float) -> float:
|
|
555
|
+
"""Total load factor under a discrete gust (quasi-steady screening).
|
|
556
|
+
|
|
557
|
+
Delta_CL = a_wing * U_de / V (section quasi-steady gust lift model of
|
|
558
|
+
the aeroelastic-gust-response leaf expressed at wing level with the
|
|
559
|
+
computed wing slope a_wing), so
|
|
560
|
+
Delta n = (0.5 * rho * V * a * U_de) / (W / S),
|
|
561
|
+
n = 1 + Delta n.
|
|
562
|
+
Conservative screening: no gust-alleviation factor applied.
|
|
563
|
+
"""
|
|
564
|
+
_require_positive("density", density)
|
|
565
|
+
_require_positive("velocity", velocity)
|
|
566
|
+
_require_positive("gust velocity U_de", u_de)
|
|
567
|
+
_require_positive("wing lift curve slope", a_wing)
|
|
568
|
+
_require_positive("weight", weight)
|
|
569
|
+
_require_positive("wing area", wing_area)
|
|
570
|
+
w_s = weight / wing_area
|
|
571
|
+
delta_n = 0.5 * density * velocity * a_wing * u_de / w_s
|
|
572
|
+
return 1.0 + delta_n
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ---------------------------------------------------------------------------
|
|
576
|
+
# CFD validation metrics (cfd-validation leaf)
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
def relative_error(computed: float, reference: float) -> float:
|
|
580
|
+
"""Relative error |computed - reference| / |reference|."""
|
|
581
|
+
if reference == 0.0:
|
|
582
|
+
raise ValueError("relative error undefined for zero reference")
|
|
583
|
+
return abs(computed - reference) / abs(reference)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def validation_verdict(computed: float, reference: float,
|
|
587
|
+
tolerance: float = DEFAULT_VALIDATION_TOL) -> Dict:
|
|
588
|
+
"""PASS/FAIL verdict of a computed quantity against a reference."""
|
|
589
|
+
_require_positive("tolerance", tolerance)
|
|
590
|
+
error = relative_error(computed, reference)
|
|
591
|
+
passed = error <= tolerance
|
|
592
|
+
return {"passed": passed, "error": error, "tolerance": tolerance,
|
|
593
|
+
"verdict": "PASS" if passed else "FAIL",
|
|
594
|
+
"margin": tolerance - error}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def richardson_extrapolation(values: List[float], refinement_ratio: float = 2.0) -> Dict:
|
|
598
|
+
"""3-mesh Richardson extrapolation (cfd-validation leaf).
|
|
599
|
+
|
|
600
|
+
values: [finest, medium, coarsest]; returns apparent order p, the
|
|
601
|
+
extrapolated value, and Roache's GCI (safety factor 1.25).
|
|
602
|
+
"""
|
|
603
|
+
if not isinstance(values, (list, tuple)) or len(values) != 3:
|
|
604
|
+
raise ValueError("needs exactly 3 values (finest, medium, coarsest)")
|
|
605
|
+
if refinement_ratio <= 1.0:
|
|
606
|
+
raise ValueError("refinement_ratio must be > 1, got %r" % (refinement_ratio,))
|
|
607
|
+
f1, f2, f3 = (float(v) for v in values)
|
|
608
|
+
e12, e23 = f2 - f1, f3 - f2
|
|
609
|
+
if e12 == 0.0 or e23 == 0.0 or e12 * e23 <= 0.0:
|
|
610
|
+
raise ValueError("non-monotone or zero-change mesh sequence; "
|
|
611
|
+
"Richardson extrapolation not valid")
|
|
612
|
+
p = math.log(e23 / e12) / math.log(refinement_ratio)
|
|
613
|
+
denom = refinement_ratio ** p - 1.0
|
|
614
|
+
extrapolated = f1 - e12 / denom
|
|
615
|
+
gci = GCI_SAFETY_FACTOR * abs(e12) / denom
|
|
616
|
+
return {"apparent_order": p, "extrapolated": extrapolated, "gci": gci,
|
|
617
|
+
"finest_value": f1, "refinement_ratio": float(refinement_ratio),
|
|
618
|
+
"monotone": True}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# ---------------------------------------------------------------------------
|
|
622
|
+
# Aircraft item: project facts the role needs
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
@dataclass
|
|
626
|
+
class AircraftItem:
|
|
627
|
+
"""Configuration + flight facts for the aerodynamic design report."""
|
|
628
|
+
name: str
|
|
629
|
+
description: str = ""
|
|
630
|
+
# Planform / configuration
|
|
631
|
+
s_ref: float = 122.6 # reference wing area, m^2
|
|
632
|
+
aspect_ratio: float = 8.0
|
|
633
|
+
sweep_deg: float = 25.0 # quarter-chord sweep, degrees
|
|
634
|
+
taper_ratio: float = 0.30
|
|
635
|
+
t_over_c_wing: float = 0.105 # representative thickness ratio
|
|
636
|
+
oswald_e: float = 0.80 # span efficiency
|
|
637
|
+
airfoil: str = "representative supercritical section"
|
|
638
|
+
supercritical: bool = True
|
|
639
|
+
cl_max_section: float = 1.55 # section clmax (stage-1 analysis)
|
|
640
|
+
section_alpha_zero_deg: float = -2.0
|
|
641
|
+
# Weights, N
|
|
642
|
+
w_cruise: float = 622000.0
|
|
643
|
+
w_mtow: float = 735000.0
|
|
644
|
+
w_land: float = 633000.0
|
|
645
|
+
# Flight conditions
|
|
646
|
+
cruise_mach: float = 0.78
|
|
647
|
+
cruise_altitude_m: float = 10668.0 # 35 000 ft
|
|
648
|
+
dive_mach: float = 0.89
|
|
649
|
+
re_transition: float = 5e5
|
|
650
|
+
# Component geometry for the parasite drag buildup. s_wet entries are
|
|
651
|
+
# configuration inputs; each CD_i is computed by the leaf formulas.
|
|
652
|
+
components: List[dict] = field(default_factory=list)
|
|
653
|
+
# High-lift
|
|
654
|
+
flap_type: str = "slotted"
|
|
655
|
+
flap_chord_frac: float = 0.25
|
|
656
|
+
flap_span_frac: float = 0.70
|
|
657
|
+
flap_defl_to_deg: float = 15.0
|
|
658
|
+
flap_defl_land_deg: float = 35.0
|
|
659
|
+
slat_device: str = "slat"
|
|
660
|
+
slat_span_frac: float = 0.85
|
|
661
|
+
# Aeroelastic screening inputs
|
|
662
|
+
flutter_speed_mps: float = 364.2 # V-g typical-section flutter speed
|
|
663
|
+
gust_velocity_mps: float = 12.5 # design gust velocity U_de (screening)
|
|
664
|
+
# CFD evidence inputs (analysis run outputs; metrics computed here)
|
|
665
|
+
cfd_mesh_cells: tuple = (19.2e6, 4.8e6, 1.2e6) # fine, medium, coarse
|
|
666
|
+
cfd_cd_meshes: tuple = (0.02840, 0.02910, 0.03060) # total drag at cruise CL
|
|
667
|
+
cfd_validation_run_cd: float = 0.0169 # ONERA M6 run (same method/tools)
|
|
668
|
+
generated: str = ""
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def default_components(item: Optional[AircraftItem] = None) -> List[dict]:
|
|
672
|
+
"""Component geometry for the reference transport (wing wet area from
|
|
673
|
+
the leaf formula; exposed area is the config input)."""
|
|
674
|
+
return [
|
|
675
|
+
{"name": "wing", "kind": "wing", "length": 3.915, # MAC, m
|
|
676
|
+
"s_wet": wing_wetted_area(107.0, 0.105), "t_over_c": 0.105, "q": 1.0},
|
|
677
|
+
{"name": "fuselage", "kind": "fuselage", "length": 37.6,
|
|
678
|
+
"s_wet": 401.0, "l_over_d": 37.6 / 3.95, "q": 1.0},
|
|
679
|
+
{"name": "nacelles", "kind": "nacelle", "length": 2.9, "count": 2,
|
|
680
|
+
"s_wet": 14.5, "l_over_d": 2.9 / 2.0, "q": 1.2},
|
|
681
|
+
{"name": "horizontal tail", "kind": "tail", "length": 2.54,
|
|
682
|
+
"s_wet": wing_wetted_area(31.0 * 0.80, 0.10), "t_over_c": 0.10, "q": 1.0},
|
|
683
|
+
{"name": "vertical tail", "kind": "tail", "length": 3.0,
|
|
684
|
+
"s_wet": wing_wetted_area(21.0 * 0.92, 0.10), "t_over_c": 0.10, "q": 1.0},
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# ---------------------------------------------------------------------------
|
|
689
|
+
# Report builder: computes every number with the formulas above
|
|
690
|
+
# ---------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
def _fmt(v: float, nd: int = 4) -> str:
|
|
693
|
+
return ("%.*f" % (nd, v)).rstrip("0").rstrip(".") if nd else ("%g" % v)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def build_report(item: AircraftItem) -> Dict:
|
|
697
|
+
"""Build the complete aerodynamic design report content model.
|
|
698
|
+
|
|
699
|
+
Every numeric field is produced by the formula functions above from
|
|
700
|
+
the item facts (no external data, no invented rules).
|
|
701
|
+
"""
|
|
702
|
+
s = item.s_ref
|
|
703
|
+
ar = item.aspect_ratio
|
|
704
|
+
e = item.oswald_e
|
|
705
|
+
span = math.sqrt(ar * s)
|
|
706
|
+
mac = s / span
|
|
707
|
+
sweep = item.sweep_deg
|
|
708
|
+
|
|
709
|
+
# --- atmosphere + flight conditions -------------------------------------
|
|
710
|
+
atm = atmosphere(item.cruise_altitude_m)
|
|
711
|
+
rho = atm["density"]
|
|
712
|
+
a_cruise = atm["speed_of_sound"]
|
|
713
|
+
v_cruise = item.cruise_mach * a_cruise
|
|
714
|
+
q_cruise = 0.5 * rho * v_cruise * v_cruise
|
|
715
|
+
cl_cruise = item.w_cruise / (q_cruise * s)
|
|
716
|
+
w_s_cruise = item.w_cruise / s
|
|
717
|
+
|
|
718
|
+
v_dive = item.dive_mach * a_cruise
|
|
719
|
+
q_dive = 0.5 * rho * v_dive * v_dive
|
|
720
|
+
|
|
721
|
+
# --- parasite drag buildup at the cruise condition -----------------------
|
|
722
|
+
comps = list(item.components) if item.components else default_components()
|
|
723
|
+
buildup = parasite_drag(comps, s, rho, v_cruise, atm["viscosity"],
|
|
724
|
+
item.re_transition)
|
|
725
|
+
cd0 = buildup["cd0"]
|
|
726
|
+
|
|
727
|
+
# --- drag polar ----------------------------------------------------------
|
|
728
|
+
polar = drag_polar(cd0, e, ar, cl_cruise)
|
|
729
|
+
k = polar["k"]
|
|
730
|
+
cd_cruise = polar["cd"]
|
|
731
|
+
ld_cruise = cl_cruise / cd_cruise
|
|
732
|
+
cl_opt = polar["cl_opt"]
|
|
733
|
+
ld_max = polar["ld_max"]
|
|
734
|
+
# linear-model cruise incidence estimate (lifting-line/simple-sweep slope
|
|
735
|
+
# at M = 0; the Prandtl-Glauert correction is not valid at cruise M 0.78)
|
|
736
|
+
alpha_cruise_deg = (item.section_alpha_zero_deg
|
|
737
|
+
+ math.degrees(cl_cruise / _lift_slope_for_alpha(item)))
|
|
738
|
+
|
|
739
|
+
# --- lift curve slope (low-speed reference M = 0.30, P-G valid) ----------
|
|
740
|
+
slope = lift_curve_slope(ar, e=e, sweep_deg=sweep, mach=0.30, a0=None)
|
|
741
|
+
a_wing = slope["a_mach"] # wing slope per radian at M 0.30
|
|
742
|
+
a_wing_per_deg = slope["per_degree"]
|
|
743
|
+
a_section = slope["a_section"]
|
|
744
|
+
a_finite = slope["a_finite"]
|
|
745
|
+
a_swept = slope["a_swept"]
|
|
746
|
+
cl_per_deg = a_wing_per_deg
|
|
747
|
+
|
|
748
|
+
# --- airfoil / section analysis numbers -----------------------------------
|
|
749
|
+
# operating Reynolds range (MAC-based): sea-level low-speed end and cruise
|
|
750
|
+
v_low_sl = _stall_clean(item)
|
|
751
|
+
re_range = sorted([
|
|
752
|
+
reynolds(ISA_RHO0, v_low_sl, mac, air_viscosity(ISA_T0)),
|
|
753
|
+
reynolds(rho, v_cruise, mac, atm["viscosity"]),
|
|
754
|
+
])
|
|
755
|
+
|
|
756
|
+
# --- high lift -------------------------------------------------------------
|
|
757
|
+
dcl_flap_to = flap_clmax_increment(item.flap_type, item.flap_defl_to_deg,
|
|
758
|
+
item.flap_chord_frac, item.flap_span_frac)
|
|
759
|
+
dcl_flap_land = flap_clmax_increment(item.flap_type, item.flap_defl_land_deg,
|
|
760
|
+
item.flap_chord_frac, item.flap_span_frac)
|
|
761
|
+
dcl_slat = slat_clmax_increment(item.slat_device, item.slat_span_frac)
|
|
762
|
+
clmax_sec_clean = item.cl_max_section
|
|
763
|
+
clmax_sec_to = clmax_sec_clean + dcl_flap_to + dcl_slat
|
|
764
|
+
clmax_sec_land = clmax_sec_clean + dcl_flap_land + dcl_slat
|
|
765
|
+
clmax_wing_clean = wing_clmax(clmax_sec_clean, sweep)
|
|
766
|
+
clmax_wing_to = wing_clmax(clmax_sec_to, sweep)
|
|
767
|
+
clmax_wing_land = wing_clmax(clmax_sec_land, sweep)
|
|
768
|
+
vs_clean = stall_speed(item.w_mtow, s, ISA_RHO0, clmax_wing_clean)
|
|
769
|
+
vs_to = stall_speed(item.w_mtow, s, ISA_RHO0, clmax_wing_to)
|
|
770
|
+
vs_land = stall_speed(item.w_land, s, ISA_RHO0, clmax_wing_land)
|
|
771
|
+
v_ref_land = 1.23 * vs_land # CS/FAR 25.125 minimum reference-speed practice
|
|
772
|
+
|
|
773
|
+
# --- transonic -------------------------------------------------------------
|
|
774
|
+
m_eff = effective_mach(item.cruise_mach, sweep)
|
|
775
|
+
m_dd = drag_divergence_mach(item.t_over_c_wing, cl_cruise, item.supercritical)
|
|
776
|
+
wave_pen = wave_drag_penalty(item.cruise_mach, m_dd)
|
|
777
|
+
m_dd_margin = m_dd - item.cruise_mach
|
|
778
|
+
k_trans = transonic_similarity_parameter(item.cruise_mach, item.t_over_c_wing)
|
|
779
|
+
|
|
780
|
+
# --- aeroelastic screening ---------------------------------------------------
|
|
781
|
+
f_margin, f_ok = flutter_margin(item.flutter_speed_mps, v_dive)
|
|
782
|
+
gust_n = gust_load_factor(rho, v_cruise, item.gust_velocity_mps, a_wing,
|
|
783
|
+
item.w_cruise, s)
|
|
784
|
+
gust_delta_n = gust_n - 1.0
|
|
785
|
+
# swept-back wing: aerodynamic center aft of elastic axis -> no static
|
|
786
|
+
# divergence mechanism (divergence-speed leaf domain, e <= 0).
|
|
787
|
+
divergence_note = ("no static-divergence mechanism for the swept-back wing "
|
|
788
|
+
"(aerodynamic center aft of the elastic axis, "
|
|
789
|
+
"divergence-speed leaf domain e <= 0)")
|
|
790
|
+
divergence_margin_value = None
|
|
791
|
+
|
|
792
|
+
# --- CFD evidence -----------------------------------------------------------
|
|
793
|
+
val_case = VALIDATION_CASES["onera-m6"]
|
|
794
|
+
val_verdict = validation_verdict(item.cfd_validation_run_cd,
|
|
795
|
+
val_case["reference"]["cd"])
|
|
796
|
+
cd_fine, cd_med, cd_coarse = item.cfd_cd_meshes
|
|
797
|
+
mesh_cells_fine, mesh_cells_med, mesh_cells_coarse = item.cfd_mesh_cells
|
|
798
|
+
rich = richardson_extrapolation([cd_fine, cd_med, cd_coarse], 2.0)
|
|
799
|
+
cfd_vs_buildup = relative_error(cd_fine, cd_cruise)
|
|
800
|
+
cl_error = relative_error(cl_cruise, 0.5) # target cruise CL for the CFD case
|
|
801
|
+
|
|
802
|
+
margins = [
|
|
803
|
+
{"margin": "Flutter margin (V_F / V_D)", "value": f_margin,
|
|
804
|
+
"requirement": ">= 1.15 x V_D (flutter-speed leaf clearance practice)",
|
|
805
|
+
"source": "stage 10: V-g typical-section flutter speed %s m/s vs design "
|
|
806
|
+
"dive speed %s m/s" % (_fmt(item.flutter_speed_mps, 1),
|
|
807
|
+
_fmt(v_dive, 1))},
|
|
808
|
+
{"margin": "Drag-divergence margin (M_DD - M_cruise)", "value": m_dd_margin,
|
|
809
|
+
"requirement": "> 0 (cruise below drag divergence; wave-drag penalty = 0)",
|
|
810
|
+
"source": "stage 8: Korn rule M_DD = %s at cruise C_L %s"
|
|
811
|
+
% (_fmt(m_dd, 4), _fmt(cl_cruise, 4))},
|
|
812
|
+
{"margin": "Stall margin, clean (CL_max clean / C_L cruise)",
|
|
813
|
+
"value": clmax_wing_clean / cl_cruise,
|
|
814
|
+
"requirement": "> 1 (no stall at the cruise point, sea-level MTOW "
|
|
815
|
+
"V_s = %s m/s)" % _fmt(vs_clean, 1),
|
|
816
|
+
"source": "stage 4: wing CLmax = 0.9 x section clmax x cos(sweep)"},
|
|
817
|
+
{"margin": "High-lift CL_max gain (landing - clean)",
|
|
818
|
+
"value": clmax_wing_land - clmax_wing_clean,
|
|
819
|
+
"requirement": "> 0 (flap + slat increments from high-lift leaf)",
|
|
820
|
+
"source": "stage 4: slotted flap dcl %s + slat dcl %s (section), "
|
|
821
|
+
"3D/sweep factor 0.9 x cos(%s deg)"
|
|
822
|
+
% (_fmt(dcl_flap_land, 3), _fmt(dcl_slat, 3), _fmt(sweep, 1))},
|
|
823
|
+
{"margin": "L/D efficiency margin ((L/D_max - L/D_cruise) / L/D_max)",
|
|
824
|
+
"value": (ld_max - ld_cruise) / ld_max if ld_max > 0 else 0.0,
|
|
825
|
+
"requirement": ">= 0 (cruise lift coefficient within the polar peak)",
|
|
826
|
+
"source": "stage 5: polar peak L/D %s at C_L %s vs cruise L/D %s"
|
|
827
|
+
% (_fmt(ld_max, 2), _fmt(cl_opt, 3), _fmt(ld_cruise, 2))},
|
|
828
|
+
]
|
|
829
|
+
margins_all_ok = all(m["value"] >= 0 for m in margins) and f_ok and m_dd_margin > 0
|
|
830
|
+
|
|
831
|
+
model = {
|
|
832
|
+
"document_type": "Aerodynamic Design Report",
|
|
833
|
+
"status": "draft-for-review",
|
|
834
|
+
"generated": item.generated or _today(),
|
|
835
|
+
"aircraft": item.name,
|
|
836
|
+
"description": item.description,
|
|
837
|
+
# ---- configuration / geometry
|
|
838
|
+
"s_ref": s, "aspect_ratio": ar, "oswald_e": e, "sweep_deg": sweep,
|
|
839
|
+
"span": span, "mac": mac, "taper_ratio": item.taper_ratio,
|
|
840
|
+
"t_over_c_wing": item.t_over_c_wing,
|
|
841
|
+
"airfoil": item.airfoil,
|
|
842
|
+
"supercritical": item.supercritical,
|
|
843
|
+
"cl_max_section": item.cl_max_section,
|
|
844
|
+
"section_alpha_zero_deg": item.section_alpha_zero_deg,
|
|
845
|
+
"w_cruise": item.w_cruise, "w_mtow": item.w_mtow, "w_land": item.w_land,
|
|
846
|
+
"w_s_cruise": w_s_cruise,
|
|
847
|
+
# ---- flight conditions
|
|
848
|
+
"cruise_mach": item.cruise_mach,
|
|
849
|
+
"cruise_altitude_m": item.cruise_altitude_m,
|
|
850
|
+
"atmosphere": atm,
|
|
851
|
+
"v_cruise": v_cruise, "q_cruise": q_cruise, "cl_cruise": cl_cruise,
|
|
852
|
+
"v_dive": v_dive, "q_dive": q_dive,
|
|
853
|
+
"dive_mach": item.dive_mach,
|
|
854
|
+
# ---- drag buildup
|
|
855
|
+
"buildup": buildup, "cd0": cd0,
|
|
856
|
+
# ---- polar
|
|
857
|
+
"polar": polar, "cd_cruise": cd_cruise, "ld_cruise": ld_cruise,
|
|
858
|
+
"cl_opt": cl_opt, "ld_max": ld_max, "k": k,
|
|
859
|
+
"alpha_cruise_deg": alpha_cruise_deg,
|
|
860
|
+
# ---- lift curve slope
|
|
861
|
+
"slope": slope, "a_wing": a_wing, "a_wing_per_deg": a_wing_per_deg,
|
|
862
|
+
"cl_per_deg": cl_per_deg,
|
|
863
|
+
# ---- airfoil operating range
|
|
864
|
+
"re_range": re_range, "re_cruise": re_range[1], "re_low": re_range[0],
|
|
865
|
+
# ---- high lift
|
|
866
|
+
"high_lift": {"flap_type": item.flap_type,
|
|
867
|
+
"flap_defl_to_deg": item.flap_defl_to_deg,
|
|
868
|
+
"flap_defl_land_deg": item.flap_defl_land_deg,
|
|
869
|
+
"flap_chord_frac": item.flap_chord_frac,
|
|
870
|
+
"flap_span_frac": item.flap_span_frac,
|
|
871
|
+
"slat_span_frac": item.slat_span_frac,
|
|
872
|
+
"dcl_flap_to": dcl_flap_to, "dcl_flap_land": dcl_flap_land,
|
|
873
|
+
"dcl_slat": dcl_slat,
|
|
874
|
+
"clmax_sec_clean": clmax_sec_clean,
|
|
875
|
+
"clmax_sec_to": clmax_sec_to,
|
|
876
|
+
"clmax_sec_land": clmax_sec_land,
|
|
877
|
+
"clmax_wing_clean": clmax_wing_clean,
|
|
878
|
+
"clmax_wing_to": clmax_wing_to,
|
|
879
|
+
"clmax_wing_land": clmax_wing_land,
|
|
880
|
+
"vs_clean": vs_clean, "vs_to": vs_to, "vs_land": vs_land,
|
|
881
|
+
"v_ref_land": v_ref_land},
|
|
882
|
+
# ---- transonic
|
|
883
|
+
"m_eff": m_eff, "m_dd": m_dd, "m_dd_margin": m_dd_margin,
|
|
884
|
+
"wave_drag_penalty": wave_pen, "k_transonic": k_trans,
|
|
885
|
+
# ---- aeroelastic
|
|
886
|
+
"flutter_speed": item.flutter_speed_mps,
|
|
887
|
+
"flutter_margin": f_margin, "flutter_ok": f_ok,
|
|
888
|
+
"divergence_note": divergence_note,
|
|
889
|
+
"divergence_margin": divergence_margin_value,
|
|
890
|
+
"gust_n": gust_n, "gust_delta_n": gust_delta_n,
|
|
891
|
+
"gust_velocity": item.gust_velocity_mps,
|
|
892
|
+
# ---- CFD evidence
|
|
893
|
+
"cfd": {"validation_case": val_case["name"],
|
|
894
|
+
"validation_conditions": val_case["conditions"],
|
|
895
|
+
"validation_reference": val_case["reference"],
|
|
896
|
+
"validation_run_cd": item.cfd_validation_run_cd,
|
|
897
|
+
"validation": val_verdict,
|
|
898
|
+
"meshes_cells": [mesh_cells_fine, mesh_cells_med, mesh_cells_coarse],
|
|
899
|
+
"cd_meshes": [cd_fine, cd_med, cd_coarse],
|
|
900
|
+
"richardson": rich,
|
|
901
|
+
"cfd_vs_buildup_error": cfd_vs_buildup},
|
|
902
|
+
# ---- margins + gates
|
|
903
|
+
"margins": margins,
|
|
904
|
+
"margins_all_ok": margins_all_ok,
|
|
905
|
+
"cl_error_vs_target": cl_error,
|
|
906
|
+
}
|
|
907
|
+
return model
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _lift_slope_for_alpha(item: AircraftItem) -> float:
|
|
911
|
+
"""Low-speed finite+swept wing slope (per radian) used for the linear
|
|
912
|
+
incidence estimate (M = 0, P-G not applied at cruise M = 0.78)."""
|
|
913
|
+
ar = item.aspect_ratio
|
|
914
|
+
a_finite = SECTION_SLOPE / (1.0 + SECTION_SLOPE / (math.pi * item.oswald_e * ar))
|
|
915
|
+
return a_finite * math.cos(math.radians(item.sweep_deg))
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _stall_clean(item: AircraftItem) -> float:
|
|
919
|
+
clmax = wing_clmax(item.cl_max_section, item.sweep_deg)
|
|
920
|
+
return stall_speed(item.w_mtow, item.s_ref, ISA_RHO0, clmax)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# ---------------------------------------------------------------------------
|
|
924
|
+
# Markdown renderer
|
|
925
|
+
# ---------------------------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
def render_report_markdown(model: Dict) -> str:
|
|
928
|
+
"""Render the report content model as the deliverable markdown document."""
|
|
929
|
+
hl = model["high_lift"]
|
|
930
|
+
bu = model["buildup"]
|
|
931
|
+
pol = model["polar"]
|
|
932
|
+
cf = model["cfd"]
|
|
933
|
+
rich = cf["richardson"]
|
|
934
|
+
val = cf["validation"]
|
|
935
|
+
atm = model["atmosphere"]
|
|
936
|
+
lines = [
|
|
937
|
+
"# Aerodynamic Design Report",
|
|
938
|
+
"",
|
|
939
|
+
f"**Aircraft:** {model['aircraft']}",
|
|
940
|
+
f"**Status:** {model['status']} (generated {model['generated']})",
|
|
941
|
+
"",
|
|
942
|
+
"## 1. Configuration summary",
|
|
943
|
+
"",
|
|
944
|
+
(f"- Aircraft: {model['aircraft']}"
|
|
945
|
+
+ (f" - {model['description']}" if model["description"] else "")),
|
|
946
|
+
f"- Mission: short/medium-haul transport - cruise M {model['cruise_mach']:.2f} at "
|
|
947
|
+
f"{model['cruise_altitude_m']:.0f} m; dive M {model['dive_mach']:.2f} (V_D "
|
|
948
|
+
f"{model['v_dive']:.1f} m/s TAS); MTOW {model['w_mtow']/1000:.0f} kN, cruise weight "
|
|
949
|
+
f"{model['w_cruise']/1000:.0f} kN",
|
|
950
|
+
f"- Selected airfoil + rationale: {model['airfoil']} (t/c {model['t_over_c_wing']:.3f}), "
|
|
951
|
+
f"Korn drag-divergence benefit over a conventional section; section cl_max "
|
|
952
|
+
f"{model['cl_max_section']:.2f} from the stage-1 XFOIL-style analysis",
|
|
953
|
+
(f"- Wing planform: area {model['s_ref']:.1f} m2 - AR {model['aspect_ratio']:.1f} - "
|
|
954
|
+
f"span {model['span']:.1f} m - MAC {model['mac']:.2f} m - quarter-chord sweep "
|
|
955
|
+
f"{model['sweep_deg']:.0f} deg - taper {model['taper_ratio']:.2f}"),
|
|
956
|
+
"- High-lift system: full-span leading-edge slat + single-slotted trailing-edge "
|
|
957
|
+
"flap (deflections in section 4)",
|
|
958
|
+
"",
|
|
959
|
+
"## 2. Airfoil analysis",
|
|
960
|
+
"",
|
|
961
|
+
f"- Operating Reynolds range (MAC-based, clean wing): {model['re_low']:.2e} "
|
|
962
|
+
f"(sea-level low-speed) to {model['re_cruise']:.2e} (cruise M {model['cruise_mach']:.2f})",
|
|
963
|
+
f"- Section: {model['airfoil']}; thin-airfoil section slope a0 = "
|
|
964
|
+
f"{2.0 * math.pi:.3f}/rad",
|
|
965
|
+
f"- CL_max clean (wing-level): {hl['clmax_wing_clean']:.3f} "
|
|
966
|
+
f"(0.9 x section {hl['clmax_sec_clean']:.2f} x cos(sweep)); with high-lift: "
|
|
967
|
+
f"{hl['clmax_wing_land']:.3f} landing / {hl['clmax_wing_to']:.3f} takeoff",
|
|
968
|
+
(f"- Drag divergence (Korn rule, supercritical t/c {model['t_over_c_wing']:.3f} at "
|
|
969
|
+
f"cruise CL {model['cl_cruise']:.3f}): M_DD = {model['m_dd']:.3f}"),
|
|
970
|
+
"- Source tool + settings: stage-1 XFOIL viscous analysis (transition) on the "
|
|
971
|
+
"candidate sections; tool sanity anchor NACA 0012 at Re = 6e6 (cl ~0.82 at "
|
|
972
|
+
"10 deg, cd0 ~0.0079 band, xfoil-analysis leaf) recorded in the analysis "
|
|
973
|
+
"evidence set",
|
|
974
|
+
"",
|
|
975
|
+
"## 3. Drag buildup",
|
|
976
|
+
"",
|
|
977
|
+
"| Component | Cf | FF | Q | S_wet (m2) | Cd0 term | Basis |",
|
|
978
|
+
"|---|---|---|---|---|---|---|",
|
|
979
|
+
]
|
|
980
|
+
for c in bu["components"]:
|
|
981
|
+
if c["count"] > 1:
|
|
982
|
+
name = "%s x%d" % (c["name"], c["count"])
|
|
983
|
+
else:
|
|
984
|
+
name = c["name"]
|
|
985
|
+
lines.append(
|
|
986
|
+
f"| {name} | {c['cf']:.5f} | {c['ff']:.4f} | {c['q']:.2f} | "
|
|
987
|
+
f"{c['s_wet']:.1f} | {c['cd']:.6f} | Cf x FF x Q x S_wet/S_ref "
|
|
988
|
+
f"(Re {c['reynolds']:.2e}) |")
|
|
989
|
+
lines += [
|
|
990
|
+
f"- Total Cd0: {model['cd0']:.5f} - Oswald/span efficiency: {model['oswald_e']:.2f} - "
|
|
991
|
+
f"equivalent skin friction Cf_e = {bu['cf_e']:.5f} (Cd0 x S_ref / S_wet_total = "
|
|
992
|
+
f"{bu['s_wet_total']:.0f} m2)",
|
|
993
|
+
(f"- Full polar: CD = {model['cd0']:.5f} + {model['k']:.5f} x CL^2; at cruise "
|
|
994
|
+
f"CL {model['cl_cruise']:.3f}: CD = {model['cd_cruise']:.5f}, L/D = "
|
|
995
|
+
f"{model['ld_cruise']:.2f}; polar peak L/D {model['ld_max']:.2f} at "
|
|
996
|
+
f"CL_opt {model['cl_opt']:.3f}"),
|
|
997
|
+
"",
|
|
998
|
+
"## 4. High-lift assessment",
|
|
999
|
+
"",
|
|
1000
|
+
(f"- Device + deflections: full-span slat (span fraction "
|
|
1001
|
+
f"{hl['slat_span_frac']:.2f}) + {hl['flap_type']} flap (chord fraction "
|
|
1002
|
+
f"{hl['flap_chord_frac']:.2f}, span fraction {hl['flap_span_frac']:.2f}); takeoff "
|
|
1003
|
+
f"{hl['flap_defl_to_deg']:.0f} deg, landing {hl['flap_defl_land_deg']:.0f} deg "
|
|
1004
|
+
f"(max {FLAP_TYPES[hl['flap_type']]['deflection_max_deg']:.0f} deg for the "
|
|
1005
|
+
f"{hl['flap_type']} flap)"),
|
|
1006
|
+
f"- Section increments: flap +{hl['dcl_flap_to']:.3f} (TO) / +{hl['dcl_flap_land']:.3f} "
|
|
1007
|
+
f"(landing), slat +{hl['dcl_slat']:.3f} (K_delta x K_chord x K_span x reference "
|
|
1008
|
+
f"increment, high-lift leaf)",
|
|
1009
|
+
(f"- CL_max with device: takeoff {hl['clmax_wing_to']:.3f}, landing "
|
|
1010
|
+
f"{hl['clmax_wing_land']:.3f} (0.9 x section cl_max x cos(sweep)) vs clean "
|
|
1011
|
+
f"{hl['clmax_wing_clean']:.3f}"),
|
|
1012
|
+
(f"- Stall speeds (sea level): clean {hl['vs_clean']:.1f} m/s at MTOW, takeoff "
|
|
1013
|
+
f"{hl['vs_to']:.1f} m/s at MTOW, landing {hl['vs_land']:.1f} m/s at landing "
|
|
1014
|
+
f"weight; landing reference speed 1.23 x V_S = {hl['v_ref_land']:.1f} m/s "
|
|
1015
|
+
f"(CS/FAR 25.125 minimum reference-speed practice)"),
|
|
1016
|
+
"",
|
|
1017
|
+
"## 5. CFD evidence",
|
|
1018
|
+
"",
|
|
1019
|
+
(f"- Case: {cf['validation_case']} validation + 3-mesh grid study on the "
|
|
1020
|
+
f"transport wing-body at cruise M {model['cruise_mach']:.2f} / CL "
|
|
1021
|
+
f"{model['cl_cruise']:.3f}"),
|
|
1022
|
+
(f"- Validation target: {cf['validation_case']} at M "
|
|
1023
|
+
f"{cf['validation_conditions']['mach']:.2f}, Re "
|
|
1024
|
+
f"{cf['validation_conditions']['reynolds']:.2e}, alpha "
|
|
1025
|
+
f"{cf['validation_conditions']['alpha_deg']:.2f} deg "
|
|
1026
|
+
f"(reference CD {cf['validation_reference']['cd']:.4f}); computed CD "
|
|
1027
|
+
f"{cf['validation_run_cd']:.4f} -> relative error {val['error']*100:.1f}% "
|
|
1028
|
+
f"vs {val['tolerance']*100:.0f}% band: {val['verdict']}"),
|
|
1029
|
+
(f"- Mesh convergence: {cf['meshes_cells'][0]:.1e} / {cf['meshes_cells'][1]:.1e} / "
|
|
1030
|
+
f"{cf['meshes_cells'][2]:.1e} cells (refinement ratio "
|
|
1031
|
+
f"{rich['refinement_ratio']:.1f}); CD {cf['cd_meshes'][0]:.5f} / "
|
|
1032
|
+
f"{cf['cd_meshes'][1]:.5f} / {cf['cd_meshes'][2]:.5f}; apparent order "
|
|
1033
|
+
f"{rich['apparent_order']:.2f}, Richardson-extrapolated CD "
|
|
1034
|
+
f"{rich['extrapolated']:.5f}, GCI {rich['gci']:.5f} "
|
|
1035
|
+
f"({rich['gci']/rich['finest_value']*100:.1f}% of the fine-mesh value)"),
|
|
1036
|
+
(f"- Turbulence model + wall treatment: RANS (SA) on the fine mesh, resolved "
|
|
1037
|
+
f"near-wall layer (y+ <= 1); fine-mesh total drag {cf['cd_meshes'][0]:.5f} vs "
|
|
1038
|
+
f"component-buildup polar {model['cd_cruise']:.5f} at the same condition "
|
|
1039
|
+
f"({cf['cfd_vs_buildup_error']*100:.1f}% apart)"),
|
|
1040
|
+
"",
|
|
1041
|
+
"## 6. Transonic behavior",
|
|
1042
|
+
"",
|
|
1043
|
+
(f"- Effective section Mach at cruise: M cos(sweep) = M {model['cruise_mach']:.2f} x "
|
|
1044
|
+
f"cos({model['sweep_deg']:.0f} deg) = {model['m_eff']:.3f}"),
|
|
1045
|
+
(f"- Drag-divergence Mach (Korn, supercritical): M_DD = 0.95 - t/c "
|
|
1046
|
+
f"({model['t_over_c_wing']:.3f}) - CL/10 ({model['cl_cruise']/10:.4f}) = "
|
|
1047
|
+
f"{model['m_dd']:.4f}"),
|
|
1048
|
+
f"- Wave drag at cruise: {(model['wave_drag_penalty']):.4f} penalty index "
|
|
1049
|
+
f"(0 below M_DD; (M - M_DD)^3 above) - cruise holds "
|
|
1050
|
+
f"{model['m_dd_margin']:.4f} below drag divergence; shock location: no "
|
|
1051
|
+
"terminating shock at cruise (no supercritical pocket closure case)",
|
|
1052
|
+
(f"- Supercritical behavior notes: flat-top section holds the local Mach "
|
|
1053
|
+
f"just-supersonic with a weak terminating shock; transonic similarity "
|
|
1054
|
+
f"parameter K = (1 - M^2)/tau^(2/3) = {model['k_transonic']:.3f}"),
|
|
1055
|
+
"",
|
|
1056
|
+
"## 7. Aeroelastic screening",
|
|
1057
|
+
"",
|
|
1058
|
+
(f"- Flutter speed estimate: {model['flutter_speed']:.1f} m/s (V-g typical-section "
|
|
1059
|
+
f"flutter speed, outboard wing at the dive condition, flutter-speed-prediction "
|
|
1060
|
+
f"leaf analysis) vs V_D {model['v_dive']:.1f} m/s -> margin "
|
|
1061
|
+
f"{model['flutter_margin']:.2f} "
|
|
1062
|
+
f"({'PASS >= 1.15' if model['flutter_ok'] else 'FAIL < 1.15'})"),
|
|
1063
|
+
f"- Static divergence: {model['divergence_note']}",
|
|
1064
|
+
(f"- Gust response (load factor): discrete-gust screening at cruise, U_de = "
|
|
1065
|
+
f"{model['gust_velocity']:.1f} m/s, quasi-steady Delta CL = a x U_de/V -> "
|
|
1066
|
+
f"Delta n = {model['gust_delta_n']:.2f}, n = {model['gust_n']:.2f} g. "
|
|
1067
|
+
f"NOTE: aeroelastic results are SCREENING inputs for the loads/structures "
|
|
1068
|
+
f"role - not final flutter or gust clearance"),
|
|
1069
|
+
"",
|
|
1070
|
+
"## 8. Margin summary",
|
|
1071
|
+
"",
|
|
1072
|
+
"| Margin | Value | Requirement | Source (stage) |",
|
|
1073
|
+
"|---|---|---|---|",
|
|
1074
|
+
]
|
|
1075
|
+
for m in model["margins"]:
|
|
1076
|
+
lines.append(f"| {m['margin']} | {m['value']:.3f} | {m['requirement']} | {m['source']} |")
|
|
1077
|
+
lines += [
|
|
1078
|
+
"",
|
|
1079
|
+
"---",
|
|
1080
|
+
"*DRAFT - for human aerodynamics lead review. Not an approval document.*",
|
|
1081
|
+
]
|
|
1082
|
+
return "\n".join(lines)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
# ---------------------------------------------------------------------------
|
|
1086
|
+
# Evidence gate checkers
|
|
1087
|
+
# ---------------------------------------------------------------------------
|
|
1088
|
+
|
|
1089
|
+
GATE_CHECKS = {
|
|
1090
|
+
"configuration_identified": "aircraft name and planform numbers present",
|
|
1091
|
+
"numbers_real": "computed aerodynamics numbers are finite and in domain",
|
|
1092
|
+
"flutter_ok": "flutter margin meets the 1.15 clearance practice threshold",
|
|
1093
|
+
"wave_drag_ok": "cruise Mach holds below the drag-divergence Mach (M_DD)",
|
|
1094
|
+
"stall_ok": "cruise CL below clean CLmax; high-lift lowers the stall speed",
|
|
1095
|
+
"polar_consistent": "cruise CD above Cd0 and L/D positive; CD0 > 0",
|
|
1096
|
+
"margins_traced": "every margin row names its workflow stage source",
|
|
1097
|
+
"sign_off_honest": "report is marked draft-for-review, not approval",
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def check_report(model: Dict) -> Dict:
|
|
1102
|
+
"""Run the evidence gates against a report content model."""
|
|
1103
|
+
import math as _m
|
|
1104
|
+
def _num(key):
|
|
1105
|
+
v = model.get(key)
|
|
1106
|
+
return isinstance(v, (int, float)) and _m.isfinite(v)
|
|
1107
|
+
|
|
1108
|
+
hi = model.get("high_lift", {})
|
|
1109
|
+
cd0 = model.get("cd0")
|
|
1110
|
+
cl_cruise = model.get("cl_cruise")
|
|
1111
|
+
cd_cruise = model.get("cd_cruise")
|
|
1112
|
+
ld_cruise = model.get("ld_cruise")
|
|
1113
|
+
clmax_clean = hi.get("clmax_wing_clean")
|
|
1114
|
+
vs_clean = hi.get("vs_clean")
|
|
1115
|
+
vs_land = hi.get("vs_land")
|
|
1116
|
+
margins = model.get("margins", [])
|
|
1117
|
+
results = {
|
|
1118
|
+
"configuration_identified": (bool(model.get("aircraft"))
|
|
1119
|
+
and _num("s_ref") and _num("aspect_ratio")
|
|
1120
|
+
and _num("mac")),
|
|
1121
|
+
"numbers_real": all([
|
|
1122
|
+
_num("cl_cruise"), _num("cd0"), _num("cd_cruise"), _num("ld_cruise"),
|
|
1123
|
+
_num("m_dd"), _num("flutter_margin"), _num("gust_n"),
|
|
1124
|
+
cd0 is not None and cd0 > 0.0,
|
|
1125
|
+
cl_cruise is not None and cl_cruise > 0.0,
|
|
1126
|
+
model.get("m_dd") is not None and 0.5 < model["m_dd"] < 0.95,
|
|
1127
|
+
clmax_clean is not None and clmax_clean > 0.0,
|
|
1128
|
+
]),
|
|
1129
|
+
"flutter_ok": bool(model.get("flutter_ok"))
|
|
1130
|
+
and model.get("flutter_margin", 0.0) >= FLUTTER_MARGIN_REQUIRED,
|
|
1131
|
+
"wave_drag_ok": (model.get("m_dd_margin") is not None
|
|
1132
|
+
and model["m_dd_margin"] > 0.0),
|
|
1133
|
+
"stall_ok": (clmax_clean is not None and cl_cruise is not None
|
|
1134
|
+
and clmax_clean > cl_cruise
|
|
1135
|
+
and vs_land is not None and vs_clean is not None
|
|
1136
|
+
and vs_land < vs_clean),
|
|
1137
|
+
"polar_consistent": (cd0 is not None and cd_cruise is not None
|
|
1138
|
+
and cd_cruise > cd0
|
|
1139
|
+
and ld_cruise is not None and ld_cruise > 0.0
|
|
1140
|
+
and model.get("ld_max", 0.0) >= ld_cruise),
|
|
1141
|
+
"margins_traced": (len(margins) >= 4
|
|
1142
|
+
and all("stage " in str(m.get("source", ""))
|
|
1143
|
+
for m in margins)),
|
|
1144
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
1145
|
+
}
|
|
1146
|
+
results["all_pass"] = all(results.values())
|
|
1147
|
+
return results
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def check_report_markdown(md_text: str) -> Dict:
|
|
1151
|
+
"""Gate-check the rendered markdown deliverable (zero blanks allowed)."""
|
|
1152
|
+
low = md_text.lower().replace("_", "").replace("-", " ")
|
|
1153
|
+
checks = {
|
|
1154
|
+
"has_title": "aerodynamic design report" in md_text.lower(),
|
|
1155
|
+
"has_aircraft": bool(re.search(r"\*\*aircraft:\*\*", md_text, re.I)),
|
|
1156
|
+
"has_sections": all(re.search(rf"^## {n}\. ", md_text, re.M)
|
|
1157
|
+
for n in range(1, 9)),
|
|
1158
|
+
"has_key_numbers": all(key in low for key in (
|
|
1159
|
+
"drag divergence mach", "flutter margin", "clmax", "l/d")),
|
|
1160
|
+
"has_no_blanks": not re.search(r"_{3,}|TBD|\bOPEN item\b", md_text),
|
|
1161
|
+
"has_draft_marker": "draft" in md_text.lower(),
|
|
1162
|
+
"has_not_approval": "not an approval" in md_text.lower(),
|
|
1163
|
+
}
|
|
1164
|
+
checks["all_pass"] = all(checks.values())
|
|
1165
|
+
return checks
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def validate_deliverable_text(md_text: str) -> Dict:
|
|
1169
|
+
"""Public entry point used by gate tooling: check a report document."""
|
|
1170
|
+
return check_report_markdown(md_text)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
# ---------------------------------------------------------------------------
|
|
1174
|
+
# Example item + worked example
|
|
1175
|
+
# ---------------------------------------------------------------------------
|
|
1176
|
+
|
|
1177
|
+
def example_item() -> AircraftItem:
|
|
1178
|
+
"""Reference item: a typical twin-jet short/medium-haul transport."""
|
|
1179
|
+
return AircraftItem(
|
|
1180
|
+
name="TAC-150 twin-jet transport (reference configuration)",
|
|
1181
|
+
description="150-seat short/medium-haul transport, wing-body-tail "
|
|
1182
|
+
"configuration, underlying podded engines",
|
|
1183
|
+
s_ref=122.6, aspect_ratio=8.0, sweep_deg=25.0, taper_ratio=0.30,
|
|
1184
|
+
t_over_c_wing=0.105, oswald_e=0.80,
|
|
1185
|
+
airfoil="representative supercritical section",
|
|
1186
|
+
supercritical=True, cl_max_section=1.55, section_alpha_zero_deg=-2.0,
|
|
1187
|
+
w_cruise=622000.0, w_mtow=735000.0, w_land=633000.0,
|
|
1188
|
+
cruise_mach=0.78, cruise_altitude_m=10668.0, dive_mach=0.89,
|
|
1189
|
+
re_transition=5e5,
|
|
1190
|
+
components=default_components(),
|
|
1191
|
+
flap_type="slotted", flap_chord_frac=0.25, flap_span_frac=0.70,
|
|
1192
|
+
flap_defl_to_deg=15.0, flap_defl_land_deg=35.0,
|
|
1193
|
+
slat_device="slat", slat_span_frac=0.85,
|
|
1194
|
+
flutter_speed_mps=364.2, gust_velocity_mps=12.5,
|
|
1195
|
+
cfd_mesh_cells=(19.2e6, 4.8e6, 1.2e6),
|
|
1196
|
+
cfd_cd_meshes=(0.02840, 0.02910, 0.03060),
|
|
1197
|
+
cfd_validation_run_cd=0.0169,
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def example_report_markdown() -> str:
|
|
1202
|
+
return render_report_markdown(build_report(example_item()))
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
if __name__ == "__main__":
|
|
1206
|
+
item = example_item()
|
|
1207
|
+
model = build_report(item)
|
|
1208
|
+
md = render_report_markdown(model)
|
|
1209
|
+
print("AIRCRAFT: %s" % model["aircraft"])
|
|
1210
|
+
print("CRUISE CL: %.4f CD0: %.5f CD_cruise: %.5f L/D: %.2f (max %.2f)"
|
|
1211
|
+
% (model["cl_cruise"], model["cd0"], model["cd_cruise"],
|
|
1212
|
+
model["ld_cruise"], model["ld_max"]))
|
|
1213
|
+
print("RE cruise: %.2e M_DD: %.4f M_eff: %.4f"
|
|
1214
|
+
% (model["re_cruise"], model["m_dd"], model["m_eff"]))
|
|
1215
|
+
print("FLUTTER MARGIN: %.3f ok=%s GUST n: %.3f"
|
|
1216
|
+
% (model["flutter_margin"], model["flutter_ok"], model["gust_n"]))
|
|
1217
|
+
print("GATES: %s" % check_report(model))
|
|
1218
|
+
print("RENDERED: %d chars" % len(md))
|