1"""Which part of a drawing is the vehicle, and which is the ground support.
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.
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.
17Shared by the cockpit and Layer X (``EngineDesign/engine/layerx``), so the two
18never disagree about which tank is which.
21from __future__
import annotations
23from collections
import defaultdict
24from dataclasses
import replace
29ENGINE_TYPES = frozenset({
"ENGINE",
"INJECTOR"})
33 """Every symbol joined to an ENGINE by drawn lines, or ``None`` without one.
35 Instruments clipped to a vehicle symbol (an RTD on a tank, a TC on the
36 engine) belong with it.
38 engines = [n.id
for n
in diagram.nodes
if n.type
in ENGINE_TYPES]
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)
48 for other
in adjacent[stack.pop()]:
53 return frozenset(seen)
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)."""
61 for edge
in diagram.edges
62 if edge.source
in members
and edge.target
in members
64 for node
in diagram.nodes:
65 if node.attached_to
and (
66 node.attached_to
in members
or node.attached_to
in lines
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)."""
75 if vehicle
is None or len(vehicle) == len(diagram.nodes):
77 return frozenset(n.id
for n
in diagram.nodes
if n.id
not in vehicle)
81VESSEL_TYPES = frozenset({
"TANK",
"KBOTTLE",
"DEWAR"})
85SUPPLY_TYPES = VESSEL_TYPES | frozenset({
"PR",
"PUMP",
"QD"})
91VENTING_TYPES = frozenset({
"SOL",
"MOV",
"ROT",
"RV",
"VENT"})
95 mate = str(node.options.get(
"pairedWith",
"")
or "").strip()
96 return "" if mate ==
"none" else mate
100 """The cart's vent lines: every off-vehicle symbol behind a vehicle
101 disconnect whose far side is only a vent.
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.
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:
125 node.id
not in vehicle
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
139 return frozenset(kept)
143 start: str, vehicle: frozenset[str], adjacent: dict[str, set[str]], ids: set[str]
148 for other
in adjacent[stack.pop()]:
149 if other
not in branch
and other
not in vehicle
and other
in ids:
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)."""
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(
172 for n
in diagram.nodes
173 if n.type ==
"QD" and n.id
in vehicle
and not _mate(n)
175 loose_vents: list[str] = []
176 for n
in diagram.nodes:
177 if n.type !=
"QD" or n.id
in vehicle
or _mate(n):
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):
184 by_id[i].label
or i
for i
in branch
if by_id[i].type
in VENTING_TYPES
187 loose_vents.append(f
"{n.label or n.id} (in front of {', '.join(valves)})")
188 if not (loose_rocket
and loose_vents):
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 "
200 """The drawing with its ground support cut away, and the labels of the
201 vessels that went with it.
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
212 if vehicle
is None or len(vehicle) == len(diagram.nodes):
215 if len(keep) == len(diagram.nodes):
219 for n
in diagram.nodes
220 if n.id
not in keep
and n.type
in VESSEL_TYPES
223 def capped(node: PidNode) -> PidNode:
225 if not mate
or mate
in keep:
227 return replace(node, options={**node.options,
"pairedWith":
""})
232 nodes=tuple(capped(n)
for n
in diagram.nodes
if n.id
in keep),
234 e
for e
in diagram.edges
if e.source
in keep
and e.target
in keep
frozenset[str]|None vehicle_ids(Diagram diagram)
None _clip_with(Diagram diagram, set[str] members)
set[str] _off_vehicle_branch(str start, frozenset[str] vehicle, dict[str, set[str]] adjacent, set[str] ids)
list[str] unpaired_vents(Diagram diagram)
frozenset[str] vent_branches(Diagram diagram)
tuple[Diagram, tuple[str,...]] vehicle_only(Diagram diagram)
frozenset[str] ground_ids(Diagram diagram)