1"""Ignoring the drawn GSE (``Setup.ignore_gse``, ``assemble_model(vehicle_only=)``).
3The twin misreads a complicated cart (the operator, 2026-10-08), and the burn
4only needs the rocket. With the option on, everything off the vehicle is cut
5before the build and the stand fills the way a drawing of the rocket alone
6does: the built-in charge, the built-in loads, the dome knob on the tank
7regulator itself. Each check is against what the drawn cart would have done
8instead, so it fails if the cart were still there.
11from __future__
import annotations
14from dataclasses
import replace
15from pathlib
import Path
28HERE = Path(__file__).resolve().parent
29DRAWING = HERE /
"fixtures" /
"le4_rocket_and_gse.json"
31STAND = STAR /
"feed-twin" /
"backend" /
"diagrams" /
"ethalox_stand.json"
32TABLES = STAR /
"feed-twin" /
"backend" /
"statemachines"
34needs_tables = pytest.mark.skipif(
not TABLES.is_dir(), reason=
"no state machine tables")
38 return json.loads(DRAWING.read_text())
41def _ids() -> dict[str, str]:
42 return {n[
"data"][
"label"]: n[
"id"]
for n
in _payload()[
"nodes"]}
45def _model(cut: bool, payload: Any |
None =
None) -> Any:
46 return assemble_model(
47 read_diagram(payload
or _payload(), name=
"LE4 (6)"),
56 hookup = suggest(whole, 500.0, 4500.0)
58 hookup = on_vehicle(hookup, model)
59 return open_session(model, load_machine(tables=TABLES), hookup=hookup)
63 """Everything off the vehicle goes but the cart's vent line, which stays
64 plugged into the rocket until launch; every other coupling is capped."""
66 diagram = read_diagram(
_payload(), name=
"LE4 (6)")
67 ground = ground_ids(diagram)
68 vents = vent_branches(diagram)
69 assert vents == {ids[x]
for x
in (
"FV-QD-A",
"FV-SOL",
"FV-MAN",
"junc_o5qqb5_95")}
70 cut, gone = vehicle_only(diagram)
71 kept = {n.id
for n
in cut.nodes}
72 assert ground
and ground & kept == vents
73 assert ground_ids(cut) == vents,
"the rocket and its vent line"
74 assert set(gone) == {
"LOX-DW-350 PSI",
"6K-GN2",
"2K-GN2",
"Fuel Transfer Tank"}
76 assert all(e.source
not in cart
and e.target
not in cart
for e
in cut.edges)
78 n.label: n.options.get(
"pairedWith",
"")
for n
in cut.nodes
if n.type ==
"QD"
80 assert mates[
"FV-QD-B"] == ids[
"FV-QD-A"],
"the vent stays coupled"
82 m
in cart
for m
in mates.values()
83 ),
"no other half paired to the cart"
84 assert ids[
"COPV"]
in kept
87 assert model.meta[
"vehicle_only"]
is True
88 assert set(model.meta[
"ground_cut"]) == set(gone)
90 assert model.built.mated == ((ids[
"FV-QD-B"], ids[
"FV-QD-A"]),)
91 assert not model.built.supplies
95 """On, against a drawing with nothing off the vehicle: the same object."""
96 diagram = read_diagram(
_payload(), name=
"LE4 (6)")
97 cut, _ = vehicle_only(diagram)
98 again, gone = vehicle_only(cut)
99 assert again
is cut
and gone == ()
103 not (STAND.exists()
and TABLES.is_dir()), reason=
"stand drawing or tables absent"
106 """The shipped stand has no GSE drawn: built with the option on it runs the
107 same pad, number for number, as with it off."""
109 def run(cut: bool) -> Any:
110 model = assemble_model(
111 read_diagram(json.loads(STAND.read_text()), name=
"s"),
115 session = open_session(
116 model, load_machine(tables=TABLES), setup=
Setup(ignore_gse=cut)
118 for state, steps
in ((
"GN2 High Press", 10), (
"Fuel Fill", 10), (
"Ready", 10)):
119 session.state = state
120 for _
in range(steps):
124 on, off =
run(
True),
run(
False)
125 assert on.setup.ignore_gse
and not off.setup.ignore_gse
126 assert on.model.diagram.nodes == off.model.diagram.nodes
127 for a, b
in zip(on.history, off.history, strict=
True):
128 assert a.pressures == b.pressures
129 assert a.signals == b.signals
134 """GN2 High Press charges the COPV to the COPV fill setting by the built-in
135 charge, Fuel Fill pours the built-in load, and the dome knob sets DPR_HP's
136 dome itself: the tanks lock up at it with no GN2 Low Press. With the cart
137 simulated, the COPV fills through PR-1, the fuel through the transfer tank,
138 and the dome stays unloaded until GN2 Low Press -- the tanks hold the spring
139 bias less the supply effect, which on a charged COPV is shut."""
141 for cut
in (
True,
False):
143 session.setup = replace(session.setup, copv_target_psi=4000.0, dome_psi=450.0)
144 copv = session.bottles[ids[
"COPV"]]
145 fuel = session.tanks[ids[
"Eth-Tank"]]
146 assert session.setup.ignore_gse
is cut
148 session.state =
"GN2 High Press"
151 assert psig(copv.pressure) == pytest.approx(4000.0, abs=40.0)
152 assert (ids[
"COPV"]
in session._drawn_fill)
is not cut
154 session.state =
"Fuel Fill"
157 assert fuel.state.liquid_mass == pytest.approx(fuel._wanted(), rel=0.02)
158 assert (ids[
"Eth-Tank"]
in session._drawn_fill)
is not cut
160 session.state =
"Fuel Press"
164 assert psig(fuel.pressure) < 5.0,
"the drawn dome is not loaded yet"
166 lockup = 450.0 + 50.0 - 14.7e-3 * psig(copv.pressure)
167 assert psig(fuel.pressure) == pytest.approx(lockup, abs=8.0)
168 dome_pt = next(i
for i
in session.model.built.instruments
if i.tag ==
"DP-PT-R")
169 assert psig(session.history[-1].pressures[dome_pt.node]) == pytest.approx(450.0)
170 assert any(
"drawn GSE is ignored" in a
for a
in session.assumptions)
175 """The built-in charge and the network's draw land in one step: dm/dt =
176 charge - draw. The charge branch returned before the draw was applied, so a
177 press valve opened during GN2 High Press filled the LOX ullage from gas the
178 COPV never lost -- 290 g of nitrogen from nothing on LE4 (3), booked by no
179 vessel and opened by the watcher as "Mass not accounted for"."""
182 copv = session.bottles[ids[
"COPV"]]
183 lox = session.tanks[ids[
"LOX-Tank"]]
184 session.state =
"GN2 High Press"
188 session.set_valve(session.binding.to_symbol[
"LOX Press"],
True)
189 before = lox.state.ullage.mass
192 assert copv.filling,
"still charging: the draw happened during the charge"
193 assert lox.state.ullage.mass - before > 0.2,
"the press valve drew gas"
194 last = session.solver_log[-1]
195 assert abs(last.mass_error_kg - last.guard_kg) < 1e-6
200 """GN2 High Press from empty: the COPV climbs, and the console said it was
201 "down to 107 psia, 3% of what it was filled to" on every tick of the first
202 quarter of the charge. A bottle is down only from what it has held."""
205 session.setup = replace(session.setup, copv_target_psi=4000.0)
206 copv = session.bottles[ids[
"COPV"]]
207 session.state =
"GN2 High Press"
209 notes = session.step(0.25).notes
210 assert not [n
for n
in notes
if "down to" in n], notes
211 assert copv.charged
and psig(copv.pressure) < 0.25 * 4000.0 + 200.0
214 assert psig(copv.pressure) == pytest.approx(4000.0, abs=40.0)
216 session.state =
"Press Standby"
217 copv.state = copv.volume.initial_state(
218 pressure=from_psig(400.0), temperature=copv.state.wall_temperature
220 (low,) = [n
for n
in session.step(0.02).notes
if "down to" in n]
221 assert low.startswith(
"COPV is down to 415 psia, 10% of the 4,01"), low
225 copv.state = copv.volume.initial_state(
226 pressure=from_psig(0.0), temperature=copv.state.wall_temperature
228 session.state =
"GN2 High Press"
231 assert 2.0 * 14.7 < psig(copv.pressure) < 0.25 * 4000.0
232 session.state =
"Press Standby"
234 notes = session.step(0.25).notes
235 assert not [n
for n
in notes
if "down to" in n], notes
240 """Emergency Abort opens the built-in COPV dump. On a bottle already at the
241 air it read 14.4 psia: the dump kept taking a ninth of the gas a second, the
242 floor put it back at the wall's density with none of its energy, and every
243 step the gas cooled and the bottle sank under atmosphere. A bottle open to
244 the air reads the air, and nothing flows out of it."""
247 copv = session.bottles[ids[
"COPV"]]
248 session.state =
"Emergency Abort"
250 assert copv.venting,
"the abort row opens the built-in dump"
251 held = copv.state.mass
254 assert psig(copv.pressure) == pytest.approx(0.0, abs=0.02)
255 assert copv.volume.temperature(copv.state) == pytest.approx(293.0, abs=1.0)
256 assert copv.state.mass == pytest.approx(held, rel=1e-3)
261 """The floor itself, apart from the dump: a bottle at atmosphere that a
262 step draws 5 % of its gas from is put back at an atmosphere, mass and
263 energy together. Floored at the wall's density with the energy left as it
264 was, it kept the mass and lost the heat that left with it, and read a psi
265 under the air after one step (the dump test above passes on the old floor
266 once the dump stops at the air: this is the floor's own check)."""
268 copv =
_session(
True).bottles[ids[
"COPV"]]
269 assert psig(copv.pressure) == pytest.approx(0.0, abs=1e-3),
"starts at the air"
270 held = copv.state.mass
272 copv.advance(0.02, mdot_out=0.05 * held / 0.02)
273 assert psig(copv.pressure) == pytest.approx(0.0, abs=1e-3)
275 assert copv.fixed_kg == pytest.approx(copv.state.mass - held + 0.15 * held)
280 """T-0 sets the bottle; it does not reach it. A bottle charged to 4,000
281 psig and then primed at 400 psig (a Study case, a second T-0 at a lower
282 charge) kept the 4,000 as the most it had held, and the console read
283 "down to 415 psia, 10% of the 4,015 psia it was filled to" of gas nobody had
287 copv = session.bottles[ids[
"COPV"]]
288 session.prime(fill_fraction=0.5, tank_psi=500.0, copv_psi=4000.0, state=
"Ready")
290 assert copv.peak == pytest.approx(from_psig(4000.0), rel=1e-5)
291 session.prime(fill_fraction=0.5, tank_psi=300.0, copv_psi=400.0, state=
"Ready")
292 assert copv.pressure < 0.25 * copv.target,
"low enough for the note"
293 notes = session.step(0.02).notes
294 assert not [n
for n
in notes
if "down to" in n], notes
298 copv.state = copv.volume.initial_state(
299 pressure=from_psig(399.0), temperature=copv.state.wall_temperature
301 assert copv.pressure < copv.peak
302 notes = session.step(0.02).notes
303 assert not [n
for n
in notes
if "down to" in n], notes
308 """The built-in load raised the liquid and shrank the ullage with no p dV
309 work on the gas; the vent then took the displaced gas at its enthalpy, and
310 what stayed behind lost p v per kilogram vented: the fuel ullage cooled
311 293 -> 285 K during a 15 s pour onto a 293 K tank at atmosphere. By hand,
312 an adiabatic displacement at constant pressure leaves the gas where it
313 was -- the liquid's work on the gas pays the flow work leaving."""
316 fuel = session.tanks[ids[
"Eth-Tank"]]
317 session.state =
"Fuel Fill"
321 coldest =
min(coldest, fuel.tank.gas_temperature(fuel.state))
322 assert fuel.state.liquid_mass > 3.0,
"poured"
323 assert psig(fuel.pressure) == pytest.approx(0.0, abs=0.1),
"on its vent"
324 assert coldest > 292.0, f
"the displaced ullage cooled to {coldest:.2f} K"
329 """A LOX load on its vent, chilled: the nitrogen ullage sits between the
330 90 K liquid and the 92 K wall, gas leaves through the vent, and no guard
331 books anything. Each time the tank dipped a hundredth of a psi under the
332 air, the floor restated the ullage's energy at a whole atmosphere of
333 nitrogen while it shared the tank with oxygen vapour -- colder, by
334 nitrogen's departure from ideal near its dew line -- and the ratchet took
335 the ullage to 77 K, drew ~1 g/s of "air" in through a vent blowing out, and
336 booked hundreds of grams as clamps: the Solver tab failed three of five
337 checks after every pad run (LE4 (3): 77.4 K, 0.70 kg drawn, -0.60 kg)."""
340 lox = session.tanks[ids[
"LOX-Tank"]]
341 session.state =
"Ox Fill"
343 assert session.skip_chilldown() == [
"LOX-Tank"]
344 coldest = float(
"inf")
349 state.ullage.wall_temperature,
350 state.surface_temperature
or state.liquid_temperature,
352 coldest =
min(coldest, lox.tank.gas_temperature(state) - surfaces)
353 assert lox.state.liquid_mass > 0.5,
"loading"
354 assert coldest > -0.5, f
"the ullage went {-coldest:.1f} K under its surfaces"
355 assert abs(lox.fixed_kg) < 1e-4, lox.fixed_kg
356 assert lox.vent_drawn_kg < 1e-3,
"air drawn in through a vent blowing out"
357 last = session.solver_log[-1]
358 assert abs(last.guard_kg) < 1e-3
363 bound =
_session(
True).binding.to_symbol
364 assert "GSE High Press Control" not in bound,
"the built-in charge's command"
365 assert "GSE Med Press Control" not in bound
366 assert {
"Fuel Press",
"LOX Press",
"Fuel Main",
"LOX Main"} <= set(bound)
374 "Fuel Main": ids[
"FM-R"],
375 "GSE High Press Control": ids[
"HPC_SOL"],
378 knobs=suggest(whole, 500.0, 4500.0).knobs,
380 cut = on_vehicle(saved, model)
381 assert cut.valves == {
"Fuel Main": ids[
"FM-R"],
"LOX Dump":
""}
382 assert [(k.id, k.regulators)
for k
in cut.knobs] == [(DOME, (ids[
"DPR_HP"],))]
387 machine = load_machine(tables=TABLES)
388 whole = open_session(
_model(
False), machine, setup=
Setup(ignore_gse=
True))
389 assert whole.setup.ignore_gse
is False and not whole.gse_ignored
390 cut = open_session(
_model(
True), machine, setup=
Setup())
391 assert cut.setup.ignore_gse
is True and cut.gse_ignored
396 """The cart's vents are a small part of the GSE that stays plugged into the
397 rocket until the last moment before launch (the team, 2026-10-10). Rocket
398 only, the fuel tank's vent coupling stays mated to the cart's vent line,
399 Fuel Vent drives the cart's FV-SOL -- not a stand-in on the capped
400 coupling -- and venting through it empties the pressed tank."""
403 assert ids[
"FV-QD-B"]
not in model.meta[
"capped"]
405 assert session.binding.to_symbol[
"Fuel Vent"] == ids[
"FV-SOL"]
406 fuel = session.tanks[ids[
"Eth-Tank"]]
407 for state, steps
in ((
"GN2 High Press", 30), (
"Fuel Fill", 40), (
"Fuel Press", 20)):
408 session.state = state
409 for _
in range(steps):
411 pressed = psig(fuel.pressure)
412 assert pressed > 400.0
413 session.state =
"Fuel Vent"
416 assert psig(fuel.pressure) < 0.1 * pressed,
"it vents through the cart's line"
420 """LE4 (6) draws the LOX vent's halves -- the rocket's QD-OV-B and the
421 cart's OV-QD-A in front of OV-MOT -- paired with nothing. The twin does
422 not guess the pair; it says so. Paired, the warning goes and the LOX vent
423 line stays plugged into the rocket only like the fuel's."""
426 warned = unpaired_vents(read_diagram(payload, name=
"LE4 (6)"))
427 assert len(warned) == 1
and "QD-OV-B" in warned[0]
and "OV-MOT" in warned[0]
428 assert warned[0]
in _model(
False).report.warnings
429 for node
in payload[
"nodes"]:
430 label = node[
"data"][
"label"]
431 if label
in (
"QD-OV-B",
"OV-QD-A"):
432 other =
"OV-QD-A" if label ==
"QD-OV-B" else "QD-OV-B"
433 node[
"data"].setdefault(
"options", {})[
"pairedWith"] = ids[other]
434 paired = read_diagram(payload, name=
"LE4 (6) paired")
435 assert unpaired_vents(paired) == []
436 assert {ids[
"OV-QD-A"], ids[
"OV-MOT"]} <= vent_branches(paired)
437 cut =
_model(
True, payload)
438 assert ids[
"QD-OV-B"]
not in cut.meta[
"capped"]
442 """``gas_swap`` replaces the pressurant wherever it is declared: the
443 network's bottles, press lines and the ullages they fill hold helium; the
444 tanks keep their propellants. Without it, the drawing's nitrogen."""
448 diagram = read_diagram(
_payload(), name=
"LE4 (6)")
449 swapped = swap_gases(diagram, {
"nitrogen":
"helium"})
450 by_id = {n.id: n
for n
in swapped.nodes}
451 assert by_id[ids[
"COPV"]].fluid ==
"helium"
452 assert by_id[ids[
"Eth-Tank"]].fluid == next(
453 n.fluid
for n
in diagram.nodes
if n.id == ids[
"Eth-Tank"]
455 assert not any(n.fluid ==
"nitrogen" for n
in swapped.nodes)
456 for gas
in (
"nitrogen",
"helium"):
457 model = assemble_model(
458 read_diagram(
_payload(), name=
"LE4 (6)"),
460 gas_swap=
None if gas ==
"nitrogen" else {
"nitrogen": gas},
462 nodes = model.built.network.nodes
463 assert nodes[ids[
"COPV"]].fluid == gas
466 for tank
in (
"Eth-Tank",
"LOX-Tank"):
467 assert nodes[ids[tank]].fluid == gas
468 assert nodes[f
"{ids[tank]}.out"].fluid
in (
"ethanol",
"oxygen")
472 """The shipped table with a "Dome Control" row open only in a "Dome Press"
473 state, reached from Press Standby -- a stand's own table (the operator,
475 table = load_machine(tables=TABLES).to_dict()
476 table[
"states"].append({
"name":
"Dome Press",
"row": 2,
"col": 5,
"abort":
False})
477 table[
"actuators"].append(
"Dome Control")
478 table[
"open"][
"Dome Press"] = [
"Dome Control"]
479 standby = table[
"allowed"][
"Press Standby"]
480 standby.append(
"Dome Press")
481 aborts = [s[
"name"]
for s
in table[
"states"]
if s[
"abort"]]
482 table[
"allowed"][
"Dome Press"] = [
"Press Standby",
"Dome Press"] + [
483 a
for a
in aborts
if a
in standby
485 return machine_from_dict(table)
489 """Rocket only, DR-CTRL-R sits on DPR_HP's dome line between the dome port
490 and the capped DR-QD-B. The loader went with the cart, so the dome line was
491 not found, and the solenoid was reported as a vent to atmosphere with its
492 dome side -- where DR-PT-R reads -- pinned there. The cart's vent line
496 assert not [w
for w
in cut.warnings
if "venting" in w
and "DR-CTRL-R" in w]
497 branch = cut.network.branches[ids[
"DR-CTRL-R"]]
498 for end
in (branch.upstream, branch.downstream):
499 assert cut.network.nodes[end].pressure
is None, end
500 assert [w
for w
in cut.warnings
if "venting" in w
and "FV-SOL" in w]
504 """The dome the rocket's regulator reads now [psig]."""
505 signal = session.model.built.actuators[
_ids()[
"DPR_HP"]]
506 return psig(session.peek_signals()[signal])
511 """Rocket only, the dome comes in through the capped DR-QD-B and the
512 rocket's DR-CTRL-R gates it -- but no row of the shipped table drives
513 DR-CTRL-R, so it passes and the stand runs bit for bit as it did with no
516 assert set(gated._cut_dome_lines) == {
_ids()[
"DPR_HP"]}
518 plain._cut_dome_lines = {}
519 for session
in (gated, plain):
520 for state, steps
in ((
"Idle", 4), (
"GN2 High Press", 20), (
"Fuel Press", 20)):
521 session.state = state
522 for _
in range(steps):
524 fuel =
_ids()[
"Eth-Tank"]
525 assert _dome(gated) == pytest.approx(500.0)
526 assert gated.tanks[fuel].pressure == plain.tanks[fuel].pressure
527 assert gated.tanks[fuel].pressure > from_psig(400.0)
532 """With DR-CTRL-R on a "Dome Control" row open only in "Dome Press", the
533 rocket's dome sits at atmosphere until Dome Press opens the line to the
534 cart's dome supply (the knob), and holds what it took once the line shuts:
535 a tank pressed before it presses nothing, one pressed after reaches
541 suggest(whole, 500.0, 4500.0), valves={
"Dome Control": ids[
"DR-CTRL-R"]}
543 session = open_session(
546 assert session.binding.to_symbol[
"Dome Control"] == ids[
"DR-CTRL-R"]
547 fuel = session.tanks[ids[
"Eth-Tank"]]
548 for state, steps
in ((
"Idle", 4), (
"GN2 High Press", 20), (
"Fuel Press", 20)):
549 session.state = state
550 for _
in range(steps):
552 assert _dome(session) == pytest.approx(0.0, abs=1e-9)
553 assert psig(fuel.pressure) < 100.0,
"an unloaded regulator is shut"
554 session.state =
"Dome Press"
556 assert _dome(session) == pytest.approx(500.0)
557 session.state =
"Press Standby"
559 assert _dome(session) == pytest.approx(500.0),
"shut in, it holds"
560 session.state =
"Fuel Press"
563 assert psig(fuel.pressure) > 400.0
Any _model(bool cut, Any|None payload=None)
None test_a_vented_lox_load_keeps_its_ullage_over_the_liquid()
None test_a_bottle_at_the_air_drawn_on_reads_the_air()
None test_a_primed_bottle_is_down_from_its_prime()
None test_on_a_one_page_drawing_it_changes_nothing()
Any _dome_press_machine()
None test_a_dome_press_state_loads_the_dome_and_it_holds()
None test_a_bottle_charging_is_not_down_to_anything()
None test_a_poured_load_displaces_the_ullage_without_cooling_it()
None test_an_undriven_dome_valve_leaves_the_rocket_only_dome_as_it_was()
None test_a_bottle_dumped_at_atmosphere_stays_at_atmosphere()
None test_the_cart_is_cut_and_its_disconnects_capped()
None test_the_cart_actuators_drive_nothing()
None test_a_gas_swap_puts_helium_in_the_bottles_lines_and_ullages()
None test_the_cart_vent_stays_plugged_in_and_vents_the_rocket()
None test_a_drawing_of_the_rocket_alone_is_not_touched()
None test_the_rocket_fills_at_the_settings_not_through_the_cart()
None test_the_rockets_dome_valve_is_no_vent_with_the_cart_cut()
None test_the_setup_says_what_the_stand_was_built_from()
None test_a_bottle_being_charged_gives_what_the_network_draws()
None test_a_vent_drawn_unpaired_is_said_and_once_paired_it_stays()
None test_the_hookup_keeps_the_vehicle_and_drops_the_cart()