1"""Gas through a flow coefficient, with expansion and choking, per IEC 60534-2-1.
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`
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):
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
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.
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
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.
49from __future__
import annotations
52from dataclasses
import dataclass
54from fluids.fittings
import Cv_to_K
65GAMMA_IDEAL_PRESSURE = 1.0e3
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
74def is_gas(flow: FlowConditions) -> bool:
75 """Whether these conditions were priced as a gas.
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.
81 return flow.gamma_ideal > 0.0
and flow.rho > 0.0
and flow.p_upstream > 0.0
85 """``x`` in ``[0, x_critical]`` solving ``x . Y(x)^2 = a``.
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.
94 if a >= 4.0 * c / 9.0:
99 y = 1.0 - x / (3.0 * c)
100 residual = x * y * y - a
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:
115@dataclass(frozen=True, slots=True)
117 """A flow coefficient seen by a gas: IEC 60534-2-1 with the library's scale."""
120 """Flow coefficient, US gpm of water at 1 psi."""
122 """The bore the Cv's K is referenced to [m] (cancels out of the flow)."""
124 """Pressure-differential ratio factor at choked flow."""
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:
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))
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)
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:
144 y = 1.0 - x / (3.0 * c)
145 return self.
_scale(rho1) * y * math.sqrt(x * p1)
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``."""
150 return self.
_scale(rho1) * (2.0 / 3.0) * math.sqrt(c *
max(p1, 0.0))
152 def drop(self, mdot: float, p1: float, rho1: float, gamma: float) -> float:
153 """Pressure drop that passes ``|mdot|`` [Pa].
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.
161 magnitude = abs(mdot)
163 if magnitude == 0.0
or scale <= 0.0
or p1 <= 0.0:
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
float flow(self, float p1, float rho1, float gamma, float dp)
float drop(self, float mdot, float p1, float rho1, float gamma)
float _scale(self, float rho1)
float x_critical(self, float gamma)
float capacity(self, float p1, float rho1, float gamma)
float iec_gamma(FlowConditions flow)
float expansion_drop(float a, float x_critical)
bool is_gas(FlowConditions flow)