diff --git a/docs/advanced_features/ude.rst b/docs/advanced_features/ude.rst index 5a81fd4c5..58e0b2031 100644 --- a/docs/advanced_features/ude.rst +++ b/docs/advanced_features/ude.rst @@ -1,325 +1,325 @@ -.. _ude_label: - -User defined equations -====================== -User defined functions provide a powerful tool to the user as they enable -the definition of generic and individual equations that can be applied to your -TESPy model. In order to implement this functionality in your model you will -use the :py:class:`tespy.tools.helpers.UserDefinedEquation`. The API -documentation provides you with an interesting example application, too. - -Getting started ---------------- - -For an easy start, let's consider two different streams. The mass flow of both -streams should be coupled within the model. There is already a possibility -covering simple relations, i.e. applying value referencing with the -:py:class:`tespy.connections.connection.Ref` class. This class allows -formulating simple linear relations: - -.. math:: - - 0 = \dot{m}_1 - \left(\dot{m}_2 \cdot a + b\right) - -Instead of this simple application, other relations could be useful. For -example, the mass flow of our first stream should be quadratic to the mass -flow of the second stream. - -.. math:: - - 0 = \dot{m}_1 - \dot{m}_2^2 - -In order to apply this relation, we need to import the -:py:class:`tespy.tools.helpers.UserDefinedEquation` class into our model and -create an instance with the respective data. First, we set up the TESPy model. - -.. code-block:: python - - >>> from tespy.networks import Network - >>> from tespy.components import Sink, Source - >>> from tespy.connections import Connection - >>> from tespy.tools import UserDefinedEquation - - >>> nw = Network(iterinfo=False) - >>> nw.units.set_defaults( - ... pressure='bar', temperature='degC', enthalpy='kJ/kg' - ... ) - - >>> so1 = Source('source 1') - >>> so2 = Source('source 2') - >>> si1 = Sink('sink 1') - >>> si2 = Sink('sink 2') - - >>> c1 = Connection(so1, 'out1', si1, 'in1') - >>> c2 = Connection(so2, 'out1', si2, 'in1') - - >>> nw.add_conns(c1, c2) - - >>> c1.set_attr(fluid={'water': 1}, p=1, T=50) - >>> c2.set_attr(fluid={'water': 1}, p=5, T=250, v=4) - -In the model both streams are well-defined regarding pressure, enthalpy and -fluid composition. The second stream's mass flow is defined through -specification of the volumetric flow, we are missing the mass flow of the -connection :code:`c1`. As described, its value should be quadratic to the -(still unknown) mass flow of :code:`c2`. First, we now need to define the -equation in a function which returns the residual value of the equation. - -.. code-block:: python - - >>> def my_ude(ude): - ... return ude.conns[0].m.val_SI - ude.conns[1].m.val_SI ** 2 - - -.. note:: - - The function must only take one parameter, i.e. the UserDefinedEquation - class instance, and must be named :code:`ude`! It serves to access some - important parameters of the equation: - - - connections or components required in the equation - - automatic numerical derivatives - - other (external) parameters (e.g. the CharLine in the API docs example of - :py:class:`tespy.tools.helpers.UserDefinedEquation`) - -.. attention:: - - When you access :code:`Connection`, :code:`PowerConnection` or - :ref:`Component variables ` it is important - that you access the :code:`.val_SI` attribute as these values are pointing - to the actual variables the solver solves for. - -The second step is to define a function which returns on which variables the -equation depends. This is used to automatically determine the derivatives of -the equation to the system's variables. - -.. code-block:: python - - >>> def my_ude_dependents(ude): - ... c1, c2 = ude.conns - ... return [c1.m, c2.m] - -This is already sufficient information to use the equation in your model. -However, it is possible to additionally provide a function specifying the -derivatives manually. This can be useful if the derivatives can be calculated -analytically. In order to do this, we create a function that updates the values -inside the Jacobian of the :code:`UserDefinedEquation`. We can use the -high-level method :code:`partial_derivative` for this. In this case the partial -derivatives are easy to find: - -- The derivative to mass flow of connection :code:`c1` is equal to :math:`1` -- The derivative to mass flow of connection :code:`c2` is equal to - :math:`-2 \cdot \dot{m}_2`. - -.. code-block:: python - - >>> def my_ude_deriv(increment_filter, k, dependents=None, ude=None): - ... c1 = ude.conns[0] - ... c2 = ude.conns[1] - ... ude.partial_derivative(c1.m, 1) - ... ude.partial_derivative(c2.m, -2 * ude.conns[1].m.val_SI) - -.. caution:: - - TESPy internally maps the connection and component variables to the solver - variables! If two variables are identified to be linearly dependent in the - presolving, meaning one variable can be expressed as a linear function of - another variable, these two variables are mapped to a single one for the - solver. If this happens, then analytical derivatives may be incorrect. You - need to be sure, that the variables are not pointing to the same solver - variable, for example, see how it is in handled in the :code:`Turbine` - component: - - .. literalinclude:: /../src/tespy/components/turbomachinery/turbine.py - :pyobject: Turbine.eta_s_deriv - - Here, the derivative towards outlet enthalpy is simple, but only if it is - not linearly connected to the inlet enthalpy. - -Now we can create our instance of the :code:`UserDefinedEquation` and add it to -the network. The class requires three mandatory arguments to be passed: - -- :code:`label` of type String. -- :code:`func` which is the function holding the equation to be applied. -- :code:`dependents` which is the function returning the dependent variables. - -And a couple of optional arguments: - -- :code:`deriv` (optional) which is the function holding the calculation of the - Jacobian. -- :code:`conns` (optional) which is a list of the connections required by the - equation. The order of the connections specified in the list is equal to the - accessing order in the equation and derivative calculation. -- :code:`comps` (optional) which is a list of the components required by the - equation. The order of the components specified in the list is equal to the - accessing order in the equation and derivative calculation. -- :code:`params` (optional) which is a dictionary holding additional data - required in the equation, dependents specification or derivative calculation. - -.. code-block:: python - - >>> ude = UserDefinedEquation( - ... 'my ude', my_ude, my_ude_dependents, - ... deriv=my_ude_deriv, conns=[c1, c2] - ... ) - >>> nw.add_ude(ude) - >>> nw.solve('design') - >>> round(c2.m.val_SI ** 2, 2) == round(c1.m.val_SI, 2) - True - >>> nw.del_ude(ude) - -More examples -------------- - -After warm-up let's create some more complex examples, e.g. the square root of -the temperature of the second stream should be equal to the logarithmic value of -the pressure squared divided by the mass flow of the first stream. - -.. math:: - - 0 = \sqrt{T_2} - \ln\left(\frac{p_1^2}{\dot{m}_1}\right) - -In order to access the temperature within the iteration process, we need to -calculate it with the respective method. We can import it from the -:py:mod:`tespy.tools.fluid_properties` module. Additionally, import numpy for -the logarithmic value. - -.. code-block:: python - - >>> import numpy as np - - >>> def my_ude(ude): - ... return ( - ... ude.conns[1].calc_T() ** 0.5 - ... - np.log(ude.conns[0].p.val_SI ** 2 / ude.conns[0].m.val_SI) - ... ) - -.. code-block:: python - - >>> def my_ude_dependents(ude): - ... c1 = ude.conns[0] - ... c2 = ude.conns[1] - ... return [c1.m, c1.p, c2.p, c2.h] - -Again, we could make a mixed analytical derivative method, as the partial -derivatives for the pressure and mass flow of the first stream are available -analytically and for the other ones they can be determined numerically. For the -temperature value, you can use the predefined fluid property functions -:code:`dT_mix_dph` and :code:`dT_mix_pdh` respectively to calculate the partial -derivatives of the temperature towards either enthalpy or pressure. Just to -show how that would look like, we have included it here, usually it is -recommended to just let the solver handle that by itself via the dependents. - -.. code-block:: python - - >>> from tespy.tools.fluid_properties import dT_mix_dph - >>> from tespy.tools.fluid_properties import dT_mix_pdh - - >>> def my_ude_deriv(increment_filter, k, dependents=None, ude=None): - ... c1 = ude.conns[0] - ... c2 = ude.conns[1] - ... ude.partial_derivative(c1.m, 1 / ude.conns[0].m.val_SI) - ... ude.partial_derivative(c1.p, - 2 / ude.conns[0].p.val_SI) - ... T = c2.calc_T() - ... # this API also works, it is not as convenient, but saves - ... # computational effort because the derivatives are only calculated - ... # on demand - ... if c2.p.is_var: - ... ude.partial_derivative( - ... c2.p, - ... dT_mix_dph(c2.p.val_SI, c2.h.val_SI, c2.fluid_data, c2.mixing_rule) - ... * 0.5 / (T ** 0.5) - ... ) - ... if c2.h.is_var: - ... ude.partial_derivative( - ... c2.h, - ... dT_mix_pdh(c2.p.val_SI, c2.h.val_SI, c2.fluid_data, c2.mixing_rule) - ... * 0.5 / (T ** 0.5) - ... ) - - >>> ude = UserDefinedEquation( - ... 'ude numerical', my_ude, my_ude_dependents, - ... deriv=my_ude_deriv, conns=[c1, c2] - ... ) - >>> nw.add_ude(ude) - >>> nw.set_attr(m_range=[.1, 100]) # stabilize algorithm - >>> nw.solve('design') - >>> round(c1.m.val, 2) - 1.17 - - >>> c1.set_attr(p=None, m=1) - >>> nw.solve('design') - >>> round(c1.p.val, 3) - 0.926 - - >>> c1.set_attr(p=1) - >>> c2.set_attr(T=None) - >>> nw.solve('design') - >>> round(c2.T.val, 1) - 257.0 - -Letting the solver handle the same problem through the dependents is easier: -Just do not pass the :code:`deriv` method to the :code:`UserDefinedEquation`. -The downside is a slower performance of the solver, as for every dependent the -function will be evaluated fully twice (central finite difference), but it is -guaranteed that the derivatives are calculated correctly. - -.. code-block:: python - - >>> nw.del_ude(ude) - >>> ude = UserDefinedEquation( - ... 'ude numerical', my_ude, my_ude_dependents, conns=[c1, c2] - ... ) - >>> nw.add_ude(ude) - >>> c1.set_attr(p=None) - >>> c2.set_attr(T=250) - >>> nw.solve('design') - >>> round(c1.p.val, 3) - 0.926 - >>> c1.set_attr(p=1) - >>> c2.set_attr(T=None) - >>> nw.solve('design') - >>> round(c2.T.val, 1) - 257.0 - -Last, we want to consider an example using additional parameters in the -UserDefinedEquation, where :math:`a` might be a factor between 0 and 1 and -:math:`b` is the steam mass fraction (also, between 0 and 1). The difference of -the enthalpy between the two streams multiplied with factor a should be equal -to the difference of the enthalpy of stream two and the enthalpy of saturated -gas at the pressure of stream 1. The definition of the UserDefinedEquation -instance must therefore be changed as below. - -.. math:: - - 0 = a \cdot \left(h_2 - h_1 \right) - - \left(h_2 - h\left(p_1, x=b \right)\right) - -.. code-block:: python - - >>> from tespy.tools.fluid_properties import h_mix_pQ - >>> from tespy.tools.fluid_properties import dh_mix_dpQ - - >>> def my_ude(ude): - ... a = ude.params['a'] - ... b = ude.params['b'] - ... c1 = ude.conns[0] - ... c2 = ude.conns[1] - ... return ( - ... a * (c2.h.val_SI - c1.h.val_SI) - - ... (c2.h.val_SI - h_mix_pQ(c1.p.val_SI, b, c1.fluid_data)) - ... ) - - >>> def my_ude_dependents(ude): - ... c1 = ude.conns[0] - ... c2 = ude.conns[1] - ... return [c1.p, c1.h, c2.h] - - >>> ude = UserDefinedEquation( - ... 'my ude', my_ude, my_ude_dependents, - ... conns=[c1, c2], params={'a': 0.5, 'b': 1} - ... ) - -One more example (using a CharLine for data point interpolation) can be found -in the API documentation of class -:py:class:`tespy.tools.helpers.UserDefinedEquation`. +.. _ude_label: + +User defined equations +====================== +User defined functions provide a powerful tool to the user as they enable +the definition of generic and individual equations that can be applied to your +TESPy model. In order to implement this functionality in your model you will +use the :py:class:`tespy.tools.helpers.UserDefinedEquation`. The API +documentation provides you with an interesting example application, too. + +Getting started +--------------- + +For an easy start, let's consider two different streams. The mass flow of both +streams should be coupled within the model. There is already a possibility +covering simple relations, i.e. applying value referencing with the +:py:class:`tespy.connections.connection.Ref` class. This class allows +formulating simple linear relations: + +.. math:: + + 0 = \dot{m}_1 - \left(\dot{m}_2 \cdot a + b\right) + +Instead of this simple application, other relations could be useful. For +example, the mass flow of our first stream should be quadratic to the mass +flow of the second stream. + +.. math:: + + 0 = \dot{m}_1 - \dot{m}_2^2 + +In order to apply this relation, we need to import the +:py:class:`tespy.tools.helpers.UserDefinedEquation` class into our model and +create an instance with the respective data. First, we set up the TESPy model. + +.. code-block:: python + + >>> from tespy.networks import Network + >>> from tespy.components import Sink, Source + >>> from tespy.connections import Connection + >>> from tespy.tools import UserDefinedEquation + + >>> nw = Network(iterinfo=False) + >>> nw.units.set_defaults( + ... pressure='bar', temperature='degC', enthalpy='kJ/kg' + ... ) + + >>> so1 = Source('source 1') + >>> so2 = Source('source 2') + >>> si1 = Sink('sink 1') + >>> si2 = Sink('sink 2') + + >>> c1 = Connection(so1, 'out1', si1, 'in1') + >>> c2 = Connection(so2, 'out1', si2, 'in1') + + >>> nw.add_conns(c1, c2) + + >>> c1.set_attr(fluid={'water': 1}, p=1, T=50) + >>> c2.set_attr(fluid={'water': 1}, p=5, T=250, v=4) + +In the model both streams are well-defined regarding pressure, enthalpy and +fluid composition. The second stream's mass flow is defined through +specification of the volumetric flow, we are missing the mass flow of the +connection :code:`c1`. As described, its value should be quadratic to the +(still unknown) mass flow of :code:`c2`. First, we now need to define the +equation in a function which returns the residual value of the equation. + +.. code-block:: python + + >>> def my_ude(ude): + ... return ude.conns[0].m.val_SI - ude.conns[1].m.val_SI ** 2 + + +.. note:: + + The function must only take one parameter, i.e. the UserDefinedEquation + class instance, and must be named :code:`ude`! It serves to access some + important parameters of the equation: + + - connections or components required in the equation + - automatic numerical derivatives + - other (external) parameters (e.g. the CharLine in the API docs example of + :py:class:`tespy.tools.helpers.UserDefinedEquation`) + +.. attention:: + + When you access :code:`Connection`, :code:`PowerConnection` or + :ref:`Component variables ` it is important + that you access the :code:`.val_SI` attribute as these values are pointing + to the actual variables the solver solves for. + +The second step is to define a function which returns on which variables the +equation depends. This is used to automatically determine the derivatives of +the equation to the system's variables. + +.. code-block:: python + + >>> def my_ude_dependents(ude): + ... c1, c2 = ude.conns + ... return [c1.m, c2.m] + +This is already sufficient information to use the equation in your model. +However, it is possible to additionally provide a function specifying the +derivatives manually. This can be useful if the derivatives can be calculated +analytically. In order to do this, we create a function that updates the values +inside the Jacobian of the :code:`UserDefinedEquation`. We can use the +high-level method :code:`partial_derivative` for this. In this case the partial +derivatives are easy to find: + +- The derivative to mass flow of connection :code:`c1` is equal to :math:`1` +- The derivative to mass flow of connection :code:`c2` is equal to + :math:`-2 \cdot \dot{m}_2`. + +.. code-block:: python + + >>> def my_ude_deriv(increment_filter, k, dependents=None, ude=None): + ... c1 = ude.conns[0] + ... c2 = ude.conns[1] + ... ude.partial_derivative(c1.m, 1) + ... ude.partial_derivative(c2.m, -2 * ude.conns[1].m.val_SI) + +.. caution:: + + TESPy internally maps the connection and component variables to the solver + variables! If two variables are identified to be linearly dependent in the + presolving, meaning one variable can be expressed as a linear function of + another variable, these two variables are mapped to a single one for the + solver. If this happens, then analytical derivatives may be incorrect. You + need to be sure, that the variables are not pointing to the same solver + variable, for example, see how it is in handled in the :code:`Turbine` + component: + + .. literalinclude:: /../src/tespy/components/turbomachinery/turbine.py + :pyobject: Turbine.eta_s_deriv + + Here, the derivative towards outlet enthalpy is simple, but only if it is + not linearly connected to the inlet enthalpy. + +Now we can create our instance of the :code:`UserDefinedEquation` and add it to +the network. The class requires three mandatory arguments to be passed: + +- :code:`label` of type String. +- :code:`func` which is the function holding the equation to be applied. +- :code:`dependents` which is the function returning the dependent variables. + +And a couple of optional arguments: + +- :code:`deriv` (optional) which is the function holding the calculation of the + Jacobian. +- :code:`conns` (optional) which is a list of the connections required by the + equation. The order of the connections specified in the list is equal to the + accessing order in the equation and derivative calculation. +- :code:`comps` (optional) which is a list of the components required by the + equation. The order of the components specified in the list is equal to the + accessing order in the equation and derivative calculation. +- :code:`params` (optional) which is a dictionary holding additional data + required in the equation, dependents specification or derivative calculation. + +.. code-block:: python + + >>> ude = UserDefinedEquation( + ... 'my ude', my_ude, my_ude_dependents, + ... deriv=my_ude_deriv, conns=[c1, c2] + ... ) + >>> nw.add_ude(ude) + >>> nw.solve('design') + >>> round(c2.m.val_SI ** 2, 2) == round(c1.m.val_SI, 2) + True + >>> nw.del_ude(ude) + +More examples +------------- + +After warm-up let's create some more complex examples, e.g. the square root of +the temperature of the second stream should be equal to the logarithmic value of +the pressure squared divided by the mass flow of the first stream. + +.. math:: + + 0 = \sqrt{T_2} - \ln\left(\frac{p_1^2}{\dot{m}_1}\right) + +In order to access the temperature within the iteration process, we need to +calculate it with the respective method. We can import it from the +:py:mod:`tespy.tools.fluid_properties` module. Additionally, import numpy for +the logarithmic value. + +.. code-block:: python + + >>> import numpy as np + + >>> def my_ude(ude): + ... return ( + ... ude.conns[1].calc_T() ** 0.5 + ... - np.log(ude.conns[0].p.val_SI ** 2 / ude.conns[0].m.val_SI) + ... ) + +.. code-block:: python + + >>> def my_ude_dependents(ude): + ... c1 = ude.conns[0] + ... c2 = ude.conns[1] + ... return [c1.m, c1.p, c2.p, c2.h] + +Again, we could make a mixed analytical derivative method, as the partial +derivatives for the pressure and mass flow of the first stream are available +analytically and for the other ones they can be determined numerically. For the +temperature value, you can use the predefined fluid property functions +:code:`dT_mix_dph` and :code:`dT_mix_pdh` respectively to calculate the partial +derivatives of the temperature towards either enthalpy or pressure. Just to +show how that would look like, we have included it here, usually it is +recommended to just let the solver handle that by itself via the dependents. + +.. code-block:: python + + >>> from tespy.tools.fluid_properties import dT_mix_dph + >>> from tespy.tools.fluid_properties import dT_mix_pdh + + >>> def my_ude_deriv(increment_filter, k, dependents=None, ude=None): + ... c1 = ude.conns[0] + ... c2 = ude.conns[1] + ... ude.partial_derivative(c1.m, 1 / ude.conns[0].m.val_SI) + ... ude.partial_derivative(c1.p, - 2 / ude.conns[0].p.val_SI) + ... T = c2.calc_T() + ... # this API also works, it is not as convenient, but saves + ... # computational effort because the derivatives are only calculated + ... # on demand + ... if c2.p.is_var: + ... ude.partial_derivative( + ... c2.p, + ... dT_mix_dph(c2.p.val_SI, c2.h.val_SI, c2.fluid_data, c2.mixing_rule) + ... * 0.5 / (T ** 0.5) + ... ) + ... if c2.h.is_var: + ... ude.partial_derivative( + ... c2.h, + ... dT_mix_pdh(c2.p.val_SI, c2.h.val_SI, c2.fluid_data, c2.mixing_rule) + ... * 0.5 / (T ** 0.5) + ... ) + + >>> ude = UserDefinedEquation( + ... 'ude numerical', my_ude, my_ude_dependents, + ... deriv=my_ude_deriv, conns=[c1, c2] + ... ) + >>> nw.add_ude(ude) + >>> nw.set_attr(m_range=[.1, 100]) # stabilize algorithm + >>> nw.solve('design') + >>> round(c1.m.val, 2) + 1.17 + + >>> c1.set_attr(p=None, m=1) + >>> nw.solve('design') + >>> round(c1.p.val, 3) + 0.926 + + >>> c1.set_attr(p=1) + >>> c2.set_attr(T=None) + >>> nw.solve('design') + >>> round(c2.T.val, 1) + 257.0 + +Letting the solver handle the same problem through the dependents is easier: +Just do not pass the :code:`deriv` method to the :code:`UserDefinedEquation`. +The downside is a slower performance of the solver, as for every dependent the +function will be evaluated fully twice (central finite difference), but it is +guaranteed that the derivatives are calculated correctly. + +.. code-block:: python + + >>> nw.del_ude(ude) + >>> ude = UserDefinedEquation( + ... 'ude numerical', my_ude, my_ude_dependents, conns=[c1, c2] + ... ) + >>> nw.add_ude(ude) + >>> c1.set_attr(p=None) + >>> c2.set_attr(T=250) + >>> nw.solve('design') + >>> round(c1.p.val, 3) + 0.926 + >>> c1.set_attr(p=1) + >>> c2.set_attr(T=None) + >>> nw.solve('design') + >>> round(c2.T.val, 1) + 257.0 + +Last, we want to consider an example using additional parameters in the +UserDefinedEquation, where :math:`a` might be a factor between 0 and 1 and +:math:`b` is the steam mass fraction (also, between 0 and 1). The difference of +the enthalpy between the two streams multiplied with factor a should be equal +to the difference of the enthalpy of stream two and the enthalpy of saturated +gas at the pressure of stream 1. The definition of the UserDefinedEquation +instance must therefore be changed as below. + +.. math:: + + 0 = a \cdot \left(h_2 - h_1 \right) - + \left(h_2 - h\left(p_1, x=b \right)\right) + +.. code-block:: python + + >>> from tespy.tools.fluid_properties import h_mix_pQ + >>> from tespy.tools.fluid_properties import dh_mix_dpQ + + >>> def my_ude(ude): + ... a = ude.params['a'] + ... b = ude.params['b'] + ... c1 = ude.conns[0] + ... c2 = ude.conns[1] + ... return ( + ... a * (c2.h.val_SI - c1.h.val_SI) - + ... (c2.h.val_SI - h_mix_pQ(c1.p.val_SI, b, c1.fluid_data)) + ... ) + + >>> def my_ude_dependents(ude): + ... c1 = ude.conns[0] + ... c2 = ude.conns[1] + ... return [c1.p, c1.h, c2.h] + + >>> ude = UserDefinedEquation( + ... 'my ude', my_ude, my_ude_dependents, + ... conns=[c1, c2], params={'a': 0.5, 'b': 1} + ... ) + +One more example (using a CharLine for data point interpolation) can be found +in the API documentation of class +:py:class:`tespy.tools.helpers.UserDefinedEquation`. diff --git a/docs/api/data.rst b/docs/api/data.rst index c76a15c0b..5271748f1 100644 --- a/docs/api/data.rst +++ b/docs/api/data.rst @@ -1,160 +1,160 @@ -.. _data_label: - -tespy.data module -================= - -Default characteristics ------------------------ - -Characteristic lines -^^^^^^^^^^^^^^^^^^^^ -**turbine** - -.. figure:: _images/turbine_eta_s_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". - :align: center - - Reference: Generic data. - -.. figure:: _images/turbine_eta_s_char_TRAUPEL.svg - :alt: Characteristic line "TRAUPEL" for parameter "eta_s_char". - :align: center - - Reference: :cite:`Traupel2001`. - -**compressor** - -.. figure:: _images/compressor_eta_s_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". - :align: center - - Reference: Generic data. - -**pump** - -.. figure:: _images/pump_eta_s_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". - :align: center - - Reference: Generic data. - -**combustion engine** - -.. figure:: _images/combustion_engine_tiP_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "tiP_char". - :align: center - - Reference: Generic data. - -.. figure:: _images/combustion_engine_Q1_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "Q1_char". - :align: center - - Reference: Generic data. - -.. figure:: _images/combustion_engine_Q2_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "Q2_char". - :align: center - - Reference: Generic data. - -.. figure:: _images/combustion_engine_Qloss_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "Qloss_char". - :align: center - - Reference: Generic data. - -**heat exchanger** - -.. figure:: _images/heat_exchanger_kA_char1_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char1". - :align: center - - Reference: Generic data. - -.. figure:: _images/heat_exchanger_kA_char1_CONDENSING_FLUID.svg - :alt: Characteristic line "CONDENSING FLUID" for parameter "kA_char1". - :align: center - - Reference: Generic data. - -.. figure:: _images/heat_exchanger_kA_char2_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char2". - :align: center - - Reference: Generic data. - -.. figure:: _images/heat_exchanger_kA_char2_EVAPORATING_FLUID.svg - :alt: Characteristic line "EVAPORATING FLUID" for parameter "kA_char2". - :align: center - - Reference: Generic data. - -**condenser** - -.. figure:: _images/condenser_kA_char1_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char1". - :align: center - - Reference: Generic data. - -.. figure:: _images/condenser_kA_char2_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char2". - :align: center - - Reference: Generic data. - -**desuperheater** - -.. figure:: _images/desuperheater_kA_char1_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char1". - :align: center - - Reference: Generic data. - -.. figure:: _images/desuperheater_kA_char2_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char2". - :align: center - - Reference: Generic data. - -**heat exchanger simple** - -.. figure:: _images/heat_exchanger_simple_kA_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char". - :align: center - - Reference: Generic data. - -**pipe** - -.. figure:: _images/pipe_kA_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "kA_char". - :align: center - - Reference: Generic data. - -**water electrolyzer** - -.. figure:: _images/water_electrolyzer_eta_char_DEFAULT.svg - :alt: Characteristic line "DEFAULT" for parameter "eta_char". - :align: center - - Reference: Generic data. - -Characteristic maps -^^^^^^^^^^^^^^^^^^^ - -**compressor** - -.. figure:: _images/compressor_char_map_pr_DEFAULT.svg - :alt: Characteristic map "DEFAULT" for parameter "char_map_pr". - :align: center - - Reference: :cite:`Plis2016`. - -.. figure:: _images/compressor_char_map_eta_s_DEFAULT.svg - :alt: Characteristic map "DEFAULT" for parameter "char_map_eta_s". - :align: center - - Reference: :cite:`Plis2016`. +.. _data_label: + +tespy.data module +================= + +Default characteristics +----------------------- + +Characteristic lines +^^^^^^^^^^^^^^^^^^^^ +**turbine** + +.. figure:: _images/turbine_eta_s_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". + :align: center + + Reference: Generic data. + +.. figure:: _images/turbine_eta_s_char_TRAUPEL.svg + :alt: Characteristic line "TRAUPEL" for parameter "eta_s_char". + :align: center + + Reference: :cite:`Traupel2001`. + +**compressor** + +.. figure:: _images/compressor_eta_s_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". + :align: center + + Reference: Generic data. + +**pump** + +.. figure:: _images/pump_eta_s_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "eta_s_char". + :align: center + + Reference: Generic data. + +**combustion engine** + +.. figure:: _images/combustion_engine_tiP_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "tiP_char". + :align: center + + Reference: Generic data. + +.. figure:: _images/combustion_engine_Q1_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "Q1_char". + :align: center + + Reference: Generic data. + +.. figure:: _images/combustion_engine_Q2_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "Q2_char". + :align: center + + Reference: Generic data. + +.. figure:: _images/combustion_engine_Qloss_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "Qloss_char". + :align: center + + Reference: Generic data. + +**heat exchanger** + +.. figure:: _images/heat_exchanger_kA_char1_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char1". + :align: center + + Reference: Generic data. + +.. figure:: _images/heat_exchanger_kA_char1_CONDENSING_FLUID.svg + :alt: Characteristic line "CONDENSING FLUID" for parameter "kA_char1". + :align: center + + Reference: Generic data. + +.. figure:: _images/heat_exchanger_kA_char2_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char2". + :align: center + + Reference: Generic data. + +.. figure:: _images/heat_exchanger_kA_char2_EVAPORATING_FLUID.svg + :alt: Characteristic line "EVAPORATING FLUID" for parameter "kA_char2". + :align: center + + Reference: Generic data. + +**condenser** + +.. figure:: _images/condenser_kA_char1_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char1". + :align: center + + Reference: Generic data. + +.. figure:: _images/condenser_kA_char2_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char2". + :align: center + + Reference: Generic data. + +**desuperheater** + +.. figure:: _images/desuperheater_kA_char1_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char1". + :align: center + + Reference: Generic data. + +.. figure:: _images/desuperheater_kA_char2_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char2". + :align: center + + Reference: Generic data. + +**heat exchanger simple** + +.. figure:: _images/heat_exchanger_simple_kA_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char". + :align: center + + Reference: Generic data. + +**pipe** + +.. figure:: _images/pipe_kA_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "kA_char". + :align: center + + Reference: Generic data. + +**water electrolyzer** + +.. figure:: _images/water_electrolyzer_eta_char_DEFAULT.svg + :alt: Characteristic line "DEFAULT" for parameter "eta_char". + :align: center + + Reference: Generic data. + +Characteristic maps +^^^^^^^^^^^^^^^^^^^ + +**compressor** + +.. figure:: _images/compressor_char_map_pr_DEFAULT.svg + :alt: Characteristic map "DEFAULT" for parameter "char_map_pr". + :align: center + + Reference: :cite:`Plis2016`. + +.. figure:: _images/compressor_char_map_eta_s_DEFAULT.svg + :alt: Characteristic map "DEFAULT" for parameter "char_map_eta_s". + :align: center + + Reference: :cite:`Plis2016`. diff --git a/docs/building_blocks/connections.rst b/docs/building_blocks/connections.rst index a1d8adf36..208b4b7c4 100644 --- a/docs/building_blocks/connections.rst +++ b/docs/building_blocks/connections.rst @@ -1,366 +1,366 @@ -.. _modules_connections_label: - -Connections -=========== - -This section provides an overview of the available :code:`Connection` classes -in the tabs below. Beyond that, it gives an introduction on how to -parametrize instances. Connections hold the variables that are solved for in -the system of equations of all your models. - -.. include:: _connections_overview.rst - -Connection Overview -------------------- - -The tables above indicate, which specification parameters are available for -each class. To set/unset values the same logic applies as is used in -components. - -Setting and unsetting values -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We will create a simple problem using a :code:`SimpleHeatExchanger` to showcase -specification options. Further down there is a second simple problem -showcasing the :code:`PowerConnection`. - -A :code:`Connection` always connects two components: - -.. code-block:: python - - >>> from tespy.networks import Network - >>> from tespy.connections import Connection - >>> from tespy.components import Sink, Source, SimpleHeatExchanger - >>> nw = Network(iterinfo=False) - >>> nw.units.set_defaults( - ... temperature="°C", power="kW", pressure="bar", enthalpy="kJ/kg" - ... ) - >>> source = Source('source') - >>> heatexchanger = SimpleHeatExchanger('heat exchanger') - >>> sink = Sink('sink') - >>> c1 = Connection(source, "out1", heatexchanger, "in1", label="c1") - >>> c2 = Connection(heatexchanger, "out1", sink, "in1", label="c2") - >>> nw.add_conns(c1, c2) - -It is possible to set simple values and then solve, e.g.: - -- mass flow, pressure and temperature (and fluid) at inlet -- enthalpy at outlet -- (pressure drop in heat exchanger) - -.. code-block:: python - - >>> c1.set_attr(fluid={"R290": 1}, m=5, p=3, T=50) - >>> c2.set_attr(h=500) - >>> heatexchanger.set_attr(dp=0) - >>> nw.solve("design") - -Both connections will have all results available, these can be accessed - -- as SI value -- as value in the network's default unit of the respective quantity -- as value with the corresponding quantity (:code:`pint.Quantity`) - -The results include mass flow, pressure, enthalpy, temperature, temperature, -vapor quality, specific volume :code:`vol` and volumetric flow. - -.. code-block:: python - - >>> round(c1.h.val, 1) # value in kJ/kg but without unit attached - 668.9 - >>> round(c1.vol.val_with_unit, 3) # will be in m3/kg - - >>> round(c2.T.val_SI, 1) # SI value - 259.0 - >>> round(c2.T.val_with_unit, 1) # will be in °C - - -You can also provide quantities to a specific parameter to individually specify -a unit to a parameter, e.g. inlet mass flow. Note, that units are retained when -set with individual quantity. - -.. code-block:: python - - >>> Q = nw.units.ureg.Quantity - >>> c1.set_attr(m=Q(2, "t/h")) - >>> nw.solve("design") - >>> c1.m.val_with_unit - - >>> round(c2.m.val_with_unit, 2) - - -For pure fluids or CoolProp/REFPROP mixtures we can also specify two-phase -properties: - -- vapor mass fraction/quality :code:`x` -- dew line temperature difference for superheating :code:`td_dew` -- bubble line temperature difference for subcooling :code:`td_bubble` -- dew line temperature :code:`T_dew` and bubble line temperature - :code:`T_bubble` to impose the corresponding pressure to the model - -We can replace the inlet temperature specification e.g. with superheating. The -unit of :code:`temperature_difference` is different from the unit for -:code:`temperature`. Unsetting a value is simple: Just set it to :code:`None`. - -.. code-block:: python - - >>> c1.set_attr(T=None) # unset the value - >>> nw.units.default["temperature"] - '°C' - >>> nw.units.default["temperature_difference"] - 'delta_degC' - >>> c1.set_attr(td_dew=20) - >>> nw.solve("design") - >>> round(c1.T.val, 2) - 5.82 - -Setting starting values -^^^^^^^^^^^^^^^^^^^^^^^ - -Setting starting values for the variables can be helpful in some situations. -You can do this for the following properties: - -- mass flow :code:`m0` -- pressure :code:`p0` -- enthalpy :code:`h0` - -These specifications are optional! - -.. code-block:: python - - >>> c1.set_attr(m0=4) - >>> c2.set_attr(h0=300, p0=4) - -Referencing specifications -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -It is also possible to set up linear relationships between parameters between -different instances of :code:`Connection` in your system in the following form: - -.. math:: - - x_0 = a * x_1 + b - -It is possible to specify these for - -- mass flow, pressure and enthalpy as well as -- temperature and volumetric flow. - -For example, instead of h we can specify a reference to the temperature at -c1. The factor is always based on SI value, the delta is in the default unit of -the respective property. The starting value for h is required in this context -because in the previous calculation the fluid was in two-phase state, meaning -the partial derivative of the temperature of c2 with respect to enthalpy would -be zero otherwise. - -.. code-block:: python - - >>> from tespy.connections import Ref - >>> factor = 1 - >>> delta = 25 - >>> c2.set_attr(h=None, T=Ref(c1, factor, delta), h0=1000) - >>> nw.solve("design") - >>> round(c1.T.val_SI * factor + delta - 273.15, 2) - 30.82 - >>> round(c2.T.val, 2) - 30.82 - -Instead we could also reference volumetric flow at outlet to find the -temperature at outlet. - -.. code-block:: python - - >>> c2.set_attr(T=None) - >>> factor = 1.2 - >>> delta = 0 - >>> c2.set_attr(v=Ref(c1, factor, delta)) - >>> nw.solve("design") - >>> round(c1.v.val_SI * factor + delta, 2) - 0.11 - >>> round(c2.v.val_SI, 2) - 0.11 - >>> round(c2.T.val, 2) - 52.91 - -For more complex (and arbitrary) relationships between variables of the system -use the :code:`UserDefinedEquation` class. Some examples can be found in -:ref:`this section `. - -Fluid specification -^^^^^^^^^^^^^^^^^^^ - -This sections shows some details on the specification of fluids. - -.. code-block:: python - - # set both elements of the fluid vector - >>> c1.set_attr(fluid={'water': 1}) - - # set starting values (might be necessary sometimes) - >>> c1.set_attr(fluid0={'water': 1}) - - # overwrite the existing fluid vector - >>> c1.fluid.is_set - {'water'} - >>> c1.set_attr(fluid={'N2': 0.7, "O2": 0.3}) - >>> c1.fluid.is_set == {'N2', 'O2'} - True - - # remove a single specification while keeping the other component inside - >>> c1.fluid.is_set.remove('N2') - >>> c1.fluid.is_set - {'O2'} - >>> c1.fluid.val - {'N2': 0.7, 'O2': 0.3} - -CoolProp and REFPROP -++++++++++++++++++++ - -It is possible to specify the fluid property back end of the fluids by adding -the name of the back end in front of the fluid's name. For incompressible binary -mixtures, you can append the water volume/mass fraction to the fluid's name, for -example: - -.. code-block:: python - - >>> c1.set_attr(fluid={'water': 1}) # HEOS back end - >>> c1.set_attr(fluid={'INCOMP::water': 1}) # incompressible fluid - >>> c1.set_attr(fluid={'BICUBIC::air': 1}) # bicubic back end - >>> c1.set_attr(fluid={'INCOMP::MPG[0.5]|mass': 1}) # binary incompressible mixture - -You can also specify REFPROP based fluids, e.g. R513A, which is a mass based -mixture of R134a and R1234yf: - -.. code-block:: python - - >>> c1.set_attr(fluid={'REFPROP::R134A[0.44]&R1234yf[0.56]|mass': 1}) # REFPROP back end - -.. note:: - - Without further specifications CoolProp will be used as fluid property - database. If you do not specify a back end, the **default back end** - :code:`HEOS` will be used. For an overview of the back ends available please - refer to the :ref:`fluid property section `. - -Other Backends -++++++++++++++ - -You can also change the engine, for example to the iapws library. It is even -possible, that you define your own custom engine, e.g. using polynomial -equations. Please check out the fluid properties' section in the docs on how to -do this. - -.. code-block:: python - - >>> from tespy.tools.fluid_properties.wrappers import IAPWSWrapper - >>> c1.set_attr(fluid={'H2O': 1}, fluid_engines={"H2O": IAPWSWrapper}) - -Please also check out the section on -:ref:`custom fluid properties ` for more -information. - -.. _powerconnections_label: - -PowerConnection Overview ------------------------- - -PowerConnections can be used to represent non-material energy flow, like power -or heat. You can make use of generators, motors and buses. - -Different use-cases for the implementation of :code:`PowerConnection` with the -respective power components can be: - -- apply motor or generator efficiencies -- connect multiple turbomachines on a single shaft -- collect all electricity production and own consumption to calculate net - power - -The handling of the :code:`PowerConnection` and the respective components is -identical to standard components. - -The :code:`PowerConnection` only holds a single parameter, namely the power -flow :code:`E` (:math:`\dot E`), which is measured in Watts. You can create a -:code:`PowerConnection` instance by connecting to a component that has a -respective inlet or outlet. For example, consider a turbine generating -electricity. First we can set up a system as we are used to do without any -:code:`PowerConnections`: - -.. code-block:: python - - >>> from tespy.components import Source, Sink, Turbine - >>> from tespy.connections import Connection - >>> from tespy.networks import Network - >>> nw = Network(iterinfo=False) - >>> nw.units.set_defaults( - ... temperature="degC", pressure="bar", power="kW" - ... ) - >>> so = Source("source") - >>> turbine = Turbine("turbine") - >>> si = Sink("sink") - >>> c1 = Connection(so, "out1", turbine, "in1", label="c1") - >>> c2 = Connection(turbine, "out1", si, "in1", label="c2") - >>> nw.add_conns(c1, c2) - -We can parametrize the model, e.g. consider the turbine part of a gas turbine, -which expands hot flue gases: - -.. code-block:: python - - >>> c1.set_attr(fluid={"air": 1}, p=10, T=1000, m=1) - >>> c2.set_attr(p=1) - >>> turbine.set_attr(eta_s=0.9) - >>> nw.solve("design") - >>> round(turbine.P.val) - -577 - -We can add a connection between the turbine and the grid. This will add one -extra variable to our problem (the energy flow :code:`E`) but also one extra -equation, namely the turbine energy balance. Therefore, after adding the new -connection, there is nothing to change to make the model solve. - -.. code-block:: python - - >>> from tespy.connections import PowerConnection - >>> from tespy.components import PowerSink - >>> grid = PowerSink("grid") - >>> e1 = PowerConnection(turbine, "power", grid, "power", label="e1") - >>> nw.add_conns(e1) - >>> nw.solve("design") - >>> round(e1.E.val) - 577 - -.. note:: - - Note that the value of the energy flow of a :code:`PowerConnection` will - always be positive in the defined direction (from one component's outlet - to another component's inlet). - -To learn what power connections are available in each of the component classes -see the respective API documentation. There are a couple of example -applications available for the :code:`PowerConnection` -:ref:`in this section `. - -Access from the :code:`Network` object --------------------------------------- - -You may want to access the network's connections or powerconnections other than -using the variable names, for example in an imported network or connections -from a subsystem. It is possible to access these using the connection's label -similar as it is possible for components. -By default, the label is generated by this logic: - -:code:`source:source_id_target:target_id`, where - -- :code:`source` and :code:`target` are the labels of the components that are - connected. -- :code:`source_id` and :code:`target_id` are e.g. :code:`out1` and - :code:`in2` respectively. - -.. code-block:: python - - >>> conn = nw.get_conn('c1') - >>> conn.label - 'c1' - >>> powerconn = nw.get_conn('e1') - >>> powerconn.label - 'e1' +.. _modules_connections_label: + +Connections +=========== + +This section provides an overview of the available :code:`Connection` classes +in the tabs below. Beyond that, it gives an introduction on how to +parametrize instances. Connections hold the variables that are solved for in +the system of equations of all your models. + +.. include:: _connections_overview.rst + +Connection Overview +------------------- + +The tables above indicate, which specification parameters are available for +each class. To set/unset values the same logic applies as is used in +components. + +Setting and unsetting values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We will create a simple problem using a :code:`SimpleHeatExchanger` to showcase +specification options. Further down there is a second simple problem +showcasing the :code:`PowerConnection`. + +A :code:`Connection` always connects two components: + +.. code-block:: python + + >>> from tespy.networks import Network + >>> from tespy.connections import Connection + >>> from tespy.components import Sink, Source, SimpleHeatExchanger + >>> nw = Network(iterinfo=False) + >>> nw.units.set_defaults( + ... temperature="°C", power="kW", pressure="bar", enthalpy="kJ/kg" + ... ) + >>> source = Source('source') + >>> heatexchanger = SimpleHeatExchanger('heat exchanger') + >>> sink = Sink('sink') + >>> c1 = Connection(source, "out1", heatexchanger, "in1", label="c1") + >>> c2 = Connection(heatexchanger, "out1", sink, "in1", label="c2") + >>> nw.add_conns(c1, c2) + +It is possible to set simple values and then solve, e.g.: + +- mass flow, pressure and temperature (and fluid) at inlet +- enthalpy at outlet +- (pressure drop in heat exchanger) + +.. code-block:: python + + >>> c1.set_attr(fluid={"R290": 1}, m=5, p=3, T=50) + >>> c2.set_attr(h=500) + >>> heatexchanger.set_attr(dp=0) + >>> nw.solve("design") + +Both connections will have all results available, these can be accessed + +- as SI value +- as value in the network's default unit of the respective quantity +- as value with the corresponding quantity (:code:`pint.Quantity`) + +The results include mass flow, pressure, enthalpy, temperature, temperature, +vapor quality, specific volume :code:`vol` and volumetric flow. + +.. code-block:: python + + >>> round(c1.h.val, 1) # value in kJ/kg but without unit attached + 668.9 + >>> round(c1.vol.val_with_unit, 3) # will be in m3/kg + + >>> round(c2.T.val_SI, 1) # SI value + 259.0 + >>> round(c2.T.val_with_unit, 1) # will be in °C + + +You can also provide quantities to a specific parameter to individually specify +a unit to a parameter, e.g. inlet mass flow. Note, that units are retained when +set with individual quantity. + +.. code-block:: python + + >>> Q = nw.units.ureg.Quantity + >>> c1.set_attr(m=Q(2, "t/h")) + >>> nw.solve("design") + >>> c1.m.val_with_unit + + >>> round(c2.m.val_with_unit, 2) + + +For pure fluids or CoolProp/REFPROP mixtures we can also specify two-phase +properties: + +- vapor mass fraction/quality :code:`x` +- dew line temperature difference for superheating :code:`td_dew` +- bubble line temperature difference for subcooling :code:`td_bubble` +- dew line temperature :code:`T_dew` and bubble line temperature + :code:`T_bubble` to impose the corresponding pressure to the model + +We can replace the inlet temperature specification e.g. with superheating. The +unit of :code:`temperature_difference` is different from the unit for +:code:`temperature`. Unsetting a value is simple: Just set it to :code:`None`. + +.. code-block:: python + + >>> c1.set_attr(T=None) # unset the value + >>> nw.units.default["temperature"] + '°C' + >>> nw.units.default["temperature_difference"] + 'delta_degC' + >>> c1.set_attr(td_dew=20) + >>> nw.solve("design") + >>> round(c1.T.val, 2) + 5.82 + +Setting starting values +^^^^^^^^^^^^^^^^^^^^^^^ + +Setting starting values for the variables can be helpful in some situations. +You can do this for the following properties: + +- mass flow :code:`m0` +- pressure :code:`p0` +- enthalpy :code:`h0` + +These specifications are optional! + +.. code-block:: python + + >>> c1.set_attr(m0=4) + >>> c2.set_attr(h0=300, p0=4) + +Referencing specifications +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is also possible to set up linear relationships between parameters between +different instances of :code:`Connection` in your system in the following form: + +.. math:: + + x_0 = a * x_1 + b + +It is possible to specify these for + +- mass flow, pressure and enthalpy as well as +- temperature and volumetric flow. + +For example, instead of h we can specify a reference to the temperature at +c1. The factor is always based on SI value, the delta is in the default unit of +the respective property. The starting value for h is required in this context +because in the previous calculation the fluid was in two-phase state, meaning +the partial derivative of the temperature of c2 with respect to enthalpy would +be zero otherwise. + +.. code-block:: python + + >>> from tespy.connections import Ref + >>> factor = 1 + >>> delta = 25 + >>> c2.set_attr(h=None, T=Ref(c1, factor, delta), h0=1000) + >>> nw.solve("design") + >>> round(c1.T.val_SI * factor + delta - 273.15, 2) + 30.82 + >>> round(c2.T.val, 2) + 30.82 + +Instead we could also reference volumetric flow at outlet to find the +temperature at outlet. + +.. code-block:: python + + >>> c2.set_attr(T=None) + >>> factor = 1.2 + >>> delta = 0 + >>> c2.set_attr(v=Ref(c1, factor, delta)) + >>> nw.solve("design") + >>> round(c1.v.val_SI * factor + delta, 2) + 0.11 + >>> round(c2.v.val_SI, 2) + 0.11 + >>> round(c2.T.val, 2) + 52.91 + +For more complex (and arbitrary) relationships between variables of the system +use the :code:`UserDefinedEquation` class. Some examples can be found in +:ref:`this section `. + +Fluid specification +^^^^^^^^^^^^^^^^^^^ + +This sections shows some details on the specification of fluids. + +.. code-block:: python + + # set both elements of the fluid vector + >>> c1.set_attr(fluid={'water': 1}) + + # set starting values (might be necessary sometimes) + >>> c1.set_attr(fluid0={'water': 1}) + + # overwrite the existing fluid vector + >>> c1.fluid.is_set + {'water'} + >>> c1.set_attr(fluid={'N2': 0.7, "O2": 0.3}) + >>> c1.fluid.is_set == {'N2', 'O2'} + True + + # remove a single specification while keeping the other component inside + >>> c1.fluid.is_set.remove('N2') + >>> c1.fluid.is_set + {'O2'} + >>> c1.fluid.val + {'N2': 0.7, 'O2': 0.3} + +CoolProp and REFPROP +++++++++++++++++++++ + +It is possible to specify the fluid property back end of the fluids by adding +the name of the back end in front of the fluid's name. For incompressible binary +mixtures, you can append the water volume/mass fraction to the fluid's name, for +example: + +.. code-block:: python + + >>> c1.set_attr(fluid={'water': 1}) # HEOS back end + >>> c1.set_attr(fluid={'INCOMP::water': 1}) # incompressible fluid + >>> c1.set_attr(fluid={'BICUBIC::air': 1}) # bicubic back end + >>> c1.set_attr(fluid={'INCOMP::MPG[0.5]|mass': 1}) # binary incompressible mixture + +You can also specify REFPROP based fluids, e.g. R513A, which is a mass based +mixture of R134a and R1234yf: + +.. code-block:: python + + >>> c1.set_attr(fluid={'REFPROP::R134A[0.44]&R1234yf[0.56]|mass': 1}) # REFPROP back end + +.. note:: + + Without further specifications CoolProp will be used as fluid property + database. If you do not specify a back end, the **default back end** + :code:`HEOS` will be used. For an overview of the back ends available please + refer to the :ref:`fluid property section `. + +Other Backends +++++++++++++++ + +You can also change the engine, for example to the iapws library. It is even +possible, that you define your own custom engine, e.g. using polynomial +equations. Please check out the fluid properties' section in the docs on how to +do this. + +.. code-block:: python + + >>> from tespy.tools.fluid_properties.wrappers import IAPWSWrapper + >>> c1.set_attr(fluid={'H2O': 1}, fluid_engines={"H2O": IAPWSWrapper}) + +Please also check out the section on +:ref:`custom fluid properties ` for more +information. + +.. _powerconnections_label: + +PowerConnection Overview +------------------------ + +PowerConnections can be used to represent non-material energy flow, like power +or heat. You can make use of generators, motors and buses. + +Different use-cases for the implementation of :code:`PowerConnection` with the +respective power components can be: + +- apply motor or generator efficiencies +- connect multiple turbomachines on a single shaft +- collect all electricity production and own consumption to calculate net + power + +The handling of the :code:`PowerConnection` and the respective components is +identical to standard components. + +The :code:`PowerConnection` only holds a single parameter, namely the power +flow :code:`E` (:math:`\dot E`), which is measured in Watts. You can create a +:code:`PowerConnection` instance by connecting to a component that has a +respective inlet or outlet. For example, consider a turbine generating +electricity. First we can set up a system as we are used to do without any +:code:`PowerConnections`: + +.. code-block:: python + + >>> from tespy.components import Source, Sink, Turbine + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + >>> nw = Network(iterinfo=False) + >>> nw.units.set_defaults( + ... temperature="degC", pressure="bar", power="kW" + ... ) + >>> so = Source("source") + >>> turbine = Turbine("turbine") + >>> si = Sink("sink") + >>> c1 = Connection(so, "out1", turbine, "in1", label="c1") + >>> c2 = Connection(turbine, "out1", si, "in1", label="c2") + >>> nw.add_conns(c1, c2) + +We can parametrize the model, e.g. consider the turbine part of a gas turbine, +which expands hot flue gases: + +.. code-block:: python + + >>> c1.set_attr(fluid={"air": 1}, p=10, T=1000, m=1) + >>> c2.set_attr(p=1) + >>> turbine.set_attr(eta_s=0.9) + >>> nw.solve("design") + >>> round(turbine.P.val) + -577 + +We can add a connection between the turbine and the grid. This will add one +extra variable to our problem (the energy flow :code:`E`) but also one extra +equation, namely the turbine energy balance. Therefore, after adding the new +connection, there is nothing to change to make the model solve. + +.. code-block:: python + + >>> from tespy.connections import PowerConnection + >>> from tespy.components import PowerSink + >>> grid = PowerSink("grid") + >>> e1 = PowerConnection(turbine, "power", grid, "power", label="e1") + >>> nw.add_conns(e1) + >>> nw.solve("design") + >>> round(e1.E.val) + 577 + +.. note:: + + Note that the value of the energy flow of a :code:`PowerConnection` will + always be positive in the defined direction (from one component's outlet + to another component's inlet). + +To learn what power connections are available in each of the component classes +see the respective API documentation. There are a couple of example +applications available for the :code:`PowerConnection` +:ref:`in this section `. + +Access from the :code:`Network` object +-------------------------------------- + +You may want to access the network's connections or powerconnections other than +using the variable names, for example in an imported network or connections +from a subsystem. It is possible to access these using the connection's label +similar as it is possible for components. +By default, the label is generated by this logic: + +:code:`source:source_id_target:target_id`, where + +- :code:`source` and :code:`target` are the labels of the components that are + connected. +- :code:`source_id` and :code:`target_id` are e.g. :code:`out1` and + :code:`in2` respectively. + +.. code-block:: python + + >>> conn = nw.get_conn('c1') + >>> conn.label + 'c1' + >>> powerconn = nw.get_conn('e1') + >>> powerconn.label + 'e1' diff --git a/src/tespy/components/combustion/base.py b/src/tespy/components/combustion/base.py index 33d2fe5c2..f1a032086 100644 --- a/src/tespy/components/combustion/base.py +++ b/src/tespy/components/combustion/base.py @@ -29,6 +29,7 @@ from tespy.tools.helpers import _numeric_deriv from tespy.tools.helpers import _numeric_deriv_vecvar from tespy.tools.helpers import fluidalias_in_list +from tespy.tools.fluid_properties.wrappers import CoolPropWrapper, PyromatWrapper @component_registry @@ -199,9 +200,15 @@ def get_parameters(self): func=self.ti_func, dependents=self.ti_dependents, num_eq_sets=1, - quantity="heat", - description="thermal input (fuel LHV multiplied with mass flow)" - ) + quantity="heat" + ), + 'f_nox': dc_cp( + func=self.stoichiometry_func, + dependents=self.stoichiometry_dependents, + #num_eq_sets=1, + quantity="ratio", + min_val=1e-9, max_val=1, + ), } def _update_num_eq(self): @@ -269,11 +276,14 @@ def _get_combustion_connections(self): def setup_reaction_parameters(self): r"""Setup parameters for reaction (gas name aliases and LHV).""" self.fuel_list = [] - all_fluids = set([f for c in self.inl + self.outl for f in c.fluid.val]) - for f in all_fluids: - if fluidalias_in_list(f, COMBUSTION_FLUIDS.fluids.keys()): - self.fuel_list += [f] - + all_fluids = {f: c.fluid.engine[f] for c in self.inl + self.outl for f in c.fluid.val } + for f in all_fluids.keys(): + if issubclass(all_fluids[f], CoolPropWrapper) or all_fluids[f] is None: + if fluidalias_in_list(f, COMBUSTION_FLUIDS.fluids.keys()): + self.fuel_list += [f] + else: + if f in COMBUSTION_FLUIDS.fluids.keys(): + self.fuel_list += [f] self.fuel_list = set(self.fuel_list) if len(self.fuel_list) == 0: @@ -306,7 +316,8 @@ def setup_reaction_parameters(self): raise TESPyComponentError(msg) else: setattr(self, fluid.lower(), fluid) - + setattr(self, "no", "NO") + self.fuels = {} for f in self.fuel_list: self.fuels[f] = {} @@ -383,7 +394,10 @@ def calc_lhv(self, f): def _add_missing_fluids(self, connections): inl, outl = self._get_combustion_connections() if set(inl + outl) & set(connections): - return ["H2O", "CO2"] + if self.f_nox.is_set: + return ["H2O", "CO2","NO"] + else: + return ["H2O", "CO2"] else: return super()._add_missing_fluids(connections) @@ -563,8 +577,9 @@ def stoichiometry(self, fluid): """ # required to work with combustion chamber and engine inl, outl = self._get_combustion_connections() + - if fluid in list(self.fuel_list) + [self.co2, self.o2, self.h2o]: + if fluid in list(self.fuel_list) + [self.co2, self.o2, self.h2o, self.n2, self.no]: ################################################################### # molar mass flow for fuel and oxygen n_fuel = {} @@ -616,6 +631,20 @@ def stoichiometry(self, fluid): n_h_exc = 0 n_c_exc = 0 + M_no = inl[0].fluid.wrapper[self.n2]._molar_mass + inl[0].fluid.wrapper[self.o2]._molar_mass + if self.f_nox.is_set: + n_nox_param = inl[1].m.val_SI * self.f_nox.val_SI / M_no + else: + n_nox_param = 0 + # nitrogen + n_nitrogen = 0 + for i in inl: + n_nitrogen += ( + i.m.val_SI + * i.fluid.val.get(self.n2, 0) + / inl[0].fluid.wrapper[self.n2]._molar_mass + ) + ################################################################### # equation for carbondioxide if fluid == self.co2: @@ -630,10 +659,19 @@ def stoichiometry(self, fluid): # equation for oxygen elif fluid == self.o2: if self.lamb.val_SI < 1: - dm = -n_oxygen * inl[0].fluid.wrapper[self.o2]._molar_mass + dn = -n_oxygen + elif n_nitrogen >= n_nox_param *0.5: + # limitation by f_nox/ enough nitrogen and oxygen for NO formation. + # NO formation as defined in parameter f_nox + dn = -(n_oxygen / self.lamb.val_SI + - n_nox_param *0.5 + ) else: - dm = -n_oxygen / self.lamb.val_SI * inl[0].fluid.wrapper[self.o2]._molar_mass - + # limitation due to nitrogen shortage. All nitrogen is converted to NO + dn = -(n_oxygen / self.lamb.val_SI + - n_nitrogen + ) + dm = dn * inl[0].fluid.wrapper[self.o2]._molar_mass ################################################################### # equation for fuel elif fluid in self.fuel_list: @@ -648,7 +686,34 @@ def stoichiometry(self, fluid): else: n_fuel_exc = 0 dm = -(n_fuel[fluid] - n_fuel_exc) * inl[0].fluid.wrapper[fluid]._molar_mass + + ################################################################### + # equation for nitrogen + # TODO take into account existing NO in inlets + elif fluid == self.n2: + if self.lamb.val_SI < 1: + # oxygen limitation: no formation of NO + dn=0 + elif n_nitrogen >= n_nox_param *0.5: + # limitation by f_nox/ enough nitrogen and oxygen for NO formation. + # NO formation as defined in parameter f_nox + dn = -(-n_nox_param *0.5) + else: + # limitation due to nitrogen shortage. All nitrogen is converted to NO + dn= -(n_nitrogen -0) + dm= dn * inl[0].fluid.wrapper[self.n2]._molar_mass + ################################################################### + # equation for nitrogen monoxide + elif fluid == self.no: + + if self.lamb.val_SI < 1: + dn=0 + elif n_nitrogen >= n_nox_param *0.5: + dn = - (0 - n_nox_param) + else: + dn = - (0 - n_nitrogen *2) + dm = dn * M_no / 2 ################################################################### # equation for other fluids else: @@ -737,6 +802,7 @@ def energy_balance_func(self): - Reference temperature: 298.15 K. - Reference pressure: 1 bar. """ + #TODO substract energy for NO formation inl, outl = self._get_combustion_connections() T_ref = 298.15 p_ref = 1e5 diff --git a/tests/test_components/test_combustion.py b/tests/test_components/test_combustion.py index d0de75753..396eeea58 100644 --- a/tests/test_components/test_combustion.py +++ b/tests/test_components/test_combustion.py @@ -14,6 +14,7 @@ from tespy.components import CombustionChamber from tespy.components import CombustionEngine +from tespy.components import Compressor from tespy.components import DiabaticCombustionChamber from tespy.components import Motor from tespy.components import PowerSink @@ -47,6 +48,15 @@ def setup_CombustionChamber_network(self, instance): self.c3 = Connection(instance, 'out1', self.fg, 'in1', label="fluegas") self.nw.add_conns(self.c1, self.c2, self.c3) + def setup_CombustionChamber_network_wNO(self, instance): + self.cp = Compressor("Compressor") + + self.c1 = Connection(self.air, 'out1', instance, 'in1', label="air") + self.c2 = Connection(self.fuel, 'out1', instance, 'in2', label="fuel") + self.c3 = Connection(instance, 'out1', self.cp, 'in1', label="heated air") + self.c4 = Connection(self.cp, 'out1', self.fg, 'in1', label="fluegas") + self.nw.add_conns(self.c1, self.c2, self.c3, self.c4) + def setup_CombustionEngine_network(self, instance): self.cw1_in = Source('cooling water 1 source') @@ -152,6 +162,52 @@ def test_CombustionChamberCarbonMonoxide(self): self.nw.assert_convergence() assert self.c3.T.val == pytest.approx(1500) + def test_CombustionChamber_NO(self): + """ + Test component properties of combustion chamber. + """ + instance = CombustionChamber('combustion chamber') + self.setup_CombustionChamber_network_wNO(instance) + + # connection parameter specification + air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'ig::NO':0.0} + fuel = {'CO2': 0.04, 'CH4': 0.96} + self.c1.set_attr(fluid=air, fluid_engines={"NO": my_PyromatWrapper}, + p=1, T=30) + self.c2.set_attr(fluid=fuel, T=30) + instance.set_attr(lamb=1.5) + self.cp.set_attr(eta_s=0.8, pr=10) + # test specified bus value on CombustionChamber (must be equal to ti) + b = Bus('thermal input', P=1e6) + b.add_comps({'comp': instance}) + self.nw.add_busses(b) + self.nw.solve('design') + self.c3.set_attr(T=1200) + instance.set_attr(lamb=None) + self.nw.solve('design') + assert self.nw.status == 0 + self.nw.assert_convergence() + msg = f'Value of thermal input must be {b.P.val}, is {instance.ti.val}.' + assert round(b.P.val, 1) == round(instance.ti.val, 1), msg + b.set_attr(P=None) + + # test specified thermal input for CombustionChamber + instance.set_attr(ti=1e6, f_nox=0.005) + self.nw.solve('design') + self.nw.assert_convergence() + ti = ( + self.c2.m.val_SI * self.c2.fluid.val['CH4'] + * instance.fuels['CH4']['LHV'] + ) + msg = f'Value of thermal input must be {instance.ti.val}, is {ti}.' + assert round(ti, 1) == round(instance.ti.val, 1), msg + + # test specified lamb for CombustionChamber + self.c3.set_attr(T=None) + instance.set_attr(lamb=1) + self.nw.solve('design') + self.nw.assert_convergence() + def test_CombustionChamberHighTemperature(self): instance = CombustionChamber('combustion chamber') self.setup_CombustionChamber_network(instance)