STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_manifold.py
Go to the documentation of this file.
1"""Manifolds: a plenum with ports, and the three kinds of port.
2
3The team's COPV manifold is the worked example -- one inlet, a transducer tap,
4a relief valve, a fill quick-disconnect and the regulator feed -- because it is
5the case that motivated the dead-end peeling in Phase 04 and the port taxonomy
6here.
7"""
8
9from __future__ import annotations
10
11import math
12
13import pytest
14
15from feedtwin.comps import FlowConditions, build_component
16from feedtwin.comps.manifold import (
17 DEFAULT_TURN_K,
18 Manifold,
19 ManifoldBranch,
20 ManifoldPort,
21 PortKind,
22 expand_manifold,
23 turn_K_from_geometry,
24)
25from feedtwin.model import ComponentInstance, Param, Provenance
26from feedtwin.model.spec import SpecError
27from feedtwin.solve.network import Network
28
29GAS = FlowConditions(rho=39.0, mu=1.78e-5, p_upstream=4500 * 6894.757293168361)
30
31
32def body(**overrides: object) -> ComponentInstance:
33 params = {
34 "bore": Param(12.7, "mm", Provenance.MEASURED, "1/2 in. plenum bore"),
35 "volume": Param(30.0, "mL", Provenance.ESTIMATED, "from the model"),
36 }
37 params.update(overrides) # type: ignore[arg-type]
38 return ComponentInstance.build("MF-01", "manifold", params)
39
40
41def copv_manifold() -> Manifold:
42 """One inlet, four takeoffs. The real hardware."""
43 ports = [
44 ManifoldPort("inlet", bore=0.0127, turn_K=0.0),
45 ManifoldPort("reg", bore=0.00775, fitting_K=0.5),
46 ManifoldPort("pt", bore=0.0033, kind=PortKind.INSTRUMENT),
47 ManifoldPort("relief", bore=0.00775, kind=PortKind.CONDITIONAL),
48 ManifoldPort("fill_qd", bore=0.00775, kind=PortKind.CONDITIONAL),
49 ]
50 built = build_component(body())
51 assert isinstance(built, Manifold)
52 return Manifold(built.instance, ports)
53
54
55# ------------------------------------------------------------------ port kinds
56
57
59 manifold = copv_manifold()
60 assert {p.id for p in manifold.flow_ports} == {"inlet", "reg"}
61 assert {p.id for p in manifold.dead_ports} == {"pt", "relief", "fill_qd"}
62
63
65 """A transducer tap has no flow, so it has no drop. Ever."""
66 manifold = copv_manifold()
67 port = next(p for p in manifold.ports if p.id == "pt")
68 branch = ManifoldBranch(manifold.instance, port)
69 assert branch.pressure_drop(0.0, GAS) == 0.0
70 # Even asked for a flow it cannot have, it reports no loss -- the mass
71 # balance is what forbids the flow, not a fictitious resistance.
72 assert branch.pressure_drop(0.05, GAS) == 0.0
73
74
76 """A shut relief and a capped port are topologically identical.
77
78 Physically they are nothing alike, which is why the kind is declared. This
79 test pins the declaration, not the behaviour -- the behaviour is the same.
80 """
81 manifold = copv_manifold()
82 relief = next(p for p in manifold.ports if p.id == "relief")
83 assert relief.kind is PortKind.CONDITIONAL
84 assert not relief.kind.carries_flow
85 assert ManifoldBranch(manifold.instance, relief).pressure_drop(0.05, GAS) == 0.0
86
87
88# --------------------------------------------------------------------- physics
89
90
92 manifold = copv_manifold()
93 port = next(p for p in manifold.ports if p.id == "reg")
94 assert port.total_K == pytest.approx(DEFAULT_TURN_K + 0.5)
95
96 branch = ManifoldBranch(manifold.instance, port)
97 mdot = 0.05
98 v = mdot / (GAS.rho * math.pi * port.bore**2 / 4.0)
99 assert branch.pressure_drop(mdot, GAS) == pytest.approx(
100 port.total_K * 0.5 * GAS.rho * v * v, rel=1e-12
101 )
102
103
105 """An in-line outlet is not a side tapping and should not be charged as one."""
106 manifold = copv_manifold()
107 inlet = next(p for p in manifold.ports if p.id == "inlet")
108 assert inlet.turn_K == 0.0
109 reg = next(p for p in manifold.ports if p.id == "reg")
110 assert reg.turn_K > 0.0
111
112
114 """A manifold port does not care which way the gas goes through it."""
115 manifold = copv_manifold()
116 branch = ManifoldBranch(
117 manifold.instance, next(p for p in manifold.ports if p.id == "reg")
118 )
119 assert branch.pressure_drop(0.05, GAS) == pytest.approx(
120 branch.pressure_drop(-0.05, GAS)
121 )
122 # total_dp signs it, which is the part that must reverse.
123 assert branch.total_dp(0.05, GAS) == pytest.approx(-branch.total_dp(-0.05, GAS))
124
125
127 """Quartic in bore: halving a port is 16x the loss at the same mass flow."""
128 instance = body()
129 big = ManifoldBranch(instance, ManifoldPort("a", bore=0.0127, turn_K=1.0))
130 small = ManifoldBranch(instance, ManifoldPort("b", bore=0.00635, turn_K=1.0))
131 assert small.pressure_drop(0.05, GAS) == pytest.approx(
132 16.0 * big.pressure_drop(0.05, GAS), rel=1e-9
133 )
134
135
137 """Rung 6 of the K ladder: derived from the area ratio when nothing better."""
138 wide = turn_K_from_geometry(port_bore=0.0127, plenum_bore=0.0127)
139 narrow = turn_K_from_geometry(port_bore=0.0033, plenum_bore=0.0127)
140 assert narrow > turn_K_from_geometry(port_bore=0.00775, plenum_bore=0.0127) > wide
141
142
143# ----------------------------------------------------------------- the assembly
144
145
147 """The failure that stops port losses from silently vanishing."""
148 manifold = copv_manifold()
149 with pytest.raises(SpecError, match="not a single branch"):
150 manifold.pressure_drop(0.05, GAS)
151
152
154 manifold = copv_manifold()
155 net = Network()
156 net.add_node("PLENUM", "nitrogen", 293.15)
157 net.add_node("COPV", "nitrogen", 293.15, pressure=4500 * 6894.757293168361)
158 nodes = {}
159 for port in manifold.ports:
160 node_id = f"N_{port.id}"
161 net.add_node(node_id, "nitrogen", 293.15)
162 nodes[port.id] = node_id
163
164 ids = expand_manifold(net, manifold, "PLENUM", nodes)
165 assert len(ids) == len(manifold.ports)
166 assert set(ids) <= set(net.branches)
167 assert all(b.upstream == "PLENUM" for b in net.branches.values())
168
169
171 """Including the instrument ones -- that is how a tap reads plenum pressure."""
172 manifold = copv_manifold()
173 net = Network()
174 net.add_node("PLENUM", "nitrogen", 293.15)
175 net.add_node("N_reg", "nitrogen", 293.15)
176 with pytest.raises(SpecError, match="no node given for port"):
177 expand_manifold(net, manifold, "PLENUM", {"reg": "N_reg"})
178
179
181 manifold = copv_manifold()
182 net = Network()
183 net.add_node("PLENUM", "nitrogen", 293.15)
184 nodes = {p.id: f"N_{p.id}" for p in manifold.ports}
185 nodes["ghost"] = "N_ghost"
186 for node in nodes.values():
187 net.add_node(node, "nitrogen", 293.15)
188 with pytest.raises(SpecError, match="unknown port"):
189 expand_manifold(net, manifold, "PLENUM", nodes)
190
191
193 with pytest.raises(SpecError, match="duplicate port"):
194 Manifold(
195 build_component(body()).instance,
196 [ManifoldPort("pt", bore=0.003), ManifoldPort("pt", bore=0.003)],
197 )
198
199
200# ------------------------------------------------------------------- the checks
201
202
204 manifold = Manifold(
205 build_component(body()).instance,
206 [ManifoldPort("a", bore=0.0254), ManifoldPort("b", bore=0.00775)],
207 )
208 limits = [v.limit for v in manifold.check()]
209 assert "port_bore" in limits
210
211
213 manifold = Manifold(
214 build_component(body()).instance,
215 [
216 ManifoldPort("inlet", bore=0.0127),
217 ManifoldPort("pt", bore=0.003, kind=PortKind.INSTRUMENT),
218 ],
219 )
220 warnings = [v for v in manifold.check() if v.limit == "ports"]
221 assert warnings and warnings[0].severity == "warning"
222
223
225 assert copv_manifold().check() == []
ComponentInstance body(**object overrides)
None test_only_flow_ports_carry_mass()
None test_smaller_ports_cost_more()
None test_an_instrument_port_reads_plenum_pressure_exactly()
None test_a_conditional_port_is_a_stub_while_shut()
None test_a_one_outlet_manifold_is_flagged_as_a_fitting()
Manifold copv_manifold()
None test_a_manifold_refuses_to_be_one_branch()
None test_expansion_requires_a_node_for_every_port()
None test_loss_is_symmetric_in_flow_direction()
None test_expansion_rejects_an_unknown_port()
None test_a_port_wider_than_its_plenum_is_flagged()
None test_a_healthy_manifold_is_clean()
None test_duplicate_ports_are_rejected()
None test_derived_turn_K_grows_as_the_port_narrows()
None test_expansion_produces_one_branch_per_port()
None test_port_loss_is_turn_plus_fitting()
None test_a_straight_through_port_does_not_turn()
static void check(bool ok, const std::string &what)