1"""Compressible flow: the pressurant path.
3The liquid side is nearly incompressible, so a loss coefficient and a dynamic
4head describe it. Gas is not, and two things change:
6**Density falls as pressure does.** A long gas line is not one density; the
7velocity climbs along it as the gas expands. Below roughly 10% pressure drop the
8error in ignoring that is small, and above it the line has to be integrated.
10**Flow chokes.** Below a critical pressure ratio the throat goes sonic and mass
11flow stops depending on downstream pressure entirely. That is not a numerical
12inconvenience -- it is how a regulator seat and a relief orifice actually behave,
13and it puts a hard ceiling on what a restriction can pass. Asked for more than
14that ceiling, this raises :class:`~feedtwin.comps.base.InfeasibleOperatingPoint`
15rather than returning a pressure that cannot exist.
17Everything here is expressed in the same direction as the liquid components --
18pressure drop as a function of mass flow -- so the network solver needs to know
19nothing about which side of the system it is assembling.
22from __future__
import annotations
26from scipy.optimize
import brentq
32 InfeasibleOperatingPoint,
40__all__ = [
"R_UNIVERSAL"]
44 """``p_throat / p_upstream`` at which the throat goes sonic.
46 About 0.528 for a diatomic gas, 0.487 for helium. Below this the throat is
47 choked and downstream pressure stops mattering.
49 return float((2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0)))
60 """The most a restriction can pass at this upstream state [kg/s].
62 ``mdot = Cd A p1 sqrt(gamma / (R T)) . (2/(gamma+1))^((gamma+1)/(2(gamma-1)))``
64 A hard ceiling, not an asymptote. Opening the downstream side further buys
65 nothing once this is reached, which is why a relief orifice is sized on it
66 and why a regulator seat behaves the way it does.
68 if area <= 0.0
or temperature <= 0.0
or r_specific <= 0.0:
70 factor = (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
72 cd * area * p_upstream * math.sqrt(gamma / (r_specific * temperature)) * factor
80 pressure_ratio: float,
85 """Mass flow through an unchoked restriction [kg/s]."""
86 if pressure_ratio >= 1.0
or area <= 0.0:
88 term = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** ((gamma + 1.0) / gamma)
91 coefficient = (2.0 * gamma) / ((gamma - 1.0) * r_specific * temperature)
92 return cd * area * p_upstream * math.sqrt(coefficient * term)
96 """A restriction in compressible flow: choked above a pressure ratio.
98 Inverted from the usual direction. The textbook relation gives mass flow
99 from a pressure ratio; a network solve carries flows and wants pressures, so
100 this brackets the pressure ratio between choking and unity and solves for
101 the one that passes the requested flow.
103 The bracket is what makes it robust: the relation is monotonic over that
104 interval, so a root always exists unless the flow exceeds the choked
105 ceiling -- and that case is reported as infeasible rather than iterated on.
110 return math.pi * d * d / 4.0, self.
p[
"Cd"]
112 def _gas(self, flow: FlowConditions) -> tuple[float, float, float]:
113 """gamma, specific gas constant, temperature -- from the conditions.
115 ``FlowConditions`` carries what a liquid needs; a gas also needs the
116 heat capacity ratio and a gas constant. Both are derived from what is
117 already there rather than added to the struct, so the liquid components
120 gamma = flow.gamma
if flow.gamma > 1.0
else 1.4
121 r_specific = flow.r_specific
if flow.r_specific > 0.0
else 296.8
124 if flow.temperature > 0.0
125 else flow.p_upstream / (flow.rho * r_specific)
127 return gamma, r_specific, temperature
131 gamma, r_specific, temperature = self.
_gas(flow)
133 area, cd, flow.p_upstream, temperature, gamma, r_specific
139 def is_choked(self, dp_available: float, flow: FlowConditions) -> bool:
140 gamma, _r, _t = self.
_gas(flow)
142 return dp_available >= critical
145 """The drop at which the throat goes sonic [Pa]. About 47% of p_up."""
146 gamma, _r, _t = self.
_gas(flow)
150 magnitude = abs(mdot)
155 gamma, r_specific, temperature = self.
_gas(flow)
157 area, cd, flow.p_upstream, temperature, gamma, r_specific
159 if magnitude >= ceiling:
164 def residual(pr: float) -> float:
167 area, cd, flow.p_upstream, pr, temperature, gamma, r_specific
173 ratio = brentq(residual, pr_crit, 1.0 - 1e-12, xtol=1e-12)
174 return flow.p_upstream * (1.0 - float(ratio))
176 def diagnostics(self, mdot: float, flow: FlowConditions) -> dict[str, float]:
177 gamma, _r, temperature = self.
_gas(flow)
180 "mdot_choked": ceiling,
181 "choked_fraction": abs(mdot) / ceiling
if ceiling > 0.0
else 0.0,
184 "temperature": temperature,
185 "velocity_throat": velocity(mdot, self.
p[
"bore"], flow.rho),
189 except InfeasibleOperatingPoint:
190 out[
"dp"] = float(
"nan")
203register_builder(
"gas_orifice",
"isentropic", GasOrifice)
204register_builder(
"gas_orifice",
"measured", _measured_gas)
float pressure_drop(self, float mdot, FlowConditions flow)
dict[str, float] diagnostics(self, float mdot, FlowConditions flow)
bool is_choked(self, float dp_available, FlowConditions flow)
tuple[float, float, float] _gas(self, FlowConditions flow)
float|None flow_ceiling(self, FlowConditions flow)
float choking_pressure_drop(self, FlowConditions flow)
float choked_flow(self, FlowConditions flow)
float pressure_drop(self, float mdot, FlowConditions flow)
tuple[float, float] _geometry(self)
float subsonic_mass_flow(float area, float cd, float p_upstream, float pressure_ratio, float temperature, float gamma, float r_specific)
float critical_pressure_ratio(float gamma)
float choked_mass_flow(float area, float cd, float p_upstream, float temperature, float gamma, float r_specific)
HydraulicComponent _measured_gas(object instance)