55def handcheck(cfg: Any, r: Mapping[str, Any], PO: float, PF: float) -> Dict[str, Any]:
56 """Rows ``{quantity, model, hand, diff, ok, source, kind}`` (kind: "compare" or "bound") and the
57 flags, for result ``r`` of ``cfg`` at tank pressures PO, PF [Pa].
59 Without rocketcea (the deployed API image) the textbook rows still run, c* is checked against
60 the committed wide table, and the rows that need live CEA say so; ``note`` carries why.
64 rows: List[Dict[str, Any]] = []
67 def compare(name, model, hand, tol, source, unit=""):
68 rel = abs(model - hand) /
max(abs(hand), 1e-300)
70 rows.append({
"quantity": name,
"model": f
"{model:.5g} {unit}".strip(),
"hand": f
"{hand:.5g} {unit}".strip(),
71 "diff": f
"{rel:.2%}",
"ok": ok,
"source": source,
"kind":
"compare",
"rel": rel,
"tol": tol})
73 flags.append(f
"{name}: model {model:.5g} vs hand {hand:.5g} ({rel:.2%} > {tol:.2%})")
75 def bound(name, value, lo, hi, source, unit=""):
76 if value
is None or not math.isfinite(value):
77 rows.append({
"quantity": name,
"model":
"n/a",
"hand":
"",
"diff":
"n/a",
"ok":
True,
78 "source": source,
"kind":
"info"})
80 ok = (lo
is None or value >= lo)
and (hi
is None or value <= hi)
81 band = f
"{'' if lo is None else f'{lo:g}'}..{'' if hi is None else f'{hi:g}'}"
82 rows.append({
"quantity": name,
"model": f
"{value:.4g} {unit}".strip(),
"hand": band,
83 "diff":
"ok" if ok
else "OUT",
"ok": ok,
"source": source,
"kind":
"bound"})
85 flags.append(f
"{name} = {value:.4g} outside {band}")
87 def info(name, value, note, source):
88 rows.append({
"quantity": name,
"model": value,
"hand": note,
"diff":
"info",
"ok":
True,
89 "source": source,
"kind":
"info"})
92 cg = cfg.chamber_geometry
94 geo = cfg.injector.geometry
95 Pc, Pc_ns, MR = r[
"Pc"], d.get(
"Pc_ns", r[
"Pc"]), r[
"MR"]
96 At, eps, Pa = r[
"A_throat"], r[
"eps"], r[
"P_ambient"]
97 mdot, F = r[
"mdot_total"], r[
"F"]
101 zeta = cg.nozzle_efficiency
102 needs_cea =
"needs rocketcea"
104 cstar_cea = cea.get_Cstar(pc_psia, MR) * 0.3048
105 cstar_src =
"CEA (rocketcea)"
106 Tc_cea = cea.get_Tcomb(pc_psia, MR) / 1.8
107 _, g_ch = cea.get_Chamber_MolWt_gamma(pc_psia, MR, eps)
108 Isp_vac_cea = cea.get_Isp(pc_psia, MR, eps)
111 cstar_src =
"CEA wide c* table (committed)"
112 if cstar_cea
is not None:
113 compare(
"c* ideal", r[
"cstar_ideal"], cstar_cea, 0.005, cstar_src,
"m/s")
115 info(
"c* ideal", f
"{r['cstar_ideal']:.5g} m/s", needs_cea,
"CEA")
117 compare(
"Tc ideal", d.get(
"Tc_ideal", r[
"Tc"]), Tc_cea, 0.005,
"CEA",
"K")
118 compare(
"gamma chamber", r[
"gamma"], g_ch, 0.01,
"CEA")
120 info(
"Tc ideal", f
"{d.get('Tc_ideal', r['Tc']):.5g} K", needs_cea,
"CEA")
121 info(
"gamma chamber", f
"{r['gamma']:.5g}", needs_cea,
"CEA")
124 compare(
"mdot = Pc_ns At / c*", mdot, Pc_ns * At / r[
"cstar_actual"], 0.003,
"Sutton 3-32",
"kg/s")
125 if cstar_cea
is not None:
126 compare(
"eta c*", r[
"eta_cstar"], r[
"cstar_actual"] / cstar_cea, 0.005, f
"c*_act / c*_CEA ({cstar_src})")
130 Cf_vac_cea = Isp_vac_cea * G0 / cstar_cea
131 Cf_hand = zeta * Cf_vac_cea - Pa * eps / Pc_ns
132 compare(
"Cf (zeta_n on vacuum Cf)", r[
"Cf"], Cf_hand, 0.01, f
"Sutton 3-30, CEA Cf_vac, zeta_n={zeta}")
134 info(
"Cf (zeta_n on vacuum Cf)", f
"{r['Cf']:.5g}", needs_cea,
"Sutton 3-30, CEA Cf_vac")
135 compare(
"F = Cf Pc_ns At", F, r[
"Cf"] * Pc_ns * At, 0.002,
"Sutton 3-31",
"N")
136 compare(
"Isp = F / (mdot g0)", r[
"Isp"], F / (mdot * G0), 0.002,
"definition",
"s")
139 pe_cea = Pc / cea.get_PcOvPe(pc_psia, MR, eps)
140 compare(
"exit pressure", r[
"P_exit"], pe_cea, 0.02,
"CEA Pc/Pe at eps",
"Pa")
142 info(
"exit pressure", f
"{r['P_exit']:.5g} Pa", needs_cea,
"CEA Pc/Pe at eps")
143 bound(
"pe / pa (Summerfield > 0.4)", r[
"P_exit"] / Pa, 0.4,
None,
"Sutton 3.3 flow separation")
146 for s, key, rho
in ((
"O",
"oxidizer", fl[
"oxidizer"].density), (
"F",
"fuel", fl[
"fuel"].density)):
147 el = getattr(geo, key)
148 A = el.n_elements * math.pi * el.d_jet ** 2 / 4.0
149 dp = d[f
"delta_p_injector_{s}"]
150 m_hand = d[f
"Cd_{s}"] * A * math.sqrt(2.0 * rho * dp)
151 if d.get(
"manifold_model") ==
"ring_network":
154 compare(f
"mdot_{s} = Cd A sqrt(2 rho dP) (port head)", r[f
"mdot_{s}"], m_hand, 0.02,
155 "Sutton 8-1; channel loss in the ring-manifold solve",
"kg/s")
157 compare(f
"mdot_{s} = Cd A sqrt(2 rho dP)", r[f
"mdot_{s}"], m_hand, 1e-6,
"Sutton 8-1",
"kg/s")
158 compare(f
"P_inj_{s} = P_tank - dP_feed", d[f
"P_injector_{s}"], (PO
if s ==
"O" else PF) - d[f
"delta_p_feed_{s}"], 1e-9,
"definition",
"Pa")
159 bound(f
"dP_inj_{s} / Pc", dp / Pc, 0.15,
None,
"Huzel & Huang 4.2; Sutton 8.1 (chug margin)")
160 v = r[f
"mdot_{s}"] / (rho * A)
161 bound(f
"jet velocity {s}", v, 10.0, 60.0,
"bulk mdot/(rho A); typical doublet range",
"m/s")
164 rO, rF = fl[
"oxidizer"].density, fl[
"fuel"].density
165 vO = r[
"mdot_O"] / (rO * geo.oxidizer.n_elements * math.pi * geo.oxidizer.d_jet ** 2 / 4.0)
166 vF = r[
"mdot_F"] / (rF * geo.fuel.n_elements * math.pi * geo.fuel.d_jet ** 2 / 4.0)
167 M = (rO * vO ** 2 * geo.oxidizer.d_jet) / (rF * vF ** 2 * geo.fuel.d_jet)
168 compare(
"Rupe M", d[
"rupe_M"], M, 1e-6,
"rho_O v_O^2 d_O / (rho_F v_F^2 d_F)")
169 bound(
"Rupe M (best mixing near 1)", M, 0.8, 1.25,
"Rupe 1953; Elverum & Morey 1959")
170 thO, thF = math.radians(geo.oxidizer.impingement_angle), math.radians(geo.fuel.impingement_angle)
171 pO, pF = r[
"mdot_O"] * vO, r[
"mdot_F"] * vF
172 beta = math.degrees(math.atan2(pO * math.sin(thO) - pF * math.sin(thF), pO * math.cos(thO) + pF * math.cos(thF)))
174 info(
"resultant spray angle", f
"{beta:.2f} deg",
"see design_audit",
"Sutton eq 8-7 (momentum resultant)")
175 bound(
"included angle", geo.oxidizer.impingement_angle + geo.fuel.impingement_angle, 60.0, 100.0,
176 "Sutton 8.3 / Huzel 4.2 practice",
"deg")
178 bound(
"free-jet length / d_O", d[
"L_imp"] / geo.oxidizer.d_jet, 2.0, 7.0,
"jet coherence before impact")
181 ch = r[
"chamber_intrinsics"]
182 rho_c = Pc / (r[
"R"] * r[
"Tc"])
183 compare(
"stay time = L* At rho_c / mdot", ch[
"residence_time"], ch[
"Lstar"] * At * rho_c / mdot, 0.02,
"Sutton 8-9",
"s")
184 Dt = math.sqrt(4 * At / math.pi)
185 CR = (cg.chamber_diameter / Dt) ** 2
186 bound(
"contraction ratio", CR, 3.0, 15.0,
"Huzel & Huang 4.3 practice")
187 bound(
"chamber Mach", ch[
"mach_number_chamber"],
None, 0.3,
"Rayleigh loss small below ~0.3")
189 out = {
"rows": rows,
"flags": flags,
190 "summary": {
"Pc_psia": Pc / PSI,
"OF": MR,
"F": F,
"Isp": r[
"Isp"],
"Pa": Pa}}
192 out[
"note"] = NO_ROCKETCEA