43 def node(nid, label, kind, side, phase, p, T_K):
44 nodes[nid] = {
"label": label,
"kind": kind,
"side": side,
"phase": phase,
"p_psia": p,
"T_K": T_K}
46 def branch(bid, label, kind, frm, to, side, mdot, cv=None):
47 branches[bid] = {
"label": label,
"kind": kind,
"from": frm,
"to": to,
"side": side,
48 "mdot": mdot,
"dp_psi": [a - b
for a, b
in zip(nodes[frm][
"p_psia"], nodes[to][
"p_psia"])],
49 "cv": cv,
"state":
None}
51 bottle =
_lin(4000.0, 3000.0)
52 node(
"KB1",
"COPV",
"bottle",
"gas",
"gas", bottle, [293.15] * N)
53 node(
"MF1",
"MAN-GN2",
"manifold",
"gas",
"gas", [p - 1.0
for p
in bottle], [293.0] * N)
54 node(
"PR_D.in",
"PR-DOME.in",
"junction",
"gas",
"gas", [p - 5.0
for p
in bottle], [292.9] * N)
55 reg_out = [600.0, 600.0, 598.0, 601.0]
56 node(
"PR_D.out",
"PR-DOME.out",
"junction",
"gas",
"gas", reg_out, [300.0] * N)
57 node(
"MF2",
"MAN-REG",
"manifold",
"gas",
"gas", [p - 1.0
for p
in reg_out], [300.0] * N)
58 gas = [0.0, 0.0, 0.02, 0.03]
59 branch(
"l_kb",
"l_kb",
"line",
"KB1",
"MF1",
"gas", gas)
60 branch(
"l_reg",
"l_reg",
"line",
"MF1",
"PR_D.in",
"gas", gas)
61 branch(
"PR_D",
"PR-DOME",
"regulator",
"PR_D.in",
"PR_D.out",
"gas", gas, cv=0.8)
62 branch(
"l_reg_out",
"l_reg_out",
"line",
"PR_D.out",
"MF2",
"gas", gas)
63 for side, tank, pre, mv, eng, T_liq, head
in ((
"ox",
"OXT",
"SV_LOX_PRESS",
"MVO",
"ENG.oxidiser", 90.0, 0.5),
64 (
"fuel",
"FUT",
"SV_FUEL_PRESS",
"MVF",
"ENG.fuel", 293.15, 0.3)):
65 mf2 = nodes[
"MF2"][
"p_psia"]
66 sv_in = [p - 0.3
for p
in mf2]
67 sv_drop = [0.0, 0.0, 1.5, 2.0]
if side ==
"ox" else [0.0, 0.0, 1.2, 1.7]
68 sv_out = [a - b
for a, b
in zip(sv_in, sv_drop)]
69 ull = [p - 0.3
for p
in sv_out]
70 node(f
"{pre}.in", f
"{pre}.in",
"junction", side,
"gas", sv_in, [300.0] * N)
71 node(f
"{pre}.out", f
"{pre}.out",
"junction", side,
"gas", sv_out, [300.0] * N)
72 node(tank, f
"TK-{side}",
"tank", side,
"gas", ull, [293.0] * N)
73 node(f
"{tank}.out", f
"TK-{side}.out",
"tank_outlet", side,
"liquid", [p + head
for p
in ull], [T_liq] * N)
74 mdot = [0.0, 0.0, 1.8, 1.9]
if side ==
"ox" else [0.0, 0.0, 1.2, 1.25]
75 line1 = [0.0, 0.0, 15.0, 16.0]
if side ==
"ox" else [0.0, 0.0, 31.0, 33.0]
76 mv_in = [a - b
for a, b
in zip(nodes[f
"{tank}.out"][
"p_psia"], line1)]
77 mv_out = [p - (564.0
if not f
else 1.1)
for p, f
in zip(mv_in, FIRING)]
78 node(f
"{mv}.in", f
"{mv}.in",
"junction", side,
"liquid", mv_in, [T_liq] * N)
79 node(f
"{mv}.out", f
"{mv}.out",
"junction", side,
"liquid", mv_out, [T_liq] * N)
80 exit_p = [p - (0.0
if not f
else 2.2)
for p, f
in zip(mv_out, FIRING)]
81 node(eng, eng,
"junction", side,
"liquid", exit_p, [T_liq] * N)
82 pv = 0.0
if side ==
"ox" else 0.0
83 gas_side = [g * (0.55
if side ==
"ox" else 0.45)
for g
in gas]
84 branch(f
"l_{side}press_in", f
"l_{side}press_in",
"line",
"MF2", f
"{pre}.in", side, gas_side)
85 branch(pre, pre,
"solenoid", f
"{pre}.in", f
"{pre}.out", side, gas_side, cv=1.7)
86 branch(f
"l_{side}press", f
"l_{side}press",
"line", f
"{pre}.out", tank, side, gas_side)
87 branch(f
"l_{side}1", f
"l_{side}1",
"line", f
"{tank}.out", f
"{mv}.in", side, mdot)
88 branch(mv, mv,
"valve", f
"{mv}.in", f
"{mv}.out", side, mdot, cv=26.1)
89 branch(f
"l_{side}2", f
"l_{side}2",
"line", f
"{mv}.out", eng, side, mdot)
91 node(
"ENG.chamber",
"ENG.chamber",
"chamber",
"ox",
"gas", [14.7, 14.7, 400.0, 401.0], [85.0] * N)
92 branch(
"ENG.oxidiser.injector",
"ENG.oxidiser.injector",
"injector",
"ENG.oxidiser",
"ENG.chamber",
"ox",
93 branches[
"l_ox2"][
"mdot"])
94 branch(
"ENG.fuel.injector",
"ENG.fuel.injector",
"injector",
"ENG.fuel",
"ENG.chamber",
"fuel",
95 branches[
"l_fuel2"][
"mdot"])
96 paths = {side: [
"l_kb",
"l_reg",
"PR_D",
"l_reg_out", f
"l_{side}press_in", pre, f
"l_{side}press",
97 f
"l_{side}1", mv, f
"l_{side}2", f
"{eng}.injector"]
98 for side, pre, mv, eng
in ((
"ox",
"SV_LOX_PRESS",
"MVO",
"ENG.oxidiser"),
99 (
"fuel",
"SV_FUEL_PRESS",
"MVF",
"ENG.fuel"))}
100 network = {
"t": list(T),
"nodes": nodes,
"branches": branches,
"paths": paths}
102 def side_series(side, eng, tank):
103 inlet = nodes[eng][
"p_psia"]
104 pc = nodes[
"ENG.chamber"][
"p_psia"]
105 dump = [0.0, 0.0, 25.0, 26.0]
if side ==
"ox" else [0.0, 0.0, 15.5, 16.0]
106 manifold = [a - b
for a, b
in zip(inlet, dump)]
107 dpi = [
max(m - c, 0.0)
if f
else 0.0
for m, c, f
in zip(manifold, pc, FIRING)]
109 dump = [d
if f
else 0.0
for d, f
in zip(dump, FIRING)]
110 dpi = [x
if f
else a - c
for x, a, c, f
in zip(dpi, inlet, pc, FIRING)]
111 return {
"tank_psia": nodes[tank][
"p_psia"],
"outlet_psia": nodes[f
"{tank}.out"][
"p_psia"],
112 "inlet_psia": inlet,
"dump_psi": dump,
"manifold_psia": manifold,
113 "dp_injector_psi": dpi,
"mdot": branches[f
"l_{side}1"][
"mdot"],
114 "liquid_K": [90.0
if side ==
"ox" else 293.15] * N}
116 series = {
"t": list(T),
"firing": list(FIRING),
117 "ox": side_series(
"ox",
"ENG.oxidiser",
"OXT"),
118 "fuel": side_series(
"fuel",
"ENG.fuel",
"FUT"),
119 "chamber": {
"pc_psia": nodes[
"ENG.chamber"][
"p_psia"]}}
120 return {
"series": series,
"network": network}
127 lad = L.build_ladder(res)
128 assert "error" not in lad, lad
129 for side, head
in ((
"ox", 0.5), (
"fuel", 0.3)):
131 kinds = [e[
"kind"]
for e
in s[
"elements"]]
133 assert kinds == [
"line",
"line",
"regulator",
"line",
"line",
"solenoid",
"line",
"tank_head",
134 "line",
"valve",
"line",
"dump",
"orifice"]
135 assert s[
"start"] ==
"KB1" and s[
"end"] ==
"ENG.chamber"
137 hand = [b - c
for b, c
in zip(res[
"network"][
"nodes"][
"KB1"][
"p_psia"],
138 res[
"network"][
"nodes"][
"ENG.chamber"][
"p_psia"])]
139 assert s[
"total_psi"] == pytest.approx(hand, abs=1e-9)
140 assert s[
"sum_psi"] == pytest.approx(hand, abs=1e-9)
141 assert s[
"closes"]
and s[
"max_abs_residual_psi"] < 1e-9
142 tank_head = s[
"elements"][7]
143 assert tank_head[
"dp_psi"] == pytest.approx([-head] * N, abs=1e-12)
146 assert sum(e[
"share"][k]
for e
in s[
"elements"]) == pytest.approx(1.0, abs=1e-12)
148 ox = {e[
"id"]: e
for e
in lad[
"ox"][
"elements"]}
149 assert ox[
"ENG.oxidiser.injector:dump"][
"dp_psi"][2] == pytest.approx(25.0)
150 inlet = res[
"series"][
"ox"][
"inlet_psia"][2]
151 assert ox[
"ENG.oxidiser.injector:orifice"][
"dp_psi"][2] == pytest.approx(inlet - 25.0 - 400.0)
152 assert ox[
"ENG.oxidiser.injector:orifice"][
"check_psi"] == pytest.approx(0.0, abs=1e-9)
154 assert ox[
"SV_LOX_PRESS"][
"dp_psi"] == pytest.approx([0.0, 0.0, 1.5, 2.0])
192 from fluids.control_valve
import size_control_valve_g
193 p1, rho, gamma, Z, mu =
_he(3000.0, 293.15)
195 got = R.iec_gas_flow(p1, p2, rho, gamma, 0.8, 0.7)
197 f_gamma = gamma / 1.40
199 x_eff =
min(x, f_gamma * 0.7)
200 Y = 1 - x_eff / (3 * f_gamma * 0.7)
201 hand = 3.16 * (0.8 / 1.156099) * Y * math.sqrt(x_eff * p1 / 1e3 * rho) / 3600.0
202 assert got[
"choked"]
is choked
203 assert got[
"mdot"] == pytest.approx(hand, rel=2e-5)
206 q_std = got[
"mdot"] / (101325.0 * 4.002602e-3 / (8.314462618 * 273.15))
207 kv = size_control_valve_g(T=293.15, MW=4.002602, mu=mu, gamma=gamma, Z=Z, P1=p1, P2=p2, Q=q_std,
209 assert kv * 1.156099 == pytest.approx(0.8, rel=3e-3)
215 p1, rho, gamma, Z, mu =
_he(581.6, 298.0)
216 dp = R.iec_gas_drop(0.01121, p1, rho, gamma, 1.7, 0.7)
217 assert dp / PSI == pytest.approx(1.724, abs=0.004)
219 assert R.iec_gas_flow(p1, p1 - dp, rho, gamma, 1.7, 0.7)[
"mdot"] == pytest.approx(0.01121, rel=1e-6)
222 dp_inc = ((0.01121 * 3600.0) / (3.16 * kv)) ** 2 / rho * 1e3
223 assert dp_inc / PSI == pytest.approx(1.720, abs=0.004)
226 dp2 = R.iec_gas_drop(0.01121, p1, rho, gamma, 3.4, 0.7)
227 assert dp2 / dp == pytest.approx(0.25, rel=0.01)
229 assert math.isnan(R.iec_gas_drop(10.0, p1, rho, gamma, 1.7, 0.7))
254 p_in = net[
"nodes"][
"PR_D.in"][
"p_psia"]
255 mdot = net[
"branches"][
"PR_D"][
"mdot"]
256 out_p = [
_reg_target(p_in[k], mdot[k])
for k
in range(N)]
258 net[
"nodes"][
"PR_D.out"][
"p_psia"] = out_p
259 reg = R.build_regulator(res,
None, params=REG_PARAMS, gas=
"helium")
260 assert "error" not in reg, reg
262 assert reg[
"spe_psi"][2] == pytest.approx(0.017 * (ATM_PSIA - p_in[2]))
263 assert reg[
"droop_psi"][2] == pytest.approx(8.3 * 0.02 / 0.09646)
264 assert reg[
"target_psia"][2] == pytest.approx(out_p[2])
265 assert reg[
"residual_psi"][:3] == pytest.approx([0.0, 0.0, 0.0], abs=1e-9)
266 assert reg[
"residual_psi"][3] == pytest.approx(-11.0)
267 assert reg[
"wide_open"] == [
False,
False,
False,
True]
268 assert reg[
"dome_psia"] == pytest.approx([528.25] * N)
270 p1, rho, gamma, _, _ =
_he(p_in[2], 292.9)
271 cap = R.iec_gas_flow(p1, out_p[2] * PSI, rho, gamma, 0.8, 0.7)[
"mdot"]
272 assert reg[
"capacity_mdot"][2] == pytest.approx(cap, rel=1e-9)
273 assert reg[
"use_frac"][2] == pytest.approx(0.02 / cap, rel=1e-9)
274 assert reg[
"choked"][2]
is True
277 assert rise[
"outlet_rise_psi"] == pytest.approx(rise[
"spe_psi"] + rise[
"droop_psi"] + rise[
"residual_psi"])
278 assert rise[
"residual_psi"] == pytest.approx(-11.0)
280 assert reg[
"model"][
"inputs"][
"supply_coefficient"][
"provenance"].startswith(
"manufacturer")
281 assert reg[
"model"][
"inputs"][
"xT"][
"provenance"].startswith(
"assumed")
286 sol = R.build_solenoids(res,
None, gas=
"helium")
287 assert isinstance(sol, list)
and len(sol) == 2, sol
288 ox = next(s
for s
in sol
if s[
"side"] ==
"ox")
289 assert ox[
"id"] ==
"SV_LOX_PRESS" and ox[
"cv"] == 1.7
291 assert ox[
"reg_to_tank_psi"][2] == pytest.approx(3.1)
292 assert ox[
"share_of_reg_to_tank"][2] == pytest.approx(1.5 / 3.1)
293 assert ox[
"dp_iec_psi"][0] == 0.0
294 p1, rho, gamma, _, _ =
_he(596.7, 300.0)
295 assert ox[
"dp_iec_psi"][2] == pytest.approx(R.iec_gas_drop(0.011, p1, rho, gamma, 1.7) / PSI, rel=1e-9)
304 """dT along the isenthalp by integrating CoolProp's own Joule-Thomson coefficient (dT/dp)_h,
305 RK4 in pressure: a different path through the equation of state than the (p, h) flash."""
306 import CoolProp.CoolProp
as CP
309 return CP.PropsSI(
"d(T)/d(P)|Hmass",
"P", p,
"T", T, fluid)
312 h = (p2 - p1) / steps
313 for _
in range(steps):
315 k2 = mu(p + h / 2, T + h / 2 * k1)
316 k3 = mu(p + h / 2, T + h / 2 * k2)
317 k4 = mu(p + h, T + h * k3)
318 T += h / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
323@pytest.mark.parametrize("fluid, sign", [("helium", +1), (
"nitrogen", -1)])
325 p1, p2 = 3000.0 * PSI, 600.0 * PSI
326 dT = P.jt_temperature_change(fluid, p1, 293.15, p2)
328 assert sign * dT > 1.0
329 name =
"Helium" if fluid ==
"helium" else "Nitrogen"
332 import CoolProp.CoolProp
as CP
333 mu = CP.PropsSI(
"d(T)/d(P)|Hmass",
"P", p1,
"T", 293.15, name)
334 small = P.jt_temperature_change(fluid, p1, 293.15, p1 - 1e4)
335 assert small == pytest.approx(-mu * 1e4, rel=1e-3)
339 import CoolProp.CoolProp
as CP
342 T_true = [293.15, 293.15, 288.0, 283.0]
343 p_b = res[
"network"][
"nodes"][
"KB1"][
"p_psia"]
344 mass = [CP.PropsSI(
"D",
"P", p * PSI,
"T", T,
"Helium") * V
for p, T
in zip(p_b, T_true)]
345 res[
"series"][
"copv_mass_kg"] = mass
346 res[
"series"][
"copv_psia"] = p_b
347 out_end = res[
"network"][
"nodes"][
"PR_D.out"][
"p_psia"][3]
348 pr = P.build_pressurant(res,
None, gas=
"helium", volume_m3=V, regulator={
"id":
"PR_D",
"cv": 0.8})
349 assert "error" not in pr, pr
350 assert pr[
"bottle_T_K"] == pytest.approx(T_true, abs=1e-6)
351 assert pr[
"loaded_kg"] == pytest.approx(mass[0])
352 assert pr[
"residual_kg"] == pytest.approx(mass[3])
353 assert pr[
"used_kg"] == pytest.approx(mass[0] - mass[3])
355 assert pr[
"floor_converged"]
356 p_b_end, T_b_end = p_b[3] * PSI, T_true[3]
357 f_in, T_in = pr[
"floor_regulator_inlet_psia"] * PSI, pr[
"floor_regulator_inlet_T_K"]
358 f_b, T_b = pr[
"floor_psia"] * PSI, pr[
"floor_bottle_T_K"]
360 rho, gamma = R.gas_state(
"helium", f_in, T_in)
361 assert R.iec_gas_flow(f_in, out_end * PSI, rho, gamma, 0.8, 0.7)[
"mdot"] == pytest.approx(0.03, rel=1e-6)
363 s_end = CP.PropsSI(
"SMASS",
"P", p_b_end,
"T", T_b_end,
"Helium")
364 assert CP.PropsSI(
"SMASS",
"P", f_b,
"T", T_b,
"Helium") == pytest.approx(s_end, rel=1e-6)
366 assert T_b == pytest.approx(T_b_end * (f_b / p_b_end) ** 0.4, rel=0.02)
367 assert T_b < T_b_end - 30.0
369 h_b = CP.PropsSI(
"HMASS",
"P", f_b,
"T", T_b,
"Helium")
370 assert CP.PropsSI(
"HMASS",
"P", f_in,
"T", T_in,
"Helium") == pytest.approx(h_b, rel=1e-6)
372 assert pr[
"line_drop_end_psi"] == pytest.approx(5.0)
373 rho_in_end = CP.PropsSI(
"D",
"P", res[
"network"][
"nodes"][
"PR_D.in"][
"p_psia"][3] * PSI,
"T", 292.9,
"Helium")
374 drop = 5.0 * rho_in_end / CP.PropsSI(
"D",
"P", f_in,
"T", T_in,
"Helium")
375 assert pr[
"floor_line_drop_psi"] == pytest.approx(drop, rel=1e-4)
376 assert pr[
"floor_psia"] == pytest.approx(pr[
"floor_regulator_inlet_psia"] + pr[
"floor_line_drop_psi"], abs=1e-3)
378 unusable = CP.PropsSI(
"D",
"P", f_b,
"T", T_b,
"Helium") * V
379 assert pr[
"unusable_kg"] == pytest.approx(unusable, rel=1e-9)
380 assert pr[
"required_kg"] == pytest.approx(mass[0] - mass[3] + unusable)
381 assert pr[
"margin_kg"] == pytest.approx(mass[3] - unusable)
383 f_held = P.regulator_inlet_floor(
"helium", 0.03, out_end * PSI, 292.9, 0.8, 0.7) + 5.0 * PSI
384 held = CP.PropsSI(
"D",
"P", f_held,
"T", T_b_end,
"Helium") * V
385 assert pr[
"unusable_held_T_kg"] == pytest.approx(held, rel=1e-9)
386 assert pr[
"unusable_held_T_kg"] < pr[
"unusable_kg"]
388 s0 = CP.PropsSI(
"SMASS",
"P", p_b[0] * PSI,
"T", T_true[0],
"Helium")
389 assert pr[
"bottle_T_end_isentropic_K"] == pytest.approx(CP.PropsSI(
"T",
"P", p_b_end,
"SMASS", s0,
"Helium"))
391 p_in = res[
"network"][
"nodes"][
"PR_D.in"][
"p_psia"][2] * PSI
392 p_out = res[
"network"][
"nodes"][
"PR_D.out"][
"p_psia"][2] * PSI
393 assert pr[
"jt_dT_K"][2] == pytest.approx(P.jt_temperature_change(
"helium", p_in, 292.9, p_out))
394 assert pr[
"jt_dT_K"][2] > 0
403 """What the diagnostics read off a prepared model: branch bores, node fluids, species."""
404 bores = {
"l_ox1": 0.01092,
"MVO": 0.0127,
"l_ox2": 0.01092,
"l_fuel1": 0.01092,
"MVF": 0.0127,
405 "l_fuel2": 0.01092,
"ENG.oxidiser.injector": 0.008,
"ENG.fuel.injector": 0.0072}
406 branches = {bid: SimpleNamespace(component=SimpleNamespace(p={
"bore": bores[bid]}
if bid
in bores
else {}))
407 for bid
in res[
"network"][
"branches"]}
408 fluid = {
"ox":
"oxygen",
"fuel":
"ethanol",
"gas":
"helium"}
409 nodes = {nid: SimpleNamespace(fluid=(
"oxygen" if nd[
"phase"] ==
"liquid" and nd[
"side"] ==
"ox" else
410 "ethanol" if nd[
"phase"] ==
"liquid" else fluid.get(nd[
"side"],
"helium")))
411 for nid, nd
in res[
"network"][
"nodes"].items()}
412 built = SimpleNamespace(network=SimpleNamespace(branches=branches, nodes=nodes), dome_loaders={})
413 return SimpleNamespace(model=SimpleNamespace(built=built), setup=
None,
414 derived={
"species": {
"oxidiser":
"oxygen",
"fuel":
"ethanol"},
"pressurant_gas":
"helium",
415 "inlet_nodes": {
"oxidiser":
"ENG.oxidiser",
"fuel":
"ENG.fuel"}})
419 geo = SimpleNamespace(
420 oxidizer=SimpleNamespace(n_elements=24, d_jet=0.0016318401623160582, impingement_angle=35.0,
421 spacing=0.008312130562622994),
422 fuel=SimpleNamespace(n_elements=24, d_jet=0.0014715307322406754, impingement_angle=48.0,
423 spacing=0.011038509387163336))
424 dis = {
"oxidizer": SimpleNamespace(orifice_l_over_d=5.0, inlet_geometry=
"sharp", inlet_radius_ratio=
None,
425 l_over_d_source=
"declared"),
426 "fuel": SimpleNamespace(orifice_l_over_d=5.545, inlet_geometry=
"sharp", inlet_radius_ratio=
None,
427 l_over_d_source=
"declared")}
428 return SimpleNamespace(injector=SimpleNamespace(geometry=geo), discharge=dis,
429 fluids={
"oxidizer": SimpleNamespace(density=1140.0),
"fuel": SimpleNamespace(density=789.0)})
433 import CoolProp.CoolProp
as CP
435 sat = S.build_saturation(res,
_fake_prep(res))
436 assert "error" not in sat, sat
437 rows = {r[
"id"]: r
for r
in sat[
"nodes"]}
438 assert "ENG.chamber" not in rows
and "OXT" not in rows
439 psat_lox = CP.PropsSI(
"P",
"T", 90.0,
"Q", 0,
"Oxygen") / PSI
440 psat_eth = CP.PropsSI(
"P",
"T", 293.15,
"Q", 0,
"Ethanol") / PSI
441 p = res[
"network"][
"nodes"][
"MVO.in"][
"p_psia"]
442 assert rows[
"MVO.in"][
"margin_psi"] == pytest.approx([x - psat_lox
for x
in p])
443 p_f = res[
"network"][
"nodes"][
"MVF.in"][
"p_psia"]
444 assert rows[
"MVF.in"][
"margin_psi"] == pytest.approx([x - psat_eth
for x
in p_f])
447 rho = CP.PropsSI(
"D",
"T", 90.0,
"Q", 0,
"Oxygen")
448 A = math.pi * 0.01092 ** 2 / 4
449 q3 = 1.9 ** 2 / (2 * rho * A * A) / PSI
450 assert rows[
"MVO.in"][
"margin_static_psi"][3] == pytest.approx(p[3] - psat_lox - q3)
451 assert 20.0 < q3 < 30.0
453 pe = res[
"network"][
"nodes"][
"ENG.oxidiser"][
"p_psia"]
454 assert rows[
"ENG.oxidiser"][
"margin_static_psi"][3] == pytest.approx(pe[3] - psat_lox - q3)
456 k = 2
if p[2] - psat_lox < p[3] - psat_lox
else 3
457 assert rows[
"MVO.in"][
"min_psi"] == pytest.approx(p[k] - psat_lox)
and rows[
"MVO.in"][
"t_min"] == T[k]
459 pmo = res[
"network"][
"nodes"][
"MVO.out"][
"p_psia"]
460 assert pmo[0] - psat_lox < 25.0
461 assert rows[
"MVO.out"][
"min_psi"] == pytest.approx(
min(pmo[2], pmo[3]) - psat_lox)
462 assert not any(r[
"flagged"]
for r
in sat[
"nodes"])
464 res[
"network"][
"nodes"][
"MVF.in"][
"p_psia"] = [psat_eth + 10.0 + q
for q
in (0, 0, 0, 0)]
465 sat = S.build_saturation(res,
_fake_prep(res))
466 assert {r[
"id"]: r
for r
in sat[
"nodes"]}[
"MVF.in"][
"flagged"]
495 import CoolProp.CoolProp
as CP
498 cav = S.build_cavitation(res,
_fake_prep(res), cfg)
499 assert "error" not in cav, cav
501 pv = CP.PropsSI(
"P",
"T", 90.0,
"Q", 0,
"Oxygen") / PSI
502 p_up = res[
"series"][
"ox"][
"manifold_psia"][3]
503 pc = res[
"series"][
"chamber"][
"pc_psia"][3]
504 assert ox[
"K"][3] == pytest.approx((p_up - pv) / (p_up - pc))
505 A = 24 * math.pi * 0.0016318401623160582 ** 2 / 4
506 cd = 1.9 / (A * math.sqrt(2 * 1140.0 * (p_up - pc) * PSI))
507 assert ox[
"Cd_eff"][3] == pytest.approx(cd)
508 assert ox[
"K_crit"][3] == pytest.approx((cd / 0.62) ** 2)
509 assert ox[
"K"][0]
is None
510 assert not ox[
"cavitates"]
and not ox[
"flip_risk"]
513 s[
"chamber"][
"pc_psia"] = [14.7, 14.7, 400.0, 150.0]
514 s[
"ox"][
"manifold_psia"][3] = 500.0
515 s[
"ox"][
"dp_injector_psi"][3] = 350.0
516 s[
"ox"][
"mdot"][3] = 0.78 * A * math.sqrt(2 * 1140.0 * 350.0 * PSI)
517 cav = S.build_cavitation(res,
_fake_prep(res), cfg)
518 assert cav[
"ox"][
"K"][3] == pytest.approx((500.0 - pv) / 350.0)
519 assert cav[
"ox"][
"K_crit"][3] == pytest.approx((0.78 / 0.62) ** 2)
520 assert cav[
"ox"][
"cavitates"]
and cav[
"ox"][
"flip_risk"]
521 assert cav[
"ox"][
"t_min"] == T[3]
522 cfg.discharge[
"oxidizer"].orifice_l_over_d = 8.0
523 cav = S.build_cavitation(res,
_fake_prep(res), cfg)
524 assert cav[
"ox"][
"cavitates"]
and not cav[
"ox"][
"flip_risk"]
525 assert cav[
"model"][
"inputs"][
"flip_l_over_d_max"][
"provenance"].startswith(
"assumed")
535 res[
"replay"] = {
"t": [0.05, 0.15],
"eta_cstar": [0.91, 0.93]}
537 fwd = {
"mdot_O": 1.85,
"mdot_F": 1.22,
"eta_cstar": 0.9105,
538 "diagnostics": {
"momentum_ratio_R": 1.0304,
"rupe_M": 1.1774,
"Cd_O": 0.784,
"Cd_F": 0.776}}
539 inj = J.build_injector(res,
None, cfg, forward=fwd)
540 assert "error" not in inj, inj
541 d_o, d_f = 0.0016318401623160582, 0.0014715307322406754
542 A_o, A_f = 24 * math.pi * d_o ** 2 / 4, 24 * math.pi * d_f ** 2 / 4
544 v_o, v_f = m_o / (1140.0 * A_o), m_f / (789.0 * A_f)
545 assert inj[
"v_ox"][3] == pytest.approx(v_o)
and inj[
"v_fuel"][3] == pytest.approx(v_f)
546 assert inj[
"v_ox"][0]
is None
547 R_hand = math.sqrt(1140.0 * v_o ** 2 / (789.0 * v_f ** 2))
548 assert inj[
"momentum_ratio"][3] == pytest.approx(R_hand)
550 assert inj[
"momentum_ratio"][3] == pytest.approx((m_o / m_f) * (A_f / A_o) * math.sqrt(789.0 / 1140.0))
551 assert inj[
"rupe_M"][3] == pytest.approx(R_hand ** 2 * d_o / d_f)
553 p_o, p_f = m_o * v_o, m_f * v_f
554 pr = p_o * math.sin(math.radians(35.0)) - p_f * math.sin(math.radians(48.0))
555 pz = p_o * math.cos(math.radians(35.0)) + p_f * math.cos(math.radians(48.0))
556 assert inj[
"resultant_angle_deg"][3] == pytest.approx(math.degrees(math.atan2(pr, pz)))
557 assert inj[
"design_momentum_ratio"] == 1.0304
559 assert inj[
"eta_cstar"][2] == pytest.approx(0.91)
560 assert inj[
"eta_cstar"][3] == pytest.approx(0.92)
561 assert inj[
"eta_cstar"][1]
is None
587 t = [0.0, 0.05, 0.1, 0.2, 0.3, 0.4]
588 firing = [
False,
True,
True,
True,
True,
True]
589 K0 = {
"ox": 17.0 * PSI / 1.9 ** 2,
"fuel": 33.0 * PSI / 1.25 ** 2}
590 head = {
"ox": 0.5,
"fuel": 0.3}
591 series = {
"t": t,
"firing": firing,
"dt": [0.05, 0.05, 0.05, 0.1, 0.1, 0.1]}
592 for s
in (
"ox",
"fuel"):
593 tank = [578.0 + 100.0 * x
for x
in t]
594 a = [9.80665] * len(t)
if accel
is None else accel
595 outlet = [p + head[s] * ai / 9.80665
for p, ai
in zip(tank, a)]
596 K = [K0[s] * (1.0 + K_drift * x)
for x
in t]
597 m = [
_fed(o * PSI, k, s)
for o, k
in zip(outlet, K)]
598 inlet = [o - k * mm * mm / PSI
for o, k, mm
in zip(outlet, K, m)]
599 series[s] = {
"tank_psia": tank,
"outlet_psia": outlet,
"inlet_psia": inlet,
"mdot": m}
600 idx = [1, 2, 3, 4, 5]
601 F_ab = [
_fake_engine(series[
"ox"][
"inlet_psia"][i] * PSI, series[
"fuel"][
"inlet_psia"][i] * PSI)[
"F"]
for i
in idx]
602 F_del = [f * (1.0 + 0.01 * t[i])
for f, i
in zip(F_ab, idx)]
603 replay = {
"t": [t[i]
for i
in idx],
"index": idx,
"thrust_N": F_del,
604 "inlet_O_psia": [series[
"ox"][
"inlet_psia"][i]
for i
in idx],
605 "inlet_F_psia": [series[
"fuel"][
"inlet_psia"][i]
for i
in idx]}
606 delivered = {
"t": [t[i]
for i
in idx],
"thrust_N": F_del,
607 "summary": {
"mean_thrust_N": sum(F_del) / len(F_del)}}
608 res = {
"series": series,
"replay": replay,
"delivered": delivered}
609 if accel
is not None:
610 res[
"flight"] = {
"ok":
True,
"schedule": {
"t": t,
"accel_m_s2": accel}}
611 return res, K0, head, F_ab, F_del
616 ts = TS.build_thrust_shape(res,
None,
None, sampler=_fake_engine, target_N=7000.0)
617 assert "error" not in ts, ts
619 assert b[
"k0"] == 2
and b[
"t0"] == 0.2
620 t = [0.05, 0.1, 0.2, 0.3, 0.4]
623 i = res[
"replay"][
"index"][j]
624 return K_THRUST * sum(
_fed(res[
"series"][s][
"outlet_psia"][i] * PSI, K0[s] * (1 + 0.5 * 0.2), s)
625 for s
in (
"ox",
"fuel"))
628 assert b[
"erosion_N"][j] == pytest.approx((F_del[j] - F_ab[j]) - (F_del[2] - F_ab[2]), rel=1e-9)
629 assert b[
"tank_pressure_N"][j] == pytest.approx(F_T(j) - F_T(2), rel=1e-6)
630 assert b[
"accel_head_N"][j] == pytest.approx(0.0, abs=1e-9)
632 assert b[
"dF_N"][j] == pytest.approx(F_del[j] - F_del[2])
633 total = b[
"tank_pressure_N"][j] + b[
"erosion_N"][j] + b[
"accel_head_N"][j] + b[
"residual_N"][j]
634 assert total == pytest.approx(b[
"dF_N"][j], rel=1e-12)
635 assert b[
"residual_N"][j] == pytest.approx((F_ab[j] - F_ab[2]) - (F_T(j) - F_T(2)), abs=1e-6)
636 assert b[
"residual_N"][j] < -1.0
637 assert b[
"dF_N"][0]
is None
640 b0 = TS.build_thrust_shape(res,
None,
None, sampler=_fake_engine)[
"breakdown"]
641 assert b0[
"residual_N"][4] == pytest.approx(0.0, abs=1e-5)
642 assert b0[
"feed_solve"][
"converged"]
644 mean = sum(F_del) / 5
645 dev = [f - mean
for f, tt
in zip(F_del, t)
if tt >= 0.2]
646 assert ts[
"dev_max_pct"] == pytest.approx(
max(abs(d)
for d
in dev) / mean * 100)
647 w = [0.05, 0.1, 0.05]
648 assert ts[
"dev_rms_pct"] == pytest.approx(math.sqrt(sum(wi * d * d
for wi, d
in zip(w, dev)) / 0.2) / mean * 100)
649 assert ts[
"target_N"] == 7000.0
and ts[
"target_source"] ==
"restated for this run"
653 accel = [9.80665 * (1.0 + 20.0 * x)
for x
in [0.0, 0.05, 0.1, 0.2, 0.3, 0.4]]
654 res, K0, head, F_ab, F_del =
_thrust_result(K_drift=0.0, accel=accel)
655 b = TS.build_thrust_shape(res,
None,
None, sampler=_fake_engine)[
"breakdown"]
656 assert b[
"accel_basis"].startswith(
"flight")
659 i = res[
"replay"][
"index"][j]
661 for s
in (
"ox",
"fuel"):
662 p = res[
"series"][s][
"tank_psia"][i] + head[s] * (accel[i] / 9.80665
if flown
else 1.0)
663 tot +=
_fed(p * PSI, K0[s], s)
664 return K_THRUST * tot
666 want = (F_T(4,
True) - F_T(4,
False)) - (F_T(2,
True) - F_T(2,
False))
667 assert b[
"accel_head_N"][4] == pytest.approx(want, rel=1e-6)
668 assert b[
"accel_head_N"][4] > 0.0
669 assert b[
"tank_pressure_N"][4] == pytest.approx(F_T(4,
False) - F_T(2,
False), rel=1e-6)
670 assert b[
"residual_N"][4] == pytest.approx(0.0, abs=1e-5)
679 import CoolProp.CoolProp
as CP
682 s[
"dt"] = [0.05, 0.05, 0.05, 0.10]
683 s[
"ox"][
"stiffness"] = [0.0, 0.0, 0.36, 0.40]
684 s[
"regulators"] = {
"PR_D": {
"label":
"PR-DOME",
"outlet_psia": res[
"network"][
"nodes"][
"PR_D.out"][
"p_psia"]}}
685 res[
"delivered"] = {
"t": [0.05, 0.10],
"pc_psia": [400.0, 401.0],
"thrust_N": [6750.0, 6800.0],
686 "throat_area_ratio": [1.0, 1.04],
"mr": [1.52, 1.51],
"ambient_psia": [13.64, 13.64],
687 "summary": {
"total_impulse_Ns": 24240.0}}
688 res[
"replay"] = {
"t": [0.05, 0.10],
"eta_cstar": [0.910, 0.912]}
689 res[
"summary"] = {
"burn_time_s": 3.456}
691 cfg.chamber_geometry = SimpleNamespace(A_throat=1.7947e-3, expansion_ratio=4.8276, Lstar=1.3586,
692 nozzle_efficiency=0.95)
693 cfg.thrust = SimpleNamespace(burn_time=3.994)
694 cfg.design_requirements = SimpleNamespace(lox_tank_capacity_kg=6.611, fuel_tank_capacity_kg=4.404)
695 fwd = {
"Pc": 393.22 * PSI,
"F": 6803.7,
"MR": 1.5232,
"Isp": 224.77,
"mdot_O": 1.8633,
"mdot_F": 1.2233,
696 "eta_cstar": 0.9105,
"P_ambient": 94069.7,
"Cd_O": 0.784,
"Cd_F": 0.776,
697 "_basis": {
"tank_psia": 578.0},
698 "diagnostics": {
"delta_p_feed_O": 41.34 * PSI,
"delta_p_feed_F": 47.31 * PSI,
699 "delta_p_injector_O": 143.44 * PSI,
"delta_p_injector_F": 137.47 * PSI,
700 "P_injector_O": 536.66 * PSI,
"P_injector_F": 530.69 * PSI}}
701 led = LG.build_ledger(res,
None, cfg, forward=fwd)
702 assert isinstance(led, list), led
703 rows = {r[
"key"]: r
for r
in led}
705 assert rows[
"pc"][
"design_value"] == pytest.approx(393.22)
706 assert rows[
"feed_loss_ox"][
"design_value"] == pytest.approx(41.34)
707 assert rows[
"stiffness_ox"][
"design_value"] == pytest.approx(143.44 / 393.22)
708 assert rows[
"tank_pressure_ox"][
"design_value"] == 578.0
709 assert rows[
"ambient"][
"design_value"] == pytest.approx(94069.7 / PSI)
710 assert rows[
"impulse"][
"design_value"] == pytest.approx((6.611 + 4.404) * 224.77 * 9.80665)
712 tank = s[
"ox"][
"tank_psia"]
713 d = rows[
"tank_pressure_ox"][
"delivered"]
714 assert d[
"start"] == pytest.approx(tank[2])
and d[
"end"] == pytest.approx(tank[3])
715 assert d[
"mean"] == pytest.approx((tank[2] * 0.05 + tank[3] * 0.10) / 0.15)
716 loss = [a - b
for a, b
in zip(s[
"ox"][
"tank_psia"], s[
"ox"][
"manifold_psia"])]
717 assert rows[
"feed_loss_ox"][
"delivered"][
"max"] == pytest.approx(
max(loss[2:]))
718 assert rows[
"stiffness_ox"][
"delivered"][
"max"] == pytest.approx(0.40)
719 assert rows[
"throat_area"][
"delivered"][
"end"] == pytest.approx(1.7947e-3 * 1.04 * 1e6)
720 assert rows[
"eta_cstar"][
"delivered"][
"mean"] == pytest.approx(0.911)
722 A = 24 * math.pi * 0.0016318401623160582 ** 2 / 4
723 dpi = s[
"ox"][
"dp_injector_psi"][3] * PSI
724 assert rows[
"cd_ox"][
"delivered"][
"end"] == pytest.approx(1.9 / (A * math.sqrt(2 * 1140.0 * dpi)))
726 assert rows[
"density_ox"][
"design_value"] == 1140.0
727 assert rows[
"density_ox"][
"delivered"][
"mean"] == pytest.approx(CP.PropsSI(
"D",
"T", 90.0,
"Q", 0,
"Oxygen"))
728 assert rows[
"density_ox"][
"replaced"] ==
"no"
730 assert rows[
"specific_force"][
"delivered"][
"mean"] == 1.0
731 assert rows[
"tank_head_ox"][
"design_value"] == 0.0
732 assert rows[
"tank_head_ox"][
"delivered"][
"mean"] == pytest.approx(0.5)
734 assert r[
"replaced"]
in (
"yes",
"partly",
"no")
736 led = {r[
"key"]: r
for r
in LG.build_ledger(res,
None, cfg)}
737 assert led[
"pc"][
"design_value"]
is None and "no Forward" in led[
"pc"][
"design_source"]
741 """Nozzle erosion off (run 20261102-123645-807b98): no replay, no ``delivered``. The burn still
742 fired, on the twin's chamber; Pc, O/F, thrust, Isp and the flows read '—' in the Overview ledger.
743 They come from ``series.chamber`` and the sides' flows; what only the replay computes says why."""
746 s[
"chamber"].update({
"mr": [0.0, 0.0, 1.50, 1.52],
"thrust_N": [0.0, 0.0, 6100.0, 6150.0],
747 "isp_s": [0.0, 0.0, 221.0, 222.0]})
748 res[
"summary"] = {
"burn_time_s": 3.86,
"total_impulse_Ns": 23730.0}
749 res[
"provenance"] = {
"settings": {
"replay":
False}}
751 cfg.chamber_geometry = SimpleNamespace(A_throat=1.7947e-3, expansion_ratio=4.8276, Lstar=1.3586,
752 nozzle_efficiency=0.95)
753 cfg.thrust = SimpleNamespace(burn_time=3.994)
754 cfg.design_requirements = SimpleNamespace(lox_tank_capacity_kg=6.611, fuel_tank_capacity_kg=4.404)
755 rows = {r[
"key"]: r
for r
in LG.build_ledger(res,
None, cfg)}
757 assert rows[
"pc"][
"delivered"][
"min"] == pytest.approx(400.0)
758 assert rows[
"pc"][
"delivered"][
"max"] == pytest.approx(401.0)
759 assert rows[
"pc"][
"series_ref"] ==
"series.chamber.pc_psia"
760 assert rows[
"of"][
"delivered"][
"mean"] == pytest.approx(1.51)
761 assert rows[
"thrust"][
"delivered"][
"end"] == pytest.approx(6150.0)
762 assert rows[
"isp"][
"delivered"][
"start"] == pytest.approx(221.0)
763 assert rows[
"mdot_ox"][
"delivered"][
"end"] == pytest.approx(1.9)
764 assert rows[
"impulse"][
"delivered"][
"mean"] == pytest.approx(23730.0)
766 for key
in (
"throat_area",
"eta_cstar"):
767 assert rows[key][
"delivered"]
is None
768 assert rows[key][
"missing"] ==
"erosion off"
769 assert "nozzle erosion off" in rows[key][
"note"]
770 assert "missing" not in rows[
"pc"]
797 lad = L.build_ladder(res)
798 for side, head
in ((
"ox", 0.5), (
"fuel", 0.3)):
801 assert [e[
"kind"]
for e
in s[
"elements"]][7] ==
"tank_head"
802 assert s[
"elements"][7][
"dp_psi"] == pytest.approx([-head] * N)
803 sol = R.build_solenoids(res,
None, gas=
"helium")
805 assert sorted(r[
"id"]
for r
in sol) == [
"SV_FUEL_PRESS",
"SV_LOX_PRESS"]
806 ox = next(r
for r
in sol
if r[
"side"] ==
"ox")
807 assert ox[
"share_of_reg_to_tank"][2] == pytest.approx(1.5 / 3.1)
808 sat = S.build_saturation(res,
_fake_prep(res))
809 mvo = {r[
"id"]: r
for r
in sat[
"nodes"]}[
"MVO.out"]
810 assert mvo[
"margin_psi"][0]
is None and mvo[
"margin_psi"][2]
is not None
812 mvf = {r[
"id"]: r
for r
in sat[
"nodes"]}[
"MVF.in"]
813 assert mvf[
"T_source"].startswith(
"series.fuel.liquid_K")
814 assert mvf[
"min_psi"]
is not None
819@pytest.mark.skipif(os.environ.get("LAYERX_GOLDEN") !=
"1", reason=
"slow LE4 burn: set LAYERX_GOLDEN=1 to run it")
821 """The baseline design on copv_study_he, pad, at AUDIT 9.6's 578 psia lockup and otherwise
822 default settings, with lib/feedtwin's network recorder on, against the hand checks of AUDIT
823 9.6-9.7 and the baseline (section 8).
825 Re-baselined 2026-10-07. The AUDIT's figures are the 2026-10-02 twin: adiabatic lines, the
826 578 psia lockup the default was then. Three things have moved them since, each deliberate:
828 * 2026-10-03 (night), line walls on (Layer X CHANGELOG): the press path's metal warms the gas
829 the bottle's expansion cooled, so each ullage litre takes less helium. He used 0.0906 ->
830 0.0818 kg; the bottle ends ~220 psi fuller, so the regulator's supply effect (17 psi/1000
831 psi of inlet fall) is ~3.8 psi smaller and its outlet rise 47.1 -> 43.86 psi; the press flow
832 at 0.5 s is 0.01105 kg/s of 304 K gas, not 0.01121 of 298 K, so the solenoid drops are
833 1.704 / 1.393 psi (hand, Cv law at the twin's state: 1.7045 / 1.3935) and the regulator's
834 peak use 14.3 %, not the AUDIT's 17.8 %.
835 * 2026-10-06, the ullage storage closure (a35563ce, ``Session._ullage_storage``): the
836 momentum ratio's range 1.0283-1.0312 -> 1.0285-1.0315 (the press flow no longer held at the
837 start-of-step tank pressure); with the closure stubbed out it returns, as does SV-LOX-PRESS's
838 0.1 % (1.7060 -> 1.7045). The supply datum (35b98bf2) moves nothing here.
839 * 2026-10-07, the dome dial defaults to the drawn dome (b67edc93): 564.7 psia on this drawing.
840 At that lockup every figure here moves (SV-LOX-PRESS 1.609 psi: 4.0 % less helium, 2.4 %
841 thinner, dp ~ mdot^2/rho), and thrust never comes within 2 % of 7.2 kN. The lockup is stated
842 below so the hand checks stay at the point they were made."""
844 import importlib.util
845 from pathlib
import Path
847 fb = pytest.importorskip(
"feedtwin.session.burn")
848 if "network" not in {f.name
for f
in dataclasses.fields(fb.Probes)}:
849 pytest.skip(
"lib/feedtwin has no network recorder")
850 root = Path(__file__).resolve().parents[1]
851 spec = importlib.util.spec_from_file_location(
"layerx_baseline", root /
"scripts" /
"layerx_baseline.py")
852 lb = importlib.util.module_from_spec(spec)
853 spec.loader.exec_module(lb)
855 from engine.layerx import LayerXSettings, prepare, run_prepared
858 find = fb.find_probes
859 monkeypatch.setattr(fb,
"find_probes",
lambda s: dataclasses.replace(
find(s), network=
True))
860 rec = fb.BurnTrace.recorder
861 monkeypatch.setattr(fb.BurnTrace,
"recorder",
lambda self, s: (traces.append(self), rec(self, s))[1])
863 config, _ = lb.load_engine_config(root /
"docs" /
"layerx" /
"baseline-2026-10-02.json")
864 drawing = lb.find_drawing(
"copv_study_he")
865 cfg = copy.deepcopy(config)
866 runner = PintleEngineRunner(cfg)
868 prep = prepare(cfg, runner, drawing, LayerXSettings(drawing_id=drawing.id, tank_pressure_psia=578.0), [])
869 result = run_prepared(prep, runner=runner, replay=
True, config=cfg)
870 if not isinstance(result.get(
"network"), dict)
or "nodes" not in result[
"network"]:
871 result[
"network"] = fb.network_record(traces[-1])
874 d = build_feed_diagnostics(result, prep, cfg, runner=runner, target_N=7200.0)
875 for key, block
in d.items():
876 assert not (isinstance(block, dict)
and block.get(
"available")
is False), (key, block)
877 t = result[
"series"][
"t"]
878 k05 =
min(range(len(t)), key=
lambda i: abs(t[i] - 0.5))
880 for side
in (
"ox",
"fuel"):
881 assert d[
"ladder"][side][
"closes"], d[
"ladder"][side][
"max_abs_residual_psi"]
882 sol = {r[
"id"]: r
for r
in d[
"solenoids"]}
883 assert sol[
"SV_LOX_PRESS"][
"dp_psi"][k05] == pytest.approx(1.704, abs=0.03)
884 assert sol[
"SV_FUEL_PRESS"][
"dp_psi"][k05] == pytest.approx(1.393, abs=0.03)
885 assert sol[
"SV_LOX_PRESS"][
"dp_iec_psi"][k05] == pytest.approx(sol[
"SV_LOX_PRESS"][
"dp_psi"][k05], rel=0.01)
889 assert max(abs(x)
for x, f
in zip(reg[
"residual_psi"], result[
"series"][
"firing"])
if f) < 0.05
890 assert reg[
"rise"][
"outlet_rise_psi"] == pytest.approx(43.86, abs=0.5)
891 assert not reg[
"any_wide_open"]
and reg[
"use_frac_max"] == pytest.approx(0.143, abs=0.005)
895 assert pr[
"species"] ==
"helium" and pr[
"used_kg"] == pytest.approx(0.0818, abs=0.001)
896 live = [(a, b)
for a, b, f
in zip(pr[
"jt_dT_K"], pr[
"twin_dT_K"], result[
"series"][
"firing"])
if f]
897 assert all(a > 0
for a, _
in live)
and max(abs(a - b)
for a, b
in live) < 0.05
898 assert pr[
"margin_kg"] > 0
900 assert min(r[
"min_psi"]
for r
in d[
"saturation"][
"nodes"]
if r[
"side"] ==
"ox") > 500.0
901 assert not d[
"cavitation"][
"ox"][
"cavitates"]
and 3.0 < d[
"cavitation"][
"ox"][
"min_K"] < 4.0
903 lo, hi = d[
"injector"][
"range"][
"momentum_ratio"]
904 assert lo == pytest.approx(1.0285, abs=5e-4)
and hi == pytest.approx(1.0315, abs=5e-4)
906 ts = d[
"thrust_shape"]
907 assert ts[
"t_first_at_target"] == pytest.approx(2.90, abs=0.051)
908 end = ts[
"breakdown"][
"at_end"]
909 assert end[
"tank_pressure_N"] + end[
"erosion_N"] + end[
"accel_head_N"] + end[
"residual_N"] == \
910 pytest.approx(end[
"dF_N"], abs=1e-6)
911 assert abs(end[
"residual_N"]) < 0.05 * end[
"dF_N"]
913 led = {r[
"key"]: r
for r
in d[
"ledger"]}
914 assert led[
"tank_pressure_ox"][
"delivered"][
"end"] == pytest.approx(618.35, abs=3.0)
915 assert led[
"feed_loss_ox"][
"design_value"] == pytest.approx(41.34, abs=0.1)