1"""A LOX load pushed in by a dewar (``Setup.dewar_psi``).
3The load is a flow through the fill line at the dewar's pressure less the
4tank's. Into a warm tank it boils on the wall and the vapour goes into the
5ullage -- the climb the stand shows during a chilldown -- and once the wall is
6at saturation it collects. Checked here against hand calculation and CoolProp,
7not against the session: the line against Crane's Cv relation and Clamond, the
8chill against ``h_fg`` per gram, and a zero dewar against the fixed-rate load
12from __future__
import annotations
15from dataclasses
import replace
17import CoolProp.CoolProp
as CP
19from fluids.friction
import Clamond
31 *, liquid_mass: float = 0.0, wall_K: float = 293.15, fuel: bool =
False
33 liquid =
Fluid(
"ethanol" if fuel
else "oxygen")
39 wall_capacity=WALL_CP,
42 state = tank.initial_state(
43 pressure=from_psig(0.0),
44 liquid_mass=liquid_mass,
45 liquid_temperature=293.15
if fuel
else 90.19,
46 gas_temperature=wall_K,
50 state = replace(state, wetted_wall_temperature=wall_K)
60 dewar_pressure=from_psig(100.0),
61 fill_line_bore=7.75e-3,
68 """Liquid through the line: drop = (f L/D + K + 1) rho v^2 / 2, with K from
69 Crane's Cv relation K = 891 d^4 / Cv^2 (d in inches) and f Clamond at the
70 Reynolds number of the answer."""
72 drop = from_psig(100.0) - from_psig(0.0)
73 flow = sim.fill_line_flow(drop)
74 T = sim.state.liquid_temperature
75 rho = CP.PropsSI(
"D",
"T", T,
"Q", 0,
"Oxygen")
76 mu = CP.PropsSI(
"V",
"T", T,
"Q", 0,
"Oxygen")
78 area = math.pi * D * D / 4
79 v = flow / (rho * area)
80 f = Clamond(rho * v * D / mu, FILL_LINE_ROUGHNESS / D)
81 K = 891.0 * (D / 25.4e-3) ** 4 / 0.019**2
82 assert drop == pytest.approx((f * L / D + K + 1.0) * rho * v * v / 2, rel=5e-3)
86 """The reason the dewar valve is calibrated rather than left open: a bare
87 3/8 in line from 100 psig pours ~0.7 kg/s."""
89 throttled = sim.fill_line_flow(from_psig(100.0) - from_psig(0.0))
91 clean = sim.fill_line_flow(from_psig(100.0) - from_psig(0.0))
92 assert 0.5 < clean < 1.0
93 assert clean > 30 * throttled
98 assert sim.fill_line_flow(0.0) == 0.0
99 assert sim.fill_line_flow(-1e5) == 0.0
103 """Chilling: nothing collects, the vapour goes into the ullage (where only
104 the vent can take it out), and the wall gives up h_fg per gram -- h_fg from
105 CoolProp here, not from the model."""
107 walls = (sim.state.ullage.wall_temperature, sim.state.wetted_wall_temperature)
108 flow = sim.fill_line_flow(sim.dewar_pressure - sim.pressure)
110 assert sim._dewar_load(dt)
111 assert sim.state.liquid_mass == 0.0
112 assert sim.state.vapour_mass == pytest.approx(flow * dt, rel=1e-12)
113 assert sim.chill_boiled == pytest.approx(flow * dt, rel=1e-12)
114 assert sim.fill_flow == pytest.approx(flow, rel=1e-12)
115 T = sim.state.liquid_temperature
116 h_fg = CP.PropsSI(
"H",
"T", T,
"Q", 1,
"Oxygen") - CP.PropsSI(
117 "H",
"T", T,
"Q", 0,
"Oxygen"
123 (sim.state.ullage.wall_temperature, sim.state.wetted_wall_temperature),
126 assert fell[0] == pytest.approx(fell[1], rel=1e-12)
127 assert WALL_MASS * WALL_CP * fell[0] == pytest.approx(flow * dt * h_fg, rel=1e-3)
131 sim =
_sim(wall_K=90.5)
132 flow = sim.fill_line_flow(sim.dewar_pressure - sim.pressure)
133 assert not sim._dewar_load(0.5)
134 assert sim.state.vapour_mass == 0.0
135 assert sim.state.liquid_mass == pytest.approx(flow * 0.5, rel=1e-12)
139 """A step that brings the wall down to saturation splits: what the wall's
140 remaining heat boils, and the rest in the liquid."""
142 target = CP.PropsSI(
"T",
"P", sim.pressure,
"Q", 0,
"Oxygen") + 2.0
146 ullage=replace(sim.state.ullage, wall_temperature=warm),
147 wetted_wall_temperature=warm,
149 sim.fill_line_bore = 30e-3
152 assert sim.state.ullage.wall_temperature == pytest.approx(target, abs=0.05)
153 T = sim.state.liquid_temperature
154 h_fg = CP.PropsSI(
"H",
"T", T,
"Q", 1,
"Oxygen") - CP.PropsSI(
155 "H",
"T", T,
"Q", 0,
"Oxygen"
157 assert sim.state.vapour_mass == pytest.approx(
158 WALL_MASS * WALL_CP * 1.0 / h_fg, rel=0.02
160 assert sim.state.liquid_mass > 1.0
164 sim =
_sim(wall_K=90.5)
165 sim.state = replace(sim.state, liquid_mass=sim._wanted())
167 assert sim.fill_flow == 0.0
168 assert sim.state.liquid_mass == pytest.approx(sim._wanted())
173 dt, mdot_liquid_out=0.0, mdot_gas_in=0.0, mdot_gas_out=0.0, enthalpy_gas_in=0.0
178 """Zero dewar pressure: the load before this existed, wanted / fill_seconds."""
179 sim =
_sim(wall_K=90.5)
180 sim.dewar_pressure = 0.0
181 wanted = sim._wanted()
183 assert sim.state.liquid_mass == pytest.approx(wanted * 0.5 / 120.0, rel=1e-9)
184 assert sim.state.vapour_mass == 0.0
188 """The dewar is a cryogen's. With it on, ethanol still pours at its rate."""
189 sim =
_sim(fuel=
True)
190 wanted = sim._wanted()
192 assert sim.state.liquid_mass == pytest.approx(wanted * 0.5 / 120.0, rel=1e-9)
193 assert sim.chill_boiled == 0.0
200 """LE4's LOX tank vented through its top disconnect, the cart's valve
201 beyond it (``test_pid_drawn_freely._le4_with_top_manifold``)."""
202 from pathlib
import Path
204 from test_pid_drawn_freely
import _le4_with_top_manifold, _machine
212 Path(__file__).resolve().parents[3] /
"feed-twin" /
"backend" /
"statemachines"
214 payload = _le4_with_top_manifold()
215 for n
in payload[
"nodes"]:
216 if n[
"id"] ==
"QD" and "vent_cv" in setup:
217 n[
"data"][
"params"] = {
219 "value": setup.pop(
"vent_cv"),
221 "source":
"measured",
224 model = assemble_model(read_diagram(payload, name=
"le4 top"), diagram_id=
"t")
225 machine = load_machine(tables=tables)
227 n.id: n.label
for n
in model.diagram.nodes
if n.id
in model.built.actuators
232 bind(machine, labels, roles=model.built.valve_roles),
233 setup=replace(
Setup(), **setup),
235 return session, model
238def _chill(seconds: float, **setup: float) -> tuple[float, float]:
242 for state
in (
"Armed",
"Ox Fill"):
243 session.command_state(state)
244 while session.t < seconds:
245 sample = session.step(0.5)
246 tank = session.tanks[
"OT"]
248 psig(sample.pressures[model.built.tanks[
"OT"].ullage]),
249 tank.state.liquid_mass,
255 assert model.built.gse_vents == (
"QD",)
256 session.command_state(
"Armed")
258 assert model.built.network.branches[
"QD"].component.p[
"Cv"] == 0.37
262 """Turned on against a drawing that sizes the vent, the setup changes
264 session, model =
_le4_session(vent_cv=1.1, gse_vent_cv=0.37)
265 assert model.built.gse_vents == ()
266 session.command_state(
"Armed")
268 assert model.built.network.branches[
"QD"].component.p[
"Cv"] == pytest.approx(1.1)
272 """The stand: a LOX tank rises while it chills, as far as the vent makes
273 it, and the dewar's pressure sets the pour."""
274 low, low_liquid =
_chill(30.0, dewar_psi=50.0)
275 high, high_liquid =
_chill(30.0, dewar_psi=100.0)
276 assert low_liquid == 0.0
and high_liquid == 0.0,
"still chilling: nothing collects"
277 assert 5.0 < low < high
282 reason=
"R035: the vent is IEC 60534 now; 0.019 peaks at 44.75 psig here and "
283 "~0.013 restores ~30. dewar_fill_cv is the team's calibration to redo.",
286 """The default Cv (0.019, the team 2026-10-08) tops LE4 out near the stand's
287 ~30 psig. 0.013 did until the vent stopped taking back air the pressurant
288 floor re-created (2026-10-06); it then peaked near 21.
290 The calibration was made against the plain valve's old gas law, which
291 passed up to 1.5x IEC just short of the choke -- where a chilling tank's
292 vent runs. With the vent priced by IEC 60534-2-1 (R035) the same 0.019
293 peaks at 44.75 psig on this stand (0.014: 33.7; 0.012: 28.6; 0.010: 23.2).
294 LE4 (3) as uploaded does not move (16.4 -> 16.5 psia at 140 s of Ox Fill).
295 Recalibrating is the team's call; strict, so it turns red when they do."""
296 high, _ =
_chill(30.0, dewar_psi=100.0)
297 assert high == pytest.approx(30.0, abs=5.0),
"calibrated to the stand's ~30 psig"
301 p, _ =
_chill(30.0, dewar_psi=0.0)
306 """The vent of a tank chilling under its own boil-off carries that boil-off.
308 Split by mass share alone, the vent took the tank's air down to the floor
309 (an atmosphere's worth, which the pressurant keeps because it carries the
310 ullage's heat capacity) and the floor put it straight back: on LE4, ~3 g/s
311 of air that did not exist and 0.2 kg over a load, booked as a guard. The
312 vent here carries what the dewar boils into the ullage; the guards must
313 book next to nothing of it.
319 flow = sim.fill_line_flow(sim.dewar_pressure - sim.pressure)
328 arrived += sim.fill_flow * dt
329 assert sim.chilling,
"still chilling: the case being checked"
331 assert abs(sim.fixed_kg) < 1e-3 * arrived, (sim.fixed_kg, arrived)
335 """Nobody rehearsing waits out the minutes a dewar load spends on the wall
336 (the team, 2026-10-08). Skipped, the wall is where the chill would have left
337 it and the next seconds of the pour collect; not skipped, they boil."""
339 for state
in (
"Armed",
"Ox Fill"):
340 session.command_state(state)
343 lox = session.tanks[
"OT"]
344 assert lox.chilling
and lox.state.liquid_mass == 0.0
346 assert session.skip_chilldown() == [lox.label]
351 assert lox.pressure > ATMOSPHERE
352 target = lox._chill_target(ATMOSPHERE)
353 assert lox.state.ullage.wall_temperature == pytest.approx(target)
354 assert lox.chill_boiled > 0.0,
"the propellant the chill would have flashed"
355 assert any(
"Chilldown skipped on" in a
for a
in session.assumptions)
358 assert not lox.chilling
and lox.state.liquid_mass > 0.01
359 assert session.skip_chilldown() == [],
"a cold wall has nothing to skip"
362 for state
in (
"Armed",
"Ox Fill"):
363 control.command_state(state)
366 assert control.tanks[
"OT"].chilling
367 assert control.tanks[
"OT"].state.liquid_mass == 0.0
372 fuel = next(s
for s
in session.tanks.values()
if s.id !=
"OT")
373 wall = fuel.state.ullage.wall_temperature
374 assert session.skip_chilldown(fuel.id) == []
375 assert fuel.state.ullage.wall_temperature == wall
376 with pytest.raises(KeyError):
377 session.skip_chilldown(
"no-such-tank")
_le4_session(**float setup)
tuple[float, float] _chill(float seconds, **float setup)
None test_a_chilling_tank_vents_its_boil_off_not_air_the_floor_puts_back()
None test_the_fill_line_is_crane_and_clamond()
None test_skipping_chilldown_leaves_a_storable_and_an_unknown_tank_alone()
None test_nothing_flows_back_into_the_dewar()
None test_into_a_warm_tank_every_gram_boils_into_the_ullage()
None test_once_the_wall_is_cold_the_load_collects()
None test_a_full_tank_takes_no_more()
None test_a_skipped_chilldown_collects_at_once_where_a_chilling_one_does_not()
None test_a_fuel_load_ignores_the_dewar()
None test_a_chilling_tank_climbs_and_climbs_more_the_harder_the_dewar_pushes()
None test_no_dewar_is_the_fixed_rate_load_exactly()
None test_without_a_dewar_the_chill_vents_outside_the_ullage_as_before()
None test_a_disconnect_that_gives_its_own_cv_keeps_it()
None test_the_fill_cv_is_calibrated_to_the_stands_30_psig_chill()
TankSim _sim(*, float liquid_mass=0.0, float wall_K=293.15, bool fuel=False)
None test_the_last_of_the_wall_boils_and_the_rest_collects()
None _advance(TankSim sim, float dt)
None test_a_clean_line_pours_tens_of_times_what_the_valve_lets_through()
None test_the_carts_vent_valve_sizes_an_unsized_gse_vent()