25 """I dm/dt = dp with no loss: m = dp t / I, exactly, at any step."""
26 I, dp, dt = 1495.0, 3.9e6, 1e-4
29 m = S.column_step(m, dp, I, 0.0, dt)
30 assert m == pytest.approx(dp * 100 * dt / I, rel=1e-12)
34 """I dm/dt = dp - R m^2 from rest: m = m_inf tanh(t/tau), m_inf = sqrt(dp/R), tau = I/sqrt(R dp)."""
35 I, R, dp, dt = 10297.0, 1.4e6, 3.9e6, 1e-6
36 m_inf, tau = math.sqrt(dp / R), I / math.sqrt(R * dp)
38 checks = {round(k * tau / 2, 9)
for k
in (1, 2, 4, 8)}
40 for n
in range(1, int(4.0 * tau / dt) + 2):
41 m = S.column_step(m, dp, I, R, dt)
44 if abs(t - tc) < dt / 2:
45 assert m == pytest.approx(m_inf * math.tanh(t / tau), rel=2e-3)
47 assert seen == len(checks)
56 """dPc/dt = (RT/V)(m - Pc At/c*), RT = (Gamma c*)^2: a first-order lag with tau = V/(Gamma^2 c* At)."""
57 V, At, cs, gam, m, pa = 2.44e-3, 1.795e-3, 1575.0, 1.135, 3.07, 94000.0
58 G = S.gamma_function(gam)
60 assert G == pytest.approx(math.sqrt(1.135) * (2 / 2.135) ** (2.135 / 0.27), rel=1e-12)
61 rt_v = (G * cs) ** 2 / V
62 tau = V / (G * G * cs * At)
64 pc, dt = pa, tau / 2000
66 pc = S.chamber_step(pc, m, rt_v, At / cs, dt)
67 assert pc == pytest.approx(pc_ss + (pa - pc_ss) * math.exp(-1.0), rel=2e-3)
69 assert 1.5e-3 < tau < 2.5e-3
87 base = dict(key=key, rho=1150.0
if key ==
"ox" else 792.8, p_tank=578.0 * PSI, I_up=747.0, R_up=2.9e4,
88 R_valve_open=2213.0, travel_s=0.05, t_open=0.0, I_dn=747.0, V_dn_line=6.6e-6, R_dn=1.0e4,
89 V_manifold=1.8e-5, A_exit=9.37e-5, I_inj=163.0, phi=1.73e-3)
91 return S.StartLine(**base)
95 ox =
_line(
"ox", V_dn_line=0.0, V_manifold=0.0)
96 fu =
_line(
"fuel", V_dn_line=0.0, V_manifold=0.0, t_open=-0.02)
97 run = S.run_start(ox, fu,
_chamber(), dt=2e-5, horizon_s=0.05)
98 assert run[
"t_prime"][0] == pytest.approx(0.0, abs=1e-12)
99 assert run[
"t_prime"][1] == pytest.approx(-0.02, abs=1e-12)
100 assert run[
"t_ignition"] == pytest.approx(0.0, abs=1e-12)
104 """No loss, no valve travel, no dump: m = dp t/I, so the volume V fills at t = sqrt(2 I rho V / dp),
105 and the column keeps ramping at dp/I once it is primed (no injector loss, no ignition)."""
106 I, rho, V, dp = 1495.0, 1150.0, 2.4e-5, 578.0 * PSI - 94069.7
107 ox =
_line(
"ox", I_up=I, R_up=0.0, R_valve_open=0.0, travel_s=0.0, I_dn=0.0, V_dn_line=0.0, R_dn=0.0,
108 V_manifold=V, K_exit_prime=0.0, I_inj=0.0, phi=math.inf)
109 fu =
_line(
"fuel", t_open=math.inf)
110 run = S.run_start(ox, fu,
_chamber(), dt=1e-5, horizon_s=0.02)
111 assert not run[
"ignited"]
112 assert run[
"t_prime"][0] == pytest.approx(math.sqrt(2 * I * rho * V / dp), rel=1e-3)
113 t_last, _, m_inj, _ = run[
"trace"][-1]
114 assert run[
"line_end"][0] == pytest.approx(dp * t_last / I, rel=1e-6)
115 assert m_inj == pytest.approx(dp * t_last / I, rel=1e-6)
119 """No inertance upstream, no loss but the K velocity heads at the front: the growing column starts
120 at its terminal velocity v = sqrt(2 dp / (rho K)) (Liou & Hunt's rigid-column equation with
121 l = 0) and fills a tube of length L at t = L / v."""
122 rho, A, L = 1150.0, math.pi * 10.92e-3 ** 2 / 4, 0.5
123 dp = 578.0 * PSI - 94069.7
125 ox =
_line(
"ox", rho=rho, I_up=0.0, R_up=0.0, R_valve_open=0.0, travel_s=0.0, I_dn=L / A, V_dn_line=A * L,
126 R_dn=0.0, V_manifold=0.0, A_exit=A, K_exit_prime=K, I_inj=0.0, phi=math.inf)
127 run = S.run_start(ox,
_line(
"fuel", t_open=math.inf),
_chamber(), dt=1e-6, horizon_s=0.02)
128 assert run[
"t_prime"][0] == pytest.approx(L / math.sqrt(2 * dp / (rho * K)), rel=1e-3)
129 assert run[
"arrival_mdot"][0] == pytest.approx(rho * A * math.sqrt(2 * dp / (rho * K)), rel=1e-6)
133 """No loss, a column of fixed length L_u filling a tube of the same bore: rho (L_u + x) dv/dt = dp,
134 so v^2 = (2 dp / rho) ln((L_u + x)/L_u) and the tube of length L_d fills at
135 t = L_u sqrt(rho / (2 dp)) sqrt(pi) erfi(sqrt(ln((L_u + L_d)/L_u)))."""
136 from scipy.special
import erfi
138 rho, A, Lu, Ld = 1150.0, math.pi * 10.92e-3 ** 2 / 4, 0.14, 0.5
139 dp = 578.0 * PSI - 94069.7
140 ox =
_line(
"ox", rho=rho, I_up=Lu / A, R_up=0.0, R_valve_open=0.0, travel_s=0.0, I_dn=Ld / A, V_dn_line=A * Ld,
141 R_dn=0.0, V_manifold=0.0, A_exit=A, K_exit_prime=0.0, I_inj=0.0, phi=math.inf)
142 run = S.run_start(ox,
_line(
"fuel", t_open=math.inf),
_chamber(), dt=1e-6, horizon_s=0.02)
143 t_fill = Lu * math.sqrt(rho / (2 * dp)) * math.sqrt(math.pi) * erfi(math.sqrt(math.log((Lu + Ld) / Lu)))
144 assert run[
"t_prime"][0] == pytest.approx(t_fill, rel=2e-3)
145 v_end = math.sqrt(2 * dp / rho * math.log((Lu + Ld) / Lu))
146 assert run[
"arrival_mdot"][0] == pytest.approx(rho * A * v_end, rel=2e-3)
150 """Primed at Fire, no loss: the whole column, tube and orifice passages, ramps at dp / (I_up + I_dn + I_inj)."""
151 dp = 578.0 * PSI - 94069.7
152 ox =
_line(
"ox", R_up=0.0, R_valve_open=0.0, travel_s=0.0, R_dn=0.0, V_dn_line=0.0, V_manifold=0.0,
153 I_up=747.0, I_dn=747.0, I_inj=163.0, phi=math.inf)
154 run = S.run_start(ox,
_line(
"fuel", t_open=math.inf),
_chamber(), dt=1e-5, horizon_s=0.01)
155 t_last = run[
"trace"][-1][0]
156 assert run[
"line_end"][0] == pytest.approx(dp * t_last / (747.0 + 747.0 + 163.0), rel=1e-9)
160 """With the valves full open, the start settles on m_s = sqrt((p_T - Pc)/R_s) and Pc At/c* = m_o + m_f."""
161 ox, fu =
_line(
"ox"),
_line(
"fuel", I_up=9700.0, phi=1.19e-3)
163 run = S.run_start(ox, fu, ch, dt=1e-5, horizon_s=0.15)
166 return ln.R_up + ln.R_valve_open + ln.R_dn + 1.0 / ln.phi ** 2
169 mo = math.sqrt((ox.p_tank - pc) / r_tot(ox))
170 mf = math.sqrt((fu.p_tank - pc) / r_tot(fu))
171 return pc * ch.throat_area / 1570.0 - (mo + mf)
173 pc = brentq(g, 1e5, 578.0 * PSI - 1.0)
174 assert run[
"pc_end"] == pytest.approx(pc, rel=1e-4)
175 assert run[
"mdot_end"][0] == pytest.approx(math.sqrt((ox.p_tank - pc) / r_tot(ox)), rel=1e-4)
176 assert run[
"F_end"] == pytest.approx(2320.0 * pc * ch.throat_area / 1570.0 - ch.ambient_pa * ch.exit_area,
178 assert run[
"t_settle"]
is not None and run[
"t_settle"] < 0.1
182 """Both primed at Fire (no gas volume), ignition delayed by tau: the pair injected before ignition,
183 burned at once in the chamber volume, gives a spike m (Gamma c*)^2 / V."""
185 ox =
_line(
"ox", V_dn_line=0.0, V_manifold=0.0, travel_s=0.0)
186 fu =
_line(
"fuel", V_dn_line=0.0, V_manifold=0.0, travel_s=0.0, I_up=9700.0, phi=1.19e-3)
188 run = S.run_start(ox, fu, ch, dt=1e-6, horizon_s=0.02)
189 mo, mf = run[
"pre_ignition_kg"]
190 pair =
min(mo, 1.52 * mf) * (1 + 1 / 1.52)
191 assert run[
"pair_kg"] == pytest.approx(pair, rel=1e-9)
192 G = S.gamma_function(1.135)
193 assert run[
"spike_pa"] == pytest.approx(pair * (G * 1570.0) ** 2 / 2.44e-3, rel=1e-9)
194 assert run[
"t_ignition"] == pytest.approx(tau, abs=1e-12)
196 m_ox_q = math.sqrt((ox.p_tank - ch.ambient_pa) / (ox.R_up + ox.R_valve_open + ox.R_dn + 1 / ox.phi ** 2))
197 assert mo < m_ox_q * tau
and mo > 0.3 * m_ox_q * tau
201 F, mo, mf = 6750.0, 1.85, 1.22
206 def trace(delay, lead=0.0, q_lead=0.0):
209 while t <= 0.2 + 1e-12:
211 out.append((t, 0.0, 0.0, q_lead))
213 out.append((t, 0.0, 0.0, 0.0))
215 out.append((t, F, mo, mf))
219 kw = dict(F_settled=F, mdot_settled=(mo, mf), c_end=c, mr_end=mr)
221 a = S.fixed_load_deficit(trace=trace(0.0), t_window=0.1, residual_extra=(0.0, 0.05), **kw)
222 assert a[
"deficit_Ns"] == pytest.approx(0.0, abs=1e-6)
224 b = S.fixed_load_deficit(trace=trace(0.02), t_window=0.1, residual_extra=(0.0, 0.05), **kw)
225 assert b[
"window_deficit_Ns"] == pytest.approx(F * 0.02, abs=F * dt)
226 assert b[
"deficit_Ns"] == pytest.approx(0.0, abs=0.5)
228 c1 = S.fixed_load_deficit(trace=trace(0.0, lead=0.02, q_lead=2.0), t_window=0.1, residual_extra=(0.0, 0.05), **kw)
229 assert c1[
"deficit_Ns"] == pytest.approx(0.0, abs=0.5)
231 c2 = S.fixed_load_deficit(trace=trace(0.0, lead=0.2, q_lead=2.0), t_window=0.1, residual_extra=(0.0, 0.05), **kw)
232 assert c2[
"deficit_Ns"] == pytest.approx(c * (1 + mr) * (2.0 * 0.2 - 0.05), rel=2e-3)
250 a =
_tail(
"ox", travel_s=0.0)
251 b =
_tail(
"fuel", travel_s=0.0, V_dn=4.7e-5)
252 tail = SD.tail_off(a, b, t0=1.0, t_cmd=1.0, dt=dt)
254 side = tail[
"sides"][s.key]
255 assert side[
"delivered_kg"] == pytest.approx(s.rho * s.V_dn, rel=1e-9)
256 assert side[
"t_end"] == pytest.approx(1.0 + s.rho * s.V_dn / s.mdot0, abs=2 * dt)
261 a =
_tail(
"ox", wet=
False)
262 tail = SD.tail_off(a,
_tail(
"fuel"), t0=0.0, t_cmd=10.0, dt=dt, horizon_s=0.0)
263 side = tail[
"sides"][
"ox"]
264 stock = a.rho * (a.V_up + a.V_dn)
265 assert side[
"delivered_kg"] == pytest.approx(stock, rel=1e-9)
266 assert side[
"t_end"] == pytest.approx(stock / a.mdot0, abs=2 * dt)
270 """Valve the only loss, linear Cv: the quasi-steady flow falls linearly with travel, so the
271 closure passes m0 * travel / 2, then the downstream liquid drains."""
273 s =
_tail(
"fuel", R_rest=0.0)
274 assert s.closing_fraction(0.3) == pytest.approx(0.3, rel=1e-12)
275 tail = SD.tail_off(s,
_tail(
"ox"), t0=0.0, t_cmd=0.0, dt=dt)
276 assert tail[
"sides"][
"fuel"][
"delivered_kg"] == pytest.approx(s.mdot0 * s.travel_s / 2 + s.rho * s.V_dn,
281 assert s2.closing_fraction(x) == pytest.approx(
282 math.sqrt((s2.R_rest + s2.R_valve_open) / (s2.R_rest + s2.R_valve_open / x ** 2)), rel=1e-12)
286 """LOX runs dry with no downstream volume and a late command D: LOX ends at rho V_up / m_ox, fuel
287 flows until D then drains rho_f V_dn_f. Fuel alone = m_f (D - t_ox) + rho_f V_dn_f."""
289 ox =
_tail(
"ox", wet=
False, V_dn=0.0, travel_s=0.0)
290 fu =
_tail(
"fuel", travel_s=0.0, V_dn=4.7e-5)
291 tail = SD.tail_off(ox, fu, t0=0.0, t_cmd=D, dt=dt)
292 cls = SD.classify(tail)
293 t_ox = ox.rho * ox.V_up / ox.mdot0
294 assert cls[
"mode"] ==
"fuel-rich" and cls[
"lox_alone_kg"] == 0.0
295 assert cls[
"end_ox_s"] == pytest.approx(t_ox, abs=2 * dt)
296 assert cls[
"alone_kg"] == pytest.approx(fu.mdot0 * (D - t_ox) + fu.rho * fu.V_dn, rel=2e-3)
297 assert cls[
"tail_mr_max"] == pytest.approx(ox.rho * ox.V_up / (fu.mdot0 * D + fu.rho * fu.V_dn), rel=2e-3)
299 ox2 =
_tail(
"ox", travel_s=0.0, V_dn=2.4e-5)
300 fu2 =
_tail(
"fuel", wet=
False, travel_s=0.0, V_up=1e-5, V_dn=0.0)
301 cls2 = SD.classify(SD.tail_off(ox2, fu2, t0=0.0, t_cmd=D, dt=dt))
302 t_fu = fu2.rho * fu2.V_up / fu2.mdot0
303 assert cls2[
"mode"] ==
"LOX-rich"
304 assert cls2[
"lox_alone_kg"] == pytest.approx(ox2.mdot0 * (D - t_fu) + ox2.rho * ox2.V_dn, rel=2e-3)
305 assert SD._note(
"LOX-rich", cls2)[
"grade"] ==
"warn"
318 """AUDIT 9.5 §5 by hand: LOX in 1/2 x 0.035 in 316 tube, a = 897 m/s."""
319 a = WH.wave_speed(LOX_A, LOX_RHO, 10.92e-3, 0.889e-3, 193e9, 1.0)
320 assert a == pytest.approx(897.0, abs=1.5)
321 assert WH.wave_speed(LOX_A, LOX_RHO, 10.92e-3, 0.889e-3, math.inf) == LOX_A
323 assert WH.joukowsky(LOX_RHO, 897.0, 17.4) / PSI == pytest.approx(2607.0, rel=2e-3)
340 v0 = pipe.mdot0 / (pipe.rho * pipe.area)
341 sim = WH.moc(pipe, t_end=8 * pipe.length / pipe.a, valve_R=
lambda t: math.inf
if t > 0
else 3.6e5,
343 assert sim[
"p_max"] - sim[
"p0_valve"] == pytest.approx(pipe.rho * pipe.a * v0, rel=1e-9)
345 J, T = pipe.rho * pipe.a * v0, 4 * pipe.length / pipe.a
346 for t, p
in zip(sim[
"t"], sim[
"p_valve"]):
347 if 0.05 * T < t < 0.45 * T:
348 assert p - sim[
"p0_valve"] == pytest.approx(J, rel=1e-9)
349 elif 0.55 * T < t < 0.95 * T:
350 assert p - sim[
"p0_valve"] == pytest.approx(-J, rel=1e-9)
354 """Frictionless, the velocity at the valve brought down linearly over t_c > 2L/a: peak 2 rho L v0 / t_c."""
356 tc = 10 * 2 * pipe.length / pipe.a
357 sim = WH.moc(pipe, t_end=2 * tc, n_reaches=32,
358 valve_flow=
lambda t: pipe.mdot0 *
max(1.0 - t / tc, 0.0))
359 v0 = pipe.mdot0 / (pipe.rho * pipe.area)
360 assert sim[
"p_max"] - sim[
"p0_valve"] == pytest.approx(2 * pipe.rho * pipe.length * v0 / tc, rel=1e-2)
361 assert WH.michaud(pipe.rho, pipe.length, v0, tc, pipe.a) == pytest.approx(2 * pipe.rho * pipe.length * v0 / tc)
362 assert WH.michaud(pipe.rho, pipe.length, v0, 0.5 * pipe.length / pipe.a, pipe.a) == pytest.approx(
363 pipe.rho * pipe.a * v0)
365 assert WH.michaud(pipe.rho, pipe.length, v0, 1.5 * pipe.length / pipe.a, pipe.a) == pytest.approx(
366 pipe.rho * pipe.a * v0)
370 B, m_arr, pa = 9.6e6, 4.9, 94000.0
371 assert WH.opening_surge(p_front=pa, pc=pa, mdot_arrival=m_arr, phi=math.inf, B=B)[
"p_peak"] == pytest.approx(pa)
372 assert WH.opening_surge(p_front=pa, pc=pa, mdot_arrival=m_arr, phi=1e-12, B=B)[
"p_peak"] == pytest.approx(
373 pa + B * m_arr, rel=1e-6)
375 o = WH.opening_surge(p_front=pa, pc=pa, mdot_arrival=m_arr, phi=phi, B=B)
377 m = (-B + math.sqrt(B * B + 4 * B * m_arr / phi ** 2)) / (2 / phi ** 2)
378 assert o[
"mdot_after"] == pytest.approx(m, rel=1e-9)
379 assert o[
"p_peak"] - pa == pytest.approx((m / phi) ** 2, rel=1e-9)
389 """h(z) = z + Q^2/(8 pi^2 g z^4) over a hemispherical sink: its minimum is h_c; the constant
390 0.69 of Lubin & Springer is that minimum over (Q^2/g)^(1/5)."""
391 from scipy.optimize
import minimize_scalar
393 Q, g = 1.73e-3, 9.80665
394 res = minimize_scalar(
lambda z: z + Q * Q / (8 * math.pi ** 2 * g * z ** 4), bounds=(1e-4, 1.0), method=
"bounded",
395 options={
"xatol": 1e-12})
396 scale = (Q * Q / g) ** 0.2
397 assert res.fun / scale == pytest.approx(OF.point_sink_constant(), rel=1e-6)
398 assert OF.point_sink_constant() == pytest.approx(OF.LS_CONSTANT, rel=3e-3)
399 assert OF.critical_height(Q, g, 1150.0) == pytest.approx(0.69 * scale, rel=1e-12)
403 Q, g = 1.73e-3, 9.80665
404 h = OF.critical_height(Q, g, 1150.0)
405 assert OF.critical_height(2 * Q, g, 1150.0) == pytest.approx(h * 2 ** 0.4, rel=1e-12)
406 assert OF.critical_height(Q, 9 * g, 1150.0) == pytest.approx(h * 9 ** -0.2, rel=1e-12)
408 assert OF.critical_height(Q, g, 1150.0, 575.0) == pytest.approx(h * 2 ** 0.2, rel=1e-12)
413 assert OF.volume_below(R, R, R) == pytest.approx(2 / 3 * math.pi * R ** 3, rel=1e-12)
414 assert OF.volume_below(R / 2, R, R / 2) == pytest.approx(2 / 3 * math.pi * R * R * R / 2, rel=1e-12)
415 assert OF.volume_below(0.03, R, 0.0) == pytest.approx(math.pi * R * R * 0.03, rel=1e-12)
417 assert OF.volume_below(0.03, R, R) == pytest.approx(math.pi * 0.03 ** 2 * (R - 0.01), rel=1e-12)
418 for b
in (0.0, R / 2, R):
419 for h
in (0.004, 0.03, 0.2):
420 assert OF.liquid_height(OF.volume_below(h, R, b), R, b) == pytest.approx(h, rel=1e-9)
424 """Constant flow from a flat-bottomed tank: the level falls linearly and h_c is constant, so the
425 dip forms at t = (M - rho pi R^2 h_c) / mdot with rho pi R^2 h_c left."""
426 rho, R, md, M, g = 1150.0, 0.08, 1.9, 6.6, 9.80665
427 t = [0.05 * k
for k
in range(1, 70)]
428 liq = [M - md * ti
for ti
in t]
429 on = OF.onset(t, liq, [md] * len(t), [g] * len(t), rho_liquid=[rho] * len(t), rho_gas=[0.0] * len(t),
430 radius=R, head_depth=0.0)
431 hc = 0.69 * ((md / rho) ** 2 / g) ** 0.2
432 m_c = rho * math.pi * R * R * hc
433 assert on[
"residual_kg"] == pytest.approx(m_c, rel=1e-6)
434 assert on[
"t"] == pytest.approx((M - m_c) / md, rel=1e-6)
436 off = OF.onset(t[:10], liq[:10], [md] * 10, [g] * 10, rho_liquid=[rho] * 10, rho_gas=[0.0] * 10,
437 radius=R, head_depth=0.0)
438 assert off[
"t"]
is None and off[
"residual_kg"]
is None
451 import importlib.util
452 from pathlib
import Path
455 from engine.layerx import LayerXSettings, prepare, run_prepared
457 root = Path(__file__).resolve().parents[1]
458 spec = importlib.util.spec_from_file_location(
"layerx_baseline", root /
"scripts" /
"layerx_baseline.py")
459 mod = importlib.util.module_from_spec(spec)
460 spec.loader.exec_module(mod)
461 cfg, _ = mod.load_engine_config(root /
"docs" /
"layerx" /
"baseline-2026-10-02.json")
462 drawing = mod.find_drawing(
"copv_study_he")
463 burned = copy.deepcopy(cfg)
464 runner = PintleEngineRunner(burned)
465 prep = prepare(burned, runner, drawing, LayerXSettings(drawing_id=drawing.id), [])
466 result = run_prepared(prep, runner=runner, replay=prep.settings.replay, config=burned,
467 progress=
lambda stage, frac:
None)
468 return prep, result, burned
473 """AUDIT 9.5 §1, by hand from the drawing: line inertance sum L/A = 1495 (LOX, 0.14 m) and
474 10297 1/m (fuel, 0.964 m) of 10.92 mm bore; orifice passages 163 / 200 1/m (24 x 8.16 mm)."""
475 prep, result, cfg = le4
476 sp = S.settled_point(result)
477 ox = S.feed_line(prep,
"oxidiser", mdot_ref=sp[
"line_ox"], p_ref=578 * PSI, T_ref=90.0)
478 fu = S.feed_line(prep,
"fuel", mdot_ref=sp[
"line_fu"], p_ref=578 * PSI, T_ref=293.15)
479 assert ox.sum(
"up",
"inertance") + ox.sum(
"dn",
"inertance") == pytest.approx(1495.0, abs=1.0)
480 assert fu.sum(
"up",
"inertance") + fu.sum(
"dn",
"inertance") == pytest.approx(10297.0, abs=5.0)
481 assert ox.valve.id ==
"MVO" and fu.valve.id ==
"MVF" and ox.valve.travel_s == pytest.approx(0.05)
482 man = S.manifold_volumes(cfg)
483 assert man[
"ox"][
"I_inj"] == pytest.approx(163.0, abs=1.0)
484 assert man[
"fuel"][
"I_inj"] == pytest.approx(200.0, abs=1.0)
487 dp_line = (ox.sum(
"up",
"R") + ox.valve.R + ox.sum(
"dn",
"R")) * 1.879 ** 2 / PSI
488 assert 5.0 < dp_line < 20.0
497 prep, result, cfg = le4
498 before = copy.deepcopy(result)
499 st = S.start_from_run(prep, result, cfg)
500 assert st[
"available"], st.get(
"error")
501 assert 0.0 < st[
"prime_ox_s"] < st[
"prime_fuel_s"] < 0.05
502 assert st[
"ignition_s"] == pytest.approx(st[
"prime_fuel_s"])
503 assert 0.0 < st[
"impulse_deficit_pct"] < 1.5
504 assert abs(st[
"steady_check"][
"thrust_rel"]) < 0.01
505 assert len(st[
"t"]) == len(st[
"pc_psia"]) == len(st[
"mdot_ox"]) == len(st[
"mr"])
506 of = OFm.outflow_from_run(prep, result)
507 assert all(r[
"available"]
for r
in of)
508 sd = SDm.shutdown_from_run(prep, result, cfg, outflow=of)
509 assert sd[
"available"]
and sd[
"first_dry"]
in (
"ox",
"fuel")
and sd[
"mode"]
in (
"LOX-rich",
"fuel-rich",
511 wh = WHm.waterhammer_from_run(prep, result, cfg, start=st)
512 assert [r[
"side"]
for r
in wh] == [
"ox",
"fuel"]
and all(r[
"available"]
for r
in wh)
516 assert r_ox[
"joukowsky_psi"] == pytest.approx(1150.5 * 897.0 * v / PSI, rel=0.01)
519 for r, side
in zip(wh, (
"oxidiser",
"fuel")):
520 assert r[
"rating_psia"] == pytest.approx(prep.derived[
"tank_mawp_psi"][prep.roles[side]], abs=1e-9)
523 for block
in [st, sd, *of, *wh]:
524 assert {
"name",
"source",
"assumptions",
"inputs"} <= set(block[
"model"])
525 json.dumps(block, allow_nan=
False)
526 assert result == before
530 """LE4's LOX side has no drawn line (tank outlet, OM-R, injector): the start took the manifold's
531 entry area from a line that is not there, indexed an empty list, and the whole start went
532 missing from the run ("Start not computed"). With no line it is the main valve's own bore; with
533 a line, the line's, exactly as before; with neither, it says so rather than guess."""
534 pipe = S.Element(id=
"L1", kind=
"pipe", R=0.0, length=0.3, bore=0.010922)
535 valve = S.Element(id=
"MV", kind=
"valve", R=0.0, bore=0.0095, travel_s=0.05)
536 bare = S.Element(id=
"MV", kind=
"valve", R=0.0, travel_s=0.05)
539 return S.FeedLine(
"oxidiser",
"ox",
"T",
"oxygen", 1140.0, 2e-4, 900.0, 1e5, tuple(up), v, tuple(dn))
541 assert S.exit_area(
feed([], valve, [pipe])) == (pipe.area,
False)
542 assert S.exit_area(
feed([pipe], valve, [])) == (pipe.area,
False)
543 area, through = S.exit_area(
feed([], valve, []))
544 assert through
and area == pytest.approx(math.pi * 0.0095 ** 2 / 4.0)
545 with pytest.raises(ValueError, match=
"no line and no main-valve bore"):
546 S.exit_area(
feed([], bare, []))
static ArmAction feed(const Frame &f, uint32_t t)