STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_hookup.py
Go to the documentation of this file.
1"""A stand's hookup: which valve each actuator drives, which knob sets which regulator.
2
3Three promises, each checked against something it cannot fake:
4
5* **The suggestion is the old behaviour, exactly.** A session given ``suggest()``
6 emits the same signals, bit for bit, as one given no hookup -- so an imported
7 drawing behaves as it always did until somebody changes it.
8* **A knob moves the regulator it is linked to.** A plain hand-loaded regulator
9 linked to a knob holds the knob's setting, and the tank it presses locks up
10 there; unlinked, it holds the drawn setpoint.
11* **A pin wins over the name match**, an empty pin leaves an actuator unbound,
12 and a pin naming a valve the drawing lost is ignored rather than leaving a
13 main valve uncommanded.
14"""
15
16from __future__ import annotations
17
18import json
19from pathlib import Path
20
21import pytest
22from test_press_path_coupling import _helium_stand
23
24from feedtwin.pid import read_diagram
25from feedtwin.session import Session, assemble_model, bind, load_machine
26from feedtwin.session.burn import press_valves
27from feedtwin.session.gauge import psig
28from feedtwin.session.hookup import DOME, Hookup, Knob, binding, regulators, suggest
29
30STAR = Path(__file__).resolve().parents[3]
31STAND = STAR / "feed-twin" / "backend" / "diagrams" / "ethalox_stand.json"
32TABLES = STAR / "feed-twin" / "backend" / "statemachines"
33
34needs_stand = pytest.mark.skipif(
35 not (STAND.exists() and TABLES.is_dir()), reason="stand drawing or tables absent"
36)
37
38
39def _session(hookup: Hookup | None) -> Session:
40 model = assemble_model(
41 read_diagram(json.loads(STAND.read_text()), name="s"), diagram_id="s"
42 )
43 machine = load_machine(tables=TABLES)
44 return Session(model, machine, binding(model, machine, hookup), hookup=hookup)
45
46
47@needs_stand
49 plain = _session(None)
50 model = plain.model
51 hookup = suggest(
52 assemble_model(
53 read_diagram(json.loads(STAND.read_text()), name="s"), diagram_id="s"
54 ),
55 plain.setup.dome_psi,
56 )
57 assert hookup.knobs and hookup.knobs[0].id == DOME, "the stand's dome knob"
58 suggested = _session(hookup)
59 for state in ("Idle", "Ready"):
60 plain.state = suggested.state = state
61 assert suggested.signals() == plain.signals()
62 assert suggested.binding.to_symbol == plain.binding.to_symbol
63 assert (
64 model.built.dome_loaders.keys() <= set(hookup.knobs[0].regulators)
65 or not model.built.dome_loaders
66 )
67
68
70 """LE4's layout with a hand-loaded regulator (drawn at 560). Linked to a knob at
71 400 psig the tanks lock up near 400; unlinked they lock up near 560. The test
72 can fail: a knob that drove nothing leaves both at 560."""
73
74 def lockup(knob_psig: float | None) -> float:
75 session = _helium_stand()
76 if knob_psig is not None:
77 (reg,) = [r for r in regulators(session.model) if r.label == "DPR_HP"]
78 assert reg.kind == "plain"
79 session.hookup = Hookup(
80 knobs=(Knob("cart", "Cart regulator", (reg.id,), knob_psig),)
81 )
82 session.knobs = {"cart": knob_psig}
83 session.prime(tank_psi=300.0, copv_psi=4500.0, state="Ready", hold_s=1.0)
84 for symbol in press_valves(session):
85 session.set_valve(symbol, True)
86 for _ in range(150):
87 session.step(0.02)
88 ullage = session.model.built.tanks["OT"].ullage
89 return psig(session.history[-1].pressures[ullage])
90
91 drawn, turned = lockup(None), lockup(400.0)
92 assert turned < drawn - 100.0
93 assert turned == pytest.approx(400.0, abs=40.0)
94
95
96@needs_stand
98 session = _session(None)
99 machine, model = session.machine, session.model
100 labels = {
101 n.id: n.label or n.id
102 for n in model.diagram.nodes
103 if n.id in model.built.actuators
104 }
105 auto = bind(machine, labels, roles=model.built.valve_roles)
106 actuator, symbol = sorted(auto.to_symbol.items())[0]
107 other = next(s for s in labels if s != symbol)
108
109 pinned = bind(
110 machine, labels, roles=model.built.valve_roles, overrides={actuator: other}
111 )
112 assert pinned.to_symbol[actuator] == other
113 assert actuator in pinned.by_user
114 assert list(pinned.to_symbol.values()).count(other) == 1, "a valve is driven once"
115
116 none = bind(
117 machine, labels, roles=model.built.valve_roles, overrides={actuator: ""}
118 )
119 assert actuator not in none.to_symbol and actuator in none.unmatched
120
121 stale = bind(
122 machine, labels, roles=model.built.valve_roles, overrides={actuator: "gone"}
123 )
124 assert stale.to_symbol[actuator] == symbol and actuator not in stale.by_user
125
126
128 raw = {
129 "knobs": [
130 {"id": "a", "regulators": ["PR1"]},
131 {"id": "b", "regulators": ["PR1"]},
132 ]
133 }
134 with pytest.raises(ValueError, match="two knobs"):
135 Hookup.from_dict(raw)
136 ok = Hookup.from_dict(
137 {
138 "valves": {"LOX Main": "OM"},
139 "knobs": [{"id": "a", "regulators": ["PR1"], "psig": 300}],
140 }
141 )
142 assert Hookup.from_dict(ok.to_dict()) == ok
143
144
146 """A console name survives the store; an empty one is no name (the tag shows)."""
147 raw = {
148 "valves": {},
149 "knobs": [],
150 "aliases": {"PC": "Chamber pressure", "SV-1": " "},
151 }
152 hookup = Hookup.from_dict(raw)
153 assert hookup.aliases == {"PC": "Chamber pressure"}
154 assert Hookup.from_dict(hookup.to_dict()) == hookup
155 assert Hookup.from_dict({"valves": {}, "knobs": []}).aliases == {}
size_type count(const key_type &key) const
Definition json.hpp:19264
None test_a_regulator_cannot_be_on_two_knobs()
None test_a_knob_sets_the_plain_regulator_it_is_linked_to()
None test_a_pin_wins_an_empty_pin_unbinds_and_a_stale_pin_is_ignored()
Session _session(Hookup|None hookup)
None test_the_suggestion_is_what_the_session_always_did()
None test_aliases_round_trip_and_blanks_are_dropped()