diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index f9a6e63e0..000000000 --- a/.coveragerc +++ /dev/null @@ -1,14 +0,0 @@ -[paths] -source = - src - -[run] -branch = true -source = - src -parallel = true - -[report] -show_missing = true -precision = 2 -omit = *migrations* diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 81c00649f..aba1d8300 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -26,9 +26,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools setuptools_scm twine wheel + python -m pip install --upgrade pip setuptools setuptools_scm twine wheel flit - name: Create packages - run: python setup.py sdist bdist_wheel + run: python -m flit build - name: Run twine check run: twine check dist/* - uses: actions/upload-artifact@v2 diff --git a/.github/workflows/tox_checks.yml b/.github/workflows/tox_checks.yml index 4ce7e6d28..5cd8e4117 100644 --- a/.github/workflows/tox_checks.yml +++ b/.github/workflows/tox_checks.yml @@ -39,7 +39,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.toxenv }}-${{ hashFiles('tox.ini', 'setup.py') }} + key: ${{ runner.os }}-pip-${{ matrix.toxenv }}-${{ hashFiles('tox.ini', 'pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.toxenv }}- ${{ runner.os }}-pip- diff --git a/.github/workflows/tox_pytest.yml b/.github/workflows/tox_pytest.yml index f44bd6c74..ba825bd49 100644 --- a/.github/workflows/tox_pytest.yml +++ b/.github/workflows/tox_pytest.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v1 diff --git a/LICENSE b/LICENSE index 67fb8fdb0..f5d7fda65 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2022, oemof developer group +Copyright (c) 2017-2023 Francesco Witte Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 001eda200..affbd4c1a 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,8 @@ Thermal Engineering Systems in Python TESPy stands for "Thermal Engineering Systems in Python" and provides a powerful simulation toolkit for thermal engineering plants such as power plants, district heating systems or heat pumps. It is an external extension -module within the `Open Energy Modelling Framework `_ and -can be used as a standalone package. +module within the Open Energy Modelling Framework `oemof `_ +and can be used as a standalone package. .. figure:: https://raw.githubusercontent.com/oemof/tespy/9915f013c40fe418947a6e4c1fd0cd0eba45893c/docs/api/_images/logo_tespy_big.svg :align: center @@ -20,8 +20,8 @@ exchangers, drum). Everybody is welcome to use and/or develop TESPy. Contribution is already possible on a low level by simply fixing typos in TESPy's documentation or rephrasing sections which are unclear. If you want to support us that way -please fork the TESPy repository to your own github account and make changes -as described in the github guidelines: +please fork the TESPy repository to your own GitHub account and make changes +as described in the GitHub guidelines: https://guides.github.com/activities/hello-world/ Key Features @@ -130,7 +130,7 @@ We have decided to start a reoccurring "Stammtisch" meeting for all interested TESPy users and (potential) developers. You are invited to join us on every 3rd Monday of a month at 17:00 CE(S)T for a casual get together. The first meeting will be held at June, 20, 2022. The intent of this meeting is to establish a -more active and well connected network of TESPy users and developers. +more active and well-connected network of TESPy users and developers. If you are interested, you can simply join the meeting at https://meet.jit.si/tespy_user_meeting. We are looking forward to seeing you! @@ -153,7 +153,7 @@ repository. They are included in the "tutorial" directory. Citation ======== The scope and functionalities of TESPy have been documented in a paper -published in the Journal of Open Source Software with an OpenAccess license. +published in the Journal of Open Source Software with an Open-Access license. Download the paper from https://doi.org/10.21105/joss.02178. As TESPy is a free software, we kindly ask that you add a reference to TESPy if you use the software for your scientific work. Please cite the article with the BibTeX @@ -200,7 +200,7 @@ zenodo. Find your version here: https://doi.org/10.5281/zenodo.2555866. License ======= -Copyright (c) 2017-2022 oemof developer group +Copyright (c) 2017-2023 Francesco Witte Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/_static/images/logo_tespy_mini.svg b/docs/_static/images/logo_tespy_mini.svg new file mode 100644 index 000000000..118a5f1f4 --- /dev/null +++ b/docs/_static/images/logo_tespy_mini.svg @@ -0,0 +1,426 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/docs/_static/js/custom.js b/docs/_static/js/custom.js new file mode 100644 index 000000000..5db6dfeff --- /dev/null +++ b/docs/_static/js/custom.js @@ -0,0 +1,20 @@ +function removeAnnouncementBanner() { + var banner = document.getElementsByClassName("announcement") + banner[0].remove() +}; + + +window.addEventListener('load', async () => { + const elmnt = document.querySelector('div[oemof-announcement]') + const response = await fetch(elmnt.getAttribute('oemof-announcement')) + if (response.status === 200) { + elmnt.innerHTML = ( + await response.text() + ) + if (elmnt.innerHTML.length === 0) { + removeAnnouncementBanner() + } + } else if (response.status === 404) { + removeAnnouncementBanner() + } +}) \ No newline at end of file diff --git a/docs/advanced/exergy.rst b/docs/advanced/exergy.rst index 56789f248..490ed6554 100644 --- a/docs/advanced/exergy.rst +++ b/docs/advanced/exergy.rst @@ -22,7 +22,7 @@ thermodynamics, the conversion of heat and internal energy into work is limited. This constraint and the idea of destruction are applied to introduce a new concept: "Exergy". -Exergy can be destroyed due to irreversibilities and is able to describe the +Exergy can be destroyed due to irreversibility and is able to describe the quality of different energy forms. The difference in quality of different forms of energy shall be illustrated by the following example. 1 kJ of electrical energy is clearly more valuable than 1 kJ of energy in a glass of water at @@ -92,12 +92,12 @@ potential exergy are neglected and therefore not considered as well. * - :code:`E_D` - exergy destruction - :math:`\dot{E}_\mathrm{D}` - - thermodynamic inefficienies associated with the irreversibilities + - thermodynamic inefficiencies associated with the irreversibility (entropy generation) within the system boundaries * - :code:`E_L` - exergy loss - :math:`\dot{E}_\mathrm{L}` - - thermodynamic inefficienies associated with the transfer of exergy + - thermodynamic inefficiencies associated with the transfer of exergy through material and energy streams to the surroundings * - :code:`epsilon` - exergetic efficiency @@ -122,7 +122,7 @@ potential exergy are neglected and therefore not considered as well. Tutorial ======== -In this short tutorial, an exergy analysis is carried out for the so called +In this short tutorial, an exergy analysis is carried out for the so-called "Solar Energy Generating System" (SEGS). The full python script is available on GitHub in an individual repository: https://github.com/fwitte/SEGS_exergy. @@ -269,7 +269,7 @@ the API documentation of class :py:class:`tespy.tools.analyses.ExergyAnalysis`. After the setup of the exergy analysis, the :py:meth:`tespy.tools.analyses.ExergyAnalysis.analyse` method expects the definition of the ambient state, thus ambient temperature and ambient pressure. -With these information, the analysis is carried out automatically. The value +With this information, the analysis is carried out automatically. The value of the ambient conditions is passed in the network's (:code:`nw`) corresponding units. @@ -300,7 +300,7 @@ destruction on the respective busses is calculated. On top of that, fuel and product exergy values as well as exergy loss are determined. The total exergy destruction must therefore be equal to the fuel exergy minus product exergy and minus exergy loss. The deviation of that equation is then calculated and -checked versus a threshold value of :math:`10^{-3}` (to componesate for +checked versus a threshold value of :math:`10^{-3}` (to compensate for rounding errors). .. math:: @@ -346,10 +346,10 @@ deselect the tables, e.g. by passing :code:`groups=False` to the method call. ean.print_results(groups=False, connections=False) For the component related tables, i.e. busses, components, aggregation and -groups, the data are sorted in descending order for the given exergy destruction value -of the individual entry. The component data contain fuel exergy, product exergy -and exergy destruction values related to the component itself ignoring losses -that might occur on the busses, for example, mechanical or electrical +groups, the data are sorted in descending order for the given exergy destruction +value of the individual entry. The component data contain fuel exergy, product +exergy and exergy destruction values related to the component itself ignoring +losses that might occur on the busses, for example, mechanical or electrical conversion losses in motors and generators. The bus data contain the respective information related to the conversion losses on the busses only. The aggregation data contain both, the component and the bus data. For instance, @@ -362,7 +362,7 @@ exergy, while the product is the electrical energy. .. note:: Please note, that in contrast to the component and bus data, group data do - not contain fuel and product exergy as well as exergy efficiency. Instead all + not contain fuel and product exergy as well as exergy efficiency. Instead, all exergy streams entering the system borders of the component group and all exergy streams leaving the system borders are calculated. On this basis, a graphical representation of the exergy flows in the network can be generated @@ -434,7 +434,7 @@ exclude relatively small values from display. The coloring of the links is defined by the type of the exergy stream (bound to a specific fluid, fuel exergy, product exergy, exergy loss, exergy -destruction or internal exergy streams not bound to mass flows). Therefore +destruction or internal exergy streams not bound to mass flows). Therefore, colors can be assigned to these types of streams. .. note:: diff --git a/docs/api.rst b/docs/api.rst index 6a2d34f7f..aaa4f0e3b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,7 +6,7 @@ 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) :cite:`Epple2017` (english). Equations and properties from other +(German) :cite:`Epple2017` (English). Equations and properties from other sources are cited individually. .. toctree:: diff --git a/docs/api/components.rst b/docs/api/components.rst index c05f296f3..a2546b671 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -78,14 +78,6 @@ tespy.components.combustion.engine module :undoc-members: :show-inheritance: -tespy.components.customs.orc_evaporator module ----------------------------------------------- - -.. automodule:: tespy.components.customs.orc_evaporator - :members: - :undoc-members: - :show-inheritance: - tespy.components.heat_exchangers.condenser module ------------------------------------------------- diff --git a/docs/api/tools.rst b/docs/api/tools.rst index f76f92c95..032babac2 100644 --- a/docs/api/tools.rst +++ b/docs/api/tools.rst @@ -38,10 +38,34 @@ tespy.tools.document_models module :undoc-members: :show-inheritance: -tespy.tools.fluid_properties module ------------------------------------ +tespy.tools.fluid_properties.functions module +--------------------------------------------- -.. automodule:: tespy.tools.fluid_properties +.. automodule:: tespy.tools.fluid_properties.functions + :members: + :undoc-members: + :show-inheritance: + +tespy.tools.fluid_properties.helpers module +------------------------------------------- + +.. automodule:: tespy.tools.fluid_properties.helpers + :members: + :undoc-members: + :show-inheritance: + +tespy.tools.fluid_properties.mixtures module +-------------------------------------------- + +.. automodule:: tespy.tools.fluid_properties.mixtures + :members: + :undoc-members: + :show-inheritance: + +tespy.tools.fluid_properties.wrappers module +-------------------------------------------- + +.. automodule:: tespy.tools.fluid_properties.wrappers :members: :undoc-members: :show-inheritance: diff --git a/docs/basics/district_heating.rst b/docs/basics/district_heating.rst index 3899c427f..964ac1909 100644 --- a/docs/basics/district_heating.rst +++ b/docs/basics/district_heating.rst @@ -30,11 +30,10 @@ Setting up the System For this model we have to import the :code:`Network` and :code:`Connection` classes as well as the respective components. After setting up the network we can create the components, connect them to the network (as shown in the other) -examples. As a fluid, we will use the incompressibles back-end of CoolProp, -since we only need liquid water. The incompressible back-end has much higher +examples. As a fluid, we will use the incompressibles back end of CoolProp, +since we only need liquid water. The incompressible back end has much higher access speed while preserving high accuracy. - .. tip:: For more information on the fluid properties in TESPy, @@ -107,7 +106,7 @@ Next, we want to investigate what happens, in case the - heat load varies. - overall temperature level in the heating system is reduced. -To do that, we will use similar setups as show in the rankine cycle +To do that, we will use similar setups as show in the Rankine cycle introduction. The :code:`KA` value of both pipes is assumed to be fixed, the efficiency of the pump and pressure losses in consumer and heat source are constant as well. diff --git a/docs/basics/gas_turbine.rst b/docs/basics/gas_turbine.rst index e0a7b3ef5..42e5d66fa 100644 --- a/docs/basics/gas_turbine.rst +++ b/docs/basics/gas_turbine.rst @@ -21,7 +21,7 @@ This tutorial introduces a new component, the combustion chamber. You will learn how to use the component and set up a simple open cycle gas turbine: It compresses air and burns fuel in the combustion chamber. The hot and pressurized flue gas expands in the turbine, which drives the compressor and -the generator. You will also learn, how to use the fluid compositon as a +the generator. You will also learn, how to use the fluid composition as a variable in your simulation. Download the full script here: @@ -51,10 +51,7 @@ documentation. In this tutorial, we will use the :py:class:`tespy.components.combustion.diabatic.DiabaticCombustionChamber`. -First, we set up a network and the components. The network's fluid list must -contain all fluid components used for the combustion chamber. **These are at** -**least the fuel, oxygen, carbon-dioxide and water**. For this example we -add Nitrogen, since it is the most important fresh air component. +First, we set up a network and the components. .. literalinclude:: /../tutorial/basics/gas_turbine.py :language: python @@ -70,8 +67,8 @@ combustion chamber is directly connected to the flue gas sink. :start-after: [sec_2] :end-before: [sec_3] -There are many different specifications possible. For the combustion chamber -we will specify its air to stoichiometric air ratio lamb and the thermal input +There are many specifications possible. For the combustion chamber we will +specify its air to stoichiometric air ratio lamb and the thermal input (:math:`LHV \cdot \dot{m}_{f}`). Furthermore, we specify the efficiency :code:`eta` of the component, which @@ -85,9 +82,9 @@ parameter specification. In this example, we set it directly. Initially, we assume adiabatic behavior :code:`eta=1` and no pressure losses :code:`pr=1`. The ambient conditions as well as the fuel gas inlet temperature are defined -in the next step. The full vector for the air and the fuel gas composition -have to be defined. The component can not handle "Air" as input fluid. We can -run the code after the specifications. +in the next step. The vectors for the air and the fuel gas composition have to +be specified using the individual fluids. The component can not handle `"Air"` +as input fluid. We can run the code after the specifications. .. literalinclude:: /../tutorial/basics/gas_turbine.py :language: python @@ -148,17 +145,19 @@ generator, assuming 98 % mechanical-electrical efficiency. Since we deleted the connection 2 and 3, all specifications for those connections have to be added again. The air fluid composition is specified on connection 1 with ambient pressure and temperature. The compressor pressure -ratio is set to 15 bar, the turbine inlet temperature to 1200 °C. Finally, set -the gas turbine outlet pressure to ambient pressure as well as the -compressor's and turbine's efficiency. +ratio is set to 15 bar. Finally, set the gas turbine outlet pressure to ambient +pressure as well as the compressor's and turbine's efficiency. We start with +simulation which specifies a fixed value for the flue gas mass flow to generate +good starting values. After that, the turbine inlet temperature is set to +1200 °C. .. literalinclude:: /../tutorial/basics/gas_turbine.py :language: python :start-after: [sec_9] :end-before: [sec_10] -Note, that the pressure of the fuel is lower than the pressure of the air at -the combustion chamber as we did not change the pressure of connection 5. A +Note, that the pressure of the fuel is lower than the pressure of the air at the +combustion chamber as we did not change the pressure of connection 5. A respective warning is printed after the calculation. We can fix it like so: .. literalinclude:: /../tutorial/basics/gas_turbine.py @@ -238,7 +237,7 @@ hydrogen and methane. With this setup, a thermal input below the lower heating value of methane or above the lower heating value of hydrogen (each multiplied with the mass flow of 1 kg/s) does not make sense as input specification. This is - individual of every fluid you use as fuel and you cannot easily abstract + individual of every fluid you use as fuel, and you cannot easily abstract the values to any other combination. .. dropdown:: Click to expand to code section diff --git a/docs/basics/heat_pump.rst b/docs/basics/heat_pump.rst index 5c3a1f5ac..0c9d40cc5 100644 --- a/docs/basics/heat_pump.rst +++ b/docs/basics/heat_pump.rst @@ -29,8 +29,8 @@ Download the full script here: Flexibility in Modeling ^^^^^^^^^^^^^^^^^^^^^^^ In TESPy the specifications for components and/or connections are -interchangable in every possible way, provided that the system of equations -representing the plant is well defined. +interchangeable in every possible way, provided that the system of equations +representing the plant is well-defined. For example, instead of the heat provided by the condenser we could specify the mass flow :code:`m` of the refrigerant. To unset a parameter you need to @@ -46,7 +46,7 @@ You can observe, that the heat transferred by the condenser now is a result of the mass flow specified. We could do similar things, for example with the heat sink temperature. We specified it in our initial set up. Now we want to insert a compressor with a fixed output to input pressure ratio. In that case, we -cannot choose the condensation temperature but it will be a result of that +cannot choose the condensation temperature, but it will be a result of that specification: .. literalinclude:: /../tutorial/basics/heat_pump.py @@ -65,7 +65,7 @@ compressor would be, in case we measure :code:`T=97.3` at connection 3. Typical Errors ^^^^^^^^^^^^^^ -If you over- or underdetermine the system by specifying too few or too many +If you over or under determine the system by specifying too few or too many parameters, you will get an error message. We could set the heat demand and the mass flow at the same time. @@ -168,4 +168,4 @@ plot using matplotlib. The figure shows the results of the COP analysis. The base case is at an evaporation temperature of 20 °C, the condensation temperature at 80 °C and the -isentropic effficiency of the compressor at 85 %. +isentropic efficiency of the compressor at 85 %. diff --git a/docs/basics/intro.rst b/docs/basics/intro.rst index 063081f8c..205957c17 100644 --- a/docs/basics/intro.rst +++ b/docs/basics/intro.rst @@ -19,19 +19,15 @@ Set up a plant In order to simulate a plant we start by creating the network (:py:class:`tespy.networks.network.Network`). The network is the main container -for the model. You need to specify a list of the fluids you require for the -calculation in your plant. For more information on the fluid properties go to -the :ref:`corresponding section ` in the -documentation. +for the model. .. literalinclude:: /../tutorial/basics/heat_pump.py :language: python :start-after: [sec_1] :end-before: [sec_2] -On top of that, it is possible to specify a unit system and value ranges for -the network's variables. If you do not specify these, TESPy will use SI-units. -We will thus only specify the unit systems, in this case. +It is possible to specify a unit system and value ranges for the network's +variables. If you do not specify these, TESPy will use SI-units. .. literalinclude:: /../tutorial/basics/heat_pump.py :language: python @@ -50,7 +46,7 @@ to specify parameters for the component, for example power :math:`P` for a pump or upper terminal temperature difference :math:`ttd_\mathrm{u}` of a heat exchanger. The full list of parameters for a specific component is stated in the respective class documentation. This example uses a compressor, a control -valve two (simple) heat exchangers and a so called cycle closer. +valve two (simple) heat exchangers and a so-called cycle closer. .. note:: @@ -76,8 +72,8 @@ Establish connections Connections are used to link two components (outlet of component 1 to inlet of component 2: source to target). If two components are connected with each other the fluid properties at the source will be equal to the properties at the -target. It is possible to set the properties on each connection in a similar -way as parameters are set for components. The basic specification options are: +target. It is possible to set the properties on each connection similarly as +parameters are set for components. The basic specification options are: * mass flow (m) * volumetric flow (v) @@ -106,13 +102,13 @@ we do not need to pass the components to the network. .. note:: The :code:`CycleCloser` is a necessary component when working with closed - cycles, because a system would always be overdetermined, if, for example, + cycles, because a system would always be over determined, if, for example, a mass flow is specified at some point within the cycle. It would propagate - through all of the components, since they have an equality constraint for - the mass flow at their inlet and their outlet. With the example here, that - would mean: **Without the cycle closer** specification of massflow at an - connection would lead to the following set of equations for massflow, which - is an overdetermination: + through all the components, since they have an equality constraint for the + mass flow at their inlet and their outlet. With the example here, that would + mean: **Without the cycle closer** specification of mass flow at a + connection would lead to the following set of equations for mass flow, which + is an over determination: .. math:: @@ -136,7 +132,7 @@ compressor. On top of that, the heat production of the heat pump can be set with :code:`Q` for the condenser. Since we are working in **subcritical** regime in this tutorial, we set the state of the fluid at the evaporator's outlet to fully saturated steam (:code:`x=1`) and at the condenser's outlet to -fully saturated liqud (:code:`x=0`). On top of that, we want to set the +fully saturated liquid (:code:`x=0`). On top of that, we want to set the condensation and the evaporation temperature levels. Last, we have to specify the fluid vector at one point in our network. @@ -166,16 +162,16 @@ with the respective component parameters. Next steps ---------- -We highly recommend to check our other basic model examples on how to set up +We highly recommend checking our other basic model examples on how to set up different standard thermodynamic cycles in TESPy. The heat pump cycle in that -section builds on this heat pump. We will introduce couple of different inputs +section builds on this heat pump. We will introduce a couple of different inputs and show, how to change the working fluid. The other tutorials show the usage of more components, for example the combustion chamber and the turbine or a condenser including the cooling water side of the system. In the more advanced tutorials, you will learn, how to set up more complex -plants ste by step, make a design calculation of the plant as well as calculate -offdesign/partload performance. +plants step by step, make a design calculation of the plant as well as calculate +offdesign/part load performance. In order to get a good overview of the TESPy functionalities, the sections on the :ref:`TESPy modules ` will guide you in detail. diff --git a/docs/basics/rankine_cycle.rst b/docs/basics/rankine_cycle.rst index d412e6aa4..9ac3c31e5 100644 --- a/docs/basics/rankine_cycle.rst +++ b/docs/basics/rankine_cycle.rst @@ -161,12 +161,12 @@ can disable the printout of the convergence history. Figure: Parametric analysis of the efficiency and power output -Partload Simulation -^^^^^^^^^^^^^^^^^^^ -In the partload simulation part, we are starting with a specific design of the -plant and calculate the partload perfomance with some assumptions on the +Part load Simulation +^^^^^^^^^^^^^^^^^^^^ +In the part load simulation part, we are starting with a specific design of the +plant and calculate the part load performance with some assumptions on the component's individual behavior. The table below summarizes the assumptions, -which we will keep as simple as possible in this moment. For more insights +which we will keep as simple as possible at this moment. For more insights have a look at the step by step :ref:`heat pump tutorial ` or at the :ref:`Network documentation `. @@ -189,7 +189,7 @@ With these specifications, the following physics are applied to the model: - Due to the constant volumetric flow of water, the temperature of the cooling water returning from the condenser will react to the total heat transferred - in the condensation: Increased heat transfer means incresing temperature, + in the condensation: Increased heat transfer means increasing temperature, decreased heat transfer means decreased temperature. - The constant heat transfer coefficient of the condenser will calculate the condensation temperature (and therefore pressure) based on the temperature @@ -239,14 +239,14 @@ Finally, we can alter the mass flow from its design value of 20 kg/s to only .. figure:: /_static/images/basics/rankine_partload.svg :align: center - :alt: Partload electric efficiency of the rankine cycle + :alt: Part load electric efficiency of the Rankine cycle :figclass: only-light - Figure: Partload electric efficiency of the rankine cycle + Figure: Part load electric efficiency of the Rankine cycle .. figure:: /_static/images/basics/rankine_partload_darkmode.svg :align: center - :alt: Partload electric efficiency of the rankine cycle + :alt: Part load electric efficiency of the Rankine cycle :figclass: only-dark - Figure: Partload electric efficiency of the rankine cycle + Figure: Part load electric efficiency of the Rankine cycle diff --git a/docs/benchmarks.rst b/docs/benchmarks.rst index 74c7f3af4..3d5c76d30 100644 --- a/docs/benchmarks.rst +++ b/docs/benchmarks.rst @@ -8,7 +8,7 @@ Model Validation TESPy has been used to model several research and engineering applications. In the paper on integration of generic exergy analysis in TESPy :cite:`Witte2022` three models have been built from literature sources: A -solar thermal power plant, a supercritical CO2 brayton cycle as well as a +solar thermal power plant, a supercritical CO2 Brayton cycle as well as a refrigeration machine using air as working fluid. For the solar thermal power plant we have created a full model of the plant @@ -16,7 +16,7 @@ using a standard industry software in parallel. **The comparison showed** **identical results**. For the other two applications we have compared the results of the TESPy model with the data published in the respective research paper and found very well matching results. Differences can be explained by -different implementations of the fluid property back-end. +different implementations of the fluid property back end. Finally, in the extension of the exergy analysis to chemical exergy :cite:`Hofmann2022` we have also compared results of the CGAM process diff --git a/docs/conf.py b/docs/conf.py index 9311251ec..574b0c293 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -118,7 +118,16 @@ html_theme_options = { "light_logo": "./images/logo_tespy_mid.svg", "dark_logo": "./images/logo_tespy_mid_darkmode.svg", + "announcement": """ +
+ """, } + +html_js_files = [ + 'js/custom.js', +] + + html_favicon = './_static/images/logo_tespy_small.svg' napoleon_use_ivar = True diff --git a/docs/development/how.rst b/docs/development/how.rst index 1d0647d3e..732629929 100644 --- a/docs/development/how.rst +++ b/docs/development/how.rst @@ -12,7 +12,7 @@ Install the developer version It is recommenden to use `virtual environments `_ for -the development process. Fork the repository and clone your forked tespy github +the development process. Fork the repository and clone your forked tespy GitHub repository and install development requirements with pip. .. code:: bash @@ -33,7 +33,7 @@ the example below). git fetch upstream git pull upstream dev --rebase -Use the :code:`--rebase` comand to avoid merge commits fo every upstream pull. +Use the :code:`--rebase` command to avoid merge commits for every upstream pull. If you want to make changes to tespy, checkout a new branch from your local dev branch. Make your changes, commit them and create a PR on the oemof/tespy dev branch. @@ -41,13 +41,13 @@ branch. Collaboration with pull requests -------------------------------- -To collaborate use the pull request functionality of github as described here: +To collaborate use the pull request functionality of GitHub as described here: https://guides.github.com/activities/hello-world/ How to create a pull request ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* Fork the oemof repository to your own github account. +* Fork the oemof repository to your own GitHub account. * Change, add or remove code. * Commit your changes. * Create a pull request and describe what you will do and why. Please use the @@ -79,7 +79,7 @@ The tests in TESPy are split up in two different parts: The tests contain code examples that expect a certain outcome. If the outcome is as expected a test will pass, if the outcome is different, the test will -fail. You can run the tests locally by navigating into your local github clone. +fail. You can run the tests locally by navigating into your local GitHub clone. The command :code:`check` tests PEP guidelines, the command :code:`docs` tests building the documentation, and the command :code:`py3X` runs the software tests in the selected Python version. diff --git a/docs/development/what.rst b/docs/development/what.rst index 1e60844cb..b7fcab7c4 100644 --- a/docs/development/what.rst +++ b/docs/development/what.rst @@ -3,12 +3,13 @@ What can I contribute? ====================== TESPy has been developed mainly by Francesco Witte at the University of Applied -Sciences Flensburg - and a lot of free time. We hope that many people can make -use of this project and that it will be a community driven project in the -future, as users might demand special components or flexible implementations of -characteristics, custom equations, basically what ever you can think of. +Sciences Flensburg - and since 2021 with a lot of free time. The goal is, that +many people can make use of this project and that it will be a community driven +project in the future, as users might demand special components or flexible +implementations of characteristics, custom equations, basically what ever you +can think of. -Therefore we would like to invite you to contribute in this process, share your +Therefore, I would like to invite you to contribute in this process, share your ideas and experience and maybe start developing the software. Your solutions may help other users as well. Contributing to the development of TESPy is easy and will help the development team and all other users of the software. If you @@ -23,7 +24,7 @@ Contribute to the documentation The easiest way of joining the developing process is by improving the documentation. If you spot mistakes or think, the documentation could be more precise or clear in some sections, feel free to fix it and create a pull -request on the github repository. +request on the GitHub repository. If you come across typos or grammatical mistakes or want to improve comprehensibility of the documentation, make your adjustments or suggestions @@ -34,14 +35,21 @@ Share your projects You have used the software in your research paper or project, maybe even in a real world application? We would love to feature your project on our :ref:`Example Applications ` page. Please reach out to -us by opening a new issue on our github page. +us by opening a new issue on our GitHub page. Add new component equations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The components equations represent the behavior of each component. Do you miss -equations? Open a discussion on the github discussions page or add them to your +equations? Open a discussion on the GitHub discussions page or add them to your fork of TESPy and create a pull request on the dev branch. +Sponsor the development +^^^^^^^^^^^^^^^^^^^^^^^ +As mentioned above, I maintain and develop this project in my free time. If you +would like to support me by sponsoring a coffee for late night sessions or need +direct support for your projects using TESPy, you sponsor me on GitHub +https://github.com/fwitte or reach out to me via e-mail. + Add component characteristics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Another highly appreciated way for you to contribute is the provision of new @@ -52,5 +60,5 @@ coefficients etc.. The component characteristics represent large added value for your calculation. If you have detailed information on components offdesign behavior - even for specific cases - it will improve the results. Every user can benefit from this -knowledge and thus we are very happy to discuss about the implementation of new +knowledge, and thus we are very happy to discuss the implementation of new characteristics. diff --git a/docs/examples.rst b/docs/examples.rst index f3bf21098..7c5156fc5 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -59,7 +59,7 @@ your project. :class: only-dark :target: https://github.com/fwitte/ORCSimulator - An ORC power plant using two-phase geothermal sources is designed and an + An ORC power plant using two-phase geothermal sources is designed, and an optimization is carried out. The plant's performance is investigated for six different working fluids. Gross and net power output are optimized. The open source library pygmo :cite:`Biscani2020` is applied in combination diff --git a/docs/introduction.rst b/docs/introduction.rst index 8544816f4..e5d67f274 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -5,8 +5,8 @@ Thermal Engineering Systems in Python TESPy stands for "Thermal Engineering Systems in Python" and provides a powerful simulation toolkit for thermal engineering plants such as power plants, district heating systems or heat pumps. It is an external extension -module within the `Open Energy Modeling Framework `_ and -can be used as a standalone package. +module within the Open Energy Modeling Framework `oemof `_ +and can be used as a standalone package. .. image:: /_static/images/logo_tespy_big.svg :align: center @@ -28,8 +28,8 @@ mixers and splitters as well as some advanced components Everybody is welcome to use and/or develop TESPy. Contribution is already possible on a low level by simply fixing typos in TESPy's documentation or rephrasing sections which are unclear. If you want to support us that way -please fork the TESPy repository to your own github account and make -changes as described in the github guidelines: +please fork the TESPy repository to your own GitHub account and make +changes as described in the GitHub guidelines: https://guides.github.com/activities/hello-world/ Key Features diff --git a/docs/modules/characteristics.rst b/docs/modules/characteristics.rst index 22130803b..476c5641c 100644 --- a/docs/modules/characteristics.rst +++ b/docs/modules/characteristics.rst @@ -24,10 +24,10 @@ the corresponding y-values. 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 +minimum value of the characteristic line the y-value corresponding to 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 most value pairs respectively. +:code:`True` linear extrapolation is performed using the two lowermost or +uppermost value pairs respectively. Characteristic maps ------------------- diff --git a/docs/modules/components.rst b/docs/modules/components.rst index 24ebf7ad7..fe1116ec2 100644 --- a/docs/modules/components.rst +++ b/docs/modules/components.rst @@ -195,7 +195,8 @@ specified range. # benefit: specification of bounds will increase stability my_pipe.set_attr(D={ 'val': 0.2, 'is_set': True, 'is_var': True, - 'min_val': 0.1, 'max_val': 0.3}) + 'min_val': 0.1, 'max_val': 0.3} + ) .. _component_characteristic_specification_label: @@ -257,7 +258,8 @@ For example, :code:`kA_char` specification for heat exchangers: # flow for kA_char1 and volumetric flow for kA_char2 he.set_attr( kA_char1={'char_func': kA_char1, 'param': 'm'}, - kA_char2={'char_func': kA_char2, 'param': 'v'}) + kA_char2={'char_func': kA_char2, 'param': 'v'} + ) # or use custom values for the characteristic line e.g. kA vs volumetric # flow @@ -276,8 +278,7 @@ Full working example for :code:`eta_s_char` specification of a turbine. from tespy.tools.characteristics import CharLine import numpy as np - fluid_list = ['water'] - nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', h_unit='kJ / kg') + nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg') si = Sink('sink') so = Source('source') t = Turbine('turbine') @@ -393,14 +394,14 @@ Extend components with new equations You can easily add custom equations to the existing components. In order to do this, you need to implement four changes to the desired component class: -- modify the :code:`get_variables(self)` method. +- modify the :code:`get_parameters(self)` method. - add a method, that returns the result of your equation. -- add a method, that places the partial derivatives in the jacobian matrix of +- add a method, that places the partial derivatives in the Jacobian matrix of your component. - add a method, that returns the LaTeX code of your equation for the automatic documentation feature. -In the :code:`get_variables(self)` method, add an entry for your new equation. +In the :code:`get_parameters(self)` method, add an entry for your new equation. If the equation uses a single parameter, use the :code:`ComponentProperties` type DataContainer (or the :code:`ComponentCharacteristics` type in case you only apply a characteristic curve). If your equations requires multiple @@ -428,7 +429,6 @@ should be applied for the corresponding purpose. For more information on defining the equations, derivatives and the LaTeX equation you will find the information in the next section on custom components. - Custom components ----------------- @@ -440,7 +440,7 @@ custom component. Now create a class for your component and at least add the following methods. - :code:`component(self)`, -- :code:`get_variables(self)`, +- :code:`get_parameters(self)`, - :code:`get_mandatory_constraints(self)`, - :code:`inlets(self)`, - :code:`outlets(self)` and @@ -454,7 +454,7 @@ The starting lines of your file should look like this: from tespy.tools import ComponentCharacteristics as dc_cc from tespy.tools import ComponentProperties as dc_cp - class my_custom_component(Component): + class MyCustomComponent(Component): """ This is a custom component. @@ -486,11 +486,6 @@ For example, the mandatory equations of a valve look are the following: .. math:: - 0=\dot{m}_{\mathrm{in,1}}-\dot{m}_{\mathrm{out,1}} - - 0=x_{fl\mathrm{,in,1}}-x_{fl\mathrm{,out,1}}\;\forall fl - \in\text{network fluids} - 0=h_{\mathrm{in,1}}-h_{\mathrm{out,1}} The corresponding method looks like this. The equations, derivatives and @@ -501,14 +496,6 @@ LaTeX string generation are individual methods you need to define def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 1}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids}, 'enthalpy_equality_constraints': { 'func': self.enthalpy_equality_func, 'deriv': self.enthalpy_equality_deriv, @@ -524,7 +511,7 @@ LaTeX string generation are individual methods you need to define Attributes ^^^^^^^^^^ -The :code:`get_variables()` method must return a dictionary with the attributes +The :code:`get_parameters()` method must return a dictionary with the attributes you want to use for your component. The keys represent the attributes and the respective values the type of data container used for this attribute. By using the data container attributes, it is possible to add defaults. Defaults for @@ -539,7 +526,7 @@ DataContainers instead of dictionaries, e.g. for the Valve: .. code:: python - def get_variables(self): + def get_parameters(self): return { 'pr': dc_cp( min_val=1e-4, max_val=1, num_eq=1, @@ -656,34 +643,43 @@ equation, too. The Valve's dp_char parameter methods are the following. k : int Position of derivatives in Jacobian matrix (k-th equation). """ - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv( - self.dp_char_func, 'm', 0) + f = self.dp_char_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) if self.dp_char.param == 'v': - self.jacobian[k, 0, 1] = self.numeric_deriv( - self.dp_char_func, 'p', 0) - self.jacobian[k, 0, 2] = self.numeric_deriv( - self.dp_char_func, 'h', 0) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv( + self.dp_char_func, 'p', i + ) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv( + self.dp_char_func, 'h', i + ) else: - self.jacobian[k, 0, 1] = 1 + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = 1 - self.jacobian[k, 1, 1] = -1 + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -1 -For the derivative, the partial derivatives to all variables of the network +For the Jacobian, the partial derivatives to all variables of the network are required. This means, that you have to calculate the partial derivatives to mass flow, pressure, enthalpy and all fluids in the fluid vector on each -connection affecting the equation defined before. +connection affecting the equation defined before. To check, whether these are +actually variable (e.g. not user specified or presolved), you can use the +`is_variable` method. You have to then assign the result of the derivative to +the correct location in the Jacobian. The row is the k-th equation and the +column is given in the `J_col` attribute of the variable. The derivatives can be calculated analytically (preferred if possible) or numerically by using the inbuilt method :py:meth:`tespy.components.component.Component.numeric_deriv`, where -- :code:`func` is the function you want to calculate the derivatives for, -- :code:`dx` is the variable you want to calculate the derivative to and -- :code:`pos` indicates the connection you want to calculate the derivative - for, e.g. :code:`pos=1` means, that counting your inlets and outlets from - low index to high index (first inlets, then outlets), the connection to be - used is the second connection in that list. +- :code:`func` is the function you want to calculate the derivatives for. +- :code:`dx` is the variable you want to calculate the derivative to. +- :code:`conn` is the connection you want to calculate the derivative for. - :code:`kwargs` are additional keyword arguments required for the function. LaTeX documentation @@ -701,14 +697,14 @@ You are very welcome to submit an issue on our GitHub! .. _tespy_subsystems_label: -Component Groups: Subystems -=========================== +Component Groups: Subsystems +============================ Subsystems are an easy way to add frequently used component groups such as a drum with evaporator or a preheater with desuperheater to your system. In this section you will learn how to create a subsystem and implement it in your work. The subsystems are highly customizable and thus a very powerful tool, if you -require to use specific component groups frequently. We provide an example, of +require using specific component groups frequently. We provide an example, of how to create a simple subsystem and use it in a simulation. Custom subsystems @@ -807,8 +803,7 @@ within the same folder as your script. # %% network definition - fluid_list = ['air', 'water'] - nw = Network(fluid_list, p_unit='bar', T_unit='C') + nw = Network(p_unit='bar', T_unit='C') # %% component definition @@ -832,8 +827,8 @@ within the same folder as your script. # %% connection parameters - fw_sg.set_attr(fluid={'air': 0, 'water': 1}, T=25) - fg_sg.set_attr(fluid={'air': 1, 'water': 0}, T=650, m=100) + fw_sg.set_attr(fluid={'water': 1}, T=25) + fg_sg.set_attr(fluid={'air': 1}, T=650, m=100) sg_ls.set_attr(p=130) sg_ch.set_attr(p=1) @@ -842,16 +837,20 @@ within the same folder as your script. # %% component parameters - sg.comps['eco'].set_attr(pr1=0.999, pr2=0.97, - design=['pr1', 'pr2', 'ttd_u'], - offdesign=['zeta1', 'zeta2', 'kA_char']) + sg.comps['eco'].set_attr( + pr1=0.999, pr2=0.97, design=['pr1', 'pr2', 'ttd_u'], + offdesign=['zeta1', 'zeta2', 'kA_char'] + ) - sg.comps['eva'].set_attr(pr1=0.999, ttd_l=20, design=['pr1', 'ttd_l'], - offdesign=['zeta1', 'kA_char']) + sg.comps['eva'].set_attr( + pr1=0.999, ttd_l=20, design=['pr1', 'ttd_l'], + offdesign=['zeta1', 'kA_char'] + ) - sg.comps['sup'].set_attr(pr1=0.999, pr2=0.99, ttd_u=50, - design=['pr1', 'pr2', 'ttd_u'], - offdesign=['zeta1', 'zeta2', 'kA_char']) + sg.comps['sup'].set_attr( + pr1=0.999, pr2=0.99, ttd_u=50, design=['pr1', 'pr2', 'ttd_u'], + offdesign=['zeta1', 'zeta2', 'kA_char'] + ) sg.conns['eco_dr'].set_attr(Td_bp=-5, design=['Td_bp']) diff --git a/docs/modules/connections.rst b/docs/modules/connections.rst index ef9ceeb08..5fa49d2fa 100644 --- a/docs/modules/connections.rst +++ b/docs/modules/connections.rst @@ -21,94 +21,136 @@ following parameters: * a fluid vector (fluid) and * a balance closer for the fluid vector (fluid_balance). -It is possible to specify values, starting values, references and data -containers. The data containers for connections are dc_prop for fluid -properties (mass flow, pressure, enthalpy, temperature and vapor mass -fraction) and dc_flu for fluid composition. If you want to specify -data_containers, you need to import them from the :py:mod:`tespy.tools` module. +It is possible to specify values, starting values and references. The data +containers for connections are `dc_prop` for fluid properties (mass flow, +pressure, enthalpy, temperature, etc.) and `dc_flu` for fluid composition. If +you want to specify information directly to these do it with caution, data types +are not enforced. In order to create the connections we create the components to connect first. .. code-block:: python - from tespy.tools import FluidProperties as dc_prop - from tespy.connections import Connection, Ref - from tespy.components import Sink, Source + >>> from tespy.connections import Connection, Ref + >>> from tespy.components import Sink, Source # create components - source1 = Source('source 1') - source2 = Source('source 2') - sink1 = Sink('sink 1') - sink2 = Sink('sink 2') + >>> source1 = Source('source 1') + >>> source2 = Source('source 2') + >>> sink1 = Sink('sink 1') + >>> sink2 = Sink('sink 2') # create connections - myconn = Connection(source1, 'out1', sink1, 'in1') - myotherconn = Connection(source2, 'out1', sink2, 'in1') + >>> myconn = Connection(source1, 'out1', sink1, 'in1') + >>> myotherconn = Connection(source2, 'out1', sink2, 'in1') # set pressure and vapor mass fraction by value, temperature and enthalpy # analogously - myconn.set_attr(p=7, x=0.5) + >>> myconn.set_attr(p=7, x=0.5) + >>> myconn.p.val, myconn.x.val + (7, 0.5) # set starting values for mass flow, pressure and enthalpy (has no effect # on temperature and vapor mass fraction!) - myconn.set_attr(m0=10, p0=15, h0=100) + >>> myconn.set_attr(m0=10, p0=15, h0=100) + >>> myconn.m.val0, myconn.p.val0, myconn.h.val0 + (10, 15, 100) - # do the same with a data container - myconn.set_attr( - p=dc_prop(val=7, val_set=True), - x=dc_prop(val=0.5, val_set=True) - ) - myconn.set_attr( - m=dc_prop(val0=10), p=dc_prop(val0=15), h=dc_prop(val0=100) - ) + # do the same directly on the data containers + >>> myconn.p.set_attr(val=7, is_set=True) + >>> myconn.x.set_attr(val=0.5, is_set=True) + + >>> myconn.m.set_attr(val0=10) + >>> myconn.p.set_attr(val0=15) + >>> myconn.h.set_attr(val0=100) + >>> myconn.m.val0, myconn.p.val0, myconn.h.val0 + (10, 15, 100) # specify a referenced value: pressure of myconn is 1.2 times pressure at # myotherconn minus 5 (unit is the network's corresponding unit) - myconn.set_attr(p=Ref(myotherconn, 1.2, -5)) + >>> myconn.set_attr(p=Ref(myotherconn, 1.2, -5)) # specify value and reference at the same time - myconn.set_attr( - p=dc_prop( - val=7, val_set=True, ref=Ref(myotherconn, 1.2, -5), ref_set=True - ) - ) + >>> myconn.p_ref.set_attr(ref=Ref(myotherconn, 1.2, -5), is_set=True) + >>> myconn.p.set_attr(val=7, is_set=True) # possibilities to unset values - myconn.set_attr(p=np.nan) - myconn.set_attr(p=None) - myconn.p.set_attr(val_set=False, ref_set=False) + >>> myconn.set_attr(p=None) + >>> myconn.p.is_set + False -If you want to specify the fluid vector you can do it in the following way. + >>> myconn.set_attr(p=10) + >>> myconn.p.set_attr(is_set=False) + >>> myconn.p.is_set + False -.. note:: + >>> myconn.p_ref.set_attr(is_set=False) # for referenced values + >>> myconn.p_ref.is_set + False - If you specify a fluid, use the fluid's name and do not include the fluid - property back end. -.. code-block:: python +If you want to specify the fluid vector you can do it in the following way. - from tespy.tools import FluidComposition as dc_flu +.. code-block:: python # set both elements of the fluid vector - myconn.set_attr(fluid={'water': 1, 'air': 0}) + >>> myconn.set_attr(fluid={'water': 1}) + # same thing, but using data container - myconn.set_attr(fluid=dc_flu(val={'water': 1, 'air': 0}, - val_set={'water': True, 'air': True})) + >>> myconn.fluid.set_attr(val={'water': 1}, is_set={'water'}) + >>> myconn.fluid.is_set + {'water'} # set starting values - myconn.set_attr(fluid0={'water': 1, 'air': 0}) + >>> myconn.set_attr(fluid0={'water': 1}) + # same thing, but using data container - myconn.set_attr(fluid=dc_flu(val0={'water': 1, 'air': 0})) + >>> myconn.fluid.set_attr(val0={'water': 1}) # unset full fluid vector - myconn.set_attr(fluid={}) + >>> myconn.set_attr(fluid={'water': None}) + >>> myconn.fluid.is_set + set() + # unset part of fluid vector - myconn.fluid.set_attr(val_set={'water': False}) + >>> myconn.set_attr(fluid={'water': 1}) + >>> myconn.fluid.is_set.remove('water') + >>> myconn.fluid.is_set + set() .. note:: References can not be used for fluid composition at the moment! +It is possible to specify the fluid property back end of the fluids by adding +the name of the back end in front of the fluid's name. For incompressible binary +mixtures, you can append the water volume/mass fraction to the fluid's name, for +example: + +.. code-block:: python + + >>> myconn.set_attr(fluid={'water': 1}) # HEOS back end + >>> myconn.set_attr(fluid={'INCOMP::water': 1}) # incompressible fluid + >>> myconn.set_attr(fluid={'BICUBIC::air': 1}) # bicubic back end + >>> myconn.set_attr(fluid={'INCOMP::MPG[0.5]': 1}) # binary incompressible mixture + +.. note:: + + Without further specifications CoolProp will be used as fluid property + database. If you do not specify a back end, the **default back end** + :code:`HEOS` will be used. For an overview of the back ends available please + refer to the :ref:`fluid property section `. + +You can also change the engine, for example to the iapws library. It is even +possible, that you define your own custom engine, e.g. using polynomial +equations. Please check out the fluid properties' section in the docs on how to +do this. + +.. code-block:: python + + >>> from tespy.tools.fluid_properties.wrappers import IAPWSWrapper + >>> myconn.set_attr(fluid={'H2O': 1}, fluid_engines={"H2O": IAPWSWrapper}) + You may want to access the network's connections other than using the variable names, for example in an imported network or connections from a subsystem. It is possible to access these using the connection's label. By default, the label @@ -123,9 +165,14 @@ is generated by this logic: .. code-block:: python - myconn = Connection(source1, 'out1', sink1, 'in1', label='myconnlabel') - mynetwork.add_conns(myconn) - mynetwork.get_conn('myconnlabel').set_attr(p=1e5) + >>> from tespy.networks import Network + + >>> mynetwork = Network() + >>> myconn = Connection(source1, 'out1', sink1, 'in1', label='myconnlabel') + >>> mynetwork.add_conns(myconn) + >>> mynetwork.get_conn('myconnlabel').set_attr(p=1e5) + >>> myconn.p.val + 100000.0 .. note:: @@ -149,7 +196,7 @@ Different use-cases for busses could be: The handling of busses is very similar to connections and components. You need to add components to your busses as a dictionary containing at least the -instance of your component. Additionally you may provide a characteristic line, +instance of your component. Additionally, you may provide a characteristic line, linking the ratio of actual value to a referenced value (design case value) to an efficiency factor the component value of the bus is multiplied with. For instance, you can provide a characteristic line of an electrical generator or @@ -231,16 +278,20 @@ consumption. .. code-block:: python - from tespy.networks import Network - from tespy.components import Pump, Turbine, CombustionEngine - from tespy.connections import Bus + >>> from tespy.networks import Network + >>> from tespy.components import Pump, Turbine, CombustionEngine + >>> from tespy.connections import Bus + + >>> my_network = Network() + >>> fwp = Pump("feed water pump") + >>> turbine_fwp = Turbine("turbine fwp") # the total power on this bus must be zero # this way we can make sure the power of the turbine has the same value as # the pump's power but with negative sign - fwp_bus = Bus('feed water pump bus', P=0) - fwp_bus.add_comps({'comp': turbine_fwp}, {'comp': fwp}) - my_network.add_busses(fwp_bus) + >>> fwp_bus = Bus("feed water pump bus", P=0) + >>> fwp_bus.add_comps({"comp": turbine_fwp}, {"comp": fwp, "base": "bus"}) + >>> my_network.add_busses(fwp_bus) Create two turbines :code:`turbine1` and :code:`turbine2` which have the same power output. @@ -250,28 +301,36 @@ power output. # the total power on this bus must be zero, too # we make sure the two turbines yield the same power output by adding the char # parameter for the second turbine and using -1 as char - turbine_bus = Bus('turbines', P=0) - turbine_bus.add_comps({'comp': turbine_1}, {'comp': turbine_2, 'char': -1}) - my_network.add_busses(turbine_bus) + >>> turbine_1 = Turbine("turbine 1") + >>> turbine_2 = Turbine("turbine 2") + + >>> turbine_bus = Bus('turbines', P=0) + >>> turbine_bus.add_comps({'comp': turbine_1}, {'comp': turbine_2, 'char': -1}) + >>> my_network.add_busses(turbine_bus) Create a bus for post-processing purpose only. Include a characteristic line -for a generator and add two turbines :code:`turbine_hp` and :code:`turbine_lp` +for a generator and add two turbines :code:`turbine_1` and :code:`turbine_2` to the bus. .. code-block:: python + >>> import numpy as np + >>> from tespy.tools.characteristics import CharLine + # bus for postprocessing, no power (or heat flow) specified but with variable # conversion efficiency - power_bus = Bus('power output') - x = np.array([0.2, 0.4, 0.6, 0.8, 1.0, 1.1]) - y = np.array([0.85, 0.93, 0.95, 0.96, 0.97, 0.96]) + >>> power_bus = Bus("power output") + >>> x = np.array([0.2, 0.4, 0.6, 0.8, 1.0, 1.1]) + >>> y = np.array([0.85, 0.93, 0.95, 0.96, 0.97, 0.96]) + # create a characteristic line for a generator - gen = CharLine(x=x, y=y) - power.add_comps( - {'comp': turbine_hp, 'char': gen1}, - {'comp': turbine_lp, 'char': gen2} - ) - my_network.add_busses(power_bus) + >>> gen1 = CharLine(x=x, y=y) + >>> gen2 = CharLine(x=x, y=y) + >>> power_bus.add_comps( + ... {'comp': turbine_1, 'char': gen1}, + ... {'comp': turbine_2, 'char': gen2} + ... ) + >>> my_network.add_busses(power_bus) Create a bus for the electrical power output of a combustion engine :code:`comb_engine`. Use a generator for power conversion and specify the total @@ -279,9 +338,11 @@ power output. .. code-block:: python + >>> comb_engine = CombustionEngine("engine") + # bus for combustion engine power - el_power_bus = Bus('combustion engine power', P=-10e6) - el_power_bus.add_comps({'comp': comb_engine, 'param': 'P', 'char': gen}) + >>> el_power_bus = Bus('combustion engine power', P=-10e6) + >>> el_power_bus.add_comps({'comp': comb_engine, 'param': 'P', 'char': gen1}) Create a bus for the electrical power input of a pump :code:`pu` with :code:`'bus'` and with :code:`'component'` as base. In both cases, the value of @@ -290,59 +351,75 @@ definitions the value of the bus power will differ in part load. .. code-block:: python - import numpy as np - from tespy.components import Pump, Sink, Source - from tespy.connections import Bus, Connection - from tespy.networks import Network - from tespy.tools.characteristics import CharLine + >>> import numpy as np + >>> from tespy.components import Pump, Sink, Source + >>> from tespy.connections import Bus, Connection + >>> from tespy.networks import Network + >>> from tespy.tools.characteristics import CharLine - nw = Network(fluids=['H2O'], p_unit='bar', T_unit='C') + >>> nw = Network(iterinfo=False, p_unit='bar', T_unit='C') - si = Sink('sink') - so = Source('source') - pu = Pump('pump') + >>> si = Sink('sink') + >>> so = Source('source') + >>> pu = Pump('pump') - so_pu = Connection(so, 'out1', pu, 'in1') - pu_si = Connection(pu, 'out1', si, 'in1') + >>> so_pu = Connection(so, 'out1', pu, 'in1') + >>> pu_si = Connection(pu, 'out1', si, 'in1') - nw.add_conns(so_pu, pu_si) + >>> nw.add_conns(so_pu, pu_si) # bus for combustion engine power - x = np.array([0.2, 0.4, 0.6, 0.8, 1.0, 1.1]) - y = np.array([0.85, 0.93, 0.95, 0.96, 0.97, 0.96]) + >>> x = np.array([0.2, 0.4, 0.6, 0.8, 1.0, 1.1]) + >>> y = np.array([0.85, 0.93, 0.95, 0.96, 0.97, 0.96]) + # create a characteristic line for a generator - mot_bus_based = CharLine(x=x, y=y) - mot_comp_based = CharLine(x=x, y=1 / y) - bus1 = Bus('pump power bus based') - bus1.add_comps({'comp': pu, 'char': mot_bus_based, 'base': 'bus'}) + >>> mot_bus_based = CharLine(x=x, y=y) + >>> mot_comp_based = CharLine(x=x, y=1 / y) + >>> bus1 = Bus('pump power bus based') + >>> bus1.add_comps({'comp': pu, 'char': mot_bus_based, 'base': 'bus'}) + # the keyword 'base': 'component' is the default value, therefore it does # not need to be passed - bus2 = Bus('pump power component based') - bus2.add_comps({'comp': pu, 'char': mot_comp_based}) + >>> bus2 = Bus('pump power component based') + >>> bus2.add_comps({'comp': pu, 'char': mot_comp_based}) + + >>> nw.add_busses(bus1, bus2) + + >>> so_pu.set_attr(fluid={'H2O': 1}, m=10, p=5, T=20) + >>> pu_si.set_attr(p=10) + + >>> pu.set_attr(eta_s=0.75) + + >>> nw.solve('design') + >>> nw.save('tmp') + >>> print('Bus based efficiency:', round(pu.calc_bus_efficiency(bus1), 2)) + Bus based efficiency: 0.97 + + >>> print('Component based efficiency:', round(1 / pu.calc_bus_efficiency(bus2), 2)) + Component based efficiency: 0.97 + + >>> print('Bus based bus power:', round(pu.calc_bus_value(bus1))) + Bus based bus power: 6883 - nw.add_busses(bus1, bus2) + >>> print('Component based bus power:', round(pu.calc_bus_value(bus2))) + Component based bus power: 6883 - so_pu.set_attr(fluid={'H2O': 1}, m=10, p=5, T=20) - pu_si.set_attr(p=10) + >>> so_pu.set_attr(m=8) + >>> nw.solve('offdesign', design_path='tmp') + >>> print('Bus based efficiency:', round(pu.calc_bus_efficiency(bus1), 2)) + Bus based efficiency: 0.96 - pu.set_attr(eta_s=0.75) + >>> print('Component based efficiency:', round(1 / pu.calc_bus_efficiency(bus2), 2)) + Component based efficiency: 0.96 - nw.solve('design') - nw.save('tmp') - print('Bus based efficiency:', pu.calc_bus_efficiency(bus1)) - print('Component based efficiency:', 1 / pu.calc_bus_efficiency(bus2)) - print('Bus based bus power:', pu.calc_bus_value(bus1)) - print('Component based bus power:', pu.calc_bus_value(bus2)) + >>> print('Bus based bus power:', round(pu.calc_bus_value(bus1))) + Bus based bus power: 5562 - so_pu.set_attr(m=9) - nw.solve('offdesign', design_path='tmp') - print('Bus based efficiency:', pu.calc_bus_efficiency(bus1)) - print('Component based efficiency:', 1 / pu.calc_bus_efficiency(bus2)) - print('Bus based bus power:', pu.calc_bus_value(bus1)) - print('Component based bus power:', pu.calc_bus_value(bus2)) + >>> print('Component based bus power:', round(pu.calc_bus_value(bus2))) + Component based bus power: 5564 # get DataFrame with the bus results - bus_results = nw.results['pump power bus based'] + >>> bus_results = nw.results['pump power bus based'] .. note:: diff --git a/docs/modules/fluid_properties.rst b/docs/modules/fluid_properties.rst index 6179352be..24d2a5958 100644 --- a/docs/modules/fluid_properties.rst +++ b/docs/modules/fluid_properties.rst @@ -2,12 +2,28 @@ Fluid properties ================ -The basic fluid properties are handled by -`CoolProp `_. All available fluids can be found on -their homepage. Also see :cite:`Bell2014`. +The default fluid property engine `CoolProp `_. All +available fluids can be found on their homepage. Also see :cite:`Bell2014`. +Since version 0.7 of TESPy it is possible to use other engines. TESPy comes with +two additional predefined engines, i.e. -CoolProp back ends ------------------- +- the `iapws `_ library and +- the `pyromat `_ library. + +For each fluid you can specify, which library should be used, and you can easily +implement your own engine, for example, if your fluid is not available through +the predefined engines, or you need a different implementation of the fluid +properties of a specific fluid. Further below on this page we have added an +example implementation of the KKH polynomial formulation to illustrate, how you +can integrate your own engine into TESPy. + +CoolProp +-------- +CoolProp is the default fluid property back end of TESPy. It provides different +back ends for different applications and allows calling mixture functions. + +Available back ends ++++++++++++++++++++ CoolProp provides multiple back ends for fluid property calculation. The back ends vary in calculation speed and calculation accuracy. It is possible to choose from the following back ends: @@ -24,51 +40,283 @@ to choose from the following back ends: - :code:`INCOMP`: Back end for incompressible fluids. - :code:`IF97`: Back end for the IAPWS-IF97 of water, very accurate and much higher calculation speed than :code:`HEOS`. Due to a bug in the CoolProp - back end this option is available with a fix (not the original - implementation), for more information see the + back end this option is instable, for more information see the `CoolProp issue #1918 `_. -For more information on the Back ends please visit the CoolProp online +For more information on the back ends please visit the CoolProp online documentation. 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 ++++++++++++++++++++++++++++ +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 `__. All fluids provided in this list cover liquid and gaseous state and the two-phase region. Incompressible fluids ---------------------- ++++++++++++++++++++++ If you are looking for heat transfer fluids, the list of incompressible `fluids `__ might be interesting for you. In contrast to the pure fluids, the properties -cover liquid state only. +cover liquid state only. TESPy supports using pure incompressibles as well as +the predefined mixtures. Fluid mixtures --------------- -CoolProp provides fluid properties for two component mixtures. BUT: These are -NOT integrated in TESPy! Nevertheless, you can use fluid mixtures for gases. - -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. - -Other mixtures -^^^^^^^^^^^^^^ -Apart from partially liquid water in flue gases it is **not possible** to use -mixtures of liquids and other liquids or gaseous fluids **at the moment**! If -you try to use a mixture of two liquid or gaseous fluids and liquid fluids, -e.g. water and methanol, the equations will still be applied, but obviously -return wrong values. If you have ideas for the implementation of new kinds of -mixtures we appreciate you contacting us. +++++++++++++++ +CoolProp provides a back end for predefined mixtures, which is rather instable +using HEOS. If you want to use the mixture feature of CoolProp we recommend +using the REFPROP back end instead. + +Using other engines +------------------- +To use any of the other fluid property engines, you can do the following, e.g. +to use the `iapws` back end: + +.. code-block:: python + + >>> from tespy.components import Sink + >>> from tespy.components import Source + >>> from tespy.components import Turbine + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + >>> from tespy.tools.fluid_properties.wrappers import IAPWSWrapper + + >>> nwk = Network(iterinfo=False) + + >>> so = Source("Source") + >>> tu = Turbine("Turbine") + >>> si = Sink("Sink") + + >>> c1 = Connection(so, "out1", tu, "in1", label="1") + >>> c2 = Connection(tu, "out1", si, "in1", label="2") + + >>> nwk.add_conns(c1, c2) + + >>> tu.set_attr(eta_s=0.9) + + >>> c1.set_attr( + ... v=1, p=1e5, T=500, + ... fluid={"IF97::H2O": 1}, fluid_engines={"H2O": IAPWSWrapper} + ... ) + >>> c2.set_attr(p=1e4) + + >>> nwk.solve("design") + >>> round(c2.x.val, 3) + 0.99 + + >>> tu.set_attr(eta_s=None) + >>> c2.set_attr(x=1) + + >>> nwk.solve("design") + >>> round(tu.eta_s.val, 3) + 0.841 + + +Implementing a custom engine +---------------------------- +The fluid property calls to different engines have to be masqueraded with +respective wrappers. The implementation of the wrappers for `CoolProp`, +`iapws` and `pyromat` can be found in the +:py:mod:`fluid_properties.wrappers ` +module, and serve as example implementations for your own wrappers: + +The wrapper for your own engine (or an engine from a different library) has to +inherit from the +:py:class:`FluidPropertyWrapper ` +class. Below we will use the polynomial formulation for **gaseous water** from +:cite:`Knacke1991` as an example. First we import the necessary dependencies. + +.. code-block:: python + + >>> import numpy as np + >>> from tespy.tools.fluid_properties.wrappers import FluidPropertyWrapper + >>> from tespy.tools.global_vars import gas_constants + +Then we set up a new class and implement the methods to calculate enthalpy and +entropy from (pressure and) temperature. The structure and names of the +functions have to match the pattern from the `FluidPropertyWrapper`, in this +case `h_pT`. On top of that, we add a backwards function `T_ph` and a function +to analytically calculate the heat capacity `_cp_pT`, the derivative of the +enthalpy to the temperature. Lastly, to make the calculation of isentropic +efficiencies possible, we can add the equation for change in enthalpy on +isentropic change of pressure for an ideal gas. + + +.. code-block:: python + + # coefficients H+ S+ a b c d M + >>> COEF = { + ... "H2O": [-253.871, -11.750, 34.376, 7.841, -0.423, 0.0, 18.0152], + ... } + + >>> class KKHWrapper(FluidPropertyWrapper): + ... + ... def __init__(self, fluid, back_end=None, reference_temperature=298.15) -> None: + ... super().__init__(fluid, back_end) + ... + ... if self.fluid not in COEF: + ... msg = "Fluid not available in KKH database" + ... raise KeyError(msg) + ... + ... self.coefficients = COEF[fluid] + ... self.h_ref = self._h_pT(None, reference_temperature) + ... self._molar_mass = self.coefficients[-1] * 1e-3 + ... self._T_min = 100 + ... self._T_max = 2000 + ... self._p_min = 1000 + ... self._p_max = 10000000 + ... + ... def cp_pT(self, p, T): + ... y = T * 1e-3 + ... return 1e3 * ( + ... self.coefficients[2] + ... + self.coefficients[3] * y + ... + self.coefficients[4] / (y ** 2) + ... + self.coefficients[5] * y ** 2 + ... ) / self.coefficients[6] + ... + ... def h_pT(self, p, T): + ... return self._h_pT(p, T) - self.h_ref + ... + ... def _h_pT(self, p, T): + ... y = T * 1e-3 + ... return 1e6 * ( + ... self.coefficients[0] + ... + self.coefficients[2] * y + ... + self.coefficients[3] / 2 * y ** 2 + ... - self.coefficients[4] / y + ... + self.coefficients[5] / 3 * y ** 3 + ... ) / self.coefficients[6] + ... + ... def T_ph(self, p, h): + ... return newton(self.h_pT, self.cp_pT, h, p) + ... + ... def isentropic(self, p_1, h_1, p_2): + ... T_1 = self.T_ph(p_1, h_1) + ... cp = self.cp_pT(p_1, T_1) + ... kappa = cp / (cp - gas_constants["uni"] / self._molar_mass) + ... T_2 = T_1 * (p_2 / p_1) ** ((kappa - 1) / kappa) + ... return self.h_pT(p_2, T_2) + +We can add a newton for the backwards function: + +.. code-block:: python + + >>> def newton(func, deriv, h, p): + ... # default valaues + ... T = 300 + ... valmin = 70 + ... valmax = 3000 + ... max_iter = 10 + ... tol_rel = 1e-6 + ... + ... # start newton loop + ... expr = True + ... i = 0 + ... while expr: + ... # calculate function residual and new value + ... res = h - func(p, T) + ... T += res / deriv(p, T) + ... + ... # check for value ranges + ... if T < valmin: + ... T = valmin + ... if T > valmax: + ... T = valmax + ... i += 1 + ... + ... if i > max_iter: + ... break + ... + ... expr = abs(res / h) >= tol_rel + ... + ... return T + +And then we can test a call to the interface and check our results, also compare +them to CoolProp. + +.. code-block:: python + + >>> kkh_water = KKHWrapper("H2O", reference_temperature=298.15) # same as in CoolProp + >>> h = kkh_water.h_pT(1e5, 400) + >>> T = kkh_water.T_ph(1e5, h) + >>> round(T, 1) + 400.0 + >>> round(h) + 189769 + + >>> from tespy.tools.fluid_properties import CoolPropWrapper + + >>> coolprop_water = CoolPropWrapper("H2O") + >>> h_cp = coolprop_water.h_pT(1e5, 400) + >>> T_cp = coolprop_water.T_ph(1e5, h_cp) + >>> round(T_cp, 1) + 400.0 + >>> round(h) + 189769 + +To use this wrapper in a simple TESPy model, we can then proceed as we have in +the previous section: + +.. code-block:: python + + + >>> from tespy.components import Sink + >>> from tespy.components import Source + >>> from tespy.components import Turbine + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + + >>> nwk = Network(T_unit="C", p_unit="MPa", iterinfo=False) + + >>> so = Source("Source") + >>> tu = Turbine("Turbine") + >>> si = Sink("Sink") + + >>> c1 = Connection(so, "out1", tu, "in1", label="1") + >>> c2 = Connection(tu, "out1", si, "in1", label="2") + + >>> nwk.add_conns(c1, c2) + + >>> c1.set_attr( + ... m=1, p=10, T=600, + ... fluid={"H2O": 1}, fluid_engines={"H2O": KKHWrapper} + ... ) + >>> c2.set_attr(p=1, T=400) + + >>> nwk.solve("design") + + >>> tu.set_attr(eta_s=0.9) + >>> c2.set_attr(T=None) + >>> nwk.solve("design") + >>> round(c2.T.val, 1) + 306.3 + +Mixture routines in TESPy +------------------------- +Different types of mixture routines are implemented in TESPy. You can select, +which routine should be applied in each separated subnetwork of your system by +specifying a mixing rule. `ideal-cond` is the default mixing rule. The following +mixing rules are available at the moment: + +- `ideal-cond`: gaseous fluids **with flash calculations for water**. +- `ideal`: gaseous fluids **without flash calculations**. +- `incompressible`: mass based mixtures of individual incompressible fluids. + +The mixtures are calculated by using the pure fluid properties from the selected +fluid property engines and combining them through corresponding equations. The +equations are documented in the +:py:mod:`fluid_properties.mixtures ` +module. + +.. note:: + + Similarly to the custom fluid property engine, you can implement your own + mixture routines. If you are interested in doing so, you can get in contact + via the :ref:`user meeting ` or the GitHub + `discussion forum `__. .. _FluProDia_label: diff --git a/docs/modules/networks.rst b/docs/modules/networks.rst index 1ee54d79d..d5dc41f77 100644 --- a/docs/modules/networks.rst +++ b/docs/modules/networks.rst @@ -2,7 +2,7 @@ Networks ======== -The network class handles preprocessing, solving and postprocessing. +The network class handles preprocessing, solving and post-processing. We will walk you through all the important steps. Setup @@ -11,19 +11,21 @@ Network container ^^^^^^^^^^^^^^^^^ The TESPy network contains all data of your plant, which in terms of the calculation is represented by a nonlinear system of equations. The system -variables of your TESPy network are: +variables of your TESPy network are a subset of the following: * mass flow, * pressure, * enthalpy and -* the mass fractions of the network's fluids. +* the mass fractions of the fluids -The solver will solve for these variables. As stated in the introduction the -list of fluids is passed to your network on creation. If your **system** -**includes fluid mixtures**, you should **always make use of the value ranges** -for the system variables. This improves the stability of the algorithm. Try to -fit the boundaries as tight as possible, for instance, if you know that the -maximum pressure in the system will be at 10 bar, use it as upper boundary. +of every connection. + +The solver will simplify the variable space in a presolving step and then solve +for the remaining variables. If your **system includes fluid mixtures**, you +might want to **make use of the value ranges** for the system variables. This +improves the stability of the algorithm. Try to fit the boundaries as tight as +possible, for instance, if you know that the maximum pressure in the system will +be at 10 bar, use it as upper boundary. .. note:: @@ -32,34 +34,17 @@ maximum pressure in the system will be at 10 bar, use it as upper boundary. .. code-block:: python - from tespy.networks import Network - - fluid_list = ['CO2', 'H2O', 'N2', 'O2', 'Ar'] - my_plant = Network(fluids=fluid_list) - my_plant.set_attr(p_unit='bar', h_unit='kJ / kg') - my_plant.set_attr(p_range=[0.05, 10], h_range=[15, 2000]) - -.. note:: - - It is possible to specify the fluid property back-end of the network fluids - by adding the name of the back-end in front of the fluid's name within the - list of fluids of your network. For example: - - .. code-block:: python + >>> from tespy.networks import Network - from tespy.networks import Network - - fluid_list = ['CO2', 'BICUBIC::H2O', 'INCOMP::DowQ'] - Network(fluids=fluid_list) - - If you do not specify a back-end, the **default back-end** :code:`HEOS` - will be used (as for :code:`CO2`). In this example, :code:`H2O` will be - using the :code:`BICUBIC` back-end and :code:`DowQ` the back-end for - incompressible fluids :code:`INCOMP`. For an overview of the back-ends - available please refer to the - :ref:`fluid property section `. - - **It is not possible to use the same fluid in different back-ends!** + >>> my_plant = Network() + >>> my_plant.p_unit + 'Pa' + >>> my_plant.set_attr(p_unit='bar', h_unit='kJ / kg') + >>> my_plant.set_attr(p_range=[0.05, 10], h_range=[15, 2000]) + >>> my_plant.p_unit + 'bar' + >>> my_plant.p_range_SI.tolist() + [5000.0, 1000000.0] .. _printout_logging_label: @@ -72,12 +57,13 @@ lines: .. code-block:: python - from tespy.tools import logger - import logging - logger.define_logging( - log_path=True, log_version=True, - screen_level=logging.INFO, file_level=logging.DEBUG - ) + >>> from tespy.tools import logger + >>> import logging + >>> ();logger.define_logging( + ... logpath="myloggings", log_the_path=True, log_the_version=True, + ... screen_level=logging.INFO, file_level=logging.DEBUG + ... );() # +doctest: ELIPSIS + (...) The log-file will be saved to :code:`~/.tespy/log_files/` by default. All available options are documented in the @@ -89,10 +75,16 @@ disable convergence progress printouts: .. code-block:: python + >>> my_plant.iterinfo + True + # disable iteration information printout - myplant.set_attr(iterinfo=False) + >>> my_plant.set_attr(iterinfo=False) + >>> my_plant.iterinfo + False + # enable iteration information printout - myplant.set_attr(iterinfo=True) + >>> my_plant.set_attr(iterinfo=True) Adding connections ++++++++++++++++++ @@ -102,8 +94,8 @@ or via subsystems using the corresponding methods: .. code-block:: python - myplant.add_conns() - myplant.add_subsys() + >>> my_plant.add_conns() + >>> my_plant.add_subsys() .. note:: @@ -114,13 +106,13 @@ or via subsystems using the corresponding methods: Busses: Energy Connectors +++++++++++++++++++++++++ Another type of connection is the bus: Busses are connections for massless -transfer of energy e.g. in turbomachines or heat exchangers. They can be used +transfer of energy e.g. in turbomachinery or heat exchangers. They can be used to model motors or generators, too. Add them to your network with the following method: .. code-block:: python - myplant.add_busses() + >>> my_plant.add_busses() You will learn more about busses and how they work in :ref:`this part `. @@ -131,7 +123,7 @@ You can start the solution process with the following line: .. code-block:: python - myplant.solve(mode='design') + my_plant.solve(mode='design') This starts the initialisation of your network and proceeds to its calculation. The specification of the **calculation mode is mandatory**, This is the list of @@ -154,9 +146,6 @@ available keywords: simulation in some cases by outsourcing calculation to graphics card. For more information please visit the `cupy documentation `_. -- :code:`always_all_equations` you can skip recalculation of converged - equations in the calculation process if you specify this parameter to be - :code:`False`. Default value is :code:`True`. There are two calculation modes available (:code:`'design'` and :code:`'offdesign'`), which are explained in the subsections below. If you @@ -191,7 +180,7 @@ Offdesign mode The offdesign mode is used to **calculate the performance of your plant, if** **parameters deviate from the plant's design point**. This can be partload operation, operation at different temperature or pressure levels etc.. Thus, -before starting an offdesing calculation you have to design your plant first. +before starting an offdesign calculation you have to design your plant first. By stating :code:`'offdesign'` as calculation mode, **components and** **connections will switch to the offdesign mode.** This means that all parameters provided as design parameters will be unset and all parameters @@ -209,8 +198,9 @@ with zeta values. .. code-block:: python - mycomponent.set_attr(design=['ttd_u', 'pr1', 'pr2'], - offdesign=['kA', 'zeta1', 'zeta2']) + mycomponent.set_attr( + design=['ttd_u', 'pr1', 'pr2'], offdesign=['kA', 'zeta1', 'zeta2'] + ) .. note:: @@ -235,85 +225,117 @@ To solve your offdesign calculation, use: .. code-block:: python - myplant.solve(mode='offdesign', design_path='path/to/network_designpoint') + my_plant.solve(mode='offdesign', design_path='path/to/network_designpoint') Solving ------- A TESPy network can be represented as a linear system of nonlinear equations, consequently the solution is obtained with numerical methods. TESPy uses the -n-dimensional Newton–Raphson method to find the systems solution, which may -only be found, if the network is parameterized correctly. **The number of -variables n** is :math:`n = num_{conn} \cdot (3 + num_{fluids})`. - -The algorithm requires starting values for all variables of the system, thus an -initialisation of the system is run prior to calculating the solution. **High** -**quality initial values are crutial for convergence speed and stability**, bad -starting values might lead to instability and diverging calculation can be the -result. There are different levels for the initialisation. +n-dimensional Newton-Raphson method to find the system's solution, which may +only be found, if the network is parameterized correctly. **The number of** +**variables n changes depending on your system's topology and your** +**specifications**. Generally, masA presolving step reduces the amount of variables, see below +for more information. -Initialisation -^^^^^^^^^^^^^^ -The initialisation is performed in the following steps. - -**General preprocessing:** +**General preprocessing** * check network consistency and initialise components (if network topology is changed to a prior calculation only). +* create a topology representation of the components and the connections. +* simplify the variable space based on the plant's topology. * perform design/offdesign switch (for offdesign calculations only). * preprocessing of offdesign case using the information from the :code:`design_path` argument. -**Finding starting values:** - -* fluid propagation. -* fluid property initialisation. -* 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 -calculation can not start without a successful check. For components, a -preprocessing of some parameters is necessary. It is performed by the -:code:`comp_init` method of the components. You will find the methods in the -:py:mod:`components module `. The design/offdesign switch is -described in the network setup section. For offdesign calculation the +calculation can not start without a successful check. The design/offdesign +switch is described in the network setup section. For offdesign calculation the :code:`design_path` argument is required. The design point information is -extracted from that path in preprocessing. For this, you will need to export +extracted from that path in preprocessing. For this, you will need to save your network's design point information using: .. code-block:: python - myplant.save('path/for/export') - -Starting value generation for your calculations starts with the fluid -propagation. **The fluid propagation is a very important step in the** -**initialisation.** Often, you will specify the fluid at one point of the -network only, all other connections are missing an initial information on the -fluid, if you are not using an :code:`init_path`. The fluid propagation will -push/pull the specified fluid through the network. If you are using combustion -chambers these will be starting points and a generic flue gas composition will -be calculated prior to the propagation. You do not necessarily need to state a -starting value for the fluid at every point of the network. + my_plant.save('path/for/savestate') + +**Simplifying the variable space** + +To reduce the size of the system of equations a reduction of the variable space +is performed in the initialisation of a calculation. For every of the primary +variables (mass flow, pressure, enthalpy and fluid mass fractions), if a value +is directly specified by the user, the respective variable is removed from the +variable space, because it does not need to be solved. + +Furthermore, there are three steps to simplify the variable space, i.e. +regarding mass flow, regarding the fluid composition and regarding pressure and +enthalpy. + +First, based on the topology of the network, different branches are created. +These are: + +- branches, in which the mass flow is equal in every of its connections and +- branches, in which the fluid composition is equal in every of its connections. + +For every mass flow branch, the variable space is reduced to a single mass flow. +For example, in a simple Clausius Rankine cycle there will only be a single +mass flow in the variable space. Analogously in every fluid composition branch, +the variable space is reduced to a single vector containing the variable fluids +of that branch. For example, if a mass flow is split in two streams using a +splitter, the fluid composition remains constant downstream of the splitter. +Therefore, all connections downstream of the splitter share the same fluid +composition as upstream of the splitter. + +The next step is a reduction of the fluid vector specifications: Consider a case +with a couple of potential fluids on a fluid branch, e.g. oxygen, nitrogen, +argon, carbon dioxide and water at the outlet of a combustion chamber. All fluid +mass fractions specified by the user will be fixed and removed from the variable +space. If then, only a single fluid remains with "unknown" mass fraction, we can +assign a mass fraction to that fluid, which is equal to 1 minus the sum of all +other fluids' mass fractions. + +Finally, presolving is applied to pressure and enthalpy, whenever the fluid +composition is fixed. If either pressure or enthalpy is specified by the user +and on top of that temperature, vapor quality or temperature difference to +saturation temperature, the respective variable (enthalpy or pressure) can +directly be calculated. Similarly, if temperature and temperature difference to +saturation temperature or vapor quality are specified, both pressure and +enthalpy can be deducted. + +**Finding starting values** -.. note:: +The algorithm requires starting values for all variables of the system, thus an +initialisation of the system is run prior to calculating the solution. **High** +**quality initial values are crucial for convergence speed and stability**, bad +starting values might lead to instability and diverging calculation can be the +result. The following steps are performed in finding starting values: - If the fluid propagation fails, you often experience an error, where the - fluid property database can not find a value, because the fluid is 'nan'. - Providing starting values manually can fix this problem. +* fluid composition guessing. +* fluid property initialisation. +* initialisation from previous simulation run (:code:`ìnit_previous`). +* initialisation from .csv (setting starting values from :code:`init_path` + argument). -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. +Starting value generation for your calculations starts with the fluid +composition guessing in case the fluid composition is not fixed. The available +fluids will be assigned the same mass fraction :math:`x`, if no starting value +is supplied. The mass fractions are distributed to 1 minus the sum of all user +specified mass fractions: :math:`x=\frac{1-\sum\text{x_spec}}{n}`. If you are +using combustion chambers these will be replaced by a generic flue gas +composition will be calculated prior to the propagation. + +Next the fluid property initialisation uses user specified starting values or +the results from the previous simulation to set starting values for mass flow, +pressure and enthalpy. 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 -:code:`init_path`, you need to provide the path to the saved/exported network. -If you specify an :code:`init_path` TESPy searches through the connections file -for the network topology and if the corresponding connection is found, the -starting values for the system variables are extracted from the connections -file. +network state. In order to initialise your calculation with this method, you +need to provide the path to the saved network in the :code:`init_path` argument +of the `solve` method. TESPy searches through the connections.csv file. If a +connection with the respective label is found, the starting values for the +system variables are taken over from that file. .. note:: @@ -324,15 +346,14 @@ file. in parts of the network you have not touched until now, you will need to state all fluids from the beginning. - Algorithm ^^^^^^^^^ In this section we will give you an introduction to the solving algorithm implemented. -Newton–Raphson method +Newton-Raphson method +++++++++++++++++++++ -The Newton–Raphson method requires the calculation of residual values for the +The Newton-Raphson method requires the calculation of residual values for the equations and of the partial derivatives to all system variables (Jacobian matrix). In the next step the matrix is inverted and multiplied with the residual vector to calculate the increment for the system variables. This @@ -400,10 +421,10 @@ power :math:`P` to be 1000 W, the set of equations will look like this: Convergence stability +++++++++++++++++++++ -One of the main downsides of the Newton–Raphson method is that the initial -stepwidth is very large and that it does not know physical boundaries, for +One of the main downsides of the Newton-Raphson method is that the initial +step width is very large and that it does not know physical boundaries, for example mass fractions smaller than 0 and larger than 1 or negative pressure. -Also, the large stepwidth can adjust enthalpy or pressure to quantities that +Also, the large step width can adjust enthalpy or pressure to quantities that are not covered by the fluid property databases. This would cause an inability e.g. to calculate a temperature from pressure and enthalpy in the next iteration of the algorithm. In order to improve convergence stability, we have @@ -414,7 +435,7 @@ added a convergence check. applied: * Cut off fluid mass fractions smaller than 0 and larger than 1. This way a - mass fraction of a single fluid components never exceeds these boundaries. + mass fraction of a single fluid component never exceeds these boundaries. * Check, whether the fluid properties of pure fluids are within the available ranges of CoolProp and readjust the values if not. @@ -448,37 +469,13 @@ after the third iteration, further checks are usually not required. Calculation speed improvement +++++++++++++++++++++++++++++ For improvement of calculation speed, the calculation of specific derivatives -is skipped if possible. If you specify :code:`always_all_equations=False` for -your simulation, equations may also be skipped: There are two criteria for -equations and one criterion for derivatives that are checked for calculation -intensive operations, e.g. whenever fluid property library calls are necessary: - -For component equations the recalculation of the residual value is skipped, - -- only if you specified :code:`always_all_equations=False` and -- if the absolute of the residual value of that equations is lower than the - threshold of :code:`1e-12` in the iteration before and -- the iteration count is not a multiple of 4. - -Connections equations are skipped +is skipped, if the change of the corresponding variable was below a +threshold of :code:`1e-12` in the iteration before. -- if you specified :code:`always_all_equations=False` and -- if the absolute of the residual value of that equations is lower than the - threshold of :code:`1e-12` in the iteration before and -- the iteration count is not a multiple of 2 and -- the specified property is not temperature. - -The calculation of derivatives is skipped, if the change of the corresponding -variable was below a threshold of :code:`1e-12` in the iteration before. -Again, this does not apply to temperature value specification, as especially -when using fluid mixtures, the convergence stability is very sensitive to -these equations and derivatives. - -.. note:: - - In order to make sure, that every equation is evaluated at least twice, - the minimum amount of iterations before convergence can be accepted is at - 4. +As a user you can take two more measures to improve calculation speed: Specify +primary variables whenever possible/reasonable. This will not only reduce the +variable space but also remove the necessity to calculate partial derivatives +towards them. Troubleshooting +++++++++++++++ @@ -487,51 +484,40 @@ up common mistakes. If you want to debug your code, make sure to enable the logger and have a look at the log-file at :code:`~/.tespy/` (or at your specified location). -First of all, make sure your network topology is set up correctly, TESPy will -prompt an Error, if not. TESPy will prompt an error, too, if you did not -provide enough or if you provide too many parameters for your calculation, but -you will not be given an information which specific parameters are under- or -overdetermined. +First, make sure your network topology is set up correctly, TESPy will prompt +an Error, if not. TESPy will prompt an error, too, if you did not provide +enough or if you provide too many parameters for your calculation, but you will +not be given an information which specific parameters are under- or +over-determined. .. note:: Always keep in mind, that the system has to find a value for mass flow, pressure, enthalpy and the fluid mass fractions. Try to build up your network step by step and have in mind, what parameters will be determined - by adding an additional component without any parametrisation. This way, - you can easily determine, which parameters are still to be specified. - -When using multiple fluids in your network, e.g. -:code:`fluids=['water', 'air', 'methane']` and at some point you want to have -water only, you still need to specify the mass fractions for both air and -methane (although beeing zero) at that point -:code:`fluid={'water': 1, 'air': 0, 'methane': 0}`. Also, setting -:code:`fluid={water: 1}, fluid_balance=True` will still not be sufficient, as -the fluid_balance parameter adds only one equation to your system. + by adding a component without any parametrisation. This way, you can easily + determine, which parameters are still to be specified. If you are modeling a cycle, e.g. the Clausius Rankine cylce, you need to make a cut in the cycle using the cycle_closer or a sink and a source not to -overdetermine the system. Have a look in the +over-determine the system. Have a look in the :ref:`tutorial section ` to understand why this is important and how it can be implemented. If you have provided the correct number of parameters in your system and the -calculations stops after or even before the first iteration, there are four -frequent reasons for that: +calculations stops after or even before the first iteration, there might be a +couple reasons for that: - Sometimes, the fluid property database does not find a specific fluid property in the initialisation process, have you specified the values in the correct unit? -- Also, fluid property calculation might fail, if the fluid propagation - failed. Provide starting values for the fluid composition, especially, if - you are using drums, merges and splitters. - A linear dependency in the Jacobian matrix due to bad parameter settings - stops the calculation (overdetermining one variable, while missing out on + stops the calculation (over-determining one variable, while missing out on another). - A linear dependency in the Jacobian matrix due to bad starting values stops the calculation. -The first reason can be eliminated by carefully choosing the parametrization. +The first reason can be eliminated by carefully choosing the parametrisation. **A linear dependency due to bad starting values is often more difficult to** **resolve, and it may require some experience.** In many cases, the linear dependency is caused by equations, that require the **calculation of a** @@ -561,9 +547,9 @@ Did you experience other errors frequently and have a workaround/tips for resolving them? You are very welcome to contact us and share your experience for other users! -Postprocessing --------------- -A postprocessing is performed automatically after the calculation finished. You +Post-processing +--------------- +A post-processing is performed automatically after the calculation finished. You have further options: - Automatically create a documentation of your model. @@ -645,7 +631,7 @@ Results printing ^^^^^^^^^^^^^^^^ To print the results in your console use the :code:`print_results()` method. It will print tables containing the component, connection and bus properties. -Some of the results will be colored, the colored results indicate +Some results will be colored, the colored results indicate * if a parameter was specified as value before calculation. * if a parameter is out of its predefined value bounds (e.g. efficiency > 1). @@ -657,7 +643,7 @@ you can instead call the method the following way: .. code-block:: python - myplant.print_results(colored=False) + my_plant.print_results(colored=False) If you want to limit your printouts to a specific subset of components, connections and busses, you can specify the :code:`printout` parameter to block @@ -716,20 +702,20 @@ provides a results dictionary. .. code:: python # key for connections is 'Connection' - results_for_conns = myplant.results['Connection'] + results_for_conns = my_plant.results['Connection'] # keys for components are the respective class name, e.g. - results_for_turbines = myplant.results['Turbine'] - results_for_heat_exchangers = myplant.results['HeatExchanger'] + results_for_turbines = my_plant.results['Turbine'] + results_for_heat_exchangers = my_plant.results['HeatExchanger'] # keys for busses are the labels, e.g. a Bus labeled 'power input' - results_for_mybus = myplant.results['power input'] + results_for_mybus = my_plant.results['power input'] The index of the DataFrames is the connection's or component's label. .. code:: python - results_for_specific_conn = myplant.results['Connection'].loc['myconn'] - results_for_specific_turbine = myplant.results['Turbine'].loc['turbine 1'] - results_for_component_on_bus = myplant.results['power input'].loc['turbine 1'] + results_for_specific_conn = my_plant.results['Connection'].loc['myconn'] + results_for_specific_turbine = my_plant.results['Turbine'].loc['turbine 1'] + results_for_component_on_bus = my_plant.results['power input'].loc['turbine 1'] The full list of connection and component parameters can be obtained from the respective API documentation. @@ -742,7 +728,7 @@ save the network first. .. code:: python - myplant.save('mynetwork') + my_plant.export('mynetwork') This generates a folder structure containing all relevant files defining your network (general network information, components, connections, busses, diff --git a/docs/modules/ude.rst b/docs/modules/ude.rst index a9daf3beb..ee1488f4f 100644 --- a/docs/modules/ude.rst +++ b/docs/modules/ude.rst @@ -14,8 +14,8 @@ Getting started For an easy start, let's consider two different streams. The mass flow of both streams should be coupled within the model. There is already a possibility covering simple relations, i.e. applying value referencing with the -:py:class:`tespy.connections.connection.Ref` class. This class allows to -formulate simple linear relations: +:py:class:`tespy.connections.connection.Ref` class. This class allows +formulating simple linear relations: .. math:: @@ -30,35 +30,33 @@ flow of the second stream. 0 = \dot{m}_1 - \dot{m}_2^2 In order to apply this relation, we need to import the -:py:class:`tespy.tools.helpers.UserDefinedEquation` class into our model an +:py:class:`tespy.tools.helpers.UserDefinedEquation` class into our model and create an instance with the respective data. First, we set up the TESPy model. .. code-block:: python - from tespy.networks import Network - from tespy.components import Sink, Source - from tespy.connections import Connection - from tespy.tools.helpers import UserDefinedEquation + >>> from tespy.networks import Network + >>> from tespy.components import Sink, Source + >>> from tespy.connections import Connection + >>> from tespy.tools.helpers import UserDefinedEquation - fluids = ['water'] + >>> nw = Network(iterinfo=False) + >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') - nw = Network(fluids=fluids) - nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') + >>> so1 = Source('source 1') + >>> so2 = Source('source 2') + >>> si1 = Sink('sink 1') + >>> si2 = Sink('sink 2') - so1 = Source('source 1') - so2 = Source('source 2') - si1 = Sink('sink 1') - si2 = Sink('sink 2') + >>> c1 = Connection(so1, 'out1', si1, 'in1') + >>> c2 = Connection(so2, 'out1', si2, 'in1') - c1 = Connection(so1, 'out1', si1, 'in1') - c2 = Connection(so2, 'out1', si2, 'in1') + >>> nw.add_conns(c1, c2) - nw.add_conns(c1, c2) + >>> c1.set_attr(fluid={'water': 1}, p=1, T=50) + >>> c2.set_attr(fluid={'water': 1}, p=5, T=250, v=4) - c1.set_attr(fluid={'water': 1}, p=1, T=50) - c2.set_attr(fluid={'water': 1}, p=5, T=250, v=4) - -In the model both streams are well defined regarding pressure, enthalpy and +In the model both streams are well-defined regarding pressure, enthalpy and fluid composition. The second stream's mass flow is defined through specification of the volumetric flow, we are missing the mass flow of the connection :code:`c1`. As described, its value should be quadratic to the @@ -67,8 +65,8 @@ equation in a function which returns the residual value of the equation. .. code-block:: python - def my_ude(self): - return self.conns[0].m.val_SI - self.conns[1].m.val_SI ** 2 + >>> def my_ude(self): + ... return self.conns[0].m.val_SI - self.conns[1].m.val_SI ** 2 .. note:: @@ -112,14 +110,17 @@ derivatives to mass flow are not zero. - The derivative to mass flow of connection :code:`c1` is equal to :math:`1` - The derivative to mass flow of connection :code:`c2` is equal to - :math:`2 \cdot \dot{m}_2`. + :math:`-2 \cdot \dot{m}_2`. .. code-block:: python - def my_ude_deriv(self): - self.jacobian[self.conns[0]][0] = 1 - self.jacobian[self.conns[1]][0] = 2 * self.conns[1].m.val_SI - return self.jacobian + >>> def my_ude_deriv(self): + ... c0 = self.conns[0] + ... c1 = self.conns[1] + ... if c0.m.is_var: + ... ude.jacobian[c0.m.J_col] = 1 + ... if c1.m.is_var: + ... self.jacobian[c1.m.J_col] = -2 * self.conns[1].m.val_SI Now we can create our instance of the :code:`UserDefinedEquation` and add it to the network. The class requires four mandatory arguments to be passed: @@ -135,18 +136,19 @@ the network. The class requires four mandatory arguments to be passed: .. code-block:: python - ude = UserDefinedEquation('my ude', my_ude, my_ude_deriv, [c1, c2]) - nw.add_ude(ude) - nw.solve('design') - nw.print_results() + >>> ude = UserDefinedEquation('my ude', my_ude, my_ude_deriv, [c1, c2]) + >>> nw.add_ude(ude) + >>> nw.solve('design') + >>> round(c2.m.val_SI ** 2, 2) == round(c1.m.val_SI, 2) + True + >>> nw.del_ude(ude) More examples ------------- -After warm-up let's create some more complex examples, e.g. the -square root of the temperature of the second stream should be equal to the -the logarithmic value of the pressure squared divided by the mass flow of the -first stream. +After warm-up let's create some more complex examples, e.g. the square root of +the temperature of the second stream should be equal to the logarithmic value of +the pressure squared divided by the mass flow of the first stream. .. math:: @@ -159,17 +161,17 @@ the logarithmic value. .. code-block:: python - from tespy.tools.fluid_properties import T_mix_ph - import numpy as np + >>> import numpy as np - def my_ude(self): - return ( - T_mix_ph(self.conns[1].get_flow()) ** 0.5 - - np.log(abs(self.conns[0].p.val_SI ** 2 / self.conns[0].m.val_SI))) + >>> def my_ude(ude): + ... return ( + ... ude.conns[1].calc_T() ** 0.5 + ... - np.log(abs(ude.conns[0].p.val_SI ** 2 / ude.conns[0].m.val_SI)) + ... ) .. note:: - We use the absolute value inside of the logarithm expression to avoid + We use the absolute value inside the logarithm expression to avoid ValueErrors within the solution process as the mass flow is not restricted to positive values. @@ -180,18 +182,27 @@ respectively to calculate the partial derivatives. .. code-block:: python - from tespy.tools.fluid_properties import dT_mix_dph - from tespy.tools.fluid_properties import dT_mix_pdh - - def my_ude_deriv(self): - self.jacobian[self.conns[0]][0] = 1 / self.conns[0].m.val_SI - self.jacobian[self.conns[0]][1] = - 2 / self.conns[0].p.val_SI - T = T_mix_ph(self.conns[1].get_flow()) - self.jacobian[self.conns[1]][1] = ( - dT_mix_dph(self.conns[1].get_flow()) * 0.5 / (T ** 0.5)) - self.jacobian[self.conns[1]][2] = ( - dT_mix_pdh(self.conns[1].get_flow()) * 0.5 / (T ** 0.5)) - return self.jacobian + >>> from tespy.tools.fluid_properties import dT_mix_dph + >>> from tespy.tools.fluid_properties import dT_mix_pdh + + >>> def my_ude_deriv(self): + ... c0 = self.conns[0] + ... c1 = self.conns[1] + ... if c0.m.is_var: + ... self.jacobian[c0.m.J_col] = 1 / self.conns[0].m.val_SI + ... if c0.p.is_var: + ... self.jacobian[c0.p.J_col] = - 2 / self.conns[0].p.val_SI + ... T = c1.calc_T() + ... if c1.p.is_var: + ... self.jacobian[c1.p.J_col] = ( + ... dT_mix_dph(c1.p.val_SI, c1.h.val_SI, c1.fluid_data, c1.mixing_rule) + ... * 0.5 / (T ** 0.5) + ... ) + ... if c1.h.is_var: + ... self.jacobian[c1.h.J_col] = ( + ... dT_mix_pdh(c1.p.val_SI, c1.h.val_SI, c1.fluid_data, c1.mixing_rule) + ... * 0.5 / (T ** 0.5) + ... ) But, what if the analytical derivative is not available? You can make use of generic numerical derivatives using the inbuilt method :code:`numeric_deriv`. @@ -202,12 +213,35 @@ for the above derivatives would therefore look like this: .. code-block:: python - def my_ude_deriv(self): - self.jacobian[self.conns[0]][0] = self.numeric_deriv('m', 0) - self.jacobian[self.conns[0]][1] = self.numeric_deriv('p', 0) - self.jacobian[self.conns[1]][1] = self.numeric_deriv('p', 1) - self.jacobian[self.conns[1]][2] = self.numeric_deriv('h', 1) - return self.jacobian + >>> def my_ude_deriv(ude): + ... c0 = ude.conns[0] + ... c1 = ude.conns[1] + ... if c0.m.is_var: + ... ude.jacobian[c0.m.J_col] = ude.numeric_deriv('m', c0) + ... if c0.p.is_var: + ... ude.jacobian[c0.p.J_col] = ude.numeric_deriv('p', c0) + ... if c1.p.is_var: + ... ude.jacobian[c1.p.J_col] = ude.numeric_deriv('p', c1) + ... if c1.h.is_var: + ... ude.jacobian[c1.h.J_col] = ude.numeric_deriv('h', c1) + + >>> ude = UserDefinedEquation('ude numerical', my_ude, my_ude_deriv, [c1, c2]) + >>> nw.add_ude(ude) + >>> nw.set_attr(m_range=[.1, 100]) # stabilize algorithm + >>> nw.solve('design') + >>> round(c1.m.val, 2) + 1.17 + + >>> c1.set_attr(p=None, m=1) + >>> nw.solve('design') + >>> round(c1.p.val, 3) + 0.926 + + >>> c1.set_attr(p=1) + >>> c2.set_attr(T=None) + >>> nw.solve('design') + >>> round(c2.T.val, 1) + 257.0 Obviously, the downside is a slower performance of the solver, as for every :code:`numeric_deriv` call the function will be evaluated fully twice @@ -228,30 +262,37 @@ instance must therefore be changed as below. .. code-block:: python - from tespy.tools.fluid_properties import h_mix_pQ - from tespy.tools.fluid_properties import dh_mix_dpQ - - def my_ude(self): - a = self.params['a'] - b = self.params['b'] - return ( - a * (self.conns[1].h.val_SI - self.conns[0].h.val_SI) - - (self.conns[1].h.val_SI - h_mix_pQ(self.conns[0].get_flow(), b))) - - def my_ude_deriv(self): - a = self.params['a'] - b = self.params['b'] - self.jacobian[self.conns[0]][1] = dh_mix_dpQ( - self.conns[0].get_flow(), b) - self.jacobian[self.conns[0]][2] = -a - self.jacobian[self.conns[1]][2] = a - 1 - return self.jacobian - - ude = UserDefinedEquation( - 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1}) - - -One more example (using a CharLine for datapoint interpolation) can be found in + >>> from tespy.tools.fluid_properties import h_mix_pQ + >>> from tespy.tools.fluid_properties import dh_mix_dpQ + + >>> def my_ude(self): + ... a = self.params['a'] + ... b = self.params['b'] + ... c0 = self.conns[0] + ... c1 = self.conns[1] + ... return ( + ... a * (c1.h.val_SI - c0.h.val_SI) - + ... (c1.h.val_SI - h_mix_pQ(c0.p.val_SI, b, c0.fluid_data)) + ... ) + + >>> def my_ude_deriv(self): + ... a = self.params['a'] + ... b = self.params['b'] + ... c0 = self.conns[0] + ... c1 = self.conns[1] + ... if c0.p.is_var: + ... self.jacobian[c0.p.J_col] = dh_mix_dpQ(c0.p.val_SI, b, c0.fluid_data) + ... if c0.h.is_var: + ... self.jacobian[c0.h.J_col] = -a + ... if c1.p.is_var: + ... self.jacobian[c1.p.J_col] = a - 1 + + >>> ude = UserDefinedEquation( + ... 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1} + ... ) + + +One more example (using a CharLine for data point interpolation) can be found in the API documentation of class :py:class:`tespy.tools.helpers.UserDefinedEquation`. @@ -265,12 +306,14 @@ latex equation string. For example, the last equation from above: .. code-block:: python latex = ( - r'0 = a \cdot \left(h_2 - h_1 \right) - ' - r'\left(h_2 - h\left(p_1, x=b \right)\right)') + r'0 = a \cdot \left(h_2 - h_1 \right) - ' + r'\left(h_2 - h\left(p_1, x=b \right)\right)' + ) ude = UserDefinedEquation( - 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1}, - latex={'equation': latex}) + 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1}, + latex={'equation': latex} + ) The documentation will also create figures of :code:`CharLine` and :code:`CharMap` objects provided. To add these, adjust the code like this. @@ -279,10 +322,10 @@ Provide the :code:`CharLine` and :code:`CharMap` objects within a list. .. code-block:: python ude = UserDefinedEquation( - 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1}, - latex={ - 'equation': latex, - 'lines': [charline1, charline2], - 'maps': [map1] - } + 'my ude', my_ude, my_ude_deriv, [c1, c2], params={'a': 0.5, 'b': 1}, + latex={ + 'equation': latex, + 'lines': [charline1, charline2], + 'maps': [map1] + } ) diff --git a/docs/references.bib b/docs/references.bib index b64482265..47210fdab 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -391,3 +391,14 @@ @phdthesis{Ahrendts1974 year={1977}, school={Ruhr-Universität Bochum} } + +@book{Knacke1991, + title={Thermochemical Properties of Inorganic Substances}, + author={Knacke, O. and Kubaschewski, O. and Hesselmann, K.}, + number={Bd. 1}, + isbn={9783514003637}, + lccn={91025087}, + series={Thermochemical Properties of Inorganic Substances}, + year={1991}, + publisher={Springer-Verlag} +} diff --git a/docs/requirements.txt b/docs/requirements.txt index 69d0a7e5f..96ccfeaab 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ furo -sphinx>=1.3 +sphinx>=7.2.2 sphinxcontrib-bibtex sphinx-copybutton sphinx-design diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 89837d250..503f4950a 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -16,8 +16,8 @@ At the example of different heat pump topologies, you will learn to setting. Furthermore, we introduce the coupling of TESPy with pygmo in order to create -an optimization problem, which optimizes thermal efficiency of a clausius -rankine power plant. Also, there is a tutorial on the implementation of an air +an optimization problem, which optimizes thermal efficiency of a Clausius +Rankine power plant. Also, there is a tutorial on the implementation of an air source heat pump in an energy system dispatch optimization problem using `oemof-solph `__. For that tutorial an air source heat pump is implemented in various details of modeling complexity and diff --git a/docs/tutorials/district_heating.rst b/docs/tutorials/district_heating.rst index f241f4b24..1246cf964 100644 --- a/docs/tutorials/district_heating.rst +++ b/docs/tutorials/district_heating.rst @@ -1,7 +1,7 @@ .. _tespy_tutorial_district_heating_label: -Distric heating system ----------------------- +District heating system +----------------------- The district heating system is a great example for the usage of flexible user-defined subsystems. The example system and data are based on the district diff --git a/docs/tutorials/heat_pump_exergy.rst b/docs/tutorials/heat_pump_exergy.rst index c9038772b..9164c7ce2 100644 --- a/docs/tutorials/heat_pump_exergy.rst +++ b/docs/tutorials/heat_pump_exergy.rst @@ -11,7 +11,7 @@ ground-coupled heat pump (GCHP). In addition, various post-processing options are presented. To investigate the impact of refrigerant choice on COP and exergetic efficiency, two Python scripts of the same network with different refrigerants (NH3 and R410A) are created. Finally, the influence of varying -different parameters on COP and exergetic efficiency is investigated and +different parameters on COP and exergy efficiency is investigated and plotted. .. note:: @@ -26,7 +26,7 @@ plotted. Since there is an existing tutorial for :ref:`creating a heat pump `, this tutorial -starts with the explanations for setting up the exergy analysis. However note, +starts with the explanations for setting up the exergy analysis. Note however, that the heat pump model differs slightly in structure from the model in the previous tutorial. All related Python scripts of the fully working GCHP-model are listed in the following: @@ -87,11 +87,10 @@ is modeled using :code:`HeatExchanger` instance. In total, the TESPy model consists of 11 components. In real systems, the circulating brine in the geothermal collector usually -consists of a mixture of water and antifreeze. Since the calculation with -mixtures of incompressible fluids is not yet fully implemented in TESPy, pure -water is used as the circulating fluid in this network. In fact, some -geothermal collectors are filled with water, provided that the ground -temperature is high enough throughout the year, such as in :cite:`Chen2015`. +consists of a mixture of water and antifreeze. Pure water is used as the +circulating fluid in this example. In fact, some geothermal collectors are +filled with water, provided that the ground temperature is high enough +throughout the year, such as in :cite:`Chen2015`. The following parameter specifications were made for the design case calculation: @@ -113,12 +112,12 @@ values is necessary to obtain a numerical solution for the first calculation. In this tutorial, the given code examples are shown exemplary for the model with NH3 as refrigerant only. -The units used and the ambient state are defined as follows: +The units used, and the ambient state are defined as follows: .. code-block:: python nw = Network( - fluids=['water', 'NH3'], T_unit='C', p_unit='bar', + T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s' ) @@ -128,9 +127,9 @@ The units used and the ambient state are defined as follows: For the model using R410A as refrigerant, the fluid definition is accordingly :code:`'R410A'` instead of :code:`'NH3'`. -The temperature of the heating system feed flow is set to 40°C in design +The temperature of the heating system feed flow is set to 40 °C in design calculation. The difference between feed and return flow temperature is kept -constant at 5°C. Therefore the return flow is set to 35°C. +constant at 5 °C. Therefore, the return flow is set to 35 °C. The geothermal heat collector temperature is defined as follows: @@ -139,9 +138,9 @@ The geothermal heat collector temperature is defined as follows: Tgeo = 9.5 :code:`Tgeo` is the mean geothermal temperature. The difference between -feed and return flow temperature is kept constant at 3°C. Therefore, the feed -flow temperature in the design calculation is set to :code:`Tgeo + 1.5°C` and -the return flow temperature is set to :code:`Tgeo - 1.5°C`. +feed and return flow temperature is kept constant at 3 °C. Therefore, the feed +flow temperature in the design calculation is set to :code:`Tgeo + 1.5 °C` and +the return flow temperature is set to :code:`Tgeo - 1.5 °C`. The complete Python code of the TESPy models is available in the scripts :download:`NH3.py ` with NH3 as @@ -231,10 +230,10 @@ saved with the code shown below. The resulting fluid property diagram is shown in the figure above. It can easily be seen, that the evaporator slightly overheats the working fluid, while -the it leaves the condenser in saturated liquid state. The working fluid -temperature after leaving the compressor is quite high with far more than -100 °C given the heat sink only requires a temperature of only 40 °C. In -comparison, the R410A leaves the compressor at about 75 °C. +it leaves the condenser in saturated liquid state. The working fluid temperature +after leaving the compressor is quite high with far more than 100 °C given the +heat sink only requires a temperature of only 40 °C. In comparison, the R410A +leaves the compressor at about 75 °C. More examples of creating fluid property diagrams can be found in the fluprodia documentation referenced above. diff --git a/docs/tutorials/heat_pump_steps.rst b/docs/tutorials/heat_pump_steps.rst index 17a128c20..26fd0f722 100644 --- a/docs/tutorials/heat_pump_steps.rst +++ b/docs/tutorials/heat_pump_steps.rst @@ -23,12 +23,12 @@ We provide the full script presented in this tutorial here: Task ^^^^ This tutorial introduces you in how to model a heat pump in TESPy. You can see -the plants topology in the figure. +the plant's topology in the figure. The main purpose of the heat pump is to deliver heat e.g. for the consumers of a heating system. Thus, the heat pump's parameters will be set in a way, which supports this target. Generally, if systems are getting more complex, it is -highly recommended to set up your plant in incremental steps. This tutorial +highly recommended setting up your plant in incremental steps. This tutorial divides the plant in three sections: The consumer part, the valve and the evaporator and the compressor as last element. Each new section will be appended to the existing ones. @@ -41,12 +41,11 @@ Set up a Network ^^^^^^^^^^^^^^^^ First, we have to create an instance of the :py:class:`tespy.networks.network.Network` class. The network is the main -container of the model and will be required in all following sections. First, -it is necessary to specify a list of the fluids used in the plant. In this +container of the model and will be required in all following sections. In this example we will work with water (H\ :sub:`2`\O) and ammonia (NH\ :sub:`3`\). Water is used for the cold side of the heat exchanger, for the consumer and for the hot side of the environmental temperature. Ammonia is used as -refrigerant within the heat pump circuit. If you don’t specify the unit +refrigerant within the heat pump circuit. If you don't specify the unit system, the variables are set to SI-Units. We also keep the working fluid a variable to make reusing the script with a different working fluid easy. @@ -78,9 +77,9 @@ Components ++++++++++ We will start with the consumer as the plant will be designed to deliver a specific heat flow. From the figure above you can determine the components of -the consumer system: condenser, pump and the consumer ( -:py:class:`tespy.components.heat_exchangers.simple.HeatExchangerSimple` -). Additionally we need a source and a sink for the consumer and the heat pump +the consumer system: condenser, pump and the consumer +(:py:class:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger`). +Additionally, we need a source and a sink for the consumer and the heat pump circuit respectively. We will import all necessary components already in the first step, so the imports will not need further adjustment. @@ -153,17 +152,17 @@ decides the overall mass flow in the systems. In order to calculate this network further parametrization is necessary, as e.g. the fluids are not determined yet: At the hot inlet of the condenser we -define the temperature, pressure and the fluid vector. A good guess for +define the temperature, pressure and the fluid informaton. A good guess for pressure can be obtained from CoolProp's PropsSI function. We know that the condensation temperature must be higher than the consumer's feed flow -temperature. Therefore we can set the pressure to a slightly higher value of +temperature. Therefore, we can set the pressure to a slightly higher value of that temperature's corresponding condensation pressure. -The same needs to be done for the consumer cycle. We suggest to set -the parameters at the pump's inlet. On top, we assume that the consumer -requires a constant inlet temperature. The :code:`CycleCloser` automatically -makes sure, that the fluid's state at the consumer's outlet is the same as at -the pump's inlet. +The same needs to be done for the consumer cycle. We suggest setting the +parameters at the pump's inlet. On top, we assume that the consumer requires a +constant inlet temperature. The :code:`CycleCloser` automatically makes sure, +that the fluid's state at the consumer's outlet is the same as at the pump's +inlet. .. literalinclude:: /../tutorial/advanced/stepwise.py :language: python @@ -258,7 +257,7 @@ connections to the model: .. attention:: - The drum is special component, it has an inbuilt CycleCloser, therefore + The drum is special component, it has an inbuilt CycleCloser, therefore, although we are technically forming a cycle at the drum's outlet 1 to its inlet 2, we do not need to include a CycleCloser here. @@ -285,7 +284,7 @@ Next step is the connection parametrization: The pressure in the drum and the enthalpy of the wet steam reentering the drum need to be determined. For the enthalpy we can specify the vapor mass fraction :code:`x` determining the degree of evaporation. On the hot side inlet of the superheater we define the -temperature, pressure and the fluid. At last we have to fully determine the +temperature, pressure and the fluid. At last, we have to fully determine the state of the incoming fluid at the superheater's hot side. .. literalinclude:: /../tutorial/advanced/stepwise.py @@ -385,7 +384,7 @@ For the ambient side, we set temperature, pressure and fluid at connection 11. On top of that, we can specify the temperature of the ambient water after leaving the intermittent cooler. -With readding of connection 0 we have to set the fluid and the pressure again, +With re-adding of connection 0 we have to set the fluid and the pressure again, but not the temperature value, because this value will be a result of the condensation pressure and the given enthalpy at the compressor's outlet. @@ -417,21 +416,21 @@ working fluid does not start to condensate at the intermittent cooler. :start-after: [sec_16] :end-before: [sec_17] -Calculate Partload Performance -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -After setting up the full system, we want to predict partload operation at -different values for the consumer's heat demand. Some of the values utilized -in the previous setup will change, if a component is not operated at its -design point. This is individual to every system, so the designer has to -answer the question: Which parameters are design point parameters and how does -the component perform at a different operation point. +Calculate Part Load Performance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +After setting up the full system, we want to predict part load operation at +different values for the consumer's heat demand. Some values utilized in the +previous setup will change, if a component is not operated at its design point. +This is individual to every system, so the designer has to answer the question: +Which parameters are design point parameters and how does the component perform +at a different operation point. .. tip:: To make the necessary changes to the model, we can specify a design and an offdesign attribute, both lists containing component or connection parameter names. All parameters specified in the design attribute of a - component or connection, will be unset in a offdesign calculation, all + component or connection, will be unset in an offdesign calculation, all parameters specified in the offdesign attribute of a component or connection will be set for the offdesign calculation. The value for these parameters is the value derived from the design-calculation. @@ -440,7 +439,7 @@ The changes we want to apply can be summarized as follows: - All heat exchangers should be calculated based on their heat transfer coefficient with a characteristic for correction of that value depending - on the change of mass flow (:code:`kA_char`). Therefore terminal temperature + on the change of mass flow (:code:`kA_char`). Therefore, terminal temperature value specifications need to be added to the design parameters. Also, the temperature at connection 14 cannot be specified anymore, since it will be a result of the intermittent cooler's characteristics. @@ -475,7 +474,7 @@ are available in the :ref:`tespy.data ` module. :ref:`TESPy components section `. Finally, we can change the heat demand and run several offdesign calculations -to calculate the partload COP value. +to calculate the part load COP value. .. literalinclude:: /../tutorial/advanced/stepwise.py :language: python diff --git a/docs/tutorials/pygmo_optimization.rst b/docs/tutorials/pygmo_optimization.rst index 2a4bf3fc1..eaad4278f 100644 --- a/docs/tutorials/pygmo_optimization.rst +++ b/docs/tutorials/pygmo_optimization.rst @@ -39,15 +39,15 @@ What is PyGMO? ^^^^^^^^^^^^^^ PyGMO (Python Parallel Global Multiobjective Optimizer, :cite:`Biscani2020`) -is a library that provides a large number of evolutionary optimization -algorithms. PyGMO can be used to solve constrained, unconstrained, single -objective and multi objective problems. +is a library that provides numerous evolutionary optimization algorithms. PyGMO +can be used to solve constrained, unconstrained, single objective and multi +objective problems. Evolutionary Algorithms +++++++++++++++++++++++ Evolutionary Algorithms (EA) are optimization algorithms inspired by biological -evolution. In a given population the algorithm uses the so called fitness +evolution. In a given population the algorithm uses the so-called fitness function to determine the quality of the solutions to each individual (set of decision variables) problem. The best possible solution of the population is called champion. Via mutation, recombination and selection your population @@ -109,9 +109,9 @@ First, we set up the class with the TESPy network. Next, we add the methods :code:`get_param`, :code:`solve_model` and -:code:`get_objective`. On top of that, we add a setter working in a similar -way as the getter. The objective is to maximize thermal efficiency as defined -in the equation below. +:code:`get_objective`. On top of that, we add a setter working similarly as the +getter. The objective is to maximize thermal efficiency as defined in the +equation below. .. math:: @@ -126,7 +126,7 @@ in the equation below. We also have to make sure, only the results of physically feasible solutions are returned. In case we have infeasible solutions, we can simply return :code:`np.nan`. An infeasible solution is obtained in case the power of a -turbine is positive, the power of a pump is negative or the heat exchanged +turbine is positive, the power of a pump is negative, or the heat exchanged in any of the preheaters is positive. We also check, if the calculation does converge. @@ -205,7 +205,7 @@ In our run, we got: Figure: Scatter plot for all individuals during the optimization -Finally, you can access the individuals in each of the generations and you +Finally, you can access the individuals in each of the generations, and you can have a look at you population. For more info on the population API please visit the pygmo documentation. diff --git a/docs/tutorials/starting_values.rst b/docs/tutorials/starting_values.rst index b3d8c22dd..8d105e87b 100644 --- a/docs/tutorials/starting_values.rst +++ b/docs/tutorials/starting_values.rst @@ -53,7 +53,7 @@ ambient. You can see the plant topology in the figure below. The system consists of a consumer system, a valve, an evaporator system, a compressor and additionally an internal heat exchanger. In order to simulate this heat pump, the TESPy model has to be built up. First, the network has to -be initialized and the refrigerants used have to be specified. This example +be initialized, and the refrigerants used have to be specified. This example shows how to make the heat pump model work with a variety of working fluids with water on both the heat source and heat sink side of the system. diff --git a/docs/whats_new.rst b/docs/whats_new.rst index d9c1ddba7..835278fa1 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -3,6 +3,7 @@ What's New Discover noteable new features and improvements in each release +.. include:: whats_new/v0-7-0.rst .. include:: whats_new/v0-6-3.rst .. include:: whats_new/v0-6-2.rst .. include:: whats_new/v0-6-1.rst diff --git a/docs/whats_new/v0-7-0.rst b/docs/whats_new/v0-7-0.rst new file mode 100644 index 000000000..158b383ef --- /dev/null +++ b/docs/whats_new/v0-7-0.rst @@ -0,0 +1,85 @@ +v0.7.0 - Development in progress +++++++++++++++++++++++++++++++++ + +For version 0.7.0 TESPy has undergone a large refactoring of its back end: + +New Features +############ + +Fluid Properties +---------------- + +The implementation of the fluid property back end was modularized and is now +much more flexible. The most notable new features are: + +- It is possible to use the same fluid name with different fluid property back + ends in different parts of a single network, e.g. in a Rankine Cycle the main + cycle can be calculated using standard water fluid properties and in the + cooling cycle water may be used as incompressible medium. +- CoolProp's binary incompressible mixtures are now supported. +- The user can implement their own fluid property equations or toolboxes by + masquerading the calls in a standard API inheriting from the new + `FluidPropertyWrapper` class. CoolProp remains the standard back end, but you + may also use other back ends with this feature. +- Similarly, the mixture model can be exchanged by implementing custom mixing + rules for fluid mixtures. +- It is not necessary anymore, to specify the full fluid vector if the sum of + all fixed fluid mass fractions is equal to 1, e.g. if the old specification + was :code:`fluid={"H2O": 1, "Air": 0}` you can now specify + :code:`fluid={"H2O": 1}`. The list of fluids is passed to the `Network` class + anymore, :code:`Network(fluids["H2O", "Air"])` becomes :code:`Network()`. + +Performance Improvements +------------------------ + +Several performance improvements have been made: + +- Primary variables are not strictly tied to all connections anymore: + + - Any directly specified value removes the respective variable from the + system's variables. For example, a user specified pressure value was part of + the system's variables previously but not touched in the Newton iterations. + Now the variable is directly eliminated effectively reducing the size of the + problem. The same is true for all variables, i.e. mass flow, pressure, + enthalpy and fluid mass fractions. + - If combinations of pressure and temperature, vapor quality or similar are + specified on a single connection, the pressure and enthalpy are pre-solved + if feasible eliminating them from the variable space and eliminating the + respective (e.g. temperature) equation from the equations. + - The plant is subdivided into two types of branches: + + 1. Branches with a single mass flow (connections related to each other in a + way, that their mass flow must be the same). Here the variable space is + reduced to a single mass flow variable. + 2. Branches with identical fluid composition (similar to mass flows, but + e.g. splitters, drums, droplet separators do not change the fluid + composition as well) can also only have a single fluid vector as a + variable and not one per connection. + +- Together with the above changes all partial derivatives now only need to be + calculated, in case a mass flow, pressure, enthalpy or the fluid mass fraction + is a system variable. + +General Improvements +-------------------- + +The code has been simplified and clean up in a lot of places to improve +readability and maintenance. + + +Breaking Changes +################ + +The release introduces two (known) breaking changes: + +- The structure for saved network states has changed to the minimum necessary + export. Connections only need their label, their parameter values and the + respective units. For that reason, the export of networks to import them at a + different place using the `load_network` functionality has changed as well. If + you want to export a network to load it again, you have to use the `export` + method instead of the `save` method of the network. +- Support for older Python versions (smaller than 3.9) has been dropped. + +Contributors +############ +- Francesco Witte (`@fwitte `__) diff --git a/pyproject.toml b/pyproject.toml index 23cf6d7e1..bc3e17b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,118 @@ [build-system] -requires = [ - "setuptools>=30.3.0", - "wheel", - "setuptools_scm>=3.3.1", +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.sdist] +include = [ + "CHANGELOG.rst", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "MANIFEST.in", + "LICENSE*", + "PULL_REQUEST_TEMPLATE.md", + ".coveragerc", + ".editorconfig", + ".pep8speaks.yml", + ".readthedocs.yml", + "paper.bib", + "paper.md", + "tox.ini", + "docs/", + "tests/", + "tutorial/", ] +exclude = ["docs/_build"] + +[project] +name = "tespy" +version = "0.7.0.dev0" +description = "Thermal Engineering Systems in Python (TESPy)" +readme = "README.rst" +authors = [ + {name = "Francesco Witte", email = "tespy@witte.sh"}, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering", +] +requires-python = ">=3.9" +dependencies = [ + "CoolProp>=6.4,<7", + "jinja2", + "matplotlib>=3.2.1,<4", + "numpy>=1.13.3,<2", + "pandas>=1.3.0,<3", + "tabulate>=0.8.2,<0.9", +] +license = {text = "MIT"} + +[project.urls] +Homepage = "https://github.com/oemof/tespy" +Documentation = "https://tespy.readthedocs.io/" +Changelog = "https://tespy.readthedocs.io/en/main/whats_new.html" +"Issue Tracker" = "https://github.com/oemof/tespy/issues" + +[project.optional-dependencies] +dev = [ + "build", + "flit", + "furo", + "iapws", + "pyromat", + "pytest", + "sphinx>=7.2.2", + "sphinx-copybutton", + "sphinx-design", + "sphinxcontrib.bibtex", + "tox", +] + +[tool.pytest.ini_options] +python_files = [ + "test_*.py", + "*_test.py", + "tests.py", +] +addopts = """ + -ra + --strict-markers + --doctest-modules + --doctest-glob=\"*.rst\" + --tb=short + --pyargs + --ignore=docs/conf.py + --ignore=docs/scripts +""" +testpaths = [ + "src/", + "tests/", + "docs/", +] + +[tool.isort] +force_single_line = true +line_length = 120 +known_first_party = "tespy" +default_section = "THIRDPARTY" +forced_separate = "test_tespy" +skip = "migrations" + +[tool.coverage.run] +branch = true +source = ["src"] +parallel = true + +[tool.coverage.report] +show_missing = true +precision = 2 +omit = ["migrations"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1d7d97e96..000000000 --- a/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -[bdist_wheel] -universal = 1 - -[flake8] -max-line-length = 140 -exclude = */migrations/*, docs/conf.py - -[metadata] -description_file = README.rst - -[options] -tests_require = pytest - -[aliases] -test = pytest - -[tool:pytest] -norecursedirs = - migrations - -python_files = - test_*.py - *_test.py - tests.py -addopts = - -ra - --strict-markers - --doctest-modules - --doctest-glob=\*.rst - --tb=short - --pyargs -testpaths = - tespy - tests/ - -[tool:isort] -force_single_line = True -line_length = 120 -known_first_party = tespy -default_section = THIRDPARTY -forced_separate = test_tespy -not_skip = __init__.py -skip = migrations - -[matrix] - -python_versions = - py37 - py38 - py39 - py310 - -coverage_flags = - cover: true - nocov: false - -environment_variables = - - diff --git a/setup.py b/setup.py deleted file mode 100644 index 4e5ee5f75..000000000 --- a/setup.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function - -import io -import re -from glob import glob -from os.path import basename -from os.path import dirname -from os.path import join -from os.path import splitext - -from setuptools import find_namespace_packages -from setuptools import setup - - -def read(*names, **kwargs): - with io.open( - join(dirname(__file__), *names), - encoding=kwargs.get('encoding', 'utf8') - ) as fh: - return fh.read() - - -setup( - name='TESPy', - version='0.6.3', - license='MIT', - description='Thermal Engineering Systems in Python (TESPy)', - long_description='%s' % ( - re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub( - '', read('README.rst') - ) - ), - author='Francesco Witte', - author_email='tespy@witte.sh', - url='https://github.com/oemof/tespy', - packages=find_namespace_packages('src'), - package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], - include_package_data=True, - zip_safe=False, - package_data={'src': ['*.json']}, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Unix', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Scientific/Engineering', - ], - project_urls={ - 'Documentation': 'https://tespy.readthedocs.io/', - 'Changelog': 'https://tespy.readthedocs.io/en/main/whats_new.html', - 'Issue Tracker': 'https://github.com/oemof/tespy/issues', - }, - python_requires='>=3.7', - install_requires=[ - 'CoolProp>=6.4,<7', - 'matplotlib>=3.2.1,<4', - 'numpy>=1.13.3,<2', - 'pandas>=1.3.0,<2', - 'tabulate>=0.8.2,<0.9' - ], - extras_require={ - 'dev': [ - 'furo', - 'pytest', - 'sphinx', - 'sphinx-copybutton', - 'sphinx-design', - 'sphinxcontrib.bibtex', - 'tox', - ], - 'dummy': ['tespy'] - } -) diff --git a/src/tespy/__init__.py b/src/tespy/__init__.py index 0d5f35dea..0c88b0858 100644 --- a/src/tespy/__init__.py +++ b/src/tespy/__init__.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -from pkg_resources import resource_filename +import importlib.resources +import os -__datapath__ = resource_filename('tespy', 'data/') -__version__ = '0.6.3 - Leidenfrost\'s Library' +__datapath__ = os.path.join(importlib.resources.files("tespy"), "data") +__version__ = '0.7.0 - dev' # tespy data and connections import from . import connections # noqa: F401 diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index 89ab949c8..d459df9af 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -7,7 +7,6 @@ from .combustion.base import CombustionChamber # noqa: F401 from .combustion.diabatic import DiabaticCombustionChamber # noqa: F401 from .combustion.engine import CombustionEngine # noqa: F401 -from .customs.orc_evaporator import ORCEvaporator # noqa: F401 from .heat_exchangers.base import HeatExchanger # noqa: F401 from .heat_exchangers.condenser import Condenser # noqa: F401 from .heat_exchangers.desuperheater import Desuperheater # noqa: F401 diff --git a/src/tespy/components/basics/cycle_closer.py b/src/tespy/components/basics/cycle_closer.py index 9dedc7e9b..23aba26da 100644 --- a/src/tespy/components/basics/cycle_closer.py +++ b/src/tespy/components/basics/cycle_closer.py @@ -74,7 +74,7 @@ class CycleCloser(Component): >>> from tespy.components import CycleCloser, Pipe, Pump >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> nw = Network(['water'], p_unit='bar', T_unit='C', iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', iterinfo=False) >>> pi = Pipe('pipe') >>> pu = Pump('pump') >>> cc = CycleCloser('cycle closing component') @@ -97,7 +97,7 @@ def component(): return 'cycle closer' @staticmethod - def get_variables(): + def get_parameters(): return { 'mass_deviation': dc_cp(val=0, max_val=1e-3, is_result=True), 'fluid_deviation': dc_cp(val=0, max_val=1e-5, is_result=True) @@ -127,67 +127,40 @@ def inlets(): def outlets(): return ['out1'] - def preprocess(self, nw, num_eq=0): - super().preprocess(nw, num_eq) - self._propagation_start = False - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Fluid propagation to target stops here. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if (not entry_point and inconn == start) or self._propagation_start: - return - - self._propagation_start = True - - conn_idx = self.inl.index(inconn) - outconn = self.outl[conn_idx] - - for fluid, x in inconn.fluid.val.items(): - if (not outconn.fluid.val_set[fluid] and - not outconn.good_starting_values): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - self._propagation_start = False - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Fluid propagation to source stops here. + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + outconn = self.outl[0] + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. + return {outconn.label: branch} - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if (not entry_point and outconn == start) or self._propagation_start: - return + def start_fluid_wrapper_branch(self): + outconn = self.outl[0] + branch = { + "connections": [outconn], + "components": [self] + } + outconn.target.propagate_wrapper_to_target(branch) - self._propagation_start = True - conn_idx = self.outl.index(outconn) - inconn = self.inl[conn_idx] + return {outconn.label: branch} - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x + def propagate_to_target(self, branch): + return - inconn.source.propagate_fluid_to_source(inconn, start) + def propagate_wrapper_to_target(self, branch): + branch["components"] += [self] + return + def preprocess(self, num_nw_vars): + super().preprocess(num_nw_vars) self._propagation_start = False def calc_parameters(self): diff --git a/src/tespy/components/basics/sink.py b/src/tespy/components/basics/sink.py index fac473a39..3f3d531c9 100644 --- a/src/tespy/components/basics/sink.py +++ b/src/tespy/components/basics/sink.py @@ -69,19 +69,11 @@ def inlets(): def get_mandatory_constraints(): return {} - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Fluid propagation to target stops here. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. + def propagate_to_target(self, branch): + return - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ + def propagate_wrapper_to_target(self, branch): + branch["components"] += [self] return def exergy_balance(self, T0): @@ -111,4 +103,4 @@ def exergy_balance(self, T0): "massless": 0 } self.E_D = np.nan - self.epsilon = np.nan + self.epsilon = self._calc_epsilon() diff --git a/src/tespy/components/basics/source.py b/src/tespy/components/basics/source.py index e65d5453d..35719fa34 100644 --- a/src/tespy/components/basics/source.py +++ b/src/tespy/components/basics/source.py @@ -66,23 +66,33 @@ def outlets(): return ['out1'] @staticmethod - def get_mandatory_constraints(): - return {} + def is_branch_source(): + return True + + def start_branch(self): + outconn = self.outl[0] + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Fluid propagation to source stops here. + return {outconn.label: branch} - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. + def start_fluid_wrapper_branch(self): + outconn = self.outl[0] + branch = { + "connections": [outconn], + "components": [self] + } + outconn.target.propagate_wrapper_to_target(branch) - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - return + return {outconn.label: branch} + + @staticmethod + def get_mandatory_constraints(): + return {} def exergy_balance(self, T0): r"""Exergy balance calculation method of a source. @@ -111,4 +121,4 @@ def exergy_balance(self, T0): "massless": 0 } self.E_D = np.nan - self.epsilon = np.nan + self.epsilon = self._calc_epsilon() diff --git a/src/tespy/components/basics/subsystem_interface.py b/src/tespy/components/basics/subsystem_interface.py index 7f86b951d..be0d433ca 100644 --- a/src/tespy/components/basics/subsystem_interface.py +++ b/src/tespy/components/basics/subsystem_interface.py @@ -90,8 +90,7 @@ class SubsystemInterface(Component): >>> from tespy.components import Sink, Source, SubsystemInterface >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> fluids = ['H2O', 'N2'] - >>> nw = Network(fluids=fluids) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> so1 = Source('source 1') >>> si1 = Sink('sink 1') @@ -110,8 +109,8 @@ class SubsystemInterface(Component): >>> inc2 = Connection(so2, 'out1', IF, 'in2') >>> outg2 = Connection(IF, 'out2', si2, 'in1') >>> nw.add_conns(inc1, outg1, inc2, outg2) - >>> inc1.set_attr(fluid={'H2O': 1, 'N2': 0}, T=40, p=3, m=100) - >>> inc2.set_attr(fluid={'H2O': 0, 'N2': 1}, T=60, p=1, v=10) + >>> inc1.set_attr(fluid={'H2O': 1}, T=40, p=3, m=100) + >>> inc2.set_attr(fluid={'N2': 1}, T=60, p=1, v=10) >>> nw.solve('design') >>> inc1.m.val_SI == outg1.m.val_SI True @@ -129,14 +128,6 @@ def component(): def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': self.num_i}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * self.num_i}, 'pressure_equality_constraints': { 'func': self.pressure_equality_func, 'deriv': self.pressure_equality_deriv, @@ -152,7 +143,7 @@ def get_mandatory_constraints(self): } @staticmethod - def get_variables(): + def get_parameters(): return {'num_inter': dc_simple()} def inlets(self): diff --git a/src/tespy/components/combustion/base.py b/src/tespy/components/combustion/base.py index e460e3cde..ed48e5aba 100644 --- a/src/tespy/components/combustion/base.py +++ b/src/tespy/components/combustion/base.py @@ -10,6 +10,7 @@ SPDX-License-Identifier: MIT """ +import itertools import CoolProp.CoolProp as CP import numpy as np @@ -20,10 +21,9 @@ from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import h_mix_pT from tespy.tools.fluid_properties import s_mix_pT +from tespy.tools.fluid_properties.helpers import fluid_structure from tespy.tools.global_vars import combustion_gases -from tespy.tools.global_vars import molar_masses from tespy.tools.helpers import TESPyComponentError -from tespy.tools.helpers import fluid_structure from tespy.tools.helpers import fluidalias_in_list @@ -117,11 +117,9 @@ class CombustionChamber(Component): >>> from tespy.components import Sink, Source, CombustionChamber >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools.fluid_properties import T_bp_p + >>> from tespy.tools.fluid_properties import T_sat_p >>> import shutil - >>> fluid_list = ['Ar', 'N2', 'H2', 'O2', 'CO2', 'CH4', 'H2O'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', iterinfo=False) >>> amb = Source('ambient air') >>> sf = Source('fuel') >>> fg = Sink('flue gas outlet') @@ -138,17 +136,18 @@ class CombustionChamber(Component): temperature of the flue gas determines the ratio of oxygen to fuel mass flow. - >>> comb.set_attr(ti=500000) + >>> comb.set_attr(ti=500000, lamb=1.5) >>> amb_comb.set_attr(p=1, T=20, fluid={'Ar': 0.0129, 'N2': 0.7553, - ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314, 'H2': 0}) - >>> sf_comb.set_attr(T=25, fluid={'CO2': 0.03, 'H2': 0.01, 'Ar': 0, - ... 'N2': 0, 'O2': 0, 'H2O': 0, 'CH4': 0.96}) + ... 'CO2': 0.0004, 'O2': 0.2314}) + >>> sf_comb.set_attr(T=25, fluid={'CO2': 0.03, 'H2': 0.01, 'CH4': 0.96}) + >>> nw.solve('design') >>> comb_fg.set_attr(T=1200) + >>> comb.set_attr(lamb=None) >>> nw.solve('design') >>> round(comb.lamb.val, 3) 2.014 >>> comb.set_attr(lamb=2) - >>> comb_fg.set_attr(T=np.nan) + >>> comb_fg.set_attr(T=None) >>> nw.solve('design') >>> round(comb_fg.T.val, 1) 1206.6 @@ -158,7 +157,7 @@ class CombustionChamber(Component): def component(): return 'combustion chamber' - def get_variables(self): + def get_parameters(self): return { 'lamb': dc_cp( min_val=1, deriv=self.lambda_deriv, func=self.lambda_func, @@ -169,6 +168,13 @@ def get_variables(self): } def get_mandatory_constraints(self): + self.fluid_eqs = set( + [ + f for c in self.inl + self.outl + for f in c.fluid.val + ] + ) + self.fluid_eqs_list = list(self.fluid_eqs) return { 'mass_flow_constraints': { 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, @@ -185,7 +191,7 @@ def get_mandatory_constraints(self): 'deriv': self.stoichiometry_deriv, 'constant_deriv': False, 'latex': self.stoichiometry_func_doc, - 'num_eq': self.num_nw_fluids}, + 'num_eq': len(self.fluid_eqs)}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -201,39 +207,80 @@ def inlets(): def outlets(): return ['out1'] - def preprocess(self, nw): - super().preprocess(nw) + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + _, outl = self._get_combustion_connections() + outconn = outl[0] + for f in ["H2O", "CO2"]: + if f not in outconn.fluid.val: + outconn.fluid.val[f] = 0 + + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) + + return {outconn.label: branch} + + def propagate_to_target(self, branch): + return + + def propagate_wrapper_to_target(self, branch): + if self in branch["components"]: + return + + outconn = self.outl[0] + branch["connections"] += [outconn] + branch["components"] += [self] + outconn.target.propagate_wrapper_to_target(branch) + + def preprocess(self, num_nw_vars): + super().preprocess(num_nw_vars) self.setup_reaction_parameters() + def _get_combustion_connections(self): + return (self.inl[:2], [self.outl[0]]) + def setup_reaction_parameters(self): r"""Setup parameters for reaction (gas name aliases and LHV).""" self.fuel_list = [] - for f in self.nw_fluids: + all_fluids = [f for c in self.inl + self.outl for f in c.fluid.val] + for f in all_fluids: if fluidalias_in_list(f, combustion_gases): self.fuel_list += [f] + self.fuel_list = set(self.fuel_list) + if len(self.fuel_list) == 0: - msg = ('Your network\'s fluids do not contain any fuels, that are ' - 'available for the component ' + self.label + ' of type ' + - self.component() + '. Available fuels are: ' + - ', '.join(combustion_gases) + '.') + msg = ( + "Your network's fluids do not contain any fuels, that are " + f"available for the component {self.label} of type " + f"{self.component()}. Available fuels are: " + + ", ".join(combustion_gases) + "." + ) logger.error(msg) raise TESPyComponentError(msg) else: - msg = ('The fuels for component ' + self.label + ' of type ' + - self.component() + ' are: ' + ', '.join(self.fuel_list) + - '.') + msg = ( + f"The fuels for component {self.label} of type " + f"{self.component()} are: " + ", ".join(self.fuel_list) + "." + ) logger.debug(msg) - for fluid in ['O2', 'CO2', 'H2O', 'N2']: - if not fluidalias_in_list(fluid, self.nw_fluids): - aliases = ', '.join(CP.get_aliases(fluid)) + for fluid in ["O2", "CO2", "H2O", "N2"]: + if not fluidalias_in_list(fluid, all_fluids): + aliases = ", ".join(CP.get_aliases(fluid)) msg = ( - 'The component ' + self.label + ' (class ' + - self.__class__.__name__ + ') requires that the fluid ' - f'{fluid} (aliases: {aliases}) is in the network\'s list ' - 'of fluids.' + f"The component {self.label} (class " + f"{self.__class__.__name__}) requires that the fluid " + f"{fluid} (aliases: {aliases}) is in the network's list of " + "fluids." ) logger.error(msg) raise TESPyComponentError(msg) @@ -277,6 +324,7 @@ def calc_lhv(self, f): \forall j \in \text{reation educts},\\ \Delta H_f^0: \text{molar formation enthalpy} """ + inl, _ = self._get_combustion_connections() hf = {} hf['hydrogen'] = 0 hf['methane'] = -74.6 @@ -293,11 +341,16 @@ def calc_lhv(self, f): set([a.replace(' ', '') for a in CP.get_aliases(f)])) - val = (-(self.fuels[f]['H'] / 2 * hf[self.h2o] + - self.fuels[f]['C'] * hf[self.co2] - - ((self.fuels[f]['C'] + self.fuels[f]['H'] / 4) * hf[self.o2] + - hf[list(key)[0]])) / - molar_masses[f] * 1000) + val = ( + -( + self.fuels[f]['H'] / 2 * hf[self.h2o] + + self.fuels[f]['C'] * hf[self.co2] + - ( + (self.fuels[f]['C'] + self.fuels[f]['H'] / 4) * hf[self.o2] + + hf[list(key)[0]] + ) + ) / inl[0].fluid.wrapper[f]._molar_mass * 1000 + ) return val @@ -314,8 +367,8 @@ def mass_flow_func(self): 0 = \dot{m}_{in,1} + \dot{m}_{in,2} - \dot{m}_{out,1} """ - return (self.inl[0].m.val_SI + self.inl[1].m.val_SI - - self.outl[0].m.val_SI) + inl, outl = self._get_combustion_connections() + return inl[0].m.val_SI + inl[1].m.val_SI - outl[0].m.val_SI def mass_flow_func_doc(self, label): r""" @@ -336,7 +389,7 @@ def mass_flow_func_doc(self, label): r'\dot{m}_\mathrm{out,1}') return generate_latex_eq(self, latex, label) - def mass_flow_deriv(self): + def mass_flow_deriv(self, k): r""" Calculate the partial derivatives for all mass flow balance equations. @@ -345,11 +398,13 @@ def mass_flow_deriv(self): deriv : ndarray Matrix with partial derivatives for the fluid equations. """ - deriv = np.zeros((1, 3, self.num_nw_vars)) - deriv[0, 0, 0] = 1 - deriv[0, 1, 0] = 1 - deriv[0, 2, 0] = -1 - return deriv + inl, outl = self._get_combustion_connections() + for i in inl: + if i.m.is_var: + self.jacobian[k, i.m.J_col] = 1 + + if outl[0].m.is_var: + self.jacobian[k, outl[0].m.J_col] = -1 def combustion_pressure_func(self): r""" @@ -365,11 +420,12 @@ def combustion_pressure_func(self): 0 = p_\mathrm{in,3} - p_\mathrm{out,3}\\ 0 = p_\mathrm{in,3} - p_\mathrm{in,4} """ - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] + inl, outl = self._get_combustion_connections() return [ - inl[0].p.val_SI - outl.p.val_SI, inl[0].p.val_SI - inl[1].p.val_SI] + inl[0].p.val_SI - outl[0].p.val_SI, + inl[1].p.val_SI - outl[0].p.val_SI + ] def combustion_pressure_func_doc(self, label): r""" @@ -385,10 +441,9 @@ def combustion_pressure_func_doc(self, label): latex : str LaTeX code of equations applied. """ - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] + inl, outl = self._get_combustion_connections() - idx_out = outl.source_id[:3] + ',' + outl.source_id[3:] + idx_out = outl[0].source_id[:3] + ',' + outl[0].source_id[3:] idx_in1 = inl[0].target_id[:2] + ',' + inl[0].target_id[2:] idx_in2 = inl[1].target_id[:2] + ',' + inl[1].target_id[2:] latex = ( @@ -400,7 +455,7 @@ def combustion_pressure_func_doc(self, label): r'\end{split}') return generate_latex_eq(self, latex, label) - def combustion_pressure_deriv(self): + def combustion_pressure_deriv(self, k): r""" Calculate the partial derivatives for combustion pressure equations. @@ -409,18 +464,16 @@ def combustion_pressure_deriv(self): deriv : ndarray Matrix with partial derivatives for the fluid equations. """ - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] - deriv = np.zeros(( - 2, self.num_i + self.num_o + self.num_vars, self.num_nw_vars)) - idx_out = self.num_i + self.outl.index(outl) - idx_in1 = self.inl.index(inl[0]) - idx_in2 = self.inl.index(inl[1]) - deriv[0, idx_in1, 1] = 1 - deriv[0, idx_out, 1] = -1 - deriv[1, idx_in1, 1] = 1 - deriv[1, idx_in2, 1] = -1 - return deriv + inl, outl = self._get_combustion_connections() + if inl[0].p.is_var: + self.jacobian[k, inl[0].p.J_col] = 1 + if outl[0].p.is_var: + self.jacobian[k, outl[0].p.J_col] = -1 + + if inl[1].p.is_var: + self.jacobian[k + 1, inl[1].p.J_col] = 1 + if outl[0].p.is_var: + self.jacobian[k + 1, outl[0].p.J_col] = -1 def stoichiometry_func(self): r""" @@ -433,7 +486,7 @@ def stoichiometry_func(self): """ # calculate equations residual = [] - for fluid in self.inl[0].fluid.val.keys(): + for fluid in self.fluid_eqs_list: residual += [self.stoichiometry(fluid)] return residual @@ -538,8 +591,7 @@ def stoichiometry(self, fluid): Residual value for corresponding fluid. """ # required to work with combustion chamber and engine - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] + inl, outl = self._get_combustion_connections() ################################################################### # molar mass flow for fuel and oxygen @@ -550,19 +602,21 @@ def stoichiometry(self, fluid): for f in self.fuel_list: n_fuel[f] = 0 for i in inl: - n = i.m.val_SI * i.fluid.val[f] / molar_masses[f] + n = i.m.val_SI * i.fluid.val[f] / inl[0].fluid.wrapper[f]._molar_mass n_fuel[f] += n n_h += n * self.fuels[f]['H'] n_c += n * self.fuels[f]['C'] # stoichiometric oxygen requirement for each fuel n_oxy_stoich[f] = n_fuel[f] * ( - self.fuels[f]['H'] / 4 + self.fuels[f]['C']) + self.fuels[f]['H'] / 4 + self.fuels[f]['C'] + ) n_oxygen = 0 for i in inl: n_oxygen += ( - i.m.val_SI * i.fluid.val[self.o2] / molar_masses[self.o2]) + i.m.val_SI * i.fluid.val[self.o2] / inl[0].fluid.wrapper[self.o2]._molar_mass + ) ################################################################### # calculate stoichiometric oxygen @@ -585,33 +639,32 @@ def stoichiometry(self, fluid): ################################################################### # equation for carbondioxide if fluid == self.co2: - dm = (n_c - n_c_exc) * molar_masses[self.co2] + dm = (n_c - n_c_exc) * inl[0].fluid.wrapper[self.co2]._molar_mass ################################################################### # equation for water elif fluid == self.h2o: - dm = (n_h - n_h_exc) / 2 * molar_masses[self.h2o] + dm = (n_h - n_h_exc) / 2 * inl[0].fluid.wrapper[self.h2o]._molar_mass ################################################################### # equation for oxygen elif fluid == self.o2: if self.lamb.val < 1: - dm = -n_oxygen * molar_masses[self.o2] + dm = -n_oxygen * inl[0].fluid.wrapper[self.o2]._molar_mass else: - dm = -n_oxygen / self.lamb.val * molar_masses[self.o2] + dm = -n_oxygen / self.lamb.val * inl[0].fluid.wrapper[self.o2]._molar_mass ################################################################### # equation for fuel elif fluid in self.fuel_list: if self.lamb.val < 1: n_fuel_exc = ( - -(n_oxygen / n_oxygen_stoich - 1) * - n_oxy_stoich[fluid] / ( - self.fuels[fluid]['H'] / 4 + - self.fuels[fluid]['C'])) + -(n_oxygen / n_oxygen_stoich - 1) * n_oxy_stoich[fluid] + / (self.fuels[fluid]['H'] / 4 + self.fuels[fluid]['C']) + ) else: n_fuel_exc = 0 - dm = -(n_fuel[fluid] - n_fuel_exc) * molar_masses[fluid] + dm = -(n_fuel[fluid] - n_fuel_exc) * inl[0].fluid.wrapper[fluid]._molar_mass ################################################################### # equation for other fluids @@ -621,7 +674,7 @@ def stoichiometry(self, fluid): res = dm for i in inl: res += i.fluid.val[fluid] * i.m.val_SI - res -= outl.fluid.val[fluid] * outl.m.val_SI + res -= outl[0].fluid.val[fluid] * outl[0].m.val_SI return res def stoichiometry_func_doc(self, label): @@ -734,20 +787,19 @@ def stoichiometry_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ # required to work with combustion chamber and engine - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] - + inl, outl = self._get_combustion_connections() f = self.stoichiometry - for fluid in self.nw_fluids: - for conn in inl + [outl]: - i = (self.inl + self.outl).index(conn) - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv( - f, 'm', i, fluid=fluid) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv( - f, 'fluid', i, fluid=fluid) - k += 1 + conns = inl + outl + for fluid, conn in itertools.product(self.fluid_eqs_list, conns): + eq_num = self.fluid_eqs_list.index(fluid) + if self.is_variable(conn.m, increment_filter): + self.jacobian[k + eq_num, conn.m.J_col] = self.numeric_deriv( + f, 'm', conn, fluid=fluid + ) + for fluid_name in conn.fluid.is_var: + self.jacobian[k + eq_num, conn.fluid.J_col[fluid_name]] = self.numeric_deriv( + f, fluid_name, conn, fluid=fluid + ) def energy_balance_func(self): r""" @@ -780,17 +832,22 @@ def energy_balance_func(self): - Reference temperature: 298.15 K. - Reference pressure: 1 bar. """ + inl, outl = self._get_combustion_connections() T_ref = 298.15 p_ref = 1e5 res = 0 - for i in self.inl: - res += i.m.val_SI * (i.h.val_SI - h_mix_pT( - [0, p_ref, 0, i.fluid.val], T_ref, force_gas=True)) + for i in inl: + res += i.m.val_SI * ( + i.h.val_SI + - h_mix_pT(p_ref, T_ref, i.fluid_data, mixing_rule="forced-gas") + ) - for o in self.outl: - res -= o.m.val_SI * (o.h.val_SI - h_mix_pT( - [0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) + for o in outl: + res -= o.m.val_SI * ( + o.h.val_SI + - h_mix_pT(p_ref, T_ref, o.fluid_data, mixing_rule="forced-gas") + ) res += self.calc_ti() return res @@ -838,15 +895,16 @@ def energy_balance_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ f = self.energy_balance_func - for i in range(3): - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - if i >= self.num_i: - self.jacobian[k, i, 2] = -(self.inl + self.outl)[i].m.val_SI - else: - self.jacobian[k, i, 2] = (self.inl + self.outl)[i].m.val_SI + for c in self.inl + self.outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + if self.is_variable(c.p, increment_filter): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h, increment_filter): + if c == self.outl[0]: + self.jacobian[k, c.h.J_col] = -c.m.val_SI + else: + self.jacobian[k, c.h.J_col] = c.m.val_SI def lambda_func(self): r""" @@ -866,20 +924,26 @@ def lambda_func(self): {M_{fluid}}\\ \forall i \in inlets """ # required to work with combustion chamber and engine - inl = self.inl[::-1][:2][::-1] + inl, _ = self._get_combustion_connections() + n_h = 0 n_c = 0 for f in self.fuel_list: n_fuel = 0 for i in inl: - n_fuel += i.m.val_SI * i.fluid.val[f] / molar_masses[f] + n_fuel += ( + i.m.val_SI * i.fluid.val[f] + / inl[0].fluid.wrapper[f]._molar_mass + ) n_h += n_fuel * self.fuels[f]['H'] n_c += n_fuel * self.fuels[f]['C'] n_oxygen = 0 for i in inl: - n_oxygen += (i.m.val_SI * i.fluid.val[self.o2] / - molar_masses[self.o2]) + n_oxygen += ( + i.m.val_SI * i.fluid.val[self.o2] + / inl[0].fluid.wrapper[self.o2]._molar_mass + ) n_oxygen_stoich = n_h / 4 + n_c @@ -923,14 +987,13 @@ def lambda_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ # required to work with combustion chamber and engine - inl = self.inl[::-1][:2][::-1] + inl, _ = self._get_combustion_connections() f = self.lambda_func for conn in inl: - i = (self.inl + self.outl).index(conn) - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) + if self.is_variable(conn.m, increment_filter): + self.jacobian[k, conn.m.J_col] = self.numeric_deriv(f, 'm', conn) + for fluid in conn.fluid.is_var: + self.jacobian[k, conn.fluid.J_col[fluid]] = self.numeric_deriv(f, fluid, conn) def ti_func(self): r""" @@ -961,7 +1024,8 @@ def ti_func_doc(self, label): latex : str LaTeX code of equations applied. """ - idx = str(self.outl.index(self.outl[-1]) + 1) + _, outl = self._get_combustion_connections() + idx = str(self.outl.index(outl[0]) + 1) latex = ( r'\begin{split}' + '\n' r'0 = & ti - LHV_\mathrm{fuel} \cdot \left[\sum_i \left(' @@ -985,18 +1049,24 @@ def ti_deriv(self, increment_filter, k): k : int Position of equation in Jacobian matrix. """ - self.jacobian[k, 0, 0] = 0 - self.jacobian[k, 1, 0] = 0 - self.jacobian[k, 2, 0] = 0 - for f in self.fuel_list: - pos = 3 + self.nw_fluids.index(f) - lhv = self.fuels[f]['LHV'] - - for i in range(2): - self.jacobian[k, i, 0] += -self.inl[i].fluid.val[f] * lhv - self.jacobian[k, i, pos] = -self.inl[i].m.val_SI * lhv - self.jacobian[k, 2, 0] += self.outl[0].fluid.val[f] * lhv - self.jacobian[k, 2, pos] = self.outl[0].m.val_SI * lhv + inl, outl = self._get_combustion_connections() + for i in inl: + if i.m.is_var: + deriv = 0 + for f in self.fuel_list: + deriv -= i.fluid.val[f] * self.fuels[f]['LHV'] + self.jacobian[k, i.m.J_col] = deriv + for f in (self.fuel_list & i.fluid.is_var): + self.jacobian[k, i.fluid.J_col[f]] = -i.m.val_SI * self.fuels[f]['LHV'] + + o = outl[0] + if o.m.is_var: + deriv = 0 + for f in self.fuel_list: + deriv += o.fluid.val[f] * self.fuels[f]['LHV'] + self.jacobian[k, o.m.J_col] = deriv + for f in (self.fuel_list & o.fluid.is_var): + self.jacobian[k, o.fluid.J_col[f]] = o.m.val_SI * self.fuels[f]['LHV'] def calc_ti(self): r""" @@ -1014,13 +1084,14 @@ def calc_ti(self): \right) - \dot{m}_{out,1} \cdot x_{fuel,out,1} \right] \; \forall i \in [1,2] """ + inl, outl = self._get_combustion_connections() ti = 0 for f in self.fuel_list: m = 0 - for i in self.inl: + for i in inl: m += i.m.val_SI * i.fluid.val[f] - for o in self.outl: + for o in outl: m -= o.m.val_SI * o.fluid.val[f] ti += m * self.fuels[f]['LHV'] @@ -1086,64 +1157,17 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 3, self.num_nw_vars)) f = self.calc_bus_value - for i in range(3): - deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus) - deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus) + for c in self.inl + self.outl: + if c.m.is_var: + if c.m.J_col not in bus.jacobian: + bus.jacobian[c.m.J_col] = 0 + bus.jacobian[c.m.J_col] -= self.numeric_deriv(f, 'm', c, bus=bus) - return deriv - - def initialise_fluids(self): - """Calculate reaction balance for generic starting values at outlet.""" - N_2 = 0.7655 - O_2 = 0.2345 - - n_fuel = 1 - lamb = 3 - - fact_fuel = {} - sum_fuel = 0 - for f in self.fuel_list: - fact_fuel[f] = 0 - for i in self.inl: - fact_fuel[f] += i.fluid.val[f] / 2 - sum_fuel += fact_fuel[f] - - for f in self.fuel_list: - fact_fuel[f] /= sum_fuel - - m_co2 = 0 - m_h2o = 0 - m_fuel = 0 - for f in self.fuel_list: - m_co2 += (n_fuel * self.fuels[f]['C'] * molar_masses[self.co2] * - fact_fuel[f]) - m_h2o += (n_fuel * self.fuels[f]['H'] / 2 * - molar_masses[self.h2o] * fact_fuel[f]) - m_fuel += n_fuel * molar_masses[f] * fact_fuel[f] - - n_o2 = (m_co2 / molar_masses[self.co2] + - 0.5 * m_h2o / molar_masses[self.h2o]) * lamb - - m_air = n_o2 * molar_masses[self.o2] / O_2 - m_fg = m_air + m_fuel - - m_o2 = n_o2 * molar_masses[self.o2] * (1 - 1 / lamb) - m_n2 = N_2 * m_air - - fg = { - self.n2: m_n2 / m_fg, - self.co2: m_co2 / m_fg, - self.o2: m_o2 / m_fg, - self.h2o: m_h2o / m_fg - } - - for o in self.outl: - for fluid, x in o.fluid.val.items(): - if not o.fluid.val_set[fluid] and fluid in fg: - o.fluid.val[fluid] = fg[fluid] - o.target.propagate_fluid_to_target(o, o.target) + for fluid in c.fluid.is_var: + if c.fluid.J_col[fluid] not in bus.jacobian: + bus.jacobian[c.fluid.J_col[fluid]] = 0 + bus.jacobian[c.fluid.J_col[fluid]] -= self.numeric_deriv(f, fluid, c, bus=bus) def convergence_check(self): r""" @@ -1157,60 +1181,58 @@ def convergence_check(self): outlet. """ # required to work with combustion chamber and engine - inl = self.inl[::-1][:2][::-1] - outl = self.outl[::-1][0] + inl, outl = self._get_combustion_connections() m = 0 for i in inl: - if not i.good_starting_values: - if i.m.val_SI < 0 and not i.m.val_set: - i.m.val_SI = 0.01 - m += i.m.val_SI + if i.m.val_SI < 0 and i.m.is_var: + i.m.val_SI = 0.01 + m += i.m.val_SI ###################################################################### # check fluid composition - if not outl.good_starting_values: - fluids = [f for f in outl.fluid.val.keys() - if not outl.fluid.val_set[f]] - for f in fluids: - if f not in [self.o2, self.co2, self.h2o] + self.fuel_list: - m_f = 0 - for i in inl: - m_f += i.fluid.val[f] * i.m.val_SI - - if abs(outl.fluid.val[f] - m_f / m) > 0.03: - outl.fluid.val[f] = m_f / m - - elif f == self.o2: - if outl.fluid.val[f] > 0.25: - outl.fluid.val[f] = 0.2 - if outl.fluid.val[f] < 0.001: - outl.fluid.val[f] = 0.05 - - elif f == self.co2: - if outl.fluid.val[f] > 0.1: - outl.fluid.val[f] = 0.075 - if outl.fluid.val[f] < 0.001: - outl.fluid.val[f] = 0.02 - - elif f == self.h2o: - if outl.fluid.val[f] > 0.1: - outl.fluid.val[f] = 0.075 - if outl.fluid.val[f] < 0.001: - outl.fluid.val[f] = 0.02 - - elif f in self.fuel_list: - if outl.fluid.val[f] > 0: - outl.fluid.val[f] = 0 + outl = outl[0] + for f in outl.fluid.is_var: + if f == self.o2: + if outl.fluid.val[f] > 0.25: + outl.fluid.val[f] = 0.2 + if outl.fluid.val[f] < 0.001: + outl.fluid.val[f] = 0.05 + + elif f == self.co2: + if outl.fluid.val[f] > 0.1: + outl.fluid.val[f] = 0.075 + if outl.fluid.val[f] < 0.001: + outl.fluid.val[f] = 0.02 + + elif f == self.h2o: + if outl.fluid.val[f] > 0.1: + outl.fluid.val[f] = 0.075 + if outl.fluid.val[f] < 0.001: + outl.fluid.val[f] = 0.02 + + elif f in self.fuel_list: + if outl.fluid.val[f] > 0: + outl.fluid.val[f] = 0 - ###################################################################### - # flue gas propagation - if not outl.good_starting_values: - if outl.m.val_SI < 0 and not outl.m.val_set: - outl.m.val_SI = 10 - outl.target.propagate_fluid_to_target(outl, outl.target) + else: + m_f = 0 + for i in inl: + m_f += i.fluid.val[f] * i.m.val_SI - if outl.h.val_SI < 7.5e5 and not outl.h.val_set: + if abs(outl.fluid.val[f] - m_f / m) > 0.03: + outl.fluid.val[f] = m_f / m + + total_mass_fractions = sum(outl.fluid.val.values()) + for fluid in outl.fluid.is_var: + outl.fluid.val[fluid] /= total_mass_fractions + outl.build_fluid_data() + + if outl.m.val_SI < 0 and outl.m.is_var: + outl.m.val_SI = 10 + + if not outl.good_starting_values: + if outl.h.val_SI < 7.5e5 and outl.h.is_var: outl.h.val_SI = 1e6 ###################################################################### @@ -1224,7 +1246,7 @@ def convergence_check(self): for f in self.fuel_list: fuel += i.fluid.val[f] # found the fuel inlet - if fuel > 0.75 and not i.m.val_set: + if fuel > 0.75 and i.m.is_var: fuel_found = True fuel_inlet = i @@ -1295,53 +1317,26 @@ def initialise_target(c, key): elif key == 'h': return 5e5 - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Fluid propagation to target stops here. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - return - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Fluid propagation to source stops here. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - return - def calc_parameters(self): r"""Postprocessing parameter calculation.""" + inl, _ = self._get_combustion_connections() self.ti.val = self.calc_ti() n_h = 0 n_c = 0 for f in self.fuel_list: n_fuel = 0 - for i in self.inl: - n_fuel += i.m.val_SI * i.fluid.val[f] / molar_masses[f] + for i in inl: + n_fuel += i.m.val_SI * i.fluid.val[f] / inl[0].fluid.wrapper[f]._molar_mass n_h += n_fuel * self.fuels[f]['H'] n_c += n_fuel * self.fuels[f]['C'] n_oxygen = 0 - for i in self.inl: - n_oxygen += (i.m.val_SI * i.fluid.val[self.o2] / - molar_masses[self.o2]) + for i in inl: + n_oxygen += ( + i.m.val_SI * i.fluid.val[self.o2] + / inl[0].fluid.wrapper[self.o2]._molar_mass + ) n_oxygen_stoich = n_h / 4 + n_c @@ -1385,12 +1380,13 @@ def entropy_balance(self): p_ref = 1e5 o = self.outl[0] self.S_comb = o.m.val_SI * ( - o.s.val_SI - - s_mix_pT([0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) - for c in self.inl: - self.S_comb -= c.m.val_SI * ( - c.s.val_SI - - s_mix_pT([0, p_ref, 0, c.fluid.val], T_ref, force_gas=True)) + o.s.val_SI - s_mix_pT(p_ref, T_ref, o.fluid_data, "forced-gas") + ) + + for i in self.inl: + self.S_Qcomb -= i.m.val_SI * ( + i.s.val_SI - s_mix_pT(p_ref, T_ref, i.fluid_data, "forced-gas") + ) self.S_irr = 0 self.T_mcomb = self.calc_ti() / self.S_comb @@ -1405,5 +1401,5 @@ def exergy_balance(self, T0): ) self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P/self.E_F + self.epsilon = self._calc_epsilon() self.E_bus = np.nan diff --git a/src/tespy/components/combustion/diabatic.py b/src/tespy/components/combustion/diabatic.py index 3b1ca9437..5f26f6880 100644 --- a/src/tespy/components/combustion/diabatic.py +++ b/src/tespy/components/combustion/diabatic.py @@ -120,11 +120,9 @@ class DiabaticCombustionChamber(CombustionChamber): >>> from tespy.components import Sink, Source, DiabaticCombustionChamber >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools.fluid_properties import T_bp_p + >>> from tespy.tools.fluid_properties import T_sat_p >>> import shutil - >>> fluid_list = ['Ar', 'N2', 'H2', 'O2', 'CO2', 'CH4', 'H2O'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', iterinfo=False) >>> amb = Source('ambient air') >>> sf = Source('fuel') >>> fg = Sink('flue gas outlet') @@ -147,12 +145,13 @@ class DiabaticCombustionChamber(CombustionChamber): identical lambda or outlet temperature as in an adiabatic combustion chamber. - >>> comb.set_attr(ti=500000, pr=0.95, eta=1) + >>> comb.set_attr(ti=500000, pr=0.95, eta=1, lamb=1.5) >>> amb_comb.set_attr(p=1.2, T=20, fluid={'Ar': 0.0129, 'N2': 0.7553, - ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314, 'H2': 0}) - >>> sf_comb.set_attr(T=25, fluid={'CO2': 0.03, 'H2': 0.01, 'Ar': 0, - ... 'N2': 0, 'O2': 0, 'H2O': 0, 'CH4': 0.96}, p=1.3) + ... 'CO2': 0.0004, 'O2': 0.2314}) + >>> sf_comb.set_attr(T=25, fluid={'CO2': 0.03, 'H2': 0.01, 'CH4': 0.96}, p=1.3) + >>> nw.solve('design') >>> comb_fg.set_attr(T=1200) + >>> comb.set_attr(lamb=None) >>> nw.solve('design') >>> round(comb.lamb.val, 3) 2.014 @@ -195,7 +194,7 @@ class DiabaticCombustionChamber(CombustionChamber): def component(): return 'diabatic combustion chamber' - def get_variables(self): + def get_parameters(self): return { 'lamb': dc_cp( min_val=1, deriv=self.lambda_deriv, func=self.lambda_func, @@ -216,16 +215,8 @@ def get_variables(self): def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 1}, - 'stoichiometry_constraints': { - 'func': self.stoichiometry_func, - 'deriv': self.stoichiometry_deriv, - 'constant_deriv': False, - 'latex': self.stoichiometry_func_doc, - 'num_eq': self.num_nw_fluids} + k: v for k, v in super().get_mandatory_constraints().items() + if k in ["mass_flow_constraints", "stoichiometry_constraints"] } def pr_func(self): @@ -275,8 +266,12 @@ def pr_deriv(self, increment_filter, k): k : int Position of equation in Jacobian matrix. """ - self.jacobian[k, 0, 1] = self.pr.val - self.jacobian[k, 2, 1] = -1 + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.p): + self.jacobian[k, i.p.J_col] = self.pr.val + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -1 def energy_balance_func(self): r""" @@ -315,12 +310,18 @@ def energy_balance_func(self): res = 0 for i in self.inl: - res += i.m.val_SI * (i.h.val_SI - h_mix_pT( - [0, p_ref, 0, i.fluid.val], T_ref, force_gas=True)) + i.build_fluid_data() + res += i.m.val_SI * ( + i.h.val_SI + - h_mix_pT(p_ref, T_ref, i.fluid_data, mixing_rule="forced-gas") + ) for o in self.outl: - res -= o.m.val_SI * (o.h.val_SI - h_mix_pT( - [0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) + o.build_fluid_data() + res -= o.m.val_SI * ( + o.h.val_SI + - h_mix_pT(p_ref, T_ref, o.fluid_data, mixing_rule="forced-gas") + ) res += self.calc_ti() * self.eta.val return res @@ -364,22 +365,28 @@ def calc_parameters(self): res = 0 for i in self.inl: - res += i.m.val_SI * (i.h.val_SI - h_mix_pT( - [0, p_ref, 0, i.fluid.val], T_ref, force_gas=True)) + i.build_fluid_data() + res += i.m.val_SI * ( + i.h.val_SI + - h_mix_pT(p_ref, T_ref, i.fluid_data, mixing_rule="forced-gas") + ) for o in self.outl: - res -= o.m.val_SI * (o.h.val_SI - h_mix_pT( - [0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) + o.build_fluid_data() + res -= o.m.val_SI * ( + o.h.val_SI + - h_mix_pT(p_ref, T_ref, o.fluid_data, mixing_rule="forced-gas") + ) self.eta.val = -res / self.ti.val self.Q_loss.val = -(1 - self.eta.val) * self.ti.val self.pr.val = self.outl[0].p.val_SI / self.inl[0].p.val_SI - for i in range(self.num_i): - if self.inl[i].p.val < self.outl[0].p.val: + for num, i in enumerate(self.inl): + if i.p.val < self.outl[0].p.val: msg = ( - f"The pressure at inlet {i + 1} is lower than the pressure " - f"at the outlet of component {self.label}." + f"The pressure at inlet {num + 1} is lower than the " + f"pressure at the outlet of component {self.label}." ) logger.warning(msg) @@ -394,5 +401,5 @@ def exergy_balance(self, T0): ) self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() self.E_bus = {"chemical": np.nan, "physical": np.nan, "massless": np.nan} diff --git a/src/tespy/components/combustion/engine.py b/src/tespy/components/combustion/engine.py index 85a186153..6f47613a3 100644 --- a/src/tespy/components/combustion/engine.py +++ b/src/tespy/components/combustion/engine.py @@ -21,7 +21,6 @@ from tespy.tools.fluid_properties import h_mix_pT from tespy.tools.fluid_properties import s_mix_ph from tespy.tools.fluid_properties import s_mix_pT -from tespy.tools.global_vars import molar_masses class CombustionEngine(CombustionChamber): @@ -183,9 +182,7 @@ class CombustionEngine(CombustionChamber): >>> from tespy.networks import Network >>> import numpy as np >>> import shutil - >>> fluid_list = ['Ar', 'N2', 'O2', 'CO2', 'CH4', 'H2O'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', iterinfo=False) >>> amb = Source('ambient') >>> sf = Source('fuel') >>> fg = Sink('flue gas outlet') @@ -217,11 +214,9 @@ class CombustionEngine(CombustionChamber): >>> chp.set_attr(pr1=0.99, P=-10e6, lamb=1.0, ... design=['pr1'], offdesign=['zeta1']) >>> amb_comb.set_attr(p=5, T=30, fluid={'Ar': 0.0129, 'N2': 0.7553, - ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314}) - >>> sf_comb.set_attr(T=30, fluid={'CO2': 0, 'Ar': 0, 'N2': 0, - ... 'O2': 0, 'H2O': 0, 'CH4': 1}) - >>> cw_sp.set_attr(p=3, T=60, m=50, fluid={'CO2': 0, 'Ar': 0, 'N2': 0, - ... 'O2': 0, 'H2O': 1, 'CH4': 0}) + ... 'CO2': 0.0004, 'O2': 0.2314}) + >>> sf_comb.set_attr(T=30, fluid={'CH4': 1}) + >>> cw_sp.set_attr(p=3, T=60, m=50, fluid={'H2O': 1}) >>> sp_chp2.set_attr(m=Ref(sp_chp1, 1, 0)) >>> mode = 'design' >>> nw.solve(mode=mode) @@ -230,14 +225,14 @@ class CombustionEngine(CombustionChamber): 25300000.0 >>> round(chp.Q1.val, 0) -4980000.0 - >>> chp.set_attr(Q1=-4e6, P=np.nan) + >>> chp.set_attr(Q1=-4e6, P=None) >>> mode = 'offdesign' >>> nw.solve(mode=mode, init_path='tmp', design_path='tmp') >>> round(chp.ti.val, 0) 17794554.0 >>> round(chp.P.val / chp.P.design, 3) 0.617 - >>> chp.set_attr(P=chp.P.design * 0.75, Q1=np.nan) + >>> chp.set_attr(P=chp.P.design * 0.75, Q1=None) >>> mode = 'offdesign' >>> nw.solve(mode=mode, init_path='tmp', design_path='tmp') >>> round(chp.ti.val, 0) @@ -251,7 +246,7 @@ class CombustionEngine(CombustionChamber): def component(): return 'combustion engine' - def get_variables(self): + def get_parameters(self): return { 'lamb': dc_cp( min_val=1, deriv=self.lambda_deriv, func=self.lambda_func, @@ -288,32 +283,8 @@ def get_variables(self): 'eta_mech': dc_simple(val=0.85), 'T_v_inner': dc_simple()} def get_mandatory_constraints(self): - return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 3}, - 'cooling_loop_fuid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': 2 * self.num_nw_fluids}, - 'reactor_pressure_constraints': { - 'func': self.combustion_pressure_func, - 'deriv': self.combustion_pressure_deriv, - 'constant_deriv': True, - 'latex': self.combustion_pressure_func_doc, - 'num_eq': 2}, - 'stoichiometry_constraints': { - 'func': self.stoichiometry_func, - 'deriv': self.stoichiometry_deriv, - 'constant_deriv': False, - 'latex': self.stoichiometry_func_doc, - 'num_eq': self.num_nw_fluids}, - 'energy_balance_constraints': { - 'func': self.energy_balance_func, - 'deriv': self.energy_balance_deriv, - 'constant_deriv': False, 'latex': self.energy_balance_func_doc, - 'num_eq': 1}, + constraints = super().get_mandatory_constraints() + constraints.update({ 'power_constraints': { 'func': self.tiP_char_func, 'deriv': self.tiP_char_deriv, @@ -334,7 +305,8 @@ def get_mandatory_constraints(self): 'deriv': self.Qloss_char_deriv, 'constant_deriv': False, 'latex': self.Qloss_char_func_doc, 'num_eq': 1, 'char': 'Qloss_char'}, - } + }) + return constraints @staticmethod def inlets(): @@ -344,7 +316,38 @@ def inlets(): def outlets(): return ['out1', 'out2', 'out3'] - def preprocess(self, nw): + def propagate_to_target(self, branch): + inl, outl = self._get_combustion_connections() + inconn = branch["connections"][-1] + if inconn in inl: + return + + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [outconn.target] + + outconn.target.propagate_to_target(branch) + + def propagate_wrapper_to_target(self, branch): + inl, outl = self._get_combustion_connections() + inconn = branch["connections"][-1] + if inconn in inl: + if self in branch["components"]: + return + + outconn = self.outl[2] + else: + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [self] + + outconn.target.propagate_wrapper_to_target(branch) + + def preprocess(self, num_nw_vars): if not self.P.is_set: self.set_attr(P='var') @@ -360,133 +363,11 @@ def preprocess(self, nw): self.label + ' as custom variable of the system.') logger.info(msg) - super().preprocess(nw) - + super().preprocess(num_nw_vars) self.setup_reaction_parameters() - def mass_flow_func(self): - r""" - Calculate the residual value for component's mass flow balance. - - Returns - ------- - residual : list - Vector with residual value for component's mass flow balance. - - .. math:: - - 0 = \dot{m}_{in,i} - \dot{m}_{out,i}\\ - \forall i \in [1, 2]\\ - 0 = \dot{m}_{in,3} + \dot{m}_{in,4} - \dot{m}_{out,3} - """ - residual = [] - for i in range(2): - residual += [self.inl[i].m.val_SI - self.outl[i].m.val_SI] - residual += [self.inl[2].m.val_SI + self.inl[3].m.val_SI - - self.outl[2].m.val_SI] - return residual - - def mass_flow_func_doc(self, label): - r""" - Calculate the residual value for component's mass flow balance. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'\begin{split}' + '\n' - r'0=&\dot{m}_\mathrm{in,1} - \dot{m}_\mathrm{out,1}\\' + '\n' - r'0=&\dot{m}_\mathrm{in,2} - \dot{m}_\mathrm{out,2}\\' + '\n' - r'0=&\dot{m}_\mathrm{in,3} + \dot{m}_\mathrm{in,3} - ' - r'\dot{m}_\mathrm{out,3}\\' + '\n' - r'\end{split}' - ) - return generate_latex_eq(self, latex, label) - - def mass_flow_deriv(self): - r""" - Calculate the partial derivatives for all mass flow balance equations. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros((3, 7 + self.num_vars, self.num_nw_vars)) - for i in range(2): - deriv[i, i, 0] = 1 - for j in range(2): - deriv[j, self.num_i + j, 0] = -1 - deriv[2, 2, 0] = 1 - deriv[2, 3, 0] = 1 - deriv[2, 6, 0] = -1 - return deriv - - def fluid_func(self): - r""" - Calculate the vector of residual values for cooling loop fluid balance. - - Returns - ------- - residual : 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 [1, 2] - """ - residual = [] - for i in range(2): - for fluid, x in self.inl[i].fluid.val.items(): - residual += [x - self.outl[i].fluid.val[fluid]] - return residual - - def fluid_func_doc(self, label): - r""" - Calculate the vector of residual values for cooling loop fluid balance. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'0=x_{i\mathrm{,in,}j}-x_{i\mathrm{,out,}j}\;' - r'\forall i \in\text{network fluids,}' - r'\; \forall j \in [1,2]') - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate the partial derivatives for cooling loop fluid balance. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros( - (self.num_nw_fluids * 2, 7 + self.num_vars, self.num_nw_vars)) - for i in range(self.num_nw_fluids): - deriv[i, 0, i + 3] = 1 - deriv[i, 4, i + 3] = -1 - for j in range(self.num_nw_fluids): - deriv[i + 1 + j, 1, j + 3] = 1 - deriv[i + 1 + j, 5, j + 3] = -1 - return deriv + def _get_combustion_connections(self): + return (self.inl[2:], [self.outl[2]]) def energy_balance_func(self): r""" @@ -521,24 +402,13 @@ def energy_balance_func(self): - Reference temperature: 298.15 K. - Reference pressure: 1 bar. """ - T_ref = 298.15 - p_ref = 1e5 - - res = 0 - for i in self.inl[2:]: - res += i.m.val_SI * (i.h.val_SI - h_mix_pT( - [0, p_ref, 0, i.fluid.val], T_ref, force_gas=True)) - - for o in self.outl[2:]: - res -= o.m.val_SI * (o.h.val_SI - h_mix_pT( - [0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) - - res += self.calc_ti() + res = super().energy_balance_func() # cooling for i in range(2): res -= self.inl[i].m.val_SI * ( - self.outl[i].h.val_SI - self.inl[i].h.val_SI) + self.outl[i].h.val_SI - self.inl[i].h.val_SI + ) # power output and heat loss res += self.P.val + self.Qloss.val @@ -590,38 +460,42 @@ def energy_balance_deriv(self, increment_filter, k): """ f = self.energy_balance_func # mass flow cooling water - for i in [0, 1]: - self.jacobian[k, i, 0] = -( - self.outl[i].h.val_SI - self.inl[i].h.val_SI) + for i, o in zip(self.inl[:2], self.outl[:2]): + if i.m.is_var: + self.jacobian[k, i.m.J_col] = -(o.h.val_SI - i.h.val_SI) # mass flow and pressure for combustion reaction - for i in [2, 3, 6]: - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - - # enthalpy - for i in range(4): - self.jacobian[k, i, 2] = self.inl[i].m.val_SI - for i in range(3): - self.jacobian[k, i + 4, 2] = -self.outl[i].m.val_SI + inl, outl = self._get_combustion_connections() + for c in inl + outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + if self.is_variable(c.p, increment_filter): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + + # enthalpy all connections + for i in self.inl: + if i.h.is_var: + self.jacobian[k, i.h.J_col] = i.m.val_SI + + for o in self.outl: + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -o.m.val_SI # fluid composition - for fl in self.fuel_list: - pos = 3 + self.nw_fluids.index(fl) - lhv = self.fuels[fl]['LHV'] - self.jacobian[k, 2, pos] = self.inl[2].m.val_SI * lhv - self.jacobian[k, 3, pos] = self.inl[3].m.val_SI * lhv - self.jacobian[k, 6, pos] = -self.outl[2].m.val_SI * lhv + for c in inl: + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = c.m.val_SI * self.fuels[fl]['LHV'] + + c = outl[0] + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = -c.m.val_SI * self.fuels[fl]['LHV'] + # power and heat loss if self.P.is_var: - self.jacobian[k, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7)) + self.jacobian[k, self.P.J_col] = 1 if self.Qloss.is_var: - self.jacobian[k, 7 + self.Qloss.var_pos, 0] = ( - self.numeric_deriv(f, 'Qloss', 7)) + self.jacobian[k, self.Qloss.J_col] = 1 def Q1_func(self): r""" @@ -672,9 +546,14 @@ def Q1_deriv(self, increment_filter, k): k : int Position of equation in Jacobian matrix. """ - self.jacobian[k, 0, 0] = self.outl[0].h.val_SI - self.inl[0].h.val_SI - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI - self.jacobian[k, 4, 2] = self.inl[0].m.val_SI + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = i.m.val_SI def Q2_func(self): r""" @@ -725,9 +604,14 @@ def Q2_deriv(self, increment_filter, k): k : int Position of equation in Jacobian matrix. """ - self.jacobian[k, 1, 0] = self.outl[1].h.val_SI - self.inl[1].h.val_SI - self.jacobian[k, 1, 2] = -self.inl[1].m.val_SI - self.jacobian[k, 5, 2] = self.inl[1].m.val_SI + i = self.inl[1] + o = self.outl[1] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = i.m.val_SI def tiP_char_func(self): r""" @@ -751,8 +635,9 @@ def tiP_char_func(self): expr = self.P.val / self.P.design return ( - self.calc_ti() + self.tiP_char.char_func.evaluate(expr) * - self.P.val) + self.calc_ti() + + self.tiP_char.char_func.evaluate(expr) * self.P.val + ) def tiP_char_func_doc(self, label): r""" @@ -793,16 +678,16 @@ def tiP_char_deriv(self, increment_filter, k): k : int Position of equation in Jacobian matrix. """ + inl, outl = self._get_combustion_connections() f = self.tiP_char_func - for i in [2, 3, 6]: - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) + for c in inl + outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = self.numeric_deriv(f, fl, c) if self.P.is_var: - self.jacobian[k, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7)) + self.jacobian[k, self.P.J_col] = self.numeric_deriv(f, 'P', None) def Q1_char_func(self): r""" @@ -880,21 +765,24 @@ def Q1_char_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ f = self.Q1_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[4, 2]: - self.jacobian[k, 4, 2] = self.numeric_deriv(f, 'h', 4) - for i in [2, 3, 6]: - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) + i = self.inl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + o = self.outl[0] + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) + + inl, outl = self._get_combustion_connections() + for c in inl + outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = self.numeric_deriv(f, fl, c) if self.P.is_var: - self.jacobian[k, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7)) + self.jacobian[k, self.P.J_col] = self.numeric_deriv(f, 'P', None) def Q2_char_func(self): r""" @@ -925,9 +813,11 @@ def Q2_char_func(self): else: expr = self.P.val / self.P.design - return (self.calc_ti() * self.Q2_char.char_func.evaluate(expr) - - self.tiP_char.char_func.evaluate(expr) * i.m.val_SI * - (o.h.val_SI - i.h.val_SI)) + return ( + self.calc_ti() * self.Q2_char.char_func.evaluate(expr) + - self.tiP_char.char_func.evaluate(expr) * i.m.val_SI + * (o.h.val_SI - i.h.val_SI) + ) def Q2_char_func_doc(self, label): r""" @@ -972,21 +862,24 @@ def Q2_char_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ f = self.Q2_char_func - if not increment_filter[1, 0]: - self.jacobian[k, 1, 0] = self.numeric_deriv(f, 'm', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) - if not increment_filter[5, 2]: - self.jacobian[k, 5, 2] = self.numeric_deriv(f, 'h', 5) - for i in [2, 3, 6]: - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) + i = self.inl[1] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + o = self.outl[1] + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) + + inl, outl = self._get_combustion_connections() + for c in inl + outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = self.numeric_deriv(f, fl, c) if self.P.is_var: - self.jacobian[k, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7)) + self.jacobian[k, self.P.J_col] = self.numeric_deriv(f, 'P', None) def Qloss_char_func(self): r""" @@ -1054,65 +947,17 @@ def Qloss_char_deriv(self, increment_filter, k): Position of equation in Jacobian matrix. """ f = self.Qloss_char_func - for i in [2, 3, 6]: - if not increment_filter[i, 0]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - if not all(increment_filter[i, 3:]): - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) + inl, outl = self._get_combustion_connections() + for c in inl + outl: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + for fl in (self.fuel_list & c.fluid.is_var): + self.jacobian[k, c.fluid.J_col[fl]] = self.numeric_deriv(f, fl, c) if self.P.is_var: - self.jacobian[k, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7)) + self.jacobian[k, self.P.J_col] = self.numeric_deriv(f, 'P', None) if self.Qloss.is_var: - self.jacobian[k, 7 + self.Qloss.var_pos, 0] = ( - self.numeric_deriv(f, 'Qloss', 7)) - - # ti_func is in class CombustionChamber - def ti_deriv(self, increment_filter, k): - """ - Calculate partial derivatives of thermal input equation. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of equation in Jacobian matrix. - """ - f = self.ti_func - for i in [2, 3, 6]: - self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i) - self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i) - - def calc_ti(self): - r""" - Calculate the thermal input of the combustion engine. - - Returns - ------- - ti : float - Thermal input. - - .. math:: - - ti = LHV \cdot \left[\sum_i \left(\dot{m}_{in,i} \cdot x_{f,i} - \right) - \dot{m}_{out,3} \cdot x_{f,3} \right] - - \forall i \in [3,4] - """ - ti = 0 - for f in self.fuel_list: - m = 0 - for i in self.inl[2:]: - m += i.m.val_SI * i.fluid.val[f] - - for o in self.outl[2:]: - m -= o.m.val_SI * o.fluid.val[f] - - ti += m * self.fuels[f]['LHV'] - - return ti + self.jacobian[k, self.Qloss.J_col] = self.numeric_deriv(f, 'Qloss', None) def calc_P(self): r""" @@ -1304,51 +1149,82 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 7 + self.num_vars, self.num_nw_vars)) + inl, outl = self._get_combustion_connections() f = self.calc_bus_value b = bus.comps.loc[self] ###################################################################### # derivatives for bus parameter of thermal input (TI) if b['param'] == 'TI': - for i in [2, 3, 6]: - deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus) - deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus) + for c in inl + outl: + if c.m.is_var: + if c.m.J_col not in bus.jacobian: + bus.jacobian[c.m.J_col] = 0 + bus.jacobian[c.m.J_col] -= self.numeric_deriv(f, 'm', c, bus=bus) + + for fluid in c.fluid.is_var: + if c.fluid.J_col[fluid] not in bus.jacobian: + bus.jacobian[c.fluid.J_col[fluid]] = 0 + bus.jacobian[c.fluid.J_col[fluid]] -= self.numeric_deriv(f, fluid, c, bus=bus) ###################################################################### # derivatives for bus parameter of power production (P) or # heat loss (Qloss) elif b['param'] == 'P' or b['param'] == 'Qloss': - for i in [2, 3, 6]: - deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus) - deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus) + for c in inl + outl: + if c.m.is_var: + if c.m.J_col not in bus.jacobian: + bus.jacobian[c.m.J_col] = 0 + bus.jacobian[c.m.J_col] -= self.numeric_deriv(f, 'm', c, bus=bus) + + for fluid in c.fluid.is_var: + if c.fluid.J_col[fluid] not in bus.jacobian: + bus.jacobian[c.fluid.J_col[fluid]] = 0 + bus.jacobian[c.fluid.J_col[fluid]] -= self.numeric_deriv(f, fluid, c, bus=bus) # variable power if self.P.is_var: - deriv[0, 7 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 7, bus=bus)) + if self.P.J_col not in bus.jacobian: + bus.jacobian[self.P.J_col] = 0 + bus.jacobian[self.P.J_col] -= self.numeric_deriv(f, 'P', None, bus=bus) ###################################################################### # derivatives for bus parameter of total heat production (Q) elif b['param'] == 'Q': - for i in range(2): - deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus) - deriv[0, i, 2] = self.numeric_deriv(f, 'h', i, bus=bus) - deriv[0, i + 4, 2] = self.numeric_deriv(f, 'h', i + 4, bus=bus) - - ###################################################################### - # derivatives for bus parameter of heat production 1 (Q1) - elif b['param'] == 'Q1': - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - deriv[0, 4, 2] = self.numeric_deriv(f, 'h', 4, bus=bus) + for i, o in zip(self.inl[:2], self.outl[:2]): + if i.m.is_var: + if i.m.J_col not in bus.jacobian: + bus.jacobian[i.m.J_col] = 0 + bus.jacobian[i.m.J_col] -= self.numeric_deriv(f, 'm', i, bus=bus) + if i.h.is_var: + if i.h.J_col not in bus.jacobian: + bus.jacobian[i.h.J_col] = 0 + bus.jacobian[i.h.J_col] -= self.numeric_deriv(f, 'h', i, bus=bus) + + if o.h.is_var: + if o.h.J_col not in bus.jacobian: + bus.jacobian[o.h.J_col] = 0 + bus.jacobian[o.h.J_col] -= self.numeric_deriv(f, 'h', o, bus=bus) ###################################################################### - # derivatives for bus parameter of heat production 2 (Q2) - elif b['param'] == 'Q2': - deriv[0, 1, 0] = self.numeric_deriv(f, 'm', 1, bus=bus) - deriv[0, 1, 2] = self.numeric_deriv(f, 'h', 1, bus=bus) - deriv[0, 5, 2] = self.numeric_deriv(f, 'h', 5, bus=bus) + # derivatives for bus parameter of heat production 1 and 2 (Q1, Q2) + elif b['param'] in ['Q1', 'Q2']: + i = self.inl[int(b["param"][-1]) - 1] + o = self.outl[int(b["param"][-1]) - 1] + + if i.m.is_var: + if i.m.J_col not in bus.jacobian: + bus.jacobian[i.m.J_col] = 0 + bus.jacobian[i.m.J_col] -= self.numeric_deriv(f, 'm', i, bus=bus) + if i.h.is_var: + if i.h.J_col not in bus.jacobian: + bus.jacobian[i.h.J_col] = 0 + bus.jacobian[i.h.J_col] -= self.numeric_deriv(f, 'h', i, bus=bus) + + if o.h.is_var: + if o.h.J_col not in bus.jacobian: + bus.jacobian[o.h.J_col] = 0 + bus.jacobian[o.h.J_col] -= self.numeric_deriv(f, 'h', o, bus=bus) ###################################################################### # missing/invalid bus parameter @@ -1358,59 +1234,6 @@ def bus_deriv(self, bus): logger.error(msg) raise ValueError(msg) - return deriv - - def initialise_fluids(self): - """Calculate reaction balance for generic starting values at outlet.""" - N_2 = 0.7655 - O_2 = 0.2345 - - n_fuel = 1 - lamb = 3 - - fact_fuel = {} - sum_fuel = 0 - for f in self.fuel_list: - fact_fuel[f] = 0 - for i in self.inl: - fact_fuel[f] += i.fluid.val[f] / 2 - sum_fuel += fact_fuel[f] - - for f in self.fuel_list: - fact_fuel[f] /= sum_fuel - - m_co2 = 0 - m_h2o = 0 - m_fuel = 0 - for f in self.fuel_list: - m_co2 += (n_fuel * self.fuels[f]['C'] * molar_masses[self.co2] * - fact_fuel[f]) - m_h2o += (n_fuel * self.fuels[f]['H'] / - 2 * molar_masses[self.h2o] * fact_fuel[f]) - m_fuel += n_fuel * molar_masses[f] * fact_fuel[f] - - n_o2 = (m_co2 / molar_masses[self.co2] + - 0.5 * m_h2o / molar_masses[self.h2o]) * lamb - - m_air = n_o2 * molar_masses[self.o2] / O_2 - m_fg = m_air + m_fuel - - m_o2 = n_o2 * molar_masses[self.o2] * (1 - 1 / lamb) - m_n2 = N_2 * m_air - - fg = { - self.n2: m_n2 / m_fg, - self.co2: m_co2 / m_fg, - self.o2: m_o2 / m_fg, - self.h2o: m_h2o / m_fg - } - - o = self.outl[2] - for fluid, x in o.fluid.val.items(): - if not o.fluid.val_set[fluid] and fluid in fg: - o.fluid.val[fluid] = fg[fluid] - o.target.propagate_fluid_to_target(o, o.target) - @staticmethod def initialise_source(c, key): r""" @@ -1471,51 +1294,6 @@ def initialise_target(c, key): elif key == 'h': return 5e5 - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - for outconn in self.outl[:2]: - for fluid, x in inconn.fluid.val.items(): - if (outconn.fluid.val_set[fluid] is False and - outconn.good_starting_values is False): - outconn.fluid.val[fluid] = x - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: - return - for inconn in self.inl[:2]: - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) - def calc_parameters(self): r"""Postprocessing parameter calculation.""" # Q, pr and zeta @@ -1637,13 +1415,13 @@ def entropy_balance(self): p_ref = 1e5 o = self.outl[2] self.S_Qcomb = o.m.val_SI * ( - o.s.val_SI - - s_mix_pT([0, p_ref, 0, o.fluid.val], T_ref, force_gas=True)) + o.s.val_SI - s_mix_pT(p_ref, T_ref, o.fluid_data, "forced-gas") + ) - for c in self.inl[2:]: - self.S_Qcomb -= c.m.val_SI * ( - c.s.val_SI - - s_mix_pT([0, p_ref, 0, c.fluid.val], T_ref, force_gas=True)) + for i in self.inl[2:]: + self.S_Qcomb -= i.m.val_SI * ( + i.s.val_SI - s_mix_pT(p_ref, T_ref, i.fluid_data, "forced-gas") + ) # (virtual) thermodynamic temperature of combustion, use default value # if not specified @@ -1656,17 +1434,27 @@ def entropy_balance(self): p_star = inl.p.val_SI * ( self.get_attr('pr' + str(i + 1)).val) ** 0.5 s_i_star = s_mix_ph( - [0, p_star, inl.h.val_SI, inl.fluid.val], T0=inl.T.val_SI) + p_star, inl.h.val_SI, inl.fluid_data, inl.mixing_rule, + T0=inl.T.val_SI + ) s_o_star = s_mix_ph( - [0, p_star, out.h.val_SI, out.fluid.val], T0=out.T.val_SI) - - setattr(self, 'S_Q' + str(i + 1) + '2', - inl.m.val_SI * (s_o_star - s_i_star)) + p_star, out.h.val_SI, out.fluid_data, out.mixing_rule, + T0=out.T.val_SI + ) + + setattr( + self, 'S_Q' + str(i + 1) + '2', + inl.m.val_SI * (s_o_star - s_i_star) + ) S_Q = self.get_attr('S_Q' + str(i + 1) + '2') - setattr(self, 'S_irr' + str(i + 1), - inl.m.val_SI * (out.s.val_SI - inl.s.val_SI) - S_Q) - setattr(self, 'T_mQ' + str(i + 1), - inl.m.val_SI * (out.h.val_SI - inl.h.val_SI) / S_Q) + setattr( + self, 'S_irr' + str(i + 1), + inl.m.val_SI * (out.s.val_SI - inl.s.val_SI) - S_Q + ) + setattr( + self, 'T_mQ' + str(i + 1), + inl.m.val_SI * (out.h.val_SI - inl.h.val_SI) / S_Q + ) # internal irreversibilty self.P_irr_i = (1 / self.eta_mech.val - 1) * self.P.val @@ -1683,15 +1471,17 @@ def entropy_balance(self): # calculate entropy production of combustion self.S_comb = ( - self.S_Qcomb - self.S_Q11 - self.S_Q21 - self.S_Qloss - - self.S_irr_i) + self.S_Qcomb - self.S_Q11 - self.S_Q21 - self.S_Qloss + - self.S_irr_i + ) # thermodynamic temperature of heat input self.T_mcomb = self.calc_ti() / self.S_comb # total irreversibilty production self.S_irr = ( - self.S_irr_i + self.S_irr2 + self.S_irr1 + self.S_Q1irr + - self.S_Q2irr) + self.S_irr_i + self.S_irr2 + self.S_irr1 + + self.S_Q1irr + self.S_Q2irr + ) def exergy_balance(self, T0): @@ -1704,7 +1494,7 @@ def exergy_balance(self, T0): - self.outl[2].Ex_chemical ) self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() self.E_bus = { "chemical": np.nan, "physical": np.nan, "massless": -self.P.val } diff --git a/src/tespy/components/component.py b/src/tespy/components/component.py index 092325c07..8c8dec496 100644 --- a/src/tespy/components/component.py +++ b/src/tespy/components/component.py @@ -28,13 +28,12 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import v_mix_ph -from tespy.tools.global_vars import err +from tespy.tools.global_vars import ERR +from tespy.tools.helpers import _numeric_deriv from tespy.tools.helpers import bus_char_derivative from tespy.tools.helpers import bus_char_evaluation from tespy.tools.helpers import newton -# %% - class Component: r""" @@ -94,15 +93,17 @@ class Component: def __init__(self, label, **kwargs): # check if components label is of type str and for prohibited chars + _forbidden = [';', ',', '.'] if not isinstance(label, str): msg = 'Component label must be of type str!' logger.error(msg) raise ValueError(msg) - elif len([x for x in [';', ',', '.'] if x in label]) > 0: + elif any([True for x in _forbidden if x in label]): msg = ( - 'You must not use ' + str([';', ',', '.']) + ' in label (' + - str(self.component()) + ').') + f"You cannot use any of " + ", ".join(_forbidden) + " in a " + f"component label ({self.component()}" + ) logger.error(msg) raise ValueError(msg) @@ -118,10 +119,11 @@ def __init__(self, label, **kwargs): self.local_offdesign = False self.char_warnings = True self.printout = True + self.fkt_group = self.label # add container for components attributes - self.variables = OrderedDict(self.get_variables().copy()) - self.__dict__.update(self.variables) + self.parameters = OrderedDict(self.get_parameters().copy()) + self.__dict__.update(self.parameters) self.set_attr(**kwargs) def set_attr(self, **kwargs): @@ -151,7 +153,7 @@ def set_attr(self, **kwargs): """ # set specified values for key in kwargs: - if key in self.variables: + if key in self.parameters: data = self.get_attr(key) if kwargs[key] is None: data.set_attr(is_set=False) @@ -175,18 +177,11 @@ def set_attr(self, **kwargs): # value specification for component properties elif isinstance(data, dc_cp) or isinstance(data, dc_simple): if is_numeric: - if np.isnan(kwargs[key]): - data.set_attr(is_set=False) - if isinstance(data, dc_cp): - data.set_attr(is_var=False) - - else: - data.set_attr(val=kwargs[key], is_set=True) - if isinstance(data, dc_cp): - data.set_attr(is_var=False) - - elif (kwargs[key] == 'var' and - isinstance(data, dc_cp)): + data.set_attr(val=kwargs[key], is_set=True) + if isinstance(data, dc_cp): + data.set_attr(is_var=False) + + elif kwargs[key] == 'var' and isinstance(data, dc_cp): data.set_attr(is_set=True, is_var=True) elif isinstance(data, dc_simple): @@ -195,8 +190,9 @@ def set_attr(self, **kwargs): # invalid datatype for keyword else: msg = ( - 'Bad datatype for keyword argument ' + key + - ' at ' + self.label + '.') + f"Bad datatype for keyword argument {key} for " + f"component {self.label}." + ) logger.error(msg) raise TypeError(msg) @@ -209,39 +205,29 @@ def set_attr(self, **kwargs): # invalid datatype for keyword else: msg = ( - 'Bad datatype for keyword argument ' + key + - ' at ' + self.label + '.') - logger.error(msg) - raise TypeError(msg) - - elif isinstance(data, dc_gcp): - # value specification of grouped component parameter method - if isinstance(kwargs[key], str): - data.method = kwargs[key] - - # invalid datatype for keyword - else: - msg = ( - 'Bad datatype for keyword argument ' + key + - ' at ' + self.label + '.') + f"Bad datatype for keyword argument {key} for " + f"component {self.label}." + ) logger.error(msg) raise TypeError(msg) elif key in ['design', 'offdesign']: if not isinstance(kwargs[key], list): msg = ( - 'Please provide the ' + key + ' parameters as list ' - 'at ' + self.label + '.') + f"Please provide the {key} parameters as list for " + f"component {self.label}." + ) logger.error(msg) raise TypeError(msg) - if set(kwargs[key]).issubset(list(self.variables.keys())): + if set(kwargs[key]).issubset(list(self.parameters.keys())): self.__dict__.update({key: kwargs[key]}) else: + keys = ", ".join(self.parameters.keys()) msg = ( - 'Available parameters for (off-)design specification ' - 'are: ' + str(list(self.variables.keys())) + ' at ' + - self.label + '.') + "Available parameters for (off-)design specification " + f"of component {self.label} are: {keys}." + ) logger.error(msg) raise ValueError(msg) @@ -249,8 +235,9 @@ def set_attr(self, **kwargs): 'printout', 'char_warnings']: if not isinstance(kwargs[key], bool): msg = ( - 'Please provide the parameter ' + key + ' as boolean ' - 'at component ' + self.label + '.') + f"Please provide the {key} parameters as bool for " + f"component {self.label}." + ) logger.error(msg) raise TypeError(msg) @@ -267,7 +254,8 @@ def set_attr(self, **kwargs): else: msg = ( 'Please provide the design_path parameter as string. ' - 'For unsetting use np.nan or None.') + 'For unsetting use None.' + ) logger.error(msg) raise TypeError(msg) @@ -275,9 +263,7 @@ def set_attr(self, **kwargs): # invalid keyword else: - msg = ( - 'Component ' + self.label + ' has no attribute ' + - str(key) + '.') + msg = f"Component {self.label} has no attribute {key}." logger.error(msg) raise KeyError(msg) @@ -298,12 +284,52 @@ def get_attr(self, key): if key in self.__dict__: return self.__dict__[key] else: - msg = ('Component ' + self.label + ' has no attribute \"' + - key + '\".') + msg = f"Component {self.label} has no attribute {key}." logger.error(msg) raise KeyError(msg) - def preprocess(self, nw, num_eq=0): + def serialize(self): + export = {} + for k in self._serializable(): + export.update({k: self.get_attr(k)}) + for k in self.parameters: + data = self.get_attr(k) + export.update({k: data.serialize()}) + + return {self.label: export} + + @staticmethod + def _serializable(): + return [ + "design", "offdesign", "local_design", "local_offdesign", + "design_path", "printout", "fkt_group", "char_warnings" + ] + + @staticmethod + def is_branch_source(): + return False + + def propagate_to_target(self, branch): + inconn = branch["connections"][-1] + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [outconn.target] + + outconn.target.propagate_to_target(branch) + + def propagate_wrapper_to_target(self, branch): + inconn = branch["connections"][-1] + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [self] + + outconn.target.propagate_wrapper_to_target(branch) + + def preprocess(self, num_nw_vars): r""" Perform component initialization in network preprocessing. @@ -312,10 +338,6 @@ def preprocess(self, nw, num_eq=0): nw : tespy.networks.network.Network Network this component is integrated in. """ - self.num_nw_fluids = len(nw.fluids) - self.nw_fluids = nw.fluids - self.always_all_equations = nw.always_all_equations - self.num_nw_vars = self.num_nw_fluids + 3 self.it = 0 self.num_eq = 0 self.vars = {} @@ -330,11 +352,11 @@ def preprocess(self, nw, num_eq=0): for constraint in self.constraints.values(): self.num_eq += constraint['num_eq'] - for key, val in self.variables.items(): + for key, val in self.parameters.items(): data = self.get_attr(key) if isinstance(val, dc_cp): if data.is_var: - data.var_pos = self.num_vars + data.J_col = num_nw_vars + self.num_vars self.num_vars += 1 self.vars[data] = key @@ -376,8 +398,9 @@ def preprocess(self, nw, num_eq=0): start = ( 'All parameters of the component group have to be ' 'specified! This component group uses the following ' - 'parameters: ') - end = ' at ' + self.label + '. Group will be set to False.' + 'parameters: ' + ) + end = f" at {self.label}. Group will be set to False." logger.warning(start + ', '.join(val.elements) + end) val.set_attr(is_set=False) else: @@ -392,40 +415,25 @@ def preprocess(self, nw, num_eq=0): if data.is_set and data.func is not None: self.num_eq += data.num_eq - # set up Jacobian matrix and residual vector - self.jacobian = np.zeros(( - self.num_eq, - self.num_i + self.num_o + self.num_vars, - self.num_nw_vars)) + self.jacobian = OrderedDict() self.residual = np.zeros(self.num_eq) sum_eq = 0 for constraint in self.constraints.values(): num_eq = constraint['num_eq'] if constraint['constant_deriv']: - self.jacobian[sum_eq:sum_eq + num_eq] = constraint['deriv']() + constraint["deriv"](sum_eq) sum_eq += num_eq # done - msg = ( - 'The component ' + self.label + ' has ' + str(self.num_vars) + - ' custom variables.') + msg = f"The component {self.label} has {self.num_vars} variables." logger.debug(msg) - def get_variables(self): + def get_parameters(self): return {} def get_mandatory_constraints(self): - return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': self.num_i}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * self.num_i} - } + return {} @staticmethod def inlets(): @@ -435,6 +443,13 @@ def inlets(): def outlets(): return [] + @staticmethod + def is_variable(var, increment_filter=None): + if var.is_var: + if increment_filter is None or not increment_filter[var.J_col]: + return True + return False + def get_char_expr(self, param, type='rel', inconn=0, outconn=0): r""" Generic method to access characteristic function parameters. @@ -463,16 +478,15 @@ def get_char_expr(self, param, type='rel', inconn=0, outconn=0): """ if type == 'rel': if param == 'm': - return ( - self.inl[inconn].m.val_SI / self.inl[inconn].m.design) + return self.inl[inconn].m.val_SI / self.inl[inconn].m.design elif param == 'm_out': - return ( - self.outl[outconn].m.val_SI / - self.outl[outconn].m.design) + return self.outl[outconn].m.val_SI / self.outl[outconn].m.design elif param == 'v': v = self.inl[inconn].m.val_SI * v_mix_ph( - self.inl[inconn].get_flow(), - T0=self.inl[inconn].T.val_SI) + self.inl[inconn].p.val_SI, self.inl[inconn].h.val_SI, + self.inl[inconn].fluid_data, self.inl[inconn].mixing_rule, + T0=self.inl[inconn].T.val_SI + ) return v / self.inl[inconn].v.design elif param == 'pr': return ( @@ -482,8 +496,9 @@ def get_char_expr(self, param, type='rel', inconn=0, outconn=0): self.outl[outconn].p.design)) else: msg = ( - 'The parameter ' + str(param) + ' is not available ' - 'for characteristic function evaluation.') + f"The parameter {param}) is not available for " + "characteristic function evaluation." + ) logger.error(msg) raise ValueError(msg) else: @@ -493,12 +508,12 @@ def get_char_expr(self, param, type='rel', inconn=0, outconn=0): return self.outl[outconn].m.val_SI elif param == 'v': return self.inl[inconn].m.val_SI * v_mix_ph( - self.inl[inconn].get_flow(), - T0=self.inl[inconn].T.val_SI) + self.inl[inconn].p.val_SI, self.inl[inconn].h.val_SI, + self.inl[inconn].fluid_data, self.inl[inconn].mixing_rule, + T0=self.inl[inconn].T.val_SI + ) elif param == 'pr': - return ( - self.outl[outconn].p.val_SI / - self.inl[inconn].p.val_SI) + return self.outl[outconn].p.val_SI / self.inl[inconn].p.val_SI else: return False @@ -575,15 +590,17 @@ def solve(self, increment_filter): sum_eq = 0 for constraint in self.constraints.values(): num_eq = constraint['num_eq'] - self.residual[sum_eq:sum_eq + num_eq] = constraint['func']() + if num_eq > 0: + self.residual[sum_eq:sum_eq + num_eq] = constraint['func']() if not constraint['constant_deriv']: constraint['deriv'](increment_filter, sum_eq) sum_eq += num_eq - for parameter, data in self.variables.items(): + for data in self.parameters.values(): if data.is_set and data.func is not None: self.residual[sum_eq:sum_eq + data.num_eq] = data.func( - **data.func_params) + **data.func_params + ) data.deriv(increment_filter, sum_eq, **data.func_params) sum_eq += data.num_eq @@ -604,38 +621,6 @@ def bus_func(self, bus): """ return 0 - def bus_func_doc(self, bus): - r""" - Base method for LaTeX equation generation of the bus function. - - Parameters - ---------- - bus : tespy.connections.bus.Bus - TESPy bus object. - - Returns - ------- - latex : str - Bus function in LaTeX format. - """ - return None - - def bus_deriv(self, bus): - r""" - Base method for partial derivatives of the bus function. - - Parameters - ---------- - bus : tespy.connections.bus.Bus - TESPy bus object. - - Returns - ------- - deriv : ndarray - Matrix of partial derivatives. - """ - return np.zeros((1, self.num_i + self.num_o, self.num_nw_vars)) - def calc_bus_expr(self, bus): r""" Return the busses' characteristic line input expression. @@ -792,58 +777,6 @@ def initialise_target(self, c, key): """ return 0 - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - - conn_idx = self.inl.index(inconn) - outconn = self.outl[conn_idx] - - for fluid, x in inconn.fluid.val.items(): - if (not outconn.fluid.val_set[fluid] and - not outconn.good_starting_values): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: - return - - conn_idx = self.outl.index(outconn) - inconn = self.inl[conn_idx] - - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) - def set_parameters(self, mode, data): r""" Set or unset design values of component parameters. @@ -860,7 +793,7 @@ def set_parameters(self, mode, data): if mode == 'design' or self.local_design: self.new_design = True - for key, dc in self.variables.items(): + for key, dc in self.parameters.items(): if isinstance(dc, dc_cp): if ((mode == 'offdesign' and not self.local_design) or (mode == 'design' and self.local_offdesign)): @@ -875,10 +808,10 @@ def calc_parameters(self): def check_parameter_bounds(self): r"""Check parameter value limits.""" - for p in self.variables.keys(): + for p in self.parameters.keys(): data = self.get_attr(p) if isinstance(data, dc_cp): - if data.val > data.max_val + err: + if data.val > data.max_val + ERR : msg = ( 'Invalid value for ' + p + ': ' + p + ' = ' + str(data.val) + ' above maximum value (' + @@ -886,7 +819,7 @@ def check_parameter_bounds(self): '.') logger.warning(msg) - elif data.val < data.min_val - err: + elif data.val < data.min_val - ERR : msg = ( 'Invalid value for ' + p + ': ' + p + ' = ' + str(data.val) + ' below minimum value (' + @@ -905,9 +838,6 @@ def check_parameter_bounds(self): char_data.param, **char_data.char_params) char_data.char_func.get_domain_errors(expr, self.label) - def initialise_fluids(self): - return - def convergence_check(self): return @@ -930,135 +860,16 @@ def exergy_balance(self, T0): "chemical": np.nan, "physical": np.nan, "massless": np.nan } self.E_D = np.nan - self.epsilon = np.nan - - def get_plotting_data(self): - return - - def fluid_func(self): - r""" - Calculate the vector of residual values for fluid balance equations. - - Returns - ------- - residual : list - Vector of residual values for component's fluid balance. - - .. math:: - - 0 = x_{fl,in,i} - x_{fl,out,i} \; \forall fl \in - \text{network fluids,} \; \forall i \in \text{inlets} - """ - residual = [] - for i in range(self.num_i): - for fluid, x in self.inl[0].fluid.val.items(): - residual += [x - self.outl[0].fluid.val[fluid]] - return residual - - def fluid_func_doc(self, label): - r""" - Get fluid balance equations in LaTeX format. + self.epsilon = self._calc_epsilon() - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - indices = list(range(1, self.num_i + 1)) - if len(indices) > 1: - indices = ', '.join(str(idx) for idx in indices) + def _calc_epsilon(self): + if self.E_F == 0: + return np.nan else: - indices = str(indices[0]) - latex = ( - r'0=x_{fl\mathrm{,in,}i}-x_{fl\mathrm{,out,}i}\;' - r'\forall fl \in\text{network fluids,}' - r'\; \forall i \in [' + indices + r']') - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate partial derivatives for all fluid balance equations. + return self.E_P / self.E_F - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros((self.fluid_constraints['num_eq'], - 2 * self.num_i + self.num_vars, - 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 - - def mass_flow_func(self): - r""" - Calculate the residual value for mass flow balance equation. - - Returns - ------- - residual : list - Vector with residual value for component's mass flow balance. - - .. math:: - - 0 = \dot{m}_{in,i} -\dot{m}_{out,i} \;\forall i\in\text{inlets} - """ - residual = [] - for i in range(self.num_i): - residual += [self.inl[i].m.val_SI - self.outl[i].m.val_SI] - return residual - - def mass_flow_func_doc(self, label): - r""" - Get mass flow equations in LaTeX format. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - indices = list(range(1, self.num_i + 1)) - if len(indices) > 1: - indices = ', '.join(str(idx) for idx in indices) - else: - indices = str(indices[0]) - latex = ( - r'0=\dot{m}_{\mathrm{in,}i}-\dot{m}_{\mathrm{out,}i}' - r'\; \forall i \in [' + indices + r']') - return generate_latex_eq(self, latex, label) - - def mass_flow_deriv(self): - r""" - Calculate partial derivatives for all mass flow balance equations. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the mass flow balance - equations. - """ - deriv = np.zeros(( - self.num_i, - self.num_i + self.num_o + self.num_vars, - self.num_nw_vars)) - for i in range(self.num_i): - deriv[i, i, 0] = 1 - for j in range(self.num_o): - deriv[j, j + i + 1, 0] = -1 - return deriv + def get_plotting_data(self): + return def pressure_equality_func(self): r""" @@ -1102,7 +913,7 @@ def pressure_equality_func_doc(self, label): r'\; \forall i \in [' + indices + r']') return generate_latex_eq(self, latex, label) - def pressure_equality_deriv(self): + def pressure_equality_deriv(self, k): r""" Calculate partial derivatives for all mass flow balance equations. @@ -1112,15 +923,11 @@ def pressure_equality_deriv(self): Matrix with partial derivatives for the mass flow balance equations. """ - deriv = np.zeros(( - self.num_i, - self.num_i + self.num_o + self.num_vars, - self.num_nw_vars)) for i in range(self.num_i): - deriv[i, i, 1] = 1 - for j in range(self.num_o): - deriv[j, j + i + 1, 1] = -1 - return deriv + if self.inl[i].p.is_var: + self.jacobian[k + i, self.inl[i].p.J_col] = 1 + if self.outl[i].p.is_var: + self.jacobian[k + i, self.outl[i].p.J_col] = -1 def enthalpy_equality_func(self): r""" @@ -1164,7 +971,7 @@ def enthalpy_equality_func_doc(self, label): r'\; \forall i \in [' + indices + r']') return generate_latex_eq(self, latex, label) - def enthalpy_equality_deriv(self): + def enthalpy_equality_deriv(self, k): r""" Calculate partial derivatives for all mass flow balance equations. @@ -1174,92 +981,19 @@ def enthalpy_equality_deriv(self): Matrix with partial derivatives for the mass flow balance equations. """ - deriv = np.zeros(( - self.num_i, - self.num_i + self.num_o + self.num_vars, - self.num_nw_vars)) for i in range(self.num_i): - deriv[i, i, 2] = 1 - for j in range(self.num_o): - deriv[j, j + i + 1, 2] = -1 - return deriv + if self.inl[i].h.is_var: + self.jacobian[k + i, self.inl[i].h.J_col] = 1 + if self.outl[i].h.is_var: + self.jacobian[k + i, self.outl[i].h.J_col] = -1 - def numeric_deriv(self, func, dx, pos, **kwargs): + def numeric_deriv(self, func, dx, conn=None, **kwargs): r""" Calculate partial derivative of the function func to dx. - Parameters - ---------- - func : function - Function :math:`f` to calculate the partial derivative for. - - dx : str - Partial derivative. - - pos : int - Position of connection regarding to inlets and outlet of the - component, logic: ['in1', 'in2', ..., 'out1', ...] -> - 0, 1, ..., n, n + 1, ..., n + m - - Returns - ------- - deriv : float/list - Partial derivative(s) of the function :math:`f` to variable(s) - :math:`x`. - - .. math:: - - \frac{\partial f}{\partial x} = \frac{f(x + d) + f(x - d)}{2 d} + For details see :py:func:`tespy.tools.helpers._numeric_deriv` """ - if dx == 'fluid': - d = 1e-5 - conns = self.inl + self.outl - deriv = [] - for f in conns[0].fluid.val.keys(): - val = conns[pos].fluid.val[f] - if conns[pos].fluid.val[f] + d <= 1: - conns[pos].fluid.val[f] += d - else: - conns[pos].fluid.val[f] = 1 - exp = func(**kwargs) - if conns[pos].fluid.val[f] - 2 * d >= 0: - conns[pos].fluid.val[f] -= 2 * d - else: - conns[pos].fluid.val[f] = 0 - exp -= func(**kwargs) - conns[pos].fluid.val[f] = val - - deriv += [exp / (2 * d)] - - elif dx in ['m', 'p', 'h']: - - if dx == 'm': - d = 1e-4 - else: - d = 1e-1 - conns = self.inl + self.outl - conns[pos].get_attr(dx).val_SI += d - exp = func(**kwargs) - - conns[pos].get_attr(dx).val_SI -= 2 * d - exp -= func(**kwargs) - deriv = exp / (2 * d) - - conns[pos].get_attr(dx).val_SI += d - - else: - d = self.get_attr(dx).d - exp = 0 - self.get_attr(dx).val += d - exp += func(**kwargs) - - self.get_attr(dx).val -= 2 * d - exp -= func(**kwargs) - deriv = exp / (2 * d) - - self.get_attr(dx).val += d - - return deriv + return _numeric_deriv(self, func, dx, conn, **kwargs) def pr_func(self, pr='', inconn=0, outconn=0): r""" @@ -1340,11 +1074,14 @@ def pr_deriv(self, increment_filter, k, pr='', inconn=0, outconn=0): Connection index of outlet. """ pr = self.get_attr(pr) - self.jacobian[k, inconn, 1] = pr.val - self.jacobian[k, self.num_i + outconn, 1] = -1 + i = self.inl[inconn] + o = self.outl[inconn] + if i.p.is_var: + self.jacobian[k, i.p.J_col] = pr.val + if o.p.is_var: + self.jacobian[k, o.p.J_col] = -1 if pr.is_var: - pos = self.num_i + self.num_o + pr.var_pos - self.jacobian[k, pos, 0] = self.inl[inconn].p.val_SI + self.jacobian[k, self.pr.J_col] = i.p.val_SI def zeta_func(self, zeta='', inconn=0, outconn=0): r""" @@ -1389,17 +1126,19 @@ def zeta_func(self, zeta='', inconn=0, outconn=0): {8 \cdot \dot{m}^2 \cdot v} """ data = self.get_attr(zeta) - i = self.inl[inconn].get_flow() - o = self.outl[outconn].get_flow() + i = self.inl[inconn] + o = self.outl[outconn] - if abs(i[0]) < 1e-4: - return i[1] - o[1] + if abs(i.m.val_SI) < 1e-4: + return i.p.val_SI - o.p.val_SI else: - 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 (data.val - (i[1] - o[1]) * np.pi ** 2 / - (8 * abs(i[0]) * i[0] * (v_i + v_o) / 2)) + v_i = v_mix_ph(i.p.val_SI, i.h.val_SI, i.fluid_data, i.mixing_rule, T0=i.T.val_SI) + v_o = v_mix_ph(o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule, T0=o.T.val_SI) + return ( + data.val - (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (8 * abs(i.m.val_SI) * i.m.val_SI * (v_i + v_o) / 2) + ) def zeta_func_doc(self, label, zeta='', inconn=0, outconn=0): r""" @@ -1460,24 +1199,19 @@ def zeta_deriv(self, increment_filter, k, zeta='', inconn=0, outconn=0): """ data = self.get_attr(zeta) f = self.zeta_func - outpos = self.num_i + outconn - if not increment_filter[inconn, 0]: - self.jacobian[k, inconn, 0] = self.numeric_deriv( - f, 'm', inconn, zeta=zeta, inconn=inconn, outconn=outconn) - if not increment_filter[inconn, 2]: - self.jacobian[k, inconn, 1] = self.numeric_deriv( - f, 'p', inconn, zeta=zeta, inconn=inconn, outconn=outconn) - if not increment_filter[inconn, 2]: - self.jacobian[k, inconn, 2] = self.numeric_deriv( - f, 'h', inconn, zeta=zeta, inconn=inconn, outconn=outconn) - if not increment_filter[outpos, 1]: - self.jacobian[k, outpos, 1] = self.numeric_deriv( - f, 'p', outpos, zeta=zeta, inconn=inconn, outconn=outconn) - if not increment_filter[outpos, 2]: - self.jacobian[k, outpos, 2] = self.numeric_deriv( - f, 'h', outpos, zeta=zeta, inconn=inconn, outconn=outconn) + i = self.inl[inconn] + o = self.outl[outconn] + kwargs = dict(zeta=zeta, inconn=inconn, outconn=outconn) + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i, **kwargs) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i, **kwargs) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i, **kwargs) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o, **kwargs) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o, **kwargs) # custom variable zeta if data.is_var: - pos = self.num_i + self.num_o + data.var_pos - self.jacobian[k, pos, 0] = self.numeric_deriv( - f, zeta, 2, zeta=zeta, inconn=inconn, outconn=outconn) + self.jacobian[k, data.J_col] = self.numeric_deriv(f, zeta, None, **kwargs) diff --git a/src/tespy/components/customs/orc_evaporator.py b/src/tespy/components/customs/orc_evaporator.py deleted file mode 100644 index d219a0d7e..000000000 --- a/src/tespy/components/customs/orc_evaporator.py +++ /dev/null @@ -1,755 +0,0 @@ -# -*- coding: utf-8 - -"""Module of class ORCEvaporator. - -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/orc_evaporator.py - -SPDX-License-Identifier: MIT -""" -import warnings - -import numpy as np - -from tespy.components.component import Component -from tespy.tools.data_containers import ComponentProperties as dc_cp -from tespy.tools.data_containers import SimpleDataContainer as dc_simple -from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import dh_mix_dpQ -from tespy.tools.fluid_properties import h_mix_pQ -from tespy.tools.fluid_properties import h_mix_pT -from tespy.tools.fluid_properties import s_mix_ph - - -class ORCEvaporator(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. - - **Mandatory Equations** - - - :py:meth:`tespy.components.component.Component.fluid_func` - - :py:meth:`tespy.components.component.Component.mass_flow_func` - - :py:meth:`tespy.components.customs.orc_evaporator.ORCEvaporator.energy_balance_func` - - steam side outlet state, function can be disabled by specifying - :code:`set_attr(subcooling=True)` - :py:meth:`tespy.components.customs.orc_evaporator.ORCEvaporator.subcooling_func` - - working fluid outlet state, function can be disabled by specifying - :code:`set_attr(overheating=True)` - :py:meth:`tespy.components.customs.orc_evaporator.ORCEvaporator.overheating_func` - - **Optional Equations** - - - :py:meth:`tespy.components.customs.orc_evaporator.ORCEvaporator.energy_balance_cold_func` - - hot side steam :py:meth:`tespy.components.component.Component.pr_func` - - hot side brine :py:meth:`tespy.components.component.Component.pr_func` - - worling fluid :py:meth:`tespy.components.component.Component.pr_func` - - hot side steam :py:meth:`tespy.components.component.Component.zeta_func` - - hot side brine :py:meth:`tespy.components.component.Component.zeta_func` - - worling fluid :py:meth:`tespy.components.component.Component.zeta_func` - - - 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/ORCEvaporator.svg - :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, dict - Heat transfer, :math:`Q/\text{W}`. - - pr1 : float, dict, :code:`"var"` - Outlet to inlet pressure ratio at hot side 1 (steam), - :math:`pr/1`. - - pr2 : float, dict, :code:`"var"` - Outlet to inlet pressure ratio at hot side 2 (brine), - :math:`pr/1`. - - pr3 : float, dict, :code:`"var"` - Outlet to inlet pressure ratio at cold side (working fluid), - :math:`pr/1`. - - zeta1 : float, dict, :code:`"var"` - Geometry independent friction coefficient at hot side 1 (steam), - :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - - zeta2 : float, dict, :code:`"var"` - Geometry independent friction coefficient at hot side 2 (brine), - :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - - zeta3 : float, dict, :code:`"var"` - Geometry independent friction coefficient at cold side (working fluid), - :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. - - subcooling : boolean - Enable/disable subcooling at oulet of the hot side 1, - default value: disabled (False). - - overheating : boolean - 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 set: 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.components import Source, Sink, ORCEvaporator - >>> from tespy.connections import Connection - >>> from tespy.networks import Network - >>> fluids = ['water', 'Isopentane'] - >>> nw = Network(fluids=fluids, iterinfo=False) - >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') - >>> evaporator = ORCEvaporator('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 - """ - - def __init__(self, label, **kwargs): - super().__init__(label, **kwargs) - msg = "The component ORCEvaporator will be depricated with the next major release." - warnings.warn(msg, DeprecationWarning) - - @staticmethod - def component(): - return 'orc evaporator' - - def get_variables(self): - return { - 'Q': dc_cp( - max_val=0, num_eq=1, latex=self.energy_balance_cold_func_doc, - func=self.energy_balance_cold_func, - deriv=self.energy_balance_cold_deriv), - 'pr1': dc_cp( - min_val=1e-4, max_val=1, num_eq=1, deriv=self.pr_deriv, - latex=self.pr_func_doc, - func=self.pr_func, func_params={'pr': 'pr1'}), - 'pr2': dc_cp( - min_val=1e-4, max_val=1, num_eq=1, latex=self.pr_func_doc, - deriv=self.pr_deriv, func=self.pr_func, - func_params={'pr': 'pr2', 'inconn': 1, 'outconn': 1}), - 'pr3': dc_cp( - min_val=1e-4, max_val=1, num_eq=1, latex=self.pr_func_doc, - deriv=self.pr_deriv, func=self.pr_func, - func_params={'pr': 'pr3', 'inconn': 2, 'outconn': 2}), - 'zeta1': dc_cp( - min_val=0, max_val=1e15, num_eq=1, latex=self.zeta_func_doc, - deriv=self.zeta_deriv, func=self.zeta_func, - func_params={'zeta': 'zeta1'}), - 'zeta2': dc_cp( - min_val=0, max_val=1e15, num_eq=1, latex=self.zeta_func_doc, - deriv=self.zeta_deriv, func=self.zeta_func, - func_params={'zeta': 'zeta2', 'inconn': 1, 'outconn': 1}), - 'zeta3': dc_cp( - min_val=0, max_val=1e15, num_eq=1, latex=self.zeta_func_doc, - deriv=self.zeta_deriv, func=self.zeta_func, - func_params={'zeta': 'zeta3', 'inconn': 2, 'outconn': 2}), - 'subcooling': dc_simple( - val=False, num_eq=1, latex=self.subcooling_func_doc, - deriv=self.subcooling_deriv, func=self.subcooling_func), - 'overheating': dc_simple( - val=False, num_eq=1, latex=self.overheating_func_doc, - deriv=self.overheating_deriv, func=self.overheating_func) - } - - def get_mandatory_constraints(self): - return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 3}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 3}, - 'energy_balance_constraints': { - 'func': self.energy_balance_func, - 'deriv': self.energy_balance_deriv, - 'constant_deriv': False, 'latex': self.energy_balance_func_doc, - 'num_eq': 1} - } - - @staticmethod - def inlets(): - return ['in1', 'in2', 'in3'] - - @staticmethod - def outlets(): - return ['out1', 'out2', 'out3'] - - def preprocess(self, nw): - - self.overheating.is_set = not self.overheating.val - self.subcooling.is_set = not self.subcooling.val - super().preprocess(nw) - - def energy_balance_func(self): - r""" - Equation for heat exchanger energy balance. - - Returns - ------- - residual : float - Residual value of equation. - - .. math:: - - \begin{split} - 0 = & - \dot{m}_{in,1} \cdot \left(h_{out,1} - h_{in,1} \right) \\ - &+ \dot{m}_{in,2} \cdot \left(h_{out,2} - h_{in,2} \right) \\ - &+ \dot{m}_{in,3} \cdot \left(h_{out,3} - h_{in,3} \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 energy_balance_func_doc(self, label): - r""" - Equation for heat exchanger energy balance. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'\begin{split}' + '\n' - r'0 = &' + '\n' - r'\dot{m}_\mathrm{in,1}\cdot\left(h_\mathrm{out,1}-' - r'h_\mathrm{in,1}\right) \\' + '\n' - r'&+ \dot{m}_\mathrm{in,2} \cdot \left(h_\mathrm{out,2} - ' - r'h_\mathrm{in,2} \right)\\' + '\n' - r'&+ \dot{m}_\mathrm{in,3} \cdot \left(h_\mathrm{out,3} - ' - r'h_\mathrm{in,3} \right)' + '\n' - r'\end{split}') - return generate_latex_eq(self, latex, latex) - - def energy_balance_deriv(self, increment_filter, k): - """ - Calculate partial derivatives of energy balance function. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ - for i in range(3): - self.jacobian[k, i, 0] = ( - self.outl[i].h.val_SI - self.inl[i].h.val_SI) - self.jacobian[k, i, 2] = -self.inl[i].m.val_SI - self.jacobian[k, i + 3, 2] = self.inl[i].m.val_SI - k += 1 - - def energy_balance_cold_func(self): - r""" - Equation for cold side heat exchanger energy balance. - - Returns - ------- - residual : float - Residual value of equation. - - .. math:: - - 0 =\dot{m}_{in,3} \cdot \left(h_{out,3}-h_{in,3}\right)+\dot{Q} - """ - return self.inl[2].m.val_SI * ( - self.outl[2].h.val_SI - self.inl[2].h.val_SI) + self.Q.val - - def energy_balance_cold_func_doc(self, label): - r""" - Equation for cold side heat exchanger energy balance. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'0 =\dot{m}_{in,3} \cdot \left(h_{out,3}-' - r'h_{in,3}\right)+\dot{Q}') - return [generate_latex_eq(self, latex, label)] - - def energy_balance_cold_deriv(self, increment_filter, k): - """ - Partial derivatives for cold side energy balance. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ - self.jacobian[k, 2, 0] = self.outl[2].h.val_SI - self.inl[2].h.val_SI - self.jacobian[k, 2, 2] = -self.inl[2].m.val_SI - self.jacobian[k, 5, 2] = self.inl[2].m.val_SI - - def subcooling_func(self): - r""" - Equation for steam side outlet state. - - Returns - ------- - residual : float - Residual value of equation. - - .. math:: - - 0=h_{out,1} -h\left(p_{out,1}, x=0 \right) - - Note - ---- - This equation is applied in case subcooling is False! - """ - return self.outl[0].h.val_SI - h_mix_pQ(self.outl[0].get_flow(), 0) - - def subcooling_func_doc(self, label): - r""" - Equation for steam side outlet state. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = r'0=h_\mathrm{out,1} -h\left(p_\mathrm{out,1}, x=0 \right)' - return generate_latex_eq(self, latex, label) - - def subcooling_deriv(self, increment_filter, k): - """ - Calculate partial derivatives for steam side outlet state. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ - self.jacobian[k, 3, 1] = -dh_mix_dpQ(self.outl[0].get_flow(), 0) - self.jacobian[k, 3, 2] = 1 - - def overheating_func(self): - r""" - Equation for cold side outlet state. - - Returns - ------- - residual : float - Residual value of equation. - - .. math:: - - 0=h_{out,3} -h\left(p_{out,3}, x=1 \right) - - Note - ---- - This equation is applied in case overheating is False! - """ - return self.outl[2].h.val_SI - h_mix_pQ(self.outl[2].get_flow(), 1) - - def overheating_func_doc(self, label): - r""" - Equation for cold side outlet state. - - Parameters - ---------- - label : str - Label for equation. - """ - latex = r'0=h_\mathrm{out,3} -h\left(p_\mathrm{out,3}, x=1 \right)' - return generate_latex_eq(self, latex, label) - - def overheating_deriv(self, increment_filter, k): - """ - Calculate partial derivatives for cold side outlet state. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ - self.jacobian[k, 5, 1] = -dh_mix_dpQ(self.outl[0].get_flow(), 0) - self.jacobian[k, 5, 2] = 1 - - def bus_func(self, bus): - r""" - Calculate the value of the bus function. - - Parameters - ---------- - bus : tespy.connections.bus.Bus - TESPy bus object. - - Returns - ------- - val : float - Value of energy transfer :math:`\dot{E}`. This value is passed to - :py:meth:`tespy.components.component.Component.calc_bus_value` - for value manipulation according to the specified characteristic - line of the bus. - - .. math:: - - \dot{E} = -\dot{m}_{in,3} \cdot \left( - h_{out,3} - h_{in,3} \right) - """ - return -self.inl[2].m.val_SI * ( - self.outl[2].h.val_SI - self.inl[2].h.val_SI) - - def bus_func_doc(self, bus): - r""" - Return LaTeX string of the bus function. - - Parameters - ---------- - bus : tespy.connections.bus.Bus - TESPy bus object. - - Returns - ------- - latex : str - LaTeX string of bus function. - """ - return ( - r'-\dot{m}_\mathrm{in,3} \cdot \left(h_\mathrm{out,3} - ' - r'h_\mathrm{in,3} \right)') - - def bus_deriv(self, bus): - r""" - Calculate the matrix of partial derivatives of the bus function. - - Parameters - ---------- - bus : tespy.connections.bus.Bus - TESPy bus object. - - Returns - ------- - deriv : ndarray - Matrix of partial derivatives. - """ - deriv = np.zeros((1, 6, self.num_nw_vars)) - f = self.calc_bus_value - deriv[0, 2, 0] = self.numeric_deriv(f, 'm', 2, bus=bus) - deriv[0, 2, 2] = self.numeric_deriv(f, 'h', 2, bus=bus) - deriv[0, 5, 2] = self.numeric_deriv(f, '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 - 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': - if c.source_id == 'out1': - T = 200 + 273.15 - return h_mix_pT(c.get_flow(), T) - elif c.source_id == 'out2': - T = 200 + 273.15 - return h_mix_pT(c.get_flow(), T) - else: - T = 250 + 273.15 - return h_mix_pT(c.get_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 - 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': - if c.target_id == 'in1': - T = 300 + 273.15 - return h_mix_pT(c.get_flow(), T) - elif c.target_id == 'in2': - T = 300 + 273.15 - return h_mix_pT(c.get_flow(), T) - else: - T = 220 + 273.15 - return h_mix_pT(c.get_flow(), T) - - def calc_parameters(self): - r"""Postprocessing parameter calculation.""" - # component parameters - self.Q.val = -self.inl[2].m.val_SI * ( - self.outl[2].h.val_SI - self.inl[2].h.val_SI) - # pressure ratios and zeta values - for i in range(3): - self.get_attr('pr' + str(i + 1)).val = ( - self.outl[i].p.val_SI / self.inl[i].p.val_SI) - self.get_attr('zeta' + str(i + 1)).val = ( - (self.inl[i].p.val_SI - self.outl[i].p.val_SI) * np.pi ** 2 / ( - 4 * self.inl[i].m.val_SI ** 2 * - (self.inl[i].vol.val_SI + self.outl[i].vol.val_SI) - )) - - def entropy_balance(self): - r""" - Calculate entropy balance of the two-phase orc evaporator. - - The allocation of the entropy streams due to heat exchanged and due to - irreversibility is performed by solving for T on all sides of the heat - exchanger: - - .. math:: - - h_\mathrm{out} - h_\mathrm{in} = \int_\mathrm{in}^\mathrm{out} v - \cdot dp - \int_\mathrm{in}^\mathrm{out} T \cdot ds - - As solving :math:`\int_\mathrm{in}^\mathrm{out} v \cdot dp` for non - isobaric processes would require perfect process knowledge (the path) - on how specific volume and pressure change throught the component, the - heat transfer is splitted into three separate virtual processes: - - - in->in*: decrease pressure to - :math:`p_\mathrm{in*}=p_\mathrm{in}\cdot\sqrt{\frac{p_\mathrm{out}}{p_\mathrm{in}}}` - without changing enthalpy. - - in*->out* transfer heat without changing pressure. - :math:`h_\mathrm{out*}-h_\mathrm{in*}=h_\mathrm{out}-h_\mathrm{in}` - - out*->out decrease pressure to outlet pressure :math:`p_\mathrm{out}` - without changing enthalpy. - - Note - ---- - The entropy balance makes the follwing parameter available: - - .. math:: - - \text{S\_Q1}=\dot{m} \cdot \left(s_\mathrm{out*,1}-s_\mathrm{in*,1} - \right)\\ - \text{S\_Q2}=\dot{m} \cdot \left(s_\mathrm{out*,2}-s_\mathrm{in*,2} - \right)\\ - \text{S\_Q3}=\dot{m} \cdot \left(s_\mathrm{out*,3}-s_\mathrm{in*,3} - \right)\\ - \text{S\_Qirr}=\text{S\_Q3} - \text{S\_Q1} - \text{S\_Q2}\\ - \text{S\_irr1}=\dot{m} \cdot \left(s_\mathrm{out,1}-s_\mathrm{in,1} - \right) - \text{S\_Q1}\\ - \text{S\_irr2}=\dot{m} \cdot \left(s_\mathrm{out,2}-s_\mathrm{in,2} - \right) - \text{S\_Q2}\\ - \text{S\_irr3}=\dot{m} \cdot \left(s_\mathrm{out,3}-s_\mathrm{in,3} - \right) - \text{S\_Q3}\\ - \text{S\_irr}=\sum \dot{S}_\mathrm{irr}\\ - \text{T\_mQ1}=\frac{\dot{Q}_1}{\text{S\_Q1}}\\ - \text{T\_mQ2}=\frac{\dot{Q}_2}{\text{S\_Q2}}\\ - \text{T\_mQ3}=\frac{\dot{Q}_1 + \dot{Q}_2}{\text{S\_Q3}} - """ - self.S_irr = 0 - for i in range(3): - inl = self.inl[i] - out = self.outl[i] - p_star = inl.p.val_SI * ( - self.get_attr('pr' + str(i + 1)).val) ** 0.5 - s_i_star = s_mix_ph( - [0, p_star, inl.h.val_SI, inl.fluid.val], T0=inl.T.val_SI) - s_o_star = s_mix_ph( - [0, p_star, out.h.val_SI, out.fluid.val], T0=out.T.val_SI) - - setattr(self, 'S_Q' + str(i + 1), - inl.m.val_SI * (s_o_star - s_i_star)) - S_Q = self.get_attr('S_Q' + str(i + 1)) - setattr(self, 'S_irr' + str(i + 1), - inl.m.val_SI * (out.s.val_SI - inl.s.val_SI) - S_Q) - setattr(self, 'T_mQ' + str(i + 1), - inl.m.val_SI * (out.h.val_SI - inl.h.val_SI) / S_Q) - - self.S_irr += self.get_attr('S_irr' + str(i + 1)) - - self.S_irr += self.S_Q1 + self.S_Q2 + self.S_Q3 - - def get_plotting_data(self): - """Generate a dictionary containing FluProDia plotting information. - - Returns - ------- - data : dict - A nested dictionary containing the keywords required by the - :code:`calc_individual_isoline` method of the - :code:`FluidPropertyDiagram` class. First level keys are the - connection index ('in1' -> 'out1', therefore :code:`1` etc.). - """ - return { - i + 1: { - 'isoline_property': 'p', - 'isoline_value': self.inl[i].p.val, - 'isoline_value_end': self.outl[i].p.val, - 'starting_point_property': 'v', - 'starting_point_value': self.inl[i].vol.val, - 'ending_point_property': 'v', - 'ending_point_value': self.outl[i].vol.val - } for i in range(3)} diff --git a/src/tespy/components/heat_exchangers/base.py b/src/tespy/components/heat_exchangers/base.py index 365bc44ea..82830fac8 100644 --- a/src/tespy/components/heat_exchangers/base.py +++ b/src/tespy/components/heat_exchangers/base.py @@ -17,7 +17,6 @@ from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import h_mix_pT from tespy.tools.fluid_properties import s_mix_ph @@ -149,8 +148,7 @@ class HeatExchanger(Component): >>> from tespy.networks import Network >>> from tespy.tools import document_model >>> import shutil - >>> nw = Network(fluids=['water', 'air'], T_unit='C', p_unit='bar', - ... h_unit='kJ / kg', iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', iterinfo=False) >>> exhaust_hot = Source('Exhaust air outlet') >>> exhaust_cold = Sink('Exhaust air inlet') >>> cw_cold = Source('cooling water inlet') @@ -170,9 +168,9 @@ class HeatExchanger(Component): >>> he.set_attr(pr1=0.98, pr2=0.98, ttd_u=5, ... design=['pr1', 'pr2', 'ttd_u'], offdesign=['zeta1', 'zeta2', 'kA_char']) - >>> cw_he.set_attr(fluid={'air': 0, 'water': 1}, T=10, p=3, + >>> cw_he.set_attr(fluid={'water': 1}, T=10, p=3, ... offdesign=['m']) - >>> ex_he.set_attr(fluid={'air': 1, 'water': 0}, v=0.1, T=35) + >>> ex_he.set_attr(fluid={'air': 1}, v=0.1, T=35) >>> he_ex.set_attr(T=17.5, p=1, design=['T']) >>> nw.solve('design') >>> nw.save('tmp') @@ -198,7 +196,7 @@ class HeatExchanger(Component): def component(): return 'heat exchanger' - def get_variables(self): + def get_parameters(self): return { 'Q': dc_cp( max_val=0, func=self.energy_balance_hot_func, num_eq=1, @@ -242,14 +240,6 @@ def get_variables(self): def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 2}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 2}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -280,10 +270,11 @@ def energy_balance_func(self): \dot{m}_{in,2} \cdot \left(h_{out,2} - h_{in,2} \right) """ 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[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) + ) def energy_balance_func_doc(self, label): r""" @@ -317,13 +308,14 @@ def energy_balance_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - for i in range(2): - self.jacobian[k, i, 0] = ( - self.outl[i].h.val_SI - self.inl[i].h.val_SI) - self.jacobian[k, i, 2] = -self.inl[i].m.val_SI - - self.jacobian[k, 2, 2] = self.inl[0].m.val_SI - self.jacobian[k, 3, 2] = self.inl[1].m.val_SI + for _c_num, i in enumerate(self.inl): + o = self.outl[_c_num] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = i.m.val_SI def energy_balance_hot_func(self): r""" @@ -339,7 +331,8 @@ def energy_balance_hot_func(self): 0 =\dot{m}_{in,1} \cdot \left(h_{out,1}-h_{in,1}\right)-\dot{Q} """ return self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - self.Q.val + self.outl[0].h.val_SI - self.inl[0].h.val_SI + ) - self.Q.val def energy_balance_hot_func_doc(self, label): r""" @@ -372,9 +365,14 @@ def energy_balance_hot_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 0, 0] = self.outl[0].h.val_SI - self.inl[0].h.val_SI - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI - self.jacobian[k, 2, 2] = self.inl[0].m.val_SI + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.h): + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if self.is_variable(o.h): + self.jacobian[k, o.h.J_col] = i.m.val_SI def calculate_td_log(self): i1 = self.inl[0] @@ -383,10 +381,10 @@ def calculate_td_log(self): o2 = self.outl[1] # temperature value manipulation for convergence stability - T_i1 = T_mix_ph(i1.get_flow(), T0=i1.T.val_SI) - T_i2 = T_mix_ph(i2.get_flow(), T0=i2.T.val_SI) - T_o1 = T_mix_ph(o1.get_flow(), T0=o1.T.val_SI) - T_o2 = T_mix_ph(o2.get_flow(), T0=o2.T.val_SI) + T_i1 = i1.calc_T(T0=i1.T.val_SI) + T_i2 = i2.calc_T(T0=i2.T.val_SI) + T_o1 = o1.calc_T(T0=o1.T.val_SI) + T_o2 = o2.calc_T(T0=o2.T.val_SI) if T_i1 <= T_o2: T_i1 = T_o2 + 0.01 @@ -466,12 +464,15 @@ def kA_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.kA_func - self.jacobian[k, 0, 0] = self.outl[0].h.val_SI - self.inl[0].h.val_SI - for i in range(4): - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - if not increment_filter[i, 2]: - self.jacobian[k, i, 2] = self.numeric_deriv(f, 'h', i) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + for c in self.inl + self.outl: + if self.is_variable(c.p): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) def kA_char_func(self): r""" @@ -556,15 +557,14 @@ def kA_char_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.kA_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[1, 0]: - self.jacobian[k, 1, 0] = self.numeric_deriv(f, 'm', 1) - for i in range(4): - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - if not increment_filter[i, 2]: - self.jacobian[k, i, 2] = self.numeric_deriv(f, 'h', i) + for i in self.inl: + if self.is_variable(i.m): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + for c in self.inl + self.outl: + if self.is_variable(c.p): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) def ttd_u_func(self): r""" @@ -579,8 +579,10 @@ def ttd_u_func(self): 0 = ttd_{u} - T_{in,1} + T_{out,2} """ - T_i1 = T_mix_ph(self.inl[0].get_flow(), T0=self.inl[0].T.val_SI) - T_o2 = T_mix_ph(self.outl[1].get_flow(), T0=self.outl[1].T.val_SI) + i = self.inl[0] + o = self.outl[1] + T_i1 = i.calc_T(T0=i.T.val_SI) + T_o2 = o.calc_T(T0=o.T.val_SI) return self.ttd_u.val - T_i1 + T_o2 def ttd_u_func_doc(self, label): @@ -613,11 +615,11 @@ def ttd_u_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.ttd_u_func - for i in [0, 3]: - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - if not increment_filter[i, 2]: - self.jacobian[k, i, 2] = self.numeric_deriv(f, 'h', i) + for c in [self.inl[0], self.outl[1]]: + if self.is_variable(c.p, increment_filter): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h, increment_filter): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) def ttd_l_func(self): r""" @@ -632,8 +634,10 @@ def ttd_l_func(self): 0 = ttd_{l} - T_{out,1} + T_{in,2} """ - T_i2 = T_mix_ph(self.inl[1].get_flow(), T0=self.inl[1].T.val_SI) - T_o1 = T_mix_ph(self.outl[0].get_flow(), T0=self.outl[0].T.val_SI) + i = self.inl[1] + o = self.outl[0] + T_i2 = i.calc_T(T0=i.T.val_SI) + T_o1 = o.calc_T(T0=o.T.val_SI) return self.ttd_l.val - T_o1 + T_i2 def ttd_l_func_doc(self, label): @@ -666,11 +670,11 @@ def ttd_l_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.ttd_l_func - for i in [1, 2]: - if not increment_filter[i, 1]: - self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i) - if not increment_filter[i, 2]: - self.jacobian[k, i, 2] = self.numeric_deriv(f, 'h', i) + for c in [self.inl[1], self.outl[0]]: + if self.is_variable(c.p, increment_filter): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h, increment_filter): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) def bus_func(self, bus): r""" @@ -695,7 +699,8 @@ def bus_func(self, bus): h_{out,1} - h_{in,1} \right) """ return self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) + self.outl[0].h.val_SI - self.inl[0].h.val_SI + ) def bus_func_doc(self, bus): r""" @@ -729,12 +734,21 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 4, self.num_nw_vars)) f = self.calc_bus_value - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - deriv[0, 2, 2] = self.numeric_deriv(f, 'h', 2, bus=bus) - return deriv + if self.inl[0].m.is_var: + if self.inl[0].m.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].m.J_col] = 0 + bus.jacobian[self.inl[0].m.J_col] -= self.numeric_deriv(f, 'm', self.inl[0], bus=bus) + + if self.inl[0].h.is_var: + if self.inl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].h.J_col] = 0 + bus.jacobian[self.inl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.inl[0], bus=bus) + + if self.outl[0].h.is_var: + if self.outl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.outl[0].h.J_col] = 0 + bus.jacobian[self.outl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.outl[0], bus=bus) def initialise_source(self, c, key): r""" @@ -766,10 +780,9 @@ def initialise_source(self, c, key): elif key == 'h': if c.source_id == 'out1': T = 200 + 273.15 - return h_mix_pT(c.get_flow(), T) else: T = 250 + 273.15 - return h_mix_pT(c.get_flow(), T) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def initialise_target(self, c, key): r""" @@ -801,10 +814,9 @@ def initialise_target(self, c, key): elif key == 'h': if c.target_id == 'in1': T = 300 + 273.15 - return h_mix_pT(c.get_flow(), T) else: T = 220 + 273.15 - return h_mix_pT(c.get_flow(), T) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def calc_parameters(self): r"""Postprocessing parameter calculation.""" @@ -887,17 +899,27 @@ def entropy_balance(self): p_star = inl.p.val_SI * ( self.get_attr('pr' + str(i + 1)).val) ** 0.5 s_i_star = s_mix_ph( - [0, p_star, inl.h.val_SI, inl.fluid.val], T0=inl.T.val_SI) + p_star, inl.h.val_SI, inl.fluid_data, inl.mixing_rule, + T0=inl.T.val_SI + ) s_o_star = s_mix_ph( - [0, p_star, out.h.val_SI, out.fluid.val], T0=out.T.val_SI) - - setattr(self, 'S_Q' + str(i + 1), - inl.m.val_SI * (s_o_star - s_i_star)) + p_star, out.h.val_SI, out.fluid_data, out.mixing_rule, + T0=out.T.val_SI + ) + + setattr( + self, 'S_Q' + str(i + 1), + inl.m.val_SI * (s_o_star - s_i_star) + ) S_Q = self.get_attr('S_Q' + str(i + 1)) - setattr(self, 'S_irr' + str(i + 1), - inl.m.val_SI * (out.s.val_SI - inl.s.val_SI) - S_Q) - setattr(self, 'T_mQ' + str(i + 1), - inl.m.val_SI * (out.h.val_SI - inl.h.val_SI) / S_Q) + setattr( + self, 'S_irr' + str(i + 1), + inl.m.val_SI * (out.s.val_SI - inl.s.val_SI) - S_Q + ) + setattr( + self, 'T_mQ' + str(i + 1), + inl.m.val_SI * (out.h.val_SI - inl.h.val_SI) / S_Q + ) self.S_irr += self.get_attr('S_irr' + str(i + 1)) @@ -1014,7 +1036,7 @@ def exergy_balance(self, T0): self.E_D = self.E_F else: self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/heat_exchangers/condenser.py b/src/tespy/components/heat_exchangers/condenser.py index a5f5f7378..241a9c01b 100644 --- a/src/tespy/components/heat_exchangers/condenser.py +++ b/src/tespy/components/heat_exchangers/condenser.py @@ -19,8 +19,8 @@ from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_bp_p from tespy.tools.fluid_properties import T_mix_ph +from tespy.tools.fluid_properties import T_sat_p from tespy.tools.fluid_properties import dh_mix_dpQ from tespy.tools.fluid_properties import h_mix_pQ @@ -156,10 +156,10 @@ class Condenser(HeatExchanger): >>> from tespy.components import Sink, Source, Condenser >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools.fluid_properties import T_bp_p + >>> from tespy.tools.fluid_properties import T_sat_p >>> import shutil - >>> nw = Network(fluids=['water', 'air'], T_unit='C', p_unit='bar', - ... h_unit='kJ / kg', m_range=[0.01, 1000], iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', + ... m_range=[0.01, 1000], iterinfo=False) >>> amb_in = Source('ambient air inlet') >>> amb_out = Sink('air outlet') >>> waste_steam = Source('waste steam') @@ -179,8 +179,8 @@ class Condenser(HeatExchanger): >>> cond.set_attr(pr1=0.98, pr2=0.999, ttd_u=15, design=['pr2', 'ttd_u'], ... offdesign=['zeta2', 'kA_char']) - >>> ws_he.set_attr(fluid={'water': 1, 'air': 0}, h=2700, m=1) - >>> amb_he.set_attr(fluid={'water': 0, 'air': 1}, T=20, offdesign=['v']) + >>> ws_he.set_attr(fluid={'water': 1}, h=2700, m=1) + >>> amb_he.set_attr(fluid={'air': 1}, T=20, offdesign=['v']) >>> he_amb.set_attr(p=1, T=40, design=['T']) >>> nw.solve('design') >>> nw.save('tmp') @@ -188,14 +188,14 @@ class Condenser(HeatExchanger): 103.17 >>> round(ws_he.T.val - he_amb.T.val, 1) 66.9 - >>> round(T_bp_p(ws_he.get_flow()) - 273.15 - he_amb.T.val, 1) + >>> round(ws_he.calc_T_sat() - 273.15 - he_amb.T.val, 1) 15.0 >>> ws_he.set_attr(m=0.7) >>> amb_he.set_attr(T=30) >>> nw.solve('offdesign', design_path='tmp') >>> round(ws_he.T.val - he_amb.T.val, 1) 62.5 - >>> round(T_bp_p(ws_he.get_flow()) - 273.15 - he_amb.T.val, 1) + >>> round(ws_he.calc_T_sat() - 273.15 - he_amb.T.val, 1) 11.3 It is possible to activate subcooling. The difference to boiling point @@ -206,7 +206,7 @@ class Condenser(HeatExchanger): >>> nw.solve('offdesign', design_path='tmp') >>> round(ws_he.T.val - he_amb.T.val, 1) 62.5 - >>> round(T_bp_p(ws_he.get_flow()) - 273.15 - he_amb.T.val, 1) + >>> round(ws_he.calc_T_sat() - 273.15 - he_amb.T.val, 1) 13.4 >>> shutil.rmtree('./tmp', ignore_errors=True) """ @@ -215,7 +215,7 @@ class Condenser(HeatExchanger): def component(): return 'condenser' - def get_variables(self): + def get_parameters(self): return { 'Q': dc_cp( max_val=0, func=self.energy_balance_hot_func, num_eq=1, @@ -260,11 +260,11 @@ def get_variables(self): deriv=self.subcooling_deriv, func=self.subcooling_func) } - def preprocess(self, nw): + def preprocess(self, num_nw_vars): # if subcooling is True, outlet state method must not be calculated self.subcooling.is_set = not self.subcooling.val - super().preprocess(nw) + super().preprocess(num_nw_vars) def subcooling_func(self): r""" @@ -283,7 +283,8 @@ def subcooling_func(self): ---- This equation is applied in case subcooling is False! """ - return self.outl[0].h.val_SI - h_mix_pQ(self.outl[0].get_flow(), 0) + o = self.outl[0] + return o.h.val_SI - h_mix_pQ(o.p.val_SI, 0, o.fluid_data) def subcooling_func_doc(self, label): r""" @@ -314,8 +315,11 @@ def subcooling_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 2, 1] = -dh_mix_dpQ(self.outl[0].get_flow(), 0) - self.jacobian[k, 2, 2] = 1 + o = self.outl[0] + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -dh_mix_dpQ(o.p.val_SI, 0, o.fluid_data) + if self.is_variable(o.h): + self.jacobian[k, o.h.J_col] = 1 def calculate_td_log(self): @@ -324,18 +328,18 @@ def calculate_td_log(self): o1 = self.outl[0] o2 = self.outl[1] - T_i1 = T_bp_p(i1.get_flow()) - T_i2 = T_mix_ph(i2.get_flow(), T0=i2.T.val_SI) - T_o1 = T_mix_ph(o1.get_flow(), T0=o1.T.val_SI) - T_o2 = T_mix_ph(o2.get_flow(), T0=o2.T.val_SI) + T_i1 = i1.calc_T_sat() + T_i2 = i2.calc_T(T0=i2.T.val_SI) + T_o1 = o1.calc_T(T0=o1.T.val_SI) + T_o2 = o2.calc_T(T0=o2.T.val_SI) - if T_i1 <= T_o2 and not i1.T.val_set: + if T_i1 <= T_o2 and not i1.T.is_set: T_i1 = T_o2 + 0.5 - if T_i1 <= T_o2 and not o2.T.val_set: + if T_i1 <= T_o2 and not o2.T.is_set: T_o2 = T_i1 - 0.5 - if T_o1 <= T_i2 and not o1.T.val_set: + if T_o1 <= T_i2 and not o1.T.is_set: T_o1 = T_i2 + 1 - if T_o1 <= T_i2 and not i2.T.val_set: + if T_o1 <= T_i2 and not i2.T.is_set: T_i2 = T_o1 - 1 ttd_u = T_i1 - T_o2 @@ -447,8 +451,10 @@ def ttd_u_func(self): The upper terminal temperature difference ttd_u refers to boiling temperature at hot side inlet. """ - T_i1 = T_bp_p(self.inl[0].get_flow()) - T_o2 = T_mix_ph(self.outl[1].get_flow(), T0=self.outl[1].T.val_SI) + i = self.inl[0] + o = self.outl[1] + T_i1 = i.calc_T_sat() + T_o2 = o.calc_T(T0=self.outl[1].T.val_SI) return self.ttd_u.val - T_i1 + T_o2 def ttd_u_func_doc(self, label): @@ -473,20 +479,21 @@ def ttd_u_func_doc(self, label): def calc_parameters(self): r"""Postprocessing parameter calculation.""" # component parameters - self.Q.val = self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - self.ttd_u.val = T_bp_p(self.inl[0].get_flow()) - self.outl[1].T.val_SI - self.ttd_l.val = self.outl[0].T.val_SI - self.inl[1].T.val_SI + i1 = self.inl[0] + i2 = self.inl[1] + o1 = self.outl[0] + o2 = self.outl[1] + self.Q.val = i1.m.val_SI * (o1.h.val_SI - i1.h.val_SI) + self.ttd_u.val = i1.calc_T_sat() - o2.T.val_SI + self.ttd_l.val = o1.T.val_SI - i2.T.val_SI # pr and zeta - for i in range(2): - self.get_attr('pr' + str(i + 1)).val = ( - self.outl[i].p.val_SI / self.inl[i].p.val_SI) - self.get_attr('zeta' + str(i + 1)).val = ( - (self.inl[i].p.val_SI - self.outl[i].p.val_SI) * np.pi ** 2 / ( - 4 * self.inl[i].m.val_SI ** 2 * - (self.inl[i].vol.val_SI + self.outl[i].vol.val_SI) - )) + for num, (i, o) in enumerate(zip(self.inl, self.outl)): + self.get_attr(f"pr{num + 1}").val = o.p.val_SI / i.p.val_SI + self.get_attr(f"zeta{num + 1}").val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) # kA and logarithmic temperature difference if self.ttd_u.val < 0 or self.ttd_l.val < 0: @@ -494,6 +501,8 @@ def calc_parameters(self): elif self.ttd_l.val == self.ttd_u.val: self.td_log.val = self.ttd_l.val else: - self.td_log.val = ((self.ttd_l.val - self.ttd_u.val) / - np.log(self.ttd_l.val / self.ttd_u.val)) + self.td_log.val = ( + (self.ttd_l.val - self.ttd_u.val) + / np.log(self.ttd_l.val / self.ttd_u.val) + ) self.kA.val = -self.Q.val / self.td_log.val diff --git a/src/tespy/components/heat_exchangers/desuperheater.py b/src/tespy/components/heat_exchangers/desuperheater.py index f9906792a..dc3a60601 100644 --- a/src/tespy/components/heat_exchangers/desuperheater.py +++ b/src/tespy/components/heat_exchangers/desuperheater.py @@ -23,8 +23,6 @@ class Desuperheater(HeatExchanger): **Mandatory Equations** - - :py:meth:`tespy.components.component.Component.fluid_func` - - :py:meth:`tespy.components.component.Component.mass_flow_func` - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.energy_balance_func` - :py:meth:`tespy.components.heat_exchangers.desuperheater.Desuperheater.saturated_gas_func` @@ -129,10 +127,9 @@ class Desuperheater(HeatExchanger): >>> from tespy.components import Sink, Source, Desuperheater >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools.fluid_properties import T_bp_p >>> import shutil - >>> nw = Network(fluids=['water', 'ethanol'], T_unit='C', p_unit='bar', - ... h_unit='kJ / kg', v_unit='l / s', m_range=[0.001, 10], iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', v_unit='l / s', + ... m_range=[0.001, 10], iterinfo=False) >>> et_in = Source('ethanol inlet') >>> et_out = Sink('ethanol outlet') >>> cw_in = Source('cooling water inlet') @@ -155,10 +152,10 @@ class Desuperheater(HeatExchanger): >>> desu.set_attr(pr1=0.99, pr2=0.98, design=['pr1', 'pr2'], ... offdesign=['zeta1', 'zeta2', 'kA_char']) - >>> cw_de.set_attr(fluid={'water': 1, 'ethanol': 0}, T=15, v=1, + >>> cw_de.set_attr(fluid={'water': 1}, T=15, v=1, ... design=['v']) >>> de_cw.set_attr(p=1) - >>> et_de.set_attr(fluid={'water': 0, 'ethanol': 1}, Td_bp=100, v=10) + >>> et_de.set_attr(fluid={'ethanol': 1}, Td_bp=100, v=10) >>> de_et.set_attr(p=1) >>> nw.solve('design') >>> nw.save('tmp') @@ -171,7 +168,7 @@ class Desuperheater(HeatExchanger): >>> round(cw_de.v.val, 2) 1.94 >>> et_de.set_attr(v=7) - >>> nw.solve('offdesign', design_path='tmp', init_path='tmp') + >>> nw.solve('offdesign', design_path='tmp') >>> round(cw_de.v.val, 2) 0.41 >>> shutil.rmtree('./tmp', ignore_errors=True) @@ -183,14 +180,6 @@ def component(): def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 2}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 2}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -216,7 +205,8 @@ def saturated_gas_func(self): 0 = h_{out,1} - h\left(p_{out,1}, x=1 \right) """ - return self.outl[0].h.val_SI - h_mix_pQ(self.outl[0].get_flow(), 1) + o = self.outl[0] + return o.h.val_SI - h_mix_pQ(o.p.val_SI, 1, o.fluid_data) def saturated_gas_func_doc(self, label): r""" @@ -247,6 +237,8 @@ def saturated_gas_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - o1 = self.outl[0].get_flow() - self.jacobian[k, 2, 1] = -dh_mix_dpQ(o1, 1) - self.jacobian[k, 2, 2] = 1 + o = self.outl[0] + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -dh_mix_dpQ(o, 1, o.fluid_data) + if self.is_variable(o.h): + self.jacobian[k, o.h.J_col] = 1 diff --git a/src/tespy/components/heat_exchangers/parabolic_trough.py b/src/tespy/components/heat_exchangers/parabolic_trough.py index af9744723..5a41e98d1 100644 --- a/src/tespy/components/heat_exchangers/parabolic_trough.py +++ b/src/tespy/components/heat_exchangers/parabolic_trough.py @@ -18,7 +18,6 @@ from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph class ParabolicTrough(SimpleHeatExchanger): @@ -35,7 +34,8 @@ class ParabolicTrough(SimpleHeatExchanger): - :py:meth:`tespy.components.component.Component.pr_func` - :py:meth:`tespy.components.component.Component.zeta_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.energy_balance_func` - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hydro_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.darcy_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hw_group_func` - :py:meth:`tespy.components.heat_exchangers.parabolic_trough.ParabolicTrough.energy_group_func` Inlets/Outlets @@ -98,13 +98,18 @@ class ParabolicTrough(SimpleHeatExchanger): Length of the absorber tube, :math:`L/\text{m}`. ks : float, dict, :code:`"var"` - Tube's roughness, :math:`ks/\text{m}` for darcy friction, - :math:`ks/\text{1}` for hazen-williams equation. + Pipe's roughness, :math:`ks/\text{m}`. - hydro_group : str, dict - Parametergroup for pressure drop calculation based on pipes dimensions. - Choose 'HW' for hazen-williams equation, else darcy friction factor is - used. + darcy_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using darcy weissbach equation. + + ks_HW : float, dict, :code:`"var"` + Pipe's roughness, :math:`ks/\text{1}`. + + hw_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using hazen williams equation. E : float, dict, :code:`"var"` Direct irradiance to tilted collector, @@ -161,8 +166,7 @@ class ParabolicTrough(SimpleHeatExchanger): >>> from tespy.networks import Network >>> import numpy as np >>> import shutil - >>> fluids = ['INCOMP::S800'] - >>> nw = Network(fluids=fluids) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> so = Source('source') >>> si = Sink('sink') @@ -183,7 +187,7 @@ class ParabolicTrough(SimpleHeatExchanger): >>> pt.set_attr(pr=1, aoi=aoi, doc=1, ... Tamb=20, A=1, eta_opt=0.816, c_1=0.0622, c_2=0.00023, E=E, ... iam_1=-1.59e-3, iam_2=9.77e-5) - >>> inc.set_attr(fluid={'S800': 1}, T=220, p=2) + >>> inc.set_attr(fluid={'INCOMP::S800': 1}, T=220, p=2) >>> outg.set_attr(T=260) >>> nw.solve('design') >>> round(pt.Q.val, 0) @@ -220,24 +224,12 @@ class ParabolicTrough(SimpleHeatExchanger): def component(): return 'parabolic trough' - def get_variables(self): - return { - 'Q': dc_cp( - deriv=self.energy_balance_deriv, - latex=self.energy_balance_func_doc, num_eq=1, - func=self.energy_balance_func), - 'pr': dc_cp( - min_val=1e-4, max_val=1, num_eq=1, - deriv=self.pr_deriv, latex=self.pr_func_doc, - func=self.pr_func, func_params={'pr': 'pr'}), - 'zeta': dc_cp( - min_val=0, max_val=1e15, num_eq=1, - deriv=self.zeta_deriv, func=self.zeta_func, - latex=self.zeta_func_doc, - func_params={'zeta': 'zeta'}), - 'D': dc_cp(min_val=1e-2, max_val=2, d=1e-4), - 'L': dc_cp(min_val=1e-1, d=1e-3), - 'ks': dc_cp(val=1e-4, min_val=1e-7, max_val=1e-3, d=1e-8), + def get_parameters(self): + data = super().get_parameters() + for k in ["kA_group", "kA_char_group", "kA", "kA_char"]: + del data[k] + + data.update({ 'E': dc_cp(min_val=0), 'A': dc_cp(min_val=0), 'eta_opt': dc_cp(min_val=0, max_val=1), 'c_1': dc_cp(min_val=0), 'c_2': dc_cp(min_val=0), @@ -246,17 +238,14 @@ def get_variables(self): 'doc': dc_cp(min_val=0, max_val=1), 'Tamb': dc_cp(), 'Q_loss': dc_cp(max_val=0, val=0), - 'dissipative': dc_simple(val=True), - 'hydro_group': dc_gcp( - elements=['L', 'ks', 'D'], num_eq=1, - latex=self.hydro_group_func_doc, - func=self.hydro_group_func, deriv=self.hydro_group_deriv), 'energy_group': dc_gcp( elements=['E', 'eta_opt', 'aoi', 'doc', 'c_1', 'c_2', 'iam_1', 'iam_2', 'A', 'Tamb'], num_eq=1, latex=self.energy_group_func_doc, - func=self.energy_group_func, deriv=self.energy_group_deriv) - } + func=self.energy_group_func, deriv=self.energy_group_deriv + ) + }) + return data def energy_group_func(self): r""" @@ -282,21 +271,23 @@ def energy_group_func(self): Reference: :cite:`Janotte2014`. """ - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] - T_m = (T_mix_ph(i, T0=self.inl[0].T.val_SI) + - T_mix_ph(o, T0=self.outl[0].T.val_SI)) / 2 + T_m = 0.5 * (i.calc_T(T0=i.T.val_SI) + o.calc_T(T0=o.T.val_SI)) iam = ( - 1 - self.iam_1.val * abs(self.aoi.val) - - self.iam_2.val * self.aoi.val ** 2) + 1 - self.iam_1.val * abs(self.aoi.val) + - self.iam_2.val * self.aoi.val ** 2 + ) return ( - i[0] * (o[2] - i[2]) - self.A.val * ( - self.E.val * self.eta_opt.val * self.doc.val ** 1.5 * iam - - (T_m - self.Tamb.val_SI) * self.c_1.val - self.c_2.val * - (T_m - self.Tamb.val_SI) ** 2)) + i.m.val_SI * (o.h.val_SI - i.h.val_SI) - self.A.val * ( + self.E.val * self.eta_opt.val * self.doc.val ** 1.5 * iam + - (T_m - self.Tamb.val_SI) * self.c_1.val + - self.c_2.val * (T_m - self.Tamb.val_SI) ** 2 + ) + ) def energy_group_func_doc(self, label): r""" @@ -341,35 +332,39 @@ def energy_group_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.energy_group_func - self.jacobian[k, 0, 0] = ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) # custom variables for the energy-group - for var in self.energy_group.elements: - var = self.get_attr(var) - if var == self.Tamb: + for variable_name in self.energy_group.elements: + parameter = self.get_attr(variable_name) + if parameter == self.Tamb: continue - if var.is_var: - self.jacobian[k, 2 + var.var_pos, 0] = ( - self.numeric_deriv(f, self.vars[var], 2)) + if parameter.is_var: + self.jacobian[k, parameter.J_col] = ( + self.numeric_deriv(f, variable_name, None) + ) def calc_parameters(self): r"""Postprocessing parameter calculation.""" - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - - self.Q.val = i[0] * (o[2] - i[2]) - self.pr.val = o[1] / i[1] - self.zeta.val = ((i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - )) + i = self.inl[0] + o = self.outl[0] + + self.Q.val = i.m.val_SI * (o.h.val_SI - i.h.val_SI) + self.pr.val = o.p.val_SI / i.p.val_SI + self.zeta.val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) if self.energy_group.is_set: self.Q_loss.val = - self.E.val * self.A.val + self.Q.val self.Q_loss.is_result = True diff --git a/src/tespy/components/heat_exchangers/simple.py b/src/tespy/components/heat_exchangers/simple.py index 0401e2ee4..29d141ea9 100644 --- a/src/tespy/components/heat_exchangers/simple.py +++ b/src/tespy/components/heat_exchangers/simple.py @@ -22,12 +22,9 @@ from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import s_mix_ph -from tespy.tools.fluid_properties import v_mix_ph -from tespy.tools.fluid_properties import visc_mix_ph +from tespy.tools.fluid_properties.helpers import darcy_friction_factor as dff from tespy.tools.helpers import convert_to_SI -from tespy.tools.helpers import darcy_friction_factor as dff class SimpleHeatExchanger(Component): @@ -50,7 +47,8 @@ class SimpleHeatExchanger(Component): - :py:meth:`tespy.components.component.Component.pr_func` - :py:meth:`tespy.components.component.Component.zeta_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.energy_balance_func` - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hydro_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.darcy_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hw_group_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.kA_group_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.kA_char_group_func` @@ -114,13 +112,18 @@ class SimpleHeatExchanger(Component): Length of the pipes, :math:`L/\text{m}`. ks : float, dict, :code:`"var"` - Pipe's roughness, :math:`ks/\text{m}` for darcy friction, - :math:`ks/\text{1}` for hazen-williams equation. + Pipe's roughness, :math:`ks/\text{m}`. - hydro_group : str, dict - Parametergroup for pressure drop calculation based on pipes dimensions. - Choose 'HW' for hazen-williams equation, else darcy friction factor is - used. + darcy_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using darcy weissbach equation. + + ks_HW : float, dict, :code:`"var"` + Pipe's roughness, :math:`ks/\text{1}`. + + hw_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using hazen williams equation. kA : float, dict, :code:`"var"` Area independent heat transfer coefficient, @@ -149,8 +152,7 @@ class SimpleHeatExchanger(Component): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluids = ['N2'] - >>> nw = Network(fluids=fluids) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> so1 = Source('source 1') >>> si1 = Sink('sink 1') @@ -197,7 +199,7 @@ class SimpleHeatExchanger(Component): def component(): return 'heat exchanger simple' - def get_variables(self): + def get_parameters(self): return { 'Q': dc_cp( deriv=self.energy_balance_deriv, @@ -215,13 +217,18 @@ def get_variables(self): 'D': dc_cp(min_val=1e-2, max_val=2, d=1e-4), 'L': dc_cp(min_val=1e-1, d=1e-3), 'ks': dc_cp(val=1e-4, min_val=1e-7, max_val=1e-3, d=1e-8), + 'ks_HW': dc_cp(val=10, min_val=1e-1, max_val=1e3, d=1e-2), 'kA': dc_cp(min_val=0, d=1), 'kA_char': dc_cc(param='m'), 'Tamb': dc_cp(), 'dissipative': dc_simple(val=True), - 'hydro_group': dc_gcp( + 'darcy_group': dc_gcp( elements=['L', 'ks', 'D'], num_eq=1, - latex=self.hydro_group_func_doc, - func=self.hydro_group_func, deriv=self.hydro_group_deriv), + latex=self.darcy_func_doc, + func=self.darcy_func, deriv=self.darcy_deriv), + 'hw_group': dc_gcp( + elements=['L', 'ks_HW', 'D'], num_eq=1, + latex=self.hazen_williams_func_doc, + func=self.hazen_williams_func, deriv=self.hazen_williams_deriv), 'kA_group': dc_gcp( elements=['kA', 'Tamb'], num_eq=1, latex=self.kA_group_func_doc, @@ -240,10 +247,10 @@ def inlets(): def outlets(): return ['out1'] - def preprocess(self, nw): - super().preprocess(nw, num_eq=len(nw.fluids) + 1) + def preprocess(self, num_nw_vars): + super().preprocess(num_nw_vars) - self.Tamb.val_SI = convert_to_SI('T', self.Tamb.val, nw.T_unit) + self.Tamb.val_SI = convert_to_SI('T', self.Tamb.val, self.inl[0].T.unit) def energy_balance_func(self): r""" @@ -259,7 +266,8 @@ def energy_balance_func(self): 0 =\dot{m}_{in}\cdot\left( h_{out}-h_{in}\right) -\dot{Q} """ return self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - self.Q.val + self.outl[0].h.val_SI - self.inl[0].h.val_SI + ) - self.Q.val def energy_balance_func_doc(self, label): r""" @@ -293,95 +301,17 @@ def energy_balance_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 0, 0] = ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI - self.jacobian[k, 1, 2] = self.inl[0].m.val_SI + i = self.inl[0] + o = self.outl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if o.h.is_var: + self.jacobian[k, o.h.J_col] = i.m.val_SI # custom variable Q if self.Q.is_var: - self.jacobian[k, 2 + self.Q.var_pos, 0] = -1 - - def hydro_group_func(self): - r""" - Equation for pressure drop calculation. - - Returns - ------- - residual : float - Residual value of corresponding equation: - - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.darcy_func` - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hazen_williams_func` - """ - # hazen williams equation - if self.hydro_group.method == 'HW': - return self.hazen_williams_func() - # darcy friction factor - else: - return self.darcy_func() - - def hydro_group_func_doc(self, label): - r""" - Equation for pressure drop calculation. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - # hazen williams equation - if self.hydro_group.method == 'HW': - msg = ( - "The Hazen-Williams equation will be accessible through its " - "own ks-value in the next major version. That means, you will " - "not need to specify hydro_group='HW' and ks. Instead of ks " - "specify ks_HW" - ) - warnings.warn(msg, FutureWarning) - return self.hazen_williams_func_doc(label) - # darcy friction factor - else: - return self.darcy_func_doc(label) - - def hydro_group_deriv(self, increment_filter, k): - r""" - Calculate partial derivatives of hydro group (pressure drop). - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. - - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ - # hazen williams equation - if self.hydro_group.method == 'HW': - func = self.hazen_williams_func - # darcy friction factor - else: - func = self.darcy_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(func, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(func, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(func, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(func, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(func, 'h', 1) - # custom variables of hydro group - for var in self.hydro_group.elements: - var = self.get_attr(var) - if var.is_var: - self.jacobian[k, 2 + var.var_pos, 0] = ( - self.numeric_deriv(func, self.vars[var], 2)) + self.jacobian[k, self.Q.J_col] = -1 def darcy_func(self): r""" @@ -404,21 +334,25 @@ def darcy_func(self): v: \text{specific volume}\\ \lambda: \text{darcy friction factor} """ - i, o = self.inl[0].get_flow(), self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] - if abs(i[0]) < 1e-4: - return i[1] - o[1] + if abs(i.m.val_SI) < 1e-4: + return i.p.val_SI - o.p.val_SI - visc_i = visc_mix_ph(i, T0=self.inl[0].T.val_SI) - visc_o = visc_mix_ph(o, T0=self.outl[0].T.val_SI) - 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) + visc_i = i.calc_viscosity(T0=i.T.val_SI) + visc_o = o.calc_viscosity(T0=o.T.val_SI) + v_i = i.calc_vol(T0=i.T.val_SI) + v_o = o.calc_vol(T0=o.T.val_SI) - Re = 4 * abs(i[0]) / (np.pi * self.D.val * (visc_i + visc_o) / 2) + Re = 4 * abs(i.m.val_SI) / (np.pi * self.D.val * (visc_i + visc_o) / 2) - return ((i[1] - o[1]) - 8 * abs(i[0]) * i[0] * (v_i + v_o) / 2 * - self.L.val * dff(Re, self.ks.val, self.D.val) / - (np.pi ** 2 * self.D.val ** 5)) + return ( + (i.p.val_SI - o.p.val_SI) + - 8 * abs(i.m.val_SI) * i.m.val_SI * (v_i + v_o) + / 2 * self.L.val * dff(Re, self.ks.val, self.D.val) + / (np.pi ** 2 * self.D.val ** 5) + ) def darcy_func_doc(self, label): r""" @@ -446,6 +380,39 @@ def darcy_func_doc(self, label): ) return generate_latex_eq(self, latex, label) + def darcy_deriv(self, increment_filter, k): + r""" + Calculate partial derivatives of hydro group (pressure drop). + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + func = self.darcy_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(func, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(func, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(func, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(func, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(func, 'h', o) + # custom variables of hydro group + for variable_name in self.darcy_group.elements: + parameter = self.get_attr(variable_name) + if parameter.is_var: + self.jacobian[k, parameter.J_col] = ( + self.numeric_deriv(func, variable_name, None) + ) + def hazen_williams_func(self): r""" Equation for pressure drop calculation from Hazen-Williams equation. @@ -471,17 +438,19 @@ def hazen_williams_func(self): ---- Gravity :math:`g` is set to :math:`9.81 \frac{m}{s^2}` """ - i, o = self.inl[0].get_flow(), self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] - if abs(i[0]) < 1e-4: - return i[1] - o[1] + if abs(i.m.val_SI) < 1e-4: + return i.p.val_SI - o.p.val_SI - 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) + v_i = i.calc_vol(T0=i.T.val_SI) + v_o = o.calc_vol(T0=o.T.val_SI) - return ((i[1] - o[1]) * np.sign(i[0]) - - (10.67 * abs(i[0]) ** 1.852 * self.L.val / - (self.ks.val ** 1.852 * self.D.val ** 4.871)) * + return ( + (i.p.val_SI - o.p.val_SI) * np.sign(i.m.val_SI) - + (10.67 * abs(i.m.val_SI) ** 1.852 * self.L.val / + (self.ks_HW.val ** 1.852 * self.D.val ** 4.871)) * (9.81 * ((v_i + v_o) / 2) ** 0.852)) def hazen_williams_func_doc(self, label): @@ -506,6 +475,39 @@ def hazen_williams_func_doc(self, label): ) return generate_latex_eq(self, latex, label) + def hazen_williams_deriv(self, increment_filter, k): + r""" + Calculate partial derivatives of hydro group (pressure drop). + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + func = self.hazen_williams_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(func, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(func, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(func, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(func, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(func, 'h', o) + # custom variables of hydro group + for variable_name in self.hw_group.elements: + parameter = self.get_attr(variable_name) + if parameter.is_var: + self.jacobian[k, parameter.J_col] = ( + self.numeric_deriv(func, variable_name, None) + ) + def kA_group_func(self): r""" Calculate heat transfer from heat transfer coefficient. @@ -530,10 +532,11 @@ def kA_group_func(self): T_{amb}: \text{ambient temperature} """ - i, o = self.inl[0].get_flow(), self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] - ttd_1 = T_mix_ph(i, T0=self.inl[0].T.val_SI) - self.Tamb.val_SI - ttd_2 = T_mix_ph(o, T0=self.outl[0].T.val_SI) - self.Tamb.val_SI + ttd_1 = i.calc_T(T0=i.T.val_SI) - self.Tamb.val_SI + ttd_2 = o.calc_T(T0=o.T.val_SI) - self.Tamb.val_SI # For numerical stability: If temperature differences have # different sign use mean difference to avoid negative logarithm. @@ -547,7 +550,7 @@ def kA_group_func(self): # both values are equal td_log = ttd_2 - return i[0] * (o[2] - i[2]) + self.kA.val * td_log + return i.m.val_SI * (o.h.val_SI - i.h.val_SI) + self.kA.val * td_log def kA_group_func_doc(self, label): r""" @@ -594,19 +597,20 @@ def kA_group_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.kA_group_func - self.jacobian[k, 0, 0] = ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) if self.kA.is_var: - self.jacobian[k, 2 + self.kA.var_pos, 0] = ( - self.numeric_deriv(f, self.vars[self.kA], 2)) + self.jacobian[k, self.kA.J_col] = self.numeric_deriv(f, self.vars[self.kA]) def kA_char_group_func(self): r""" @@ -641,13 +645,14 @@ def kA_char_group_func(self): """ p = self.kA_char.param expr = self.get_char_expr(p, **self.kA_char.char_params) - i, o = self.inl[0].get_flow(), self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] # For numerical stability: If temperature differences have # different sign use mean difference to avoid negative logarithm. - ttd_1 = T_mix_ph(i, T0=self.inl[0].T.val_SI) - self.Tamb.val_SI - ttd_2 = T_mix_ph(o, T0=self.outl[0].T.val_SI) - self.Tamb.val_SI + ttd_1 = i.calc_T(T0=i.T.val_SI) - self.Tamb.val_SI + ttd_2 = o.calc_T(T0=o.T.val_SI) - self.Tamb.val_SI if (ttd_1 / ttd_2) < 0: td_log = (ttd_2 + ttd_1) / 2 @@ -661,7 +666,7 @@ def kA_char_group_func(self): fkA = 2 / (1 + 1 / self.kA_char.char_func.evaluate(expr)) - return i[0] * (o[2] - i[2]) + self.kA.design * fkA * td_log + return i.m.val_SI * (o.h.val_SI - i.h.val_SI) + self.kA.design * fkA * td_log def kA_char_group_func_doc(self, label): r""" @@ -710,16 +715,18 @@ def kA_char_group_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.kA_char_group_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) def bus_func(self, bus): r""" @@ -777,12 +784,21 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 2, self.num_nw_vars)) f = self.calc_bus_value - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - deriv[0, 1, 2] = self.numeric_deriv(f, 'h', 1, bus=bus) - return deriv + if self.inl[0].m.is_var: + if self.inl[0].m.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].m.J_col] = 0 + bus.jacobian[self.inl[0].m.J_col] -= self.numeric_deriv(f, 'm', self.inl[0], bus=bus) + + if self.inl[0].h.is_var: + if self.inl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].h.J_col] = 0 + bus.jacobian[self.inl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.inl[0], bus=bus) + + if self.outl[0].h.is_var: + if self.outl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.outl[0].h.J_col] = 0 + bus.jacobian[self.outl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.outl[0], bus=bus) def initialise_source(self, c, key): r""" @@ -863,18 +879,19 @@ def initialise_target(self, c, key): def calc_parameters(self): r"""Postprocessing parameter calculation.""" - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - - self.Q.val = i[0] * (o[2] - i[2]) - self.pr.val = o[1] / i[1] - self.zeta.val = ((i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - )) + i = self.inl[0] + o = self.outl[0] + + self.Q.val = i.m.val_SI * (o.h.val_SI - i.h.val_SI) + self.pr.val = o.p.val_SI / i.p.val_SI + self.zeta.val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) if self.Tamb.is_set: - ttd_1 = self.inl[0].T.val_SI - self.Tamb.val_SI - ttd_2 = self.outl[0].T.val_SI - self.Tamb.val_SI + ttd_1 = i.T.val_SI - self.Tamb.val_SI + ttd_2 = o.T.val_SI - self.Tamb.val_SI if (ttd_1 / ttd_2) < 0: td_log = np.nan @@ -886,7 +903,7 @@ def calc_parameters(self): # both values are equal td_log = ttd_1 - self.kA.val = abs(i[0] * (o[2] - i[2]) / td_log) + self.kA.val = abs(self.Q.val / td_log) self.kA.is_result = True else: self.kA.is_result = False @@ -928,16 +945,19 @@ def entropy_balance(self): \right) - \text{S\_Q}\\ \text{T\_mQ}=\frac{\dot{Q}}{\text{S\_Q}} """ - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] - p1_star = i[1] * (o[1] / i[1]) ** 0.5 - s1_star = s_mix_ph([0, p1_star, i[2], i[3]], T0=self.inl[0].T.val_SI) - s2_star = s_mix_ph([0, p1_star, o[2], o[3]], T0=self.outl[0].T.val_SI) - self.S_Q = i[0] * (s2_star - s1_star) - self.S_irr = i[0] * ( - self.outl[0].s.val_SI - self.inl[0].s.val_SI) - self.S_Q - self.T_mQ = (o[2] - i[2]) / (s2_star - s1_star) + p1_star = i.p.val_SI * (o.p.val_SI / i.p.val_SI) ** 0.5 + s1_star = s_mix_ph( + p1_star, i.h.val_SI, i.fluid_data, i.mixing_rule, T0=i.T.val_SI + ) + s2_star = s_mix_ph( + p1_star, o.h.val_SI, o.fluid_data, o.mixing_rule, T0=o.T.val_SI + ) + self.S_Q = i.m.val_SI * (s2_star - s1_star) + self.S_irr = i.m.val_SI * (o.s.val_SI - i.s.val_SI) - self.S_Q + self.T_mQ = (o.h.val_SI - i.h.val_SI) / (s2_star - s1_star) def exergy_balance(self, T0): r""" @@ -1059,7 +1079,7 @@ def exergy_balance(self, T0): "chemical": np.nan, "physical": np.nan, "massless": np.nan } elif self.Q.val > 0: - if self.inl[0].T.val_SI >= T0 and self.outl[0].T.val_SI >= T0: + if self.inl[0].T.val_SI >= T0 - 1e-6 and self.outl[0].T.val_SI >= T0 - 1e-6: self.E_P = self.outl[0].Ex_physical - self.inl[0].Ex_physical self.E_F = self.outl[0].Ex_therm - self.inl[0].Ex_therm self.E_bus = { @@ -1106,7 +1126,7 @@ def exergy_balance(self, T0): self.E_D = self.E_F else: self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/heat_exchangers/solar_collector.py b/src/tespy/components/heat_exchangers/solar_collector.py index 71ad77739..bce3198ce 100644 --- a/src/tespy/components/heat_exchangers/solar_collector.py +++ b/src/tespy/components/heat_exchangers/solar_collector.py @@ -18,7 +18,6 @@ from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph class SolarCollector(SimpleHeatExchanger): @@ -35,7 +34,8 @@ class SolarCollector(SimpleHeatExchanger): - :py:meth:`tespy.components.component.Component.pr_func` - :py:meth:`tespy.components.component.Component.zeta_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.energy_balance_func` - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hydro_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.darcy_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hw_group_func` - :py:meth:`tespy.components.heat_exchangers.solar_collector.SolarCollector.energy_group_func` Inlets/Outlets @@ -98,13 +98,18 @@ class SolarCollector(SimpleHeatExchanger): Length of the pipes, :math:`L/\text{m}`. ks : float, dict, :code:`"var"` - Pipe's roughness, :math:`ks/\text{m}` for darcy friction, - :math:`ks/\text{1}` for hazen-williams equation. + Pipe's roughness, :math:`ks/\text{m}`. - hydro_group : str, dict - Parametergroup for pressure drop calculation based on pipes dimensions. - Choose 'HW' for hazen-williams equation, else darcy friction factor is - used. + darcy_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using darcy weissbach equation. + + ks_HW : float, dict, :code:`"var"` + Pipe's roughness, :math:`ks/\text{1}`. + + hw_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using hazen williams equation. E : float, dict, :code:`"var"` irradiance at tilted collector surface area, @@ -143,8 +148,7 @@ class SolarCollector(SimpleHeatExchanger): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluids = ['H2O'] - >>> nw = Network(fluids=fluids) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> so = Source('source') >>> si = Sink('sink') @@ -183,39 +187,25 @@ class SolarCollector(SimpleHeatExchanger): def component(): return 'solar collector' - def get_variables(self): - return { - 'Q': dc_cp( - deriv=self.energy_balance_deriv, - latex=self.energy_balance_func_doc, num_eq=1, - func=self.energy_balance_func), - 'pr': dc_cp( - min_val=1e-4, max_val=1, num_eq=1, - deriv=self.pr_deriv, latex=self.pr_func_doc, - func=self.pr_func, func_params={'pr': 'pr'}), - 'zeta': dc_cp( - min_val=0, max_val=1e15, num_eq=1, - deriv=self.zeta_deriv, func=self.zeta_func, - latex=self.zeta_func_doc, - func_params={'zeta': 'zeta'}), - 'D': dc_cp(min_val=1e-2, max_val=2, d=1e-4), - 'L': dc_cp(min_val=1e-1, d=1e-3), - 'ks': dc_cp(val=1e-4, min_val=1e-7, max_val=1e-3, d=1e-8), + def get_parameters(self): + data = super().get_parameters() + for k in ["kA_group", "kA_char_group", "kA", "kA_char"]: + del data[k] + + data.update({ 'E': dc_cp(min_val=0), 'A': dc_cp(min_val=0), 'eta_opt': dc_cp(min_val=0, max_val=1), - 'lkf_lin': dc_cp(min_val=0), 'lkf_quad': dc_cp(min_val=0), + 'lkf_lin': dc_cp(min_val=0), + 'lkf_quad': dc_cp(min_val=0), 'Tamb': dc_cp(), 'Q_loss': dc_cp(max_val=0, val=0), - 'dissipative': dc_simple(val=True), - 'hydro_group': dc_gcp( - elements=['L', 'ks', 'D'], num_eq=1, - latex=self.hydro_group_func_doc, - func=self.hydro_group_func, deriv=self.hydro_group_deriv), 'energy_group': dc_gcp( elements=['E', 'eta_opt', 'lkf_lin', 'lkf_quad', 'A', 'Tamb'], num_eq=1, latex=self.energy_group_func_doc, - func=self.energy_group_func, deriv=self.energy_group_deriv) - } + func=self.energy_group_func, deriv=self.energy_group_deriv + ) + }) + return data def energy_group_func(self): r""" @@ -238,17 +228,19 @@ def energy_group_func(self): Reference: :cite:`Quaschning2013`. """ - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - - T_m = (T_mix_ph(i, T0=self.inl[0].T.val_SI) + - T_mix_ph(o, T0=self.outl[0].T.val_SI)) / 2 - - return (i[0] * (o[2] - i[2]) - - self.A.val * ( - self.E.val * self.eta_opt.val - - (T_m - self.Tamb.val_SI) * self.lkf_lin.val - - self.lkf_quad.val * (T_m - self.Tamb.val_SI) ** 2)) + i = self.inl[0] + o = self.outl[0] + + T_m = 0.5 * (i.calc_T(T0=i.T.val_SI) + o.calc_T(T0=o.T.val_SI)) + + return ( + i.m.val_SI * (o.h.val_SI - i.h.val_SI) + - self.A.val * ( + self.E.val * self.eta_opt.val + - (T_m - self.Tamb.val_SI) * self.lkf_lin.val + - self.lkf_quad.val * (T_m - self.Tamb.val_SI) ** 2 + ) + ) def energy_group_func_doc(self, label): r""" @@ -290,35 +282,39 @@ def energy_group_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.energy_group_func - self.jacobian[k, 0, 0] = ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) # custom variables for the energy-group - for var in self.energy_group.elements: - var = self.get_attr(var) - if var == self.Tamb: + for variable_name in self.energy_group.elements: + parameter = self.get_attr(variable_name) + if parameter == self.Tamb: continue - if var.is_var: - self.jacobian[k, 2 + var.var_pos, 0] = ( - self.numeric_deriv(f, self.vars[var], 2)) + if parameter.is_var: + self.jacobian[k, parameter.J_col] = ( + self.numeric_deriv(f, variable_name, None) + ) def calc_parameters(self): r"""Postprocessing parameter calculation.""" - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - - self.Q.val = i[0] * (o[2] - i[2]) - self.pr.val = o[1] / i[1] - self.zeta.val = ((i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - )) + i = self.inl[0] + o = self.outl[0] + + self.Q.val = i.m.val_SI * (o.h.val_SI - i.h.val_SI) + self.pr.val = o.p.val_SI / i.p.val_SI + self.zeta.val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) if self.energy_group.is_set: self.Q_loss.val = -(self.E.val * self.A.val - self.Q.val) self.Q_loss.is_result = True diff --git a/src/tespy/components/nodes/base.py b/src/tespy/components/nodes/base.py index 0b93b9b07..55a22b82d 100644 --- a/src/tespy/components/nodes/base.py +++ b/src/tespy/components/nodes/base.py @@ -59,7 +59,7 @@ def mass_flow_func_doc(self, label): r'\;\forall i \in \text{inlets}, \forall j \in \text{outlets}') return generate_latex_eq(self, latex, label) - def mass_flow_deriv(self): + def mass_flow_deriv(self, k): r""" Calculate partial derivatives for mass flow equation. @@ -68,12 +68,12 @@ def mass_flow_deriv(self): deriv : list Matrix with partial derivatives for the fluid equations. """ - deriv = np.zeros((1, self.num_i + self.num_o, self.num_nw_vars)) - for i in range(self.num_i): - deriv[0, i, 0] = 1 - for j in range(self.num_o): - deriv[0, j + i + 1, 0] = -1 - return deriv + for i in self.inl: + if i.m.is_var: + self.jacobian[k, i.m.J_col] = 1 + for o in self.outl: + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -1 def pressure_equality_func(self): r""" @@ -122,7 +122,7 @@ def pressure_equality_func_doc(self, label): ) return generate_latex_eq(self, latex, label) - def pressure_equality_deriv(self): + def pressure_equality_deriv(self, k): r""" Calculate partial derivatives for all pressure equations. @@ -131,18 +131,16 @@ def pressure_equality_deriv(self): deriv : ndarray Matrix with partial derivatives for the fluid equations. """ - deriv = np.zeros(( - self.num_i + self.num_o - 1, - self.num_i + self.num_o, - self.num_nw_vars)) - - inl = [] if self.num_i > 1: - inl = self.inl[1:] - for k in range(len(inl + self.outl)): - deriv[k, 0, 1] = 1 - deriv[k, k + 1, 1] = -1 - return deriv + conns = self.inl[1:] + self.outl + else: + conns = self.outl + + for eq, o in enumerate(conns): + if self.inl[0].p.is_var: + self.jacobian[k + eq, self.inl[0].p.J_col] = 1 + if o.p.is_var: + self.jacobian[k + eq, o.p.J_col] = -1 @staticmethod def initialise_source(c, key): @@ -203,3 +201,14 @@ def initialise_target(c, key): return 1e5 elif key == 'h': return 5e5 + + def propagate_to_target(self, branch): + + for outconn in self.outl: + subbranch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(subbranch) + branch["subbranches"][outconn.label] = subbranch diff --git a/src/tespy/components/nodes/droplet_separator.py b/src/tespy/components/nodes/droplet_separator.py index 8fe1df918..9bed7a934 100644 --- a/src/tespy/components/nodes/droplet_separator.py +++ b/src/tespy/components/nodes/droplet_separator.py @@ -10,9 +10,6 @@ SPDX-License-Identifier: MIT """ - -import numpy as np - from tespy.components.nodes.base import NodeBase from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import dh_mix_dpQ @@ -84,10 +81,8 @@ class DropletSeparator(NodeBase): >>> from tespy.components import Sink, Source, DropletSeparator >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools.fluid_properties import Q_ph, T_bp_p >>> import shutil - >>> nw = Network(fluids=['water'], T_unit='C', p_unit='bar', - ... h_unit='kJ / kg', iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', iterinfo=False) >>> so = Source('two phase inflow') >>> sig = Sink('gas outflow') >>> sil = Sink('liquid outflow') @@ -114,14 +109,14 @@ class DropletSeparator(NodeBase): >>> so_ds.set_attr(fluid={'water': 1}, p=1, h=1500, m=10) >>> nw.solve('design') - >>> Q_in = Q_ph(so_ds.p.val_SI, so_ds.h.val_SI, 'water') + >>> Q_in = so_ds.calc_Q() >>> round(Q_in * so_ds.m.val_SI, 6) == round(ds_sig.m.val_SI, 6) True >>> round((1 - Q_in) * so_ds.m.val_SI, 6) == round(ds_sil.m.val_SI, 6) True - >>> Q_ph(ds_sig.p.val_SI, ds_sig.h.val_SI, 'water') + >>> ds_sig.calc_Q() 1.0 - >>> Q_ph(ds_sil.p.val_SI, ds_sil.h.val_SI, 'water') + >>> ds_sil.calc_Q() 0.0 In a different setup, we unset pressure and enthalpy and specify gas @@ -133,9 +128,9 @@ class DropletSeparator(NodeBase): >>> so_ds.set_attr(fluid={'water': 1}, p=None, h=None, T=150, m=10) >>> ds_sig.set_attr(m=9.5) >>> nw.solve('design') - >>> round(Q_ph(so_ds.p.val_SI, so_ds.h.val_SI, 'water'), 6) + >>> round(so_ds.calc_Q(), 6) 0.95 - >>> T_boil = T_bp_p(so_ds.get_flow()) + >>> T_boil = so_ds.calc_T_sat() >>> round(T_boil, 6) == round(so_ds.T.val_SI, 6) True """ @@ -150,10 +145,6 @@ def get_mandatory_constraints(self): 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, 'constant_deriv': True, 'latex': self.mass_flow_func_doc, 'num_eq': 1}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 2}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -181,65 +172,6 @@ def inlets(): def outlets(): return ['out1', 'out2'] - def fluid_func(self): - r""" - Calculate the vector of residual values for fluid balance equations. - - Returns - ------- - residual : list - Vector of residual values for component's fluid balance. - - .. math:: - - 0 = fluid_{i,in,1} - fluid_{i,out,j}\\ - \forall i \in \text{network fluids}, \; \forall j \in - \text{outlets} - """ - residual = [] - for o in self.outl: - for fluid, x in self.inl[0].fluid.val.items(): - residual += [x - o.fluid.val[fluid]] - return residual - - def fluid_func_doc(self, label): - r""" - Calculate the vector of residual values for fluid balance equations. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'0 = x_{fl\mathrm{,in,1}} - x_{fl\mathrm{,out,}j}' - r'\; \forall fl \in \text{network fluids,} \; \forall j \in' - r'\text{outlets}' - ) - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate partial derivatives for all fluid balance equations. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros(( - 2 * self.num_nw_fluids, self.num_i + self.num_o, self.num_nw_vars)) - for k in range(2): - for i in range(self.num_nw_fluids): - deriv[i + k * self.num_nw_fluids, 0, i + 3] = 1 - deriv[i + k * self.num_nw_fluids, k + self.num_i, i + 3] = -1 - return deriv - def energy_balance_func(self): r""" Calculate energy balance. @@ -260,6 +192,7 @@ def energy_balance_func(self): res += i.m.val_SI * i.h.val_SI for o in self.outl: res -= o.m.val_SI * o.h.val_SI + return res def energy_balance_func_doc(self, label): @@ -296,16 +229,18 @@ def energy_balance_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - j = 0 for i in self.inl: - self.jacobian[k, j, 0] = i.h.val_SI - self.jacobian[k, j, 2] = i.m.val_SI - j += 1 - j = 0 + if i.m.is_var: + self.jacobian[k, i.m.J_col] = i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = i.m.val_SI + for o in self.outl: - self.jacobian[k, j + self.num_i, 0] = -o.h.val_SI - self.jacobian[k, j + self.num_i, 2] = -o.m.val_SI - j += 1 + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -o.h.val_SI + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -o.m.val_SI + def outlet_states_func(self): r""" @@ -321,9 +256,12 @@ def outlet_states_func(self): 0 = h_{out,1} - h\left(p, x=0 \right)\\ 0 = h_{out,2} - h\left(p, x=1 \right) """ + o0 = self.outl[0] + o1 = self.outl[1] return [ - h_mix_pQ(self.outl[0].get_flow(), 0) - self.outl[0].h.val_SI, - h_mix_pQ(self.outl[1].get_flow(), 1) - self.outl[1].h.val_SI] + h_mix_pQ(o0.p.val_SI, 0, o0.fluid_data) - o0.h.val_SI, + h_mix_pQ(o1.p.val_SI, 1, o1.fluid_data) - o1.h.val_SI + ] def outlet_states_func_doc(self, label): r""" @@ -359,57 +297,30 @@ def outlet_states_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, self.num_i, 1] = dh_mix_dpQ(self.outl[0].get_flow(), 0) - self.jacobian[k, self.num_i, 2] = -1 - self.jacobian[k + 1, self.num_i + 1, 1] = ( - dh_mix_dpQ(self.outl[1].get_flow(), 1)) - self.jacobian[k + 1, self.num_i + 1, 2] = -1 - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - for outconn in self.outl: - for fluid, x in inconn.fluid.val.items(): - if (outconn.fluid.val_set[fluid] is False and - outconn.good_starting_values is False): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: + o0 = self.outl[0] + o1 = self.outl[1] + if o0.p.is_var: + self.jacobian[k, o0.p.J_col] = ( + dh_mix_dpQ(o0.p.val_SI, 0, o0.fluid_data) + ) + if o0.h.is_var and self.it == 0: + self.jacobian[k, o0.h.J_col] = -1 + + if o1.p.is_var: + self.jacobian[k + 1, o1.p.J_col] = ( + dh_mix_dpQ(o1.p.val_SI, 1, o1.fluid_data) + ) + if o1.h.is_var and self.it == 0: + self.jacobian[k + 1, o1.h.J_col] = -1 + + def propagate_wrapper_to_target(self, branch): + if self in branch["components"]: return - inconn = self.inl[0] - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - inconn.source.propagate_fluid_to_source(inconn, start) + for outconn in self.outl: + branch["connections"] += [outconn] + branch["components"] += [self] + outconn.target.propagate_wrapper_to_target(branch) @staticmethod def initialise_source(c, key): @@ -441,9 +352,9 @@ def initialise_source(c, key): return 10e5 elif key == 'h': if c.source_id == 'out1': - return h_mix_pQ(c.get_flow(), 1) + return h_mix_pQ(c.p.val_SI, 0, c.fluid_data) else: - return h_mix_pQ(c.get_flow(), 0) + return h_mix_pQ(c.p.val_SI, 1, c.fluid_data) @staticmethod def initialise_target(c, key): @@ -473,7 +384,7 @@ def initialise_target(c, key): if key == 'p': return 10e5 elif key == 'h': - return h_mix_pQ(c.get_flow(), 0.5) + return h_mix_pQ(c.p.val_SI, 0.5, c.fluid_data) def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/nodes/drum.py b/src/tespy/components/nodes/drum.py index 5779e6be8..d32c47b42 100644 --- a/src/tespy/components/nodes/drum.py +++ b/src/tespy/components/nodes/drum.py @@ -98,8 +98,7 @@ class Drum(DropletSeparator): >>> from tespy.tools.characteristics import load_default_char as ldc >>> import shutil >>> import numpy as np - >>> nw = Network(fluids=['NH3', 'air'], T_unit='C', p_unit='bar', - ... h_unit='kJ / kg', iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', iterinfo=False) >>> fa = Source('feed ammonia') >>> amb_in = Source('air inlet') >>> amb_out = Sink('air outlet') @@ -134,8 +133,8 @@ class Drum(DropletSeparator): >>> ev.set_attr(Q=-1e6) >>> erp.set_attr(eta_s=0.8) >>> f_dr.set_attr(p=5, T=-5) - >>> erp_ev.set_attr(m=Ref(f_dr, 4, 0), fluid={'air': 0, 'NH3': 1}) - >>> amb_ev.set_attr(fluid={'air': 1, 'NH3': 0}, T=30) + >>> erp_ev.set_attr(m=Ref(f_dr, 4, 0), fluid={'NH3': 1}) + >>> amb_ev.set_attr(fluid={'air': 1}, T=30) >>> ev_amb.set_attr(p=1) >>> nw.solve('design') >>> nw.save('tmp') @@ -170,43 +169,144 @@ def inlets(): def outlets(): return ['out1', 'out2'] - def preprocess(self, nw, num_eq=0): - super().preprocess(nw, num_eq) + def get_mandatory_constraints(self): + if self.inl[1].m == self.outl[0].m: + num_mass_eq = 0 + return { + 'mass_flow_constraints': { + 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, + 'constant_deriv': True, 'latex': self.mass_flow_func_doc, + 'num_eq': num_mass_eq}, + 'energy_balance_constraints': { + 'func': self.energy_balance_func, + 'deriv': self.energy_balance_deriv, + 'constant_deriv': False, 'latex': self.energy_balance_func_doc, + 'num_eq': 1}, + 'pressure_constraints': { + 'func': self.pressure_equality_func, + 'deriv': self.pressure_equality_deriv, + 'constant_deriv': True, + 'latex': self.pressure_equality_func_doc, + 'num_eq': self.num_i + self.num_o - 1}, + 'outlet_constraints': { + 'func': self.outlet_states_func, + 'deriv': self.outlet_states_deriv, + 'constant_deriv': False, + 'latex': self.outlet_states_func_doc, + 'num_eq': 2} + } + + def preprocess(self, num_nw_vars): + super().preprocess(num_nw_vars) self._propagation_start = False - @staticmethod - def initialise_source(c, key): + def mass_flow_func(self): r""" - Return a starting value for pressure and enthalpy at outlet. + Calculate the residual value for mass flow balance equation. - Parameters - ---------- - c : tespy.connections.connection.Connection - Connection to perform initialisation on. + Returns + ------- + res : float + Residual value of equation. - key : str - Fluid property to retrieve. + .. math:: + + 0 = \sum \dot{m}_{in,i} - \sum \dot{m}_{out,j} \; + \forall i \in inlets, \forall j \in outlets + """ + if self.inl[1].m == self.outl[0].m: + return self.inl[0].m.val_SI - self.outl[1].m.val_SI + else: + for i in self.inl: + res += i.m.val_SI + for o in self.outl: + res -= o.m.val_SI + + return res + + def mass_flow_deriv(self, k): + r""" + Calculate partial derivatives for mass flow equation. Returns ------- - val : float - Starting value for pressure/enthalpy in SI units. + deriv : list + Matrix with partial derivatives for the fluid equations. + """ + if self.inl[1].m == self.outl[0].m: + if self.inl[0].m.is_var: + self.jacobian[k, self.inl[0].m.J_col] = 1 + if self.outl[1].m.is_var: + self.jacobian[k, self.outl[1].m.J_col] = -1 + + else: + for i in self.inl: + if i.m.is_var: + self.jacobian[k, i.m.J_col] = 1 + for o in self.outl: + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -1 + + def energy_balance_func(self): + r""" + Calculate energy balance. + + Returns + ------- + residual : float + Residual value of energy balance. .. math:: - val = \begin{cases} - 10^6 & \text{key = 'p'}\\ - h\left(p, x=0 \right) & \text{key = 'h' at outlet 1}\\ - h\left(p, x=1 \right) & \text{key = 'h' at outlet 2} - \end{cases} + 0 = \sum_i \left(\dot{m}_{in,i} \cdot h_{in,i} \right) - + \sum_j \left(\dot{m}_{out,j} \cdot h_{out,j} \right)\\ + \forall i \in \text{inlets} \; \forall j \in \text{outlets} """ - if key == 'p': - return 10e5 - elif key == 'h': - if c.source_id == 'out1': - return h_mix_pQ(c.get_flow(), 0) - else: - return h_mix_pQ(c.get_flow(), 1) + if self.inl[1].m == self.outl[0].m: + res = ( + (self.inl[1].h.val_SI - self.outl[0].h.val_SI) + * self.outl[0].m.val_SI + + (self.inl[0].h.val_SI - self.outl[1].h.val_SI) + * self.inl[0].m.val_SI + ) + else: + res = 0 + for i in self.inl: + res += i.m.val_SI * i.h.val_SI + for o in self.outl: + res -= o.m.val_SI * o.h.val_SI + + return res + + def energy_balance_deriv(self, increment_filter, k): + r""" + Calculate partial derivatives of energy balance. + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + # due to topology reduction this is the case quite often + if self.inl[1].m == self.outl[0].m: + if self.outl[0].m.is_var: + self.jacobian[k, self.outl[0].m.J_col] = (self.inl[1].h.val_SI - self.outl[0].h.val_SI) + if self.inl[1].h.is_var: + self.jacobian[k, self.inl[1].h.J_col] = self.outl[0].m.val_SI + if self.outl[0].h.is_var: + self.jacobian[k, self.outl[0].h.J_col] = -self.outl[0].m.val_SI + + if self.inl[0].m.is_var: + self.jacobian[k, self.inl[0].m.J_col] = self.inl[0].h.val_SI - self.outl[1].h.val_SI + if self.inl[0].h.is_var: + self.jacobian[k, self.inl[0].h.J_col] = self.inl[0].m.val_SI + if self.outl[1].h.is_var: + self.jacobian[k, self.outl[1].h.J_col] = -self.outl[1].m.val_SI + else: + super().energy_balance_deriv(increment_filter, k) @staticmethod def initialise_target(c, key): @@ -238,65 +338,43 @@ def initialise_target(c, key): return 10e5 elif key == 'h': if c.target_id == 'in1': - return h_mix_pQ(c.get_flow(), 0) + return h_mix_pQ(c.p.val_SI, 0, c.fluid_data) else: - return h_mix_pQ(c.get_flow(), 0.7) + return h_mix_pQ(c.p.val_SI, 0.7, c.fluid_data) - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. + def propagate_wrapper_to_target(self, branch): + return super().propagate_wrapper_to_target(branch) - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. + def propagate_to_target(self, branch): - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if (not entry_point and inconn == start) or self._propagation_start: + if branch["connections"][-1].target_id == "in2": return - self._propagation_start = True - - for outconn in self.outl: - for fluid, x in inconn.fluid.val.items(): - if (outconn.fluid.val_set[fluid] is False and - outconn.good_starting_values is False): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - self._propagation_start = False - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if (not entry_point and outconn == start) or self._propagation_start: - return + outconn = self.outl[0] + subbranch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(subbranch) + branch["subbranches"][outconn.label] = subbranch - self._propagation_start = True + outconn = self.outl[1] + if subbranch["components"][-1] == self: - for inconn in self.inl: - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x + branch["connections"] += [outconn] + branch["components"] += [outconn.target] - inconn.source.propagate_fluid_to_source(inconn, start) + outconn.target.propagate_to_target(branch) - self._propagation_start = False + else: + subbranch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(subbranch) + branch["subbranches"][outconn.label] = subbranch def exergy_balance(self, T0): r""" @@ -323,7 +401,7 @@ def exergy_balance(self, T0): "chemical": np.nan, "physical": np.nan, "massless": np.nan } self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() def get_plotting_data(self): """ diff --git a/src/tespy/components/nodes/merge.py b/src/tespy/components/nodes/merge.py index 5068eb13d..b3b286cb2 100644 --- a/src/tespy/components/nodes/merge.py +++ b/src/tespy/components/nodes/merge.py @@ -16,7 +16,6 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import s_mix_pT -from tespy.tools.helpers import num_fluids class Merge(NodeBase): @@ -87,8 +86,7 @@ class Merge(NodeBase): >>> from tespy.networks import Network >>> import shutil >>> import numpy as np - >>> fluid_list = ['O2', 'N2'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', iterinfo=False) + >>> nw = Network(p_unit='bar', iterinfo=False) >>> so1 = Source('source1') >>> so2 = Source('source2') >>> so3 = Source('source3') @@ -112,8 +110,8 @@ class Merge(NodeBase): >>> T = 293.15 >>> inc1.set_attr(fluid={'O2': 0.23, 'N2': 0.77}, p=1, T=T, m=5) - >>> inc2.set_attr(fluid={'O2': 1, 'N2':0}, T=T, m=5) - >>> inc3.set_attr(fluid={'O2': 0, 'N2': 1}, T=T) + >>> inc2.set_attr(fluid={'O2': 1}, T=T, m=5) + >>> inc3.set_attr(fluid={'N2': 1}, T=T) >>> outg.set_attr(fluid={'N2': 0.4}) >>> nw.solve('design') >>> round(inc3.m.val_SI, 2) @@ -134,19 +132,29 @@ def component(): return 'merge' @staticmethod - def get_variables(): + def get_parameters(): return {'num_in': dc_simple()} def get_mandatory_constraints(self): + variable_fluids = set( + [fluid for c in self.inl + self.outl for fluid in c.fluid.is_var] + ) + num_fluid_eq = len(variable_fluids) + + if num_fluid_eq == 0: + num_fluid_eq = len(self.inl[0].fluid.val) + num_m_eq = 0 + else: + num_m_eq = 1 return { 'mass_flow_constraints': { 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 1}, + 'num_eq': num_m_eq}, 'fluid_constraints': { 'func': self.fluid_func, 'deriv': self.fluid_deriv, 'constant_deriv': False, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids}, + 'num_eq': num_fluid_eq}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -171,6 +179,10 @@ def inlets(self): def outlets(): return ['out1'] + @staticmethod + def is_branch_source(): + return True + def fluid_func(self): r""" Calculate the vector of residual values for fluid balance equations. @@ -229,16 +241,17 @@ def fluid_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - i = 0 + o = self.outl[0] for fluid, x in self.outl[0].fluid.val.items(): - j = 0 - for inl in self.inl: - self.jacobian[k, j, 0] = inl.fluid.val[fluid] - self.jacobian[k, j, i + 3] = inl.m.val_SI - j += 1 - self.jacobian[k, j, 0] = -x - self.jacobian[k, j, i + 3] = -self.outl[0].m.val_SI - i += 1 + for i in self.inl: + if i.m.is_var: + self.jacobian[k, i.m.J_col] = i.fluid.val[fluid] + if fluid in i.fluid.is_var: + self.jacobian[k, i.fluid.J_col[fluid]] = i.m.val_SI + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -x + if fluid in o.fluid.is_var: + self.jacobian[k, o.fluid.J_col[fluid]] = -o.m.val_SI k += 1 def energy_balance_func(self): @@ -294,76 +307,43 @@ def energy_balance_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, self.num_i, 0] = -self.outl[0].h.val_SI - self.jacobian[k, self.num_i, 2] = -self.outl[0].m.val_SI - j = 0 for i in self.inl: - self.jacobian[k, j, 0] = i.h.val_SI - self.jacobian[k, j, 2] = i.m.val_SI - j += 1 + if i.m.is_var: + self.jacobian[k, i.m.J_col] = i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = i.m.val_SI + o = self.outl[0] + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -o.h.val_SI + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -o.m.val_SI - def initialise_fluids(self): - """Fluid initialisation for fluid mixture at outlet of the node.""" - num_fl = {} - for o in self.outl: - num_fl[o] = num_fluids(o.fluid.val) + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + outconn = self.outl[0] + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) - for i in self.inl: - num_fl[i] = num_fluids(i.fluid.val) - - ls = [] - if any(num_fl.values()) and not all(num_fl.values()): - for conn, num in num_fl.items(): - if num == 1: - ls += [conn] - - for c in ls: - for fluid in self.nw_fluids: - if not self.outl[0].fluid.val_set[fluid]: - self.outl[0].fluid.val[fluid] = c.fluid.val[fluid] - for i in self.inl: - if not i.fluid.val_set[fluid]: - i.fluid.val[fluid] = c.fluid.val[fluid] - self.outl[0].target.propagate_fluid_to_target(o, o, entry_point=True) - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Fluid propagation stops here. + return {outconn.label: branch} - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ + def propagate_to_target(self, branch): return - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: + def propagate_wrapper_to_target(self, branch): + if self in branch["components"]: return - for inconn in self.inl: - for fluid, x in outconn.fluid.val.items(): - if (not inconn.fluid.val_set[fluid] and - not inconn.good_starting_values): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) + outconn = self.outl[0] + branch["connections"] += [outconn] + branch["components"] += [self] + outconn.target.propagate_wrapper_to_target(branch) def entropy_balance(self): r""" @@ -386,13 +366,14 @@ def entropy_balance(self): """ T_ref = 298.15 p_ref = 1e5 - self.S_irr = self.outl[0].m.val_SI * ( - self.outl[0].s.val_SI - - s_mix_pT([0, p_ref, 0, self.outl[0].fluid.val], T_ref)) + o = self.outl[0] + self.S_irr = o.m.val_SI * ( + o.s.val_SI - s_mix_pT(p_ref, T_ref, o.fluid_data, o.mixing_rule) + ) for i in self.inl: self.S_irr -= i.m.val_SI * ( - i.s.val_SI - - s_mix_pT([0, p_ref, 0, i.fluid.val], T_ref)) + i.s.val_SI - s_mix_pT(p_ref, T_ref, i.fluid_data, i.mixing_rule) + ) def exergy_balance(self, T0): r""" @@ -496,7 +477,7 @@ def exergy_balance(self, T0): "chemical": np.nan, "physical": np.nan, "massless": np.nan } self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/nodes/separator.py b/src/tespy/components/nodes/separator.py index e4d72913e..c826f022c 100644 --- a/src/tespy/components/nodes/separator.py +++ b/src/tespy/components/nodes/separator.py @@ -15,10 +15,10 @@ from tespy.components.nodes.base import NodeBase from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import dT_mix_dph from tespy.tools.fluid_properties import dT_mix_pdh -from tespy.tools.fluid_properties import dT_mix_ph_dfluid + +# from tespy.tools.fluid_properties import dT_mix_ph_dfluid class Separator(NodeBase): @@ -94,8 +94,7 @@ class Separator(NodeBase): >>> from tespy.networks import Network >>> import shutil >>> import numpy as np - >>> fluid_list = ['O2', 'N2'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', + >>> nw = Network(p_unit='bar', T_unit='C', ... iterinfo=False) >>> so = Source('source') >>> si1 = Sink('sink1') @@ -127,7 +126,7 @@ class Separator(NodeBase): will leave the separator at the second outlet the case of 30 % oxygen mass fraction for this outlet. - >>> outg1.set_attr(m=np.nan) + >>> outg1.set_attr(m=None) >>> outg2.set_attr(fluid={'O2': 0.3}) >>> nw.solve('design') >>> outg2.fluid.val['O2'] @@ -141,10 +140,17 @@ def component(): return 'separator' @staticmethod - def get_variables(): + def get_parameters(): return {'num_out': dc_simple()} def get_mandatory_constraints(self): + self.variable_fluids = set( + [fluid for c in self.inl + self.outl for fluid in c.fluid.is_var] + ) + num_fluid_eq = len(self.variable_fluids) + if num_fluid_eq == 0: + num_fluid_eq = 1 + self.variable_fluids = [list(self.inl[0].fluid.is_set)[0]] return { 'mass_flow_constraints': { 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, @@ -153,7 +159,7 @@ def get_mandatory_constraints(self): 'fluid_constraints': { 'func': self.fluid_func, 'deriv': self.fluid_deriv, 'constant_deriv': False, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids}, + 'num_eq': num_fluid_eq}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -178,6 +184,32 @@ def outlets(self): self.set_attr(num_out=2) return self.outlets() + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + branches = {} + for outconn in self.outl: + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) + + branches[outconn.label] = branch + return branches + + def propagate_to_target(self, branch): + return + + def propagate_wrapper_to_target(self, branch): + branch["components"] += [self] + for outconn in self.outl: + branch["connections"] += [outconn] + outconn.target.propagate_wrapper_to_target(branch) + def fluid_func(self): r""" Calculate the vector of residual values for fluid balance equations. @@ -194,9 +226,10 @@ def fluid_func(self): \forall fl \in \text{network fluids,} \; \forall j \in \text{outlets} """ + i = self.inl[0] residual = [] - for fluid, x in self.inl[0].fluid.val.items(): - res = x * self.inl[0].m.val_SI + for fluid in self.variable_fluids: + res = i.fluid.val[fluid] * i.m.val_SI for o in self.outl: res -= o.fluid.val[fluid] * o.m.val_SI residual += [res] @@ -236,17 +269,20 @@ def fluid_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - i = 0 - for fluid in self.nw_fluids: - j = 0 + i = self.inl[0] + for fluid in self.variable_fluids: for o in self.outl: - self.jacobian[k, j + 1, 0] = -o.fluid.val[fluid] - self.jacobian[k, j + 1, i + 3] = -o.m.val_SI - j += 1 - self.jacobian[k, 0, 0] = self.inl[0].fluid.val[fluid] - self.jacobian[k, 0, i + 3] = self.inl[0].m.val_SI + if self.is_variable(o.m): + self.jacobian[k, o.m.J_col] = -o.fluid.val[fluid] + if fluid in o.fluid.is_var: + self.jacobian[k, o.fluid.J_col[fluid]] = -o.m.val_SI + + if self.is_variable(i.m): + self.jacobian[k, i.m.J_col] = i.fluid.val[fluid] + if fluid in i.fluid.is_var: + self.jacobian[k, i.fluid.J_col[fluid]] = i.m.val_SI + k += 1 - i += 1 def energy_balance_func(self): r""" @@ -263,9 +299,9 @@ def energy_balance_func(self): \forall j \in \text{outlets} """ residual = [] - T_in = T_mix_ph(self.inl[0].get_flow(), T0=self.inl[0].T.val_SI) + T_in = self.inl[0].calc_T(T0=300) for o in self.outl: - residual += [T_in - T_mix_ph(o.get_flow(), T0=o.T.val_SI)] + residual += [T_in - o.calc_T(T0=300)] return residual def energy_balance_func_doc(self, label): @@ -300,48 +336,22 @@ def energy_balance_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - i = self.inl[0].get_flow() - dT_dp_in = dT_mix_dph(i) - dT_dh_in = dT_mix_pdh(i) - dT_dfluid_in = dT_mix_ph_dfluid(i) - j = 0 - for c in self.outl: - o = c.get_flow() - self.jacobian[k, 0, 1] = dT_dp_in - self.jacobian[k, 0, 2] = dT_dh_in - self.jacobian[k, 0, 3:] = dT_dfluid_in - self.jacobian[k, j + 1, 1] = -dT_mix_dph(o) - self.jacobian[k, j + 1, 2] = -dT_mix_pdh(o) - self.jacobian[k, j + 1, 3:] = -np.array(dT_mix_ph_dfluid(o)) - j += 1 + i = self.inl[0] + dT_dp_in = dT_mix_dph(i.p.val_SI, i.h.val_SI, i.fluid_data, i.mixing_rule) + dT_dh_in = dT_mix_pdh(i.p.val_SI, i.h.val_SI, i.fluid_data, i.mixing_rule) + # dT_dfluid_in = {} + # for fluid in i.fluid.is_var: + # dT_dfluid_in[fluid] = dT_mix_ph_dfluid(i) + for o in self.outl: + if self.is_variable(i.p): + self.jacobian[k, i.p.J_col] = dT_dp_in + if self.is_variable(i.h): + self.jacobian[k, i.h.J_col] = dT_dh_in + # for fluid in i.fluid.is_var: + # self.jacobian[k, i.fluid.J_col[fluid]] = dT_dfluid_in[fluid] + args = (o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) + self.jacobian[k, o.p.J_col] = -dT_mix_dph(*args) + self.jacobian[k, o.h.J_col] = -dT_mix_pdh(*args) + # for fluid in o.fluid.is_var: + # self.jacobian[k, o.fluid.J_col[fluid]] = -dT_mix_ph_dfluid(o) k += 1 - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Fluid propagation stops here. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - return - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - return diff --git a/src/tespy/components/nodes/splitter.py b/src/tespy/components/nodes/splitter.py index 45b1e0925..ed774a076 100644 --- a/src/tespy/components/nodes/splitter.py +++ b/src/tespy/components/nodes/splitter.py @@ -84,8 +84,7 @@ class Splitter(NodeBase): >>> from tespy.networks import Network >>> import shutil >>> import numpy as np - >>> fluid_list = ['O2', 'N2'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', + >>> nw = Network(p_unit='bar', T_unit='C', ... iterinfo=False) >>> so = Source('source') >>> si1 = Sink('sink1') @@ -123,7 +122,7 @@ def component(): return 'splitter' @staticmethod - def get_variables(): + def get_parameters(): return {'num_out': dc_simple()} def get_mandatory_constraints(self): @@ -132,10 +131,6 @@ def get_mandatory_constraints(self): 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, 'constant_deriv': True, 'latex': self.mass_flow_func_doc, 'num_eq': 1}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_o * self.num_nw_fluids}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -160,72 +155,15 @@ def outlets(self): self.set_attr(num_out=2) return self.outlets() - def preprocess(self, nw, num_eq=0): - super().preprocess(nw, num_eq) - self._propagation_start = False - - def fluid_func(self): - r""" - Calculate the vector of residual values for fluid balance equations. - - Returns - ------- - residual : list - Vector of residual values for component's fluid balance. - - .. math:: - - 0 = x_{fl,in} - x_{fl,out,j} \; - \forall fl \in \text{network fluids,} \; \forall j \in - \text{outlets} - """ - residual = [] - for o in self.outl: - for fluid, x in self.inl[0].fluid.val.items(): - residual += [x - o.fluid.val[fluid]] - return residual - - def fluid_func_doc(self, label): - r""" - Calculate the vector of residual values for fluid balance equations. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'0 = x_{fl\mathrm{,in}} - x_{fl\mathrm{,out,}j}' - r'\; \forall fl \in \text{network fluids,} \; \forall j \in' - r'\text{outlets}' - ) - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate partial derivatives for all fluid balance equations. + def propagate_wrapper_to_target(self, branch): + branch["components"] += [self] + for outconn in self.outl: + branch["connections"] += [outconn] + outconn.target.propagate_wrapper_to_target(branch) - Returns - ------- - deriv : list - Matrix with partial derivatives for the fluid equations. - """ - deriv = np.zeros((self.num_nw_fluids * self.num_o, 1 + self.num_o, - self.num_nw_vars)) - k = 0 - for o in self.outl: - i = 0 - for fluid in self.nw_fluids: - deriv[i + k * self.num_nw_fluids, 0, i + 3] = 1 - deriv[i + k * self.num_nw_fluids, k + 1, i + 3] = -1 - i += 1 - k += 1 - return deriv + def preprocess(self, num_nw_vars): + super().preprocess(num_nw_vars) + self._propagation_start = False def energy_balance_func(self): r""" @@ -258,7 +196,7 @@ def energy_balance_func_doc(self, label): latex = r'0=h_{in}-h_{\mathrm{out,}j}\;\forall j \in\text{outlets}' return generate_latex_eq(self, latex, label) - def energy_balance_deriv(self): + def energy_balance_deriv(self, k): r""" Calculate partial derivatives for energy balance equation. @@ -267,61 +205,8 @@ def energy_balance_deriv(self): deriv : list Matrix of partial derivatives. """ - deriv = np.zeros((self.num_o, 1 + self.num_o, self.num_nw_vars)) - k = 0 - for o in self.outl: - deriv[k, 0, 2] = 1 - deriv[k, k + 1, 2] = -1 - k += 1 - return deriv - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - for outconn in self.outl: - for fluid, x in inconn.fluid.val.items(): - if (not outconn.fluid.val_set[fluid] and - not outconn.good_starting_values): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if (not entry_point and outconn == start) or self._propagation_start: - return - - self._propagation_start = True - - inconn = self.inl[0] - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) - - self._propagation_start = False + for eq, o in enumerate(self.outl): + if self.inl[0].h.is_var: + self.jacobian[k + eq, self.inl[0].h.J_col] = 1 + if o.h.is_var: + self.jacobian[k + eq, o.h.J_col] = -1 diff --git a/src/tespy/components/piping/pipe.py b/src/tespy/components/piping/pipe.py index 621806dbb..fb575b437 100644 --- a/src/tespy/components/piping/pipe.py +++ b/src/tespy/components/piping/pipe.py @@ -27,7 +27,8 @@ class Pipe(SimpleHeatExchanger): - :py:meth:`tespy.components.component.Component.pr_func` - :py:meth:`tespy.components.component.Component.zeta_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.energy_balance_func` - - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hydro_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.darcy_group_func` + - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.hw_group_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.kA_group_func` - :py:meth:`tespy.components.heat_exchangers.simple.SimpleHeatExchanger.kA_char_group_func` @@ -91,13 +92,18 @@ class Pipe(SimpleHeatExchanger): Length of the pipes, :math:`L/\text{m}`. ks : float, dict, :code:`"var"` - Pipe's roughness, :math:`ks/\text{m}` for darcy friction, - :math:`ks/\text{1}` for hazen-williams equation. + Pipe's roughness, :math:`ks/\text{m}`. - hydro_group : str, dict - Parametergroup for pressure drop calculation based on pipes dimensions. - Choose 'HW' for hazen-williams equation, else darcy friction factor is - used. + darcy_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using darcy weissbach equation. + + ks_HW : float, dict, :code:`"var"` + Pipe's roughness, :math:`ks/\text{1}`. + + hw_group : str, dict + Parametergroup for pressure drop calculation based on pipes dimensions + using hazen williams equation. kA : float, dict, :code:`"var"` Area independent heat transfer coefficient, @@ -126,8 +132,7 @@ class Pipe(SimpleHeatExchanger): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluid_list = ['ethanol'] - >>> nw = Network(fluids=fluid_list) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> so = Source('source 1') >>> si = Sink('sink 1') diff --git a/src/tespy/components/piping/valve.py b/src/tespy/components/piping/valve.py index 3e76a9254..3b23aadcb 100644 --- a/src/tespy/components/piping/valve.py +++ b/src/tespy/components/piping/valve.py @@ -97,9 +97,7 @@ class Valve(Component): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluid_list = ['CH4'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', iterinfo=False) >>> so = Source('source') >>> si = Sink('sink') >>> v = Valve('valve') @@ -118,7 +116,7 @@ class Valve(Component): >>> round(v.pr.val, 3) 0.188 - The simulation determined the area independant zeta value + The simulation determined the area independent zeta value :math:`\frac{\zeta}{D^4}`. This zeta remains constant if the cross sectional area of the valve opening does not change. Using the zeta value we can determine the pressure ratio at a different feed pressure. @@ -136,7 +134,7 @@ class Valve(Component): def component(): return 'valve' - def get_variables(self): + def get_parameters(self): return { 'pr': dc_cp( min_val=1e-4, max_val=1, num_eq=1, @@ -154,14 +152,6 @@ def get_variables(self): def get_mandatory_constraints(self): return { - 'mass_flow_constraints': { - 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, - 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 1}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids}, 'enthalpy_equality_constraints': { 'func': self.enthalpy_equality_func, 'deriv': self.enthalpy_equality_deriv, @@ -242,18 +232,26 @@ def dp_char_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv( - self.dp_char_func, 'm', 0) + f = self.dp_char_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) if self.dp_char.param == 'v': - self.jacobian[k, 0, 1] = self.numeric_deriv( - self.dp_char_func, 'p', 0) - self.jacobian[k, 0, 2] = self.numeric_deriv( - self.dp_char_func, 'h', 0) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv( + self.dp_char_func, 'p', i + ) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv( + self.dp_char_func, 'h', i + ) else: - self.jacobian[k, 0, 1] = 1 + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = 1 - self.jacobian[k, 1, 1] = -1 + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -1 def initialise_source(self, c, key): r""" @@ -315,12 +313,13 @@ def initialise_target(self, c, key): def calc_parameters(self): r"""Postprocessing parameter calculation.""" - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - self.pr.val = o[1] / i[1] - self.zeta.val = ((i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - )) + i = self.inl[0] + o = self.outl[0] + self.pr.val = o.p.val_SI / i.p.val_SI + self.zeta.val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) def entropy_balance(self): r""" @@ -336,7 +335,8 @@ def entropy_balance(self): \right)\\ """ self.S_irr = self.inl[0].m.val_SI * ( - self.outl[0].s.val_SI - self.inl[0].s.val_SI) + self.outl[0].s.val_SI - self.inl[0].s.val_SI + ) def exergy_balance(self, T0): r""" @@ -395,7 +395,7 @@ def exergy_balance(self, T0): self.E_D = self.E_F else: self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/reactors/fuel_cell.py b/src/tespy/components/reactors/fuel_cell.py index 5b4418304..088d09757 100644 --- a/src/tespy/components/reactors/fuel_cell.py +++ b/src/tespy/components/reactors/fuel_cell.py @@ -10,9 +10,6 @@ SPDX-License-Identifier: MIT """ - - -import CoolProp.CoolProp as CP import numpy as np from tespy.components.component import Component @@ -20,8 +17,6 @@ from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import h_mix_pT -from tespy.tools.global_vars import molar_masses -from tespy.tools.helpers import TESPyComponentError class FuelCell(Component): @@ -120,9 +115,7 @@ class FuelCell(Component): >>> from tespy.networks import Network >>> from tespy.tools import ComponentCharacteristics as dc_cc >>> import shutil - >>> fluid_list = ['H2O', 'O2', 'H2'] - >>> nw = Network(fluids=fluid_list, T_unit='C', p_unit='bar', - ... v_unit='l / s', iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', v_unit='l / s', iterinfo=False) >>> fc = FuelCell('fuel cell') >>> fc.component() 'fuel cell' @@ -144,7 +137,7 @@ class FuelCell(Component): the given power output. The cooling fluid is pure water. >>> fc.set_attr(eta=0.45, P=-200e03, Q=-200e03, pr=0.9) - >>> cw_in.set_attr(T=25, p=1, m=1, fluid={'H2O': 1, 'O2': 0, 'H2': 0}) + >>> cw_in.set_attr(T=25, p=1, m=1, fluid={'H2O': 1}) >>> oxygen_in.set_attr(T=25, p=1) >>> hydrogen_in.set_attr(T=25) >>> nw.solve('design') @@ -158,9 +151,7 @@ class FuelCell(Component): def component(): return 'fuel cell' -# %% Variables - - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp(max_val=0), 'Q': dc_cp( @@ -185,18 +176,16 @@ def get_variables(self): latex=self.specific_energy_func_doc) } -# %% Mandatory constraints - def get_mandatory_constraints(self): + num_mass_eq = ( + (self.inl[1].m.is_var or self.outl[1].m.is_var) + + (self.inl[1].m.is_var or self.outl[2].m.is_var) + ) return { 'mass_flow_constraints': { 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 3}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 4}, + 'num_eq': num_mass_eq}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -210,17 +199,15 @@ def get_mandatory_constraints(self): 'num_eq': 2}, } -# %% Inlets and outlets - - def inlets(self): + @staticmethod + def inlets(): return ['in1', 'in2', 'in3'] - def outlets(self): + @staticmethod + def outlets(): return ['out1', 'out2'] -# %% Equations and derivatives - - def preprocess(self, nw): + def preprocess(self, num_nw_vars): if not self.P.is_set: self.set_attr(P='var') @@ -229,27 +216,12 @@ def preprocess(self, nw): self.label + ' as custom variable of the system.') logger.info(msg) - for fluid in ['H2', 'H2O', 'O2']: - try: - setattr( - self, fluid, [x for x in nw.fluids if x in [ - a.replace(' ', '') for a in - CP.get_aliases(fluid.upper()) - ]][0]) - except IndexError: - msg = ( - 'The component ' + self.label + ' (class ' + - self.__class__.__name__ + ') requires that the fluid ' - '[fluid] is in the network\'s list of fluids.') - aliases = ', '.join(CP.get_aliases(fluid.upper())) - msg = msg.replace( - '[fluid]', fluid.upper() + ' (aliases: ' + aliases + ')') - logger.error(msg) - raise TESPyComponentError(msg) - + self.o2 = "O2" + self.h2 = "H2" + self.h2o = "H2O" self.e0 = self.calc_e0() - super().preprocess(nw) + super().preprocess(num_nw_vars) def calc_e0(self): r""" @@ -257,7 +229,7 @@ def calc_e0(self): Returns ------- - val : float + float Specific energy. .. math:: @@ -273,7 +245,7 @@ def calc_e0(self): hf['H2O'] = -286000 hf['H2'] = 0 hf['O2'] = 0 - M = molar_masses[self.H2] + M = self.inl[2].fluid.wrapper[self.h2]._molar_mass e0 = (2 * hf['H2O'] - 2 * hf['H2'] - hf['O2']) / (2 * M) return e0 @@ -322,11 +294,11 @@ def eta_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - # derivative for m_H2,in: - self.jacobian[k, 2, 0] = -self.eta.val * self.e0 - # derivatives for variable P: + if self.inl[2].m.is_var: + self.jacobian[k, self.inl[2].m.J_col] = -self.eta.val * self.e0 + # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 + self.jacobian[k, self.P.J_col] = 1 def heat_func(self): r""" @@ -375,11 +347,14 @@ def heat_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 0, 0] = -( - self.inl[0].h.val_SI - self.outl[0].h.val_SI - ) - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI - self.jacobian[k, 3, 2] = self.inl[0].m.val_SI + i = self.inl[0] + o = self.outl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if o.h.is_var: + self.jacobian[k, o.h.J_col] = i.m.val_SI def specific_energy_func(self): r""" @@ -425,13 +400,14 @@ def specific_energy_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 2, 0] = -self.e.val + if self.inl[2].m.is_var: + self.jacobian[k, self.inl[2].m.J_col] = -self.e.val # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 + self.jacobian[k, self.P.J_col] = 1 # derivatives for variable e if self.e.is_var: - self.jacobian[k, 5 + self.e.var_pos, 0] = -self.inl[2].m.val_SI + self.jacobian[k, self.e.J_col] = -self.inl[2].m.val_SI def energy_balance_func(self): r""" @@ -505,161 +481,46 @@ def energy_balance_deriv(self, increment_filter, k): # derivatives determined from calc_P function T_ref = 298.15 p_ref = 1e5 - h_refh2o = h_mix_pT([1, p_ref, 0, self.outl[1].fluid.val], T_ref) - h_refh2 = h_mix_pT([1, p_ref, 0, self.inl[2].fluid.val], T_ref) - h_refo2 = h_mix_pT([1, p_ref, 0, self.inl[1].fluid.val], T_ref) + h_refh2o = h_mix_pT(p_ref, T_ref, self.outl[1].fluid_data, self.outl[1].mixing_rule) + h_refo2 = h_mix_pT(p_ref, T_ref, self.inl[1].fluid_data, self.inl[1].mixing_rule) + h_refh2 = h_mix_pT(p_ref, T_ref, self.inl[2].fluid_data, self.inl[2].mixing_rule) # derivatives cooling water inlet - self.jacobian[k, 0, 0] = self.outl[0].h.val_SI - self.inl[0].h.val_SI - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI + i = self.inl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = self.outl[0].h.val_SI - i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI # derivatives water outlet - self.jacobian[k, 4, 0] = (self.outl[1].h.val_SI - h_refh2o) - self.jacobian[k, 4, 2] = self.outl[1].m.val_SI + o = self.outl[1] + if o.m.is_var: + self.jacobian[k, o.m.J_col] = o.h.val_SI - h_refh2o + if o.h.is_var: + self.jacobian[k, o.h.J_col] = o.m.val_SI # derivative cooling water outlet - self.jacobian[k, 3, 2] = self.inl[0].m.val_SI + o = self.outl[0] + if o.h.is_var: + self.jacobian[k, o.h.J_col] = self.inl[0].m.val_SI # derivatives oxygen inlet - self.jacobian[k, 1, 0] = -(self.inl[1].h.val_SI - h_refo2) - self.jacobian[k, 1, 2] = -self.inl[1].m.val_SI + i = self.inl[1] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = -(i.h.val_SI - h_refo2) + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI # derivatives hydrogen inlet - self.jacobian[k, 2, 0] = -(self.inl[2].h.val_SI - h_refh2 - self.e0) - self.jacobian[k, 2, 2] = -self.inl[2].m.val_SI + i = self.inl[2] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = -(i.h.val_SI - h_refh2 - self.e0) + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 - - def fluid_func(self): - r""" - Equations for fluid composition. - - Returns - ------- - residual : list - Residual values of equation. - - .. math:: - - 0 = x_\mathrm{i,in,1} - x_\mathrm{i,out,1} - \forall i \in \text{network fluids}\\ - 0 = \begin{cases} - 1 - x_\mathrm{i,in2} & \text{i=}H_{2}O\\ - x_\mathrm{i,in2} & \text{else} - \end{cases} \forall i \in \text{network fluids}\\ - 0 = \begin{cases} - 1 - x_\mathrm{i,out,2} & \text{i=}O_{2}\\ - x_\mathrm{i,out,2} & \text{else} - \end{cases} \forall i \in \text{network fluids}\\ - 0 = \begin{cases} - 1 - x_\mathrm{i,out,3} & \text{i=}H_{2}\\ - x_\mathrm{i,out,3} & \text{else} - \end{cases} \forall i \in \text{network fluids} - """ - residual = [] - # equations for fluid composition in cooling loop - for fluid, x in self.inl[0].fluid.val.items(): - residual += [x - self.outl[0].fluid.val[fluid]] - - # equations to constrain fluids to inlets/outlets - residual += [1 - self.inl[1].fluid.val[self.O2]] - residual += [1 - self.inl[2].fluid.val[self.H2]] - residual += [1 - self.outl[1].fluid.val[self.H2O]] - - # equations to ban other fluids off inlets/outlets - for fluid in self.inl[1].fluid.val.keys(): - if fluid != self.H2O: - residual += [0 - self.outl[1].fluid.val[fluid]] - if fluid != self.O2: - residual += [0 - self.inl[1].fluid.val[fluid]] - if fluid != self.H2: - residual += [0 - self.inl[2].fluid.val[fluid]] - - return residual - - def fluid_func_doc(self, label): - r""" - Equations for fluid composition. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'\begin{split}' + '\n' - r'0 = &x_\mathrm{i,in,1} - x_\mathrm{i,out,1}\\' + '\n' - r'0 = &\begin{cases}' + '\n' - r'1 - x_\mathrm{i,out,2} & \text{i=}H_{2}O\\' + '\n' - r'x_\mathrm{i,out,2} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'0 =&\begin{cases}' + '\n' - r'1 - x_\mathrm{i,in,2} & \text{i=}O_{2}\\' + '\n' - r'x_\mathrm{i,in,2} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'0 =&\begin{cases}' + '\n' - r'1 - x_\mathrm{i,in,3} & \text{i=}H_{2}\\' + '\n' - r'x_\mathrm{i,in,3} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'&\forall i \in \text{network fluids}' + '\n' - r'\end{split}') - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate the partial derivatives for cooling loop fluid balance. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - # derivatives for cooling fluid composition - deriv = np.zeros(( - self.num_nw_fluids * 4, - 5 + self.num_vars, - self.num_nw_vars)) - - k = 0 - for fluid, x in self.inl[0].fluid.val.items(): - deriv[k, 0, 3 + k] = 1 - deriv[k, 3, 3 + k] = -1 - k += 1 - - # derivatives to constrain fluids to inlets/outlets - i = 0 - for fluid in self.nw_fluids: - if fluid == self.H2O: - deriv[k, 4, 3 + i] = -1 - elif fluid == self.O2: - deriv[k + 1, 1, 3 + i] = -1 - elif fluid == self.H2: - deriv[k + 2, 2, 3 + i] = -1 - i += 1 - k += 3 - - # derivatives to ban fluids off inlets/outlets - i = 0 - for fluid in self.nw_fluids: - if fluid != self.H2O: - deriv[k, 4, 3 + i] = -1 - k += 1 - if fluid != self.O2: - deriv[k, 1, 3 + i] = -1 - k += 1 - if fluid != self.H2: - deriv[k, 2, 3 + i] = -1 - k += 1 - i += 1 - - return deriv + self.jacobian[k, self.P.J_col] = 1 def mass_flow_func(self): r""" @@ -679,14 +540,15 @@ def mass_flow_func(self): \dot{m}_\mathrm{H_2,in,1} """ # calculate the ratio of o2 in water - o2 = molar_masses[self.O2] / ( - molar_masses[self.O2] + 2 * molar_masses[self.H2]) - # equation for mass flow balance cooling water - residual = [] - residual += [self.inl[0].m.val_SI - self.outl[0].m.val_SI] + M_o2 = self.inl[1].fluid.wrapper[self.o2]._molar_mass + M_h2 = self.inl[2].fluid.wrapper[self.h2]._molar_mass + o2 = M_o2 / (M_o2 + 2 * M_h2) # equations for mass flow balance of the fuel cell - residual += [o2 * self.outl[1].m.val_SI - self.inl[1].m.val_SI] - residual += [(1 - o2) * self.outl[1].m.val_SI - self.inl[2].m.val_SI] + residual = [] + if self.inl[1].m.is_var or self.outl[1].m.is_var: + residual += [o2 * self.outl[1].m.val_SI - self.inl[1].m.val_SI] + if self.inl[2].m.is_var or self.outl[1].m.is_var: + residual += [(1 - o2) * self.outl[1].m.val_SI - self.inl[2].m.val_SI] return residual def mass_flow_func_doc(self, label): @@ -714,7 +576,7 @@ def mass_flow_func_doc(self, label): ) return generate_latex_eq(self, latex, label) - def mass_flow_deriv(self): + def mass_flow_deriv(self, k): r""" Calculate the partial derivatives for all mass flow balance equations. @@ -723,20 +585,22 @@ def mass_flow_deriv(self): deriv : ndarray Matrix with partial derivatives for the mass flow equations. """ - # derivatives for mass flow balance in the heat exchanger - deriv = np.zeros((3, 5 + self.num_vars, self.num_nw_vars)) - deriv[0, 0, 0] = 1 - deriv[0, 3, 0] = -1 - # derivatives for mass flow balance for oxygen input - o2 = molar_masses[self.O2] / ( - molar_masses[self.O2] + 2 * molar_masses[self.H2]) - deriv[1, 4, 0] = o2 - deriv[1, 1, 0] = -1 - # derivatives for mass flow balance for hydrogen input - deriv[2, 4, 0] = (1 - o2) - deriv[2, 2, 0] = -1 + M_o2 = self.inl[1].fluid.wrapper[self.o2]._molar_mass + M_h2 = self.inl[2].fluid.wrapper[self.h2]._molar_mass + o2 = M_o2 / (M_o2 + 2 * M_h2) + # number of equations may vary here + if self.inl[1].m.is_var or self.outl[1].m.is_var: + if self.inl[1].m.is_var: + self.jacobian[k, self.inl[1].m.J_col] = -1 + if self.outl[1].m.is_var: + self.jacobian[k, self.outl[1].m.J_col] = o2 + k += 1 - return deriv + # derivatives for mass flow balance for hydrogen input + if self.outl[1].m.is_var: + self.jacobian[k, self.outl[1].m.J_col] = (1 - o2) + if self.inl[2].m.is_var: + self.jacobian[k, self.inl[2].m.J_col] = -1 def reactor_pressure_func(self): r""" @@ -754,7 +618,8 @@ def reactor_pressure_func(self): """ return [ self.outl[1].p.val_SI - self.inl[1].p.val_SI, - self.outl[1].p.val_SI - self.inl[2].p.val_SI] + self.outl[1].p.val_SI - self.inl[2].p.val_SI + ] def reactor_pressure_func_doc(self, label): r""" @@ -777,7 +642,7 @@ def reactor_pressure_func_doc(self, label): r'\end{split}') return generate_latex_eq(self, latex, label) - def reactor_pressure_deriv(self): + def reactor_pressure_deriv(self, k): r""" Calculate the partial derivatives for combustion pressure equations. @@ -786,15 +651,13 @@ def reactor_pressure_deriv(self): deriv : ndarray Matrix with partial derivatives for the pressure equations. """ - deriv = np.zeros((2, 5 + self.num_vars, self.num_nw_vars)) - # derivatives for pressure oxygen inlet - deriv[0, 1, 1] = -1 - deriv[0, 4, 1] = 1 - # derivatives for pressure hydrogen inlet - deriv[1, 2, 1] = -1 - deriv[1, 4, 1] = 1 - - return deriv + o = self.outl[1] + for i in self.inl[1:]: + if i.p.is_var: + self.jacobian[k, i.p.J_col] = -1 + if o.p.is_var: + self.jacobian[k, o.p.J_col] = 1 + k += 1 def calc_P(self): r""" @@ -832,35 +695,76 @@ def calc_P(self): T_ref = 298.15 p_ref = 1e5 - # equations to set a reference point for each h2o, h2 and o2 - h_refh2o = h_mix_pT([1, p_ref, 0, self.outl[1].fluid.val], T_ref) - h_refh2 = h_mix_pT([1, p_ref, 0, self.inl[2].fluid.val], T_ref) - h_refo2 = h_mix_pT([1, p_ref, 0, self.inl[1].fluid.val], T_ref) + # equations to set a reference point for each h2o, h2 and o2 # derivatives determined from calc_P function + h_refh2o = h_mix_pT(p_ref, T_ref, self.outl[1].fluid_data, self.outl[1].mixing_rule) + h_refo2 = h_mix_pT(p_ref, T_ref, self.inl[1].fluid_data, self.inl[1].mixing_rule) + h_refh2 = h_mix_pT(p_ref, T_ref, self.inl[2].fluid_data, self.inl[2].mixing_rule) val = ( - self.inl[2].m.val_SI * ( - self.inl[2].h.val_SI - h_refh2 - self.e0 - ) + self.inl[2].m.val_SI * (self.inl[2].h.val_SI - h_refh2 - self.e0) + self.inl[1].m.val_SI * (self.inl[1].h.val_SI - h_refo2) - - self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI - ) + - self.inl[0].m.val_SI * (self.outl[0].h.val_SI - self.inl[0].h.val_SI) - self.outl[1].m.val_SI * (self.outl[1].h.val_SI - h_refh2o) ) - return val - def initialise_fluids(self): - # Set values to pure fluid on gas inlets and water outlet. - self.inl[1].fluid.val[self.O2] = 1 - self.inl[2].fluid.val[self.H2] = 1 - self.outl[1].fluid.val[self.H2O] = 1 - self.inl[1].source.propagate_fluid_to_source( - self.inl[1], self.inl[1].source) - self.inl[2].source.propagate_fluid_to_source( - self.inl[2], self.inl[2].source) - self.outl[1].target.propagate_fluid_to_target( - self.outl[1], self.outl[1].target) + + + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + outconn = self.outl[1] + if "H2O" not in outconn.fluid.val: + outconn.fluid.val["H2O"] = 1 + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) + return {outconn.label: branch} + + def start_fluid_wrapper_branch(self): + outconn = self.outl[1] + branch = { + "connections": [outconn], + "components": [self] + } + outconn.target.propagate_wrapper_to_target(branch) + return {outconn.label: branch} + + def propagate_to_target(self, branch): + inconn = branch["connections"][-1] + if inconn == self.inl[0]: + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [outconn.target] + + outconn.target.propagate_to_target(branch) + else: + if inconn == self.inl[1] and "O2" not in inconn.fluid.val: + inconn.fluid.val["O2"] = 1 + if inconn == self.inl[2] and "H2" not in inconn.fluid.val: + inconn.fluid.val["H2"] = 1 + return + + def propagate_wrapper_to_target(self, branch): + inconn = branch["connections"][-1] + if inconn == self.inl[0]: + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [self] + + outconn.target.propagate_wrapper_to_target(branch) + else: + branch["components"] += [self] + return def initialise_source(self, c, key): r""" @@ -889,9 +793,8 @@ def initialise_source(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = c.get_flow() T = 20 + 273.15 - return h_mix_pT(flow, T) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def initialise_target(self, c, key): r""" @@ -920,60 +823,8 @@ def initialise_target(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = c.get_flow() T = 50 + 273.15 - return h_mix_pT(flow, T) - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - if inconn == self.inl[0]: - outconn = self.outl[0] - - for fluid, x in inconn.fluid.val.items(): - if (outconn.fluid.val_set[fluid] is False and - outconn.good_starting_values is False): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: - return - - if outconn == self.outl[0]: - inconn = self.inl[0] - - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def calc_parameters(self): r"""Postprocessing parameter calculation.""" @@ -983,11 +834,9 @@ def calc_parameters(self): self.e.val = self.P.val / self.inl[2].m.val_SI self.eta.val = self.e.val / self.e0 - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() + i = self.inl[0] + o = self.outl[0] self.zeta.val = ( - (i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * - (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - ) + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) ) diff --git a/src/tespy/components/reactors/water_electrolyzer.py b/src/tespy/components/reactors/water_electrolyzer.py index fbb7db212..9e8e959d8 100644 --- a/src/tespy/components/reactors/water_electrolyzer.py +++ b/src/tespy/components/reactors/water_electrolyzer.py @@ -11,7 +11,6 @@ SPDX-License-Identifier: MIT """ -import CoolProp.CoolProp as CP import numpy as np from tespy.components.component import Component @@ -19,12 +18,9 @@ from tespy.tools.data_containers import ComponentCharacteristics as dc_cc from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import dT_mix_dph from tespy.tools.fluid_properties import dT_mix_pdh from tespy.tools.fluid_properties import h_mix_pT -from tespy.tools.global_vars import molar_masses -from tespy.tools.helpers import TESPyComponentError class WaterElectrolyzer(Component): @@ -135,9 +131,7 @@ class WaterElectrolyzer(Component): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluid_list = ['O2', 'H2O', 'H2'] - >>> nw = Network(fluids=fluid_list, T_unit='C', p_unit='bar', - ... v_unit='l / s', iterinfo=False) + >>> nw = Network(T_unit='C', p_unit='bar', v_unit='l / s', iterinfo=False) >>> fw = Source('feed water') >>> oxy = Sink('oxygen sink') >>> hydro = Sink('hydrogen sink') @@ -164,7 +158,7 @@ class WaterElectrolyzer(Component): >>> el_cw = Connection(el, 'out1', cw_hot, 'in1') >>> nw.add_conns(fw_el, el_o, el_cmp, cmp_h, cw_el, el_cw) >>> fw_el.set_attr(p=10, T=15) - >>> cw_el.set_attr(p=5, T=15, fluid={'H2O': 1, 'H2': 0, 'O2': 0}) + >>> cw_el.set_attr(p=5, T=15, fluid={'H2O': 1}) >>> el_cw.set_attr(T=45) >>> cmp_h.set_attr(p=25) >>> el_cmp.set_attr(v=100, T=50) @@ -193,7 +187,7 @@ class WaterElectrolyzer(Component): def component(): return 'water electrolyzer' - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp(min_val=0), 'Q': dc_cp( @@ -224,15 +218,15 @@ def get_variables(self): } def get_mandatory_constraints(self): + num_mass_eq = ( + (self.inl[1].m.is_var or self.outl[1].m.is_var) + + (self.inl[1].m.is_var or self.outl[2].m.is_var) + ) return { 'mass_flow_constraints': { 'func': self.mass_flow_func, 'deriv': self.mass_flow_deriv, 'constant_deriv': True, 'latex': self.mass_flow_func_doc, - 'num_eq': 3}, - 'fluid_constraints': { - 'func': self.fluid_func, 'deriv': self.fluid_deriv, - 'constant_deriv': True, 'latex': self.fluid_func_doc, - 'num_eq': self.num_nw_fluids * 4}, + 'num_eq': num_mass_eq}, 'energy_balance_constraints': { 'func': self.energy_balance_func, 'deriv': self.energy_balance_deriv, @@ -260,7 +254,7 @@ def inlets(): def outlets(): return ['out1', 'out2', 'out3'] - def preprocess(self, nw): + def preprocess(self, num_nw_vars): if not self.P.is_set: self.set_attr(P='var') @@ -269,27 +263,13 @@ def preprocess(self, nw): self.label + ' as custom variable of the system.') logger.info(msg) - for fluid in ['o2', 'h2o', 'h2']: - try: - setattr( - self, fluid, [x for x in nw.fluids if x in [ - a.replace(' ', '') for a in - CP.get_aliases(fluid.upper()) - ]][0]) - except IndexError: - msg = ( - 'The component ' + self.label + ' (class ' + - self.__class__.__name__ + ') requires that the fluid ' - '[fluid] is in the network\'s list of fluids.') - aliases = ', '.join(CP.get_aliases(fluid.upper())) - msg = msg.replace( - '[fluid]', fluid.upper() + ' (aliases: ' + aliases + ')') - logger.error(msg) - raise TESPyComponentError(msg) + self.o2 = "O2" + self.h2 = "H2" + self.h2o = "H2O" self.e0 = self.calc_e0() - super().preprocess(nw) + super().preprocess(num_nw_vars) def calc_e0(self): r""" @@ -297,7 +277,7 @@ def calc_e0(self): Returns ------- - val : float + float Minimum specific energy. .. math:: @@ -312,7 +292,7 @@ def calc_e0(self): hf['H2O'] = -286000 hf['H2'] = 0 hf['O2'] = 0 - M = molar_masses[self.h2] + M = self.outl[2].fluid.wrapper[self.h2]._molar_mass e0 = -(2 * hf['H2O'] - 2 * hf['H2'] - hf['O2']) / (2 * M) return e0 @@ -330,9 +310,7 @@ def gas_temperature_func(self): 0 = T_\mathrm{out,2} - T_\mathrm{out,3} """ - return ( - T_mix_ph(self.outl[1].get_flow()) - - T_mix_ph(self.outl[2].get_flow())) + return self.outl[1].calc_T() - self.outl[2].calc_T() def gas_temperature_func_doc(self, label): r""" @@ -364,16 +342,18 @@ def gas_temperature_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ # derivatives for outlet 1 - if not increment_filter[3, 1]: - self.jacobian[k, 3, 1] = dT_mix_dph(self.outl[1].get_flow()) - if not increment_filter[3, 2]: - self.jacobian[k, 3, 2] = dT_mix_pdh(self.outl[1].get_flow()) + o = self.outl[1] + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = dT_mix_dph(o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = dT_mix_pdh(o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) # derivatives for outlet 2 - if not increment_filter[4, 1]: - self.jacobian[k, 4, 1] = -dT_mix_dph(self.outl[2].get_flow()) - if not increment_filter[4, 2]: - self.jacobian[k, 4, 2] = -dT_mix_pdh(self.outl[2].get_flow()) + o = self.outl[2] + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = -dT_mix_dph(o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = -dT_mix_pdh(o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) def eta_func(self): r""" @@ -419,10 +399,11 @@ def eta_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 4, 0] = -self.e0 + if self.outl[2].m.is_var: + self.jacobian[k, self.outl[2].m.J_col] = -self.e0 # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = self.eta.val + self.jacobian[k, self.P.J_col] = self.eta.val def heat_func(self): r""" @@ -471,9 +452,14 @@ def heat_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 0, 0] = self.outl[0].h.val_SI - self.inl[0].h.val_SI - self.jacobian[k, 0, 2] = -self.inl[0].m.val_SI - self.jacobian[k, 2, 2] = self.inl[0].m.val_SI + i = self.inl[0] + o = self.outl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = o.h.val_SI - i.h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = -i.m.val_SI + if o.h.is_var: + self.jacobian[k, o.h.J_col] = i.m.val_SI def specific_energy_consumption_func(self): r""" @@ -519,13 +505,14 @@ def specific_energy_consumption_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 4, 0] = -self.e.val + if self.outl[2].m.is_var: + self.jacobian[k, self.outl[2].m.J_col] = -self.e.val # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 + self.jacobian[k, self.P.J_col] = 1 # derivatives for variable e if self.e.is_var: - self.jacobian[k, 5 + self.e.var_pos, 0] = -self.outl[2].m.val_SI + self.jacobian[k, self.e.J_col] = -self.outl[2].m.val_SI def energy_balance_func(self): r""" @@ -599,32 +586,46 @@ def energy_balance_deriv(self, increment_filter, k): # derivatives determined from calc_P function T_ref = 298.15 p_ref = 1e5 - h_refh2o = h_mix_pT([1, p_ref, 0, self.inl[1].fluid.val], T_ref) - h_refh2 = h_mix_pT([1, p_ref, 0, self.outl[2].fluid.val], T_ref) - h_refo2 = h_mix_pT([1, p_ref, 0, self.outl[1].fluid.val], T_ref) + h_refh2o = h_mix_pT(p_ref, T_ref, self.inl[1].fluid_data, self.inl[1].mixing_rule) + h_refo2 = h_mix_pT(p_ref, T_ref, self.outl[1].fluid_data, self.outl[1].mixing_rule) + h_refh2 = h_mix_pT(p_ref, T_ref, self.outl[2].fluid_data, self.outl[2].mixing_rule) # derivatives cooling water inlet - self.jacobian[k, 0, 0] = self.inl[0].h.val_SI - self.outl[0].h.val_SI - self.jacobian[k, 0, 2] = self.inl[0].m.val_SI + i = self.inl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = i.h.val_SI - self.outl[0].h.val_SI + if i.h.is_var: + self.jacobian[k, i.h.J_col] = i.m.val_SI # derivatives feed water inlet - self.jacobian[k, 1, 0] = (self.inl[1].h.val_SI - h_refh2o) - self.jacobian[k, 1, 2] = self.inl[1].m.val_SI + i = self.inl[1] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = i.h.val_SI - h_refh2o + if i.h.is_var: + self.jacobian[k, i.h.J_col] = i.m.val_SI # derivative cooling water outlet - self.jacobian[k, 2, 2] = -self.inl[0].m.val_SI + o = self.outl[0] + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -self.inl[0].m.val_SI # derivatives oxygen outlet - self.jacobian[k, 3, 0] = -(self.outl[1].h.val_SI - h_refo2) - self.jacobian[k, 3, 2] = -self.outl[1].m.val_SI + o = self.outl[1] + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -(o.h.val_SI - h_refo2) + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -o.m.val_SI # derivatives hydrogen outlet - self.jacobian[k, 4, 0] = -(self.outl[2].h.val_SI - h_refh2 + self.e0) - self.jacobian[k, 4, 2] = -self.outl[2].m.val_SI + o = self.outl[2] + if o.m.is_var: + self.jacobian[k, o.m.J_col] = -(o.h.val_SI - h_refh2 + self.e0) + if o.h.is_var: + self.jacobian[k, o.h.J_col] = -o.m.val_SI # derivatives for variable P if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 + self.jacobian[k, self.P.J_col] = 1 def eta_char_func(self): r""" @@ -691,142 +692,13 @@ def eta_char_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ - self.jacobian[k, 4, 0] = self.numeric_deriv(self.eta_char_func, 'm', 4) - # derivatives for variable P - if self.P.is_var: - self.jacobian[k, 5 + self.P.var_pos, 0] = 1 - - def fluid_func(self): - r""" - Equations for fluid composition. + o = self.outl[2] + if o.m.is_var: + f = self.eta_char_func + self.jacobian[k, o.m.J_col] = self.numeric_deriv(f, 'm', o) - Returns - ------- - residual : list - Residual values of equation. - - .. math:: - - 0 = x_\mathrm{i,in,1} - x_\mathrm{i,out,1} - \forall i \in \text{network fluids} - - 0 = \begin{cases} - 1 - x_\mathrm{i,in2} & \text{i=}H_{2}O\\ - x_\mathrm{i,in2} & \text{else} - \end{cases} \forall i \in \text{network fluids} - - 0 = \begin{cases} - 1 - x_\mathrm{i,out,2} & \text{i=}O_{2}\\ - x_\mathrm{i,out,2} & \text{else} - \end{cases} \forall i \in \text{network fluids} - - 0 = \begin{cases} - 1 - x_\mathrm{i,out,3} & \text{i=}H_{2}\\ - x_\mathrm{i,out,3} & \text{else} - \end{cases} \forall i \in \text{network fluids} - """ - residual = [] - # equations for fluid composition in cooling water - for fluid, x in self.inl[0].fluid.val.items(): - residual += [x - self.outl[0].fluid.val[fluid]] - - # equations to constrain fluids to inlets/outlets - residual += [1 - self.inl[1].fluid.val[self.h2o]] - residual += [1 - self.outl[1].fluid.val[self.o2]] - residual += [1 - self.outl[2].fluid.val[self.h2]] - - # equations to ban other fluids off inlets/outlets - for fluid in self.inl[1].fluid.val.keys(): - if fluid != self.h2o: - residual += [0 - self.inl[1].fluid.val[fluid]] - if fluid != self.o2: - residual += [0 - self.outl[1].fluid.val[fluid]] - if fluid != self.h2: - residual += [0 - self.outl[2].fluid.val[fluid]] - - return residual - - def fluid_func_doc(self, label): - r""" - Equations for fluid composition. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'\begin{split}' + '\n' - r'0 = &x_\mathrm{i,in,1} - x_\mathrm{i,out,1}\\' + '\n' - r'0 = &\begin{cases}' + '\n' - r'1 - x_\mathrm{i,in2} & \text{i=}H_{2}O\\' + '\n' - r'x_\mathrm{i,in2} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'0 =&\begin{cases}' + '\n' - r'1 - x_\mathrm{i,out,2} & \text{i=}O_{2}\\' + '\n' - r'x_\mathrm{i,out,2} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'0 =&\begin{cases}' + '\n' - r'1 - x_\mathrm{i,out,3} & \text{i=}H_{2}\\' + '\n' - r'x_\mathrm{i,out,3} & \text{else}\\' + '\n' - r'\end{cases}\\' + '\n' - r'&\forall i \in \text{network fluids}' + '\n' - r'\end{split}') - return generate_latex_eq(self, latex, label) - - def fluid_deriv(self): - r""" - Calculate the partial derivatives for cooling loop fluid balance. - - Returns - ------- - deriv : ndarray - Matrix with partial derivatives for the fluid equations. - """ - # derivatives for cooling liquid composition - deriv = np.zeros(( - self.num_nw_fluids * 4, - 5 + self.num_vars, - self.num_nw_vars)) - - k = 0 - for fluid, x in self.inl[0].fluid.val.items(): - deriv[k, 0, 3 + k] = 1 - deriv[k, 2, 3 + k] = -1 - k += 1 - - # derivatives to constrain fluids to inlets/outlets - i = 0 - for fluid in self.nw_fluids: - if fluid == self.h2o: - deriv[k, 1, 3 + i] = -1 - elif fluid == self.o2: - deriv[k + 1, 3, 3 + i] = -1 - elif fluid == self.h2: - deriv[k + 2, 4, 3 + i] = -1 - i += 1 - k += 3 - - # derivatives to ban fluids off inlets/outlets - i = 0 - for fluid in self.nw_fluids: - if fluid != self.h2o: - deriv[k, 1, 3 + i] = -1 - k += 1 - if fluid != self.o2: - deriv[k, 3, 3 + i] = -1 - k += 1 - if fluid != self.h2: - deriv[k, 4, 3 + i] = -1 - k += 1 - i += 1 - - return deriv + if self.P.is_var: + self.jacobian[k, self.P.J_col] = 1 def mass_flow_func(self): r""" @@ -847,14 +719,16 @@ def mass_flow_func(self): \dot{m}_\mathrm{H_2,out,3} """ # calculate the ratio of o2 in water - o2 = molar_masses[self.o2] / ( - molar_masses[self.o2] + 2 * molar_masses[self.h2]) - # equation for mass flow balance cooling water - residual = [] - residual += [self.inl[0].m.val_SI - self.outl[0].m.val_SI] + M_o2 = self.outl[1].fluid.wrapper[self.o2]._molar_mass + M_h2 = self.outl[2].fluid.wrapper[self.h2]._molar_mass + + o2 = M_o2 / (M_o2 + 2 * M_h2) # equations for mass flow balance electrolyzer - residual += [o2 * self.inl[1].m.val_SI - self.outl[1].m.val_SI] - residual += [(1 - o2) * self.inl[1].m.val_SI - self.outl[2].m.val_SI] + residual = [] + if self.inl[1].m.is_var or self.outl[1].m.is_var: + residual += [o2 * self.inl[1].m.val_SI - self.outl[1].m.val_SI] + if self.inl[1].m.is_var or self.outl[2].m.is_var: + residual += [(1 - o2) * self.inl[1].m.val_SI - self.outl[2].m.val_SI] return residual def mass_flow_func_doc(self, label): @@ -883,7 +757,7 @@ def mass_flow_func_doc(self, label): ) return generate_latex_eq(self, latex, label) - def mass_flow_deriv(self): + def mass_flow_deriv(self, k): r""" Calculate the partial derivatives for all mass flow balance equations. @@ -892,20 +766,24 @@ def mass_flow_deriv(self): deriv : ndarray Matrix with partial derivatives for the mass flow equations. """ - # deritatives for mass flow balance in the heat exchanger - deriv = np.zeros((3, 5 + self.num_vars, self.num_nw_vars)) - deriv[0, 0, 0] = 1 - deriv[0, 2, 0] = -1 - # derivatives for mass flow balance for oxygen output - o2 = molar_masses[self.o2] / ( - molar_masses[self.o2] + 2 * molar_masses[self.h2]) - deriv[1, 1, 0] = o2 - deriv[1, 3, 0] = -1 - # derivatives for mass flow balance for hydrogen output - deriv[2, 1, 0] = (1 - o2) - deriv[2, 4, 0] = -1 + M_o2 = self.outl[1].fluid.wrapper[self.o2]._molar_mass + M_h2 = self.outl[2].fluid.wrapper[self.h2]._molar_mass + + o2 = M_o2 / (M_o2 + 2 * M_h2) + + # number of equations may vary here + if self.inl[1].m.is_var or self.outl[1].m.is_var: + if self.inl[1].m.is_var: + self.jacobian[k, self.inl[1].m.J_col] = o2 + if self.outl[1].m.is_var: + self.jacobian[k, self.outl[1].m.J_col] = -1 + k += 1 - return deriv + # derivatives for mass flow balance for hydrogen output + if self.inl[1].m.is_var: + self.jacobian[k, self.inl[1].m.J_col] = (1 - o2) + if self.outl[2].m.is_var: + self.jacobian[k, self.outl[2].m.J_col] = -1 def reactor_pressure_func(self): r""" @@ -923,7 +801,8 @@ def reactor_pressure_func(self): """ return [ self.inl[1].p.val_SI - self.outl[1].p.val_SI, - self.inl[1].p.val_SI - self.outl[2].p.val_SI] + self.inl[1].p.val_SI - self.outl[2].p.val_SI + ] def reactor_pressure_func_doc(self, label): r""" @@ -946,7 +825,7 @@ def reactor_pressure_func_doc(self, label): r'\end{split}') return generate_latex_eq(self, latex, label) - def reactor_pressure_deriv(self): + def reactor_pressure_deriv(self, k): r""" Calculate the partial derivatives for combustion pressure equations. @@ -955,15 +834,13 @@ def reactor_pressure_deriv(self): deriv : ndarray Matrix with partial derivatives for the pressure equations. """ - deriv = np.zeros((2, 5 + self.num_vars, self.num_nw_vars)) - # derivatives for pressure oxygen outlet - deriv[0, 1, 1] = 1 - deriv[0, 3, 1] = -1 - # derivatives for pressure hydrogen outlet - deriv[1, 1, 1] = 1 - deriv[1, 4, 1] = -1 - - return deriv + i = self.inl[1] + for o in self.outl[1:]: + if i.p.is_var: + self.jacobian[k, i.p.J_col] = 1 + if o.p.is_var: + self.jacobian[k, o.p.J_col] = -1 + k += 1 def calc_P(self): r""" @@ -1003,16 +880,16 @@ def calc_P(self): p_ref = 1e5 # equations to set a reference point for each h2o, h2 and o2 - h_refh2o = h_mix_pT([1, p_ref, 0, self.inl[1].fluid.val], T_ref) - h_refh2 = h_mix_pT([1, p_ref, 0, self.outl[2].fluid.val], T_ref) - h_refo2 = h_mix_pT([1, p_ref, 0, self.outl[1].fluid.val], T_ref) - - val = (-self.inl[1].m.val_SI * (self.inl[1].h.val_SI - h_refh2o) + - self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) + - self.outl[1].m.val_SI * (self.outl[1].h.val_SI - h_refo2) + - self.outl[2].m.val_SI * ( - self.outl[2].h.val_SI - h_refh2 + self.e0)) + h_refh2o = h_mix_pT(p_ref, T_ref, self.inl[1].fluid_data, self.inl[1].mixing_rule) + h_refo2 = h_mix_pT(p_ref, T_ref, self.outl[1].fluid_data, self.outl[1].mixing_rule) + h_refh2 = h_mix_pT(p_ref, T_ref, self.outl[2].fluid_data, self.outl[2].mixing_rule) + + val = ( + -self.inl[1].m.val_SI * (self.inl[1].h.val_SI - h_refh2o) + + self.inl[0].m.val_SI * (self.outl[0].h.val_SI - self.inl[0].h.val_SI) + + self.outl[1].m.val_SI * (self.outl[1].h.val_SI - h_refo2) + + self.outl[2].m.val_SI * (self.outl[2].h.val_SI - h_refh2 + self.e0) + ) return val def bus_func(self, bus): @@ -1050,7 +927,8 @@ def bus_func(self, bus): elif bus['param'] == 'Q': val = -self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) + self.outl[0].h.val_SI - self.inl[0].h.val_SI + ) ###################################################################### # missing/invalid bus parameter @@ -1100,38 +978,49 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 5 + self.num_vars, self.num_nw_vars)) f = self.calc_bus_value b = bus.comps.loc[self] ###################################################################### # derivatives for power on bus if b['param'] == 'P': - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - - deriv[0, 1, 0] = self.numeric_deriv(f, 'm', 1, bus=bus) - deriv[0, 1, 2] = self.numeric_deriv(f, 'h', 1, bus=bus) - - deriv[0, 2, 2] = self.numeric_deriv(f, 'h', 2, bus=bus) + for c in self.inl + self.outl: + if c.m.is_var and c != self.outl[0]: + if c.m.J_col not in bus.jacobian: + bus.jacobian[c.m.J_col] = 0 + bus.jacobian[c.m.J_col] -= self.numeric_deriv(f, 'm', c, bus=bus) - deriv[0, 3, 0] = self.numeric_deriv(f, 'm', 3, bus=bus) - deriv[0, 3, 2] = self.numeric_deriv(f, 'h', 3, bus=bus) + if c.h.is_var: + if c.h.J_col not in bus.jacobian: + bus.jacobian[c.h.J_col] = 0 + bus.jacobian[c.h.J_col] -= self.numeric_deriv(f, 'h', c, bus=bus) - deriv[0, 4, 0] = self.numeric_deriv(f, 'm', 4, bus=bus) - deriv[0, 4, 2] = self.numeric_deriv(f, 'h', 4, bus=bus) # variable power if self.P.is_var: - deriv[0, 5 + self.P.var_pos, 0] = ( - self.numeric_deriv(f, 'P', 5, bus=bus)) + if self.P.J_col not in bus.jacobian: + bus.jacobian[self.P.J_col] = 0 + bus.jacobian[self.P.J_col] -= self.numeric_deriv(f, 'P', None, bus=bus) ###################################################################### # derivatives for heat on bus elif b['param'] == 'Q': - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - deriv[0, 2, 2] = self.numeric_deriv(f, 'h', 2, bus=bus) + i = self.inl[0] + o = self.outl[0] + if i.m.is_var: + if i.m.J_col not in bus.jacobian: + bus.jacobian[i.m.J_col] = 0 + bus.jacobian[i.m.J_col] -= self.numeric_deriv(f, 'm', i, bus=bus) + + if o.h.is_var: + if o.h.J_col not in bus.jacobian: + bus.jacobian[o.h.J_col] = 0 + bus.jacobian[o.h.J_col] -= self.numeric_deriv(f, 'h', o, bus=bus) + + if o.h.is_var: + if o.h.J_col not in bus.jacobian: + bus.jacobian[o.h.J_col] = 0 + bus.jacobian[o.h.J_col] -= self.numeric_deriv(f, 'h', o, bus=bus) ###################################################################### # missing/invalid bus parameter @@ -1144,17 +1033,67 @@ def bus_deriv(self, bus): logger.error(msg) raise ValueError(msg) - return deriv + @staticmethod + def is_branch_source(): + return True + + def start_branch(self): + branches = {} + for outconn in self.outl[1:]: + if outconn == self.outl[1] and "O2" not in outconn.fluid.val: + outconn.fluid.val["O2"] = 1 + if outconn == self.outl[2] and "H2" not in outconn.fluid.val: + outconn.fluid.val["H2"] = 1 + branch = { + "connections": [outconn], + "components": [self, outconn.target], + "subbranches": {} + } + outconn.target.propagate_to_target(branch) + branches.update({outconn.label: branch}) + + return branches + + def start_fluid_wrapper_branch(self): + branches = {} + for outconn in self.outl[1:]: + branch = { + "connections": [outconn], + "components": [self] + } + outconn.target.propagate_wrapper_to_target(branch) + branches.update({outconn.label: branch}) + + return branches + + def propagate_to_target(self, branch): + inconn = branch["connections"][-1] + if inconn == self.inl[0]: + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [outconn.target] + + outconn.target.propagate_to_target(branch) + else: + if "H2O" not in inconn.fluid.val: + inconn.fluid.val["H2O"] = 1 + return + + def propagate_wrapper_to_target(self, branch): + inconn = branch["connections"][-1] + if inconn == self.inl[0]: + conn_idx = self.inl.index(inconn) + outconn = self.outl[conn_idx] + + branch["connections"] += [outconn] + branch["components"] += [self] - def initialise_fluids(self): - """Set values to pure fluid on water inlet and gas outlets.""" - self.outl[1].fluid.val[self.o2] = 1 - self.outl[2].fluid.val[self.h2] = 1 - self.inl[1].fluid.val[self.h2o] = 1 - for c in self.outl[1:]: - c.target.propagate_fluid_to_target(c, c.target) - self.inl[1].source.propagate_fluid_to_source( - self.inl[1], self.inl[1].source) + outconn.target.propagate_wrapper_to_target(branch) + else: + branch["components"] += [self] + return def initialise_source(self, c, key): r""" @@ -1183,9 +1122,8 @@ def initialise_source(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = c.get_flow() T = 50 + 273.15 - return h_mix_pT(flow, T) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def initialise_target(self, c, key): r""" @@ -1214,60 +1152,8 @@ def initialise_target(self, c, key): if key == 'p': return 5e5 elif key == 'h': - flow = c.get_flow() T = 20 + 273.15 - return h_mix_pT(flow, T) - - def propagate_fluid_to_target(self, inconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's target in recursion. - - Parameters - ---------- - inconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and inconn == start: - return - if inconn == self.inl[0]: - outconn = self.outl[0] - - for fluid, x in inconn.fluid.val.items(): - if (outconn.fluid.val_set[fluid] is False and - outconn.good_starting_values is False): - outconn.fluid.val[fluid] = x - - outconn.target.propagate_fluid_to_target(outconn, start) - - def propagate_fluid_to_source(self, outconn, start, entry_point=False): - r""" - Propagate the fluids towards connection's source in recursion. - - Parameters - ---------- - outconn : tespy.connections.connection.Connection - Connection to initialise. - - start : tespy.components.component.Component - This component is the fluid propagation starting point. - The starting component is saved to prevent infinite looping. - """ - if not entry_point and outconn == start: - return - - if outconn == self.outl[0]: - inconn = self.inl[0] - - for fluid, x in outconn.fluid.val.items(): - if (inconn.fluid.val_set[fluid] is False and - inconn.good_starting_values is False): - inconn.fluid.val[fluid] = x - - inconn.source.propagate_fluid_to_source(inconn, start) + return h_mix_pT(c.p.val_SI, T, c.fluid_data, c.mixing_rule) def calc_parameters(self): r"""Postprocessing parameter calculation.""" @@ -1277,11 +1163,12 @@ def calc_parameters(self): self.e.val = self.P.val / self.outl[2].m.val_SI self.eta.val = self.e0 / self.e.val - i = self.inl[0].get_flow() - o = self.outl[0].get_flow() - self.zeta.val = ((i[1] - o[1]) * np.pi ** 2 / ( - 4 * i[0] ** 2 * (self.inl[0].vol.val_SI + self.outl[0].vol.val_SI) - )) + i = self.inl[0] + o = self.outl[0] + self.zeta.val = ( + (i.p.val_SI - o.p.val_SI) * np.pi ** 2 + / (4 * i.m.val_SI ** 2 * (i.vol.val_SI + o.vol.val_SI)) + ) def exergy_balance(self, T0): self.E_P = ( @@ -1292,5 +1179,5 @@ def exergy_balance(self, T0): self.E_F = self.P.val self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P/self.E_F + self.epsilon = self._calc_epsilon() self.E_bus = self.P.val diff --git a/src/tespy/components/subsystem.py b/src/tespy/components/subsystem.py index a8527e100..f17b00cb9 100644 --- a/src/tespy/components/subsystem.py +++ b/src/tespy/components/subsystem.py @@ -15,8 +15,6 @@ class is the base class for custom subsystems. from tespy.tools import logger -# %% - class Subsystem: r""" diff --git a/src/tespy/components/turbomachinery/base.py b/src/tespy/components/turbomachinery/base.py index fa9348a97..35ae7e083 100644 --- a/src/tespy/components/turbomachinery/base.py +++ b/src/tespy/components/turbomachinery/base.py @@ -11,8 +11,6 @@ SPDX-License-Identifier: MIT """ -import numpy as np - from tespy.components.component import Component from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.document_models import generate_latex_eq @@ -82,7 +80,7 @@ class Turbomachine(Component): def component(): return 'turbomachine' - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp( deriv=self.energy_balance_deriv, num_eq=1, @@ -175,7 +173,8 @@ def bus_func(self, bus): \dot{E} = \dot{m}_{in} \cdot \left(h_{out} - h_{in} \right) """ return self.inl[0].m.val_SI * ( - self.outl[0].h.val_SI - self.inl[0].h.val_SI) + self.outl[0].h.val_SI - self.inl[0].h.val_SI + ) def bus_func_doc(self, bus): r""" @@ -209,12 +208,21 @@ def bus_deriv(self, bus): deriv : ndarray Matrix of partial derivatives. """ - deriv = np.zeros((1, 2, self.num_nw_vars)) f = self.calc_bus_value - deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus) - deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus) - deriv[0, 1, 2] = self.numeric_deriv(f, 'h', 1, bus=bus) - return deriv + if self.inl[0].m.is_var: + if self.inl[0].m.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].m.J_col] = 0 + bus.jacobian[self.inl[0].m.J_col] -= self.numeric_deriv(f, 'm', self.inl[0], bus=bus) + + if self.inl[0].h.is_var: + if self.inl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.inl[0].h.J_col] = 0 + bus.jacobian[self.inl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.inl[0], bus=bus) + + if self.outl[0].h.is_var: + if self.outl[0].h.J_col not in bus.jacobian: + bus.jacobian[self.outl[0].h.J_col] = 0 + bus.jacobian[self.outl[0].h.J_col] -= self.numeric_deriv(f, 'h', self.outl[0], bus=bus) def calc_parameters(self): r"""Postprocessing parameter calculation.""" @@ -236,7 +244,8 @@ def entropy_balance(self): \right)\\ """ self.S_irr = self.inl[0].m.val_SI * ( - self.outl[0].s.val_SI - self.inl[0].s.val_SI) + self.outl[0].s.val_SI - self.inl[0].s.val_SI + ) def get_plotting_data(self): """Generate a dictionary containing FluProDia plotting information. diff --git a/src/tespy/components/turbomachinery/compressor.py b/src/tespy/components/turbomachinery/compressor.py index f45b3b3a2..a28d0d344 100644 --- a/src/tespy/components/turbomachinery/compressor.py +++ b/src/tespy/components/turbomachinery/compressor.py @@ -20,7 +20,6 @@ from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.document_models import generate_latex_eq -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import isentropic @@ -118,9 +117,8 @@ class Compressor(Turbomachine): >>> from tespy.connections import Connection >>> from tespy.networks import Network >>> import shutil - >>> fluid_list = ['air'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... h_unit='kJ / kg', v_unit='l / s', iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', v_unit='l / s', + ... iterinfo=False) >>> si = Sink('sink') >>> so = Source('source') >>> comp = Compressor('compressor') @@ -155,7 +153,7 @@ class Compressor(Turbomachine): def component(): return 'compressor' - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp( min_val=0, num_eq=1, @@ -203,11 +201,20 @@ def eta_s_func(self): 0 = -\left( h_{out} - h_{in} \right) \cdot \eta_{s} + \left( h_{out,s} - h_{in} \right) """ + i = self.inl[0] + o = self.outl[0] return ( - -(self.outl[0].h.val_SI - self.inl[0].h.val_SI) * - self.eta_s.val + (isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - self.inl[0].h.val_SI)) + (o.h.val_SI - i.h.val_SI) * self.eta_s.val - ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - self.inl[0].h.val_SI + ) + ) def eta_s_func_doc(self, label): r""" @@ -240,14 +247,17 @@ def eta_s_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ + i = self.inl[0] + o = self.outl[0] f = self.eta_s_func - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - self.jacobian[k, 1, 2] = -self.eta_s.val + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.eta_s.val def eta_s_char_func(self): r""" @@ -274,10 +284,19 @@ def eta_s_char_func(self): i = self.inl[0] o = self.outl[0] return ( - self.eta_s.design * self.eta_s_char.char_func.evaluate(expr) * - (o.h.val_SI - i.h.val_SI) - (isentropic( - i.get_flow(), o.get_flow(), T0=self.inl[0].T.val_SI) - - i.h.val_SI)) + (o.h.val_SI - i.h.val_SI) + * self.eta_s.design * self.eta_s_char.char_func.evaluate(expr) + - ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - i.h.val_SI + ) + ) def eta_s_char_func_doc(self, label): r""" @@ -312,16 +331,18 @@ def eta_s_char_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.eta_s_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) def char_map_pr_func(self): r""" @@ -352,9 +373,8 @@ def char_map_pr_func(self): """ i = self.inl[0] o = self.outl[0] - T = T_mix_ph(i.get_flow(), T0=i.T.val_SI) - x = np.sqrt(i.T.design / T) + x = np.sqrt(i.T.design / i.calc_T(T0=i.T.val_SI)) y = (i.m.val_SI * i.p.design) / (i.m.design * i.p.val_SI * x) yarr, zarr = self.char_map_pr.char_func.evaluate_x(x) @@ -363,7 +383,7 @@ def char_map_pr_func(self): zarr *= (1 - self.igva.val / 100) pr = self.char_map_pr.char_func.evaluate_y(y, yarr, zarr) - return (o.p.val_SI / i.p.val_SI) / self.pr.design - pr + return (o.p.val_SI / i.p.val_SI) - pr * self.pr.design def char_map_pr_func_doc(self, label): r""" @@ -410,21 +430,23 @@ def char_map_pr_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.char_map_pr_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = 1 / i.p.val_SI + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) if self.igva.is_var: - self.jacobian[k, 2 + self.igva.var_pos, 0] = self.numeric_deriv( - f, 'igva', 1) + self.jacobian[k, self.igva.J_col] = self.numeric_deriv( + f, 'igva', None + ) def char_map_eta_s_func(self): r""" @@ -454,9 +476,8 @@ def char_map_eta_s_func(self): """ i = self.inl[0] o = self.outl[0] - T = T_mix_ph(i.get_flow(), T0=i.T.val_SI) - x = np.sqrt(i.T.design / T) + x = np.sqrt(i.T.design / i.calc_T(T0=i.T.val_SI)) y = (i.m.val_SI * i.p.design) / (i.m.design * i.p.val_SI * x) yarr, zarr = self.char_map_eta_s.char_func.evaluate_x(x) @@ -466,9 +487,17 @@ def char_map_eta_s_func(self): eta = self.char_map_eta_s.char_func.evaluate_y(y, yarr, zarr) return ( - (isentropic(i.get_flow(), o.get_flow(), T0=T) - - i.h.val_SI) / (o.h.val_SI - i.h.val_SI) / self.eta_s.design - - eta) + ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=i.T.val_SI + ) - i.h.val_SI) + / (o.h.val_SI - i.h.val_SI) - eta * self.eta_s.design + ) def char_map_eta_s_func_doc(self, label): r""" @@ -515,21 +544,23 @@ def char_map_eta_s_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.char_map_eta_s_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) if self.igva.is_var: - self.jacobian[k, 2 + self.igva.var_pos, 0] = self.numeric_deriv( - f, 'igva', 1) + self.jacobian[k, self.igva.J_col] = self.numeric_deriv( + f, 'igva', None + ) def convergence_check(self): r""" @@ -542,15 +573,15 @@ def convergence_check(self): """ i, o = self.inl, self.outl - if not o[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: + if o[0].p.is_var 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: + if o[0].h.is_var 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 i[0].p.val_set and o[0].p.val_SI < i[0].p.val_SI: + if i[0].p.is_var 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: + if i[0].h.is_var and o[0].h.val_SI < i[0].h.val_SI: i[0].h.val_SI = o[0].h.val_SI * 0.9 @staticmethod @@ -617,11 +648,18 @@ def calc_parameters(self): r"""Postprocessing parameter calculation.""" super().calc_parameters() - self.eta_s.val = ( - (isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - self.inl[0].h.val_SI) / - (self.outl[0].h.val_SI - self.inl[0].h.val_SI)) + i = self.inl[0] + o = self.outl[0] + self.eta_s.val = ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - self.inl[0].h.val_SI + ) / (o.h.val_SI - i.h.val_SI) def check_parameter_bounds(self): r"""Check parameter value limits.""" @@ -693,4 +731,4 @@ def exergy_balance(self, T0): "chemical": 0, "physical": 0, "massless": self.P.val } self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() diff --git a/src/tespy/components/turbomachinery/pump.py b/src/tespy/components/turbomachinery/pump.py index d02d209c8..2fd0568f8 100644 --- a/src/tespy/components/turbomachinery/pump.py +++ b/src/tespy/components/turbomachinery/pump.py @@ -18,7 +18,6 @@ from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic -from tespy.tools.fluid_properties import v_mix_ph class Pump(Turbomachine): @@ -111,9 +110,8 @@ class Pump(Turbomachine): >>> from tespy.tools.characteristics import CharLine >>> import numpy as np >>> import shutil - >>> fluid_list = ['water'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... h_unit='kJ / kg', v_unit='l / s', iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', v_unit='l / s', + ... iterinfo=False) >>> si = Sink('sink') >>> so = Source('source') >>> pu = Pump('pump') @@ -159,7 +157,7 @@ class Pump(Turbomachine): def component(): return 'pump' - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp( min_val=0, num_eq=1, @@ -203,11 +201,20 @@ def eta_s_func(self): 0 = -\left( h_{out} - h_{in} \right) \cdot \eta_{s} + \left( h_{out,s} - h_{in} \right) """ + i = self.inl[0] + o = self.outl[0] return ( - -(self.outl[0].h.val_SI - self.inl[0].h.val_SI) * - self.eta_s.val + (isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - self.inl[0].h.val_SI)) + (o.h.val_SI - i.h.val_SI) * self.eta_s.val - ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - self.inl[0].h.val_SI + ) + ) def eta_s_func_doc(self, label): r""" @@ -240,14 +247,17 @@ def eta_s_deriv(self, increment_filter, k): k : int Position of derivatives in Jacobian matrix (k-th equation). """ + i = self.inl[0] + o = self.outl[0] f = self.eta_s_func - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - self.jacobian[k, 1, 2] = -self.eta_s.val + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.eta_s.val def eta_s_char_func(self): r""" @@ -274,10 +284,19 @@ def eta_s_char_func(self): i = self.inl[0] o = self.outl[0] return ( - (o.h.val_SI - i.h.val_SI) * self.eta_s.design * - self.eta_s_char.char_func.evaluate(expr) - (isentropic( - i.get_flow(), o.get_flow(), T0=self.inl[0].T.val_SI) - - i.h.val_SI)) + (o.h.val_SI - i.h.val_SI) + * self.eta_s.design * self.eta_s_char.char_func.evaluate(expr) + - ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - i.h.val_SI + ) + ) def eta_s_char_func_doc(self, label): r""" @@ -312,16 +331,18 @@ def eta_s_char_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.eta_s_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, 'h', o) def flow_char_func(self): r""" @@ -373,14 +394,16 @@ def flow_char_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.flow_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) def convergence_check(self): r""" @@ -391,27 +414,27 @@ def convergence_check(self): Manipulate enthalpies/pressure at inlet and outlet if not specified by user to match physically feasible constraints. """ - i, o = self.inl, self.outl + i = self.inl[0] + o = self.outl[0] - 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 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 o.p.is_var and o.p.val_SI < i.p.val_SI: + o.p.val_SI = o.p.val_SI * 2 + if i.p.is_var and o.p.val_SI < i.p.val_SI: + i.p.val_SI = o.p.val_SI * 0.5 - 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 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 o.h.is_var and o.h.val_SI < i.h.val_SI: + o.h.val_SI = o.h.val_SI * 1.1 + if i.h.is_var and o.h.val_SI < i.h.val_SI: + i.h.val_SI = o.h.val_SI * 0.9 if self.flow_char.is_set: - expr = i[0].m.val_SI * v_mix_ph(i[0].get_flow(), T0=i[0].T.val_SI) - - if expr > self.flow_char.char_func.x[-1] and not i[0].m.val_set: - i[0].m.val_SI = (self.flow_char.char_func.x[-1] / - v_mix_ph(i[0].get_flow(), T0=i[0].T.val_SI)) - elif expr < self.flow_char.char_func.x[1] and not i[0].m.val_set: - i[0].m.val_SI = (self.flow_char.char_func.x[0] / - v_mix_ph(i[0].get_flow(), T0=i[0].T.val_SI)) + vol = i.calc_vol(T0=i.T.val_SI) + expr = i.m.val_SI * vol + + if expr > self.flow_char.char_func.x[-1] and i.m.is_var: + i.m.val_SI = self.flow_char.char_func.x[-1] / vol + elif expr < self.flow_char.char_func.x[1] and i.m.is_var: + i.m.val_SI = self.flow_char.char_func.x[0] / vol else: pass @@ -479,11 +502,18 @@ def calc_parameters(self): r"""Postprocessing parameter calculation.""" super().calc_parameters() - self.eta_s.val = ( - (isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - self.inl[0].h.val_SI) / - (self.outl[0].h.val_SI - self.inl[0].h.val_SI)) + i = self.inl[0] + o = self.outl[0] + self.eta_s.val = ( + isentropic( + i.p.val_SI, + i.h.val_SI, + o.p.val_SI, + i.fluid_data, + i.mixing_rule, + T0=None + ) - self.inl[0].h.val_SI + ) / (o.h.val_SI - i.h.val_SI) def exergy_balance(self, T0): r""" @@ -542,4 +572,4 @@ def exergy_balance(self, T0): "chemical": 0, "physical": 0, "massless": self.P.val } self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index 5e0e69036..8995e265d 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -20,7 +20,6 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic -from tespy.tools.fluid_properties import v_mix_ph class Turbine(Turbomachine): @@ -110,9 +109,7 @@ class Turbine(Turbomachine): >>> from tespy.networks import Network >>> from tespy.tools import ComponentCharacteristics as dc_cc >>> import shutil - >>> fluid_list = ['water'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... h_unit='kJ / kg', iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> si = Sink('sink') >>> so = Source('source') >>> t = Turbine('turbine') @@ -149,7 +146,7 @@ class Turbine(Turbomachine): def component(): return 'turbine' - def get_variables(self): + def get_parameters(self): return { 'P': dc_cp( max_val=0, num_eq=1, @@ -188,12 +185,22 @@ def eta_s_func(self): 0 = -\left( h_{out} - h_{in} \right) + \left( h_{out,s} - h_{in} \right) \cdot \eta_{s,e} """ + inl = self.inl[0] + outl = self.outl[0] return ( - -(self.outl[0].h.val_SI - self.inl[0].h.val_SI) + ( + -(outl.h.val_SI - inl.h.val_SI) + + ( isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - - self.inl[0].h.val_SI) * self.eta_s.val) + inl.p.val_SI, + inl.h.val_SI, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + - inl.h.val_SI + ) * self.eta_s.val + ) def eta_s_func_doc(self, label): r""" @@ -227,13 +234,16 @@ def eta_s_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.eta_s_func - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - self.jacobian[k, 1, 2] = -1 + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, "p", i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, "p", o) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) + if o.h.is_var and self.it == 0: + self.jacobian[k, o.h.J_col] = -1 def cone_func(self): r""" @@ -255,12 +265,17 @@ def cone_func(self): n = 1 i = self.inl[0] o = self.outl[0] - vol = v_mix_ph(i.get_flow(), T0=self.inl[0].T.val_SI) + vol = i.calc_vol(T0=i.T.val_SI) return ( - - i.m.val_SI + i.m.design * i.p.val_SI / i.p.design * - np.sqrt(i.p.design * i.vol.design / (i.p.val_SI * vol)) * - np.sqrt(abs((1 - (o.p.val_SI / i.p.val_SI) ** ((n + 1) / n)) / - (1 - (self.pr.design) ** ((n + 1) / n))))) + - i.m.val_SI + i.m.design * i.p.val_SI / i.p.design + * np.sqrt(i.p.design * i.vol.design / (i.p.val_SI * vol)) + * np.sqrt( + abs( + (1 - (o.p.val_SI / i.p.val_SI) ** ((n + 1) / n)) + / (1 - (self.pr.design) ** ((n + 1) / n)) + ) + ) + ) def cone_func_doc(self, label): r""" @@ -299,13 +314,16 @@ def cone_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.cone_func - self.jacobian[k, 0, 0] = -1 - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'p', 1) + i = self.inl[0] + o = self.outl[0] + if i.m.is_var: + self.jacobian[k, i.m.J_col] = -1 + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, 'p', i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, 'h', i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, 'p', o) def eta_s_char_func(self): r""" @@ -325,18 +343,29 @@ def eta_s_char_func(self): p = self.eta_s_char.param expr = self.get_char_expr(p) if not expr: - msg = ('Please choose a valid parameter, you want to link the ' - 'isentropic efficiency to at component ' + self.label + '.') + msg = ( + "Please choose a valid parameter, you want to link the " + f"isentropic efficiency to at component {self.label}." + ) logger.error(msg) raise ValueError(msg) - i = self.inl[0] - o = self.outl[0] + inl = self.inl[0] + outl = self.outl[0] return ( - -(o.h.val_SI - i.h.val_SI) + self.eta_s.design * - self.eta_s_char.char_func.evaluate(expr) * (isentropic( - i.get_flow(), o.get_flow(), T0=self.inl[0].T.val_SI) - - i.h.val_SI)) + -(outl.h.val_SI - inl.h.val_SI) + + self.eta_s.design * self.eta_s_char.char_func.evaluate(expr) + * ( + isentropic( + inl.p.val_SI, + inl.h.val_SI, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) - inl.h.val_SI + ) + ) def eta_s_char_func_doc(self, label): r""" @@ -371,16 +400,18 @@ def eta_s_char_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.eta_s_char_func - if not increment_filter[0, 0]: - self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0) - if not increment_filter[0, 1]: - self.jacobian[k, 0, 1] = self.numeric_deriv(f, 'p', 0) - if not increment_filter[0, 2]: - self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0) - if not increment_filter[1, 1]: - self.jacobian[k, 1, 1] = self.numeric_deriv(f, 'p', 1) - if not increment_filter[1, 2]: - self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1) + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.m, increment_filter): + self.jacobian[k, i.m.J_col] = self.numeric_deriv(f, 'm', i) + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, "p", i) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, "p", o) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) def convergence_check(self): r""" @@ -394,19 +425,19 @@ def convergence_check(self): i, o = self.inl[0], self.outl[0] if not i.good_starting_values: - if i.p.val_SI <= 1e5 and not i.p.val_set: + if i.p.val_SI <= 1e5 and i.p.is_var: i.p.val_SI = 1e5 - if i.h.val_SI < 10e5 and not i.h.val_set: + if i.h.val_SI < 10e5 and i.h.is_var: i.h.val_SI = 10e5 - if o.h.val_SI < 5e5 and not o.h.val_set: + if o.h.val_SI < 5e5 and o.h.is_var: o.h.val_SI = 5e5 - if i.h.val_SI <= o.h.val_SI and not o.h.val_set: + if i.h.val_SI <= o.h.val_SI and o.h.is_var: o.h.val_SI = i.h.val_SI * 0.9 - if i.p.val_SI <= o.p.val_SI and not o.p.val_set: + if i.p.val_SI <= o.p.val_SI and o.p.is_var: o.p.val_SI = i.p.val_SI * 0.9 @staticmethod @@ -473,11 +504,22 @@ def calc_parameters(self): r"""Postprocessing parameter calculation.""" super().calc_parameters() + inl = self.inl[0] + outl = self.outl[0] self.eta_s.val = ( - (self.outl[0].h.val_SI - self.inl[0].h.val_SI) / ( + (outl.h.val_SI - inl.h.val_SI) + / ( isentropic( - self.inl[0].get_flow(), self.outl[0].get_flow(), - T0=self.inl[0].T.val_SI) - self.inl[0].h.val_SI)) + inl.p.val_SI, + inl.h.val_SI, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + - inl.h.val_SI + ) + ) def exergy_balance(self, T0): r""" @@ -534,4 +576,4 @@ def exergy_balance(self, T0): self.E_bus = {"chemical": 0, "physical": 0, "massless": -self.P.val} self.E_D = self.E_F - self.E_P - self.epsilon = self.E_P / self.E_F + self.epsilon = self._calc_epsilon() diff --git a/src/tespy/connections/bus.py b/src/tespy/connections/bus.py index 666b9a8ee..2a7573c96 100644 --- a/src/tespy/connections/bus.py +++ b/src/tespy/connections/bus.py @@ -54,9 +54,7 @@ class Bus: >>> from tespy.tools import CharLine >>> import numpy as np >>> import shutil - >>> fluid_list = ['Ar', 'N2', 'O2', 'CO2', 'CH4', 'H2O'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... p_range=[0.5, 10], iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', p_range=[0.5, 10], iterinfo=False) >>> amb = Source('ambient') >>> sf = Source('fuel') >>> fg = Sink('flue gas outlet') @@ -88,20 +86,19 @@ class Bus: ... offdesign=['zeta1', 'zeta2', 'kA_char']) >>> pu.set_attr(eta_s=0.8, design=['eta_s'], offdesign=['eta_s_char']) >>> amb_comb.set_attr(p=5, T=30, fluid={'Ar': 0.0129, 'N2': 0.7553, - ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314}) - >>> sf_comb.set_attr(T=30, fluid={'CO2': 0, 'Ar': 0, 'N2': 0, - ... 'O2': 0, 'H2O': 0, 'CH4': 1}) - >>> cw_pu.set_attr(p=3, T=60, fluid={'CO2': 0, 'Ar': 0, 'N2': 0, - ... 'O2': 0, 'H2O': 1, 'CH4': 0}) + ... 'CO2': 0.0004, 'O2': 0.2314}) + >>> sf_comb.set_attr(T=30, fluid={'CH4': 1}) + >>> cw_pu.set_attr(p=3, T=60, fluid={'H2O': 1}, m=100) >>> sp_chp2.set_attr(m=Ref(sp_chp1, 1, 0)) Cooling water mass flow is calculated given the feed water temperature (90 °C). The pressure at the cooling water outlet should be identical to - pressure before pump. The flue gases of the combustion engine leave the - flue gas cooler at 120 °C. + pressure before pump. The flue gases of the combustion engine should leave + the flue gas cooler at 120 °C. For a good start we specify the cooling water + mass flow in the first simulation run. Then we will solve a second time and + swtich the specification to the temperature value. >>> fgc_cw.set_attr(p=Ref(cw_pu, 1, 0), T=90) - >>> fgc_fg.set_attr(T=120, design=['T']) Now add the busses, pump and combustion engine generator will get a characteristic function for conversion efficiency. In case of the @@ -143,6 +140,9 @@ class Bus: >>> nw.add_busses(power_bus, heat_bus, fuel_bus) >>> mode = 'design' >>> nw.solve(mode=mode) + >>> cw_pu.set_attr(m=None) + >>> fgc_fg.set_attr(T=120, design=['T']) + >>> nw.solve(mode=mode) >>> nw.save('tmp') The heat bus characteristic for the combustion engine and the flue gas @@ -160,9 +160,9 @@ class Bus: >>> round(chp.Q1.val + chp.Q2.val, 0) -8899014.0 >>> round(fgc_cw.m.val_SI * (fgc_cw.h.val_SI - pu_sp.h.val_SI), 0) - 12477089.0 + 12477091.0 >>> round(heat_bus.P.val, 0) - 12477089.0 + 12477091.0 >>> round(pu.calc_bus_efficiency(power_bus), 2) 0.98 >>> power_bus.set_attr(P=-7.5e6) @@ -176,21 +176,28 @@ class Bus: 0.968 >>> shutil.rmtree('./tmp', ignore_errors=True) """ - def __init__(self, label, **kwargs): + dtypes = { + "param": str, + "P_ref": float, + "char": object, + "efficiency": float, + "base": str, + } self.comps = pd.DataFrame( - columns=['param', 'P_ref', 'char', 'efficiency', 'base'], - dtype='object') + columns=list(dtypes.keys()) + ).astype(dtypes) self.label = label self.P = dc_simple(val=np.nan, is_set=False) self.char = CharLine(x=np.array([0, 3]), y=np.array([1, 1])) self.printout = True + self.jacobian = {} self.set_attr(**kwargs) - msg = 'Created bus ' + self.label + '.' + msg = f"Created bus {self.label}." logger.debug(msg) def set_attr(self, **kwargs): @@ -229,13 +236,13 @@ def set_attr(self, **kwargs): elif kwargs[key] is None: self.P.set_attr(is_set=False) else: - msg = ('Keyword argument ' + key + ' must be numeric.') + msg = f"Keyword argument {key} must be numeric." logger.error(msg) raise TypeError(msg) elif key == 'printout': if not isinstance(kwargs[key], bool): - msg = ('Please provide the ' + key + ' as boolean.') + msg = f"Please provide the {key} as boolean." logger.error(msg) raise TypeError(msg) else: @@ -243,7 +250,7 @@ def set_attr(self, **kwargs): # invalid keyword else: - msg = 'A bus has no attribute ' + key + '.' + msg = f"A bus has no attribute {key}." logger.error(msg) raise KeyError(msg) @@ -264,7 +271,7 @@ def get_attr(self, key): if key in self.__dict__: return self.__dict__[key] else: - msg = 'Bus ' + self.label + ' has no attribute ' + key + '.' + msg = f"Bus {self.label} has no attribute {key}." logger.error(msg) raise KeyError(msg) @@ -328,7 +335,8 @@ def add_comps(self, *args): # default values if isinstance(comp, Component): self.comps.loc[comp] = [ - None, np.nan, self.char, np.nan, 'component'] + None, np.nan, self.char, np.nan, 'component' + ] else: msg = 'Keyword "comp" must hold a TESPy component.' logger.error(msg) @@ -344,8 +352,8 @@ def add_comps(self, *args): self.comps.loc[comp, 'param'] = v else: msg = ( - 'The bus parameter selection must be a ' - 'string (at bus ' + self.label + ').') + "The bus parameter selection must be a string " + f"at bus {self.label}.") logger.error(msg) raise TypeError(msg) @@ -398,7 +406,26 @@ def add_comps(self, *args): logger.error(msg) raise TypeError(msg) - msg = ( - 'Added component ' + comp.label + ' to bus ' + - self.label + '.') + msg = f"Added component {comp.label} to bus {self.label}." logger.debug(msg) + + def serialize(self): + export = {} + export["P"] = self.P.serialize() + for cp in self.comps.index: + export[cp.label] = {} + export[cp.label]["param"] = self.comps.loc[cp, "param"] + export[cp.label]["base"] = self.comps.loc[cp, "base"] + export[cp.label]["char"] = self.comps.loc[cp, "char"].serialize() + + return {self.label: export} + + def solve(self): + self.residual = self.P.val + for cp in self.comps.index: + self.residual -= cp.calc_bus_value(self) + cp.bus_deriv(self) + + def clear_jacobian(self): + for k in self.jacobian: + self.jacobian[k] = 0 diff --git a/src/tespy/connections/connection.py b/src/tespy/connections/connection.py index 8346e361c..ceb64a66f 100644 --- a/src/tespy/connections/connection.py +++ b/src/tespy/connections/connection.py @@ -9,15 +9,41 @@ SPDX-License-Identifier: MIT """ +from collections import OrderedDict + import numpy as np from tespy.components.component import Component from tespy.tools import fluid_properties as fp from tespy.tools import logger +from tespy.tools.data_containers import DataContainer as dc from tespy.tools.data_containers import FluidComposition as dc_flu from tespy.tools.data_containers import FluidProperties as dc_prop +from tespy.tools.data_containers import ReferencedFluidProperties as dc_ref from tespy.tools.data_containers import SimpleDataContainer as dc_simple +from tespy.tools.fluid_properties import CoolPropWrapper +from tespy.tools.fluid_properties import Q_mix_ph +from tespy.tools.fluid_properties import T_mix_ph +from tespy.tools.fluid_properties import T_sat_p +from tespy.tools.fluid_properties import dh_mix_dpQ +from tespy.tools.fluid_properties import dT_mix_dph +from tespy.tools.fluid_properties import dT_mix_pdh +from tespy.tools.fluid_properties import dT_sat_dp +from tespy.tools.fluid_properties import dv_mix_dph +from tespy.tools.fluid_properties import dv_mix_pdh +from tespy.tools.fluid_properties import h_mix_pQ +from tespy.tools.fluid_properties import h_mix_pT +from tespy.tools.fluid_properties import s_mix_ph +from tespy.tools.fluid_properties import v_mix_ph +from tespy.tools.fluid_properties import viscosity_mix_ph +from tespy.tools.fluid_properties.functions import dT_mix_ph_dfluid +from tespy.tools.fluid_properties.functions import p_sat_T +from tespy.tools.fluid_properties.helpers import get_number_of_fluids +from tespy.tools.global_vars import ERR +from tespy.tools.global_vars import fluid_property_data as fpd from tespy.tools.helpers import TESPyConnectionError +from tespy.tools.helpers import TESPyNetworkError +from tespy.tools.helpers import convert_from_SI class Connection: @@ -115,8 +141,6 @@ class Connection: >>> from tespy.components import Sink, Source >>> from tespy.connections import Connection, Ref - >>> from tespy.tools import FluidComposition as dc_flu - >>> from tespy.tools import FluidProperties as dc_prop >>> import numpy as np >>> so1 = Source('source1') >>> so2 = Source('source2') @@ -142,7 +166,7 @@ class Connection: - a string (for attributes design_path and state). - a list (for attributes design and offdesign). - >>> so_si1.set_attr(v=0.012, m0=10, p=5, h=400, fluid={'H2O': 1, 'N2': 0}) + >>> so_si1.set_attr(v=0.012, m0=10, p=5, h=400, fluid={'H2O': 1}) >>> so_si2.set_attr(m=Ref(so_si1, 2, -5), h0=700, T=200, ... fluid={'N2': 1}, fluid_balance=True, ... design=['T'], offdesign=['m', 'v']) @@ -160,33 +184,33 @@ class Connection: >>> so_si1.m.val0 10 - >>> so_si1.m.val_set + >>> so_si1.m.is_set False - >>> so_si1.m.get_attr('val_set') + >>> so_si1.m.get_attr('is_set') False - >>> type(so_si2.m.ref) + >>> type(so_si2.m_ref.ref) - >>> so_si2.fluid.get_attr('balance') + >>> so_si2.fluid_balance.is_set True - >>> so_si2.m.ref.get_attr('delta') + >>> so_si2.m_ref.ref.get_attr('delta') -5 - >>> so_si2.m.ref_set + >>> so_si2.m_ref.is_set True - >>> type(so_si2.m.ref.get_attr('obj')) + >>> type(so_si2.m_ref.ref.get_attr('obj')) Unset the specified temperature and specify temperature difference to boiling point instead. - >>> so_si2.T.val_set + >>> so_si2.T.is_set True - >>> so_si2.set_attr(Td_bp=5, T=np.nan) - >>> so_si2.T.val_set + >>> so_si2.set_attr(Td_bp=5, T=None) + >>> so_si2.T.is_set False >>> so_si2.Td_bp.val 5 >>> so_si2.set_attr(Td_bp=None) - >>> so_si2.Td_bp.val_set + >>> so_si2.Td_bp.is_set False Specify the state keyword: The fluid will be forced to liquid or gaseous @@ -195,7 +219,7 @@ class Connection: >>> so_si2.set_attr(state='l') >>> so_si2.state.is_set True - >>> so_si2.set_attr(state=np.nan) + >>> so_si2.set_attr(state=None) >>> so_si2.state.is_set False >>> so_si2.set_attr(state='g') @@ -206,55 +230,26 @@ class Connection: False """ - def __init__(self, comp1, outlet_id, comp2, inlet_id, + def __init__(self, source, outlet_id, target, inlet_id, label=None, **kwargs): - # check input parameters - if not (isinstance(comp1, Component) and - isinstance(comp2, Component)): - msg = ('Error creating connection. Check if comp1, comp2 are of ' - 'type component.') - logger.error(msg) - raise TypeError(msg) - - if comp1 == comp2: - msg = ('Error creating connection. Cannot connect component ' + - comp1.label + ' to itself.') - logger.error(msg) - raise TESPyConnectionError(msg) - - if outlet_id not in comp1.outlets(): - msg = ('Error creating connection. Specified oulet_id (' + - outlet_id + ') is not valid for component ' + - comp1.component() + '. Valid ids are: ' + - str(comp1.outlets()) + '.') - logger.error(msg) - raise ValueError(msg) - - if inlet_id not in comp2.inlets(): - msg = ( - 'Error creating connection. Specified inlet_id (' + inlet_id + - ') is not valid for component ' + comp2.component() + - '. Valid ids are: ' + str(comp2.inlets()) + '.') - logger.error(msg) - raise ValueError(msg) + self._check_types(source, target) + self._check_self_connect(source, target) + self._check_connector_id(source, outlet_id, source.outlets()) + self._check_connector_id(target, inlet_id, target.inlets()) - if label is None: - self.label = ( - comp1.label + ':' + outlet_id + '_' + - comp2.label + ':' + inlet_id) - else: + self.label = f"{source.label}:{outlet_id}_{target.label}:{inlet_id}" + if label is not None: self.label = label - - if not isinstance(self.label, str): - msg = 'Please provide the label as string.' - logger.error(msg) - raise TypeError(msg) + if not isinstance(label, str): + msg = "Please provide the label as string." + logger.error(msg) + raise TypeError(msg) # set specified values - self.source = comp1 + self.source = source self.source_id = outlet_id - self.target = comp2 + self.target = target self.target_id = inlet_id # defaults @@ -267,16 +262,54 @@ def __init__(self, comp1, outlet_id, comp2, inlet_id, self.printout = True # set default values for kwargs - self.variables = self.attr() - self.variables0 = [x + '0' for x in self.variables.keys()] - self.__dict__.update(self.variables) - self.set_attr(**kwargs) - + self.property_data = self.get_parameters() + self.parameters = { + k: v for k, v in self.get_parameters().items() + if hasattr(v, "func") and v.func is not None + } + self.state = dc_simple() + self.property_data0 = [x + '0' for x in self.property_data.keys()] + self.__dict__.update(self.property_data) + self.mixing_rule = None msg = ( - 'Created connection ' + self.source.label + ' (' + self.source_id + - ') -> ' + self.target.label + ' (' + self.target_id + ').') + f"Created connection from {self.source.label} ({self.source_id}) " + f"to {self.target.label} ({self.target_id})." + ) logger.debug(msg) + self.set_attr(**kwargs) + + def _check_types(self, source, target): + # check input parameters + if not (isinstance(source, Component) and + isinstance(target, Component)): + msg = ( + "Error creating connection. Check if source and target are " + "tespy.components." + ) + logger.error(msg) + raise TypeError(msg) + + def _check_self_connect(self, source, target): + if source == target: + msg = ( + "Error creating connection. Cannot connect component " + f"{source.label} to itself." + ) + logger.error(msg) + raise TESPyConnectionError(msg) + + def _check_connector_id(self, component, connector_id, connecter_locations): + if connector_id not in connecter_locations: + msg = ( + "Error creating connection. Specified connector for " + f"{component.label} ({connector_id} is not available. Choose " + f"from " + ", ".join(connecter_locations) + "." + ) + logger.error(msg) + raise ValueError(msg) + + def set_attr(self, **kwargs): r""" Set, reset or unset attributes of a connection. @@ -302,10 +335,10 @@ def set_attr(self, **kwargs): Starting value specification for enthalpy. fluid : dict - Fluid compostition specification. + Fluid composition specification. fluid0 : dict - Starting value specification for fluid compostition. + Starting value specification for fluid composition. fluid_balance : boolean Fluid balance equation specification. @@ -348,8 +381,8 @@ def set_attr(self, **kwargs): Note ---- - The fluid balance parameter applies a balancing of the fluid vector - on the specified conntion to 100 %. For example, you have four fluid - components (a, b, c and d) in your vector, you set two of them + on the specified connection to 100 %. For example, you have four + fluid components (a, b, c and d) in your vector, you set two of them (a and b) and want the other two (components c and d) to be a result of your calculation. If you set this parameter to True, the equation (0 = 1 - a - b - c - d) will be applied. @@ -366,137 +399,55 @@ def set_attr(self, **kwargs): # set specified values for key in kwargs: if key == 'label': - # bad datatype msg = 'Label can only be specified on instance creation.' logger.error(msg) raise TESPyConnectionError(msg) - elif key in self.variables or key in self.variables0: - # fluid specification - try: - float(kwargs[key]) - is_numeric = True - except (TypeError, ValueError): - is_numeric = False - if 'fluid' in key and key != 'fluid_balance': - if isinstance(kwargs[key], dict): - # starting values - if key in self.variables0: - self.fluid.set_attr(val0=kwargs[key].copy()) - # specified parameters - else: - self.fluid.set_attr(val=kwargs[key].copy()) - self.fluid.set_attr( - val_set={f: True for f in kwargs[key].keys()}) - - else: - # bad datatype - msg = ( - 'Datatype for fluid vector specification must be ' - 'dict.') - logger.error(msg) - raise TypeError(msg) - - elif key == 'state': - if kwargs[key] in ['l', 'g']: - self.state.set_attr(val=kwargs[key], is_set=True) - elif kwargs[key] is None: - self.state.set_attr(is_set=False) - elif is_numeric: - if np.isnan(kwargs[key]): - self.get_attr(key).set_attr(is_set=False) - else: - msg = ( - 'To unset the state specification either use ' - 'np.nan or None.') - logger.error(msg) - raise ValueError(msg) - else: - msg = ( - 'Keyword argument "state" must either be ' - '"l" or "g" or be None or np.nan.') - logger.error(msg) - raise TypeError(msg) + elif 'fluid' in key: + self._fluid_specification(key, kwargs[key]) - elif kwargs[key] is None: - self.get_attr(key).set_attr(val_set=False) - self.get_attr(key).set_attr(ref_set=False) - - elif is_numeric: - if np.isnan(kwargs[key]): - self.get_attr(key).set_attr(val_set=False) - self.get_attr(key).set_attr(ref_set=False) - else: - # value specification - if key in self.variables: - self.get_attr(key).set_attr( - val_set=True, - val=kwargs[key]) - # starting value specification - else: - self.get_attr(key.replace('0', '')).set_attr( - val0=kwargs[key]) - - # reference object - elif isinstance(kwargs[key], Ref): - if key in ['x', 'Td_bp']: - msg = ( - 'References for vapor mass fraction and ' - 'subcooling/superheating are not implemented.' - ) - logger.error(msg) - raise NotImplementedError(msg) - else: - self.get_attr(key).set_attr(ref=kwargs[key]) - self.get_attr(key).set_attr(ref_set=True) - - # invalid datatype for keyword - else: - msg = 'Bad datatype for keyword argument ' + key + '.' - logger.error(msg) - raise TypeError(msg) + elif key in self.property_data or key in self.property_data0: + self._parameter_specification(key, kwargs[key]) - # fluid balance - elif key == 'fluid_balance': - if isinstance(kwargs[key], bool): - self.get_attr('fluid').set_attr(balance=kwargs[key]) + elif key == 'state': + if kwargs[key] in ['l', 'g']: + self.state.set_attr(val=kwargs[key], is_set=True) + elif kwargs[key] is None: + self.state.set_attr(is_set=False) else: msg = ( - 'Datatype for keyword argument fluid_balance must be ' - 'boolean.') + 'Keyword argument "state" must either be ' + '"l" or "g" or be None.' + ) logger.error(msg) raise TypeError(msg) # design/offdesign parameter list - elif key == 'design' or key == 'offdesign': + elif key in ['design', 'offdesign']: if not isinstance(kwargs[key], list): - msg = 'Please provide the ' + key + ' parameters as list!' + msg = f"Please provide the {key} parameters as list!" logger.error(msg) raise TypeError(msg) - elif set(kwargs[key]).issubset(self.variables.keys()): + elif set(kwargs[key]).issubset(self.property_data.keys()): self.__dict__.update({key: kwargs[key]}) else: - params = ', '.join(self.variables.keys()) + params = ', '.join(self.property_data.keys()) msg = ( - 'Available parameters for (off-)design specification ' - 'are: ' + params + '.') + "Available parameters for (off-)design specification " + f"are: {params}." + ) logger.error(msg) raise ValueError(msg) # design path elif key == 'design_path': - if isinstance(kwargs[key], str): + if isinstance(kwargs[key], str) or kwargs[key] is None: self.__dict__.update({key: kwargs[key]}) - elif np.isnan(kwargs[key]): - self.design_path = None + self.new_design = True else: - msg = ( - 'Please provide the design_path parameter as string ' - 'or as nan.') + msg = "Provide the a string or None for 'design_path'." logger.error(msg) raise TypeError(msg) - self.new_design = True - # other boolean keywords elif key in ['printout', 'local_design', 'local_offdesign']: if not isinstance(kwargs[key], bool): @@ -506,12 +457,103 @@ def set_attr(self, **kwargs): else: self.__dict__.update({key: kwargs[key]}) + elif key == "mixing_rule": + self.mixing_rule = kwargs[key] + # invalid keyword else: msg = 'Connection has no attribute ' + key + '.' logger.error(msg) raise KeyError(msg) + def _fluid_specification(self, key, value): + + self._check_fluid_datatypes(key, value) + + if key == "fluid": + for fluid, fraction in value.items(): + if "::" in fluid: + back_end, fluid = fluid.split("::") + else: + back_end = None + + if fraction is None: + if fluid in self.fluid.is_set: + self.fluid.is_set.remove(fluid) + self.fluid.is_var.add(fluid) + else: + self.fluid.val[fluid] = fraction + self.fluid.is_set.add(fluid) + if fluid in self.fluid.is_var: + self.fluid.is_var.remove(fluid) + self.fluid.back_end[fluid] = back_end + + elif key == "fluid0": + self.fluid.val0.update(value) + + elif key == "fluid_engines": + self.fluid.engine = value + + elif key == "fluid_balance": + self.fluid_balance.is_set = value + + else: + msg = f"Connections do not have an attribute named {key}" + logger.error(msg) + raise KeyError(msg) + + def _check_fluid_datatypes(self, key, value): + if key == "fluid_balance": + if not isinstance(value, bool): + msg = "Datatype for 'fluid_balance' must be boolean." + logger.error(msg) + raise TypeError(msg) + else: + if not isinstance(value, dict): + msg = "Datatype for fluid vector specification must be dict." + logger.error(msg) + raise TypeError(msg) + + def _parameter_specification(self, key, value): + try: + float(value) + is_numeric = True + except (TypeError, ValueError): + is_numeric = False + + if value is None: + self.get_attr(key).set_attr(is_set=False) + if f"{key}_ref" in self.property_data: + self.get_attr(key).set_attr(is_set=False) + if key in ["m", "p", "h"]: + self.get_attr(key).is_var = True + + elif is_numeric: + # value specification + if key in self.property_data: + self.get_attr(key).set_attr(is_set=True, val=value) + if key in ["m", "p", "h"]: + self.get_attr(key).is_var = False + # starting value specification + else: + self.get_attr(key.replace('0', '')).set_attr(val0=value) + + # reference object + elif isinstance(value, Ref): + if f"{key}_ref" not in self.property_data: + msg = f"Referencing {key} is not implemented." + logger.error(msg) + raise NotImplementedError(msg) + else: + self.get_attr(f"{key}_ref").set_attr(ref=value) + self.get_attr(f"{key}_ref").set_attr(is_set=True) + + # invalid datatype for keyword + else: + msg = f"Wrong datatype for keyword argument {key}." + logger.error(msg) + raise TypeError(msg) + def get_attr(self, key): r""" Get the value of a connection's attribute. @@ -533,33 +575,490 @@ def get_attr(self, key): logger.error(msg) raise KeyError(msg) + def serialize(self): + export = {} + export.update({"source": self.source.label}) + export.update({"target": self.target.label}) + for k in self._serializable(): + export.update({k: self.get_attr(k)}) + for k in self.property_data: + data = self.get_attr(k) + export.update({k: data.serialize()}) + + export.update({"state": self.state.serialize()}) + + return {self.label: export} + @staticmethod - def attr(): + def _serializable(): + return [ + "source_id", "target_id", + "design_path", "design", "offdesign", "local_design", "local_design", + "printout", "mixing_rule" + ] + + def _create_fluid_wrapper(self): + for fluid in self.fluid.val: + if fluid in self.fluid.wrapper: + continue + if fluid not in self.fluid.engine: + self.fluid.engine[fluid] = CoolPropWrapper + + back_end = None + if fluid in self.fluid.back_end: + back_end = self.fluid.back_end[fluid] + else: + self.fluid.back_end[fluid] = None + + self.fluid.wrapper[fluid] = self.fluid.engine[fluid](fluid, back_end) + + def preprocess(self): + self.num_eq = 0 + self.it = 0 + self.equations = {} + + for parameter in self.parameters: + container = self.get_attr(parameter) + if container.is_set and not container._solved: + self.equations[self.num_eq] = parameter + self.num_eq += self.parameters[parameter].num_eq + elif container._solved: + container._solved = False + + self.residual = np.zeros(self.num_eq) + self.jacobian = OrderedDict() + + def simplify_specifications(self): + systemvar_specs = [] + nonsystemvar_specs = [] + for name, container in self.property_data.items(): + if container.is_set: + if name in ["m", "p", "h"]: + systemvar_specs += [name] + elif name in ["T", "x", "Td_bp", "v"]: + nonsystemvar_specs += [name] + + specs = set(systemvar_specs + nonsystemvar_specs) + num_specs = len(specs) + + if num_specs > 3: + inputs = ", ".join(specs) + msg = ( + "You have specified more than 3 parameters for the connection " + f"{self.label} with a known fluid compoistion: {inputs}. This " + "overdetermines the state of the fluid." + ) + raise TESPyNetworkError(msg) + + if not self.h.is_set and self.p.is_set: + if self.T.is_set: + self.h.val_SI = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule) + self.h._solved = True + self.T._solved = True + elif self.Td_bp.is_set: + T_sat = T_sat_p(self.p.val_SI, self.fluid_data) + self.h.val_SI = h_mix_pT(self.p.val_SI, T_sat + self.Td_bp.val, self.fluid_data) + self.h._solved = True + self.Td_bp._solved = True + elif self.x.is_set: + self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.h._solved = True + self.x._solved = True + + elif not self.h.is_set and not self.p.is_set: + if self.T.is_set and self.x.is_set: + self.p.val_SI = p_sat_T(self.T.val_SI, self.fluid_data) + self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.T._solved = True + self.x._solved = True + self.p._solved = True + self.h._solved = True + + def get_parameters(self): + return { + "m": dc_prop(is_var=True), + "p": dc_prop(is_var=True), + "h": dc_prop(is_var=True), + "vol": dc_prop(), + "s": dc_prop(), + "fluid": dc_flu(), + "fluid_balance": dc_simple( + func=self.fluid_balance_func, deriv=self.fluid_balance_deriv, + val=False, num_eq=1 + ), + "T": dc_prop(func=self.T_func, deriv=self.T_deriv, num_eq=1), + "v": dc_prop(func=self.v_func, deriv=self.v_deriv, num_eq=1), + "x": dc_prop(func=self.x_func, deriv=self.x_deriv, num_eq=1), + "Td_bp": dc_prop( + func=self.Td_bp_func, deriv=self.Td_bp_deriv, num_eq=1 + ), + "m_ref": dc_ref( + func=self.primary_ref_func, deriv=self.primary_ref_deriv, + num_eq=1, func_params={"variable": "m"} + ), + "p_ref": dc_ref( + func=self.primary_ref_func, deriv=self.primary_ref_deriv, + num_eq=1, func_params={"variable": "p"} + ), + "h_ref": dc_ref( + func=self.primary_ref_func, deriv=self.primary_ref_deriv, + num_eq=1, func_params={"variable": "h"} + ), + "T_ref": dc_ref( + func=self.T_ref_func, deriv=self.T_ref_deriv, num_eq=1 + ), + "v_ref": dc_ref( + func=self.v_ref_func, deriv=self.v_ref_deriv, num_eq=1 + ), + } + + def build_fluid_data(self): + self.fluid_data = { + fluid: { + "wrapper": self.fluid.wrapper[fluid], + "mass_fraction": self.fluid.val[fluid] + } for fluid in self.fluid.val + } + + def primary_ref_func(self, k, **kwargs): + variable = kwargs["variable"] + self.get_attr(variable) + ref = self.get_attr(f"{variable}_ref").ref + self.residual[k] = ( + self.get_attr(variable).val_SI + - ref.obj.get_attr(variable).val_SI * ref.factor + ref.delta_SI + ) + + def primary_ref_deriv(self, k, **kwargs): + variable = kwargs["variable"] + ref = self.get_attr(f"{variable}_ref").ref + if self.get_attr(variable).is_var: + self.jacobian[k, self.get_attr(variable).J_col] = 1 + + if ref.obj.get_attr(variable).is_var: + self.jacobian[k, ref.obj.get_attr(variable).J_col] = -ref.factor + + def calc_T(self, T0=None): + return T_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0) + + def T_func(self, k, **kwargs): + self.residual[k] = self.calc_T() - self.T.val_SI + + def T_deriv(self, k, **kwargs): + if self.p.is_var: + self.jacobian[k, self.p.J_col] = ( + dT_mix_dph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI) + ) + if self.h.is_var: + self.jacobian[k, self.h.J_col] = ( + dT_mix_pdh(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI) + ) + for fluid in self.fluid.is_var: + self.jacobian[k, self.fluid.J_col[fluid]] = dT_mix_ph_dfluid( + self.p.val_SI, self.h.val_SI, fluid, self.fluid_data, self.mixing_rule + ) + + def T_ref_func(self, k, **kwargs): + ref = self.T_ref.ref + self.residual[k] = ( + self.calc_T() - ref.obj.calc_T() * ref.factor + ref.delta_SI + ) + + def T_ref_deriv(self, k, **kwargs): + # first part of sum is identical to direct temperature specification + self.T_deriv(k, **kwargs) + ref = self.T_ref.ref + if ref.obj.p.is_var: + self.jacobian[k, ref.obj.p.J_col] = -( + dT_mix_dph(ref.obj.p.val_SI, ref.obj.h.val_SI, ref.obj.fluid_data, ref.obj.mixing_rule) + ) * ref.factor + if ref.obj.h.is_var: + self.jacobian[k, ref.obj.h.J_col] = -( + dT_mix_pdh(ref.obj.p.val_SI, ref.obj.h.val_SI, ref.obj.fluid_data, ref.obj.mixing_rule) + ) * ref.factor + for fluid in ref.obj.fluid.is_var: + if not self._increment_filter[ref.obj.fluid.J_col[fluid]]: + self.jacobian[k, ref.obj.fluid.J_col[fluid]] = -dT_mix_ph_dfluid( + ref.obj.p.val_SI, ref.obj.h.val_SI, fluid, ref.obj.fluid_data, ref.obj.mixing_rule + ) + + def calc_viscosity(self, T0=None): + try: + return viscosity_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0) + except NotImplementedError: + return np.nan + + + def calc_vol(self, T0=None): + try: + return v_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0) + except NotImplementedError: + return np.nan + + def v_func(self, k, **kwargs): + self.residual[k] = self.calc_vol(T0=self.T.val_SI) * self.m.val_SI - self.v.val_SI + + def v_deriv(self, k, **kwargs): + if self.m.is_var: + self.jacobian[k, self.m.J_col] = self.calc_vol(T0=self.T.val_SI) + if self.p.is_var: + self.jacobian[k, self.p.J_col] = dv_mix_dph(self.p.val_SI, self.h.val_SI, self.fluid_data) * self.m.val_SI + if self.h.is_var: + self.jacobian[k, self.h.J_col] = dv_mix_pdh(self.p.val_SI, self.h.val_SI, self.fluid_data) * self.m.val_SI + + def v_ref_func(self, k, **kwargs): + ref = self.v_ref.ref + self.residual[k] = ( + self.calc_vol(T0=self.T.val_SI) * self.m.val_SI + - ref.obj.calc_vol(T0=ref.obj.T.val_SI) * ref.obj.m.val_SI * ref.factor + ref.delta_SI + ) + + def v_ref_deriv(self, k, **kwargs): + # first part of sum is identical to direct volumetric flow specification + self.v_deriv(k, **kwargs) + + ref = self.v_ref.ref + if ref.obj.m.is_var: + self.jacobian[k, ref.obj.m.J_col] = -( + ref.obj.calc_vol(T0=ref.obj.T.val_SI) * ref.factor + ) + if ref.obj.p.is_var: + self.jacobian[k, ref.obj.p.J_col] = -( + dv_mix_dph(ref.obj.p.val_SI, ref.obj.h.val_SI, ref.obj.fluid_data) + * ref.obj.m.val_SI * ref.factor + ) + if ref.obj.h.is_var: + self.jacobian[k, ref.obj.h.J_col] = -( + dv_mix_pdh(ref.obj.p.val_SI, ref.obj.h.val_SI, ref.obj.fluid_data) + * ref.obj.m.val_SI * ref.factor + ) + + def calc_x(self): + try: + return Q_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) + except NotImplementedError: + return np.nan + + def x_func(self, k, **kwargs): + # saturated steam fraction + self.residual[k] = self.h.val_SI - h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + + def x_deriv(self, k, **kwargs): + if self.p.is_var: + self.jacobian[k, self.p.J_col] = -dh_mix_dpQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + if self.h.is_var: + self.jacobian[k, self.h.J_col] = 1 + + def calc_T_sat(self): + try: + return T_sat_p(self.p.val_SI, self.fluid_data) + except NotImplementedError: + return np.nan + + def calc_Td_bp(self): + try: + return self.calc_T() - T_sat_p(self.p.val_SI, self.fluid_data) + except NotImplementedError: + return np.nan + + def Td_bp_func(self, k, **kwargs): + # temperature difference to boiling point + self.residual[k] = self.calc_Td_bp() - self.Td_bp.val_SI + + def Td_bp_deriv(self, k, **kwargs): + if self.p.is_var: + self.jacobian[k, self.p.J_col] = ( + dT_mix_dph(self.p.val_SI, self.h.val_SI, self.fluid_data) + - dT_sat_dp(self.p.val_SI, self.fluid_data) + ) + if self.h.is_var: + self.jacobian[k, self.h.J_col] = dT_mix_pdh( + self.p.val_SI, self.h.val_SI, self.fluid_data + ) + + def fluid_balance_func(self, k, **kwargs): + residual = 1 - sum(self.fluid.val[f] for f in self.fluid.is_set) + residual -= sum(self.fluid.val[f] for f in self.fluid.is_var) + self.residual[k] = residual + + def fluid_balance_deriv(self, k, **kwargs): + for f in self.fluid.is_var: + self.jacobian[k, self.fluid.J_col[f]] = -self.fluid.val[f] + + def calc_s(self): + try: + return s_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=self.T.val_SI) + except NotImplementedError: + return np.nan + + def calc_Q(self): + return Q_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) + + def solve(self, increment_filter): + self._increment_filter = increment_filter + for k, parameter in self.equations.items(): + data = self.get_attr(parameter) + data.func(k, **data.func_params) + data.deriv(k, **data.func_params) + + def calc_results(self): + self.T.val_SI = self.calc_T() + number_fluids = get_number_of_fluids(self.fluid_data) + _converged = True + if number_fluids > 1: + h_from_T = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule) + if abs(h_from_T - self.h.val_SI) > ERR ** .5: + self.T.val_SI = np.nan + self.vol.val_SI = np.nan + self.v.val_SI = np.nan + self.s.val_SI = np.nan + msg = ( + "Could not find a feasible value for mixture temperature at " + f"connection {self.label}. The values for temperature, " + "specific volume, volumetric flow and entropy are set to nan." + ) + logger.error(msg) + _converged = False + + else: + try: + if not self.x.is_set: + self.x.val_SI = self.calc_x() + except ValueError: + self.x.val_SI = np.nan + try: + if not self.Td_bp.is_set: + self.Td_bp.val_SI = self.calc_Td_bp() + except ValueError: + self.x.val_SI = np.nan + + if _converged: + self.vol.val_SI = self.calc_vol() + self.v.val_SI = self.vol.val_SI * self.m.val_SI + self.s.val_SI = self.calc_s() + + for prop in fpd.keys(): + self.get_attr(prop).val = convert_from_SI( + prop, self.get_attr(prop).val_SI, self.get_attr(prop).unit + ) + + self.m.val0 = self.m.val + self.p.val0 = self.p.val + self.h.val0 = self.h.val + self.fluid.val0 = self.fluid.val.copy() + + def check_pressure_bounds(self, fluid): + if self.p.val_SI > self.fluid.wrapper[fluid]._p_max: + self.p.val_SI = self.fluid.wrapper[fluid]._p_max + logger.debug(self._property_range_message('p')) + + elif self.p.val_SI < self.fluid.wrapper[fluid]._p_min: + try: + # if this works, the temperature is higher than the minimum + # temperature, we can access pressure values below minimum + # pressure + self.fluid.wrapper[fluid].T_ph(self.p.val_SI, self.h.val_SI) + except ValueError: + self.p.val_SI = self.fluid.wrapper[fluid]._p_min + 1e1 + logger.debug(self._property_range_message('p')) + + def check_enthalpy_bounds(self, fluid): + # enthalpy + try: + hmin = self.fluid.wrapper[fluid].h_pT( + self.p.val_SI, self.fluid.wrapper[fluid]._T_min + 1e-1 + ) + except ValueError: + f = 1.05 + hmin = self.fluid.wrapper[fluid].h_pT( + self.p.val_SI, self.fluid.wrapper[fluid]._T_min * f + ) + if self.h.val_SI < hmin: + if hmin < 0: + self.h.val_SI = hmin * 0.9999 + else: + self.h.val_SI = hmin * 1.0001 + logger.debug(self._property_range_message('h')) + else: + + T = self.fluid.wrapper[fluid]._T_max + while True: + try: + hmax = self.fluid.wrapper[fluid].h_pT(self.p.val_SI, T) + break + except ValueError as e: + T *= 0.99 + if T < self.fluid.wrapper[fluid]._T_min: + raise ValueError(e) from e + + if self.h.val_SI > hmax: + self.h.val_SI = hmax * 0.9999 + logger.debug(self._property_range_message('h')) + + def check_two_phase_bounds(self, fluid): + + if (self.Td_bp.val_SI > 0 or (self.state.val == 'g' and self.state.is_set)): + h = self.fluid.wrapper[fluid].h_pQ(self.p.val_SI, 1) + if self.h.val_SI < h: + self.h.val_SI = h * 1.01 + logger.debug(self._property_range_message('h')) + elif (self.Td_bp.val_SI < 0 or (self.state.val == 'l' and self.state.is_set)): + h = self.fluid.wrapper[fluid].h_pQ(self.p.val_SI, 0) + if self.h.val_SI > h: + self.h.val_SI = h * 0.99 + logger.debug(self._property_range_message('h')) + + def check_temperature_bounds(self): r""" - Return available attributes of a connection. + Check if temperature is within user specified limits. - Returns - ------- - out : list - List of available attributes of a connection. + Parameters + ---------- + c : tespy.connections.connection.Connection + Connection to check fluid properties. """ - return {'m': dc_prop(), 'p': dc_prop(), 'h': dc_prop(), 'T': dc_prop(), - 'x': dc_prop(), 'v': dc_prop(), 'vol': dc_prop(), - 's': dc_prop(), - 'fluid': dc_flu(), 'Td_bp': dc_prop(), 'state': dc_simple()} - - def get_flow(self): + Tmin = max( + [w._T_min for f, w in self.fluid.wrapper.items() if self.fluid.val[f] > ERR] + ) * 1.01 + Tmax = min( + [w._T_max for f, w in self.fluid.wrapper.items() if self.fluid.val[f] > ERR] + ) * 0.99 + hmin = h_mix_pT(self.p.val_SI, Tmin, self.fluid_data, self.mixing_rule) + hmax = h_mix_pT(self.p.val_SI, Tmax, self.fluid_data, self.mixing_rule) + + if self.h.val_SI < hmin: + self.h.val_SI = hmin + logger.debug(self._property_range_message('h')) + + if self.h.val_SI > hmax: + self.h.val_SI = hmax + logger.debug(self._property_range_message('h')) + + def _property_range_message(self, prop): r""" - Return the SI-values for the network variables. + Return debugging message for fluid property range adjustments. + + Parameters + ---------- + c : tespy.connections.connection.Connection + Connection to check fluid properties. + + prop : str + Fluid property. Returns ------- - out : list - List of mass flow and fluid property information. + msg : str + Debugging message. """ - return [self.m.val_SI, self.p.val_SI, self.h.val_SI, self.fluid.val] + msg = ( + f"{fpd[prop]['text'][0].upper()}{fpd[prop]['text'][1:]} out of " + f"fluid property range at connection {self.label}, adjusting value " + f"to {self.get_attr(prop).val_SI} {fpd[prop]['SI_unit']}." + ) + return msg - def get_physical_exergy(self, p0, T0): + def get_physical_exergy(self, pamb, Tamb): r""" Get the value of a connection's specific physical exergy. @@ -580,14 +1079,17 @@ def get_physical_exergy(self, p0, T0): E^\mathrm{M} = \dot{m} \cdot e^\mathrm{M}\\ E^\mathrm{PH} = \dot{m} \cdot e^\mathrm{PH} """ - self.ex_therm, self.ex_mech = fp.calc_physical_exergy(self, p0, T0) + self.ex_therm, self.ex_mech = fp.functions.calc_physical_exergy( + self.h.val_SI, self.s.val_SI, self.p.val_SI, pamb, Tamb, + self.fluid_data, self.mixing_rule, self.T.val_SI + ) self.Ex_therm = self.ex_therm * self.m.val_SI self.Ex_mech = self.ex_mech * self.m.val_SI self.ex_physical = self.ex_therm + self.ex_mech self.Ex_physical = self.m.val_SI * self.ex_physical - def get_chemical_exergy(self, p0, T0, Chem_Ex): + def get_chemical_exergy(self, pamb, Tamb, Chem_Ex): r""" Get the value of a connection's specific chemical exergy. @@ -611,7 +1113,10 @@ def get_chemical_exergy(self, p0, T0, Chem_Ex): if Chem_Ex is None: self.ex_chemical = 0 else: - self.ex_chemical = fp.calc_chemical_exergy(self, p0, T0, Chem_Ex) + self.ex_chemical = fp.functions.calc_chemical_exergy( + pamb, Tamb, self.fluid_data, Chem_Ex, self.mixing_rule, + self.T.val_SI + ) self.Ex_chemical = self.m.val_SI * self.ex_chemical @@ -662,10 +1167,10 @@ def __init__(self, ref_obj, factor, delta): self.delta = delta self.delta_SI = None - msg = ('Created reference object with factor ' + str(self.factor) + - ' and delta ' + str(self.delta) + ' referring to connection ' + - ref_obj.source.label + ' (' + ref_obj.source_id + ') -> ' + - ref_obj.target.label + ' (' + ref_obj.target_id + ').') + msg = ( + f"Created reference object with factor {self.factor} and delta " + f"{self.delta} referring to connection {ref_obj.label}" + ) logger.debug(msg) def get_attr(self, key): diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 26aaeeabd..405c0b2eb 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -13,7 +13,6 @@ SPDX-License-Identifier: MIT """ -import ast import json import os from collections import OrderedDict @@ -31,10 +30,10 @@ from tespy.tools.data_containers import ComponentCharacteristicMaps as dc_cm from tespy.tools.data_containers import ComponentCharacteristics as dc_cc from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.data_containers import FluidComposition as dc_flu from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp -from tespy.tools.data_containers import SimpleDataContainer as dc_simple -from tespy.tools.global_vars import err +from tespy.tools.global_vars import ERR from tespy.tools.global_vars import fluid_property_data as fpd # Only require cupy if Cuda shall be used @@ -50,13 +49,6 @@ class Network: Parameters ---------- - fluids : list - A list of all fluids within the network container. - - memorise_fluid_properties : boolean - Activate or deactivate fluid property value memorization. Default - state is activated (:code:`True`). - h_range : list List with minimum and maximum values for enthalpy value range. @@ -106,17 +98,16 @@ class Network: Example ------- - Basic example for a setting up a tespy.networks.network.Network object. Specifying - the fluids is mandatory! Unit systems, fluid property range and iterinfo - are optional. + Basic example for a setting up a tespy.networks.network.Network object. + Specifying the fluids is mandatory! Unit systems, fluid property range and + iterinfo are optional. Standard value for iterinfo is :code:`True`. This will print out convergence progress to the console. You can stop the printouts by setting this property to :code:`False`. >>> from tespy.networks import Network - >>> fluid_list = ['water', 'air', 'R134a'] - >>> mynetwork = Network(fluids=fluid_list, p_unit='bar', T_unit='C') + >>> mynetwork = Network(p_unit='bar', T_unit='C') >>> mynetwork.set_attr(p_range=[1, 10]) >>> type(mynetwork) @@ -136,7 +127,7 @@ class Network: >>> from tespy.networks import Network >>> from tespy.components import Source, Sink, Pipe >>> from tespy.connections import Connection, Bus - >>> nw = Network(['CH4'], T_unit='C', p_unit='bar', v_unit='m3 / s') + >>> nw = Network(T_unit='C', p_unit='bar', v_unit='m3 / s') >>> so = Source('source') >>> si = Sink('sink') >>> p = Pipe('pipe', Q=0, pr=0.95, printout=False) @@ -154,30 +145,47 @@ class Network: >>> nw.print_results() """ - def __init__(self, fluids, memorise_fluid_properties=True, **kwargs): - - # fluid list and constants - if isinstance(fluids, list): - self.fluids = sorted(fluids) - else: - msg = ('Please provide a list containing the network\'s fluids on ' - 'creation.') - logger.error(msg) - raise TypeError(msg) - + def __init__(self, fluids=None, **kwargs): self.set_defaults() - self.set_fluid_back_ends(memorise_fluid_properties) self.set_attr(**kwargs) + def serialize(self): + return { + "m_unit": self.m_unit, + "m_range": list(self.m_range), + "p_unit": self.p_unit, + "p_range": list(self.p_range), + "h_unit": self.h_unit, + "h_range": list(self.h_range), + "T_unit": self.T_unit, + "x_unit": self.x_unit, + "v_unit": self.v_unit, + "s_unit": self.s_unit, + } + def set_defaults(self): """Set default network properties.""" # connection dataframe + + dtypes={ + "object": object, + "source": object, + "source_id": str, + "target": object, + "target_id": str + } self.conns = pd.DataFrame( - columns=['object', 'source', 'source_id', 'target', 'target_id'], - dtype='object' - ) + columns=list(dtypes.keys()) + ).astype(dtypes) + self.all_fluids = set() # component dataframe - self.comps = pd.DataFrame(dtype='object') + dtypes = { + "comp_type": str, + "object": object, + } + self.comps = pd.DataFrame( + columns=list(dtypes.keys()) + ).astype(dtypes) # user defined function dictionary for fast access self.user_defined_eq = {} # bus dictionary @@ -186,6 +194,13 @@ def set_defaults(self): self.results = {} self.specifications = {} + self.specifications['lookup'] = { + 'properties': 'prop_specifications', + 'chars': 'char_specifications', + 'variables': 'var_specifications', + 'groups': 'group_specifications' + } + # in case of a design calculation after an offdesign calculation self.redesign = False @@ -216,53 +231,6 @@ def set_defaults(self): 'max: ' + str(limits[1]) + ' ' + self.get_attr(prop + '_unit')) logger.debug(msg) - def set_fluid_back_ends(self, memorise_fluid_properties): - """Set the fluid back ends.""" - # this must be ordered as the fluid property memorisation calls - # the mass fractions of the different fluids as keys in a given order. - self.fluids_backends = OrderedDict() - - msg = 'Network fluids are: ' - i = 0 - for f in self.fluids: - try: - data = f.split('::') - backend = data[0] - fluid = data[1] - except IndexError: - backend = 'HEOS' - fluid = f - - self.fluids_backends[fluid] = backend - self.fluids[i] = fluid - - msg += fluid + ', ' - i += 1 - - msg = msg[:-2] + '.' - logger.debug(msg) - - # initialise fluid property memorisation function for this network - fp.Memorise.add_fluids(self.fluids_backends, memorise_fluid_properties) - - # set up results dataframe for connections - cols = ( - ['m', 'p', 'h', 'T', 'v', 'vol', 's', 'x', 'Td_bp'] - + self.fluids) - self.results['Connection'] = pd.DataFrame( - columns=cols, dtype='float64') - # include column for fluid balance in specs dataframe - self.specifications['Connection'] = pd.DataFrame( - columns=cols + ['balance'], dtype='bool') - self.specifications['Ref'] = pd.DataFrame( - columns=cols, dtype='bool') - self.specifications['lookup'] = { - 'properties': 'prop_specifications', - 'chars': 'char_specifications', - 'variables': 'var_specifications', - 'groups': 'group_specifications' - } - def set_attr(self, **kwargs): r""" Set, resets or unsets attributes of a network. @@ -378,7 +346,7 @@ def get_attr(self, key): if key in self.__dict__: return self.__dict__[key] else: - msg = 'Network has no attribute \"' + str(key) + '\".' + msg = f"Network has no attribute '{key}'." logger.error(msg) raise KeyError(msg) @@ -414,7 +382,7 @@ def get_conn(self, label): try: return self.conns.loc[label, 'object'] except KeyError: - logger.warning('Connection with label %s not found.', label) + logger.warning(f"Connection with label {label} not found.") return None def get_comp(self, label): @@ -435,7 +403,7 @@ def get_comp(self, label): try: return self.comps.loc[label, 'object'] except KeyError: - logger.warning('Component with label %s not found.', label) + logger.warning(f"Component with label {label} not found.") return None def add_conns(self, *args): @@ -465,9 +433,8 @@ def add_conns(self, *args): c.good_starting_values = False self.conns.loc[c.label] = [ - c, c.source, c.source_id, c.target, c.target_id] - - self.results['Connection'].loc[c.label] = np.nan + c, c.source, c.source_id, c.target, c.target_id + ] msg = 'Added connection ' + c.label + ' to network.' logger.debug(msg) @@ -488,7 +455,6 @@ def del_conns(self, *args): comps = list({cp for c in args for cp in [c.source, c.target]}) for c in args: self.conns.drop(c.label, inplace=True) - self.results['Connection'].drop(c.label, inplace=True) msg = ('Deleted connection ' + c.label + ' from network.') logger.debug(msg) @@ -600,7 +566,7 @@ def add_ude(self, *args): ---------- c : tespy.tools.helpers.UserDefinedEquation The objects to be added to the network, UserDefinedEquation objects - ci :code:`del_conns(c1, c2, c3, ...)`. + ci :code:`add_ude(c1, c2, c3, ...)`. """ for c in args: if not isinstance(c, hlp.UserDefinedEquation): @@ -618,7 +584,7 @@ def add_ude(self, *args): raise ValueError(msg) self.user_defined_eq[c.label] = c - msg = 'Added UserDefinedEquation ' + c.label + ' to network.' + msg = f"Added UserDefinedEquation {c.label} to network." logger.debug(msg) def del_ude(self, *args): @@ -628,12 +594,12 @@ def del_ude(self, *args): Parameters ---------- c : tespy.tools.helpers.UserDefinedEquation - The objects to be added deleted from the network, - UserDefinedEquation objects ci :code:`del_conns(c1, c2, c3, ...)`. + The objects to be deleted from the network, + UserDefinedEquation objects ci :code:`del_ude(c1, c2, c3, ...)`. """ for c in args: del self.user_defined_eq[c.label] - msg = 'Deleted UserDefinedEquation ' + c.label + ' from network.' + msg = f"Deleted UserDefinedEquation {c.label} from network." logger.debug(msg) def add_busses(self, *args): @@ -657,7 +623,8 @@ def add_busses(self, *args): 'component value', 'bus value', 'efficiency', 'design value' ], - dtype='float64') + dtype='float64' + ) def del_busses(self, *args): r""" @@ -720,48 +687,73 @@ def check_network(self): logger.error(msg) raise hlp.TESPyNetworkError(msg) - if len(self.fluids) == 0: - msg = ( - 'Network has no fluids, please specify a list with fluids on ' - 'network creation.' - ) - logger.error(msg) - raise hlp.TESPyNetworkError(msg) - self.check_conns() self.init_components() - # count number of incoming and outgoing connections and compare to - # expected values - for comp in self.comps['object']: - num_o = (self.conns[['source', 'target']] == comp).sum().source - num_i = (self.conns[['source', 'target']] == comp).sum().target - - if num_o != comp.num_o: - msg = ( - f"The component {comp.label} is missing " - f"{comp.num_o - num_o} outgoing connections. Make sure " - "all outlets are connected and all connections have been " - "added to the network." - ) - logger.error(msg) - # raise an error in case network check is unsuccesful - raise hlp.TESPyNetworkError(msg) - elif num_i != comp.num_i: - msg = ( - f"The component {comp.label} is missing " - f"{comp.num_i - num_i} incoming connections. Make sure " - "all inlets are connected and all connections have been " - "added to the network." - ) - logger.error(msg) - # raise an error in case network check is unsuccesful - raise hlp.TESPyNetworkError(msg) + self.check_components() + self.create_massflow_and_fluid_branches() + self.create_fluid_wrapper_branches() # network checked self.checked = True msg = 'Networkcheck successful.' logger.info(msg) + def create_massflow_and_fluid_branches(self): + + self.branches = {} + mask = self.comps["object"].apply(lambda c: c.is_branch_source()) + start_components = self.comps["object"].loc[mask] + if len(start_components) == 0: + msg = ( + "You cannot build a system without at least one CycleCloser or " + "a Source and Sink." + ) + raise hlp.TESPyNetworkError(msg) + + for start in start_components: + self.branches.update(start.start_branch()) + + self.massflow_branches = hlp.get_all_subdictionaries(self.branches) + + self.fluid_branches = {} + for branch_name, branch_data in self.branches.items(): + subbranches = hlp.get_all_subdictionaries(branch_data["subbranches"]) + main = {k: v for k, v in branch_data.items() if k != "subbranches"} + self.fluid_branches[branch_name] = [main] + subbranches + + def create_fluid_wrapper_branches(self): + + self.fluid_wrapper_branches = {} + mask = self.comps["comp_type"].isin( + ["Source", "CycleCloser", "WaterElectrolyzer", "FuelCell"] + ) + start_components = self.comps["object"].loc[mask] + + for start in start_components: + self.fluid_wrapper_branches.update(start.start_fluid_wrapper_branch()) + + merged = self.fluid_wrapper_branches.copy() + for branch_name, branch_data in self.fluid_wrapper_branches.items(): + if branch_name not in merged: + continue + for ob_name, ob_data in self.fluid_wrapper_branches.items(): + if ob_name != branch_name: + common_connections = list( + set(branch_data["connections"]) + & set(ob_data["connections"]) + ) + if len(common_connections) > 0: + merged[branch_name]["connections"] = list( + set(branch_data["connections"] + ob_data["connections"]) + ) + merged[branch_name]["components"] = list( + set(branch_data["components"] + ob_data["components"]) + ) + del merged[ob_name] + break + + self.fluid_wrapper_branches = merged + def init_components(self): r"""Set up necessary component information.""" for comp in self.comps["object"]: @@ -777,21 +769,19 @@ def init_components(self): comp.num_i = len(comp.inlets()) comp.num_o = len(comp.outlets()) - # save the connection locations to the components - comp.conn_loc = [] - for c in comp.inl + comp.outl: - comp.conn_loc += [self.conns.index.get_loc(c.label)] - # set up restults and specification dataframes comp_type = comp.__class__.__name__ if comp_type not in self.results: - cols = [col for col, data in comp.variables.items() - if isinstance(data, dc_cp)] + cols = [ + col for col, data in comp.parameters.items() + if isinstance(data, dc_cp) + ] self.results[comp_type] = pd.DataFrame( columns=cols, dtype='float64') if comp_type not in self.specifications: + cols, groups, chars = [], [], [] - for col, data in comp.variables.items(): + for col, data in comp.parameters.items(): if isinstance(data, dc_cp): cols += [col] elif isinstance(data, dc_gcp) or isinstance(data, dc_gcc): @@ -805,6 +795,33 @@ def init_components(self): 'properties': pd.DataFrame(columns=cols, dtype='bool') } + def check_components(self): + # count number of incoming and outgoing connections and compare to + # expected values + for comp in self.comps['object']: + counts = (self.conns[['source', 'target']] == comp).sum() + + if counts["source"] != comp.num_o: + msg = ( + f"The component {comp.label} is missing " + f"{comp.num_o - counts['source']} outgoing connections. " + "Make sure all outlets are connected and all connections " + "have been added to the network." + ) + logger.error(msg) + # raise an error in case network check is unsuccesful + raise hlp.TESPyNetworkError(msg) + elif counts["target"] != comp.num_i: + msg = ( + f"The component {comp.label} is missing " + f"{comp.num_i - counts['target']} incoming connections. " + "Make sure all inlets are connected and all connections " + "have been added to the network." + ) + logger.error(msg) + # raise an error in case network check is unsuccesful + raise hlp.TESPyNetworkError(msg) + def initialise(self): r""" Initilialise the network depending on calclation mode. @@ -825,15 +842,22 @@ def initialise(self): self.num_bus_eq = 0 self.num_comp_eq = 0 self.num_conn_eq = 0 + self.num_vars = 0 self.num_comp_vars = 0 + self.num_conn_vars = 0 + self.variables_dict = {} + + self.propagate_fluid_wrappers() + self.presolve_massflow_topology() + self.presolve_fluid_topology() + self.init_set_properties() if self.mode == 'offdesign': self.redesign = True if self.design_path is None: # must provide design_path - msg = ('Please provide "design_path" for every offdesign ' - 'calculation.') + msg = "Please provide a design_path for offdesign mode." logger.error(msg) raise hlp.TESPyNetworkError(msg) @@ -846,9 +870,6 @@ def initialise(self): else: # reset any preceding offdesign calculation self.init_design() - # generic fluid initialisation - # for offdesign cases good starting values should be available - self.init_fluids() # generic fluid property initialisation self.init_properties() @@ -856,88 +877,324 @@ def initialise(self): msg = 'Network initialised.' logger.info(msg) + def propagate_fluid_wrappers(self): + + for branch_data in self.fluid_wrapper_branches.values(): + all_connections = [c for c in branch_data["connections"]] + + any_fluids_set = [] + engines = {} + back_ends = {} + for c in all_connections: + for f in c.fluid.is_set: + any_fluids_set += [f] + if f in c.fluid.engine: + engines[f] = c.fluid.engine[f] + if f in c.fluid.back_end: + back_ends[f] = c.fluid.back_end[f] + + mixing_rules = [ + c.mixing_rule for c in all_connections + if c.mixing_rule is not None + ] + mixing_rule = set(mixing_rules) + if len(mixing_rule) > 1: + msg = "You have provided more than one mixing rule." + raise hlp.TESPyNetworkError(msg) + elif len(mixing_rule) == 0: + mixing_rule = set(["ideal-cond"]) + + if not any_fluids_set: + msg = "You are missing fluid specifications." + any_fluids = [f for c in all_connections for f in c.fluid.val] + any_fluids0 = [f for c in all_connections for f in c.fluid.val] + + potential_fluids = set(any_fluids_set + any_fluids + any_fluids0) + num_potential_fluids = len(potential_fluids) + if num_potential_fluids == 0: + msg = ( + "The follwing connections of your network are missing any " + "kind of fluid composition information:" + + ", ".join([c.label for c in all_connections]) + "." + ) + raise hlp.TESPyNetworkError(msg) + + for c in all_connections: + c.mixing_rule = list(mixing_rule)[0] + c._potential_fluids = potential_fluids + if num_potential_fluids == 1: + f = list(potential_fluids)[0] + c.fluid.val[f] = 1 + + else: + for f in potential_fluids: + if (f not in c.fluid.is_set and f not in c.fluid.val and f not in c.fluid.val0): + c.fluid.val[f] = 1 / len(potential_fluids) + elif f not in c.fluid.is_set and f not in c.fluid.val and f in c.fluid.val0: + c.fluid.val[f] = c.fluid.val0[f] + + for f, engine in engines.items(): + c.fluid.engine[f] = engine + for f, back_end in back_ends.items(): + c.fluid.back_end[f] = back_end + + c._create_fluid_wrapper() + + def presolve_massflow_topology(self): + + # mass flow is a single variable in each sub branch + # fluid composition is a single variable in each main branch + for branch in self.massflow_branches: + + num_massflow_specs = 0 + for c in branch["connections"]: + # number of specifications cannot exceed 1 + num_massflow_specs += c.m.is_set + + if c.m.is_set: + main_conn = c + + # self reference is not allowed + if c.m_ref.is_set: + if c.m_ref.ref.obj in branch["connections"]: + msg = ( + "You cannot reference a mass flow in the same " + f"linear branch. The connection {c.label} " + "references the connection " + f"{c.m_ref.ref.obj.label}." + ) + raise hlp.TESPyNetworkError(msg) + + if num_massflow_specs == 1: + # set every mass flow in branch to the specified value + for c in branch["connections"]: + # map all connection's mass flow data containers to first + # branch element + c._m_tmp = c.m + c.m = main_conn.m + + msg = ( + "Removing " + f"{len(branch['connections']) - num_massflow_specs} " + "mass flow variables from system variables." + ) + logger.debug(msg) + elif num_massflow_specs > 1: + msg = ( + "You cannot specify two or more values for mass flow in " + "the same linear branch (starting at " + f"{branch['components'][0].label} and ending at " + f"{branch['components'][-1].label})." + ) + raise hlp.TESPyNetworkError(msg) + + else: + main_conn = branch["connections"][0] + for c in branch["connections"][1:]: + # map all connection's mass flow data containers to first + # branch element + c._m_tmp = c.m + c.m = main_conn.m + + def presolve_fluid_topology(self): + + for branch_name, branch in self.fluid_branches.items(): + all_connections = [ + c for b in branch for c in b["connections"] + ] + main_conn = all_connections[0] + fluid_specs = [f for c in all_connections for f in c.fluid.is_set] + if len(fluid_specs) == 0: + main_conn._fluid_tmp = dc_flu() + main_conn._fluid_tmp.val = main_conn.fluid.val.copy() + main_conn._fluid_tmp.is_set = main_conn.fluid.is_set.copy() + main_conn._fluid_tmp.is_var = main_conn.fluid.is_var.copy() + main_conn._fluid_tmp.wrapper = main_conn.fluid.wrapper.copy() + main_conn._fluid_tmp.engine = main_conn.fluid.engine.copy() + main_conn._fluid_tmp.back_end = main_conn.fluid.back_end.copy() + + for c in all_connections[1:]: + c._fluid_tmp = c.fluid + c.fluid = main_conn.fluid + + if len(main_conn._potential_fluids) > 1: + main_conn.fluid.is_var = {f for f in main_conn.fluid.val} + else: + main_conn.fluid.val[list(main_conn._potential_fluids)[0]] = 1 + + elif len(fluid_specs) != len(set(fluid_specs)): + msg = ( + "The mass fraction of a single fluid cannot be specified " + "twice within a branch." + ) + raise hlp.TESPyNetworkError(msg) + else: + fixed_fractions = { + f: c.fluid.val[f] + for c in all_connections + for f in fluid_specs + if f in c.fluid.is_set + } + mass_fraction_sum = sum(fixed_fractions.values()) + if mass_fraction_sum > 1 + ERR: + msg = "Total mass fractions within a branch cannot exceed 1" + raise ValueError(msg) + elif mass_fraction_sum < 1 - ERR: + # set the fluids with specified mass fraction + # remaining fluids are variable, create wrappers for them + all_fluids = main_conn.fluid.val.keys() + num_remaining_fluids = len(all_fluids) - len(fixed_fractions) + if num_remaining_fluids == 1: + missing_fluid = list( + main_conn.fluid.val.keys() - fixed_fractions.keys() + )[0] + fixed_fractions[missing_fluid] = 1 - mass_fraction_sum + variable = set() + else: + missing_fluids = ( + main_conn.fluid.val.keys() - fixed_fractions.keys() + ) + variable = {f for f in missing_fluids} + + else: + # fluid mass fraction is 100 %, all other fluids are 0 % + all_fluids = main_conn.fluid.val.keys() + remaining_fluids = ( + main_conn.fluid.val.keys() - fixed_fractions.keys() + ) + for f in remaining_fluids: + fixed_fractions[f] = 0 + + variable = set() + + main_conn._fluid_tmp = dc_flu() + main_conn._fluid_tmp.val = main_conn.fluid.val.copy() + main_conn._fluid_tmp.is_set = main_conn.fluid.is_set.copy() + main_conn._fluid_tmp.is_var = main_conn.fluid.is_var.copy() + main_conn._fluid_tmp.wrapper = main_conn.fluid.wrapper.copy() + main_conn._fluid_tmp.engine = main_conn.fluid.engine.copy() + main_conn._fluid_tmp.back_end = main_conn.fluid.back_end.copy() + + for c in all_connections[1:]: + c._fluid_tmp = c.fluid + c.fluid = main_conn.fluid + + main_conn.fluid.val.update(fixed_fractions) + main_conn.fluid.is_set = {f for f in fixed_fractions} + main_conn.fluid.is_var = variable + num_var = len(variable) + for f in variable: + main_conn.fluid.val[f]: (1 - mass_fraction_sum) / num_var + + [c.build_fluid_data() for c in all_connections] + for fluid in main_conn.fluid.is_var: + main_conn.fluid.J_col[fluid] = self.num_conn_vars + self.variables_dict[self.num_conn_vars] = { + "obj": main_conn, "variable": "fluid", "fluid": fluid + } + self.num_conn_vars += 1 + + def _reset_topology_reduction_specifications(self): + for c in self.conns["object"]: + if hasattr(c, "_m_tmp"): + value = c.m.val_SI + unit = c.m.unit + c.m = c._m_tmp + c.m.val_SI = value + c.m.unit = unit + del c._m_tmp + if hasattr(c, "_fluid_tmp"): + val = c.fluid.val + c.fluid = c._fluid_tmp + c.fluid.val = val + del c._fluid_tmp + def init_set_properties(self): """Specification of SI values for user set values.""" + self.all_fluids = [] # fluid property values for c in self.conns['object']: - # set all specifications to False - self.specifications['Connection'].loc[c.label] = False + self.all_fluids += c.fluid.val.keys() + if not self.init_previous: c.good_starting_values = False - c.conn_loc = self.conns.index.get_loc(c.label) - - for key in ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp', 'vol', 's']: + for key in c.property_data: # read unit specifications - if key == 'Td_bp': + prop = key.split("_ref")[0] + if "fluid" in key: + continue + elif key == 'Td_bp': c.get_attr(key).unit = self.get_attr('T_unit') else: - c.get_attr(key).unit = self.get_attr(key + '_unit') + c.get_attr(key).unit = self.get_attr(f"{prop}_unit") # set SI value - if c.get_attr(key).val_set: - c.get_attr(key).val_SI = hlp.convert_to_SI( - key, c.get_attr(key).val, c.get_attr(key).unit) - if c.get_attr(key).ref_set: - if key == 'T': - c.get_attr(key).ref.delta_SI = hlp.convert_to_SI( - 'Td_bp', c.get_attr(key).ref.delta, - c.get_attr(key).unit) + if c.get_attr(key).is_set: + if "ref" in key: + if prop == 'T': + c.get_attr(key).ref.delta_SI = hlp.convert_to_SI( + 'Td_bp', c.get_attr(key).ref.delta, + c.get_attr(prop).unit + ) + else: + c.get_attr(key).ref.delta_SI = hlp.convert_to_SI( + prop, c.get_attr(key).ref.delta, + c.get_attr(prop).unit + ) else: - c.get_attr(key).ref.delta_SI = hlp.convert_to_SI( - key, c.get_attr(key).ref.delta, - c.get_attr(key).unit) - - # fluid vector specification - tmp = c.fluid.val - for fluid in tmp.keys(): - if fluid not in self.fluids: - msg = ('Your connection ' + c.label + ' holds a fluid, ' - 'that is not part of the networks\'s fluids (' + - fluid + ').') - raise hlp.TESPyNetworkError(msg) - tmp0 = c.fluid.val0 - tmp_set = c.fluid.val_set - - c.fluid.val = OrderedDict() - c.fluid.val0 = OrderedDict() - c.fluid.val_set = OrderedDict() - - # if the number of fluids is one the mass fraction is 1 for every - # connection - if len(self.fluids) == 1: - c.fluid.val[self.fluids[0]] = 1 - c.fluid.val0[self.fluids[0]] = 1 - if self.fluids[0] in tmp_set: - c.fluid.val_set[self.fluids[0]] = tmp_set[self.fluids[0]] - else: - c.fluid.val_set[self.fluids[0]] = False + c.get_attr(key).val_SI = hlp.convert_to_SI( + key, c.get_attr(key).val, c.get_attr(key).unit + ) - # jump to next connection - continue + if len(self.all_fluids) == 0: + msg = ( + 'Network has no fluids, please specify a list with fluids on ' + 'network creation.' + ) + logger.error(msg) + raise hlp.TESPyNetworkError(msg) - for fluid in self.fluids: - # take over values from temporary dicts - if fluid in tmp and fluid in tmp_set: - c.fluid.val[fluid] = tmp[fluid] - c.fluid.val0[fluid] = tmp[fluid] - c.fluid.val_set[fluid] = tmp_set[fluid] - # take over starting values - elif fluid in tmp0: - if fluid not in tmp_set: - c.fluid.val[fluid] = tmp0[fluid] - c.fluid.val0[fluid] = tmp0[fluid] - c.fluid.val_set[fluid] = False - # if fluid not in keys - else: - c.fluid.val[fluid] = 0 - c.fluid.val0[fluid] = 0 - c.fluid.val_set[fluid] = False + # set up results dataframe for connections + # this should be done based on the connections + properties = list(fpd.keys()) + self.all_fluids = set(self.all_fluids) + cols = ( + [col for prop in properties for col in [prop, f"{prop}_unit"]] + + list(self.all_fluids) + ) + self.results['Connection'] = pd.DataFrame(columns=cols, dtype='float64') + # include column for fluid balance in specs dataframe + self.specifications['Connection'] = pd.DataFrame( + columns=cols + ['balance'], dtype='bool' + ) + cols = ["m_ref", "p_ref", "h_ref", "T_ref", "v_ref"] + self.specifications['Ref'] = pd.DataFrame(columns=cols, dtype='bool') msg = ( - 'Updated fluid property SI values and fluid mass fraction for ' - 'user specified connection parameters.') + "Updated fluid property SI values and fluid mass fraction for user " + "specified connection parameters." + ) logger.debug(msg) + def _assign_variable_space(self, c): + for key in ["m", "p", "h"]: + variable = c.get_attr(key) + if not variable.is_set and variable not in self._conn_variables: + if not variable._solved: + variable.is_var = True + variable.J_col = self.num_conn_vars + self.variables_dict[self.num_conn_vars] = { + "obj": c, "variable": key + } + self._conn_variables += [variable] + self.num_conn_vars += 1 + else: + variable.is_var = False + # reset presolve flag + variable._solved = False + elif variable.is_set: + variable.is_var = False + def init_design(self): r""" Initialise a design calculation. @@ -949,33 +1206,39 @@ def init_design(self): unset, the offdesign values set. """ # connections + self._conn_variables = [] + _local_designs = {} for c in self.conns['object']: # read design point information of connections with # local_offdesign activated from their respective design path if c.local_offdesign: if c.design_path is None: msg = ( - 'The parameter local_offdesign is True for the ' - 'connection ' + c.label + ', an individual ' - 'design_path must be specified in this case!') + "The parameter local_offdesign is True for the " + f"connection {c.label}, an individual design_path must " + "be specified in this case!" + ) logger.error(msg) raise hlp.TESPyNetworkError(msg) # unset design parameters for var in c.design: - c.get_attr(var).val_set = False + c.get_attr(var).is_set = False # set offdesign parameters for var in c.offdesign: - c.get_attr(var).val_set = True + c.get_attr(var).is_set = True # read design point information - df = self.init_read_connections(c.design_path) msg = ( - 'Reading individual design point information for ' - 'connection ' + c.label + ' from path ' + c.design_path + - 'connections.') + "Reading individual design point information for " + f"connection {c.label} from {c.design_path}/connections.csv." + ) logger.debug(msg) - + if c.design_path not in _local_designs: + _local_designs[c.design_path] = self.init_read_connections( + c.design_path + ) + df = _local_designs[c.design_path] # write data to connections self.init_conn_design_params(c, df) @@ -991,37 +1254,47 @@ def init_design(self): # switch connections to design mode if self.redesign: for var in c.design: - c.get_attr(var).val_set = True + c.get_attr(var).is_set = True for var in c.offdesign: - c.get_attr(var).val_set = False + c.get_attr(var).is_set = False + + if not c.fluid.is_var: + c.simplify_specifications() + self._assign_variable_space(c) + c.preprocess() # unset design values for busses, count bus equations and # reindex bus dictionary for b in self.busses.values(): self.busses[b.label] = b self.num_bus_eq += b.P.is_set * 1 - for cp in b.comps.index: - b.comps.loc[cp, 'P_ref'] = np.nan + b.comps['P_ref'] = np.nan series = pd.Series(dtype='float64') + _local_design_paths = {} for cp in self.comps['object']: + c = cp.__class__.__name__ # read design point information of components with # local_offdesign activated from their respective design path if cp.local_offdesign: if cp.design_path is not None: - # get type of component (class name) - c = cp.__class__.__name__ # read design point information path = hlp.modify_path_os( - cp.design_path + '/components/' + c + '.csv') - df = pd.read_csv( - path, sep=';', decimal='.', converters={ - 'busses': ast.literal_eval, - 'bus_P_ref': ast.literal_eval}) - df.set_index('label', inplace=True) + f"{cp.design_path}/components/{c}.csv" + ) + msg = ( + f"Reading design point information for component " + f"{cp.label} of type {c} from path {path}." + ) + logger.debug(msg) + if path not in _local_design_paths: + _local_design_paths[path] = pd.read_csv( + path, sep=';', decimal='.', index_col=0 + ) + data = _local_design_paths[path].loc[cp.label] # write data - self.init_comp_design_params(cp, df.loc[cp.label]) + self.init_comp_design_params(cp, data) # unset design parameters for var in cp.design: @@ -1040,11 +1313,10 @@ def init_design(self): if isinstance(data, dc_cp): cp.get_attr(var).val = cp.get_attr(var).design switched = True - msg += var + ', ' + msg += var + ", " if switched: - msg = (msg[:-2] + ' to design value at component ' + - cp.label + '.') + msg = f"{msg[:-2]} to design value at component {cp.label}." logger.debug(msg) cp.new_design = False @@ -1061,14 +1333,18 @@ def init_design(self): cp.set_parameters(self.mode, series) # component initialisation - cp.preprocess(self) - ct = cp.__class__.__name__ - for spec in self.specifications[ct].keys(): + cp.preprocess(self.num_conn_vars + self.num_comp_vars) + + for spec in self.specifications[c].keys(): if len(cp.get_attr(self.specifications['lookup'][spec])) > 0: - self.specifications[ct][spec].loc[cp.label] = ( + self.specifications[c][spec].loc[cp.label] = ( cp.get_attr(self.specifications['lookup'][spec])) # count number of component equations and variables + i = self.num_conn_vars + self.num_comp_vars + for container, name in cp.vars.items(): + self.variables_dict[i] = {"obj": container, "variable": name} + i += 1 self.num_comp_vars += cp.num_vars self.num_comp_eq += cp.num_eq @@ -1089,41 +1365,44 @@ def init_offdesign_params(self): (connections) handle the parameter specification. """ # components without any parameters - not_required = [ - 'source', 'sink', 'node', 'merge', 'splitter', 'separator', 'drum', - 'subsystem_interface', 'droplet_separator'] + components_with_parameters = [ + cp.label for cp in self.comps["object"] if len(cp.parameters) > 0 + ] # fetch all components, reindex with label - df_comps = self.comps.copy() - df_comps = df_comps[~df_comps['comp_type'].isin(not_required)] - + df_comps = self.comps.loc[components_with_parameters].copy() # iter through unique types of components (class names) for c in df_comps['comp_type'].unique(): - path = hlp.modify_path_os( - self.design_path + '/components/' + c + '.csv') + path = hlp.modify_path_os(f"{self.design_path}/components/{c}.csv") msg = ( - 'Reading design point information for components of type ' - + c + ' from path ' + path + '.') + f"Reading design point information for components of type {c} " + f"from path {path}." + ) logger.debug(msg) + df = pd.read_csv(path, sep=';', decimal='.', index_col=0) + df.index = df.index.astype(str) - # read data - df = pd.read_csv( - path, sep=';', decimal='.', converters={ - 'busses': ast.literal_eval, - 'bus_P_ref': ast.literal_eval}) - df.set_index('label', inplace=True) # iter through all components of this type and set data + _individual_design_paths = {} for c_label in df.index: - comp = df_comps.loc[c_label, 'object'] + comp = self.comps.loc[c_label, 'object'] # read data of components with individual design_path if comp.design_path is not None: path_c = hlp.modify_path_os( - comp.design_path + '/components/' + c + '.csv') - df_c = pd.read_csv( - path_c, sep=';', decimal='.', converters={ - 'busses': ast.literal_eval, - 'bus_P_ref': ast.literal_eval}) - df_c.set_index('label', inplace=True) - data = df_c.loc[comp.label] + f"{comp.design_path}/components/{c}.csv" + ) + msg = ( + f"Reading design point information for component " + f"{comp.label} of type {c} from path {path_c}." + ) + logger.debug(msg) + if path_c not in _individual_design_paths: + _individual_design_paths[path_c] = pd.read_csv( + path_c, sep=';', decimal='.', index_col=0 + ) + _individual_design_paths[path_c].index = ( + _individual_design_paths[path_c].index.astype(str) + ) + data = _individual_design_paths[path_c].loc[comp.label] else: data = df.loc[comp.label] @@ -1134,24 +1413,35 @@ def init_offdesign_params(self): msg = 'Done reading design point information for components.' logger.debug(msg) + if len(self.busses) > 0: + path = hlp.modify_path_os(f"{self.design_path}/busses.json") + with open(path, "r", encoding="utf-8") as f: + bus_data = json.load(f) + + for b in bus_data: + for comp, value in bus_data[b].items(): + comp = self.get_comp(comp) + self.busses[b].comps.loc[comp, "P_ref"] = value + # read connection design point information - df = self.init_read_connections(self.design_path) msg = ( - 'Reading design point information for connections from path ' + - self.design_path + '/connections.csv.') + "Reading design point information for connections from " + f"{self.design_path}/connections.csv." + ) logger.debug(msg) + df = self.init_read_connections(self.design_path) # iter through connections for c in self.conns['object']: # read data of connections with individual design_path if c.design_path is not None: - df_c = self.init_read_connections(c.design_path) msg = ( - 'Reading individual design point information for ' - 'connection ' + c.label + ' from path ' + c.design_path + - '/connections.csv.') + "Reading connection design point information for " + f"{c.label} from {c.design_path}/connections.csv." + ) logger.debug(msg) + df_c = self.init_read_connections(c.design_path) # write data self.init_conn_design_params(c, df_c) @@ -1177,12 +1467,6 @@ def init_comp_design_params(self, component, data): """ # write component design data component.set_parameters(self.mode, data) - # write design values to busses - i = 0 - for b in data.busses: - bus = self.busses[b].comps - bus.loc[component, 'P_ref'] = data['bus_P_ref'][i] - i += 1 def init_conn_design_params(self, c, df): r""" @@ -1198,30 +1482,59 @@ def init_conn_design_params(self, c, df): """ # match connection (source, source_id, target, target_id) on # connection objects of design file - conn = df.loc[ - df['source'].isin([c.source.label]) & - df['target'].isin([c.target.label]) & - df['source_id'].isin([c.source_id]) & - df['target_id'].isin([c.target_id])] - - try: - # read connection information - conn_id = conn.index[0] - for var in ['m', 'p', 'h', 'v', 'x', 'T', 'Td_bp']: - c.get_attr(var).design = hlp.convert_to_SI( - var, df.loc[conn_id, var], df.loc[conn_id, var + '_unit']) - c.vol.design = c.v.design / c.m.design - for fluid in self.fluids: - c.fluid.design[fluid] = df.loc[conn_id, fluid] - except IndexError as iex: + df.index = df.index.astype(str) + if c.label not in df.index: # no matches in the connections of the network and the design files msg = ( - 'Could not find connection %s in design case. ' - 'Please, make sure no connections have been modified or ' - 'components have been relabeled for your offdesign ' - 'calculation.') - logger.exception(msg, c.label) - raise hlp.TESPyNetworkError(msg) from iex + f"Could not find connection {c.label} in design case. Please " + "make sure no connections have been modified or components " + "havebeen relabeled for your offdesign calculation." + ) + logger.exception(msg) + raise hlp.TESPyNetworkError(msg) + + conn = df.loc[c.label] + for var in fpd.keys(): + c.get_attr(var).design = hlp.convert_to_SI( + var, conn[var], conn[f"{var}_unit"] + ) + c.vol.design = c.v.design / c.m.design + for fluid in c.fluid.val: + c.fluid.design[fluid] = conn[fluid] + + def init_conn_params_from_path(self, c, df): + r""" + Write parameter information from init_path to connections. + + Parameters + ---------- + c : tespy.connections.connection.Connection + Write init path information to this connection. + + df : pandas.core.frame.DataFrame + Dataframe containing init path information. + """ + # match connection (source, source_id, target, target_id) on + # connection objects of design file + df.index = df.index.astype(str) + if c.label not in df.index: + # no matches in the connections of the network and the design files + msg = f"Could not find connection {c.label} in init path file." + logger.debug(msg) + return + + conn = df.loc[c.label] + + for prop in ['m', 'p', 'h']: + data = c.get_attr(prop) + data.val0 = conn[prop] + data.unit = conn[prop + '_unit'] + + for fluid in c.fluid.is_var: + c.fluid.val[fluid] = conn[fluid] + c.fluid.val0[fluid] = c.fluid.val[fluid] + + c.good_starting_values = True def init_offdesign(self): r""" @@ -1245,19 +1558,30 @@ def init_offdesign(self): :code:`cp.offdesign` will be set instead. This does also affect referenced values! """ + self._conn_variables = [] for c in self.conns['object']: if not c.local_design: # switch connections to offdesign mode for var in c.design: - c.get_attr(var).val_set = False - c.get_attr(var).ref_set = False + param = c.get_attr(var) + param.is_set = False + param.is_var = True + if f"{var}_ref" in c.property_data: + c.get_attr(f"{var}_ref").is_set = False for var in c.offdesign: - c.get_attr(var).val_set = True - c.get_attr(var).val_SI = c.get_attr(var).design + param = c.get_attr(var) + param.is_set = True + param.is_var = False + param.val_SI = param.design c.new_design = False + if not c.fluid.is_var: + c.simplify_specifications() + self._assign_variable_space(c) + c.preprocess() + msg = 'Switched connections from design to offdesign.' logger.debug(msg) @@ -1287,13 +1611,17 @@ def init_offdesign(self): logger.debug(msg) # start component initialisation - cp.preprocess(self) + cp.preprocess(self.num_conn_vars + self.num_comp_vars) ct = cp.__class__.__name__ for spec in self.specifications[ct].keys(): if len(cp.get_attr(self.specifications['lookup'][spec])) > 0: self.specifications[ct][spec].loc[cp.label] = ( cp.get_attr(self.specifications['lookup'][spec])) + i = self.num_conn_vars + self.num_comp_vars + for container, name in cp.vars.items(): + self.variables_dict[i] = {"obj": container, "variable": name} + i += 1 cp.new_design = False self.num_comp_vars += cp.num_vars self.num_comp_eq += cp.num_eq @@ -1306,44 +1634,6 @@ def init_offdesign(self): self.busses[b.label] = b self.num_bus_eq += b.P.is_set * 1 - def init_fluids(self): - r""" - Initialise the fluid vector on every connection of the network. - - - Create fluid vector for every component as dict, - index: nw.fluids, - values: 0 if not set by user. - - Create fluid_set vector with same logic, - index: nw.fluids, - values: False if not set by user. - - If there are any combustion chambers in the network, calculate fluid - vector starting from there. - - Propagate fluid vector in direction of sources and targets. - """ - # stop fluid propagation for single fluid networks - if len(self.fluids) == 1: - return - - # fluid propagation from set values - for c in self.conns['object']: - if any(c.fluid.val_set.values()): - c.target.propagate_fluid_to_target(c, c, entry_point=True) - c.source.propagate_fluid_to_source(c, c, entry_point=True) - - # To save resources: - # find empty fluid data and propagate from connections with data that - # are interfaced directly to those connections by a component - if any(c.fluid.val0.values()): - c.target.propagate_fluid_to_target(c, c, entry_point=True) - c.source.propagate_fluid_to_source(c, c, entry_point=True) - - # fluid starting value generation based on components - for cp in self.comps['object']: - cp.initialise_fluids() - - msg = 'Fluid initialisation done.' - logger.debug(msg) - def init_properties(self): """ Initialise the fluid properties on every connection of the network. @@ -1361,33 +1651,9 @@ def init_properties(self): # specified vapour content values, temperature values as well as # subccooling/overheating and state specification for c in self.conns['object']: + c.build_fluid_data() if self.init_path is not None: - conn = df.loc[ - df['source'].isin([c.source.label]) & - df['target'].isin([c.target.label]) & - df['source_id'].isin([c.source_id]) & - df['target_id'].isin([c.target_id])] - try: - conn_id = conn.index[0] - # overwrite SI-values with values from init_file, - # except user specified values - for prop in ['m', 'p', 'h']: - data = c.get_attr(prop) - data.val0 = df.loc[conn_id, prop] - data.unit = df.loc[conn_id, prop + '_unit'] - - for fluid in self.fluids: - if not c.fluid.val_set[fluid]: - c.fluid.val[fluid] = df.loc[conn_id, fluid] - c.fluid.val0[fluid] = c.fluid.val[fluid] - - c.good_starting_values = True - - except IndexError: - msg = ( - 'Could not find connection ' + c.label + ' in ' - 'connections.csv of init_path ' + self.init_path + '.') - logger.debug(msg) + self.init_conn_params_from_path(c, df) if sum(c.fluid.val.values()) == 0: msg = ( @@ -1400,23 +1666,26 @@ def init_properties(self): logger.warning(msg) for key in ['m', 'p', 'h']: - if not c.good_starting_values: - self.init_val0(c, key) - if not c.get_attr(key).val_set: + if c.get_attr(key).is_var: + if not c.good_starting_values: + self.init_val0(c, key) c.get_attr(key).val_SI = hlp.convert_to_SI( - key, c.get_attr(key).val0, c.get_attr(key).unit) + key, c.get_attr(key).val0, c.get_attr(key).unit + ) self.init_count_connections_parameters(c) for c in self.conns['object']: if not c.good_starting_values: - for key in ['m', 'p', 'h', 'T']: - if (c.get_attr(key).ref_set and - not c.get_attr(key).val_set): - c.get_attr(key).val_SI = ( - c.get_attr(key).ref.obj.get_attr(key).val_SI * - c.get_attr(key).ref.factor + - c.get_attr(key).ref.delta_SI) + if self.specifications["Ref"].loc[c.label].any(): + for key in self.specifications["Ref"].columns: + prop = key.split("_ref")[0] + if c.get_attr(key).is_set and not c.get_attr(prop).is_set: + c.get_attr(prop).val_SI = ( + c.get_attr(key).ref.obj.get_attr(prop).val_SI + * c.get_attr(key).ref.factor + + c.get_attr(key).ref.delta_SI + ) self.init_precalc_properties(c) @@ -1424,16 +1693,15 @@ def init_properties(self): # and state specification. These should be recalculated even with # good starting values, for example, when one exchanges enthalpy # with boiling point temperature difference. - if ((c.Td_bp.val_set or c.state.is_set) and - not c.h.val_set): - if ((c.Td_bp.val_SI > 0 and c.Td_bp.val_set) or + if (c.Td_bp.is_set or c.state.is_set) and c.h.is_var: + if ((c.Td_bp.val_SI > 0 and c.Td_bp.is_set) or (c.state.val == 'g' and c.state.is_set)): - h = fp.h_mix_pQ(c.get_flow(), 1) + h = fp.h_mix_pQ(c.p.val_SI, 1, c.fluid_data) if c.h.val_SI < h: c.h.val_SI = h * 1.001 - elif ((c.Td_bp.val_SI < 0 and c.Td_bp.val_set) or + elif ((c.Td_bp.val_SI < 0 and c.Td_bp.is_set) or (c.state.val == 'l' and c.state.is_set)): - h = fp.h_mix_pQ(c.get_flow(), 0) + h = fp.h_mix_pQ(c.p.val_SI, 0, c.fluid_data) if c.h.val_SI > h: c.h.val_SI = h * 0.999 @@ -1450,27 +1718,26 @@ def init_count_connections_parameters(self, c): Connection count parameters of. """ # variables 0 to 9: fluid properties - local_vars = self.specifications['Connection'].columns[:9] - row = [c.get_attr(var).val_set for var in local_vars] - self.num_conn_eq += row.count(True) + local_vars = list(fpd.keys()) + row = [c.get_attr(var).is_set for var in local_vars] # write information to specifaction dataframe self.specifications['Connection'].loc[c.label, local_vars] = row - row = [c.get_attr(var).ref_set for var in local_vars] - self.num_conn_eq += row.count(True) + row = [c.get_attr(var).is_set for var in self.specifications['Ref'].columns] # write refrenced value information to specifaction dataframe - self.specifications['Ref'].loc[c.label, local_vars] = row + self.specifications['Ref'].loc[c.label] = row # variables 9 to last but one: fluid mass fractions - fluids = self.specifications['Connection'].columns[9:-1] - row = [c.fluid.val_set[fluid] for fluid in fluids] - self.num_conn_eq += row.count(True) + fluids = list(self.all_fluids) + row = [True if f in c.fluid.is_set else False for f in fluids] self.specifications['Connection'].loc[c.label, fluids] = row # last one: fluid balance specification - self.num_conn_eq += c.fluid.balance * 1 self.specifications['Connection'].loc[ - c.label, 'balance'] = c.fluid.balance + c.label, 'balance'] = c.fluid_balance.is_set + + # get number of equations + self.num_conn_eq += c.num_eq def init_precalc_properties(self, c): """ @@ -1485,17 +1752,18 @@ def init_precalc_properties(self, c): Connection to precalculate values for. """ # starting values for specified vapour content or temperature - if c.x.val_set and not c.h.val_set: - try: - c.h.val_SI = fp.h_mix_pQ(c.get_flow(), c.x.val_SI) - except ValueError: - pass - - if c.T.val_set and not c.h.val_set: - try: - c.h.val_SI = fp.h_mix_pT(c.get_flow(), c.T.val_SI) - except ValueError: - pass + if c.h.is_var: + if c.x.is_set: + try: + c.h.val_SI = fp.h_mix_pQ(c.p.val_SI, c.x.val_SI, c.fluid_data, c.mixing_rule) + except ValueError: + pass + + if c.T.is_set: + try: + c.h.val_SI = fp.h_mix_pT(c.p.val_SI, c.T.val_SI, c.fluid_data, c.mixing_rule) + except ValueError: + pass def init_val0(self, c, key): r""" @@ -1536,7 +1804,8 @@ def init_val0(self, c, key): # change value according to specified unit system c.get_attr(key).val0 = hlp.convert_from_SI( - key, c.get_attr(key).val0, self.get_attr(key + '_unit')) + key, c.get_attr(key).val0, self.get_attr(key + '_unit') + ) @staticmethod def init_read_connections(base_path): @@ -1554,7 +1823,7 @@ def init_read_connections(base_path): def solve(self, mode, init_path=None, design_path=None, max_iter=50, min_iter=4, init_only=False, init_previous=True, - use_cuda=False, always_all_equations=True, print_results=True): + use_cuda=False, print_results=True, prepare_fast_lane=False): r""" Solve the network. @@ -1598,18 +1867,13 @@ def solve(self, mode, init_path=None, design_path=None, Use cuda instead of numpy for matrix inversion, default: :code:`False`. - always_all_equations : boolean - Calculate all equations in every iteration. Disabling this flag, - will increase calculation speed, especially for mixtures, default: - :code:`True`. - Note ---- For more information on the solution process have a look at the online documentation at tespy.readthedocs.io in the section "TESPy modules". """ + ## to own function self.new_design = False - self.converged = False if self.design_path == design_path and design_path is not None: for c in self.conns['object']: if c.new_design: @@ -1624,6 +1888,7 @@ def solve(self, mode, init_path=None, design_path=None, else: self.new_design = True + self.converged = False self.init_path = init_path self.design_path = design_path self.max_iter = max_iter @@ -1631,49 +1896,60 @@ def solve(self, mode, init_path=None, design_path=None, self.init_previous = init_previous self.iter = 0 self.use_cuda = use_cuda - self.always_all_equations = always_all_equations if self.use_cuda and cu is None: - msg = ('Specifying use_cuda=True requires cupy to be installed on ' - 'your machine. Numpy will be used instead.') + msg = ( + 'Specifying use_cuda=True requires cupy to be installed on ' + 'your machine. Numpy will be used instead.' + ) logger.warning(msg) self.use_cuda = False - if mode != 'offdesign' and mode != 'design': + if mode not in ['offdesign', 'design']: msg = 'Mode must be "design" or "offdesign".' logger.error(msg) raise ValueError(msg) else: self.mode = mode - msg = ( - 'Solver properties: mode=' + self.mode + ', init_path=' + - str(self.init_path) + ', design_path=' + str(self.design_path) + - ', max_iter=' + str(max_iter) + ', min_iter=' + str(min_iter) + - ', init_only=' + str(init_only)) - logger.debug(msg) - if not self.checked: self.check_network() msg = ( - 'Network properties: ' - 'number of components=' + str(len(self.comps)) + - ', number of connections=' + str(len(self.conns.index)) + - ', number of busses=' + str(len(self.busses))) + "Solver properties:\n" + f" - mode: {self.mode}\n" + f" - init_path: {self.init_path}\n" + f" - design_path: {self.design_path}\n" + f" - min_iter: {self.min_iter}\n" + f" - max_iter: {self.max_iter}\n" + f" - init_path: {self.init_path}" + ) + logger.debug(msg) + + msg = ( + "Network information:\n" + f" - Number of components: {len(self.comps)}\n" + f" - Number of connections: {len(self.conns)}\n" + f" - Number of busses: {len(self.busses)}" + ) logger.debug(msg) self.initialise() if init_only: + self._reset_topology_reduction_specifications() return msg = 'Starting solver.' logger.info(msg) self.solve_determination() + self.solve_loop(print_results=print_results) + if not prepare_fast_lane: + self._reset_topology_reduction_specifications() + if self.lin_dep: msg = ( 'Singularity in jacobian matrix, calculation aborted! Make ' @@ -1685,12 +1961,12 @@ def solve(self, mode, init_path=None, design_path=None, 'or kA-values for heat exchangers, \n-> support better ' 'starting values.\n-> bad starting value for fuel mass flow ' 'of combustion chamber, provide small (near to zero, but not ' - 'zero) starting value.') + 'zero) starting value.' + ) logger.error(msg) return self.postprocessing() - fp.Memorise.del_memory(self.fluids) if not self.progress: msg = ( @@ -1698,7 +1974,8 @@ def solve(self, mode, init_path=None, design_path=None, 'calculation. Residual value is ' '{:.2e}'.format(norm(self.residual)) + '. This frequently ' 'happens, if the solver pushes the fluid properties out of ' - 'their feasible range.') + 'their feasible range.' + ) logger.warning(msg) return @@ -1709,7 +1986,7 @@ def solve(self, mode, init_path=None, design_path=None, def solve_loop(self, print_results=True): r"""Loop of the newton algorithm.""" # parameter definitions - self.res = np.array([]) + self.residual_history = np.array([]) self.residual = np.zeros([self.num_vars]) self.increment = np.ones([self.num_vars]) self.jacobian = np.zeros((self.num_vars, self.num_vars)) @@ -1720,23 +1997,30 @@ def solve_loop(self, print_results=True): if self.iterinfo: self.iterinfo_head(print_results) - for count in range(self.max_iter): - self.iter = count - self.increment_filter = np.absolute(self.increment) < err ** 2 + for self.iter in range(self.max_iter): + self.increment_filter = np.absolute(self.increment) < ERR ** 2 self.solve_control() - self.res = np.append(self.res, norm(self.residual)) + self.residual_history = np.append( + self.residual_history, norm(self.residual) + ) if self.iterinfo: self.iterinfo_body(print_results) - if ((self.iter >= self.min_iter and self.res[-1] < err ** 0.5) or - self.lin_dep): - self.converged = True + if ( + (self.iter >= self.min_iter - 1 + and (self.residual_history[-2:] < ERR ** 0.5).all()) + or self.lin_dep + ): + self.converged = not self.lin_dep break if self.iter > 40: - if (all(self.res[(self.iter - 3):] >= self.res[-3] * 0.95) and - self.res[-1] >= self.res[-2] * 0.95): + if ( + all( + self.residual_history[(self.iter - 3):] >= self.residual_history[-3] * 0.95 + ) and self.residual_history[-1] >= self.residual_history[-2] * 0.95 + ): self.progress = False break @@ -1746,67 +2030,65 @@ def solve_loop(self, print_results=True): self.iterinfo_tail(print_results) if self.iter == self.max_iter - 1: - msg = ('Reached maximum iteration count (' + str(self.max_iter) + - '), calculation stopped. Residual value is ' - '{:.2e}'.format(norm(self.residual))) + msg = ( + f"Reached maximum iteration count ({self.max_iter})), " + "calculation stopped. Residual value is " + "{:.2e}".format(norm(self.residual)) + ) logger.warning(msg) return def solve_determination(self): r"""Check, if the number of supplied parameters is sufficient.""" - # number of variables per connection - self.num_conn_vars = len(self.fluids) + 3 - # number of user defined functions self.num_ude_eq = len(self.user_defined_eq) for func in self.user_defined_eq.values(): # remap connection objects func.conns = [ - self.conns.loc[c.label, 'object'] for c in func.conns] + self.conns.loc[c.label, 'object'] for c in func.conns + ] # remap jacobian - func.jacobian = { - c: np.zeros(self.num_conn_vars) - for c in func.conns} + func.jacobian = {} # total number of variables self.num_vars = ( - self.num_conn_vars * len(self.conns.index) + self.num_comp_vars) + self.num_conn_vars + self.num_comp_vars + ) - msg = 'Number of connection equations: ' + str(self.num_conn_eq) + '.' + msg = f'Number of connection equations: {self.num_conn_eq}.' logger.debug(msg) - - msg = 'Number of bus equations: ' + str(self.num_bus_eq) + '.' + msg = f'Number of bus equations: {self.num_bus_eq}.' logger.debug(msg) - - msg = 'Number of component equations: ' + str(self.num_comp_eq) + '.' + msg = f'Number of component equations: {self.num_comp_eq}.' logger.debug(msg) - - msg = 'Number of user defined equations: ' + str(self.num_ude_eq) + '.' + msg = f'Number of user defined equations: {self.num_ude_eq}.' logger.debug(msg) - msg = 'Total number of variables: ' + str(self.num_vars) + '.' + msg = f'Total number of variables: {self.num_vars}.' logger.debug(msg) - msg = 'Number of component variables: ' + str(self.num_comp_vars) + '.' + msg = f'Number of component variables: {self.num_comp_vars}.' logger.debug(msg) - msg = ('Number of connection variables: ' + - str(self.num_conn_vars * len(self.conns.index)) + '.') + msg = f"Number of connection variables: {self.num_conn_vars}." logger.debug(msg) n = ( self.num_comp_eq + self.num_conn_eq + - self.num_bus_eq + self.num_ude_eq) + self.num_bus_eq + self.num_ude_eq + ) if n > self.num_vars: - msg = ('You have provided too many parameters: ' + - str(self.num_vars) + ' required, ' + str(n) + - ' supplied. Aborting calculation!') + msg = ( + f"You have provided too many parameters: {self.num_vars} " + f"required, {n} supplied. Aborting calculation!" + ) logger.error(msg) raise hlp.TESPyNetworkError(msg) elif n < self.num_vars: - msg = ('You have not provided enough parameters: ' - + str(self.num_vars) + ' required, ' + str(n) + - ' supplied. Aborting calculation!') + msg = ( + f"You have not provided enough parameters: {self.num_vars} " + f"required, {n} supplied. Aborting calculation!" + ) logger.error(msg) raise hlp.TESPyNetworkError(msg) @@ -1815,21 +2097,20 @@ def iterinfo_head(self, print_results=True): # Start with defining the format here self.iterinfo_fmt = ' {iter:5s} | {residual:10s} | {progress:10s} ' self.iterinfo_fmt += '| {massflow:10s} | {pressure:10s} | {enthalpy:10s} ' - self.iterinfo_fmt += '| {fluid:10s} | {custom:10s} ' + self.iterinfo_fmt += '| {fluid:10s} | {component:10s} ' # Use the format to create the first logging entry - custom = '' if self.num_comp_vars == 0 else 'custom' - msg = self.iterinfo_fmt.format(iter='iter', - residual='residual', - progress='progress', - massflow='massflow', - pressure='pressure', - enthalpy='enthalpy', - fluid='fluid', - custom=custom) + msg = self.iterinfo_fmt.format( + iter='iter', + residual='residual', + progress='progress', + massflow='massflow', + pressure='pressure', + enthalpy='enthalpy', + fluid='fluid', + component='component' + ) logger.progress(0, msg) - msg2 = '-' * 7 + '+------------' * 6 + "+" - if self.num_comp_vars > 0: - msg2 += '+-------------' + msg2 = '-' * 7 + '+------------' * 7 logger.progress(0, msg2) if print_results: @@ -1838,7 +2119,13 @@ def iterinfo_head(self, print_results=True): def iterinfo_body(self, print_results=True): """Print convergence progress.""" - vec = self.increment[0:-(self.num_comp_vars + 1)] + m = [k for k, v in self.variables_dict.items() if v["variable"] == "m"] + p = [k for k, v in self.variables_dict.items() if v["variable"] == "h"] + p = [k for k, v in self.variables_dict.items() if v["variable"] == "p"] + h = [k for k, v in self.variables_dict.items() if v["variable"] == "h"] + fl = [k for k, v in self.variables_dict.items() if v["variable"] == "fluid"] + cp = [k for k in self.variables_dict if k not in m + p + h + fl] + iter_str = str(self.iter + 1) residual_norm = norm(self.residual) residual = 'NaN' @@ -1847,47 +2134,48 @@ def iterinfo_body(self, print_results=True): pressure = 'NaN' enthalpy = 'NaN' fluid = 'NaN' - custom = 'NaN' + component = 'NaN' + + progress_val = -1 if not np.isnan(residual_norm): residual = '{:.2e}'.format(residual_norm) - if not self.lin_dep and not np.isnan(residual_norm): - massflow = '{:.2e}'.format(norm(vec[0::self.num_conn_vars])) - pressure = '{:.2e}'.format(norm(vec[1::self.num_conn_vars])) - enthalpy = '{:.2e}'.format(norm(vec[2::self.num_conn_vars])) + if not self.lin_dep: + massflow = '{:.2e}'.format(norm(self.increment[m])) + pressure = '{:.2e}'.format(norm(self.increment[p])) + enthalpy = '{:.2e}'.format(norm(self.increment[h])) + fluid = '{:.2e}'.format(norm(self.increment[fl])) + component = '{:.2e}'.format(norm(self.increment[cp])) - ls = [] - for f in range(len(self.fluids)): - ls += vec[3 + f::self.num_conn_vars].tolist() - fluid = '{:.2e}'.format(norm(ls)) - - if self.num_comp_vars > 0: - custom = '{:.2e}'.format(norm( - self.increment[-self.num_comp_vars:])) + # This should not be hardcoded here. + if residual_norm > np.finfo(float).eps * 100: + progress_min = np.log(ERR) + progress_max = np.log(ERR ** 0.5) * -1 + progress_val = np.log(max(residual_norm, ERR)) * -1 + # Scale to 0-1 + progres_scaled = ( + (progress_val - progress_min) + / (progress_max - progress_min) + ) + progress_val = max(0, min(1, progres_scaled)) + # Scale to 100% + progress_val = int(progress_val * 100) else: - custom = '' + progress_val = 100 - progress_val = -1 - if not np.isnan(residual_norm) and residual_norm > np.finfo(float).eps*100: - # This should not be hardcoded here. - progress_min = np.log(err) - progress_max = np.log(err) * -1 - progress_val = np.log(max(residual_norm, err)) * -1 - # Scale to 0-1 - progress_val = max(0, min(1, (progress_val - progress_min) / (progress_max - progress_min))) - # Scale to 100% - progress_val = int((progress_val - progress_min) / (progress_max - progress_min) * 100) progress = '{:d} %'.format(progress_val) - msg = self.iterinfo_fmt.format(iter=iter_str, - residual=residual, - progress=progress, - massflow=massflow, - pressure=pressure, - enthalpy=enthalpy, - fluid=fluid, - custom=custom) + msg = self.iterinfo_fmt.format( + iter=iter_str, + residual=residual, + progress=progress, + massflow=massflow, + pressure=pressure, + enthalpy=enthalpy, + fluid=fluid, + component=component + ) logger.progress(progress_val, msg) if print_results: print(msg) @@ -1900,7 +2188,10 @@ def iterinfo_tail(self, print_results=True): num_ips = num_iter / clc_time if clc_time > 1e-10 else np.Inf msg = '-' * 7 + '+------------' * 7 logger.progress(100, msg) - msg = 'Total iterations: {0:d}, Calculation time: {1:.2f} s, Iterations per second: {2:.2f}'.format(num_iter, clc_time, num_ips) + msg = ( + "Total iterations: {0:d}, Calculation time: {1:.2f} s, " + "Iterations per second: {2:.2f}" + ).format(num_iter, clc_time, num_ips) logger.debug(msg) if print_results: print(msg) @@ -1923,6 +2214,57 @@ def matrix_inversion(self): except np.linalg.linalg.LinAlgError: self.increment = self.residual * 0 + def update_variables(self): + # add the increment + for data in self.variables_dict.values(): + if data["variable"] in ["m", "h"]: + container = data["obj"].get_attr(data["variable"]) + container.val_SI += self.increment[container.J_col] + elif data["variable"] == "p": + container = data["obj"].p + increment = self.increment[container.J_col] + relax = max(1, -2 * increment / container.val_SI) + container.val_SI += increment / relax + elif data["variable"] == "fluid": + container = data["obj"].fluid + container.val[data["fluid"]] += self.increment[ + container.J_col[data["fluid"]] + ] + + if container.val[data["fluid"]] < ERR : + container.val[data["fluid"]] = 0 + elif container.val[data["fluid"]] > 1 - ERR : + container.val[data["fluid"]] = 1 + else: + # add increment + data["obj"].val += self.increment[data["obj"].J_col] + + # keep value within specified value range + if data["obj"].val < data["obj"].min_val: + data["obj"].val = data["obj"].min_val + elif data["obj"].val > data["obj"].max_val: + data["obj"].val = data["obj"].max_val + + def check_variable_bounds(self): + + for c in self.conns['object']: + # check the fluid properties for physical ranges + if len(c.fluid.is_var) > 0: + total_mass_fractions = sum(c.fluid.val.values()) + for fluid in c.fluid.is_var: + c.fluid.val[fluid] /= total_mass_fractions + + c.build_fluid_data() + self.check_connection_properties(c) + + # second property check for first three iterations without an init_file + if self.iter < 3: + for cp in self.comps['object']: + cp.convergence_check() + + for c in self.conns['object']: + self.check_connection_properties(c) + def solve_control(self): r""" Control iteration step of the newton algorithm. @@ -1943,95 +2285,10 @@ def solve_control(self): if self.lin_dep: return - # add the increment - i = 0 - for c in self.conns['object']: - # mass flow, pressure and enthalpy - if not c.m.val_set: - c.m.val_SI += self.increment[i * (self.num_conn_vars)] - if not c.p.val_set: - # this prevents negative pressures - relax = max(1, -self.increment[i * (self.num_conn_vars) + 1] / - (0.5 * c.p.val_SI)) - c.p.val_SI += self.increment[ - i * (self.num_conn_vars) + 1] / relax - if not c.h.val_set: - c.h.val_SI += self.increment[i * (self.num_conn_vars) + 2] - - # fluid vector (only if number of fluids is greater than 1) - if len(self.fluids) > 1: - j = 0 - for fluid in self.fluids: - # add increment - if not c.fluid.val_set[fluid]: - c.fluid.val[fluid] += ( - self.increment[ - i * (self.num_conn_vars) + 3 + j]) - - # keep mass fractions within [0, 1] - if c.fluid.val[fluid] < err: - c.fluid.val[fluid] = 0 - elif c.fluid.val[fluid] > 1 - err: - c.fluid.val[fluid] = 1 - - j += 1 - - # check the fluid properties for physical ranges - self.solve_check_props(c) - i += 1 + self.update_variables() + self.check_variable_bounds() - # increment for the custom variables - if self.num_comp_vars > 0: - sum_c_var = 0 - for cp in self.comps['object']: - for var in cp.vars.keys(): - pos = var.var_pos - - # add increment - var.val += self.increment[ - self.num_conn_vars * len(self.conns) + sum_c_var + pos] - - # keep value within specified value range - if var.val < var.min_val: - var.val = var.min_val - elif var.val > var.max_val: - var.val = var.max_val - - sum_c_var += cp.num_vars - - # second property check for first three iterations without an init_file - if self.iter < 3: - for cp in self.comps['object']: - cp.convergence_check() - - for c in self.conns['object']: - self.solve_check_props(c) - - def property_range_message(self, c, prop): - r""" - Return debugging message for fluid property range adjustments. - - Parameters - ---------- - c : tespy.connections.connection.Connection - Connection to check fluid properties. - - prop : str - Fluid property. - - Returns - ------- - msg : str - Debugging message. - """ - msg = ( - fpd[prop]['text'][0].upper() + fpd[prop]['text'][1:] + - ' out of fluid property range at connection ' + c.label + - ' adjusting value to ' + str(c.get_attr(prop).val_SI) + - ' ' + fpd[prop]['SI_unit'] + '.') - return msg - - def solve_check_props(self, c): + def check_connection_properties(self, c): r""" Check for invalid fluid property values. @@ -2040,540 +2297,130 @@ def solve_check_props(self, c): c : tespy.connections.connection.Connection Connection to check fluid properties. """ - fl = hlp.single_fluid(c.fluid.val) + fl = fp.single_fluid(c.fluid_data) + # pure fluid if fl is not None: # pressure - if c.p.val_SI < fp.Memorise.value_range[fl][0] and not c.p.val_set: - c.p.val_SI = fp.Memorise.value_range[fl][0] - logger.debug(self.property_range_message(c, 'p')) - elif (c.p.val_SI > fp.Memorise.value_range[fl][1] and - not c.p.val_set): - c.p.val_SI = fp.Memorise.value_range[fl][1] - logger.debug(self.property_range_message(c, 'p')) + if c.p.is_var: + c.check_pressure_bounds(fl) # enthalpy - try: - hmin = fp.h_pT( - c.p.val_SI, fp.Memorise.value_range[fl][2] * 1.001, fl) - except ValueError: - f = 1.05 - hmin = fp.h_pT( - c.p.val_SI, fp.Memorise.value_range[fl][2] * f, fl) - - T = fp.Memorise.value_range[fl][3] - while True: - try: - hmax = fp.h_pT(c.p.val_SI, T, fl) - break - except ValueError as e: - T *= 0.99 - if T < fp.Memorise.value_range[fl][2]: - raise ValueError(e) from e - - if c.h.val_SI < hmin and not c.h.val_set: - if hmin < 0: - c.h.val_SI = hmin * 0.9999 - else: - c.h.val_SI = hmin * 1.0001 - logger.debug(self.property_range_message(c, 'h')) + if c.h.is_var: + c.check_enthalpy_bounds(fl) - elif c.h.val_SI > hmax and not c.h.val_set: - c.h.val_SI = hmax * 0.9999 - logger.debug(self.property_range_message(c, 'h')) - - if ((c.Td_bp.val_set or c.state.is_set) and - not c.h.val_set and self.iter < 3): - if (c.Td_bp.val_SI > 0 or - (c.state.val == 'g' and c.state.is_set)): - h = fp.h_mix_pQ(c.get_flow(), 1) - if c.h.val_SI < h: - c.h.val_SI = h * 1.01 - logger.debug(self.property_range_message(c, 'h')) - elif (c.Td_bp.val_SI < 0 or - (c.state.val == 'l' and c.state.is_set)): - h = fp.h_mix_pQ(c.get_flow(), 0) - if c.h.val_SI > h: - c.h.val_SI = h * 0.99 - logger.debug(self.property_range_message(c, 'h')) + # two-phase related + if (c.Td_bp.is_set or c.state.is_set) and self.iter < 3: + c.check_two_phase_bounds(fl) + # mixture elif self.iter < 4 and not c.good_starting_values: # 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] - logger.debug(self.property_range_message(c, 'p')) + if c.p.is_var: + if c.p.val_SI <= self.p_range_SI[0]: + c.p.val_SI = self.p_range_SI[0] + logger.debug(c._property_range_message('p')) - elif c.p.val_SI >= self.p_range_SI[1] and not c.p.val_set: - c.p.val_SI = self.p_range_SI[1] - logger.debug(self.property_range_message(c, 'p')) + elif c.p.val_SI >= self.p_range_SI[1]: + c.p.val_SI = self.p_range_SI[1] + logger.debug(c._property_range_message('p')) # enthalpy - if c.h.val_SI < self.h_range_SI[0] and not c.h.val_set: - c.h.val_SI = self.h_range_SI[0] - logger.debug(self.property_range_message(c, 'h')) + if c.h.is_var: + if c.h.val_SI < self.h_range_SI[0]: + c.h.val_SI = self.h_range_SI[0] + logger.debug(c._property_range_message('h')) - elif c.h.val_SI > self.h_range_SI[1] and not c.h.val_set: - c.h.val_SI = self.h_range_SI[1] - logger.debug(self.property_range_message(c, 'h')) + elif c.h.val_SI > self.h_range_SI[1]: + c.h.val_SI = self.h_range_SI[1] + logger.debug(c._property_range_message('h')) - # temperature - if c.T.val_set and not c.h.val_set: - self.solve_check_temperature(c) + # temperature + if c.T.is_set: + c.check_temperature_bounds() # mass flow - if c.m.val_SI <= self.m_range_SI[0] and not c.m.val_set: + if c.m.val_SI <= self.m_range_SI[0] and c.m.is_var: c.m.val_SI = self.m_range_SI[0] - logger.debug(self.property_range_message(c, 'm')) + logger.debug(c._property_range_message('m')) - elif c.m.val_SI >= self.m_range_SI[1] and not c.m.val_set: + elif c.m.val_SI >= self.m_range_SI[1] and c.m.is_var: c.m.val_SI = self.m_range_SI[1] - logger.debug(self.property_range_message(c, 'm')) - - def solve_check_temperature(self, c): - r""" - Check if temperature is within user specified limits. - - Parameters - ---------- - c : tespy.connections.connection.Connection - Connection to check fluid properties. - """ - flow = c.get_flow() - Tmin = max( - [fp.Memorise.value_range[f][2] for - f in flow[3].keys() if flow[3][f] > err] - ) + 100 - Tmax = min( - [fp.Memorise.value_range[f][3] for - f in flow[3].keys() if flow[3][f] > err] - ) - 100 - hmin = fp.h_mix_pT(flow, Tmin) - hmax = fp.h_mix_pT(flow, Tmax) - - if c.h.val_SI < hmin: - c.h.val_SI = hmin - logger.debug(self.property_range_message(c, 'h')) - - if c.h.val_SI > hmax: - c.h.val_SI = hmax - logger.debug(self.property_range_message(c, 'h')) + logger.debug(c._property_range_message('m')) def solve_components(self): r""" Calculate the residual and derivatives of component equations. - - - Iterate through components in network to get residuals and - derivatives. - - Place residual values in residual value vector of the network. - - Place partial derivatives in jacobian matrix of the network. """ # fetch component equation residuals and component partial derivatives sum_eq = 0 - sum_c_var = 0 for cp in self.comps['object']: - - indices = [] - for c in cp.conn_loc: - start = c * self.num_conn_vars - end = (c + 1) * self.num_conn_vars - indices += [np.arange(start, end)] - - cp.solve(self.increment_filter[np.array(indices)]) - + cp.solve(self.increment_filter) self.residual[sum_eq:sum_eq + cp.num_eq] = cp.residual - deriv = cp.jacobian - - if deriv is not None: - i = 0 - # place derivatives in jacobian matrix - for loc in cp.conn_loc: - coll_s = loc * self.num_conn_vars - coll_e = (loc + 1) * self.num_conn_vars - self.jacobian[ - sum_eq:sum_eq + cp.num_eq, coll_s:coll_e] = deriv[:, i] - i += 1 - - # derivatives for custom variables - for j in range(cp.num_vars): - coll = self.num_vars - self.num_comp_vars + sum_c_var - self.jacobian[sum_eq:sum_eq + cp.num_eq, coll] = ( - deriv[:, i + j, :1].transpose()[0]) - sum_c_var += 1 + if len(cp.jacobian) > 0: + rows = [k[0] + sum_eq for k in cp.jacobian] + columns = [k[1] for k in cp.jacobian] + data = list(cp.jacobian.values()) + self.jacobian[rows, columns] = data sum_eq += cp.num_eq - cp.it += 1 - - def solve_user_defined_eq(self): - """ - Calculate the residual and jacobian of user defined equations. - - Iterate through user defined functions and calculate residual value - and corresponding jacobian. - - Place residual values in residual value vector of the network. - - Place partial derivatives regarding connection parameters in jacobian - matrix of the network. - """ - row = self.num_comp_eq + self.num_conn_eq + self.num_bus_eq - for ude in self.user_defined_eq.values(): - self.residual[row] = ude.func(ude) - jacobian = ude.deriv(ude) - for c, derivative in jacobian.items(): - col = c.conn_loc * self.num_conn_vars - self.jacobian[row, col:col + self.num_conn_vars] = derivative - row += 1 + cp.it += 1 def solve_connections(self): r""" Calculate the residual and derivatives of connection equations. + """ + sum_eq = self.num_comp_eq + for c in self.conns['object']: + c.solve(self.increment_filter) + self.residual[sum_eq:sum_eq + c.num_eq] = c.residual - - Iterate through connections in network to get residuals and - derivatives. - - Place residual values in residual value vector of the network. - - Place partial derivatives in jacobian matrix of the network. - - Note - ---- - **Equations** - - **mass flow, pressure and enthalpy** - - .. math:: - val = 0 - - **temperatures** - - .. math:: - val = T_{j} - T \left( p_{j}, h_{j}, fluid_{j} \right) - - **volumetric flow** - - .. math:: - val = \dot{V}_{j} - v \left( p_{j}, h_{j} \right) \cdot \dot{m}_j - - **superheating or subcooling** *Works with pure fluids only!* - - .. math:: - val = T_{j} - td_{bp} - T_{bp}\left( p_{j}, fluid_{j} \right) - - \text{td: temperature difference, bp: boiling point} - - **vapour mass fraction** *Works with pure fluids only!* - - .. math:: - val = h_{j} - h \left( p_{j}, x_{j}, fluid_{j} \right) - - **Referenced values** - - **mass flow, pressure and enthalpy** - - .. math:: - val = x_{j} - x_{j,ref} \cdot a + b - - **temperatures** - - .. math:: - val = T \left( p_{j}, h_{j}, fluid_{j} \right) - - T \left( p_{j}, h_{j}, fluid_{j} \right) \cdot a + b - - **Derivatives** - - **mass flow, pressure and enthalpy** - - .. math:: - - J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) = 1\\ - \text{for equation i, connection j}\\ - \text{pressure and enthalpy analogously} - - **temperatures** - - .. math:: - - J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) = - -\frac{\partial T_{j}}{\partial p_{j}}\\ - J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = - -\frac{\partial T_{j}}{\partial h_{j}}\\ - J\left(\frac{\partial f_{i}}{\partial fluid_{j,k}}\right) = - - \frac{\partial T_{j}}{\partial fluid_{j,k}} - - \forall k \in \text{fluid components}\\ - \text{for equation i, connection j} - - **volumetric flow** - - .. math:: - - J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) = - -v \left( p_{j}, h_{j} \right)\\ - J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) = - -\frac{\partial v_{j}}{\partial p_{j}} \cdot \dot{m}_j\\ - J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = - -\frac{\partial v_{j}}{\partial h_{j}} \cdot \dot{m}_j\\ - - \forall k \in \text{fluid components}\\ - \text{for equation i, connection j} - - **superheating or subcooling** *Works with pure fluids only!* - - .. math:: - - J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) = - \frac{\partial T \left( p_{j}, h_{j}, fluid_{j} \right)} - {\partial p_{j}} - - \frac{\partial T_{bp} \left( p_{j}, fluid_{j} \right)} - {\partial p_{j}} \\ - J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = - \frac{\partial T \left( p_{j}, h_{j}, fluid_{j} \right)} - {\partial h_{j}}\\ - - \text{for equation i, connection j}\\ - \text{td: temperature difference, bp: boiling point} - - **vapour mass fraction** *Works with pure fluids only!* - - .. math:: - - J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) = - -\frac{\partial h \left( p_{j}, x_{j}, fluid_{j} \right)} - {\partial p_{j}}\\ - J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = 1\\ - \text{for equation i, connection j, x: vapour mass fraction} - - **Referenced values** - - **mass flow, pressure and enthalpy** - - .. math:: - J\left(\frac{\partial f_{i}}{\partial m_{j}}\right) = 1\\ - J\left(\frac{\partial f_{i}}{\partial m_{j,ref}}\right) = - a\\ - \text{for equation i, connection j}\\ - \text{pressure and enthalpy analogously} + if len(c.jacobian) > 0: + rows = [k[0] + sum_eq for k in c.jacobian] + columns = [k[1] for k in c.jacobian] + data = list(c.jacobian.values()) + self.jacobian[rows, columns] = data + sum_eq += c.num_eq - **temperatures** + c.it += 1 - .. math:: - J\left(\frac{\partial f_{i}}{\partial p_{j}}\right) = - \frac{dT_{j}}{dp_{j}}\\ - J\left(\frac{\partial f_{i}}{\partial h_{j}}\right) = - \frac{dT_{j}}{dh_{j}}\\ - J\left(\frac{\partial f_{i}}{\partial fluid_{j,k}}\right) = - \frac{dT_{j}}{dfluid_{j,k}} - \; , \forall k \in \text{fluid components}\\ - J\left(\frac{\partial f_{i}}{\partial p_{j,ref}}\right) = - \frac{dT_{j,ref}}{dp_{j,ref}} \cdot a \\ - J\left(\frac{\partial f_{i}}{\partial h_{j,ref}}\right) = - \frac{dT_{j,ref}}{dh_{j,ref}} \cdot a \\ - J\left(\frac{\partial f_{i}}{\partial fluid_{j,k,ref}}\right) = - \frac{dT_{j}}{dfluid_{j,k,ref}} \cdot a - \; , \forall k \in \text{fluid components}\\ - \text{for equation i, connection j} + def solve_user_defined_eq(self): """ - k = self.num_comp_eq - primary_vars = {'m': 0, 'p': 1, 'h': 2} - for c in self.conns['object']: - flow = c.get_flow() - col = c.conn_loc * self.num_conn_vars - - # referenced mass flow, pressure or enthalpy - for var, pos in primary_vars.items(): - if c.get_attr(var).ref_set: - ref = c.get_attr(var).ref - ref_col = ref.obj.conn_loc * self.num_conn_vars - self.residual[k] = ( - c.get_attr(var).val_SI - ( - ref.obj.get_attr(var).val_SI * ref.factor + - ref.delta_SI)) - self.jacobian[k, col + pos] = 1 - self.jacobian[k, ref_col + pos] = -c.get_attr(var).ref.factor - k += 1 - - # temperature - if c.T.val_set: - self.residual[k] = fp.T_mix_ph( - flow, T0=c.T.val_SI) - c.T.val_SI - - self.jacobian[k, col + 1] = ( - fp.dT_mix_dph(flow, T0=c.T.val_SI)) - self.jacobian[k, col + 2] = ( - fp.dT_mix_pdh(flow, T0=c.T.val_SI)) - if len(self.fluids) != 1: - col_s = c.conn_loc * self.num_conn_vars + 3 - col_e = (c.conn_loc + 1) * self.num_conn_vars - if not all(self.increment_filter[col_s:col_e]): - self.jacobian[k, col_s:col_e] = fp.dT_mix_ph_dfluid( - flow, T0=c.T.val_SI) - k += 1 - - # referenced temperature - if c.T.ref_set: - ref = c.T.ref - flow_ref = ref.obj.get_flow() - ref_col = ref.obj.conn_loc * self.num_conn_vars - self.residual[k] = fp.T_mix_ph(flow, T0=c.T.val_SI) - ( - fp.T_mix_ph(flow_ref, T0=ref.obj.T.val_SI) * - ref.factor + ref.delta_SI) - - self.jacobian[k, col + 1] = ( - fp.dT_mix_dph(flow, T0=c.T.val_SI)) - self.jacobian[k, col + 2] = ( - fp.dT_mix_pdh(flow, T0=c.T.val_SI)) - - self.jacobian[k, ref_col + 1] = -( - fp.dT_mix_dph(flow_ref, T0=ref.obj.T.val_SI) * ref.factor) - self.jacobian[k, ref_col + 2] = -( - fp.dT_mix_pdh(flow_ref, T0=ref.obj.T.val_SI) * ref.factor) - - # dT / dFluid - if len(self.fluids) != 1: - col_s = c.conn_loc * self.num_conn_vars + 3 - col_e = (c.conn_loc + 1) * self.num_conn_vars - ref_col_s = ref.obj.conn_loc * self.num_conn_vars + 3 - ref_col_e = (ref.obj.conn_loc + 1) * self.num_conn_vars - if not all(self.increment_filter[col_s:col_e]): - self.jacobian[k, col_s:col_e] = ( - fp.dT_mix_ph_dfluid(flow, T0=c.T.val_SI)) - if not all(self.increment_filter[ref_col_s:ref_col_e]): - self.jacobian[k, ref_col_s:ref_col_e] = -np.array([ - fp.dT_mix_ph_dfluid( - flow_ref, T0=ref.obj.T.val_SI)]) - k += 1 - - # saturated steam fraction - if c.x.val_set: - if (np.absolute(self.residual[k]) > err ** 2 or - self.iter % 2 == 0 or self.always_all_equations): - self.residual[k] = c.h.val_SI - ( - fp.h_mix_pQ(flow, c.x.val_SI)) - if not self.increment_filter[col + 1]: - self.jacobian[k, col + 1] = -( - fp.dh_mix_dpQ(flow, c.x.val_SI)) - self.jacobian[k, col + 2] = 1 - k += 1 - - # volumetric flow - if c.v.val_set: - if (np.absolute(self.residual[k]) > err ** 2 or - self.iter % 2 == 0 or self.always_all_equations): - self.residual[k] = ( - fp.v_mix_ph(flow, T0=c.T.val_SI) * c.m.val_SI - - c.v.val_SI) - self.jacobian[k, col] = fp.v_mix_ph(flow, T0=c.T.val_SI) - self.jacobian[k, col + 1] = ( - fp.dv_mix_dph(flow, T0=c.T.val_SI) * c.m.val_SI) - self.jacobian[k, col + 2] = ( - fp.dv_mix_pdh(flow, T0=c.T.val_SI) * c.m.val_SI) - k += 1 - - # referenced volumetric flow - if c.v.ref_set: - ref = c.v.ref - flow_ref = ref.obj.get_flow() - ref_col = ref.obj.conn_loc * self.num_conn_vars - v = fp.v_mix_ph(flow, T0=c.T.val_SI) - v_ref = fp.v_mix_ph(flow_ref, T0=ref.obj.T.val_SI) - self.residual[k] = ( - (v * c.m.val_SI) - - ((v_ref * ref.obj.m.val_SI) * ref.factor + ref.delta_SI) - ) + Calculate the residual and jacobian of user defined equations. + """ + sum_eq = self.num_comp_eq + self.num_conn_eq + self.num_bus_eq + for ude in self.user_defined_eq.values(): + ude.solve() + self.residual[sum_eq] = ude.residual - self.jacobian[k, col] = v - self.jacobian[k, col + 1] = ( - fp.dv_mix_dph(flow, T0=c.T.val_SI) * c.m.val_SI - ) - self.jacobian[k, col + 2] = ( - fp.dv_mix_pdh(flow, T0=c.T.val_SI) * c.m.val_SI - ) - - self.jacobian[k, ref_col] = -v_ref * ref.factor - self.jacobian[k, ref_col + 1] = -( - fp.dv_mix_dph(flow_ref, T0=ref.obj.T.val_SI) - * ref.factor * ref.obj.m.val_SI - ) - self.jacobian[k, ref_col + 2] = -( - fp.dv_mix_pdh(flow_ref, T0=ref.obj.T.val_SI) - * ref.factor * ref.obj.m.val_SI - ) - k += 1 - - # temperature difference to boiling point - if c.Td_bp.val_set: - if (np.absolute(self.residual[k]) > err ** 2 or - self.iter % 2 == 0 or self.always_all_equations): - self.residual[k] = ( - fp.T_mix_ph(flow, T0=c.T.val_SI) - c.Td_bp.val_SI - - fp.T_bp_p(flow)) - if not self.increment_filter[col + 1]: - self.jacobian[k, col + 1] = ( - fp.dT_mix_dph(flow, T0=c.T.val_SI) - fp.dT_bp_dp(flow)) - if not self.increment_filter[col + 2]: - self.jacobian[k, col + 2] = fp.dT_mix_pdh( - flow, T0=c.T.val_SI) - k += 1 - - # fluid composition balance - if c.fluid.balance: - j = 0 - res = 1 - for f in self.fluids: - res -= c.fluid.val[f] - self.jacobian[k, c.conn_loc + 3 + j] = -1 - j += 1 - - self.residual[k] = res - k += 1 - - # equations and derivatives for specified primary variables are static - if self.iter == 0: - for c in self.conns['object']: - col = c.conn_loc * self.num_conn_vars - - # specified mass flow, pressure and enthalpy - for var, pos in primary_vars.items(): - if c.get_attr(var).val_set: - self.residual[k] = 0 - self.jacobian[k, col + pos] = 1 - k += 1 - - j = 0 - # specified fluid mass fraction - for f in self.fluids: - if c.fluid.val_set[f]: - self.jacobian[k, col + 3 + j] = 1 - k += 1 - j += 1 + if len(ude.jacobian) > 0: + columns = [k for k in ude.jacobian] + data = list(ude.jacobian.values()) + self.jacobian[sum_eq, columns] = data + sum_eq += 1 def solve_busses(self): r""" Calculate the equations and the partial derivatives for the busses. - - - Iterate through busses in network to get residuals and derivatives. - - Place residual values in residual value vector of the network. - - Place partial derivatives in jacobian matrix of the network. """ - row = self.num_comp_eq + self.num_conn_eq + sum_eq = self.num_comp_eq + self.num_conn_eq for bus in self.busses.values(): if bus.P.is_set: - P_res = 0 - for cp in bus.comps.index: - P_res -= cp.calc_bus_value(bus) - deriv = -cp.bus_deriv(bus) + bus.solve() + self.residual[sum_eq] = bus.residual - j = 0 - for loc in cp.conn_loc: - # start collumn index - coll_s = loc * self.num_conn_vars - # end collumn index - coll_e = (loc + 1) * self.num_conn_vars - self.jacobian[row, coll_s:coll_e] = deriv[:, j] - j += 1 + if len(bus.jacobian) > 0: + columns = [k for k in bus.jacobian] + data = list(bus.jacobian.values()) + self.jacobian[sum_eq, columns] = data - self.residual[row] = bus.P.val + P_res - row += 1 + bus.clear_jacobian() + sum_eq += 1 def postprocessing(self): r"""Calculate connection, bus and component parameters.""" - self.process_connections() self.process_components() self.process_busses() @@ -2584,47 +2431,18 @@ def postprocessing(self): def process_connections(self): """Process the Connection results.""" for c in self.conns['object']: - flow = c.get_flow() c.good_starting_values = True - c.T.val_SI = fp.T_mix_ph(flow, T0=c.T.val_SI) - fluid = hlp.single_fluid(c.fluid.val) - if (fluid is None and - abs( - fp.h_mix_pT(flow, c.T.val_SI) - c.h.val_SI - ) > err ** .5): - c.T.val_SI = np.nan - c.vol.val_SI = np.nan - c.v.val_SI = np.nan - c.s.val_SI = np.nan - msg = ( - 'Could not find a feasible value for mixture temperature ' - 'at connection ' + c.label + '. The values for ' - 'temperature, specific volume, volumetric flow and ' - 'entropy are set to nan.') - logger.error(msg) - - else: - c.vol.val_SI = fp.v_mix_ph(flow, T0=c.T.val_SI) - c.v.val_SI = c.vol.val_SI * c.m.val_SI - c.s.val_SI = fp.s_mix_ph(flow, T0=c.T.val_SI) - if fluid is not None: - if not c.x.val_set: - c.x.val_SI = fp.Q_ph(c.p.val_SI, c.h.val_SI, fluid) - if not c.Td_bp.val_set: - c.Td_bp.val_SI = np.nan - - for prop in fpd.keys(): - c.get_attr(prop).val = hlp.convert_from_SI( - prop, c.get_attr(prop).val_SI, c.get_attr(prop).unit) - - c.m.val0 = c.m.val - c.p.val0 = c.p.val - c.h.val0 = c.h.val - c.fluid.val0 = c.fluid.val.copy() + c.calc_results() self.results['Connection'].loc[c.label] = ( - [c.m.val, c.p.val, c.h.val, c.T.val, c.v.val, c.vol.val, - c.s.val, c.x.val, c.Td_bp.val] + list(c.fluid.val.values())) + [ + _ for key in fpd.keys() + for _ in [c.get_attr(key).val, c.get_attr(key).unit] + ] + [ + c.fluid.val[fluid] if fluid in c.fluid.val else np.nan + for fluid in self.all_fluids + ] + ) def process_components(self): """Process the component results.""" @@ -2667,13 +2485,11 @@ def process_busses(self): else: design_value = b.comps.loc[cp, 'P_ref'] - self.results[b.label].loc[cp.label] = ( - [cmp_val, bus_val, eff, design_value]) + result = [cmp_val, bus_val, eff, design_value] + self.results[b.label].loc[cp.label] = result b.P.val = self.results[b.label]['bus value'].sum() -# %% printing and plotting - def print_results(self, colored=True, colors=None, print_results=True): r"""Print the calculations results to prompt.""" # Define colors for highlighting values in result table @@ -2711,7 +2527,7 @@ def print_results(self, colored=True, colors=None, print_results=True): if len(df) > 0: # printout with tabulate - result += ('\n##### RESULTS (' + cp + ') #####\n') + result += f"\n##### RESULTS ({cp}) #####\n" result += ( tabulate( df, headers='keys', tablefmt='psql', @@ -2720,7 +2536,8 @@ def print_results(self, colored=True, colors=None, print_results=True): ) # connection properties - df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T']] + df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T']].copy() + df = df.astype(str) for c in df.index: if not self.get_conn(c).printout: df.drop([c], axis=0, inplace=True) @@ -2728,7 +2545,7 @@ def print_results(self, colored=True, colors=None, print_results=True): elif colored: conn = self.get_conn(c) for col in df.columns: - if conn.get_attr(col).val_set: + if conn.get_attr(col).is_set: df.loc[c, col] = ( coloring['set'] + str(conn.get_attr(col).val) + coloring['end']) @@ -2742,14 +2559,18 @@ def print_results(self, colored=True, colors=None, print_results=True): for b in self.busses.values(): if b.printout: df = self.results[b.label].loc[ - :, ['component value', 'bus value', 'efficiency']] + :, ['component value', 'bus value', 'efficiency'] + ].copy() df.loc['total'] = df.sum() df.loc['total', 'efficiency'] = np.nan - if colored and b.P.is_set: - df.loc['total', 'bus value'] = ( - coloring['set'] + str(df.loc['total', 'bus value']) + - coloring['end']) - result += ('\n##### RESULTS (Bus: ' + b.label + ') #####\n') + if colored: + df["bus value"] = df["bus value"].astype(str) + if b.P.is_set: + df.loc['total', 'bus value'] = ( + coloring['set'] + str(df.loc['total', 'bus value']) + + coloring['end'] + ) + result += f"\n##### RESULTS (Bus: {b.label}) #####\n" result += ( tabulate( df, headers='keys', tablefmt='psql', @@ -2793,18 +2614,24 @@ def print_components(self, c, *args): if not colored: return str(val) # else part - if (val < comp.get_attr(param).min_val - err or - val > comp.get_attr(param).max_val + err): - return coloring['err'] + ' ' + str(val) + ' ' + coloring['end'] + if (val < comp.get_attr(param).min_val - ERR or + val > comp.get_attr(param).max_val + ERR ): + return f"{coloring['err']} {val} {coloring['end']}" if comp.get_attr(args[0]).is_var: - return coloring['var'] + ' ' + str(val) + ' ' + coloring['end'] + return f"{coloring['var']} {val} {coloring['end']}" if comp.get_attr(args[0]).is_set: - return coloring['set'] + ' ' + str(val) + ' ' + coloring['end'] + return f"{coloring['set']} {val} {coloring['end']}" return str(val) else: return np.nan -# %% saving + def export(self, path): + """Export the network structure and parametrization.""" + path, path_comps = self._modify_export_paths(path) + self.export_network(path) + self.export_connections(path) + self.export_components(path_comps) + self.export_busses(path) def save(self, path, **kwargs): r""" @@ -2825,6 +2652,15 @@ def save(self, path, **kwargs): characteristics as well as .csv files for all types of components within your network. """ + path, path_comps = self._modify_export_paths(path) + + # save relevant design point information + self.save_connections(path) + self.save_components(path_comps) + self.save_busses(path) + + def _modify_export_paths(self, path): + if path[-1] != '/' and path[-1] != '\\': path += '/' path = hlp.modify_path_os(path) @@ -2839,14 +2675,9 @@ def save(self, path, **kwargs): if not os.path.exists(path_comps): os.makedirs(path_comps) - # save all network information - self.save_network(path + 'network.json') - self.save_connections(path + 'connections.csv') - self.save_components(path_comps) - self.save_busses(path_comps + 'bus.csv') - self.save_characteristics(path_comps) + return path, path_comps - def save_network(self, fn): + def export_network(self, fn): r""" Save basic network configuration. @@ -2855,21 +2686,8 @@ def save_network(self, fn): fn : str Path/filename for the network configuration file. """ - data = {} - data['m_unit'] = self.m_unit - data['m_range'] = list(self.m_range) - data['p_unit'] = self.p_unit - data['p_range'] = list(self.p_range) - data['h_unit'] = self.h_unit - data['h_range'] = list(self.h_range) - data['T_unit'] = self.T_unit - data['x_unit'] = self.x_unit - data['v_unit'] = self.v_unit - data['s_unit'] = self.s_unit - data['fluids'] = self.fluids_backends - - with open(fn, 'w') as f: - f.write(json.dumps(data, indent=4)) + with open(fn + 'network.json', 'w') as f: + f.write(json.dumps(self.serialize(), indent=4)) logger.debug('Network information saved to %s.', fn) @@ -2877,152 +2695,28 @@ def save_connections(self, fn): r""" Save the connection properties. - - Uses connections object id as row identifier and saves - - - connections source and target as well as - - properties with references and - - fluid vector (including user specification if structure is True). - - - Connections source and target are identified by its labels. - Parameters ---------- fn : str Path/filename for the file. """ - f = Network.get_props - df = self.conns.copy() - df.set_index('object', inplace=True) - # connection id - df['id'] = df.apply(Network.get_id, axis=1) - cols = df.columns.tolist() - df = df[cols[-1:] + cols[:-1]] - - # general connection parameters - # source - df['source'] = df.apply(f, axis=1, args=('source', 'label')) - # target - df['target'] = df.apply(f, axis=1, args=('target', 'label')) - - # design and offdesign properties - cols = ['label', 'design', 'offdesign', 'design_path', 'local_design', - 'local_offdesign', 'label'] - - for key in cols: - df[key] = df.apply(f, axis=1, args=(key,)) - - # fluid properties - cols = ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp'] - for key in cols: - # values and units - df[key] = df.apply(f, axis=1, args=(key, 'val')) - df[key + '_unit'] = df.apply(f, axis=1, args=(key, 'unit')) - df[key + '0'] = df.apply(f, axis=1, args=(key, 'val0')) - df[key + '_set'] = df.apply(f, axis=1, args=(key, 'val_set')) - df[key + '_ref'] = df.apply( - f, axis=1, args=(key, 'ref', 'obj',)).astype(str) - df[key + '_ref'] = df[key + '_ref'].str.extract( - r' at (.*?)>', expand=False) - df[key + '_ref_f'] = df.apply( - f, axis=1, args=(key, 'ref', 'factor',)) - df[key + '_ref_d'] = df.apply( - f, axis=1, args=(key, 'ref', 'delta',)) - df[key + '_ref_set'] = df.apply(f, axis=1, args=(key, 'ref_set',)) - - # state property - key = 'state' - df[key] = df.apply(f, axis=1, args=(key, 'val')) - df[key + '_set'] = df.apply(f, axis=1, args=(key, 'is_set')) - - # fluid composition - for val in self.fluids: - # fluid mass fraction - df[val] = df.apply(f, axis=1, args=('fluid', 'val', val)) - - # fluid mass fraction parametrisation - df[val + '0'] = df.apply(f, axis=1, args=('fluid', 'val0', val)) - df[val + '_set'] = df.apply( - f, axis=1, args=('fluid', 'val_set', val)) - - # fluid balance - df['balance'] = df.apply(f, axis=1, args=('fluid', 'balance')) - - df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan') + self.results["Connection"].to_csv( + fn + "connections.csv", sep=';', decimal='.', index=True, na_rep='nan' + ) logger.debug('Connection information saved to %s.', fn) def save_components(self, path): r""" Save the component properties. - - Uses components labels as row identifier. - - Writes: - - - component's incomming and outgoing connections (object id) and - - component's parametrisation. - Parameters ---------- path : str Path/filename for the file. """ - busses = self.busses.values() - # create / overwrite csv file - - df_comps = self.comps.copy() - df_comps.set_index('object', inplace=True) - - # busses - df_comps['busses'] = df_comps.apply( - Network.get_busses, axis=1, args=(busses,)) - - for var in ['param', 'P_ref', 'char', 'base']: - df_comps['bus_' + var] = df_comps.apply( - Network.get_bus_data, axis=1, args=(busses, var)) - - pd.options.mode.chained_assignment = None - f = Network.get_props - for c in df_comps['comp_type'].unique(): - df = df_comps[df_comps['comp_type'] == c] - - # basic information - cols = ['label', 'design', 'offdesign', 'design_path', - 'local_design', 'local_offdesign'] - for col in cols: - df[col] = df.apply(f, axis=1, args=(col,)) - - # attributes - for col, data in df.index[0].variables.items(): - # component characteristics container - if isinstance(data, dc_cc) or isinstance(data, dc_cm): - df[col] = df.apply( - f, axis=1, args=(col, 'char_func')).astype(str) - df[col] = df[col].str.extract(r' at (.*?)>', expand=False) - df[col + '_set'] = df.apply( - f, axis=1, args=(col, 'is_set')) - df[col + '_param'] = df.apply( - f, axis=1, args=(col, 'param')) - - # component property container - elif isinstance(data, dc_cp): - df[col] = df.apply(f, axis=1, args=(col, 'val')) - df[col + '_set'] = df.apply( - f, axis=1, args=(col, 'is_set')) - df[col + '_var'] = df.apply( - f, axis=1, args=(col, 'is_var')) - - # component property container - elif isinstance(data, dc_simple): - df[col] = df.apply(f, axis=1, args=(col, 'val')) - df[col + '_set'] = df.apply( - f, axis=1, args=(col, 'is_set')) - - # component property container - elif isinstance(data, dc_gcp): - df[col] = df.apply(f, axis=1, args=(col, 'method')) - - df.set_index('label', inplace=True) + for c in self.comps['comp_type'].unique(): fn = path + c + '.csv' - df.to_csv(fn, sep=';', decimal='.', index=True, na_rep='nan') + self.results[c].to_csv(fn, sep=';', decimal='.', index=True, na_rep='nan') logger.debug('Component information (%s) saved to %s.', c, fn) def save_busses(self, fn): @@ -3035,140 +2729,41 @@ def save_busses(self, fn): Path/filename for the file. """ if len(self.busses) > 0: - df = pd.DataFrame( - {'id': self.busses.values()}, index=self.busses.values(), - dtype='object') - df['label'] = df.apply(Network.get_props, axis=1, args=('label',)) - df['P'] = df.apply(Network.get_props, axis=1, args=('P', 'val')) - df['P_set'] = df.apply(Network.get_props, axis=1, - args=('P', 'is_set')) - df.drop('id', axis=1, inplace=True) - - df.set_index('label', inplace=True) - df.to_csv(fn, sep=';', decimal='.', index=True, na_rep='nan') + bus_data = {} + for label, bus in self.busses.items(): + bus_data[label] = self.results[label]["design value"].to_dict() + fn = fn + 'busses.json' + with open(fn, "w", encoding="utf-8") as f: + f.write(json.dumps(bus_data, indent=4)) logger.debug('Bus information saved to %s.', fn) - def save_characteristics(self, path): - r""" - Save the characteristics. - - Parameters - ---------- - fn : str - Path/filename for the file. - """ - # characteristic lines in components - char_lines = [] - char_maps = [] - for c in self.comps['object']: - for _col, data in c.variables.items(): - if isinstance(data, dc_cc): - char_lines += [data.char_func] - elif isinstance(data, dc_cm): - char_maps += [data.char_func] - - # characteristic lines in busses - for bus in self.busses.values(): - for c in bus.comps.index: - ch = bus.comps.loc[c, 'char'] - if ch not in char_lines: - char_lines += [ch] - - # characteristic line export - if len(char_lines) > 0: - # get id and data - df = pd.DataFrame( - {'id': char_lines}, index=char_lines, dtype='object') - df['id'] = df.apply(Network.get_id, axis=1) - df['type'] = df.apply(Network.get_class_base, axis=1) - - cols = ['x', 'y', 'extrapolate'] - for val in cols: - df[val] = df.apply(Network.get_props, axis=1, args=(val,)) - - # write to char.csv - fn = path + 'char_line.csv' - df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan') - logger.debug('Characteristic line information saved to %s.', fn) - - if len(char_maps) > 0: - # get id and data - df = pd.DataFrame( - {'id': char_maps}, index=char_maps, dtype='object') - df['id'] = df.apply(Network.get_id, axis=1) - df['type'] = df.apply(Network.get_class_base, axis=1) - - cols = ['x', 'y', 'z'] - for val in cols: - df[val] = df.apply(Network.get_props, axis=1, args=(val,)) - - # write to char_map.csv - fn = path + 'char_map.csv' - df.to_csv(fn, sep=';', decimal='.', index=False, na_rep='nan') - logger.debug('Characteristic map information saved to %s.', fn) - - @staticmethod - def get_id(c): - """Return the id of the python object.""" - return str(c.name)[str(c.name).find(' at ') + 4:-1] - - @staticmethod - def get_class_base(c): - """Return the class name.""" - return c.name.__class__.__name__ - - @staticmethod - def get_props(c, *args): - """Return properties.""" - if hasattr(c.name, args[0]): - if (not isinstance(c.name.get_attr(args[0]), int) and - not isinstance(c.name.get_attr(args[0]), str) and - not isinstance(c.name.get_attr(args[0]), float) and - not isinstance(c.name.get_attr(args[0]), list) and - not isinstance(c.name.get_attr(args[0]), np.ndarray) and - not isinstance(c.name.get_attr(args[0]), con.Connection)): - if len(args) == 1: - return c.name.get_attr(args[0]) - elif args[0] == 'fluid' and args[1] != 'balance': - return c.name.fluid.get_attr(args[1])[args[2]] - elif args[1] == 'ref': - obj = c.name.get_attr(args[0]).get_attr(args[1]) - if obj is not None: - return obj.get_attr(args[2]) - else: - return np.nan - else: - return c.name.get_attr(args[0]).get_attr(args[1]) - elif isinstance(c.name.get_attr(args[0]), np.ndarray): - if len(c.name.get_attr(args[0]).shape) > 1: - return tuple(c.name.get_attr(args[0]).tolist()) - else: - return c.name.get_attr(args[0]).tolist() - else: - return c.name.get_attr(args[0]) + def export_connections(self, fn): + connections = {} + for c in self.conns["object"]: + connections.update(c.serialize()) - @staticmethod - def get_busses(c, *args): - """Return the list of busses a component is integrated in.""" - busses = [] - for bus in args[0]: - if c.name in bus.comps.index: - busses += [bus.label] - return busses + fn = fn + "connections.json" + with open(fn, "w", encoding="utf-8") as f: + f.write(json.dumps(connections, indent=4).replace("NaN", "null")) + logger.debug('Connection information exported to %s.', fn) - @staticmethod - def get_bus_data(c, *args): - """Return bus information of a component.""" - items = [] - if args[1] == 'char': - for bus in args[0]: - if c.name in bus.comps.index: - val = bus.comps.loc[c.name, args[1]] - items += [str(val)[str(val).find(' at ') + 4:-1]] + def export_components(self, fn): + for c in self.comps["comp_type"].unique(): + components = {} + for cp in self.comps.loc[self.comps["comp_type"] == c, "object"]: + components.update(cp.serialize()) - else: - for bus in args[0]: - if c.name in bus.comps.index: - items += [bus.comps.loc[c.name, args[1]]] + fname = f"{fn}{c}.json" + with open(fname, "w", encoding="utf-8") as f: + f.write(json.dumps(components, indent=4).replace("NaN", "null")) + logger.debug('Component information exported to %s.', fname) - return items + def export_busses(self, fn): + if len(self.busses) > 0: + busses = {} + for bus in self.busses.values(): + busses.update(bus.serialize()) + fn = fn + 'busses.json' + with open(fn, "w", encoding="utf-8") as f: + f.write(json.dumps(busses, indent=4).replace("NaN", "null")) + logger.debug('Bus information exported to %s.', fn) diff --git a/src/tespy/networks/network_reader.py b/src/tespy/networks/network_reader.py index adca87c45..ecbe2bbac 100644 --- a/src/tespy/networks/network_reader.py +++ b/src/tespy/networks/network_reader.py @@ -12,7 +12,6 @@ SPDX-License-Identifier: MIT """ -import ast import json import os @@ -29,7 +28,6 @@ from tespy.components import HeatExchanger from tespy.components import HeatExchangerSimple from tespy.components import Merge -from tespy.components import ORCEvaporator from tespy.components import ParabolicTrough from tespy.components import Pipe from tespy.components import Pump @@ -52,21 +50,20 @@ from tespy.tools.characteristics import CharMap from tespy.tools.data_containers import ComponentCharacteristicMaps as dc_cm from tespy.tools.data_containers import ComponentCharacteristics as dc_cc -from tespy.tools.data_containers import ComponentProperties as dc_cp -from tespy.tools.data_containers import FluidComposition as dc_flu +from tespy.tools.data_containers import DataContainer as dc from tespy.tools.data_containers import FluidProperties as dc_prop -from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp -from tespy.tools.data_containers import SimpleDataContainer as dc_simple +from tespy.tools.fluid_properties.wrappers import CoolPropWrapper +from tespy.tools.fluid_properties.wrappers import IAPWSWrapper +from tespy.tools.fluid_properties.wrappers import PyromatWrapper from tespy.tools.helpers import modify_path_os -comp_target_classes = { +COMP_TARGET_CLASSES = { 'CycleCloser': CycleCloser, 'Sink': Sink, 'Source': Source, 'SubsystemInterface': SubsystemInterface, 'CombustionChamber': CombustionChamber, 'CombustionEngine': CombustionEngine, - 'ORCEvaporator': ORCEvaporator, 'Condenser': Condenser, 'Desuperheater': Desuperheater, 'HeatExchanger': HeatExchanger, @@ -87,8 +84,11 @@ 'Turbine': Turbine } -# %% network loading - +ENGINE_TARGET_CLASSES = { + "CoolPropWrapper": CoolPropWrapper, + "IAPWSWrapper": IAPWSWrapper, + "PyromatWrapper": PyromatWrapper, +} def load_network(path): r""" @@ -106,7 +106,7 @@ def load_network(path): Note ---- - If you save the network structure of an existing TESPy network, it will be + If you export the network structure of an existing TESPy network, it will be saved to the path you specified. The structure of the saved data in that path is the structure you need to provide in the path for loading the network. @@ -115,28 +115,12 @@ def load_network(path): - Folder: path (e.g. 'mynetwork') - Subfolder: components ('mynetwork/components') containing + {component_class_name}.json (e.g. HeatExchanger.json) - - bus.csv* - - char.csv* - - char_map.csv* - - component_class_name.csv (e.g. heat_exchanger.csv) - - - connections.csv + - connections.json + - busses.json - network.json - The imported network has the following additional features: - - - Connections are accessible by label, e.g. - :code:`myimportednetwork.get_conn('myconnection')`. The default label - logic is :code:`source:source_id_target:target_id`, where the source - means the label of the component the connection originates from and - target means the label of the component, the connections targets on. - - Components are accessible by label as well, e.g. for a component - 'heat exchanger' :code:`myimportednetwork.get_comp('heat exchanger')`. - - Busses are stored in a dict like structure, therefore accessible like - follows, e.g. a bus labeld 'power input' - :code:`myimportednetwork.busses['power input']`. - Example ------- Create a network and export it. This is followed by loading the network @@ -153,9 +137,7 @@ def load_network(path): >>> from tespy.connections import Connection, Ref, Bus >>> from tespy.networks import load_network, Network >>> import shutil - >>> fluid_list = ['CH4', 'O2', 'N2', 'CO2', 'H2O', 'Ar'] - >>> nw = Network(fluids=fluid_list, p_unit='bar', T_unit='C', - ... h_unit='kJ / kg', iterinfo=False) + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> air = Source('air') >>> f = Source('fuel') >>> c = Compressor('compressor') @@ -181,15 +163,13 @@ def load_network(path): ... offdesign=['char_map_eta_s', 'char_map_pr']) >>> t.set_attr(eta_s=0.9, design=['eta_s'], ... offdesign=['eta_s_char', 'cone']) - >>> inc.set_attr(fluid={'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'CH4': 0, - ... 'H2O': 0}, fluid_balance=True, T=25, p=1) - >>> fp.set_attr(fluid={'N2': 0, 'O2': 0, 'Ar': 0, 'CH4': 0.96, 'H2O': 0, - ... 'CO2': 0.04}, T=25, p=40) + >>> comb.set_attr(lamb=2) + >>> inc.set_attr(fluid={'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129}, T=25, p=1) + >>> fp.set_attr(fluid={'CH4': 0.96, 'CO2': 0.04}, T=25, p=40) >>> pc.set_attr(T=25) - >>> ct.set_attr(T=1100) >>> outg.set_attr(p=Ref(inc, 1, 0)) >>> power = Bus('total power output') - >>> power.add_comps({'comp': c}, {'comp': t}) + >>> power.add_comps({"comp": c, "base": "bus"}, {"comp": t}) >>> nw.add_busses(power) For a stable start, we specify the fresh air mass flow. @@ -202,19 +182,22 @@ def load_network(path): example in class :py:class:`tespy.connections.bus.Bus` provides more information on efficiencies of generators, for instance. - >>> inc.set_attr(m=np.nan) + >>> comb.set_attr(lamb=None) + >>> ct.set_attr(T=1100) + >>> inc.set_attr(m=None) >>> power.set_attr(P=-1e6) >>> nw.solve('design') >>> nw.lin_dep False - >>> nw.save('exported_nwk') + >>> nw.save('design_state') + >>> nw.export('exported_nwk') >>> mass_flow = round(nw.get_conn('ambient air').m.val_SI, 1) >>> c.set_attr(igva='var') - >>> nw.solve('offdesign', design_path='exported_nwk') + >>> nw.solve('offdesign', design_path='design_state') >>> round(t.eta_s.val, 1) 0.9 >>> power.set_attr(P=-0.75e6) - >>> nw.solve('offdesign', design_path='exported_nwk') + >>> nw.solve('offdesign', design_path='design_state') >>> nw.lin_dep False >>> eta_s_t = round(t.eta_s.val, 3) @@ -230,7 +213,7 @@ def load_network(path): >>> imported_nwk = load_network('exported_nwk') >>> imported_nwk.set_attr(iterinfo=False) - >>> imported_nwk.solve('design', init_path='exported_nwk') + >>> imported_nwk.solve('design') >>> imported_nwk.lin_dep False >>> round(imported_nwk.get_conn('ambient air').m.val_SI, 1) == mass_flow @@ -238,16 +221,17 @@ def load_network(path): >>> round(imported_nwk.get_comp('turbine').eta_s.val, 3) 0.9 >>> imported_nwk.get_comp('compressor').set_attr(igva='var') - >>> imported_nwk.solve('offdesign', design_path='exported_nwk') + >>> imported_nwk.solve('offdesign', design_path='design_state') >>> round(imported_nwk.get_comp('turbine').eta_s.val, 3) 0.9 >>> imported_nwk.busses['total power output'].set_attr(P=-0.75e6) - >>> imported_nwk.solve('offdesign', design_path='exported_nwk') + >>> imported_nwk.solve('offdesign', design_path='design_state') >>> round(imported_nwk.get_comp('turbine').eta_s.val, 3) == eta_s_t True >>> round(imported_nwk.get_comp('compressor').igva.val, 3) == igva True >>> shutil.rmtree('./exported_nwk', ignore_errors=True) + >>> shutil.rmtree('./design_state', ignore_errors=True) """ if path[-1] != '/' and path[-1] != '\\': path += '/' @@ -258,63 +242,22 @@ def load_network(path): msg = 'Reading network data from base path ' + path + '.' logger.info(msg) - # load characteristics - fn = path_comps + 'char_line.csv' - try: - char_lines = pd.read_csv(fn, sep=';', decimal='.', - converters={'x': ast.literal_eval, - 'y': ast.literal_eval}) - msg = 'Reading characteristic lines data from ' + fn + '.' - logger.debug(msg) + # load components + comps = {} - except FileNotFoundError: - char_lines = pd.DataFrame( - columns=['id', 'type', 'x', 'y'], dtype='object') + files = os.listdir(path_comps) + for f in files: + fn = path_comps + f + component = f.replace(".json", "") - # load characteristic maps - fn = path_comps + 'char_map.csv' - try: - msg = 'Reading characteristic maps data from ' + fn + '.' + msg = f"Reading component data ({component}) from {fn}." logger.debug(msg) - char_maps = pd.read_csv(fn, sep=';', decimal='.', - converters={'x': ast.literal_eval, - 'y': ast.literal_eval, - 'z': ast.literal_eval}) - except FileNotFoundError: - char_maps = pd.DataFrame( - columns=['id', 'type', 'x', 'y', 'z'], dtype='object') + with open(path_comps + f, "r", encoding="utf-8") as c: + data = json.loads(c.read()) - # load components - comps = pd.DataFrame(dtype='object') + comps.update(construct_components(component, data)) - files = os.listdir(path_comps) - for f in files: - if f != 'bus.csv' and f != 'char_line.csv' and f != 'char_map.csv': - fn = path_comps + f - df = pd.read_csv(fn, sep=';', decimal='.', - converters={'design': ast.literal_eval, - 'offdesign': ast.literal_eval, - 'busses': ast.literal_eval, - 'bus_param': ast.literal_eval, - 'bus_P_ref': ast.literal_eval, - 'bus_char': ast.literal_eval, - 'bus_base': ast.literal_eval}) - - # create components - df['instance'] = df.apply( - construct_components, axis=1, args=(char_lines, char_maps)) - - cols = [ - 'instance', 'label', 'busses', 'bus_param', 'bus_P_ref', - 'bus_char', 'bus_base'] - - comps = pd.concat((comps, df[cols]), axis=0) - - msg = 'Reading component data (' + f[:-4] + ') from ' + fn + '.' - logger.debug(msg) - - comps = comps.set_index('label') msg = 'Created network components.' logger.info(msg) @@ -322,53 +265,44 @@ def load_network(path): nw = construct_network(path) # load connections - fn = path + 'connections.csv' - conns = pd.read_csv(fn, sep=';', decimal='.', - converters={'design': ast.literal_eval, - 'offdesign': ast.literal_eval}) - - msg = 'Reading connection data from ' + fn + '.' + fn = path + 'connections.json' + msg = f"Reading connection data from {fn}." logger.debug(msg) - # create connections - conns['instance'] = conns.apply( - construct_connections, axis=1, args=(comps, nw,)) - conns.apply(conns_set_ref, axis=1, args=(conns,)) - conns = conns.set_index('id') + with open(fn, "r", encoding="utf-8") as c: + data = json.loads(c.read()) + + conns = construct_connections(data, comps) # add connections to network - for c in conns['instance']: + for c in conns.values(): nw.add_conns(c) msg = 'Created connections.' logger.info(msg) # load busses - try: - fn = path_comps + 'bus.csv' - busses = pd.read_csv(fn, sep=';', decimal='.') - msg = 'Reading bus data from ' + fn + '.' - logger.debug(msg) + fn = path + 'busses.json' + if os.path.isfile(fn): - except FileNotFoundError: - busses = pd.DataFrame(dtype='object') - msg = 'No bus data found!' + msg = f"Reading bus data from {fn}." logger.debug(msg) - # create busses - if len(busses) > 0: - busses['instance'] = busses.apply(construct_busses, axis=1) - - # add components to busses - comps.apply(busses_add_comps, axis=1, args=(busses, char_lines,)) + with open(fn, "r", encoding="utf-8") as c: + data = json.loads(c.read()) + busses = construct_busses(data, comps) # add busses to network - for b in busses['instance']: + for b in busses.values(): nw.add_busses(b) msg = 'Created busses.' logger.info(msg) + else: + msg = 'No bus data found!' + logger.debug(msg) + msg = 'Created network.' logger.info(msg) @@ -377,117 +311,46 @@ def load_network(path): return nw -# %% create components - - -def construct_components(c, *args): +def construct_components(component, data): r""" Create TESPy component from class name and set parameters. Parameters ---------- - c : pandas.core.series.Series - Component information from .csv-file. - - args[0] : pandas.core.frame.DataFrame - DataFrame containing the data of characteristic lines. + component : str + Name of the component class to be constructed. - args[1] : pandas.core.frame.DataFrame - DataFrame containing the data of characteristic maps. + data : dict + Dictionary with component information. Returns ------- - instance : tespy.components.component.Component - TESPy component object. + dict + Dictionary of all components of the specified type. """ - target_class = comp_target_classes[c['comp_type']] - instance = target_class(str(c['label'])) - kwargs = {} - - # basic properties - for key in ['design', 'offdesign', 'design_path', 'local_design', - 'local_offdesign']: - if key in c: - if isinstance(c[key], float): - kwargs[key] = None + target_class = COMP_TARGET_CLASSES[component] + instances = {} + for cp, cp_data in data.items(): + instances[cp] = target_class(cp) + for param, param_data in cp_data.items(): + container = instances[cp].get_attr(param) + if isinstance(container, dc): + if isinstance(container, dc_cc): + param_data["char_func"] = CharLine(**param_data["char_func"]) + elif isinstance(container, dc_cm): + param_data["char_func"] = CharMap(**param_data["char_func"]) + if isinstance(container, dc_prop): + param_data["val0"] = param_data["val"] + container.set_attr(**param_data) else: - kwargs[key] = c[key] - - for key, value in instance.variables.items(): - if key in c: - # component parameters - if isinstance(value, dc_cp): - kwargs[key] = { - 'val': c[key], - 'is_set': c[key + '_set'], - 'is_var': c[key + '_var']} - - # component parameters - elif isinstance(value, dc_simple): - instance.get_attr(key).set_attr( - **{'val': c[key], 'is_set': c[key + '_set']}) - - # component characteristics - elif isinstance(value, dc_cc): - # finding x and y values of the characteristic function - values = args[0]['id'] == c[key] - - try: - x = args[0][values].x.values[0] - y = args[0][values].y.values[0] - extrapolate = False - if 'extrapolate' in args[0].columns: - extrapolate = args[0][values].extrapolate.values[0] - char = CharLine(x=x, y=y, extrapolate=extrapolate) - - except IndexError: - - char = None - msg = ('Could not find x and y values for characteristic ' - 'line, using defaults instead for function ' + key + - ' at component ' + c.label + '.') - logger.warning(msg) - - kwargs[key] = { - 'is_set': c[key + '_set'], - 'param': c[key + '_param'], - 'char_func': char} - - # component characteristics - elif isinstance(value, dc_cm): - # finding x and y values of the characteristic function - values = args[1]['id'] == c[key] - - try: - x = list(args[1][values].x.values[0]) - y = list(args[1][values].y.values[0]) - z = list(args[1][values].z.values[0]) - char = CharMap(x=x, y=y, z=z) - - except IndexError: - char = None - msg = ('Could not find x, y and z values for ' - 'characteristic map of component ' + c.label + '!') - logger.warning(msg) - - kwargs[key] = { - 'is_set': c[key + '_set'], - 'param': c[key + '_param'], - 'char_func': char} - - # grouped component parameters - elif isinstance(value, dc_gcp): - kwargs[key] = {'method': c[key]} - - instance.set_attr(**kwargs) - return instance - -# %% create network object + instances[cp].set_attr(**{param: param_data}) + + return instances def construct_network(path): r""" - Create TESPy network from the data provided in the netw.csv-file. + Create TESPy network from the path provided by the user. Parameters ---------- @@ -499,171 +362,98 @@ def construct_network(path): nw : tespy.networks.network.Network TESPy network object. """ - # read network .csv-file + # read network .json-file with open(path + 'network.json', 'r') as f: data = json.loads(f.read()) - # construct fluid list - fluid_list = [ - backend + '::' + fluid for fluid, backend in data['fluids'].items()] - - # delete fluids from data - del data['fluids'] - # create network object with its properties - nw = Network(fluids=fluid_list, **data) + return Network(**data) - return nw - -# %% create connections - -def construct_connections(c, *args): +def construct_connections(data, comps): r""" - Create TESPy connection from data in the .csv-file and its parameters. + Create TESPy connection from data in the .json-file and its parameters. Parameters ---------- - c : pandas.core.series.Series - Connection information from .csv-file. + data : dict + Dictionary with connection data. - args[0] : pandas.core.frame.DataFrame - DataFrame containing all created components. + comps : dict + Dictionary of constructed components. Returns ------- - conn : tespy.connections.connection.Connection - TESPy connection object. + dict + Dictionary of TESPy connection objects. """ - # create connection - conn = Connection( - args[0].instance[c.source], c.source_id, - args[0].instance[c.target], c.target_id, label=str(c.label) - ) - - # read basic properties - for key in ['design', 'offdesign', 'design_path', 'local_design', - 'local_offdesign']: - if key in c: - if isinstance(c[key], float): - setattr(conn, key, None) + conns = {} + + arglist = [ + _ for _ in data[list(data.keys())[0]] + if _ not in ["source", "source_id", "target", "target_id", "label", "fluid"] + and "ref" not in _ + ] + arglist_ref = [_ for _ in data[list(data.keys())[0]] if "ref" in _] + for label, conn in data.items(): + conns[label] = Connection( + comps[conn["source"]], conn["source_id"], + comps[conn["target"]], conn["target_id"], + label=label + ) + for arg in arglist: + container = conns[label].get_attr(arg) + if isinstance(container, dc): + container.set_attr(**conn[arg]) else: - setattr(conn, key, c[key]) - - # read fluid properties - for key in ['m', 'p', 'h', 'T', 'x', 'v', 'Td_bp']: - if key in c: - setattr(conn, key, dc_prop( - val=c[key], val0=c[key + '0'], val_set=c[key + '_set'], - unit=c[key + '_unit'], ref=None, ref_set=c[key + '_ref_set'])) - - if 'state' in c: - conn.state = dc_simple(val=c[key], is_set=c[key + '_set']) - - # read fluid vector - val = {} - val0 = {} - val_set = {} - for key in args[1].fluids: - if key in c: - val[key] = c[key] - val0[key] = c[key + '0'] - val_set[key] = c[key + '_set'] - - conn.fluid = dc_flu( - val=val, val0=val0, val_set=val_set, balance=c['balance']) + conns[label].set_attr(**{arg: conn[arg]}) - # write properties to connection and return connection object - return conn - -# %% set references on connections - - -def conns_set_ref(c, *args): - r""" - Set references on connections as specified in connection data. - - Parameters - ---------- - c : pandas.core.series.Series - Connection information from .csv-file. + for f, engine in conn["fluid"]["engine"].items(): + conn["fluid"]["engine"][f] = ENGINE_TARGET_CLASSES[engine] - args[0] : pandas.core.frame.DataFrame - DataFrame containing all created connections. + conns[label].fluid.set_attr(**conn["fluid"]) + conns[label]._create_fluid_wrapper() - Returns - ------- - instance : tespy.connections.ref - TESPy reference object. - """ - for col in ['m', 'p', 'h', 'T']: - # search for referenced connections - if isinstance(c[col + '_ref'], str): - # create reference object - instance = args[0].instance[c[col + '_ref'] == - args[0]['id']].values[0] - # write to connection properties - c['instance'].get_attr(col).ref = Ref( - instance, c[col + '_ref_f'], c[col + '_ref_d']) + for label, conn in data.items(): + for arg in arglist_ref: + if len(conn[arg]) > 0: + param = arg.replace("_ref", "") + ref = Ref(conns[conn[arg]["conn"]], conn[arg]["factor"], conn[arg]["delta"]) + conns[label].set_attr(**{param: ref}) -# %% create busses + return conns -def construct_busses(c, *args): +def construct_busses(data, comps): r""" Create busses of the network. Parameters ---------- - c : pandas.core.series.Series - Bus information from .csv-file. + data : dict + Bus information from .json file. + + comps : dict + TESPy components dictionary. Returns ------- - b : tespy.connections.bus.Bus - TESPy bus object. - """ - # set up bus with label and specify value for power - b = Bus(str(c.label), P=c.P) - b.P.is_set = c.P_set - return b - -# %% add components to busses - - -def busses_add_comps(c, *args): - r""" - Add components to busses according to data from .csv file. - - Parameters - ---------- - c : pandas.core.series.Series - Component information from .csv-file. - - args[0] : pandas.core.frame.DataFrame - DataFrame containing all created busses. - - args[1] : pandas.core.frame.DataFrame - DataFrame containing all created characteristic lines. + dict + Dict with TESPy bus objects. """ - i = 0 - for b in c.busses: - param = c.bus_param[i] - P_ref = c.bus_P_ref[i] - char = c.bus_char[i] - base = 'component' - if 'bus_base' in c.index: - base = c.bus_base[i] - - values = char == args[1]['id'] - char = CharLine(x=args[1][values].x.values[0], - y=args[1][values].y.values[0]) - - # add component with corresponding details to bus - args[0].instance[b == args[0]['label']].values[0].add_comps({ - 'comp': c.instance, - 'param': param, - 'P_ref': P_ref, - 'char': char, - 'base': base}) - i += 1 + busses = {} + + for label, bus_data in data.items(): + busses[label] = Bus(label) + busses[label].P.set_attr(**bus_data["P"]) + + components = [_ for _ in bus_data if _ != "P"] + for cp in components: + char = CharLine(**bus_data[cp]["char"]) + component_data = { + "comp": comps[cp], "param": bus_data[cp]["param"], + "base": bus_data[cp]["base"], "char": char + } + busses[label].add_comps(component_data) + + return busses diff --git a/src/tespy/tools/analyses.py b/src/tespy/tools/analyses.py index 4e96d4eef..c3dba2de3 100644 --- a/src/tespy/tools/analyses.py +++ b/src/tespy/tools/analyses.py @@ -20,18 +20,19 @@ from tespy.tools import helpers as hlp from tespy.tools import logger +from tespy.tools.fluid_properties import single_fluid +from tespy.tools.global_vars import ERR from tespy.tools.global_vars import combustion_gases -from tespy.tools.global_vars import err idx = pd.IndexSlice def categorize_fluids(conn): - fluid = hlp.single_fluid(conn.fluid.val) + fluid = single_fluid(conn.fluid_data) if fluid is None: cat = "non-combustion-gas" for f, x in conn.fluid.val.items(): - if x > err: + if x > ERR : try: if hlp.fluidalias_in_list(f, combustion_gases): cat = "combustion-gas" @@ -175,8 +176,7 @@ def __init__(self, network, E_F, E_P, E_L=[], internal_busses=[]): >>> Tamb = 20 >>> pamb = 1 - >>> fluids = ['water'] - >>> nw = Network(fluids=fluids) + >>> nw = Network() >>> nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg', ... iterinfo=False) @@ -346,12 +346,20 @@ def analyse(self, pamb, Tamb, Chem_Ex=None): Tamb_SI = hlp.convert_to_SI('T', Tamb, self.nw.T_unit) # reset data + dtypes = { + "E_F": float, + "E_P": float, + "E_D": float, + "epsilon": float, + "group": str + } self.component_data = pd.DataFrame( - columns=['E_F', 'E_P', 'E_D', 'epsilon', 'group'], dtype='float64' - ) + columns=list(dtypes.keys()) + ).astype(dtypes) self.bus_data = self.component_data.copy() - self.bus_data['base'] = np.nan + self.bus_data["base"] = np.nan + self.bus_data["base"] = self.bus_data["base"].astype(str) conn_exergy_data_cols = ['e_PH', 'e_T', 'e_M', 'E_PH', 'E_T', 'E_M'] if Chem_Ex is not None: @@ -401,8 +409,6 @@ def analyse(self, pamb, Tamb, Chem_Ex=None): for cp in self.nw.comps['object']: # save component information cp.exergy_balance(Tamb_SI) - if not hasattr(cp, 'fkt_group'): - cp.fkt_group = cp.label self.component_data.loc[cp.label] = [ cp.E_F, cp.E_P, cp.E_D, cp.epsilon, cp.fkt_group ] @@ -473,11 +479,11 @@ def analyse(self, pamb, Tamb, Chem_Ex=None): self.network_data.loc['E_D'] - self.network_data.loc['E_L'] ) - if residual >= err ** 0.5: + if residual >= ERR ** 0.5: msg = ( 'The exergy balance of your network is not closed (residual ' 'value is ' + str(round(residual, 6)) + ', but should be ' - 'smaller than ' + str(err ** 0.5) + '), you should check the ' + 'smaller than ' + str(ERR ** 0.5) + '), you should check the ' 'component and network exergy data and check, if network is ' 'properly setup for the exergy analysis.') logger.error(msg) diff --git a/src/tespy/tools/characteristics.py b/src/tespy/tools/characteristics.py index 4ab408d0d..3fa27cc85 100644 --- a/src/tespy/tools/characteristics.py +++ b/src/tespy/tools/characteristics.py @@ -13,7 +13,6 @@ SPDX-License-Identifier: MIT """ - import json import os @@ -23,8 +22,6 @@ from tespy.tools import logger from tespy.tools.helpers import extend_basic_path -# %% - class CharLine: r""" @@ -167,6 +164,13 @@ def get_attr(self, key): logger.error(msg) raise KeyError(msg) + def serialize(self): + export = {} + export["x"] = self.x.tolist() + export["y"] = self.y.tolist() + export["extrapolate"] = self.extrapolate + return export + def plot(self, path, title, xlabel, ylabel): from matplotlib import pyplot as plt @@ -185,8 +189,6 @@ def plot(self, path, title, xlabel, ylabel): fig.savefig(path, bbox_inches='tight') plt.close(fig) -# %% - class CharMap: r""" @@ -445,6 +447,13 @@ def get_attr(self, key): logger.error(msg) raise KeyError(msg) + def serialize(self): + export = {} + export["x"] = self.x.tolist() + export["y"] = self.y.tolist() + export["z"] = self.z.tolist() + return export + def plot(self, path, title, xlabel, ylabel): from matplotlib import pyplot as plt @@ -489,9 +498,9 @@ def load_default_char(component, parameter, function_name, char_type): The characteristics (CharLine, CharMap) object. """ if char_type == CharLine: - path = __datapath__ + 'char_lines.json' + path = os.path.join(__datapath__, 'char_lines.json') else: - path = __datapath__ + 'char_maps.json' + path = os.path.join(__datapath__, 'char_maps.json') with open(path) as f: data = json.loads(f.read()) diff --git a/src/tespy/tools/data_containers.py b/src/tespy/tools/data_containers.py index cf6442b65..f55447d32 100644 --- a/src/tespy/tools/data_containers.py +++ b/src/tespy/tools/data_containers.py @@ -12,15 +12,12 @@ SPDX-License-Identifier: MIT """ - import collections import numpy as np from tespy.tools import logger -# %% - class DataContainer: """ @@ -79,16 +76,15 @@ class DataContainer: ... max_val=1000, min_val=1)) >>> pi = Pipe('testpipe', L=100, D=0.5, ks=5e-5) - >>> type(GroupedComponentProperties(is_set=True, - ... elements=[pi.L, pi.D, pi.ks], method='default')) + >>> type(GroupedComponentProperties( + ... is_set=True, elements=["L", "D", "ks"] + ... )) >>> type(FluidComposition( - ... val={'CO2': 0.1, 'H2O': 0.11, 'N2': 0.75, 'O2': 0.03}, - ... val_set={'CO2': False, 'H2O': False, 'N2': False, 'O2': True}, - ... balance=False)) + ... val={'CO2': 0.1, 'H2O': 0.11, 'N2': 0.75, 'O2': 0.03}, is_set={'O2'} + ... )) - >>> type(FluidProperties(val=5, val_SI=500000, val_set=True, unit='bar', - ... ref=None, ref_set=False)) + >>> type(FluidProperties(val=5, val_SI=500000, is_set=True, unit='bar')) >>> type(SimpleDataContainer(val=5, is_set=False)) @@ -121,8 +117,10 @@ def set_attr(self, **kwargs): self.__dict__.update({key: kwargs[key]}) else: - msg = ('Data container of type ' + self.__class__.__name__ + - ' has no attribute ' + key + '.') + msg = ( + f"Datacontainer of type {self.__class__.__name__} has no " + f"attribute \"{key}\"." + ) logger.error(msg) raise KeyError(msg) @@ -143,8 +141,10 @@ def get_attr(self, key): if key in self.__dict__: return self.__dict__[key] else: - msg = ('Datacontainer of type ' + self.__class__.__name__ + - ' has no attribute \"' + str(key) + '\".') + msg = ( + f"Datacontainer of type {self.__class__.__name__} has no " + f"attribute \"{key}\"." + ) logger.error(msg) raise KeyError(msg) @@ -161,6 +161,9 @@ def attr(): """ return {} + def serialize(self): + return {} + class ComponentCharacteristics(DataContainer): """ @@ -195,7 +198,17 @@ def attr(): 'char_func': None, 'is_set': False, 'param': None, 'func_params': {}, 'func': None, 'deriv': None, 'latex': None, 'char_params': {'type': 'rel', 'inconn': 0, 'outconn': 0}, - 'num_eq': 0} + 'num_eq': 0 + } + + def serialize(self): + export = {} + if self.char_func is not None: + export.update({"char_func": self.char_func.serialize()}) + + for k in ["is_set", "param", "char_params"]: + export.update({k: self.get_attr(k)}) + return export class ComponentCharacteristicMaps(DataContainer): @@ -230,7 +243,17 @@ def attr(): return { 'char_func': None, 'is_set': False, 'param': None, 'latex': None, 'func_params': {}, 'func': None, 'deriv': None, - 'num_eq': 0} + 'num_eq': 0 + } + + def serialize(self): + export = {} + if self.char_func is not None: + export.update({"char_func": self.char_func.serialize()}) + + for k in ["is_set", "param"]: + export.update({k: self.get_attr(k)}) + return export class ComponentProperties(DataContainer): @@ -279,9 +302,20 @@ def attr(): return { 'val': 1, 'val_SI': 0, 'is_set': False, 'd': 1e-4, 'min_val': -1e12, 'max_val': 1e12, 'is_var': False, - 'val_ref': 1, 'design': np.nan, 'is_result': False, + 'design': np.nan, 'is_result': False, 'num_eq': 0, 'func_params': {}, 'func': None, 'deriv': None, - 'latex': None} + 'latex': None + } + + def serialize(self): + keys = self._serializable_keys() + return {k: self.get_attr(k) for k in keys} + + @staticmethod + def _serializable_keys(): + return [ + "val", "val_SI", "is_set", "d", "min_val", "max_val", "is_var", + ] class FluidComposition(DataContainer): @@ -299,8 +333,8 @@ class FluidComposition(DataContainer): default: val0={}. Pattern for dictionary: keys are fluid name, values are mass fractions. - val_set : dict - Which fluid mass fractions have been set, default val_set={}. + is_set : dict + Which fluid mass fractions have been set, default is_set={}. Pattern for dictionary: keys are fluid name, values are True or False. balance : boolean @@ -319,8 +353,24 @@ def attr(): Dictionary of available attributes (dictionary keys) with default values. """ - return {'val': {}, 'val0': {}, 'val_set': {}, - 'design': collections.OrderedDict(), 'balance': False} + return { + 'val': collections.OrderedDict(), + 'val0': collections.OrderedDict(), + 'is_set': set(), + 'design': collections.OrderedDict(), + 'wrapper': collections.OrderedDict(), + 'back_end': collections.OrderedDict(), + 'engine': collections.OrderedDict(), + "is_var": set(), + "J_col": collections.OrderedDict(), + } + + def serialize(self): + export = {"val": self.val} + export["is_set"] = list(self.is_set) + export["engine"] = {k: e.__name__ for k, e in self.engine.items()} + export["back_end"] = {k: b for k, b in self.back_end.items()} + return export class GroupedComponentProperties(DataContainer): @@ -354,9 +404,11 @@ def attr(): Dictionary of available attributes (dictionary keys) with default values. """ - return {'is_set': False, 'method': 'default', 'elements': [], - 'func': None, 'deriv': None, 'num_eq': 0, 'latex': None, - 'func_params': {}} + return { + 'is_set': False, 'elements': [], + 'func': None, 'deriv': None, 'num_eq': 0, 'latex': None, + 'func_params': {} + } class GroupedComponentCharacteristics(DataContainer): @@ -386,8 +438,10 @@ def attr(): Dictionary of available attributes (dictionary keys) with default values. """ - return {'is_set': False, 'elements': [], 'func': None, 'deriv': None, - 'num_eq': 0, 'latex': None, 'func_params': {}} + return { + 'is_set': False, 'elements': [], 'func': None, 'deriv': None, + 'num_eq': 0, 'latex': None, 'func_params': {} + } class FluidProperties(DataContainer): @@ -407,15 +461,8 @@ class FluidProperties(DataContainer): val_SI : float Value in SI_unit, default: val_SI=0. - val_set : boolean - Has the value for this property been set? default: val_set=False. - - ref : tespy.connections.ref - Reference object, default: ref=None. - - ref_set : boolean - Has a value for this property been referenced to another connection? - default: ref_set=False. + is_set : boolean + Has the value for this property been set? default: is_set=False. unit : str Unit for this property, default: ref=None. @@ -436,8 +483,63 @@ def attr(): Dictionary of available attributes (dictionary keys) with default values. """ - return {'val': np.nan, 'val0': np.nan, 'val_SI': 0, 'val_set': False, - 'ref': None, 'ref_set': False, 'unit': None, 'design': np.nan} + return { + 'design': np.nan, + 'val': np.nan, + 'val0': np.nan, + 'val_SI': 0, + 'unit': None, + 'is_set': False, + "is_var": False, + "func": None, + "deriv": None, + "constant_deriv": False, + "latex": None, + "num_eq": 0, + "J_col": None, + "func_params": {}, + "_solved": False + } + + def serialize(self): + keys = ["val", "val0", "val_SI", "is_set", "unit"] + return {k: self.get_attr(k) for k in keys} + + +class ReferencedFluidProperties(DataContainer): + + @staticmethod + def attr(): + r""" + Return the available attributes for a FluidProperties type object. + + Returns + ------- + out : dict + Dictionary of available attributes (dictionary keys) with default + values. + """ + return { + "ref": None, + "is_set": False, + "unit": None, + "func": None, + "deriv": None, + "num_eq": 0, + "func_params": {}, + "_solved": False + } + + def serialize(self): + if self.ref is not None: + keys = ["is_set", "unit"] + export = {k: self.get_attr(k) for k in keys} + export["conn"] = self.ref.obj.label + export["factor"] = self.ref.factor + export["delta"] = self.ref.delta + return export + else: + return {} class SimpleDataContainer(DataContainer): @@ -450,7 +552,7 @@ class SimpleDataContainer(DataContainer): Value for the property, no predefined datatype. is_set : boolean - Has the value for this property been set? default: val_set=False. + Has the value for this property been set? default: is_set=False. """ @staticmethod @@ -465,6 +567,15 @@ def attr(): values. """ return { - 'val': np.nan, 'is_set': False, - 'func_params': {}, 'func': None, 'deriv': None, 'latex': None, - 'num_eq': 0} + "val": np.nan, + "is_set": False, + "func_params": {}, + "func": None, + "deriv": None, + "latex": None, + "num_eq": 0, + "_solved": False + } + + def serialize(self): + return {"val": self.val, "is_set": self.is_set} diff --git a/src/tespy/tools/document_models.py b/src/tespy/tools/document_models.py index ad51f9ba6..4c26fb92b 100644 --- a/src/tespy/tools/document_models.py +++ b/src/tespy/tools/document_models.py @@ -101,16 +101,18 @@ def set_defaults(nw): } classes = [ - nw.comps[nw.comps['comp_type'] == cp]['object'][0] - for cp in nw.comps['comp_type'].unique()] + nw.comps[nw.comps['comp_type'] == cp]['object'].iloc[0] + for cp in nw.comps['comp_type'].unique() + ] for c in classes: rpt[c.__class__.__name__] = {'params': []} - rpt[c.__class__.__name__].update({ - param: {'float_fmt': '{:,.2f}'} - for param, data in c.variables.items() - if isinstance(data, dc_cp) - }) + if hasattr(c, "parameters"): + rpt[c.__class__.__name__].update({ + param: {'float_fmt': '{:,.2f}'} + for param, data in c.parameters.items() + if isinstance(data, dc_cp) + }) rpt['Connection']['fluid'] = { 'float_fmt': '{:.3f}', 'include_results': True} @@ -242,46 +244,45 @@ def document_connections(nw, rpt): ref_data = {'m': [], 'p': [], 'h': [], 'T': []} cols = nw.results['Connection'].columns - conn_data = nw.results['Connection'].copy().loc[:, ~cols.isin(nw.fluids)] - fluid_data = nw.results['Connection'].copy().loc[:, nw.fluids] + property_cols = [c for c in cols[~cols.isin(nw.all_fluids)] if "unit" not in c] + property_data = nw.results['Connection'].copy().loc[:, property_cols] + fluid_data = nw.results['Connection'].copy().loc[:, list(nw.all_fluids)] specs = nw.specifications['Connection'].copy() if not rpt['include_results']: - conn_data = conn_data[specs] + property_data = property_data[specs] fluid_data = fluid_data[specs] # it is possible to exclude fluid results elif not rpt['Connection']['fluid']['include_results']: fluid_data = fluid_data[specs] - ref_spec = nw.specifications['Ref'].dropna( - how='all').dropna(how='all', axis=1) - + ref_spec = nw.specifications['Ref'] # get some Connection object for equation generator c = nw.get_conn(specs.index[0]) - for c in nw.get_conn(ref_spec.index): - for param in ref_data.keys(): - if c.get_attr(param).ref_set: + for c in nw.get_conn(ref_spec.any(axis=1).index): + for param in ref_spec.columns: + if c.get_attr(param).is_set: ref_dict = {'label': c.label.replace('_', r'\_')} ref_dict.update( {'reference': - c.get_attr(param).ref.obj.label.replace('_', r'\_'), - 'factor in -': c.get_attr(param).ref.factor, + c.get_attr(param).val.obj.label.replace('_', r'\_'), + 'factor in -': c.get_attr(param).val.factor, 'delta in ' + hlp.latex_unit( - nw.get_attr(param + '_unit')): - c.get_attr(param).ref.delta}) + nw.get_attr(param.split("_ref")[0] + '_unit')): + c.get_attr(param).val.delta}) - ref_data[param] += [ref_dict] + ref_data[param.split("_ref")[0]] += [ref_dict] latex = r'\section{Connections in ' + nw.mode + ' mode}' + '\n\n' # if list is empty, all parameters will be included if len(rpt['Connection']['params']) > 0: - for col in conn_data.columns: + for col in property_data.columns: if col not in rpt['Connection']['params'] and not any(specs[col]): - conn_data[col] = np.nan + property_data[col] = np.nan - df = data_to_df(conn_data) + df = data_to_df(property_data) if len(df) > 0: eqs = df[specs].dropna(how='all').dropna(how='all', axis=1).columns latex += document_connection_params(nw, df, specs, eqs, c, rpt) @@ -333,6 +334,7 @@ def document_connection_params(nw, df, specs, eqs, c, rpt): label = 'Specified connection parameters' latex = r'\subsection{' + label + '}' + '\n\n' + df_out = df.astype(str) equations = '' for col in df.columns: unit = col + '_unit' @@ -350,15 +352,15 @@ def document_connection_params(nw, df, specs, eqs, c, rpt): for row in df.index: fmt = rpt['Connection'][col]['float_fmt'] if specs.loc[row, col] and rpt['include_results']: - df.loc[row, col] = r'\bftab ' + fmt.format(df.loc[row, col]) + df_out.loc[row, col] = r'\bftab ' + fmt.format(df.loc[row, col]) else: - df.loc[row, col] = fmt.format(df.loc[row, col]) + df_out.loc[row, col] = fmt.format(df.loc[row, col]) - df.rename(columns={col: col_header}, inplace=True) + df_out.rename(columns={col: col_header}, inplace=True) - num_col = len(df.columns) + num_col = len(df_out.columns) - latex += create_latex_table(df, label, col_fmt='l' + num_col * 'r') + latex += create_latex_table(df_out, label, col_fmt='l' + num_col * 'r') latex += r'\subsection{Equations applied}' + '\n\n' latex += equations return latex @@ -392,6 +394,7 @@ def document_connection_fluids(df, specs, eqs, c, rpt): label = 'Specified fluids' latex = r'\subsection{' + label + '}' + '\n\n' + df_out = df.astype(str) equations = '' fmt = rpt['Connection']['fluid']['float_fmt'] for col in eqs: @@ -406,19 +409,20 @@ def document_connection_fluids(df, specs, eqs, c, rpt): for row in df.index: if specs.loc[row, col] and rpt['include_results']: - df.loc[row, col] = r'\bftab ' + fmt.format( - df.loc[row, col]) + df_out.loc[row, col] = r'\bftab ' + fmt.format( + df.loc[row, col] + ) else: - df.loc[row, col] = fmt.format(df.loc[row, col]) + df_out.loc[row, col] = fmt.format(df.loc[row, col]) col_header = ( col.replace('_', r'\_') + ' (' r'\ref{eq:Connection_' + col + '})') - df.rename(columns={col: col_header}, inplace=True) + df_out.rename(columns={col: col_header}, inplace=True) - num_col = len(df.columns) + num_col = len(df_out.columns) - latex += create_latex_table(df, label, col_fmt='l' + num_col * 'r') + latex += create_latex_table(df_out, label, col_fmt='l' + num_col * 'r') latex += r'\subsection{Equations applied}' + '\n\n' latex += equations return latex @@ -577,7 +581,7 @@ def get_component_mandatory_constraints(cp, component_list, path): num_mandatory_eq = 0 mandatory_eq = '' figures = [] - for label, data in component_list[0].constraints.items(): + for label, data in component_list.iloc[0].constraints.items(): if 'char' in data: for component in component_list: local_path = ( @@ -641,20 +645,22 @@ def get_component_specifications(nw, cp, rpt): and not any(specs['variables'][col])): result[col] = np.nan - result = result.dropna(how='all', axis=1) + result_out = result.dropna(how='all', axis=1).astype(str) cols = result.columns.tolist() for col in cols: fmt = rpt[cp][col]['float_fmt'] for row in result.index: if specs['variables'].loc[row, col]: - result.loc[row, col] = ( - r'\iftab ' + fmt.format(result.loc[row, col])) + result_out.loc[row, col] = ( + r'\iftab ' + fmt.format(result.loc[row, col]) + ) elif specs['properties'].loc[row, col] and rpt['include_results']: - result.loc[row, col] = ( - r'\bftab ' + fmt.format(result.loc[row, col])) + result_out.loc[row, col] = ( + r'\bftab ' + fmt.format(result.loc[row, col]) + ) else: - result.loc[row, col] = fmt.format(result.loc[row, col]) + result_out.loc[row, col] = fmt.format(result.loc[row, col]) group_data = specs['groups'][specs['groups']].dropna(how='all', axis=1) char_data = specs['chars'][specs['chars']].dropna(how='all', axis=1) @@ -663,7 +669,7 @@ def get_component_specifications(nw, cp, rpt): [specs['properties'] | specs['variables'], specs['groups'], specs['chars']], axis=1) - df_data = pd.concat([result, group_data, char_data], axis=1) + df_data = pd.concat([result_out, group_data, char_data], axis=1) for col in char_data.columns: for row in char_data.index: diff --git a/src/tespy/tools/fluid_properties.py b/src/tespy/tools/fluid_properties.py deleted file mode 100644 index 2525ba86c..000000000 --- a/src/tespy/tools/fluid_properties.py +++ /dev/null @@ -1,1816 +0,0 @@ -# -*- coding: utf-8 - -"""Module for fluid property integration. - -TESPy uses the CoolProp python interface for all fluid property functions. - - -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/tools/fluid_properties.py - -SPDX-License-Identifier: MIT -""" - -import CoolProp as CP -import numpy as np -from CoolProp.CoolProp import PropsSI as CPPSI -from CoolProp.CoolProp import get_aliases - -from tespy.tools import logger -from tespy.tools.global_vars import err -from tespy.tools.global_vars import gas_constants -from tespy.tools.global_vars import molar_masses -from tespy.tools.helpers import molar_mass_flow -from tespy.tools.helpers import newton -from tespy.tools.helpers import single_fluid - - -class Memorise: - r"""Memorization of fluid properties.""" - - @staticmethod - def add_fluids(fluids, memorise_fluid_properties=True): - r""" - Add list of fluids to fluid memorisation class. - - - Generate arrays for fluid property lookup if memorisation is - activated. - - Calculate/set fluid property value ranges for convergence checks. - - Parameters - ---------- - fluids : dict - Dict of fluid and corresponding CoolProp back end for fluid - property memorization. - - memorise_fluid_properties : boolean - Activate or deactivate fluid property value memorisation. Default - state is activated (:code:`True`). - - Note - ---- - The Memorise class creates globally accessible variables for different - fluid property calls as dictionaries: - - - T(p,h) - - T(p,s) - - v(p,h) - - visc(p,h) - - s(p,h) - - Each dictionary uses the list of fluids passed to the Memorise class as - identifier for the fluid property memorisation. The fluid properties - are stored as numpy array, where each column represents the mass - fraction of the respective fluid and the additional columns are the - values for the fluid properties. The fluid property function will then - look for identical fluid property inputs (p, h, (s), fluid mass - fraction). If the inputs are in the array, the first column of that row - is returned, see example. - - Example - ------- - T(p,h) for set of fluids ('water', 'air'): - - - row 1: [282.64527752319697, 10000, 40000, 1, 0] - - row 2: [284.3140698256616, 10000, 47000, 1, 0] - """ - # number of fluids - num_fl = len(fluids) - if memorise_fluid_properties and num_fl > 0: - fl = tuple(fluids.keys()) - # fluid property tables - Memorise.T_ph[fl] = np.empty((0, num_fl + 4), float) - Memorise.T_ps[fl] = np.empty((0, num_fl + 5), float) - Memorise.v_ph[fl] = np.empty((0, num_fl + 4), float) - Memorise.visc_ph[fl] = np.empty((0, num_fl + 4), float) - Memorise.s_ph[fl] = np.empty((0, num_fl + 4), float) - - msg = ( - 'Added fluids ' + ', '.join(fl) + - ' to memorise lookup tables.') - logger.debug(msg) - - Memorise.water = None - for f, back_end in fluids.items(): - - # save name for water in memorise - if f in get_aliases("H2O"): - Memorise.water = f - - if f in Memorise.state: - del Memorise.state[f] - - # create CoolProp.AbstractState object - try: - Memorise.state[f] = CP.AbstractState(back_end, f) - Memorise.back_end[f] = back_end - except ValueError: - msg = ( - 'Could not find the fluid "' + f + '" in the fluid ' - 'property database.' - ) - logger.warning(msg) - continue - - msg = ( - 'Created CoolProp.AbstractState object for fluid ' + - f + ' with back end ' + back_end + '.') - logger.debug(msg) - # pressure range - try: - pmin = Memorise.state[f].trivial_keyed_output(CP.iP_min) - pmax = Memorise.state[f].trivial_keyed_output(CP.iP_max) - except ValueError: - pmin = 1e4 - pmax = 1e8 - msg = ( - 'Could not find values for maximum and minimum ' - 'pressure.') - logger.warning(msg) - - # temperature range - Tmin = Memorise.state[f].trivial_keyed_output(CP.iT_min) - Tmax = Memorise.state[f].trivial_keyed_output(CP.iT_max) - - # value range for fluid properties - Memorise.value_range[f] = [pmin, pmax, Tmin, Tmax] - - try: - molar_masses[f] = Memorise.state[f].molar_mass() - gas_constants[f] = Memorise.state[f].gas_constant() - except ValueError: - try: - molar_masses[f] = CPPSI('M', f) - gas_constants[f] = CPPSI('GAS_CONSTANT', f) - except ValueError: - molar_masses[f] = 1 - gas_constants[f] = 1 - msg = ( - 'Could not find values for molar mass and gas ' - 'constant.') - logger.warning(msg) - - msg = ( - 'Specifying fluid property ranges for pressure and ' - 'temperature for convergence check of fluid ' + f + '.') - logger.debug(msg) - - @staticmethod - def del_memory(fluids): - r""" - Delete non frequently used fluid property values from memorise class. - - Parameters - ---------- - fluids : list - List of fluid for fluid property memorization. - """ - fl = tuple(fluids) - threshold = 3 - try: - # delete memory - Memorise.s_ph[fl] = Memorise.s_ph[fl][ - Memorise.s_ph[fl][:, -1] > threshold] - Memorise.s_ph[fl][:, -1] = 0 - - Memorise.T_ph[fl] = Memorise.T_ph[fl][ - Memorise.T_ph[fl][:, -1] > threshold] - Memorise.T_ph[fl][:, -1] = 0 - - Memorise.T_ps[fl] = Memorise.T_ps[fl][ - Memorise.T_ps[fl][:, -1] > threshold] - Memorise.T_ps[fl][:, -1] = 0 - - Memorise.v_ph[fl] = Memorise.v_ph[fl][ - Memorise.v_ph[fl][:, -1] > threshold] - Memorise.v_ph[fl][:, -1] = 0 - - Memorise.visc_ph[fl] = Memorise.visc_ph[fl][ - Memorise.visc_ph[fl][:, -1] > threshold] - Memorise.visc_ph[fl][:, -1] = 0 - - msg = ('Dropping not frequently used fluid property values from ' - 'memorise class for fluids ' + ', '.join(fl) + '.') - logger.debug(msg) - except KeyError: - pass - - -# create memorise dictionaries -Memorise.state = {} -Memorise.back_end = {} -Memorise.T_ph = {} -Memorise.T_ps = {} -Memorise.v_ph = {} -Memorise.visc_ph = {} -Memorise.s_ph = {} -Memorise.value_range = {} - - -def T_mix_ph(flow, T0=675): - r""" - Calculate the temperature from pressure and enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - T : float - Temperature T / K. - - Note - ---- - First, check if fluid property has been memorised already. - If this is the case, return stored value, otherwise calculate value and - store it in the memorisation class. - - Uses CoolProp interface for pure fluids, newton algorithm for mixtures: - - .. math:: - - T_{mix}\left(p,h\right) = T_{i}\left(p,h_{i}\right)\; - \forall i \in \text{fluid components}\\ - - h_{i} = h \left(pp_{i}, T_{mix} \right)\\ - pp: \text{partial pressure} - """ - # check if fluid properties have been calculated before - fl = tuple(flow[3].keys()) - memorisation = fl in Memorise.T_ph - if memorisation: - a = Memorise.T_ph[fl][:, :-2] - b = np.array([flow[1], flow[2]] + list(flow[3].values())) - ix = np.where(np.all(abs(a - b) <= err, axis=1))[0] - - if ix.size == 1: - # known fluid properties - Memorise.T_ph[fl][ix, -1] += 1 - return Memorise.T_ph[fl][ix, -2][0] - - # unknown fluid properties - fluid = single_fluid(flow[3]) - if fluid is None: - # calculate the fluid properties for fluid mixtures - valmin = max( - [Memorise.value_range[f][2] for f in fl if flow[3][f] > err] - ) + 0.1 - if T0 < valmin or np.isnan(T0): - T0 = valmin * 1.1 - - val = newton(h_mix_pT, dh_mix_pdT, flow, flow[2], val0=T0, - valmin=valmin, valmax=3000, imax=10) - else: - # calculate fluid property for pure fluids - val = T_ph(flow[1], flow[2], fluid) - - if memorisation: - # memorise the newly calculated value - new = np.asarray( - [[flow[1], flow[2]] + list(flow[3].values()) + [val, 0]]) - Memorise.T_ph[fl] = np.append(Memorise.T_ph[fl], new, axis=0) - - return val - - -def T_ph(p, h, fluid): - r""" - Calculate the temperature from pressure and enthalpy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - T : float - Temperature T / K. - """ - if Memorise.back_end[fluid] == 'IF97': - return entropy_iteration_IF97(p, h, fluid, 'T') - else: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - return Memorise.state[fluid].T() - - -def dT_mix_dph(flow, T0=675): - r""" - Calculate partial derivate of temperature to pressure. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dT / dp : float - Partial derivative of temperature to pressure dT /dp / (K/Pa). - - .. math:: - - \frac{\partial T_{mix}}{\partial p} = \frac{T_{mix}(p+d,h)- - T_{mix}(p-d,h)}{2 \cdot d} - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[1] += d - lo[1] -= d - return (T_mix_ph(up, T0=T0) - T_mix_ph(lo, T0=T0)) / (2 * d) - - -def dT_mix_pdh(flow, T0=675): - r""" - Calculate partial derivate of temperature to enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dT / dh : float - Partial derivative of temperature to enthalpy dT /dh / ((kgK)/J). - - .. math:: - - \frac{\partial T_{mix}}{\partial h} = \frac{T_{mix}(p,h+d)- - T_{mix}(p,h-d)}{2 \cdot d} - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[2] += d - lo[2] -= d - return (T_mix_ph(up, T0=T0) - T_mix_ph(lo, T0=T0)) / (2 * d) - - -def dT_mix_ph_dfluid(flow, T0=675): - r""" - Calculate partial derivate of temperature to fluid composition. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dT / dfluid : ndarray - Partial derivatives of temperature to fluid composition - dT / dfluid / K. - - .. math:: - - \frac{\partial T_{mix}}{\partial fluid_{i}} = - \frac{T_{mix}(p,h,fluid_{i}+d)- - T_{mix}(p,h,fluid_{i}-d)}{2 \cdot d} - """ - d = 1e-5 - up = flow.copy() - lo = flow.copy() - vec_deriv = [] - for fluid, x in flow[3].items(): - if x > err: - up[3][fluid] += d - lo[3][fluid] -= d - vec_deriv += [ - (T_mix_ph(up, T0=T0) - T_mix_ph(lo, T0=T0)) / (2 * d)] - up[3][fluid] -= d - lo[3][fluid] += d - else: - vec_deriv += [0] - - return vec_deriv - - -def T_mix_ps(flow, s, T0=675): - r""" - Calculate the temperature from pressure and entropy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - s : float - Entropy of flow in J / (kgK). - - Returns - ------- - T : float - Temperature T / K. - - Note - ---- - First, check if fluid property has been memorised already. - If this is the case, return stored value, otherwise calculate value and - store it in the memorisation class. - - Uses CoolProp interface for pure fluids, newton algorithm for mixtures: - - .. math:: - - T_{mix}\left(p,s\right) = T_{i}\left(p,s_{i}\right)\; - \forall i \in \text{fluid components}\\ - - s_{i} = s \left(pp_{i}, T_{mix} \right)\\ - pp: \text{partial pressure} - - """ - # check if fluid properties have been calculated before - fl = tuple(flow[3].keys()) - memorisation = fl in Memorise.T_ps - if memorisation: - a = Memorise.T_ps[fl][:, :-2] - b = np.asarray([flow[1], flow[2]] + list(flow[3].values()) + [s]) - ix = np.where(np.all(abs(a - b) <= err, axis=1))[0] - if ix.size == 1: - # known fluid properties - Memorise.T_ps[fl][ix, -1] += 1 - return Memorise.T_ps[fl][ix, -2][0] - - # unknown fluid properties - fluid = single_fluid(flow[3]) - if fluid is None: - # calculate the fluid properties for fluid mixtures - valmin = max( - [Memorise.value_range[f][2] for f in fl if flow[3][f] > err] - ) + 0.1 - if T0 < valmin or np.isnan(T0): - T0 = valmin * 1.1 - - val = newton(s_mix_pT, ds_mix_pdT, flow, s, val0=T0, - valmin=valmin, valmax=3000, imax=10) - - else: - # calculate fluid property for pure fluids - val = T_ps(flow[1], s, fluid) - - if memorisation: - new = np.asarray( - [[flow[1], flow[2]] + list(flow[3].values()) + [s, val, 0]]) - # memorise the newly calculated value - Memorise.T_ps[fl] = np.append(Memorise.T_ps[fl], new, axis=0) - - return val - - -def T_ps(p, s, fluid): - r""" - Calculate the temperature from pressure and entropy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - s : float - Specific entropy h / (J/(kgK)). - - fluid : str - Fluid name. - - Returns - ------- - T : float - Temperature T / K. - """ - Memorise.state[fluid].update(CP.PSmass_INPUTS, p, s) - return Memorise.state[fluid].T() - - -def h_mix_pT(flow, T, force_gas=False): - r""" - Calculate the enthalpy from pressure and Temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature of flow T / K. - - Returns - ------- - h : float - Enthalpy h / (J/kg). - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - h_{mix}(p,T)=\sum_{i} h(pp_{i},T,fluid_{i})\; - \forall i \in \text{fluid components}\\ - pp: \text{partial pressure} - """ - n = molar_mass_flow(flow[3]) - - h = 0 - fluid_name = single_fluid(flow[3]) - if fluid_name is None: - - x_i = { - fluid: y / (molar_masses[fluid] * n) - for fluid, y in flow[3].items() - } - - water = Memorise.water - if (water is not None and not force_gas and flow[3][water] > err): - y_i_gas, x_i_gas, y_water_liq, x_water_liq = ( - cond_check(flow[3], x_i, flow[1], n, T) - ) - - else: - y_i_gas = flow[3] - y_water_liq = 0 - x_i_gas = x_i - - for fluid, y in y_i_gas.items(): - if y > err: - if fluid == water and y_water_liq > 0: - Memorise.state[fluid].update(CP.QT_INPUTS, 0, T) - h += Memorise.state[fluid].hmass() * y_water_liq - Memorise.state[fluid].update(CP.QT_INPUTS, 1, T) - h += Memorise.state[fluid].hmass() * y * (1 - y_water_liq) - - else: - h += h_pT( - flow[1] * x_i_gas[fluid], T, fluid, force_gas - ) * y * (1 - y_water_liq) - - else: - h = h_pT(flow[1], T, fluid_name, force_gas) - - return h - - -def h_pT(p, T, fluid, force_gas=False): - r""" - Calculate the enthalpy from pressure and temperature for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - T : float - Temperature T / K. - - fluid : str - Fluid name. - - Returns - ------- - h : float - Specific enthalpy h / (J/kg). - """ - if force_gas: - if T < get_T_crit(fluid): - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - h = Memorise.state[fluid].hmass() - Memorise.state[fluid].update(CP.QT_INPUTS, 1, T) - h_sat = Memorise.state[fluid].hmass() - return max(h, h_sat) - - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - return Memorise.state[fluid].hmass() - - -def dh_mix_pdT(flow, T): - r""" - Calculate partial derivate of enthalpy to temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - dh / dT : float - Partial derivative of enthalpy to temperature dh / dT / (J/(kgK)). - - .. math:: - - \frac{\partial h_{mix}}{\partial T} = - \frac{h_{mix}(p,T+d)-h_{mix}(p,T-d)}{2 \cdot d} - """ - d = 0.01 - return (h_mix_pT(flow, T + d) - h_mix_pT(flow, T - d)) / (2 * d) - - -def h_mix_ps(flow, s, T0=675): - r""" - Calculate the enthalpy from pressure and temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - s : float - Specific entropy of flow s / (J/(kgK)). - - Returns - ------- - h : float - Specific enthalpy h / (J/kg). - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - h_{mix}\left(p,s\right)=h\left(p, T_{mix}\left(p,s\right)\right) - """ - return h_mix_pT(flow, T_mix_ps(flow, s, T0=T0)) - - -def h_ps(p, s, fluid): - r""" - Calculate the enthalpy from pressure and entropy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - s : float - Specific entropy h / (J/(kgK)). - - fluid : str - Fluid name. - - Returns - ------- - h : float - Specific enthalpy h / (J/kg). - """ - Memorise.state[fluid].update(CP.PSmass_INPUTS, p, s) - return Memorise.state[fluid].hmass() - - -def h_ps_IF97(params, s): - r""" - Calculate the enthalpy from pressure and entropy for IF97 backend. - - Parameters - ---------- - fluid : str - Fluid name. - - p : float - Pressure p / Pa. - - s : float - Specific entropy h / (J/(kgK)). - - Returns - ------- - h : float - Specific enthalpy h / (J/kg). - """ - Memorise.state[params[0]].update(CP.PSmass_INPUTS, params[1], s) - return Memorise.state[params[0]].hmass() - - -def dh_pds_IF97(params, s): - r""" - Calculate the derivative of enthalpy to entropy at constant pressure. - - For pure fluids only, required for IF97 entropy iteration only. - - Parameters - ---------- - p : float - Pressure p / Pa. - - s : float - Specific entropy h / (J/(kgK)). - - fluid : str - Fluid name. - - Returns - ------- - dh : float - Derivative of specific enthalpy dh / ds / K. - """ - d = 1e-2 - Memorise.state[params[0]].update(CP.PSmass_INPUTS, params[1], s + d) - h_upper = Memorise.state[params[0]].hmass() - - Memorise.state[params[0]].update(CP.PSmass_INPUTS, params[1], s - d) - h_lower = Memorise.state[params[0]].hmass() - - return (h_upper - h_lower) / (2 * d) - - -def h_mix_pQ(flow, Q): - r""" - Calculate the enthalpy from pressure and vapour mass fraction. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Q : float - Vapour mass fraction Q / 1. - - Returns - ------- - h : float - Specific enthalpy h / (J/kg). - - Note - ---- - This function works for pure fluids only! - """ - fluid = single_fluid(flow[3]) - if fluid is None: - if sum(flow[3].values()) == 0: - msg = 'The function h_mix_pQ is called without fluid information.' - logger.error(msg) - raise ValueError(msg) - else: - msg = 'The function h_mix_pQ can only be used for pure fluids.' - logger.error(msg) - raise ValueError(msg) - - try: - Memorise.state[fluid].update(CP.PQ_INPUTS, flow[1], Q) - except ValueError: - p_crit = get_p_crit(fluid) - Memorise.state[fluid].update(CP.PQ_INPUTS, p_crit * 0.99, Q) - - return Memorise.state[fluid].hmass() - - -def dh_mix_dpQ(flow, Q): - r""" - Calculate partial derivate of enthalpy to vapour mass fraction. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Q : float - Vapour mass fraction Q / 1. - - Returns - ------- - dh / dQ : float - Partial derivative of enthalpy to vapour mass fraction - dh / dQ / (J/kg). - - .. math:: - - \frac{\partial h_{mix}}{\partial p} = - \frac{h_{mix}(p+d,Q)-h_{mix}(p-d,Q)}{2 \cdot d}\\ - Q: \text{vapour mass fraction} - - Note - ---- - This works for pure fluids only! - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[1] += d - lo[1] -= d - return (h_mix_pQ(up, Q) - h_mix_pQ(lo, Q)) / (2 * d) - - -def get_p_crit(fluid): - """ - Get critical point pressure. - - Parameters - ---------- - fluid : str - Fluid name. - - Returns - ------- - p_crit : float - Critical point pressure. - """ - return Memorise.state[fluid].trivial_keyed_output(CP.iP_critical) - - -def get_T_crit(fluid): - """ - Get critical point temperature. - - Parameters - ---------- - fluid : str - Fluid name. - - Returns - ------- - T_crit : float - Critical point temperature. - """ - return Memorise.state[fluid].trivial_keyed_output(CP.iT_critical) - - -def T_bp_p(flow): - r""" - Calculate temperature from boiling point pressure. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - T : float - Temperature at boiling point. - - Note - ---- - This function works for pure fluids only! - """ - fluid = single_fluid(flow[3]) - p_crit = get_p_crit(fluid) - if flow[1] > p_crit: - Memorise.state[fluid].update(CP.PQ_INPUTS, p_crit * 0.99, 1) - else: - Memorise.state[fluid].update(CP.PQ_INPUTS, flow[1], 1) - return Memorise.state[fluid].T() - - -def dT_bp_dp(flow): - r""" - Calculate partial derivate of temperature to boiling point pressure. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dT / dp : float - Partial derivative of temperature to boiling point pressure in K / Pa. - - .. math:: - - \frac{\partial h_{mix}}{\partial p} = - \frac{T_{bp}(p+d)-T_{bp}(p-d)}{2 \cdot d}\\ - Q: \text{vapour mass fraction} - - Note - ---- - This works for pure fluids only! - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[1] += d - lo[1] -= d - return (T_bp_p(up) - T_bp_p(lo)) / (2 * d) - - -def cond_check(y_i, x_i, p, n, T): - """Check if water is partially condensing in gaseous mixture. - - Parameters - ---------- - y_i : dict - Mass specific fluid composition. - x_i : dict - Mole specific fluid composition. - p : float - Pressure of mass flow. - n : float - Molar mass flow. - T : float - Temperature of mass flow. - - Returns - ------- - tuple - Tuple containing gas phase mass specific and molar specific - compositions and overall liquid water mass fraction. - """ - x_i_gas = x_i.copy() - y_i_gas = y_i.copy() - y_water_liq = 0 - x_water_liq = 0 - water_label = Memorise.water - - if T < get_T_crit(water_label): - Memorise.state[water_label].update(CP.QT_INPUTS, 1, T) - p_sat = Memorise.state[water_label].p() - - pp_water = p * y_i[water_label] / ( - molar_masses[water_label] * n - ) - - if p_sat < pp_water: - x_water_gas = (1 - x_i[water_label]) / (p / p_sat - 1) - x_water_liq = x_i[water_label] - x_water_gas - x_gas_sum = 1 - x_water_liq - - x_i_gas = {f: x / x_gas_sum for f, x in x_i.items()} - x_i_gas[water_label] = x_water_gas / x_gas_sum - - y_water_liq = x_water_liq * molar_masses[water_label] / ( - sum([ - x * molar_masses[fluid] - for fluid, x in x_i.items() - ]) - ) - - M = sum([x * molar_masses[fluid] for fluid, x in x_i_gas.items()]) - y_i_gas = { - fluid: x / M * molar_masses[fluid] - for fluid, x in x_i_gas.items() - } - - return y_i_gas, x_i_gas, y_water_liq, x_water_liq - - -def v_mix_ph(flow, T0=675): - r""" - Calculate the specific volume from pressure and enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - v : float - Specific volume v / (:math:`\mathrm{m}^3`/kg). - - Note - ---- - First, check if fluid property has been memorised already. - If this is the case, return stored value, otherwise calculate value and - store it in the memorisation class. - - Uses CoolProp interface for pure fluids, newton algorithm for mixtures: - - .. math:: - - v_{mix}\left(p,h\right) = v\left(p,T_{mix}(p,h)\right) - """ - # check if fluid properties have been calculated before - fl = tuple(flow[3].keys()) - memorisation = fl in Memorise.v_ph - if memorisation: - a = Memorise.v_ph[fl][:, :-2] - b = np.asarray([flow[1], flow[2]] + list(flow[3].values())) - ix = np.where(np.all(abs(a - b) <= err, axis=1))[0] - if ix.size == 1: - # known fluid properties - Memorise.v_ph[fl][ix, -1] += 1 - return Memorise.v_ph[fl][ix, -2][0] - - # unknown fluid properties - fluid = single_fluid(flow[3]) - if fluid is None: - # calculate the fluid properties for fluid mixtures - val = v_mix_pT(flow, T_mix_ph(flow, T0=T0)) - else: - # calculate fluid property for pure fluids - val = 1 / d_ph(flow[1], flow[2], fluid) - - if memorisation: - # memorise the newly calculated value - new = np.asarray( - [[flow[1], flow[2]] + list(flow[3].values()) + [val, 0]]) - Memorise.v_ph[fl] = np.append(Memorise.v_ph[fl], new, axis=0) - - return val - - -def d_ph(p, h, fluid): - r""" - Calculate the density from pressure and enthalpy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - d : float - Density d / (kg/:math:`\mathrm{m}^3`). - """ - if Memorise.back_end[fluid] == 'IF97': - return entropy_iteration_IF97(p, h, fluid, 'rho') - else: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - return Memorise.state[fluid].rhomass() - - -def Q_ph(p, h, fluid): - r""" - Calculate vapor mass fraction from pressure and enthalpy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - x : float - Vapor mass fraction. - """ - try: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - return Memorise.state[fluid].Q() - except (KeyError, ValueError, AttributeError): - return np.nan - - -def dv_mix_dph(flow, T0=675): - r""" - Calculate partial derivate of specific volume to pressure. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dv / dp : float - Partial derivative of specific volume to pressure - dv /dp / (:math:`\mathrm{m}^3`/(Pa kg)). - - .. math:: - - \frac{\partial v_{mix}}{\partial p} = \frac{v_{mix}(p+d,h)- - v_{mix}(p-d,h)}{2 \cdot d} - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[1] += d - lo[1] -= d - return (v_mix_ph(up, T0=T0) - v_mix_ph(lo, T0=T0)) / (2 * d) - - -def dv_mix_pdh(flow, T0=675): - r""" - Calculate partial derivate of specific volume to enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - dv / dh : float - Partial derivative of specific volume to enthalpy - dv /dh / (:math:`\mathrm{m}^3`/J). - - .. math:: - - \frac{\partial v_{mix}}{\partial h} = \frac{v_{mix}(p,h+d)- - v_{mix}(p,h-d)}{2 \cdot d} - """ - d = 0.1 - up = flow.copy() - lo = flow.copy() - up[2] += d - lo[2] -= d - return (v_mix_ph(up, T0=T0) - v_mix_ph(lo, T0=T0)) / (2 * d) - - -def v_mix_pT(flow, T): - r""" - Calculate the specific volume from pressure and temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - v : float - Specific volume v / (:math:`\mathrm{m}^3`/kg). - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - v_{mix}(p,T)=\frac{1}{\sum_{i} \rho(pp_{i}, T, fluid_{i})}\; - \forall i \in \text{fluid components}\\ - pp: \text{partial pressure} - """ - n = molar_mass_flow(flow[3]) - - d = 0 - for fluid, x in flow[3].items(): - if x > err: - ni = x / molar_masses[fluid] - d += d_pT(flow[1] * ni / n, T, fluid) - - return 1 / d - - -def d_mix_pT(flow, T): - r""" - Calculate the density from pressure and temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - d : float - Density d / (kg/:math:`\mathrm{m}^3`). - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - \rho_{mix}\left(p,T\right)=\frac{1}{v_{mix}\left(p,T\right)} - """ - return 1 / v_mix_pT(flow, T) - - -def d_pT(p, T, fluid): - r""" - Calculate the density from pressure and temperature for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - T : float - Temperature T / K. - - fluid : str - Fluid name. - - Returns - ------- - d : float - Density d / (kg/:math:`\mathrm{m}^3`). - """ - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - return Memorise.state[fluid].rhomass() - - -def visc_mix_ph(flow, T0=675): - r""" - Calculate the dynamic viscorsity from pressure and enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - visc : float - Dynamic viscosity visc / Pa s. - - Note - ---- - First, check if fluid property has been memorised already. - If this is the case, return stored value, otherwise calculate value and - store it in the memorisation class. - - Uses CoolProp interface for pure fluids, newton algorithm for mixtures: - - .. math:: - - \eta_{mix}\left(p,h\right) = \eta\left(p,T_{mix}(p,h)\right) - """ - # check if fluid properties have been calculated before - fl = tuple(flow[3].keys()) - memorisation = fl in Memorise.visc_ph - if memorisation: - a = Memorise.visc_ph[fl][:, :-2] - b = np.asarray([flow[1], flow[2]] + list(flow[3].values())) - ix = np.where(np.all(abs(a - b) <= err, axis=1))[0] - if ix.size == 1: - # known fluid properties - Memorise.visc_ph[fl][ix, -1] += 1 - return Memorise.visc_ph[fl][ix, -2][0] - - # unknown fluid properties - fluid = single_fluid(flow[3]) - if fluid is None: - # calculate the fluid properties for fluid mixtures - val = visc_mix_pT(flow, T_mix_ph(flow, T0=T0)) - else: - # calculate the fluid properties for pure fluids - val = visc_ph(flow[1], flow[2], fluid) - - if memorisation: - # memorise the newly calculated value - new = np.asarray( - [[flow[1], flow[2]] + list(flow[3].values()) + [val, 0]]) - Memorise.visc_ph[fl] = np.append(Memorise.visc_ph[fl], new, axis=0) - return val - - -def visc_ph(p, h, fluid): - r""" - Calculate dynamic viscosity from pressure and enthalpy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - visc : float - Viscosity visc / Pa s. - """ - if Memorise.back_end[fluid] == 'IF97': - return entropy_iteration_IF97(p, h, fluid, 'visc') - else: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - return Memorise.state[fluid].viscosity() - - -def visc_mix_pT(flow, T): - r""" - Calculate dynamic viscosity from pressure and temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - visc : float - Dynamic viscosity visc / Pa s. - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - \eta_{mix}(p,T)=\frac{\sum_{i} \left( \eta(p,T,fluid_{i}) \cdot y_{i} - \cdot \sqrt{M_{i}} \right)} - {\sum_{i} \left(y_{i} \cdot \sqrt{M_{i}} \right)}\; - \forall i \in \text{fluid components}\\ - y: \text{volume fraction}\\ - M: \text{molar mass} - - Reference: :cite:`Herning1936`. - """ - n = molar_mass_flow(flow[3]) - - a = 0 - b = 0 - for fluid, x in flow[3].items(): - if x > err: - bi = x * np.sqrt(molar_masses[fluid]) / (molar_masses[fluid] * n) - b += bi - a += bi * visc_pT(flow[1], T, fluid) - - return a / b - - -def visc_pT(p, T, fluid): - r""" - Calculate dynamic viscosity from pressure and temperature for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - T : float - Temperature T / K. - - fluid : str - Fluid name. - - Returns - ------- - visc : float - Viscosity visc / Pa s. - """ - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - return Memorise.state[fluid].viscosity() - - -def s_mix_ph(flow, T0=675): - r""" - Calculate the entropy from pressure and enthalpy. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - s : float - Specific entropy s / (J/(kgK)). - - Note - ---- - First, check if fluid property has been memorised already. - If this is the case, return stored value, otherwise calculate value and - store it in the memorisation class. - - Uses CoolProp interface for pure fluids, newton algorithm for mixtures: - - .. math:: - - s_{mix}\left(p,h\right) = s\left(p,T_{mix}(p,h)\right) - """ - # check if fluid properties have been calculated before - fl = tuple(flow[3].keys()) - memorisation = fl in Memorise.s_ph - if memorisation: - a = Memorise.s_ph[fl][:, :-2] - b = np.asarray([flow[1], flow[2]] + list(flow[3].values())) - ix = np.where(np.all(abs(a - b) <= err, axis=1))[0] - if ix.size == 1: - # known fluid properties - Memorise.s_ph[fl][ix, -1] += 1 - return Memorise.s_ph[fl][ix, -2][0] - - # unknown fluid properties - fluid = single_fluid(flow[3]) - if fluid is None: - # calculate the fluid properties for fluid mixtures - val = s_mix_pT(flow, T_mix_ph(flow, T0=T0)) - else: - # calculate fluid property for pure fluids - val = s_ph(flow[1], flow[2], fluid) - - if memorisation: - # memorise the newly calculated value - new = np.asarray( - [[flow[1], flow[2]] + list(flow[3].values()) + [val, 0]]) - Memorise.s_ph[fl] = np.append(Memorise.s_ph[fl], new, axis=0) - - return val - - -def s_ph(p, h, fluid): - r""" - Calculate the entropy from pressure and enthalpy for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - s : float - Specific entropy s / (J/(kgK)). - """ - if Memorise.back_end[fluid] == 'IF97': - return entropy_iteration_IF97(p, h, fluid, 's') - else: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - return Memorise.state[fluid].smass() - - -def s_mix_pT(flow, T, force_gas=False): - r""" - Calculate the entropy from pressure and temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - s : float - Specific entropy s / (J/(kgK)). - - Note - ---- - Calculation for fluid mixtures. - - .. math:: - - s_{mix}(p,T)=\sum_{i} x_{i} \cdot s(pp_{i},T,fluid_{i})- - \sum_{i} x_{i} \cdot R_{i} \cdot \ln \frac{pp_{i}}{p}\; - \forall i \in \text{fluid components}\\ - pp: \text{partial pressure}\\ - R: \text{gas constant} - """ - n = molar_mass_flow(flow[3]) - s = 0 - - fluid_name = single_fluid(flow[3]) - if fluid_name is None: - - x_i = { - fluid: y / (molar_masses[fluid] * n) - for fluid, y in flow[3].items() - } - - water = Memorise.water - if (water is not None and not force_gas and flow[3][water] > err): - y_i_gas, x_i_gas, y_water_liq, x_water_liq = ( - cond_check(flow[3], x_i, flow[1], n, T) - ) - - else: - y_i_gas = flow[3] - y_water_liq = 0 - x_i_gas = x_i - - for fluid, y in y_i_gas.items(): - if y > err: - if fluid == water and y_water_liq > 0: - Memorise.state[water].update(CP.QT_INPUTS, 1, T) - s += Memorise.state[water].smass() * y * ( - 1 - y_water_liq - ) - Memorise.state[water].update(CP.QT_INPUTS, 0, T) - s += Memorise.state[water].smass() * y_water_liq - - else: - pp = flow[1] * x_i_gas[fluid] - s += y * (1 - y_water_liq) * s_pT(pp, T, fluid, force_gas) - - else: - s = s_pT(flow[1], T, fluid_name, force_gas) - - return s - - -def s_pT(p, T, fluid, force_gas): - r""" - Calculate the entropy from pressure and temperature for a pure fluid. - - Parameters - ---------- - p : float - Pressure p / Pa. - - T : float - Temperature T / K. - - fluid : str - Fluid name. - - Returns - ------- - s : float - Specific entropy s / (J/(kgK)). - """ - if force_gas: - if T < get_T_crit(fluid): - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - s = Memorise.state[fluid].smass() - Memorise.state[fluid].update(CP.QT_INPUTS, 1, T) - s_sat = Memorise.state[fluid].smass() - return max(s, s_sat) - - Memorise.state[fluid].update(CP.PT_INPUTS, p, T) - return Memorise.state[fluid].smass() - - -def ds_mix_pdT(flow, T): - r""" - Calculate partial derivate of entropy to temperature. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - T : float - Temperature T / K. - - Returns - ------- - ds / dT : float - Partial derivative of specific entropy to temperature - ds / dT / (J/(kg :math:`\mathrm{K}^2`)). - - .. math:: - - \frac{\partial s_{mix}}{\partial T} = - \frac{s_{mix}(p,T+d)-s_{mix}(p,T-d)}{2 \cdot d} - """ - d = 0.01 - return (s_mix_pT(flow, T + d) - s_mix_pT(flow, T - d)) / (2 * d) - - -def isentropic(inflow, outflow, T0=675): - r""" - Calculate the enthalpy at the outlet after isentropic process. - - Parameters - ---------- - inflow : list - Inflow fluid property vector containing mass flow, pressure, enthalpy - and fluid composition. - - outflow : list - Outflow fluid property vector containing mass flow, pressure, enthalpy - and fluid composition. - - Returns - ------- - h_s : float - Enthalpy after isentropic state change. - - .. math:: - - h_\mathrm{s} = \begin{cases} - h\left(p_{out}, s\left(p_{in}, h_{in}\right) \right) & - \text{pure fluids}\\ - h\left(p_{out}, s\left(p_{in}, T_{in}\right) \right) & - \text{mixtures}\\ - \end{cases} - """ - fluid = single_fluid(inflow[3]) - if fluid is not None: - return h_ps(outflow[1], s_ph(inflow[1], inflow[2], fluid), fluid) - else: - s_mix = s_mix_ph(inflow) - return h_mix_ps(outflow, s_mix, T0=T0) - - -def calc_physical_exergy(conn, p0, T0): - r""" - Calculate specific physical exergy. - - Physical exergy is allocated to a thermal and a mechanical share according - to :cite:`Morosuk2019`. - - Parameters - ---------- - conn : tespy.connections.connection.Connection - Connection to calculate specific physical exergy for. - - p0 : float - Ambient pressure p0 / Pa. - - T0 : float - Ambient temperature T0 / K. - - Returns - ------- - e_ph : tuple - Specific thermal and mechanical exergy - (:math:`e^\mathrm{T}`, :math:`e^\mathrm{M}`) in J / kg. - - .. math:: - - e^\mathrm{T} = \left( h - h \left( p, T_0 \right) \right) - - T_0 \cdot \left(s - s\left(p, T_0\right)\right) - - e^\mathrm{M}=\left(h\left(p,T_0\right)-h\left(p_0,T_0\right)\right) - -T_0\cdot\left(s\left(p, T_0\right)-s\left(p_0,T_0\right)\right) - - e^\mathrm{PH} = e^\mathrm{T} + e^\mathrm{M} - """ - h_T0_p = h_mix_pT([0, conn.p.val_SI, 0, conn.fluid.val], T0) - s_T0_p = s_mix_pT([0, conn.p.val_SI, 0, conn.fluid.val], T0) - ex_therm = (conn.h.val_SI - h_T0_p) - T0 * (conn.s.val_SI - s_T0_p) - h0 = h_mix_pT([0, p0, 0, conn.fluid.val], T0) - s0 = s_mix_pT([0, p0, 0, conn.fluid.val], T0) - ex_mech = (h_T0_p - h0) - T0 * (s_T0_p - s0) - return ex_therm, ex_mech - - -def calc_chemical_exergy(conn, p0, T0, Chem_Ex): - """ - Calculate specific chemical exergy. - - Parameters - ---------- - conn : tespy.connections.connection.Connection - Connection to calculate specific chemical exergy for. - - p0 : float - Ambient pressure p0 / Pa. - - T0 : float - Ambient temperature T0 / K. - - Chem_Ex : dict - Lookup table for standard specific chemical exergy. - - Returns - ------- - e_ch : float - Specific chemical exergy in J / kg. - """ - fluid_name = single_fluid(conn.fluid.val) - - if fluid_name is None: - - n = molar_mass_flow(conn.fluid.val) - x = { - fluid: y / (molar_masses[fluid] * n) - for fluid, y in conn.fluid.val.items() - } - - molar_mass_mixture = sum( - [x * molar_masses[fluid] for fluid, x in x.items()] - ) - - y_i_gas, x_i_gas, y_water_liq, x_water_liq = ( - cond_check(conn.fluid.val, x, p0, n, T0) - ) - - else: - - fluid_aliases = CP.CoolProp.get_aliases(fluid_name) - y = [Chem_Ex[k][Chem_Ex[k][4]] for k in fluid_aliases if k in Chem_Ex] - return y[0] / molar_masses[fluid_name] * 1e3 - - ex_cond = 0 - ex_dry = 0 - for fluid, x in x_i_gas.items(): - if x == 0: - continue - - fluid_aliases = CP.CoolProp.get_aliases(fluid) - if fluid in CP.CoolProp.get_aliases('H2O') and x_water_liq > 0: - - y = [Chem_Ex[k][2] for k in fluid_aliases if k in Chem_Ex] - ex_cond += x_water_liq * y[0] - - y = [Chem_Ex[k][3] for k in fluid_aliases if k in Chem_Ex] - ex_dry += x * y[0] + T0 * gas_constants['uni'] * 1e-3 * x * np.log(x) - - ex_chemical = ex_cond + ex_dry * (1 - x_water_liq) - ex_chemical *= 1 / molar_mass_mixture - - return ex_chemical * 1e3 # Data from Chem_Ex are in kJ / mol - - -def entropy_iteration_IF97(p, h, fluid, output): - r""" - Calculate state in IF97 back-end via entropy iteration. - - Parameters - ---------- - p : float - Pressure p / Pa. - - h : float - Specific enthalpy h / (J/kg). - - fluid : str - Fluid name. - - Returns - ------- - T : float - Temperature T / K. - """ - # region 1 exclusive issue! - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - if p <= 16.529164252605 * 1e6: - h_at_ph = Memorise.state[fluid].hmass() - deviation = abs(h_at_ph - h) - if deviation / h > 0.001: - # region 1, where isenthalpic lines are tangent to saturation dome - if p > 1e6 and p < 1e7 and h > 2700000 and h < 2850000: - smin = 5750 - smax = 6500 - # bottom left corner in Ts diagram - elif h < 10000: - smin = 0 - smax = 50 - else: - # proximity to saturated liquid - Memorise.state[fluid].update(CP.PQ_INPUTS, p, 0) - h_sat_l = Memorise.state[fluid].hmass() - if abs(h - h_sat_l) / h_sat_l < 1e-1: - if p < 1000: - smin = 0 - elif p < 60000: - smin = Memorise.state[fluid].smass() * 0.9 - else: - smin = Memorise.state[fluid].smass() * 0.95 - - Memorise.state[fluid].update(CP.PQ_INPUTS, p, 0.3) - smax = Memorise.state[fluid].smass() - # all others - else: - Memorise.state[fluid].update(CP.HmassP_INPUTS, h, p) - s0 = Memorise.state[fluid].smass() - smin = 0.8 * s0 - smax = 1.2 * s0 - - s0 = (smax + smin) / 2 - s = newton(func=h_ps_IF97, deriv=dh_pds_IF97, params=[fluid, p], - y=h, val0=s0, valmin=smin, valmax=smax, max_iter=5, - tol_rel=1e-3, tol_mode='rel') - Memorise.state[fluid].update(CP.PSmass_INPUTS, p, s) - - if output == 'T': - return Memorise.state[fluid].T() - elif output == 's': - return Memorise.state[fluid].smass() - elif output == 'rho': - return Memorise.state[fluid].rhomass() - else: - return Memorise.state[fluid].viscosity() diff --git a/src/tespy/tools/fluid_properties/__init__.py b/src/tespy/tools/fluid_properties/__init__.py new file mode 100644 index 000000000..987173bd5 --- /dev/null +++ b/src/tespy/tools/fluid_properties/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 + +from .functions import Q_mix_ph # noqa: F401 +from .functions import T_mix_ph # noqa: F401 +from .functions import T_mix_ps # noqa: F401 +from .functions import T_sat_p # noqa: F401 +from .functions import dh_mix_dpQ # noqa: F401 +from .functions import dT_mix_dph # noqa: F401 +from .functions import dT_mix_pdh # noqa: F401 +from .functions import dT_sat_dp # noqa: F401 +from .functions import dv_mix_dph # noqa: F401 +from .functions import dv_mix_pdh # noqa: F401 +from .functions import h_mix_pQ # noqa: F401 +from .functions import h_mix_pT # noqa: F401 +from .functions import isentropic # noqa: F401 +from .functions import s_mix_ph # noqa: F401 +from .functions import s_mix_pT # noqa: F401 +from .functions import v_mix_ph # noqa: F401 +from .functions import v_mix_pT # noqa: F401 +from .functions import viscosity_mix_ph # noqa: F401 +from .functions import viscosity_mix_pT # noqa: F401 +from .helpers import single_fluid # noqa: F401 +from .wrappers import CoolPropWrapper # noqa: F401 diff --git a/src/tespy/tools/fluid_properties/functions.py b/src/tespy/tools/fluid_properties/functions.py new file mode 100644 index 000000000..f97ce42d1 --- /dev/null +++ b/src/tespy/tools/fluid_properties/functions.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 + +"""Module for fluid property functions. + + +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/tools/fluid_properties/functions.py + +SPDX-License-Identifier: MIT +""" + +from .helpers import _check_mixing_rule +from .helpers import get_number_of_fluids +from .helpers import get_pure_fluid +from .helpers import inverse_temperature_mixture +from .mixtures import EXERGY_CHEMICAL +from .mixtures import H_MIX_PT_DIRECT +from .mixtures import S_MIX_PT_DIRECT +from .mixtures import T_MIX_PH_REVERSE +from .mixtures import T_MIX_PS_REVERSE +from .mixtures import V_MIX_PT_DIRECT +from .mixtures import VISCOSITY_MIX_PT_DIRECT + + +def isentropic(p_1, h_1, p_2, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].isentropic(p_1, h_1, p_2) + else: + s_1 = s_mix_ph(p_1, h_1, fluid_data, mixing_rule) + T_2 = T_mix_ps(p_2, s_1, fluid_data, mixing_rule) + return h_mix_pT(p_2, T_2, fluid_data, mixing_rule) + + +def calc_physical_exergy(h, s, p, pamb, Tamb, fluid_data, mixing_rule=None, T0=None): + r""" + Calculate specific physical exergy. + + Physical exergy is allocated to a thermal and a mechanical share according + to :cite:`Morosuk2019`. + + Parameters + ---------- + pamb : float + Ambient pressure p0 / Pa. + + Tamb : float + Ambient temperature T0 / K. + + Returns + ------- + e_ph : tuple + Specific thermal and mechanical exergy + (:math:`e^\mathrm{T}`, :math:`e^\mathrm{M}`) in J / kg. + + .. math:: + + e^\mathrm{T} = \left( h - h \left( p, T_0 \right) \right) - + T_0 \cdot \left(s - s\left(p, T_0\right)\right) + + e^\mathrm{M}=\left(h\left(p,T_0\right)-h\left(p_0,T_0\right)\right) + -T_0\cdot\left(s\left(p, T_0\right)-s\left(p_0,T_0\right)\right) + + e^\mathrm{PH} = e^\mathrm{T} + e^\mathrm{M} + """ + h_T0_p = h_mix_pT(p, Tamb, fluid_data, mixing_rule) + s_T0_p = s_mix_pT(p, Tamb, fluid_data, mixing_rule) + ex_therm = (h - h_T0_p) - Tamb * (s - s_T0_p) + h0 = h_mix_pT(pamb, Tamb, fluid_data, mixing_rule) + s0 = s_mix_pT(pamb, Tamb, fluid_data, mixing_rule) + ex_mech = (h_T0_p - h0) - Tamb * (s_T0_p - s0) + return ex_therm, ex_mech + + +def calc_chemical_exergy(pamb, Tamb, fluid_data, Chem_Ex, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + fluid_aliases = pure_fluid["wrapper"]._aliases + y = [Chem_Ex[k][Chem_Ex[k][4]] for k in fluid_aliases if k in Chem_Ex] + return y[0] / pure_fluid["wrapper"]._molar_mass * 1e3 + else: + _check_mixing_rule(mixing_rule, EXERGY_CHEMICAL, "chemical exergy") + return EXERGY_CHEMICAL[mixing_rule](pamb, Tamb, fluid_data, Chem_Ex) + + +def T_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].T_ph(p, h) + else: + _check_mixing_rule(mixing_rule, T_MIX_PH_REVERSE, "temperature (from enthalpy)") + kwargs = { + "p": p, "target_value": h, "fluid_data": fluid_data, "T0": T0, + "f": T_MIX_PH_REVERSE[mixing_rule] + } + return inverse_temperature_mixture(**kwargs) + + +def dT_mix_pdh(p, h, fluid_data, mixing_rule=None, T0=None): + d = 1e-1 + upper = T_mix_ph(p, h + d, fluid_data, mixing_rule=mixing_rule, T0=T0) + lower = T_mix_ph(p, h - d, fluid_data, mixing_rule=mixing_rule, T0=upper) + return (upper - lower) / (2 * d) + + +def dT_mix_dph(p, h, fluid_data, mixing_rule=None, T0=None): + d = 1e-1 + upper = T_mix_ph(p + d, h, fluid_data, mixing_rule=mixing_rule, T0=T0) + lower = T_mix_ph(p - d, h, fluid_data, mixing_rule=mixing_rule, T0=upper) + return (upper - lower) / (2 * d) + + +def dT_mix_ph_dfluid(p, h, fluid, fluid_data, mixing_rule=None, T0=None): + d = 1e-5 + fluid_data[fluid]["mass_fraction"] += d + upper = T_mix_ph(p, h, fluid_data, mixing_rule=mixing_rule, T0=T0) + fluid_data[fluid]["mass_fraction"] -= 2 * d + lower = T_mix_ph(p, h, fluid_data, mixing_rule=mixing_rule, T0=upper) + fluid_data[fluid]["mass_fraction"] += d + return (upper - lower) / (2 * d) + + +def h_mix_pT(p, T, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].h_pT(p, T) + else: + _check_mixing_rule(mixing_rule, H_MIX_PT_DIRECT, "enthalpy") + return H_MIX_PT_DIRECT[mixing_rule](p, T, fluid_data) + + +def h_mix_pQ(p, Q, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].h_pQ(p, Q) + else: + msg = "Saturation function cannot be called on mixtures." + raise ValueError(msg) + + +def dh_mix_dpQ(p, Q, fluid_data, mixing_rule=None): + d = 0.1 + upper = h_mix_pQ(p + d, Q, fluid_data) + lower = h_mix_pQ(p - d, Q, fluid_data) + return (upper - lower) / (2 * d) + + +def Q_mix_ph(p, h, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].Q_ph(p, h) + else: + msg = "Saturation function cannot be called on mixtures." + raise ValueError(msg) + + +def p_sat_T(T, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].p_sat(T) + else: + msg = "Saturation function cannot be called on mixtures." + raise ValueError(msg) + + +def T_sat_p(p, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].T_sat(p) + else: + msg = "Saturation function cannot be called on mixtures." + raise ValueError(msg) + + +def dT_sat_dp(p, fluid_data, mixing_rule=None): + d = 0.01 + upper = T_sat_p(p + d, fluid_data) + lower = T_sat_p(p - d, fluid_data) + return (upper - lower) / (2 * d) + + +def s_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].s_ph(p, h) + else: + T = T_mix_ph(p, h , fluid_data, mixing_rule, T0) + return s_mix_pT(p, T, fluid_data, mixing_rule) + + + +def s_mix_pT(p, T, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].s_pT(p, T) + else: + _check_mixing_rule(mixing_rule, S_MIX_PT_DIRECT, "entropy") + return S_MIX_PT_DIRECT[mixing_rule](p, T, fluid_data) + + +def T_mix_ps(p, s, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].T_ps(p, s) + else: + _check_mixing_rule(mixing_rule, T_MIX_PS_REVERSE, "temperature (from entropy)") + kwargs = { + "p": p, "target_value": s, "fluid_data": fluid_data, "T0": T0, + "f": T_MIX_PS_REVERSE[mixing_rule] + } + return inverse_temperature_mixture(**kwargs) + + +def v_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return 1 / pure_fluid["wrapper"].d_ph(p, h) + else: + T = T_mix_ph(p, h , fluid_data, mixing_rule, T0) + return v_mix_pT(p, T, fluid_data, mixing_rule) + + +def dv_mix_dph(p, h, fluid_data, mixing_rule=None, T0=None): + d = 1e-1 + upper = v_mix_ph(p + d, h, fluid_data, mixing_rule=mixing_rule, T0=T0) + lower = v_mix_ph(p - d, h, fluid_data, mixing_rule=mixing_rule, T0=upper) + return (upper - lower) / (2 * d) + + +def dv_mix_pdh(p, h, fluid_data, mixing_rule=None, T0=None): + d = 1e-1 + upper = v_mix_ph(p, h + d, fluid_data, mixing_rule=mixing_rule, T0=T0) + lower = v_mix_ph(p, h - d, fluid_data, mixing_rule=mixing_rule, T0=upper) + return (upper - lower) / (2 * d) + + +def v_mix_pT(p, T, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return 1 / pure_fluid["wrapper"].d_pT(p, T) + else: + _check_mixing_rule(mixing_rule, V_MIX_PT_DIRECT, "specific volume") + return V_MIX_PT_DIRECT[mixing_rule](p, T, fluid_data) + + +def viscosity_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].viscosity_ph(p, h) + else: + T = T_mix_ph(p, h , fluid_data, mixing_rule, T0) + return viscosity_mix_pT(p, T, fluid_data, mixing_rule) + + +def viscosity_mix_pT(p, T, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].viscosity_pT(p, T) + else: + _check_mixing_rule(mixing_rule, V_MIX_PT_DIRECT, "viscosity") + return VISCOSITY_MIX_PT_DIRECT[mixing_rule](p, T, fluid_data) diff --git a/src/tespy/tools/fluid_properties/helpers.py b/src/tespy/tools/fluid_properties/helpers.py new file mode 100644 index 000000000..27740208a --- /dev/null +++ b/src/tespy/tools/fluid_properties/helpers.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 + +"""Module for fluid property helper functions. + + +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/tools/fluid_properties/helpers.py + +SPDX-License-Identifier: MIT +""" + +import CoolProp.CoolProp as CP +import numpy as np + +from tespy.tools.global_vars import ERR +from tespy.tools.helpers import central_difference +from tespy.tools.helpers import newton_with_kwargs +from tespy.tools.logger import logger + + +def _is_larger_than_precision(value): + return value > ERR + + +def _check_mixing_rule(mixing_rule, mixing_functions, propertyfunction): + if mixing_rule not in mixing_functions: + msg = ( + f"The mixing rule '{mixing_rule}' is not available for " + f"the fluid property functions for {propertyfunction}. Available " + f"rules are '" + "', '".join(mixing_functions.keys()) + "'." + ) + logger.exception(msg) + raise KeyError(msg) + + +def get_number_of_fluids(fluid_data): + return sum([1 for f in fluid_data.values() if _is_larger_than_precision(f["mass_fraction"])]) + + +def get_pure_fluid(fluid_data): + for f in fluid_data.values(): + if _is_larger_than_precision(f["mass_fraction"]): + return f + + +def single_fluid(fluid_data): + r"""Return the name of the pure fluid in a fluid vector.""" + if get_number_of_fluids(fluid_data) > 1: + return None + else: + for fluid, data in fluid_data.items(): + if _is_larger_than_precision(data["mass_fraction"]): + return fluid + + +def get_molar_fractions(fluid_data): + molarflow = { + key: value["mass_fraction"] / value["wrapper"]._molar_mass + for key, value in fluid_data.items() + } + molarflow_sum = sum(molarflow.values()) + return {key: value / molarflow_sum for key, value in molarflow.items()} + + +def inverse_temperature_mixture(p=None, target_value=None, fluid_data=None, T0=None, f=None): + # calculate the fluid properties for fluid mixtures + valmin, valmax = get_mixture_temperature_range(fluid_data) + if T0 is None: + T0 = (valmin + valmax) / 2.0 + + function_kwargs = { + "p": p, "fluid_data": fluid_data, "T": T0, + "function": f, "parameter": "T" , "delta": 0.01 + } + return newton_with_kwargs( + central_difference, + target_value, + val0=T0, + valmin=valmin, + valmax=valmax, + **function_kwargs + ) + + +def get_mixture_temperature_range(fluid_data): + valmin = max( + [v["wrapper"]._T_min for v in fluid_data.values() if _is_larger_than_precision(v["mass_fraction"])] + ) + 0.1 + valmax = min( + [v["wrapper"]._T_max for v in fluid_data.values() if _is_larger_than_precision(v["mass_fraction"])] + ) - 0.1 + return valmin, valmax + + +def calc_molar_mass_mixture(fluid_data, molar_fractions): + return sum([x * fluid_data[fluid]["wrapper"]._molar_mass for fluid, x in molar_fractions.items()]) + + +def fluid_structure(fluid): + r""" + Return the checmical formula of fluid. + + Parameters + ---------- + fluid : str + Name of the fluid. + + Returns + ------- + parts : dict + Dictionary of the chemical base elements as keys and the number of + atoms in a molecule as values. + + Example + ------- + Get the chemical formula of methane. + + >>> from tespy.tools.fluid_properties.helpers import fluid_structure + >>> elements = fluid_structure('methane') + >>> elements['C'], elements['H'] + (1, 4) + """ + parts = {} + for element in CP.get_fluid_param_string( + fluid, 'formula').split('}'): + if element != '': + el = element.split('_{') + parts[el[0]] = int(el[1]) + + return parts + + +def darcy_friction_factor(re, ks, d): + r""" + Calculate the Darcy friction factor. + + Parameters + ---------- + re : float + Reynolds number re / 1. + + ks : float + Pipe roughness ks / m. + + d : float + Pipe diameter/characteristic lenght d / m. + + Returns + ------- + darcy_friction_factor : float + Darcy friction factor :math:`\lambda` / 1 + + Note + ---- + **Laminar flow** (:math:`re \leq 2320`) + + .. math:: + + \lambda = \frac{64}{re} + + **turbulent flow** (:math:`re > 2320`) + + *hydraulically smooth:* :math:`\frac{re \cdot k_{s}}{d} < 65` + + .. math:: + + \lambda = \begin{cases} + 0.03164 \cdot re^{-0.25} & re \leq 10^4\\ + \left(1.8 \cdot \log \left(re\right) -1.5 \right)^{-2} & + 10^4 < re < 10^6\\ + solve \left(0 = 2 \cdot \log\left(re \cdot \sqrt{\lambda} \right) -0.8 + - \frac{1}{\sqrt{\lambda}}\right) & re \geq 10^6\\ + \end{cases} + + *transition zone and hydraulically rough:* + + .. math:: + + \lambda = solve \left( 0 = 2 \cdot \log \left( \frac{2.51}{re \cdot + \sqrt{\lambda}} + \frac{k_{s}}{d \cdot 3.71} \right) - + \frac{1}{\sqrt{\lambda}} \right) + + Reference: :cite:`Nirschl2018`. + + Example + ------- + Calculate the Darcy friction factor at different hydraulic states. + + >>> from tespy.tools.fluid_properties.helpers import darcy_friction_factor + >>> ks = 5e-5 + >>> d = 0.05 + >>> re_laminar = 2000 + >>> re_turb_smooth = 5000 + >>> re_turb_trans = 70000 + >>> re_high = 1000000 + >>> d_high = 0.8 + >>> re_very_high = 6000000 + >>> d_very_high = 1 + >>> ks_low = 1e-5 + >>> ks_rough = 1e-3 + >>> darcy_friction_factor(re_laminar, ks, d) + 0.032 + >>> round(darcy_friction_factor(re_turb_smooth, ks, d), 3) + 0.038 + >>> round(darcy_friction_factor(re_turb_trans, ks, d), 3) + 0.023 + >>> round(darcy_friction_factor(re_turb_trans, ks_rough, d), 3) + 0.049 + >>> round(darcy_friction_factor(re_high, ks, d_high), 3) + 0.012 + >>> round(darcy_friction_factor(re_very_high, ks_low, d_very_high), 3) + 0.009 + """ + if re <= 2320: + return 64 / re + else: + if re * ks / d < 65: + if re <= 1e4: + return blasius(re) + elif re < 1e6: + return hanakov(re) + else: + l0 = 0.02 + function_kwargs = { + "function": prandtl_karman, + "parameter": "darcy_friction_factor", + "reynolds": re + + } + return newton_with_kwargs( + prandtl_karman_derivative, + 0, + val0=l0, + valmin=0.00001, + valmax=0.2, + **function_kwargs + ) + + else: + l0 = 0.002 + function_kwargs = { + "function": colebrook, + "parameter": "darcy_friction_factor", + "reynolds": re, + "ks": ks, + "diameter": d, + "delta": 0.001 + } + return newton_with_kwargs( + central_difference, + 0, + val0=l0, + valmin=0.0001, + valmax=0.2, + **function_kwargs + ) + + +def blasius(re): + """ + Calculate friction coefficient according to Blasius. + + Parameters + ---------- + re : float + Reynolds number. + + Returns + ------- + darcy_friction_factor : float + Darcy friction factor. + """ + return 0.3164 * re ** (-0.25) + + +def hanakov(re): + """ + Calculate friction coefficient according to Hanakov. + + Parameters + ---------- + re : float + Reynolds number. + + Returns + ------- + darcy_friction_factor : float + Darcy friction factor. + """ + return (1.8 * np.log10(re) - 1.5) ** (-2) + + +def prandtl_karman(reynolds, darcy_friction_factor, **kwargs): + """ + Calculate friction coefficient according to Prandtl and v. Kármán. + + Applied in smooth conditions. + + Parameters + ---------- + re : float + Reynolds number. + + darcy_friction_factor : float + Darcy friction factor. + + Returns + ------- + darcy_friction_factor : float + Darcy friction factor. + """ + return ( + 2 * np.log10(reynolds * darcy_friction_factor ** 0.5) + - 0.8 - 1 / darcy_friction_factor ** 0.5 + ) + + +def prandtl_karman_derivative(reynolds, darcy_friction_factor, **kwargs): + """Calculate derivative for Prandtl and v. Kármán equation.""" + return ( + 1 / (darcy_friction_factor * np.log(10)) + + 0.5 * darcy_friction_factor ** (-1.5) + ) + + +def colebrook(reynolds, ks, diameter, darcy_friction_factor, **kwargs): + """ + Calculate friction coefficient accroding to Colebrook-White equation. + + Applied in transition zone and rough conditions. + + Parameters + ---------- + re : float + Reynolds number. + + ks : float + Equivalent sand roughness. + + d : float + Pipe's diameter. + + darcy_friction_factor : float + Darcy friction factor. + + Returns + ------- + darcy_friction_factor : float + Darcy friction factor. + """ + return ( + 2 * np.log10( + 2.51 / (reynolds * darcy_friction_factor ** 0.5) + ks + / (3.71 * diameter) + ) + 1 / darcy_friction_factor ** 0.5 + ) diff --git a/src/tespy/tools/fluid_properties/mixtures.py b/src/tespy/tools/fluid_properties/mixtures.py new file mode 100644 index 000000000..b156a133f --- /dev/null +++ b/src/tespy/tools/fluid_properties/mixtures.py @@ -0,0 +1,380 @@ +# -*- coding: utf-8 + +"""Module for fluid property mixture routines. + + +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/tools/fluid_properties/mixtures.py + +SPDX-License-Identifier: MIT +""" + +import CoolProp as CP +import numpy as np + +from tespy.tools.global_vars import gas_constants + +from .helpers import _is_larger_than_precision +from .helpers import calc_molar_mass_mixture +from .helpers import get_molar_fractions + + +def h_mix_pT_ideal(p=None, T=None, fluid_data=None, **kwargs): + molar_fractions = get_molar_fractions(fluid_data) + + h = 0 + for fluid, data in fluid_data.items(): + + if _is_larger_than_precision(data["mass_fraction"]): + pp = p * molar_fractions[fluid] + h += data["wrapper"].h_pT(pp, T) * data["mass_fraction"] + + return h + + +def h_mix_pT_ideal_cond(p=None, T=None, fluid_data=None, **kwargs): + + water_alias = _water_in_mixture(fluid_data) + if water_alias: + water_alias = next(iter(water_alias)) + mass_fractions_gas, molar_fraction_gas, mass_liquid, molar_liquid = cond_check(p, T, fluid_data, water_alias) + if not _is_larger_than_precision(mass_liquid): + return h_mix_pT_ideal(p, T, fluid_data, **kwargs) + h = 0 + for fluid, data in fluid_data.items(): + if _is_larger_than_precision(data["mass_fraction"]): + if fluid == water_alias: + h += fluid_data[water_alias]["wrapper"].h_QT(0, T) * mass_liquid + h += fluid_data[water_alias]["wrapper"].h_QT(1, T) * mass_fractions_gas[fluid] * (1 - mass_liquid) + else: + pp = p * molar_fraction_gas[fluid] + h += data["wrapper"].h_pT(pp, T) * mass_fractions_gas[fluid] * (1 - mass_liquid) + return h + else: + return h_mix_pT_ideal(p, T, fluid_data, **kwargs) + + +def h_mix_pT_forced_gas(p, T, fluid_data, **kwargs): + molar_fractions = get_molar_fractions(fluid_data) + + h = 0 + for fluid, data in fluid_data.items(): + + if _is_larger_than_precision(data["mass_fraction"]): + pp = p * molar_fractions[fluid] + if fluid == "H2O" and pp >= data["wrapper"]._p_min: + if T <= data["wrapper"].T_sat(pp): + h += data["wrapper"].h_QT(1, T) * data["mass_fraction"] + else: + h += data["wrapper"].h_pT(pp, T) * data["mass_fraction"] + else: + h += data["wrapper"].h_pT(pp, T) * data["mass_fraction"] + + return h + + +def h_mix_pT_incompressible(p, T, fluid_data, **kwargs): + + h = 0 + for data in fluid_data.values(): + if _is_larger_than_precision(data["mass_fraction"]): + h += data["wrapper"].h_pT(p, T) * data["mass_fraction"] + + return h + + +def s_mix_pT_ideal(p=None, T=None, fluid_data=None, **kwargs): + molar_fractions = get_molar_fractions(fluid_data) + + s = 0 + for fluid, data in fluid_data.items(): + + if _is_larger_than_precision(data["mass_fraction"]): + pp = p * molar_fractions[fluid] + s += data["wrapper"].s_pT(pp, T) * data["mass_fraction"] + + return s + + +def s_mix_pT_ideal_cond(p=None, T=None, fluid_data=None, **kwargs): + + water_alias = _water_in_mixture(fluid_data) + if water_alias: + water_alias = next(iter(water_alias)) + mass_fractions_gas, molar_fraction_gas, mass_liquid, molar_liquid = cond_check(p, T, fluid_data, water_alias) + if mass_liquid == 0: + return s_mix_pT_ideal(p, T, fluid_data, **kwargs) + s = 0 + for fluid, data in fluid_data.items(): + if _is_larger_than_precision(data["mass_fraction"]): + if fluid == water_alias: + s += fluid_data[water_alias]["wrapper"].s_QT(0, T) * mass_liquid + s += fluid_data[water_alias]["wrapper"].s_QT(1, T) * mass_fractions_gas[fluid] * (1 - mass_liquid) + else: + pp = p * molar_fraction_gas[fluid] + s += data["wrapper"].s_pT(pp, T) * mass_fractions_gas[fluid] * (1 - mass_liquid) + return s + else: + return s_mix_pT_ideal(p, T, fluid_data, **kwargs) + + +def s_mix_pT_incompressible(p=None, T=None, fluid_data=None, **kwargs): + + s = 0 + for data in fluid_data.values(): + + if _is_larger_than_precision(data["mass_fraction"]): + s += data["wrapper"].s_pT(p, T) * data["mass_fraction"] + + return s + + +def v_mix_pT_ideal(p=None, T=None, fluid_data=None, **kwargs): + molar_fractions = get_molar_fractions(fluid_data) + + d = 0 + for fluid, data in fluid_data.items(): + + if _is_larger_than_precision(data["mass_fraction"]): + pp = p * molar_fractions[fluid] + d += data["wrapper"].d_pT(pp, T) + + return 1 / d + + +def v_mix_pT_ideal_cond(p=None, T=None, fluid_data=None, **kwargs): + + water_alias = _water_in_mixture(fluid_data) + if water_alias: + water_alias = next(iter(water_alias)) + mass_fractions_gas, molar_fraction_gas, mass_liquid, molar_liquid = cond_check(p, T, fluid_data, water_alias) + if mass_liquid == 0: + return v_mix_pT_ideal(p, T, fluid_data, **kwargs) + d = 0 + for fluid, data in fluid_data.items(): + if _is_larger_than_precision(data["mass_fraction"]): + if fluid == water_alias: + d += fluid_data[water_alias]["wrapper"].d_QT(0, T) * mass_liquid + d += fluid_data[water_alias]["wrapper"].d_QT(1, T) * (1 - mass_liquid) + else: + pp = p * molar_fraction_gas[fluid] + d += data["wrapper"].d_pT(pp, T) * (1 - mass_liquid) + return 1 / d + else: + return v_mix_pT_ideal(p, T, fluid_data, **kwargs) + + +def v_mix_pT_incompressible(p=None, T=None, fluid_data=None, **kwargs): + + v = 0 + for data in fluid_data.values(): + if _is_larger_than_precision(data["mass_fraction"]): + v += 1 / data["wrapper"].d_pT(p, T) * data["mass_fraction"] + + return v + + +def viscosity_mix_pT_ideal(p=None, T=None, fluid_data=None, **kwargs): + r""" + Calculate dynamic viscosity from pressure and temperature. + + Parameters + ---------- + flow : list + Fluid property vector containing mass flow, pressure, enthalpy and + fluid composition. + + T : float + Temperature T / K. + + Returns + ------- + visc : float + Dynamic viscosity visc / Pa s. + + Note + ---- + Calculation for fluid mixtures. + + .. math:: + + \eta_{mix}(p,T)=\frac{\sum_{i} \left( \eta(p,T,fluid_{i}) \cdot y_{i} + \cdot \sqrt{M_{i}} \right)} + {\sum_{i} \left(y_{i} \cdot \sqrt{M_{i}} \right)}\; + \forall i \in \text{fluid components}\\ + y: \text{volume fraction}\\ + M: \text{molar mass} + + Reference: :cite:`Herning1936`. + """ + molar_fractions = get_molar_fractions(fluid_data) + + a = 0 + b = 0 + for fluid, data in fluid_data.items(): + if _is_larger_than_precision(data["mass_fraction"]): + bi = molar_fractions[fluid] * data["wrapper"]._molar_mass ** 0.5 + b += bi + a += bi * data["wrapper"].viscosity_pT(p, T) + + return a / b + + +def viscosity_mix_pT_incompressible(p=None, T=None, fluid_data=None, **kwargs): + + viscosity = 0 + for data in fluid_data.values(): + if _is_larger_than_precision(data["mass_fraction"]): + viscosity += data["wrapper"].viscosity_pT(p, T) * data["mass_fraction"] + + return viscosity + + +def exergy_chemical_ideal_cond(pamb, Tamb, fluid_data, Chem_Ex): + + molar_fractions = get_molar_fractions(fluid_data) + water_alias = _water_in_mixture(fluid_data) + if water_alias: + water_alias = next(iter(water_alias)) + _, molar_fractions_gas, _, molar_liquid = cond_check( + pamb, Tamb, fluid_data, water_alias + ) + else: + molar_fractions_gas = molar_fractions + molar_liquid = 0 + + ex_cond = 0 + ex_dry = 0 + for fluid, x in molar_fractions_gas.items(): + if x == 0: + continue + + fluid_aliases = fluid_data[fluid]["wrapper"]._aliases + + if molar_liquid > 0: + y = [ + Chem_Ex[k][2] for k in fluid_aliases if k in Chem_Ex + ] + ex_cond += molar_liquid * y[0] + + y = [Chem_Ex[k][3] for k in fluid_aliases if k in Chem_Ex] + ex_dry += x * y[0] + Tamb * gas_constants['uni'] * 1e-3 * x * np.log(x) + + ex_chemical = ex_cond + ex_dry * (1 - molar_liquid) + ex_chemical *= 1 / calc_molar_mass_mixture( + fluid_data, molar_fractions + ) + + return ex_chemical * 1e3 # Data from Chem_Ex are in kJ / mol + + +def _water_in_mixture(fluid_data): + water_aliases = set(CP.CoolProp.get_aliases("H2O")) + return water_aliases & set([f for f in fluid_data if _is_larger_than_precision(fluid_data[f]["mass_fraction"])]) + + +def cond_check(p, T, fluid_data, water_alias): + """Check if water is partially condensing in gaseous mixture. + + Parameters + ---------- + y_i : dict + Mass specific fluid composition. + x_i : dict + Mole specific fluid composition. + p : float + Pressure of mass flow. + n : float + Molar mass flow. + T : float + Temperature of mass flow. + + Returns + ------- + tuple + Tuple containing gas phase mass specific and molar specific + compositions and overall liquid water mass fraction. + """ + molar_fractions = get_molar_fractions(fluid_data) + molar_fractions_gas = molar_fractions + mass_fractions_gas = {f: v["mass_fraction"] for f, v in fluid_data.items()} + water_mass_liquid = 0 + water_molar_liquid = 0 + + if fluid_data[water_alias]["wrapper"]._is_below_T_critical(T): + p_sat = fluid_data[water_alias]["wrapper"].p_sat(T) + pp_water = p * molar_fractions[water_alias] + + if p_sat < pp_water: + water_molar_gas = (1 - molar_fractions[water_alias]) / (p / p_sat - 1) + water_molar_liquid = molar_fractions[water_alias] - water_molar_gas + x_gas_sum = 1 - water_molar_liquid + + molar_fractions_gas = {f: x / x_gas_sum for f, x in molar_fractions.items()} + molar_fractions_gas[water_alias] = water_molar_gas / x_gas_sum + + water_mass_liquid = ( + water_molar_liquid + * fluid_data[water_alias]["wrapper"]._molar_mass + / calc_molar_mass_mixture(fluid_data, molar_fractions) + ) + + molar_mass_mixture = calc_molar_mass_mixture(fluid_data, molar_fractions_gas) + mass_fractions_gas = { + fluid: ( + x / molar_mass_mixture + * fluid_data[fluid]["wrapper"]._molar_mass + ) + for fluid, x in molar_fractions_gas.items() + } + + return mass_fractions_gas, molar_fractions_gas, water_mass_liquid, water_molar_liquid + + +T_MIX_PH_REVERSE = { + "ideal": h_mix_pT_ideal, + "ideal-cond": h_mix_pT_ideal_cond, + "incompressible": h_mix_pT_incompressible +} + + +T_MIX_PS_REVERSE = { + "ideal": s_mix_pT_ideal, + "ideal-cond": s_mix_pT_ideal_cond, + "incompressible": s_mix_pT_incompressible +} + + +H_MIX_PT_DIRECT = { + "ideal": h_mix_pT_ideal, + "ideal-cond": h_mix_pT_ideal_cond, + "incompressible": h_mix_pT_incompressible, + "forced-gas": h_mix_pT_forced_gas +} + + +S_MIX_PT_DIRECT = { + "ideal": s_mix_pT_ideal, + "ideal-cond": s_mix_pT_ideal_cond, + "incompressible": s_mix_pT_incompressible +} + + +V_MIX_PT_DIRECT = { + "ideal": v_mix_pT_ideal, + "ideal-cond": v_mix_pT_ideal_cond, + "incompressible": v_mix_pT_incompressible +} + + +VISCOSITY_MIX_PT_DIRECT = { + "ideal": viscosity_mix_pT_ideal, + "ideal-cond": viscosity_mix_pT_ideal, + "incompressible": viscosity_mix_pT_incompressible +} + +EXERGY_CHEMICAL = { + "ideal-cond": exergy_chemical_ideal_cond, +} diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py new file mode 100644 index 000000000..97ce00647 --- /dev/null +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 + +"""Module for fluid property wrappers. + + +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/tools/fluid_properties/wrappers.py + +SPDX-License-Identifier: MIT +""" + +import CoolProp as CP + +from tespy.tools.global_vars import ERR + + +class FluidPropertyWrapper: + + def __init__(self, fluid, back_end=None) -> None: + """Base class for fluid property wrappers + + Parameters + ---------- + fluid : str + Name of the fluid. + back_end : str, optional + Name of the back end, by default None + """ + self.back_end = back_end + self.fluid = fluid + if "[" in self.fluid: + self.fluid, self._fractions = self.fluid.split("[") + self._fractions = self._fractions.replace("]", "") + else: + self._fractions = None + + def _not_implemented(self) -> None: + raise NotImplementedError( + f"Method is not implemented for {self.__class__.__name__}." + ) + + def isentropic(self, p_1, h_1, p_2): + self._not_implemented() + + def _is_below_T_critical(self, T): + self._not_implemented() + + def _make_p_subcritical(self, p): + self._not_implemented() + + def T_ph(self, p, h): + self._not_implemented() + + def T_ps(self, p, s): + self._not_implemented() + + def h_pT(self, p, T): + self._not_implemented() + + def h_QT(self, Q, T): + self._not_implemented() + + def s_QT(self, Q, T): + self._not_implemented() + + def T_sat(self, p): + self._not_implemented() + + def p_sat(self, T): + self._not_implemented() + + def Q_ph(self, p, h): + self._not_implemented() + + def d_ph(self, p, h): + self._not_implemented() + + def d_pT(self, p, T): + self._not_implemented() + + def d_QT(self, Q, T): + self._not_implemented() + + def viscosity_ph(self, p, h): + self._not_implemented() + + def viscosity_pT(self, p, T): + self._not_implemented() + + def s_ph(self, p, h): + self._not_implemented() + + def s_pT(self, p, T): + self._not_implemented() + + +class CoolPropWrapper(FluidPropertyWrapper): + + def __init__(self, fluid, back_end=None) -> None: + """Wrapper for CoolProp.CoolProp.AbstractState instance calls + + Parameters + ---------- + fluid : str + Name of the fluid + back_end : str, optional + CoolProp back end for the AbstractState object, by default "HEOS" + """ + if back_end is None: + back_end = "HEOS" + + super().__init__(fluid, back_end) + self.AS = CP.CoolProp.AbstractState(self.back_end, self.fluid) + self._set_constants() + + def _set_constants(self): + self._T_min = self.AS.trivial_keyed_output(CP.iT_min) + self._T_max = self.AS.trivial_keyed_output(CP.iT_max) + try: + self._aliases = CP.CoolProp.get_aliases(self.fluid) + except RuntimeError: + self._aliases = [self.fluid] + + if self.back_end == "INCOMP": + if self._fractions is not None: + # how to find if a mixture is volumetric of mass based? + try: + self.AS.set_volu_fractions([float(self._fractions)]) + except ValueError: + self.AS.set_mass_fractions([float(self._fractions)]) + self._p_min = 1e2 + self._p_max = 1e8 + self._p_crit = 1e8 + self._T_crit = None + self._molar_mass = 1 + try: + # how to know that we have a binary mixture? + self._T_min = self.AS.trivial_keyed_output(CP.iT_freeze) + except ValueError: + pass + else: + self._p_min = self.AS.trivial_keyed_output(CP.iP_min) + self._p_max = self.AS.trivial_keyed_output(CP.iP_max) + self._p_crit = self.AS.trivial_keyed_output(CP.iP_critical) + self._T_crit = self.AS.trivial_keyed_output(CP.iT_critical) + self._molar_mass = self.AS.trivial_keyed_output(CP.imolar_mass) + + def _is_below_T_critical(self, T): + return T < self._T_crit + + def _make_p_subcritical(self, p): + if p > self._p_crit: + p = self._p_crit * 0.99 + return p + + def get_T_max(self, p): + if self.back_end == "INCOMP": + return self.T_sat(p) + else: + return self._T_max + + def isentropic(self, p_1, h_1, p_2): + return self.h_ps(p_2, self.s_ph(p_1, h_1)) + + def T_ph(self, p, h): + self.AS.update(CP.HmassP_INPUTS, h, p) + return self.AS.T() + + def T_ps(self, p, s): + self.AS.update(CP.PSmass_INPUTS, p, s) + return self.AS.T() + + def h_pQ(self, p, Q): + self.AS.update(CP.PQ_INPUTS, p, Q) + return self.AS.hmass() + + def h_ps(self, p, s): + self.AS.update(CP.PSmass_INPUTS, p, s) + return self.AS.hmass() + + def h_pT(self, p, T): + if self.back_end == "INCOMP": + if T == (self._T_max + self._T_min) / 2: + T += ERR + self.AS.update(CP.PT_INPUTS, p, T) + h = self.AS.hmass() * 0.5 + T -= 2 * ERR + self.AS.update(CP.PT_INPUTS, p, T) + h += self.AS.hmass() * 0.5 + return h + self.AS.update(CP.PT_INPUTS, p, T) + return self.AS.hmass() + + def h_QT(self, Q, T): + self.AS.update(CP.QT_INPUTS, Q, T) + return self.AS.hmass() + + def s_QT(self, Q, T): + self.AS.update(CP.QT_INPUTS, Q, T) + return self.AS.smass() + + def T_sat(self, p): + p = self._make_p_subcritical(p) + self.AS.update(CP.PQ_INPUTS, p, 0) + return self.AS.T() + + def p_sat(self, T): + if T > self._T_crit: + T = self._T_crit * 0.99 + + self.AS.update(CP.QT_INPUTS, 0, T) + return self.AS.p() + + def Q_ph(self, p, h): + p = self._make_p_subcritical(p) + self.AS.update(CP.HmassP_INPUTS, h, p) + return self.AS.Q() + + def d_ph(self, p, h): + self.AS.update(CP.HmassP_INPUTS, h, p) + return self.AS.rhomass() + + def d_pT(self, p, T): + self.AS.update(CP.PT_INPUTS, p, T) + return self.AS.rhomass() + + def d_QT(self, Q, T): + self.AS.update(CP.QT_INPUTS, Q, T) + return self.AS.rhomass() + + def viscosity_ph(self, p, h): + self.AS.update(CP.HmassP_INPUTS, h, p) + return self.AS.viscosity() + + def viscosity_pT(self, p, T): + self.AS.update(CP.PT_INPUTS, p, T) + return self.AS.viscosity() + + def s_ph(self, p, h): + self.AS.update(CP.HmassP_INPUTS, h, p) + return self.AS.smass() + + def s_pT(self, p, T): + self.AS.update(CP.PT_INPUTS, p, T) + return self.AS.smass() + + +class IAPWSWrapper(FluidPropertyWrapper): + + + def __init__(self, fluid, back_end=None) -> None: + """Wrapper for iapws library calls + + Parameters + ---------- + fluid : str + Name of the fluid + back_end : str, optional + CoolProp back end for the AbstractState object, by default "IF97" + """ + # avoid unncessary loading time if not used + try: + import iapws + except ModuleNotFoundError: + msg = ( + "To use the iapws fluid properties you need to install " + "iapws." + ) + raise ModuleNotFoundError(msg) + + if back_end is None: + back_end = "IF97" + super().__init__(fluid, back_end) + self._aliases = CP.CoolProp.get_aliases("H2O") + + if self.fluid not in self._aliases: + msg = "The iapws wrapper only supports water as fluid." + raise ValueError(msg) + + if self.back_end == "IF97": + self.AS = iapws.IAPWS97 + elif self.back_end == "IF95": + self.AS = iapws.IAPWS95 + else: + msg = f"The specified back_end {self.back_end} is not available." + raise NotImplementedError(msg) + self._set_constants(iapws) + + def _set_constants(self, iapws): + self._T_min = iapws._iapws.Tt + self._T_max = 2000 + self._p_min = iapws._iapws.Pt * 1e6 + self._p_max = 100e6 + self._p_crit = iapws._iapws.Pc * 1e6 + self._T_crit = iapws._iapws.Tc + self._molar_mass = iapws._iapws.M + + def _is_below_T_critical(self, T): + return T < self._T_crit + + def _make_p_subcritical(self, p): + if p > self._p_crit: + p = self._p_crit * 0.99 + return p + + def isentropic(self, p_1, h_1, p_2): + return self.h_ps(p_2, self.s_ph(p_1, h_1)) + + def T_ph(self, p, h): + return self.AS(h=h / 1e3, P=p / 1e6).T + + def T_ps(self, p, s): + return self.AS(s=s / 1e3, P=p / 1e6).T + + def h_pQ(self, p, Q): + return self.AS(P=p / 1e6, x=Q).h * 1e3 + + def h_ps(self, p, s): + return self.AS(P=p / 1e6, s=s / 1e3).h * 1e3 + + def h_pT(self, p, T): + return self.AS(P=p / 1e6, T=T).h * 1e3 + + def h_QT(self, Q, T): + return self.AS(T=T, x=Q).h * 1e3 + + def s_QT(self, Q, T): + return self.AS(T=T, x=Q).s * 1e3 + + def T_sat(self, p): + p = self._make_p_subcritical(p) + return self.AS(P=p / 1e6, x=0).T + + def p_sat(self, T): + if T > self._T_crit: + T = self._T_crit * 0.99 + + return self.AS(T=T / 1e6, x=0).P * 1e6 + + def Q_ph(self, p, h): + p = self._make_p_subcritical(p) + return self.AS(h=h / 1e3, P=p / 1e6).x + + def d_ph(self, p, h): + return self.AS(h=h / 1e3, P=p / 1e6).rho + + def d_pT(self, p, T): + return self.AS(T=T, P=p / 1e6).rho + + def d_QT(self, Q, T): + return self.AS(T=T, x=Q).rho + + def viscosity_ph(self, p, h): + return self.AS(P=p / 1e6, h=h / 1e3).mu + + def viscosity_pT(self, p, T): + return self.AS(T=T, P=p / 1e6).mu + + def s_ph(self, p, h): + return self.AS(P=p / 1e6, h=h / 1e3).s * 1e3 + + def s_pT(self, p, T): + return self.AS(P=p / 1e6, T=T).s * 1e3 + + +class PyromatWrapper(FluidPropertyWrapper): + + def __init__(self, fluid, back_end=None) -> None: + """_summary_ + + Parameters + ---------- + fluid : str + Name of the fluid + back_end : str, optional + CoolProp back end for the AbstractState object, by default None + """ + # avoid unnecessary loading time if not used + try: + import pyromat as pm + pm.config['unit_energy'] = "J" + pm.config['unit_pressure'] = "Pa" + pm.config['unit_molar'] = "mol" + except ModuleNotFoundError: + msg = ( + "To use the pyromat fluid properties you need to install " + "pyromat." + ) + raise ModuleNotFoundError(msg) + + super().__init__(fluid, back_end) + self._create_AS(pm) + self._set_constants() + + def _create_AS(self, pm): + self.AS = pm.get(f"{self.back_end}.{self.fluid}") + + def _set_constants(self): + self._p_min, self._p_max = 100, 1000e5 + self._T_min, self._T_max = self.AS.Tlim() + self._molar_mass = self.AS.mw() + + def isentropic(self, p_1, h_1, p_2): + return self.h_ps(p_2, self.s_ph(p_1, h_1)) + + def T_ph(self, p, h): + return self.AS.T(p=p, h=h)[0] + + def T_ps(self, p, s): + return self.AS.T(p=p, s=s)[0] + + def h_pT(self, p, T): + return self.AS.h(p=p, T=T)[0] + + def T_ph(self, p, h): + return self.AS.T(p=p, h=h)[0] + + def T_ps(self, p, s): + return self.AS.T(p=p, s=s)[0] + + def h_pT(self, p, T): + return self.AS.h(p=p, T=T)[0] + + def h_ps(self, p, s): + return self.AS.h(p=p, s=s)[0] + + def d_ph(self, p, h): + return self.AS.d(p=p, h=h)[0] + + def d_pT(self, p, T): + return self.AS.d(p=p, T=T)[0] + + def s_ph(self, p, h): + return self.AS.s(p=p, h=h)[0] + + def s_pT(self, p, T): + if self.back_end == "ig": + self._not_implemented() + return self.AS.s(p=p, T=T)[0] + + def h_QT(self, Q, T): + if self.back_end == "ig": + self._not_implemented() + return self.AS.h(x=Q, T=T)[0] + + def s_QT(self, Q, T): + if self.back_end == "ig": + self._not_implemented() + return self.AS.s(x=Q, T=T)[0] + + def T_boiling(self, p): + if self.back_end == "ig": + self._not_implemented() + return self.AS.T(x=1, p=p)[0] + + def p_boiling(self, T): + if self.back_end == "ig": + self._not_implemented() + return self.AS.p(x=1, T=T)[0] + + def Q_ph(self, p, h): + if self.back_end == "ig": + self._not_implemented() + return self.AS.x(p=p, h=h)[0] + + def d_QT(self, Q, T): + if self.back_end == "ig": + self._not_implemented() + return self.AS.d(x=Q, T=T)[0] diff --git a/src/tespy/tools/global_vars.py b/src/tespy/tools/global_vars.py index 3dd99569c..37a9273e5 100644 --- a/src/tespy/tools/global_vars.py +++ b/src/tespy/tools/global_vars.py @@ -8,13 +8,14 @@ SPDX-License-Identifier: MIT """ +from collections import OrderedDict -err = 1e-6 +ERR = 1e-6 molar_masses = {} gas_constants = {} gas_constants['uni'] = 8.314462618 -fluid_property_data = { +fluid_property_data = OrderedDict({ 'm': { 'text': 'mass flow', 'SI_unit': 'kg / s', @@ -98,6 +99,6 @@ 'latex_eq': r'0 = s_\mathrm{spec} - s\left(p, h \right)', 'documentation': {'float_fmt': '{:,.2f}'} } -} +}) combustion_gases = ['methane', 'ethane', 'propane', 'butane', 'hydrogen', 'nDodecane'] diff --git a/src/tespy/tools/helpers.py b/src/tespy/tools/helpers.py index 8ec5f53a3..e177474ec 100644 --- a/src/tespy/tools/helpers.py +++ b/src/tespy/tools/helpers.py @@ -16,13 +16,27 @@ from copy import deepcopy import CoolProp.CoolProp as CP -import numpy as np from tespy import __datapath__ from tespy.tools import logger -from tespy.tools.global_vars import err +from tespy.tools.global_vars import ERR from tespy.tools.global_vars import fluid_property_data -from tespy.tools.global_vars import molar_masses + + +def get_all_subdictionaries(data): + subdictionaries = [] + for value in data.values(): + if len(value["subbranches"]) == 0: + subdictionaries.append( + {k: v for k, v in value.items() if k != "subbranches"} + ) + else: + subdictionaries.append( + {k: v for k, v in value.items() if k != "subbranches"} + ) + subdictionaries.extend(get_all_subdictionaries(value["subbranches"])) + + return subdictionaries def get_chem_ex_lib(name): @@ -216,7 +230,7 @@ def __init__(self, label, func, deriv, conns, params={}, >>> from tespy.tools.helpers import UserDefinedEquation >>> from tespy.tools import CharLine >>> from tespy.tools.fluid_properties import T_mix_ph, v_mix_ph - >>> nw = Network(fluids=['water'], p_unit='bar', T_unit='C') + >>> nw = Network(p_unit='bar', T_unit='C') >>> nw.set_attr(iterinfo=False) >>> so = Source('source') >>> si = Sink('sink') @@ -243,10 +257,11 @@ def __init__(self, label, func, deriv, conns, params={}, >>> def myfunc(ude): ... char = ude.params['char'] ... return ( - ... T_mix_ph(ude.conns[0].get_flow()) - - ... T_mix_ph(ude.conns[1].get_flow()) - char.evaluate( + ... ude.conns[0].calc_T() - ude.conns[1].calc_T() + ... - char.evaluate( ... ude.conns[0].m.val_SI * - ... v_mix_ph(ude.conns[0].get_flow())) + ... ude.conns[0].calc_vol() + ... ) ... ) The function does only take one parameter, we name it :code:`ude` in @@ -263,30 +278,38 @@ def __init__(self, label, func, deriv, conns, params={}, pressure, enthalpy and fluid composition. In this case, the derivatives to the mass flow, pressure and enthalpy of the inflow as well as the derivatives to the pressure and enthalpy of the outflow will be - required. Similar to the equation definition, define a function - returning the corresponding jacobian matrix. The jacobian is a - dictionary containing numpy arrays for every connection. Therefore - the first key is the connection you want to calculate the derivative - for and the second key is the index of the variable in the jacobian. - The indices correspond to - - - 0: mass flow - - 1: pressure - - 2: enthalpy - - 3 until end (:code:`3:`): fluid composition + required. You have to define a function placing the derivatives in the + Jacobian matrix. The Jacobian is a dictionary containing tuples as keys + with the derivative as their value. The tuples indicate the equation + number (always 0 for user defined equations, since there is only a + single equation) and the position of the variable in the system matrix. + The position of the variables is stored in the :code:`J_col` attribute. + Before calculating and placing a result in the Jacobian, you have to + make sure, that the variable you want to calculate the partial + derivative for is actually a variable. For example, in case you + specified a value for the mass flow, it will not be part of the + variables' space, since it has a constant value, and thus, no derivate + needs to be calculated. You can use the :code:`is_var` keyword to check, + whether a mass flow, pressure or enthalpy is actually variable. We can calculate the derivatives numerically, if an easy analytical solution is not available. Simply use the :code:`numeric_deriv` method passing the variable ('m', 'p', 'h', 'fluid') as well as the - connection's index. + connection. >>> def myjacobian(ude): - ... ude.jacobian[ude.conns[0]][0] = ude.numeric_deriv('m', 0) - ... ude.jacobian[ude.conns[0]][1] = ude.numeric_deriv('p', 0) - ... ude.jacobian[ude.conns[0]][2] = ude.numeric_deriv('h', 0) - ... ude.jacobian[ude.conns[1]][1] = ude.numeric_deriv('p', 1) - ... ude.jacobian[ude.conns[1]][2] = ude.numeric_deriv('h', 1) - ... return ude.jacobian + ... c0 = ude.conns[0] + ... c1 = ude.conns[1] + ... if c0.m.is_var: + ... ude.jacobian[c0.m.J_col] = ude.numeric_deriv('m', c0) + ... if c0.p.is_var: + ... ude.jacobian[c0.p.J_col] = ude.numeric_deriv('p', c0) + ... if c0.h.is_var: + ... ude.jacobian[c0.h.J_col] = ude.numeric_deriv('h', c0) + ... if c1.p.is_var: + ... ude.jacobian[c1.p.J_col] = ude.numeric_deriv('p', c1) + ... if c1.h.is_var: + ... ude.jacobian[c1.h.J_col] = ude.numeric_deriv('h', c1) After that, we only need to th specify the characteristic line we want out temperature drop to follow as well as create the @@ -366,71 +389,152 @@ def __init__(self, label, func, deriv, conns, params={}, logger.error(msg) raise TypeError(msg) - def numeric_deriv(self, param, idx): + def solve(self): + self.residual = self.func(self) + self.deriv(self) + + def numeric_deriv(self, dx, conn): r""" - Calculate partial derivative of the function func to dx numerically. + Calculate partial derivative of the user defined function to dx. - Parameters - ---------- - param : str - Parameter to calculate partial derivative for. + For details see :py:func:`tespy.tools.helpers._numeric_deriv` + """ + return _numeric_deriv(self, self.func, dx, conn, ude=self) - idx : int - Position of the connection to calculate the partial derivative for - within the list of the connections :code:`conns`. - Returns - ------- - deriv : float/list - Partial derivative(s) of the function :math:`f` to variable(s) - :math:`x`. +def _numeric_deriv(obj, func, dx, conn=None, **kwargs): + r""" + Calculate partial derivative of the function func to dx. - .. math:: + Parameters + ---------- + obj : object + Instance, which provides the equation to calculate the derivative for. - \frac{\partial f}{\partial x}=\frac{f(x+d)+f(x-d)}{2\cdot d} - """ - if param == 'fluid': - d = 1e-5 - deriv = [] - for f in self.conns[0].fluid.val.keys(): - val = self.conns[idx].fluid.val[f] - if self.conns[idx].fluid.val[f] + d <= 1: - self.conns[idx].fluid.val[f] += d - else: - self.conns[idx].fluid.val[f] = 1 - exp = self.func(self) - if self.conns[idx].fluid.val[f] - 2 * d >= 0: - self.conns[idx].fluid.val[f] -= 2 * d - else: - self.conns[idx].fluid.val[f] = 0 - exp -= self.func(self) - self.conns[idx].fluid.val[f] = val - - deriv += [exp / (2 * d)] - - elif param in ['m', 'p', 'h']: - - if param == 'm': - d = 1e-4 - else: - d = 1e-1 - - self.conns[idx].get_attr(param).val_SI += d - exp = self.func(self) - self.conns[idx].get_attr(param).val_SI -= 2 * d - exp -= self.func(self) - self.conns[idx].get_attr(param).val_SI += d - - deriv = exp / (2 * d) + func : function + Function :math:`f` to calculate the partial derivative for. + + dx : str + Partial derivative. + + conn : tespy.connections.connection.Connection + Connection to calculate the numeric derivative for. + + Returns + ------- + deriv : float/list + Partial derivative(s) of the function :math:`f` to variable(s) + :math:`x`. + .. math:: + + \frac{\partial f}{\partial x} = \frac{f(x + d) + f(x - d)}{2 d} + """ + if conn is None: + d = obj.get_attr(dx).d + exp = 0 + obj.get_attr(dx).val += d + exp += func(**kwargs) + + obj.get_attr(dx).val -= 2 * d + exp -= func(**kwargs) + deriv = exp / (2 * d) + + obj.get_attr(dx).val += d + + elif dx in conn.fluid.is_var: + d = 1e-5 + + val = conn.fluid.val[dx] + if conn.fluid.val[dx] + d <= 1: + conn.fluid.val[dx] += d else: - msg = ( - 'Can only calculate numerical derivative to primary variables.' - 'Please specify "m", "p", "h" or "fluid" as param.') - logger.error(msg) - raise ValueError(msg) + conn.fluid.val[dx] = 1 + + conn.build_fluid_data() + exp = func(**kwargs) + + if conn.fluid.val[dx] - 2 * d >= 0: + conn.fluid.val[dx] -= 2 * d + else: + conn.fluid.val[dx] = 0 + + conn.build_fluid_data() + exp -= func(**kwargs) + + conn.fluid.val[dx] = val + conn.build_fluid_data() + + deriv = exp / (2 * d) + + elif dx in ['m', 'p', 'h']: + + if dx == 'm': + d = 1e-4 + else: + d = 1e-1 + conn.get_attr(dx).val_SI += d + exp = func(**kwargs) + + conn.get_attr(dx).val_SI -= 2 * d + exp -= func(**kwargs) + deriv = exp / (2 * d) + + conn.get_attr(dx).val_SI += d + + else: + msg = ( + "Your variable specification for the numerical derivative " + "calculation seems to be wrong. It has to be a fluid name, m, " + "p, h or the name of a component variable." + ) + logger.exception(msg) + raise ValueError(msg) + return deriv + + +def bus_char_evaluation(params, bus_value): + r""" + Calculate the value of a bus. + + Parameters + ---------- + comp_value : float + Value of the energy transfer at the component. + + reference_value : float + Value of the bus in reference state. + + char_func : tespy.tools.characteristics.char_line + Characteristic function of the bus. + + Returns + ------- + residual : float + Residual of the equation. + + .. math:: + + residual = \dot{E}_\mathrm{bus} - \frac{\dot{E}_\mathrm{component}} + {f\left(\frac{\dot{E}_\mathrm{bus}} + {\dot{E}_\mathrm{bus,ref}}\right)} + """ + comp_value = params[0] + reference_value = params[1] + char_func = params[2] + return bus_value - comp_value / char_func.evaluate( + bus_value / reference_value) - return deriv + +def bus_char_derivative(params, bus_value): + """Calculate derivative for bus char evaluation.""" + reference_value = params[1] + char_func = params[2] + d = 1e-3 + return (1 - ( + 1 / char_func.evaluate((bus_value + d) / reference_value) - + 1 / char_func.evaluate((bus_value - d) / reference_value) + ) / (2 * d)) def newton(func, deriv, params, y, **kwargs): @@ -497,8 +601,8 @@ def newton(func, deriv, params, y, **kwargs): valmin = kwargs.get('valmin', 70) valmax = kwargs.get('valmax', 3000) max_iter = kwargs.get('max_iter', 10) - tol_rel = kwargs.get('tol_rel', err) - tol_abs = kwargs.get('tol_abs', err) + tol_rel = kwargs.get('tol_rel', ERR ) + tol_abs = kwargs.get('tol_abs', ERR ) tol_mode = kwargs.get('tol_mode', 'abs') # start newton loop @@ -533,422 +637,65 @@ def newton(func, deriv, params, y, **kwargs): return x -# %% - - -def reverse_2d(params, y): - r""" - Calculate the residual value of an inverse function. - - Parameters - ---------- - params : list - Variable function parameters. - - y : float - Function value of function :math:`y = f \left( x_1, x_2 \right)`. - - Returns - ------- - deriv : float - Residual value of inverse function :math:`x_2 - f\left(x_1, y \right)`. - """ - func, x1, x2 = params[0], params[1], params[2] - return x2 - func.ev(x1, y) - - -def reverse_2d_deriv(params, y): - r""" - Calculate derivative of an inverse function. - - Parameters - ---------- - params : list - Variable function parameters. - - y : float - Function value of function :math:`y = f \left( x_1, x_2 \right)`, - so that :math:`x_2 - f\left(x_1, y \right) = 0` - - Returns - ------- - deriv : float - Partial derivative :math:`\frac{\partial f}{\partial y}`. - """ - func, x1 = params[0], params[1] - return - func.ev(x1, y, dy=1) - - -def bus_char_evaluation(params, bus_value): - r""" - Calculate the value of a bus. - - Parameters - ---------- - comp_value : float - Value of the energy transfer at the component. - - reference_value : float - Value of the bus in reference state. - char_func : tespy.tools.characteristics.char_line - Characteristic function of the bus. - - Returns - ------- - residual : float - Residual of the equation. - - .. math:: +def newton_with_kwargs( + derivative, target_value, val0=300, valmin=70, valmax=3000, max_iter=10, + tol_rel=ERR, tol_abs=ERR, tol_mode="rel", **function_kwargs + ): - residual = \dot{E}_\mathrm{bus} - \frac{\dot{E}_\mathrm{component}} - {f\left(\frac{\dot{E}_\mathrm{bus}} - {\dot{E}_\mathrm{bus,ref}}\right)} - """ - comp_value = params[0] - reference_value = params[1] - char_func = params[2] - return bus_value - comp_value / char_func.evaluate( - bus_value / reference_value) - - -def bus_char_derivative(params, bus_value): - """Calculate derivative for bus char evaluation.""" - reference_value = params[1] - char_func = params[2] - d = 1e-3 - return (1 - ( - 1 / char_func.evaluate((bus_value + d) / reference_value) - - 1 / char_func.evaluate((bus_value - d) / reference_value)) / (2 * d)) - - -def molar_mass_flow(flow): - r""" - Calculate molar mass flow. - - Parameters - ---------- - flow : list - Fluid property vector containing mass flow, pressure, enthalpy and - fluid composition. - - Returns - ------- - m_m : float - Molar mass flow m_m / (mol/s). - - .. math:: - - \dot{m}_\mathrm{m} = \sum_{i} \left( \frac{x_{i}}{M_{i}} \right) - """ - mm = 0 - for fluid, x in flow.items(): - if x > err: - mm += x / molar_masses[fluid] - return mm - -# %% - - -def num_fluids(fluids): - r""" - Return number of fluids in fluid mixture. - - Parameters - ---------- - fluids : dict - Fluid mass fractions. - - Returns - ------- - n : int - Number of fluids in fluid mixture n / 1. - - .. math:: - - n = \sum_{i} \left( \begin{cases} - 0 & x_{i} < \epsilon \\ - 1 & x_{i} \geq \epsilon - \end{cases} \right)\; - \forall i \in \text{network fluids} - """ - n = 0 - for fluid, x in fluids.items(): - if x > err: - n += 1 - - return n - -# %% - - -def single_fluid(fluids): - r""" - Return the name of the pure fluid in a fluid vector. - - Parameters - ---------- - fluids : dict - Fluid mass fractions. - - Returns - ------- - fluid : str - Name of the single fluid or None in case of mixtures. - """ - if num_fluids(fluids) == 1: - for fluid, x in fluids.items(): - if x > err: - return fluid - else: - return None - -# %% - - -def fluid_structure(fluid): - r""" - Return the checmical formula of fluid. - - Parameters - ---------- - fluid : str - Name of the fluid. - - Returns - ------- - parts : dict - Dictionary of the chemical base elements as keys and the number of - atoms in a molecule as values. - - Example - ------- - Get the chemical formula of methane. - - >>> from tespy.tools.helpers import fluid_structure - >>> elements = fluid_structure('methane') - >>> elements['C'], elements['H'] - (1, 4) - """ - parts = {} - for element in CP.get_fluid_param_string( - fluid, 'formula').split('}'): - if element != '': - el = element.split('_{') - parts[el[0]] = int(el[1]) - - return parts - -# %% - - -def darcy_friction_factor(re, ks, d): - r""" - Calculate the Darcy friction factor. - - Parameters - ---------- - re : float - Reynolds number re / 1. - - ks : float - Pipe roughness ks / m. - - d : float - Pipe diameter/characteristic lenght d / m. - - Returns - ------- - darcy_friction_factor : float - Darcy friction factor :math:`\lambda` / 1 - - Note - ---- - **Laminar flow** (:math:`re \leq 2320`) - - .. math:: - - \lambda = \frac{64}{re} - - **turbulent flow** (:math:`re > 2320`) - - *hydraulically smooth:* :math:`\frac{re \cdot k_{s}}{d} < 65` - - .. math:: - - \lambda = \begin{cases} - 0.03164 \cdot re^{-0.25} & re \leq 10^4\\ - \left(1.8 \cdot \log \left(re\right) -1.5 \right)^{-2} & - 10^4 < re < 10^6\\ - solve \left(0 = 2 \cdot \log\left(re \cdot \sqrt{\lambda} \right) -0.8 - - \frac{1}{\sqrt{\lambda}}\right) & re \geq 10^6\\ - \end{cases} - - *transition zone and hydraulically rough:* + # start newton loop + iteration = 0 + expr = True + x = val0 + parameter = function_kwargs["parameter"] + function = function_kwargs["function"] + relax = 1 - .. math:: + while expr: + # calculate function residual and new value + function_kwargs[parameter] = x + residual = target_value - function(**function_kwargs) + x += residual / derivative(**function_kwargs) * relax - \lambda = solve \left( 0 = 2 \cdot \log \left( \frac{2.51}{re \cdot - \sqrt{\lambda}} + \frac{k_{s}}{d \cdot 3.71} \right) - - \frac{1}{\sqrt{\lambda}} \right) + # check for value ranges + if x < valmin: + x = valmin + if x > valmax: + x = valmax - Reference: :cite:`Nirschl2018`. + iteration += 1 + # relaxation to help convergence in case of jumping + if iteration == 5: + relax = 0.75 + max_iter = 12 - Example - ------- - Calculate the Darcy friction factor at different hydraulic states. - - >>> from tespy.tools.helpers import darcy_friction_factor - >>> ks = 5e-5 - >>> d = 0.05 - >>> re_laminar = 2000 - >>> re_turb_smooth = 5000 - >>> re_turb_trans = 70000 - >>> re_high = 1000000 - >>> d_high = 0.8 - >>> re_very_high = 6000000 - >>> d_very_high = 1 - >>> ks_low = 1e-5 - >>> ks_rough = 1e-3 - >>> darcy_friction_factor(re_laminar, ks, d) - 0.032 - >>> round(darcy_friction_factor(re_turb_smooth, ks, d), 3) - 0.038 - >>> round(darcy_friction_factor(re_turb_trans, ks, d), 3) - 0.023 - >>> round(darcy_friction_factor(re_turb_trans, ks_rough, d), 3) - 0.049 - >>> round(darcy_friction_factor(re_high, ks, d_high), 3) - 0.012 - >>> round(darcy_friction_factor(re_very_high, ks_low, d_very_high), 3) - 0.009 - """ - if re <= 2320: - return 64 / re - else: - if re * ks / d < 65: - if re <= 1e4: - return blasius(re) - elif re < 1e6: - return hanakov(re) - else: - l0 = 0.02 - return newton( - prandtl_karman, prandtl_karman_derivative, [re], - 0, val0=l0, valmin=0.00001, valmax=0.2) + if iteration > max_iter: + msg = ( + 'The Newton algorithm was not able to find a feasible value ' + f'for function {function}. Current value with x={x} is ' + f'{function(**function_kwargs)}, target value is ' + f'{target_value}, residual is {residual} after {iteration} ' + 'iterations.' + ) + logger.debug(msg) + break + if tol_mode == 'abs': + expr = abs(residual) >= tol_abs + elif tol_mode == 'rel': + expr = abs(residual / target_value) >= tol_rel else: - l0 = 0.002 - return newton( - colebrook, colebrook_derivative, [re, ks, d], 0, - val0=l0, valmin=0.0001, valmax=0.2) - - -def blasius(re): - """ - Calculate friction coefficient according to Blasius. - - Parameters - ---------- - re : float - Reynolds number. - - Returns - ------- - darcy_friction_factor : float - Darcy friction factor. - """ - return 0.3164 * re ** (-0.25) - - -def hanakov(re): - """ - Calculate friction coefficient according to Hanakov. - - Parameters - ---------- - re : float - Reynolds number. - - Returns - ------- - darcy_friction_factor : float - Darcy friction factor. - """ - return (1.8 * np.log10(re) - 1.5) ** (-2) - - -def prandtl_karman(params, darcy_friction_factor): - """ - Calculate friction coefficient according to Prandtl and v. Kármán. - - Applied in smooth conditions. - - Parameters - ---------- - re : float - Reynolds number. - - darcy_friction_factor : float - Darcy friction factor. - - Returns - ------- - darcy_friction_factor : float - Darcy friction factor. - """ - re = params[0] - return ( - 2 * np.log10(re * darcy_friction_factor ** 0.5) - 0.8 - - 1 / darcy_friction_factor ** 0.5) - - -def prandtl_karman_derivative(params, darcy_friction_factor): - """Calculate derivative for Prandtl and v. Kármán equation.""" - return ( - 1 / (darcy_friction_factor * np.log(10)) + - 1 / 2 * darcy_friction_factor ** (-1.5)) - - -def colebrook(params, darcy_friction_factor): - """ - Calculate friction coefficient accroding to Colebrook-White equation. - - Applied in transition zone and rough conditions. - - Parameters - ---------- - re : float - Reynolds number. - - ks : float - Equivalent sand roughness. - - d : float - Pipe's diameter. - - darcy_friction_factor : float - Darcy friction factor. - - Returns - ------- - darcy_friction_factor : float - Darcy friction factor. - """ - re, ks, d = params[0], params[1], params[2] - return ( - 2 * np.log10( - 2.51 / (re * darcy_friction_factor ** 0.5) + ks / (3.71 * d)) + - 1 / darcy_friction_factor ** 0.5) + expr = abs(residual / target_value) >= tol_rel or abs(residual) >= tol_abs + return x -def colebrook_derivative(params, darcy_friction_factor): - """Calculate derivative for Colebrook-White equation.""" - d = 0.001 - return (colebrook(params, darcy_friction_factor + d) - - colebrook(params, darcy_friction_factor - d)) / (2 * d) -# %% +def central_difference(function=None, parameter=None, delta=None, **kwargs): + upper = kwargs.copy() + upper[parameter] += delta + lower = kwargs + lower[parameter] -= delta + return (function(**upper) - function(**lower)) / (2 * delta) def modify_path_os(path): @@ -983,8 +730,6 @@ def modify_path_os(path): return path -# %% - def get_basic_path(): """ diff --git a/src/tespy/tools/logger.py b/src/tespy/tools/logger.py index e73833b90..52c649d8b 100644 --- a/src/tespy/tools/logger.py +++ b/src/tespy/tools/logger.py @@ -12,6 +12,7 @@ import logging import os import sys +import warnings from logging import handlers import tespy @@ -31,6 +32,20 @@ logger.setLevel(logging.DEBUG) +class FutureWarningHandler: + def __init__(self, logger): + self.logger = logger + + def __call__(self, message, category, filename, lineno, file=None, line=None): + self.logger.warning( + f"FutureWarning: {message}", + stacklevel=2 # Adjust the stack level accordingly + ) + +# Register the custom warning handler for FutureWarnings +warnings.showwarning = FutureWarningHandler(logger) + + # Create a bunch of shorthand functions, this is mostly # copied straight from the logging module. def get_logger(): @@ -270,6 +285,9 @@ def add_file_logging( if logfile is not None: logfile_setting = os.path.join(logpath_setting, logfile) + if not os.path.isdir(logpath_setting): + os.makedirs(logpath_setting) + logrotation_setting = {'when': 'midnight', 'backupCount': 10} if logrotation is not None: logrotation_setting.update(logrotation) diff --git a/tests/test_advanced_tutorials.py b/tests/test_advanced_tutorials.py new file mode 100644 index 000000000..af240cce2 --- /dev/null +++ b/tests/test_advanced_tutorials.py @@ -0,0 +1,15 @@ +import os +import runpy + +import pytest + +path = os.path.join(os.path.dirname(__file__), "..", "tutorial", "advanced") +scripts = (os.path.join(path, f) for f in os.listdir(path) if f.endswith(".py")) + + +@pytest.mark.parametrize('script', scripts) +def test_tutorial_execution(script): + try: + runpy.run_path(script) + except ModuleNotFoundError: + pytest.skip("Test skipped due to missing dependency") \ No newline at end of file diff --git a/tests/test_analyses/test_entropy_analysis.py b/tests/test_analyses/test_entropy_analysis.py index 8c29f0657..54403b871 100644 --- a/tests/test_analyses/test_entropy_analysis.py +++ b/tests/test_analyses/test_entropy_analysis.py @@ -26,8 +26,7 @@ def setup_method(self): """Set up clausis rankine cycle with turbine driven feed water pump.""" self.Tamb = 20 self.pamb = 1 - fluids = ['water'] - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # create components diff --git a/tests/test_analyses/test_exergy_analysis.py b/tests/test_analyses/test_exergy_analysis.py index 3ae0bae16..54c27ee8e 100644 --- a/tests/test_analyses/test_exergy_analysis.py +++ b/tests/test_analyses/test_exergy_analysis.py @@ -26,7 +26,7 @@ from tespy.connections import Connection from tespy.networks import Network from tespy.tools import ExergyAnalysis -from tespy.tools.global_vars import err +from tespy.tools.global_vars import ERR from tespy.tools.helpers import TESPyNetworkError @@ -36,8 +36,7 @@ def setup_method(self): """Set up clausis rankine cycle with turbine driven feed water pump.""" self.Tamb = 20 self.pamb = 1 - fluids = ['water'] - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # create components @@ -74,8 +73,9 @@ def setup_method(self): cond = Connection(condenser, 'out1', fwp, 'in1', label='cond') fw = Connection(fwp, 'out1', steam_generator, 'in1', label='fw') fs_out = Connection(steam_generator, 'out1', cycle_close, 'in1') - self.nw.add_conns(fs_in, fs_fwpt, fs_t, fwpt_ws, t_ws, ws, cond, fw, - fs_out) + self.nw.add_conns( + fs_in, fs_fwpt, fs_t, fwpt_ws, t_ws, ws, cond, fw, fs_out + ) # component parameters turb.set_attr(eta_s=1) @@ -86,10 +86,11 @@ def setup_method(self): # connection parameters fs_in.set_attr(m=10, p=120, T=600, fluid={'water': 1}) - cond.set_attr(T=self.Tamb, x=0) + cond.set_attr(T=self.Tamb, x=0.0) # solve network self.nw.solve('design') + self.nw.print_results() self.nw._convergence_check() def test_exergy_analysis_perfect_cycle(self): @@ -100,9 +101,9 @@ def test_exergy_analysis_perfect_cycle(self): ean.analyse(pamb=self.pamb, Tamb=self.Tamb) msg = ( 'Exergy destruction of this network must be 0 (smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(ean.network_data.E_D), 4)) + ' .') - assert abs(ean.network_data.E_D) <= err ** 0.5, msg + assert abs(ean.network_data.E_D) <= 1e-2, msg msg = ( 'Exergy efficiency of this network must be 1 for this test but ' @@ -114,17 +115,17 @@ def test_exergy_analysis_perfect_cycle(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + ' .') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg msg = ( 'Fuel exergy and product exergy must be identical for this test. ' - 'Fuel exergy value: ' + str(round(ean.network_data.E_F, 4)) + - '. Product exergy value: ' + str(round(ean.network_data.E_P, 4)) + + 'Fuel exergy value: ' + str(round(ean.network_data.E_F, 2)) + + '. Product exergy value: ' + str(round(ean.network_data.E_P, 2)) + '.') - delta = round(abs(ean.network_data.E_F - ean.network_data.E_P), 4) - assert delta < err ** 0.5, msg + delta = round(abs(ean.network_data.E_F - ean.network_data.E_P), 2) + assert delta < 1e-2, msg def test_exergy_analysis_plotting_data(self): """Test exergy analysis plotting.""" @@ -164,9 +165,9 @@ def test_exergy_analysis_plotting_data(self): ) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + ' .') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg nodes = [ 'E_F', 'heat_input', 'steam generator', 'splitter 1', @@ -206,9 +207,9 @@ def test_exergy_analysis_violated_balance(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be violated for this test (larger than ' + - str(err ** 0.5) + ') but is ' + + str(ERR ** 0.5) + ') but is ' + str(round(abs(exergy_balance), 4)) + ' .') - assert abs(exergy_balance) > err ** 0.5, msg + assert abs(exergy_balance) > ERR ** 0.5, msg def test_exergy_analysis_bus_conversion(self): """Test exergy analysis bus conversion factors.""" @@ -274,7 +275,7 @@ def setup_method(self): self.Tamb = 20 self.pamb = 1 fluids = ['R134a'] - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # create components @@ -328,9 +329,9 @@ def test_exergy_analysis_bus_conversion(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + ' .') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg class TestCompressedAirIn: @@ -342,7 +343,7 @@ def setup_method(self): fluids = ['Air'] # compressor part - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # components @@ -387,9 +388,9 @@ def test_exergy_analysis_bus_conversion(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + ' .') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg class TestCompressedAirOut: @@ -401,7 +402,7 @@ def setup_method(self): fluids = ['Air'] # turbine part - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # components @@ -451,12 +452,13 @@ def test_exergy_analysis_bus_conversion(self): exergy_balance = ( ean.network_data.E_F - ean.network_data.E_P - - ean.network_data.E_L - ean.network_data.E_D) + ean.network_data.E_L - ean.network_data.E_D + ) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + '.') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg msg = ( 'Exergy efficiency must be equal to 1.0 for this test but is ' + @@ -490,7 +492,7 @@ def setup_method(self): fluids = ['Air'] # turbine part - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # components @@ -547,9 +549,9 @@ def run_analysis(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + '.') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg E_D_agg = ean.aggregation_data['E_D'].sum() E_D_nw = ean.network_data.loc['E_D'] @@ -569,7 +571,7 @@ def setup_method(self): fluids = ['Air'] # turbine part - self.nw = Network(fluids=fluids) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', h_unit='kJ / kg') # components @@ -627,9 +629,9 @@ def run_analysis(self): ean.network_data.E_L - ean.network_data.E_D) msg = ( 'Exergy balance must be closed (residual value smaller than ' + - str(err ** 0.5) + ') for this test but is ' + + str(ERR ** 0.5) + ') for this test but is ' + str(round(abs(exergy_balance), 4)) + '.') - assert abs(exergy_balance) <= err ** 0.5, msg + assert abs(exergy_balance) <= ERR ** 0.5, msg E_D_agg = ean.aggregation_data['E_D'].sum() E_D_nw = ean.network_data.loc['E_D'] diff --git a/tests/test_basic_tutorials.py b/tests/test_basic_tutorials.py new file mode 100644 index 000000000..80f127f31 --- /dev/null +++ b/tests/test_basic_tutorials.py @@ -0,0 +1,12 @@ +import os +import runpy + +import pytest + +path = os.path.join(os.path.dirname(__file__), "..", "tutorial", "basics") +scripts = (os.path.join(path, f) for f in os.listdir(path) if f.endswith(".py")) + + +@pytest.mark.parametrize('script', scripts) +def test_tutorial_execution(script): + runpy.run_path(script) \ No newline at end of file diff --git a/tests/test_busses.py b/tests/test_busses.py index d297d0f1b..074a6dbd6 100644 --- a/tests/test_busses.py +++ b/tests/test_busses.py @@ -58,11 +58,8 @@ def setup_method(self): # %% connection parameters amb_cp.set_attr( T=20, p=1, m=100, - fluid={'Ar': 0.0129, 'N2': 0.7553, 'H2O': 0, 'CH4': 0, - 'CO2': 0.0004, 'O2': 0.2314}) - sf_cc.set_attr( - T=20, fluid={'CO2': 0.04, 'Ar': 0, 'N2': 0, - 'O2': 0, 'H2O': 0, 'CH4': 0.96}) + fluid={'Ar': 0.0129, 'N2': 0.7553, 'CO2': 0.0004, 'O2': 0.2314}) + sf_cc.set_attr(T=20, fluid={'CO2': 0.04, 'CH4': 0.96}) gt_fg.set_attr(p=1) # motor efficiency @@ -166,7 +163,7 @@ def test_model(self): ti.label + ' (' + str(eta_ti) + ') must be equal to 1.0.') assert eta_ti == 1.0, msg - # test partload for bus functions + # test part load for bus functions # first test in identical conditions self.nw.get_conn('ambient air flow').set_attr(m=None) diff --git a/tests/test_components/test_combustion.py b/tests/test_components/test_combustion.py index 1fc25bf8b..b26bcbafa 100644 --- a/tests/test_components/test_combustion.py +++ b/tests/test_components/test_combustion.py @@ -28,8 +28,7 @@ class TestCombustion: def setup_method(self): - self.nw = Network(['H2O', 'N2', 'O2', 'Ar', 'CO2', 'CH4'], - T_unit='C', p_unit='bar', v_unit='m3 / s') + self.nw = Network(T_unit='C', p_unit='bar', v_unit='m3 / s') self.fuel = Source('fuel') self.air = Source('ambient air') self.fg = Sink('flue gas') @@ -67,23 +66,25 @@ def test_CombustionChamber(self): self.setup_CombustionChamber_network(instance) # connection parameter specification - air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'H2O': 0, 'CO2': 0, - 'CH4': 0} - fuel = {'N2': 0, 'O2': 0, 'Ar': 0, 'H2O': 0, 'CO2': 0.04, 'CH4': 0.96} + air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} + fuel = {'CO2': 0.04, 'CH4': 0.96} self.c1.set_attr(fluid=air, p=1, T=30) self.c2.set_attr(fluid=fuel, T=30) - self.c3.set_attr(T=1200) + instance.set_attr(lamb=1.5) # test specified bus value on CombustionChamber (must be equal to ti) b = Bus('thermal input', P=1e6) b.add_comps({'comp': instance}) self.nw.add_busses(b) self.nw.solve('design') + self.c3.set_attr(T=1200) + instance.set_attr(lamb=None) + self.nw.solve('design') self.nw._convergence_check() msg = ('Value of thermal input must be ' + str(b.P.val) + ', is ' + str(instance.ti.val) + '.') assert round(b.P.val, 1) == round(instance.ti.val, 1), msg - b.set_attr(P=np.nan) + b.set_attr(P=None) # test specified thermal input for CombustionChamber instance.set_attr(ti=1e6) @@ -96,7 +97,7 @@ def test_CombustionChamber(self): assert round(ti, 1) == round(instance.ti.val, 1), msg # test specified lamb for CombustionChamber - self.c3.set_attr(T=np.nan) + self.c3.set_attr(T=None) instance.set_attr(lamb=1) self.nw.solve('design') self.nw._convergence_check() @@ -112,15 +113,16 @@ def test_DiabaticCombustionChamber(self): self.setup_CombustionChamber_network(instance) # connection parameter specification - air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'H2O': 0, 'CO2': 0, - 'CH4': 0} - fuel = {'N2': 0, 'O2': 0, 'Ar': 0, 'H2O': 0, 'CO2': 0.04, 'CH4': 0.96} + air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} + fuel = {'CO2': 0.04, 'CH4': 0.96} self.c1.set_attr(fluid=air, p=1.2, T=30) self.c2.set_attr(fluid=fuel, T=30, p=1.5) - self.c3.set_attr(T=1200) pr = 0.97 - instance.set_attr(pr=pr, eta=0.95, ti=1e6) + instance.set_attr(pr=pr, eta=0.95, ti=1e6, lamb=1.5) + self.nw.solve('design') + self.c3.set_attr(T=1200) + instance.set_attr(lamb=None) self.nw.solve('design') self.nw._convergence_check() @@ -134,16 +136,11 @@ def test_DiabaticCombustionChamber(self): ) assert valid == check, msg - # test invalid pressure specification -> leading to linear dependency - self.c2.set_attr(p=None) - self.c3.set_attr(p=1.3) - self.nw.solve('design') - assert self.nw.lin_dep, "Calculation must not converge in this case." - # test invalid pressure ratio instance.set_attr(pr=None) self.c1.set_attr(p=1.2) self.c2.set_attr(p=1.5) + self.c3.set_attr(p=1.3) self.nw.solve('design') self.nw._convergence_check() @@ -155,6 +152,13 @@ def test_DiabaticCombustionChamber(self): ) assert valid == check, msg + # test invalid pressure specification -> leading to linear dependency + instance.set_attr(pr=pr) + self.c2.set_attr(p=None) + self.c3.set_attr(p=1.3) + self.nw.solve('design') + assert self.nw.lin_dep, "Calculation must not converge in this case." + def test_CombustionEngine(self): """Test component properties of combustion engine.""" instance = CombustionEngine('combustion engine') @@ -162,9 +166,9 @@ def test_CombustionEngine(self): air = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'H2O': 0, 'CO2': 0, 'CH4': 0} - fuel = {'N2': 0, 'O2': 0, 'Ar': 0, 'H2O': 0, 'CO2': 0.04, 'CH4': 0.96} - water1 = {'N2': 0, 'O2': 0, 'Ar': 0, 'H2O': 1, 'CO2': 0, 'CH4': 0} - water2 = {'N2': 0, 'O2': 0, 'Ar': 0, 'H2O': 1, 'CO2': 0, 'CH4': 0} + fuel = {'CO2': 0.04, 'CH4': 0.96} + water1 = {'H2O': 1} + water2 = {'H2O': 1} # connection parametrisation instance.set_attr(pr1=0.99, pr2=0.99, lamb=1.0, @@ -203,7 +207,7 @@ def test_CombustionEngine(self): assert round(TI.P.val, 1) == round(instance.ti.val, 1), msg # test specified thermal input in component - TI.set_attr(P=np.nan) + TI.set_attr(P=None) instance.set_attr(ti=ti) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') self.nw._convergence_check() @@ -227,7 +231,7 @@ def test_CombustionEngine(self): msg = ('Value of heat output 1 must be ' + str(-heat1) + ', is ' + str(instance.Q1.val) + '.') assert round(heat1, 1) == -round(instance.Q1.val, 1), msg - Q1.set_attr(P=np.nan) + Q1.set_attr(P=None) # test specified heat output 2 bus value Q2.set_attr(P=1.2 * instance.Q2.val) @@ -241,7 +245,7 @@ def test_CombustionEngine(self): assert round(heat2, 1) == -round(instance.Q2.val, 1), msg # test specified heat output 2 in component - Q2.set_attr(P=np.nan) + Q2.set_attr(P=None) instance.set_attr(Q2=-heat2) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') self.nw._convergence_check() @@ -251,7 +255,7 @@ def test_CombustionEngine(self): assert round(heat2, 1) == -round(instance.Q2.val, 1), msg # test total heat output bus value - instance.set_attr(Q2=np.nan) + instance.set_attr(Q2=None) Q.set_attr(P=1.5 * instance.Q1.val) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') self.nw._convergence_check() @@ -262,7 +266,7 @@ def test_CombustionEngine(self): assert round(Q.P.val, 1) == -round(heat, 1), msg # test specified heat loss bus value - Q.set_attr(P=np.nan) + Q.set_attr(P=None) Qloss.set_attr(P=-1e5) self.nw.solve('offdesign', init_path='tmp', design_path='tmp') self.nw._convergence_check() diff --git a/tests/test_components/test_customs.py b/tests/test_components/test_customs.py index b2e968ab0..b3360a352 100644 --- a/tests/test_components/test_customs.py +++ b/tests/test_components/test_customs.py @@ -7,137 +7,3 @@ tests/test_components/test_customs.py SPDX-License-Identifier: MIT """ -import shutil - -import numpy as np - -from tespy.components import ORCEvaporator -from tespy.components import Sink -from tespy.components import Source -from tespy.connections import Bus -from tespy.connections import Connection -from tespy.networks import Network -from tespy.tools.fluid_properties import T_bp_p - - -class TestOrcEvaporator: - - def setup_method(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') - - self.instance = ORCEvaporator('orc evaporator') - - self.c1 = Connection(self.inl1, 'out1', self.instance, 'in1') - self.c2 = Connection(self.instance, 'out1', self.outl1, 'in1') - self.c3 = Connection(self.inl2, 'out1', self.instance, 'in2') - self.c4 = Connection(self.instance, 'out2', self.outl2, 'in1') - self.c5 = Connection(self.inl3, 'out1', self.instance, 'in3') - self.c6 = Connection(self.instance, 'out3', self.outl3, 'in1') - - self.nw.add_conns(self.c1, self.c2, self.c3, self.c4, self.c5, self.c6) - - def test_ORCEvaporator(self): - """Test component properties of orc evaporator.""" - # design specification - self.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 - self.instance.set_attr(Q=Q) - self.nw.solve('design') - self.nw._convergence_check() - 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)) + '.') - assert round(Q, 0) == round(Q_is, 0), msg - - # test bus - self.instance.set_attr(Q=np.nan) - P = -6.64e+07 - b = Bus('heat transfer', P=P) - b.add_comps({'comp': self.instance}) - self.nw.add_busses(b) - self.nw.solve('design') - self.nw._convergence_check() - 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)) + '.') - assert 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 = self.instance.zeta1.val - zeta2 = self.instance.zeta2.val - zeta3 = self.instance.zeta3.val - - msg = ('Vapor mass fraction of steam outlet must be 0.0, is ' + - str(round(x_outl1_calc, 1)) + '.') - assert 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)) + '.') - assert round(x_outl3_calc, 1) == 1.0, msg - - # Check offdesign by zeta values - # geometry independent friction coefficient - self.nw.solve('offdesign', design_path='tmp') - self.nw._convergence_check() - - msg = ('Geometry independent friction coefficient ' - 'at hot side 1 (steam) ' - 'must be ' + str(round(zeta1, 1)) + ', is ' + - str(round(self.instance.zeta1.val, 1)) + '.') - assert round(self.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(self.instance.zeta2.val, 1)) + '.') - assert round(self.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(self.instance.zeta3.val, 1)) + '.') - assert round(self.instance.zeta3.val, 1) == round(zeta3, 1), msg - - # test parameters of 'subcooling' and 'overheating' - self.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') - self.nw._convergence_check() - - T_steam = T_bp_p(self.c2.get_flow()) - dT - T_isop = T_bp_p(self.c6.get_flow()) + dT - - msg = ('Temperature of working fluid outlet must be ' + - str(round(T_isop, 1)) + ', is ' + - str(round(self.c6.T.val_SI, 1)) + '.') - assert 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)) + '.') - assert round(T_steam, 1) == round(self.c2.T.val_SI, 1), msg - - shutil.rmtree('./tmp', ignore_errors=True) diff --git a/tests/test_components/test_heat_exchangers.py b/tests/test_components/test_heat_exchangers.py index 30fd4e667..1df513de3 100644 --- a/tests/test_components/test_heat_exchangers.py +++ b/tests/test_components/test_heat_exchangers.py @@ -23,16 +23,14 @@ from tespy.connections import Bus from tespy.connections import Connection from tespy.networks import Network -from tespy.tools.fluid_properties import T_bp_p +from tespy.tools.fluid_properties import T_sat_p class TestHeatExchangers: def setup_method(self): - self.nw = Network( - ['H2O', 'Ar', 'INCOMP::S800'], T_unit='C', p_unit='bar', - v_unit='m3 / s') + self.nw = Network(T_unit='C', p_unit='bar', v_unit='m3 / s') self.inl1 = Source('inlet 1') self.outl1 = Sink('outlet 1') @@ -59,78 +57,75 @@ def test_SimpleHeatExchanger(self): """Test component properties of simple heat exchanger.""" instance = SimpleHeatExchanger('heat exchanger') self.setup_SimpleHeatExchanger_network(instance) - fl = {'Ar': 0, 'H2O': 1, 'S800': 0} + fl = {'H2O': 1} self.c1.set_attr(fluid=fl, m=1, p=10, T=100) # trigger heat exchanger parameter groups - instance.set_attr(hydro_group='HW', L=100, ks=100, pr=0.99, Tamb=20) + instance.set_attr(L=100, ks_HW=100, pr=0.99, Tamb=20) # test grouped parameter settings with missing parameters - instance.hydro_group.is_set = True + instance.darcy_group.is_set = True instance.kA_group.is_set = True instance.kA_char_group.is_set = True self.nw.solve('design', init_only=True) - msg = ('Hydro group must no be set, if one parameter is missing!') - assert instance.hydro_group.is_set is False, msg + msg = ('Darcy group must no be set, if one parameter is missing!') + assert not instance.darcy_group.is_set, msg msg = ('kA group must no be set, if one parameter is missing!') - assert instance.kA_group.is_set is False, msg + assert not instance.kA_group.is_set, msg msg = ('kA char group must no be set, if one parameter is missing!') - assert instance.kA_char_group.is_set is False, msg + assert not instance.kA_char_group.is_set, msg # test diameter calculation from specified dimensions (as pipe) # with Hazen-Williams method - instance.set_attr(hydro_group='HW', D='var', L=100, - ks=100, pr=0.99, Tamb=20) + instance.set_attr(D='var', L=100, ks_HW=100, pr=0.99, Tamb=20) b = Bus('heat', P=-1e5) b.add_comps({'comp': instance}) self.nw.add_busses(b) self.nw.solve('design') self.nw._convergence_check() 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) + '.') + msg = f"Value of pressure ratio must be {pr}, is {instance.pr.val}." assert pr == round(instance.pr.val, 3), msg # make zeta system variable and use previously calculated diameter # to calculate zeta. The value for zeta must not change zeta = round(instance.zeta.val, 0) - instance.set_attr(D=instance.D.val, zeta='var', pr=np.nan) + instance.set_attr(D=instance.D.val, zeta='var', pr=None) instance.D.is_var = False self.nw.solve('design') self.nw._convergence_check() - msg = ('Value of zeta must be ' + str(zeta) + ', is ' + - str(round(instance.zeta.val, 0)) + '.') + msg = f"Value of pressure ratio must be {zeta}, is {instance.zeta.val}." assert zeta == round(instance.zeta.val, 0), msg # same test with pressure ratio as sytem variable pr = round(instance.pr.val, 3) - instance.set_attr(zeta=np.nan, pr='var') + instance.set_attr(zeta=None, pr='var') self.nw.solve('design') self.nw._convergence_check() - msg = ('Value of pressure ratio must be ' + str(pr) + - ', is ' + str(round(instance.pr.val, 3)) + '.') + msg = f"Value of pressure ratio must be {pr}, is {instance.pr.val}." assert pr == round(instance.pr.val, 3), msg # test heat transfer coefficient as variable of the system (ambient # temperature required) - instance.set_attr(kA='var', pr=np.nan) + instance.set_attr(kA='var', pr=None) b.set_attr(P=-5e4) self.nw.solve('design') self.nw._convergence_check() # due to heat output being half of reference (for Tamb) kA should be # somewhere near to that (actual value is 677) - msg = ('Value of heat transfer coefficient must be 677, is ' + - str(instance.kA.val) + '.') + msg = ( + "Value of heat transfer coefficient must be 677, is " + f"{instance.kA.val}." + ) assert 677 == round(instance.kA.val, 0), msg # test heat transfer as variable of the system - instance.set_attr(Q='var', kA=np.nan) + instance.set_attr(Q='var', kA=None) Q = -5e4 b.set_attr(P=Q) self.nw.solve('design') self.nw._convergence_check() - msg = ('Value of heat transfer must be ' + str(Q) + - ', is ' + str(instance.Q.val) + '.') + msg = f"Value of heat transfer must be {Q}, is {instance.Q.val}." assert Q == round(instance.Q.val, 0), msg # test kA as network results parameter @@ -158,18 +153,17 @@ def test_ParabolicTrough(self): """Test component properties of parabolic trough.""" instance = ParabolicTrough('parabolic trough') self.setup_SimpleHeatExchanger_network(instance) - fl = {'Ar': 0, 'H2O': 0, 'S800': 1} - self.c1.set_attr(fluid=fl, p=2, T=200) + self.c1.set_attr(fluid={'INCOMP::S800': 1}, p=2, T=200) self.c2.set_attr(T=350) # test grouped parameter settings with missing parameters - instance.hydro_group.is_set = True + instance.darcy_group.is_set = True instance.energy_group.is_set = True self.nw.solve('design', init_only=True) - msg = ('Hydro group must no be set, if one parameter is missing!') - assert instance.hydro_group.is_set is False, msg + msg = ('Darcy group must no be set, if one parameter is missing!') + assert not instance.darcy_group.is_set, msg msg = ('Energy group must no be set, if one parameter is missing!') - assert instance.energy_group.is_set is False, msg + assert not instance.energy_group.is_set, msg # test solar collector params as system variables instance.set_attr( @@ -263,18 +257,18 @@ def test_SolarCollector(self): """Test component properties of solar collector.""" instance = SolarCollector('solar collector') self.setup_SimpleHeatExchanger_network(instance) - fl = {'Ar': 0, 'H2O': 1, 'S800': 0} + fl = {'H2O': 1} self.c1.set_attr(fluid=fl, p=10, T=30) self.c2.set_attr(T=70) # test grouped parameter settings with missing parameters - instance.hydro_group.is_set = True + instance.darcy_group.is_set = True instance.energy_group.is_set = True self.nw.solve('design', init_only=True) - msg = ('Hydro group must no be set, if one parameter is missing!') - assert instance.hydro_group.is_set is False, msg + msg = ('Darcy group must no be set, if one parameter is missing!') + assert not instance.darcy_group.is_set, msg msg = ('Energy group must no be set, if one parameter is missing!') - assert instance.energy_group.is_set is False, msg + assert not instance.energy_group.is_set, msg # test solar collector params as system variables instance.set_attr(E=1e3, lkf_lin=1.0, lkf_quad=0.005, A='var', @@ -343,9 +337,9 @@ def test_HeatExchanger(self): instance.set_attr(pr1=0.98, pr2=0.98, ttd_u=5, design=['pr1', 'pr2', 'ttd_u'], offdesign=['zeta1', 'zeta2', 'kA_char']) - self.c1.set_attr(T=120, p=3, fluid={'Ar': 0, 'H2O': 1, 'S800': 0}) + self.c1.set_attr(T=120, p=3, fluid={'H2O': 1}) self.c2.set_attr(T=70) - self.c3.set_attr(T=40, p=5, fluid={'Ar': 1, 'H2O': 0, 'S800': 0}) + self.c3.set_attr(T=40, p=5, fluid={'Ar': 1}) b = Bus('heat transfer', P=-80e3) b.add_comps({'comp': instance}) self.nw.add_busses(b) @@ -393,7 +387,7 @@ def test_HeatExchanger(self): assert ttd_u_calc == ttd_u, msg # check lower terminal temperature difference - self.c2.set_attr(T=np.nan) + self.c2.set_attr(T=None) instance.set_attr(ttd_l=20) self.nw.solve('design') self.nw._convergence_check() @@ -407,7 +401,7 @@ def test_HeatExchanger(self): # check specified kA value (by offdesign parameter), reset temperatures # to design state self.c2.set_attr(T=70) - instance.set_attr(ttd_l=np.nan) + instance.set_attr(ttd_l=None) self.nw.solve('offdesign', design_path='tmp') self.nw._convergence_check() msg = ('Value of heat flow must be ' + str(instance.Q.val) + ', is ' + @@ -418,7 +412,7 @@ def test_HeatExchanger(self): assert kA == round(instance.kA.val, 0), msg # trigger negative lower terminal temperature difference as result - self.c4.set_attr(T=np.nan) + self.c4.set_attr(T=None) self.c2.set_attr(T=30) self.nw.solve('design') self.nw._convergence_check() @@ -429,10 +423,10 @@ def test_HeatExchanger(self): # trigger negative upper terminal temperature difference as result self.c4.set_attr(T=100) - self.c2.set_attr(h=200e3, T=np.nan) - instance.set_attr(pr1=0.98, pr2=0.98, ttd_u=np.nan, + self.c2.set_attr(h=200e3, T=None) + instance.set_attr(pr1=0.98, pr2=0.98, ttd_u=None, design=['pr1', 'pr2']) - self.c1.set_attr(h=150e3, T=np.nan) + self.c1.set_attr(h=150e3, T=None) self.c3.set_attr(T=40) self.nw.solve('design') self.nw._convergence_check() @@ -449,10 +443,11 @@ def test_Condenser(self): self.setup_HeatExchanger_network(instance) # design specification - instance.set_attr(pr1=0.98, pr2=0.98, ttd_u=5, - offdesign=['zeta2', 'kA_char']) - self.c1.set_attr(T=100, p0=0.5, fluid={'Ar': 0, 'H2O': 1, 'S800': 0}) - self.c3.set_attr(T=30, p=5, fluid={'Ar': 0, 'H2O': 1, 'S800': 0}) + instance.set_attr( + pr1=0.98, pr2=0.98, ttd_u=5, offdesign=['zeta2', 'kA_char'] + ) + self.c1.set_attr(T=100, p0=0.5, fluid={'H2O': 1}) + self.c3.set_attr(T=30, p=5, fluid={'H2O': 1}) self.c4.set_attr(T=40) instance.set_attr(Q=-80e3) self.nw.solve('design') @@ -485,7 +480,7 @@ def test_Condenser(self): # test upper terminal temperature difference. For the component # condenser the temperature of the condensing fluid is relevant. - ttd_u = round(T_bp_p(self.c1.get_flow()) - self.c4.T.val_SI, 1) + ttd_u = round(self.c1.calc_T_sat() - self.c4.T.val_SI, 1) p = round(self.c1.p.val_SI, 5) msg = ('Value of terminal temperature difference must be ' + str(round(instance.ttd_u.val, 1)) + ', is ' + @@ -493,7 +488,7 @@ def test_Condenser(self): assert ttd_u == round(instance.ttd_u.val, 1), msg # test lower terminal temperature difference - instance.set_attr(ttd_l=20, ttd_u=np.nan, design=['pr2', 'ttd_l']) + instance.set_attr(ttd_l=20, ttd_u=None, design=['pr2', 'ttd_l']) self.nw.solve('design') self.nw._convergence_check() msg = ('Value of terminal temperature difference must be ' + @@ -519,10 +514,8 @@ def test_CondenserWithEvaporation(self): # design specification instance.set_attr(pr1=1, pr2=1, offdesign=["kA"]) - self.c1.set_attr(x=1, p=1, fluid={'Ar': 0, 'H2O': 1, 'S800': 0}, m=1) - self.c3.set_attr( - x=0, p=0.7, fluid={'Ar': 0, 'H2O': 1, 'S800': 0}, m=2, design=["m"] - ) + self.c1.set_attr(x=1, p=1, fluid={'H2O': 1}, m=1) + self.c3.set_attr(x=0, p=0.7, fluid={'H2O': 1}, m=2, design=["m"]) self.nw.solve('design') self.nw._convergence_check() ttd_l = round(instance.ttd_l.val, 3) diff --git a/tests/test_components/test_merge.py b/tests/test_components/test_merge.py new file mode 100644 index 000000000..fcc6065f2 --- /dev/null +++ b/tests/test_components/test_merge.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 + +"""Module for testing components of type merge. +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/test_components/test_merge.py +SPDX-License-Identifier: MIT +""" + +from pytest import approx + +from tespy.components import Merge +from tespy.components import SimpleHeatExchanger +from tespy.components import Sink +from tespy.components import Source +from tespy.components import Splitter +from tespy.connections import Connection +from tespy.networks import Network + + +class TestMerge: + + def setup_method(self): + self.nwk = Network(T_unit="C", p_unit="bar", h_unit="kJ / kg") + + so1 = Source("Source1") + so2 = Source("Source2") + me = Merge("Merge") + si = Sink("Sink") + + c1 = Connection(so1, "out1", me, "in1", label="1") + c2 = Connection(so2, "out1", me, "in2", label="2") + c3 = Connection(me, "out1", si, "in1", label="3") + + self.nwk.add_conns(c1, c2, c3) + + def test_single_fluid_at_outlet(self): + + c1, c2, c3 = self.nwk.get_conn(["1", "2", "3"]) + c1.set_attr(m=5, p=10, h=200) + c2.set_attr(m=5, h=200) + c3.set_attr(fluid={"water": 1}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = c1.m.val_SI + c2.m.val_SI + msg = f"Target value for mass flow at connection 3 must be {target}." + assert c3.m.val_SI == approx(target), msg + + def test_massflows_from_two_fluid_fractions(self): + + c1, c2, c3 = self.nwk.get_conn(["1", "2", "3"]) + c1.set_attr(m=5, p=10, h=200, fluid={"N2": 1}) + c2.set_attr(h=200, fluid={"O2": 1}) + c3.set_attr(fluid={"N2": 0.3, "O2": 0.7}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = c1.m.val / c3.fluid.val["N2"] + msg = f"Target value for mass flow at connection 3 is {target}" + assert c3.m.val_SI == approx(target), msg + + +class TestCyclicMerging: + """ + Testing issue raised in https://github.com/oemof/tespy/issues/424 + """ + + def setup_method(self): + + self.nwk = Network(T_unit="C", p_unit="bar", h_unit="kJ / kg") + + source = Source("source1") + merge = Merge("merge") + component1 = SimpleHeatExchanger("comp1", pr=1) + splitter = Splitter("splitter") + component2 = SimpleHeatExchanger("comp2") + sink = Sink("sink") + + c1 = Connection(source, "out1", merge, "in1", label="1") + c2 = Connection(merge, "out1", component1, "in1", label="2") + c3 = Connection(component1, "out1", splitter, "in1", label="3") + c4 = Connection(splitter, "out1", component2, "in1", label="4") + c5 = Connection(component2, "out1", merge, "in2", label="5") + c6 = Connection(splitter, "out2", sink, "in1", label="6") + + self.nwk.add_conns(c1, c2, c3, c4, c5, c6) + + def test_single_fluid_setup(self): + + c1, c3, c4, c5, c6 = self.nwk.get_conn(["1", "3", "4", "5", "6"]) + + c1.set_attr(p=1, h=200, m=10, fluid={"R134a": 1}) + c3.set_attr(h=180) + c4.set_attr(m=1) + c5.set_attr(h=170) + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = c1.m.val_SI + msg = f"Target value for mass flow at connection 3 is {target}" + assert c6.m.val_SI == approx(target), msg + + def test_two_fluid_setup(self): + c1, c3, c4, c5, c6 = self.nwk.get_conn(["1", "3", "4", "5", "6"]) + + c1.set_attr(p=1, h=200, m=10, fluid={"R134a": 1}) + c3.set_attr(h=180) + c4.set_attr(m=1) + c5.set_attr(h=170) + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = c1.m.val_SI + msg = f"Target value for mass flow at connection 3 is {target}" + assert c6.m.val_SI == approx(target), msg + + +test = TestMerge() +test.setup_method() +test.test_single_fluid_at_outlet() +test.setup_method() +test.test_massflows_from_two_fluid_fractions() + + +test2 = TestCyclicMerging() +test2.setup_method() +test2.test_single_fluid_setup() +test2.setup_method() +test2.test_two_fluid_setup() diff --git a/tests/test_components/test_piping.py b/tests/test_components/test_piping.py index 576fa9ebd..d75b4e7db 100644 --- a/tests/test_components/test_piping.py +++ b/tests/test_components/test_piping.py @@ -23,7 +23,7 @@ class TestPiping: def setup_piping_network(self, instance): - self.nw = Network(['CH4'], T_unit='C', p_unit='bar') + self.nw = Network(T_unit='C', p_unit='bar') self.source = Source('source') self.sink = Sink('sink') self.c1 = Connection(self.source, 'out1', instance, 'in1') @@ -65,7 +65,7 @@ def test_Valve(self): 'char_func': dp_char, 'is_set': True}) m = 11 self.c1.set_attr(m=m) - self.c2.set_attr(p=np.nan) + self.c2.set_attr(p=None) self.nw.solve('design') self.nw._convergence_check() self.nw.print_results() diff --git a/tests/test_components/test_reactors.py b/tests/test_components/test_reactors.py index 5c16cbb21..e20d1c8d0 100644 --- a/tests/test_components/test_reactors.py +++ b/tests/test_components/test_reactors.py @@ -26,7 +26,7 @@ class TestReactors: def setup_method(self): """Set up network for electrolyzer tests.""" - self.nw = Network(['O2', 'H2', 'H2O'], T_unit='C', p_unit='bar') + self.nw = Network(T_unit='C', p_unit='bar') self.instance = WaterElectrolyzer('electrolyzer') fw = Source('feed water') @@ -37,8 +37,9 @@ def setup_method(self): self.instance.set_attr(pr=0.99, eta=1) - cw_el = Connection(cw_in, 'out1', self.instance, 'in1', - fluid={'H2O': 1, 'H2': 0, 'O2': 0}, T=20, p=1) + cw_el = Connection( + cw_in, 'out1', self.instance, 'in1', fluid={'H2O': 1}, T=20, p=1 + ) el_cw = Connection(self.instance, 'out1', cw_out, 'in1', T=45) self.nw.add_conns(cw_el, el_cw) @@ -74,7 +75,7 @@ def test_WaterElectrolyzer(self): assert round(self.instance.Q.val, 4) == 0.0, msg # reset power, change efficiency value and specify heat bus value - power.set_attr(P=np.nan) + power.set_attr(P=None) self.nw.get_conn('h2o').set_attr(T=25, p=1) self.nw.get_conn('h2').set_attr(T=50) self.instance.set_attr(eta=0.8) @@ -118,7 +119,7 @@ def test_WaterElectrolyzer(self): # test efficiency value > 1, Q must be larger than 0 e = 130e6 - self.instance.set_attr(e=np.nan, eta=np.nan) + self.instance.set_attr(e=None, eta=None) self.instance.set_attr(e=e) self.nw.solve('design') self.nw._convergence_check() @@ -135,7 +136,7 @@ def test_WaterElectrolyzer(self): # test specific energy consumption e = 150e6 - self.instance.set_attr(e=np.nan, eta=np.nan) + self.instance.set_attr(e=None, eta=None) self.instance.set_attr(e=e) self.nw.solve('design') self.nw._convergence_check() @@ -157,7 +158,7 @@ def test_WaterElectrolyzer(self): # use zeta as offdesign parameter, at design point pressure # ratio must not change - self.instance.set_attr(zeta=np.nan, offdesign=['zeta']) + self.instance.set_attr(zeta=None, offdesign=['zeta']) self.nw.solve('offdesign', design_path='tmp') self.nw._convergence_check() msg = ('Value of pressure ratio must be ' + str(pr) + ', is ' + @@ -166,7 +167,7 @@ def test_WaterElectrolyzer(self): # test heat output specification in offdesign mode Q = self.instance.Q.val * 0.9 - self.instance.set_attr(Q=Q, P=np.nan) + self.instance.set_attr(Q=Q, P=None) self.nw.solve('offdesign', design_path='tmp') self.nw._convergence_check() msg = ('Value of heat must be ' + str(Q) + ', is ' + diff --git a/tests/test_components/test_turbomachinery.py b/tests/test_components/test_turbomachinery.py index e96be4a2b..38901e8e9 100644 --- a/tests/test_components/test_turbomachinery.py +++ b/tests/test_components/test_turbomachinery.py @@ -31,8 +31,7 @@ class TestTurbomachinery: def setup_network(self, instance): - self.nw = Network(['INCOMP::DowQ', 'NH3', 'N2', 'O2', 'Ar'], - T_unit='C', p_unit='bar', v_unit='m3 / s') + self.nw = Network(T_unit='C', p_unit='bar', v_unit='m3 / s') self.source = Source('source') self.sink = Sink('sink') self.c1 = Connection(self.source, 'out1', instance, 'in1') @@ -45,7 +44,7 @@ def test_Compressor(self): self.setup_network(instance) # compress NH3, other fluids in network are for turbine, pump, ... - fl = {'N2': 1, 'O2': 0, 'Ar': 0, 'DowQ': 0, 'NH3': 0} + fl = {'N2': 1} 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) @@ -55,7 +54,7 @@ def test_Compressor(self): # test isentropic efficiency value eta_s_d = ( - (isentropic(self.c1.get_flow(), self.c2.get_flow()) - + (isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - self.c1.h.val_SI) / (self.c2.h.val_SI - self.c1.h.val_SI)) msg = ('Value of isentropic efficiency must be ' + str(eta_s_d) + ', is ' + str(instance.eta_s.val) + '.') @@ -68,7 +67,7 @@ def test_Compressor(self): # test calculated value eta_s = ( - (isentropic(self.c1.get_flow(), self.c2.get_flow()) - + (isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - self.c1.h.val_SI) / (self.c2.h.val_SI - self.c1.h.val_SI)) msg = ('Value of isentropic efficiency must be ' + str(eta_s) + ', is ' + str(instance.eta_s.val) + '.') @@ -76,7 +75,7 @@ def test_Compressor(self): # remove pressure at outlet, use characteristic map for pressure # rise calculation - self.c2.set_attr(p=np.nan) + self.c2.set_attr(p=None) instance.set_attr( char_map_pr={'char_func': ldc( 'compressor', 'char_map_pr', 'DEFAULT', CharMap), @@ -84,7 +83,7 @@ def test_Compressor(self): char_map_eta_s={'char_func': ldc( 'compressor', 'char_map_eta_s', 'DEFAULT', CharMap), 'is_set': True}, - eta_s=np.nan, igva=0) + eta_s=None, igva=0) # offdesign test, efficiency value should be at design value self.nw.solve('offdesign', design_path='tmp') @@ -95,7 +94,7 @@ def test_Compressor(self): # 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=None, m=self.c1.m.val * 0.8, T=-30) self.nw.solve('offdesign', design_path='tmp') self.nw._convergence_check() @@ -118,7 +117,7 @@ def test_Compressor(self): # back to design properties, test eta_s_char self.c2.set_attr(p=6) - self.c1.set_attr(v=1, T=5, m=np.nan) + self.c1.set_attr(v=1, T=5, m=None) # test parameter specification for eta_s_char with unset char map instance.set_attr(eta_s_char={'char_func': ldc( @@ -161,8 +160,7 @@ def test_Pump(self): """Test component properties of pumps.""" instance = Pump('pump') self.setup_network(instance) - fl = {'N2': 0, 'O2': 0, 'Ar': 0, 'DowQ': 1, 'NH3': 0} - self.c1.set_attr(fluid=fl, v=1, p=5, T=50) + self.c1.set_attr(fluid={'INCOMP::DowQ': 1}, v=1, p=5, T=50) self.c2.set_attr(p=7) instance.set_attr(eta_s=1) self.nw.solve('design') @@ -170,7 +168,7 @@ def test_Pump(self): # test calculated value for efficiency eta_s = ( - (isentropic(self.c1.get_flow(), self.c2.get_flow()) - + (isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - self.c1.h.val_SI) / (self.c2.h.val_SI - self.c1.h.val_SI)) msg = ('Value of isentropic efficiency must be ' + str(eta_s) + ', is ' + str(instance.eta_s.val) + '.') @@ -178,8 +176,8 @@ def test_Pump(self): # isentropic efficiency of 1 means inlet and outlet entropy are # identical - s1 = round(s_mix_ph(self.c1.get_flow()), 4) - s2 = round(s_mix_ph(self.c2.get_flow()), 4) + s1 = round(self.c1.calc_s(), 4) + s2 = round(self.c2.calc_s(), 4) msg = ('Value of entropy must be identical for inlet (' + str(s1) + ') and outlet (' + str(s2) + ') at 100 % isentropic efficiency.') @@ -192,7 +190,7 @@ def test_Pump(self): self.nw.solve('design') self.nw._convergence_check() self.nw.save('tmp') - self.c2.set_attr(p=np.nan) + self.c2.set_attr(p=None) # flow char (pressure rise vs. volumetric flow) x = [0, 0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4] @@ -200,7 +198,7 @@ def test_Pump(self): char = {'char_func': CharLine(x, y), 'is_set': True} # apply flow char and eta_s char instance.set_attr( - flow_char=char, eta_s=np.nan, eta_s_char={ + flow_char=char, eta_s=None, eta_s_char={ 'char_func': ldc('pump', 'eta_s_char', 'DEFAULT', CharLine), 'is_set': True}) self.nw.solve('offdesign', design_path='tmp') @@ -252,8 +250,7 @@ def test_Turbine(self): """Test component properties of turbines.""" instance = Turbine('turbine') self.setup_network(instance) - fl = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'DowQ': 0, - 'NH3': 0} + fl = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} self.c1.set_attr(fluid=fl, m=15, p=10) self.c2.set_attr(p=1, T=25) instance.set_attr(eta_s=0.85) @@ -264,7 +261,7 @@ def test_Turbine(self): # design value of isentropic efficiency eta_s_d = ( (self.c2.h.val_SI - self.c1.h.val_SI) / ( - isentropic(self.c1.get_flow(), self.c2.get_flow()) - + isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - self.c1.h.val_SI)) msg = ('Value of isentropic efficiency must be ' + str(round(eta_s_d, 3)) + ', is ' + str(instance.eta_s.val) + @@ -277,7 +274,7 @@ def test_Turbine(self): self.nw._convergence_check() eta_s = ( (self.c2.h.val_SI - self.c1.h.val_SI) / ( - isentropic(self.c1.get_flow(), self.c2.get_flow()) - + isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - self.c1.h.val_SI)) msg = ('Value of isentropic efficiency must be ' + str(eta_s) + ', is ' + str(instance.eta_s.val) + '.') @@ -286,7 +283,7 @@ def test_Turbine(self): # unset isentropic efficiency and inlet pressure, # use characteristcs and cone law instead, parameters have to be in # design state - self.c1.set_attr(p=np.nan) + self.c1.set_attr(p=None) instance.cone.is_set = True instance.eta_s_char.is_set = True instance.eta_s.is_set = False @@ -343,7 +340,7 @@ def test_Turbomachine(self): instance.component() + '.') assert 'turbomachine' == instance.component(), msg self.setup_network(instance) - fl = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129, 'DowQ': 0, 'NH3': 0} + fl = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} self.c1.set_attr(fluid=fl, m=10, p=1, h=1e5) self.c2.set_attr(p=3, h=2e5) @@ -361,7 +358,7 @@ def test_Turbomachine(self): assert pr == instance.pr.val, msg # test pressure ratio specification - self.c2.set_attr(p=np.nan) + self.c2.set_attr(p=None) instance.set_attr(pr=5) self.nw.solve('design') self.nw._convergence_check() @@ -371,7 +368,7 @@ def test_Turbomachine(self): assert pr == instance.pr.val, msg # test power specification - self.c2.set_attr(h=np.nan) + self.c2.set_attr(h=None) instance.set_attr(P=1e5) self.nw.solve('design') self.nw._convergence_check() diff --git a/tests/test_connections.py b/tests/test_connections.py index 6f379a175..acc620a20 100644 --- a/tests/test_connections.py +++ b/tests/test_connections.py @@ -15,34 +15,28 @@ from tespy.connections import Connection from tespy.connections import Ref from tespy.networks import Network +from tespy.tools.helpers import convert_from_SI class TestConnections: def setup_method(self): """Set up the model.""" - # %% network setup - fluid_list = ['Air'] - self.nw = Network( - fluids=fluid_list, p_unit='bar', T_unit='C', p_range=[0.5, 20]) + self.nw = Network(p_unit='bar', T_unit='C', v_unit="l / s", m_unit="t / h") - # %% components so1 = Source('source 1') so2 = Source('source 2') si1 = Sink('sink 1') si2 = Sink('sink 2') - # %% connections c1 = Connection(so1, 'out1', si1, 'in1', label='Some example label') c2 = Connection(so2, 'out1', si2, 'in1') self.nw.add_conns(c1, c2) - # %% component parameters c1.set_attr(m=1, p=1, T=25, fluid={'Air': 1}) c2.set_attr(m=0.5, p=10, T=25, fluid={'Air': 1}) - # %% solving self.nw.solve('design') def test_volumetric_flow_reference(self): @@ -51,13 +45,78 @@ def test_volumetric_flow_reference(self): ['Some example label', 'source 2:out1_sink 2:in1'] ) c2.set_attr(m=None, v=Ref(c1, 1, 0)) + self.nw.solve('design') + m_expected = round(c1.m.val * c1.vol.val / c2.vol.val, 4) + m_is = round(c2.m.val, 4) + msg = ( + 'The mass flow of the connection 2 should be equal to ' + f'{m_expected} kg/s, but is {m_is} kg/s' + ) + assert m_is == m_expected, msg + + c2.set_attr(v=Ref(c1, 2, 10)) self.nw.solve('design') - # expected result: mass flow of second connection is lower by - # the fraction of the specific volumes + v_expected = round(c1.v.val * 2 - 10, 4) + v_is = round(c2.v.val, 4) + msg = ( + 'The mass flow of the connection 2 should be equal to ' + f'{v_expected} l/s, but is {v_is} l/s' + ) + assert v_is == v_expected, msg - m_expected = round(c1.m.val * c1.vol.val / c2.vol.val, 4) + def test_temperature_reference(self): + """Test the referenced temperature.""" + c1, c2 = self.nw.get_conn( + ['Some example label', 'source 2:out1_sink 2:in1'] + ) + c2.set_attr(T=None) + c2.set_attr(T=Ref(c1, 1, 0)) + + self.nw.solve('design') + + T_expected = round(c1.T.val, 4) + T_is = round(c2.T.val, 4) + msg = ( + 'The temperature of the connection 2 should be equal to ' + f'{T_expected} C, but is {T_is} C' + ) + assert T_is == T_expected, msg + + c2.set_attr(T=Ref(c1, 1.5, -75)) + self.nw.solve('design') + + T_expected = round(convert_from_SI("T", c1.T.val_SI * 1.5, c1.T.unit) + 75, 4) + T_is = round(c2.T.val, 4) + msg = ( + 'The temperature of the connection 2 should be equal to ' + f'{T_expected} C, but is {T_is} C' + ) + assert T_is == T_expected, msg + + def test_primary_reference(self): + """Test referenced primary variable.""" + c1, c2 = self.nw.get_conn( + ['Some example label', 'source 2:out1_sink 2:in1'] + ) + c2.set_attr(m=None) + c2.set_attr(m=Ref(c1, 1, 0)) + + self.nw.solve('design') + + m_expected = round(c1.m.val, 4) + m_is = round(c2.m.val, 4) + msg = ( + 'The mass flow of the connection 2 should be equal to ' + f'{m_expected} kg/s, but is {m_is} kg/s' + ) + assert m_is == m_expected, msg + + c2.set_attr(m=Ref(c1, 2, -0.5)) + self.nw.solve('design') + + m_expected = round(convert_from_SI("m", c1.m.val_SI * 2, c1.m.unit) + 0.5, 4) m_is = round(c2.m.val, 4) msg = ( 'The mass flow of the connection 2 should be equal to ' diff --git a/tests/test_errors.py b/tests/test_errors.py index e15fd1a21..b71d53fb2 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -74,7 +74,7 @@ def set_attr_ValueError(instance, **kwargs): def test_set_attr_errors(): """Test errors of set_attr methods.""" - nw = Network(['water', 'air']) + nw = Network() comb = CombustionEngine('combustion engine') pipeline = Pipe('pipeline') conn = Connection(comb, 'out1', pipeline, 'in1') @@ -90,7 +90,6 @@ def test_set_attr_errors(): set_attr_ValueError(nw, p_unit='kg') set_attr_ValueError(nw, T_unit='kg') set_attr_ValueError(nw, v_unit='kg') - set_attr_ValueError(conn, state=5) # TypeErrors set_attr_TypeError(comb, P=[5]) @@ -101,7 +100,6 @@ def test_set_attr_errors(): set_attr_TypeError(comb, design_path=7) set_attr_TypeError(comb, local_design=5) set_attr_TypeError(comb, local_offdesign=5) - set_attr_TypeError(pipeline, hydro_group=5) set_attr_TypeError(comb, printout=5) set_attr_TypeError(conn, design='h') @@ -112,7 +110,7 @@ def test_set_attr_errors(): set_attr_TypeError(conn, local_design=5) set_attr_TypeError(conn, local_offdesign=5) set_attr_TypeError(conn, printout=5) - set_attr_TypeError(conn, state='f') + set_attr_TypeError(conn, state=5) set_attr_TypeError(nw, m_range=5) set_attr_TypeError(nw, p_range=5) @@ -134,7 +132,7 @@ def test_set_attr_errors(): def test_get_attr_errors(): """Test errors of get_attr methods.""" - nw = Network(['water', 'air']) + nw = Network() comb = CombustionEngine('combustion engine') pipeline = Pipe('pipeline') conn = Connection(comb, 'out1', pipeline, 'in1') @@ -264,7 +262,7 @@ def test_UserDefinedEquation_errors(): def test_CombustionChamber_missing_fuel(): """Test no fuel in network.""" - nw = Network(['H2O', 'N2', 'O2', 'Ar', 'CO2']) + nw = Network() instance = CombustionChamber('combustion chamber') c1 = Connection(Source('air'), 'out1', instance, 'in1') c2 = Connection(Source('fuel'), 'out1', instance, 'in2') @@ -276,7 +274,7 @@ def test_CombustionChamber_missing_fuel(): def test_CombustionChamber_missing_oxygen(): """Test no oxygen in network.""" - nw = Network(['H2O', 'N2', 'Ar', 'CO2', 'CH4']) + nw = Network() instance = CombustionChamber('combustion chamber') c1 = Connection(Source('air'), 'out1', instance, 'in1') c2 = Connection(Source('fuel'), 'out1', instance, 'in2') @@ -292,7 +290,7 @@ def test_CombustionChamber_missing_oxygen(): class TestCombustionEngineBusErrors: def setup_method(self): - self.nw = Network(['water', 'air']) + self.nw = Network() self.instance = CombustionEngine('combustion engine') self.bus = Bus('power') self.bus.add_comps({'comp': self.instance, 'param': 'Param'}) @@ -307,9 +305,8 @@ def test_missing_Bus_param_deriv(self): # both values do not matter, but are required for the test self.instance.num_nw_vars = 1 self.instance.num_vars = 1 - self.instance.inl = [Connection(self.instance, 'out1', - Sink('sink'), 'in1')] - self.instance.inl[0].fluid = dc_flu(val={'water': 1}) + self.instance.inl = ["foo", "bar", "baz", "foo"] + self.instance.outl = ["bar", "baz", "foo"] with raises(ValueError): self.instance.bus_deriv(self.bus) @@ -319,13 +316,14 @@ def test_missing_Bus_param_deriv(self): def test_compressor_missing_char_parameter(): """Compressor with invalid parameter for eta_s_char function.""" - nw = Network(['CH4']) + nw = Network() so = Source('source') si = Sink('sink') instance = Compressor('compressor') c1 = Connection(so, 'out1', instance, 'in1') c2 = Connection(instance, 'out1', si, 'in1') nw.add_conns(c1, c2) + c1.set_attr(fluid={"CH4": 1}) instance.set_attr(eta_s_char={ 'func': CharLine([0, 1], [1, 2]), 'is_set': True, 'param': None}) nw.solve('design', init_only=True) @@ -351,67 +349,20 @@ def test_subsys_label_forbidden(): def test_Turbine_missing_char_parameter(): """Turbine with invalid parameter for eta_s_char function.""" - nw = Network(['CH4']) + nw = Network() so = Source('source') si = Sink('sink') instance = Turbine('turbine') c1 = Connection(so, 'out1', instance, 'in1') c2 = Connection(instance, 'out1', si, 'in1') nw.add_conns(c1, c2) + c1.set_attr(fluid={"CH4": 1}) instance.set_attr(eta_s_char={ 'char_func': CharLine([0, 1], [1, 2]), 'is_set': True, 'param': None}) nw.solve('design', init_only=True) with raises(ValueError): instance.eta_s_char_func() -############################################################################## -# WaterElectrolyzer - - -class TestWaterElectrolyzerErrors: - - def setup_electrolyzer_Network(self): - """Set up Network for electrolyzer tests.""" - self.instance = WaterElectrolyzer('electrolyzer') - - fw = Source('feed water') - cw_in = Source('cooling water') - o2 = Sink('oxygen sink') - h2 = Sink('hydrogen sink') - cw_out = Sink('cooling water sink') - - cw_el = Connection(cw_in, 'out1', self.instance, 'in1') - el_cw = Connection(self.instance, 'out1', cw_out, 'in1') - - self.nw.add_conns(cw_el, el_cw) - - fw_el = Connection(fw, 'out1', self.instance, 'in2') - el_o2 = Connection(self.instance, 'out2', o2, 'in1') - el_h2 = Connection(self.instance, 'out3', h2, 'in1') - - self.nw.add_conns(fw_el, el_o2, el_h2) - - def test_missing_hydrogen_in_Network(self): - """Test missing hydrogen in Network fluids with water electrolyzer.""" - self.nw = Network(['H2O', 'O2']) - self.setup_electrolyzer_Network() - with raises(TESPyComponentError): - self.nw.solve('design') - - def test_missing_oxygen_in_Network(self): - """Test missing oxygen in Network fluids with water electrolyzer.""" - self.nw = Network(['H2O', 'H2']) - self.setup_electrolyzer_Network() - with raises(TESPyComponentError): - self.nw.solve('design') - - def test_missing_water_in_Network(self): - """Test missing water in Network fluids with water electrolyzer.""" - self.nw = Network(['O2', 'H2']) - self.setup_electrolyzer_Network() - with raises(TESPyComponentError): - self.nw.solve('design') - def test_wrong_Bus_param_func(): """Test missing/wrong bus parameter specification in equations.""" @@ -447,7 +398,7 @@ def test_wrong_Bus_param_deriv(): class TestNetworkErrors: def setup_method(self): - self.nw = Network(['water']) + self.nw = Network() def test_add_conns_TypeError(self): with raises(TypeError): @@ -569,21 +520,6 @@ def test_buslabel_duplicate(self): b = Bus('mybus') self.nw.add_busses(a, b) - -def test_Network_instanciation_no_fluids(): - nw = Network([]) - so = Source('source') - si = Sink('sink') - conn = Connection(so, 'out1', si, 'in1') - nw.add_conns(conn) - with raises(TESPyNetworkError): - nw.solve('design', init_only=True) - - -def test_Network_instanciation_single_fluid(): - with raises(TypeError): - Network('water') - ############################################################################## # test errors of characteristics classes @@ -656,5 +592,9 @@ def test_missing_CharMap_files(): def test_h_mix_pQ_on_mixtures(): + c = Connection(Source("test"), "out1", Sink("test2"), "in1") + c.set_attr(fluid={"O2": 0.24, "N2": 0.76}) + c._create_fluid_wrapper() + c.build_fluid_data() with raises(ValueError): - h_mix_pQ([0, 0, 0, {'O2': 0.24, 'N2': 0.76}], 0.75) + h_mix_pQ(1e5, 0.5, c.fluid_data, c.mixing_rule) diff --git a/tests/test_heat_pump_exergy.py b/tests/test_heat_pump_exergy.py new file mode 100644 index 000000000..ca9d7bf94 --- /dev/null +++ b/tests/test_heat_pump_exergy.py @@ -0,0 +1,15 @@ +import os +import runpy + +import pytest + +path = os.path.join(os.path.dirname(__file__), "..", "tutorial", "heat_pump_exergy") +scripts = (os.path.join(path, f) for f in os.listdir(path) if f.endswith(".py") and f != "plots.py") + + +@pytest.mark.parametrize('script', scripts) +def test_tutorial_execution(script): + try: + runpy.run_path(script) + except ModuleNotFoundError: + pytest.skip("Test skipped due to missing dependency") \ No newline at end of file diff --git a/tests/test_models/test_CGAM_model.py b/tests/test_models/test_CGAM_model.py index 2b4dd4f77..1857485ac 100644 --- a/tests/test_models/test_CGAM_model.py +++ b/tests/test_models/test_CGAM_model.py @@ -32,7 +32,7 @@ class TestCGAM: def setup_method(self): fluid_list = ['O2', 'H2O', 'N2', 'CO2', 'CH4'] - self.nwk = Network(fluids=fluid_list, p_unit='bar', T_unit='C') + self.nwk = Network(p_unit='bar', T_unit='C') air_molar = { 'O2': 0.2059, 'N2': 0.7748, 'CO2': 0.0003, 'H2O': 0.019, 'CH4': 0 @@ -92,12 +92,11 @@ def setup_method(self): c10.set_attr(T=25, fluid=fuel, p=12) c7.set_attr(p=1.013) c3.set_attr(T=850 - 273.15) - c4.set_attr(T=1520 - 273.15) c8p.set_attr(Td_bp=-15) c11p.set_attr(x=0.5) cmp.set_attr(pr=10, eta_s=0.86) - cb.set_attr(eta=0.98, pr=0.95) + cb.set_attr(eta=0.98, pr=0.95, lamb=2) tur.set_attr(eta_s=0.86) aph.set_attr(pr1=0.97, pr2=0.95) eva.set_attr(pr1=0.95 ** 0.5) @@ -121,7 +120,10 @@ def setup_method(self): power.set_attr(P=-30e6) c1.set_attr(m=None) + c4.set_attr(T=1520 - 273.15) + cb.set_attr(lamb=None) self.nwk.solve('design') + self.nwk._convergence_check() self.result = self.nwk.results["Connection"].copy() diff --git a/tests/test_models/test_heat_pump_model.py b/tests/test_models/test_heat_pump_model.py index 5e510690d..9a54d47ae 100644 --- a/tests/test_models/test_heat_pump_model.py +++ b/tests/test_models/test_heat_pump_model.py @@ -17,8 +17,8 @@ from tespy.components import CycleCloser from tespy.components import Drum from tespy.components import HeatExchanger -from tespy.components import HeatExchangerSimple from tespy.components import Pump +from tespy.components import SimpleHeatExchanger from tespy.components import Sink from tespy.components import Source from tespy.components import Valve @@ -33,8 +33,7 @@ class TestHeatPump: def setup_method(self): # %% network setup - self.nw = Network(fluids=['water', 'NH3'], T_unit='C', p_unit='bar', - h_unit='kJ / kg', m_unit='kg / s') + self.nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s') # %% components # sources & sinks @@ -48,7 +47,7 @@ def setup_method(self): # consumer system cd = HeatExchanger('condenser') rp = Pump('recirculation pump') - cons = HeatExchangerSimple('consumer') + cons = SimpleHeatExchanger('consumer') # evaporator system va = Valve('valve') @@ -225,10 +224,9 @@ def setup_method(self): cd.set_attr(kA_char1=kA_char1, kA_char2=kA_char2, pr2=0.9998, design=['pr2'], offdesign=['zeta2', 'kA_char']) - # %% connection parametrization # condenser system - c_in_cd.set_attr(fluid={'water': 0, 'NH3': 1}, p=60) - rp_cd.set_attr(T=60, fluid={'water': 1, 'NH3': 0}, p=10) + c_in_cd.set_attr(fluid={'NH3': 1}, p=60) + rp_cd.set_attr(T=60, fluid={'water': 1}, p=10) self.cd_cons.set_attr(T=105) cd_va.set_attr(p=Ref(c_in_cd, 1, -0.01), Td_bp=-5, design=['Td_bp']) @@ -238,14 +236,14 @@ def setup_method(self): su_cp1.set_attr(p=Ref(dr_su, 1, -0.05), Td_bp=5, design=['Td_bp', 'p']) # evaporator system hot side - self.amb_in_su.set_attr(m=20, T=12, p=1, fluid={'water': 1, 'NH3': 0}) + self.amb_in_su.set_attr(m=20, T=12, p=1, fluid={'water': 1}) su_ev.set_attr(p=Ref(self.amb_in_su, 1, -0.001), design=['p']) ev_amb_out.set_attr() # compressor-system cp1_he.set_attr(p=15) he_cp2.set_attr(T=40, p=Ref(cp1_he, 1, -0.01), design=['T', 'p']) - ic_in_he.set_attr(p=1, T=20, m=5, fluid={'water': 1, 'NH3': 0}) + ic_in_he.set_attr(p=1, T=20, m=5, fluid={'water': 1}) he_ic_out.set_attr(p=Ref(ic_in_he, 1, -0.002), design=['p']) def test_model(self): @@ -278,21 +276,23 @@ def test_model(self): self.amb_in_su.set_attr(m=m) if j == 0: self.nw.solve( - 'offdesign', design_path='tmp', init_path='tmp') + 'offdesign', design_path='tmp', init_path='tmp' + ) else: self.nw.solve('offdesign', design_path='tmp') + self.nw._convergence_check() # relative deviation should not exceed 6.5 % # this should be much less, unfortunately not all ebsilon # characteristics are available, thus it is # difficult/impossible to match the models perfectly! 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.065, is ' + + msg = ('The deviation in COP should be less than 0.07, is ' + str(d_rel_COP) + ' at mass flow ' + str(m) + ' and temperature ' + str(T) + '.') - assert d_rel_COP < 0.065, msg + assert d_rel_COP < 0.07, msg j += 1 i += 1 shutil.rmtree('./tmp', ignore_errors=True) diff --git a/tests/test_models/test_solar_energy_generating_system.py b/tests/test_models/test_solar_energy_generating_system.py index a4b30bbae..5dcfcacbf 100644 --- a/tests/test_models/test_solar_energy_generating_system.py +++ b/tests/test_models/test_solar_energy_generating_system.py @@ -42,7 +42,7 @@ def setup_method(self): self.Tamb = 25 # setting up network - self.nw = Network(fluids=['water', 'INCOMP::TVP1', 'air']) + self.nw = Network() self.nw.set_attr( T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s', s_unit="kJ / kgK") @@ -282,7 +282,7 @@ def setup_method(self): # connection parameters # parabolic trough cycle - c70.set_attr(fluid={'TVP1': 1, 'water': 0, 'air': 0}, T=390, p=23.304) + c70.set_attr(fluid={'INCOMP::TVP1': 1}, T=390, p=23.304) c76.set_attr(m=Ref(c70, 0.1284, 0)) c73.set_attr(p=22.753) c74.set_attr(p=21.167) @@ -291,16 +291,16 @@ def setup_method(self): # cooling water c62.set_attr( - fluid={'TVP1': 0, 'water': 1, 'air': 0}, T=30, p=self.pamb) + fluid={'water': 1}, T=30, p=self.pamb) # cooling tower c64.set_attr( - fluid={'water': 0, 'TVP1': 0, 'air': 1}, p=self.pamb, T=self.Tamb) + fluid={'air': 1}, p=self.pamb, T=self.Tamb) c65.set_attr(p=self.pamb + 0.0005) c66.set_attr(p=self.pamb, T=30) # power cycle c32.set_attr(Td_bp=-2) c34.set_attr(x=0.5) - c1.set_attr(fluid={'water': 1, 'TVP1': 0, 'air': 0}, p=100, T=371) + c1.set_attr(fluid={'water': 1}, p=100, T=371) # steam generator pressure values c31.set_attr(p=103.56) diff --git a/tests/test_networks/test_binary_incompressible.py b/tests/test_networks/test_binary_incompressible.py new file mode 100644 index 000000000..c5aa7be94 --- /dev/null +++ b/tests/test_networks/test_binary_incompressible.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 + +"""Module for testing mixing rule propagation in networks. +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/test_networks/test_binary_incompressible.py +SPDX-License-Identifier: MIT +""" + +from tespy.components import HeatExchanger +from tespy.components import Pump +from tespy.components import Sink +from tespy.components import Source +from tespy.connections import Connection +from tespy.networks import Network + + +class TestBinaryIncompressibles: + + + def setup_method(self): + self.nw = Network() + + so1 = Source("Source1") + so2 = Source("Source2") + pu = Pump("Pump") + he = HeatExchanger("Heat") + si1 = Sink("Sink1") + si2 = Sink("Sink2") + + c1 = Connection(so1, "out1", pu, "in1", label="1") + c2 = Connection(pu, "out1", he, "in2", label="2") + c3 = Connection(he, "out2", si1, "in1", label="3") + + c4 = Connection(so2, "out1", he, "in1", label="4") + c5 = Connection(he, "out1", si2, "in1", label="5") + + self.nw.add_conns(c1, c2, c3, c4, c5) + + c1.set_attr(v=1, p=1e5, T=300, fluid={"INCOMP::MPG[0.2]": 1}) + c2.set_attr(h0=2e4) + c3.set_attr(p=1e5, T=320, h0=1e5) + + he.set_attr(pr1=0.98, pr2=0.98, ttd_l=10) + pu.set_attr(eta_s=0.7) + + c4.set_attr(p=1e5, T=350, fluid={"INCOMP::MEG[0.2]": 1}) + c5.set_attr(h0=1e5) + + self.nw.solve("design") + + def test_binaries(self): + self.nw._convergence_check() \ No newline at end of file diff --git a/tests/test_networks/test_mixing_rules.py b/tests/test_networks/test_mixing_rules.py new file mode 100644 index 000000000..a5cb5af96 --- /dev/null +++ b/tests/test_networks/test_mixing_rules.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 + +"""Module for testing mixing rule propagation in networks. +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/test_networks/test_mixing_rules.py +SPDX-License-Identifier: MIT +""" +from pytest import approx + +from tespy.components import Compressor +from tespy.components import Merge +from tespy.components import Pipe +from tespy.components import SimpleHeatExchanger +from tespy.components import Sink +from tespy.components import Source +from tespy.components import Splitter +from tespy.components import Turbine +from tespy.connections import Connection +from tespy.networks import Network + + +class TestGasMixingRules: + + def setup_method(self): + + self.nwk = Network(T_unit="C", p_unit="bar", h_unit="kJ / kg") + self.nwk.set_attr(iterinfo=False) + + so1 = Source("air") + so2 = Source("Other gases") + m = Merge("gas mixing") + p = Pipe("Pipe", pr=1, Q=0) + sp = Splitter("Splitter") + t = Turbine("Turbine", pr=.1, eta_s=.8) + cp = Compressor("Compressor", pr=10, eta_s=.8) + si1 = Sink("Sink1") + si2 = Sink("Sink2") + + c1 = Connection(so1, "out1", m, "in1", label="1") + c2 = Connection(so2, "out1", m, "in2", label="2") + c3 = Connection(m, "out1", p, "in1", label="3") + c4 = Connection(p, "out1", sp, "in1", label="4") + c5 = Connection(sp, "out1", t, "in1", label="5") + c6 = Connection(t, "out1", si1, "in1", label="6") + c7 = Connection(sp, "out2", cp, "in1", label="7") + c8 = Connection(cp, "out1", si2, "in1", label="8") + + self.nwk.add_conns(c1, c2, c3, c4, c5, c6, c7, c8) + + def test_ideal_ideal_cond(self): + + c1, c2, c3, c6, c7 = self.nwk.get_conn(["1", "2", "3", "6", "7"]) + c1.set_attr(fluid={"N2": 0.76, "O2": 0.23, "Ar": 0.01}, m=10, T=400, p=1, mixing_rule="ideal-cond") + c2.set_attr(fluid={"H2O": 1}, m=.5, T=400) + c3.set_attr(fluid0={"H2O": 0.05}) + c7.set_attr(m=4) + + p, t, cp = self.nwk.get_comp(["Pipe", "Turbine", "Compressor"]) + p.set_attr(pr=1, Q=0) + t.set_attr(pr=.1, eta_s=.8) + cp.set_attr(pr=10, eta_s=.8) + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = c2.m.val_SI / (c1.m.val_SI + c2.m.val_SI) + msg = f"The H2O mass fraction in connection 7 must be {target}" + assert c7.fluid.val["H2O"] == approx(target), msg + + h_ideal_cond = c6.h.val_SI + for c in self.nwk.conns["object"]: + c.mixing_rule = "ideal" + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = h_ideal_cond + msg = f"The enthalpy at connection 6 must be equal to {target}" + assert c6.h.val_SI == approx(target), msg + + c1.set_attr(T=200) + c2.set_attr(T=200) + + self.nwk.solve("design") + self.nwk._convergence_check() + + h_ideal = c6.h.val_SI + for c in self.nwk.conns["object"]: + c.mixing_rule = "ideal-cond" + + self.nwk.solve("design") + self.nwk._convergence_check() + + target = h_ideal + msg = ( + "Using ideal-cond mixing, the enthalpy at connection 6 must be " + f"larger than using ideal mixing rule ({target})" + ) + assert c6.h.val_SI > target, msg + +class TestIncompressibleMixingRule: + + def setup_method(self): + + self.nw = Network(m_unit='kg / s', p_unit='bar', T_unit='C') + + source = Source('source') + boiler = SimpleHeatExchanger('boiler') + sink = Sink('sink') + + c1 = Connection(source, 'out1', boiler, 'in1', label="1") + c2 = Connection(boiler, 'out1', sink, 'in1', label="2") + + self.nw.add_conns(c1,c2) + + def test_binary_mixture(self): + """""" + c1, c2 = self.nw.get_conn(["1", "2"]) + c1.set_attr( + fluid={'INCOMP::Water': 0.8,'INCOMP::PHE': 0.2}, + m=100, T=40, p=2, mixing_rule="incompressible" + ) + c2.set_attr(T=60, p=2) + self.nw.solve('design') + self.nw._convergence_check() + expected = { + c.label: sum([ + c.fluid.wrapper[fl].h_pT(c.p.val_SI, c.T.val_SI) * x + for fl, x in c.fluid.val.items() + ]) for c in [c1, c2] + } + for c, h in expected.items(): + result = self.nw.get_conn(c).h.val_SI + msg = f"Enthalpy of mixture should be {h} but is {result}." + assert round(h, 6) == round(result, 6), msg + + def test_ternary_mixture(self): + """""" + c1, c2 = self.nw.get_conn(["1", "2"]) + c1.set_attr( + fluid={'INCOMP::Water': 0.81,'INCOMP::PHE': 0.163,'INCOMP::S800': 0.027}, + m=100, T=40, p=2, mixing_rule="incompressible" + ) + c2.set_attr(T=60, p=2) + self.nw.solve('design') + self.nw._convergence_check() + expected = { + c.label: sum([ + c.fluid.wrapper[fl].h_pT(c.p.val_SI, c.T.val_SI) * x + for fl, x in c.fluid.val.items() + ]) for c in [c1, c2] + } + for c, h in expected.items(): + result = self.nw.get_conn(c).h.val_SI + msg = f"Enthalpy of mixture should be {h} but is {result}." + assert round(h, 6) == round(result, 6), msg diff --git a/tests/test_networks/test_network.py b/tests/test_networks/test_network.py index 81a483be8..e9c92e52a 100644 --- a/tests/test_networks/test_network.py +++ b/tests/test_networks/test_network.py @@ -14,27 +14,31 @@ import shutil import numpy as np +from pytest import mark from pytest import raises from tespy.components import Compressor from tespy.components import Merge from tespy.components import Pipe from tespy.components import Pump +from tespy.components import SimpleHeatExchanger from tespy.components import Sink from tespy.components import SolarCollector from tespy.components import Source from tespy.components import Splitter from tespy.components import SubsystemInterface +from tespy.components import Turbine from tespy.components import Valve from tespy.connections import Connection +from tespy.connections import Ref from tespy.networks import Network from tespy.networks import load_network from tespy.tools.helpers import TESPyNetworkError class TestNetworks: - def setup_Network_tests(self): - self.nw = Network(['water'], p_unit='bar', v_unit='m3 / s', T_unit='C') + def setup_method(self): + self.nw = Network(p_unit='bar', v_unit='m3 / s', T_unit='C') self.source = Source('source') self.sink = Sink('sink') @@ -44,19 +48,16 @@ def offdesign_TESPyNetworkError(self, **kwargs): def test_Network_linear_dependency(self): """Test network linear dependency.""" - self.setup_Network_tests() a = Connection( - self.source, 'out1', self.sink, 'in1', m=1, p=1, x=1, T=7 + self.source, 'out1', self.sink, 'in1', p=5, x=1, T=7, fluid={"H2": 1} ) self.nw.add_conns(a) self.nw.solve('design') - msg = ('This test must result in a linear dependency of the jacobian ' - 'matrix.') + msg = 'This test must result in a linear dependency of the jacobian matrix.' assert self.nw.lin_dep, msg def test_Network_no_progress(self): """Test no convergence progress.""" - self.setup_Network_tests() pi = Pipe('pipe', pr=1, Q=-100e3) a = Connection( self.source, 'out1', pi, 'in1', m=1, p=1, T=7, fluid={'water': 1} @@ -66,11 +67,10 @@ def test_Network_no_progress(self): self.nw.solve('design') msg = ('This test must result in a calculation making no progress, as ' 'the pipe\'s outlet enthalpy is below fluid property range.') - assert self.nw.progress is False, msg + assert not self.nw.progress, msg def test_Network_max_iter(self): """Test reaching maximum iteration count.""" - self.setup_Network_tests() pi = Pipe('pipe', pr=1, Q=100e3) a = Connection( self.source, 'out1', pi, 'in1', m=1, p=1, T=7, fluid={'water': 1} @@ -84,7 +84,6 @@ def test_Network_max_iter(self): def test_Network_delete_conns(self): """Test deleting a network's connection.""" - self.setup_Network_tests() a = Connection(self.source, 'out1', self.sink, 'in1') self.nw.add_conns(a) self.nw.check_network() @@ -98,7 +97,6 @@ def test_Network_delete_conns(self): def test_Network_delete_comps(self): """Test deleting components by deleting connections.""" - self.setup_Network_tests() p = Pipe("Dummy") a = Connection(self.source, 'out1', self.sink, 'in1') self.nw.add_conns(a) @@ -122,9 +120,9 @@ def test_Network_delete_comps(self): def test_Network_missing_connection_in_init_path(self): """Test debug message for missing connection in init_path.""" - self.setup_Network_tests() IF = SubsystemInterface('IF') a = Connection(self.source, 'out1', self.sink, 'in1') + a.set_attr(fluid={"Air": 1}) self.nw.add_conns(a) self.nw.solve('design', init_only=True) self.nw.save('tmp') @@ -134,6 +132,7 @@ def test_Network_missing_connection_in_init_path(self): self.nw.del_conns(a) a = Connection(self.source, 'out1', IF, 'in1') b = Connection(IF, 'out1', self.sink, 'in1') + a.set_attr(fluid={"Air": 1}) self.nw.add_conns(a, b) self.nw.solve('design', init_path='tmp', init_only=True) msg = ('After the network check, the .checked-property must be True.') @@ -141,56 +140,29 @@ def test_Network_missing_connection_in_init_path(self): shutil.rmtree('./tmp', ignore_errors=True) - def test_Network_export_no_chars_busses(self): + def test_Network_export_no_busses(self): """Test export of network without characteristics or busses.""" - self.setup_Network_tests() a = Connection(self.source, 'out1', self.sink, 'in1') self.nw.add_conns(a) + a.set_attr(fluid={"H2O": 1}) self.nw.solve('design', init_only=True) self.nw.save('tmp') - msg = ('The exported network does not contain any char_line, there ' - 'must be no file char_line.csv!') - assert os.path.isfile('tmp/components/char_line.csv') is False, msg - msg = ('The exported network does not contain any char_map, there ' - 'must be no file char_map.csv!') - assert os.path.isfile('tmp/components/char_map.csv') is False, msg - - msg = ('The exported network does not contain any busses, there ' - 'must be no file bus.csv!') - assert os.path.isfile('tmp/components/bus.csv') is False, msg + msg = ( + 'The exported network does not contain any busses, there must be ' + 'no file busses.csv!' + ) + assert not os.path.isfile('tmp/busses.csv'), msg shutil.rmtree('./tmp', ignore_errors=True) - def test_Network_reader_no_chars_busses(self): + def test_Network_reader_checked(self): """Test import of network without characteristics or busses.""" - self.setup_Network_tests() a = Connection(self.source, 'out1', self.sink, 'in1') self.nw.add_conns(a) + a.set_attr(fluid={"H2O": 1}) self.nw.solve('design', init_only=True) - self.nw.save('tmp') - - imported_nwk = load_network('tmp') - imported_nwk.solve('design', init_only=True) - msg = ('If the network import was successful the network check ' - 'should have been successful, too, but it is not.') - assert imported_nwk.checked, msg - shutil.rmtree('./tmp', ignore_errors=True) - - def test_Network_reader_deleted_chars(self): - """Test import of network with missing characteristics.""" - self.setup_Network_tests() - comp = Compressor('compressor') - a = Connection(self.source, 'out1', comp, 'in1') - b = Connection(comp, 'out1', self.sink, 'in1') - self.nw.add_conns(a, b) - self.nw.solve('design', init_only=True) - self.nw.save('tmp') - - # # remove char_line and char_map folders - os.unlink('tmp/components/char_line.csv') - os.unlink('tmp/components/char_map.csv') + self.nw.export('tmp') - # import network with missing files imported_nwk = load_network('tmp') imported_nwk.solve('design', init_only=True) msg = ('If the network import was successful the network check ' @@ -200,7 +172,6 @@ def test_Network_reader_deleted_chars(self): def test_Network_missing_data_in_design_case_files(self): """Test for missing data in design case files.""" - self.setup_Network_tests() pi = Pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) a = Connection( self.source, 'out1', pi, 'in1', m=1, p=1, T=20, fluid={'water': 1} @@ -227,7 +198,6 @@ def test_Network_missing_data_in_design_case_files(self): def test_Network_missing_data_in_individual_design_case_file(self): """Test for missing data in individual design case files.""" - self.setup_Network_tests() pi = Pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) a = Connection(self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, fluid={'water': 1}) @@ -253,7 +223,6 @@ def test_Network_missing_data_in_individual_design_case_file(self): def test_Network_missing_connection_in_design_path(self): """Test for missing connection data in design case files.""" - self.setup_Network_tests() pi = Pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) a = Connection(self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, fluid={'water': 1}) @@ -277,7 +246,6 @@ def test_Network_missing_connection_in_design_path(self): def test_Network_get_comp_without_connections_added(self): """Test if components are found prior to initialization.""" - self.setup_Network_tests() pi = Pipe('pipe') a = Connection(self.source, 'out1', pi, 'in1') Connection(pi, 'out1', self.sink, 'in1') @@ -291,7 +259,6 @@ def test_Network_get_comp_without_connections_added(self): def test_Network_get_comp_before_initialization(self): """Test if components are found prior to initialization.""" - self.setup_Network_tests() pi = Pipe('pipe') a = Connection(self.source, 'out1', pi, 'in1') b = Connection(pi, 'out1', self.sink, 'in1') @@ -307,7 +274,7 @@ class TestNetworkIndividualOffdesign: def setup_Network_individual_offdesign(self): """Set up network for individual offdesign tests.""" - self.nw = Network(['H2O'], T_unit='C', p_unit='bar', v_unit='m3 / s') + self.nw = Network(T_unit='C', p_unit='bar', v_unit='m3 / s') so = Source('source') sp = Splitter('splitter', num_out=2) @@ -368,7 +335,7 @@ def test_individual_design_path_on_connections_and_components(self): v2_design = self.sc2_v2.v.val_SI zeta_sc2_design = self.sc2.zeta.val - self.sc1_v1.set_attr(m=np.nan) + self.sc1_v1.set_attr(m=None) self.sc1_v1.set_attr(design=['T'], offdesign=['v'], state='l') self.sc2_v2.set_attr(design=['T'], offdesign=['v'], state='l') @@ -385,29 +352,34 @@ def test_individual_design_path_on_connections_and_components(self): self.nw.solve('offdesign', design_path='design1') self.nw._convergence_check() - self.sc2_v2.set_attr(design_path=np.nan) + self.sc2_v2.set_attr(design_path=None) # volumetric flow comparison - msg = ('Design path was set to None, is ' + - str(self.sc2_v2.design_path) + '.') + msg = f"Design path was set to None, is {self.sc2_v2.design_path}." assert self.sc2_v2.design_path is None, msg # volumetric flow comparison - msg = ('Value of volumetric flow must be ' + str(v1_design) + ', is ' + - str(self.sc1_v1.v.val_SI) + '.') + msg = ( + f"Value of volumetric flow must be {v1_design}, is " + f"{self.sc1_v1.v.val_SI}." + ) assert round(v1_design, 5) == round(self.sc1_v1.v.val_SI, 5), msg - msg = ('Value of volumetric flow must be ' + str(v2_design) + ', is ' + - str(self.sc2_v2.v.val_SI) + '.') + msg = ( + f"Value of volumetric flow must be {v2_design}, is " + f"{self.sc2_v2.v.val_SI}." + ) assert round(v2_design, 5) == round(self.sc2_v2.v.val_SI, 5), msg # zeta value of solar collector comparison - msg = ('Value of zeta must be ' + str(zeta_sc1_design) + ', is ' + - str(self.sc1.zeta.val) + '.') + msg = ( + f"Value of zeta must be {zeta_sc1_design}, is {self.sc1.zeta.val}." + ) assert round(zeta_sc1_design, 0) == round(self.sc1.zeta.val, 0), msg - msg = ('Value of zeta must be ' + str(zeta_sc2_design) + ', is ' + - str(self.sc2.zeta.val) + '.') + msg = ( + f"Value of zeta must be {zeta_sc2_design}, is {self.sc2.zeta.val}." + ) assert round(zeta_sc2_design, 0) == round(self.sc2.zeta.val, 0), msg shutil.rmtree('./design1', ignore_errors=True) @@ -433,7 +405,7 @@ def test_local_offdesign_on_connections_and_components(self): self.sc1_v1.set_attr(local_offdesign=True, design_path='design1') self.sc1.set_attr(E=500) - self.sc2_v2.set_attr(T=95, m=np.nan) + self.sc2_v2.set_attr(T=95, m=None) self.nw.solve('design') self.nw._convergence_check() self.nw.save('design2') @@ -445,12 +417,14 @@ def test_local_offdesign_on_connections_and_components(self): ', is ' + str(round(self.sc1_v1.T.val, 1)) + '.') assert self.sc1_v1.T.design > self.sc1_v1.T.val, msg - msg = ('Parameter eta_s_char must be set for pump one.') + msg = "Parameter eta_s_char must be set for pump one." assert self.pump1.eta_s_char.is_set, msg - msg = ('Parameter v must be set for connection from solar collector1 ' - 'to pump1.') - assert self.sc1_v1.v.val_set, msg + msg = ( + "Parameter v must be set for connection from solar collector1 to " + "pump1." + ) + assert self.sc1_v1.v.is_set, msg shutil.rmtree('./design1', ignore_errors=True) shutil.rmtree('./design2', ignore_errors=True) @@ -475,10 +449,196 @@ def test_missing_design_path_local_offdesign_on_connections(self): self.sc1_v1.set_attr(local_offdesign=True) self.sc1.set_attr(E=500) - self.sc2_v2.set_attr(T=95, m=np.nan) + self.sc2_v2.set_attr(T=95, m=None) try: self.nw.solve('design', init_only=True) except TESPyNetworkError: pass shutil.rmtree('./design1', ignore_errors=True) + + +class TestNetworkPreprocessing: + + def setup_method(self): + self.nwk = Network(T_unit="C", p_unit="bar", h_unit='kJ / kg') + + def _create_linear_branch(self): + a = Source("source") + b = Pipe("pipe") + c = Sink("sink") + + c1 = Connection(a, "out1", b, "in1", label="1") + c2 = Connection(b, "out1", c, "in1", label="2") + + self.nwk.add_conns(c1, c2) + + def _create_recirculation(self): + self.nwk = Network() + + self.nwk.set_attr(T_unit='C', p_unit='bar', h_unit='kJ / kg') + + source = Source('source') + merge = Merge('merge') + component1 = SimpleHeatExchanger('comp1', pr=1) + splitter = Splitter('splitter') + component2 = SimpleHeatExchanger('comp2') + sink = Sink('sink') + + c1 = Connection(source, 'out1', merge, 'in1', label="1") + c2 = Connection(merge, 'out1', component1, 'in1', label="2") + c3 = Connection(component1, 'out1', splitter, 'in1', label="3") + c4 = Connection(splitter, 'out1', component2, 'in1', label="4") + c5 = Connection(component2, 'out1', merge, 'in2', label="5") + c6 = Connection(splitter, 'out2', sink, 'in1', label="6") + + self.nwk.add_conns(c1, c2, c3, c4, c5, c6) + + c1.set_attr(p=1, h=200, m=10) + c3.set_attr(h=180) + c4.set_attr(m=1) + c5.set_attr(h=170) + + @mark.skip("Not implemented") + def test_fluid_linear_branch_distribution(self): + raise NotImplementedError() + + @mark.skip("Not implemented") + def test_fluid_connected_branches_distribution(self): + raise NotImplementedError() + + @mark.skip("Not implemented") + def test_fluid_independent_branches_distribution(self): + raise NotImplementedError() + + def test_linear_branch_massflow_presolve(self): + self._create_linear_branch() + c1, c2 = self.nwk.get_conn(["1", "2"]) + b = self.nwk.get_comp("pipe") + c1.set_attr(fluid={"O2": 1}, p=10, T=-20, m=5) + c2.set_attr(T=-15) + b.set_attr(pr=1) + self.nwk.solve("design") + self.nwk._convergence_check() + variables = [data["obj"].get_attr(data["variable"]) for data in self.nwk.variables_dict.values()] + # no mass flow is variable + assert c1.m not in variables + assert c2.m not in variables + # first connection pressure and enthalpy not variable + assert c1.p not in variables + assert c1.h not in variables + # second connection pressure and enthalpy are variable + assert c2.p in variables + assert c2.h in variables + + @mark.skip("Not implemented") + def test_splitting_branch_massflow_presolve(self): + raise NotImplementedError() + + def test_double_massflow_specification_linear_branch(self): + self._create_linear_branch() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"O2": 1}, m=5) + c2.set_attr(m=5) + with raises(TESPyNetworkError): + self.nwk.solve("design") + + def test_missing_fluid_information(self): + self._create_linear_branch() + with raises(TESPyNetworkError): + self.nwk.solve("design") + + def test_referencing_massflow_specification_linear_branch(self): + self._create_linear_branch() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"O2": 1}) + c2.set_attr(m=Ref(c1, 1, 0)) + with raises(TESPyNetworkError): + self.nwk.solve("design") + + @mark.skip("Not implemented") + def test_linear_branch_fluid_presolve(self): + raise NotImplementedError() + + @mark.skip("Not implemented") + def test_splitting_branch_fluid_presolve(self): + raise NotImplementedError() + + @mark.skip("Not implemented") + def test_independent_branch_fluid_presolve(self): + raise NotImplementedError() + + def test_recirculation_structure_two_fluids_without_starting(self): + self._create_recirculation() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"water": 0, "R134a": 1}) + self.nwk.solve("design", init_only=True) + assert c2.fluid.val["R134a"] == 0.5 + assert c2.fluid.val["water"] == 0.5 + + def test_recirculation_structure_two_fluids_with_starting(self): + self._create_recirculation() + c1, c2, c6 = self.nwk.get_conn(["1", "2", "6"]) + c1.set_attr(fluid={"water": 0, "R134a": 1}) + c2.set_attr(fluid0={"water": 0, "R134a": 1}) + self.nwk.solve("design", init_only=True) + assert c6.fluid.val["R134a"] == 1 + + def test_recirculation_structure_single_fluids(self): + self._create_recirculation() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"R134a": 1}) + self.nwk.solve("design", init_only=True) + assert c2.fluid.val["R134a"] == 1 + +def test_use_cuda_without_it_being_installed(): + nw = Network() + + a = Source("turbine") + b = Sink("Compressor") + + c1 = Connection(a, "out1", b, "in1") + + nw.add_conns(c1) + + c1.set_attr(m=1, p=1e5, T=300, fluid={"INCOMP::Water": 1}) + nw.solve("design", use_cuda=True) + nw._convergence_check() + assert not nw.use_cuda + +def test_component_not_found(): + nw = Network() + + a = Turbine("turbine") + b = Compressor("Compressor") + + c1 = Connection(a, "out1", b, "in1") + c2 = Connection(b, "out1", a, "in1") + + nw.add_conns(c1, c2) + assert nw.get_comp("Turbine") is None + +def test_connection_not_found(): + nw = Network() + + a = Turbine("turbine") + b = Compressor("Compressor") + + c1 = Connection(a, "out1", b, "in1") + c2 = Connection(b, "out1", a, "in1") + + nw.add_conns(c1, c2) + assert nw.get_conn("1") is None + +def test_missing_source_sink_cycle_closer(): + nw = Network() + + a = Turbine("turbine") + b = Compressor("Compressor") + + c1 = Connection(a, "out1", b, "in1") + c2 = Connection(b, "out1", a, "in1") + + nw.add_conns(c1, c2) + with raises(TESPyNetworkError): + nw.check_network() diff --git a/tests/test_tools/test_characteristics.py b/tests/test_tools/test_characteristics.py index 7bbdb3ef1..883f36d9c 100644 --- a/tests/test_tools/test_characteristics.py +++ b/tests/test_tools/test_characteristics.py @@ -27,7 +27,7 @@ def test_custom_CharLine_import(): """Test importing a custom characteristc lines.""" # we need to write some data to the path first, using defaults - data_path = __datapath__ + 'char_lines.json' + data_path = os.path.join(__datapath__, 'char_lines.json') path = extend_basic_path('data') tmp_path = extend_basic_path('tmp_dir_for_testing') @@ -75,7 +75,7 @@ def test_custom_CharMap_import(): """Test importing a custom characteristc map.""" # we need to write some data to the path first, using defaults - data_path = __datapath__ + 'char_maps.json' + data_path = os.path.join(__datapath__, 'char_maps.json') path = extend_basic_path('data') tmp_path = extend_basic_path('tmp_dir_for_testing') diff --git a/tests/test_tools/test_fluid_properties.py b/tests/test_tools/test_fluid_properties/test_coolprop.py similarity index 82% rename from tests/test_tools/test_fluid_properties.py rename to tests/test_tools/test_fluid_properties/test_coolprop.py index 8b187dbaf..bb5dd67c2 100644 --- a/tests/test_tools/test_fluid_properties.py +++ b/tests/test_tools/test_fluid_properties/test_coolprop.py @@ -30,14 +30,14 @@ class TestFluidProperties: def setup_method(self): - fp.Memorise.add_fluids({'Air': 'HEOS'}) - fp.Memorise.add_fluids({ - 'N2': 'HEOS', 'O2': 'HEOS', 'Ar': 'HEOS', 'CO2': 'HEOS'}) - - mix = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} - pure = {'Air': 1} - self.flow_mix = [0, 0, 0, mix] - self.flow_pure = [0, 0, 0, pure] + self.pure_data = { + "Air": {"wrapper": fp.CoolPropWrapper("Air"), "mass_fraction": 1} + } + self.mixture_data = { + "N2": {"wrapper": fp.CoolPropWrapper("N2"), "mass_fraction": 0.7556}, + "O2": {"wrapper": fp.CoolPropWrapper("O2"), "mass_fraction": 0.2315}, + "Ar": {"wrapper": fp.CoolPropWrapper("Ar"), "mass_fraction": 0.0129}, + } self.p_range = np.linspace(1e-2, 200, 40) * 1e5 self.T_range = np.linspace(220, 2220, 40) self.errormsg = ('Relative deviation of fluid mixture to base ' @@ -52,30 +52,31 @@ def test_properties(self): A CoolProp mixture object could be check as well! """ - funcs = {'h': fp.h_mix_pT, - 's': fp.s_mix_pT, - 'v': fp.v_mix_pT, - 'visc': fp.visc_mix_pT} + funcs = { + 'h': fp.h_mix_pT, + 's': fp.s_mix_pT, + 'v': fp.v_mix_pT, + 'visc': fp.viscosity_mix_pT + } for name, func in funcs.items(): # enthalpy and entropy need reference point definition if name == 'h' or name == 's': p_ref = 1e5 T_ref = 500 - mix_ref = func([0, p_ref, 0, self.flow_mix[3]], T_ref) - pure_ref = func([0, p_ref, 0, self.flow_pure[3]], T_ref) + mix_ref = func(p_ref, T_ref, self.mixture_data, "ideal") + pure_ref = func(p_ref, T_ref, self.pure_data, None) for p in self.p_range: - self.flow_mix[1] = p - self.flow_pure[1] = p for T in self.T_range: - val_mix = func(self.flow_mix, T) - val_pure = func(self.flow_pure, T) + val_mix = func(p, T, self.mixture_data, "ideal") + val_pure = func(p, T, self.pure_data, None) # enthalpy and entropy need reference point if name == 'h' or name == 's': - d_rel = abs(((val_mix - mix_ref) - - (val_pure - pure_ref)) / - (val_pure - pure_ref)) + d_rel = abs( + ((val_mix - mix_ref) - (val_pure - pure_ref)) + / (val_pure - pure_ref) + ) else: d_rel = abs((val_mix - val_pure) / val_pure) @@ -148,9 +149,9 @@ def test_properties(self): class TestFluidPropertyBackEnds: """Testing full models with different fluid property back ends.""" - def setup_clausius_rankine(self, fluid_list): + def setup_clausius_rankine(self, fluid, back_end): """Setup a Clausius-Rankine cycle.""" - self.nw = Network(fluids=fluid_list) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', iterinfo=True) # %% components @@ -189,9 +190,9 @@ def setup_clausius_rankine(self, fluid_list): # %% parametrization of connections - fs_in.set_attr(p=100, T=500, m=100, fluid={self.nw.fluids[0]: 1}) + fs_in.set_attr(p=100, T=500, m=100, fluid={f"{back_end}::{fluid}": 1}) fw.set_attr(h=200e3) - cw_in.set_attr(T=20, p=5, fluid={self.nw.fluids[0]: 1}) + cw_in.set_attr(T=20, p=5, fluid={f"{back_end}::{fluid}": 1}) cw_out.set_attr(T=30) # %% solving @@ -200,9 +201,9 @@ def setup_clausius_rankine(self, fluid_list): fw.set_attr(h=None) self.nw.solve('design') - def setup_pipeline_network(self, fluid_list): + def setup_pipeline_network(self, fluid, back_end): """Setup a pipeline network.""" - self.nw = Network(fluids=fluid_list) + self.nw = Network() self.nw.set_attr(p_unit='bar', T_unit='C', iterinfo=False) # %% components @@ -228,7 +229,7 @@ def setup_pipeline_network(self, fluid_list): # %% parametrization of connections - pu_pi.set_attr(p=20, T=100, m=10, fluid={self.nw.fluids[0]: 1}) + pu_pi.set_attr(p=20, T=100, m=10, fluid={f"{back_end}::{fluid}": 1}) # %% solving self.nw.solve('design') @@ -243,20 +244,13 @@ def test_clausius_rankine_tabular(self): back_ends = ['HEOS', 'BICUBIC', 'TTSE'] results = {} for back_end in back_ends: - # delete the fluid from the memorisation class - if fluid in fp.Memorise.state.keys(): - del fp.Memorise.state[fluid] - del fp.Memorise.back_end[fluid] - self.setup_clausius_rankine([back_end + '::' + fluid]) + self.setup_clausius_rankine(fluid, back_end) results[back_end] = ( 1 - abs(self.nw.get_comp('condenser').Q.val) / self.nw.get_comp('steam generator').Q.val) efficiency = results['HEOS'] - if fluid in fp.Memorise.state.keys(): - del fp.Memorise.state[fluid] - del fp.Memorise.back_end[fluid] for back_end in back_ends: if back_end == 'HEOS': continue @@ -269,26 +263,19 @@ def test_clausius_rankine_tabular(self): str(d_rel) + ' but should not be larger than 1e-4.') assert d_rel <= 1e-4, msg + @pytest.mark.skip def test_clausius_rankine(self): """Test the Clausius-Rankine cycle with different back ends.""" fluid = 'water' back_ends = ['HEOS', 'IF97'] results = {} for back_end in back_ends: - # delete the fluid from the memorisation class - if fluid in fp.Memorise.state.keys(): - del fp.Memorise.state[fluid] - del fp.Memorise.back_end[fluid] - self.setup_clausius_rankine([back_end + '::' + fluid]) + self.setup_clausius_rankine(fluid, back_end) results[back_end] = ( 1 - abs(self.nw.get_comp('condenser').Q.val) / self.nw.get_comp('steam generator').Q.val) efficiency = results['HEOS'] - - if fluid in fp.Memorise.state.keys(): - del fp.Memorise.state[fluid] - del fp.Memorise.back_end[fluid] for back_end in back_ends: if back_end == 'HEOS': continue @@ -306,10 +293,7 @@ def test_pipeline_network(self): fluids_back_ends = {'DowJ': 'INCOMP', 'water': 'HEOS'} for fluid, back_end in fluids_back_ends.items(): - # delete the fluid from the memorisation class - if fluid in fp.Memorise.state.keys(): - del fp.Memorise.state[fluid] - self.setup_pipeline_network([back_end + '::' + fluid]) + self.setup_pipeline_network(fluid, back_end) self.nw._convergence_check() value = round(self.nw.get_comp('pipeline').pr.val, 5) diff --git a/tests/test_tools/test_fluid_properties/test_iapws.py b/tests/test_tools/test_fluid_properties/test_iapws.py new file mode 100644 index 000000000..a2e5b8e12 --- /dev/null +++ b/tests/test_tools/test_fluid_properties/test_iapws.py @@ -0,0 +1,69 @@ +from tespy.components import Sink +from tespy.components import Source +from tespy.components import Turbine +from tespy.connections import Connection +from tespy.networks import Network +from tespy.tools.fluid_properties.wrappers import IAPWSWrapper + + +class TestIAPWS: + + def setup_method(self): + self.nwk = Network() + + so = Source("Source") + tu = Turbine("Pump") + si = Sink("Sink") + + c1 = Connection(so, "out1", tu, "in1", label="1") + c2 = Connection(tu, "out1", si, "in1", label="2") + + self.nwk.add_conns(c1, c2) + + tu.set_attr(eta_s=0.9) + + c1.set_attr(v=1, p=1e5, T=500, fluid={"H2O": 1}) + c2.set_attr(p=1e4) + + def test_iapws_95(self): + c1, c2 = self.nwk.get_conn(["1", "2"]) + + self.nwk.solve("design") + self.nwk._convergence_check() + + h_out_ref = round(c2.h.val_SI, 3) + T_out_ref = round(c2.T.val_SI, 3) + x_out_ref = round(c2.x.val_SI, 3) + + self.setup_method() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"IF95::H2O": 1}, fluid_engines={"H2O": IAPWSWrapper}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + assert h_out_ref == round(c2.h.val_SI, 3) + assert T_out_ref == round(c2.T.val_SI, 3) + assert x_out_ref == round(c2.x.val_SI, 3) + + def test_iapws_97(self): + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"IF97::H2O": 1}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + h_out_ref = round(c2.h.val_SI / 1000) + T_out_ref = round(c2.T.val_SI) + x_out_ref = round(c2.x.val_SI, 3) + + self.setup_method() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"IF97::H2O": 1}, fluid_engines={"H2O": IAPWSWrapper}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + assert h_out_ref == round(c2.h.val_SI / 1000) + assert T_out_ref == round(c2.T.val_SI) + assert x_out_ref == round(c2.x.val_SI, 3) diff --git a/tests/test_tools/test_fluid_properties/test_pyromat.py b/tests/test_tools/test_fluid_properties/test_pyromat.py new file mode 100644 index 000000000..06accf64a --- /dev/null +++ b/tests/test_tools/test_fluid_properties/test_pyromat.py @@ -0,0 +1,47 @@ +from tespy.components import Sink +from tespy.components import Source +from tespy.components import Turbine +from tespy.connections import Connection +from tespy.networks import Network +from tespy.tools.fluid_properties.wrappers import PyromatWrapper + + +class TestPyromat: + + def setup_method(self): + self.nwk = Network() + + so = Source("Source") + tu = Turbine("Pump") + si = Sink("Sink") + + c1 = Connection(so, "out1", tu, "in1", label="1") + c2 = Connection(tu, "out1", si, "in1", label="2") + + self.nwk.add_conns(c1, c2) + + tu.set_attr(eta_s=0.9) + + c1.set_attr(v=1, p=1e5, T=500, fluid={"H2O": 1}) + c2.set_attr(p=1e4) + + def test_pyromat(self): + c1, c2 = self.nwk.get_conn(["1", "2"]) + + self.nwk.solve("design") + self.nwk._convergence_check() + + h_out_ref = round(c2.h.val_SI / 1e3) + T_out_ref = round(c2.T.val_SI) + x_out_ref = round(c2.x.val_SI, 3) + + self.setup_method() + c1, c2 = self.nwk.get_conn(["1", "2"]) + c1.set_attr(fluid={"mp::H2O": 1}, fluid_engines={"H2O": PyromatWrapper}) + + self.nwk.solve("design") + self.nwk._convergence_check() + + assert h_out_ref == round(c2.h.val_SI / 1e3) + assert T_out_ref == round(c2.T.val_SI) + assert x_out_ref == round(c2.x.val_SI, 3) diff --git a/tox.ini b/tox.ini index 2a5c8ff6a..8e5d3fd79 100644 --- a/tox.ini +++ b/tox.ini @@ -3,30 +3,28 @@ envlist = clean, check, docs, - py37, - py38, py39, py310, + py311, report [gh-actions] python = - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] basepython = docs: {env:TOXPYTHON:python3.10} - {bootstrap,clean,check,report,codecov,coveralls}: {env:TOXPYTHON:python3} + {bootstrap,clean,check,report,coveralls}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes passenv = * deps = - pytest + .[dev] commands = {posargs:pytest -vv --ignore=src} @@ -40,18 +38,15 @@ commands = [testenv:check] deps = - docutils check-manifest - flake8 + flit readme-renderer pygments isort skip_install = true commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} --ignore=docs/_build/** - flake8 src tests setup.py - isort --verbose --check-only --diff --recursive src tests setup.py + check-manifest {toxinidir} + isort --verbose --check-only --diff --recursive src tests [testenv:docs] usedevelop = true @@ -68,13 +63,6 @@ skip_install = true commands = coveralls [] -[testenv:codecov] -deps = - codecov -skip_install = true -commands = - codecov [] - [testenv:report] deps = coverage skip_install = true @@ -85,21 +73,10 @@ commands = [testenv:clean] commands = coverage erase skip_install = true -deps = coverage - -[testenv:py37] -basepython = {env:TOXPYTHON:python3.7} -setenv = - {[testenv]setenv} -usedevelop = true -commands = - {posargs:pytest --cov --cov-report=term-missing -vv} -deps = - {[testenv]deps} - pytest-cov +deps = coverage[toml] -[testenv:py38] -basepython = {env:TOXPYTHON:python3.8} +[testenv:py39] +basepython = {env:TOXPYTHON:python3.9} setenv = {[testenv]setenv} usedevelop = true @@ -109,8 +86,8 @@ deps = {[testenv]deps} pytest-cov -[testenv:py39] -basepython = {env:TOXPYTHON:python3.9} +[testenv:py310] +basepython = {env:TOXPYTHON:python3.10} setenv = {[testenv]setenv} usedevelop = true @@ -120,8 +97,8 @@ deps = {[testenv]deps} pytest-cov -[testenv:py310] -basepython = {env:TOXPYTHON:python3.10} +[testenv:py311] +basepython = {env:TOXPYTHON:python3.11} setenv = {[testenv]setenv} usedevelop = true diff --git a/tutorial/README.rst b/tutorial/README.rst new file mode 100644 index 000000000..855be4033 --- /dev/null +++ b/tutorial/README.rst @@ -0,0 +1,5 @@ +Tutorials +--------- +These are the python scripts presented as tutorials in the online documentation +of TESPy. The description and explanations on the tutorials can be found in the +`respective sections `_ of the documentation. diff --git a/tutorial/advanced/optimization_example.py b/tutorial/advanced/optimization_example.py index fca805dc1..ee1bbe336 100644 --- a/tutorial/advanced/optimization_example.py +++ b/tutorial/advanced/optimization_example.py @@ -24,7 +24,7 @@ class SamplePlant: """Class template for TESPy model usage in optimization module.""" def __init__(self): - self.nw = Network(fluids=["water"]) + self.nw = Network() self.nw.set_attr( p_unit="bar", T_unit="C", h_unit="kJ / kg", iterinfo=False ) diff --git a/tutorial/advanced/starting_values.py b/tutorial/advanced/starting_values.py index da8c53de9..357b0403e 100644 --- a/tutorial/advanced/starting_values.py +++ b/tutorial/advanced/starting_values.py @@ -3,7 +3,7 @@ from tespy.components import ( Condenser, Compressor, CycleCloser, HeatExchanger, - HeatExchangerSimple, Pump, Sink, Source, Valve + SimpleHeatExchanger, Pump, Sink, Source, Valve ) from tespy.connections import Connection, Bus @@ -11,10 +11,7 @@ wf = "NH3" # network -nw = Network( - fluids=["water", wf], - T_unit="C", p_unit="bar", h_unit="kJ / kg", m_unit="kg / s" - ) +nw = Network(T_unit="C", p_unit="bar", h_unit="kJ / kg", m_unit="kg / s") # components cycle_closer = CycleCloser("Refrigerant Cycle Closer") @@ -31,7 +28,7 @@ # heat sink cons_pump = Pump("Heat Sink Recirculation Pump") condenser = Condenser("Heat Sink Condenser") -cons_heatsink = HeatExchangerSimple("Heat Consumer") +cons_heatsink = SimpleHeatExchanger("Heat Consumer") cons_cycle_closer = CycleCloser("Consumer Feed Flow") # internal heat exchange @@ -50,10 +47,7 @@ c5 = Connection(int_heatex, "out1", valve, "in1", label="5") c6 = Connection(valve, "out1", cycle_closer, "in1", label="6") -nw.add_conns( - c0, c1, c2, c3, c4, - c5, c6 - ) +nw.add_conns(c0, c1, c2, c3, c4, c5, c6) # heat source c11 = Connection(heatsource_feedflow, "out1", heatsource_pump, "in1", label="11") @@ -78,15 +72,15 @@ T_cons_ff = 90 # consumer cycle -c23.set_attr(T=T_cons_ff, p=10, fluid={"water": 1, wf: 0}) +c23.set_attr(T=T_cons_ff, p=10, fluid={"water": 1}) c24.set_attr(T=T_cons_bf) # heat source cycle -c11.set_attr(T=T_hs_ff, p=1, fluid={"water": 1, wf: 0}) +c11.set_attr(T=T_hs_ff, p=1, fluid={"water": 1}) c13.set_attr(T=T_hs_bf, p=1) # evaporation to fully saturated gas -c1.set_attr(x=1, fluid={"water": 0, wf: 1}) +c1.set_attr(x=1, fluid={wf: 1}) # degree of overheating after internal heat exchanger (evaporation side) c2.set_attr(Td_bp=10) @@ -113,6 +107,7 @@ nw.solve("design") except ValueError as e: print(e) + nw._reset_topology_reduction_specifications() # %%[sec_4] import CoolProp.CoolProp as CP @@ -157,7 +152,6 @@ def generate_network_with_starting_values(wf): # network nw = Network( - fluids=["water", wf], T_unit="C", p_unit="bar", h_unit="kJ / kg", m_unit="kg / s", iterinfo=False ) @@ -177,7 +171,7 @@ def generate_network_with_starting_values(wf): # heat sink cons_pump = Pump("Heat Sink Recirculation Pump") condenser = Condenser("Heat Sink Condenser") - cons_heatsink = HeatExchangerSimple("Heat Consumer") + cons_heatsink = SimpleHeatExchanger("Heat Consumer") cons_cycle_closer = CycleCloser("Consumer Feed Flow") # internal heat exchange @@ -196,10 +190,7 @@ def generate_network_with_starting_values(wf): c5 = Connection(int_heatex, "out1", valve, "in1", label="5") c6 = Connection(valve, "out1", cycle_closer, "in1", label="6") - nw.add_conns( - c0, c1, c2, c3, c4, - c5, c6 - ) + nw.add_conns(c0, c1, c2, c3, c4, c5, c6) # heat source c11 = Connection(heatsource_feedflow, "out1", heatsource_pump, "in1", label="11") @@ -223,15 +214,15 @@ def generate_network_with_starting_values(wf): T_cons_ff = 90 # consumer cycle - c23.set_attr(T=T_cons_ff, p=10, fluid={"water": 1, wf: 0}) + c23.set_attr(T=T_cons_ff, p=10, fluid={"water": 1}) c24.set_attr(T=T_cons_bf) # heat source cycle - c11.set_attr(T=T_hs_ff, p=1, fluid={"water": 1, wf: 0}) + c11.set_attr(T=T_hs_ff, p=1, fluid={"water": 1}) c13.set_attr(T=T_hs_bf, p=1) # evaporation to fully saturated gas - c1.set_attr(x=1, fluid={"water": 0, wf: 1}) + c1.set_attr(x=1, fluid={wf: 1}) # parametrization components # isentropic efficiency diff --git a/tutorial/advanced/stepwise.py b/tutorial/advanced/stepwise.py index ec64ea71b..4da0d8145 100644 --- a/tutorial/advanced/stepwise.py +++ b/tutorial/advanced/stepwise.py @@ -3,13 +3,12 @@ working_fluid = "NH3" nw = Network( - fluids=["water", working_fluid], T_unit="C", p_unit="bar", h_unit="kJ / kg", m_unit="kg / s" ) # %%[sec_2] from tespy.components import Condenser from tespy.components import CycleCloser -from tespy.components import HeatExchangerSimple +from tespy.components import SimpleHeatExchanger from tespy.components import Pump from tespy.components import Sink from tespy.components import Source @@ -22,7 +21,7 @@ # consumer system cd = Condenser("condenser") rp = Pump("recirculation pump") -cons = HeatExchangerSimple("consumer") +cons = SimpleHeatExchanger("consumer") # %%[sec_3] from tespy.connections import Connection @@ -42,8 +41,8 @@ # %%[sec_5] from CoolProp.CoolProp import PropsSI as PSI p_cond = PSI("P", "Q", 1, "T", 273.15 + 95, working_fluid) / 1e5 -c0.set_attr(T=170, p=p_cond, fluid={"water": 0, working_fluid: 1}) -c20.set_attr(T=60, p=2, fluid={"water": 1, working_fluid: 0}) +c0.set_attr(T=170, p=p_cond, fluid={working_fluid: 1}) +c20.set_attr(T=60, p=2, fluid={"water": 1}) c22.set_attr(T=90) # key design paramter @@ -89,14 +88,13 @@ su.set_attr(pr1=0.99, pr2=0.99) # %%[sec_10] # evaporator system cold side -p_evap = PSI("P", "Q", 1, "T", 273.15 + 5, working_fluid) / 1e5 -c4.set_attr(x=0.9, p=p_evap) +c4.set_attr(x=0.9, T=5) h_sat = PSI("H", "Q", 1, "T", 273.15 + 15, working_fluid) / 1e3 c6.set_attr(h=h_sat) # evaporator system hot side -c17.set_attr(T=15, fluid={"water": 1, working_fluid: 0}) +c17.set_attr(T=15, fluid={"water": 1}) c19.set_attr(T=9, p=1.013) # %%[sec_11] nw.solve("design") @@ -140,7 +138,7 @@ ic.set_attr(pr1=0.99, pr2=0.98) hsp.set_attr(eta_s=0.75) # %%[sec_15] -c0.set_attr(p=p_cond, fluid={"water": 0, working_fluid: 1}) +c0.set_attr(p=p_cond, fluid={working_fluid: 1}) c6.set_attr(h=c5.h.val + 10) c8.set_attr(h=c5.h.val + 10) @@ -148,7 +146,7 @@ c7.set_attr(h=c5.h.val * 1.2) c9.set_attr(h=c5.h.val * 1.2) -c11.set_attr(p=1.013, T=15, fluid={"water": 1, working_fluid: 0}) +c11.set_attr(p=1.013, T=15, fluid={"water": 1}) c14.set_attr(T=30) # %% [sec_16] nw.solve("design") @@ -156,7 +154,7 @@ c0.set_attr(p=None) cd.set_attr(ttd_u=5) -c4.set_attr(p=None) +c4.set_attr(T=None) ev.set_attr(ttd_l=5) c6.set_attr(h=None) diff --git a/tutorial/basics/district_heating.py b/tutorial/basics/district_heating.py index 24d2f262e..c9c7212bb 100755 --- a/tutorial/basics/district_heating.py +++ b/tutorial/basics/district_heating.py @@ -6,8 +6,7 @@ ) from tespy.connections import Connection -fluid_list = ['INCOMP::Water'] -nw = Network(fluids=fluid_list) +nw = Network() nw.set_attr(T_unit='C', p_unit='bar', h_unit='kJ / kg') # central heating plant @@ -39,7 +38,7 @@ pipe_feed.set_attr(Q=-250, pr=0.98) pipe_return.set_attr(Q=-200, pr=0.98) -c1.set_attr(T=90, p=10, fluid={'Water': 1}) +c1.set_attr(T=90, p=10, fluid={'INCOMP::Water': 1}) c2.set_attr(p=13) c4.set_attr(T=65) diff --git a/tutorial/basics/gas_turbine.py b/tutorial/basics/gas_turbine.py index f4353d76c..fbb5d2332 100755 --- a/tutorial/basics/gas_turbine.py +++ b/tutorial/basics/gas_turbine.py @@ -6,8 +6,7 @@ from tespy.connections import Connection, Ref, Bus # define full fluid list for the network"s variable space -fluid_list = ["Ar", "N2", "O2", "CO2", "CH4", "H2O", "H2"] -nw = Network(fluids=fluid_list, p_unit="bar", T_unit="C") +nw = Network(p_unit="bar", T_unit="C") cp = Compressor("Compressor") cc = DiabaticCombustionChamber("combustion chamber") @@ -25,18 +24,9 @@ c2.set_attr( p=1, T=20, - fluid={ - "Ar": 0.0129, "N2": 0.7553, "H2O": 0, - "CH4": 0, "CO2": 0.0004, "O2": 0.2314, "H2": 0 - } -) -c5.set_attr( - p=1, T=20, - fluid={ - "CO2": 0.04, "Ar": 0, "N2": 0, "O2": 0, - "H2O": 0, "CH4": 0.96, "H2": 0 - } + fluid={"Ar": 0.0129, "N2": 0.7553, "CO2": 0.0004, "O2": 0.2314} ) +c5.set_attr(p=1, T=20, fluid={"CO2": 0.04, "CH4": 0.96, "H2": 0}) nw.solve(mode="design") nw.print_results() @@ -49,12 +39,7 @@ c3.set_attr(T=1400) nw.solve(mode="design") # %%[sec_6] -c5.set_attr( - fluid={ - "CO2": 0.03, "Ar": 0, "N2": 0, "O2": 0, - "H2O": 0, "CH4": 0.92, "H2": 0.05 - } -) +c5.set_attr(fluid={"CO2": 0.03, "CH4": 0.92, "H2": 0.05}) nw.solve(mode="design") # %%[sec_7] print(nw.results["Connection"]) @@ -77,14 +62,13 @@ tu.set_attr(eta_s=0.90) c1.set_attr( p=1, T=20, - fluid={ - "Ar": 0.0129, "N2": 0.7553, "H2O": 0, - "CH4": 0, "CO2": 0.0004, "O2": 0.2314, "H2": 0 - } + fluid={"Ar": 0.0129, "N2": 0.7553, "CO2": 0.0004, "O2": 0.2314} ) -c3.set_attr(T=1200) +c3.set_attr(m=30) c4.set_attr(p=Ref(c1, 1, 0)) nw.solve("design") +c3.set_attr(m=None, T=1200) +nw.solve("design") nw.print_results() # %%[sec_10] # unset the value, set Referenced value instead @@ -153,9 +137,7 @@ # %%[sec_12] c3.set_attr(T=None) - -data = np.linspace(0.025, 0.15, 6) - +data = np.linspace(0.1, 0.2, 6) T3 = [] for oxy in data[::-1]: @@ -164,7 +146,7 @@ T3 += [c3.T.val] # reset to base value -c3.fluid.val_set["O2"] = False +c3.fluid.is_set.remove("O2") c3.set_attr(T=1200) fig, ax = plt.subplots(1, figsize=(16, 8)) @@ -178,12 +160,13 @@ plt.tight_layout() fig.savefig('gas_turbine_oxygen.svg') plt.close() + # %%[sec_13] -# retain starting values for CH4 and H2 with this variant -c5.fluid.val_set["CH4"] = False -c5.fluid.val_set["H2"] = False +# fix mass fractions of all potential fluids except combustion gases +c5.set_attr(fluid={"CO2": 0.03, "O2": 0, "H2O": 0, "Ar": 0, "N2": 0, "CH4": None, "H2": None}) c5.set_attr(fluid_balance=True) + data = np.linspace(50, 60, 11) CH4 = [] @@ -195,6 +178,8 @@ CH4 += [c5.fluid.val["CH4"] * 100] H2 += [c5.fluid.val["H2"] * 100] +nw._convergence_check() + fig, ax = plt.subplots(1, figsize=(16, 8)) ax.scatter(data, CH4, s=100, color="#1f567d", label="CH4 mass fraction") @@ -209,4 +194,4 @@ plt.tight_layout() fig.savefig('gas_turbine_fuel_composition.svg') plt.close() -# %%[sec_14] +# %%[sec_14] \ No newline at end of file diff --git a/tutorial/basics/heat_pump.py b/tutorial/basics/heat_pump.py index 15ab6b716..28951a126 100644 --- a/tutorial/basics/heat_pump.py +++ b/tutorial/basics/heat_pump.py @@ -2,8 +2,7 @@ from tespy.networks import Network # create a network object with R134a as fluid -fluid_list = ['R134a'] -my_plant = Network(fluids=fluid_list) +my_plant = Network() # %%[sec_2] # set the unitsystem for temperatures to °C and for pressure to bar my_plant.set_attr(T_unit='C', p_unit='bar', h_unit='kJ / kg') diff --git a/tutorial/basics/rankine.py b/tutorial/basics/rankine.py index 0e9cb137c..19115cff1 100755 --- a/tutorial/basics/rankine.py +++ b/tutorial/basics/rankine.py @@ -2,16 +2,15 @@ from tespy.networks import Network # create a network object with R134a as fluid -fluid_list = ['water'] -my_plant = Network(fluids=fluid_list) +my_plant = Network() my_plant.set_attr(T_unit='C', p_unit='bar', h_unit='kJ / kg') # %%[sec_2] from tespy.components import ( - CycleCloser, Pump, Condenser, Turbine, HeatExchangerSimple, Source, Sink + CycleCloser, Pump, Condenser, Turbine, SimpleHeatExchanger, Source, Sink ) cc = CycleCloser('cycle closer') -sg = HeatExchangerSimple('steam generator') +sg = SimpleHeatExchanger('steam generator') mc = Condenser('main condenser') tu = Turbine('steam turbine') fp = Pump('feed pump') diff --git a/tutorial/heat_pump_exergy/NH3.py b/tutorial/heat_pump_exergy/NH3.py index 1bb8bbdc5..92a4ba8c1 100644 --- a/tutorial/heat_pump_exergy/NH3.py +++ b/tutorial/heat_pump_exergy/NH3.py @@ -15,15 +15,13 @@ from tespy.tools.characteristics import load_default_char as ldc import numpy as np -# %% network pamb = 1.013 # ambient pressure Tamb = 2.8 # ambient temperature # mean geothermal temperature (mean value of ground feed and return flow) Tgeo = 9.5 -nw = Network(fluids=['water', 'NH3'], T_unit='C', p_unit='bar', - h_unit='kJ / kg', m_unit='kg / s') +nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s') # %% components @@ -90,15 +88,15 @@ # %% connection parametrization # heat pump system -cc_cd.set_attr(fluid={'water': 0, 'NH3': 1}) +cc_cd.set_attr(fluid={'NH3': 1}) ev_cp.set_attr(Td_bp=3) # geothermal heat collector -gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1, 'NH3': 0}) +gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1}) ev_gh_out.set_attr(T=Tgeo - 1.5, p=1.5) # heating system -cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1, 'NH3': 0}) +cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1}) hs_ret_hsp.set_attr(T=35, p=2) # starting values @@ -114,9 +112,11 @@ # power bus char = CharLine(x=x, y=y) power = Bus('power input') -power.add_comps({'comp': cp, 'char': char, 'base': 'bus'}, - {'comp': ghp, 'char': char, 'base': 'bus'}, - {'comp': hsp, 'char': char, 'base': 'bus'}) +power.add_comps( + {'comp': cp, 'char': char, 'base': 'bus'}, + {'comp': ghp, 'char': char, 'base': 'bus'}, + {'comp': hsp, 'char': char, 'base': 'bus'} +) # consumer heat bus heat_cons = Bus('heating system') @@ -124,8 +124,7 @@ # geothermal heat bus heat_geo = Bus('geothermal heat') -heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, - {'comp': gh_out}) +heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, {'comp': gh_out}) nw.add_busses(power, heat_cons, heat_geo) diff --git a/tutorial/heat_pump_exergy/NH3_calculations.py b/tutorial/heat_pump_exergy/NH3_calculations.py index 18f6f1727..a602ae0dc 100644 --- a/tutorial/heat_pump_exergy/NH3_calculations.py +++ b/tutorial/heat_pump_exergy/NH3_calculations.py @@ -27,8 +27,7 @@ # mean geothermal temperature (mean value of ground feed and return flow) Tgeo = 9.5 -nw = Network(fluids=['water', 'NH3'], T_unit='C', p_unit='bar', - h_unit='kJ / kg', m_unit='kg / s') +nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s') # %% components @@ -95,16 +94,16 @@ # %% connection parametrization # heat pump system -cc_cd.set_attr(fluid={'water': 0, 'NH3': 1}) +cc_cd.set_attr(fluid={'NH3': 1}) ev_cp.set_attr(Td_bp=3) # geothermal heat collector -gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1, 'NH3': 0}, +gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1}, ) ev_gh_out.set_attr(T=Tgeo - 1.5, p=1.5) # heating system -cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1, 'NH3': 0}) +cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1}) hs_ret_hsp.set_attr(T=35, p=2) # starting values @@ -120,9 +119,11 @@ # power bus char = CharLine(x=x, y=y) power = Bus('power input') -power.add_comps({'comp': cp, 'char': char, 'base': 'bus'}, - {'comp': ghp, 'char': char, 'base': 'bus'}, - {'comp': hsp, 'char': char, 'base': 'bus'}) +power.add_comps( + {'comp': cp, 'char': char, 'base': 'bus'}, + {'comp': ghp, 'char': char, 'base': 'bus'}, + {'comp': hsp, 'char': char, 'base': 'bus'} +) # consumer heat bus heat_cons = Bus('heating system') @@ -130,8 +131,7 @@ # geothermal heat bus heat_geo = Bus('geothermal heat') -heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, - {'comp': gh_out}) +heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, {'comp': gh_out}) nw.add_busses(power, heat_cons, heat_geo) @@ -179,9 +179,7 @@ # %% exergy analysis -ean = ExergyAnalysis(network=nw, - E_F=[power, heat_geo], - E_P=[heat_cons]) +ean = ExergyAnalysis(network=nw, E_F=[power, heat_geo], E_P=[heat_cons]) ean.analyse(pamb, Tamb) print("\n##### EXERGY ANALYSIS #####\n") ean.print_results() diff --git a/tutorial/heat_pump_exergy/R410A.py b/tutorial/heat_pump_exergy/R410A.py index fd59e3aca..c38a7f141 100644 --- a/tutorial/heat_pump_exergy/R410A.py +++ b/tutorial/heat_pump_exergy/R410A.py @@ -22,8 +22,7 @@ # mean geothermal temperature (mean value of ground feed and return flow) Tgeo = 9.5 -nw = Network(fluids=['water', 'R410A'], T_unit='C', p_unit='bar', - h_unit='kJ / kg', m_unit='kg / s') +nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s') # %% components @@ -90,15 +89,15 @@ # %% connection parametrization # heat pump system -cc_cd.set_attr(fluid={'water': 0, 'R410A': 1}) +cc_cd.set_attr(fluid={'R410A': 1}) ev_cp.set_attr(Td_bp=3) # geothermal heat collector -gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1, 'R410A': 0}) +gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1}) ev_gh_out.set_attr(T=Tgeo - 1.5, p=1.5) # heating system -cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1, 'R410A': 0}) +cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1}) hs_ret_hsp.set_attr(T=35, p=2) # starting values @@ -114,9 +113,11 @@ # power bus char = CharLine(x=x, y=y) power = Bus('power input') -power.add_comps({'comp': cp, 'char': char, 'base': 'bus'}, - {'comp': ghp, 'char': char, 'base': 'bus'}, - {'comp': hsp, 'char': char, 'base': 'bus'}) +power.add_comps( + {'comp': cp, 'char': char, 'base': 'bus'}, + {'comp': ghp, 'char': char, 'base': 'bus'}, + {'comp': hsp, 'char': char, 'base': 'bus'} +) # consumer heat bus heat_cons = Bus('heating system') @@ -124,8 +125,7 @@ # geothermal heat bus heat_geo = Bus('geothermal heat') -heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, - {'comp': gh_out}) +heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, {'comp': gh_out}) nw.add_busses(power, heat_cons, heat_geo) diff --git a/tutorial/heat_pump_exergy/R410A_calculations.py b/tutorial/heat_pump_exergy/R410A_calculations.py index 39575354d..457a10472 100644 --- a/tutorial/heat_pump_exergy/R410A_calculations.py +++ b/tutorial/heat_pump_exergy/R410A_calculations.py @@ -27,8 +27,7 @@ # mean geothermal temperature (mean value of ground feed and return flow) Tgeo = 9.5 -nw = Network(fluids=['water', 'R410A'], T_unit='C', p_unit='bar', - h_unit='kJ / kg', m_unit='kg / s') +nw = Network(T_unit='C', p_unit='bar', h_unit='kJ / kg', m_unit='kg / s') # %% components @@ -95,15 +94,15 @@ # %% connection parametrization # heat pump system -cc_cd.set_attr(fluid={'water': 0, 'R410A': 1}) +cc_cd.set_attr(fluid={'R410A': 1}) ev_cp.set_attr(Td_bp=3) # geothermal heat collector -gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1, 'R410A': 0}) +gh_in_ghp.set_attr(T=Tgeo + 1.5, p=1.5, fluid={'water': 1}) ev_gh_out.set_attr(T=Tgeo - 1.5, p=1.5) # heating system -cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1, 'R410A': 0}) +cd_hs_feed.set_attr(T=40, p=2, fluid={'water': 1}) hs_ret_hsp.set_attr(T=35, p=2) # starting values @@ -119,9 +118,11 @@ # power bus char = CharLine(x=x, y=y) power = Bus('power input') -power.add_comps({'comp': cp, 'char': char, 'base': 'bus'}, - {'comp': ghp, 'char': char, 'base': 'bus'}, - {'comp': hsp, 'char': char, 'base': 'bus'}) +power.add_comps( + {'comp': cp, 'char': char, 'base': 'bus'}, + {'comp': ghp, 'char': char, 'base': 'bus'}, + {'comp': hsp, 'char': char, 'base': 'bus'} +) # consumer heat bus heat_cons = Bus('heating system') @@ -129,8 +130,7 @@ # geothermal heat bus heat_geo = Bus('geothermal heat') -heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, - {'comp': gh_out}) +heat_geo.add_comps({'comp': gh_in, 'base': 'bus'}, {'comp': gh_out}) nw.add_busses(power, heat_cons, heat_geo) @@ -178,9 +178,7 @@ # %% exergy analysis -ean = ExergyAnalysis(network=nw, - E_F=[power, heat_geo], - E_P=[heat_cons]) +ean = ExergyAnalysis(network=nw, E_F=[power, heat_geo], E_P=[heat_cons]) ean.analyse(pamb, Tamb) print("\n##### EXERGY ANALYSIS #####\n") ean.print_results() diff --git a/tutorial/heat_pump_exergy/plots.py b/tutorial/heat_pump_exergy/plots.py index b380d0287..7e4454ed5 100644 --- a/tutorial/heat_pump_exergy/plots.py +++ b/tutorial/heat_pump_exergy/plots.py @@ -6,8 +6,9 @@ # color range -colors = ['#00395b', '#74adc1', '#b54036', '#ec6707', - '#bfbfbf', '#999999', '#010101'] +colors = [ + '#00395b', '#74adc1', '#b54036', '#ec6707', '#bfbfbf', '#999999', '#010101' +] # %% figure 1: plot component exergy destruction