1"""A spring-loaded pressure relief valve: shut until the pressure says otherwise.
3Until this existed a drawn ``RV`` became a plain Cv valve with no actuator, so it
4was *open* -- a permanent vent hung off whatever it was drawn on -- and the only
5overpressure protection the twin had was tripping the stand at a vessel's MAWP
6(EngineDesign/docs/layerx/AUDIT.md 5.2, 9.6 E). A drawn RV that declares a
7``set_pressure`` now builds as this; one that does not keeps the old behaviour and
8the build says it is not a relief model (:func:`feedtwin.pid.network.build_network`).
12Terms are ISO 4126-1 (*Safety devices for protection against excessive pressure
13-- Part 1: Safety valves*): **set pressure** (the valve begins to open),
14**overpressure** (the rise above set at which it reaches its rated lift, as a
15fraction of set), **blowdown** (set less the **reseating pressure**, as a
20 shut: opens once dp_act >= p_set
21 open: lift = (dp_act - p_reseat) / (p_full - p_reseat), clipped to [0, 1]
22 shuts once dp_act < p_reseat
23 p_reseat = p_set (1 - blowdown), p_full = p_set (1 + overpressure)
25``dp_act`` is the pressure **across** the valve, inlet over outlet: a
26conventional (unbalanced) spring valve's set pressure is referenced to its
27outlet, so a superimposed back pressure raises the inlet pressure it opens at.
28Discharging to atmosphere, it is the gauge pressure the valve is stamped with.
29The loop between opening at ``p_set`` and shutting at ``p_reseat`` is the
30hysteresis; inside it the lift follows the pressure, so a valve that has popped
31throttles down continuously to its seat rather than chattering between shut and
34The lift is a state, decided by the session once per coupling step from the
35pressures of the last solve (:meth:`ReliefValve.lift_for`; the session's
36``_relief_signals``), and handed to the component as the ``<id>.lift`` signal --
37the same way a solenoid's position is. A component evaluated without a session
38(a steady solve) reads no signal and is **shut**, which is a relief valve's
43Through ``Cv x lift`` (linear), and for a gas by IEC 60534-2-1 with its expansion
44factor and choke (:mod:`feedtwin.comps.iec_gas`): the rated capacity is the
45choked flow at full lift, ``W = N6 C (2/3) sqrt(F_gamma xT p1 rho1)`` at the
46relieving pressure ``p1 = p_set (1 + overpressure) + p_out``. A liquid relief
47takes the plain valve's incompressible Cv law and its IEC liquid choke.
51API 520 Part I (*Sizing, Selection, and Installation of Pressure-relieving
52Devices -- Sizing and Selection*) sizes a gas relief at critical flow as
53``W = C(k) Kd A p1 sqrt(M / (Z T))``, by orifice area and certified discharge
54coefficient; ISO 4126-1 rates the same way. This valve is sized by the Cv a
55datasheet gives instead, so ``Kd A`` is not an input. The choked capacity above
56has API 520's form in ``p1``, ``M``, ``Z`` and ``T`` (``rho1 = p1 M / (Z R T)``);
57only the dependence on ``k`` differs -- IEC's ``sqrt(F_gamma)`` against API's
58``C(k)``, which puts helium 2.9 % higher relative to nitrogen here
59(``sqrt(1.667/1.40)`` = 1.091 against ``C(1.667)/C(1.40)`` = 1.061). A relief
60whose datasheet gives ``Kd`` and an orifice area, not a Cv, needs an area model.
63from __future__
import annotations
65from typing
import Any, Mapping
67from fluids.fittings
import Cv_to_K
76MODEL_NAME =
"spring relief valve (ISO 4126-1 terms, IEC 60534-2-1 gas flow)"
78 "ISO 4126-1, Safety devices for protection against excessive pressure - "
79 "Part 1: Safety valves (set pressure, overpressure, blowdown, reseating "
80 "pressure); API 520 Part I, Sizing, Selection, and Installation of "
81 "Pressure-relieving Devices (sizing basis: choked gas capacity scaling as "
82 "p1 sqrt(M/(Z T)), here through the datasheet Cv rather than Kd A); "
83 "IEC 60534-2-1:2011, Industrial-process control valves - Part 2-1: "
84 "Flow capacity - Sizing equations for fluid flow under installed conditions "
85 "(compressible flow: Y = 1 - x/(3 F_gamma xT), choked at x = F_gamma xT)"
91 """A pressure-actuated relief valve. See the module docstring."""
97 """Differential at which it begins to open [Pa]."""
98 return float(self.
p[
"set_pressure"])
102 """Differential below which an open valve shuts [Pa]."""
103 return self.
set_pressure * (1.0 - float(self.
p.get(
"blowdown", 0.0)))
107 """Differential at which it reaches rated lift [Pa]."""
108 return self.
set_pressure * (1.0 + float(self.
p.get(
"overpressure", 0.0)))
110 def lift_for(self, dp_actuating: float, was_open: bool) -> tuple[float, bool]:
111 """``(lift, open)`` for the pressure across the valve, given its history.
113 Shut, it opens once ``dp_actuating`` reaches the set pressure; open, it
114 stays open until ``dp_actuating`` falls below the reseating pressure. The
115 lift is proportional between the reseating pressure and full lift.
127 return min(
max(lift, 0.0), 1.0),
True
131 def _lift(self, signals: Mapping[str, float]) -> float:
132 for key
in (f
"{self.id}.{LIFT}", LIFT):
134 return min(
max(float(signals[key]), 0.0), 1.0)
137 def isolates(self, signals: Mapping[str, float] |
None =
None) -> bool:
138 return self.
_lift(signals
or {}) <= SHUT_POSITION
140 def _gas(self, flow: FlowConditions) -> GasCv |
None:
144 return GasCv(cv, float(self.
p[
"bore"]), float(self.
p.get(
"xT", XT_TYPICAL)))
147 gas = self.
_gas(flow)
149 return gas.drop(mdot, flow.p_upstream, flow.rho, iec_gamma(flow))
151 bore = float(self.
p[
"bore"])
152 return Cv_to_K(Cv, bore) * _dynamic_head(mdot, bore, flow.rho)
154 def _gas_choke(self, flow: FlowConditions) -> tuple[float, float] |
None:
157 gamma = iec_gamma(flow)
158 xT = float(self.
p.get(
"xT", XT_TYPICAL))
159 return GasCv(1.0, 1.0, xT).x_critical(gamma), gamma / 1.40
162 gas = self.
_gas(flow)
165 return gas.capacity(flow.p_upstream, flow.rho, iec_gamma(flow))
168 """Choked gas flow at full lift [kg/s] for the inlet state ``flow``.
170 Pass the conditions at the relieving pressure, ``full_lift_pressure``
171 plus the outlet's, for the valve's rated capacity. ``None`` for a liquid.
177 float(self.
p[
"bore"]),
178 float(self.
p.get(
"xT", XT_TYPICAL)),
180 return gas.capacity(flow.p_upstream, flow.rho, iec_gamma(flow))
182 def diagnostics(self, mdot: float, flow: FlowConditions) -> dict[str, float]:
183 lift = self.
_lift(flow.signals)
184 out: dict[str, float] = {
193 if ceiling
is not None:
194 out[
"capacity"] = ceiling
198 out: list[Violation] = []
201 Violation(self.
id,
"set_pressure",
"set pressure is not positive")
203 blowdown = float(self.
p.get(
"blowdown", 0.0))
204 if not 0.0 <= blowdown < 1.0:
209 f
"blowdown {blowdown:g} is outside [0, 1): the valve would "
210 "reseat at or below zero",
213 for name
in (
"Cv",
"overpressure",
"blowdown",
"xT"):
214 param = self.
instance.params.get(name)
215 if param
is not None and param.source.is_assumed:
220 f
"{name} is {param.source.value}, not from the valve's "
221 "datasheet; it sets how much this relief can pass.",
229 def model(self) -> dict[str, Any]:
230 """The run record's ``model`` block for this valve."""
231 inputs: dict[str, Any] = {}
232 for name
in (
"set_pressure",
"overpressure",
"blowdown",
"Cv",
"bore",
"xT"):
233 param = self.
instance.params.get(name)
237 "value": param.value,
239 "provenance": f
"{param.source.value}: {param.reference}".rstrip(
": "),
243 "source": MODEL_SOURCE,
245 "conventional spring valve: opens on the pressure across it "
246 "(inlet over outlet), so back pressure raises the inlet set point",
247 "lift linear in pressure between the reseating pressure and full "
248 "lift at set x (1 + overpressure); Cv linear in lift",
249 "no opening or closing time: the lift follows the pressure each "
250 "coupling step (relief pop and reseat dynamics not modelled)",
251 "gas flow per IEC 60534-2-1 with the ideal-gas ratio of specific "
252 "heats; scale from the Cv definition (fluids Cv_to_K)",
253 "decided from the previous coupling step's pressures (one coupling "
254 "step of lag, milliseconds)",
260register_builder(
"relief_valve",
"spring", ReliefValve)
float|None flow_ceiling(self, FlowConditions flow)
float pressure_drop(self, float mdot, FlowConditions flow)
GasCv|None _gas(self, FlowConditions flow)
float effective_cv(self, float position)
float pressure_drop(self, float mdot, FlowConditions flow)
dict[str, float] diagnostics(self, float mdot, FlowConditions flow)
float reseat_pressure(self)
float|None flow_ceiling(self, FlowConditions flow)
GasCv|None _gas(self, FlowConditions flow)
float full_lift_pressure(self)
bool isolates(self, Mapping[str, float]|None signals=None)
list[Violation] check(self)
float _lift(self, Mapping[str, float] signals)
tuple[float, float]|None _gas_choke(self, FlowConditions flow)
tuple[float, bool] lift_for(self, float dp_actuating, bool was_open)
float|None rated_capacity(self, FlowConditions flow)