# -*- coding: utf-8
"""Module of class Valve.
This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
by the contributors recorded in the version control history of the file,
available from its original location tespy/components/piping.py
SPDX-License-Identifier: MIT
"""
import numpy as np
from tespy.components.component import Component
from tespy.components.component import component_registry
from tespy.tools import logger
from tespy.tools.data_containers import ComponentCharacteristics as dc_cc
from tespy.tools.data_containers import ComponentMandatoryConstraints as dc_cmc
from tespy.tools.data_containers import ComponentProperties as dc_cp
from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp
from tespy.tools.data_containers import SimpleDataContainer as dc_simple
from tespy.tools.fluid_properties import single_fluid
[docs]
@component_registry
class Valve(Component):
r"""
The Valve throttles a fluid without changing enthalpy.
**Mandatory Equations**
- fluid: :py:meth:`tespy.components.component.Component.variable_equality_structure_matrix`
- mass flow: :py:meth:`tespy.components.component.Component.variable_equality_structure_matrix`
**Optional Equations**
- :py:meth:`tespy.components.component.Component.dp_structure_matrix`
- :py:meth:`tespy.components.component.Component.pr_structure_matrix`
- :py:meth:`tespy.components.component.Component.zeta_func`
- :py:meth:`tespy.components.piping.valve.Valve.dp_char_func`
Inlets/Outlets
- in1
- out1
Image
.. image:: /api/_images/Valve.svg
:alt: flowsheet of the valve
:align: center
:class: only-light
.. image:: /api/_images/Valve_darkmode.svg
:alt: flowsheet of the valve
:align: center
:class: only-dark
Parameters
----------
label : str
The label of the component.
design : list
List containing design parameters (stated as String).
offdesign : list
List containing offdesign parameters (stated as String).
design_path : str
Path to the components design case.
local_offdesign : boolean
Treat this component in offdesign mode in a design calculation.
local_design : boolean
Treat this component in design mode in an offdesign calculation.
char_warnings : boolean
Ignore warnings on default characteristics usage for this component.
printout : boolean
Include this component in the network's results printout.
pr : float, dict, :code:`"var"`
Outlet to inlet pressure ratio, :math:`pr/1`
zeta : float, dict, :code:`"var"`
Geometry independent friction coefficient,
:math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`.
dp_char : tespy.tools.characteristics.CharLine, dict
Characteristic line for difference pressure to mass flow.
Example
-------
A mass flow of 1 kg/s methane is throttled from 80 bar to 15 bar in a
valve. The inlet temperature is at 50 °C. It is possible to determine the
outlet temperature as the throttling does not change enthalpy.
>>> from tespy.components import Sink, Source, Valve
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> nw = Network(iterinfo=False)
>>> nw.units.set_defaults(**{
... "pressure": "bar", "pressure_difference": "bar",
... "temperature": "degC"
... })
>>> so = Source('source')
>>> si = Sink('sink')
>>> v = Valve('valve')
>>> so_v = Connection(so, 'out1', v, 'in1')
>>> v_si = Connection(v, 'out1', si, 'in1')
>>> nw.add_conns(so_v, v_si)
>>> v.set_attr(offdesign=['zeta'])
>>> so_v.set_attr(fluid={'CH4': 1}, m=1, T=50, p=80, design=['m'])
>>> v_si.set_attr(p=15)
>>> nw.solve('design')
>>> design_state = nw.save(as_dict=True)
>>> round(v_si.T.val, 1)
26.3
>>> round(v.pr.val, 3)
0.188
The simulation determined the area independent zeta value
:math:`\frac{\zeta}{D^4}`. This zeta remains constant if the cross
sectional area of the valve opening does not change. Using the zeta value
we can determine the pressure ratio at a different feed pressure.
>>> so_v.set_attr(p=70)
>>> nw.solve('offdesign', design_path=design_state)
>>> round(so_v.m.val, 1)
0.9
>>> round(v_si.T.val, 1)
30.0
You can also specify the flow coefficient of the valve :code:`Kv` which is
used in context of liquids. For this there are several methods available:
- direct specification with :code:`Kv`
- lookup table specification :math:`Kv=f\left(opening\right)` with
:code:`Kv_char` and :code:`opening`
- arbitrary function specification :math:`Kv=f\left(opening, params\right)`
with :code:`Kv_analytical` and :code:`opening`
>>> nw = Network(iterinfo=False)
>>> nw.units.set_defaults(**{
... "pressure": "bar", "pressure_difference": "bar",
... "temperature": "degC"
... })
>>> so = Source('source')
>>> si = Sink('sink')
>>> v = Valve('valve')
>>> so_v = Connection(so, 'out1', v, 'in1')
>>> v_si = Connection(v, 'out1', si, 'in1')
>>> nw.add_conns(so_v, v_si)
>>> so_v.set_attr(fluid={'water': 1}, T=50, p=5)
>>> v_si.set_attr(p=4)
First we specify Kv:
>>> v.set_attr(Kv=10)
>>> nw.solve('design')
>>> round(so_v.v.val, 4)
0.0028
Then, for example an analytical function:
>>> def analytical(opening, *params):
... return params[0] * opening
>>> v.set_attr(
... Kv=None,
... opening=0.5,
... Kv_analytical={"method": analytical, "params": [10]}
... )
>>> nw.solve("design")
>>> round(v.Kv.val_SI, 1)
5.0
Or, use the :code:`Kv_char`, which is a :code:`CharLine` instance:
>>> from tespy.tools import CharLine
>>> kv_data = np.array([
... 0.09,0.63,1.1,2.1,3.1,4.2,5.2,6.2,7.2,8.2,9.2,10.3,11.3
... ])
>>> opening_data = np.array([0,5,10,20,30,40,50,60,70,80,90,100,110]) / 100
>>> Kv_char = {
... "char_func": CharLine(x=opening_data, y=kv_data) , "is_set": True
... }
>>> v.set_attr(Kv_char=Kv_char, Kv_analytical=None)
>>> nw.solve("design")
>>> round(v.Kv.val, 1)
5.2
"""
[docs]
def get_parameters(self):
return {
'pr': dc_cp(
min_val=1e-4, max_val=1, num_eq_sets=1,
structure_matrix=self.pr_structure_matrix,
func_params={'pr': 'pr'},
quantity="ratio",
description="outlet to inlet pressure ratio",
calc=self._calc_pr
),
'dp': dc_cp(
min_val=0,
num_eq_sets=1,
structure_matrix=self.dp_structure_matrix,
func_params={"inconn": 0, "outconn": 0, "dp": "dp"},
quantity="pressure_difference",
description="inlet to outlet absolute pressure change",
calc=self._calc_dp
),
'zeta': dc_cp(
min_val=0, max_val=1e15, num_eq_sets=1,
func=self.zeta_func,
dependents=self.zeta_dependents,
func_params={'zeta': 'zeta'},
description="non-dimensional friction coefficient for pressure loss calculation",
calc=self._calc_zeta
),
'dp_char': dc_cc(
param='m', num_eq_sets=1,
dependents=self.dp_char_dependents,
func=self.dp_char_func,
char_params={'type': 'abs'},
description="inlet to outlet absolute pressure change as function of mass flow lookup table"
),
'Kv': dc_cp(
min_val=0, max_val=1e15, num_eq_sets=1,
func=self.Kv_func,
dependents=self.Kv_dependents,
description="flow coefficient in m3/h",
calc=self._calc_Kv
),
'Kv_char': dc_cc(
description="lookup-table data for flow coefficient as function of opening"
),
'opening': dc_cp(
# opening can be more than 100 % sometimes
min_val=0, max_val=1.1,
_potential_var=True,
description="opening ratio of the valve",
quantity="ratio"
),
'Kv_char_group': dc_gcp(
num_eq_sets=1,
elements=["Kv_char", "opening"],
func=self.Kv_char_func,
dependents=self.Kv_char_dependents,
description="equation for flow coefficient over opening"
),
'Kv_analytical': dc_simple(
description=(
"fitting parameters and method for the analytical Kv "
"evaluation provided in a dictionary with keys 'method' "
"(callable) and 'params' (list)"
)
),
'Kv_char_analytical_group': dc_gcp(
num_eq_sets=1,
elements=["Kv_analytical", "opening"],
func=self.Kv_char_analytical_func,
dependents=self.Kv_char_analytical_dependents
),
}
[docs]
def get_mandatory_constraints(self):
constraints = super().get_mandatory_constraints()
constraints.update({
'enthalpy_constraints': dc_cmc(**{
'structure_matrix': self.variable_equality_structure_matrix,
'num_eq_sets': 1,
'func_params': {'variable': 'h'},
"description": "equation for enthalpy equality"
})
})
return constraints
[docs]
def get_bypass_constraints(self):
return {
'mass_flow_constraints': dc_cmc(**{
'structure_matrix': self.variable_equality_structure_matrix,
'num_eq_sets': self.num_i,
'func_params': {'variable': 'm'}
}),
'pressure_constraints': dc_cmc(**{
'structure_matrix': self.variable_equality_structure_matrix,
'num_eq_sets': self.num_i,
'func_params': {'variable': 'p'}
}),
'enthalpy_constraints': dc_cmc(**{
'structure_matrix': self.variable_equality_structure_matrix,
'num_eq_sets': self.num_i,
'func_params': {'variable': 'h'}
}),
'fluid_constraints': dc_cmc(**{
'structure_matrix': self.variable_equality_structure_matrix,
'num_eq_sets': self.num_i,
'func_params': {'variable': 'fluid'}
})
}
[docs]
@staticmethod
def inlets():
return ['in1']
[docs]
@staticmethod
def outlets():
return ['out1']
[docs]
def dp_char_func(self):
r"""
Equation for characteristic line of difference pressure to mass flow.
Returns
-------
float
Residual value of equation.
.. math::
0=p_\text{in}-p_\text{out}-f\left( expr \right)
"""
p = self.dp_char.param
expr = self.get_char_expr(p, **self.dp_char.char_params)
if not expr:
msg = (
"Please choose a valid parameter for the usage of the "
f"'dp_char_func' of the component {self.label}."
)
logger.error(msg)
raise ValueError(msg)
return (
self.inl[0].p.val_SI - self.outl[0].p.val_SI
- self.dp_char.char_func.evaluate(expr)
)
[docs]
def dp_char_dependents(self):
dependents = [
self.inl[0].m,
self.inl[0].p,
self.outl[0].p,
]
if self.dp_char.param == 'v':
dependents += [self.inl[0].h]
return dependents
def _Kv_eq(self, Kv):
# 1000 * delta p (bar) is 1000 * delta p / 100000, simplified to
# delta p / 100
# (vol * m * 3600) ** 2 / vol simplified to
# (m * 3600) ** 2 * vol
return (
Kv ** 2 * (self.inl[0].p.val_SI - self.outl[0].p.val_SI) / 1e2
- self.inl[0].calc_vol() * (self.inl[0].m.val_SI * 3600) ** 2
)
[docs]
def Kv_func(self):
r"""
Equation for Kv value of a Valve
The equation is as follows:
.. math::
K_v=\dot V \cdot \sqrt{\frac{\rho}{1000\cdot \Delta p}}
The residual is reformulated as below:
Returns
-------
float
Residual value of equation.
.. math::
0=K_v ^ 2 \cdot \frac{\Delta p}{100}
-\frac{\left(3600 \cdot \dot m \right) ^ 2}{\rho}
"""
Kv = self.Kv.val_SI
return self._Kv_eq(Kv)
[docs]
def Kv_dependents(self):
return [self.inl[0].m, self.inl[0].p, self.inl[0].h, self.outl[0].p]
[docs]
def Kv_char_func(self):
r"""
Equation for Kv characteristic of a Valve opening
:math:`K_v=f\left(opening\right)`
Kv is determined from the degree of opening with a lookup table, the
Kv equation is then applied:
.. math::
K_v=\dot V \cdot \sqrt{\frac{\rho}{1000\cdot \Delta p}}
The residual is reformulated as below:
Returns
-------
float
Residual value of equation.
.. math::
0=K_v ^ 2 \cdot \frac{\Delta p}{100}
-\frac{\left(3600 \cdot \dot m \right) ^ 2}{\rho}
"""
Kv = self.Kv_char.char_func.evaluate(self.opening.val_SI)
return self._Kv_eq(Kv)
[docs]
def Kv_char_dependents(self):
return [
self.inl[0].m, self.inl[0].p, self.inl[0].h, self.outl[0].p,
self.opening
]
[docs]
def Kv_char_analytical_func(self):
r"""
Equation for Kv characteristic of a Valve opening
:math:`K_v=f\left(opening\right)`
Kv is determined from the method supplied by the user in the
:code:`Kv_analytical` specification and the additional parameters next
to the opening. The method must accept parameters in the following way:
:code:`method(opening, *params)`
.. math::
K_v=\dot V \cdot \sqrt{\frac{\rho}{1000\cdot \Delta p}}
The residual is reformulated as below:
Returns
-------
float
Residual value of equation.
.. math::
0=K_v ^ 2 \cdot \frac{\Delta p}{100}
-\frac{\left(3600 \cdot \dot m \right) ^ 2}{\rho}
"""
params = self.Kv_analytical.val["params"]
method = self.Kv_analytical.val["method"]
opening = self.opening.val_SI
Kv = method(opening, *params)
return self._Kv_eq(Kv)
[docs]
def Kv_char_analytical_dependents(self):
return [
self.inl[0].m, self.inl[0].p, self.inl[0].h, self.outl[0].p,
self.opening
]
def _calc_Kv(self):
i = self.inl[0]
fluid = single_fluid(i.fluid_data)
if fluid is not None and self.dp.val_SI > 0 and i.calc_phase() == "l":
return i.v.val_SI * 3600 * (100 / (i.vol.val_SI * self.dp.val_SI)) ** 0.5
return np.nan
[docs]
def entropy_balance(self):
r"""
Calculate entropy balance of a valve.
Note
----
The entropy balance makes the following parameter available:
.. math::
\text{S\_irr}=\dot{m} \cdot \left(s_\text{out}-s_\text{in}
\right)\\
"""
self.S_irr = self.inl[0].m.val_SI * (
self.outl[0].s.val_SI - self.inl[0].s.val_SI
)
[docs]
def get_plotting_data(self):
"""Generate a dictionary containing FluProDia plotting information.
Returns
-------
data : dict
A nested dictionary containing the keywords required by the
:code:`calc_individual_isoline` method of the
:code:`FluidPropertyDiagram` class. First level keys are the
connection index ('in1' -> 'out1', therefore :code:`1` etc.).
"""
return {
1: {
'isoline_property': 'h',
'isoline_value': self.inl[0].h.val,
'isoline_value_end': self.outl[0].h.val,
'starting_point_property': 'vol',
'starting_point_value': self.inl[0].vol.val,
'ending_point_property': 'vol',
'ending_point_value': self.outl[0].vol.val
}
}