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):
44 kappa = (1 + gamma M^2) / (1 + (gamma-1)/2 M^2)^(gamma/(gamma-1)),
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.
49 if contraction_ratio
is None or not np.isfinite(contraction_ratio)
or contraction_ratio <= 1.0:
52 M = mach_from_area_ratio(contraction_ratio, gamma,
False)
54 return float((1.0 + g * M * M) / (1.0 + 0.5 * (g - 1.0) * M * M) ** (g / (g - 1.0)))
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"])
64 return cea_cache.aux.eps_for_exit_pressure(MR, P0, Pe)
74 reaction_progress: Optional[Dict] =
None,
78 Calculate delivered engine thrust (RPA methodology).
80 F(Pa) = zeta_n * Cf_vac * P0 * At - Pa * Ae, P0 = Pc / kappa
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.
97 Total mass flow rate [kg/s]
99 CEA cache for thermochemical properties
100 config : PintleEngineConfig
101 Complete engine configuration
103 Ambient pressure [Pa] (default: sea level)
104 reaction_progress : dict, optional
105 Chamber reaction progress; accepted for the call signature, not used
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]
122 cg = config.chamber_geometry
124 raise ValueError(
"config.chamber_geometry must be provided")
126 A_throat = cg.A_throat
128 eps = cg.expansion_ratio
129 efficiency = cg.nozzle_efficiency
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}")
140 eps_calc = A_exit / A_throat
141 if not np.isclose(eps, eps_calc, rtol=1e-4):
143 f
"Geometric inconsistency: config.chamber_geometry.expansion_ratio ({eps:.6f}) "
144 f
"does not match A_exit / A_throat ({eps_calc:.6f})"
150 cea_first = cea_cache.eval(MR, Pc_val, Pa, eps)
154 cea_props = cea_cache.eval(MR, P0, Pa, eps)
155 Cf_ideal = cea_props[
"Cf_ideal"]
156 Cf_vac = cea_props[
"Cf_vac"]
157 gamma = cea_props[
"gamma"]
160 gamma_val = float(gamma)
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}")
167 raise ValueError(f
"Invalid mass flow rate: mdot_total={mdot_total} kg/s. Must be positive.")
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"])
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
183 equilibrium_factor = 1.0
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"
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
205 if not np.isfinite(F):
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"
212 Cf_actual = F / (P0 * A_throat)
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"
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))
229 Isp = F / (mdot_total * g0)
231 if not np.isfinite(Isp):
233 f
"Non-finite Isp: {Isp}. F={F:.2f} N, mdot_total={mdot_total:.4f} kg/s"
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"
244 "F_momentum": float(F_momentum),
245 "F_pressure": float(F_pressure),
246 "F_cf_method": float(F_cf),
247 "Cf": float(Cf_actual),
248 "Cf_actual": float(Cf_actual),
249 "Cf_ideal": float(Cf_ideal),
250 "Cf_theoretical": float(Cf),
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),
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),
270 if results[
"M_exit"] <= 1.0:
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}"
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)