1"""A valve on a gas line, at and past its critical pressure ratio.
3Every vent and every press solenoid on a stand is a valve passing gas, and until
4this existed none of them could choke: `Valve.choked_dp` is the IEC 60534 *liquid*
5flashing model and returns None for a gas, so the base class reported no ceiling
6and the incompressible Cv law ran unbounded. Measured on a 550 psi tank vented to
7atmosphere, that was 2.5x to 5.5x the sonic mass flow depending on bore.
9Below the choke a gas valve is IEC 60534-2-1 with its expansion factor too
10(``feedtwin.comps.iec_gas.GasCv``): the incompressible law up to a separate
11ceiling never met it, and the flow *fell* 36 % as the downstream did.
14from __future__
import annotations
16import fluids.control_valve
as cv_lib
26PSI = 6894.757293168361
27R_UNIVERSAL_J = 8.31446261815324 * 1000.0
28KV_PER_CV = 1.0 / 1.1560992283536566
30HELIUM = (4.0026, 1.667)
31NITROGEN = (28.014, 1.40)
34def valve(Cv: float, bore_mm: float, **params) -> object:
36 "Cv":
Param(Cv,
"Cv", Provenance.ESTIMATED),
37 "bore":
Param(bore_mm,
"mm", Provenance.MANUFACTURER),
40 return build_component(ComponentInstance.build(
"V",
"valve", fields, model=
"cv"))
43def gas(mw: float, gamma: float, p_upstream: float, temperature: float):
44 r_specific = R_UNIVERSAL_J / mw
46 rho=p_upstream / (r_specific * temperature),
48 p_upstream=p_upstream,
49 temperature=temperature,
51 r_specific=r_specific,
64def library_mdot(Cv, mw, gamma, p1, temperature, bore, p2=101325.0) -> float:
65 """What `fluids` says the same valve passes at ``p2``. The reference.
67 ``fluids`` takes Q as volume at **0 degC** and 1 atm (its docstring, and the
68 N9 it uses). This reference used to read it at 15 degC, exactly as the
69 valve's own calibration did -- so the two agreed to 1e-6 while both were
72 r_specific = R_UNIVERSAL_J / mw
73 rho_std = 101325.0 / (r_specific * 273.15)
74 sized = cv_lib.size_control_valve_g(
89 return rho_std * (Cv * KV_PER_CV) / sized[
"Kv"]
93 (
"helium 3/8in vent", 3.8, *HELIUM, 550 * PSI, 293.15, 0.00953),
94 (
"helium regulator", 1.6, *HELIUM, 4500 * PSI, 293.15, 0.00775),
95 (
"nitrogen 3/8in", 8.0, *NITROGEN, 1000 * PSI, 293.15, 0.00953),
96 (
"nitrogen cold 1/4in", 1.7, *NITROGEN, 200 * PSI, 250.0, 0.00635),
97 (
"helium 1in dump", 60.0, *HELIUM, 550 * PSI, 293.15, 0.0254),
101@pytest.mark.parametrize("name,Cv,mw,gamma,p1,T,bore", CASES)
103 """Against `fluids`' own IEC 60534 implementation, not against arithmetic.
105 The coefficient is calibrated from that library rather than transcribed,
106 because the published N6 depends on which unit set *and* which flow
107 coefficient the table is written for -- pairing N6 = 27.3 with Cv and kPa
108 over-predicts by 10.5x, which is how this was first written.
110 got =
valve(Cv, bore * 1000.0).flow_ceiling(
gas(mw, gamma, p1, T))
111 assert got == pytest.approx(
library_mdot(Cv, mw, gamma, p1, T, bore), rel=SCALE)
115 v, flow =
valve(3.8, 9.53),
gas(*HELIUM, 550 * PSI, 293.15)
116 assert v.is_choked(550 * PSI - 101325.0, flow)
is True
120 """Choking starts at x = (gamma/1.40) * xT, not at any drop."""
121 v, flow =
valve(3.8, 9.53),
gas(*HELIUM, 550 * PSI, 293.15)
122 assert v.is_choked(0.05 * 550 * PSI, flow)
is False
126 """F_gamma carries the specific-heat ratio; it is why the two gases differ."""
127 dp = 0.75 * 550 * PSI
128 assert valve(3.8, 9.53).is_choked(dp,
gas(*NITROGEN, 550 * PSI, 293.15))
is True
129 assert valve(3.8, 9.53).is_choked(dp,
gas(*HELIUM, 550 * PSI, 293.15))
is False
133 """LOX through a main valve must not acquire a sonic limit."""
135 rho=1141.0, mu=1.9e-4, p_upstream=550 * PSI, temperature=90.0
137 assert valve(26.1, 12.7).flow_ceiling(liquid)
is None
145R037_DEFERRED = pytest.mark.xfail(
146 strict=
True, reason=
"R037 deferred: liquids keep the old gas ceiling"
151@pytest.mark.parametrize(
152 "species,T", [(
"oxygen", 90.2), (
"ethanol", 293.0)], ids=[
"LOX",
"ethanol"]
155 species: str, T: float
157 """The conditions a network builds for a liquid carry the saturated
158 liquid's cp/cv (LOX 1.83, ethanol 1.19), and the old gas test (gamma > 0)
159 read that as a gas: every liquid valve got a sonic ceiling at
160 x = gamma/1.4 * 0.7, and a LOX dump to atmosphere drained at 0.69x IEC."""
161 flow = conditions_from_fluid(
Fluid(species), 300 * PSI, T)
162 assert flow.gamma > 0.0,
"the trap: a liquid's cp/cv is positive too"
164 assert v.flow_ceiling(flow)
is None
165 assert not v.is_choked(0.99 * 300 * PSI, flow)
or v.choked_dp(flow)
is not None
169@pytest.mark.parametrize(
171 [(
"oxygen", 90.2, 300.0), (
"oxygen", 90.2, 600.0), (
"ethanol", 293.0, 600.0)],
174 species: str, T: float, p1_psia: float
176 """Solved through a network, a Cv 1 valve discharging liquid to atmosphere
177 passes what fluids' IEC 60534 liquid sizing gives for the same drop
178 (non-choked form: the incompressible law is the one the twin solves; the
179 IEC liquid choke is reported by ``choked_dp``, not imposed)."""
182 net.add_node(
"tank", species, T, pressure=p1)
183 net.add_node(
"atm", species, T, pressure=101325.0)
184 for node
in net.nodes.values():
185 node.phase =
"liquid"
186 net.add_branch(
"V",
valve(1.0, 9.5),
"tank",
"atm")
187 result = solve_steady(net)
188 assert result.converged
189 flow = conditions_from_fluid(
Fluid(species), p1, T)
191 kv = cv_lib.size_control_valve_l(
204 expected = flow.rho * Q * KV_PER_CV / kv
205 assert result.flows[
"V"] == pytest.approx(expected, rel=SCALE)
208def _swept(p1: float, p2: float, Cv: float = 4.0) -> float:
209 """A Cv 4 valve between two fixed nitrogen pressures, solved."""
211 net.add_node(
"up",
"nitrogen", 293.0, pressure=p1)
212 net.add_node(
"dn",
"nitrogen", 293.0, pressure=p2)
213 for node
in net.nodes.values():
215 net.add_branch(
"V",
valve(Cv, 9.5),
"up",
"dn")
216 result = solve_steady(net)
217 assert result.converged
218 return result.flows[
"V"]
222 """IEC 60534-2-1: W = N6 C Y sqrt(x p1 rho1), Y = 1 - x/(3 F_gamma xT).
224 The plain valve used Y = 1 below a separately calibrated ceiling: at
225 500 psia, 140 psia downstream it passed 0.956 kg/s (IEC 0.628, 1.52x), and
226 at 130 psia 0.609 -- flow *falling* 36 % as the downstream fell 10 psi.
229 downstream = [450, 400, 300, 200, 160, 150, 145, 140, 135, 130, 120, 100, 14.7]
230 flows = [
_swept(p1, p2 * PSI)
for p2
in downstream]
231 for before, after
in zip(flows, flows[1:]):
232 assert after >= before * (1.0 - 1e-9),
"flow must not fall as dp rises"
233 for p2, flow
in zip(downstream, flows):
234 reference =
library_mdot(4.0, *NITROGEN, p1, 293.0, 0.0095, p2=p2 * PSI)
237 z = conditions_from_fluid(
Fluid(
"nitrogen"), p1, 293.0, phase=
"gas").rho
238 ideal = p1 / (R_UNIVERSAL_J / NITROGEN[0] * 293.0)
239 assert flow == pytest.approx(reference * (z / ideal) ** 0.5, rel=SCALE), p2
243 """Just under the critical ratio the flow is within a hair of the ceiling;
244 the old law was 50 % above it there."""
246 flow = conditions_from_fluid(
Fluid(
"nitrogen"), p1, 293.0, phase=
"gas")
248 ceiling = v.flow_ceiling(flow)
249 x_crit = (flow.gamma_ideal / 1.40) * 0.7
250 near =
_swept(p1, p1 * (1.0 - 0.999 * x_crit))
251 assert near == pytest.approx(ceiling, rel=1e-5)
252 assert near <= ceiling
256 """Nothing flows, so there is nothing to limit -- and a zero ceiling would
257 pin the branch at zero rather than let `isolates()` remove it."""
258 v =
valve(3.8, 9.53, leak_closed=
Param(0.0,
"Cv", Provenance.ESTIMATED))
259 flow =
gas(*HELIUM, 550 * PSI, 293.15)
260 object.__setattr__(flow,
"signals", {
"V.command": 0.0})
261 assert v.flow_ceiling(flow)
is None
265 """A butterfly (xT ~ 0.3) chokes lower and passes less than a globe."""
266 flow =
gas(*HELIUM, 550 * PSI, 293.15)
267 globe =
valve(3.8, 9.53).flow_ceiling(flow)
269 3.8, 9.53, xT=
Param(0.3,
"-", Provenance.MANUFACTURER)
271 assert butterfly < globe
None test_a_choked_gas_valve_meets_its_ceiling_continuously()
None test_a_liquid_valve_to_atmosphere_is_the_iec_liquid_law(str species, float T, float p1_psia)
None test_a_liquid_valve_still_has_no_gas_ceiling()
None test_xT_moves_the_ceiling()
None test_the_ceiling_matches_the_library(name, Cv, mw, gamma, p1, T, bore)
float library_mdot(Cv, mw, gamma, p1, temperature, bore, p2=101325.0)
gas(float mw, float gamma, float p_upstream, float temperature)
None test_a_gas_vent_to_atmosphere_reports_choked()
None test_a_liquid_priced_by_the_network_has_no_gas_ceiling(str species, float T)
float _swept(float p1, float p2, float Cv=4.0)
None test_helium_and_nitrogen_choke_at_different_ratios()
object valve(float Cv, float bore_mm, **params)
None test_a_shut_valve_has_no_ceiling()
None test_a_small_pressure_ratio_is_not_choked()
None test_gas_flow_rises_continuously_to_the_choke_and_stays()