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,1291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""propulsion_core.py - Propulsion Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given a propulsion requirement (vehicle
|
|
5
|
+
class, thrust, flight point, cycle architecture) it computes the
|
|
6
|
+
on-design gas-turbine/turbofan cycle state points, net thrust, TSFC,
|
|
7
|
+
and efficiencies; builds the off-design envelope with corrected-flow
|
|
8
|
+
matching and component-map verdicts; sizes nozzles; and produces a
|
|
9
|
+
rocket-cycle (or electric) option when the item is a space vehicle.
|
|
10
|
+
It also gate-checks deliverables. Standalone: no external repo needed.
|
|
11
|
+
|
|
12
|
+
Domain rules encoded here come from the bound Aero Agent Skills leaves
|
|
13
|
+
(AeroSkills propulsion pack) which encode standard propulsion practice
|
|
14
|
+
(Mattingly-style cycle analysis, isentropic compressible flow, the
|
|
15
|
+
ideal-cycle and real-cycle gas-turbine relations, standard-day
|
|
16
|
+
correction). All numbers are produced by these relations from stated
|
|
17
|
+
inputs; representative inputs (component maps, stage counts) are
|
|
18
|
+
labeled as such. Fuel LHV is the kerosene-class 43.2 MJ/kg used by the
|
|
19
|
+
combustor-design leaf anchors. No proprietary text is reproduced.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import date
|
|
27
|
+
|
|
28
|
+
def _today() -> str:
|
|
29
|
+
import os
|
|
30
|
+
from datetime import date
|
|
31
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Physical constants (SI). Cold stream: air gamma=1.4 (R=287 => cp~1004.5).
|
|
35
|
+
# Hot stream (combustion products): gamma=1.33 per the propelling-nozzle leaf.
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
G0 = 9.80665
|
|
38
|
+
R_GAS = 287.0
|
|
39
|
+
GAMMA_C = 1.4 # cold (air) specific heat ratio
|
|
40
|
+
GAMMA_H = 1.33 # hot (products) specific heat ratio
|
|
41
|
+
CP_C = GAMMA_C * R_GAS / (GAMMA_C - 1.0) # ~1004.5 J/(kg K)
|
|
42
|
+
CP_H = GAMMA_H * R_GAS / (GAMMA_H - 1.0) # ~1156.5 J/(kg K)
|
|
43
|
+
LHV_JET_A = 43.2e6 # kerosene-class fuel LHV, J/kg (combustor leaf)
|
|
44
|
+
T_REF = 288.15 # ISA sea-level temperature, K
|
|
45
|
+
P_REF = 101325.0 # ISA sea-level pressure, Pa
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Domain tables (real rules / reference-typical values, from bound leaves)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# Component efficiency / loss defaults - reference-typical modern values,
|
|
52
|
+
# stated inputs in every deliverable (design assumptions, not vendor data).
|
|
53
|
+
DEFAULTS = {
|
|
54
|
+
"etaf": 0.90, # fan isentropic efficiency
|
|
55
|
+
"etac": 0.90, # core compressor isentropic efficiency
|
|
56
|
+
"etat": 0.90, # turbine isentropic efficiency
|
|
57
|
+
"etab": 0.995, # combustor efficiency
|
|
58
|
+
"etam": 0.99, # mechanical (shaft) efficiency
|
|
59
|
+
"pib": 0.95, # combustor total-pressure ratio (~5% loss)
|
|
60
|
+
"pid": 0.995, # inlet total-pressure recovery (subsonic duct)
|
|
61
|
+
"pifd": 0.99, # fan-duct total-pressure ratio (~1% loss)
|
|
62
|
+
"cv": 0.99, # nozzle velocity coefficient
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Rocket propellant reference table (reference-only typical values from the
|
|
66
|
+
# rocket-engine-cycle leaf): (rho_ox, rho_fuel, r_m, isp_vac). Densities
|
|
67
|
+
# kg/m3, r_m mixture ratio (O/F by mass), isp_vac in seconds at vacuum.
|
|
68
|
+
# r_m None marks the monopropellant hydrazine (single stream).
|
|
69
|
+
PROPELLANTS = {
|
|
70
|
+
"LOX/RP-1": (1140.0, 820.0, 2.56, 300.0),
|
|
71
|
+
"LOX/LH2": (1140.0, 71.0, 5.5, 430.0),
|
|
72
|
+
"N2O4/MMH": (1450.0, 880.0, 1.9, 320.0),
|
|
73
|
+
"hydrazine": (1010.0, 1010.0, None, 230.0),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Feed-cycle chamber-pressure bounds (documented limits from the
|
|
77
|
+
# rocket-engine-cycle leaf): pressure-fed to 3 MPa, expander (LOX/LH2 only)
|
|
78
|
+
# to 10 MPa. Gas-generator and staged-combustion: no pressure bound in the
|
|
79
|
+
# leaf's feasibility model for bipropellant pairs.
|
|
80
|
+
PRESSURE_FED_MAX_P_C = 3.0e6
|
|
81
|
+
EXPANDER_MAX_P_C = 10.0e6
|
|
82
|
+
P_LOSSES_DEFAULT = 2.0e6 # injector plus line losses, Pa
|
|
83
|
+
ETA_PUMP_DEFAULT = 0.7
|
|
84
|
+
ETA_TURB_DEFAULT = 0.6
|
|
85
|
+
PUMP_INLET_PRESSURE = 0.3e6
|
|
86
|
+
GG_FRACTION_DEFAULT = 0.03
|
|
87
|
+
GG_PRESSURE_FRACTION = 0.8
|
|
88
|
+
TURBINE_EXIT_PRESSURE = 0.2e6
|
|
89
|
+
GG_CP = 2000.0
|
|
90
|
+
GG_T_INLET = 1200.0
|
|
91
|
+
GG_GAMMA = 1.2
|
|
92
|
+
STAGED_INLET_FRACTION = 1.5
|
|
93
|
+
EXPANDER_CP = 12000.0 # warm hydrogen gas cp, reference-only
|
|
94
|
+
EXPANDER_T_INLET = 500.0
|
|
95
|
+
EXPANDER_GAMMA = 1.4
|
|
96
|
+
EXPANDER_EXIT_FRACTION = 0.85
|
|
97
|
+
|
|
98
|
+
# Throttle bands (turbofan-off-design leaf): fractions of max rating.
|
|
99
|
+
def throttle_verdict(frac: float) -> str:
|
|
100
|
+
"""Classify a throttle fraction: below-idle/idle/cruise/climb/
|
|
101
|
+
max-continuous/over-throttle."""
|
|
102
|
+
if frac <= 0.0 or frac > 1.05:
|
|
103
|
+
raise ValueError("throttle fraction must be in (0, 1.05]")
|
|
104
|
+
if frac < 0.05:
|
|
105
|
+
return "below-idle"
|
|
106
|
+
if frac < 0.30:
|
|
107
|
+
return "idle"
|
|
108
|
+
if frac < 0.65:
|
|
109
|
+
return "cruise"
|
|
110
|
+
if frac < 0.95:
|
|
111
|
+
return "climb"
|
|
112
|
+
if frac <= 1.00:
|
|
113
|
+
return "max-continuous"
|
|
114
|
+
return "over-throttle"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# ISA atmosphere (public standard): lapse to 11 km, isothermal above.
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def isa(alt_m: float) -> dict:
|
|
122
|
+
"""ISA temperature (K), pressure (Pa), density (kg/m3), speed of sound."""
|
|
123
|
+
if alt_m < 0:
|
|
124
|
+
raise ValueError("altitude must be >= 0")
|
|
125
|
+
if alt_m <= 11000.0:
|
|
126
|
+
t = 288.15 - 0.0065 * alt_m
|
|
127
|
+
p = P_REF * (t / T_REF) ** 5.2561
|
|
128
|
+
else:
|
|
129
|
+
t = 216.65
|
|
130
|
+
p = 22632.0 * math.exp(-G0 * (alt_m - 11000.0) / (R_GAS * t))
|
|
131
|
+
rho = p / (R_GAS * t)
|
|
132
|
+
a = math.sqrt(GAMMA_C * R_GAS * t)
|
|
133
|
+
return {"t": t, "p": p, "rho": rho, "a": a, "alt": alt_m}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Ideal/real-cycle relations (gas-turbine-cycle + real-cycle-effects leaves)
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def brayton_thermal_efficiency(pr: float, gamma: float = GAMMA_C) -> float:
|
|
141
|
+
"""Ideal Brayton thermal efficiency 1 - PR**((1-gamma)/gamma)."""
|
|
142
|
+
if pr <= 1:
|
|
143
|
+
raise ValueError("pressure ratio must be > 1")
|
|
144
|
+
if gamma <= 1:
|
|
145
|
+
raise ValueError("gamma must be > 1")
|
|
146
|
+
return 1.0 - pr ** ((1.0 - gamma) / gamma)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compressor_exit_temperature(t1: float, pr: float, gamma: float = GAMMA_C,
|
|
150
|
+
eta_c: float = 1.0) -> float:
|
|
151
|
+
"""Actual compressor exit temp: T2 = T1 + (T2s - T1)/eta_c."""
|
|
152
|
+
if t1 <= 0 or pr <= 1 or gamma <= 1 or not (0 < eta_c <= 1):
|
|
153
|
+
raise ValueError("invalid compressor input")
|
|
154
|
+
t2s = t1 * pr ** ((gamma - 1.0) / gamma)
|
|
155
|
+
return t1 + (t2s - t1) / eta_c
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def turbine_exit_temperature(t3: float, pr: float, gamma: float = GAMMA_H,
|
|
159
|
+
eta_t: float = 1.0) -> float:
|
|
160
|
+
"""Actual turbine exit temp: T4 = T3 - eta_t*(T3 - T4s)."""
|
|
161
|
+
if t3 <= 0 or pr <= 1 or gamma <= 1 or not (0 < eta_t <= 1):
|
|
162
|
+
raise ValueError("invalid turbine input")
|
|
163
|
+
t4s = t3 / pr ** ((gamma - 1.0) / gamma)
|
|
164
|
+
return t3 - eta_t * (t3 - t4s)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def real_thermal_efficiency(t1, t2, t3, t4) -> float:
|
|
168
|
+
"""Real-cycle thermal efficiency (cp cancels): net work / heat added."""
|
|
169
|
+
if not (0 < t1 < t2 < t3 and t1 < t4 < t3):
|
|
170
|
+
raise ValueError("station temperatures out of physical order")
|
|
171
|
+
return (t3 - t4 - t2 + t1) / (t3 - t2)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Compressible-flow / nozzle relations (propelling-nozzle + nozzle-design
|
|
176
|
+
# leaves): choked flow factor, critical ratio, isentropic area-Mach.
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def _choked_factor(gamma: float) -> float:
|
|
180
|
+
return (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _critical_ratio(gamma: float) -> float:
|
|
184
|
+
return ((gamma + 1.0) / 2.0) ** (gamma / (gamma - 1.0))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _area_ratio_at_mach(m: float, gamma: float) -> float:
|
|
188
|
+
return ((1.0 / m)
|
|
189
|
+
* (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
|
190
|
+
* (1.0 + (gamma - 1.0) / 2.0 * m * m) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0))))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def exit_mach_from_area_ratio(area_ratio: float, gamma: float = 1.2) -> float:
|
|
194
|
+
"""Supersonic exit Mach for a nozzle area ratio (bisection, M>1 root)."""
|
|
195
|
+
if area_ratio <= 1 or gamma <= 1:
|
|
196
|
+
raise ValueError("area ratio must be > 1, gamma > 1")
|
|
197
|
+
lo, hi = 1.0, 1.0
|
|
198
|
+
while _area_ratio_at_mach(hi, gamma) < area_ratio:
|
|
199
|
+
hi *= 2.0
|
|
200
|
+
for _ in range(200):
|
|
201
|
+
mid = 0.5 * (lo + hi)
|
|
202
|
+
if _area_ratio_at_mach(mid, gamma) < area_ratio:
|
|
203
|
+
lo = mid
|
|
204
|
+
else:
|
|
205
|
+
hi = mid
|
|
206
|
+
return 0.5 * (lo + hi)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def nozzle_regime(pt: float, p_amb: float, gamma: float) -> dict:
|
|
210
|
+
"""Choked/unchoked regime for a convergent nozzle at total pressure pt."""
|
|
211
|
+
if pt <= 0 or p_amb <= 0:
|
|
212
|
+
raise ValueError("pressures must be positive")
|
|
213
|
+
npr = pt / p_amb
|
|
214
|
+
if npr <= 1.0:
|
|
215
|
+
raise ValueError("nozzle pressure ratio must exceed 1")
|
|
216
|
+
return {"npr": npr, "critical": _critical_ratio(gamma),
|
|
217
|
+
"choked": npr >= _critical_ratio(gamma)}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def fully_expanded_velocity(tt: float, pt: float, p_amb: float,
|
|
221
|
+
gamma: float, cv: float = 1.0) -> float:
|
|
222
|
+
"""Isentropic fully-expanded jet velocity (Pe = Pa): m/s."""
|
|
223
|
+
if tt <= 0 or pt <= 0 or p_amb <= 0 or pt <= p_amb:
|
|
224
|
+
raise ValueError("invalid nozzle state")
|
|
225
|
+
if not (0 < cv <= 1):
|
|
226
|
+
raise ValueError("velocity coefficient in (0, 1]")
|
|
227
|
+
ratio = (p_amb / pt) ** ((gamma - 1.0) / gamma)
|
|
228
|
+
ve = math.sqrt((2.0 * gamma * R_GAS * tt / (gamma - 1.0)) * (1.0 - ratio))
|
|
229
|
+
return cv * ve
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def exit_static_state(tt: float, pt: float, p_amb: float, gamma: float):
|
|
233
|
+
"""Static state (Te, pe, ve) at a nozzle exit.
|
|
234
|
+
|
|
235
|
+
Choked (convergent): Me=1 at the throat. Unchoked: subsonic Mach
|
|
236
|
+
from the isentropic relation, exit static pressure = ambient.
|
|
237
|
+
"""
|
|
238
|
+
regime = nozzle_regime(pt, p_amb, gamma)
|
|
239
|
+
if regime["choked"]:
|
|
240
|
+
te = tt * 2.0 / (gamma + 1.0)
|
|
241
|
+
pe = pt * (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
|
242
|
+
ve = math.sqrt(gamma * R_GAS * te)
|
|
243
|
+
return {"te": te, "pe": pe, "ve": ve, "mach": 1.0, "choked": True}
|
|
244
|
+
me = math.sqrt(2.0 / (gamma - 1.0)
|
|
245
|
+
* (regime["npr"] ** ((gamma - 1.0) / gamma) - 1.0))
|
|
246
|
+
te = tt / (1.0 + (gamma - 1.0) / 2.0 * me * me)
|
|
247
|
+
ve = me * math.sqrt(gamma * R_GAS * te)
|
|
248
|
+
return {"te": te, "pe": p_amb, "ve": ve, "mach": me, "choked": False}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def throat_area_from_mass_flow(mdot: float, pt: float, tt: float,
|
|
252
|
+
gamma: float) -> float:
|
|
253
|
+
"""Choked throat area (m2) passing mdot at total state (pt, tt)."""
|
|
254
|
+
if mdot <= 0 or pt <= 0 or tt <= 0 or gamma <= 1:
|
|
255
|
+
raise ValueError("invalid choked-flow input")
|
|
256
|
+
return (mdot * math.sqrt(tt)
|
|
257
|
+
/ (pt * math.sqrt(gamma / R_GAS) * _choked_factor(gamma)))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def pressure_thrust(mdot: float, ve: float, pe: float, p_amb: float,
|
|
261
|
+
area: float) -> float:
|
|
262
|
+
"""Nozzle thrust F = mdot*ve + (pe - p_amb)*A (momentum + pressure term)."""
|
|
263
|
+
if mdot < 0 or area < 0 or pe <= 0 or p_amb <= 0:
|
|
264
|
+
raise ValueError("invalid thrust input")
|
|
265
|
+
return mdot * ve + (pe - p_amb) * area
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# Turbofan on-design point: closed-form separate-stream cycle
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# Stations: 0 freestream | 2 fan face | 13 fan exit | 3 compressor exit |
|
|
272
|
+
# 4 turbine inlet (TIT) | 5 turbine exit | 9 core nozzle exit | 19 fan
|
|
273
|
+
# nozzle exit. Power balance: fan (all air) + core compressor (core air)
|
|
274
|
+
# driven by the turbine (core gas). All quantities per kg/s of TOTAL
|
|
275
|
+
# inlet airflow unless scaled; component maps are representative input.
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def turbofan_design_point(m0: float, alt_m: float, bpr: float, fpr: float,
|
|
279
|
+
opr: float, tit: float, eta=None,
|
|
280
|
+
mdot0: float = 1.0) -> dict:
|
|
281
|
+
"""Separate-stream turbofan on-design cycle analysis (closed form).
|
|
282
|
+
|
|
283
|
+
Returns station total states (K, Pa), nozzle states, net thrust per
|
|
284
|
+
kg/s inlet air (momentum + pressure term, fully expanded or choked
|
|
285
|
+
convergent nozzle per the real regime), fuel/air ratio, TSFC and
|
|
286
|
+
the efficiency decomposition. Raise on non-physical inputs.
|
|
287
|
+
"""
|
|
288
|
+
if not (0 <= m0 < 1.0):
|
|
289
|
+
raise ValueError("subsonic Mach number required")
|
|
290
|
+
if alt_m < 0:
|
|
291
|
+
raise ValueError("altitude must be >= 0")
|
|
292
|
+
if bpr <= 0 or fpr <= 1 or opr <= 1 or tit <= 1500:
|
|
293
|
+
raise ValueError("invalid cycle architecture inputs")
|
|
294
|
+
if mdot0 <= 0:
|
|
295
|
+
raise ValueError("mass flow must be positive")
|
|
296
|
+
e = dict(DEFAULTS)
|
|
297
|
+
if eta:
|
|
298
|
+
e.update({k: v for k, v in eta.items() if k in DEFAULTS})
|
|
299
|
+
for k in ("etaf", "etac", "etat", "etab", "etam"):
|
|
300
|
+
if not (0 < e[k] <= 1):
|
|
301
|
+
raise ValueError(f"{k} must be in (0, 1]")
|
|
302
|
+
for k in ("pib", "pid", "pifd"):
|
|
303
|
+
if not (0 < e[k] <= 1):
|
|
304
|
+
raise ValueError(f"{k} must be in (0, 1]")
|
|
305
|
+
if not (0 < e["cv"] <= 1):
|
|
306
|
+
raise ValueError("cv must be in (0, 1]")
|
|
307
|
+
|
|
308
|
+
atm = isa(alt_m)
|
|
309
|
+
t0, p0 = atm["t"], atm["p"]
|
|
310
|
+
v0 = m0 * atm["a"]
|
|
311
|
+
ram = 1.0 + 0.5 * (GAMMA_C - 1.0) * m0 * m0
|
|
312
|
+
|
|
313
|
+
# 0 -> 2 inlet
|
|
314
|
+
tt2 = t0 * ram
|
|
315
|
+
pt2 = e["pid"] * p0 * ram ** (GAMMA_C / (GAMMA_C - 1.0))
|
|
316
|
+
# fan (all air)
|
|
317
|
+
tt13 = compressor_exit_temperature(tt2, fpr, GAMMA_C, e["etaf"])
|
|
318
|
+
pt13 = fpr * pt2
|
|
319
|
+
# core compressor: ratio OPR/FPR (OPR measured fan face -> HPC exit)
|
|
320
|
+
pr_core = opr / fpr
|
|
321
|
+
tt3 = compressor_exit_temperature(tt13, pr_core, GAMMA_C, e["etac"])
|
|
322
|
+
pt3 = opr * pt2
|
|
323
|
+
# combustor
|
|
324
|
+
pt4 = e["pib"] * pt3
|
|
325
|
+
tt4 = tit
|
|
326
|
+
f = (CP_H * tt4 - CP_C * tt3) / (e["etab"] * LHV_JET_A - CP_H * tt4)
|
|
327
|
+
if f <= 0:
|
|
328
|
+
raise ValueError("combustor energy balance gives non-positive f/a")
|
|
329
|
+
# power balance per kg/s total inlet air
|
|
330
|
+
cor = 1.0 / (1.0 + bpr) # core-air fraction
|
|
331
|
+
wfan = CP_C * (tt13 - tt2) # fan work per kg air (whole flow)
|
|
332
|
+
whpc = CP_C * (tt3 - tt13) # core compressor work per kg core air
|
|
333
|
+
wshaft = wfan + cor * whpc # shaft work per kg/s total air
|
|
334
|
+
wgas = wshaft / (e["etam"] * (1.0 + f) * cor) # turbine work per kg gas
|
|
335
|
+
tt5 = tt4 - wgas / CP_H
|
|
336
|
+
if tt5 <= 0:
|
|
337
|
+
raise ValueError("turbine work exceeds available enthalpy")
|
|
338
|
+
tt5s = tt4 - (tt4 - tt5) / e["etat"]
|
|
339
|
+
pt5 = pt4 * (tt5s / tt4) ** (GAMMA_H / (GAMMA_H - 1.0))
|
|
340
|
+
if pt5 <= p0:
|
|
341
|
+
raise ValueError("turbine exit below ambient: architecture infeasible")
|
|
342
|
+
|
|
343
|
+
# ---- nozzles (real regime: choked convergent or unchoked) ----
|
|
344
|
+
mdot_gas = cor * (1.0 + f) # core nozzle flow per kg/s inlet air
|
|
345
|
+
mdot_fan = 1.0 - cor
|
|
346
|
+
pt19 = e["pifd"] * pt13 # fan nozzle entry after duct loss
|
|
347
|
+
|
|
348
|
+
core = exit_static_state(tt5, pt5, p0, GAMMA_H)
|
|
349
|
+
fan = exit_static_state(tt13, pt19, p0, GAMMA_C)
|
|
350
|
+
# velocity coefficient applied to nozzle velocities
|
|
351
|
+
ve9 = e["cv"] * core["ve"]
|
|
352
|
+
ve19 = e["cv"] * fan["ve"]
|
|
353
|
+
# fully-expanded (Pe = Pa) jet velocities: the effective velocities of
|
|
354
|
+
# the momentum-only thrust form used by the bypass-ratio trade leaf
|
|
355
|
+
ve9_fe = (fully_expanded_velocity(tt5, pt5, p0, GAMMA_H, e["cv"])
|
|
356
|
+
if pt5 > p0 else ve9)
|
|
357
|
+
ve19_fe = (fully_expanded_velocity(tt13, pt19, p0, GAMMA_C, e["cv"])
|
|
358
|
+
if pt19 > p0 else ve19)
|
|
359
|
+
|
|
360
|
+
# nozzle exit streamtube areas (per kg/s of the stream flow)
|
|
361
|
+
a9 = mdot_gas * R_GAS * core["te"] / (p0 * ve9)
|
|
362
|
+
a19 = mdot_fan * R_GAS * fan["te"] / (p0 * ve19)
|
|
363
|
+
# choked throat area where the regime is choked
|
|
364
|
+
a9_star = (throat_area_from_mass_flow(mdot_gas, pt5, tt5, GAMMA_H)
|
|
365
|
+
if core["choked"] else None)
|
|
366
|
+
a19_star = (throat_area_from_mass_flow(mdot_fan, pt19, tt13, GAMMA_C)
|
|
367
|
+
if fan["choked"] else None)
|
|
368
|
+
|
|
369
|
+
# net thrust per kg/s total air: F = sum [mdot*(Vj - V0) + (Pe-P0)*A]
|
|
370
|
+
# (fuel mass flow neglected in the inlet momentum term, standard
|
|
371
|
+
# cycle-deck convention)
|
|
372
|
+
f_net = mdot_gas * (ve9 - v0) + (core["pe"] - p0) * a9 \
|
|
373
|
+
+ mdot_fan * (ve19 - v0) + (fan["pe"] - p0) * a19
|
|
374
|
+
if f_net <= 0:
|
|
375
|
+
raise ValueError("non-positive net thrust at this point")
|
|
376
|
+
|
|
377
|
+
# ---- efficiencies (per unit total inlet air) ----
|
|
378
|
+
# Overall: useful thrust power / fuel power. Propulsive: Froude-loss
|
|
379
|
+
# method, eta_p = F*V0 / (F*V0 + sum 0.5 mdot (Ve - V0)^2) - the
|
|
380
|
+
# residual kinetic energy left in both jets at the flight velocity.
|
|
381
|
+
# Thermal is the consistent remainder eta_o/eta_p (kinetic-energy
|
|
382
|
+
# method with the pressure-thrust work credited to the useful side).
|
|
383
|
+
f_air = f * cor # fuel per kg/s total inlet air
|
|
384
|
+
eta_o = f_net * v0 / (f_air * LHV_JET_A) if v0 > 0 else 0.0
|
|
385
|
+
p_useful = f_net * v0
|
|
386
|
+
p_loss = (0.5 * mdot_gas * (ve9 - v0) ** 2
|
|
387
|
+
+ 0.5 * mdot_fan * (ve19 - v0) ** 2)
|
|
388
|
+
eta_p = p_useful / (p_useful + p_loss) if p_useful > 0 else 0.0
|
|
389
|
+
eta_th = (eta_o / eta_p) if eta_p > 0 else 0.0
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"m0": m0, "alt": alt_m, "bpr": bpr, "fpr": fpr, "opr": opr,
|
|
393
|
+
"tit": tit, "t0": t0, "p0": p0, "v0": v0,
|
|
394
|
+
"stations": {
|
|
395
|
+
"2": {"tt": tt2, "pt": pt2, "name": "fan face"},
|
|
396
|
+
"13": {"tt": tt13, "pt": pt13, "name": "fan exit"},
|
|
397
|
+
"3": {"tt": tt3, "pt": pt3, "name": "compressor exit"},
|
|
398
|
+
"4": {"tt": tt4, "pt": pt4, "name": "turbine inlet (TIT)"},
|
|
399
|
+
"5": {"tt": tt5, "pt": pt5, "name": "turbine exit"},
|
|
400
|
+
},
|
|
401
|
+
"nozzle_core": {"tt": tt5, "pt": pt5, "te": core["te"],
|
|
402
|
+
"pe": core["pe"], "ve": ve9, "ve_fe": ve9_fe,
|
|
403
|
+
"mach": core["mach"],
|
|
404
|
+
"choked": core["choked"], "a_exit": a9,
|
|
405
|
+
"a_star": a9_star},
|
|
406
|
+
"nozzle_fan": {"tt": tt13, "pt": pt19, "te": fan["te"],
|
|
407
|
+
"pe": fan["pe"], "ve": ve19, "ve_fe": ve19_fe,
|
|
408
|
+
"mach": fan["mach"],
|
|
409
|
+
"choked": fan["choked"], "a_exit": a19,
|
|
410
|
+
"a_star": a19_star},
|
|
411
|
+
"f_air_core": f, "f_air_total": f_air, "mdot_core_frac": cor,
|
|
412
|
+
"mdot_fan_frac": mdot_fan, "f_net_per_kg": f_net,
|
|
413
|
+
"tsfc_kg_Ns": f_air / f_net,
|
|
414
|
+
"eta_o": eta_o, "eta_th": eta_th, "eta_p": eta_p,
|
|
415
|
+
"eta_brayton_ideal": brayton_thermal_efficiency(opr),
|
|
416
|
+
"w_shaft_per_kg": wshaft, "pr_core": pr_core, "mdot0": mdot0,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# Off-design envelope: corrected-flow matching (compressor-map + turbofan-
|
|
422
|
+
# off-design leaves) at fixed corrected flow and turbine inlet temperature.
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
def corrected_flow(mdot: float, tt: float, pt: float) -> float:
|
|
426
|
+
"""m_corr = mdot*sqrt(theta)/delta, theta=Tt/288.15, delta=Pt/101325."""
|
|
427
|
+
if mdot <= 0 or tt <= 0 or pt <= 0:
|
|
428
|
+
raise ValueError("invalid corrected-flow input")
|
|
429
|
+
theta = tt / T_REF
|
|
430
|
+
delta = pt / P_REF
|
|
431
|
+
return mdot * math.sqrt(theta) / delta
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def corrected_speed(n_phys: float, tt: float) -> float:
|
|
435
|
+
if n_phys < 0 or tt <= 0:
|
|
436
|
+
raise ValueError("invalid corrected-speed input")
|
|
437
|
+
return n_phys / math.sqrt(tt / T_REF)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def operating_line_clearance(pr_op: float, pr_surge: float) -> float:
|
|
441
|
+
"""Pressure-ratio clearance to the surge line at same corrected flow (%).
|
|
442
|
+
Positive = operating line below surge (safe side)."""
|
|
443
|
+
if pr_op <= 0 or pr_surge <= 0:
|
|
444
|
+
raise ValueError("pressure ratios must be positive")
|
|
445
|
+
return (pr_surge - pr_op) / pr_op * 100.0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def map_verdict(pr: float, pr_surge: float, threshold: float = 0.05) -> str:
|
|
449
|
+
"""on-map / approaching-surge / on-surge-line from PR gap to surge."""
|
|
450
|
+
if pr <= 1 or pr_surge <= pr:
|
|
451
|
+
raise ValueError("pr must exceed 1 and lie below surge")
|
|
452
|
+
if (pr_surge - pr) / pr_surge <= threshold:
|
|
453
|
+
return "approaching-surge"
|
|
454
|
+
return "on-map"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def surge_margin_flow(q_surge: float, q_op: float) -> float:
|
|
458
|
+
"""Flow-basis surge margin (%), (q_op - q_surge)/q_op*100."""
|
|
459
|
+
if q_surge <= 0 or q_op <= 0:
|
|
460
|
+
raise ValueError("flows must be positive")
|
|
461
|
+
return (q_op - q_surge) / q_op * 100.0
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def off_design_envelope(design: dict, points: list) -> list:
|
|
465
|
+
"""Off-design rows for flight points (name, alt_m, m0, throttle_frac).
|
|
466
|
+
|
|
467
|
+
Quick off-design model (turbofan-off-design leaf): the engine holds
|
|
468
|
+
constant corrected mass flow (nozzle-matched) and turbine inlet
|
|
469
|
+
temperature; physical airflow scales by delta/sqrt(theta); net
|
|
470
|
+
thrust re-computed through the same on-design cycle with the point's
|
|
471
|
+
flight conditions. Component map representative input: core
|
|
472
|
+
compressor operating point at design pressure ratio vs a
|
|
473
|
+
representative surge line 15% above the operating line.
|
|
474
|
+
"""
|
|
475
|
+
mdot_corr = corrected_flow(design["mdot0"], design["stations"]["2"]["tt"],
|
|
476
|
+
design["stations"]["2"]["pt"])
|
|
477
|
+
rows = []
|
|
478
|
+
for name, alt_m, m0, throttle in points:
|
|
479
|
+
p = turbofan_design_point(m0, alt_m, design["bpr"], design["fpr"],
|
|
480
|
+
design["opr"], design["tit"])
|
|
481
|
+
# matched corrected flow => physical flow at this point
|
|
482
|
+
tt2 = p["stations"]["2"]["tt"]
|
|
483
|
+
pt2 = p["stations"]["2"]["pt"]
|
|
484
|
+
mdot_phys = mdot_corr / (math.sqrt(tt2 / T_REF) / (pt2 / P_REF))
|
|
485
|
+
f_pt = p["f_net_per_kg"] * mdot_phys
|
|
486
|
+
# throttle scaling on net thrust (fuel flow scales with thrust at
|
|
487
|
+
# roughly constant SFC for the quick model)
|
|
488
|
+
f_thr = f_pt * throttle
|
|
489
|
+
mdot_fuel = p["tsfc_kg_Ns"] * f_pt * throttle
|
|
490
|
+
pr_surge = design["opr"] * 1.15 # representative surge line
|
|
491
|
+
op_clear = operating_line_clearance(design["opr"], pr_surge)
|
|
492
|
+
# representative map point at this flight condition (same corrected
|
|
493
|
+
# speed as design, core PR = design OPR): surge verdict
|
|
494
|
+
verdict = map_verdict(design["opr"], pr_surge)
|
|
495
|
+
rows.append({
|
|
496
|
+
"name": name, "alt_m": alt_m, "m0": m0, "throttle": throttle,
|
|
497
|
+
"throttle_verdict": throttle_verdict(throttle),
|
|
498
|
+
"mdot_phys": mdot_phys, "f_net": f_pt,
|
|
499
|
+
"f_throttled": f_thr, "tsfc_kg_Ns": p["tsfc_kg_Ns"],
|
|
500
|
+
"mdot_fuel": mdot_fuel, "corr_flow": mdot_corr,
|
|
501
|
+
"surge_clearance_pct": op_clear, "map_verdict": verdict,
|
|
502
|
+
"t0": p["t0"], "p0": p["p0"], "tt2": tt2, "pt2": pt2,
|
|
503
|
+
})
|
|
504
|
+
return rows
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# Bypass-ratio trade (bypass-ratio-trade leaf): first-order fan/core split
|
|
509
|
+
# at fixed core conditions and fixed total mass flow. mdot_fan =
|
|
510
|
+
# BPR/(1+BPR)*mdot_total, mdot_core = mdot_total/(1+BPR), F per stream =
|
|
511
|
+
# mdot*(Vj - V0), mdot_fuel = f*mdot_core, TSFC = 1e6*f*mdot_core/F_total.
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
def thrust_split(bpr: float, mdot_total: float, vj_core: float,
|
|
515
|
+
vj_fan: float, v0: float) -> dict:
|
|
516
|
+
"""Net thrust per stream F = mdot*(Vj - V0); each jet must thrust."""
|
|
517
|
+
if bpr < 0 or mdot_total <= 0:
|
|
518
|
+
raise ValueError("bpr >= 0 and mdot_total > 0 required")
|
|
519
|
+
for name, v in (("vj_core", vj_core), ("vj_fan", vj_fan), ("v0", v0)):
|
|
520
|
+
if v <= 0:
|
|
521
|
+
raise ValueError(f"{name} must be > 0")
|
|
522
|
+
if vj_core <= v0 or vj_fan <= v0:
|
|
523
|
+
raise ValueError("jet velocities must exceed flight velocity")
|
|
524
|
+
mdot_core = mdot_total / (1.0 + bpr)
|
|
525
|
+
mdot_fan = mdot_total * bpr / (1.0 + bpr)
|
|
526
|
+
f_core = mdot_core * (vj_core - v0)
|
|
527
|
+
f_fan = mdot_fan * (vj_fan - v0)
|
|
528
|
+
return {"bpr": bpr, "mdot_fan": mdot_fan, "mdot_core": mdot_core,
|
|
529
|
+
"f_core": f_core, "f_fan": f_fan,
|
|
530
|
+
"f_total": f_core + f_fan,
|
|
531
|
+
"specific_thrust": (f_core + f_fan) / mdot_total}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def bpr_trend(mdot_total: float, vj_core: float, vj_fan: float, v0: float,
|
|
535
|
+
f_core: float) -> list:
|
|
536
|
+
"""Trade across bypass ratios at fixed core/fan jet velocities.
|
|
537
|
+
|
|
538
|
+
Fixed core conditions and fixed total mass flow (leaf method):
|
|
539
|
+
raising BPR shifts flow to the fan stream, whose jet velocity is
|
|
540
|
+
much lower than the core's, so average jet velocity drops and TSFC
|
|
541
|
+
falls. Returns {bpr, f_total, specific_thrust, tsfc_g_kN_s}.
|
|
542
|
+
"""
|
|
543
|
+
if not 0.0 < f_core < 1.0:
|
|
544
|
+
raise ValueError("core fuel/air ratio must lie in (0, 1)")
|
|
545
|
+
out = []
|
|
546
|
+
for bpr in (2.0, 4.0, 6.0, 8.0, 10.0, 12.0):
|
|
547
|
+
s = thrust_split(bpr, mdot_total, vj_core, vj_fan, v0)
|
|
548
|
+
mdot_fuel = f_core * s["mdot_core"]
|
|
549
|
+
tsfc = 1e6 * mdot_fuel / s["f_total"] # g/(kN*s)
|
|
550
|
+
out.append({"bpr": bpr, "f_total": s["f_total"],
|
|
551
|
+
"specific_thrust": s["specific_thrust"],
|
|
552
|
+
"tsfc_g_kN_s": tsfc})
|
|
553
|
+
return out
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def propulsive_efficiency_froude(vj: float, v0: float) -> float:
|
|
557
|
+
"""Froude propulsive efficiency eta_p = 2/(1 + vj/v0)."""
|
|
558
|
+
if vj <= 0 or v0 <= 0:
|
|
559
|
+
raise ValueError("velocities must be positive")
|
|
560
|
+
return 2.0 / (1.0 + vj / v0)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
# Rocket propulsion (rocket-engine-cycle + nozzle-design + propellant-
|
|
565
|
+
# selection leaves): feed-cycle feasibility, mass split, nozzle sizing.
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
def propellant_pair_properties(pair: str):
|
|
569
|
+
try:
|
|
570
|
+
return PROPELLANTS[pair]
|
|
571
|
+
except (KeyError, TypeError):
|
|
572
|
+
raise ValueError(f"unknown propellant pair: {pair}")
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def rocket_mass_flow(f_n: float, isp_s: float, g0: float = G0) -> float:
|
|
576
|
+
"""mdot = F/(Isp*g0): mass flow from thrust and specific impulse."""
|
|
577
|
+
if f_n <= 0 or isp_s <= 0 or g0 <= 0:
|
|
578
|
+
raise ValueError("thrust, Isp and g0 must be positive")
|
|
579
|
+
return f_n / (isp_s * g0)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def mass_flow_split(f_n: float, isp_s: float, r_m, g0: float = G0):
|
|
583
|
+
"""(mdot, mdot_ox, mdot_f) for a bipropellant or monopropellant."""
|
|
584
|
+
mdot = rocket_mass_flow(f_n, isp_s, g0)
|
|
585
|
+
if r_m is None:
|
|
586
|
+
return mdot, mdot, 0.0
|
|
587
|
+
if r_m <= 0:
|
|
588
|
+
raise ValueError("mixture ratio must be positive")
|
|
589
|
+
mdot_ox = mdot * r_m / (1.0 + r_m)
|
|
590
|
+
mdot_f = mdot / (1.0 + r_m)
|
|
591
|
+
return mdot, mdot_ox, mdot_f
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def cycle_feasibility(cycle: str, propellant: str, p_c: float):
|
|
595
|
+
"""(feasible, reason): pressure-fed <=3 MPa; expander LOX/LH2 only
|
|
596
|
+
<=10 MPa; gas-generator and staged-combustion feasible for any
|
|
597
|
+
bipropellant pair (documented bounds from the rocket-engine-cycle
|
|
598
|
+
leaf)."""
|
|
599
|
+
if cycle not in ("pressure-fed", "gas-generator", "staged-combustion",
|
|
600
|
+
"expander"):
|
|
601
|
+
raise ValueError(f"unknown engine cycle: {cycle}")
|
|
602
|
+
if p_c <= 0:
|
|
603
|
+
raise ValueError("chamber pressure must be positive")
|
|
604
|
+
rho_ox, rho_fuel, r_m, isp = propellant_pair_properties(propellant)
|
|
605
|
+
if cycle == "pressure-fed":
|
|
606
|
+
if p_c <= PRESSURE_FED_MAX_P_C:
|
|
607
|
+
return True, "chamber pressure within the 3 MPa pressure-fed bound"
|
|
608
|
+
return False, (f"chamber pressure above the "
|
|
609
|
+
f"{PRESSURE_FED_MAX_P_C / 1e6:.0f} MPa pressure-fed bound")
|
|
610
|
+
if cycle == "expander":
|
|
611
|
+
if propellant != "LOX/LH2":
|
|
612
|
+
return False, ("expander drive needs the high heat-capacity LH2 "
|
|
613
|
+
f"fuel; {propellant} does not qualify")
|
|
614
|
+
if p_c > EXPANDER_MAX_P_C:
|
|
615
|
+
return False, ("chamber pressure above the 10 MPa expander bound")
|
|
616
|
+
return True, "LH2 fuel heated in the cooling jacket drives the turbine"
|
|
617
|
+
if r_m is None:
|
|
618
|
+
return False, (f"{cycle} needs an oxidizer/fuel pair; monopropellant "
|
|
619
|
+
"has no turbine drive gas split")
|
|
620
|
+
return True, "turbine drive gas carried at part or full chamber pressure"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def pump_power(mdot: float, p_discharge: float, p_inlet: float, rho: float,
|
|
624
|
+
eta_pump: float = ETA_PUMP_DEFAULT) -> float:
|
|
625
|
+
if mdot <= 0 or p_discharge <= p_inlet or rho <= 0:
|
|
626
|
+
raise ValueError("invalid pump input")
|
|
627
|
+
if not (0 < eta_pump <= 1):
|
|
628
|
+
raise ValueError("pump efficiency in (0, 1]")
|
|
629
|
+
return mdot * (p_discharge - p_inlet) / (rho * eta_pump)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def turbine_power(mdot_gas: float, cp: float, t_inlet: float, eta_t: float,
|
|
633
|
+
gamma: float, p_inlet: float, p_exit: float) -> float:
|
|
634
|
+
if mdot_gas <= 0 or cp <= 0 or t_inlet <= 0 or p_exit >= p_inlet:
|
|
635
|
+
raise ValueError("invalid turbine input")
|
|
636
|
+
if not (0 < eta_t <= 1) or gamma <= 1:
|
|
637
|
+
raise ValueError("turbine efficiency/gamma invalid")
|
|
638
|
+
pr = (p_exit / p_inlet) ** ((gamma - 1.0) / gamma)
|
|
639
|
+
return mdot_gas * cp * t_inlet * eta_t * (1.0 - pr)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def bulk_density(rho_ox: float, rho_fuel: float, r_m) -> float:
|
|
643
|
+
if rho_ox <= 0 or rho_fuel <= 0:
|
|
644
|
+
raise ValueError("densities must be positive")
|
|
645
|
+
if r_m is None:
|
|
646
|
+
return rho_ox
|
|
647
|
+
return (1.0 + r_m) / (r_m / rho_ox + 1.0 / rho_fuel)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def rocket_engine_cycle_analysis(cycle: str, f_n: float, p_c: float,
|
|
651
|
+
propellant: str, isp=None, p_losses=None,
|
|
652
|
+
eta_pump=None, eta_turb=None,
|
|
653
|
+
gg_fraction=None) -> dict:
|
|
654
|
+
"""Feed-cycle balance: mass flows, pump/turbine power, feasibility,
|
|
655
|
+
tank pressure, verdict. Mirrors the rocket-engine-cycle leaf model."""
|
|
656
|
+
rho_ox, rho_fuel, r_m, isp_vac = propellant_pair_properties(propellant)
|
|
657
|
+
isp_use = isp_vac if isp is None else isp
|
|
658
|
+
if isp_use <= 0:
|
|
659
|
+
raise ValueError("Isp must be positive")
|
|
660
|
+
p_losses = P_LOSSES_DEFAULT if p_losses is None else p_losses
|
|
661
|
+
eta_pump = ETA_PUMP_DEFAULT if eta_pump is None else eta_pump
|
|
662
|
+
eta_turb = ETA_TURB_DEFAULT if eta_turb is None else eta_turb
|
|
663
|
+
gg_fraction = GG_FRACTION_DEFAULT if gg_fraction is None else gg_fraction
|
|
664
|
+
if not (0 < eta_pump <= 1) or not (0 < eta_turb <= 1):
|
|
665
|
+
raise ValueError("efficiencies must lie in (0, 1]")
|
|
666
|
+
if not (0 < gg_fraction < 1):
|
|
667
|
+
raise ValueError("gas-generator fraction must lie in (0, 1)")
|
|
668
|
+
|
|
669
|
+
mdot, mdot_ox, mdot_f = mass_flow_split(f_n, isp_use, r_m)
|
|
670
|
+
feasible, reason = cycle_feasibility(cycle, propellant, p_c)
|
|
671
|
+
bulk = bulk_density(rho_ox, rho_fuel, r_m)
|
|
672
|
+
|
|
673
|
+
p_discharge = p_c + p_losses
|
|
674
|
+
pump_ox = pump_power(mdot_ox, p_discharge, PUMP_INLET_PRESSURE, rho_ox,
|
|
675
|
+
eta_pump) if mdot_ox > 0 else 0.0
|
|
676
|
+
pump_fuel = pump_power(mdot_f, p_discharge, PUMP_INLET_PRESSURE, rho_fuel,
|
|
677
|
+
eta_pump) if mdot_f > 0 else 0.0
|
|
678
|
+
pump_total = pump_ox + pump_fuel
|
|
679
|
+
turb_p = 0.0
|
|
680
|
+
drive_fraction = 0.0
|
|
681
|
+
if cycle == "gas-generator":
|
|
682
|
+
mdot_gg = gg_fraction * mdot
|
|
683
|
+
p_gg = GG_PRESSURE_FRACTION * p_c
|
|
684
|
+
turb_p = turbine_power(mdot_gg, GG_CP, GG_T_INLET, eta_turb, GG_GAMMA,
|
|
685
|
+
p_gg, TURBINE_EXIT_PRESSURE)
|
|
686
|
+
drive_fraction = gg_fraction
|
|
687
|
+
elif cycle == "staged-combustion":
|
|
688
|
+
p_pb = STAGED_INLET_FRACTION * p_c
|
|
689
|
+
turb_p = turbine_power(mdot, GG_CP, GG_T_INLET, eta_turb, GG_GAMMA,
|
|
690
|
+
p_pb, TURBINE_EXIT_PRESSURE)
|
|
691
|
+
drive_fraction = 1.0
|
|
692
|
+
elif cycle == "expander":
|
|
693
|
+
p_exit = EXPANDER_EXIT_FRACTION * p_c
|
|
694
|
+
turb_p = turbine_power(mdot_f, EXPANDER_CP, EXPANDER_T_INLET, eta_turb,
|
|
695
|
+
EXPANDER_GAMMA, p_c, p_exit)
|
|
696
|
+
drive_fraction = mdot_f / mdot if mdot > 0 else 0.0
|
|
697
|
+
power_balance = turb_p - pump_total
|
|
698
|
+
|
|
699
|
+
if not feasible:
|
|
700
|
+
verdict = f"cycle rejected: {reason}"
|
|
701
|
+
elif cycle == "pressure-fed":
|
|
702
|
+
verdict = (f"pressure-fed feasible at p_c {p_c / 1e6:.2f} MPa: tank "
|
|
703
|
+
f"pressure {p_discharge / 1e6:.2f} MPa; pump-fed trades "
|
|
704
|
+
"machinery against the heavy feed tank")
|
|
705
|
+
elif power_balance >= 0.0:
|
|
706
|
+
verdict = (f"{cycle} feasible: pump power {pump_total / 1e6:.3f} MW, "
|
|
707
|
+
f"turbine power {turb_p / 1e6:.3f} MW, surplus "
|
|
708
|
+
f"{power_balance / 1e6:.3f} MW")
|
|
709
|
+
else:
|
|
710
|
+
verdict = (f"{cycle} feasible on the pressure bound but underpowered: "
|
|
711
|
+
f"turbine {turb_p / 1e6:.3f} MW vs pump demand "
|
|
712
|
+
f"{pump_total / 1e6:.3f} MW")
|
|
713
|
+
return {
|
|
714
|
+
"cycle": cycle, "propellant": propellant, "feasible": feasible,
|
|
715
|
+
"reason": reason, "isp": isp_use, "mdot": mdot, "mdot_ox": mdot_ox,
|
|
716
|
+
"mdot_f": mdot_f, "mixture_ratio": r_m, "p_c": p_c,
|
|
717
|
+
"p_discharge": p_discharge, "pump_power_total": pump_total,
|
|
718
|
+
"turbine_power": turb_p, "power_balance": power_balance,
|
|
719
|
+
"drive_mass_fraction": drive_fraction, "verdict": verdict,
|
|
720
|
+
"bulk_density": bulk,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def rocket_nozzle_sizing(mdot: float, p_c: float, t_c: float, gamma: float,
|
|
725
|
+
area_ratio: float, r_gas: float = 693.0) -> dict:
|
|
726
|
+
"""Rocket nozzle from chamber state and area ratio (nozzle-design leaf).
|
|
727
|
+
|
|
728
|
+
Computes throat area from the choked-flow relation, supersonic exit
|
|
729
|
+
Mach from the isentropic area-Mach relation, exit pressure/velocity,
|
|
730
|
+
and the ideal thrust coefficient. Chamber properties (Tc, gamma,
|
|
731
|
+
R of the product gas) are stated inputs for the chosen propellant,
|
|
732
|
+
to be confirmed by equilibrium CEA at the design O/F.
|
|
733
|
+
"""
|
|
734
|
+
if mdot <= 0 or p_c <= 0 or t_c <= 0 or gamma <= 1 or area_ratio <= 1:
|
|
735
|
+
raise ValueError("invalid nozzle sizing input")
|
|
736
|
+
if r_gas <= 0:
|
|
737
|
+
raise ValueError("product-gas constant must be positive")
|
|
738
|
+
a_star = (mdot * math.sqrt(t_c)
|
|
739
|
+
/ (p_c * math.sqrt(gamma / r_gas) * _choked_factor(gamma)))
|
|
740
|
+
me = exit_mach_from_area_ratio(area_ratio, gamma)
|
|
741
|
+
pe = p_c / (1.0 + (gamma - 1.0) / 2.0 * me * me) ** (gamma / (gamma - 1.0))
|
|
742
|
+
ve = math.sqrt((2.0 * gamma * r_gas * t_c / (gamma - 1.0))
|
|
743
|
+
* (1.0 - (pe / p_c) ** ((gamma - 1.0) / gamma)))
|
|
744
|
+
a_exit = area_ratio * a_star
|
|
745
|
+
# ideal thrust coefficient at vacuum (pe > 0): Cf = F/(Pc At)
|
|
746
|
+
cf_ideal = (mdot * ve + pe * a_exit) / (p_c * a_star)
|
|
747
|
+
return {"a_star": a_star, "a_exit": a_exit, "me": me, "pe": pe,
|
|
748
|
+
"ve": ve, "cf_ideal": cf_ideal, "area_ratio": area_ratio,
|
|
749
|
+
"t_c": t_c, "gamma": gamma, "r_gas": r_gas}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# ---------------------------------------------------------------------------
|
|
753
|
+
# Item model + report builder
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
@dataclass
|
|
757
|
+
class PropulsionItem:
|
|
758
|
+
"""Project facts the role needs to build the propulsion design report."""
|
|
759
|
+
item_name: str
|
|
760
|
+
mission: str = "" # air transport / upper stage / ...
|
|
761
|
+
vehicle: str = ""
|
|
762
|
+
engine_kind: str = "turbofan" # turbofan | rocket | electric
|
|
763
|
+
thrust_requirement_n: float = 30000.0 # net thrust per engine (design pt)
|
|
764
|
+
cruise_mach: float = 0.78
|
|
765
|
+
cruise_alt_m: float = 10668.0 # FL350
|
|
766
|
+
bpr: float = 8.0
|
|
767
|
+
fpr: float = 1.6
|
|
768
|
+
opr: float = 30.0
|
|
769
|
+
tit_k: float = 1650.0
|
|
770
|
+
# rocket item fields
|
|
771
|
+
rocket_cycle: str = "expander"
|
|
772
|
+
propellant: str = "LOX/LH2"
|
|
773
|
+
chamber_pressure_pa: float = 4.0e6
|
|
774
|
+
chamber_temp_k: float = 3500.0 # stated input (CEA-confirmable)
|
|
775
|
+
nozzle_gamma: float = 1.2 # stated input (CEA-confirmable)
|
|
776
|
+
nozzle_area_ratio: float = 84.0
|
|
777
|
+
product_gas_r: float = 693.0 # LOX/LH2 products, MW~12 (stated input)
|
|
778
|
+
thrust_vac_n: float = 110000.0
|
|
779
|
+
# supporting facts
|
|
780
|
+
component_efficiencies: dict = field(default_factory=dict)
|
|
781
|
+
surge_line_factor: float = 1.15 # representative surge line above op
|
|
782
|
+
certification_basis: str = "FAR 33 context (engine), design study"
|
|
783
|
+
reference: str = ""
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def example_turbofan_item() -> PropulsionItem:
|
|
787
|
+
"""Reference item: high-bypass turbofan for a 200-seat-class twin
|
|
788
|
+
(BPR 8, OPR 30, TIT 1650 K) - the filled deliverable in templates/."""
|
|
789
|
+
return PropulsionItem(
|
|
790
|
+
item_name="High-bypass turbofan for a 200-seat twin (design study)",
|
|
791
|
+
mission="Subsonic transport cruise design point (M0.78, FL350)",
|
|
792
|
+
vehicle="200-seat twin-aisle-class transport, 2 wing-mounted engines",
|
|
793
|
+
engine_kind="turbofan",
|
|
794
|
+
thrust_requirement_n=30000.0,
|
|
795
|
+
cruise_mach=0.78,
|
|
796
|
+
cruise_alt_m=10668.0,
|
|
797
|
+
bpr=8.0, fpr=1.6, opr=30.0, tit_k=1650.0,
|
|
798
|
+
certification_basis="Design study in a FAR 33 engine-type context; "
|
|
799
|
+
"no certification approval claimed",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def example_rocket_item() -> PropulsionItem:
|
|
804
|
+
"""Reference space item: LOX/LH2 expander upper-stage engine."""
|
|
805
|
+
return PropulsionItem(
|
|
806
|
+
item_name="LOX/LH2 expander upper-stage engine (reference design)",
|
|
807
|
+
mission="Vacuum upper stage, reference sizing study",
|
|
808
|
+
vehicle="Expendable launch vehicle upper stage",
|
|
809
|
+
engine_kind="rocket",
|
|
810
|
+
rocket_cycle="expander",
|
|
811
|
+
propellant="LOX/LH2",
|
|
812
|
+
chamber_pressure_pa=4.0e6,
|
|
813
|
+
chamber_temp_k=3500.0,
|
|
814
|
+
nozzle_gamma=1.2,
|
|
815
|
+
nozzle_area_ratio=84.0,
|
|
816
|
+
thrust_vac_n=110000.0,
|
|
817
|
+
certification_basis="Design study; engine certification per FAR 33 "
|
|
818
|
+
"is out of scope (no approval claimed)",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
# ---------------------------------------------------------------------------
|
|
823
|
+
# Content model builders
|
|
824
|
+
# ---------------------------------------------------------------------------
|
|
825
|
+
|
|
826
|
+
def build_turbofan_report(item: PropulsionItem) -> dict:
|
|
827
|
+
"""Full propulsion design report content model (turbofan item)."""
|
|
828
|
+
if item.engine_kind != "turbofan":
|
|
829
|
+
raise ValueError("turbofan builder for a turbofan item")
|
|
830
|
+
design = turbofan_design_point(item.cruise_mach, item.cruise_alt_m,
|
|
831
|
+
item.bpr, item.fpr, item.opr, item.tit_k,
|
|
832
|
+
item.component_efficiencies, mdot0=1.0)
|
|
833
|
+
# scale to thrust requirement
|
|
834
|
+
mdot0 = item.thrust_requirement_n / design["f_net_per_kg"]
|
|
835
|
+
design["mdot0"] = mdot0
|
|
836
|
+
design["f_net"] = design["f_net_per_kg"] * mdot0
|
|
837
|
+
design["mdot_fuel"] = design["tsfc_kg_Ns"] * design["f_net"]
|
|
838
|
+
# SFC conversions
|
|
839
|
+
design["tsfc_g_kN_s"] = design["tsfc_kg_Ns"] * 1e6
|
|
840
|
+
design["sfc_lb_lbf_hr"] = design["tsfc_g_kN_s"] / 28.33
|
|
841
|
+
|
|
842
|
+
# envelope points: SLS static, climb, design cruise, cruise part-power,
|
|
843
|
+
# and top-of-cruise (name, alt_m, m0, throttle)
|
|
844
|
+
pts = [("SLS static", 0.0, 0.0, 1.00),
|
|
845
|
+
("FL250 climb", 7620.0, 0.78, 1.00),
|
|
846
|
+
("FL350 design cruise", 10668.0, 0.78, 1.00),
|
|
847
|
+
("FL350 part power", 10668.0, 0.78, 0.60),
|
|
848
|
+
("FL390 top of cruise", 11887.0, 0.80, 1.00)]
|
|
849
|
+
envelope = off_design_envelope(design, pts)
|
|
850
|
+
|
|
851
|
+
# representative core-compressor map point (map input, not vendor data):
|
|
852
|
+
# surge line set a stated factor above the operating-line pressure ratio
|
|
853
|
+
pr_surge = item.opr * item.surge_line_factor
|
|
854
|
+
|
|
855
|
+
# bypass-ratio trade (bypass-ratio-trade leaf): fixed core conditions
|
|
856
|
+
# and fixed total mass flow; jets held at the design-point velocities.
|
|
857
|
+
f_core = design["f_air_total"] * (1.0 + design["bpr"]) # core f/a
|
|
858
|
+
trade_rows = bpr_trend(mdot0, design["nozzle_core"]["ve_fe"],
|
|
859
|
+
design["nozzle_fan"]["ve_fe"], design["v0"],
|
|
860
|
+
f_core)
|
|
861
|
+
for r in trade_rows:
|
|
862
|
+
# overall efficiency proxy on the leaf's momentum thrust at V0
|
|
863
|
+
p_useful = r["f_total"] * design["v0"]
|
|
864
|
+
mdot_fuel = f_core * mdot0 / (1.0 + r["bpr"])
|
|
865
|
+
r["eta_o"] = (p_useful / (mdot_fuel * LHV_JET_A)
|
|
866
|
+
if mdot_fuel > 0 else 0.0)
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
"document_type": "Propulsion Design Report",
|
|
870
|
+
"status": "draft-for-review",
|
|
871
|
+
"item": item.item_name,
|
|
872
|
+
"vehicle": item.vehicle,
|
|
873
|
+
"mission": item.mission,
|
|
874
|
+
"engine_kind": "turbofan",
|
|
875
|
+
"certification_basis": item.certification_basis,
|
|
876
|
+
"requirement_n": item.thrust_requirement_n,
|
|
877
|
+
"design": design,
|
|
878
|
+
"component_eta": dict(DEFAULTS, **item.component_efficiencies),
|
|
879
|
+
"envelope": envelope,
|
|
880
|
+
"surge_line_factor": item.surge_line_factor,
|
|
881
|
+
"pr_surge_rep": pr_surge,
|
|
882
|
+
"bpr_trade": trade_rows,
|
|
883
|
+
"spool_note": "fan (LP) + core compressor (HP) driven by the turbine "
|
|
884
|
+
"through the closed-form power balance",
|
|
885
|
+
"generated": _today(),
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def build_rocket_report(item: PropulsionItem) -> dict:
|
|
890
|
+
"""Propulsion design report content model (rocket item)."""
|
|
891
|
+
if item.engine_kind != "rocket":
|
|
892
|
+
raise ValueError("rocket builder for a rocket item")
|
|
893
|
+
cycle = rocket_engine_cycle_analysis(
|
|
894
|
+
item.rocket_cycle, item.thrust_vac_n, item.chamber_pressure_pa,
|
|
895
|
+
item.propellant)
|
|
896
|
+
nozzle = rocket_nozzle_sizing(cycle["mdot"], item.chamber_pressure_pa,
|
|
897
|
+
item.chamber_temp_k, item.nozzle_gamma,
|
|
898
|
+
item.nozzle_area_ratio,
|
|
899
|
+
r_gas=item.product_gas_r)
|
|
900
|
+
# Isp check: F = mdot*Isp*g0 identity holds by construction of mdot;
|
|
901
|
+
# ideal-expansion Isp from the computed exit velocity
|
|
902
|
+
isp_ideal = nozzle["ve"] / G0
|
|
903
|
+
alternatives = []
|
|
904
|
+
for pair, (r_ox, r_f, r_m, isp_v) in PROPELLANTS.items():
|
|
905
|
+
alternatives.append({"pair": pair, "isp_vac": isp_v,
|
|
906
|
+
"bulk_density": bulk_density(r_ox, r_f, r_m)})
|
|
907
|
+
return {
|
|
908
|
+
"document_type": "Propulsion Design Report",
|
|
909
|
+
"status": "draft-for-review",
|
|
910
|
+
"item": item.item_name,
|
|
911
|
+
"vehicle": item.vehicle,
|
|
912
|
+
"mission": item.mission,
|
|
913
|
+
"engine_kind": "rocket",
|
|
914
|
+
"certification_basis": item.certification_basis,
|
|
915
|
+
"requirement_n": item.thrust_vac_n,
|
|
916
|
+
"cycle": cycle,
|
|
917
|
+
"nozzle": nozzle,
|
|
918
|
+
"isp_ideal": isp_ideal,
|
|
919
|
+
"alternatives": alternatives,
|
|
920
|
+
"generated": _today(),
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def build_report(item: PropulsionItem) -> dict:
|
|
925
|
+
if item.engine_kind == "rocket":
|
|
926
|
+
return build_rocket_report(item)
|
|
927
|
+
if item.engine_kind == "turbofan":
|
|
928
|
+
return build_turbofan_report(item)
|
|
929
|
+
raise ValueError(f"unsupported engine_kind {item.engine_kind!r}")
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
# ---------------------------------------------------------------------------
|
|
933
|
+
# Markdown renderers
|
|
934
|
+
# ---------------------------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
def _fmt(x, spec=".6g"):
|
|
937
|
+
return f"{x:{spec}}"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _kpa(p):
|
|
941
|
+
return p / 1000.0
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def render_turbofan_markdown(model: dict) -> str:
|
|
945
|
+
d = model["design"]
|
|
946
|
+
st = d["stations"]
|
|
947
|
+
nc, nf = d["nozzle_core"], d["nozzle_fan"]
|
|
948
|
+
lines = [
|
|
949
|
+
"# Propulsion Design Report",
|
|
950
|
+
"",
|
|
951
|
+
f"**Item:** {model['item']}",
|
|
952
|
+
f"**Vehicle / mission:** {model['vehicle']} - {model['mission']}",
|
|
953
|
+
f"**Engine class:** high-bypass turbofan "
|
|
954
|
+
f"(BPR {d['bpr']:.0f}, FPR {d['fpr']:.2f}, OPR {d['opr']:.0f}, "
|
|
955
|
+
f"TIT {d['tit']:.0f} K)",
|
|
956
|
+
f"**Status:** {model['status']}",
|
|
957
|
+
f"**Certification basis:** {model['certification_basis']}",
|
|
958
|
+
"",
|
|
959
|
+
"## 1. Requirement and selected cycle",
|
|
960
|
+
"",
|
|
961
|
+
f"- Thrust requirement (per engine, design point): "
|
|
962
|
+
f"{model['requirement_n'] / 1000.0:.1f} kN net at "
|
|
963
|
+
f"M{d['m0']:.2f}, {d['alt'] / 1000.0:.0f} km ISA.",
|
|
964
|
+
"- Selected cycle: separate-stream turbofan with the fan on the "
|
|
965
|
+
"low-pressure spool and the core compressor/turbine on the high-"
|
|
966
|
+
"pressure spool. Rationale: high bypass ratio (8) lowers mean jet "
|
|
967
|
+
"velocity and raises propulsive efficiency at a transonic cruise "
|
|
968
|
+
"Mach; the OPR 30 / TIT 1650 K core delivers the cruise SFC of "
|
|
969
|
+
f"{d['sfc_lb_lbf_hr']:.3f} lb/(lbf.h) computed below.",
|
|
970
|
+
f"- Design airflow (total, matched to the requirement): "
|
|
971
|
+
f"{d['mdot0']:.1f} kg/s.",
|
|
972
|
+
"",
|
|
973
|
+
"## 2. Cycle analysis (airbreathing)",
|
|
974
|
+
"",
|
|
975
|
+
"On-design station state table (total temperature / total pressure, "
|
|
976
|
+
"per kg/s inlet airflow basis; component efficiencies are stated "
|
|
977
|
+
"inputs below):",
|
|
978
|
+
"",
|
|
979
|
+
"| Station | Name | Tt (K) | Pt (kPa) |",
|
|
980
|
+
"|---|---|---|---|",
|
|
981
|
+
]
|
|
982
|
+
for s in ("2", "13", "3", "4", "5"):
|
|
983
|
+
lines.append(f"| {s} | {st[s]['name']} | {st[s]['tt']:.1f} | "
|
|
984
|
+
f"{_kpa(st[s]['pt']):.1f} |")
|
|
985
|
+
lines += [
|
|
986
|
+
"",
|
|
987
|
+
f"Core nozzle (station 9): exit static {nc['te']:.0f} K / "
|
|
988
|
+
f"{_kpa(nc['pe']):.1f} kPa, jet velocity {nc['ve']:.1f} m/s, "
|
|
989
|
+
f"Mach {nc['mach']:.3f}, {'choked' if nc['choked'] else 'unchoked'}, "
|
|
990
|
+
f"exit area {nc['a_exit'] * d['mdot0']:.4f} m^2 "
|
|
991
|
+
f"(per engine).",
|
|
992
|
+
f"Fan nozzle (station 19): exit static {nf['te']:.0f} K / "
|
|
993
|
+
f"{_kpa(nf['pe']):.1f} kPa, jet velocity {nf['ve']:.1f} m/s, "
|
|
994
|
+
f"Mach {nf['mach']:.3f}, {'choked' if nf['choked'] else 'unchoked'}, "
|
|
995
|
+
f"exit area {nf['a_exit'] * d['mdot0']:.4f} m^2 (per engine).",
|
|
996
|
+
"",
|
|
997
|
+
f"- Net thrust (per engine, design point): "
|
|
998
|
+
f"{d['f_net'] / 1000.0:.1f} kN "
|
|
999
|
+
f"= sum of m_dot*(V9 - V0) + (P9 - P0)*A9 over both streams.",
|
|
1000
|
+
f"- TSFC: {d['tsfc_g_kN_s']:.1f} g/(kN.s) "
|
|
1001
|
+
f"({d['sfc_lb_lbf_hr']:.3f} lb/(lbf.h)).",
|
|
1002
|
+
f"- Fuel/air ratio (core): {d['f_air_core']:.5f} "
|
|
1003
|
+
f"(total basis {d['f_air_total']:.5f}); fuel flow "
|
|
1004
|
+
f"{d['mdot_fuel']:.3f} kg/s per engine.",
|
|
1005
|
+
f"- Efficiencies: overall {d['eta_o']:.3f}, thermal "
|
|
1006
|
+
f"{d['eta_th']:.3f}, propulsive {d['eta_p']:.3f} (kinetic-energy "
|
|
1007
|
+
"method over both streams); ideal Brayton at OPR 30: "
|
|
1008
|
+
f"{d['eta_brayton_ideal']:.3f}.",
|
|
1009
|
+
"",
|
|
1010
|
+
"Input assumptions (stated): fan efficiency 0.90, core compressor "
|
|
1011
|
+
f"efficiency 0.90, turbine 0.90, combustor 0.995 (pressure ratio "
|
|
1012
|
+
f"{model['component_eta']['pib']:.2f}), inlet recovery "
|
|
1013
|
+
f"{model['component_eta']['pid']:.3f}, fan-duct ratio "
|
|
1014
|
+
f"{model['component_eta']['pifd']:.2f}, mechanical 0.99, nozzle "
|
|
1015
|
+
f"velocity coefficient {model['component_eta']['cv']:.2f}; "
|
|
1016
|
+
f"fuel LHV 43.2 MJ/kg (kerosene class). "
|
|
1017
|
+
"Component maps and stage counts are representative input.",
|
|
1018
|
+
"",
|
|
1019
|
+
"## 3. Off-design",
|
|
1020
|
+
"",
|
|
1021
|
+
"Quick off-design model (turbofan-off-design leaf): corrected mass "
|
|
1022
|
+
"flow and turbine inlet temperature held at the design value; "
|
|
1023
|
+
"physical airflow scales by delta/sqrt(theta); net thrust is "
|
|
1024
|
+
"recomputed through the on-design cycle at each flight point.",
|
|
1025
|
+
"",
|
|
1026
|
+
"| Point | Alt (km) | Mach | Throttle | Verdict | m_dot (kg/s) | "
|
|
1027
|
+
"F_net (kN) | TSFC g/(kN.s) | Surge clearance | Map verdict |",
|
|
1028
|
+
"|---|---|---|---|---|---|---|---|---|---|",
|
|
1029
|
+
]
|
|
1030
|
+
for r in model["envelope"]:
|
|
1031
|
+
lines.append(
|
|
1032
|
+
f"| {r['name']} | {r['alt_m'] / 1000.0:.0f} | {r['m0']:.2f} | "
|
|
1033
|
+
f"{r['throttle']:.2f} | {r['throttle_verdict']} | "
|
|
1034
|
+
f"{r['mdot_phys']:.1f} | {r['f_throttled'] / 1000.0:.1f} | "
|
|
1035
|
+
f"{r['tsfc_kg_Ns'] * 1e6:.1f} | "
|
|
1036
|
+
f"{r['surge_clearance_pct']:.0f}% | {r['map_verdict']} |")
|
|
1037
|
+
lines += [
|
|
1038
|
+
"",
|
|
1039
|
+
f"Surge clearance: representative core-compressor surge line set "
|
|
1040
|
+
f"{model['surge_line_factor'] * 100.0:.0f}% above the operating "
|
|
1041
|
+
f"line pressure ratio ({model['pr_surge_rep']:.0f} vs OPR "
|
|
1042
|
+
f"{d['opr']:.0f}); the operating line clears surge at every "
|
|
1043
|
+
"envelope point (representative map input, not vendor data).",
|
|
1044
|
+
"",
|
|
1045
|
+
"## 4. Component selection",
|
|
1046
|
+
"",
|
|
1047
|
+
"| Component | Choice | Basis |",
|
|
1048
|
+
"|---|---|---|",
|
|
1049
|
+
f"| Inlet | subsonic pitot, recovery {model['component_eta']['pid']:.3f} | cruise M{d['m0']:.2f} |",
|
|
1050
|
+
f"| Fan | single stage, PR {d['fpr']:.2f}, eta 0.90 | Tt {st['13']['tt']:.0f} K at design |",
|
|
1051
|
+
f"| Core compressor | multi-stage axial, PR {d['pr_core']:.1f} (OPR {d['opr']:.0f}/fan {d['fpr']:.2f}) | Tt {st['3']['tt']:.0f} K, representative map margin |",
|
|
1052
|
+
f"| Combustor | annular, eta 0.995, pressure ratio {model['component_eta']['pib']:.2f} | f/a {d['f_air_core']:.5f} |",
|
|
1053
|
+
f"| HP turbine | cooled (turbine-blade-cooling stage), eta 0.90 | Tt in {st['4']['tt']:.0f} K, Tt out {st['5']['tt']:.0f} K |",
|
|
1054
|
+
f"| Core nozzle | convergent, {'choked' if nc['choked'] else 'unchoked'} at design | area {nc['a_exit'] * d['mdot0']:.4f} m^2 |",
|
|
1055
|
+
f"| Fan nozzle | convergent, {'choked' if nf['choked'] else 'unchoked'} at design | area {nf['a_exit'] * d['mdot0']:.4f} m^2 |",
|
|
1056
|
+
"",
|
|
1057
|
+
"## 5. Integration",
|
|
1058
|
+
"",
|
|
1059
|
+
"- Ram-drag bookkeeping (engine-airframe integration): net thrust "
|
|
1060
|
+
"above already subtracts the inlet momentum drag m_dot_0*V0 "
|
|
1061
|
+
f"(= {d['mdot0'] * d['v0'] / 1000.0:.1f} kN at the design point).",
|
|
1062
|
+
f"- Installed performance: nacelle, pylon and bleed/accessory "
|
|
1063
|
+
"losses are NOT yet deducted; the uninstalled net thrust is "
|
|
1064
|
+
f"{d['f_net'] / 1000.0:.1f} kN per engine. Installation "
|
|
1065
|
+
"bookkeeping is a follow-on engine-airframe integration pass.",
|
|
1066
|
+
"",
|
|
1067
|
+
"## 6. Rocket / space option (if applicable)",
|
|
1068
|
+
"",
|
|
1069
|
+
"Not applicable to this airbreathing transport item - the engine "
|
|
1070
|
+
"selection is turbofan. For a space item the rocket-engine-cycle "
|
|
1071
|
+
"and nozzle-design stages apply (see the rocket reference example "
|
|
1072
|
+
"in the core / cli build --engine rocket).",
|
|
1073
|
+
"",
|
|
1074
|
+
"## 7. Electric option (if applicable)",
|
|
1075
|
+
"",
|
|
1076
|
+
"Not applicable: no electric-propulsion role for a 30 kN cruise "
|
|
1077
|
+
"thrust requirement at M0.78. Electric thrusters cover the "
|
|
1078
|
+
"low-thrust space regime (hall/gridded-ion), not airbreathing "
|
|
1079
|
+
"main propulsion.",
|
|
1080
|
+
"",
|
|
1081
|
+
"## 8. Bypass-ratio trade and recommendation",
|
|
1082
|
+
"",
|
|
1083
|
+
"First-order trade at fixed core conditions, fixed total mass flow "
|
|
1084
|
+
"and jet velocities held at the design-point values "
|
|
1085
|
+
"(bypass-ratio-trade leaf method):",
|
|
1086
|
+
"",
|
|
1087
|
+
"| BPR | F/m_dot (N per kg/s) | TSFC g/(kN.s) | eta_o |",
|
|
1088
|
+
"|---|---|---|---|",
|
|
1089
|
+
]
|
|
1090
|
+
for r in model["bpr_trade"]:
|
|
1091
|
+
lines.append(f"| {r['bpr']:.0f} | {r['specific_thrust']:.1f} | "
|
|
1092
|
+
f"{r['tsfc_g_kN_s']:.1f} | {r['eta_o']:.3f} |")
|
|
1093
|
+
lines += [
|
|
1094
|
+
"",
|
|
1095
|
+
"Raising BPR lowers specific thrust and TSFC by shifting flow to "
|
|
1096
|
+
"the fan stream (bypass-ratio-trade trend), at the cost of fan "
|
|
1097
|
+
"diameter and nacelle drag. BPR 8 balances specific thrust with "
|
|
1098
|
+
"nacelle integration for a 200-seat twin; the design point above "
|
|
1099
|
+
"is the recommendation. Open items: nacelle drag, turbine "
|
|
1100
|
+
"cooling flow, and the component map verification.",
|
|
1101
|
+
"",
|
|
1102
|
+
"---",
|
|
1103
|
+
f"*Generated by Aero Agent Roles propulsion-engineer core "
|
|
1104
|
+
f"({model['generated']}). DRAFT for human propulsion lead review. "
|
|
1105
|
+
"Not an approval document.*",
|
|
1106
|
+
]
|
|
1107
|
+
return "\n".join(lines)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def render_rocket_markdown(model: dict) -> str:
|
|
1111
|
+
c = model["cycle"]
|
|
1112
|
+
n = model["nozzle"]
|
|
1113
|
+
lines = [
|
|
1114
|
+
"# Propulsion Design Report",
|
|
1115
|
+
"",
|
|
1116
|
+
f"**Item:** {model['item']}",
|
|
1117
|
+
f"**Vehicle / mission:** {model['vehicle']} - {model['mission']}",
|
|
1118
|
+
f"**Engine class:** {c['cycle']} rocket engine, {c['propellant']}",
|
|
1119
|
+
f"**Status:** {model['status']}",
|
|
1120
|
+
f"**Certification basis:** {model['certification_basis']}",
|
|
1121
|
+
"",
|
|
1122
|
+
"## 1. Requirement and selected cycle",
|
|
1123
|
+
"",
|
|
1124
|
+
f"- Thrust requirement (vacuum): {model['requirement_n'] / 1000.0:.0f} kN.",
|
|
1125
|
+
f"- Selected cycle: {c['cycle']} ({c['reason']}). Propellant "
|
|
1126
|
+
f"rationale: {c['propellant']} gives the highest vacuum Isp "
|
|
1127
|
+
f"({c['isp']:.0f} s) of the bipropellant pairs screened; "
|
|
1128
|
+
f"LOX/LH2 is required for the expander drive gas.",
|
|
1129
|
+
"",
|
|
1130
|
+
"## 2. Cycle analysis (rocket feed system)",
|
|
1131
|
+
"",
|
|
1132
|
+
f"- Chamber pressure: {c['p_c'] / 1e6:.2f} MPa.",
|
|
1133
|
+
f"- Isp (vacuum, reference-typical): {c['isp']:.0f} s; mass flow "
|
|
1134
|
+
f"m_dot = F/(Isp*g0) = {c['mdot']:.2f} kg/s "
|
|
1135
|
+
f"(oxidizer {c['mdot_ox']:.2f} kg/s, fuel {c['mdot_f']:.2f} kg/s at "
|
|
1136
|
+
f"O/F {c['mixture_ratio']:.2f}).",
|
|
1137
|
+
f"- Pump discharge pressure: {c['p_discharge'] / 1e6:.2f} MPa; "
|
|
1138
|
+
f"pump power {c['pump_power_total'] / 1e6:.3f} MW, turbine power "
|
|
1139
|
+
f"{c['turbine_power'] / 1e6:.3f} MW.",
|
|
1140
|
+
f"- Feasibility verdict: {c['verdict']}.",
|
|
1141
|
+
"",
|
|
1142
|
+
"## 3. Nozzle design (expansion ratio trade)",
|
|
1143
|
+
"",
|
|
1144
|
+
"Chamber conditions (stated inputs for the propellant, to be "
|
|
1145
|
+
f"confirmed by equilibrium CEA): Tc {n['t_c']:.0f} K, gamma "
|
|
1146
|
+
f"{n['gamma']:.2f}.",
|
|
1147
|
+
f"- Throat area (choked flow): {n['a_star'] * 1e4:.2f} cm^2; "
|
|
1148
|
+
f"exit area at area ratio {n['area_ratio']:.0f}: "
|
|
1149
|
+
f"{n['a_exit'] * 1e4:.2f} cm^2.",
|
|
1150
|
+
f"- Exit Mach {n['me']:.2f}, exit static pressure "
|
|
1151
|
+
f"{n['pe'] / 1000.0:.2f} kPa, ideal exit velocity "
|
|
1152
|
+
f"{n['ve']:.0f} m/s (ideal vacuum Isp {model['isp_ideal']:.0f} s).",
|
|
1153
|
+
f"- Ideal vacuum thrust coefficient Cf = {n['cf_ideal']:.3f} at "
|
|
1154
|
+
f"this area ratio; vacuum operation keeps the nozzle underexpanded "
|
|
1155
|
+
"for any finite area ratio (optimum expansion is at infinite "
|
|
1156
|
+
"ratio), so the expansion trade is Cf gained vs nozzle mass.",
|
|
1157
|
+
"",
|
|
1158
|
+
"## 4. Propellant trade",
|
|
1159
|
+
"",
|
|
1160
|
+
"| Propellant | Isp_vac (s) | Bulk density (kg/m^3) |",
|
|
1161
|
+
"|---|---|---|",
|
|
1162
|
+
]
|
|
1163
|
+
for a in model["alternatives"]:
|
|
1164
|
+
lines.append(f"| {a['pair']} | {a['isp_vac']:.0f} | "
|
|
1165
|
+
f"{a['bulk_density']:.0f} |")
|
|
1166
|
+
lines += [
|
|
1167
|
+
"",
|
|
1168
|
+
"Density impulse (Isp x bulk density) matters for vehicle sizing: "
|
|
1169
|
+
"LOX/LH2 wins on Isp but loses on tank volume to LOX/RP-1 and "
|
|
1170
|
+
"N2O4/MMH; the reference choice is Isp-driven for an upper stage.",
|
|
1171
|
+
"",
|
|
1172
|
+
"## 5. Off-design / throttling",
|
|
1173
|
+
"",
|
|
1174
|
+
f"Expander-cycle throttling is bounded by the turbine drive-gas "
|
|
1175
|
+
f"temperature; reference-typical bounds: pressure-fed up to "
|
|
1176
|
+
f"{PRESSURE_FED_MAX_P_C / 1e6:.0f} MPa, expander (LOX/LH2) up to "
|
|
1177
|
+
f"{EXPANDER_MAX_P_C / 1e6:.0f} MPa chamber pressure. Deep "
|
|
1178
|
+
"throttling and start transients are follow-on stages "
|
|
1179
|
+
"(injector-design, thrust-vector-control).",
|
|
1180
|
+
"",
|
|
1181
|
+
"## 6. Integration",
|
|
1182
|
+
"",
|
|
1183
|
+
"Vacuum operation removes the nozzle pressure ambient term; "
|
|
1184
|
+
"installation losses (nozzle divergence, film cooling, TVC "
|
|
1185
|
+
"gimbal mass) are follow-on. Upper-stage burn sizing and "
|
|
1186
|
+
"propellant tanks are covered by the vehicle-level pass.",
|
|
1187
|
+
"",
|
|
1188
|
+
"---",
|
|
1189
|
+
f"*Generated by Aero Agent Roles propulsion-engineer core "
|
|
1190
|
+
f"({model['generated']}). DRAFT for human propulsion lead review. "
|
|
1191
|
+
"Not an approval document.*",
|
|
1192
|
+
]
|
|
1193
|
+
return "\n".join(lines)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def render_report_markdown(model: dict) -> str:
|
|
1197
|
+
if model["engine_kind"] == "rocket":
|
|
1198
|
+
return render_rocket_markdown(model)
|
|
1199
|
+
return render_turbofan_markdown(model)
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
# ---------------------------------------------------------------------------
|
|
1203
|
+
# Evidence gate checkers
|
|
1204
|
+
# ---------------------------------------------------------------------------
|
|
1205
|
+
|
|
1206
|
+
GATE_CHECKS = {
|
|
1207
|
+
"requirement_present": "thrust requirement is a positive number",
|
|
1208
|
+
"cycle_identified": "engine class and design point stated",
|
|
1209
|
+
"state_points_complete": "station T/p present with efficiencies",
|
|
1210
|
+
"thrust_computed": "net thrust number present",
|
|
1211
|
+
"sfc_computed": "TSFC/SFC number present",
|
|
1212
|
+
"assumptions_stated": "input assumptions and representative-map labels",
|
|
1213
|
+
"sign_off_honest": "document marked draft-for-review, not approval",
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def check_report(model: dict) -> dict:
|
|
1218
|
+
"""Run the evidence gates against a report content model."""
|
|
1219
|
+
if model["engine_kind"] == "turbofan":
|
|
1220
|
+
d = model["design"]
|
|
1221
|
+
results = {
|
|
1222
|
+
"requirement_present": model["requirement_n"] > 0,
|
|
1223
|
+
"cycle_identified": (model["engine_kind"] == "turbofan"
|
|
1224
|
+
and d["bpr"] > 0 and d["opr"] > 1),
|
|
1225
|
+
"state_points_complete": (len(d["stations"]) >= 5
|
|
1226
|
+
and all("tt" in s and "pt" in s
|
|
1227
|
+
for s in d["stations"].values())),
|
|
1228
|
+
"thrust_computed": d["f_net"] > 0,
|
|
1229
|
+
"sfc_computed": d["tsfc_kg_Ns"] > 0,
|
|
1230
|
+
"assumptions_stated": bool(model["component_eta"]),
|
|
1231
|
+
"sign_off_honest": model["status"] == "draft-for-review",
|
|
1232
|
+
}
|
|
1233
|
+
else:
|
|
1234
|
+
c = model["cycle"]
|
|
1235
|
+
results = {
|
|
1236
|
+
"requirement_present": model["requirement_n"] > 0,
|
|
1237
|
+
"cycle_identified": model["engine_kind"] == "rocket",
|
|
1238
|
+
"state_points_complete": c["mdot"] > 0 and c["p_c"] > 0,
|
|
1239
|
+
"thrust_computed": c["mdot"] * c["isp"] * G0 > 0,
|
|
1240
|
+
"sfc_computed": c["isp"] > 0,
|
|
1241
|
+
"assumptions_stated": bool(model["nozzle"]),
|
|
1242
|
+
"sign_off_honest": model["status"] == "draft-for-review",
|
|
1243
|
+
}
|
|
1244
|
+
results["all_pass"] = all(results.values())
|
|
1245
|
+
return results
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def check_report_markdown(md_text: str) -> dict:
|
|
1249
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
1250
|
+
low = md_text.lower()
|
|
1251
|
+
checks = {
|
|
1252
|
+
"has_title": "propulsion design report" in low,
|
|
1253
|
+
"has_cycle": ("turbofan" in low or "rocket engine" in low),
|
|
1254
|
+
"has_numbers": bool(re.search(r"kN|\d+\.\d+ g/\(kn\.s\)|\d+ s", low)),
|
|
1255
|
+
"has_draft_marker": "draft" in low,
|
|
1256
|
+
"has_not_approval": "not an approval" in low,
|
|
1257
|
+
}
|
|
1258
|
+
checks["all_pass"] = all(checks.values())
|
|
1259
|
+
return checks
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
1263
|
+
"""Public entry point used by gate tooling."""
|
|
1264
|
+
return check_report_markdown(md_text)
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
# ---------------------------------------------------------------------------
|
|
1268
|
+
# Examples for tests / worked-example generation
|
|
1269
|
+
# ---------------------------------------------------------------------------
|
|
1270
|
+
|
|
1271
|
+
def example_turbofan_markdown() -> str:
|
|
1272
|
+
return render_turbofan_markdown(build_turbofan_report(example_turbofan_item()))
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def example_rocket_markdown() -> str:
|
|
1276
|
+
return render_rocket_markdown(build_rocket_report(example_rocket_item()))
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
if __name__ == "__main__":
|
|
1280
|
+
item = example_turbofan_item()
|
|
1281
|
+
model = build_report(item)
|
|
1282
|
+
md = render_report_markdown(model)
|
|
1283
|
+
d = model["design"]
|
|
1284
|
+
print(f"ENGINE: {item.item_name}")
|
|
1285
|
+
print(f"F_NET: {d['f_net'] / 1000.0:.1f} kN airflow {d['mdot0']:.1f} kg/s")
|
|
1286
|
+
print(f"TSFC: {d['tsfc_g_kN_s']:.1f} g/(kN.s) = "
|
|
1287
|
+
f"{d['sfc_lb_lbf_hr']:.3f} lb/(lbf.h)")
|
|
1288
|
+
print(f"ETA: overall {d['eta_o']:.3f} thermal {d['eta_th']:.3f} "
|
|
1289
|
+
f"propulsive {d['eta_p']:.3f}")
|
|
1290
|
+
print(f"GATES: {check_report(model)}")
|
|
1291
|
+
print(f"RENDERED: {len(md)} chars")
|