STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
gas.py
Go to the documentation of this file.
1"""Compressible flow: the pressurant path.
2
3The liquid side is nearly incompressible, so a loss coefficient and a dynamic
4head describe it. Gas is not, and two things change:
5
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.
9
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.
16
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.
20"""
21
22from __future__ import annotations
23
24import math
25
26from scipy.optimize import brentq
27
28from feedtwin.comps.base import (
29 R_UNIVERSAL,
30 FlowConditions,
31 HydraulicComponent,
32 InfeasibleOperatingPoint,
33 register_builder,
34)
35from feedtwin.comps.correlations import velocity
36
37#: Re-exported. It is defined in :mod:`feedtwin.comps.base` so that
38#: :mod:`feedtwin.comps.elements` can reach it without a per-call import --
39#: this module imports ``elements``, so ``elements`` cannot import this one.
40__all__ = ["R_UNIVERSAL"]
41
42
43def critical_pressure_ratio(gamma: float) -> float:
44 """``p_throat / p_upstream`` at which the throat goes sonic.
45
46 About 0.528 for a diatomic gas, 0.487 for helium. Below this the throat is
47 choked and downstream pressure stops mattering.
48 """
49 return float((2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0)))
50
51
53 area: float,
54 cd: float,
55 p_upstream: float,
56 temperature: float,
57 gamma: float,
58 r_specific: float,
59) -> float:
60 """The most a restriction can pass at this upstream state [kg/s].
61
62 ``mdot = Cd A p1 sqrt(gamma / (R T)) . (2/(gamma+1))^((gamma+1)/(2(gamma-1)))``
63
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.
67 """
68 if area <= 0.0 or temperature <= 0.0 or r_specific <= 0.0:
69 return 0.0
70 factor = (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
71 return float(
72 cd * area * p_upstream * math.sqrt(gamma / (r_specific * temperature)) * factor
73 )
74
75
77 area: float,
78 cd: float,
79 p_upstream: float,
80 pressure_ratio: float,
81 temperature: float,
82 gamma: float,
83 r_specific: float,
84) -> float:
85 """Mass flow through an unchoked restriction [kg/s]."""
86 if pressure_ratio >= 1.0 or area <= 0.0:
87 return 0.0
88 term = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** ((gamma + 1.0) / gamma)
89 if term <= 0.0:
90 return 0.0
91 coefficient = (2.0 * gamma) / ((gamma - 1.0) * r_specific * temperature)
92 return cd * area * p_upstream * math.sqrt(coefficient * term)
93
94
96 """A restriction in compressible flow: choked above a pressure ratio.
97
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.
102
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.
106 """
107
108 def _geometry(self) -> tuple[float, float]:
109 d = self.p["bore"]
110 return math.pi * d * d / 4.0, self.p["Cd"]
111
112 def _gas(self, flow: FlowConditions) -> tuple[float, float, float]:
113 """gamma, specific gas constant, temperature -- from the conditions.
114
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
118 are untouched.
119 """
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
122 temperature = (
123 flow.temperature
124 if flow.temperature > 0.0
125 else flow.p_upstream / (flow.rho * r_specific)
126 )
127 return gamma, r_specific, temperature
128
129 def choked_flow(self, flow: FlowConditions) -> float:
130 area, cd = self._geometry()
131 gamma, r_specific, temperature = self._gas(flow)
132 return choked_mass_flow(
133 area, cd, flow.p_upstream, temperature, gamma, r_specific
134 )
135
136 def flow_ceiling(self, flow: FlowConditions) -> float | None:
137 return self.choked_flow(flow)
138
139 def is_choked(self, dp_available: float, flow: FlowConditions) -> bool:
140 gamma, _r, _t = self._gas(flow)
141 critical = flow.p_upstream * (1.0 - critical_pressure_ratio(gamma))
142 return dp_available >= critical
143
144 def choking_pressure_drop(self, flow: FlowConditions) -> float:
145 """The drop at which the throat goes sonic [Pa]. About 47% of p_up."""
146 gamma, _r, _t = self._gas(flow)
147 return flow.p_upstream * (1.0 - critical_pressure_ratio(gamma))
148
149 def pressure_drop(self, mdot: float, flow: FlowConditions) -> float:
150 magnitude = abs(mdot)
151 if magnitude == 0.0:
152 return 0.0
153
154 area, cd = self._geometry()
155 gamma, r_specific, temperature = self._gas(flow)
156 ceiling = choked_mass_flow(
157 area, cd, flow.p_upstream, temperature, gamma, r_specific
158 )
159 if magnitude >= ceiling:
160 raise InfeasibleOperatingPoint(self.id, mdot, flow.p_upstream)
161
162 pr_crit = critical_pressure_ratio(gamma)
163
164 def residual(pr: float) -> float:
165 return (
167 area, cd, flow.p_upstream, pr, temperature, gamma, r_specific
168 )
169 - magnitude
170 )
171
172 # Monotonic between choking and no-flow, so a bracket is guaranteed.
173 ratio = brentq(residual, pr_crit, 1.0 - 1e-12, xtol=1e-12)
174 return flow.p_upstream * (1.0 - float(ratio))
175
176 def diagnostics(self, mdot: float, flow: FlowConditions) -> dict[str, float]:
177 gamma, _r, temperature = self._gas(flow)
178 ceiling = self.choked_flow(flow)
179 out = {
180 "mdot_choked": ceiling,
181 "choked_fraction": abs(mdot) / ceiling if ceiling > 0.0 else 0.0,
182 "pressure_ratio_critical": critical_pressure_ratio(gamma),
183 "gamma": gamma,
184 "temperature": temperature,
185 "velocity_throat": velocity(mdot, self.p["bore"], flow.rho),
186 }
187 try:
188 out["dp"] = self.pressure_drop(mdot, flow)
189 except InfeasibleOperatingPoint:
190 out["dp"] = float("nan")
191 out["choked"] = 1.0
192 else:
193 out["choked"] = 0.0
194 return out
195
196
197def _measured_gas(instance: object) -> HydraulicComponent:
198 from feedtwin.comps.elements import MeasuredElement
199
200 return MeasuredElement(instance) # type: ignore[arg-type]
201
202
203register_builder("gas_orifice", "isentropic", GasOrifice)
204register_builder("gas_orifice", "measured", _measured_gas)
float pressure_drop(self, float mdot, FlowConditions flow)
Definition base.py:190
dict[str, float] diagnostics(self, float mdot, FlowConditions flow)
Definition gas.py:176
bool is_choked(self, float dp_available, FlowConditions flow)
Definition gas.py:139
tuple[float, float, float] _gas(self, FlowConditions flow)
Definition gas.py:112
float|None flow_ceiling(self, FlowConditions flow)
Definition gas.py:136
float choking_pressure_drop(self, FlowConditions flow)
Definition gas.py:144
float choked_flow(self, FlowConditions flow)
Definition gas.py:129
float pressure_drop(self, float mdot, FlowConditions flow)
Definition gas.py:149
tuple[float, float] _geometry(self)
Definition gas.py:108
float subsonic_mass_flow(float area, float cd, float p_upstream, float pressure_ratio, float temperature, float gamma, float r_specific)
Definition gas.py:84
float critical_pressure_ratio(float gamma)
Definition gas.py:43
float choked_mass_flow(float area, float cd, float p_upstream, float temperature, float gamma, float r_specific)
Definition gas.py:59
HydraulicComponent _measured_gas(object instance)
Definition gas.py:197