STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_rocket_and_gse.py
Go to the documentation of this file.
1"""LE4 with its ground support drawn: a Rocket page and a GSE page, joined at
2paired disconnects (``fixtures/le4_rocket_and_gse.json``, the team's drawing of
32026-10-07).
4
5Read page-blind, the cart was more vehicle: its fuel transfer tank took the
6tanker load alongside the flight tank, its K-bottles started empty, its hand
7vents were open for good, its dome-control line was plumbed into the press
8manifold, and only the dome regulator had a knob. Each check here is one of
9those, against what the stand does.
10"""
11
12from __future__ import annotations
13
14import json
15from dataclasses import replace
16from pathlib import Path
17from typing import Any
18
19import pytest
20
21from feedtwin.pid import build_network, read_diagram
22
23HERE = Path(__file__).resolve().parent
24DRAWING = HERE / "fixtures" / "le4_rocket_and_gse.json"
25STAR = HERE.parents[2]
26ENGINE = STAR / "EngineDesign" / "configs" / "ethalox_6800N.yaml"
27CEA = STAR / "EngineDesign" / "output" / "cache" / "cea_cache_LOX_Ethanol_3D.npz"
28TABLES = STAR / "feed-twin" / "backend" / "statemachines"
29
30
31def _payload() -> Any:
32 return json.loads(DRAWING.read_text())
33
34
35def _ids() -> dict[str, str]:
36 return {n["data"]["label"]: n["id"] for n in _payload()["nodes"]}
37
38
39@pytest.fixture(scope="module")
40def built() -> Any:
41 return build_network(read_diagram(_payload(), name="LE4 (6)"))
42
43
45 ids = _ids()
46 assert built.vehicle is not None
47 for label in ("COPV", "Eth-Tank", "LOX-Tank", "DPR_HP", "QD-FFB"):
48 assert ids[label] in built.vehicle, label
49 for label in ("Fuel Transfer Tank", "6K-GN2", "PR-1", "QD-FFA", "HPC_SOL"):
50 assert ids[label] not in built.vehicle, label
51
52
54 ids = _ids()
55 assert built.supplies == {ids["Fuel Transfer Tank"]: frozenset({ids["Eth-Tank"]})}
56
57
58def test_every_hand_valve_rests_shut(built: Any) -> None:
59 """Vents, bleeds and inline isolation alike: shut until a hand opens it."""
60 ids = _ids()
61 for valve in ("FV-MAN", "FF-MAN-Vent", "DR-MV", "HP-Down-MAN", "FF-MAN-Output"):
62 assert ids[valve] in built.hand_valves
63 assert built.rest[ids[valve]] == 0.0, valve
64
65
67 payload = _payload()
68 for node in payload["nodes"]:
69 if node["data"]["label"] == "FF-MAN-Output":
70 node["data"].setdefault("options", {})["normalPosition"] = "open"
71 built = build_network(read_diagram(payload, name="LE4 (6)"))
72 assert built.rest[_ids()["FF-MAN-Output"]] == 1.0
73
74
76 ids = _ids()
77 mov = ids["OF-MOT-Dump"]
78 assert mov in built.network.branches
79 assert built.actuators[mov] == "OF-MOT-Dump.command"
80 assert mov not in built.hand_valves
81
82
84 """DR-REG-G reaches DPR_HP's dome through DR-CTRL-G, the mated DR QDs and
85 DR-CTRL-R. It is the loader, and nothing joins the dome line to the
86 regulator's body."""
87 ids = _ids()
88 assert ids["DR-REG-G"] in built.dome_loaders
89 assert built.dome_loaders[ids["DR-REG-G"]].signal == "DPR_HP.dome"
90 body = {f"{ids['DPR_HP']}.in", f"{ids['DPR_HP']}.out"}
91 dome_side = built.node_of.get(ids["DR-CTRL-R"])
92 for branch in built.network.branches.values():
93 if branch.id == ids["DPR_HP"]:
94 continue
95 ends = {branch.upstream, branch.downstream}
96 assert not (ends & body and dome_side in ends), branch.id
97
98
99def _model() -> Any:
100 from feedtwin.session import assemble_model
101
102 return assemble_model(read_diagram(_payload(), name="LE4 (6)"), diagram_id="le4")
103
104
106 from feedtwin.session.hookup import CHARGE, DOME, suggest
107
108 ids = _ids()
109 knobs = {k.id: k for k in suggest(_model(), 500.0, 4500.0).knobs}
110 assert knobs[DOME].regulators == (ids["DR-REG-G"],)
111 assert knobs[CHARGE].regulators == (ids["PR-1"],)
112 assert knobs[CHARGE].label == "COPV fill (PR-1)"
113 # DR-REG-G and PR-1 are blank; the dome they load and the bottle they fill
114 # are not (test_blank_loader_and_fill_start_at_what_they_feed).
115 assert knobs[DOME].psig == pytest.approx(535.0)
116 assert knobs[CHARGE].psig == pytest.approx(4000.0)
117 assert knobs[ids["LP-PR"]].regulators == (ids["LP-PR"],)
118
119
121 """LE4 (6) draws DPR_HP's dome at 535 and the COPV at 4000, and leaves its
122 dome loader (DR-REG-G) and COPV fill (PR-1) blank. The knobs start at those:
123 a loader's job is the dome it loads, a fill's the charge it fills to. They
124 started at the session's 500 and 4,500, so the cockpit primed 35 psi under
125 Layer X on the same drawing and read its COPV amber at its own charge.
126 Blank all the way down, they still start at the session's numbers."""
127 from feedtwin.session import assemble_model
128 from feedtwin.session.hookup import CHARGE, DOME, knob_starts, regulators, suggest
129
130 ids = _ids()
131 model = _model()
132 hookup = suggest(model, 500.0, 4500.0)
133 assert knob_starts(hookup, model) == {
134 DOME: pytest.approx(535.0),
135 CHARGE: pytest.approx(4000.0),
136 }
137 loader = next(r for r in regulators(model) if r.id == ids["DR-REG-G"])
138 assert loader.drawn_psig == pytest.approx(535.0)
139
140 payload = _payload()
141 for node in payload["nodes"]:
142 params = node["data"].get("params") or {}
143 if node["data"].get("label") == "DPR_HP":
144 params.pop("dome_pressure")
145 if node["data"].get("label") == "COPV":
146 params.pop("pressure")
147 blank = assemble_model(read_diagram(payload, name="LE4"), diagram_id="le4")
148 knobs = {k.id: k for k in suggest(blank, 500.0, 4500.0).knobs}
149 assert knobs[DOME].psig == 500.0
150 assert knobs[CHARGE].psig == 4500.0
151 assert knob_starts(suggest(blank, 500.0, 4500.0), blank) == {}
152
153
154@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
156 from feedtwin.session import load_machine
157 from feedtwin.session.hookup import binding
158
159 ids = _ids()
160 bound = binding(_model(), load_machine(tables=TABLES), None).to_symbol
161 assert bound["Fuel Vent"] == ids["FV-SOL"]
162 assert bound["Fuel Fill Vent"] == ids["FF-SOL-Vent"]
163 assert bound["GSE High Press Control"] == ids["HPC_SOL"]
164 assert bound["LOX Dump"] == ids["OF-MOT-Dump"]
165 # The OV disconnects are not paired on this drawing, so the cart's LOX vent
166 # goes nowhere; the tank-top half is still the vent that works.
167 assert bound["LOX Vent"] == ids["QD-OV-B"]
168 assert ids["FF-MAN-Output"] not in bound.values(), "hand valves are the crew's"
169
170
171@pytest.mark.skipif(
172 not (ENGINE.exists() and CEA.exists() and TABLES.is_dir()),
173 reason="engine, CEA table or state machine tables absent",
174)
176 """GN2 High Press charges the COPV through PR-1 at the COPV fill knob; Fuel
177 Fill presses the transfer tank to its drawn 150 psig and pushes fuel across
178 the QD pair until the flight tank holds its load. Nothing moves before, no
179 propellant appears from nowhere, and the stand keeps its mass."""
180 import yaml
181
182 from feedtwin.engine.importer import engine_from_config
183 from feedtwin.session import assemble_model, load_machine
184 from feedtwin.session.burn import open_session
185 from feedtwin.session.gauge import psig
186 from feedtwin.session.hookup import suggest
187
188 ids = _ids()
189 design = engine_from_config(yaml.safe_load(ENGINE.read_text()), name="6800N")
190 model = assemble_model(
191 read_diagram(_payload(), name="LE4 (6)"),
192 diagram_id="le4",
193 engine=design,
194 cea_cache=str(CEA),
195 )
196 session = open_session(
197 model, load_machine(tables=TABLES), hookup=suggest(model, 500.0, 4000.0)
198 )
199 session.setup = replace(session.setup, copv_target_psi=4000.0)
200 fuel, cart = (
201 session.tanks[ids["Eth-Tank"]],
202 session.tanks[ids["Fuel Transfer Tank"]],
203 )
204 copv = session.bottles[ids["COPV"]]
205 start_cart = cart.state.liquid_mass
206 assert start_cart > 10.0 and fuel.state.liquid_mass == 0.0
207
208 session.state = "GN2 High Press"
209 for _ in range(40):
210 session.step(0.5)
211 assert psig(copv.pressure) == pytest.approx(4000.0, abs=40.0)
212 assert fuel.state.liquid_mass == 0.0, "nothing moves before Fuel Fill"
213
214 session.state = "Fuel Fill"
215 for _ in range(80):
216 session.step(0.5)
217 load = fuel.state.liquid_mass
218 assert load == pytest.approx(fuel._wanted(), rel=0.02)
219 # Less a little ethanol that evaporates into the vented ullage; the strict
220 # check is the balance below.
221 assert start_cart - cart.state.liquid_mass == pytest.approx(load, abs=5e-3)
222 assert psig(cart.pressure) == pytest.approx(150.0, abs=2.0)
223 last = session.solver_log[-1]
224 assert abs(last.mass_error_kg - last.guard_kg) < 1e-3
225
226
227# ----------------------------------------------------------------- the dome line
228
229
230def _session(payload: Any | None = None) -> Any:
231 from feedtwin.session import assemble_model, load_machine
232 from feedtwin.session.burn import open_session
233 from feedtwin.session.hookup import suggest
234
235 model = assemble_model(
236 read_diagram(payload or _payload(), name="LE4 (6)"), diagram_id="le4"
237 )
238 return open_session(
239 model, load_machine(tables=TABLES), hookup=suggest(model, 500.0, 4500.0)
240 )
241
242
244 ids = _ids()
245 line = built.dome_lines[ids["DPR_HP"]]
246 assert line.loader == ids["DR-REG-G"]
247 assert line.valves == frozenset(
248 ids[v] for v in ("DR-CTRL-G", "DR-CTRL-R", "DR-MV", "DR-Vent")
249 )
250 assert line.vents == frozenset({ids["DR-MV"], ids["DR-Vent"]})
251 assert built.valve_roles[ids["DR-CTRL-G"]] == frozenset(
252 {"gse", "med", "press", "control"}
253 )
254
255
256@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
258 """Unloaded, the 1092 holds its 50 psi spring bias less its supply effect --
259 with the COPV charged, less than nothing, so the tank does not press. GN2 Low Press
260 opens DR-CTRL-G ("GSE Med Press Control") and the dome follows DR-REG-G; the
261 next state shuts it and the dome keeps what is in it, as the stand keeps it
262 through the burn with the GSE disconnected. The dome PT reads it."""
263 from feedtwin.session.gauge import psig
264
265 ids = _ids()
266 session = _session()
267 assert session.binding.to_symbol["GSE Med Press Control"] == ids["DR-CTRL-G"]
268 session.state = "GN2 High Press"
269 for _ in range(40):
270 session.step(0.5)
271 session.state = "Fuel Press"
272 for _ in range(20):
273 session.step(0.5)
274 fuel = session.tanks[ids["Eth-Tank"]]
275 copv = session.bottles[ids["COPV"]]
276 # dome + bias - S x inlet, S 14.7 psi per 1000 on the drawing (zero inlet).
277 supply = 14.7e-3 * psig(copv.pressure)
278 assert supply > 50.0, "a charged COPV outweighs the spring bias"
279 assert psig(fuel.pressure) == pytest.approx(0.0, abs=2.0), "unloaded: shut"
280
281 session.state = "GN2 Low Press"
282 for _ in range(4):
283 session.step(0.5)
284 session.state = "Fuel Press"
285 for _ in range(30):
286 session.step(0.5)
287 lockup = 500.0 + 50.0 - 14.7e-3 * psig(copv.pressure)
288 assert psig(fuel.pressure) == pytest.approx(
289 lockup, abs=8.0
290 ), "dome + bias - S x COPV"
291 dome_pt = next(i for i in session.model.built.instruments if i.id == ids["DP-PT-R"])
292 assert psig(session.history[-1].pressures[dome_pt.node]) == pytest.approx(500.0)
293 assert ids["DR-CTRL-G"] not in session.forced
294 assert psig(session._dome_held[ids["DPR_HP"]]) == pytest.approx(500.0)
295
296
297# ------------------------------------------------------- the dewar is a supply
298
299
301 """LE4 (6) with its LOX fill line finished, as ADR 0006 asks of the drawing:
302 the fill valve's junction to the fill manifold, the manifold to OF-QDA, and
303 the LOX vent disconnects paired."""
304 payload = _payload()
305
306 def line(i: str, s: str, sh: str, t: str, th: str) -> dict[str, Any]:
307 return {
308 "id": i,
309 "source": s,
310 "sourceHandle": sh,
311 "target": t,
312 "targetHandle": th,
313 "type": "smoothstep",
314 "data": {},
315 }
316
317 ids = _ids()
318 payload["edges"] += [
319 line("fix-1", "junc_85", "r", "junc_86", "r"),
320 line("fix-2", ids["OF-Manifold"], "in", ids["OF-QDA"], "r"),
321 ]
322 for node in payload["nodes"]:
323 if node["data"]["label"] == "OV-QD-A":
324 node["data"].setdefault("options", {})["pairedWith"] = ids["QD-OV-B"]
325 return payload
326
327
329 built = build_network(read_diagram(_with_lox_fill_drawn(), name="LE4 (6)"))
330 ids = _ids()
331 assert ids["LOX-DW-350 PSI"] in built.tanks
332 assert built.supplies[ids["LOX-DW-350 PSI"]] == frozenset({ids["LOX-Tank"]})
333
334
335@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
337 from feedtwin.session.gauge import psig
338
339 ids = _ids()
340 session = _session(_with_lox_fill_drawn())
341 dewar, lox = session.tanks[ids["LOX-DW-350 PSI"]], session.tanks[ids["LOX-Tank"]]
342 assert psig(dewar.pressure) == pytest.approx(50.0, abs=0.5)
343 start = dewar.state.liquid_mass
344 session.state = "Ox Fill"
345 for _ in range(40):
346 session.step(0.5)
347 assert lox.state.liquid_mass == pytest.approx(lox._wanted(), rel=0.03)
348 assert start - dewar.state.liquid_mass >= lox.state.liquid_mass
349 session.state = "Armed"
350 for _ in range(40):
351 session.step(0.5)
352 assert psig(dewar.pressure) == pytest.approx(50.0, abs=5.0), "its own circuit"
353 last = session.solver_log[-1]
354 assert abs(last.mass_error_kg - last.guard_kg) < 1e-3
355
356
357@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
359 """Topped to its load in Ox Fill, the LOX tank passes ~20 g/s of boil-off
360 through a 0.45 g ullage to its vent. The solve closed that ullage on the
361 stiffer of its gas-in and gas-out slopes, and over boiling LOX gas in is
362 eight times stiffer: the node the vent flowed from sat at 3.1 psig while
363 the tank read 7.2, a gap that grew with the coupling step. And the 10 %
364 mass rule counted the vented gas, so the whole stand was solved nine
365 times every 20 ms -- the fill ran at a third of real time. A venting
366 ullage is closed on its own gas-out slope, and gas gone to the sky asks
367 for no extra solves."""
368 from feedtwin.session.gauge import psig
369
370 ids = _ids()
371 session = _session(_with_lox_fill_drawn())
372 lox = session.tanks[ids["LOX-Tank"]]
373 session.state = "Ox Fill"
374 for _ in range(36):
375 session.step(0.25)
376 assert lox.state.liquid_mass == pytest.approx(lox._wanted(), rel=0.01)
377 session.step(0.02)
378 gas_in, gas_out = session._split_at(lox.ullage_node, session._last_flows)
379 assert gas_out > 20.0 * lox.state.ullage.mass, "the ullage turns over each 50 ms"
380 node = session._last_pressures[lox.ullage_node]
381 assert psig(lox.pressure) == pytest.approx(psig(node), abs=0.3)
382 assert session.solver_log[-1].couplings == 1
383
384
385# ------------------------------------------------------------ the cart rests
386
387
388def _fired(rests: bool) -> Any:
389 """LE4 (6) at T-0 with the engine on, then 0.3 s into Fire."""
390 import yaml
391
392 from feedtwin.engine.importer import engine_from_config
393 from feedtwin.session import assemble_model, load_machine
394 from feedtwin.session.burn import jump_to_t0, open_session
395 from feedtwin.session.hookup import suggest
396
397 design = engine_from_config(yaml.safe_load(ENGINE.read_text()), name="6800N")
398 model = assemble_model(
399 read_diagram(_payload(), name="LE4 (6)"),
400 diagram_id="le4",
401 engine=design,
402 cea_cache=str(CEA),
403 )
404 session = open_session(
405 model, load_machine(tables=TABLES), hookup=suggest(model, 500.0, 4500.0)
406 )
407 session.setup = replace(session.setup, ground_rests=rests)
408 jump_to_t0(session, copv_psi=4500.0, fill_fraction=0.95)
409 session.command_state("Fire")
410 most = 0.0
411 for _ in range(15): # the first 0.3 s, a study step at a time
412 session.step(0.02)
413 most = max(most, session.solver_log[-1].couplings)
414 return session, most
415
416
417@pytest.mark.skipif(
418 not (ENGINE.exists() and CEA.exists() and TABLES.is_dir()),
419 reason="engine, CEA table or state machine tables absent",
420)
422 """Fire on LE4 (6) with the cart drawn: every cart vessel is cut off from the
423 vehicle by shut valves, so none is integrated and its lines leave the solve,
424 and the burn is the one the whole stand integrated every step gives."""
425 ids = _ids()
426 rested, _ = _fired(True)
427 worked, _ = _fired(False)
428 cart = {
429 ids[label]
430 for label in ("Fuel Transfer Tank", "LOX-DW-350 PSI", "6K-GN2")
431 if ids[label] in rested.tanks or ids[label] in rested.bottles
432 }
433 branches, vessels = rested._resting
434 assert cart and cart <= vessels
435 assert branches, "the cart's lines are out of the solve"
436 assert not vessels & set(rested.vehicle_tanks)
437 held = {k: rested.tanks[k].state for k in vessels if k in rested.tanks}
438 rested.step(0.1)
439 for k, state in held.items():
440 assert rested.tanks[k].state is state, "a resting vessel is not integrated"
441 worked.step(0.1)
442 for a, b in zip(rested.history, worked.history):
443 assert a.chamber is not None and b.chamber is not None
444 assert a.chamber.thrust == pytest.approx(b.chamber.thrust, rel=1e-9, abs=1e-6)
445
446
447@pytest.mark.skipif(
448 not (ENGINE.exists() and CEA.exists() and TABLES.is_dir()),
449 reason="engine, CEA table or state machine tables absent",
450)
452 """The fuel tank's top QD hangs a wide fitting off its ullage that ends at a
453 cap. It carries nothing, so it is no press path: priced as one at the drain
454 flow, it asked the first steps of Fire for 0.07 ms couplings -- 64 in a 20 ms
455 step here, ~300 on the cockpit's first tick, most of a second before the
456 first frame of the burn."""
457 session, most = _fired(True)
458 assert session.history[-1].chamber.thrust > 5000.0
459 assert most < 20, f"{most} coupling steps in one 20 ms step"
460
461
462@pytest.mark.skipif(
463 not (ENGINE.exists() and CEA.exists() and TABLES.is_dir()),
464 reason="engine, CEA table or state machine tables absent",
465)
467 """The vehicle carries what the competition allows (the engine config's
468 lox_tank.mass and fuel_tank.mass), not 95 % of the tanks drawn: LE4 (6)
469 draws both at 8.19 L, a third more LOX than a fire is loaded with."""
470 import yaml
471
472 from feedtwin.engine.importer import engine_from_config
473 from feedtwin.session import assemble_model, load_machine
474 from feedtwin.session.burn import jump_to_t0, open_session
475 from feedtwin.session.hookup import suggest
476
477 config = yaml.safe_load(ENGINE.read_text())
478 design = engine_from_config(config, name="6800N")
479 assert design.fire_load == {
480 "lox": pytest.approx(config["lox_tank"]["mass"]),
481 "fuel": pytest.approx(config["fuel_tank"]["mass"]),
482 }
483 model = assemble_model(
484 read_diagram(_payload(), name="LE4 (6)"),
485 diagram_id="le4",
486 engine=design,
487 cea_cache=str(CEA),
488 )
489 session = open_session(
490 model, load_machine(tables=TABLES), hookup=suggest(model, 500.0, 4500.0)
491 )
492 jump_to_t0(session, copv_psi=4500.0, fill_fraction=0.95)
493 ids = _ids()
494 lox, fuel = session.tanks[ids["LOX-Tank"]], session.tanks[ids["Eth-Tank"]]
495 assert lox.state.liquid_mass == pytest.approx(config["lox_tank"]["mass"])
496 assert fuel.state.liquid_mass == pytest.approx(config["fuel_tank"]["mass"])
497 # A pad load stops there too (the session hands it over each tick).
498 session.step(0.02)
499 assert lox._wanted() == pytest.approx(config["lox_tank"]["mass"])
500
501
503 """A cart regulator the drawing sets is still turned by hand on the pad, so it
504 gets a knob -- starting where the drawing sets it. It used to get none, and the
505 dome and COPV fill started at 500 and 4,500 whatever the sheet said."""
506 from feedtwin.session import assemble_model
507 from feedtwin.session.hookup import CHARGE, DOME, knob_starts, suggest
508
509 ids = _ids()
510 payload = _payload()
511 settings = {"PR-1": 3750, "DR-REG-G": 535, "LP-PR": 150}
512 for node in payload["nodes"]:
513 label = node["data"]["label"]
514 if label in settings:
515 node["data"].setdefault("params", {})["setpoint"] = {
516 "value": settings[label],
517 "unit": "psi",
518 "source": "estimated",
519 }
520 model = assemble_model(read_diagram(payload, name="LE4"), diagram_id="le4")
521 hookup = suggest(model, 500.0, 4500.0)
522 knobs = {k.id: k for k in hookup.knobs}
523 assert knobs[DOME].regulators == (ids["DR-REG-G"],)
524 assert knobs[CHARGE].regulators == (ids["PR-1"],)
525 assert knobs[ids["LP-PR"]].regulators == (ids["LP-PR"],)
526 assert knob_starts(hookup, model) == {
527 DOME: pytest.approx(535.0),
528 CHARGE: pytest.approx(3750.0),
529 ids["LP-PR"]: pytest.approx(150.0),
530 }
531 assert knobs[DOME].psig == pytest.approx(535.0)
532 assert knobs[CHARGE].psig == pytest.approx(3750.0)
533 assert knobs[ids["LP-PR"]].psig == pytest.approx(150.0)
534
535
536@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
538 """The flight fuel tank's fill line stays open to it when dry so it can be
539 loaded -- in its fill state, and only then. Open always, with a dump open
540 beyond it (LE4's FD-ROT-G, uncommanded and resting open; here the cart's
541 hand dump and transfer valve opened, the transfer held by hand), the dry
542 tank "drained" through the line forever and the vessel's floor re-made the
543 mass: ~0.46 kg/s of propellant from nothing, booked as guard."""
544 from feedtwin.session.burn import jump_to_t0
545
546 ids = _ids()
547 payload = _payload()
548 for node in payload["nodes"]:
549 if node["data"]["label"] == "FF-MAN-Dump":
550 node["data"].setdefault("options", {})["normalPosition"] = "open"
551 session = _session(payload)
552 jump_to_t0(session, copv_psi=4000.0, fill_fraction=0.95)
553 session.release(ids["FF-MAN-Output"])
554 session.set_valve(ids["FF-MAN-Output"], True)
555 fuel = session.tanks[ids["Eth-Tank"]]
556 for _ in range(20):
557 session.step(0.05)
558 # Where the fuel is going is said, and that nothing commands the way out.
559 (leaving,) = [n for n in session.history[-1].notes if "losing ethanol" in n]
560 assert "FF-MAN-Dump" in leaving and "nothing in the state table" in leaving
561 for _ in range(280): # 15 s: the tank drains through the dump, then is dry
562 session.step(0.05)
563 assert fuel.state.liquid_mass == pytest.approx(0.0, abs=1e-3)
564 guard = session.solver_log[-1].guard_kg
565 for _ in range(200): # 10 s dry
566 session.step(0.05)
567 last = session.solver_log[-1]
568 assert last.guard_kg - guard < 0.05, "the floor is re-making propellant"
569 assert abs(last.mass_error_kg - last.guard_kg) < 1e-6
570
571
572@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
574 """A fresh stand with a drawn load has the crew's transfer valve shut by the
575 twin. That is not the operator's hand: no "1 held", and Release leaves it."""
576 ids = _ids()
577 session = _session()
578 valve = ids["FF-MAN-Output"]
579 assert valve in session.forced
580 assert session.operator_held == []
581 session.set_valve(ids["FM-R"], True)
582 assert session.operator_held == [ids["FM-R"]]
583 session.release()
584 assert session.operator_held == [] and valve in session.forced
585 # Turned the other way by hand, it is the operator's.
586 session.set_valve(valve, True)
587 assert session.operator_held == [valve]
588
589
590@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
592 """Both run tanks primed over MAWP trip the stand at once. It named the
593 first tank it looked at (Eth-Tank, 935.7 psia) while the LOX tank stood
594 at 963.5 psia (D-33); it names the one furthest over its MAWP."""
595 from feedtwin.session.gauge import from_psig
596
597 ids = _ids()
598 session = _session()
599 fuel, lox = session.tanks[ids["Eth-Tank"]], session.tanks[ids["LOX-Tank"]]
600 assert fuel.mawp == pytest.approx(lox.mawp)
601 session.prime(fill_fraction=0.5, tank_psi=900.0, copv_psi=4000.0, state="Ready")
602 lox.state = lox.tank.initial_state(
603 pressure=from_psig(950.0),
604 liquid_mass=lox.state.liquid_mass,
605 liquid_temperature=lox.state.liquid_temperature,
606 gas_temperature=293.15,
607 )
608 assert fuel.pressure > fuel.mawp and lox.pressure > fuel.pressure
609 session._check_limits()
610 assert session.trip is not None and session.trip.vessel == ids["LOX-Tank"]
611 assert session.tripped and session.tripped.startswith("LOX-Tank reached")
612
613
614@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
616 """The trip limit is the drawing's burst over a factor of two, and the trip
617 said "The tank would have failed" at half the burst. LE4's tank reliefs are
618 drawn at 750 psi, the trip limit itself, so they can never lift before it:
619 the message says so rather than leave the operator wondering why the drawn
620 relief did nothing."""
621 from feedtwin.session.gauge import PSI, psig
622
623 ids = _ids()
624 session = _session()
625 lox = session.tanks[ids["LOX-Tank"]]
626 session.prime(fill_fraction=0.5, tank_psi=500.0, copv_psi=4000.0, state="Ready")
627 # Both tank reliefs at the limit; the COPV's well above it, behind a
628 # regulator, protecting something else.
629 sets = {
630 label: branch.component.set_pressure / PSI
631 for branch_id, branch in session._reliefs
632 for label in [next(k for k, v in ids.items() if v == branch_id)]
633 }
634 assert sets["RV_O"] == pytest.approx(psig(lox.mawp), abs=0.5), sets
635 assert sets["RV-HPF"] > psig(lox.mawp) + 1000.0, sets
636 lox.state = lox.tank.initial_state(
637 pressure=lox.mawp + 5.0 * PSI,
638 liquid_mass=lox.state.liquid_mass,
639 liquid_temperature=lox.state.liquid_temperature,
640 gas_temperature=293.15,
641 )
642 session._check_limits()
643 message = session.tripped or ""
644 assert message.startswith("LOX-Tank reached"), message
645 assert "would have failed" not in message
646 assert "MAWP" in message and "at risk" in message
647 assert "RV_O is set at or above that limit and cannot lift" in message, message
648 assert "RV-HPF" not in message, "the COPV relief protects the COPV"
649
650
651# ------------------------------------------- the cart's tanks end no burn
652
653
654def _cart_tank_ran_dry(session: Any) -> Any:
655 """The cart's Fuel Transfer Tank after the loads it gave: dry, as a real fill
656 leaves it (``TankSim.advance`` sets ``empty``, and a resting cart vessel is
657 never advanced again to clear it)."""
658 cart = session.tanks[_ids()["Fuel Transfer Tank"]]
659 cart.state = replace(cart.state, liquid_mass=0.0)
660 cart.empty = True
661 return cart
662
663
664@pytest.mark.skipif(
665 not (ENGINE.exists() and CEA.exists() and TABLES.is_dir()),
666 reason="engine, CEA table or state machine tables absent",
667)
669 """Only the vehicle's tanks end a burn. ``_burnout_check`` counted every tank
670 on the stand, so a cart transfer tank emptied by the loads it gave sent each
671 Fire to Vent at T+0.00 s with the flight tanks full ("Burnout at T+0.00 s:
672 Fuel Transfer Tank ran dry")."""
673 import yaml
674
675 from feedtwin.engine.importer import engine_from_config
676 from feedtwin.session import assemble_model, load_machine
677 from feedtwin.session.burn import jump_to_t0, open_session
678 from feedtwin.session.hookup import suggest
679
680 design = engine_from_config(yaml.safe_load(ENGINE.read_text()), name="6800N")
681 model = assemble_model(
682 read_diagram(_payload(), name="LE4 (6)"),
683 diagram_id="le4",
684 engine=design,
685 cea_cache=str(CEA),
686 )
687 session = open_session(
688 model, load_machine(tables=TABLES), hookup=suggest(model, 500.0, 4500.0)
689 )
690 jump_to_t0(session, copv_psi=4500.0, fill_fraction=0.95)
691 _cart_tank_ran_dry(session)
692 session.command_state("Fire")
693 for _ in range(25): # 0.5 s
694 session.step(0.02)
695 assert session.burnout is None
696 assert session.state == "Fire"
697 assert session.history[-1].chamber.thrust > 5000.0
698
699
700@pytest.mark.skipif(not TABLES.is_dir(), reason="no state machine tables")
702 """With no engine model the ENGINE symbol's design pressure is the boundary
703 while the feed can hold it. "The feed" counted the cart: a dewar at 50 psig
704 or a dry transfer tank under the 385 psia design kept the chamber at
705 atmosphere with both flight tanks pressed well above it."""
706 from feedtwin.session.burn import jump_to_t0
707
708 session = _session()
709 jump_to_t0(session, copv_psi=4000.0, fill_fraction=0.95)
710 _cart_tank_ran_dry(session)
711 session.command_state("Fire")
712 session.step(0.02)
713 session.step(0.02)
714 vehicle = [session.tanks[k] for k in session.vehicle_tanks]
715 assert min(sim.outlet_pressure for sim in vehicle) > session._design_chamber
716 chamber = session.model.built.network.nodes[session._chamber_node]
717 assert chamber.pressure == pytest.approx(session._design_chamber)
718
719
721 """HP-QD-A and HP-QD-B are paired and joined by their own line; OF-QDA has
722 no line at all, which is worth saying."""
723 unmated = [w for w in built.warnings if "unmated" in w]
724 assert not [w for w in unmated if "HP-QD" in w], unmated
725 assert [w for w in unmated if "OF-QDA" in w], "the real one is still said"
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None test_ox_fill_loads_the_flight_lox_tank_from_the_dewar()
None test_a_cold_flow_is_lit_by_the_vehicle_tanks_not_the_cart()
None test_t0_loads_the_fire_load_not_the_tank()
None test_the_pad_charges_the_copv_and_loads_fuel_through_the_drawing()
None test_the_vehicle_is_what_is_joined_to_the_engine(Any built)
None test_a_trip_names_the_vessel_furthest_over()
None test_the_dome_line_is_gated_by_its_valves(Any built)
None test_a_motorised_valve_is_a_commanded_valve(Any built)
None test_a_pair_drawn_joined_is_not_called_unmated(Any built)
None test_the_transfer_tank_supplies_the_flight_fuel_tank(Any built)
None test_the_dome_line_loads_the_dome_and_carries_no_feed(Any built)
None test_every_hand_valve_rests_shut(Any built)
None test_while_the_engine_burns_the_cart_rests_and_the_burn_is_the_same()
None test_the_crews_transfer_valve_is_not_the_operators_hold()
None test_blank_loader_and_fill_start_at_what_they_feed()
None test_an_empty_cart_tank_is_not_a_burnout()
None test_ignition_is_not_stepped_on_a_capped_port()
None test_the_cart_regulators_drawn_with_settings_get_knobs_at_them()
None test_a_topped_lox_tank_on_its_vent_lands_where_the_solve_put_it()
None test_a_dry_tank_does_not_drain_back_through_its_fill_line()
None test_a_trip_says_what_it_is_and_which_reliefs_it_beat()
Any _cart_tank_ran_dry(Any session)
None test_a_hand_valve_drawn_normally_open_rests_open()
None test_the_dome_loads_on_the_pad_and_holds_when_its_valve_shuts()
Any _session(Any|None payload=None)
None test_every_hand_loaded_regulator_gets_a_knob()