STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
nozzle.py
Go to the documentation of this file.
1"""Nozzle model: RPA delivered thrust + isentropic exit state.
2
3This is the AUTHORITATIVE nozzle: every runner.evaluate() call uses it (flight sim,
4time series, pintle, and Layer-1 finalization). Delivered thrust is the RPA basis
5
6 F(Pa) = zeta_n * Cf_vac * P0 * At - Pa * Ae
7
8where Cf_vac is CEA's shifting-equilibrium vacuum thrust coefficient from the cache
9tables, zeta_n = nozzle_efficiency, and combustion efficiency zeta_c (eta_c*) rides
10in through the efficiency-reduced Pc from the chamber solve — so delivered
11Isp = zeta_c * zeta_n * Isp_ideal. See docs/thrust_efficiency_bug_analysis.md for
12why the former momentum-method reconstruction (ideal-Tc exhaust velocity) was
13retired: it never applied combustion efficiency and over-predicted thrust by
14~(1 - zeta_c*zeta_n). The exit state reported here (P_exit, T_exit, M_exit) is CEA's
15shifting-equilibrium expansion at the same (P0, O/F, eps); thrust does not depend on it.
16Pc is the injector-end pressure; the nozzle expands from P0 = Pc/kappa, the Rayleigh
17stagnation loss of a finite-area combustor.
18
19The numba accelerator (engine/accel/kernels.py) computes the SAME delivered
20formula from the same tables for the Layer-1 inner loop; parity is enforced live by
21tests/test_native_ab_parity.py.
22"""
23
24import numpy as np
25import logging
26from typing import Dict, Optional, Any
27from engine.pipeline.cea_cache import CEACache
28from engine.pipeline.numerical_robustness import PhysicalConstraints
29
30
31def contraction_ratio_of(cg) -> Optional[float]:
32 """A_chamber/A_throat of a ChamberGeometryConfig, or None when the bore is not declared."""
33 D = getattr(cg, "chamber_diameter", None)
34 At = getattr(cg, "A_throat", None)
35 if not D or not At or D <= 0 or At <= 0:
36 return None
37 return float(np.pi * 0.25 * D * D / At)
38
39
40def nozzle_stagnation_loss(contraction_ratio: Optional[float], gamma: float) -> float:
41 """kappa = P_injector_end / P0_nozzle of a finite-area combustor (constant-area heat
42 addition, Rayleigh line; Huzel & Huang sec. 2, Sutton & Biblarz Table 3-2):
43
44 kappa = (1 + gamma M^2) / (1 + (gamma-1)/2 M^2)^(gamma/(gamma-1)),
45
46 M the chamber-end Mach number, A/A* = contraction ratio on the subsonic branch.
47 Reproduces CEA's finite-area "Pinj/Pinf" to 3e-5. 1.0 when the bore is unknown.
48 """
49 if contraction_ratio is None or not np.isfinite(contraction_ratio) or contraction_ratio <= 1.0:
50 return 1.0
51 from engine.pipeline.thermal.gas_side import mach_from_area_ratio
52 M = mach_from_area_ratio(contraction_ratio, gamma, False)
53 g = gamma
54 return float((1.0 + g * M * M) / (1.0 + 0.5 * (g - 1.0) * M * M) ** (g / (g - 1.0)))
55
56
57def expansion_ratio_for_exit_pressure(cea_cache: CEACache, MR: float, Pc: float, Pe: float,
58 contraction_ratio: Optional[float] = None) -> float:
59 """Area ratio whose CEA shifting-equilibrium exit pressure is Pe (optimum expansion is
60 Pe = Pa, Sutton & Biblarz ch. 3). ``Pc`` is the injector-end pressure; with a declared
61 contraction ratio the nozzle expands from Pc/kappa."""
62 gamma = float(cea_cache.eval(MR, Pc, Pe)["gamma"])
63 P0 = float(Pc) / nozzle_stagnation_loss(contraction_ratio, gamma)
64 return cea_cache.aux.eps_for_exit_pressure(MR, P0, Pe)
65
66
68 Pc: float,
69 MR: float,
70 mdot_total: float,
71 cea_cache: CEACache,
72 config: Any,
73 Pa: float = 101325.0,
74 reaction_progress: Optional[Dict] = None,
75 debug: bool = False,
76) -> dict:
77 """
78 Calculate delivered engine thrust (RPA methodology).
79
80 F(Pa) = zeta_n * Cf_vac * P0 * At - Pa * Ae, P0 = Pc / kappa
81
82 Cf_vac is CEA's shifting-equilibrium vacuum thrust coefficient (cache lookup);
83 zeta_n is config.chamber_geometry.nozzle_efficiency; combustion efficiency
84 zeta_c (eta_c*) is already carried by the efficiency-reduced Pc from the
85 chamber solve, so delivered Isp = zeta_c * zeta_n * Isp_ideal. The exit state
86 also returned here (P_exit, T_exit, M_exit: CEA shifting equilibrium) is
87 reporting-only; thrust does not depend on it. See the module docstring and
88 docs/thrust_efficiency_bug_analysis.md.
89
90 Parameters:
91 -----------
92 Pc : float
93 Chamber pressure [Pa]
94 MR : float
95 Mixture ratio (O/F)
96 mdot_total : float
97 Total mass flow rate [kg/s]
98 cea_cache : CEACache
99 CEA cache for thermochemical properties
100 config : PintleEngineConfig
101 Complete engine configuration
102 Pa : float
103 Ambient pressure [Pa] (default: sea level)
104 reaction_progress : dict, optional
105 Chamber reaction progress; accepted for the call signature, not used
106
107 Returns:
108 --------
109 results : dict
110 Dictionary containing:
111 - F: Delivered thrust [N]
112 - F_momentum: vacuum term zeta_n*Cf_vac*P0*At [N]
113 - F_pressure: ambient back-pressure term -Pa*Ae [N]
114 - Cf / Cf_actual: delivered thrust coefficient F/(P0*At)
115 - Cf_ideal: CEA ambient thrust coefficient at this Pa
116 - P_exit, T_exit, M_exit: CEA shifting-equilibrium exit state; v_exit the ideal
117 exhaust velocity on the same basis (reporting-only)
118 - Pc_ns, stagnation_loss_kappa: nozzle stagnation pressure and Pc/Pc_ns
119 - Isp: Delivered specific impulse [s]
120 """
121 # Extract geometry from config.chamber_geometry
122 cg = config.chamber_geometry
123 if cg is None:
124 raise ValueError("config.chamber_geometry must be provided")
125
126 A_throat = cg.A_throat
127 A_exit = cg.A_exit
128 eps = cg.expansion_ratio
129 efficiency = cg.nozzle_efficiency
130
131 # Validate geometry inputs
132 if A_throat is None or A_throat <= 0:
133 raise ValueError(f"Invalid A_throat: {A_throat}")
134 if A_exit is None or A_exit <= 0:
135 raise ValueError(f"Invalid A_exit: {A_exit}")
136 if eps is None or eps <= 1.0:
137 raise ValueError(f"Invalid expansion ratio: eps={eps}")
138
139 # Verify geometric consistency: eps = A_exit / A_throat
140 eps_calc = A_exit / A_throat
141 if not np.isclose(eps, eps_calc, rtol=1e-4):
142 raise ValueError(
143 f"Geometric inconsistency: config.chamber_geometry.expansion_ratio ({eps:.6f}) "
144 f"does not match A_exit / A_throat ({eps_calc:.6f})"
145 )
146
147 # Finite-area combustor: the nozzle's stagnation pressure is the injector-end Pc less
148 # the Rayleigh loss of heat addition at the chamber Mach number.
149 Pc_val = float(Pc)
150 cea_first = cea_cache.eval(MR, Pc_val, Pa, eps)
151 kappa = nozzle_stagnation_loss(contraction_ratio_of(cg), float(cea_first["gamma"]))
152 P0 = Pc_val / kappa
153
154 cea_props = cea_cache.eval(MR, P0, Pa, eps)
155 Cf_ideal = cea_props["Cf_ideal"] # ambient coefficient at this Pa: Cf_vac - Pa*eps/P0
156 Cf_vac = cea_props["Cf_vac"] # vacuum thrust coefficient (RPA delivered-thrust basis)
157 gamma = cea_props["gamma"]
158 Tc = cea_props["Tc"]
159 R = cea_props["R"]
160 gamma_val = float(gamma)
161 eps_val = float(eps)
162
163 gamma_check = PhysicalConstraints.validate_gamma(gamma_val)
164 if not gamma_check.passed and gamma_check.severity == "error":
165 raise ValueError(f"Invalid gamma: {gamma_check.message}")
166 if mdot_total <= 0:
167 raise ValueError(f"Invalid mass flow rate: mdot_total={mdot_total} kg/s. Must be positive.")
168
169 # Exit state on CEA's shifting-equilibrium expansion -- the thermochemistry the thrust
170 # uses -- not an isentrope at the chamber's gamma (which lands above both CEA bounds).
171 exit_state = cea_cache.aux.exit_state(MR, P0, eps_val)
172 P_exit = float(exit_state["P_exit"])
173 T_exit = float(exit_state["T_exit"])
174 M_exit = float(exit_state["M_exit"])
175 # Ideal exhaust velocity on the same basis: F_vac = mdot v_e + Pe Ae with mdot = P0 At / c*.
176 v_exit = float(cea_props["cstar_ideal"]) * (float(Cf_vac) - eps_val * P_exit / P0)
177 if not (np.isfinite(P_exit) and P_exit > 0 and np.isfinite(T_exit) and T_exit > 0
178 and M_exit > 1.0 and v_exit > 0):
179 raise ValueError(f"Invalid CEA exit state at MR={MR:.4f}, P0={P0:.4g} Pa, eps={eps_val:.4f}: "
180 f"Pe={P_exit}, Te={T_exit}, Me={M_exit}, ve={v_exit}")
181 gamma_exit = gamma_val
182 R_exit = R
183 equilibrium_factor = 1.0
184
185 if debug:
186 logging.getLogger("evaluate").info(
187 f"[NOZZLE][CEA] Pc={Pc_val:.3e} Pa, P0_ns={P0:.3e} Pa (kappa={kappa:.5f}), Pa={Pa:.3e} Pa, "
188 f"MR={MR:.4f}, eps={eps_val:.3f} | Cf_vac={Cf_vac:.4f}, Pe={P_exit:.4g} Pa, Te={T_exit:.1f} K"
189 )
190
191 # ------------------------------------------------------------------
192 # Delivered thrust -- RPA methodology (see docs/thrust_efficiency_bug_analysis.md):
193 # F(Pa) = zeta_n * Cf_vac * P0 * At - Pa * Ae
194 # P0 is the nozzle stagnation pressure. Combustion efficiency zeta_c (= eta_c*) is
195 # already carried by Pc (the chamber solve reduced it), so delivered Isp =
196 # zeta_c * zeta_n * Isp_vac_ideal minus the ambient term.
197 # ------------------------------------------------------------------
198 zeta_n = efficiency
199 Cf_vac_delivered = zeta_n * Cf_vac
200 F_momentum = Cf_vac_delivered * P0 * A_throat
201 F_pressure = -Pa * A_exit
202 F = F_momentum + F_pressure
203 F_cf = F
204
205 if not np.isfinite(F):
206 raise ValueError(
207 f"Non-finite thrust: Cf_vac={Cf_vac}, zeta_n={zeta_n}, P0={P0:.3e} Pa, "
208 f"At={A_throat:.3e} m^2, Pa={Pa:.3e} Pa, Ae={A_exit:.3e} m^2"
209 )
210
211 # Delivered thrust coefficient, referenced to the nozzle stagnation pressure.
212 Cf_actual = F / (P0 * A_throat)
213 Cf = Cf_actual
214
215 if debug:
216 logging.getLogger("evaluate").info(
217 f"[NOZZLE RPA] Cf_vac={Cf_vac:.4f} zeta_n={zeta_n:.3f} -> Cf_vac_del={Cf_vac_delivered:.4f} | "
218 f"F_vac={F_momentum:.1f} N, ambient(-Pa*Ae)={F_pressure:.1f} N, F={F:.1f} N | "
219 f"Cf_actual={Cf_actual:.3f}, Isp={F/(mdot_total*9.80665):.1f} s"
220 )
221
222 # Throat state at the chamber gamma (reporting).
223 throat_temp_ratio = 2.0 / (gamma_val + 1.0)
224 T_throat = Tc * throat_temp_ratio
225 P_throat = P0 * throat_temp_ratio ** (gamma_val / (gamma_val - 1.0))
226
227 # Calculate Isp
228 g0 = 9.80665 # m/s²
229 Isp = F / (mdot_total * g0)
230
231 if not np.isfinite(Isp):
232 raise ValueError(
233 f"Non-finite Isp: {Isp}. F={F:.2f} N, mdot_total={mdot_total:.4f} kg/s"
234 )
235
236 if Isp < 0:
237 raise ValueError(
238 f"Negative Isp: {Isp:.2f} s. This indicates negative thrust or negative mass flow. "
239 f"F={F:.2f} N, mdot_total={mdot_total:.4f} kg/s"
240 )
241
242 results = {
243 "F": float(F),
244 "F_momentum": float(F_momentum),
245 "F_pressure": float(F_pressure),
246 "F_cf_method": float(F_cf), # For comparison
247 "Cf": float(Cf_actual), # Return actual Cf (measured from thrust)
248 "Cf_actual": float(Cf_actual), # Explicit actual value
249 "Cf_ideal": float(Cf_ideal), # CEA ambient coefficient at this Pa
250 "Cf_theoretical": float(Cf), # zeta_n*Cf_vac - Pa*eps/P0, the delivered basis
251 "Pc_ns": float(P0), # nozzle stagnation pressure
252 "stagnation_loss_kappa": float(kappa),
253 "P_exit": float(P_exit),
254 "exit_to_ambient_ratio": float(P_exit / Pa) if Pa > 0 else float("inf"),
255 "exit_state_basis": "CEA shifting equilibrium",
256 "P_throat": float(P_throat),
257 "v_exit": float(v_exit),
258 "T_exit": float(T_exit),
259 "T_throat": float(T_throat),
260 "Isp": float(Isp),
261 "gamma_chamber": float(gamma_val),
262 "gamma_exit": float(gamma_exit),
263 "R_chamber": float(R),
264 "R_exit": float(R_exit),
265 "equilibrium_factor": float(equilibrium_factor),
266 "M_exit": float(M_exit),
267 }
268
269 # Final validation of M_exit before returning
270 if results["M_exit"] <= 1.0:
271 raise ValueError(
272 f"Invalid M_exit in results: {results['M_exit']:.6f}. "
273 f"For supersonic nozzle, M_exit must be > 1.0. "
274 f"eps={eps_val:.4f}, gamma_exit={gamma_exit:.4f}"
275 )
276
277 return results
static board_sleep_info_t info
Definition board_sleep.c:14
float expansion_ratio_for_exit_pressure(CEACache cea_cache, float MR, float Pc, float Pe, Optional[float] contraction_ratio=None)
Definition nozzle.py:58
float nozzle_stagnation_loss(Optional[float] contraction_ratio, float gamma)
Definition nozzle.py:40
dict calculate_thrust(float Pc, float MR, float mdot_total, CEACache cea_cache, Any config, float Pa=101325.0, Optional[Dict] reaction_progress=None, bool debug=False)
Definition nozzle.py:76
Optional[float] contraction_ratio_of(cg)
Definition nozzle.py:31