STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_press_path_coupling.py
Go to the documentation of this file.
1"""Two tanks on one helium press manifold, stepped finer than their press path.
2
3Every LE4 helium burn plotted a 0.5-0.8 s tank-pressure sawtooth, 15-30 psi
4deep, with gas running backwards from one tank to the other through the shared
5manifold -- and GN2 runs were smooth. It was the coupling step. The session
6solved the network once every 25 ms and held each flow for the whole step; the
7only time constant it stepped under was the regulator's (~27 ms), but two
8ullages trade gas through nothing but their solenoids, which on helium is a few
9milliseconds. A solve that found a tank a tenth of a psi above the manifold
10sent its gas out for the full 25 ms while the liquid drained, the tank fell,
11the other dumped into it, and the pair handed the error back and forth. At a
122 ms step it was gone, and the mean thrust was 170 N higher.
13
14``Session._press_path_timescale`` gives the guard that constant. These check
15it is what it says, and that a stand like LE4's no longer rings.
16"""
17
18from __future__ import annotations
19
20from typing import Any
21
22import pytest
23from test_pid_drawn_freely import P, _le4_like, _machine, line, node
24
25from feedtwin.pid import read_diagram
26from feedtwin.session import Session, assemble_model
27from feedtwin.session import core as C
28from feedtwin.session.gauge import psig
29from feedtwin.session.statemachine import bind
30
31
32def _helium_stand() -> Session:
33 """LE4's layout: a helium bottle, a regulator, one manifold to two
34 press solenoids, mains into a fixed-pressure engine."""
35 payload: Any = _le4_like()
36 for n in payload["nodes"]:
37 if n["id"] == "KB":
38 n["data"]["fluidType"] = "helium"
39 n["data"]["params"] = {
40 "pressure": P(4500, "psi"),
41 "temperature": P(293, "K"),
42 "volume": P(4.7, "L"),
43 }
44 payload["nodes"].append(
45 node(
46 "RG",
47 "PR",
48 "DPR_HP",
49 params={
50 "setpoint": P(560, "psi"),
51 "Cv": P(0.8, "Cv"),
52 "bore": P(7.75, "mm"),
53 "flow_droop": P(20, "psi"),
54 "rated_flow": P(0.02, "kg/s"),
55 },
56 )
57 )
58 payload["edges"] = [e for e in payload["edges"] if e["id"] != "kb-mf"] + [
59 line("kb-rg", "KB", "RG", to_port="l"),
60 line("rg-mf", "RG", "MF", from_port="r"),
61 ]
62 model = assemble_model(read_diagram(payload, name="he manifold"), diagram_id="t")
63 machine = _machine()
64 labels = {
65 n.id: n.label for n in model.diagram.nodes if n.id in model.built.actuators
66 }
67 session = Session(
68 model, machine, bind(machine, labels, roles=model.built.valve_roles)
69 )
70 session.prime(tank_psi=575.0, copv_psi=4500.0, state="Ready", hold_s=1.0)
71 return session
72
73
74def _burn(session: Session, seconds: float = 2.0) -> list[float]:
75 for symbol in session.model.built.actuators:
76 session.set_valve(symbol, True)
77 ullage = session.model.built.tanks["OT"].ullage
78 trace = []
79 while session.t < seconds:
80 trace.append(psig(session.step(0.02).pressures[ullage]))
81 return trace[5:] # past the mains opening
82
83
84def _roughness(trace: list[float]) -> float:
85 """Mean |second difference| [psi]: zero for a straight line."""
86 return sum(
87 abs(trace[i + 1] - 2 * trace[i] + trace[i - 1])
88 for i in range(1, len(trace) - 1)
89 ) / (len(trace) - 2)
90
91
93 session = _helium_stand()
94 _burn(session, 0.3)
95 regulator_only = float("inf")
96 for branch in session.model.built.network.branches.values():
97 comp = branch.component
98 if "Regulator" in type(comp).__name__:
99 slope = comp.p["flow_droop"] / comp.p["rated_flow"]
100 for sim in session.tanks.values():
101 regulator_only = min(
102 regulator_only, sim.state.ullage.mass / sim.pressure * slope
103 )
104 tau = session._coupling_timescale({}, 0.02)
105 assert (
106 0.0 < tau < regulator_only / 3
107 ), "the press path is the stiffer loop on helium"
108
109
111 """No liquid leaving, no constant: a pad hold is not stepped at the
112 solenoids' zero-flow slope."""
113 session = _helium_stand()
114 session.step(0.02)
115 sim = session.tanks["OT"]
116 assert session._press_path_timescale(sim, 1.0, {}) == float("inf")
117
118
120 monkeypatch: pytest.MonkeyPatch,
121) -> None:
122 smooth = _roughness(_burn(_helium_stand()))
123 assert smooth < 0.2
124
125 # Either guard is enough on its own. The ullage storage closure
126 # (`Session._ullage_storage`) solves the press path inside the step, so
127 # without the press-path constant the pair still does not ring...
128 monkeypatch.setattr(C.Session, "_press_path_timescale", lambda *a: float("inf"))
129 assert _roughness(_burn(_helium_stand())) < 0.2
130
131 # ...and the test can fail: without both, the pair rings.
132 monkeypatch.setattr(C.Session, "_ullage_storage", lambda *a: {})
133 ringing = _roughness(_burn(_helium_stand()))
134 assert ringing > 1.0
135
136
138 """Engine Abort with the fuel loaded and the LOX tank empty (the whole LOX
139 chill window): the fuel drains by gravity through its open main, and its
140 press path is the manifold, vented through the empty LOX tank. That
141 "supply" sits at atmosphere with the tank, so the band was met and the
142 drain over a step beat a tenth of it: 0.27 ms couplings, 33-79 per 20 ms
143 step, the panel 40x slower than real time. A vent one node further on
144 carries nothing across a regulated supply."""
145 from test_ignore_gse import TABLES, _ids, _session
146
147 if not TABLES.is_dir():
148 pytest.skip("no state machine tables")
149 ids = _ids()
150 session = _session(True)
151 session.state = "Fuel Fill"
152 for _ in range(40):
153 session.step(0.5)
154 fuel = session.tanks[ids["Eth-Tank"]]
155 assert fuel.state.liquid_mass > 4.0
156 assert session.tanks[ids["LOX-Tank"]].state.liquid_mass == 0.0
157 session.command_state("Engine Abort")
158 start = fuel.state.liquid_mass
159 most = 0
160 worst = 0.0
161 for _ in range(10):
162 session.step(0.02)
163 most = max(most, session.solver_log[-1].couplings)
164 worst = max(worst, abs(psig(fuel.pressure)))
165 assert start - fuel.state.liquid_mass > 0.01, "the fuel drains"
166 assert abs(psig(fuel.pressure)) < 0.1, "at atmosphere"
167 assert most <= 2, f"{most} coupling steps in one 20 ms step"
168 # One coupling a step leaves a two-step ring of one step's drain over the
169 # ullage, -0.001/+0.013 psig on LE4: bounded, and under the panel's tenth of
170 # a psi. It must not grow.
171 assert worst < 0.05, f"the fuel ullage rang {worst:.3f} psi off the air"
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None test_a_draining_helium_tank_is_stepped_under_its_press_path()
None test_two_helium_tanks_on_one_manifold_do_not_ring(pytest.MonkeyPatch monkeypatch)
None test_a_tank_draining_onto_a_vented_manifold_asks_for_nothing()
float _roughness(list[float] trace)
list[float] _burn(Session session, float seconds=2.0)