diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..9dfa9d328 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the [project team](https://oemof.org/contact/). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..802ab066e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +The developer rules can be found in the chapter [developing TESPy](https://tespy.readthedocs.io/en/latest/developing_tespy.html) of the tespy.documentation. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index ed0ed434d..7cde00c8b 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,27 @@ -* Describe your pull request as transparent as possible - * What functionality does it implement? - * Where is it located? - * How does the API look? - ... +**General** -* Related issues? +Describe your pull request as transparent as possible: -* Share your knowledge: Insights/Remarks +* What functionality does it implement? +* Where is it located? +* How does the API look? +* Which bugs are fixed +* Are there related issues? -* Other comments and questions +**Documentation** -* [ ] For new features: Remember the documentation! +Please document all changes and/or new features: + +* Update the API documentation +* Update the corresponding sections in the online documentation +* Add inline comments to your code, if required +* Update the corresponding What's New file + +**Testing** + +* Adapt the software tests (including doc-tests), if the PR does affect existing tests +* Add new tests for new features + +**Questions and Comments** + +You are welcome to ask for help or start a discussion on a feature. We very much appreciate your contribution. diff --git a/README.rst b/README.rst index 3c07fbe17..d3bacf7ff 100644 --- a/README.rst +++ b/README.rst @@ -63,9 +63,10 @@ Examples For a short introduction on how TESPy works and how you can use it, we provide some -`examples and tutorials `_, -go and check them out. You can download the python scripts of all example plants from -the `tespy_examples `_ +`examples and tutorials `_, +go and check them out. You can download the python scripts of all example plants +from the +`tespy_examples `_ repository. License diff --git a/doc/api/_images/orc_evaporator.svg b/doc/api/_images/orc_evaporator.svg new file mode 100644 index 000000000..7ee203823 --- /dev/null +++ b/doc/api/_images/orc_evaporator.svg @@ -0,0 +1,174 @@ + + + + + + + + + + image/svg+xml + + + + + + + in3 + out3 + + + + + + + in2 + out2 + + + in1 + out1 + + diff --git a/doc/api/modules.rst b/doc/api/modules.rst index ca5eb9554..ff27b5a5a 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -1,6 +1,12 @@ tespy ===== +All component and connection property equations derive from balance equations +for fluid composition, mass flow and energy in regarding thermal as well as +hydraulic state and thermodynamic fluid property equations respectively. +Standard literature is for example :cite:`Baehr2016,Epple2012,Bswirth2012` +(german). Equations and properties from other sources are cited individually. + .. toctree:: :maxdepth: 4 diff --git a/doc/api/tespy.components.rst b/doc/api/tespy.components.rst index b943e8067..d0ccb405f 100644 --- a/doc/api/tespy.components.rst +++ b/doc/api/tespy.components.rst @@ -25,6 +25,14 @@ tespy.components.components module :undoc-members: :show-inheritance: +tespy.components.customs module +------------------------------- + +.. automodule:: tespy.components.customs + :members: + :undoc-members: + :show-inheritance: + tespy.components.heat_exchangers module --------------------------------------- diff --git a/doc/conf.py b/doc/conf.py index e9b2a1c96..5ab32d1db 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -89,7 +89,7 @@ def plot_map(component, parameter, name, data): plt.close(fig) -def generate_api_doc(component, parameter, name, char_type, rst): +def generate_api_doc(component, parameter, name, char_type, ref): path = '_images/' + component + '_' + parameter + '_' + name + '.svg' rst = ( '.. figure:: ' + path.replace(' ', '_') + '\n' @@ -98,6 +98,11 @@ def generate_api_doc(component, parameter, name, char_type, rst): '" for parameter "' + parameter + '".\n' ' :align: center\n\n' ) + if ref is not False: + rst += ' Reference: :cite:`' + ref + '`.\n\n' + else: + rst += ' Reference: Generic data.\n\n' + return rst @@ -127,7 +132,8 @@ def generate_api_doc(component, parameter, name, char_type, rst): for param, lines in params.items(): for line, data in lines.items(): plot_line(component, param, line, data) - rst += generate_api_doc(component, param, line, 'line', rst) + rst += generate_api_doc( + component, param, line, 'line', data['ref']) rst += ( 'Characteristic maps\n' @@ -139,7 +145,7 @@ def generate_api_doc(component, parameter, name, char_type, rst): for param, chars in params.items(): for char, data in chars.items(): plot_map(component, param, char, data) - rst += generate_api_doc(component, param, char, 'map', rst) + rst += generate_api_doc(component, param, char, 'map', data['ref']) with open('./api/tespy.data.rst', 'w') as f: f.write(rst) @@ -164,7 +170,8 @@ def generate_api_doc(component, parameter, name, char_type, rst): 'sphinx.ext.autosummary', 'sphinx.ext.imgmath', 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode' + 'sphinx.ext.viewcode', + 'sphinxcontrib.bibtex' ] numpydoc_show_class_members = False diff --git a/doc/index.rst b/doc/index.rst index 12e0dda03..8a6359f9f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,8 +1,3 @@ -.. TESPY documentation master file, created by - sphinx-quickstart on Sat Nov 18 11:20:45 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Welcome to TESPy's documentation! ================================= @@ -20,6 +15,7 @@ Contents: tespy_modules developing_tespy whats_new + zliterature api Indices and tables @@ -28,4 +24,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/references.bib b/doc/references.bib new file mode 100644 index 000000000..d31cb841a --- /dev/null +++ b/doc/references.bib @@ -0,0 +1,79 @@ +@book{Baehr2016, + doi = {10.1007/978-3-662-49568-1}, + year = {2016}, + publisher = {Springer Berlin Heidelberg}, + author = {Baehr, Hans Dieter and Kabelac. Stephan}, + title = {Thermodynamik} +} + +@article{Bell2014, + author = {Bell, Ian H. and Wronski, Jorrit and Quoilin, Sylvain and + Lemort, Vincent}, + title = {Pure and Pseudo-pure Fluid Thermophysical Property Evaluation + and the Open-Source Thermophysical Property Library CoolProp}, + journal = {Industrial \& Engineering Chemistry Research}, + volume = {53}, + number = {6}, + pages = {2498-2508}, + year = {2014}, + doi = {10.1021/ie4033999} +} + +@book{Bswirth2012, + doi = {10.1007/978-3-8348-8647-7}, + year = {2012}, + publisher = {Vieweg+Teubner Verlag}, + author = {B\"{o}swirth, Leopold and Bschorer, Sabine}, + title = {Technische Str\"{o}mungslehre} +} + +@book{Epple2012, + doi = {10.1007/978-3-7091-1182-6}, + year = {2012}, + publisher = {Springer Vienna}, + editor = {Epple, Bernd and Leithner, Reinhard and Linzer, Wladimir and + Walter, Heimo}, + title = {Simulation von Kraftwerken und Feuerungen} +} + +@misc{GasTurb2018, + author = {GasTurb GmbH}, + title = {GasTurb 13: Design and Off-Design Performance of Gas Turbines}, + year = {2018} +} + +@article{Herning1936, + author = {Herning, Fritz and Zipperer, L}, + title = {Calculation of the Viscosity of Technical Gas Mixtures From the + Viscosity of the Individual Gases}, + journal = {Gas-und Wasserfach}, + volume = {79}, + pages = {69-73}, + year = {2015} +} + +@article{Plis2016, + author = {Plis, Marcin and Rusinowski, Henryk}, + title = {Mathematical Modeling of an Axial Compressor in a Gas Turbine + Cycle.}, + journal = {Journal of Power Technologies}, + volume = {96}, + number = {3}, + pages = {194-199}, + year = {2016} +} + +@book{Quaschning2013, + year = {2013}, + publisher = {Carl Hanser Verlag M\"{u}nchen}, + author = {Quaschning, Volker}, + title = {Regenerative Energiesysteme} +} + +@book{Traupel2001, + doi = {10.1007/978-3-642-17465-0}, + year = {2001}, + publisher = {Springer Berlin Heidelberg}, + author = {Walter Traupel}, + title = {Thermische Turbomaschinen} +} diff --git a/doc/tespy_modules/characteristics.rst b/doc/tespy_modules/characteristics.rst index a379a7bfc..eafd7f4b0 100644 --- a/doc/tespy_modules/characteristics.rst +++ b/doc/tespy_modules/characteristics.rst @@ -1,7 +1,7 @@ Characteristics =============== -In this part we give an introduction on the TESPy characteritics +In this part we give an introduction on the TESPy characteristics implementation. There two different types of characteristics available in TESPy: lines (:py:class:`char_line `) and maps (:py:class:`char_map `). @@ -19,16 +19,19 @@ the corresponding y-values. .. math:: y = y_0 + \frac{x-x_0}{x_1-x_0} \cdot \left(y_1-y_0 \right) - -If the x value is above the maximum x value or below the minium x value of the -characteristic line the y value corresponding to the the maximum/minimum value -is returned instead. + +It is possible to specify an :code:`extrapolate` parameter. If the value is +:code:`False` (default state) and the x-value is above the maximum or below the +minimum value of the characteristic line the y-value corresponding to the the +maximum/minimum value is returned instead. If the :code:`extrapolate` is +:code:`True` linear extrapolation is performed using the two lower most or +upper moste value pairs respectively. Characteristic maps ------------------- The characteristic maps use linear interpolation as well. First step is -interpolation on the x-dimension similar to the characteristic line +interpolation on the x-dimension similar to the characteristic line functionality. As the y, z1 and z2 data are two-dimensional, **each row of** **the data corresponds to one x value**. Thus, the result of the first step is a vector for each dimesion (y, z1 and z2). @@ -37,22 +40,38 @@ a vector for each dimesion (y, z1 and z2). \vec{y} = \vec{y_0} + \frac{x-x_0}{x_1-x_0} \cdot \left(\vec{y_1}- \vec{y_0} \right) - + \vec{z1} = \vec{z1_0} + \frac{x-x_0}{x_1-x_0} \cdot \left(\vec{z1_1}- \vec{z1_0} \right) - + \vec{z2} = \vec{z2_0} + \frac{x-x_0}{x1-x_0} \cdot \left(\vec{z2_1}- \vec{z2_0}\right) - + Using the y value as second input dimension the corresponding z1 and z2 values are calculated, again using linear interpolation. .. math:: z1 = z1_0 + \frac{y-y_0}{y_1-y_0} \cdot \left(z1_1-z1_0 \right) - + z2 = z2_0 + \frac{y-y_0}{y_1-y_0} \cdot \left(z2_1-z2_0 \right) +.. note:: + + Using compressor maps :math:`\vec{y}`, :math:`\vec{z1}` and + :math:`\vec{z2}` are manipulated with by the value of the inlet guide vane + angle: + + .. math:: + + \vec{y} = \vec{y} \cdot \left(1-\frac{igva}{100}\right)\\ + \vec{z1} = \vec{z1} \cdot \left(1-\frac{igva}{100}\right)\\ + \vec{z2} = \vec{z2} \cdot \left(1-\frac{igva}{100}^2\right) + + Also see the corresponding + :py:class:`section ` in the + API documentation. + .. _import_custom_characteristics_label: Import your own characteristics @@ -64,7 +83,7 @@ writing the x, y (z1 and z2) data into your python script, for example: .. code-block:: python from tespy.tools.characteristics import load_custom_char, char_line - + gen_char = load_custom_char('generator', char_line) @@ -103,7 +122,7 @@ stated as list. } The :code:`char_maps.json` should also have names for identification of the -characteristic lines on the first level. On the second level we aditionally +characteristic lines on the first level. On the second level we additionally need z1 and z2 data. The x data are a list of values, the y, z1 and z2 data are arrays with a list of values for each dimension of the x data. The example below has 3 x values, thus the y, z1 and z2 data must contain 3 sets of values. diff --git a/doc/tespy_modules/components.rst b/doc/tespy_modules/components.rst index 044f8dd30..d957d3d44 100644 --- a/doc/tespy_modules/components.rst +++ b/doc/tespy_modules/components.rst @@ -7,9 +7,8 @@ show you, how to create custom components. List of components ------------------ - More information on the components can be gathered from the code documentation. -We have linked the base class containing a figure and basic informations as +We have linked the base class containing a figure and basic information as well as the equations. - Basics @@ -19,10 +18,10 @@ well as the equations. * :py:class:`Subsystem interface ` - Combustion * :py:class:`Combustion chamber ` - * :py:class:`Combustion chamber stoichiometric ` + * :py:class:`Stoichiometric Combustion chamber ` * :py:class:`Combustion engine ` - Heat exchangers - * :py:class:`Heat exchanger simple ` + * :py:class:`Simplified heat exchanger ` * :py:class:`Heat exchanger ` * :py:class:`Condenser ` * :py:class:`Desuperheater ` @@ -42,6 +41,13 @@ well as the equations. * :py:class:`Pump ` * :py:class:`Turbine ` +List of custom components +------------------------- +Here we list the components integrated in the +:py:mod:`customs ` module. + +- :py:class:`Evaporator for geothermal organic rankine cycle ` + .. _using_tespy_components_parametrisation_label: Component parametrisation @@ -116,12 +122,13 @@ There are three components using parameter groups: Custom variables ^^^^^^^^^^^^^^^^ - It is possible to use component parameters as variables of your system of -equations. For example, give a pressure ratio :code:`pr`, length :code:`L` and -roughness :code:`ks` of a pipe you want to calculate the pipe's diameter -:code:`D` required to achieve the specified pressure ratio. In this case you -need to specify the diameter the following way. +equations. In the component parameter list, if a parameter can be a string, it +is possible to specify this parameter as custom variable. For example, given +the pressure ratio :code:`pr`, length :code:`L` and roughness :code:`ks` of a +pipe you may want to calculate the pipe's diameter :code:`D` required to +achieve the specified pressure ratio. In this case you need to specify the +diameter the following way. .. code-block:: python @@ -152,6 +159,8 @@ specified range. my_pipe.set_attr(D=dc_cp(val=0.2, is_set=True, is_var=True, min_val=0.1, max_val=0.3)) +.. _component_characteristic_specification_label: + Component characteristics ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -197,7 +206,7 @@ For example, :code:`kA` specification for heat exchangers: he = heat_exchanger('evaporator', kA=1e5) # use a characteristic line from the defaults: specify the component, the - # parameter and the name of the characteristc function. Also, specify, what + # parameter and the name of the characteristic function. Also, specify, what # type of characteristic function you want to use. kA_char1 = ldc('heat exchanger', 'kA_char1', 'EVAPORATING FLUID', char_line) kA_char2 = ldc('heat exchanger', 'kA_char2', 'EVAPORATING FLUID', char_line) @@ -253,6 +262,21 @@ from there. For additional information on formatting and usage, look into eta_s_char = dc_cc(func=lcc('my_custom_char', char_line), is_set=True) pu.set_attr(eta_s_char=eta_s_char) +It is possible to allow value extrapolation at the lower and upper limit of the +value range at the creation of characteristic lines. If you are using default +lines, you need to set the extrapolation to :code:`True` manually. + +.. code-block:: python + + # use custom specification parameters + x = np.array([0, 0.5, 1, 2]) + y = np.array([0, 0.8, 1, 1.2]) + kA_char1 = char_line(x, y, extrapolate=True) + he.set_attr(kA_char1=kA_char1) + + # manually set extrapolation to True, e. g. + he.kA_char1.func.extrapolate = True + pu.eta_s_char.func.extrapolate = True Characteristics are available for the following components and parameters: @@ -278,7 +302,7 @@ Characteristics are available for the following components and parameters: - water electrolyzer * :py:meth:`eta_char `: efficiency vs. load ratio. -For more information to how the characteristic functions work +For more information on how the characteristic functions work :ref:`click here `. Custom components @@ -286,9 +310,11 @@ Custom components You can add own components. The class should inherit from the :py:class:`component ` class or its -children. In order to do that, create a python file in your working directory -and import the base class for your custom component. Now create a class for -your component and at least add the following methods. +children. In order to do that, use the +:py:mod:`customs ` module or create a +python file in your working directory and import the base class for your +custom component. Now create a class for your component and at least add the +following methods. - :code:`component(self)`, - :code:`attr(self)`, @@ -367,8 +393,8 @@ an attribute :code:`'num_in'` your code could look like this: Component initialisation ^^^^^^^^^^^^^^^^^^^^^^^^ In the component initialisation you need to determine the total number of -equations and set up the residual value vector as well as the matrix of patial -derivates. The method +equations and set up the residual value vector as well as the matrix of partial +derivatives. The method :py:meth:`tespy.components.components.component.comp_init` already handles counting the custom variables and setting up default characteristic lines for you. The :code:`comp_init()` method of your new component should use call that @@ -376,7 +402,7 @@ method. In order to determine the total number of equations, determine the number of mandatory equations and the number of optional equations applied. Then set up the residual value vector and the matrix of partial derivatives. -If the component delivers derivates that are constant, you can paste those +If the component delivers derivatives that are constant, you can paste those values into the matrix already. The code example shows the implementation of the :py:meth:`tespy.components.turbomachinery.turbine.comp_init` method. @@ -423,7 +449,7 @@ at the outlet :math:`p_{out}` is smaller than the pressure at the inlet The connections connected to your component are available as a list in :code:`self.inl` and :code:`self.outl` respectively. Optional equations should -only be applied, if the paramter has been specified by the user. +only be applied, if the parameter has been specified by the user. .. code:: python @@ -444,7 +470,7 @@ Derivatives You need to calculate the partial derivatives of the equations to all variables of the network. This means, that you have to calculate the partial derivatives to mass flow, pressure, enthalpy and all fluids in the fluid vector on each -incomming or outgoing connection of the component. +incoming or outgoing connection of the component. Add all derivatives to the matrix (*in the same order as the equations!*). The derivatives can be calculated analytically or numerically by using the diff --git a/doc/tespy_modules/connections.rst b/doc/tespy_modules/connections.rst index 295529c41..52c9c78f4 100644 --- a/doc/tespy_modules/connections.rst +++ b/doc/tespy_modules/connections.rst @@ -124,7 +124,7 @@ case, the ratio will always be 1. .. note:: The available keywords for the dictionary are: - + - 'c' for the component instance. - 'p' for the parameter (the combustion engine has various parameters, have a look at the :ref:`combustion engine example `). @@ -132,14 +132,15 @@ case, the ratio will always be 1. - 'char' for the characteristic line. There are different specification possibilites: - + - If you specify the component only, the parameter will be default (not working with cogeneration unit) and the conversion factor of the characteristic line will be 1 for every load. - If you specify a numeric value for char, the conversion factor will be equal to that value for every load. - If you want to specify a characteristic line, provide - a :py:class:` ` object. + a :py:class:`char_line ` + object. The examples below shows the implementation of busses in your TESPy simulation. diff --git a/doc/tespy_modules/fluid_properties.rst b/doc/tespy_modules/fluid_properties.rst index d36f66b08..d12de48aa 100644 --- a/doc/tespy_modules/fluid_properties.rst +++ b/doc/tespy_modules/fluid_properties.rst @@ -5,7 +5,7 @@ Fluid properties The basic fluid properties are handled by `CoolProp `_. All available fluids can be found on -their homepage. +their homepage. Also see :cite:`Bell2014`. Pure and pseudo-pure fluids --------------------------- @@ -13,16 +13,16 @@ Pure and pseudo-pure fluids If you use pure fluids, TESPy directly uses CoolProp functions to gather all fluid properties. CoolProp covers the most important fluids such as water, air as a pseudo-pure fluid as well as its components, several fuels and -refrigerants etc.. Look for the aliases in the -`list of fluids `_. +refrigerants etc.. Look for the aliases in the list of +`fluids `_. All fluids provided in this list cover liquid and gaseous state and the two-phase region. Incompressible fluids --------------------- -If you are looking for heat transer fluids, the -`list of incompressible fluids `_ +If you are looking for heat transer fluids, the list of incompressible +`fluids `_ might be interesting for you. In contrast to the pure fluids, the properties cover liquid state only. @@ -37,8 +37,9 @@ Ideal mixtures of gaseous fluids TESPy can handle mixtures of gaseous fluids, by using the single fluid properties from CoolProp together with corresponding equations for mixtures. -The equations can be found in the :py:mod:`fluid_properties ` -module and are applied automatically to the fluid vector. +The equations can be found in the +:py:mod:`fluid_properties ` module and are +applied automatically to the fluid vector. It is also possible create lookup-tables for fluid mixtures with fixed mass fractions of the components, as this reduces the amount of CoolProp fluid diff --git a/doc/tespy_modules/networks.rst b/doc/tespy_modules/networks.rst index 64cf99a6b..726476b4c 100644 --- a/doc/tespy_modules/networks.rst +++ b/doc/tespy_modules/networks.rst @@ -124,6 +124,8 @@ available keywords: * :code:`max_iter` is the maximum amount of iterations performed by the solver. * :code:`init_only` stop after initialisation (True/False). + * :code:`init_previous` use starting values from previous simulation + (True/False). There are two calculation modes available (:code:`'design'` and :code:`'offdesign'`), which are explained in the subsections below. If you @@ -138,6 +140,11 @@ initialisation from priorly saved results will be skipped. feature to export a not solved network, if you want to do the parametrisation in .csv-files rather than your python script. +The :code:`init_previous` parameter can be used in design and offdesign +calculations and works very similar to specifying an :code:`init_path`. +In contrast, starting values are taken from the previous calculation. Specifying +the :code:`ìnit_path` overwrites :code:`init_previous`. + Design mode +++++++++++ The design mode is used to design your system and is always the first @@ -229,8 +236,9 @@ The initialisation is performed in the following steps. * fluid propagation. * fluid property initialisation. - * initialisation from .csv (setting starting values from - :code:`init_path` argument). + * initialisation from previous simulation run (:code:`ìnit_previous`). + * initialisation from .csv (setting starting values from :code:`init_path` + argument). The network check is used to find errors in the network topology, the calulation can not start without a successful check. For components, a @@ -261,9 +269,11 @@ starting value for the fluid at every point of the network. fluid property database can not find a value, because the fluid is 'nan'. Providing starting values manually can fix this problem. -The fluid property initialisation takes the user specified starting values, if -available, and otherwise uses generic starting values on the bases of which -components the connection is linked to. +If available, the fluid property initialisation uses the user specified starting +values or the results from the previous simulation. Otherwise generic starting +values are generated on basis of which components a connection is linked to. +If you do not want to use the results of a previous calculation, you need to +specify :code:`init_previous=False` on the :code:`network.solve` method call. Last step in starting value generation is the initialisation from a saved network structure. In order to initialise your calculation from the @@ -280,8 +290,7 @@ file. network from the :code:`init_path`. Be aware that a change within the fluid vector does not alow this practice! If you plan to use additional fluids in parts of the network you have not touched until now, you will need to state - all fluids from the beginning. Generally, initialisation from a converged - calculation yields the best performance and is highly receommended. + all fluids from the beginning. Algorithm diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 7ad2b48b8..75b3480a8 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -8,6 +8,7 @@ Discover noteable new features and improvements in each release :local: :backlinks: top +.. include:: whats_new/v0-2-2.rst .. include:: whats_new/v0-2-1.rst .. include:: whats_new/v0-2-0.rst .. include:: whats_new/v0-1-4.rst diff --git a/doc/whats_new/v0-2-2.rst b/doc/whats_new/v0-2-2.rst new file mode 100644 index 000000000..8491877b0 --- /dev/null +++ b/doc/whats_new/v0-2-2.rst @@ -0,0 +1,65 @@ +v0.2.2 Rankine's Realm - (March, 6, 2020) ++++++++++++++++++++++++++++++++++++++++++ + +New Features +############ +- Allow initialisation for the primary variables from previous calculation. + Until now, the user needed to save the network's state and reload that state + for his next simulation. This feature is enabled as default. If you want to + disable this feature, you need to state + :code:`mynetwork.solve(..., init_previous=False)` + (`PR #156 `_). +- Extrapolation for characteristic lines is available. In default state, the + upper or lower value range limit is used when a characteristic line is + evaluated outside of the available x-value range. The :code:`extrapolate` + parameter allows linear extrapolation, for an example see the corresponding + sections in the online documentation: + :ref:`component characteristics `, + :ref:`tespy characteristics ` + (`PR #159 `_). +- Add a new component evaporator for geothermal organic rankine cycle. The + component has inlets for geothermal steam brine. On the cold side, the orc + working fluid is evaporated. Read more about this component in the API + documentation: :py:class:`tespy.components.customs.orc_evaporator` + (`PR #148 `_). + +Documentation +############# +- Add method for automatic citations and references + (`PR #163 `_). + +Parameter renaming +################## + +Testing +####### +- Add convergence checks for all component tests. Some tests did not fail, even + if the calculation did not converge + (`PR #153 `_). +- Improve coverage of the networks module + (`PR #153 `_). +- Add tests for characteristic line and map evaluation + (`PR #159 `_). + +Bug fixes +######### +- Fix the bugged tests for compressor characteristic maps + (:py:meth:`tespy.components.turbomachinery.compressor.char_map_func`). The + pressure ratio factor of the lowest speedline available in the default data + ranges from about 0.2 to 0.5. Therefore the design pressure ratio should be + higher than 5 (`PR #156 `_). + +Other changes +############# +- Use the method :py:meth:`tespy.components.components.component.fluid_deriv` + for all components, that do not change composition between an inlet and the + respective outlet (`PR #153 `_). +- Adjust the method :py:meth:`tespy.components.components.component.zeta_func` + to work with all zeta value specifications + (`PR #153 `_). + +Contributors +############ +- Francesco Witte (`@fwitte `_) +- `@maltefritz `_ +- `@ChaofanChen `_ diff --git a/doc/zliterature.rst b/doc/zliterature.rst new file mode 100644 index 000000000..5d756bbc2 --- /dev/null +++ b/doc/zliterature.rst @@ -0,0 +1,5 @@ +Literature +========== + +.. bibliography:: references.bib + :all: diff --git a/paper.bib b/paper.bib new file mode 100644 index 000000000..3f70b342d --- /dev/null +++ b/paper.bib @@ -0,0 +1,131 @@ +@article{CoolProp, + author = {Bell, Ian H. and Wronski, Jorrit and Quoilin, Sylvain and Lemort, Vincent}, + title = {Pure and Pseudo-pure Fluid Thermophysical Property Evaluation and the Open-Source Thermophysical Property Library CoolProp}, + journal = {Industrial \& Engineering Chemistry Research}, + volume = {53}, + number = {6}, + pages = {2498-2508}, + year = {2014}, + doi = {10.1021/ie4033999} +} + +@article{pandas, + title={pandas: a foundational Python library for data analysis and statistics}, + author={Wes McKinney}, + journal={Python for High Performance and Scientific Computing}, + volume={14}, + year={2011} +} + +@article{SciPy, + author = {{Virtanen}, Pauli and {Gommers}, Ralf and {Oliphant}, + Travis E. and {Haberland}, Matt and {Reddy}, Tyler and + {Cournapeau}, David and {Burovski}, Evgeni and {Peterson}, Pearu + and {Weckesser}, Warren and {Bright}, Jonathan and {van der Walt}, + St{\'e}fan J. and {Brett}, Matthew and {Wilson}, Joshua and + {Jarrod Millman}, K. and {Mayorov}, Nikolay and {Nelson}, Andrew + R.~J. and {Jones}, Eric and {Kern}, Robert and {Larson}, Eric and + {Carey}, CJ and {Polat}, {\.I}lhan and {Feng}, Yu and {Moore}, + Eric W. and {Vand erPlas}, Jake and {Laxalde}, Denis and + {Perktold}, Josef and {Cimrman}, Robert and {Henriksen}, Ian and + {Quintero}, E.~A. and {Harris}, Charles R and {Archibald}, Anne M. + and {Ribeiro}, Ant{\^o}nio H. and {Pedregosa}, Fabian and + {van Mulbregt}, Paul and {Contributors}, SciPy 1. 0}, + title = {SciPy 1.0: Fundamental Algorithms for Scientific + Computing in Python}, + journal = {Nature Methods}, + year = "2020", + doi = {10.1038/s41592-019-0686-2}, +} + +@article{NumPy, + doi = {10.1109/mcse.2011.37}, + year = {2011}, + month = mar, + publisher = {Institute of Electrical and Electronics Engineers ({IEEE})}, + volume = {13}, + number = {2}, + pages = {22--30}, + author = {St{\'{e}}fan van der Walt and S Chris Colbert and Gaël Varoquaux}, + title = {The {NumPy} Array: A Structure for Efficient Numerical Computation}, + journal = {Computing in Science {\&} Engineering} +} + +@article{BTES, + author = {Chen, Shuang and Witte, Francesco and Kolditz, Olaf and Shao, + Haibing}, + year = {2019}, + month = {12}, + pages = {}, + title = {Shifted thermal extraction rates in large Borehole Heat Exchanger + array -a numerical experiment}, + journal = {Applied Thermal Engineering}, + doi = {10.1016/j.applthermaleng.2019.114750} +} + +@article{CAES, + author = {Pfeiffer, Wolf-Tilman and Witte, Francesco and Tuschy, Ilja and + Bauer, Sebastian}, + year = {2019}, + title = {Test cases for a coupled power-plant and geostorage model to + simulate compressed air energy storage in geological porous media}, + journal = {International Conference on Applied Energy, 12.-15.08.2019, + Västeras} +} + +@book{Baehr, + doi = {10.1007/978-3-662-49568-1}, + year = {2016}, + publisher = {Springer Berlin Heidelberg}, + author = {Baehr, Hans Dieter and Kabelac. Stephan}, + title = {Thermodynamik} +} + +@article{oemof, + doi = {10.1016/j.esr.2018.07.001}, + year = {2018}, + month = nov, + publisher = {Elsevier {BV}}, + volume = {22}, + pages = {16--25}, + author = {Hilpert, Simon and Kaldemeyer, Cord and Krien, Uwe and G\"{u}nther, Stephan and Wingenbach, Clemens and Plessmann, Guido}, + title = {The Open Energy Modelling Framework (oemof) - A new approach to facilitate open science in energy system modelling}, + journal = {Energy Strategy Reviews} +} + +@misc{ogs, + doi = {10.5281/ZENODO.591265}, + author = {Naumov, Dmitry Yu. and Fischer, Tom and Bilke, Lars and Rink, Karsten and Lehmann, Christoph and Watanabe, Norihiro and {Wenqing} and {Renchao.Lu} and Grunwald, Norbert and {Yonghui56} and {Jbathmann} and {Chaofan Chen} and {HBShaoUFZ} and {FZill} and {Xingyuanmiao} and {ShuangChen88} and {Boyanmeng} and Walther, Marc and {Joboog} and {Tianyuan Zheng} and {KeitaYoshioka} and {ZhangNing} and {ThieJan} and {Fparisio} and {Joergbuchwald} and {Ogsbot} and {Nagelt} and {NoCodeYet} and {Carolinh}}, + title = {ufz/ogs 6.2.2}, + publisher = {Zenodo}, + year = {2020} +} + +@book{Epple, + doi = {10.1007/978-3-7091-1182-6}, + year = {2012}, + publisher = {Springer Vienna}, + editor = {Epple, Bernd and Leithner, Reinhard and Linzer, Wladimir and + Walter, Heimo}, + title = {Simulation von Kraftwerken und Feuerungen} +} + +@book{IEA, + year = {2014}, + publisher = {IEA}, + editor = {{International Energy Agency}}, + title = {Technology Roadmap - Energy Storage} +} + +@article{Pfeiffer, + doi = {10.1144/petgeo2016-050}, + year = {2017}, + month = mar, + publisher = {Geological Society of London}, + volume = {23}, + number = {3}, + pages = {315--326}, + author = {Wolf Tilmann Pfeiffer and Christof Beyer and Sebastian Bauer}, + title = {Hydrogen storage in a heterogeneous sandstone formation: dimensioning and induced hydraulic effects}, + journal = {Petroleum Geoscience} +} diff --git a/paper.md b/paper.md new file mode 100644 index 000000000..403a04558 --- /dev/null +++ b/paper.md @@ -0,0 +1,153 @@ +--- +title: 'TESPy: Thermal Engineering Systems in Python' + +tags: + - Thermal Engineering + - Thermodynamics + - Power Plant Simulation + - Python + +authors: + - name: Francesco Witte + orcid: 0000-0003-4019-0390 + affiliation: "1, 2" + - name: Ilja Tuschy + affiliation: "1, 2" + +affiliations: + - name: Center for Sustainable Energy Systems, Flensburg + index: 1 + - name: Flensburg University of Applied Sciences + index: 2 + +date: 18. February 2020 + +bibliography: paper.bib + +--- + +# Summary + +TESPy (Thermal Engineering Systems in Python) provides a powerful +simulation toolkit for thermal process engineering, for instance power plants, +district heating systems or heat pumps. With the TESPy package it is possible to +design plants and simulate stationary operation. From that point, part-load +behavior can be predicted using underlying characteristics for each of the +plant's components. The component based structure combined with the solution +method offer very high flexibility regarding the plant's topology and its +parametrisation. + +# Motivation + +Thermal process simulation is a fundamental discipline in energy engineering. +Consequently, there are several well known commercial software solutions +available in this field, for example EBSILON Professional or Aspen Plus, mainly +used in the industrial environment. In order to open this kind of software to a +wide (scientific) audience, an open source solution is very promising. Its +prevalence in the scientific field and the availability of interfaces to other +programming languages plead for a python implementation. + +# Method + +The package is built of basic components, for example different types of +turbomachinery, heat exchangers, pipes, valves and mixers or splitters. To +simulate a specific plant an individual model is created connecting the +components to form a topological network. Steady state operation of the plant is +determined by the fluid's state on every connection (and therefore its change +within components) between the plant's individual components. Thus, based on the +components and parameters applied, TESPy generates a set of nonlinear equations +representing the plant's topology and its parametrisation. By formulating the +equations implicitly parameters and results are generally interchangeable +offering high flexibility in the user specifications. The system of equations is +numerically solved with an inbuilt solver applying the multi-dimensional +Newton-Raphson method to determine the mass flow, pressure, enthalpy and fluid +composition of every connection. This way, it is possible to solve for both +thermal as well as hydraulic state of the plant. + +To achieve this, TESPy implements balance equations (based on standard +literature, for example [@Baehr; @Epple]) for every component regarding + +• mass flow and fluid composition as well as + +• energy (covering thermal and hydraulic properties). + +In steady state, the total mass flow into a component must be equal to the total +mass flow leaving the component (eq. 1). Additionally, the mass balance of a +specific fluid fl is applied (eq. 2). The energy balance of all components is +derived from the stationary energy balance of open systems with multiple inlets +and outlets. Differences in flow velocity as well as height are neglected as +these are relatively small compared to change in enthalpy in thermal engineering +applications. The values of heat and power transferred, depend on the individual +component properties. For example, a pipe does not transfer power, thus only +heat may be transferred. In contrast, turbomachinery is considered adiabatic, +thus only transfers power (eq. 3). If chemical reactions take place, the +corresponding chemical mass balance is taken into account instead of equation 2. +On top of that, the energy balance is different, as the reaction enthalpy has to +be considered, too. Furthermore, it is necessary to compensate for the different +zero point definitions of enthalpy in the fluid properties of the reaction +components by defining a reference state. E. g. for a combustion chamber +equation 4 is implemented. + +$$0=\underset{i}{\sum}\dot{m}_{\mathrm{in,}i}-\underset{o}{\sum}\dot{m}_{\mathrm{out,}o}$$ + +$$0=\underset{i}{\sum}\dot{m}_{\mathrm{in,}i}\cdot x_{fl\mathrm{,in,}i}- +\underset{o}{\sum}\dot{m}_{\mathrm{out,}o}\cdot x_{fl\mathrm{,out,}o} +\ \forall fl\in\mathrm{network\ fluids}$$ + +$$0=\underset{o}{\sum}\dot{m}_{\mathrm{out,}o}\cdot h_{\mathrm{out,}o}- +\underset{i}{\sum}\dot{m}_{\mathrm{in,}i}\cdot h_{\mathrm{in,}i}-P-\dot{Q}$$ + +$$0= +\dot{m}_{\mathrm{out}}\cdot\left(h_{\mathrm{out}}-h_{\mathrm{out,ref}}\right)- +\underset{i}{\sum}\dot{m}_{\mathrm{in,}i}\cdot\left(h_{\mathrm{in}}- +h_{\mathrm{in,ref}}\right)_{i}- +\dot{m}_{\mathrm{in}}\cdot\underset{j}{\sum}LHV_{j}\cdot x_{j}$$ + +After designing a specific plant, part-load performance can be determined. For +this, design specific component parameters are calculated in the design case, +for example the area independent heat transfer coefficient $kA$ of heat +exchangers. The heat transferred at a different operation point may then be +calculated using the design value of $kA$ applying equation (5). + +$$0=\dot{Q}-kA\cdot\Delta\vartheta_{\mathrm{log}}$$ + +In general, these parameters can be adjusted using lookup table functions to +match the model behavior to measured data. This is especially useful if +components with well known characteristics should be implemented in a different +plant or at different operating conditions. Due to the modular structure of +TESPy, new equations or characteristic functions to further improve the +representation of an actual plant can easily be added. + +# Example Implementations + +The core strength of TESPy lies in the generic and component based architecture +allowing to simulate technologically and topologically different thermal +engineering applications. + +For example, as the increasing share of renewable energy sources to mitigate +climate change will result in a significant storage demand, underground gas +storage is considered a large scale energy storage option [@IEA; @Pfeiffer]. Due +to the feedback regarding the physical parameters of the fluid exchanged between +the geological storage and the above-ground plant, an integrated simulation of +the storage and the power plant is necessary for a detailed assessment of such +storage options [@CAES]. Another important task in energy system transition is +renewable heating: Heat pumps using subsurface heat to provide heating on +household or even district level show analogous feedback reactions with the +underground. As their electricity consumption highly depends on the heat source +temperature level, simulator coupling provides valuable assessment possibilities +in this field, too. Additionally, TESPy has been coupled with OpenGeoSys +[@ogs] for pipeline network simulation of borehole thermal energy storage arrays +[@BTES]. + +# Acknowledgements + +This work is supported by University of Applied Sciences and the Center for +Sustainable Energy Systems in Flensburg. It is part of the open energy modeling +framework (oemof) [@oemof]. Many thanks to all +[contributers](https://github.com/oemof/tespy/graphs/contributors). + +Key parts of TESPy require the following scientific software packages: CoolProp +[@CoolProp], NumPy [@NumPy], pandas [@pandas]. Other packages implemented are +tabulate and SciPy. + +# References diff --git a/rtd_requirements b/rtd_requirements index 6ccafc3f9..b4e780d5e 100644 --- a/rtd_requirements +++ b/rtd_requirements @@ -1 +1,2 @@ matplotlib +sphinxcontrib-bibtex diff --git a/setup.py b/setup.py index f873f422d..0b31af6d4 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(fname): setup(name='TESPy', - version='0.2.1', + version='0.2.2', description='Thermal Engineering Systems in Python (TESPy)', url='http://github.com/oemof/tespy', author='Francesco Witte', diff --git a/tespy/__init__.py b/tespy/__init__.py index b1a5834b6..6324ecc3e 100644 --- a/tespy/__init__.py +++ b/tespy/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -__version__ = '0.2.1 - Fourier\'s Fable' +__version__ = '0.2.2 - Rankine\'s Realm' # tespy networks imports from tespy.networks import ( diff --git a/tespy/components/basics.py b/tespy/components/basics.py index e1f7e872d..399381c90 100644 --- a/tespy/components/basics.py +++ b/tespy/components/basics.py @@ -356,7 +356,7 @@ class subsystem_interface(component): printout: boolean Include this component in the network's results printout. - num_inter : float/tespy.helpers.dc_simple + num_inter : float/tespy.tools.data_containers.dc_simple Number of interfaces for subsystem. Note @@ -494,24 +494,6 @@ def derivatives(self, vek_z): ###################################################################### # all derivatives are static - def fluid_deriv(self): - r""" - Calculate the partial derivatives for all fluid balance equations. - - Returns - ------- - deriv : list - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros((self.num_nw_fluids * self.num_i, - 2 * self.num_i, - self.num_nw_vars)) - for i in range(self.num_i): - for j in range(self.num_nw_fluids): - deriv[i * self.num_nw_fluids + j, i, j + 3] = 1 - deriv[i * self.num_nw_fluids + j, self.num_i + i, j + 3] = -1 - return deriv.tolist() - def inout_deriv(self, pos): r""" Calculate partial derivatives. @@ -534,4 +516,4 @@ def inout_deriv(self, pos): deriv[i, i, pos] = 1 for j in range(self.num_i): deriv[j, j + self.num_i, pos] = -1 - return deriv.tolist() + return deriv diff --git a/tespy/components/combustion.py b/tespy/components/combustion.py index 783e89e3d..54043c20b 100644 --- a/tespy/components/combustion.py +++ b/tespy/components/combustion.py @@ -105,10 +105,10 @@ class combustion_chamber(component): printout: boolean Include this component in the network's results printout. - lamb : float/tespy.helpers.dc_cp + lamb : float/tespy.tools.data_containers.dc_cp Actual oxygen to stoichiometric oxygen ratio, :math:`\lambda/1`. - ti : float/tespy.helpers.dc_cp + ti : float/tespy.tools.data_containers.dc_cp Thermal input, (:math:`{LHV \cdot \dot{m}_f}`), :math:`ti/\text{W}`. @@ -759,7 +759,6 @@ def energy_balance(self): try: flow = [0, p, 0, {self.h2o: 1}] h_steam = h_mix_pQ(flow, 1) - # CP.PropsSI('H', 'P', p, 'Q', 1, self.h2o) except ValueError: flow = [0, 615, 0, {self.h2o: 1}] h_steam = h_mix_pQ(flow, 1) @@ -986,7 +985,7 @@ def convergence_check(self, nw): m = 0 for i in inl: - if i.init_csv is False: + if i.good_starting_values is False: if i.m.val_SI < 0 and not i.m.val_set: i.m.val_SI = 0.01 m += i.m.val_SI @@ -994,7 +993,7 @@ def convergence_check(self, nw): ###################################################################### # check fluid composition for o in outl: - if o.init_csv is False: + if o.good_starting_values is False: fluids = [f for f in o.fluid.val.keys() if not o.fluid.val_set[f]] for f in fluids: @@ -1031,7 +1030,7 @@ def convergence_check(self, nw): ###################################################################### # flue gas propagation for o in outl: - if o.init_csv is False: + if o.good_starting_values is False: if o.m.val_SI < 0 and not o.m.val_set: o.m.val_SI = 10 nw.init_target(o, o.t) @@ -1045,7 +1044,7 @@ def convergence_check(self, nw): # search fuel and air inlet for i in inl: fuel_found = False - if i.init_csv is False: + if i.good_starting_values is False: fuel = 0 for f in self.fuel_list: fuel += i.fluid.val[f] @@ -1236,10 +1235,10 @@ class combustion_chamber_stoich(combustion_chamber): path : str Path to existing fluid property table. - lamb : float/tespy.helpers.dc_cp + lamb : float/tespy.tools.data_containers.dc_cp Air to stoichiometric air ratio, :math:`\lambda/1`. - ti : float/tespy.helpers.dc_cp + ti : float/tespy.tools.data_containers.dc_cp Thermal input, (:math:`{LHV \cdot \dot{m}_f}`), :math:`ti/\text{W}`. @@ -2040,8 +2039,8 @@ class combustion_engine(combustion_chamber): 0 = p_{1,in} \cdot pr1 - p_{1,out}\\ 0 = p_{2,in} \cdot pr2 - p_{2,out} - - :func:`tespy.components.components.component.zeta_func` - - :func:`tespy.components.components.component.zeta2_func` + - loop 1 :func:`tespy.components.components.component.zeta_func` + - loop 2 :func:`tespy.components.components.component.zeta_func` Available fuels @@ -2090,49 +2089,49 @@ class combustion_engine(combustion_chamber): printout: boolean Include this component in the network's results printout. - lamb : float/tespy.helpers.dc_cp + lamb : float/tespy.tools.data_containers.dc_cp Air to stoichiometric air ratio, :math:`\lambda/1`. - ti : float/tespy.helpers.dc_cp + ti : float/tespy.tools.data_containers.dc_cp Thermal input, (:math:`{LHV \cdot \dot{m}_f}`), :math:`ti/\text{W}`. - P : str/float/tespy.helpers.dc_cp + P : str/float/tespy.tools.data_containers.dc_cp Power output, :math:`P/\text{W}`. - Q1 : str/float/tespy.helpers.dc_cp + Q1 : float/tespy.tools.data_containers.dc_cp Heat output 1, :math:`\dot Q/\text{W}`. - Q2 : str/float/tespy.helpers.dc_cp + Q2 : float/tespy.tools.data_containers.dc_cp Heat output 2, :math:`\dot Q/\text{W}`. - Qloss : str/float/tespy.helpers.dc_cp + Qloss : str/float/tespy.tools.data_containers.dc_cp Heat loss, :math:`\dot Q_{loss}/\text{W}`. - pr1 : str/float/tespy.helpers.dc_cp + pr1 : float/tespy.tools.data_containers.dc_cp Pressure ratio heat outlet 1, :math:`pr/1`. - pr2 : str/float/tespy.helpers.dc_cp + pr2 : float/tespy.tools.data_containers.dc_cp Pressure ratio heat outlet 2, :math:`pr/1`. - zeta1 : str/float/tespy.helpers.dc_cp - Pressure ratio heat outlet 2, + zeta1 : float/tespy.tools.data_containers.dc_cp + Geometry independent friction coefficient heating loop 1, :math:`\zeta/\frac{1}{\text{m}^4}`. - zeta2 : str/float/tespy.helpers.dc_cp - Pressure ratio heat outlet 2, + zeta2 : float/tespy.tools.data_containers.dc_cp + Geometry independent friction coefficient heating loop 2, :math:`\zeta/\frac{1}{\text{m}^4}`. - tiP_char : str/tespy.helpers.dc_cc + tiP_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc Characteristic line linking fuel input to power output. - Q1_char : str/tespy.helpers.dc_cc + Q1_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc Characteristic line linking heat output 1 to power output. - Q2_char : str/tespy.helpers.dc_cc + Q2_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc Characteristic line linking heat output 2 to power output. - Qloss_char : str/tespy.helpers.dc_cc + Qloss_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc Characteristic line linking heat loss to power output. Note @@ -2391,12 +2390,14 @@ def equations(self): # equations for specified zeta values at cooling loops if self.zeta1.is_set: if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: - self.vec_res[k] = self.zeta_func() + self.vec_res[k] = self.zeta_func( + zeta='zeta1', inconn=0, outconn=0) k += 1 if self.zeta2.is_set: if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: - self.vec_res[k] = self.zeta2_func() + self.vec_res[k] = self.zeta_func( + zeta='zeta2', inconn=1, outconn=1) k += 1 def derivatives(self, vec_z): @@ -2601,29 +2602,39 @@ def derivatives(self, vec_z): if self.zeta1.is_set: f = self.zeta_func if not vec_z[0, 0]: - self.mat_deriv[k, 0, 0] = self.numeric_deriv(f, 'm', 0) + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[0, 1]: - self.mat_deriv[k, 0, 1] = self.numeric_deriv(f, 'p', 0) + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[0, 2]: - self.mat_deriv[k, 0, 2] = self.numeric_deriv(f, 'h', 0) + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[4, 1]: - self.mat_deriv[k, 4, 1] = self.numeric_deriv(f, 'p', 4) + self.mat_deriv[k, 4, 1] = self.numeric_deriv( + f, 'p', 4, zeta='zeta1', inconn=0, outconn=0) if not vec_z[4, 2]: - self.mat_deriv[k, 4, 2] = self.numeric_deriv(f, 'h', 4) + self.mat_deriv[k, 4, 2] = self.numeric_deriv( + f, 'h', 4, zeta='zeta1', inconn=0, outconn=0) k += 1 if self.zeta2.is_set: - f = self.zeta2_func + f = self.zeta_func if not vec_z[1, 0]: - self.mat_deriv[k, 1, 0] = self.numeric_deriv(f, 'm', 1) + self.mat_deriv[k, 1, 0] = self.numeric_deriv( + f, 'm', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[1, 1]: - self.mat_deriv[k, 1, 1] = self.numeric_deriv(f, 'p', 1) + self.mat_deriv[k, 1, 1] = self.numeric_deriv( + f, 'p', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[1, 2]: - self.mat_deriv[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + self.mat_deriv[k, 1, 2] = self.numeric_deriv( + f, 'h', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[5, 1]: - self.mat_deriv[k, 5, 1] = self.numeric_deriv(f, 'p', 5) + self.mat_deriv[k, 5, 1] = self.numeric_deriv( + f, 'p', 5, zeta='zeta2', inconn=1, outconn=1) if not vec_z[5, 2]: - self.mat_deriv[k, 5, 2] = self.numeric_deriv(f, 'h', 5) + self.mat_deriv[k, 5, 2] = self.numeric_deriv( + f, 'h', 5, zeta='zeta2', inconn=1, outconn=1) k += 1 def fluid_func(self): @@ -2735,7 +2746,7 @@ def energy_balance(self): .. math:: \begin{split} - 0 = & \sum_i \dot{m}_{in,i} \cdot + res = & \sum_i \dot{m}_{in,i} \cdot \left( h_{in,i} - h_{in,i,ref} \right)\\ & - \sum_j \dot{m}_{out,3} \cdot \left( h_{out,3} - h_{out,3,ref} \right)\\ diff --git a/tespy/components/components.py b/tespy/components/components.py index 63a1c21d7..153825781 100644 --- a/tespy/components/components.py +++ b/tespy/components/components.py @@ -492,13 +492,15 @@ def fluid_func(self): Vector of residual values for component's fluid balance. .. math:: - 0 = fluid_{i,in} - fluid_{i,out} \; - \forall i \in \mathrm{fluid} + + 0 = fluid_{i,in_{j}} - fluid_{i,out_{j}} \; + \forall i \in \mathrm{fluid}, \; \forall j \in inlets/outlets """ vec_res = [] - for fluid, x in self.inl[0].fluid.val.items(): - vec_res += [x - self.outl[0].fluid.val[fluid]] + for i in range(self.num_i): + for fluid, x in self.inl[0].fluid.val.items(): + vec_res += [x - self.outl[0].fluid.val[fluid]] return vec_res def fluid_deriv(self): @@ -510,14 +512,13 @@ def fluid_deriv(self): deriv : list Matrix with partial derivatives for the fluid equations. """ - deriv = np.zeros((self.num_nw_fluids, - 2 + self.num_vars, + deriv = np.zeros((self.num_nw_fluids * self.num_i, + 2 * self.num_i + self.num_vars, self.num_nw_vars)) - i = 0 - for fluid in self.nw_fluids: - deriv[i, 0, i + 3] = 1 - deriv[i, 1, i + 3] = -1 - i += 1 + for i in range(self.num_i): + for j in range(self.num_nw_fluids): + deriv[i * self.num_nw_fluids + j, i, j + 3] = 1 + deriv[i * self.num_nw_fluids + j, self.num_i + i, j + 3] = -1 return deriv # %% @@ -651,10 +652,22 @@ def numeric_deriv(self, func, dx, pos, **kwargs): # %% - def zeta_func(self): + def zeta_func(self, zeta='', inconn=0, outconn=0): r""" Calculate residual value of :math:`\zeta`-function. + Parameters + ---------- + zeta : str + Component parameter to evaluate the zeta_func on, e. g. + :code:`zeta1`. + + inconn : int + Connection index of inlet. + + outconn : int + Connection index of outlet. + Returns ------- val : float @@ -681,59 +694,15 @@ def zeta_func(self): \frac{\zeta}{D^4} = \frac{\Delta p \cdot \pi^2} {8 \cdot \dot{m}^2 \cdot v} """ - i = self.inl[0].to_flow() - o = self.outl[0].to_flow() - if hasattr(self, 'zeta'): - val = self.zeta.val - else: - val = self.zeta1.val + zeta = self.get_attr(zeta).val + i = self.inl[inconn].to_flow() + o = self.outl[outconn].to_flow() if abs(i[0]) < 1e-4: return i[1] - o[1] else: - v_i = v_mix_ph(i, T0=self.inl[0].T.val_SI) - v_o = v_mix_ph(o, T0=self.outl[0].T.val_SI) - return (val - (i[1] - o[1]) * np.pi ** 2 / - (8 * abs(i[0]) * i[0] * (v_i + v_o) / 2)) - - def zeta2_func(self): - r""" - Calculate residual value of :math:`\zeta_2`-function. - - Returns - ------- - val : float - Residual value of function. - - .. math:: - - val = \begin{cases} - p_{in} - p_{out} & |\dot{m}| < \epsilon \\ - \frac{\zeta_2}{D^4} - \frac{(p_{2,in} - p_{2,out}) \cdot \pi^2} - {8 \cdot \dot{m}_{2,in} \cdot |\dot{m}_{2,in}| \cdot - \frac{v_{2,in} + v_{2,out}}{2}} & - |\dot{m}| > \epsilon - \end{cases} - - Note - ---- - The zeta value is caluclated on the basis of a given pressure loss at - a given flow rate in the design case. As the cross sectional area A - will not change, it is possible to handle the equation in this way: - - .. math:: - - \frac{\zeta_2}{D^4} = \frac{\Delta p_2 \cdot \pi^2} - {8 \cdot \dot{m}_2^2 \cdot v} - """ - i = self.inl[1].to_flow() - o = self.outl[1].to_flow() - - if abs(i[0]) < 1e-4: - return i[1] - o[1] - else: - v_i = v_mix_ph(i, T0=self.inl[1].T.val_SI) - v_o = v_mix_ph(o, T0=self.outl[1].T.val_SI) - return (self.zeta2.val - (i[1] - o[1]) * np.pi ** 2 / + v_i = v_mix_ph(i, T0=self.inl[inconn].T.val_SI) + v_o = v_mix_ph(o, T0=self.outl[outconn].T.val_SI) + return (zeta - (i[1] - o[1]) * np.pi ** 2 / (8 * abs(i[0]) * i[0] * (v_i + v_o) / 2)) diff --git a/tespy/components/customs.py b/tespy/components/customs.py new file mode 100644 index 000000000..26c3398f1 --- /dev/null +++ b/tespy/components/customs.py @@ -0,0 +1,742 @@ +# -*- coding: utf-8 + +"""Module for custom components. + +Components in this module: + + - :func:`tespy.components.customs.orc_evaporator` + +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/customs.py + +SPDX-License-Identifier: MIT +""" + +import logging + +import numpy as np + +from tespy.components.components import component + +from tespy.tools.data_containers import dc_cp, dc_simple +from tespy.tools.fluid_properties import ( + h_mix_pT, s_mix_ph, v_mix_ph, visc_mix_ph, T_mix_ph, + dh_mix_dpQ, h_mix_pQ, T_bp_p, memorise + ) +from tespy.tools.helpers import lamb, single_fluid + +# %% + + +class orc_evaporator(component): + r"""Evaporator of the geothermal Organic Rankine Cycle (ORC). + + Generally, the hot side of the geo-fluid from the geothermal wells deliver + two phases: steam and brine. In order to fully use the energy of the + geo-fluid, there are 2 inlets at the hot side. + + The ORC evaporator represents counter current evaporators. Both, two hot + and one cold side of the evaporator, are simulated. + + Equations + + **mandatory equations** + + - :func:`tespy.components.components.component.fluid_func` + - :func:`tespy.components.customs.orc_evaporator.mass_flow_func` + + - :func:`tespy.components.customs.orc_evaporator.energy_func` + + .. math:: + + 0 = p_{1,in} \cdot pr1 - p_{1,out}\\ + 0 = p_{2,in} \cdot pr2 - p_{2,out}\\ + 0 = p_{3,in} \cdot pr3 - p_{3,out} + + - hot side steam :func:`tespy.components.components.component.zeta_func` + - hot side brine :func:`tespy.components.components.component.zeta_func` + - worling fluid :func:`tespy.components.components.component.zeta_func` + + **mandatory equations at outlet of the steam + from geothermal heat source side** + + .. math:: + + 0 = h_{1,out} - h\left(p, x=0 \right)\\ + x: \text{vapour mass fraction} + + **mandatory equations at outlet of the working fluid + of being evaporated side** + + .. math:: + + 0 = h_{3,out} - h\left(p, x=1 \right)\\ + x: \text{vapour mass fraction} + + Inlets/Outlets + + - in1, in2, in3 (index 1: steam from geothermal heat source, + index 2: brine from geothermal heat source, + index 3: working fluid of being evaporated) + - out1, out2, out3 (index 1: steam from geothermal heat source, + index 2: brine from geothermal heat source, + index 3: working fluid of being evaporated) + + Image + + .. image:: _images/orc_evaporator.svg + :scale: 100 % + :alt: alternative text + :align: center + + 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. + + Q : float/tespy.tools.data_containers.dc_cp + Heat transfer, :math:`Q/\text{W}`. + + pr1 : float/tespy.tools.data_containers.dc_cp + Outlet to inlet pressure ratio at hot side 1 (steam), + :math:`pr/1`. + + pr2 : float/tespy.tools.data_containers.dc_cp + Outlet to inlet pressure ratio at hot side 2 (brine), + :math:`pr/1`. + + pr3 : float/tespy.tools.data_containers.dc_cp + Outlet to inlet pressure ratio at cold side (working fluid), + :math:`pr/1`. + + zeta1 : float/tespy.tools.data_containers.dc_cp + Geometry independent friction coefficient at hot side 1 (steam), + :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. + + zeta2 : float/tespy.tools.data_containers.dc_cp + Geometry independent friction coefficient at hot side 2 (brine), + :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. + + zeta3 : float/tespy.tools.data_containers.dc_cp + Geometry independent friction coefficient at cold side (working fluid), + :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. + + subcooling : bool + Enable/disable subcooling at oulet of the hot side 1, + default value: disabled (False). + + overheating : bool + Enable/disable overheating at oulet of the cold side, + default value: disabled (False). + + Note + ---- + The ORC evaporator has an additional equation for enthalpy at the outlet of + the geothermal steam: The fluid leaves the component in saturated liquid + state. If code:`subcooling` is activated (:code:`True`), it is possible to + specify the enthalpy at the outgoing connection manually. + + Additionally, an equation for enthalpy at the outlet of the working fluid + is imposed: It leaves the component in saturated gas state. If + :code:`overheating` is enabled (:code:`True`), it is possible to specify + the enthalpy at the outgoing connection manually. + + Example + ------- + A two-phase geo-fluid is used as the heat source for evaporating the + working fluid. We calculate the mass flow of the working fluid with known + steam and brine mass flow. + + >>> from tespy.connections import connection + >>> from tespy.networks import network + >>> from tespy.components import source, sink + >>> from tespy.components.customs import orc_evaporator + >>> fluids = ['water', 'Isopentane'] + >>> nw = network(fluids=fluids, iterinfo=False) + >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') + >>> evaporator = orc_evaporator('geothermal orc evaporator') + >>> evaporator.component() + 'orc_evaporator' + >>> source_wf = source('working fluid source') + >>> sink_wf = sink('working fluid sink') + >>> source_s = source('steam source') + >>> source_b = source('brine source') + >>> sink_s = sink('steam sink') + >>> sink_b = sink('brine sink') + >>> eva_wf_in = connection(source_wf, 'out1', evaporator, 'in3') + >>> eva_wf_out = connection(evaporator, 'out3', sink_wf, 'in1') + >>> eva_steam_in = connection(source_s, 'out1', evaporator, 'in1') + >>> eva_sink_s = connection(evaporator, 'out1', sink_s, 'in1') + >>> eva_brine_in = connection(source_b, 'out1', evaporator, 'in2') + >>> eva_sink_b = connection(evaporator, 'out2', sink_b, 'in1') + >>> nw.add_conns(eva_wf_in, eva_wf_out) + >>> nw.add_conns(eva_steam_in, eva_sink_s) + >>> nw.add_conns(eva_brine_in, eva_sink_b) + + The orc working fluids leaves the evaporator in saturated steam state, the + geothermal steam leaves the component in staturated liquid state. We imply + the state of geothermal steam and brine with the corresponding mass flow as + well as the working fluid's state at the evaporator inlet. The pressure + ratio is specified for each of the three streams. + + >>> evaporator.set_attr(pr1=0.95, pr2=0.98, pr3=0.99) + >>> eva_wf_in.set_attr(T=111, p=11, + ... fluid={'water': 0, 'Isopentane': 1}) + >>> eva_steam_in.set_attr(T=147, p=4.3, m=20, + ... fluid={'water': 1, 'Isopentane': 0}) + >>> eva_brine_in.set_attr(T=147, p=10.2, m=190, + ... fluid={'water': 1, 'Isopentane': 0}) + >>> eva_sink_b.set_attr(T=117) + >>> nw.solve(mode='design') + + Check the state of the steam and working fluid outlet: + + >>> eva_wf_out.x.val + 1.0 + >>> eva_sink_s.x.val + 0.0 + """ + + @staticmethod + def component(): + return 'orc_evaporator' + + @staticmethod + def attr(): + return {'Q': dc_cp(max_val=0), + 'pr1': dc_cp(max_val=1), 'pr2': dc_cp(max_val=1), + 'pr3': dc_cp(max_val=1), + 'zeta1': dc_cp(min_val=0), 'zeta2': dc_cp(min_val=0), + 'zeta3': dc_cp(min_val=0), + 'subcooling': dc_simple(val=False), + 'overheating': dc_simple(val=False), + 'SQ1': dc_simple(), 'SQ2': dc_simple(), 'SQ3': dc_simple(), + 'Sirr': dc_simple()} + + @staticmethod + def inlets(): + return ['in1', 'in2', 'in3'] + + @staticmethod + def outlets(): + return ['out1', 'out2', 'out3'] + + def comp_init(self, nw): + + component.comp_init(self, nw) + + # number of mandatroy equations for + # fluid balance: num_fl * 3 + # mass flow: 3 + # energy balance: 1 + self.num_eq = self.num_nw_fluids * 3 + 3 + 1 + # enthalpy hot side 1 outlet (if not subcooling): 1 + if self.subcooling.val is False: + self.num_eq += 1 + # enthalpy cold side outlet (if not overheating): 1 + if self.overheating.val is False: + self.num_eq += 1 + for var in [self.Q, self.pr1, self.pr2, self.pr3, + self.zeta1, self.zeta2, self.zeta3, ]: + if var.is_set is True: + self.num_eq += 1 + + self.mat_deriv = np.zeros(( + self.num_eq, + self.num_i + self.num_o + self.num_vars, + self.num_nw_vars)) + + self.vec_res = np.zeros(self.num_eq) + pos = self.num_nw_fluids * 3 + self.mat_deriv[0:pos] = self.fluid_deriv() + self.mat_deriv[pos:pos + 3] = self.mass_flow_deriv() + + def equations(self): + r""" + Calculate vector vec_res with results of equations for this component. + + Returns + ------- + vec_res : list + Vector of residual values. + """ + k = 0 + + ###################################################################### + # equations for fluid balance + self.vec_res[k:k + self.num_nw_fluids * 3] = self.fluid_func() + k += self.num_nw_fluids * 3 + + ###################################################################### + # equations for mass flow balance + self.vec_res[k:k + 3] = self.mass_flow_func() + k += 3 + + ###################################################################### + # equations for energy balance + self.vec_res[k] = self.energy_func() + k += 1 + + ###################################################################### + # equations for specified heat transfer + if self.Q.is_set: + self.vec_res[k] = ( + self.inl[2].m.val_SI * ( + self.outl[2].h.val_SI - self.inl[2].h.val_SI) - self.Q.val) + k += 1 + + ###################################################################### + # equations for specified pressure ratio at hot side 1 + if self.pr1.is_set: + self.vec_res[k] = ( + self.pr1.val * self.inl[0].p.val_SI - + self.outl[0].p.val_SI) + k += 1 + + ###################################################################### + # equations for specified pressure ratio at hot side 2 + if self.pr2.is_set: + self.vec_res[k] = ( + self.pr2.val * self.inl[1].p.val_SI - + self.outl[1].p.val_SI) + k += 1 + + ###################################################################### + # equations for specified pressure ratio at cold side + if self.pr3.is_set: + self.vec_res[k] = ( + self.pr3.val * self.inl[2].p.val_SI - + self.outl[2].p.val_SI) + k += 1 + + ###################################################################### + # equations for specified zeta at hot side 1 + if self.zeta1.is_set: + self.vec_res[k] = self.zeta_func(zeta='zeta1', inconn=0, outconn=0) + k += 1 + + ###################################################################### + # equations for specified zeta at hot side 2 + if self.zeta2.is_set: + self.vec_res[k] = self.zeta_func(zeta='zeta2', inconn=1, outconn=1) + k += 1 + + ###################################################################### + # equations for specified zeta at cold side + if self.zeta3.is_set: + self.vec_res[k] = self.zeta_func(zeta='zeta3', inconn=2, outconn=2) + k += 1 + + ###################################################################### + # equation for saturated liquid at hot side 1 outlet + if self.subcooling.val is False: + o1 = self.outl[0].to_flow() + self.vec_res[k] = o1[2] - h_mix_pQ(o1, 0) + k += 1 + + ###################################################################### + # equation for saturated gas at cold side outlet + if self.overheating.val is False: + o3 = self.outl[2].to_flow() + self.vec_res[k] = o3[2] - h_mix_pQ(o3, 1) + k += 1 + + def derivatives(self, vec_z): + r""" + Calculate matrix of partial derivatives for given equations. + + Returns + ------- + mat_deriv : ndarray + Matrix of partial derivatives. + """ + + ###################################################################### + # derivatives fluid and mass balance are static + k = self.num_nw_fluids * 3 + 3 + + ###################################################################### + # derivatives for energy balance equation + # mat_deriv += self.energy_deriv() + for i in range(3): + self.mat_deriv[k, i, 0] = ( + self.outl[i].h.val_SI - self.inl[i].h.val_SI) + self.mat_deriv[k, i, 2] = -self.inl[i].m.val_SI + + self.mat_deriv[k, i + 3, 2] = self.inl[i].m.val_SI + k += 1 + + ###################################################################### + # derivatives for specified heat transfer + if self.Q.is_set: + self.mat_deriv[k, 2, 0] = ( + self.outl[2].h.val_SI - self.inl[2].h.val_SI) + self.mat_deriv[k, 2, 2] = -self.inl[2].m.val_SI + self.mat_deriv[k, 5, 2] = self.inl[2].m.val_SI + k += 1 + + ###################################################################### + # derivatives for specified pressure ratio at hot side 1 + if self.pr1.is_set: + self.mat_deriv[k, 0, 1] = self.pr1.val + self.mat_deriv[k, 3, 1] = -1 + k += 1 + + ###################################################################### + # derivatives for specified pressure ratio at hot side 2 + if self.pr2.is_set: + self.mat_deriv[k, 1, 1] = self.pr2.val + self.mat_deriv[k, 4, 1] = -1 + k += 1 + + ###################################################################### + # derivatives for specified pressure ratio at cold side + if self.pr3.is_set: + self.mat_deriv[k, 2, 1] = self.pr3.val + self.mat_deriv[k, 5, 1] = -1 + k += 1 + + ###################################################################### + # derivatives for specified zeta at hot side 1 + if self.zeta1.is_set: + f = self.zeta_func + if not vec_z[0, 0]: + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta1', inconn=0, outconn=0) + if not vec_z[0, 1]: + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta1', inconn=0, outconn=0) + if not vec_z[0, 2]: + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta1', inconn=0, outconn=0) + if not vec_z[3, 1]: + self.mat_deriv[k, 3, 1] = self.numeric_deriv( + f, 'p', 3, zeta='zeta1', inconn=0, outconn=0) + if not vec_z[3, 2]: + self.mat_deriv[k, 3, 2] = self.numeric_deriv( + f, 'h', 3, zeta='zeta1', inconn=0, outconn=0) + k += 1 + + ###################################################################### + # derivatives for specified zeta at hot side 2 + if self.zeta2.is_set: + f = self.zeta_func + if not vec_z[1, 0]: + self.mat_deriv[k, 1, 0] = self.numeric_deriv( + f, 'm', 1, zeta='zeta2', inconn=1, outconn=1) + if not vec_z[1, 1]: + self.mat_deriv[k, 1, 1] = self.numeric_deriv( + f, 'p', 1, zeta='zeta2', inconn=1, outconn=1) + if not vec_z[1, 2]: + self.mat_deriv[k, 1, 2] = self.numeric_deriv( + f, 'h', 1, zeta='zeta2', inconn=1, outconn=1) + if not vec_z[4, 1]: + self.mat_deriv[k, 4, 1] = self.numeric_deriv( + f, 'p', 4, zeta='zeta2', inconn=1, outconn=1) + if not vec_z[4, 2]: + self.mat_deriv[k, 4, 2] = self.numeric_deriv( + f, 'h', 4, zeta='zeta2', inconn=1, outconn=1) + k += 1 + + ###################################################################### + # derivatives for specified zeta at cold side + if self.zeta3.is_set: + f = self.zeta_func + if not vec_z[2, 0]: + self.mat_deriv[k, 2, 0] = self.numeric_deriv( + f, 'm', 2, zeta='zeta3', inconn=2, outconn=2) + if not vec_z[2, 1]: + self.mat_deriv[k, 2, 1] = self.numeric_deriv( + f, 'p', 2, zeta='zeta3', inconn=2, outconn=2) + if not vec_z[2, 2]: + self.mat_deriv[k, 2, 2] = self.numeric_deriv( + f, 'h', 2, zeta='zeta3', inconn=2, outconn=2) + if not vec_z[5, 1]: + self.mat_deriv[k, 5, 1] = self.numeric_deriv( + f, 'p', 5, zeta='zeta3', inconn=2, outconn=2) + if not vec_z[5, 2]: + self.mat_deriv[k, 5, 2] = self.numeric_deriv( + f, 'h', 5, zeta='zeta3', inconn=2, outconn=2) + k += 1 + + ###################################################################### + # derivatives for saturated liquid at hot side 1 outlet equation + if self.subcooling.val is False: + o1 = self.outl[0].to_flow() + self.mat_deriv[k, 3, 1] = -dh_mix_dpQ(o1, 0) + self.mat_deriv[k, 3, 2] = 1 + k += 1 + + ###################################################################### + # derivatives for saturated gas at cold side outlet 3 equation + if self.overheating.val is False: + o3 = self.outl[2].to_flow() + self.mat_deriv[k, 5, 1] = -dh_mix_dpQ(o3, 1) + self.mat_deriv[k, 5, 2] = 1 + k += 1 + + def mass_flow_func(self): + r""" + Calculate the residual value of mass flow balance equations. + + Returns + ------- + vec_res : list + Vector with residual value for component's mass flow balance. + + .. math:: + + 0 = \dot{m}_{in,i} - \dot{m}_{out,i} \; + \forall i \in inlets/outlets + """ + vec_res = [] + for i in range(self.num_i): + vec_res += [self.inl[i].m.val_SI - self.outl[i].m.val_SI] + return vec_res + + def mass_flow_deriv(self): + r""" + Calculate the partial derivatives for all mass flow balance equations. + + Returns + ------- + deriv : list + Matrix with partial derivatives for the mass flow balance + equations. + """ + deriv = np.zeros((self.num_i, 2 * self.num_i, self.num_nw_vars)) + for i in range(self.num_i): + deriv[i, i, 0] = 1 + for j in range(self.num_i): + deriv[j, j + self.num_i, 0] = -1 + return deriv + + def energy_func(self): + r""" + Equation for heat exchanger energy balance. + + Returns + ------- + res : float + Residual value of equation. + + .. math:: + + \begin{split} + res = & + \dot{m}_{1,in} \cdot \left(h_{1,out} - h_{1,in} \right) \\ + &+ \dot{m}_{2,in} \cdot \left(h_{2,out} - h_{2,in} \right) \\ + &+ \dot{m}_{3,in} \cdot \left(h_{3,out} - h_{3,in} \right) + \end{split} + """ + + return (self.inl[0].m.val_SI * (self.outl[0].h.val_SI - + self.inl[0].h.val_SI) + + self.inl[1].m.val_SI * (self.outl[1].h.val_SI - + self.inl[1].h.val_SI) + + self.inl[2].m.val_SI * (self.outl[2].h.val_SI - + self.inl[2].h.val_SI)) + + def bus_func(self, bus): + r""" + Calculate the residual value of the bus function. + + Parameters + ---------- + bus : tespy.connections.bus + TESPy bus object. + + Returns + ------- + val : float + Residual value of equation. + + .. math:: + + val = P \cdot f\left( \frac{P}{P_{ref}}\right) + + P = \dot{m}_{3,in} \cdot \left( h_{3,out} - h_{3,in} \right) + """ + i = self.inl[2].to_flow() + o = self.outl[2].to_flow() + + val = i[0] * (o[2] - i[2]) + if np.isnan(bus.P_ref): + expr = 1 + else: + expr = abs(val / bus.P_ref) + return val * bus.char.evaluate(expr) + + def bus_deriv(self, bus): + r""" + Calculate the matrix of partial derivatives of the bus function. + + Parameters + ---------- + bus : tespy.connections.bus + TESPy bus object. + + Returns + ------- + mat_deriv : ndarray + Matrix of partial derivatives. + """ + deriv = np.zeros((1, 6, self.num_nw_vars)) + deriv[0, 2, 0] = self.numeric_deriv(self.bus_func, 'm', 2, bus=bus) + deriv[0, 2, 2] = self.numeric_deriv(self.bus_func, 'h', 2, bus=bus) + deriv[0, 5, 2] = self.numeric_deriv(self.bus_func, 'h', 5, bus=bus) + return deriv + + def initialise_source(self, c, key): + r""" + Return a starting value for pressure and enthalpy at outlet. + + Parameters + ---------- + c : tespy.connections.connection + Connection to perform initialisation on. + + key : str + Fluid property to retrieve. + + Returns + ------- + val : float + Starting value for pressure/enthalpy in SI units. + + .. math:: + + val = \begin{cases} + 10 \cdot 10^5 & \text{key = 'p'}\\ + h\left(p, 473.15 \text{K} \right) & + \text{key = 'h' at outlet 1}\\ + h\left(p, 473.15 \text{K} \right) & + \text{key = 'h' at outlet 2}\\ + h\left(p, 523.15 \text{K} \right) & + \text{key = 'h' at outlet 3} + \end{cases} + """ + if key == 'p': + return 10e5 + elif key == 'h': + flow = c.to_flow() + if c.s_id == 'out1': + T = 200 + 273.15 + return h_mix_pT(flow, T) + elif c.s_id == 'out2': + T = 200 + 273.15 + return h_mix_pT(flow, T) + else: + T = 250 + 273.15 + return h_mix_pT(flow, T) + + def initialise_target(self, c, key): + r""" + Return a starting value for pressure and enthalpy at inlet. + + Parameters + ---------- + c : tespy.connections.connection + Connection to perform initialisation on. + + key : str + Fluid property to retrieve. + + Returns + ------- + val : float + Starting value for pressure/enthalpy in SI units. + + .. math:: + + val = \begin{cases} + 10 \cdot 10^5 & \text{key = 'p'}\\ + h\left(p, 573.15 \text{K} \right) & + \text{key = 'h' at inlet 1}\\ + h\left(p, 573.15 \text{K} \right) & + \text{key = 'h' at inlet 2}\\ + h\left(p, 493.15 \text{K} \right) & + \text{key = 'h' at inlet 3} + \end{cases} + """ + if key == 'p': + return 10e5 + elif key == 'h': + flow = c.to_flow() + if c.t_id == 'in1': + T = 300 + 273.15 + return h_mix_pT(flow, T) + elif c.t_id == 'in2': + T = 300 + 273.15 + return h_mix_pT(flow, T) + else: + T = 220 + 273.15 + return h_mix_pT(flow, T) + + def calc_parameters(self): + r"""Postprocessing parameter calculation.""" + # connection information + i1 = self.inl[0].to_flow() + i2 = self.inl[1].to_flow() + i3 = self.inl[2].to_flow() + o1 = self.outl[0].to_flow() + o2 = self.outl[1].to_flow() + o3 = self.outl[2].to_flow() + + # specific volume + v_i1 = v_mix_ph(i1, T0=self.inl[0].T.val_SI) + v_i2 = v_mix_ph(i2, T0=self.inl[1].T.val_SI) + v_i3 = v_mix_ph(i3, T0=self.inl[2].T.val_SI) + v_o1 = v_mix_ph(o1, T0=self.outl[0].T.val_SI) + v_o2 = v_mix_ph(o2, T0=self.outl[1].T.val_SI) + v_o3 = v_mix_ph(o3, T0=self.outl[2].T.val_SI) + + # specific entropy + s_i1 = s_mix_ph(i1, T0=self.inl[0].T.val_SI) + s_i2 = s_mix_ph(i2, T0=self.inl[1].T.val_SI) + s_i3 = s_mix_ph(i3, T0=self.inl[2].T.val_SI) + s_o1 = s_mix_ph(o1, T0=self.outl[0].T.val_SI) + s_o2 = s_mix_ph(o2, T0=self.outl[1].T.val_SI) + s_o3 = s_mix_ph(o3, T0=self.outl[2].T.val_SI) + + # component parameters + self.Q.val = -i3[0] * (o3[2] - i3[2]) + + self.pr1.val = o1[1] / i1[1] + self.pr2.val = o2[1] / i2[1] + self.pr3.val = o3[1] / i3[1] + self.zeta1.val = ((i1[1] - o1[1]) * np.pi ** 2 / + (8 * i1[0] ** 2 * (v_i1 + v_o1) / 2)) + self.zeta2.val = ((i2[1] - o2[1]) * np.pi ** 2 / + (8 * i2[0] ** 2 * (v_i2 + v_o2) / 2)) + self.zeta3.val = ((i3[1] - o3[1]) * np.pi ** 2 / + (8 * i3[0] ** 2 * (v_i3 + v_o3) / 2)) + + self.SQ1.val = self.inl[0].m.val_SI * (s_o1 - s_i1) + self.SQ2.val = self.inl[1].m.val_SI * (s_o2 - s_i2) + self.SQ3.val = self.inl[2].m.val_SI * (s_o3 - s_i3) + self.Sirr.val = self.SQ1.val + self.SQ2.val + self.SQ3.val + + self.check_parameter_bounds() diff --git a/tespy/components/heat_exchangers.py b/tespy/components/heat_exchangers.py index 6e795badc..6cfdd7f50 100644 --- a/tespy/components/heat_exchangers.py +++ b/tespy/components/heat_exchangers.py @@ -106,48 +106,43 @@ class heat_exchanger_simple(component): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr : str/float/tespy.helpers.dc_cp + pr : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1`. - zeta : str/float/tespy.helpers.dc_cp + zeta : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - D : str/float/tespy.helpers.dc_cp + D : str/float/tespy.tools.data_containers.dc_cp Diameter of the pipes, :math:`D/\text{m}`. - L : str/float/tespy.helpers.dc_cp + L : str/float/tespy.tools.data_containers.dc_cp Length of the pipes, :math:`L/\text{m}`. - ks : str/float/tespy.helpers.dc_cp + ks : str/float/tespy.tools.data_containers.dc_cp Pipes roughness, :math:`ks/\text{m}` for darcy friction, :math:`ks/\text{1}` for hazen-williams equation. - hydro_group : str/tespy.helpers.dc_gcp + hydro_group : str/tespy.tools.data_containers.dc_gcp Parametergroup for pressure drop calculation based on pipes dimensions. Choose 'HW' for hazen-williams equation, else darcy friction factor is used. - kA : str/float/tespy.helpers.dc_cp + kA : str/float/tespy.tools.data_containers.dc_cp Area independent heat transition coefficient, :math:`kA/\frac{\text{W}}{\text{K}}`. - kA_char : tespy.helpers.dc_cc - Characteristic curve for heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for heat transfer coefficient. - Tamb : float/tespy.helpers.dc_cp + Tamb : str/float/tespy.tools.data_containers.dc_cp Ambient temperature, provide parameter in network's temperature unit. - Tamb_ref : float/tespy.helpers.dc_cp - Ambient temperature for reference in offdesign case, provide - parameter in network's temperature unit. - - kA_group : tespy.helpers.dc_gcp + kA_group : tespy.tools.data_containers.dc_gcp Parametergroup for heat transfer calculation from ambient temperature and area independent heat transfer coefficient kA. @@ -348,7 +343,7 @@ def equations(self): # equations for specified zeta if self.zeta.is_set: if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: - self.vec_res[k] = self.zeta_func() + self.vec_res[k] = self.zeta_func(zeta='zeta') k += 1 ###################################################################### @@ -381,7 +376,8 @@ def additional_equations(self, k): ###################################################################### # equation for specified kA-group paremeters if self.kA_group.is_set: - self.vec_res[k] = self.kA_func() + if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: + self.vec_res[k] = self.kA_func() k += 1 def derivatives(self, vec_z): @@ -425,19 +421,24 @@ def derivatives(self, vec_z): if self.zeta.is_set: f = self.zeta_func if not vec_z[0, 0]: - self.mat_deriv[k, 0, 0] = self.numeric_deriv(f, 'm', 0) + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta') if not vec_z[0, 2]: - self.mat_deriv[k, 0, 1] = self.numeric_deriv(f, 'p', 0) + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta') if not vec_z[0, 2]: - self.mat_deriv[k, 0, 2] = self.numeric_deriv(f, 'h', 0) + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta') if not vec_z[1, 1]: - self.mat_deriv[k, 1, 1] = self.numeric_deriv(f, 'p', 1) + self.mat_deriv[k, 1, 1] = self.numeric_deriv( + f, 'p', 1, zeta='zeta') if not vec_z[1, 2]: - self.mat_deriv[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + self.mat_deriv[k, 1, 2] = self.numeric_deriv( + f, 'h', 1, zeta='zeta') # custom variable zeta if self.zeta.is_var: self.mat_deriv[k, 2 + self.zeta.var_pos, 0] = ( - self.numeric_deriv(f, 'zeta', 2)) + self.numeric_deriv(f, 'zeta', 2, zeta='zeta')) k += 1 ###################################################################### @@ -805,10 +806,11 @@ class solar_collector(heat_exchanger_simple): **optional equations** - - :func:`tespy.components.heat_exchangers.heat_exchanger_simple.Q_func` - .. math:: + 0 = \dot{m}_{in} \cdot \left(h_{out} - h_{in} \right) - + \dot{Q} + 0 = p_{in} \cdot pr - p_{out} - :func:`tespy.components.components.component.zeta_func` @@ -858,54 +860,54 @@ class solar_collector(heat_exchanger_simple): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr : str/float/tespy.helpers.dc_cp + pr : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1`. - zeta : str/float/tespy.helpers.dc_cp + zeta : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - D : str/float/tespy.helpers.dc_cp + D : str/float/tespy.tools.data_containers.dc_cp Diameter of the pipes, :math:`D/\text{m}`. - L : str/float/tespy.helpers.dc_cp + L : str/float/tespy.tools.data_containers.dc_cp Length of the pipes, :math:`L/\text{m}`. - ks : str/float/tespy.helpers.dc_cp + ks : str/float/tespy.tools.data_containers.dc_cp Pipes roughness, :math:`ks/\text{m}` for darcy friction, :math:`ks/\text{1}` for hazen-williams equation. - hydro_group : str/tespy.helpers.dc_gcp + hydro_group : str/tespy.tools.data_containers.dc_gcp Parametergroup for pressure drop calculation based on pipes dimensions. Choose 'HW' for hazen-williams equation, else darcy friction factor is used. - E : str/float/tespy.helpers.dc_cp + E : str/float/tespy.tools.data_containers.dc_cp Radiation at tilted collector surface area, :math:`E/\frac{\text{W}}{\text{m}^2}`. - eta_opt : str/float/tespy.helpers.dc_cp + eta_opt : str/float/tespy.tools.data_containers.dc_cp optical loss at surface cover, :math:`\eta_{opt}`. - lkf_lin : str/float/tespy.helpers.dc_cp + lkf_lin : str/float/tespy.tools.data_containers.dc_cp Linear loss key figure, :math:`\alpha_1/\frac{\text{W}}{\text{K} \cdot \text{m}^2}`. - lkf_quad : str/float/tespy.helpers.dc_cp + lkf_quad : str/float/tespy.tools.data_containers.dc_cp Quadratic loss key figure, :math:`\alpha_2/\frac{\text{W}}{\text{K}^2 \cdot \text{m}^2}`. - A : str/float/tespy.helpers.dc_cp + A : str/float/tespy.tools.data_containers.dc_cp Collector surface area :math:`A/\text{m}^2`. - Tamb : float/tespy.helpers.dc_cp + Tamb : float/tespy.tools.data_containers.dc_cp Ambient temperature, provide parameter in network's temperature unit. - energy_group : tespy.helpers.dc_gcp + energy_group : tespy.tools.data_containers.dc_gcp Parametergroup for energy balance of solarthermal collector. Example @@ -1094,16 +1096,20 @@ def energy_func(self): res : float Residual value of equation. - .. math:: + Note + ---- + .. math:: + + T_m = \frac{T_{out} + T_{in}}{2}\\ - T_m = \frac{T_{out} + T_{in}}{2}\\ + \begin{split} + 0 = & \dot{m} \cdot \left( h_{out} - h_{in} \right)\\ + & - A \cdot \left[E \cdot \eta_{opt} - \alpha_1 \cdot + \left(T_m - T_{amb} \right) - \alpha_2 \cdot + \left(T_m - T_{amb}\right)^2 \right] + \end{split} - \begin{split} - 0 = & \dot{m} \cdot \left( h_{out} - h_{in} \right)\\ - & - A \cdot \left[E \cdot \eta_{opt} - \alpha_1 \cdot - \left(T_m - T_{amb} \right) - \alpha_2 \cdot - \left(T_m - T_{amb}\right)^2 \right] - \end{split} + Reference: :cite:`Quaschning2013`. """ i = self.inl[0].to_flow() o = self.outl[0].to_flow() @@ -1166,8 +1172,8 @@ class heat_exchanger(component): 0 = p_{1,in} \cdot pr1 - p_{1,out}\\ 0 = p_{2,in} \cdot pr2 - p_{2,out} - - :func:`tespy.components.components.component.zeta_func` - - :func:`tespy.components.components.component.zeta2_func` + - hot side :func:`tespy.components.components.component.zeta_func` + - cold side :func:`tespy.components.components.component.zeta_func` **additional equations** @@ -1211,34 +1217,32 @@ class heat_exchanger(component): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr1 : str/float/tespy.helpers.dc_cp + pr1 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at hot side, :math:`pr/1`. - pr2 : str/float/tespy.helpers.dc_cp + pr2 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at cold side, :math:`pr/1`. - zeta1 : str/float/tespy.helpers.dc_cp + zeta1 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at hot side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - zeta2 : str/float/tespy.helpers.dc_cp + zeta2 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at cold side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - kA : str/float/tespy.helpers.dc_cp + kA : str/float/tespy.tools.data_containers.dc_cp Area independent heat transition coefficient, :math:`kA/\frac{\text{W}}{\text{K}}`. - kA_char1 : tespy.helpers.dc_cc - Characteristic curve for hot side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char1 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for hot side heat transfer coefficient. - kA_char2 : tespy.helpers.dc_cc - Characteristic curve for cold side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char2 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for cold side heat transfer coefficient. Note ---- @@ -1379,7 +1383,8 @@ def equations(self): ###################################################################### # equations for specified heat transfer coefficient if self.kA.is_set: - self.vec_res[k] = self.kA_func() + if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: + self.vec_res[k] = self.kA_func() k += 1 ###################################################################### @@ -1411,13 +1416,17 @@ def equations(self): ###################################################################### # equations for specified zeta at hot side if self.zeta1.is_set: - self.vec_res[k] = self.zeta_func() + if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: + self.vec_res[k] = self.zeta_func( + zeta='zeta1', inconn=0, outconn=0) k += 1 ###################################################################### # equations for specified zeta at cold side if self.zeta2.is_set: - self.vec_res[k] = self.zeta2_func() + if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: + self.vec_res[k] = self.zeta_func( + zeta='zeta2', inconn=1, outconn=1) k += 1 ###################################################################### @@ -1517,31 +1526,41 @@ def derivatives(self, vec_z): if self.zeta1.is_set: f = self.zeta_func if not vec_z[0, 0]: - self.mat_deriv[k, 0, 0] = self.numeric_deriv(f, 'm', 0) + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[0, 1]: - self.mat_deriv[k, 0, 1] = self.numeric_deriv(f, 'p', 0) + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[0, 2]: - self.mat_deriv[k, 0, 2] = self.numeric_deriv(f, 'h', 0) + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta1', inconn=0, outconn=0) if not vec_z[2, 1]: - self.mat_deriv[k, 2, 1] = self.numeric_deriv(f, 'p', 2) + self.mat_deriv[k, 2, 1] = self.numeric_deriv( + f, 'p', 2, zeta='zeta1', inconn=0, outconn=0) if not vec_z[2, 2]: - self.mat_deriv[k, 2, 2] = self.numeric_deriv(f, 'h', 2) + self.mat_deriv[k, 2, 2] = self.numeric_deriv( + f, 'h', 2, zeta='zeta1', inconn=0, outconn=0) k += 1 ###################################################################### # derivatives for specified zeta at cold side if self.zeta2.is_set: - f = self.zeta2_func + f = self.zeta_func if not vec_z[1, 0]: - self.mat_deriv[k, 1, 0] = self.numeric_deriv(f, 'm', 1) + self.mat_deriv[k, 1, 0] = self.numeric_deriv( + f, 'm', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[1, 1]: - self.mat_deriv[k, 1, 1] = self.numeric_deriv(f, 'p', 1) + self.mat_deriv[k, 1, 1] = self.numeric_deriv( + f, 'p', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[1, 2]: - self.mat_deriv[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + self.mat_deriv[k, 1, 2] = self.numeric_deriv( + f, 'h', 1, zeta='zeta2', inconn=1, outconn=1) if not vec_z[3, 1]: - self.mat_deriv[k, 3, 1] = self.numeric_deriv(f, 'p', 3) + self.mat_deriv[k, 3, 1] = self.numeric_deriv( + f, 'p', 3, zeta='zeta2', inconn=1, outconn=1) if not vec_z[3, 2]: - self.mat_deriv[k, 3, 2] = self.numeric_deriv(f, 'h', 3) + self.mat_deriv[k, 3, 2] = self.numeric_deriv( + f, 'h', 3, zeta='zeta2', inconn=1, outconn=1) k += 1 ###################################################################### @@ -1552,27 +1571,6 @@ def additional_derivatives(self, vec_z, k): r"""Calculate partial derivatives for given additional equations.""" return - def fluid_func(self): - r""" - Calculate residual values for fluid balance equations. - - Returns - ------- - vec_res : list - Vector of residual values for component's fluid balance. - - .. math:: - - 0 = fluid_{i,in_{j}} - fluid_{i,out_{j}} \; - \forall i \in \mathrm{fluid}, \; \forall j \in inlets/outlets - """ - vec_res = [] - - for i in range(self.num_i): - for fluid, x in self.inl[i].fluid.val.items(): - vec_res += [x - self.outl[i].fluid.val[fluid]] - return vec_res - def mass_flow_func(self): r""" Calculate the residual value for mass flow balance equation. @@ -1592,32 +1590,6 @@ def mass_flow_func(self): vec_res += [self.inl[i].m.val_SI - self.outl[i].m.val_SI] return vec_res - def fluid_deriv(self): - r""" - Calculate partial derivatives for all fluid balance equations. - - Returns - ------- - deriv : list - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros((self.num_nw_fluids * 2, - 4 + self.num_vars, - self.num_nw_vars)) - # hot side - i = 0 - for fluid in self.nw_fluids: - deriv[i, 0, i + 3] = 1 - deriv[i, 2, i + 3] = -1 - i += 1 - # cold side - j = 0 - for fluid in self.nw_fluids: - deriv[i + j, 1, j + 3] = 1 - deriv[i + j, 3, j + 3] = -1 - j += 1 - return deriv.tolist() - def mass_flow_deriv(self): r""" Calculate partial derivatives for all mass flow balance equations. @@ -1633,7 +1605,7 @@ def mass_flow_deriv(self): deriv[i, i, 0] = 1 for j in range(self.num_o): deriv[j, j + i + 1, 0] = -1 - return deriv.tolist() + return deriv def energy_func(self): r""" @@ -1891,7 +1863,7 @@ def initialise_source(self, c, key): if key == 'p': return 50e5 elif key == 'h': - flow = [c.m.val0, c.p.val_SI, c.h.val_SI, c.fluid.val] + flow = c.to_flow() if c.s_id == 'out1': T = 200 + 273.15 return h_mix_pT(flow, T) @@ -1927,7 +1899,7 @@ def initialise_target(self, c, key): if key == 'p': return 50e5 elif key == 'h': - flow = [c.m.val0, c.p.val_SI, c.h.val_SI, c.fluid.val] + flow = c.to_flow() if c.t_id == 'in1': T = 300 + 273.15 return h_mix_pT(flow, T) @@ -2040,8 +2012,8 @@ class condenser(heat_exchanger): 0 = p_{1,in} \cdot pr1 - p_{1,out}\\ 0 = p_{2,in} \cdot pr2 - p_{2,out} - - :func:`tespy.components.components.component.zeta_func` - - :func:`tespy.components.components.component.zeta2_func` + - hot side :func:`tespy.components.components.component.zeta_func` + - cold side :func:`tespy.components.components.component.zeta_func` **additional equations** @@ -2085,34 +2057,32 @@ class condenser(heat_exchanger): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr1 : str/float/tespy.helpers.dc_cp + pr1 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at hot side, :math:`pr/1`. - pr2 : str/float/tespy.helpers.dc_cp + pr2 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at cold side, :math:`pr/1`. - zeta1 : str/float/tespy.helpers.dc_cp + zeta1 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at hot side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - zeta2 : str/float/tespy.helpers.dc_cp + zeta2 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at cold side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - kA : str/float/tespy.helpers.dc_cp + kA : str/float/tespy.tools.data_containers.dc_cp Area independent heat transition coefficient, :math:`kA/\frac{\text{W}}{\text{K}}`. - kA_char1 : tespy.helpers.dc_cc - Characteristic curve for hot side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char1 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for hot side heat transfer coefficient. - kA_char2 : tespy.helpers.dc_cc - Characteristic curve for cold side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char2 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for cold side heat transfer coefficient. subcooling : bool Enable/disable subcooling, default value: disabled. @@ -2175,9 +2145,9 @@ class condenser(heat_exchanger): >>> amb_he.set_attr(T=30) >>> nw.solve('offdesign', design_path='tmp') >>> round(ws_he.T.val - he_amb.T.val, 1) - 62.6 + 62.5 >>> round(T_bp_p(ws_he.to_flow()) - 273.15 - he_amb.T.val, 1) - 12.3 + 11.3 It is possible to activate subcooling. The difference to boiling point temperature is specified to 5 K. @@ -2186,9 +2156,9 @@ class condenser(heat_exchanger): >>> he_c.set_attr(Td_bp=-5) >>> nw.solve('offdesign', design_path='tmp') >>> round(ws_he.T.val - he_amb.T.val, 1) - 62.6 + 62.5 >>> round(T_bp_p(ws_he.to_flow()) - 273.15 - he_amb.T.val, 1) - 14.4 + 13.4 >>> shutil.rmtree('./tmp', ignore_errors=True) """ @@ -2357,7 +2327,7 @@ def kA_func(self): td_log = ((T_o1 - T_i2 - T_i1 + T_o2) / np.log((T_o1 - T_i2) / (T_i1 - T_o2))) - return i1[0] * (o1[2] - i1[2]) + self.kA.val * fkA1 * fkA2 * td_log + return i1[0] * (o1[2] - i1[2]) + self.kA.val * fkA * td_log def ttd_u_func(self): r""" @@ -2411,8 +2381,8 @@ class desuperheater(heat_exchanger): 0 = p_{1,in} \cdot pr1 - p_{1,out}\\ 0 = p_{2,in} \cdot pr2 - p_{2,out} - - :func:`tespy.components.components.component.zeta_func` - - :func:`tespy.components.components.component.zeta2_func` + - hot side :func:`tespy.components.components.component.zeta_func` + - cold side :func:`tespy.components.components.component.zeta_func` **additional equations** @@ -2456,34 +2426,32 @@ class desuperheater(heat_exchanger): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr1 : str/float/tespy.helpers.dc_cp + pr1 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at hot side, :math:`pr/1`. - pr2 : str/float/tespy.helpers.dc_cp + pr2 : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio at cold side, :math:`pr/1`. - zeta1 : str/float/tespy.helpers.dc_cp + zeta1 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at hot side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - zeta2 : str/float/tespy.helpers.dc_cp + zeta2 : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient at cold side, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - kA : str/float/tespy.helpers.dc_cp + kA : str/float/tespy.tools.data_containers.dc_cp Area independent heat transition coefficient, :math:`kA/\frac{\text{W}}{\text{K}}`. - kA_char1 : tespy.helpers.dc_cc - Characteristic curve for hot side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char1 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for hot side heat transfer coefficient. - kA_char2 : tespy.helpers.dc_cc - Characteristic curve for cold side heat transfer coefficient, provide - char_line as function :code:`func`. Standard parameter 'm'. + kA_char2 : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for cold side heat transfer coefficient. Note ---- diff --git a/tespy/components/nodes.py b/tespy/components/nodes.py index a1db0c50c..2e25aa60e 100644 --- a/tespy/components/nodes.py +++ b/tespy/components/nodes.py @@ -90,10 +90,10 @@ class node(component): printout: boolean Include this component in the network's results printout. - num_in : float/tespy.helpers.dc_simple + num_in : float/tespy.tools.data_containers.dc_simple Number of inlets for this component, default value: 2. - num_out : float/tespy.helpers.dc_simple + num_out : float/tespy.tools.data_containers.dc_simple Number of outlets for this component, default value: 2. Note @@ -900,7 +900,7 @@ class merge(node): printout: boolean Include this component in the network's results printout. - num_in : float/tespy.helpers.dc_simple + num_in : float/tespy.tools.data_containers.dc_simple Number of inlets for this component, default value: 2. Example @@ -1095,10 +1095,10 @@ class separator(node): :alt: alternative text :align: center - Todo + Note ---- - - fluid separation requires power and cooling, equations have not been - implemented! + Fluid separation requires power and cooling, equations have not been + implemented, yet! Parameters ---------- @@ -1126,7 +1126,7 @@ class separator(node): printout: boolean Include this component in the network's results printout. - num_out : float/tespy.helpers.dc_simple + num_out : float/tespy.tools.data_containers.dc_simple Number of outlets for this component, default value: 2. Example @@ -1359,7 +1359,7 @@ class splitter(node): printout: boolean Include this component in the network's results printout. - num_out : float/tespy.helpers.dc_simple + num_out : float/tespy.tools.data_containers.dc_simple Number of outlets for this component, default value: 2. Example diff --git a/tespy/components/piping.py b/tespy/components/piping.py index 6433021ba..9d00c0009 100644 --- a/tespy/components/piping.py +++ b/tespy/components/piping.py @@ -94,23 +94,23 @@ class pipe(heat_exchanger_simple): printout: boolean Include this component in the network's results printout. - Q : str/float/tespy.helpers.dc_cp + Q : str/float/tespy.tools.data_containers.dc_cp Heat transfer, :math:`Q/\text{W}`. - pr : str/float/tespy.helpers.dc_cp + pr : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1`. - zeta : str/float/tespy.helpers.dc_cp + zeta : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - D : str/float/tespy.helpers.dc_cp + D : str/float/tespy.tools.data_containers.dc_cp Diameter of the pipes, :math:`D/\text{m}`. - L : str/float/tespy.helpers.dc_cp + L : str/float/tespy.tools.data_containers.dc_cp Length of the pipes, :math:`L/\text{m}`. - ks : str/float/tespy.helpers.dc_cp + ks : str/float/tespy.tools.data_containers.dc_cp Pipes roughness, :math:`ks/\text{m}` for darcy friction, :math:`ks/\text{1}` for hazen-williams equation. @@ -119,23 +119,17 @@ class pipe(heat_exchanger_simple): Choose 'HW' for hazen-williams equation, else darcy friction factor is used. - kA : str/float/tespy.helpers.dc_cp + kA : str/float/tespy.tools.data_containers.dc_cp Area independent heat transition coefficient, :math:`kA/\frac{\text{W}}{\text{K}}`. - kA_char : str/tespy.helpers.dc_cc - Characteristic curve for heat transfer coefficient, provide x and y - values or use generic values (e. g. calculated from design case). - Standard parameter 'm'. + kA_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc + Characteristic line for heat transfer coefficient. - Tamb : float/tespy.helpers.dc_cp + Tamb : float/tespy.tools.data_containers.dc_cp Ambient temperature, provide parameter in network's temperature unit. - Tamb_ref : float/tespy.helpers.dc_cp - Ambient temperature for reference in offdesign case, provide - parameter in network's temperature unit. - kA_group : tespy.helpers.dc_gcp Parametergroup for heat transfer calculation from ambient temperature and area independent heat transfer coefficient kA. @@ -250,14 +244,14 @@ class valve(component): printout: boolean Include this component in the network's results printout. - pr : str/float/tespy.helpers.dc_cp + pr : str/float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1` - zeta : str/float/tespy.helpers.dc_cp + zeta : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - dp_char : str/tespy.helpers.dc_cc + dp_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Characteristic line for difference pressure to mass flow. Example @@ -384,7 +378,7 @@ def equations(self): # eqation specified zeta if self.zeta.is_set: if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: - self.vec_res[k] = self.zeta_func() + self.vec_res[k] = self.zeta_func(zeta='zeta') k += 1 ###################################################################### @@ -422,18 +416,23 @@ def derivatives(self, vec_z): if self.zeta.is_set: f = self.zeta_func if not vec_z[0, 0]: - self.mat_deriv[k, 0, 0] = self.numeric_deriv(f, 'm', 0) + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta') if not vec_z[0, 1]: - self.mat_deriv[k, 0, 1] = self.numeric_deriv(f, 'p', 0) + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta') if not vec_z[0, 2]: - self.mat_deriv[k, 0, 2] = self.numeric_deriv(f, 'h', 0) + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta') if not vec_z[1, 1]: - self.mat_deriv[k, 1, 1] = self.numeric_deriv(f, 'p', 1) + self.mat_deriv[k, 1, 1] = self.numeric_deriv( + f, 'p', 1, zeta='zeta') if not vec_z[1, 2]: - self.mat_deriv[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + self.mat_deriv[k, 1, 2] = self.numeric_deriv( + f, 'h', 1, zeta='zeta') if self.zeta.is_var: self.mat_deriv[k, 2 + self.zeta.var_pos, 0] = ( - self.numeric_deriv(f, 'zeta', 2)) + self.numeric_deriv(f, 'zeta', 2, zeta='zeta')) k += 1 ###################################################################### diff --git a/tespy/components/reactors.py b/tespy/components/reactors.py index 53f5f8323..adbaa11ba 100644 --- a/tespy/components/reactors.py +++ b/tespy/components/reactors.py @@ -119,26 +119,26 @@ class water_electrolyzer(component): offdesign : list List containing offdesign parameters (stated as String). - P : float/tespy.helpers.dc_cp + P : str/float/tespy.tools.data_containers.dc_cp Power input, :math:`P/\text{W}`. - Q : float/tespy.helpers.dc_cp + Q : float/tespy.tools.data_containers.dc_cp Heat output of cooling, :math:`Q/\text{W}` - e : float/tespy.helpers.dc_cp + e : str/float/tespy.tools.data_containers.dc_cp Electrolysis specific energy consumption, :math:`e/(\text{J}/\text{m}^3)`. - eta : float/tespy.helpers.dc_cp + eta : str/float/tespy.tools.data_containers.dc_cp Electrolysis efficiency, :math:`\eta/1`. - eta_char : str/tespy.helpers.dc_cc + eta_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Electrolysis efficiency characteristic line. - pr : float/tespy.helpers.dc_cp + pr : float/tespy.tools.data_containers.dc_cp Cooling loop pressure ratio, :math:`pr/1`. - zeta : float/tespy.helpers.dc_cp + zeta : str/float/tespy.tools.data_containers.dc_cp Geometry independent friction coefficient for cooling loop pressure drop, :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. @@ -179,7 +179,7 @@ class water_electrolyzer(component): pressure is 25 bars. The electrolysis efficiency is at 80 % and the compressor isentropic efficiency at 85 %. After designing the plant the offdesign electrolysis efficiency is predicted by the characteristic line. - TODO: LINKTODEFAULTCHAR? + The default characteristic line can be found here: :py:mod:`tespy.data`. >>> fw_el = connection(fw, 'out1', el, 'in2') >>> el_o = connection(el, 'out2', oxy, 'in1') @@ -419,7 +419,7 @@ def equations(self): # specified zeta value if self.zeta.is_set: if np.absolute(self.vec_res[k]) > err ** 2 or self.it % 4 == 0: - self.vec_res[k] = self.zeta_func() + self.vec_res[k] = self.zeta_func(zeta='zeta') k += 1 # equation for heat transfer @@ -532,20 +532,26 @@ def derivatives(self, vec_z): if self.zeta.is_set: f = self.zeta_func if not vec_z[0, 0]: - self.mat_deriv[k, 0, 0] = self.numeric_deriv(f, 'm', 0) + self.mat_deriv[k, 0, 0] = self.numeric_deriv( + f, 'm', 0, zeta='zeta') if not vec_z[0, 1]: - self.mat_deriv[k, 0, 1] = self.numeric_deriv(f, 'p', 0) + self.mat_deriv[k, 0, 1] = self.numeric_deriv( + f, 'p', 0, zeta='zeta') if not vec_z[0, 2]: - self.mat_deriv[k, 0, 2] = self.numeric_deriv(f, 'h', 0) + self.mat_deriv[k, 0, 2] = self.numeric_deriv( + f, 'h', 0, zeta='zeta') if not vec_z[2, 1]: - self.mat_deriv[k, 2, 1] = self.numeric_deriv(f, 'p', 2) + self.mat_deriv[k, 2, 1] = self.numeric_deriv( + f, 'p', 2, zeta='zeta') if not vec_z[2, 2]: - self.mat_deriv[k, 2, 2] = self.numeric_deriv(f, 'h', 2) + self.mat_deriv[k, 2, 2] = self.numeric_deriv( + f, 'h', 2, zeta='zeta') # derivatives for variable zeta if self.zeta.is_var: self.mat_deriv[k, 5 + self.zeta.var_pos, 0] = ( - self.numeric_deriv(f, 'zeta', 5)) + self.numeric_deriv( + f, 'zeta', 5, zeta='zeta')) k += 1 ###################################################################### @@ -898,7 +904,7 @@ def initialise_source(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = [c.m.val0, 5e5, c.h.val_SI, c.fluid.val] + flow = c.to_flow() T = 50 + 273.15 return h_mix_pT(flow, T) @@ -929,7 +935,7 @@ def initialise_target(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = [c.m.val0, 5e5, c.h.val_SI, c.fluid.val] + flow = c.to_flow() T = 20 + 273.15 return h_mix_pT(flow, T) diff --git a/tespy/components/turbomachinery.py b/tespy/components/turbomachinery.py index 841fffb47..35fe402e7 100644 --- a/tespy/components/turbomachinery.py +++ b/tespy/components/turbomachinery.py @@ -95,10 +95,10 @@ class turbomachine(component): printout: boolean Include this component in the network's results printout. - P : str/float/tespy.helpers.dc_cp + P : float/tespy.tools.data_containers.dc_cp Power, :math:`P/\text{W}` - pr : str/float/tespy.helpers.dc_cp + pr : float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1` Example @@ -385,26 +385,26 @@ class compressor(turbomachine): printout: boolean Include this component in the network's results printout. - P : str/float/tespy.helpers.dc_cp + P : float/tespy.tools.data_containers.dc_cp Power, :math:`P/\text{W}` - eta_s : str/float/tespy.helpers.dc_cp + eta_s : float/tespy.tools.data_containers.dc_cp Isentropic efficiency, :math:`\eta_s/1` - pr : str/float/tespy.helpers.dc_cp + pr : float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1` - eta_s_char : tespy.helpers.dc_cc + eta_s_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Characteristic curve for isentropic efficiency, provide char_line as function :code:`func`. - char_map : tespy.helpers.dc_cm + char_map : tespy.tools.characteristics.compressor_map/tespy.tools.data_containers.dc_cm Characteristic map for pressure rise and isentropic efficiency vs. nondimensional mass flow, see tespy.tools.characteristics.compressor_map for further information. Provide a compressor_map as function :code:`func`. - igva : str/float/tespy.helpers.dc_cp + igva : str/float/tespy.tools.data_containers.dc_cp Inlet guide vane angle, :math:`igva/^\circ`. Example @@ -725,18 +725,16 @@ def convergence_check(self, nw): """ i, o = self.inl, self.outl - if o[0].init_csv is False: - if not o[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: - o[0].p.val_SI = o[0].p.val_SI * 2 + if not o[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: + o[0].p.val_SI = o[0].p.val_SI * 1.1 - if not o[0].h.val_set and o[0].h.val_SI < i[0].h.val_SI: - o[0].h.val_SI = o[0].h.val_SI * 1.1 + if not o[0].h.val_set and o[0].h.val_SI < i[0].h.val_SI: + o[0].h.val_SI = o[0].h.val_SI * 1.1 - if i[0].init_csv is False: - if not i[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: - i[0].p.val_SI = o[0].p.val_SI * 0.5 - if not i[0].h.val_set and o[0].h.val_SI < i[0].h.val_SI: - i[0].h.val_SI = o[0].h.val_SI * 0.9 + if not i[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: + i[0].p.val_SI = o[0].p.val_SI * 0.9 + if not i[0].h.val_set and o[0].h.val_SI < i[0].h.val_SI: + i[0].h.val_SI = o[0].h.val_SI * 0.9 @staticmethod def initialise_source(c, key): @@ -897,20 +895,20 @@ class pump(turbomachine): printout: boolean Include this component in the network's results printout. - P : str/float/tespy.helpers.dc_cp + P : float/tespy.tools.data_containers.dc_cp Power, :math:`P/\text{W}` - eta_s : str/float/tespy.helpers.dc_cp + eta_s : float/tespy.tools.data_containers.dc_cp Isentropic efficiency, :math:`\eta_s/1` - pr : str/float/tespy.helpers.dc_cp + pr : float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1` - eta_s_char : tespy.helpers.dc_cc + eta_s_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Characteristic curve for isentropic efficiency, provide char_line as function :code:`func`. - flow_char : tespy.helpers.dc_cc + flow_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Characteristic curve for pressure rise vs. volumetric flow rate, provide char_line as function :code:`func`. :math:`x/\frac{\text{m}^3}{\text{s}} \, y/\text{Pa}`. @@ -943,8 +941,9 @@ class pump(turbomachine): After that we calculate offdesign performance using the pump curve and a characteristic function for the pump efficiency. We can calulate the offdesign efficiency and the volumetric flow, if the - difference pressure changed. - TODO: Link to deufalt characteristic lines? + difference pressure changed. The default characteristc lines are to be + found in the :py:mod:`tespy.data` module. Of course, you are able to + specify your own characteristcs. >>> v = np.array([0, 0.4, 0.8, 1.2, 1.6, 2]) / 1000 >>> dp = np.array([15, 14, 12, 9, 5, 0]) * 1e5 @@ -1337,20 +1336,20 @@ class turbine(turbomachine): printout: boolean Include this component in the network's results printout. - P : str/float/tespy.helpers.dc_cp + P : float/tespy.tools.data_containers.dc_cp Power, :math:`P/\text{W}` - eta_s : str/float/tespy.helpers.dc_cp + eta_s : float/tespy.tools.data_containers.dc_cp Isentropic efficiency, :math:`\eta_s/1` - pr : str/float/tespy.helpers.dc_cp + pr : float/tespy.tools.data_containers.dc_cp Outlet to inlet pressure ratio, :math:`pr/1` - eta_s_char : tespy.helpers.dc_cc + eta_s_char : tespy.tools.characteristics.char_line/tespy.tools.data_containers.dc_cc Characteristic curve for isentropic efficiency, provide char_line as function :code:`func`. - cone : tespy.helpers.dc_simple + cone : tespy.tools.data_containers.dc_simple Apply Stodola's cone law. Example @@ -1622,23 +1621,22 @@ def convergence_check(self, nw): """ i, o = self.inl, self.outl - if i[0].init_csv is False: + if i[0].good_starting_values is False: if i[0].p.val_SI <= 1e5 and not i[0].p.val_set: i[0].p.val_SI = 1e5 if i[0].h.val_SI < 10e5 and not i[0].h.val_set: i[0].h.val_SI = 10e5 - if o[0].init_csv is False: - if i[0].h.val_SI <= o[0].h.val_SI and not o[0].h.val_set: - o[0].h.val_SI = i[0].h.val_SI * 0.75 - - if i[0].p.val_SI <= o[0].p.val_SI and not o[0].p.val_set: - o[0].p.val_SI = i[0].p.val_SI / 2 - if o[0].h.val_SI < 5e5 and not o[0].h.val_set: o[0].h.val_SI = 5e5 + if i[0].h.val_SI <= o[0].h.val_SI and not o[0].h.val_set: + o[0].h.val_SI = i[0].h.val_SI * 0.9 + + if i[0].p.val_SI <= o[0].p.val_SI and not o[0].p.val_set: + o[0].p.val_SI = i[0].p.val_SI * 0.9 + @staticmethod def initialise_source(c, key): r""" diff --git a/tespy/connections.py b/tespy/connections.py index 980ffd825..357c13649 100644 --- a/tespy/connections.py +++ b/tespy/connections.py @@ -678,9 +678,9 @@ class bus: gas cooler. >>> heat_bus.comps.loc[fgc]['char'].x - array([0, 3]) + array([0., 3.]) >>> heat_bus.comps.loc[fgc]['char'].y - array([-1, -1]) + array([-1., -1.]) >>> round(chp.ti.val) 25813247.0 >>> round(chp.Q1.val + chp.Q2.val, 0) diff --git a/tespy/data/char_lines.json b/tespy/data/char_lines.json index 87460dc71..eafedd848 100644 --- a/tespy/data/char_lines.json +++ b/tespy/data/char_lines.json @@ -5,13 +5,15 @@ "x": [0.000, 0.300, 0.600, 0.700, 0.800, 0.900, 1.000, 1.100, 1.200, 1.300, 1.400, 1.500], "y": [0.950, 0.980, 0.993, 0.996, 0.998, 0.9995, 1.000, 0.999, - 0.996, 0.990, 0.980, 0.960] + 0.996, 0.990, 0.980, 0.960], + "ref": false }, "TRAUPEL": { "x": [0.0000, 0.1905, 0.3810, 0.5714, 0.7619, 0.9524, 1.0000, 1.1429, 1.3333, 1.5238, 1.7143, 1.9048], "y": [0.0000, 0.3975, 0.6772, 0.8581, 0.9593, 0.9985, 1.0000, - 0.9875, 0.9357, 0.8464, 0.7219, 0.5643] + 0.9875, 0.9357, 0.8464, 0.7219, 0.5643], + "ref": "Traupel2001" } } }, @@ -23,7 +25,8 @@ 1.13795, 1.16365, 1.18604, 1.2049, 1.22], "y": [0.78, 0.82066, 0.86025, 0.89742, 0.93083, 0.95914, 0.981, 0.99507, 1.0, 0.99733, 0.98913, 0.97496, - 0.95435, 0.92687, 0.89205, 0.84944, 0.79859] + 0.95435, 0.92687, 0.89205, 0.84944, 0.79859], + "ref": false } } }, @@ -33,7 +36,8 @@ "x": [0.071, 0.282, 0.635, 0.776, 0.917, 1.000, 1.128, 1.270, 1.410, 1.763, 2.115, 2.500], "y": [0.250, 0.547, 0.900, 0.965, 0.995, 1.000, 0.990, 0.959, - 0.911, 0.737, 0.519, 0.250] + 0.911, 0.737, 0.519, 0.250], + "ref": false } } }, @@ -41,25 +45,29 @@ "tiP_char": { "DEFAULT": { "x": [0.50, 0.625, 0.75, 0.875, 1.00], - "y": [3.03, 2.875, 2.74, 2.625, 2.53] + "y": [3.03, 2.875, 2.74, 2.625, 2.53], + "ref": false } }, "Q1_char": { "DEFAULT": { "x": [0.50, 0.625, 0.75, 0.875, 1.00], - "y": [0.699, 0.645, 0.593, 0.544, 0.498] + "y": [0.699, 0.645, 0.593, 0.544, 0.498], + "ref": false } }, "Q2_char": { "DEFAULT": { "x": [0.50, 0.625, 0.75, 0.875, 1.00], - "y": [0.524, 0.484, 0.445, 0.409, 0.374] + "y": [0.524, 0.484, 0.445, 0.409, 0.374], + "ref": false } }, "Qloss_char": { "DEFAULT": { "x": [0.50, 0.625, 0.75, 0.875, 1.00], - "y": [0.147, 0.153, 0.166, 0.187, 0.215] + "y": [0.147, 0.153, 0.166, 0.187, 0.215], + "ref": false } } }, @@ -73,7 +81,8 @@ "y": [0.01, 0.09598, 0.18995, 0.28191, 0.37185, 0.45974, 0.54557, 0.62932, 0.71098, 0.79053, 0.86796, 0.94324, 1.0, 1.0873, 1.15605, 1.22259, 1.28691, 1.34898, - 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645] + 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645], + "ref": false }, "CONDENSING FLUID": { "x": [0.0, 0.02476, 0.08114, 0.167, 0.28023, 0.41869, @@ -81,7 +90,8 @@ 1.66053, 1.91619, 2.18009, 2.4501, 2.72412, 3.0], "y": [0.01, 0.19836, 0.37181, 0.52217, 0.65124, 0.76085, 0.8528, 0.92891, 0.99099, 1.0, 1.04084, 1.0803, - 1.11116, 1.13524, 1.15435, 1.1703, 1.18492, 1.2] + 1.11116, 1.13524, 1.15435, 1.1703, 1.18492, 1.2], + "ref": false } }, "kA_char2": { @@ -91,7 +101,8 @@ 2.71261, 3.0012], "y": [0.01, 0.21181, 0.40637, 0.58484, 0.74792, 0.89627, 1.0, 1.03058, 1.15152, 1.25977, 1.35602, 1.44093, - 1.51519, 1.562] + 1.51519, 1.562], + "ref": false }, "EVAPORATING FLUID": { "x": [0.0, 0.00412, 0.0076, 0.01101, 0.01488, 0.0211, @@ -105,7 +116,8 @@ 0.75388, 0.80274, 0.84415, 0.87699, 0.90012, 0.92187, 0.94082, 0.95729, 0.97163, 0.98417, 0.99525, 1.0, 1.00521, 1.01439, 1.02312, 1.03175, 1.0406, 1.05002, - 1.0575] + 1.0575], + "ref": false } } }, @@ -117,7 +129,8 @@ 1.66053, 1.91619, 2.18009, 2.4501, 2.72412, 3.0], "y": [0.01, 0.19836, 0.37181, 0.52217, 0.65124, 0.76085, 0.8528, 0.92891, 0.99099, 1.0, 1.04084, 1.0803, - 1.11116, 1.13524, 1.15435, 1.1703, 1.18492, 1.2] + 1.11116, 1.13524, 1.15435, 1.1703, 1.18492, 1.2], + "ref": false } }, "kA_char2": { @@ -127,8 +140,9 @@ 2.71261, 3.0012], "y": [0.01, 0.21181, 0.40637, 0.58484, 0.74792, 0.89627, 1.0, 1.03058, 1.15152, 1.25977, 1.35602, 1.44093, - 1.51519, 1.562] - } + 1.51519, 1.562], + "ref": false + } } }, "desuperheater": { @@ -141,7 +155,8 @@ "y": [0.01, 0.09598, 0.18995, 0.28191, 0.37185, 0.45974, 0.54557, 0.62932, 0.71098, 0.79053, 0.86796, 0.94324, 1.0, 1.0873, 1.15605, 1.22259, 1.28691, 1.34898, - 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645] + 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645], + "ref": false } }, "kA_char2": { @@ -151,7 +166,8 @@ 2.71261, 3.0012], "y": [0.01, 0.21181, 0.40637, 0.58484, 0.74792, 0.89627, 1.0, 1.03058, 1.15152, 1.25977, 1.35602, 1.44093, - 1.51519, 1.562] + 1.51519, 1.562], + "ref": false } } }, @@ -165,7 +181,8 @@ "y": [0.01, 0.09598, 0.18995, 0.28191, 0.37185, 0.45974, 0.54557, 0.62932, 0.71098, 0.79053, 0.86796, 0.94324, 1.0, 1.0873, 1.15605, 1.22259, 1.28691, 1.34898, - 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645] + 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645], + "ref": false } } }, @@ -179,7 +196,8 @@ "y": [0.01, 0.09598, 0.18995, 0.28191, 0.37185, 0.45974, 0.54557, 0.62932, 0.71098, 0.79053, 0.86796, 0.94324, 1.0, 1.0873, 1.15605, 1.22259, 1.28691, 1.34898, - 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645] + 1.40879, 1.46633, 1.52157, 1.5745, 1.62511, 1.645], + "ref": false } } }, @@ -187,7 +205,8 @@ "eta_char": { "DEFAULT": { "x": [0.000, 0.0833, 0.250, 0.500, 0.750, 1.000, 1.333], - "y": [1.100, 1.075, 1.050, 1.025, 1.010, 1.000, 0.990] + "y": [1.100, 1.075, 1.050, 1.025, 1.010, 1.000, 0.990], + "ref": false } } } diff --git a/tespy/data/char_maps.json b/tespy/data/char_maps.json index a10cbb234..942a15855 100644 --- a/tespy/data/char_maps.json +++ b/tespy/data/char_maps.json @@ -30,7 +30,7 @@ [1.34, 1.276, 1.213, 1.149, 1.085, 1.022, 0.958, 0.894, 0.831, 0.767], [1.441, 1.37, 1.3, 1.229, 1.158, - 1.088, 1.017, 0.946, 0.876, 0.805]], + 1.088, 1.017, 0.946, 0.876, 0.805]], "z2": [[0.872, 0.885, 0.898, 0.911, 0.925, 0.94, 0.945, 0.926, 0.903, 0.879], [0.887, 0.909, 0.93, 0.947, 0.963, @@ -44,7 +44,8 @@ [0.948, 0.959, 0.962, 0.949, 0.935, 0.922, 0.908, 0.895, 0.881, 0.868], [0.879, 0.888, 0.898, 0.907, 0.916, - 0.924, 0.915, 0.906, 0.896, 0.887]] + 0.924, 0.915, 0.906, 0.896, 0.887]], + "ref": "Plis2016" } } } diff --git a/tespy/networks/network_reader.py b/tespy/networks/network_reader.py index 46712ca7f..cb00e7633 100644 --- a/tespy/networks/network_reader.py +++ b/tespy/networks/network_reader.py @@ -349,10 +349,10 @@ def construct_comps(c, *args): Component information from .csv-file. args[0] : pandas.core.frame.DataFrame - DataFrame containing the x and y data of characteristic functions. + DataFrame containing the data of characteristic lines. args[1] : pandas.core.frame.DataFrame - DataFrame containing the x, y, z1 and z2 data of characteristic maps. + DataFrame containing the data of characteristic maps. Returns ------- @@ -388,7 +388,11 @@ def construct_comps(c, *args): try: x = args[0][values].x.values[0] y = args[0][values].y.values[0] - char = char_line(x=x, y=y) + if 'extrapolate' in args[0].columns: + extrapolate = args[0][values].extrapolate.values[0] + else: + extrapolate = False + char = char_line(x=x, y=y, extrapolate=extrapolate) except IndexError: diff --git a/tespy/networks/networks.py b/tespy/networks/networks.py index 8a278e259..49ef7ab04 100644 --- a/tespy/networks/networks.py +++ b/tespy/networks/networks.py @@ -38,6 +38,7 @@ from tespy.components.basics import (sink, source, subsystem_interface, cycle_closer) from tespy.components.combustion import combustion_chamber, combustion_engine +from tespy.components.customs import orc_evaporator from tespy.components.heat_exchangers import heat_exchanger from tespy.components.nodes import drum, merge, splitter from tespy.components.reactors import water_electrolyzer @@ -509,6 +510,8 @@ def add_conns(self, *args): logging.error(msg) raise TypeError(msg) + c.good_starting_values = False + self.conns.loc[c] = [c.s, c.s_id, c.t, c.t_id] msg = ('Added connection ' + c.s.label + ' (' + c.s_id + ') -> ' + c.t.label + ' (' + c.t_id + ') to network.') @@ -1160,12 +1163,7 @@ def init_fluids(self): # if there is a starting value elif fluid in tmp0.keys(): - if fluid in tmp_set.keys(): - if not tmp_set[fluid]: - c.fluid.val[fluid] = tmp0[fluid] - c.fluid.val0[fluid] = tmp0[fluid] - c.fluid.val_set[fluid] = False - else: + if fluid not in tmp_set.keys(): c.fluid.val[fluid] = tmp0[fluid] c.fluid.val0[fluid] = tmp0[fluid] c.fluid.val_set[fluid] = False @@ -1235,24 +1233,27 @@ def init_target(self, c, start): if ((len(c.t.inlets()) == 1 and len(c.t.outlets()) == 1 and not isinstance(c.t, cycle_closer)) or isinstance(c.t, heat_exchanger) or - isinstance(c.t, subsystem_interface)): + isinstance(c.t, subsystem_interface) or + isinstance(c.t, orc_evaporator)): outc = pd.DataFrame() outc['s'] = self.conns.s == c.t outc['s_id'] = self.conns.s_id == c.t_id.replace('in', 'out') conn, cid = outc['s'] == True, outc['s_id'] == True - outc = outc.index[conn & cid][0] + outconn = outc.index[conn & cid][0] for fluid, x in c.fluid.val.items(): - if not outc.fluid.val_set[fluid]: - outc.fluid.val[fluid] = x + if (outconn.fluid.val_set[fluid] is False and + outconn.good_starting_values is False): + outconn.fluid.val[fluid] = x - self.init_target(outc, start) + self.init_target(outconn, start) if isinstance(c.t, splitter): for outconn in self.comps.loc[c.t].o: for fluid, x in c.fluid.val.items(): - if not outconn.fluid.val_set[fluid]: + if (outconn.fluid.val_set[fluid] is False and + outconn.good_starting_values is False): outconn.fluid.val[fluid] = x self.init_target(outconn, start) @@ -1262,13 +1263,15 @@ def init_target(self, c, start): outconn = self.comps.loc[c.t].o[0] for fluid, x in c.fluid.val.items(): - if not outconn.fluid.val_set[fluid]: + if (outconn.fluid.val_set[fluid] is False and + outconn.good_starting_values is False): outconn.fluid.val[fluid] = x if isinstance(c.t, combustion_engine): for outconn in self.comps.loc[c.t].o[:2]: for fluid, x in c.fluid.val.items(): - if not outconn.fluid.val_set[fluid]: + if (outconn.fluid.val_set[fluid] is False and + outconn.good_starting_values is False): outconn.fluid.val[fluid] = x self.init_target(outconn, start) @@ -1277,7 +1280,8 @@ def init_target(self, c, start): start = c.t for outconn in self.comps.loc[c.t].o: for fluid, x in c.fluid.val.items(): - if not outconn.fluid.val_set[fluid]: + if (outconn.fluid.val_set[fluid] is False and + outconn.good_starting_values is False): outconn.fluid.val[fluid] = x self.init_target(outconn, start) @@ -1304,24 +1308,27 @@ def init_source(self, c, start): if ((len(c.s.inlets()) == 1 and len(c.s.outlets()) == 1 and not isinstance(c.s, cycle_closer)) or isinstance(c.s, heat_exchanger) or - isinstance(c.s, subsystem_interface)): + isinstance(c.s, subsystem_interface) or + isinstance(c.s, orc_evaporator)): inc = pd.DataFrame() inc['t'] = self.conns.t == c.s inc['t_id'] = self.conns.t_id == c.s_id.replace('out', 'in') conn, cid = inc['t'] == True, inc['t_id'] == True - inc = inc.index[conn & cid][0] + inconn = inc.index[conn & cid][0] for fluid, x in c.fluid.val.items(): - if not inc.fluid.val_set[fluid]: - inc.fluid.val[fluid] = x + if (inconn.fluid.val_set[fluid] is False and + inconn.good_starting_values is False): + inconn.fluid.val[fluid] = x - self.init_source(inc, start) + self.init_source(inconn, start) if isinstance(c.s, splitter): for inconn in self.comps.loc[c.s].i: for fluid, x in c.fluid.val.items(): - if not inconn.fluid.val_set[fluid]: + if (inconn.fluid.val_set[fluid] is False and + inconn.good_starting_values is False): inconn.fluid.val[fluid] = x self.init_source(inconn, start) @@ -1329,7 +1336,8 @@ def init_source(self, c, start): if isinstance(c.s, merge): for inconn in self.comps.loc[c.s].i: for fluid, x in c.fluid.val.items(): - if not inconn.fluid.val_set[fluid]: + if (inconn.fluid.val_set[fluid] is False and + inconn.good_starting_values is False): inconn.fluid.val[fluid] = x self.init_source(inconn, start) @@ -1337,7 +1345,8 @@ def init_source(self, c, start): if isinstance(c.s, combustion_engine): for inconn in self.comps.loc[c.s].i[:2]: for fluid, x in c.fluid.val.items(): - if not inconn.fluid.val_set[fluid]: + if (inconn.fluid.val_set[fluid] is False and + inconn.good_starting_values is False): inconn.fluid.val[fluid] = x self.init_source(inconn, start) @@ -1346,7 +1355,8 @@ def init_source(self, c, start): start = c.s for inconn in self.comps.loc[c.s].i: for fluid, x in c.fluid.val.items(): - if not inconn.fluid.val_set[fluid]: + if (inconn.fluid.val_set[fluid] is False and + inconn.good_starting_values is False): inconn.fluid.val[fluid] = x self.init_source(inconn, start) @@ -1363,7 +1373,8 @@ def init_properties(self): """ # fluid properties for c in self.conns.index: - c.init_csv = False + if self.init_previous is False: + c.good_starting_values = False for key in ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp']: if c.get_attr(key).unit_set is False and key != 'x': if key == 'Td_bp': @@ -1372,7 +1383,8 @@ def init_properties(self): c.get_attr(key).unit = self.get_attr(key + '_unit') if (key not in ['T', 'x', 'v', 'Td_bp'] and c.get_attr(key).val_set is False): - self.init_val0(c, key) + if c.good_starting_values is False: + self.init_val0(c, key) c.get_attr(key).val_SI = ( c.get_attr(key).val0 * self.get_attr(key)[ c.get_attr(key).unit]) @@ -1519,7 +1531,7 @@ def init_csv(self): c.p.val0 = c.p.val_SI / self.p[c.p.unit] c.h.val0 = c.h.val_SI / self.h[c.h.unit] c.fluid.val0 = c.fluid.val.copy() - c.init_csv = True + c.good_starting_values = True else: msg = ('Could not find connection ' + c.s.label + ' (' + c.s_id + ') -> ' + c.t.label + ' (' + c.t_id + @@ -1530,7 +1542,7 @@ def init_csv(self): logging.debug(msg) def solve(self, mode, init_path=None, design_path=None, - max_iter=50, init_only=False): + max_iter=50, init_only=False, init_previous=True): r""" Solve the network. @@ -1558,7 +1570,11 @@ def solve(self, mode, init_path=None, design_path=None, Maximum number of iterations before calculation stops, default: 50. init_only : boolean - Perform initialisation only? default: :code:`False`. + Perform initialisation only, default: :code:`False`. + + init_previous : boolean + Initialise the calculation with values from the previous + calculation, default: :code:`True`. Note ---- @@ -1583,6 +1599,7 @@ def solve(self, mode, init_path=None, design_path=None, self.init_path = init_path self.design_path = design_path self.max_iter = max_iter + self.init_previous = init_previous if mode != 'offdesign' and mode != 'design': msg = 'Mode must be \'design\' or \'offdesign\'.' @@ -1651,7 +1668,7 @@ def solve(self, mode, init_path=None, design_path=None, logging.error(msg) return - self.post_processing() + self.postprocessing() fp.memorise.del_memory(self.fluids) if not self.progress: @@ -1872,14 +1889,14 @@ def solve_control(self): i = 0 for c in self.conns.index: # mass flow, pressure and enthalpy - if not c.m.val_set: + if c.m.val_set is False: c.m.val_SI += self.vec_z[i * (self.num_conn_vars)] - if not c.p.val_set: + if c.p.val_set is False: # this prevents negative pressures relax = max(1, -self.vec_z[i * (self.num_conn_vars) + 1] / (0.5 * c.p.val_SI)) c.p.val_SI += self.vec_z[i * (self.num_conn_vars) + 1] / relax - if not c.h.val_set: + if c.h.val_set is False: c.h.val_SI += self.vec_z[i * (self.num_conn_vars) + 2] # fluid vector (only if number of fluids is greater than 1) @@ -1887,7 +1904,7 @@ def solve_control(self): j = 0 for fluid in self.fluids: # add increment - if not c.fluid.val_set[fluid]: + if c.fluid.val_set[fluid] is False: c.fluid.val[fluid] += ( self.vec_z[i * (self.num_conn_vars) + 3 + j]) @@ -1997,7 +2014,7 @@ def solve_check_props(self, c): c.h.val_SI = hmin * 1.05 logging.debug(self.property_range_message(c, 'h')) if c.h.val_SI > hmax and not c.h.val_set: - c.h.val_SI = hmax * 0.9 + c.h.val_SI = hmax * 0.95 logging.debug(self.property_range_message(c, 'h')) if ((c.Td_bp.val_set is True or c.state.is_set is True) and @@ -2015,7 +2032,7 @@ def solve_check_props(self, c): c.h.val_SI = h * 0.98 logging.debug(self.property_range_message(c, 'h')) - elif self.iter < 4 and c.init_csv is False: + elif self.iter < 4 and c.good_starting_values is False: # pressure if c.p.val_SI <= self.p_range_SI[0] and not c.p.val_set: c.p.val_SI = self.p_range_SI[0] @@ -2448,7 +2465,7 @@ def solve_busses(self): row += 1 - def post_processing(self): + def postprocessing(self): r"""Calculate bus, component parameters and connection parameters.""" # components self.comps.apply(network.process_components, axis=1) @@ -2468,6 +2485,7 @@ def post_processing(self): # connections for c in self.conns.index: + c.good_starting_values = True c.T.val_SI = fp.T_mix_ph(c.to_flow(), T0=c.T.val_SI) c.v.val_SI = fp.v_mix_ph(c.to_flow(), T0=c.T.val_SI) * c.m.val_SI c.T.val = (c.T.val_SI / self.T[c.T.unit][1] - self.T[c.T.unit][0]) @@ -2870,7 +2888,7 @@ def save_characteristics(self, path): df['id'] = df.apply(network.get_id, axis=1) df['type'] = df.apply(network.get_class_base, axis=1) - cols = ['x', 'y'] + cols = ['x', 'y', 'extrapolate'] for val in cols: df[val] = df.apply(network.get_props, axis=1, args=(val,)) diff --git a/tespy/tools/characteristics.py b/tespy/tools/characteristics.py index 06e69ab06..d8dec7f8f 100644 --- a/tespy/tools/characteristics.py +++ b/tespy/tools/characteristics.py @@ -27,7 +27,7 @@ class char_line: r""" - Class characteristics for components. + Class for characteristc lines. Parameters ---------- @@ -39,6 +39,10 @@ class char_line: The corresponding y-values for the lookup table. Number of x and y values must be identical. + extrapolate : boolean + If :code:`True` linear extrapolation is performed when the x value is + out of the defined value range. + Note ---- This class generates a lookup table from the given input data x and y, @@ -50,16 +54,21 @@ class char_line: :code:`x = [0, 1], y = [1, 1]`. """ - def __init__(self, x=np.array([0, 1]), y=np.array([1, 1])): + def __init__( + self, x=np.array([0, 1]), y=np.array([1, 1]), extrapolate=False): self.x = x self.y = y + self.extrapolate = extrapolate if isinstance(self.x, list): self.x = np.array(self.x) if isinstance(self.y, list): self.y = np.array(self.y) + self.x = self.x.astype(float) + self.y = self.y.astype(float) + if len(self.x) != len(self.y): msg = ('Please provide the same amount of x-values and y-values. ' 'Number of x-values is ' + str(len(self.x)) + ', number of ' @@ -77,7 +86,7 @@ def evaluate(self, x): Parameters ---------- x : float - Input value for lookup table. + Input value for linear interpolation. Returns ------- @@ -86,28 +95,44 @@ def evaluate(self, x): Note ---- - This methods checks for the value range first. If the x-value is - outside of the specified range, the function will return the values at - the corresponding boundary. + This methods checks for the value range first. If :code:`extrapolate` + is :code:`False` (default) and the x-value is outside of the specified + range, the function will return the values at the corresponding + boundary. If :code:`extrapolate` is :code:`True` the y-value is + calculated by linear extrapolation. + + .. math:: + + y = y_0 + \frac{x-x_0}{x_1-x_0} \cdot \left(y_1-y_0 \right) + + where the index :math:`x_0` represents the lower and :math:`x_1` the + upper adjacent x-value. :math:`y_0` and :math:`y_1` are the + corresponding y-values. On extrapolation the two smallest or the two + largest value pairs are used respectively. """ xpos = np.searchsorted(self.x, x) if xpos == len(self.x): - y = self.y[xpos - 1] + if self.extrapolate is True: + xpos = -1 + else: + return self.y[-1] elif xpos == 0: - y = self.y[0] - else: - yfrac = (x - self.x[xpos - 1]) / (self.x[xpos] - self.x[xpos - 1]) - y = self.y[xpos - 1] + yfrac * (self.y[xpos] - self.y[xpos - 1]) - return y + if self.extrapolate is True: + xpos = 1 + else: + return self.y[0] + + yfrac = (x - self.x[xpos - 1]) / (self.x[xpos] - self.x[xpos - 1]) + return self.y[xpos - 1] + yfrac * (self.y[xpos] - self.y[xpos - 1]) def get_bound_errors(self, x, c): r""" - Prompt error messages, if value is out of bounds. + Prompt error messages, if x value is out of bounds. Parameters ---------- x : float - Input value for lookup table. + Input value for linear interpolation. Returns ------- @@ -192,6 +217,11 @@ def __init__(self, x=np.array([0, 1]), y=np.array([[1, 1], [1, 1]]), if isinstance(self.z2, list): self.z2 = np.array(self.z2) + self.x = self.x.astype(float) + self.y = self.y.astype(float) + self.z1 = self.z1.astype(float) + self.z2 = self.z2.astype(float) + if self.x.shape[0] != self.y.shape[0]: msg = ('The number of x-values determines the number of dimension ' 'for the characteristic map. You have provided ' + @@ -302,8 +332,34 @@ def evaluate(self, x, y): z2 : float Resulting z2 value. + + Note + ---- + This methods checks for the value range first. If the x-value is + outside of the specified range, the function will return the arrays + for :math:`y`, :math:`z1` and :math:`z2`. + + .. math:: + + \vec{y} = \vec{y_0} + \frac{x-x_0}{x_1-x_0} \cdot + \left(\vec{y_1}-\vec{y_0} \right)\\ + \vec{z1} = \vec{z1_0} + \frac{x-x_0}{x_1-x_0} \cdot + \left(\vec{z1_1}-\vec{z1_0} \right)\\ + \vec{z2} = \vec{z2_0} + \frac{x-x_0}{x1-x_0} \cdot + \left(\vec{z2_1}-\vec{z2_0}\right) + + The index :math:`x_0` represents the lower and :math:`x_1` the + upper adjacent x-value. Using the y-value as second input dimension + the corresponding z1- and z2-values are calculated, again using linear + interpolation. + + .. math:: + + z1 = z1_0 + \frac{y-y_0}{y_1-y_0} \cdot \left(z1_1-z1_0 \right)\\ + z2 = z2_0 + \frac{y-y_0}{y_1-y_0} \cdot \left(z2_1-z2_0 \right) """ - z1, z2 = self.evaluate_y(y, self.evaluate_x(x)) + yarr, z1arr, z2arr = self.evaluate_x(x) + z1, z2 = self.evaluate_y(y, yarr, z1arr, z2arr) return z1, z2 @@ -463,13 +519,16 @@ def evaluate(self, x, y, igva): Note ---- - Value manipulation by igva: + In contrast to the :py:class:`tespy.tools.characteristics.char_map` + the values are manipulated by the inlet guide vane angle (igva): .. math:: - \vec{y} = \vec{y} * \left( 1 - \frac{igva}{100} \right)\\ - \vec{z1} = \vec{z1} * \left( 1 - \frac{igva}{100} \right)\\ - \vec{z2} = \vec{z2} * \left( 1 - \frac{igva^2}{10000} \right) + \vec{y} = \vec{y} \cdot \left( 1 - \frac{igva}{100} \right)\\ + \vec{z1} = \vec{z1} \cdot \left( 1 - \frac{igva}{100} \right)\\ + \vec{z2} = \vec{z2} \cdot \left( 1 - \frac{igva}{100}^2 \right) + + Reference: :cite:`GasTurb2018`. """ yarr, z1arr, z2arr = self.evaluate_x(x) diff --git a/tespy/tools/fluid_properties.py b/tespy/tools/fluid_properties.py index 91dbc581d..1662d5960 100644 --- a/tespy/tools/fluid_properties.py +++ b/tespy/tools/fluid_properties.py @@ -1511,6 +1511,8 @@ def visc_mix_pT(flow, T): \forall i \in \text{fluid components}\\ y: \text{volume fraction}\\ M: \text{molar mass} + + Reference: :cite:`Herning1936`. """ n = molar_mass_flow(flow[3]) diff --git a/tespy/tools/helpers.py b/tespy/tools/helpers.py index 58b62ae7f..8ec891ada 100644 --- a/tespy/tools/helpers.py +++ b/tespy/tools/helpers.py @@ -14,7 +14,7 @@ import CoolProp as CP -import math +import numpy as np import os import logging @@ -379,12 +379,12 @@ def lamb(re, ks, d): elif re > 1e5 and re < 5e6: return 0.0032 + 0.221 * re ** (-0.237) else: - l0 = 0.0001 + l0 = 0.02 return newton(lamb_smooth, dlamb_smooth_dlamb, [re], 0, val0=l0, valmin=0.00001, valmax=0.2) elif re * ks / d > 1300: - return 1 / (2 * math.log(3.71 * d / ks, 10)) ** 2 + return 1 / (-2 * np.log10(ks / (3.71 * d))) ** 2 else: l0 = 0.002 @@ -395,19 +395,19 @@ def lamb(re, ks, d): def lamb_smooth(params, lamb): """Calculate lambda in smooth conditions.""" re = params[0] - return 2 * math.log(re * math.sqrt(lamb), 10) - 0.8 - 1 / math.sqrt(lamb) + return 2 * np.log10(re * lamb ** 0.5) - 0.8 - 1 / lamb ** 0.5 def dlamb_smooth_dlamb(params, lamb): """Calculate derivative of lambda in smooth conditions.""" - return 1 / (lamb * math.log(10)) + 1 / 2 * lamb ** (-1.5) + return 1 / (lamb * np.log(10)) + 1 / 2 * lamb ** (-1.5) def lamb_trans(params, lamb): """Calculate lambda in transition region (smooth to rough).""" re, ks, d = params[0], params[1], params[2] - return (2 * math.log(2.51 / (re * math.sqrt(lamb)) + ks / d * 0.269, 10) + - 1 / math.sqrt(lamb)) + return (2 * np.log10(2.51 / (re * lamb ** 0.5) + ks / (3.71 * d)) + + 1 / lamb ** 0.5) def dlamb_trans_dlamb(params, lamb): @@ -442,16 +442,12 @@ def modify_path_os(path): if path[0] != '\\' and path[1:2] != ':' and path[0] != '.': # relative path path = '.\\' + path - elif os.name == 'posix': - # linux, max + else: + # linux, mac path = path.replace('\\', '/') if path[0] != '/' and path[0] != '.': - # absolute path + # relative path path = './' + path - else: - # unkown os - msg = 'Unknown operating system, using posix pathing logic.' - logging.warning(msg) return path diff --git a/tests/component_tests/combustion_tests.py b/tests/component_tests/combustion_tests.py index c306eb683..d0ac9f2d0 100644 --- a/tests/component_tests/combustion_tests.py +++ b/tests/component_tests/combustion_tests.py @@ -21,6 +21,12 @@ import shutil +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + class component_tests: def setup(self): @@ -75,6 +81,7 @@ def test_combustion_chamber(self): b.add_comps({'c': instance}) self.nw.add_busses(b) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of thermal input must be ' + str(b.P.val) + ', is ' + str(instance.ti.val) + '.') eq_(round(b.P.val, 1), round(instance.ti.val, 1), msg) @@ -83,6 +90,7 @@ def test_combustion_chamber(self): # test specified thermal input for combustion_chamber instance.set_attr(ti=1e6) self.nw.solve('design') + convergence_check(self.nw.lin_dep) ti = (self.c2.m.val_SI * self.c2.fluid.val['CH4'] * instance.fuels['CH4']['LHV']) msg = ('Value of thermal input must be ' + str(instance.ti.val) + @@ -93,6 +101,7 @@ def test_combustion_chamber(self): self.c3.set_attr(T=np.nan) instance.set_attr(lamb=1) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of oxygen in flue gas must be 0.0, is ' + str(round(self.c3.fluid.val['O2'], 4)) + '.') eq_(0.0, round(self.c3.fluid.val['O2'], 4), msg) @@ -137,9 +146,11 @@ def test_combustion_engine(self): ti = 1e6 TI.set_attr(P=ti) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') # calculate in offdesign mode self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of thermal input must be ' + str(TI.P.val) + ', is ' + str(instance.ti.val) + '.') eq_(round(TI.P.val, 1), round(instance.ti.val, 1), msg) @@ -148,6 +159,7 @@ def test_combustion_engine(self): TI.set_attr(P=np.nan) instance.set_attr(ti=ti) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of thermal input must be ' + str(ti) + ', is ' + str(instance.ti.val) + '.') eq_(round(ti, 1), round(instance.ti.val, 1), msg) @@ -156,6 +168,7 @@ def test_combustion_engine(self): # test specified heat output 1 bus value Q1.set_attr(P=instance.Q1.val) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) # heat output is at design point value, thermal input must therefore # not have changed msg = ('Value of thermal input must be ' + str(ti) + ', is ' + @@ -172,6 +185,7 @@ def test_combustion_engine(self): # test specified heat output 2 bus value Q2.set_attr(P=1.2 * instance.Q2.val) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) # calculate heat output over cooling loop heat2 = self.c5.m.val_SI * (self.c7.h.val_SI - self.c5.h.val_SI) @@ -183,6 +197,7 @@ def test_combustion_engine(self): Q2.set_attr(P=np.nan) instance.set_attr(Q2=heat2) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) heat2 = self.c5.m.val_SI * (self.c7.h.val_SI - self.c5.h.val_SI) msg = ('Value of heat output 2 must be ' + str(heat2) + ', is ' + str(instance.Q2.val) + '.') @@ -192,6 +207,7 @@ def test_combustion_engine(self): instance.set_attr(Q2=np.nan) Q.set_attr(P=1.5 * instance.Q1.val) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) heat = (self.c4.m.val_SI * (self.c6.h.val_SI - self.c4.h.val_SI) + self.c5.m.val_SI * (self.c7.h.val_SI - self.c5.h.val_SI)) msg = ('Value of total heat output must be ' + str(Q.P.val) + @@ -202,6 +218,7 @@ def test_combustion_engine(self): Q.set_attr(P=np.nan) Qloss.set_attr(P=1e5) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of heat loss must be ' + str(Qloss.P.val) + ', is ' + str(instance.Qloss.val) + '.') eq_(round(Qloss.P.val, 1), round(instance.Qloss.val, 1), msg) diff --git a/tests/component_tests/heat_exchanger_tests.py b/tests/component_tests/heat_exchanger_tests.py index e0ac6022a..f0d8e7719 100644 --- a/tests/component_tests/heat_exchanger_tests.py +++ b/tests/component_tests/heat_exchanger_tests.py @@ -26,6 +26,12 @@ import shutil +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + class heat_exchanger_tests: def setup(self): @@ -82,6 +88,7 @@ def test_heat_ex_simple(self): b.add_comps({'c': instance}) self.nw.add_busses(b) self.nw.solve('design') + convergence_check(self.nw.lin_dep) pr = round(self.c2.p.val_SI / self.c1.p.val_SI, 3) msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + str(instance.pr.val) + '.') @@ -93,6 +100,7 @@ def test_heat_ex_simple(self): instance.set_attr(D=instance.D.val, zeta='var', pr=np.nan) instance.D.is_var = False self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of zeta must be ' + str(zeta) + ', is ' + str(round(instance.zeta.val, 0)) + '.') eq_(zeta, round(instance.zeta.val, 0), msg) @@ -101,6 +109,7 @@ def test_heat_ex_simple(self): pr = round(instance.pr.val, 3) instance.set_attr(zeta=np.nan, pr='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + str(round(instance.pr.val, 3)) + '.') eq_(pr, round(instance.pr.val, 3), msg) @@ -110,6 +119,7 @@ def test_heat_ex_simple(self): instance.set_attr(kA='var', pr=np.nan) b.set_attr(P=-5e4) self.nw.solve('design') + convergence_check(self.nw.lin_dep) # due to heat output being half of reference (for Tamb) kA should be # somewhere near to that (actual value is 677) @@ -122,6 +132,7 @@ def test_heat_ex_simple(self): Q = -5e4 b.set_attr(P=Q) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of heat transfer must be ' + str(Q) + ', is ' + str(instance.Q.val) + '.') eq_(Q, round(instance.Q.val, 0), msg) @@ -149,6 +160,7 @@ def test_solar_collector(self): instance.set_attr(E=1e3, lkf_lin=1.0, lkf_quad=0.005, A='var', eta_opt=0.9, Q=1e5, Tamb=20, pr=0.99) self.nw.solve('design') + convergence_check(self.nw.lin_dep) # heat loss must be identical to Q - E * A (internal heat loss # calculation) T_diff = (self.c2.T.val + self.c1.T.val) / 2 - instance.Tamb.val @@ -164,26 +176,31 @@ def test_solar_collector(self): # test all parameters of the energy group: E instance.set_attr(A=instance.A.val, E='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) eq_(Q_loss, round(instance.Q_loss.val, 0), msg) # test all parameters of the energy group: eta_opt instance.set_attr(E=instance.E.val, eta_opt='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) eq_(Q_loss, round(instance.Q_loss.val, 0), msg) # test all parameters of the energy group: lkf_lin instance.set_attr(eta_opt=instance.eta_opt.val, lkf_lin='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) eq_(Q_loss, round(instance.Q_loss.val, 0), msg) # test all parameters of the energy group: lkf_quad instance.set_attr(lkf_lin=instance.lkf_lin.val, lkf_quad='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) eq_(Q_loss, round(instance.Q_loss.val, 0), msg) # test all parameters of the energy group: Tamb instance.set_attr(lkf_lin=instance.lkf_lin.val, lkf_quad='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) eq_(Q_loss, round(instance.Q_loss.val, 0), msg) def test_heat_ex(self): @@ -204,6 +221,7 @@ def test_heat_ex(self): b.add_comps({'c': instance}) self.nw.add_busses(b) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') # check heat transfer @@ -228,6 +246,7 @@ def test_heat_ex(self): self.c2.set_attr(T=np.nan) instance.set_attr(ttd_l=20) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of terminal temperature difference must be ' + str(instance.ttd_l.val) + ', is ' + str(self.c2.T.val - self.c3.T.val) + '.') @@ -239,6 +258,7 @@ def test_heat_ex(self): self.c2.set_attr(T=70) instance.set_attr(ttd_l=np.nan) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of heat flow must be ' + str(instance.Q.val) + ', is ' + str(round(Q, 0)) + '.') eq_(round(Q, 0), round(instance.Q.val, 0), msg) @@ -250,6 +270,7 @@ def test_heat_ex(self): self.c4.set_attr(T=np.nan) self.c2.set_attr(T=30) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of upper terminal temperature differences must be ' 'smaller than zero, is ' + str(round(instance.ttd_l.val, 1)) + '.') @@ -263,6 +284,7 @@ def test_heat_ex(self): self.c1.set_attr(h=150e3, T=np.nan) self.c3.set_attr(T=40) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of upper terminal temperature differences must be ' 'smaller than zero, is ' + str(round(instance.ttd_u.val, 1)) + '.') @@ -285,6 +307,7 @@ def test_condenser(self): self.c4.set_attr(T=40) instance.set_attr(Q=-80e3) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') # test heat transfer @@ -306,6 +329,7 @@ def test_condenser(self): # test lower terminal temperature difference instance.set_attr(ttd_l=20, ttd_u=np.nan, design=['pr2', 'ttd_l']) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of terminal temperature difference must be ' + str(instance.ttd_l.val) + ', is ' + str(self.c2.T.val - self.c3.T.val) + '.') @@ -315,6 +339,7 @@ def test_condenser(self): # check kA value with condensing pressure in offdesign mode: # no changes to design point means: identical pressure self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of condensing pressure be ' + str(p) + ', is ' + str(round(self.c1.p.val_SI, 5)) + '.') eq_(p, round(self.c1.p.val_SI, 5), msg) diff --git a/tests/component_tests/orc_evaporator_tests.py b/tests/component_tests/orc_evaporator_tests.py new file mode 100644 index 000000000..d74d99f25 --- /dev/null +++ b/tests/component_tests/orc_evaporator_tests.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 + +"""Module for testing components of type orc evaporator. +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 +tests/component_tests/orc_evaporator_tests.py +SPDX-License-Identifier: MIT +""" + +from nose.tools import eq_ + +from tespy.components.basics import sink, source +from tespy.components.customs import orc_evaporator +from tespy.connections import connection, bus +from tespy.networks.networks import network +from tespy.tools.fluid_properties import T_bp_p + +import logging + +import numpy as np +import shutil + + +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + +class orc_evaporator_tests: + + def setup(self): + + self.nw = network(['water', 'Isopentane'], T_unit='C', p_unit='bar', + h_unit='kJ / kg') + self.inl1 = source('inlet 1') + self.outl1 = sink('outlet 1') + + self.inl2 = source('inlet 2') + self.outl2 = sink('outlet 2') + + self.inl3 = source('inlet 3') + self.outl3 = sink('outlet 3') + + def setup_orc_evaporator_network(self, instance): + + self.c1 = connection(self.inl1, 'out1', instance, 'in1') + self.c2 = connection(instance, 'out1', self.outl1, 'in1') + self.c3 = connection(self.inl2, 'out1', instance, 'in2') + self.c4 = connection(instance, 'out2', self.outl2, 'in1') + self.c5 = connection(self.inl3, 'out1', instance, 'in3') + self.c6 = connection(instance, 'out3', self.outl3, 'in1') + + self.nw.add_conns(self.c1, self.c2, self.c3, + self.c4, self.c5, self.c6) + + def test_orc_evap(self): + """ + Test component properties of orc evaporator. + """ + instance = orc_evaporator('orc evaporator') + self.setup_orc_evaporator_network(instance) + + # design specification + instance.set_attr(pr1=0.95, pr2=0.975, pr3=0.975, + design=['pr1', 'pr2', 'pr3'], + offdesign=['zeta1', 'zeta2', 'zeta3']) + self.c1.set_attr(T=146.6, p=4.34, m=20.4, state='g', + fluid={'water': 1, 'Isopentane': 0}) + self.c3.set_attr(T=146.6, p=10.2, + fluid={'water': 1, 'Isopentane': 0}) + self.c4.set_attr(T=118.6) + self.c5.set_attr(T=111.6, p=10.8, + fluid={'water': 0, 'Isopentane': 1}) + + # test heat transfer + Q = -6.64e+07 + instance.set_attr(Q=Q) + self.nw.solve('design') + convergence_check(self.nw.lin_dep) + Q_is = self.c5.m.val_SI * (self.c6.h.val_SI - self.c5.h.val_SI) + msg = ('Value of heat flow must be ' + str(round(Q, 0)) + + ', is ' + str(round(Q_is, 0)) + '.') + eq_(round(Q, 0), round(Q_is, 0), msg) + + # test bus + instance.set_attr(Q=np.nan) + P = 6.64e+07 + b = bus('heat transfer', P=P) + b.add_comps({'c': instance}) + self.nw.add_busses(b) + self.nw.solve('design') + convergence_check(self.nw.lin_dep) + self.nw.save('tmp') + + Q_is = self.c5.m.val_SI * (self.c6.h.val_SI - self.c5.h.val_SI) + msg = ('Value of heat flow must be ' + str(round(P, 0)) + + ', is ' + str(round(Q_is, 0)) + '.') + eq_(round(P, 0), round(Q_is, 0), msg) + + # Check the state of the steam and working fluid outlet: + x_outl1_calc = self.c2.x.val + x_outl3_calc = self.c6.x.val + zeta1 = instance.zeta1.val + zeta2 = instance.zeta2.val + zeta3 = instance.zeta3.val + m = self.c5.m.val + + msg = ('Vapor mass fraction of steam outlet must be 0.0, is ' + + str(round(x_outl1_calc, 1)) + '.') + eq_(round(x_outl1_calc, 1), 0.0, msg) + + msg = ('Vapor mass fraction of working fluid outlet must be 1.0, is ' + + str(round(x_outl3_calc, 1)) + '.') + eq_(round(x_outl3_calc, 1), 1.0, msg) + + # Check offdesign by zeta values + # geometry independent friction coefficient + self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) + + msg = ('Geometry independent friction coefficient ' + 'at hot side 1 (steam) ' + 'must be ' + str(round(zeta1, 1)) + ', is ' + + str(round(instance.zeta1.val, 1)) + '.') + eq_(round(instance.zeta1.val, 1), round(zeta1, 1), msg) + msg = ('Geometry independent friction coefficient at ' + 'hot side 2 (brine) ' + 'must be ' + str(round(zeta2, 1)) + ', is ' + + str(round(instance.zeta2.val, 1)) + '.') + eq_(round(instance.zeta2.val, 1), round(zeta2, 1), msg) + msg = ('Geometry independent friction coefficient at cold side ' + '(Isopentane) must be ' + str(round(zeta3, 1)) + ', is ' + + str(round(instance.zeta3.val, 1)) + '.') + eq_(round(instance.zeta3.val, 1), round(zeta3, 1), msg) + + # test parameters of 'subcooling' and 'overheating' + instance.set_attr(subcooling=True, overheating=True) + dT = 0.5 + self.c2.set_attr(Td_bp=-dT) + self.c6.set_attr(Td_bp=dT) + self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) + + T_steam = T_bp_p(self.c2.to_flow()) - dT + T_isop = T_bp_p(self.c6.to_flow()) + dT + + msg = ('Temperature of working fluid outlet must be ' + + str(round(T_isop, 1)) + ', is ' + + str(round(self.c6.T.val_SI, 1)) + '.') + eq_(round(T_isop, 1), round(self.c6.T.val_SI, 1), msg) + + msg = ('Temperature of steam outlet must be ' + + str(round(T_steam, 1)) + ', is ' + + str(round(self.c2.T.val_SI, 1)) + '.') + eq_(round(T_steam, 1), round(self.c2.T.val_SI, 1), msg) + + shutil.rmtree('./tmp', ignore_errors=True) diff --git a/tests/component_tests/piping_tests.py b/tests/component_tests/piping_tests.py index 6289a51e2..9d59f0588 100644 --- a/tests/component_tests/piping_tests.py +++ b/tests/component_tests/piping_tests.py @@ -23,6 +23,12 @@ import shutil +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + class piping_tests: def setup_piping_network(self, instance): @@ -47,6 +53,7 @@ def test_valve(self): # test variable pressure ration instance.set_attr(pr='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) pr = round(self.c2.p.val_SI / self.c1.p.val_SI, 2) msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + str(round(instance.pr.val, 2)) + '.') @@ -56,6 +63,7 @@ def test_valve(self): zeta = round(instance.zeta.val, 0) instance.set_attr(zeta='var', pr=np.nan) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of dimension independent zeta value must be ' + str(zeta) + ', is ' + str(round(instance.zeta.val, 0)) + '.') eq_(zeta, round(instance.zeta.val, 0), msg) @@ -70,6 +78,7 @@ def test_valve(self): self.c1.set_attr(m=m) self.c2.set_attr(p=np.nan) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.print_results() dp = round(-dp_char.evaluate(m), 0) dp_act = round(self.c2.p.val_SI - self.c1.p.val_SI) diff --git a/tests/component_tests/reactors_tests.py b/tests/component_tests/reactors_tests.py index 47e8ceaa6..a040f87af 100644 --- a/tests/component_tests/reactors_tests.py +++ b/tests/component_tests/reactors_tests.py @@ -21,6 +21,12 @@ import shutil +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + class reactors_tests: def setup(self): @@ -62,6 +68,7 @@ def test_water_electrolyzer(self): self.nw.add_busses(power) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of power must be ' + str(power.P.val) + ', is ' + str(self.instance.P.val) + '.') eq_(round(power.P.val, 1), round(self.instance.P.val), msg) @@ -75,6 +82,7 @@ def test_water_electrolyzer(self): self.nw.add_busses(heat) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of heat flow must be ' + str(heat.P.val) + ', is ' + str(self.instance.Q.val) + '.') eq_(round(heat.P.val, 1), round(self.instance.Q.val), msg) @@ -85,6 +93,7 @@ def test_water_electrolyzer(self): Q = heat.P.val * 0.9 heat.set_attr(P=Q) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of heat flow must be ' + str(Q) + ', is ' + str(self.instance.Q.val) + '.') eq_(round(Q, 1), round(self.instance.Q.val), msg) @@ -95,6 +104,7 @@ def test_water_electrolyzer(self): # test efficiency vs. specific energy consumption self.instance.set_attr(eta=0.9, e='var') self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of efficiency must be ' + str(self.instance.eta.val) + ', is ' + str(self.instance.e0 / self.instance.e.val) + '.') eq_(round(self.instance.eta.val, 2), @@ -106,6 +116,7 @@ def test_water_electrolyzer(self): self.instance.set_attr(e=np.nan, eta=np.nan) self.instance.set_attr(e=e) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of efficiency must be ' + str(self.instance.e0 / e) + ', is ' + str(self.instance.eta.val) + '.') eq_(round(self.instance.e0 / e, 2), round(self.instance.eta.val, 2), @@ -116,6 +127,7 @@ def test_water_electrolyzer(self): self.instance.set_attr(e=np.nan, eta=np.nan) self.instance.set_attr(e=e) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of specific energy consumption e must be ' + str(e) + ', is ' + str(self.instance.e.val) + '.') eq_(round(e, 1), round(self.instance.e.val, 1), msg) @@ -125,7 +137,9 @@ def test_water_electrolyzer(self): self.instance.set_attr(pr_c=pr, e=np.nan, zeta='var', P=2.5e6, design=['pr_c']) self.nw.solve('design') + shutil.rmtree('./tmp', ignore_errors=True) self.nw.save('tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + str(self.instance.pr_c.val) + '.') eq_(round(pr, 2), round(self.instance.pr_c.val, 2), msg) @@ -134,6 +148,7 @@ def test_water_electrolyzer(self): # ratio must not change self.instance.set_attr(zeta=np.nan, offdesign=['zeta']) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + str(self.instance.pr_c.val) + '.') eq_(round(pr, 2), round(self.instance.pr_c.val, 2), msg) @@ -142,6 +157,7 @@ def test_water_electrolyzer(self): Q = self.instance.Q.val * 0.9 self.instance.set_attr(Q=Q, P=np.nan) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of heat must be ' + str(Q) + ', is ' + str(self.instance.Q.val) + '.') eq_(round(Q, 0), round(self.instance.Q.val, 0), msg) diff --git a/tests/component_tests/turbomachinery_tests.py b/tests/component_tests/turbomachinery_tests.py index 5d27d4692..2c9f6c8f2 100644 --- a/tests/component_tests/turbomachinery_tests.py +++ b/tests/component_tests/turbomachinery_tests.py @@ -25,6 +25,12 @@ import shutil +def convergence_check(lin_dep): + """Check convergence status of a simulation.""" + msg = 'Calculation did not converge!' + eq_(lin_dep, False, msg) + + class turbomachinery_tests: def setup_network(self, instance): @@ -42,11 +48,12 @@ def test_compressor(self): self.setup_network(instance) # compress NH3, other fluids in network are for turbine, pump, ... - fl = {'N2': 0, 'O2': 0, 'Ar': 0, 'INCOMP::DowQ': 0, 'NH3': 1} - self.c1.set_attr(fluid=fl, v=1, p=5, T=100) - self.c2.set_attr(p=7) + fl = {'N2': 1, 'O2': 0, 'Ar': 0, 'INCOMP::DowQ': 0, 'NH3': 0} + self.c1.set_attr(fluid=fl, v=1, p=1, T=5) + self.c2.set_attr(p=6) instance.set_attr(eta_s=0.8) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') # test isentropic efficiency value @@ -59,6 +66,7 @@ def test_compressor(self): # trigger invalid value for isentropic efficiency instance.set_attr(eta_s=1.1) self.nw.solve('design') + convergence_check(self.nw.lin_dep) # test calculated value eta_s = ((instance.h_os('') - self.c1.h.val_SI) / @@ -76,14 +84,17 @@ def test_compressor(self): # offdesign test, efficiency value should be at design value self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of isentropic efficiency (' + str(instance.eta_s.val) + ') must be identical to design case (' + str(eta_s) + ').') eq_(round(eta_s_d, 2), round(instance.eta_s.val, 2), msg) # move to highest available speedline, mass flow below lowest value # at that line - self.c1.set_attr(v=np.nan, m=self.c1.m.val * 0.8, T=30) + self.c1.set_attr(v=np.nan, m=self.c1.m.val * 0.8, T=-30) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) + # should be value eta_s = eta_s_d * instance.char_map.func.z2[6, 0] msg = ('Value of isentropic efficiency (' + str(instance.eta_s.val) + @@ -92,8 +103,9 @@ def test_compressor(self): # going below lowest available speedline, above highest mass flow at # that line - self.c1.set_attr(T=300) + self.c1.set_attr(T=175) self.nw.solve('offdesign', design_path='tmp', init_path='tmp') + convergence_check(self.nw.lin_dep) # should be value eta_s = eta_s_d * instance.char_map.func.z2[0, 9] msg = ('Value of isentropic efficiency (' + str(instance.eta_s.val) + @@ -101,8 +113,8 @@ def test_compressor(self): eq_(round(eta_s, 4), round(instance.eta_s.val, 4), msg) # back to design properties, test eta_s_char - self.c2.set_attr(p=7) - self.c1.set_attr(v=1, T=100, m=np.nan) + self.c2.set_attr(p=6) + self.c1.set_attr(v=1, T=5, m=np.nan) # test parameter specification for eta_s_char with unset char map instance.set_attr(eta_s_char=dc_cc(func=ldc( @@ -110,6 +122,7 @@ def test_compressor(self): is_set=True, param='m')) instance.char_map.is_set = False self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of isentropic efficiency must be ' + str(eta_s_d) + ', is ' + str(instance.eta_s.val) + '.') eq_(round(eta_s_d, 3), round(instance.eta_s.val, 3), msg) @@ -117,6 +130,7 @@ def test_compressor(self): # move up in volumetric flow self.c1.set_attr(v=1.5) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) eta_s = round(eta_s_d * instance.eta_s_char.func.evaluate( self.c1.m.val_SI / self.c1.m.design), 3) msg = ('Value of isentropic efficiency must be ' + str(eta_s) + @@ -126,8 +140,9 @@ def test_compressor(self): # test parameter specification for pr instance.eta_s_char.set_attr(param='pr') self.c1.set_attr(v=1) - self.c2.set_attr(p=7.5) + self.c2.set_attr(p=6) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) expr = (self.c2.p.val_SI * self.c1.p.design / (self.c2.p.design * self.c1.p.val_SI)) eta_s = round(eta_s_d * instance.eta_s_char.func.evaluate(expr), 3) @@ -146,6 +161,7 @@ def test_pump(self): self.c2.set_attr(p=7) instance.set_attr(eta_s=1) self.nw.solve('design') + convergence_check(self.nw.lin_dep) # test calculated value for efficiency eta_s = ((instance.h_os('') - self.c1.h.val_SI) / @@ -168,6 +184,7 @@ def test_pump(self): eta_s_d = 0.8 instance.set_attr(eta_s=eta_s_d) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') self.c2.set_attr(p=np.nan) @@ -181,6 +198,7 @@ def test_pump(self): 'DEFAULT', char_line), is_set=True)) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) # value for difference pressure dp = 650000.0 @@ -191,6 +209,7 @@ def test_pump(self): # test ohter volumetric flow on flow char self.c1.set_attr(v=0.9) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) dp = 775000.0 msg = ('Value of pressure rise must be ' + str(dp) + ', is ' + str(self.c2.p.val_SI - self.c1.p.val_SI) + '.') @@ -209,6 +228,7 @@ def test_pump(self): self.c2.set_attr(T=ref(self.c1, 0, 20)) self.c1.set_attr(v=-0.1) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of power must be ' + str(14e5) + ', is ' + str(self.c2.p.val_SI - self.c1.p.val_SI) + '.') eq_(self.c2.p.val_SI - self.c1.p.val_SI, 14e5, msg) @@ -216,6 +236,7 @@ def test_pump(self): # upper boundary self.c1.set_attr(v=1.5) self.nw.solve('design') + convergence_check(self.nw.lin_dep) msg = ('Value of power must be ' + str(0) + ', is ' + str(self.c2.p.val_SI - self.c1.p.val_SI) + '.') eq_(self.c2.p.val_SI - self.c1.p.val_SI, 0, msg) @@ -231,6 +252,7 @@ def test_turbine(self): self.c2.set_attr(p=1, T=20) instance.set_attr(eta_s=0.85) self.nw.solve('design') + convergence_check(self.nw.lin_dep) self.nw.save('tmp') # design value of isentropic efficiency @@ -244,6 +266,7 @@ def test_turbine(self): # trigger invalid value for isentropic efficiency instance.set_attr(eta_s=1.1) self.nw.solve('design') + convergence_check(self.nw.lin_dep) eta_s = round((self.c2.h.val_SI - self.c1.h.val_SI) / (instance.h_os('') - self.c1.h.val_SI), 3) msg = ('Value of isentropic efficiency must be ' + str(eta_s) + @@ -258,6 +281,7 @@ def test_turbine(self): instance.eta_s_char.is_set = True instance.eta_s.is_set = False self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) # check efficiency msg = ('Value of isentropic efficiency (' + str(instance.eta_s.val) + ') must be identical to design case (' + str(eta_s_d) + ').') @@ -271,6 +295,7 @@ def test_turbine(self): # lowering mass flow, inlet pressure must sink according to cone law self.c1.set_attr(m=self.c1.m.val * 0.8) self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) msg = ('Value of pressure ratio (' + str(instance.pr.val) + ') must be at (' + str(0.128) + ').') eq_(0.128, round(instance.pr.val, 3), msg) @@ -280,6 +305,7 @@ def test_turbine(self): self.c1.set_attr(m=10) instance.eta_s_char.param = 'v' self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) expr = self.c1.v.val_SI / self.c1.v.design eta_s = round(eta_s_d * instance.eta_s_char.func.evaluate(expr), 3) msg = ('Value of isentropic efficiency (' + @@ -290,6 +316,7 @@ def test_turbine(self): # test parameter specification pr instance.eta_s_char.param = 'pr' self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) expr = (self.c2.p.val_SI * self.c1.p.design / (self.c2.p.design * self.c1.p.val_SI)) eta_s = round(eta_s_d * instance.eta_s_char.func.evaluate(expr), 3) @@ -301,6 +328,7 @@ def test_turbine(self): # test parameter specification dh_s instance.eta_s_char.param = 'dh_s' self.nw.solve('offdesign', design_path='tmp') + convergence_check(self.nw.lin_dep) expr = (instance.h_os('') - self.c1.h.val_SI) / instance.dh_s_ref eta_s = round(eta_s_d * instance.eta_s_char.func.evaluate(expr), 3) msg = ('Value of isentropic efficiency (' + @@ -324,6 +352,7 @@ def test_turbomachine(self): # pressure ratio and power are the basic functions for turbomachines, # these are inherited by all children, thus only tested here self.nw.solve('design') + convergence_check(self.nw.lin_dep) power = self.c1.m.val_SI * (self.c2.h.val_SI - self.c1.h.val_SI) pr = self.c2.p.val_SI / self.c1.p.val_SI msg = ('Value of power must be ' + str(power) + ', is ' + @@ -337,6 +366,7 @@ def test_turbomachine(self): self.c2.set_attr(p=np.nan) instance.set_attr(pr=5) self.nw.solve('design') + convergence_check(self.nw.lin_dep) pr = self.c2.p.val_SI / self.c1.p.val_SI msg = ('Value of power must be ' + str(pr) + ', is ' + str(instance.pr.val) + '.') @@ -346,6 +376,7 @@ def test_turbomachine(self): self.c2.set_attr(h=np.nan) instance.set_attr(P=1e5) self.nw.solve('design') + convergence_check(self.nw.lin_dep) power = self.c1.m.val_SI * (self.c2.h.val_SI - self.c1.h.val_SI) msg = ('Value of power must be ' + str(power) + ', is ' + str(instance.P.val) + '.') diff --git a/tests/error_tests.py b/tests/error_tests.py index 095ecb8b5..cf4a6d12c 100644 --- a/tests/error_tests.py +++ b/tests/error_tests.py @@ -375,6 +375,18 @@ def test_char_map_number_of_dimensions(): char_map(x=[0, 1, 2], y=[[1, 2, 3, 4], [1, 2, 3, 4]]) +@raises(ValueError) +def test_char_map_y_z_dimension_mismatch(): + char_map(x=[0, 1], y=[[1, 2, 3, 4], [1, 2, 3, 4]], + z1=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + z2=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]) + + +@raises(KeyError) +def test_char_map_get_attr(): + char_map().get_attr('Stuff') + + @raises(FileNotFoundError) def test_missing_char_files(): load_custom_char('stuff', char_line) diff --git a/tests/heat_pump_test.py b/tests/heat_pump_test.py index 00f6db9a6..7ba86494e 100644 --- a/tests/heat_pump_test.py +++ b/tests/heat_pump_test.py @@ -280,8 +280,6 @@ def test_model(self): self.nw.solve('design') self.nw.save('tmp') - self.nw.save('tmp2') - self.nw.print_results() # input values from ebsilon @@ -302,8 +300,7 @@ def test_model(self): for m in m_source[i]: self.amb_in_su.set_attr(m=m) if j == 0: - self.nw.solve('offdesign', design_path='tmp', init_path='tmp2') - self.nw.save('tmp2') + self.nw.solve('offdesign', design_path='tmp', init_path='tmp') else: self.nw.solve('offdesign', design_path='tmp') @@ -314,14 +311,13 @@ def test_model(self): # Another issue is, that the component characteristics for # generators and motors do not work on the same basis (tespy: mechanical, ebsilon: electrical)! d_rel_COP = abs(self.heat.P.val / self.power.P.val - COP[i, j]) / COP[i, j] - msg = ('The deviation in COP should be less than 0.075, is ' + + msg = ('The deviation in COP should be less than 0.065, is ' + str(d_rel_COP) + ' at mass flow ' + str(m) + ' and temperature ' + str(T) + '.') - eq_(d_rel_COP < 0.075, True, msg) + eq_(d_rel_COP < 0.065, True, msg) j += 1 i += 1 shutil.rmtree('./tmp', ignore_errors=True) - shutil.rmtree('./tmp2', ignore_errors=True) a = test_heat_pump() diff --git a/tests/network_tests/network_tests.py b/tests/network_tests/network_tests.py index 30cf772bb..289698591 100644 --- a/tests/network_tests/network_tests.py +++ b/tests/network_tests/network_tests.py @@ -123,7 +123,7 @@ def test_network_reader_deleted_chars(self): @raises(TESPyNetworkError) def offdesign_TESPyNetworkError(nw, **kwargs): - nw.solve('offdesign', kwargs) + nw.solve('offdesign', **kwargs) def test_network_missing_data_in_design_case_files(): @@ -188,6 +188,35 @@ def test_network_missing_data_in_individual_design_case_file(): shutil.rmtree('./tmp2', ignore_errors=True) +def test_network_missing_connection_in_design_path(): + """ + Test for missing connection data in design case files. + """ + nw = network(['water']) + source = basics.source('source') + pipe = piping.pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) + sink = basics.sink('sink') + a = connection(source, 'out1', pipe, 'in1', m=1, p=1e5, T=293.15, + fluid={'water': 1}) + b = connection(pipe, 'out1', sink, 'in1') + nw.add_conns(a, b) + nw.solve('design') + nw.save('tmp') + + inputs = open('./tmp/conn.csv') + all_lines = inputs.readlines() + all_lines.pop(len(all_lines) - 1) + inputs.close() + + with open('./tmp/conn.csv', 'w') as out: + for line in all_lines: + out.write(line.strip() + '\n') + + offdesign_TESPyNetworkError(nw, design_path='tmp') + + shutil.rmtree('./tmp', ignore_errors=True) + + class test_network_individual_offdesign: def setup_network_individual_offdesign(self): @@ -340,3 +369,34 @@ def test_local_offdesign_on_connections_and_components(self): shutil.rmtree('./design1', ignore_errors=True) shutil.rmtree('./design2', ignore_errors=True) + + def test_missing_design_path_local_offdesign_on_connections(self): + """ + Test missing design path specification on connections in local + offdesign mode. + """ + self.setup_network_individual_offdesign() + self.nw.solve('design') + self.sc2_v2.set_attr(m=0) + self.nw.solve('design') + self.nw.save('design1') + v1_design = self.sc1_v1.v.val_SI + zeta_sc1_design = self.sc1.zeta.val + + self.sc1_v1.set_attr(design=['T'], offdesign=['v'], state='l') + self.sc2_v2.set_attr(design=['T'], offdesign=['v'], state='l') + + self.sc1.set_attr(local_offdesign=True, design_path='design1') + self.pump1.set_attr(local_offdesign=True, design_path='design1') + self.sp_p1.set_attr(local_offdesign=True, design_path='design1') + self.p1_sc1.set_attr(local_offdesign=True, design_path='design1') + self.sc1_v1.set_attr(local_offdesign=True) + self.sc1.set_attr(E=500) + + self.sc2_v2.set_attr(T=95, m=np.nan) + try: + self.nw.solve('design', init_only=True) + except TESPyNetworkError: + pass + + shutil.rmtree('./design1', ignore_errors=True) diff --git a/tests/tools_tests/characteristics_tests.py b/tests/tools_tests/characteristics_tests.py index a0196509e..c863b05ea 100644 --- a/tests/tools_tests/characteristics_tests.py +++ b/tests/tools_tests/characteristics_tests.py @@ -12,7 +12,7 @@ from nose.tools import eq_ -from tespy.tools.characteristics import (char_line, compressor_map, +from tespy.tools.characteristics import (char_line, char_map, compressor_map, load_default_char, load_custom_char) from tespy.tools.helpers import extend_basic_path @@ -23,13 +23,14 @@ import shutil -class characteristics_tests: +class characteristics_loader_tests: def setup(self): # create data path and write json files into path self.path = extend_basic_path('data') def test_custom_char_line_import(self): + """Test importing a custom characteristc lines.""" # we need to write some data to the path first, using defaults data_path = resource_filename('tespy.data', 'char_lines.json') @@ -63,6 +64,7 @@ def test_custom_char_line_import(self): eq_(True, y_cond, msg) def test_custom_char_map_import(self): + """Test importing a custom characteristc map.""" # we need to write some data to the path first, using defaults data_path = resource_filename('tespy.data', 'char_maps.json') @@ -108,3 +110,170 @@ def test_custom_char_map_import(self): 'the default characteristic line ' + str(char_original.z2) + ' ' 'as these have been duplicated before load.') eq_(True, z2_cond, msg) + + +def test_char_line_evaluation(): + """Test the characteristc line evaluation.""" + + # create a characteristc line with values of y=(x-2)^2 + line = char_line(x=[0, 1, 2, 3, 4], y=[4, 1, 0, 1, 4]) + + # test evaluation at given x value (x=3, y=1) + x = 3 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 1.0, but is " + + str(y) + ".") + eq_(1.0, round(y, 1), msg) + + # test evaluation at x=0.5 to force interpolation, result: y=2.5 + x = 0.5 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 2.5, but is " + + str(y) + ".") + eq_(2.5, round(y, 1), msg) + + # test evaluation at x=-1 to check lower limits, result: y=4 + x = -1 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 4, but is " + + str(y) + ".") + eq_(4, round(y, 1), msg) + + # test evaluation at x=5 to check upper limits, result: y=4 + x = 5 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 4, but is " + + str(y) + ".") + eq_(4, round(y, 1), msg) + + +def test_char_line_extrapolation(): + """Test the characteristc line with extrapolation.""" + + # create a characteristc line with values of y=(x-2)^2 + line = char_line(x=[0, 1, 2, 3, 4], y=[4, 1, 0, 1, 4], extrapolate=True) + + # test evaluation at x=-1 to check lower limits, result: y=7 + x = -1 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 7, but is " + + str(y) + ".") + eq_(7.0, round(y, 1), msg) + + # test evaluation at x=5 to check upper limits, result: y=7 + x = 5 + y = line.evaluate(x) + msg = ("The evaluation of x=" + str(x) + " must be 7, but is " + + str(y) + ".") + eq_(7.0, round(y, 1), msg) + + +def test_char_map_evaluation(): + """Test the characteristc map evaluation.""" + + # create a characteristc line with values of y=(x-2)^2 + x = [1, 2, 3] + y = np.array([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + z1 = y ** 0.5 + z2 = y ** 2 + map = char_map(x=x, y=y, z1=z1, z2=z2) + + # test evaluation at x=2 and y=3, result: z1=1.73, z2=9 + x = 2 + y = 3 + z1, z2 = map.evaluate(x=x, y=y) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 1.73, but is " + str(round(z1, 2)) + ".") + eq_(1.73, round(z1, 2), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 9.0, but is " + str(round(z2, 1)) + ".") + eq_(9.0, round(z2, 1), msg) + + # test evaluation at x=0 and y=0 for lower value range limit, + # result: z1=1, z2=1 + x = 0 + y = 0 + z1, z2 = map.evaluate(x=x, y=y) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 1.0, but is " + str(round(z1, 1)) + ".") + eq_(1.0, round(z1, 1), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 1.0, but is " + str(round(z2, 1)) + ".") + eq_(1.0, round(z2, 1), msg) + + # test evaluation at x=4 and y=6 for upper value range limit, + # result: z1=2.24, z2=25 + x = 4 + y = 6 + z1, z2 = map.evaluate(x=x, y=y) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 2.24, but is " + str(round(z1, 2)) + ".") + eq_(2.24, round(z1, 2), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 25.0, but is " + str(round(z2, 1)) + ".") + eq_(25.0, round(z2, 1), msg) + + # check, if bound errors go through + map.get_bound_errors(x, y, 'Componentlabel') + + +def test_compressor_map_evaluation(): + """Test the characteristc compressor map evaluation.""" + + # create a characteristc line with values of y=(x-2)^2 + x = [1, 2, 3] + y = np.array([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + z1 = y ** 0.5 + z2 = y ** 2 + map = compressor_map(x=x, y=y, z1=z1, z2=z2) + igva = 20 + + # test evaluation at x=2 and y=3, result: z1=1.55, z2=13.7 + # f_igva1 = (1 - igva / 100) + # --> = 0.8 + # f_igva2 = (1 - igva ** 2 / 10000) + # --> = 0.96 + # --> yarr = yarr * f_igva + # --> = [1.6, 2.4, 3.2] with y=3 + # --> z1, z2 between second and third value at 0.75 (3-2.4)/(3.2-2.4) + # --> z1 0.75 * (2 - 3 ** 0.5) * 0.8 + 3 ** 0.5 * 0.8 + # --> z2 0.75 * (16 - 9) * 0.96 + 9 * 0.96 + x = 2 + y = 3 + z1, z2 = map.evaluate(x=x, y=y, igva=igva) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 1.55, but is " + str(round(z1, 2)) + ".") + eq_(1.55, round(z1, 2), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 13.7, but is " + str(round(z2, 1)) + ".") + eq_(13.68, round(z2, 2), msg) + + # test evaluation at x=0 and y=0 for lower value range limit, + # result: z1=0.8, z2=0.96 + x = 0 + y = 0 + z1, z2 = map.evaluate(x=x, y=y, igva=igva) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 0.8, but is " + str(round(z1, 1)) + ".") + eq_(0.8, round(z1, 1), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 0.96, but is " + str(round(z2, 1)) + ".") + eq_(0.96, round(z2, 2), msg) + + # test evaluation at x=4 and y=6 for upper value range limit, + # result: z1=1.79, z2=24 + x = 4 + y = 6 + z1, z2 = map.evaluate(x=x, y=y, igva=igva) + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z1 " + "must be 1.79, but is " + str(round(z1, 2)) + ".") + eq_(1.79, round(z1, 2), msg) + + msg = ("The evaluation of x=" + str(x) + " and y=" + str(y) + " for z2 " + "must be 24.0, but is " + str(round(z2, 1)) + ".") + eq_(24.0, round(z2, 1), msg)