STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
iec_gas.py
Go to the documentation of this file.
1"""Gas through a flow coefficient, with expansion and choking, per IEC 60534-2-1.
2
3For the components that opt in: the relief valve (:mod:`feedtwin.comps.relief`)
4always, and the regulator seat when ``Setup.regulator_compressible_seat`` is on
5(:mod:`feedtwin.comps.regulator`). The plain :class:`~feedtwin.comps.elements.Valve`
6is untouched.
7
8The standard
9------------
10IEC 60534-2-1:2011, *Industrial-process control valves -- Part 2-1: Flow
11capacity -- Sizing equations for fluid flow under installed conditions*,
12compressible fluids, turbulent flow, no fittings (F_P = 1):
13
14.. code-block:: text
15
16 W = N6 . C . Y . sqrt(x . p1 . rho1) non-choked
17 Y = 1 - x / (3 . F_gamma . xT) expansion factor
18 F_gamma = gamma / 1.40 specific heat ratio factor
19 choked once x >= F_gamma . xT, where Y = 2/3 and W stops depending on p2
20
21with ``x = (p1 - p2) / p1``. Inverting it for the drop at a given flow is a cubic
22in ``x`` (:func:`expansion_drop`); past the choke there is no drop that passes
23the flow, and the caller pins the flow at :meth:`GasCv.capacity` instead.
24
25The scale, ``N6 . C``
26---------------------
27Not the tabulated N6. The coefficient is the library's own Cv-to-K conversion,
28``fluids.fittings.Cv_to_K`` -- the Cv definition, one US gallon per minute of
29water at one psi -- so that ``Y -> 1`` reproduces exactly the incompressible law
30the rest of the library already uses for the same Cv. It agrees with IEC's
31N6 = 2.73 (Cv, kg/h, kPa) to 0.15 %. (The library's
32:func:`~feedtwin.comps.elements._calibrate_choke`, used by the plain valve's
33choked ceiling, is 5.4 % lower: it calibrates against ``fluids``'s N9, which is
34referenced to 0 degC, with a standard density taken at 15 degC. Reported, not
35changed here.)
36
37gamma
38-----
39The standard's ``gamma`` is the gas's ratio of specific heats as an ideal-gas
40property (air and nitrogen 1.40, helium 1.66), not the real-gas cp/cv at the
41inlet: for nitrogen at 1055 psia and 200 K, near a GN2 burnout, cp/cv is 1.84
42(CoolProp), and using it would raise the flow at the drop seen there by 6 %.
43:func:`iec_gamma` takes ``FlowConditions.gamma_ideal`` -- cp/cv at 1 kPa and the
44node temperature, which :func:`~feedtwin.comps.elements.conditions_from_fluid`
45fills for every gas-priced node -- and falls back to the real-gas value only when
46the caller built conditions by hand without it.
47"""
48
49from __future__ import annotations
50
51import math
52from dataclasses import dataclass
53
54from fluids.fittings import Cv_to_K
55
56from feedtwin.comps.base import FlowConditions
57
58#: IEC 60534-2-1's typical pressure-differential ratio factor at choked flow,
59#: for a valve that does not state its own. **Assumed**: it is the single
60#: largest uncertainty in a choked-gas prediction (about 0.3 for a butterfly to
61#: 0.8 for some globe trims).
62XT_TYPICAL = 0.70
63
64#: Reference pressure the ideal-gas ratio of specific heats is taken at [Pa].
65GAMMA_IDEAL_PRESSURE = 1.0e3
66
67
68def iec_gamma(flow: FlowConditions) -> float:
69 """The ratio of specific heats IEC 60534's F_gamma is built from: the
70 ideal-gas value when the conditions carry it, else the real-gas cp/cv."""
71 return flow.gamma_ideal if flow.gamma_ideal > 0.0 else flow.gamma
72
73
74def is_gas(flow: FlowConditions) -> bool:
75 """Whether these conditions were priced as a gas.
76
77 :func:`~feedtwin.comps.elements.conditions_from_fluid` fills
78 ``gamma_ideal`` only for a gas-priced node; a liquid leaves it zero and
79 keeps the incompressible law.
80 """
81 return flow.gamma_ideal > 0.0 and flow.rho > 0.0 and flow.p_upstream > 0.0
82
83
84def expansion_drop(a: float, x_critical: float) -> float:
85 """``x`` in ``[0, x_critical]`` solving ``x . Y(x)^2 = a``.
86
87 ``Y = 1 - x / (3 x_critical)``. The left side rises monotonically from 0 to
88 ``4 x_critical / 9`` at the choke; ``a`` at or past that returns the choke.
89 Newton on a bracket, bisecting whenever a step leaves it.
90 """
91 if a <= 0.0:
92 return 0.0
93 c = x_critical
94 if a >= 4.0 * c / 9.0:
95 return c
96 lo, hi = 0.0, c
97 x = min(a, c)
98 for _ in range(100):
99 y = 1.0 - x / (3.0 * c)
100 residual = x * y * y - a
101 if residual > 0.0:
102 hi = x
103 else:
104 lo = x
105 slope = y * (1.0 - x / c)
106 candidate = x - residual / slope if slope > 0.0 else 0.5 * (lo + hi)
107 if not lo < candidate < hi:
108 candidate = 0.5 * (lo + hi)
109 if abs(candidate - x) <= 1e-15 * c + 1e-300:
110 return candidate
111 x = candidate
112 return x
113
114
115@dataclass(frozen=True, slots=True)
116class GasCv:
117 """A flow coefficient seen by a gas: IEC 60534-2-1 with the library's scale."""
118
119 cv: float
120 """Flow coefficient, US gpm of water at 1 psi."""
121 bore: float
122 """The bore the Cv's K is referenced to [m] (cancels out of the flow)."""
123 xT: float
124 """Pressure-differential ratio factor at choked flow."""
125
126 def _scale(self, rho1: float) -> float:
127 """``N6 . C . sqrt(rho1)`` in SI: kg/s per sqrt(Pa) at Y = 1."""
128 if self.cv <= 0.0 or self.bore <= 0.0 or rho1 <= 0.0:
129 return 0.0
130 area = math.pi * self.bore * self.bore / 4.0
131 return area * math.sqrt(2.0 * rho1 / Cv_to_K(self.cv, self.bore))
132
133 def x_critical(self, gamma: float) -> float:
134 """``F_gamma . xT``: the drop ratio at which the flow chokes, never past
135 1 (a downstream pressure below zero)."""
136 return min((gamma / 1.40) * self.xT, 1.0)
137
138 def flow(self, p1: float, rho1: float, gamma: float, dp: float) -> float:
139 """Mass flow at a drop ``dp`` [kg/s]; the choked value past the choke."""
140 if dp <= 0.0 or p1 <= 0.0:
141 return 0.0
142 c = self.x_critical(gamma)
143 x = min(dp / p1, c)
144 y = 1.0 - x / (3.0 * c)
145 return self._scale(rho1) * y * math.sqrt(x * p1)
146
147 def capacity(self, p1: float, rho1: float, gamma: float) -> float:
148 """Choked mass flow [kg/s]: ``Y = 2/3`` at ``x = F_gamma . xT``."""
149 c = self.x_critical(gamma)
150 return self._scale(rho1) * (2.0 / 3.0) * math.sqrt(c * max(p1, 0.0))
151
152 def drop(self, mdot: float, p1: float, rho1: float, gamma: float) -> float:
153 """Pressure drop that passes ``|mdot|`` [Pa].
154
155 Up to the choked capacity this is the inverse of :meth:`flow`. Past it no
156 drop passes the flow -- the caller should pin it at :meth:`capacity` --
157 and the value returned there is ``x_crit p1 (mdot / capacity)^2``:
158 continuous at the choke and rising, so a solver iterate that strays past
159 it is pushed back rather than handed a cliff.
160 """
161 magnitude = abs(mdot)
162 scale = self._scale(rho1)
163 if magnitude == 0.0 or scale <= 0.0 or p1 <= 0.0:
164 return 0.0
165 c = self.x_critical(gamma)
166 choked = scale * (2.0 / 3.0) * math.sqrt(c * p1)
167 if magnitude >= choked:
168 return c * p1 * (magnitude / choked) ** 2
169 a = (magnitude / scale) ** 2 / p1
170 return expansion_drop(a, c) * p1
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
float flow(self, float p1, float rho1, float gamma, float dp)
Definition iec_gas.py:138
float drop(self, float mdot, float p1, float rho1, float gamma)
Definition iec_gas.py:152
float _scale(self, float rho1)
Definition iec_gas.py:126
float x_critical(self, float gamma)
Definition iec_gas.py:133
float capacity(self, float p1, float rho1, float gamma)
Definition iec_gas.py:147
float iec_gamma(FlowConditions flow)
Definition iec_gas.py:68
float expansion_drop(float a, float x_critical)
Definition iec_gas.py:84
bool is_gas(FlowConditions flow)
Definition iec_gas.py:74