1"""Manifolds: a plenum with ports, and the three kinds of port.
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
9from __future__
import annotations
29GAS =
FlowConditions(rho=39.0, mu=1.78e-5, p_upstream=4500 * 6894.757293168361)
32def body(**overrides: object) -> ComponentInstance:
34 "bore":
Param(12.7,
"mm", Provenance.MEASURED,
"1/2 in. plenum bore"),
35 "volume":
Param(30.0,
"mL", Provenance.ESTIMATED,
"from the model"),
37 params.update(overrides)
38 return ComponentInstance.build(
"MF-01",
"manifold", params)
42 """One inlet, four takeoffs. The real hardware."""
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),
50 built = build_component(
body())
51 assert isinstance(built, Manifold)
52 return Manifold(built.instance, ports)
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"}
65 """A transducer tap has no flow, so it has no drop. Ever."""
67 port = next(p
for p
in manifold.ports
if p.id ==
"pt")
69 assert branch.pressure_drop(0.0, GAS) == 0.0
72 assert branch.pressure_drop(0.05, GAS) == 0.0
76 """A shut relief and a capped port are topologically identical.
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.
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
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)
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
105 """An in-line outlet is not a side tapping and should not be charged as one."""
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
114 """A manifold port does not care which way the gas goes through it."""
117 manifold.instance, next(p
for p
in manifold.ports
if p.id ==
"reg")
119 assert branch.pressure_drop(0.05, GAS) == pytest.approx(
120 branch.pressure_drop(-0.05, GAS)
123 assert branch.total_dp(0.05, GAS) == pytest.approx(-branch.total_dp(-0.05, GAS))
127 """Quartic in bore: halving a port is 16x the loss at the same mass flow."""
131 assert small.pressure_drop(0.05, GAS) == pytest.approx(
132 16.0 * big.pressure_drop(0.05, GAS), rel=1e-9
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
147 """The failure that stops port losses from silently vanishing."""
149 with pytest.raises(SpecError, match=
"not a single branch"):
150 manifold.pressure_drop(0.05, GAS)
156 net.add_node(
"PLENUM",
"nitrogen", 293.15)
157 net.add_node(
"COPV",
"nitrogen", 293.15, pressure=4500 * 6894.757293168361)
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
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())
171 """Including the instrument ones -- that is how a tap reads plenum pressure."""
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"})
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)
193 with pytest.raises(SpecError, match=
"duplicate port"):
195 build_component(
body()).instance,
205 build_component(
body()).instance,
208 limits = [v.limit
for v
in manifold.check()]
209 assert "port_bore" in limits
214 build_component(
body()).instance,
217 ManifoldPort(
"pt", bore=0.003, kind=PortKind.INSTRUMENT),
220 warnings = [v
for v
in manifold.check()
if v.limit ==
"ports"]
221 assert warnings
and warnings[0].severity ==
"warning"
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()
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)