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,1112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""structures_loads_core.py - Structures and Loads Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given an airplane's loads facts and the
|
|
5
|
+
candidate wing design it derives the FAR 25 limit envelope (discrete
|
|
6
|
+
gust per FAR 25.341 + maneuver per FAR 25.337), converts the design
|
|
7
|
+
limit condition to ultimate loads with the 1.5 factor of safety
|
|
8
|
+
(FAR 25.303), resolves root shear/bending with an elliptic span-load
|
|
9
|
+
idealization, computes margins of safety per critical component from
|
|
10
|
+
real allowables, runs the fatigue screening (S-N basis + Miner sum),
|
|
11
|
+
and BUILDS the loads + strength report content. It also gate-checks
|
|
12
|
+
deliverables. Standalone: no external repo needed.
|
|
13
|
+
|
|
14
|
+
Domain rules encoded here mirror the real AeroSkills leaves the role
|
|
15
|
+
is bound to (gust-maneuver-loads, landing-ground-loads, buckling,
|
|
16
|
+
plate-buckling, lug-joint, mmpsd-allowables, material-selection,
|
|
17
|
+
stress-life, goodman, miner-damage, notch-sensitivity, beam-vibration):
|
|
18
|
+
the formulas are the same ones those leaves' behavior contracts assert.
|
|
19
|
+
The specific values in the worked example are the leaf anchors (Kg in
|
|
20
|
+
the 0.77-0.80 band, 7075-T6 allowables Ftu 83.0 / Fty 73.0 / Fsu 48.0 /
|
|
21
|
+
Fbru 152.3 ksi, fatigue strength coefficient ~100 ksi at b = -0.10),
|
|
22
|
+
stated with their basis. Nothing here reproduces MMPDS or FAR text.
|
|
23
|
+
|
|
24
|
+
Units: US customary throughout the report and engine internals (lb,
|
|
25
|
+
in, ksi, ft/s, psf) - the register of the FAR 25 loads rules. Metric
|
|
26
|
+
leaf anchors are converted at the module boundary (1 ksi = 6.894757
|
|
27
|
+
MPa, 1 GPa = 145.0377 ksi). Invalid inputs raise ValueError.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import math
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from datetime import date
|
|
35
|
+
|
|
36
|
+
def _today() -> str:
|
|
37
|
+
import os
|
|
38
|
+
from datetime import date
|
|
39
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Domain constants (FAR 25.341 / 25.337 / 25.303, public regulation)
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
RHO0 = 0.002378 # sea-level standard density, slugs/ft^3
|
|
46
|
+
G = 32.174 # ft/s^2
|
|
47
|
+
KNOT = 1.68781 # ft/s per knot (used for unit labels, not calcs)
|
|
48
|
+
|
|
49
|
+
# FAR 25.341(a) design gust velocities at sea level, fps EAS.
|
|
50
|
+
GUST_VELOCITY_SEA_LEVEL = {"vb-vc": 66.0, "vc": 50.0, "vd": 25.0}
|
|
51
|
+
GUST_VELOCITY_FLOOR = {"vb-vc": 38.0, "vc": 25.0, "vd": 12.5}
|
|
52
|
+
GUST_ALTITUDE_FLOOR = {"vb-vc": 15000.0, "vc": 15000.0, "vd": 50000.0}
|
|
53
|
+
|
|
54
|
+
# FAR 25.337(b): positive limit maneuvering load factor at VA.
|
|
55
|
+
MANEUVER_LIMIT_VA = {"normal": 2.5, "commuter": 3.8, "transport": 3.8}
|
|
56
|
+
NEGATIVE_MANEUVER_LIMIT = -1.0
|
|
57
|
+
FACTOR_OF_SAFETY = 1.5 # FAR 25.303
|
|
58
|
+
MAX_GUST_VELOCITY = 66.0
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Materials - 7075-T6 screening allowables. Values are the real anchors used
|
|
62
|
+
# by the bound AeroSkills leaves (material-selection leaf: E 71.7 GPa,
|
|
63
|
+
# Fty 503 MPa, UTS 572 MPa; lug-joint worked example: Fsu 331 MPa,
|
|
64
|
+
# Fbru 1050 MPa; strain-life leaf 7075-T6 class fatigue constants).
|
|
65
|
+
# Basis: typical/reference-only screening values - confirm against MMPDS
|
|
66
|
+
# (licensed) before release. Never reproduced as a table here.
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
KSI = 6.894757 # MPa per ksi (conversion, not an allowable)
|
|
69
|
+
|
|
70
|
+
MATERIALS = {
|
|
71
|
+
"al-7075-t6": {
|
|
72
|
+
"label": "Aluminum 7075-T6 (screening allowables per bound "
|
|
73
|
+
"AeroSkills material/lug/strain-life anchors; confirm "
|
|
74
|
+
"B-basis values against MMPDS before release)",
|
|
75
|
+
"E_ksi": 10400.0, # 71.7 GPa
|
|
76
|
+
"Ftu_ksi": 572.0 / KSI, # 83.0
|
|
77
|
+
"Fty_ksi": 503.0 / KSI, # 73.0
|
|
78
|
+
"Fsu_ksi": 331.0 / KSI, # 48.0
|
|
79
|
+
"Fbru_ksi": 1050.0 / KSI, # 152.3
|
|
80
|
+
"rho_lb_in3": 0.101,
|
|
81
|
+
"nu": 0.33,
|
|
82
|
+
"sig_f_prime_ksi": 690.0 / KSI, # fatigue strength coeff, ~100.1 ksi
|
|
83
|
+
"b_fatigue": -0.10, # fatigue strength exponent
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Core loads logic (mirrors bound leaf gust_load_logic.py)
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _require_positive(value, name):
|
|
93
|
+
if value is None:
|
|
94
|
+
raise ValueError("%s is required, got None" % (name,))
|
|
95
|
+
value = float(value)
|
|
96
|
+
if not math.isfinite(value) or value <= 0.0:
|
|
97
|
+
raise ValueError("%s must be positive, got %r" % (name, value))
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _require_nonnegative(value, name):
|
|
102
|
+
value = float(value)
|
|
103
|
+
if not math.isfinite(value) or value < 0.0:
|
|
104
|
+
raise ValueError("%s must be nonnegative, got %r" % (name, value))
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def far25_gust_velocity(speed_region, altitude_ft=0.0):
|
|
109
|
+
"""Design discrete gust velocity U_de (fps EAS) per FAR 25.341(a)."""
|
|
110
|
+
if speed_region not in GUST_VELOCITY_SEA_LEVEL:
|
|
111
|
+
raise ValueError("speed_region must be one of vb-vc/vc/vd")
|
|
112
|
+
altitude_ft = _require_nonnegative(altitude_ft, "altitude_ft")
|
|
113
|
+
v0 = GUST_VELOCITY_SEA_LEVEL[speed_region]
|
|
114
|
+
alt_floor = GUST_ALTITUDE_FLOOR[speed_region]
|
|
115
|
+
v_floor = GUST_VELOCITY_FLOOR[speed_region]
|
|
116
|
+
if altitude_ft >= alt_floor:
|
|
117
|
+
return v_floor
|
|
118
|
+
return v0 - (v0 - v_floor) * (altitude_ft / alt_floor)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def gust_mass_ratio(ws, cbar, a, rho=RHO0):
|
|
122
|
+
"""Mass ratio mu_g = 2*(W/S)/(rho*cbar*a*g), FAR 25.341(b)(2)."""
|
|
123
|
+
return (2.0 * _require_positive(ws, "ws")
|
|
124
|
+
/ (_require_positive(rho, "rho") * _require_positive(cbar, "cbar")
|
|
125
|
+
* _require_positive(a, "a") * G))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def gust_alleviation_factor(ws, cbar, a, rho=RHO0):
|
|
129
|
+
"""Gust alleviation factor Kg = 0.88*mu_g/(5.3+mu_g)."""
|
|
130
|
+
mu = gust_mass_ratio(ws, cbar, a, rho)
|
|
131
|
+
return 0.88 * mu / (5.3 + mu)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def gust_load_factor(ve_fps, ws, a, u_de_fps, kg=None, cbar=None, rho=RHO0):
|
|
135
|
+
"""n = 1 + (rho0*V_e*a*K_g*U_de)/(2*W/S) with V_e in ft/s (FAR 25.341)."""
|
|
136
|
+
ve = _require_positive(ve_fps, "ve")
|
|
137
|
+
ws = _require_positive(ws, "ws")
|
|
138
|
+
a = _require_positive(a, "a")
|
|
139
|
+
u = float(u_de_fps)
|
|
140
|
+
if not math.isfinite(u) or u == 0.0:
|
|
141
|
+
raise ValueError("u_de must be a nonzero finite gust velocity")
|
|
142
|
+
if abs(u) > MAX_GUST_VELOCITY:
|
|
143
|
+
raise ValueError("u_de magnitude %.3f fps exceeds the FAR 25.341 "
|
|
144
|
+
"maximum design gust velocity of %.1f fps EAS"
|
|
145
|
+
% (abs(u), MAX_GUST_VELOCITY))
|
|
146
|
+
if kg is None:
|
|
147
|
+
if cbar is None:
|
|
148
|
+
raise ValueError("kg or cbar must be given")
|
|
149
|
+
kg = gust_alleviation_factor(ws, cbar, a, rho)
|
|
150
|
+
kg = _require_positive(kg, "kg")
|
|
151
|
+
return 1.0 + (RHO0 * ve * a * kg * u) / (2.0 * ws)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def maneuver_limit_load_factor(weight_lb, category="transport",
|
|
155
|
+
negative=False):
|
|
156
|
+
"""Positive limit maneuvering load factor at VA per FAR 25.337(b):
|
|
157
|
+
n = max(2.5, min(3.8, 2.1 + 24000/(W + 10000))) with W the design
|
|
158
|
+
maximum takeoff weight in lb (regulation text, public domain). The
|
|
159
|
+
bound gust leaf's category table (normal 2.5, commuter 3.8) is used
|
|
160
|
+
for the non-transport entries; for transport the exact regulation
|
|
161
|
+
formula is applied - a 100,000 lb transport lands on the 2.5 floor,
|
|
162
|
+
exactly as the leaf's own worked envelope (VA = VS*sqrt(2.5))."""
|
|
163
|
+
if negative:
|
|
164
|
+
return NEGATIVE_MANEUVER_LIMIT
|
|
165
|
+
if category == "transport":
|
|
166
|
+
w = _require_positive(weight_lb, "weight_lb")
|
|
167
|
+
n = 2.1 + 24000.0 / (w + 10000.0)
|
|
168
|
+
return max(2.5, min(3.8, n))
|
|
169
|
+
try:
|
|
170
|
+
return MANEUVER_LIMIT_VA[category]
|
|
171
|
+
except KeyError:
|
|
172
|
+
raise ValueError("unknown category %r; use normal/commuter/transport"
|
|
173
|
+
% (category,))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _positive_limit_at(vn, v):
|
|
177
|
+
"""Positive envelope limit load factor at speed v (ft/s)."""
|
|
178
|
+
vs, va, vd = vn["vs"], vn["va"], vn["vd"]
|
|
179
|
+
n_pos = vn["n_positive"]
|
|
180
|
+
if v <= va:
|
|
181
|
+
return min(n_pos, (v / vs) ** 2)
|
|
182
|
+
if v >= vd:
|
|
183
|
+
return 0.0
|
|
184
|
+
return n_pos * (vd - v) / (vd - va)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _negative_limit_at(vn, v):
|
|
188
|
+
vs, vc, vd = vn["vs"], vn["vc"], vn["vd"]
|
|
189
|
+
n_neg = vn["n_negative"]
|
|
190
|
+
if v <= vc:
|
|
191
|
+
return max(n_neg, -((v / vs) ** 2))
|
|
192
|
+
if v >= vd:
|
|
193
|
+
return 0.0
|
|
194
|
+
return n_neg * (vd - v) / (vd - vc)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def vn_diagram(ws, vs, vd, a, cbar, category="transport", altitude_ft=0.0,
|
|
198
|
+
rho=RHO0, weight_lb=None):
|
|
199
|
+
"""Construct the FAR 25 V-n flight envelope as a dict.
|
|
200
|
+
|
|
201
|
+
Returns corner speeds (vs < va < vb < vc < vd enforced), limit
|
|
202
|
+
maneuvering load factors, gust points at VB/VC/VD with the FAR
|
|
203
|
+
25.341(a) design gust velocities, and the gust velocities used.
|
|
204
|
+
weight_lb (design MTOW) is passed through to the FAR 25.337(b)
|
|
205
|
+
maneuvering-factor formula for the transport category.
|
|
206
|
+
"""
|
|
207
|
+
ws = _require_positive(ws, "ws")
|
|
208
|
+
vs = _require_positive(vs, "vs")
|
|
209
|
+
vd = _require_positive(vd, "vd")
|
|
210
|
+
a = _require_positive(a, "a")
|
|
211
|
+
cbar = _require_positive(cbar, "cbar")
|
|
212
|
+
n_pos = maneuver_limit_load_factor(weight_lb, category)
|
|
213
|
+
n_neg = NEGATIVE_MANEUVER_LIMIT
|
|
214
|
+
va = vs * math.sqrt(n_pos)
|
|
215
|
+
vb = max(1.8 * vs, 1.05 * va)
|
|
216
|
+
vc = max(2.0 * vs, 1.1 * vb)
|
|
217
|
+
if not (vs < va < vb < vc < vd):
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"speed ordering violated: need vs < va < vb < vc < vd, got "
|
|
220
|
+
"vs=%.1f va=%.1f vb=%.1f vc=%.1f vd=%.1f" % (vs, va, vb, vc, vd))
|
|
221
|
+
kg = gust_alleviation_factor(ws, cbar, a, rho)
|
|
222
|
+
u_bc = far25_gust_velocity("vb-vc", altitude_ft)
|
|
223
|
+
u_c = far25_gust_velocity("vc", altitude_ft)
|
|
224
|
+
u_d = far25_gust_velocity("vd", altitude_ft)
|
|
225
|
+
gust_points = []
|
|
226
|
+
for label, v, u_pos in (("VB", vb, u_bc), ("VC", vc, u_c),
|
|
227
|
+
("VD", vd, u_d)):
|
|
228
|
+
gust_points.append({
|
|
229
|
+
"speed": label, "v": v,
|
|
230
|
+
"u_de": u_pos,
|
|
231
|
+
"n_pos": gust_load_factor(v, ws, a, u_pos, kg=kg),
|
|
232
|
+
"n_neg": gust_load_factor(v, ws, a, -u_pos, kg=kg),
|
|
233
|
+
})
|
|
234
|
+
return {
|
|
235
|
+
"category": category, "ws": ws, "cbar_ft": cbar, "a": a,
|
|
236
|
+
"vs": vs, "va": va, "vb": vb,
|
|
237
|
+
"vc": vc, "vd": vd, "n_positive": n_pos, "n_negative": n_neg,
|
|
238
|
+
"mu_g": gust_mass_ratio(ws, cbar, a, rho), "kg": kg,
|
|
239
|
+
"altitude_ft": altitude_ft,
|
|
240
|
+
"maneuver_envelope": {
|
|
241
|
+
"positive": [(vs, 1.0), (va, n_pos), (vd, 0.0)],
|
|
242
|
+
"negative": [(vs, n_neg), (vc, n_neg), (vd, 0.0)],
|
|
243
|
+
},
|
|
244
|
+
"gust_points": gust_points,
|
|
245
|
+
"gust_velocities": {"vb-vc": u_bc, "vc": u_c, "vd": u_d},
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def design_limit_load_factor(vn):
|
|
250
|
+
"""The limit design condition = max of the envelope (maneuver corner,
|
|
251
|
+
positive/negative plateau values and every gust line point). Returns
|
|
252
|
+
(n_limit, condition_label, speed_fps)."""
|
|
253
|
+
best = (vn["n_positive"], "maneuver at VA", vn["va"])
|
|
254
|
+
for p in vn["gust_points"]:
|
|
255
|
+
if p["n_pos"] > best[0]:
|
|
256
|
+
best = (p["n_pos"], "gust at %s (%.1f fps EAS)"
|
|
257
|
+
% (p["speed"], p["u_de"]), p["v"])
|
|
258
|
+
n_neg = vn["n_negative"]
|
|
259
|
+
if n_neg < 0.0:
|
|
260
|
+
for p in vn["gust_points"]:
|
|
261
|
+
if p["n_neg"] < n_neg:
|
|
262
|
+
n_neg = p["n_neg"]
|
|
263
|
+
return {"n_limit_pos": best[0], "n_limit_neg": n_neg,
|
|
264
|
+
"condition_pos": best[1], "speed_pos": best[2]}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def envelope_margins(vn):
|
|
268
|
+
"""Margin check of each gust point against the maneuver envelope."""
|
|
269
|
+
out = {}
|
|
270
|
+
for p in vn["gust_points"]:
|
|
271
|
+
v = p["v"]
|
|
272
|
+
env_pos = _positive_limit_at(vn, v)
|
|
273
|
+
env_neg = _negative_limit_at(vn, v)
|
|
274
|
+
g_pos, g_neg = p["n_pos"], p["n_neg"]
|
|
275
|
+
m_pos = (env_pos - g_pos) / env_pos if env_pos > 0 else 1.0
|
|
276
|
+
m_neg = (g_neg - env_neg) / abs(env_neg) if env_neg < 0 else 1.0
|
|
277
|
+
out[p["speed"]] = {
|
|
278
|
+
"v": v, "n_envelope_pos": env_pos, "n_gust_pos": g_pos,
|
|
279
|
+
"margin_pos": m_pos, "gust_critical_pos": m_pos < 0.0,
|
|
280
|
+
"n_envelope_neg": env_neg, "n_gust_neg": g_neg,
|
|
281
|
+
"margin_neg": m_neg, "gust_critical_neg": m_neg < 0.0,
|
|
282
|
+
}
|
|
283
|
+
return out
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# Landing / ground loads (mirrors bound leaf landing_ground_loads_logic.py)
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
N_LEVEL_DEFAULT = 2.5 # typical level-landing limit vertical inertia factor
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def level_landing_reactions(weight_lb, a_ft, b_ft, lf=N_LEVEL_DEFAULT):
|
|
294
|
+
"""Level landing reactions at a limit vertical inertia load factor."""
|
|
295
|
+
weight = _require_positive(weight_lb, "weight")
|
|
296
|
+
a = _require_positive(a_ft, "a (nose gear to CG)")
|
|
297
|
+
b = _require_positive(b_ft, "b (CG to main gear)")
|
|
298
|
+
lf = _require_positive(lf, "load factor")
|
|
299
|
+
r_nose = lf * weight * b / (a + b)
|
|
300
|
+
r_main = lf * weight * a / (a + b)
|
|
301
|
+
return {"nose_lb": r_nose, "main_lb": r_main, "total_lb": r_nose + r_main}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def braked_roll_deceleration(a_ft, b_ft, friction=0.8):
|
|
305
|
+
"""Braked-roll deceleration in g: friction*a/(a+b)."""
|
|
306
|
+
a = _require_positive(a_ft, "a")
|
|
307
|
+
b = _require_positive(b_ft, "b")
|
|
308
|
+
friction = _require_positive(friction, "friction")
|
|
309
|
+
return friction * a / (a + b)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# Strength logic
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def margin_of_safety(allowable, applied):
|
|
318
|
+
"""MS = allowable/applied - 1 (must be >= 0 at ultimate for a pass)."""
|
|
319
|
+
allowable = _require_positive(allowable, "allowable")
|
|
320
|
+
applied = _require_positive(applied, "applied")
|
|
321
|
+
return allowable / applied - 1.0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def effective_length_factor(end_condition):
|
|
325
|
+
"""K for named end conditions (bound buckling leaf table)."""
|
|
326
|
+
table = {"pinned-pinned": 1.0, "fixed-fixed": 0.5, "fixed-pinned": 0.7,
|
|
327
|
+
"fixed-free": 2.0}
|
|
328
|
+
name = end_condition.strip().lower().replace(" ", "-")
|
|
329
|
+
while "--" in name:
|
|
330
|
+
name = name.replace("--", "-")
|
|
331
|
+
aliases = {"pinned": "pinned-pinned", "hinged": "pinned-pinned",
|
|
332
|
+
"fixed": "fixed-fixed", "clamped": "fixed-fixed",
|
|
333
|
+
"cantilever": "fixed-free", "clamped-free": "fixed-free",
|
|
334
|
+
"clamped-clamped": "fixed-fixed"}
|
|
335
|
+
name = aliases.get(name, name)
|
|
336
|
+
if name not in table:
|
|
337
|
+
raise ValueError("unknown end condition %r" % (end_condition,))
|
|
338
|
+
return table[name]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def euler_buckling_load(e_ksi, i_in4, length_in,
|
|
342
|
+
end_condition="pinned-pinned", k_factor=None):
|
|
343
|
+
"""Euler critical buckling load Pcr = pi^2 E I / (K L)^2, lb.
|
|
344
|
+
|
|
345
|
+
end_condition is a name resolved by effective_length_factor(); pass
|
|
346
|
+
k_factor directly to skip name resolution. E in ksi, I in in^4,
|
|
347
|
+
length in in -> Pcr in lb (E converted to psi internally).
|
|
348
|
+
"""
|
|
349
|
+
k = (_require_positive(k_factor, "K")
|
|
350
|
+
if k_factor is not None else effective_length_factor(end_condition))
|
|
351
|
+
return (math.pi ** 2 * _require_positive(e_ksi, "E") * 1000.0
|
|
352
|
+
* _require_positive(i_in4, "I")
|
|
353
|
+
/ (k * _require_positive(length_in, "L")) ** 2)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def column_check(e_ksi, i_in4, area_in2, length_in, end_condition,
|
|
357
|
+
applied_load_lb, fcy_ksi):
|
|
358
|
+
"""Complete Euler column check with the yield transition. Returns the
|
|
359
|
+
governing allowable load (yield below lambda_1, Euler above), the
|
|
360
|
+
slenderness verdict and the margin of safety. All loads in lb."""
|
|
361
|
+
k = effective_length_factor(end_condition)
|
|
362
|
+
le = k * length_in
|
|
363
|
+
r = math.sqrt(i_in4 / area_in2)
|
|
364
|
+
lam = le / r
|
|
365
|
+
pcr = euler_buckling_load(e_ksi, i_in4, length_in, k_factor=k)
|
|
366
|
+
lam1 = math.pi * math.sqrt(e_ksi / fcy_ksi)
|
|
367
|
+
euler_governs = lam > lam1
|
|
368
|
+
p_allow = pcr if euler_governs else fcy_ksi * 1000.0 * area_in2
|
|
369
|
+
return {
|
|
370
|
+
"end_condition": end_condition, "K": k, "effective_length_in": le,
|
|
371
|
+
"radius_of_gyration_in": r, "slenderness_ratio": lam,
|
|
372
|
+
"transition_slenderness": lam1, "euler_governs": euler_governs,
|
|
373
|
+
"pcr_lb": pcr, "allowable_load_lb": p_allow,
|
|
374
|
+
"margin_of_safety": margin_of_safety(p_allow, applied_load_lb),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def plate_buckling_stress(e_ksi, nu, t_in, b_in, k=4.0):
|
|
379
|
+
"""sigma_cr = k*pi^2 E/(12(1-nu^2)) (t/b)^2, ksi. k=4 long ssss plate
|
|
380
|
+
under uniform edge compression (bound plate-buckling leaf)."""
|
|
381
|
+
return (k * math.pi ** 2 * _require_positive(e_ksi, "E")
|
|
382
|
+
/ (12.0 * (1.0 - nu ** 2))
|
|
383
|
+
* (_require_positive(t_in, "t") / _require_positive(b_in, "b")) ** 2)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def shear_buckling_stress(e_ksi, nu, t_in, b_in, a_over_b):
|
|
387
|
+
"""tau_cr = k_s pi^2 E/(12(1-nu^2)) (t/b)^2 with Timoshenko shear
|
|
388
|
+
coefficient k_s = 5.34 + 4/(a/b)^2 (a/b >= 1), ksi."""
|
|
389
|
+
ar = _require_positive(a_over_b, "a/b")
|
|
390
|
+
k_s = 5.34 + 4.0 / ar ** 2 if ar >= 1.0 else 5.34 * ar ** 2 + 4.0
|
|
391
|
+
return (k_s * math.pi ** 2 * _require_positive(e_ksi, "E")
|
|
392
|
+
/ (12.0 * (1.0 - nu ** 2))
|
|
393
|
+
* (_require_positive(t_in, "t") / _require_positive(b_in, "b")) ** 2)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def lug_analysis(load_lb, hole_diameter_in, thickness_in, lug_width_in,
|
|
397
|
+
edge_distance_in, f_tu_ksi, f_su_ksi, f_bru_ksi):
|
|
398
|
+
"""Round-end lug margins (bound lug-joint-analysis leaf): bearing,
|
|
399
|
+
net-section tension, tearout; governing mode = lowest margin."""
|
|
400
|
+
if load_lb < 0:
|
|
401
|
+
raise ValueError("lug load must be non-negative")
|
|
402
|
+
D, t = hole_diameter_in, thickness_in
|
|
403
|
+
w, e = lug_width_in, edge_distance_in
|
|
404
|
+
if not (D > 0 and t > 0 and w > 0 and e > D / 2 and w > D):
|
|
405
|
+
raise ValueError("degenerate lug geometry")
|
|
406
|
+
sig_b = load_lb / (D * t) / 1000.0 # ksi
|
|
407
|
+
sig_nt = load_lb / ((w - D) * t) / 1000.0 # ksi
|
|
408
|
+
l_te = math.sqrt(e ** 2 - (D / 2.0) ** 2)
|
|
409
|
+
sig_te = load_lb / (2.0 * t * l_te) / 1000.0 # ksi
|
|
410
|
+
margins = {
|
|
411
|
+
"bearing": f_bru_ksi / sig_b - 1.0,
|
|
412
|
+
"net_tension": f_tu_ksi / sig_nt - 1.0,
|
|
413
|
+
"tearout": f_su_ksi / sig_te - 1.0,
|
|
414
|
+
}
|
|
415
|
+
governing = min(margins, key=lambda key: margins[key])
|
|
416
|
+
return {
|
|
417
|
+
"bearing_stress_ksi": sig_b, "net_tension_stress_ksi": sig_nt,
|
|
418
|
+
"tearout_stress_ksi": sig_te, "e_over_d": e / D, "d_over_t": D / t,
|
|
419
|
+
"bearing_margin": margins["bearing"],
|
|
420
|
+
"net_tension_margin": margins["net_tension"],
|
|
421
|
+
"tearout_margin": margins["tearout"],
|
|
422
|
+
"governing_mode": governing, "min_margin": margins[governing],
|
|
423
|
+
"passes": margins[governing] >= 0.0,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ---------------------------------------------------------------------------
|
|
428
|
+
# Fatigue logic (mirrors bound stress-life / goodman / miner leaves)
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def basquin_life(sig_a_ksi, a_ksi, b):
|
|
433
|
+
"""Cycles to failure N = (S/A)^(1/b) (Basquin S-N, bound leaf)."""
|
|
434
|
+
if not sig_a_ksi > 0 or not a_ksi > 0 or b == 0.0:
|
|
435
|
+
raise ValueError("need S > 0, A > 0, b != 0")
|
|
436
|
+
return (sig_a_ksi / a_ksi) ** (1.0 / b)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def goodman_effective_amplitude(sig_a_ksi, sig_m_ksi, f_tu_ksi):
|
|
440
|
+
"""Fully-reversed equivalent amplitude: Sa/(1 - Sm/Ftu) (modified
|
|
441
|
+
Goodman mean correction, bound goodman leaf)."""
|
|
442
|
+
f = _require_positive(f_tu_ksi, "Ftu")
|
|
443
|
+
sm = _require_nonnegative(sig_m_ksi, "Sm")
|
|
444
|
+
if sm >= f:
|
|
445
|
+
raise ValueError("mean stress must be below Ftu")
|
|
446
|
+
return _require_positive(sig_a_ksi, "Sa") / (1.0 - sm / f)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def cumulative_damage(cycles):
|
|
450
|
+
"""Palmgren-Miner damage fraction over (n, N) blocks (bound leaf)."""
|
|
451
|
+
if not cycles:
|
|
452
|
+
raise ValueError("cycle blocks must not be empty")
|
|
453
|
+
total = 0.0
|
|
454
|
+
for n, n_fail in cycles:
|
|
455
|
+
if n < 0:
|
|
456
|
+
raise ValueError("applied cycles must be >= 0")
|
|
457
|
+
if n_fail <= 0:
|
|
458
|
+
raise ValueError("cycles to failure must be > 0")
|
|
459
|
+
total += n / float(n_fail)
|
|
460
|
+
return total
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def fatigue_life_report(spectrum, sig_a_ksi, sig_m_ksi, mat):
|
|
464
|
+
"""Miner sum and equivalent-life multiples for a per-flight spectrum.
|
|
465
|
+
|
|
466
|
+
spectrum: list of (cycles_per_flight, load_factor_amplitude_dn).
|
|
467
|
+
The stress amplitude for each block scales with dn: the reference
|
|
468
|
+
amplitude sig_a_ksi corresponds to dn = 1.0.
|
|
469
|
+
"""
|
|
470
|
+
m = MATERIALS[mat]
|
|
471
|
+
a = m["sig_f_prime_ksi"]
|
|
472
|
+
b = m["b_fatigue"]
|
|
473
|
+
f_tu = m["Ftu_ksi"]
|
|
474
|
+
blocks = []
|
|
475
|
+
total_damage_per_flight = 0.0
|
|
476
|
+
for n_per_flight, dn in spectrum:
|
|
477
|
+
s_a = sig_a_ksi * dn
|
|
478
|
+
s_ar = goodman_effective_amplitude(s_a, sig_m_ksi, f_tu)
|
|
479
|
+
n_fail = basquin_life(s_ar, a, b)
|
|
480
|
+
blocks.append({"dn": dn, "cycles_per_flight": n_per_flight,
|
|
481
|
+
"Sa_ksi": s_a, "Sa_equiv_ksi": s_ar,
|
|
482
|
+
"N_fail": n_fail,
|
|
483
|
+
"damage_per_flight": n_per_flight / n_fail})
|
|
484
|
+
total_damage_per_flight += n_per_flight / n_fail
|
|
485
|
+
return {"blocks": blocks, "damage_per_flight": total_damage_per_flight,
|
|
486
|
+
"A_ksi": a, "b": b}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ---------------------------------------------------------------------------
|
|
490
|
+
# Dynamics (mirrors bound beam-vibration leaf: cantilever first root)
|
|
491
|
+
# ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
CANTILEVER_BETA1_L = 1.87510407
|
|
494
|
+
# Equivalent uniform-beam stiffness factor: a real wing tapers, so the
|
|
495
|
+
# equivalent uniform cantilever uses a fraction of the root box EI
|
|
496
|
+
# (standard preliminary-dynamics idealization, ~0.25-0.35 of root EI).
|
|
497
|
+
WING_EI_EQUIVALENT_FACTOR = 0.30
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def cantilever_frequency_hz(e_ksi, i_in4, mass_per_len_lb_s2_in,
|
|
501
|
+
length_in, stiffness_factor=1.0):
|
|
502
|
+
"""First cantilever bending frequency f = (b1 L)^2/(2pi) sqrt(EI/(m L^4))
|
|
503
|
+
with m the mass per unit length in lb*s^2/in and stiffness_factor the
|
|
504
|
+
fraction of the given EI used by the equivalent uniform beam."""
|
|
505
|
+
ei = (_require_positive(e_ksi, "E") * 1000.0
|
|
506
|
+
* _require_positive(i_in4, "I")
|
|
507
|
+
* _require_positive(stiffness_factor, "stiffness_factor"))
|
|
508
|
+
m = _require_positive(mass_per_len_lb_s2_in, "m")
|
|
509
|
+
length = _require_positive(length_in, "L")
|
|
510
|
+
return (CANTILEVER_BETA1_L ** 2 / (2.0 * math.pi)
|
|
511
|
+
* math.sqrt(ei / (m * length ** 4)))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
# Example wing (the role's reference item) - one coherent, self-consistent
|
|
516
|
+
# regional-transport-class wing sized by the rules above.
|
|
517
|
+
# ---------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@dataclass
|
|
521
|
+
class ExampleWing:
|
|
522
|
+
"""Project facts for the worked-example wing (reference item).
|
|
523
|
+
|
|
524
|
+
A 100,000 lb regional-transport-class wing (FAR 25). FAR 25.337(b)
|
|
525
|
+
gives the positive limit maneuvering factor n = max(2.5,
|
|
526
|
+
min(3.8, 2.1 + 24000/(W+10000))) = 2.5 at this weight, so the
|
|
527
|
+
corner sits at VA = VS*sqrt(2.5), matching the bound gust leaf's
|
|
528
|
+
own worked-envelope anchor.
|
|
529
|
+
"""
|
|
530
|
+
name: str = "Example regional transport wing"
|
|
531
|
+
aircraft: str = "Example 100,000 lb regional transport (FAR 25)"
|
|
532
|
+
category: str = "transport"
|
|
533
|
+
gross_weight_lb: float = 100_000.0
|
|
534
|
+
wing_loading_psf: float = 100.0 # W/S (kept when re-sizing by weight)
|
|
535
|
+
aspect_ratio: float = 8.0 # AR (kept when re-sizing by weight)
|
|
536
|
+
wing_area_ft2: float = 1_000.0
|
|
537
|
+
span_ft: float = 89.44 # AR 8, cbar = S/b = 11.18 ft
|
|
538
|
+
cbar_ft: float = 11.18
|
|
539
|
+
lift_curve_slope: float = 5.7 # 1/rad
|
|
540
|
+
stall_speed_fps: float = 230.0 # EAS
|
|
541
|
+
dive_speed_fps: float = 621.0 # EAS
|
|
542
|
+
altitude_ft: float = 0.0
|
|
543
|
+
# wing weight fraction for dynamics (regional transport band)
|
|
544
|
+
wing_weight_fraction: float = 0.15
|
|
545
|
+
material: str = "al-7075-t6"
|
|
546
|
+
# Root box section (candidate design being checked)
|
|
547
|
+
box_depth_in: float = 40.0 # spar cap centroid separation
|
|
548
|
+
cap_area_in2: float = 17.5 # total cap area per surface (both spars)
|
|
549
|
+
web_thickness_in: float = 0.08 # per web
|
|
550
|
+
web_depth_in: float = 38.0 # effective web depth
|
|
551
|
+
# Compression surface (upper skin/stringer between ribs)
|
|
552
|
+
stringer_pitch_in: float = 6.0 # skin bay width b
|
|
553
|
+
skin_thickness_in: float = 0.25
|
|
554
|
+
stringer_area_in2: float = 1.2
|
|
555
|
+
stringer_i_in4: float = 0.60 # weak axis, panel buckling direction
|
|
556
|
+
rib_pitch_in: float = 22.0 # column length for stringer
|
|
557
|
+
# Root attachment lug (wing-to-fuselage vertical shear, per lug pair)
|
|
558
|
+
lug_hole_diameter_in: float = 3.0
|
|
559
|
+
lug_thickness_in: float = 0.75
|
|
560
|
+
lug_edge_distance_in: float = 4.5
|
|
561
|
+
# Landing gear stations (for the ground-loads row)
|
|
562
|
+
gear_nose_to_cg_ft: float = 18.0
|
|
563
|
+
gear_cg_to_main_ft: float = 6.0
|
|
564
|
+
# Fatigue: gust cycle count per flight at VC-scale gust intensity
|
|
565
|
+
fatigue_spectrum: list = field(default_factory=lambda: [
|
|
566
|
+
(1.0, 0.80), # 1 light-gust cycle/flight at dn = 0.80
|
|
567
|
+
(0.05, 1.40), # 1 per 20 flights at dn = 1.40
|
|
568
|
+
])
|
|
569
|
+
|
|
570
|
+
def resize_for_weight(self, weight_lb):
|
|
571
|
+
"""Re-derive the wing AND its box section for a new MTOW under
|
|
572
|
+
geometric similarity with the 100,000 lb baseline: planform keeps
|
|
573
|
+
the example's W/S and AR, and every length scales with the span
|
|
574
|
+
ratio while areas scale with its square (so margins land in the
|
|
575
|
+
same band)."""
|
|
576
|
+
w0 = 100_000.0
|
|
577
|
+
s0 = w0 / self.wing_loading_psf # baseline area
|
|
578
|
+
b0 = math.sqrt(self.aspect_ratio * s0) # baseline span
|
|
579
|
+
self.gross_weight_lb = _require_positive(weight_lb, "weight_lb")
|
|
580
|
+
self.wing_area_ft2 = self.gross_weight_lb / self.wing_loading_psf
|
|
581
|
+
self.span_ft = math.sqrt(self.aspect_ratio * self.wing_area_ft2)
|
|
582
|
+
self.cbar_ft = self.wing_area_ft2 / self.span_ft
|
|
583
|
+
r = self.span_ft / b0 # length ratio
|
|
584
|
+
self.box_depth_in *= r
|
|
585
|
+
self.cap_area_in2 *= r * r
|
|
586
|
+
self.web_thickness_in *= r
|
|
587
|
+
self.web_depth_in *= r
|
|
588
|
+
self.stringer_area_in2 *= r * r
|
|
589
|
+
self.stringer_i_in4 *= r ** 4
|
|
590
|
+
self.rib_pitch_in *= r
|
|
591
|
+
self.lug_hole_diameter_in *= r
|
|
592
|
+
self.lug_thickness_in *= r
|
|
593
|
+
self.lug_edge_distance_in *= r
|
|
594
|
+
return self
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _mat(item):
|
|
598
|
+
if item.material not in MATERIALS:
|
|
599
|
+
raise ValueError("unknown material %r" % (item.material,))
|
|
600
|
+
return MATERIALS[item.material]
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ---------------------------------------------------------------------------
|
|
604
|
+
# Report builder
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
def build_report(item: ExampleWing) -> dict:
|
|
608
|
+
"""Build the complete loads + strength report content model."""
|
|
609
|
+
mat = _mat(item)
|
|
610
|
+
vn = vn_diagram(
|
|
611
|
+
ws=item.wing_loading_psf, vs=item.stall_speed_fps,
|
|
612
|
+
vd=item.dive_speed_fps, a=item.lift_curve_slope,
|
|
613
|
+
cbar=item.cbar_ft, category=item.category,
|
|
614
|
+
altitude_ft=item.altitude_ft, weight_lb=item.gross_weight_lb)
|
|
615
|
+
design = design_limit_load_factor(vn)
|
|
616
|
+
env_margins = envelope_margins(vn)
|
|
617
|
+
|
|
618
|
+
n_lim = design["n_limit_pos"]
|
|
619
|
+
n_ult = FACTOR_OF_SAFETY * n_lim
|
|
620
|
+
# Elliptic span-load idealization: root shear per wing = n*W/2 and
|
|
621
|
+
# root bending M = n*W*b/(3 pi) (semi-ellipse centroid at 4/(3 pi) of
|
|
622
|
+
# the semi-span from the root).
|
|
623
|
+
v_root_ult = n_ult * item.gross_weight_lb / 2.0
|
|
624
|
+
m_root_ult_ftlb = (n_ult * item.gross_weight_lb * item.span_ft
|
|
625
|
+
/ (3.0 * math.pi))
|
|
626
|
+
m_root_ult_inlb = m_root_ult_ftlb * 12.0
|
|
627
|
+
m_root_limit_inlb = m_root_ult_inlb / FACTOR_OF_SAFETY
|
|
628
|
+
|
|
629
|
+
h = item.box_depth_in
|
|
630
|
+
a_cap = item.cap_area_in2
|
|
631
|
+
p_cap = m_root_ult_inlb / h # cap axial load, lb
|
|
632
|
+
sig_cap = p_cap / a_cap / 1000.0 # ksi
|
|
633
|
+
# Web shear: two webs carry the root shear
|
|
634
|
+
tau_web = v_root_ult / (2.0 * item.web_thickness_in
|
|
635
|
+
* item.web_depth_in) / 1000.0
|
|
636
|
+
|
|
637
|
+
e_ksi = mat["E_ksi"]
|
|
638
|
+
nu = mat["nu"]
|
|
639
|
+
# Compression surface: skin local buckling between stringers (long
|
|
640
|
+
# simply-supported plate, k = 4.0) and stringer column over the rib
|
|
641
|
+
# pitch (pinned-pinned).
|
|
642
|
+
sig_cr_skin = plate_buckling_stress(
|
|
643
|
+
e_ksi, nu, item.skin_thickness_in, item.stringer_pitch_in, k=4.0)
|
|
644
|
+
# Applied compression stress in the skin at the same fibre as the cap.
|
|
645
|
+
col = column_check(
|
|
646
|
+
e_ksi, item.stringer_i_in4, item.stringer_area_in2,
|
|
647
|
+
item.rib_pitch_in, "pinned-pinned",
|
|
648
|
+
applied_load_lb=sig_cap * 1000.0 * item.stringer_area_in2,
|
|
649
|
+
fcy_ksi=mat["Fty_ksi"])
|
|
650
|
+
|
|
651
|
+
lug = lug_analysis(
|
|
652
|
+
v_root_ult / 2.0, item.lug_hole_diameter_in, item.lug_thickness_in,
|
|
653
|
+
2.0 * item.lug_edge_distance_in, item.lug_edge_distance_in,
|
|
654
|
+
mat["Ftu_ksi"], mat["Fsu_ksi"], mat["Fbru_ksi"])
|
|
655
|
+
|
|
656
|
+
# Fatigue at the lower spar cap (root): 1g stress amplitude scale
|
|
657
|
+
m_1g_ftlb = (item.gross_weight_lb * item.span_ft / (3.0 * math.pi))
|
|
658
|
+
m_1g_inlb = m_1g_ftlb * 12.0
|
|
659
|
+
sig_1g = m_1g_inlb / h / a_cap / 1000.0 # ksi at 1g
|
|
660
|
+
# Reference amplitude for dn=1.0 about the 1g mean (gust cycle) and a
|
|
661
|
+
# GAG cycle 0->1g (mean sig_1g/2, amplitude sig_1g/2).
|
|
662
|
+
sig_a_ref = sig_1g * 0.5 # dn 1.0 about 1g mean
|
|
663
|
+
sig_m_gust = sig_1g
|
|
664
|
+
fatigue_gust = fatigue_life_report(item.fatigue_spectrum, sig_a_ref,
|
|
665
|
+
sig_m_gust, item.material)
|
|
666
|
+
# GAG cycle: 1 per flight, 0g -> 1g (wing root bending)
|
|
667
|
+
g_ar = goodman_effective_amplitude(sig_1g / 2.0, sig_1g / 2.0,
|
|
668
|
+
mat["Ftu_ksi"])
|
|
669
|
+
n_gag = basquin_life(g_ar, mat["sig_f_prime_ksi"], mat["b_fatigue"])
|
|
670
|
+
damage_per_flight = fatigue_gust["damage_per_flight"] + 1.0 / n_gag
|
|
671
|
+
|
|
672
|
+
# Dynamics: first wing bending frequency (uniform cantilever, root box
|
|
673
|
+
# stiffness, wing weight fraction over the semi-span)
|
|
674
|
+
i_box_in4 = 2.0 * a_cap * (h / 2.0) ** 2
|
|
675
|
+
wing_weight_lb = item.wing_weight_fraction * item.gross_weight_lb
|
|
676
|
+
semi_span_in = item.span_ft * 12.0 / 2.0
|
|
677
|
+
w_per_in = (wing_weight_lb / 2.0) / semi_span_in # lb/in per wing
|
|
678
|
+
mass_per_len = w_per_in / 386.09 # lb*s^2/in per wing
|
|
679
|
+
f1 = cantilever_frequency_hz(e_ksi, i_box_in4, mass_per_len,
|
|
680
|
+
semi_span_in,
|
|
681
|
+
stiffness_factor=WING_EI_EQUIVALENT_FACTOR)
|
|
682
|
+
|
|
683
|
+
# Ground loads (level landing at MLW = gross weight in example)
|
|
684
|
+
ground = level_landing_reactions(
|
|
685
|
+
item.gross_weight_lb, item.gear_nose_to_cg_ft,
|
|
686
|
+
item.gear_cg_to_main_ft, N_LEVEL_DEFAULT)
|
|
687
|
+
decel_g = braked_roll_deceleration(item.gear_nose_to_cg_ft,
|
|
688
|
+
item.gear_cg_to_main_ft, 0.8)
|
|
689
|
+
|
|
690
|
+
margins = [
|
|
691
|
+
{"component": "Lower spar cap (tension, root)",
|
|
692
|
+
"load": "P = M_ult/h = %.0f lb" % p_cap,
|
|
693
|
+
"applied_ksi": sig_cap,
|
|
694
|
+
"allowable": "Ftu = %.1f ksi" % mat["Ftu_ksi"],
|
|
695
|
+
"MS": margin_of_safety(mat["Ftu_ksi"], sig_cap),
|
|
696
|
+
"basis": "boom couple at box depth %.0f in; 7075-T6 Ftu per bound "
|
|
697
|
+
"materials/lug leaf anchors (MMPDS B-basis to confirm)" % h},
|
|
698
|
+
{"component": "Upper spar cap (compression, root)",
|
|
699
|
+
"load": "P = M_ult/h = %.0f lb" % p_cap,
|
|
700
|
+
"applied_ksi": sig_cap,
|
|
701
|
+
"allowable": "Fcy = %.1f ksi" % mat["Fty_ksi"],
|
|
702
|
+
"MS": margin_of_safety(mat["Fty_ksi"], sig_cap),
|
|
703
|
+
"basis": "compression material limit (yield); local stability of "
|
|
704
|
+
"the stiffened panel carried by the skin/stringer rows"},
|
|
705
|
+
{"component": "Spar web (shear, two webs)",
|
|
706
|
+
"load": "V_ult = %.0f lb" % v_root_ult,
|
|
707
|
+
"applied_ksi": tau_web,
|
|
708
|
+
"allowable": "Fsu = %.1f ksi" % mat["Fsu_ksi"],
|
|
709
|
+
"MS": margin_of_safety(mat["Fsu_ksi"], tau_web),
|
|
710
|
+
"basis": "V_ult/(2 t h_web); 7075-T6 Fsu per bound lug leaf anchor "
|
|
711
|
+
"(MMPDS to confirm)"},
|
|
712
|
+
{"component": "Upper skin panel (compression buckling, between "
|
|
713
|
+
"stringers)",
|
|
714
|
+
"load": "sigma_app = %.1f ksi" % sig_cap,
|
|
715
|
+
"applied_ksi": sig_cap,
|
|
716
|
+
"allowable": "sigma_cr = %.1f ksi (k=4, ssss long plate)"
|
|
717
|
+
% sig_cr_skin,
|
|
718
|
+
"MS": margin_of_safety(sig_cr_skin, sig_cap),
|
|
719
|
+
"basis": "plate buckling sigma_cr = k pi^2 E/(12(1-nu^2)) (t/b)^2 "
|
|
720
|
+
"with b = stringer pitch %.1f in, t = %.2f in (bound "
|
|
721
|
+
"plate-buckling leaf)" % (item.stringer_pitch_in,
|
|
722
|
+
item.skin_thickness_in)},
|
|
723
|
+
{"component": "Upper stringer (column over rib pitch)",
|
|
724
|
+
"load": "P_app = %.0f lb" % (sig_cap * 1000.0
|
|
725
|
+
* item.stringer_area_in2),
|
|
726
|
+
"applied_ksi": sig_cap,
|
|
727
|
+
"allowable": ("Pcr = %.0f lb (Euler)" % col["pcr_lb"]
|
|
728
|
+
if col["euler_governs"]
|
|
729
|
+
else "Fcy*A = %.0f lb (yield-governed column)"
|
|
730
|
+
% col["allowable_load_lb"]),
|
|
731
|
+
"MS": col["margin_of_safety"],
|
|
732
|
+
"basis": ("Euler Pcr = pi^2 E I/(K L)^2, K=1 (pinned-pinned), "
|
|
733
|
+
"L = rib pitch %.1f in, lambda = %.1f > lambda_1 = %.1f"
|
|
734
|
+
% (item.rib_pitch_in, col["slenderness_ratio"],
|
|
735
|
+
col["transition_slenderness"])
|
|
736
|
+
if col["euler_governs"] else
|
|
737
|
+
"short column: lambda = %.1f < lambda_1 = %.1f, yield "
|
|
738
|
+
"governs (bound buckling leaf)"
|
|
739
|
+
% (col["slenderness_ratio"], col["transition_slenderness"]))},
|
|
740
|
+
{"component": "Wing root lug (vertical shear, one lug of pair)",
|
|
741
|
+
"load": "P = V_ult/2 = %.0f lb" % (v_root_ult / 2.0),
|
|
742
|
+
"applied_ksi": lug["bearing_stress_ksi"],
|
|
743
|
+
"allowable": ("min over bearing Fbru, net-tension Ftu, tearout Fsu; "
|
|
744
|
+
"governing mode %s" % lug["governing_mode"]),
|
|
745
|
+
"MS": lug["min_margin"],
|
|
746
|
+
"basis": "round-end lug: bearing, net section, tearout margins per "
|
|
747
|
+
"bound lug-joint leaf (e/D = %.2f, D/t = %.2f)"
|
|
748
|
+
% (lug["e_over_d"], lug["d_over_t"])},
|
|
749
|
+
]
|
|
750
|
+
for m in margins:
|
|
751
|
+
m["status"] = "PASS" if m["MS"] >= 0.0 else "RESIZE"
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
"document_type": "Loads and Strength Report",
|
|
755
|
+
"status": "draft-for-review",
|
|
756
|
+
"item": item.name,
|
|
757
|
+
"aircraft": item.aircraft,
|
|
758
|
+
"category": item.category,
|
|
759
|
+
"material": mat["label"],
|
|
760
|
+
"vn": vn,
|
|
761
|
+
"envelope_margins": env_margins,
|
|
762
|
+
"design": design,
|
|
763
|
+
"n_limit": n_lim,
|
|
764
|
+
"n_limit_neg": design["n_limit_neg"],
|
|
765
|
+
"n_ult": n_ult,
|
|
766
|
+
"loads_condition": design["condition_pos"],
|
|
767
|
+
"m_root_limit_inlb": m_root_limit_inlb,
|
|
768
|
+
"m_root_ult_inlb": m_root_ult_inlb,
|
|
769
|
+
"m_root_ult_ftlb": m_root_ult_ftlb,
|
|
770
|
+
"v_root_ult_lb": v_root_ult,
|
|
771
|
+
"ground": ground,
|
|
772
|
+
"decel_g": decel_g,
|
|
773
|
+
"margins": margins,
|
|
774
|
+
"dynamics_f1_hz": f1,
|
|
775
|
+
"geometry": {"box_depth_in": h, "cap_area_in2": a_cap,
|
|
776
|
+
"span_ft": item.span_ft,
|
|
777
|
+
"semi_span_ft": item.span_ft / 2.0,
|
|
778
|
+
"web_thickness_in": item.web_thickness_in,
|
|
779
|
+
"web_depth_in": item.web_depth_in,
|
|
780
|
+
"stringer_pitch_in": item.stringer_pitch_in,
|
|
781
|
+
"skin_thickness_in": item.skin_thickness_in,
|
|
782
|
+
"wing_weight_fraction": item.wing_weight_fraction},
|
|
783
|
+
"materials": {"E_ksi": mat["E_ksi"], "Ftu_ksi": mat["Ftu_ksi"],
|
|
784
|
+
"Fty_ksi": mat["Fty_ksi"], "Fsu_ksi": mat["Fsu_ksi"],
|
|
785
|
+
"Fbru_ksi": mat["Fbru_ksi"]},
|
|
786
|
+
"fatigue": {"sig_1g_ksi": sig_1g, "damage_per_flight": damage_per_flight,
|
|
787
|
+
"n_gag": n_gag, "gust_blocks": fatigue_gust["blocks"],
|
|
788
|
+
"A_ksi": fatigue_gust["A_ksi"], "b": fatigue_gust["b"],
|
|
789
|
+
"sig_m_gust_ksi": sig_m_gust},
|
|
790
|
+
"generated": _today(),
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
# Renderer
|
|
796
|
+
# ---------------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
def _fmt(x, nd=3):
|
|
799
|
+
return ("%.*f" % (nd, x))
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def render_report_markdown(model: dict) -> str:
|
|
803
|
+
"""Render the report content model as the deliverable markdown."""
|
|
804
|
+
vn = model["vn"]
|
|
805
|
+
em = model["envelope_margins"]
|
|
806
|
+
lines = [
|
|
807
|
+
"# Loads and Strength Report",
|
|
808
|
+
"",
|
|
809
|
+
f"**Item:** {model['item']}",
|
|
810
|
+
f"**Aircraft:** {model['aircraft']}",
|
|
811
|
+
f"**Category / basis:** {model['category']} category, FAR 25 "
|
|
812
|
+
"(Subpart C loads, Subpart D structure)",
|
|
813
|
+
f"**Status:** {model['status']}",
|
|
814
|
+
"",
|
|
815
|
+
"## 1. Loads",
|
|
816
|
+
"",
|
|
817
|
+
f"- Wing loading W/S = {_fmt(vn['ws'])} psf, mean geometric chord "
|
|
818
|
+
f"cbar = {_fmt(vn['cbar_ft'])} ft (cbar = S/b), lift-curve slope "
|
|
819
|
+
f"a = {_fmt(vn['a'])}/rad.",
|
|
820
|
+
f"- Design speeds (EAS): VS = {_fmt(vn['vs'])} ft/s, "
|
|
821
|
+
f"VA = {_fmt(vn['va'])} ft/s, VB = {_fmt(vn['vb'])} ft/s, "
|
|
822
|
+
f"VC = {_fmt(vn['vc'])} ft/s, VD = {_fmt(vn['vd'])} ft/s.",
|
|
823
|
+
f"- Gust alleviation (FAR 25.341(b)(2)): mass ratio "
|
|
824
|
+
f"mu_g = {_fmt(vn['mu_g'])}, alleviation factor "
|
|
825
|
+
f"K_g = {_fmt(vn['kg'])}.",
|
|
826
|
+
f"- Limit maneuvering load factor at VA = {_fmt(vn['n_positive'])} "
|
|
827
|
+
f"({vn['category']} category, FAR 25.337); negative limit "
|
|
828
|
+
f"{_fmt(vn['n_negative'])} at speeds up to VC.",
|
|
829
|
+
"- Discrete-gust limit load factors (design gust velocities U_de "
|
|
830
|
+
"of FAR 25.341(a) at sea level, fps EAS):",
|
|
831
|
+
"",
|
|
832
|
+
]
|
|
833
|
+
for p in vn["gust_points"]:
|
|
834
|
+
lines.append(
|
|
835
|
+
f" - {p['speed']} ({_fmt(p['v'])} ft/s, U_de = "
|
|
836
|
+
f"{_fmt(p['u_de'])} fps): n = {_fmt(p['n_pos'])} (positive), "
|
|
837
|
+
f"{_fmt(p['n_neg'])} (negative).")
|
|
838
|
+
env_lines = []
|
|
839
|
+
for label in ("VB", "VC", "VD"):
|
|
840
|
+
m = em[label]
|
|
841
|
+
verdict = ("gust-critical" if m["gust_critical_pos"]
|
|
842
|
+
else "maneuver envelope governs")
|
|
843
|
+
env_lines.append("%s margin %+.3f (%s)" % (label, m["margin_pos"],
|
|
844
|
+
verdict))
|
|
845
|
+
lines += [
|
|
846
|
+
"",
|
|
847
|
+
f"- Gust line vs maneuver envelope at VB/VC/VD: "
|
|
848
|
+
+ "; ".join(env_lines) + ".",
|
|
849
|
+
"",
|
|
850
|
+
f"- **Design limit condition: {model['loads_condition']}, "
|
|
851
|
+
f"n_limit = +{_fmt(model['n_limit'])}** (negative envelope "
|
|
852
|
+
f"limit {_fmt(model['n_limit_neg'])}).",
|
|
853
|
+
f"- **Ultimate loads: n_ult = 1.5 x {_fmt(model['n_limit'])} = "
|
|
854
|
+
f"{_fmt(model['n_ult'])} (factor of safety, FAR 25.303).**",
|
|
855
|
+
"",
|
|
856
|
+
"- Root loads per wing at the design condition (elliptic span-load "
|
|
857
|
+
"idealization: root shear = nW/2 and root bending M = nWb/(3 pi), "
|
|
858
|
+
"the semi-ellipse lift resultant standing at 4/(3 pi) of the "
|
|
859
|
+
"semi-span from the root):",
|
|
860
|
+
f" - Limit root bending moment = "
|
|
861
|
+
f"{model['m_root_limit_inlb'] / 1e6:.2f} M in-lb; ultimate root "
|
|
862
|
+
f"bending M_ult = {model['m_root_ult_inlb'] / 1e6:.2f} M in-lb "
|
|
863
|
+
f"({model['m_root_ult_ftlb'] / 1e6:.2f} M ft-lb).",
|
|
864
|
+
f" - Ultimate root shear V_ult = "
|
|
865
|
+
f"{model['v_root_ult_lb'] / 1000:.0f} klb per wing.",
|
|
866
|
+
"",
|
|
867
|
+
"- Ground loads - level landing at the limit vertical inertia "
|
|
868
|
+
f"factor {_fmt(N_LEVEL_DEFAULT)} (bound landing-ground-loads "
|
|
869
|
+
"leaf):",
|
|
870
|
+
f" - Nose gear reaction {model['ground']['nose_lb'] / 1000:.0f} "
|
|
871
|
+
f"klb; main gear reaction "
|
|
872
|
+
f"{model['ground']['main_lb'] / 1000:.0f} klb (total "
|
|
873
|
+
f"{model['ground']['total_lb'] / 1000:.0f} klb).",
|
|
874
|
+
f" - Braked-roll deceleration {_fmt(model['decel_g'], 2)} g "
|
|
875
|
+
"(braking friction 0.8 on the main gear).",
|
|
876
|
+
"- Spectra: fatigue and dynamic load spectra feed the fatigue "
|
|
877
|
+
"screening (Section 6); random-vibration / shock-response-spectrum "
|
|
878
|
+
"conditions for equipment-mounted structure are derived with the "
|
|
879
|
+
"bound random-vibration-analysis / shock-response-spectrum leaves "
|
|
880
|
+
"when that structure is in scope.",
|
|
881
|
+
"",
|
|
882
|
+
"## 2. Global model",
|
|
883
|
+
"",
|
|
884
|
+
"- Idealization: each wing is a cantilever box beam from the root "
|
|
885
|
+
"to the tip carrying the elliptic spanwise lift distribution "
|
|
886
|
+
"l(y) = l0 sqrt(1 - (2y/b)^2); root shear and bending then follow "
|
|
887
|
+
"in closed form (above), which is sufficient for the screening "
|
|
888
|
+
"margins of this report. A refined beam-frame/truss/CalculiX model "
|
|
889
|
+
"(bound FEM leaves) confirms station-by-station internal loads and "
|
|
890
|
+
"deflections in the detailed phase.",
|
|
891
|
+
f"- Boundary conditions: wing root fixed at the fuselage side of "
|
|
892
|
+
f"body; box depth h = {_fmt(model['geometry']['box_depth_in'])} "
|
|
893
|
+
"in between spar cap centroids at the root section (example "
|
|
894
|
+
"geometry).",
|
|
895
|
+
f"- Internal loads at the root: M_ult = "
|
|
896
|
+
f"{model['m_root_ult_inlb'] / 1e6:.2f} M in-lb, V_ult = "
|
|
897
|
+
f"{model['v_root_ult_lb'] / 1000:.0f} klb - used for every margin "
|
|
898
|
+
"below.",
|
|
899
|
+
"",
|
|
900
|
+
"## 3. Strength/stability margins (ultimate load)",
|
|
901
|
+
"",
|
|
902
|
+
"| Component | Load | Allowable | MS | Basis |",
|
|
903
|
+
"|---|---|---|---|---|",
|
|
904
|
+
]
|
|
905
|
+
for m in model["margins"]:
|
|
906
|
+
lines.append("| %s | %s | %s | **%+.3f** | %s |"
|
|
907
|
+
% (m["component"], m["load"], m["allowable"], m["MS"],
|
|
908
|
+
m["basis"]))
|
|
909
|
+
lines += [
|
|
910
|
+
"",
|
|
911
|
+
"## 4. Dynamics",
|
|
912
|
+
"",
|
|
913
|
+
f"- First wing bending frequency f1 = "
|
|
914
|
+
f"{_fmt(model['dynamics_f1_hz'])} Hz (uniform cantilever "
|
|
915
|
+
"idealization over the semi-span with the root box stiffness EI, "
|
|
916
|
+
"wing weight fraction applied; beta1*L = "
|
|
917
|
+
f"{_fmt(CANTILEVER_BETA1_L)}, bound beam-vibration leaf).",
|
|
918
|
+
"- Frequency placement vs rotor/propeller 1P and control-surface "
|
|
919
|
+
"excitation bands is confirmed against the bound "
|
|
920
|
+
"modal-analysis / beam-vibration leaves in the detailed phase.",
|
|
921
|
+
"",
|
|
922
|
+
"## 5. Materials",
|
|
923
|
+
"",
|
|
924
|
+
f"- Allowables used: 7075-T6 screening values from the bound "
|
|
925
|
+
"AeroSkills anchors (material-selection leaf E/Fty/UTS, "
|
|
926
|
+
"lug-joint worked example Fsu/Fbru, strain-life 7075-T6 class "
|
|
927
|
+
"fatigue constants). MMPDS (licensed) is referenced, never "
|
|
928
|
+
"reproduced; B-basis design allowables are confirmed by the "
|
|
929
|
+
"materials group before release:",
|
|
930
|
+
f" - E = {_fmt(model['materials']['E_ksi'], 0)} ksi; "
|
|
931
|
+
f"Ftu = {_fmt(model['materials']['Ftu_ksi'], 1)} ksi; "
|
|
932
|
+
f"Fty = {_fmt(model['materials']['Fty_ksi'], 1)} ksi; "
|
|
933
|
+
f"Fsu = {_fmt(model['materials']['Fsu_ksi'], 1)} ksi; "
|
|
934
|
+
f"Fbru = {_fmt(model['materials']['Fbru_ksi'], 1)} ksi.",
|
|
935
|
+
"- Ramberg-Osgood / creep / fracture data: not required for the "
|
|
936
|
+
"7075-T6 screening at room temperature; the bound "
|
|
937
|
+
"ramberg-osgood / creep-rupture / fracture-toughness leaves apply "
|
|
938
|
+
"where those regimes are in scope.",
|
|
939
|
+
"",
|
|
940
|
+
"## 6. Fatigue",
|
|
941
|
+
"",
|
|
942
|
+
f"- Critical location: lower spar cap at the root; 1g bending "
|
|
943
|
+
f"stress sigma_1g = {_fmt(model['fatigue']['sig_1g_ksi'], 1)} ksi.",
|
|
944
|
+
f"- S-N basis: Basquin S = A N^b with A = "
|
|
945
|
+
f"{_fmt(model['fatigue']['A_ksi'], 1)} ksi, b = "
|
|
946
|
+
f"{_fmt(model['fatigue']['b'], 2)} (7075-T6 class fatigue strength "
|
|
947
|
+
"constants, bound stress-life / strain-life leaves, "
|
|
948
|
+
"reference-only). Mean stress is corrected with the modified "
|
|
949
|
+
"Goodman rule Sa_eq = Sa / (1 - Sm/Ftu).",
|
|
950
|
+
"- Spectrum (per-flight cycle blocks at the lower spar cap, "
|
|
951
|
+
"amplitude dn about the 1g mean):",
|
|
952
|
+
"",
|
|
953
|
+
]
|
|
954
|
+
for blk in model["fatigue"]["gust_blocks"]:
|
|
955
|
+
lines.append(
|
|
956
|
+
" - %.2f cycle(s)/flight at dn = %.2f: Sa = %.1f ksi, "
|
|
957
|
+
"Sa_eq = %.1f ksi, N = %.2e cycles, damage per flight = %.2e"
|
|
958
|
+
% (blk["cycles_per_flight"], blk["dn"], blk["Sa_ksi"],
|
|
959
|
+
blk["Sa_equiv_ksi"], blk["N_fail"],
|
|
960
|
+
blk["damage_per_flight"]))
|
|
961
|
+
sig_1g = model["fatigue"]["sig_1g_ksi"]
|
|
962
|
+
mat = MATERIALS["al-7075-t6"]
|
|
963
|
+
g_ar = goodman_effective_amplitude(sig_1g / 2.0, sig_1g / 2.0,
|
|
964
|
+
mat["Ftu_ksi"])
|
|
965
|
+
lines += [
|
|
966
|
+
"",
|
|
967
|
+
f"- GAG cycle (0g to 1g, once per flight): Sa = {sig_1g / 2.0:.1f} "
|
|
968
|
+
f"ksi, Sm = {sig_1g / 2.0:.1f} ksi, Sa_eq = {g_ar:.1f} ksi, "
|
|
969
|
+
f"N = {model['fatigue']['n_gag']:.2e} cycles, damage per flight = "
|
|
970
|
+
f"{1.0 / model['fatigue']['n_gag']:.2e}.",
|
|
971
|
+
f"- **Palmgren-Miner cumulative damage sum per flight D = "
|
|
972
|
+
f"{model['fatigue']['damage_per_flight']:.2e}** (limit 1.0): "
|
|
973
|
+
"safe-life screening passes with equivalent life far beyond the "
|
|
974
|
+
"design life; production fatigue refines with the counted load "
|
|
975
|
+
"spectrum and detail notch factors from the bound "
|
|
976
|
+
"load-spectrum-counting / notch-sensitivity / goodman-diagram "
|
|
977
|
+
"leaves.",
|
|
978
|
+
"",
|
|
979
|
+
"## 7. Damage tolerance",
|
|
980
|
+
"",
|
|
981
|
+
"- Safe-life screening (Section 6) is the scope of this example "
|
|
982
|
+
"report; the damage tolerance evaluation per FAR 25.571 (crack "
|
|
983
|
+
"growth, residual strength, inspection intervals) is executed with "
|
|
984
|
+
"the bound crack-growth / residual-strength / "
|
|
985
|
+
"widespread-fatigue-damage leaves once the production spectrum and "
|
|
986
|
+
"crack-growth data are baselined.",
|
|
987
|
+
"- Bird strike (FAR 25.631) is a design load case for the leading "
|
|
988
|
+
"edge and windshield supports; the bound bird-strike leaf applies "
|
|
989
|
+
"when that structure is in scope.",
|
|
990
|
+
"",
|
|
991
|
+
"## 8. Composites / thermal",
|
|
992
|
+
"",
|
|
993
|
+
"- The example item is metallic (7075-T6); laminate first-ply-"
|
|
994
|
+
"failure and sandwich margins from the bound "
|
|
995
|
+
"laminate-stiffness / laminate-first-ply-failure / "
|
|
996
|
+
"failure-criteria / sandwich-panels leaves apply when composite "
|
|
997
|
+
"structure enters the design.",
|
|
998
|
+
"- Thermal stress and thermal buckling margins are computed with "
|
|
999
|
+
"the bound thermal-stress-analysis / thermal-buckling leaves for "
|
|
1000
|
+
"thermal-critical structure; no thermal-critical metallic margin "
|
|
1001
|
+
"is driven in this room-temperature screening.",
|
|
1002
|
+
"",
|
|
1003
|
+
"## 9. Margin summary",
|
|
1004
|
+
"",
|
|
1005
|
+
"| Component | MS | Status |",
|
|
1006
|
+
"|---|---|---|",
|
|
1007
|
+
]
|
|
1008
|
+
for m in model["margins"]:
|
|
1009
|
+
lines.append("| %s | %+.3f | %s |"
|
|
1010
|
+
% (m["component"], m["MS"], m["status"]))
|
|
1011
|
+
lines += [
|
|
1012
|
+
"",
|
|
1013
|
+
"---",
|
|
1014
|
+
f"*Generated by Aero Agent Roles structures-loads-engineer core "
|
|
1015
|
+
f"({model['generated']}). DRAFT for human stress lead review. Not "
|
|
1016
|
+
"an approval document - no certification approval is claimed, no "
|
|
1017
|
+
"compliance finding is declared. FEM results are as good as the "
|
|
1018
|
+
"model idealizations stated above.*",
|
|
1019
|
+
]
|
|
1020
|
+
return "\n".join(lines)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
# ---------------------------------------------------------------------------
|
|
1024
|
+
# Evidence gate checkers
|
|
1025
|
+
# ---------------------------------------------------------------------------
|
|
1026
|
+
|
|
1027
|
+
GATE_CHECKS = {
|
|
1028
|
+
"loads_stated": "limit/ultimate load factors present as numbers",
|
|
1029
|
+
"envelope_present": "V-n gust + maneuver envelope present",
|
|
1030
|
+
"margins_present": "every margin row has a load, an allowable, an MS "
|
|
1031
|
+
"and a basis",
|
|
1032
|
+
"fatigue_stated": "fatigue life basis + Miner sum present",
|
|
1033
|
+
"sign_off_honest": "document marked draft-for-review, not approval",
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def check_report(model: dict) -> dict:
|
|
1038
|
+
"""Run the evidence gates against a report model."""
|
|
1039
|
+
vn = model.get("vn", {})
|
|
1040
|
+
results = {
|
|
1041
|
+
"loads_stated": (isinstance(model.get("n_limit"), (int, float))
|
|
1042
|
+
and isinstance(model.get("n_ult"), (int, float))
|
|
1043
|
+
and isinstance(model.get("n_limit_neg"),
|
|
1044
|
+
(int, float))),
|
|
1045
|
+
"envelope_present": bool(vn.get("gust_points"))
|
|
1046
|
+
and bool(vn.get("maneuver_envelope")),
|
|
1047
|
+
"margins_present": bool(model.get("margins")) and all(
|
|
1048
|
+
all(m.get(k) is not None for k in
|
|
1049
|
+
("component", "load", "allowable", "MS", "basis"))
|
|
1050
|
+
for m in model["margins"]),
|
|
1051
|
+
"fatigue_stated": (model.get("fatigue", {}).get("damage_per_flight")
|
|
1052
|
+
is not None),
|
|
1053
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
1054
|
+
}
|
|
1055
|
+
results["all_pass"] = all(results.values())
|
|
1056
|
+
return results
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def check_report_markdown(md_text: str) -> dict:
|
|
1060
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
1061
|
+
low = md_text.lower()
|
|
1062
|
+
checks = {
|
|
1063
|
+
"has_title": "loads and strength report" in low,
|
|
1064
|
+
"has_loads": bool(re.search(r"n_ult\s*=\s*\d", md_text))
|
|
1065
|
+
and "design limit condition" in low,
|
|
1066
|
+
"has_margins_table": ("| component | load | allowable | ms |"
|
|
1067
|
+
in low),
|
|
1068
|
+
"has_ms_numbers": len(re.findall(r"\*\*[+-]?\d+\.\d{2,}\*\*",
|
|
1069
|
+
md_text)) >= 6,
|
|
1070
|
+
"has_fatigue": ("palmgren-miner" in low and "s-n basis" in low
|
|
1071
|
+
and "damage" in low),
|
|
1072
|
+
"has_draft_marker": "draft" in low,
|
|
1073
|
+
"has_not_approval": "not an approval" in low,
|
|
1074
|
+
}
|
|
1075
|
+
checks["all_pass"] = all(checks.values())
|
|
1076
|
+
return checks
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
1080
|
+
"""Public entry point used by gate tooling: check a report document."""
|
|
1081
|
+
return check_report_markdown(md_text)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
# ---------------------------------------------------------------------------
|
|
1085
|
+
# Example item / worked output (for tests and the filled template)
|
|
1086
|
+
# ---------------------------------------------------------------------------
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def example_item() -> ExampleWing:
|
|
1090
|
+
return ExampleWing()
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def example_report_markdown() -> str:
|
|
1094
|
+
return render_report_markdown(build_report(example_item()))
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
if __name__ == "__main__":
|
|
1098
|
+
item = example_item()
|
|
1099
|
+
model = build_report(item)
|
|
1100
|
+
md = render_report_markdown(model)
|
|
1101
|
+
print("DESIGN LIMIT: %s n_limit=%.2f n_ult=%.2f"
|
|
1102
|
+
% (model["loads_condition"], model["n_limit"], model["n_ult"]))
|
|
1103
|
+
print("Kg=%.3f mu_g=%.1f" % (model["vn"]["kg"], model["vn"]["mu_g"]))
|
|
1104
|
+
print("M_ult=%.2f M in-lb V_ult=%.0f lb"
|
|
1105
|
+
% (model["m_root_ult_inlb"] / 1e6, model["v_root_ult_lb"]))
|
|
1106
|
+
for m in model["margins"]:
|
|
1107
|
+
print(" MS %+7.3f %s" % (m["MS"], m["component"]))
|
|
1108
|
+
print("f1=%.2f Hz D/flight=%.2e" % (model["dynamics_f1_hz"],
|
|
1109
|
+
model["fatigue"]["damage_per_flight"]))
|
|
1110
|
+
print("GATES: %s" % check_report(model))
|
|
1111
|
+
print("MD GATES: %s" % check_report_markdown(md))
|
|
1112
|
+
print("RENDERED: %d chars" % len(md))
|