STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
roles.py
Go to the documentation of this file.
1"""Which part of a drawing is the vehicle, and which is the ground support.
2
3pid-designer draws a stand on pages -- the rocket on one, the GSE on another --
4joined only by quick-disconnects paired across the pages (``options.pairedWith``).
5The twin mates those pairs into one network, because filling the vehicle is what
6the GSE is for. But a vessel's *job* depends on which side of the couplings it
7sits: a TANK on the vehicle is a load the engine burns, a TANK on the cart is
8where a load comes from; a KBOTTLE on the vehicle is the pressurant the burn
9spends, a 49 L K-bottle on the cart is delivered full and charges it.
10
11The vehicle is everything joined to the engine by a drawn line, walked through
12any symbol. A paired disconnect is not a drawn line, so the walk stops at the
13coupling, which is where the vehicle ends. A drawing with no engine, or whose
14GSE is drawn with lines into the vehicle, is one piece: the whole drawing is the
15vehicle, and every drawing that read before reads exactly as it did.
16
17Shared by the cockpit and Layer X (``EngineDesign/engine/layerx``), so the two
18never disagree about which tank is which.
19"""
20
21from __future__ import annotations
22
23from collections import defaultdict
24from dataclasses import replace
25
26from feedtwin.pid.document import Diagram, PidNode
27
28#: Symbols that make an engine end of the vehicle.
29ENGINE_TYPES = frozenset({"ENGINE", "INJECTOR"})
30
31
32def vehicle_ids(diagram: Diagram) -> frozenset[str] | None:
33 """Every symbol joined to an ENGINE by drawn lines, or ``None`` without one.
34
35 Instruments clipped to a vehicle symbol (an RTD on a tank, a TC on the
36 engine) belong with it.
37 """
38 engines = [n.id for n in diagram.nodes if n.type in ENGINE_TYPES]
39 if not engines:
40 return None
41 adjacent: dict[str, set[str]] = defaultdict(set)
42 for edge in diagram.edges:
43 adjacent[edge.source].add(edge.target)
44 adjacent[edge.target].add(edge.source)
45 seen = set(engines)
46 stack = list(engines)
47 while stack:
48 for other in adjacent[stack.pop()]:
49 if other not in seen:
50 seen.add(other)
51 stack.append(other)
52 _clip_with(diagram, seen)
53 return frozenset(seen)
54
55
56def _clip_with(diagram: Diagram, members: set[str]) -> None:
57 """Add to ``members`` every probe clipped to one of them, or to a line both
58 of whose ends are among them (pid-designer clips probes to lines too)."""
59 lines = {
60 edge.id
61 for edge in diagram.edges
62 if edge.source in members and edge.target in members
63 }
64 for node in diagram.nodes:
65 if node.attached_to and (
66 node.attached_to in members or node.attached_to in lines
67 ):
68 members.add(node.id)
69
70
71def ground_ids(diagram: Diagram) -> frozenset[str]:
72 """Every symbol that is ground support: off the vehicle. Empty for a drawing
73 that is one piece (or has no engine to say which piece is the vehicle)."""
74 vehicle = vehicle_ids(diagram)
75 if vehicle is None or len(vehicle) == len(diagram.nodes):
76 return frozenset()
77 return frozenset(n.id for n in diagram.nodes if n.id not in vehicle)
78
79
80#: Symbols :func:`vehicle_only` names when it says what it cut.
81VESSEL_TYPES = frozenset({"TANK", "KBOTTLE", "DEWAR"})
82
83#: What makes a branch of the cart a supply rather than a vent: something that
84#: holds or sets a pressure, or a further coupling that leads on.
85SUPPLY_TYPES = VESSEL_TYPES | frozenset({"PR", "PUMP", "QD"})
86
87#: What a vent line does its job with: a valve the state table opens, a relief,
88#: or the outlet itself. A hand valve alone is not one: nobody's table opens
89#: it, so a coupling with only that behind it stays capped and the tank keeps
90#: the stand-in vent the binding gives a capped tank-top disconnect.
91VENTING_TYPES = frozenset({"SOL", "MOV", "ROT", "RV", "VENT"})
92
93
94def _mate(node: PidNode) -> str:
95 mate = str(node.options.get("pairedWith", "") or "").strip()
96 return "" if mate == "none" else mate
97
98
99def vent_branches(diagram: Diagram) -> frozenset[str]:
100 """The cart's vent lines: every off-vehicle symbol behind a vehicle
101 disconnect whose far side is only a vent.
102
103 The vents sit on the GSE, but they are a small part of it that stays
104 plugged into the rocket until the last moment before launch (the team,
105 2026-10-10): with the rest of the cart unplugged a tank still vents
106 through its own line and valve. A branch is a vent when the walk from the
107 mate along drawn lines, off the vehicle, finds a valve or an outlet and
108 nothing that holds or sets a pressure (``SUPPLY_TYPES``: a vessel, a
109 regulator, another coupling). Instruments clipped to it come with it. A
110 coupling with nothing drawn behind it, or only a hand valve, is no vent
111 line: it stays capped.
112 """
113 vehicle = vehicle_ids(diagram)
114 if vehicle is None:
115 return frozenset()
116 by_id = {n.id: n for n in diagram.nodes}
117 adjacent: dict[str, set[str]] = defaultdict(set)
118 for edge in diagram.edges:
119 adjacent[edge.source].add(edge.target)
120 adjacent[edge.target].add(edge.source)
121 kept: set[str] = set()
122 for node in diagram.nodes:
123 mate = _mate(node)
124 if (
125 node.id not in vehicle
126 or node.type != "QD"
127 or mate not in by_id
128 or mate in vehicle
129 ):
130 continue
131 branch = _off_vehicle_branch(mate, vehicle, adjacent, set(by_id))
132 types = [by_id[i].type for i in branch if i != mate]
133 if any(t in SUPPLY_TYPES for t in types) or not any(
134 t in VENTING_TYPES for t in types
135 ):
136 continue
137 kept |= branch
138 _clip_with(diagram, kept)
139 return frozenset(kept)
140
141
143 start: str, vehicle: frozenset[str], adjacent: dict[str, set[str]], ids: set[str]
144) -> set[str]:
145 branch = {start}
146 stack = [start]
147 while stack:
148 for other in adjacent[stack.pop()]:
149 if other not in branch and other not in vehicle and other in ids:
150 branch.add(other)
151 stack.append(other)
152 return branch
153
154
155def unpaired_vents(diagram: Diagram) -> list[str]:
156 """What to say when the cart draws a vent line whose coupling is paired
157 with nothing while the rocket has a coupling paired with nothing: they are
158 likely the vent's two halves, and as drawn the vent valve behind the cart's
159 half is never reached (the tank vents through the rocket's half as a
160 stand-in). Said, not guessed: which half goes with which is the drawing's
161 to state (``options.pairedWith`` in pid-designer)."""
162 vehicle = vehicle_ids(diagram)
163 if vehicle is None:
164 return []
165 by_id = {n.id: n for n in diagram.nodes}
166 adjacent: dict[str, set[str]] = defaultdict(set)
167 for edge in diagram.edges:
168 adjacent[edge.source].add(edge.target)
169 adjacent[edge.target].add(edge.source)
170 loose_rocket = sorted(
171 n.label or n.id
172 for n in diagram.nodes
173 if n.type == "QD" and n.id in vehicle and not _mate(n)
174 )
175 loose_vents: list[str] = []
176 for n in diagram.nodes:
177 if n.type != "QD" or n.id in vehicle or _mate(n):
178 continue
179 branch = _off_vehicle_branch(n.id, vehicle, adjacent, set(by_id))
180 types = [by_id[i].type for i in branch if i != n.id]
181 if any(t in SUPPLY_TYPES for t in types):
182 continue
183 valves = sorted(
184 by_id[i].label or i for i in branch if by_id[i].type in VENTING_TYPES
185 )
186 if valves:
187 loose_vents.append(f"{n.label or n.id} (in front of {', '.join(valves)})")
188 if not (loose_rocket and loose_vents):
189 return []
190 return [
191 f"The rocket's {', '.join(loose_rocket)} and the cart's vent coupling "
192 f"{'; '.join(sorted(loose_vents))} are paired with nothing. If they are a "
193 "vent's two halves, pair them in pid-designer and the tank vents through "
194 "the cart's vent valve; as drawn it vents through the rocket's half as a "
195 "stand-in."
196 ]
197
198
199def vehicle_only(diagram: Diagram) -> tuple[Diagram, tuple[str, ...]]:
200 """The drawing with its ground support cut away, and the labels of the
201 vessels that went with it.
202
203 What a stand with its GSE unplugged is: every symbol off the vehicle is
204 gone, with every line that touches one, and each vehicle disconnect whose
205 mate was cut is a capped half -- which is how a one-page drawing of the
206 rocket reads its fill and vent ports. The cart's vent lines stay
207 (:func:`vent_branches`): they are plugged into the rocket until launch.
208 A drawing that is one piece comes back as the same object, with nothing
209 cut.
210 """
211 vehicle = vehicle_ids(diagram)
212 if vehicle is None or len(vehicle) == len(diagram.nodes):
213 return diagram, ()
214 keep = vehicle | vent_branches(diagram)
215 if len(keep) == len(diagram.nodes):
216 return diagram, ()
217 cut = tuple(
218 n.label or n.id
219 for n in diagram.nodes
220 if n.id not in keep and n.type in VESSEL_TYPES
221 )
222
223 def capped(node: PidNode) -> PidNode:
224 mate = _mate(node)
225 if not mate or mate in keep:
226 return node
227 return replace(node, options={**node.options, "pairedWith": ""})
228
229 return (
230 replace(
231 diagram,
232 nodes=tuple(capped(n) for n in diagram.nodes if n.id in keep),
233 edges=tuple(
234 e for e in diagram.edges if e.source in keep and e.target in keep
235 ),
236 ),
237 cut,
238 )
frozenset[str]|None vehicle_ids(Diagram diagram)
Definition roles.py:32
None _clip_with(Diagram diagram, set[str] members)
Definition roles.py:56
set[str] _off_vehicle_branch(str start, frozenset[str] vehicle, dict[str, set[str]] adjacent, set[str] ids)
Definition roles.py:144
list[str] unpaired_vents(Diagram diagram)
Definition roles.py:155
str _mate(PidNode node)
Definition roles.py:94
frozenset[str] vent_branches(Diagram diagram)
Definition roles.py:99
tuple[Diagram, tuple[str,...]] vehicle_only(Diagram diagram)
Definition roles.py:199
frozenset[str] ground_ids(Diagram diagram)
Definition roles.py:71