├── docs ├── _static │ └── README ├── _templates │ ├── README │ └── layout.html ├── thermostate.rst ├── examples.rst ├── Makefile ├── installation.rst ├── make.bat ├── index.rst ├── conf.py ├── cold-air-brayton-cycle-example.ipynb └── Tutorial.ipynb ├── .vscode └── settings.json ├── MANIFEST.in ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── pythonpackage.yml ├── src └── thermostate │ ├── __init__.py │ ├── abbreviations.py │ ├── plotting.py │ └── thermostate.py ├── .flake8 ├── .coveragerc ├── tox.ini ├── tests ├── test_abbreviations.py ├── test_plotting.py └── test_thermostate.py ├── .readthedocs.yml ├── paper ├── codemeta.json ├── paper.bib └── paper.md ├── LICENSE.md ├── CONTRIBUTING.md ├── pyproject.toml ├── CODE_OF_CONDUCT.md ├── README.md ├── .gitignore └── CHANGELOG.md /docs/_static/README: -------------------------------------------------------------------------------- 1 | This folder contains static files for Sphinx 2 | -------------------------------------------------------------------------------- /docs/_templates/README: -------------------------------------------------------------------------------- 1 | This folder contains templates for Sphinx 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include CODE_OF_CONDUCT.md 3 | include CONTRIBUTING.md 4 | include CHANGELOG.md 5 | include README.md 6 | include VERSION.txt 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Fixes # . 2 | - [ ] Tests added 3 | - [ ] Added entry into `CHANGELOG.md` 4 | 5 | Changes proposed in this pull request: 6 | - 7 | - 8 | - 9 | -------------------------------------------------------------------------------- /src/thermostate/__init__.py: -------------------------------------------------------------------------------- 1 | from .abbreviations import EnglishEngineering, SystemInternational # noqa 2 | from .thermostate import Q_, State, set_default_units, units # noqa 3 | 4 | __version__ = "2.0.0.post1" 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max_line_length = 88 3 | # Ignore docstring requirement for dunder methods and __init__, 4 | # the latter only when the class is documented separately 5 | extend_ignore = D105,D107 6 | per-file-ignores = 7 | docs/conf.py:D100 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Code sample, preferably able to be copy-pasted and run with no changes 2 | 3 | 4 | ### Expected behavior 5 | 6 | 7 | ### Actual behavior, including any error messages 8 | 9 | 10 | ### Thermostate version, Python version, OS version 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | # List of packages to include, not necessarily directories 4 | source = thermostate 5 | 6 | [report] 7 | exclude_lines = 8 | if self.debug: 9 | pragma: no cover 10 | raise NotImplementedError 11 | if __name__ == .__main__.: 12 | 13 | [paths] 14 | # List of paths that are equivalent and should be combined 15 | source = 16 | src/thermostate 17 | **/site-packages/thermostate 18 | -------------------------------------------------------------------------------- /docs/thermostate.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | ThermoState 3 | =========== 4 | 5 | ``thermostate.thermostate`` module 6 | ---------------------------------- 7 | 8 | .. automodule:: thermostate.thermostate 9 | 10 | ``thermostate.abbreviations`` module 11 | ------------------------------------ 12 | 13 | .. automodule:: thermostate.abbreviations 14 | 15 | ``thermostate.plotting`` module 16 | ---------------------------------- 17 | 18 | .. automodule:: thermostate.plotting 19 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | These examples show possible use cases for ThermoState in solving somewhat involved Applied 6 | Thermodynamics problems. These types of problems would be very difficult to solve by hand using 7 | tables, due to the number of calculations required from the property tables. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | air-standard-brayton-cycle-example 14 | cold-air-brayton-cycle-example 15 | diesel-cycle-example 16 | rankine-cycle-example 17 | regen-reheat-rankine-cycle-example 18 | cascade-refrigeration-cycle-example 19 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py3{9,10,11}, lint, notebooks 8 | isolated_build = True 9 | requires = tox-pdm 10 | 11 | [testenv] 12 | groups = testing 13 | description = run the tests with pytest under {basepython} 14 | commands = 15 | test {posargs} 16 | 17 | [testenv:notebooks] 18 | description = run the Notebooks in the docs folder 19 | groups = docs 20 | commands = notebooks 21 | 22 | [testenv:lint] 23 | groups = dev 24 | skip_install = true 25 | commands = lint 26 | -------------------------------------------------------------------------------- /tests/test_abbreviations.py: -------------------------------------------------------------------------------- 1 | """Test module for the units abbreviations code.""" 2 | from thermostate import EnglishEngineering as EE 3 | from thermostate import SystemInternational as SI 4 | 5 | 6 | def test_EE(): 7 | """Test the English Engineering abbreviations.""" 8 | assert EE.s == "BTU/(lb*degR)" 9 | assert EE.h == "BTU/lb" 10 | assert EE.T == "degF" 11 | assert EE.u == "BTU/lb" 12 | assert EE.v == "ft**3/lb" 13 | assert EE.p == "psi" 14 | 15 | 16 | def test_SI(): 17 | """Test the Système Internationale d'Unités abbreviations.""" 18 | assert SI.s == "kJ/(kg*K)" 19 | assert SI.h == "kJ/kg" 20 | assert SI.T == "degC" 21 | assert SI.u == "kJ/kg" 22 | assert SI.v == "m**3/kg" 23 | assert SI.p == "bar" 24 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | apt_packages: 14 | - "pandoc" 15 | 16 | # Build documentation in the docs/ directory with Sphinx 17 | sphinx: 18 | configuration: docs/conf.py 19 | 20 | # If using Sphinx, optionally build your docs in additional formats such as PDF 21 | formats: 22 | - pdf 23 | - epub 24 | 25 | # Optionally declare the Python requirements required to build your docs 26 | python: 27 | install: 28 | - method: pip 29 | path: . 30 | extra_requirements: 31 | - docs 32 | -------------------------------------------------------------------------------- /paper/codemeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", 3 | "@type": "Code", 4 | "author": [ 5 | { 6 | "@id": "https://orcid.org/0000-0003-0815-9270", 7 | "@type": "Person", 8 | "email": "bryan.w.weber@gmail.com", 9 | "name": "Bryan W. Weber", 10 | "affiliation": "University of Connecticut" 11 | } 12 | ], 13 | "identifier": "https://doi.org/10.5281/zenodo.995682", 14 | "codeRepository": "https://github.com/bryanwweber/thermostate", 15 | "datePublished": "2018-09-21", 16 | "dateModified": "2018-09-21", 17 | "dateCreated": "2018-09-21", 18 | "description": "A State manager for Thermodynamics Courses", 19 | "keywords": "thermodynamics, engineering, coolprop, pint", 20 | "license": "BSD", 21 | "title": "ThermoState", 22 | "version": "v0.4.1" 23 | } 24 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Conda 6 | ----- 7 | 8 | The preferred installation method is to use `conda `__. Using Conda, ThermoState can be installed for Python 3.9 and up. You can create a new environment with: 9 | 10 | .. code-block:: bash 11 | 12 | conda create -n thermostate -c conda-forge thermostate 13 | 14 | Pip 15 | --- 16 | 17 | Alternatively, ThermoState can be installed with pip. 18 | 19 | .. code-block:: bash 20 | 21 | python -m pip install thermostate 22 | 23 | From Source 24 | ----------- 25 | 26 | ThermoState is a pure-Python package that supports any Python version 3.9 and higher. 27 | To install from source, clone the source code repository and install using ``pip``. 28 | 29 | .. code-block:: bash 30 | 31 | git clone https://github.com/bryanwweber/thermostate 32 | cd thermostate 33 | python -m pip install . 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. thermostate documentation master file, created by 2 | sphinx-quickstart on Tue Jan 10 10:16:52 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ThermoState's documentation! 7 | ======================================= 8 | 9 | ThermoState is a Python package that provides easy management of thermodynamic states of simple 10 | compressible systems. ThermoState relies on `CoolProp `__ and 11 | `Pint `__ to provide the equations of state and units handling, 12 | respectively. ThermoState replaces tables that are typically used in engineering courses to 13 | evaluate properties when solving for the behavior of systems. 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :caption: Contents: 18 | 19 | installation 20 | Tutorial 21 | Plot-Tutorial 22 | examples 23 | thermostate 24 | CHANGELOG 25 | CODE_OF_CONDUCT 26 | CONTRIBUTING 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {%- block footer %} 3 | 19 | 20 | {% if theme_github_banner|lower != 'false' %} 21 | 22 | Fork me on GitHub 23 | 24 | {% endif %} 25 | 26 | {%- endblock %} 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2020, Bryan W. Weber 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/thermostate/abbreviations.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains classes with attributes representing the common property units. 3 | 4 | Example 5 | ------- 6 | These classes are shortcuts to the units for common properties:: 7 | 8 | >>> st = State("water", T=Q_(300.0, "K"), p=Q_(101325, "Pa")) 9 | >>> h = st.h.to(SI.h) 10 | >>> u = st.u.to(EE.u) 11 | 12 | """ 13 | 14 | 15 | class EnglishEngineering: 16 | """String representations of common units. 17 | 18 | The attributes of this class are strings that represent the common units for 19 | thermodynamics calculations. 20 | 21 | Attributes 22 | ---------- 23 | h : `str` 24 | BTU/lb 25 | p : `str` 26 | psi 27 | s : `str` 28 | BTU/(lb*degR) 29 | T : `str` 30 | degF 31 | u : `str` 32 | BTU/lb 33 | v : `str` 34 | ft**3/lb 35 | cp : `str` 36 | BTU/(lb*degR) 37 | cv : `str` 38 | BTU/(lb*degR) 39 | 40 | """ 41 | 42 | s = "BTU/(lb*degR)" 43 | h = "BTU/lb" 44 | T = "degF" 45 | u = "BTU/lb" 46 | v = "ft**3/lb" 47 | p = "psi" 48 | cp = "BTU/(lb*degR)" 49 | cv = "BTU/(lb*degR)" 50 | 51 | 52 | class SystemInternational: 53 | """String representations of common units. 54 | 55 | The attributes of this class are strings that represent the common units for 56 | thermodynamics calculations. 57 | 58 | Attributes 59 | ---------- 60 | h : `str` 61 | kJ/kg 62 | p : `str` 63 | bar 64 | s : `str` 65 | kJ/(kg*K) 66 | T : `str` 67 | degC 68 | u : `str` 69 | kJ/kg 70 | v : `str` 71 | m**3/lb 72 | cv : `str` 73 | kJ/(K*kg) 74 | cp : `str` 75 | kJ/(K*kg) 76 | 77 | """ 78 | 79 | s = "kJ/(kg*K)" 80 | h = "kJ/kg" 81 | T = "degC" 82 | u = "kJ/kg" 83 | v = "m**3/kg" 84 | p = "bar" 85 | cv = "kJ/(K*kg)" 86 | cp = "kJ/(K*kg)" 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions in the form of bug reports, bug fixes, improvements to the documentation, ideas for enhancements, or the enhancements themselves! 4 | 5 | You can find a [list of current issues](https://github.com/bryanwweber/thermostate/issues) in the project's GitHub repository. Feel free to tackle any existing bugs or enhancement ideas by submitting a [pull request](https://github.com/bryanwweber/thermostate/pulls). Some issues are marked as `beginner-friendly`. These issues are a great place to start working with PyKED and ChemKED, if you're new here. 6 | 7 | ## Bug Reports 8 | 9 | * Please include a short (but detailed) Python snippet or explanation for reproducing the problem. Attach or include a link to any input files that will be needed to reproduce the error. 10 | * Explain the behavior you expected, and how what you got differed. 11 | * Include the full text of any error messages that are printed on the screen. 12 | 13 | ## Pull Requests 14 | 15 | * If you're unfamiliar with Pull Requests, please take a look at the [GitHub documentation for them](https://help.github.com/articles/proposing-changes-to-a-project-with-pull-requests/). 16 | * **Make sure the test suite passes** on your computer, and that test coverage doesn't go down. To do this, run `pytest -vv --cov=./` from the top-level directory. 17 | * *Always* add tests and docs for your code. 18 | * Please reference relevant GitHub issues in your commit messages using `GH123` or `#123`. 19 | * Changes should be [PEP8](https://www.python.org/dev/peps/pep-0008/) and [PEP257](https://www.python.org/dev/peps/pep-0257/) compatible. 20 | * Keep style fixes to a separate commit to make your pull request more readable. 21 | * Add your changes into the [`CHANGELOG`](https://github.com/bryanwweber/thermostate/blob/master/CHANGELOG.md) 22 | * Docstrings are required and should follow the [NumPy style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html). 23 | * When you start working on a pull request, start by creating a new branch pointing at the latest commit on [GitHub master](https://github.com/bryanwweber/thermostate/tree/master). 24 | * The copyright policy is detailed in the [`LICENSE`](https://github.com/bryanwweber/thermostate/blob/master/LICENSE.md). 25 | 26 | ## Meta 27 | 28 | Thanks to the useful [contributing guide of pyrk](https://github.com/pyrk/pyrk/blob/master/CONTRIBUTING.md), which served as an inspiration and starting point for this guide. 29 | -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{cp-article, 2 | author = {Bell, Ian H. and Wronski, Jorrit and Quoilin, Sylvain and Lemort, Vincent}, 3 | title = {Pure and Pseudo-pure Fluid Thermophysical Property Evaluation and 4 | the Open-Source Thermophysical Property Library CoolProp}, 5 | journal = {Industrial \& Engineering Chemistry Research}, 6 | volume = {53}, 7 | number = {6}, 8 | pages = {2498--2508}, 9 | year = {2014}, 10 | doi = {10.1021/ie4033999}, 11 | URL = {http://pubs.acs.org/doi/abs/10.1021/ie4033999}, 12 | eprint = {http://pubs.acs.org/doi/pdf/10.1021/ie4033999} 13 | } 14 | 15 | @software{coolprop, 16 | author = {Bell, Ian H. and Wronski, Jorrit and Quoilin, Sylvain and Lemort, Vincent}, 17 | title = {CoolProp}, 18 | url = {http://coolprop.org}, 19 | version = {6.1.0}, 20 | date = {2016-10-25} 21 | } 22 | 23 | @software{pint, 24 | author = {Grecco, Hernan E. and others}, 25 | title = {Pint}, 26 | url = {https://pint.readthedocs.io}, 27 | version = {0.8.1}, 28 | date = {2017-06-05} 29 | } 30 | 31 | @article{matplotlib, 32 | Author = {Hunter, J. D.}, 33 | Title = {Matplotlib: A 2D graphics environment}, 34 | Journal = {Computing In Science \& Engineering}, 35 | Volume = {9}, 36 | Number = {3}, 37 | Pages = {90--95}, 38 | abstract = {Matplotlib is a 2D graphics package used for Python 39 | for application development, interactive scripting, and 40 | publication-quality image generation across user 41 | interfaces and operating systems.}, 42 | publisher = {IEEE COMPUTER SOC}, 43 | doi = {10.1109/MCSE.2007.55}, 44 | year = 2007 45 | } 46 | 47 | @book{numpy, 48 | title = {Guide to {NumPy}}, 49 | isbn = {978-1-5173-0007-4}, 50 | language = {English}, 51 | author = {Oliphant, Travis E}, 52 | year = {2015}, 53 | note = {OCLC: 1030608394}, 54 | publisher = {CreateSpace Independent Publishing Platform}, 55 | edition = 2, 56 | } 57 | 58 | @conference{jupyter_notebook, 59 | Author = {Thomas Kluyver and Benjamin Ragan-Kelley and Fernando P{\'e}rez and Brian Granger and Matthias Bussonnier and Jonathan Frederic and Kyle Kelley and Jessica Hamrick and Jason Grout and Sylvain Corlay and Paul Ivanov and Dami{\'a}n Avila and Safia Abdalla and Carol Willing}, 60 | Booktitle = {Positioning and Power in Academic Publishing: Players, Agents and Agendas}, 61 | Editor = {F. Loizides and B. Schmidt}, 62 | Organization = {IOS Press}, 63 | Pages = {87--90}, 64 | Title = {Jupyter Notebooks -- a publishing format for reproducible computational workflows}, 65 | Year = {2016} 66 | } 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pdm-pep517>=1.0.0"] 3 | build-backend = "pdm.pep517.api" 4 | 5 | 6 | [project] 7 | name = "thermostate" 8 | description = "A package to manage thermodynamic states" 9 | keywords = ["thermodynamics", "chemistry", "state", "education"] 10 | readme = "README.md" 11 | authors = [ 12 | {name = "Bryan W. Weber"}, 13 | ] 14 | maintainers = [ 15 | {name = "Bryan W. Weber", email = "bryan.w.weber@gmail.com"}, 16 | ] 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "License :: OSI Approved :: BSD License", 20 | "Operating System :: MacOS", 21 | "Operating System :: MacOS :: MacOS X", 22 | "Operating System :: Microsoft", 23 | "Operating System :: Microsoft :: Windows", 24 | "Operating System :: Microsoft :: Windows :: Windows 11", 25 | "Operating System :: Microsoft :: Windows :: Windows 10", 26 | "Operating System :: POSIX", 27 | "Operating System :: POSIX :: Linux", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.9", 32 | ] 33 | requires-python = ">=3.9" 34 | dependencies = [ 35 | "coolprop>=6.4.3.post1", 36 | "matplotlib>=3.5", 37 | "numpy>=1.22", 38 | "pint>=0.20,<1.0", 39 | ] 40 | license = {text = "BSD-3-clause"} 41 | dynamic = ["version"] 42 | 43 | [project.urls] 44 | Homepage = "https://thermostate.readthedocs.io/" 45 | Source = "https://github.com/bryanwweber/thermostate" 46 | Tracker = "https://github.com/bryanwweber/thermostate/issues" 47 | 48 | [project.optional-dependencies] 49 | docs = [ 50 | "ipykernel~=6.21", 51 | "ipython~=8.10", 52 | "jupyter-client~=8.0", 53 | "nbsphinx~=0.8", 54 | "recommonmark~=0.7", 55 | "sphinx~=6.1", 56 | ] 57 | 58 | [tool] 59 | [tool.pdm] 60 | package-dir = "src" 61 | [tool.pdm.version] 62 | source = "file" 63 | path = "src/thermostate/__init__.py" 64 | 65 | [tool.pdm.dev-dependencies] 66 | dev = [ 67 | "flake8>=6.0.0", 68 | "black[jupyter]>=23.1.0", 69 | "mypy>=1.0.0", 70 | "isort>=5.12.0", 71 | "flake8-docstrings>=1.7.0", 72 | ] 73 | testing = [ 74 | "pytest>=7.2", 75 | "pytest-cov>=4.0", 76 | ] 77 | ci = [ 78 | "tox>=4.4.5", 79 | "tox-pdm>=0.6.1", 80 | ] 81 | 82 | [tool.pdm.scripts] 83 | test = "pytest -vv --cov --cov-report=xml tests/" 84 | docs = "sphinx-build -b html docs/ docs/_build -W --keep-going" 85 | 86 | [tool.pdm.scripts.lint] 87 | shell = """ 88 | flake8 src/thermostate tests docs 89 | isort --check src/thermostate tests 90 | black --check src/thermostate tests docs 91 | """ 92 | 93 | [tool.pdm.scripts.format] 94 | shell = """ 95 | isort src/thermostate tests 96 | black src/thermostate tests docs 97 | """ 98 | 99 | [tool.pdm.scripts.notebooks] 100 | shell = """ 101 | jupyter nbconvert --to notebook --execute docs/*-example.ipynb 102 | jupyter nbconvert --to notebook --execute docs/Plot-Tutorial.ipynb 103 | jupyter nbconvert --to notebook --execute --ExecutePreprocessor.allow_errors=True docs/Tutorial.ipynb 104 | """ 105 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | # Build on tags that look like releases 6 | tags: 7 | - v* 8 | # Build when main is pushed to 9 | branches: 10 | - main 11 | pull_request: 12 | # Build when a pull request targets main 13 | branches: 14 | - main 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | build: 22 | name: Build and test on ${{ matrix.os }} with Python ${{ matrix.python }} 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | matrix: 26 | python-version: ["3.9", "3.10", "3.11"] 27 | os: [ubuntu-latest, macos-latest, windows-latest] 28 | fail-fast: false 29 | defaults: 30 | run: 31 | shell: bash {0} 32 | 33 | steps: 34 | - name: Check out the repository 35 | uses: actions/checkout@v3 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: pdm-project/setup-pdm@v3 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Install dependencies 41 | run: pdm install -G ci 42 | - name: Test with tox 43 | run: | 44 | pyversion="${{ matrix.python-version }}" 45 | pdm run tox -e py${pyversion/./} 46 | - name: Upload coverage to Codecov 47 | uses: codecov/codecov-action@v2 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | file: ./coverage.xml 51 | 52 | notebooks: 53 | name: Run the notebooks on Python ${{ matrix.python-version }} 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | python-version: ["3.9", "3.10", "3.11"] 58 | fail-fast: false 59 | steps: 60 | - name: Check out the repository 61 | uses: actions/checkout@v3 62 | - name: Set up Python ${{ matrix.python-version }} 63 | uses: pdm-project/setup-pdm@v3 64 | with: 65 | python-version: ${{ matrix.python-version }} 66 | cache: true 67 | - name: Install dependencies 68 | run: pdm install --prod -G docs 69 | 70 | - name: Run notebooks test 71 | run: pdm notebooks 72 | 73 | lint: 74 | name: Lint the code 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - name: Check out the repository 79 | uses: actions/checkout@v3 80 | - name: Set up Python 3.10 81 | uses: pdm-project/setup-pdm@v3 82 | with: 83 | python-version: "3.10" 84 | cache: true 85 | - name: Install dependencies 86 | run: pdm install -G ci 87 | - name: Test with tox 88 | run: pdm run tox -e lint 89 | 90 | pypi-build-and-upload: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Checkout source 94 | uses: actions/checkout@v3 95 | - uses: pdm-project/setup-pdm@v3 96 | name: Setup PDM 97 | with: 98 | python-version: "3.10" 99 | cache: true 100 | - name: Build 101 | run: pdm build 102 | - name: Publish 103 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 104 | uses: pypa/gh-action-pypi-publish@v1.6.4 105 | with: 106 | user: __token__ 107 | password: ${{ secrets.PYPI_TOKEN }} 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at bryan.w.weber@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThermoState 2 | 3 | This package provides a wrapper around [CoolProp](https://github.com/CoolProp/CoolProp) that integrates [Pint](https://pint.readthedocs.io) for easy thermodynamic state management in any unit system. 4 | 5 | ## Installation 6 | 7 | ### Conda 8 | 9 | The preferred installation method is to use [conda](https://anaconda.com/download). Using Conda, ThermoState can be installed for Python 3.9 and up. You can create a new environment with: 10 | 11 | ```shell 12 | conda create -n thermostate -c conda-forge thermostate 13 | ``` 14 | 15 | ### Pip 16 | 17 | Alternatively, ThermoState can be installed with `pip`. 18 | 19 | ```shell 20 | python -m pip install thermostate 21 | ``` 22 | 23 | ### From Source 24 | 25 | ThermoState is a pure-Python package that supports any Python version 3.9 and higher. To install from source, clone the source code repository and install using `pip`. 26 | 27 | ```shell 28 | git clone https://github.com/bryanwweber/thermostate 29 | cd thermostate 30 | python -m pip install . 31 | ``` 32 | 33 | ## Documentation 34 | 35 | Documentation can be found at . The documentation contains a short [tutorial](https://thermostate.readthedocs.io/en/stable/Tutorial.html), [examples](https://thermostate.readthedocs.io/en/stable/examples.html), and [API documentation](https://thermostate.readthedocs.io/en/stable/thermostate.html) for the package. 36 | 37 | [![Documentation Status](https://readthedocs.org/projects/thermostate/badge/?version=stable)](https://thermostate.readthedocs.io/en/stable/?badge=stable) 38 | 39 | ## Citation 40 | 41 | If you have used ThermoState in your work, we would appreciate including a citation to the software! ThermoState has been published in [JOSE](https://jose.theoj.org/), available at the link below. 42 | 43 | [![DOI](https://jose.theoj.org/papers/10.21105/jose.00033/status.svg)](https://doi.org/10.21105/jose.00033) 44 | 45 | For those using Bib(La)TeX, you can use the following entry 46 | 47 | ```bibtex 48 | @article{weber_thermostate_2018, 49 | title = {{ThermoState}: {A} state manager for thermodynamics courses}, 50 | volume = {1}, 51 | issn = {2577-3569}, 52 | shorttitle = {{ThermoState}}, 53 | url = {https://jose.theoj.org/papers/10.21105/jose.00033}, 54 | doi = {10.21105/jose.00033}, 55 | number = {8}, 56 | urldate = {2018-10-24}, 57 | journal = {Journal of Open Source Education}, 58 | author = {Weber, Bryan}, 59 | month = oct, 60 | year = {2018}, 61 | pages = {33} 62 | } 63 | ``` 64 | 65 | ## Code of Conduct & Contributing 66 | 67 | We welcome contributions from anyone in the community. Please look at the [Contributing instructions](https://github.com/bryanwweber/thermostate/blob/master/CONTRIBUTING.md) for more information. This project follows the [Contributor Covenant Code of Conduct](https://github.com/bryanwweber/thermostate/blob/master/CODE_OF_CONDUCT.md), version 1.4\. In short, be excellent to each other. 68 | 69 | ## Continuous Integration Status 70 | 71 | [![codecov](https://codecov.io/gh/bryanwweber/thermostate/branch/master/graph/badge.svg)](https://codecov.io/gh/bryanwweber/thermostate)[![Python package](https://github.com/bryanwweber/thermostate/actions/workflows/pythonpackage.yml/badge.svg)](https://github.com/bryanwweber/thermostate/actions/workflows/pythonpackage.yml) 72 | 73 | ## Anaconda Package Version 74 | 75 | [![Anaconda-Server Badge Version](https://anaconda.org/conda-forge/thermostate/badges/version.svg)](https://anaconda.org/conda-forge/thermostate) [![Anaconda-Server Badge Downloads](https://anaconda.org/conda-forge/thermostate/badges/downloads.svg)](https://anaconda.org/conda-forge/thermostate) 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | *.nbconvert.ipynb 163 | 164 | docs/CHANGELOG.md 165 | docs/CONTRIBUTING.md 166 | docs/CODE_OF_CONDUCT.md 167 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # thermostate documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jan 10 10:16:52 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import datetime 17 | import shutil 18 | 19 | from importlib import metadata 20 | 21 | shutil.copy2("../CHANGELOG.md", "CHANGELOG.md") 22 | shutil.copy2("../CODE_OF_CONDUCT.md", "CODE_OF_CONDUCT.md") 23 | shutil.copy2("../CONTRIBUTING.md", "CONTRIBUTING.md") 24 | 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.intersphinx", 38 | "sphinx.ext.napoleon", 39 | "sphinx.ext.mathjax", 40 | "nbsphinx", 41 | "recommonmark", 42 | ] 43 | 44 | # add_function_parentheses = False 45 | autodoc_default_options = {"members": True} 46 | # autodoc_member_order = 'bysource' 47 | autoclass_content = "class" 48 | napoleon_numpy_docstring = True 49 | napoleon_google_docstring = False 50 | nbsphinx_allow_errors = True 51 | nbsphinx_execute = "always" 52 | intersphinx_mapping = { 53 | "python": ("https://docs.python.org/3", None), 54 | "pint": ("https://pint.readthedocs.io/en/latest/", None), 55 | } 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ["_templates"] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | # source_suffix = ['.rst', '.md'] 64 | source_suffix = { 65 | ".rst": "restructuredtext", 66 | ".md": "markdown", 67 | } 68 | 69 | # The master toctree document. 70 | master_doc = "index" 71 | 72 | # General information about the project. 73 | project = "thermostate" 74 | author = "Bryan W. Weber" 75 | this_year = datetime.date.today().year 76 | copyright = "{}, {}".format(this_year, author) 77 | 78 | # The version info for the project you're documenting, acts as replacement for 79 | # |version| and |release|, also used in various other places throughout the 80 | # built documents. 81 | # 82 | # The full version, including alpha/beta/rc tags. 83 | release = metadata.version("thermostate") 84 | # The short X.Y version. 85 | version = ".".join(release.split(".")[:1]) 86 | 87 | # The language for content autogenerated by Sphinx. Refer to documentation 88 | # for a list of supported languages. 89 | # 90 | # This is also used if you do content translation via gettext catalogs. 91 | # Usually you set "language" from the command line for these cases. 92 | language = "en" 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This patterns also effect to html_static_path and html_extra_path 97 | exclude_patterns = [ 98 | "_build", 99 | "Thumbs.db", 100 | ".DS_Store", 101 | ".ipynb_checkpoints", 102 | "*.nbconvert.ipynb", 103 | ] 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = "sphinx" 107 | 108 | # The reST default role (used for this markup: `text`) to use for all 109 | # documents. 110 | default_role = "py:obj" 111 | 112 | # If true, `todo` and `todoList` produce output, else they produce nothing. 113 | todo_include_todos = False 114 | 115 | 116 | # -- Options for HTML output ---------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | # 121 | html_theme = "alabaster" 122 | 123 | # Theme options are theme-specific and customize the look and feel of a theme 124 | # further. For a list of options available for each theme, see the 125 | # documentation. 126 | html_theme_options = { 127 | "github_user": "bryanwweber", 128 | "github_repo": "thermostate", 129 | "github_banner": True, 130 | "github_button": True, 131 | "show_powered_by": True, 132 | } 133 | 134 | # Add any paths that contain custom static files (such as style sheets) here, 135 | # relative to this directory. They are copied after the builtin static files, 136 | # so a file named "default.css" will overwrite the builtin "default.css". 137 | html_static_path = ["_static"] 138 | 139 | 140 | # -- Options for HTMLHelp output ------------------------------------------ 141 | 142 | # Output file base name for HTML help builder. 143 | htmlhelp_basename = "thermostatedoc" 144 | 145 | 146 | # -- Options for LaTeX output --------------------------------------------- 147 | 148 | latex_elements = { 149 | # The paper size ('letterpaper' or 'a4paper'). 150 | # 151 | # 'papersize': 'letterpaper', 152 | # The font size ('10pt', '11pt' or '12pt'). 153 | # 154 | # 'pointsize': '10pt', 155 | # Additional stuff for the LaTeX preamble. 156 | # 157 | # 'preamble': '', 158 | # Latex figure (float) alignment 159 | # 160 | # 'figure_align': 'htbp', 161 | } 162 | 163 | # Grouping the document tree into LaTeX files. List of tuples 164 | # (source start file, target name, title, 165 | # author, documentclass [howto, manual, or own class]). 166 | latex_documents = [ 167 | ( 168 | master_doc, 169 | "thermostate.tex", 170 | "thermostate Documentation", 171 | "Bryan W. Weber", 172 | "manual", 173 | ), 174 | ] 175 | 176 | 177 | # -- Options for manual page output --------------------------------------- 178 | 179 | # One entry per manual page. List of tuples 180 | # (source start file, name, description, authors, manual section). 181 | man_pages = [(master_doc, "thermostate", "thermostate Documentation", [author], 1)] 182 | 183 | 184 | # -- Options for Texinfo output ------------------------------------------- 185 | 186 | # Grouping the document tree into Texinfo files. List of tuples 187 | # (source start file, target name, title, author, 188 | # dir menu entry, description, category) 189 | texinfo_documents = [ 190 | ( 191 | master_doc, 192 | "thermostate", 193 | "thermostate Documentation", 194 | author, 195 | "thermostate", 196 | "One line description of project.", 197 | "Miscellaneous", 198 | ), 199 | ] 200 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ThermoState: A state manager for thermodynamics courses" 3 | tags: 4 | - thermodynamics 5 | - engineering 6 | - coolprop 7 | - pint 8 | authors: 9 | - name: Bryan W. Weber 10 | ORCID: 0000-0003-0815-9270 11 | affiliation: 1 12 | affiliations: 13 | - name: Department of Mechanical Engineering, University of Connecticut, Storrs, CT USA 06269 14 | index: 1 15 | date: 21 September 2018 16 | bibliography: paper.bib 17 | --- 18 | 19 | # Summary 20 | 21 | ThermoState is a Python package that provides easy management of thermodynamic states of simple 22 | compressible systems. ThermoState relies on CoolProp [@coolprop; @cp-article] and Pint [@pint] to 23 | provide the equations of state and units handling, respectively. ThermoState replaces tables that 24 | are typically used in engineering courses to evaluate properties when solving for the behavior of 25 | systems. 26 | 27 | # Statement of Need 28 | 29 | In traditional engineering thermodynamics courses, properties of simple compressible systems (such 30 | as the temperature, pressure, specific volume, specific enthalpy, etc.) are evaluated from tabulated 31 | data. This process often involves an inordinate number of arithmetic calculations, simply to 32 | determine the appropriate properties at a given state. The length of the simple calculations 33 | inhibits students' ability to recognize patterns in problems; they get lost in the calculations, and 34 | fail to see the larger point of a problem. Aside from calculations from a table, students also often 35 | get stuck performing unit conversions, especially in the so-called "English Engineering" (EE) units 36 | system. 37 | 38 | Therefore, there is a need for a software package that simplifies or reduces the number of rote 39 | calculations the students must accomplish and removes the burden of improper unit conversion. 40 | Existing software packages that solve the equation of state for a substance have APIs that are 41 | confusing for students who are learning not only thermodynamics, but Python as well. In addition, 42 | packages such as CoolProp [@coolprop; @cp-article] do not have a facility to automatically manage 43 | units and require all quantities to be in base SI units, which is inconvenient for problems working 44 | with EE units. 45 | 46 | ThermoState combines the CoolProp [@coolprop; @cp-article] and Pint [@pint] packages to enable easy 47 | evaluation of equations of state without needing tables as well as automatic unit conversion. In 48 | addition, ThermoState has an easy to understand API that uses instance attributes to store the 49 | properties of interest. 50 | 51 | Because of the simplicity of use and because students do not have to perform trivial arithmetic, 52 | ThermoState also enables students to engage in higher levels of learning by evaluating the 53 | performance of systems for a range of input parameters. Packages such as NumPy [@numpy] and 54 | Matplotlib [@matplotlib] can be used to generate ranges of input parameters and plot the output. 55 | Students can interpret the plots and understand the behavior of systems for a range of input values. 56 | This type of analysis would not be possible with traditional table-based techniques because of the 57 | sheer number of calculations required. 58 | 59 | # Functionality and Usage 60 | 61 | The primary interface for students to the ThermoState package is the `State` class. The `State` 62 | class constructor takes one mandatory argument, the name of the substance, and two optional keyword 63 | arguments, which must be a pair of independent, intensive properties. Two independent, intensive 64 | properties are required to solve the equation of state of the substance. These keyword arguments 65 | must be instances of the `Quantity` class from Pint [@pint], and must have the appropriate 66 | dimensions for the property being set. 67 | 68 | ```python 69 | from thermostate import State, Q_ 70 | T_1 = Q_(100.0, 'degC') 71 | p_1 = Q_(0.5, 'bar') 72 | st_1a = State('water', T=T_1, p=p_1) 73 | ``` 74 | 75 | Alternatively, only the name of the substance can be specified, and the state can be fixed by 76 | assigning a value to an instance attribute representing the appropriate pair of properties. 77 | 78 | ```python 79 | st_1b = State('water') 80 | st_1b.Tp = T_1, p_1 81 | ``` 82 | 83 | Once the instance of the `State` class is created, the properties of the substance are available as 84 | attributes of the instance. These instance attributes are themselves instances of the `Quantity` 85 | class from Pint [@pint]. Internally, the instance attributes are always in SI base units, to 86 | simplify passing arguments to the appropriate CoolProp [@coolprop; @cp-article] functions. However, 87 | the `Quantity` class has a `to` method that converts the units of the quantity, allowing the 88 | students to use whatever units are natural for a problem. 89 | 90 | ```python 91 | v_1 = st_1a.v.to('m**3/kg') 92 | u_1 = st_1a.u.to('BTU/lb') 93 | ``` 94 | 95 | Finally, Pint [@pint] `Quantity` instances can be used in arithmetic statements in Python, and 96 | provided that the statement is dimensionally consistent, can be easily used to calculate, e.g., the 97 | work and heat transfer during a process. 98 | 99 | # Recent Uses 100 | 101 | I originally wrote ThermoState to use in my Applied Thermodynamics course in the Spring 2016 102 | semester at the University of Connecticut. This course covers the major thermodynamic cycles 103 | (Rankine, Brayton, Refrigeration, Otto, Diesel, etc.). Having the students use ThermoState to 104 | evaluate properties allowed me to assign problems that would have been impossible without some 105 | software assistance. In particular, I expect the students to produce 2-3 design reports of systems 106 | that implement one or more of the previously mentioned cycles, and I expect students to perform some 107 | level of analysis of the cycle with respect to varying the input parameters. 108 | 109 | Since then, I have used ThermoState in two other Applied Thermodynamics courses (Spring 2017 and 110 | 2018). I have also used ThermoState in my Thermodynamic Principles course, a prerequisite for 111 | Applied Thermodynamics. In all the courses, students had positive feedback about the use of 112 | ThermoState, strongly preferring using the software to using tables. 113 | 114 | To avoid having to have students install any software on their personal computers, I use a 115 | JupyterHub instance hosted by the University where students can log in and work on their homework 116 | and projects. Using Jupyter Notebooks [@jupyter_notebook], students can combine their code to solve 117 | the problem with Markdown and equations that explains their process. 118 | 119 | A tutorial on the basic use of the package and several examples can be found in the `docs` folder 120 | of the [repository](https://github.com/bryanwweber/thermostate) as well as the 121 | [online documentation](https://bryanwweber.github.io/thermostate). 122 | 123 | # References 124 | -------------------------------------------------------------------------------- /docs/cold-air-brayton-cycle-example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Cold-Air Standard Brayton Cycle Example\n", 8 | "\n", 9 | "## Imports" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from thermostate import State, Q_, units\n", 19 | "import numpy as np\n", 20 | "\n", 21 | "%matplotlib inline\n", 22 | "import matplotlib.pyplot as plt" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "---\n", 30 | "\n", 31 | "## Definitions" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "substance = \"air\"\n", 41 | "p_1 = Q_(1.0, \"bar\")\n", 42 | "T_1 = Q_(300.0, \"K\")\n", 43 | "mdot = Q_(6.0, \"kg/s\")\n", 44 | "T_3 = Q_(1400.0, \"K\")\n", 45 | "p2_p1 = Q_(10.0, \"dimensionless\")\n", 46 | "T_3_low = Q_(1000.0, \"K\")\n", 47 | "T_3_high = Q_(1800.0, \"K\")" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "---\n", 55 | "\n", 56 | "## Problem Statement" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "An ideal **cold** air-standard Brayton cycle operates at steady state with compressor inlet conditions of 300.0 kelvin and 1.0 bar and a fixed turbine inlet temperature of 1400.0 kelvin and a compressor pressure ratio of 10.0 dimensionless. The mass flow rate of the air is 6.0 kilogram / second. For the cycle,\n", 64 | "\n", 65 | "1. determine the back work ratio\n", 66 | "2. determine the net power output, in kW\n", 67 | "3. determine the thermal efficiency\n", 68 | "4. plot the net power output, in kW, and the thermal efficiency, as a function of the turbine inlet temperature from 1000.0 kelvin to 1800.0 kelvin. Discuss any trends you find." 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "---\n", 76 | "\n", 77 | "## Solution" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "### 1. the back work ratio" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "In the ideal Brayton cycle, work occurs in the isentropic compression and expansion. Therefore, the works are\n", 92 | "\n", 93 | "$$\n", 94 | "\\begin{aligned}\n", 95 | "\\frac{\\dot{W}_c}{\\dot{m}} &= h_1 - h_2 = c_p(T_1 - T_2) & \\frac{\\dot{W}_t}{\\dot{m}} &= h_3 - h_4 = c_p(T_3 - T_4)\n", 96 | "\\end{aligned}\n", 97 | "$$\n", 98 | "\n", 99 | "First, fixing the four states using a cold air-standard analysis" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "st_amb = State(substance, T=T_1, p=p_1)\n", 109 | "c_v = st_amb.cv\n", 110 | "c_p = st_amb.cp\n", 111 | "k = c_p / c_v\n", 112 | "\n", 113 | "T_2 = T_1 * p2_p1 ** ((k - 1) / k)\n", 114 | "p_2 = p2_p1 * p_1\n", 115 | "\n", 116 | "p_3 = p_2\n", 117 | "\n", 118 | "p_4 = p_1\n", 119 | "T_4 = T_3 * (p_4 / p_3) ** ((k - 1) / k)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "Summarizing the states,\n", 127 | "\n", 128 | "| State | T | p |\n", 129 | "|-------|---------------------------|---------------------------|\n", 130 | "| 1 | 300.00 K | 1.00 bar |\n", 131 | "| 2 | 580.34 K | 10.00 bar |\n", 132 | "| 3 | 1400.00 K | 10.00 bar |\n", 133 | "| 4 | 723.71 K | 1.00 bar |\n", 134 | "\n", 135 | "Then, the back work ratio can be found by" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "Wdot_c = (mdot * c_p * (T_1 - T_2)).to(\"kW\")\n", 145 | "Wdot_t = (mdot * c_p * (T_3 - T_4)).to(\"kW\")\n", 146 | "bwr = abs(Wdot_c) / Wdot_t" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "
\n", 154 | "\n", 155 | "**Answer:** The power outputs are $\\dot{W}_c =$ -1692.75 kW, $\\dot{W}_t =$ 4083.53 kW, and the back work ratio is $\\mathrm{bwr} =$ 0.41 = 41.45%\n", 156 | "\n", 157 | "
" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "### 2. the net power output" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "Wdot_net = Wdot_c + Wdot_t" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "
\n", 181 | "\n", 182 | "**Answer:** The net power output is $\\dot{W}_{net} =$ 2390.78 kW\n", 183 | "\n", 184 | "
" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "### 3. the thermal efficiency" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "Qdot_23 = (mdot * c_p * (T_3 - T_2)).to(\"kW\")\n", 201 | "eta = Wdot_net / Qdot_23" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "
\n", 209 | "\n", 210 | "**Answer:** The thermal efficiency is $\\eta =$ 0.48 = 48.31%\n", 211 | "\n", 212 | "
" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": {}, 218 | "source": [ 219 | "### 4. plot the net power output and thermal efficiency" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": null, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "T_range = np.linspace(T_3_low, T_3_high, 200)\n", 229 | "Wdot_net_l = np.zeros(T_range.shape) * units.kW\n", 230 | "eta_l = np.zeros(T_range.shape) * units.dimensionless\n", 231 | "for i, T_3 in enumerate(T_range):\n", 232 | " T_4 = T_3 * (p_4 / p_3) ** ((k - 1) / k)\n", 233 | " Wdot_t = (mdot * c_p * (T_3 - T_4)).to(\"kW\")\n", 234 | " Wdot_net = Wdot_c + Wdot_t\n", 235 | " Wdot_net_l[i] = Wdot_net\n", 236 | "\n", 237 | " Qdot_23 = (mdot * c_p * (T_3 - T_2)).to(\"kW\")\n", 238 | " eta = Wdot_net / Qdot_23\n", 239 | " eta_l[i] = eta" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "fig, power_ax = plt.subplots()\n", 249 | "power_ax.plot(T_range, Wdot_net_l, label=\"Net power output\", color=\"C0\")\n", 250 | "eta_ax = power_ax.twinx()\n", 251 | "eta_ax.plot(T_range, eta_l, label=\"Thermal efficiency\", color=\"C1\")\n", 252 | "power_ax.set_xlabel(\"Turbine Inlet Temperature (K)\")\n", 253 | "power_ax.set_ylabel(\"Net power output (kW)\")\n", 254 | "eta_ax.set_ylabel(\"Thermal efficiency\")\n", 255 | "lines, labels = power_ax.get_legend_handles_labels()\n", 256 | "lines2, labels2 = eta_ax.get_legend_handles_labels()\n", 257 | "power_ax.legend(lines + lines2, labels + labels2, loc=\"best\");" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "From this graph, we note that for a fixed compressor pressure ratio, the thermal efficiency is constant, while the net power output increases with increasing turbine temperature." 265 | ] 266 | } 267 | ], 268 | "metadata": { 269 | "anaconda-cloud": {}, 270 | "kernelspec": { 271 | "display_name": "Python 3", 272 | "language": "python", 273 | "name": "python3" 274 | }, 275 | "language_info": { 276 | "codemirror_mode": { 277 | "name": "ipython", 278 | "version": 3 279 | }, 280 | "file_extension": ".py", 281 | "mimetype": "text/x-python", 282 | "name": "python", 283 | "nbconvert_exporter": "python", 284 | "pygments_lexer": "ipython3", 285 | "version": "3.9.2" 286 | } 287 | }, 288 | "nbformat": 4, 289 | "nbformat_minor": 2 290 | } 291 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | ## [2.0.0] - 12-FEB-2023 9 | ### Added 10 | - Builds for Python 3.11 11 | 12 | ### Changed 13 | - Switched to using pdm for dependency and build management 14 | 15 | ### Fixed 16 | - Support pint >=0.20 by updating a few imports 17 | 18 | ### Removed 19 | - The GitHub Actions docs job, which now runs directly on readthedocs for each PR 20 | 21 | ## [1.4.0] - 11-FEB-2023 22 | ### Changed 23 | - Capped the version of pint. We don't support 0.20 with this version due to a missing module. 24 | 25 | ## [1.3.0] - 14-MAR-2022 26 | ### Added 27 | - Plots! 28 | - Python 3.10 support 29 | - Default units can now be specified for `State` instances 30 | 31 | ### Changed 32 | - Python >= 3.9 requires CoolProp from their source repository 33 | 34 | ## [1.2.1] - 21-JUL-2020 35 | ### Changed 36 | - Allow Pint up to 1.0, they seem to be pretty stable between minor version releases 37 | 38 | ### Fixed 39 | - Typo in pythonpackage.yml 40 | 41 | ## [1.2.0] - 14-JUL-2020 42 | ### Added 43 | - Build CoolProp from the master branch to avoid any regressions 44 | - Cache the built CoolProp wheel, based on the CoolProp master commit hash 45 | 46 | ### Changed 47 | - CoolProp 6.4.0 was released which supports Python 3.8 with their built wheels. Move the tests for Python 3.8 to the main test build. 48 | - The default branch is now called `main`. 49 | 50 | ### Fixed 51 | - Bump the `MACOSX_DEPLOYMENT_TARGET` for GitHub Actions, seems like they moved to 10.14 52 | - Bump Pint version in the Conda recipe 53 | - Add Matplotlib as a dependency in the Conda recipe 54 | 55 | ## [1.1.0] - 12-APR-2020 56 | ### Added 57 | - Build CoolProp and run the tests on Python 3.8 58 | - Set up the Matplotlib functionality built into Pint. This bumps the minimum Pint version to 0.9 and adds Matplotlib as a dependency 59 | 60 | ### Changed 61 | - Updated documentation links in README and conda recipe to ReadTheDocs 62 | 63 | ### Fixed 64 | - The Rankine cycle example had a dimensionality error due to better NumPy support in Pint. Fixes #24. 65 | 66 | ## [1.0.0] - 03-MAR-2020 67 | ### Added 68 | - Switch to ReadTheDocs for documentation website 69 | - Use `setup.cfg` and `pyproject.toml` for PEP 517 compliance 70 | 71 | ### Changed 72 | - Switch to `src` directory source layout 73 | - Move tests outside of the package 74 | - Apply Black formatter to tests 75 | - Use tox to test against multiple Python versions 76 | - Use GitHub Actions for CI services 77 | - Run Black formatter on `abbreviations.py` and `_version.py` 78 | - License year in `LICENSE.md`. Happy New Year :tada: 79 | 80 | ### Fixed 81 | - README.md and CHANGELOG.md are now included in the sdist 82 | - `hx` and `xh` are added to the disallowed property pairs because they raise `ValueError`s in CoolProp 83 | - Missing docstrings from some functions in `thermostate.py` 84 | 85 | ## [0.5.3] - 04-MAR-2019 86 | ### Added 87 | - Check if temperature, pressure, and specific volume are positive (in absolute units) 88 | - Check if the quality is between 0 and 1 89 | 90 | ### Changed 91 | - Bump maximum allowed version of Pint 92 | 93 | ## [0.5.2] - 01-FEB-2019 94 | ### Added 95 | - Install `conda-verify` on Travis when building tags to fix a warning from `conda-build` 96 | 97 | ### Changed 98 | - Formatted `thermostate.py` with the Black formatter 99 | 100 | ### Fixed 101 | - Broken link in `CONTRIBUTING.md` to `LICENSE.md` 102 | - Installation instructions for CoolProp updated for Python 3.7 103 | - Equality checking for `State`s now considers the substance [[#17](https://github.com/bryanwweber/thermostate/pull/17)]. Resolves [#16](https://github.com/bryanwweber/thermostate/issues/16) (Thanks [@egurra](https://github.com/egurra)!) 104 | 105 | ## [0.5.1] - 05-JAN-2019 106 | ### Added 107 | - JOSE badge to README 108 | 109 | ### Changed 110 | - Allow version 6.2.* of CoolProp 111 | - Install CoolProp package for Python 3.7 from conda 112 | 113 | ### Fixed 114 | - License year in LICENSE.md. Happy new year! :tada: 115 | 116 | ## [0.5.0] - 23-OCT-2018 117 | ### Added 118 | - Add JOSE paper 119 | - Add installation, documentation, code of conduct, and contributing links to README 120 | - Document the classes in the `abbreviations` module 121 | - Example of a cascade refrigeration cycle using EE units 122 | - Test on Python 3.7 using the nightly version of CoolProp 123 | 124 | ### Changed 125 | - Use the generic Python 3 for the intersphinx config rather than version specific 126 | 127 | ### Fixed 128 | - Fix numpy and matplotlib need to be installed on Travis to build the docs 129 | - Fix typo in code of conduct 130 | 131 | ### Removed 132 | - Don't load the Sphinx coverage extensions 133 | 134 | ## [0.4.2] - 21-SEP-2018 135 | ### Fixed 136 | - Travis PyPI password 137 | 138 | ## [0.4.1] - 21-SEP-2018 139 | ### Added 140 | - Add codemeta.json 141 | 142 | ### Fixed 143 | - Fix builds in .travis.yml 144 | - Can't use Python 3.6 type hinting with Python 3.5 145 | 146 | ## [0.4.0] - 21-SEP-2018 147 | ### Added 148 | - `_render_traceback_` function added to `StateError` to improve formatting of the traceback in IPython and Jupyter 149 | - Add several examples demonstrating the use of ThermoState 150 | 151 | ### Changed 152 | - Bump intersphinx mapping to Python 3.7 153 | - Change docs license to CC-BY 4.0 154 | 155 | ### Fixed 156 | - Ignore more pytest files 157 | 158 | ## [0.3.0] - 09-JUL-2018 159 | ### Fixed 160 | - Added flake8 configuration to setup.cfg since linter-flake8 reads it and ignores built-in options 161 | - Only define `_render_traceback_` if IPython is installed 162 | 163 | ## [0.2.4] - 08-JUL-2018 164 | ### Added 165 | - Added `_render_traceback_` function to improve traceback formatting of `pint.DimensionalityError` 166 | 167 | ### Fixed 168 | - Added `oxygen`, `nitrogen`, and `carbondioxide` as available substances to the Tutorial 169 | 170 | ## [0.2.3] - 24-SEP-2017 171 | ### Added 172 | - Distributions are now uploaded to PyPI 173 | 174 | ### Changed 175 | - Conda packages are `noarch` builds 176 | - Appveyor tests run in a single job to speed them up 177 | - Minimum Python version is 3.5 178 | 179 | ## [0.2.2] - 13-APR-2017 180 | ### Added 181 | - Oxygen (O2) is available as a substance 182 | - Nitrogen (N2) is available as a substance 183 | 184 | ### Fixed 185 | - Deploy doctr to the root directory (see [drdoctr/doctr#157](https://github.com/drdoctr/doctr/issues/157) and [drdoctr/doctr#160](https://github.com/drdoctr/doctr/issues/160)) 186 | 187 | ## [0.2.1] 188 | ### Added 189 | - Carbon dioxide is available as a substance 190 | - The software version is available as the module-level `__version__` attribute 191 | 192 | ## [0.2.0] 193 | ### Added 194 | - Equality comparison of `State` instances 195 | 196 | ### Changed 197 | - Improve several error messages 198 | - Refactor property getting/setting to use less boilerplate code 199 | - Preface all class attributes with `_` 200 | - Refactor `_set_properties` to use CoolProp low-level API 201 | 202 | ## [0.1.7] 203 | ### Added 204 | - Phase as a gettable attribute of the State 205 | - Isobutane is an available substance 206 | - Add cp and cv to Tutorial 207 | 208 | ### Changed 209 | - Updated Tutorial with more detail of setting properties 210 | - Fail Travis when a single command fails 211 | 212 | ## [0.1.6] 213 | ### Added 214 | - Tutorial in the docs using `nbsphinx` for formatting 215 | - Specific heat capacities at constant pressure and volume are now accessible via `cp` and `cv` attributes 216 | 217 | ### Changed 218 | - Offset units are automatically converted to base units in Pint 219 | 220 | ## [0.1.5] 221 | ### Changed 222 | - Unknown property pairs are no longer allowed to be set 223 | 224 | ## [0.1.4] 225 | ### Fixed 226 | - Rename units module to abbreviations so it no longer shadows units registry in thermostate 227 | 228 | ## [0.1.3] 229 | ### Added 230 | - Common unit abbreviations in thermostate.EnglishEngineering and thermostate.SystemInternational 231 | 232 | ### Fixed 233 | - Typo in CHANGELOG.md 234 | 235 | ## [0.1.2] 236 | ### Fixed 237 | - Fix Anaconda.org upload keys 238 | 239 | ## [0.1.1] 240 | ### Fixed 241 | - Only load pytest-runner if tests are being run 242 | 243 | ## [0.1.0] 244 | ### Added 245 | - First Release 246 | 247 | [2.0.0]: https://github.com/bryanwweber/thermostate/compare/v1.4.0...v2.0.0.post1 248 | [1.4.0]: https://github.com/bryanwweber/thermostate/compare/v1.3.0...v1.4.0 249 | [1.3.0]: https://github.com/bryanwweber/thermostate/compare/v1.2.1...v1.3.0 250 | [1.2.1]: https://github.com/bryanwweber/thermostate/compare/v1.2.0...v1.2.1 251 | [1.2.0]: https://github.com/bryanwweber/thermostate/compare/v1.1.0...v1.2.0 252 | [1.1.0]: https://github.com/bryanwweber/thermostate/compare/v1.0.0...v1.1.0 253 | [1.0.0]: https://github.com/bryanwweber/thermostate/compare/v0.5.3...v1.0.0 254 | [0.5.3]: https://github.com/bryanwweber/thermostate/compare/v0.5.2...v0.5.3 255 | [0.5.2]: https://github.com/bryanwweber/thermostate/compare/v0.5.1...v0.5.2 256 | [0.5.1]: https://github.com/bryanwweber/thermostate/compare/v0.5.0...v0.5.1 257 | [0.5.0]: https://github.com/bryanwweber/thermostate/compare/v0.4.2...v0.5.0 258 | [0.4.2]: https://github.com/bryanwweber/thermostate/compare/v0.4.1...v0.4.2 259 | [0.4.1]: https://github.com/bryanwweber/thermostate/compare/v0.4.0...v0.4.1 260 | [0.4.0]: https://github.com/bryanwweber/thermostate/compare/v0.3.0...v0.4.0 261 | [0.3.0]: https://github.com/bryanwweber/thermostate/compare/v0.2.4...v0.3.0 262 | [0.2.4]: https://github.com/bryanwweber/thermostate/compare/v0.2.3...v0.2.4 263 | [0.2.3]: https://github.com/bryanwweber/thermostate/compare/v0.2.2...v0.2.3 264 | [0.2.2]: https://github.com/bryanwweber/thermostate/compare/v0.2.1...v0.2.2 265 | [0.2.1]: https://github.com/bryanwweber/thermostate/compare/v0.2.0...v0.2.1 266 | [0.2.0]: https://github.com/bryanwweber/thermostate/compare/v0.1.7...v0.2.0 267 | [0.1.7]: https://github.com/bryanwweber/thermostate/compare/v0.1.6...v0.1.7 268 | [0.1.6]: https://github.com/bryanwweber/thermostate/compare/v0.1.5...v0.1.6 269 | [0.1.5]: https://github.com/bryanwweber/thermostate/compare/v0.1.4...v0.1.5 270 | [0.1.4]: https://github.com/bryanwweber/thermostate/compare/v0.1.3...v0.1.4 271 | [0.1.3]: https://github.com/bryanwweber/thermostate/compare/v0.1.2...v0.1.3 272 | [0.1.2]: https://github.com/bryanwweber/thermostate/compare/v0.1.1...v0.1.2 273 | [0.1.1]: https://github.com/bryanwweber/thermostate/compare/v0.1.0...v0.1.1 274 | [0.1.0]: https://github.com/bryanwweber/thermostate/compare/491975d84317abdaf289c01be02567ab33bbc390...v0.1.0 275 | -------------------------------------------------------------------------------- /tests/test_plotting.py: -------------------------------------------------------------------------------- 1 | """Test module for the plotting code.""" 2 | import numpy as np 3 | import pytest 4 | 5 | from thermostate.plotting import IdealGas, VaporDome 6 | from thermostate.thermostate import State, units 7 | 8 | 9 | def get_vapordome(): 10 | """Return an instance of the VaporDome Class.""" 11 | return VaporDome("water", ("v", "T"), ("s", "T")) 12 | 13 | 14 | def test_plot_additon(): 15 | """Test adding a plot.""" 16 | v = VaporDome("CARBONDIOXIDE", ("v", "T"), ("s", "T")) 17 | v.plot("p", "v") 18 | assert ("pv") in v.plots 19 | 20 | 21 | def test_plot_already_added(): 22 | """Test adding a plot that already exists in the instance.""" 23 | v = get_vapordome() 24 | with pytest.raises( 25 | ValueError, match="Plot has already been added to this class instance" 26 | ): 27 | v.plot("v", "T") 28 | 29 | 30 | def test_remove_state_no_input(): 31 | """Test error handling of remove_state function with no input.""" 32 | v = get_vapordome() 33 | with pytest.raises( 34 | ValueError, match="No state or key was entered. Unable to find state" 35 | ): 36 | v.remove_state() 37 | 38 | 39 | def test_remove_state_no_key(): 40 | """Test ability of remove_state function to work with input of the state.""" 41 | v = get_vapordome() 42 | state_3 = State("water", T=500 * units.kelvin, v=1 * units.m**3 / units.kg) 43 | v.add_state(state_3) # test of repr(state) 44 | v.remove_state(state_3) 45 | # assert v.states[repr(state_3)] == None 46 | 47 | 48 | def test_remove_state_key_input(): 49 | """Test ability of remove_state function to work with input of a key.""" 50 | v = get_vapordome() 51 | state_4 = State("water", T=400 * units.kelvin, v=1 * units.m**3 / units.kg) 52 | v.add_state(state_4, key="st4") # test of key 53 | v.remove_state(key="st4") 54 | # assert state_4 not in v.states #fails whether its "in" or "not in". whats a better 55 | # way to define this 56 | 57 | 58 | def test_remove_state_wrong_key_no_state(): 59 | """Test error handling of remove_state function with the wrong key.""" 60 | v = get_vapordome() 61 | state_5 = State("water", T=700 * units.kelvin, v=1 * units.m**3 / units.kg) 62 | v.add_state(state_5, key="st5") # test of wrong key and state = none 63 | with pytest.raises(ValueError, match="Couldn't find key"): 64 | v.remove_state(key="wrong key") 65 | 66 | 67 | def test_remove_state_altered_key(): 68 | """Test ability of remove_state function to work with input of an altered key.""" 69 | v = get_vapordome() 70 | state_6 = State("water", T=700 * units.kelvin, v=0.01 * units.m**3 / units.kg) 71 | v.add_state(state_6, key="st6") # test of state input with an altered key 72 | v.remove_state(state_6) 73 | 74 | 75 | def test_remove_state_state_not_added(): 76 | """Test error handling of remove_state function with the wrong key.""" 77 | v = get_vapordome() 78 | state_7 = State("water", T=400 * units.kelvin, v=0.01 * units.m**3 / units.kg) 79 | with pytest.raises(ValueError, match="Couldn't find the state"): 80 | v.remove_state(state_7) # test of removing a state that was never added 81 | 82 | 83 | def test_remove_process_without_remove_states(): 84 | """Test ability of remove_process function to remove a line but not the states.""" 85 | v = get_vapordome() 86 | state_1 = State( 87 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 88 | ) 89 | state_2 = State( 90 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 91 | ) 92 | v.add_state(state_1) 93 | v.add_state(state_2) 94 | v.add_process(state_1, state_2) 95 | v.remove_process(state_1, state_2, remove_states=False) 96 | # would like to assert if the states are in v.states and if process was 97 | # removed from v.states 98 | 99 | 100 | def test_remove_process_with_remove_states(): 101 | """Test ability of remove_process function to remove a line and the states.""" 102 | v = get_vapordome() 103 | state_1 = State( 104 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 105 | ) 106 | state_2 = State( 107 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 108 | ) 109 | v.add_state(state_1) 110 | v.add_state(state_2) 111 | v.add_process(state_1, state_2) 112 | v.remove_process(state_1, state_2, remove_states=True) 113 | # would like to assert if the states are removed from v.states and if process was 114 | # removed from v.states 115 | 116 | 117 | def test_add_process_states_already_added(): 118 | """Test ability of add_process function when states have been previously added.""" 119 | v = get_vapordome() 120 | state_1 = State( 121 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 122 | ) 123 | state_2 = State( 124 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 125 | ) 126 | v.add_state(state_1) 127 | v.add_state(state_2) 128 | v.add_process(state_1, state_2) 129 | 130 | 131 | def test_add_process_states_not_added(): 132 | """Test ability of add_process function when states have not been added.""" 133 | v = get_vapordome() 134 | state_1 = State( 135 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 136 | ) 137 | state_2 = State( 138 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 139 | ) 140 | v.add_process(state_1, state_2) 141 | 142 | 143 | def test_add_process_substance_match(): 144 | """Test error handling of add_process to catch a mismatch of states.""" 145 | v = get_vapordome() 146 | state_1 = State( 147 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 148 | ) 149 | state_2 = State( 150 | "carbondioxide", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 151 | ) 152 | v.add_state(state_1) 153 | v.add_state(state_2) 154 | with pytest.raises(ValueError, match="Substance of input states do not match"): 155 | v.add_process(state_1, state_2) 156 | 157 | 158 | def test_add_process_isobaric(): 159 | """Test add_process when process_type = isobaric.""" 160 | v = get_vapordome() 161 | state_1 = State("water", p=1500 * units.Pa, s=1.5 * units.kJ / (units.kg * units.K)) 162 | state_2 = State("water", p=3500 * units.Pa, s=3 * units.kJ / (units.K * units.kg)) 163 | state_3 = State("water", p=state_2.p, v=100 * units.m**3 / units.kg) 164 | v.add_state(state_1, key="st_1") 165 | v.add_state(state_2, key="st_2") 166 | v.add_state(state_3, key="st_3") 167 | with pytest.raises(ValueError, match="Property: 'p' was not held constant"): 168 | v.add_process(state_1, state_2, "isobaric") 169 | 170 | v.add_process(state_2, state_3, "isobaric") 171 | line = v.processes["st_2st_3"]["vT"] 172 | v_range = ( 173 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 174 | * units.m**3 175 | / units.kg 176 | ) 177 | assert np.all(np.isclose(line.get_xdata(), v_range)) 178 | 179 | 180 | def test_add_process_isothermal(): 181 | """Test add_process when process_type = isothermal.""" 182 | v = get_vapordome() 183 | state_1 = State( 184 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 185 | ) 186 | state_2 = State( 187 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 188 | ) 189 | state_3 = State("water", T=state_2.T, v=100 * units.m**3 / units.kg) 190 | v.add_state(state_1, key="st_1") 191 | v.add_state(state_2, key="st_2") 192 | v.add_state(state_3, key="st_3") 193 | with pytest.raises(ValueError, match="Property: 'T' was not held constant"): 194 | v.add_process(state_1, state_2, "isothermal") 195 | 196 | v.add_process(state_2, state_3, "isothermal") 197 | line = v.processes["st_2st_3"]["vT"] 198 | v_range = ( 199 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 200 | * units.m**3 201 | / units.kg 202 | ) 203 | assert np.all(np.isclose(line.get_xdata(), v_range)) 204 | 205 | 206 | def test_add_process_isoenergetic(): 207 | """Test add_process when process_type = isoenergetic.""" 208 | v = get_vapordome() 209 | state_1 = State( 210 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 211 | ) 212 | state_2 = State( 213 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 214 | ) 215 | state_3 = State("water", u=state_2.u, v=state_2.v + 5 * units.m**3 / units.kg) 216 | v.add_state(state_1, key="st_1") 217 | v.add_state(state_2, key="st_2") 218 | v.add_state(state_3, key="st_3") 219 | with pytest.raises(ValueError, match="Property: 'u' was not held constant"): 220 | v.add_process(state_1, state_2, "isoenergetic") 221 | 222 | v.add_process(state_2, state_3, "isoenergetic") 223 | line = v.processes["st_2st_3"]["vT"] 224 | v_range = ( 225 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 226 | * units.m**3 227 | / units.kg 228 | ) 229 | assert np.all(np.isclose(line.get_xdata(), v_range)) 230 | 231 | 232 | def test_add_process_isoenthalpic(): 233 | """Test add_process when process_type = isoenthalpic.""" 234 | v = get_vapordome() 235 | state_1 = State( 236 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 237 | ) 238 | state_2 = State( 239 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 240 | ) 241 | state_3 = State("water", h=state_2.h, v=state_2.v + 5 * units.m**3 / units.kg) 242 | v.add_state(state_1, key="st_1") 243 | v.add_state(state_2, key="st_2") 244 | v.add_state(state_3, key="st_3") 245 | with pytest.raises(ValueError, match="Property: 'h' was not held constant"): 246 | v.add_process(state_1, state_2, "isoenthalpic") 247 | 248 | v.add_process(state_2, state_3, "isoenthalpic") 249 | line = v.processes["st_2st_3"]["vT"] 250 | v_range = ( 251 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 252 | * units.m**3 253 | / units.kg 254 | ) 255 | assert np.all(np.isclose(line.get_xdata(), v_range)) 256 | 257 | 258 | def test_add_process_isentropic(): 259 | """Test add_process when process_type = isentropic.""" 260 | v = get_vapordome() 261 | state_1 = State( 262 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 263 | ) 264 | state_2 = State( 265 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 266 | ) 267 | state_3 = State("water", s=state_2.s, T=450 * units.kelvin) 268 | v.add_state(state_1, key="st_1") 269 | v.add_state(state_2, key="st_2") 270 | v.add_state(state_3, key="st_3") 271 | with pytest.raises(ValueError, match="Property: 's' was not held constant"): 272 | v.add_process(state_1, state_2, "isentropic") 273 | 274 | v.add_process(state_2, state_3, "isentropic") 275 | line = v.processes["st_2st_3"]["vT"] 276 | v_range = ( 277 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 278 | * units.m**3 279 | / units.kg 280 | ) 281 | assert np.all(np.isclose(line.get_xdata(), v_range)) 282 | 283 | 284 | def test_add_process_isochoric(): 285 | """Test add_process when process_type = isochoric.""" 286 | v = get_vapordome() 287 | state_1 = State( 288 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 289 | ) 290 | state_2 = State( 291 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 292 | ) 293 | state_3 = State("water", v=state_2.v, T=450 * units.kelvin) 294 | v.add_state(state_1, key="st_1") 295 | v.add_state(state_2, key="st_2") 296 | v.add_state(state_3, key="st_3") 297 | with pytest.raises(ValueError, match="Property: 'v' was not held constant"): 298 | v.add_process(state_1, state_2, "isochoric") 299 | 300 | v.add_process(state_2, state_3, "isochoric") 301 | line = v.processes["st_2st_3"]["vT"] 302 | v_range = ( 303 | np.logspace(np.log10(state_2.v.magnitude), np.log10(state_3.v.magnitude)) 304 | * units.m**3 305 | / units.kg 306 | ) 307 | assert np.all(np.isclose(line.get_xdata(), v_range)) 308 | 309 | 310 | def test_add_process_invalid_process_type(): 311 | """Test error handling of add_process when process_type is not an accepted form.""" 312 | v = get_vapordome() 313 | state_1 = State( 314 | "water", T=300 * units.degC, s=1.5 * units.kJ / (units.kg * units.K) 315 | ) 316 | state_2 = State( 317 | "water", T=300 * units.kelvin, s=3 * units.kJ / (units.K * units.kg) 318 | ) 319 | v.add_state(state_1, key="st_1") 320 | v.add_state(state_2, key="st_2") 321 | with pytest.raises(ValueError, match="Not a supported process type"): 322 | v.add_process(state_1, state_2, "hogwash") 323 | 324 | 325 | def test_IdealGas_plot_additon(): 326 | """Test adding a plot.""" 327 | g = IdealGas(("v", "T"), ("s", "T")) 328 | g.plot("p", "v") 329 | assert ("pv") in g.plots 330 | 331 | 332 | def test_IdealGas_plot_already_added(): 333 | """Test adding a plot that already exists in the instance.""" 334 | g = IdealGas("air", ("v", "T")) 335 | with pytest.raises( 336 | ValueError, match="Plot has already been added to this class instance" 337 | ): 338 | g.plot("v", "T") 339 | 340 | 341 | def test_label_add_state(): 342 | """Test using a label in add_state.""" 343 | vd = VaporDome("water", ("v", "T")) 344 | st_1 = State("water", x=1.0 * units.dimensionless, T=100 * units.degC) 345 | st_2 = State("water", x=0.0 * units.dimensionless, T=100 * units.degC) 346 | assert st_1.label is None 347 | assert st_2.label is None 348 | vd.add_state(st_1, label=1) 349 | vd.add_state(st_2, label="2") 350 | assert st_1.label == "1" 351 | assert st_2.label == "2" 352 | 353 | 354 | def test_label_add_process(): 355 | """Test using label in add_process.""" 356 | vd = VaporDome("water", ("v", "T")) 357 | st_1 = State("water", x=1.0 * units.dimensionless, T=100 * units.degC) 358 | st_2 = State("water", x=0.0 * units.dimensionless, T=100 * units.degC) 359 | assert st_1.label is None 360 | assert st_2.label is None 361 | vd.add_process(st_1, st_2, label_1=1, label_2="2") 362 | assert st_1.label == "1" 363 | assert st_2.label == "2" 364 | 365 | 366 | @pytest.mark.xfail(strict=True) 367 | def test_multiple_processes_with_the_same_states(): 368 | """Test adding multiple processes with the same states. 369 | 370 | This expected failure is because no ValueError is raised. 371 | """ 372 | g = IdealGas("air", ("v", "T")) 373 | state_1 = State("air", T=300 * units.K, s=1.5 * units("kJ/kg/K")) 374 | state_2 = State("air", T=300 * units.K, s=3.0 * units("kJ/kg/K")) 375 | g.add_process(state_1, state_2) 376 | with pytest.raises(ValueError): 377 | g.add_process(state_1, state_2, "isothermal") 378 | with pytest.raises(ValueError): 379 | g.remove_process(state_1, state_2) 380 | -------------------------------------------------------------------------------- /src/thermostate/plotting.py: -------------------------------------------------------------------------------- 1 | """Base Plotting module.""" 2 | from __future__ import annotations 3 | 4 | from abc import ABC, abstractmethod 5 | from dataclasses import dataclass, field 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | from CoolProp.CoolProp import PropsSI 10 | 11 | from . import State, units 12 | 13 | 14 | @dataclass 15 | class PlottedState: 16 | """Data class to efficiently store states in the self.states dictionary.""" 17 | 18 | key: str 19 | state: State 20 | # key: Plot axes string (Tv, pv) 21 | # value: Line2D instance for that plot of the marker for this state 22 | markers: dict = field(default_factory=dict) 23 | 24 | 25 | class PlottingBase(ABC): 26 | """Basic Plotting manager for thermodynamic states. 27 | 28 | Parameters 29 | ---------- 30 | substance : `str` 31 | One of the substances supported by CoolProp 32 | """ 33 | 34 | axis_units = { 35 | "v": "m**3/kg", 36 | "T": "K", 37 | "s": "J/(kg*K)", 38 | "p": "pascal", 39 | "u": "J/kg", 40 | "h": "J/kg", 41 | "x": "dimensionless", 42 | } 43 | 44 | allowed_processes = { 45 | "isochoric": "v", 46 | "isovolumetric": "v", 47 | "isometric": "v", 48 | "isobaric": "p", 49 | "isothermal": "T", 50 | "isoenergetic": "u", 51 | "isoenthalpic": "h", 52 | "isentropic": "s", 53 | } 54 | 55 | def __init__(self, substance: str): 56 | self.states = {} 57 | self.plots = {} 58 | self.processes = {} 59 | 60 | @abstractmethod 61 | def plot(self, x_axis: str, y_axis: str): # pragma: no cover 62 | """Hold the place of a plot function that a child class must establish.""" 63 | pass 64 | 65 | def add_state(self, state: State, key: str | None = None, label: str | None = None): 66 | """Add a state to the self.states dictionary and plot it.""" 67 | if key is None: 68 | key = repr(state) 69 | 70 | if label is not None: 71 | state.label = label 72 | 73 | plotted_state = PlottedState(key=key, state=state) 74 | 75 | for plot_key, value in self.plots.items(): 76 | x_data = [] 77 | y_data = [] 78 | fig, axis = value 79 | x_axis, y_axis = plot_key 80 | x_data.append(getattr(state, x_axis).magnitude) 81 | y_data.append(getattr(state, y_axis).magnitude) 82 | x_data = np.array(x_data) * getattr(units, self.axis_units[x_axis]) 83 | y_data = np.array(y_data) * getattr(units, self.axis_units[y_axis]) 84 | (line,) = axis.plot(x_data, y_data, marker="o") 85 | if state.label is not None: 86 | axis.annotate( 87 | state.label, 88 | (x_data[0], y_data[0]), 89 | textcoords="offset pixels", 90 | xytext=(5, 5), 91 | ) 92 | plotted_state.markers[plot_key] = line 93 | 94 | self.states[key] = plotted_state 95 | 96 | def remove_state(self, state: State | None = None, key: str | None = None): 97 | """Remove a state from the self.states dictionary and plots.""" 98 | if state is None and key is None: 99 | raise ValueError("No state or key was entered. Unable to find state") 100 | if state is not None and repr(state) in self.states: 101 | state_to_be_removed = self.states[repr(state)] 102 | elif key is not None and key in self.states: 103 | state_to_be_removed = self.states[key] 104 | elif key is not None and key not in self.states and state is None: 105 | raise ValueError("Couldn't find key") 106 | else: 107 | for key, s_2 in self.states.items(): 108 | if state == s_2.state: 109 | state_to_be_removed = self.states[key] 110 | break 111 | else: 112 | raise ValueError("Couldn't find the state") 113 | 114 | for line in state_to_be_removed.markers.values(): 115 | line.remove() 116 | del self.states[state_to_be_removed.key] 117 | 118 | def remove_process( 119 | self, state_1: State, state_2: State, remove_states: bool = False 120 | ): 121 | """Remove a process from the self.process dictionary. 122 | 123 | The process to be removed is specified by the states that were used to 124 | initially create the process. It is optional to keep the points associated 125 | with the states while still removing the line object. 126 | 127 | Parameters 128 | ---------- 129 | state_1: `~thermostate.thermostate.State` 130 | The starting state for this process. 131 | state_2: `~thermostate.thermostate.State` 132 | The final state for this process. 133 | remove_states: `bool` 134 | If ``True``, the associated states are removed from the instance. 135 | """ 136 | key_1 = None 137 | key_2 = None 138 | for key, plotted_state in self.states.items(): 139 | if state_1 is plotted_state.state: 140 | key_1 = key 141 | if state_2 is plotted_state.state: 142 | key_2 = key 143 | 144 | for line in self.processes[key_1 + key_2].values(): 145 | line.remove() 146 | del self.processes[key_1 + key_2] 147 | 148 | if remove_states: 149 | self.remove_state(state_1) 150 | self.remove_state(state_2) 151 | 152 | def add_process( 153 | self, 154 | state_1: State, 155 | state_2: State, 156 | process_type: str | None = None, 157 | label_1: str | None = None, 158 | label_2: str | None = None, 159 | ): 160 | """Add a thermodynamic process to the self.process dictionary and plots it. 161 | 162 | A property of the states is held constant and all intermediate states are traced 163 | out in a line between the two states on the graph. The property that is held 164 | constant is specified by the user with the ``process_type`` input. 165 | If no property is to be held constant then a straight line between the 166 | two points is drawn. 167 | 168 | Parameters 169 | ---------- 170 | state_1: `~thermostate.thermostate.State` 171 | The starting state for this process. 172 | state_2: `~thermostate.thermostate.State` 173 | The final state for this process. 174 | process_type: optional, `str` 175 | If given, specifies the property that is held constant during the process. 176 | Must be one of ``"isochoric"``, ``"isovolumetric"``, ``"isobaric"``, 177 | ``"isothermal"``, ``"isoenergetic"``, ``"isoenthalpic"``, 178 | ``"isentropic"``, or ``None``. If not specified, a straight line is drawn 179 | between the states. 180 | label_1: optional, `str` 181 | If given, will be used to label the first state. 182 | label_2: optional, `str` 183 | If given, will be used to label the second state. 184 | """ 185 | if ( 186 | process_type not in self.allowed_processes.keys() 187 | and process_type is not None 188 | ): 189 | raise ValueError( 190 | f"Not a supported process type: '{process_type}.\n" 191 | f"Supported process types are: {list(self.allowed_processes.keys())}" 192 | ) 193 | 194 | if process_type is not None: 195 | constant_prop = self.allowed_processes[process_type] 196 | constant1 = getattr(state_1, constant_prop) 197 | constant2 = getattr(state_2, constant_prop) 198 | if not np.isclose(constant1, constant2): 199 | raise ValueError(f"Property: '{constant_prop}' was not held constant") 200 | 201 | missing_state_1 = True 202 | missing_state_2 = True 203 | key_1 = None 204 | key_2 = None 205 | sub1 = state_1.sub 206 | sub2 = state_2.sub 207 | if sub1 != sub2: 208 | raise ValueError( 209 | f"Substance of input states do not match: '{sub1}', '{sub2}'" 210 | ) 211 | 212 | for key, plotted_state in self.states.items(): 213 | if state_1 is plotted_state.state: 214 | missing_state_1 = False 215 | key_1 = key 216 | if state_2 is plotted_state.state: 217 | missing_state_2 = False 218 | key_2 = key 219 | 220 | if missing_state_1: 221 | key_1 = repr(state_1) 222 | self.add_state(state_1, key_1, label_1) 223 | 224 | if missing_state_2: 225 | key_2 = repr(state_2) 226 | self.add_state(state_2, key_2, label_2) 227 | 228 | plot_key = key_1 + key_2 229 | 230 | self.processes[plot_key] = {} 231 | 232 | if process_type in ("isochoric", "isovolumetric", "isometric"): 233 | p_1 = np.log10(state_1.p.magnitude) 234 | p_2 = np.log10(state_2.p.magnitude) 235 | v_range = np.logspace(p_1, p_2) * units.pascal 236 | elif process_type is not None: 237 | v_1 = np.log10(state_1.v.magnitude) 238 | v_2 = np.log10(state_2.v.magnitude) 239 | # Due to numerical approximation by CoolProp, an error occurs 240 | # if the state is too close to a saturated liquid. Here an 241 | # imperceptibly small offset is introduced to the specific volume 242 | # to avoid this error. 243 | if state_1.x is not None: 244 | if np.isclose(state_1.x.magnitude, 0.0): 245 | v_1 *= 1.0 + 1.0e-14 246 | elif np.isclose(state_1.x.magnitude, 1.0): 247 | v_1 *= 1.0 - 1.0e-12 248 | if state_2.x is not None: 249 | if np.isclose(state_2.x.magnitude, 0.0): 250 | v_2 *= 1.0 + 1.0e-14 251 | elif np.isclose(state_2.x.magnitude, 1.0): 252 | v_2 *= 1.0 - 1.0e-12 253 | v_range = np.logspace(v_1, v_2) * units("m**3/kg") 254 | 255 | for key, value in self.plots.items(): 256 | x_data = [] 257 | y_data = [] 258 | fig, axis = value 259 | x_axis, y_axis = key 260 | 261 | if process_type is None: 262 | x_data.append(getattr(state_1, x_axis).magnitude) 263 | y_data.append(getattr(state_1, y_axis).magnitude) 264 | x_data.append(getattr(state_2, x_axis).magnitude) 265 | y_data.append(getattr(state_2, y_axis).magnitude) 266 | 267 | x_data = np.array(x_data) * getattr(units, self.axis_units[x_axis]) 268 | y_data = np.array(y_data) * getattr(units, self.axis_units[y_axis]) 269 | (line,) = axis.plot(x_data, y_data, marker="None", linestyle="--") 270 | self.processes[plot_key][key] = line 271 | else: 272 | state = State(state_1.sub) 273 | for v in v_range: 274 | if process_type in ("isochoric", "isovolumetric", "isometric"): 275 | state.pv = v, state_1.v 276 | elif process_type == "isobaric": 277 | state.pv = state_1.p, v 278 | elif process_type == "isothermal": 279 | state.Tv = state_1.T, v 280 | elif process_type == "isoenergetic": 281 | state.uv = state_1.u, v 282 | elif process_type == "isoenthalpic": 283 | state.hv = state_1.h, v 284 | elif process_type == "isentropic": 285 | state.sv = state_1.s, v 286 | 287 | x_data.append(getattr(state, x_axis).magnitude) 288 | y_data.append(getattr(state, y_axis).magnitude) 289 | 290 | x_data = np.array(x_data) * getattr(units, self.axis_units[x_axis]) 291 | y_data = np.array(y_data) * getattr(units, self.axis_units[y_axis]) 292 | (line,) = axis.plot(x_data, y_data, linestyle="-") 293 | self.processes[plot_key][key] = line 294 | 295 | def set_xscale(self, x_axis, y_axis, scale="linear"): 296 | """Access a plot in self.plots and change the scale of its x axis.""" 297 | key = x_axis + y_axis 298 | fig, axis = self.plots[key] 299 | axis.set_xscale(scale) 300 | 301 | def set_yscale(self, x_axis, y_axis, scale="linear"): 302 | """Access a plot in self.plots and change the scale of its y axis.""" 303 | key = x_axis + y_axis 304 | fig, axis = self.plots[key] 305 | axis.set_yscale(scale) 306 | 307 | 308 | class VaporDome(PlottingBase): 309 | """Class for plotting graphs with a vapor dome.""" 310 | 311 | def __init__(self, substance, *args): 312 | super().__init__(substance) 313 | min_temp = PropsSI("Tmin", substance) 314 | max_temp = PropsSI("Tcrit", substance) 315 | 316 | T_range = np.logspace(np.log10(min_temp), np.log10(max_temp), 400) * units.K 317 | self.st_f = [State(substance, T=T, x=0 * units.dimensionless) for T in T_range] 318 | self.st_g = [State(substance, T=T, x=1 * units.dimensionless) for T in T_range] 319 | for axes in args: 320 | self.plot(axes[0], axes[1]) 321 | 322 | def plot(self, x_axis, y_axis): 323 | """Add a plot with a vapor dome to this instance with given x and y axes. 324 | 325 | Parameters 326 | ---------- 327 | x_axis: `str` 328 | The string representing the x axis for this plot. Allowed axes are 329 | "T", "p", "u", "s", "v", and "h". 330 | y_axis: `str` 331 | The string representing the y axis for this plot. Allowed axes are 332 | "T", "p", "u", "s", "v", and "h". 333 | """ 334 | if x_axis + y_axis not in self.plots: 335 | fig, axis = plt.subplots() 336 | self.plots[x_axis + y_axis] = (fig, axis) 337 | 338 | x_f = [getattr(st, x_axis).magnitude for st in self.st_f] 339 | x_f = np.array(x_f) * getattr(units, self.axis_units[x_axis]) 340 | y_f = [getattr(st, y_axis).magnitude for st in self.st_f] 341 | y_f = np.array(y_f) * getattr(units, self.axis_units[y_axis]) 342 | axis.plot(x_f, y_f) 343 | 344 | x_g = np.array( 345 | [getattr(st, x_axis).magnitude for st in self.st_g] 346 | ) * getattr(units, self.axis_units[x_axis]) 347 | y_g = np.array( 348 | [getattr(st, y_axis).magnitude for st in self.st_g] 349 | ) * getattr(units, self.axis_units[y_axis]) 350 | axis.plot(x_g, y_g) 351 | if x_axis in ("p", "v"): 352 | self.set_xscale(x_axis, y_axis, "log") 353 | if y_axis in ("p", "v"): 354 | self.set_yscale(x_axis, y_axis, "log") 355 | else: 356 | raise ValueError("Plot has already been added to this class instance") 357 | 358 | 359 | class IdealGas(PlottingBase): 360 | """Class for plotting graphs modeled as an Ideal Gas.""" 361 | 362 | def __init__(self, substance, *args): 363 | super().__init__(substance) 364 | for axes in args: 365 | self.plot(axes[0], axes[1]) 366 | 367 | def plot(self, x_axis, y_axis): 368 | """Add a plot to this instance with given x and y axes. 369 | 370 | Parameters 371 | ----------- 372 | x_axis: `str` 373 | The string representing the x axis for this plot. Allowed axes are 374 | "T", "p", "u", "s", "v", and "h". 375 | y_axis: `str` 376 | The string representing the y axis for this plot. Allowed axes are 377 | "T", "p", "u", "s", "v", and "h". 378 | """ 379 | if x_axis + y_axis not in self.plots: 380 | fig, axis = plt.subplots() 381 | self.plots[x_axis + y_axis] = (fig, axis) 382 | if x_axis in ("p", "v"): 383 | self.set_xscale(x_axis, y_axis, "log") 384 | if y_axis in ("p", "v"): 385 | self.set_yscale(x_axis, y_axis, "log") 386 | else: 387 | raise ValueError("Plot has already been added to this class instance") 388 | -------------------------------------------------------------------------------- /src/thermostate/thermostate.py: -------------------------------------------------------------------------------- 1 | """Base ThermoState module.""" 2 | # Needed to support Python < 3.9 3 | from __future__ import annotations 4 | 5 | import enum 6 | import sys 7 | from collections import OrderedDict 8 | from typing import TYPE_CHECKING 9 | 10 | import CoolProp 11 | import numpy as np 12 | from pint import DimensionalityError, UnitRegistry 13 | from pint.util import UnitsContainer 14 | 15 | from .abbreviations import EnglishEngineering as default_EE 16 | from .abbreviations import SystemInternational as default_SI 17 | 18 | try: # pragma: no cover 19 | from IPython.core.ultratb import AutoFormattedTB 20 | 21 | except ImportError: # pragma: no cover 22 | AutoFormattedTB = None 23 | 24 | if TYPE_CHECKING: # pragma: no cover 25 | from typing import Union 26 | 27 | import pint 28 | 29 | units = UnitRegistry(autoconvert_offset_to_baseunit=True) 30 | Q_ = units.Quantity 31 | units.define("_pct = 0.01 = pct = percent") 32 | units.setup_matplotlib() 33 | 34 | default_units = None 35 | 36 | 37 | def set_default_units(units): 38 | """Set default units to be used in class initialization.""" 39 | if units is None or units in ("SI", "EE"): 40 | global default_units 41 | default_units = units 42 | else: 43 | raise TypeError( 44 | f"The given units '{units!r}' are not supported. Must be 'SI', " 45 | "'EE', or None." 46 | ) 47 | 48 | 49 | # Don't add the _render_traceback_ function to DimensionalityError if 50 | # IPython isn't present. This function is only used by the IPython/ipykernel 51 | # anyways, so it doesn't matter if it's missing if IPython isn't available. 52 | if AutoFormattedTB is not None: # pragma: no cover 53 | 54 | def render_traceback(self: DimensionalityError): 55 | """Render a minimized version of the DimensionalityError traceback. 56 | 57 | The default Jupyter/IPython traceback includes a lot of 58 | context from within pint that actually raises the 59 | DimensionalityError. This context isn't really needed for 60 | this particular error, since the problem is almost certainly in 61 | the user code. This function removes the additional context. 62 | """ 63 | a = AutoFormattedTB( 64 | mode="Context", color_scheme="Neutral", tb_offset=1 65 | ) # type: ignore 66 | etype, evalue, tb = sys.exc_info() 67 | stb = a.structured_traceback(etype, evalue, tb, tb_offset=1) 68 | for i, line in enumerate(stb): 69 | if "site-packages" in line: 70 | first_line = slice(i) 71 | break 72 | else: 73 | # This is deliberately an "else" on the for loop 74 | first_line = slice(-1) 75 | return stb[first_line] + stb[-1:] 76 | 77 | DimensionalityError._render_traceback_ = render_traceback.__get__( # type: ignore 78 | DimensionalityError 79 | ) 80 | 81 | 82 | class CoolPropPhaseNames(enum.Enum): 83 | """Map the phase names in CoolProp.""" 84 | 85 | critical_point = CoolProp.iphase_critical_point 86 | gas = CoolProp.iphase_gas 87 | liquid = CoolProp.iphase_liquid 88 | not_imposed = CoolProp.iphase_not_imposed 89 | supercritical = CoolProp.iphase_supercritical 90 | supercritical_gas = CoolProp.iphase_supercritical_gas 91 | supercritical_liquid = CoolProp.iphase_supercritical_liquid 92 | twophase = CoolProp.iphase_twophase 93 | unknown = CoolProp.iphase_unknown 94 | 95 | 96 | def munge_coolprop_input_prop(prop: str) -> str: 97 | """Munge an input property pair from CoolProp into our format. 98 | 99 | Example CoolProp input: ``XY_INPUTS``, where ``X`` and ``Y`` are one of 100 | ``T``, ``P``, ``Dmass``, ``Hmass``, ``Umass``, ``Q``, or ``Smass``. For 101 | use in ThermoState, we use lower case letters (except for T), replace 102 | ``D`` with ``v``, and replace ``Q`` with ``x``. 103 | 104 | Examples 105 | -------- 106 | * ``DmassHmass_INPUTS``: ``vh`` 107 | * ``DmassT_INPUTS``: ``vT`` 108 | * ``PUmass_INPUTS``: ``pu`` 109 | 110 | """ 111 | prop = prop.replace("_INPUTS", "").replace("mass", "").replace("D", "V") 112 | return prop.replace("Q", "X").lower().replace("t", "T") 113 | 114 | 115 | class StateError(Exception): 116 | """Errors associated with setting the `State` object.""" 117 | 118 | def _render_traceback_(self): # pragma: no cover 119 | """Render a minimized version of the `StateError` traceback. 120 | 121 | The default Jupyter/IPython traceback includes a lot of 122 | context from within `State` where the `StateError` is raised. 123 | This context isn't really needed, since the problem is almost certainly in 124 | the user code. This function removes the additional context. 125 | """ 126 | if AutoFormattedTB is not None: 127 | a = AutoFormattedTB(mode="Context", color_scheme="Neutral", tb_offset=1) 128 | etype, evalue, tb = sys.exc_info() 129 | stb = a.structured_traceback(etype, evalue, tb, tb_offset=1) 130 | for i, line in enumerate(stb): 131 | if "site-packages" in line: 132 | first_line = slice(i) 133 | break 134 | else: 135 | first_line = slice(-1) 136 | return stb[first_line] + stb[-1:] 137 | 138 | 139 | class State(object): 140 | """Basic State manager for thermodyanmic states. 141 | 142 | Parameters 143 | ---------- 144 | substance : `str` 145 | One of the substances supported by CoolProp 146 | T : `pint.UnitRegistry.Quantity` 147 | Temperature 148 | p : `pint.UnitRegistry.Quantity` 149 | Pressure 150 | u : `pint.UnitRegistry.Quantity` 151 | Mass-specific internal energy 152 | s : `pint.UnitRegistry.Quantity` 153 | Mass-specific entropy 154 | v : `pint.UnitRegistry.Quantity` 155 | Mass-specific volume 156 | h : `pint.UnitRegistry.Quantity` 157 | Mass-specific enthalpy 158 | x : `pint.UnitRegistry.Quantity` 159 | Quality 160 | 161 | """ 162 | 163 | _allowed_subs = [ 164 | "AIR", 165 | "AMMONIA", 166 | "WATER", 167 | "PROPANE", 168 | "R134A", 169 | "R22", 170 | "ISOBUTANE", 171 | "CARBONDIOXIDE", 172 | "OXYGEN", 173 | "NITROGEN", 174 | ] 175 | 176 | _all_pairs = set( 177 | munge_coolprop_input_prop(k) 178 | for k in dir(CoolProp.constants) 179 | if "INPUTS" in k and "molar" not in k 180 | ) 181 | _all_pairs.update([k[::-1] for k in _all_pairs]) 182 | 183 | _unsupported_pairs = {"Tu", "Th", "us", "hx"} 184 | _unsupported_pairs.update([k[::-1] for k in _unsupported_pairs]) 185 | 186 | _allowed_pairs = _all_pairs - _unsupported_pairs 187 | 188 | _all_props = set("Tpvuhsx") 189 | 190 | _read_only_props = {"cp", "cv", "phase"} 191 | 192 | _dimensions = { 193 | "T": UnitsContainer({"[temperature]": 1.0}), 194 | "p": UnitsContainer({"[mass]": 1.0, "[length]": -1.0, "[time]": -2.0}), 195 | "v": UnitsContainer({"[length]": 3.0, "[mass]": -1.0}), 196 | "u": UnitsContainer({"[length]": 2.0, "[time]": -2.0}), 197 | "h": UnitsContainer({"[length]": 2.0, "[time]": -2.0}), 198 | "s": UnitsContainer({"[length]": 2.0, "[time]": -2.0, "[temperature]": -1.0}), 199 | "x": UnitsContainer({}), 200 | } 201 | 202 | _SI_units = { 203 | "T": "kelvin", 204 | "p": "pascal", 205 | "v": "meter**3/kilogram", 206 | "u": "joules/kilogram", 207 | "h": "joules/kilogram", 208 | "s": "joules/(kilogram*kelvin)", 209 | "x": "dimensionless", 210 | "cp": "joules/(kilogram*kelvin)", 211 | "cv": "joules/(kilogram*kelvin)", 212 | } 213 | 214 | def __setattr__( 215 | self, 216 | key: str, 217 | value: "Union[str, pint.Quantity, tuple[pint.Quantity, pint.Quantity]]", 218 | ) -> None: 219 | if key.startswith("_") or key in ("sub", "label", "units"): 220 | object.__setattr__(self, key, value) 221 | elif key in self._allowed_pairs: 222 | if not isinstance(value, tuple): # pragma: no cover, for typing 223 | raise ValueError("Must pass a tuple of Quantities") 224 | self._check_dimensions(key, value) 225 | self._check_values(key, value) 226 | self._set_properties(key, value) 227 | elif key in self._unsupported_pairs: 228 | raise StateError( 229 | f"The pair of input properties entered ({key}) isn't supported yet. " 230 | "Sorry!" 231 | ) 232 | else: 233 | raise AttributeError(f"Unknown attribute {key}") 234 | 235 | def __getattr__( 236 | self, key: str 237 | ) -> "Union[str, tuple[pint.Quantity, pint.Quantity], pint.Quantity]": 238 | if key in self._all_props: 239 | return object.__getattribute__(self, "_" + key) 240 | elif key in self._all_pairs: 241 | val_0 = object.__getattribute__(self, "_" + key[0]) 242 | val_1 = object.__getattribute__(self, "_" + key[1]) 243 | return val_0, val_1 244 | elif key == "phase": 245 | return object.__getattribute__(self, "_" + key) 246 | elif key in self._read_only_props: 247 | return object.__getattribute__(self, "_" + key) 248 | else: 249 | raise AttributeError(f"Unknown attribute {key}") 250 | 251 | def __eq__(self, other: object) -> bool: 252 | """Check if two `State`s are equivalent. 253 | 254 | Check that they are using the same substance and two properties that are 255 | always independent. Choose T and v because the EOS tends to be defined 256 | in terms of T and density. 257 | """ 258 | if not isinstance(other, State): 259 | return NotImplemented 260 | if ( 261 | self.sub == other.sub 262 | # Pylance does not support NumPy ufuncs 263 | and np.isclose(other.T, self.T) # type: ignore 264 | and np.isclose(other.v, self.v) # type: ignore 265 | ): 266 | return True 267 | return False 268 | 269 | def __le__(self, other: "State"): 270 | return NotImplemented 271 | 272 | def __lt__(self, other: "State"): 273 | return NotImplemented 274 | 275 | def __gt__(self, other: "State"): 276 | return NotImplemented 277 | 278 | def __ge__(self, other: "State"): 279 | return NotImplemented 280 | 281 | def __init__( 282 | self, substance: str, label=None, units=None, **kwargs: "pint.Quantity" 283 | ): 284 | if units is None: 285 | units = default_units 286 | self.units = units 287 | 288 | self.label = label 289 | 290 | if substance.upper() in self._allowed_subs: 291 | self.sub = substance.upper() 292 | else: 293 | raise ValueError( 294 | f"{substance} is not an allowed substance. " 295 | f"Choose one of {self._allowed_subs}." 296 | ) 297 | 298 | self._abstract_state = CoolProp.AbstractState("HEOS", self.sub) 299 | 300 | input_props = "" 301 | for arg in kwargs: 302 | if arg not in self._all_props: 303 | raise ValueError(f"The argument {arg} is not allowed.") 304 | else: 305 | input_props += arg 306 | 307 | if len(input_props) > 2 or len(input_props) == 1: 308 | raise ValueError( 309 | "Incorrect number of properties specified. Must be 2 or 0." 310 | ) 311 | 312 | if len(input_props) > 0 and input_props not in self._allowed_pairs: 313 | raise StateError( 314 | f"The pair of input properties entered ({input_props}) isn't supported " 315 | "yet. Sorry!" 316 | ) 317 | 318 | if len(input_props) > 0: 319 | setattr(self, input_props, (kwargs[input_props[0]], kwargs[input_props[1]])) 320 | 321 | @property 322 | def label(self): 323 | """Get or set the string label for this state, used in plotting.""" 324 | return self._label 325 | 326 | @label.setter 327 | def label(self, value: str | None): 328 | if value is None: 329 | self._label = value 330 | return 331 | try: 332 | label = str(value) 333 | except Exception: 334 | raise TypeError( 335 | f"The given label '{value!r}' could not be converted to a string" 336 | ) from None 337 | self._label = label 338 | 339 | @property 340 | def units(self): 341 | """Get or set the string units for this state to set attribute units.""" 342 | return self._units 343 | 344 | @units.setter 345 | def units(self, value: str | None): 346 | if value is None or value in ("EE", "SI"): 347 | self._units = value 348 | if hasattr(self, "T"): 349 | setattr(self, "Tv", (self.T, self.v)) 350 | else: 351 | raise TypeError( 352 | f"The given units '{units!r}' are not supported. Must be 'SI', " 353 | "'EE', or None." 354 | ) 355 | 356 | def to_SI(self, prop: str, value: "pint.Quantity") -> "pint.Quantity": 357 | """Convert the input ``value`` to the appropriate SI base units.""" 358 | return value.to(self._SI_units[prop]) 359 | 360 | def to_PropsSI(self, prop: str, value: "pint.Quantity") -> float: # noqa: D403 361 | """CoolProp can't handle Pint Quantites so return the magnitude only. 362 | 363 | Convert to the appropriate SI units first. 364 | """ # noqa: D403 365 | return self.to_SI(prop, value).magnitude 366 | 367 | @staticmethod 368 | def _check_values( 369 | properties: str, values: "tuple[pint.Quantity, pint.Quantity]" 370 | ) -> None: 371 | for p, v in zip(properties, values): 372 | if p in "Tvp" and v.to_base_units().magnitude < 0.0: 373 | raise StateError(f"The value of {p} must be positive in absolute units") 374 | elif p == "x" and not (0.0 <= v.to_base_units().magnitude <= 1.0): 375 | raise StateError("The value of the quality must be between 0 and 1") 376 | 377 | def _check_dimensions( 378 | self, properties: str, values: "tuple[pint.Quantity, pint.Quantity]" 379 | ) -> None: 380 | for p, v in zip(properties, values): 381 | # Dimensionless values are a special case and don't work with 382 | # the "check" method. 383 | try: 384 | valid = v.check(self._SI_units[p]) 385 | except KeyError: 386 | valid = v.dimensionality == self._dimensions[p] 387 | if not valid: 388 | raise StateError( 389 | f"The dimensions for {p} must be {self._dimensions[p]}" 390 | ) 391 | 392 | def _set_properties( 393 | self, known_props: str, known_values: "tuple[pint.Quantity, pint.Quantity]" 394 | ) -> None: 395 | known_state: OrderedDict[str, float] = OrderedDict() 396 | 397 | for prop, val in zip(known_props, known_values): 398 | if prop == "x": 399 | known_state["Q"] = self.to_PropsSI(prop, val) 400 | elif prop == "v": 401 | known_state["Dmass"] = 1.0 / self.to_PropsSI(prop, val) 402 | else: 403 | postfix = "" if prop in ["T", "p"] else "mass" 404 | known_state[prop.upper() + postfix] = self.to_PropsSI(prop, val) 405 | 406 | for key in sorted(known_state): 407 | known_state.move_to_end(key) 408 | 409 | inputs = getattr(CoolProp, "".join(known_state.keys()) + "_INPUTS") 410 | try: 411 | self._abstract_state.update(inputs, *known_state.values()) 412 | except ValueError as e: 413 | if "Saturation pressure" in str(e): 414 | raise StateError( 415 | f"The given values for {known_props[0]} and {known_props[1]} are " 416 | "not independent." 417 | ) 418 | else: 419 | raise 420 | 421 | for prop in self._all_props.union(self._read_only_props): 422 | if prop == "v": 423 | v = 1.0 / self._abstract_state.keyed_output(CoolProp.iDmass) 424 | value = v * units(self._SI_units[prop]) 425 | elif prop == "x": 426 | x = self._abstract_state.keyed_output(CoolProp.iQ) 427 | if x == -1.0: 428 | value = None 429 | else: 430 | value = x * units(self._SI_units[prop]) 431 | elif prop == "phase": 432 | value = CoolPropPhaseNames( 433 | self._abstract_state.keyed_output(CoolProp.iPhase) 434 | ).name 435 | else: 436 | postfix = "" if prop in "Tp" else "mass" 437 | p = getattr(CoolProp, "i" + prop.title() + postfix) 438 | value = self._abstract_state.keyed_output(p) * units( 439 | self._SI_units[prop] 440 | ) 441 | 442 | set_units = None 443 | if self.units == "SI": 444 | set_units = getattr(default_SI, prop, None) 445 | elif self.units == "EE": 446 | set_units = getattr(default_EE, prop, None) 447 | if set_units is not None: 448 | value.ito(set_units) 449 | setattr(self, "_" + prop, value) 450 | -------------------------------------------------------------------------------- /docs/Tutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tutorial\n", 8 | "\n", 9 | "This tutorial will guide you in the basic use of ThermoState. The ThermoState\n", 10 | "package is designed to ease the evaluation of thermodynamic properties for\n", 11 | "common substances used in Mechanical Engineering courses. Rather than looking up\n", 12 | "the information in a table and interpolating, we can input properties for the\n", 13 | "states directly, and all unknown values are automatically calculated." 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "ThermoState uses [CoolProp](http://www.coolprop.org/) and [Pint](https://pint.readthedocs.io) to enable easy property evaluation in any unit system. The first thing we need to do is import the parts of ThermoState that we will use. This adds them to the set of local variables" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "metadata": { 27 | "scrolled": true 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "from thermostate import State, Q_, units" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Pint and Units\n", 39 | "\n", 40 | "Now that the interface has been imported, we can create some properties. For instance, let's say we're given the pressure and temperature properties for water, and asked to determine the specific volume. First, let's create variables that set the pressure and temperature. We will use the Pint `Quantity` function, which we have called `Q_`. The syntax for the `Q_` function is `Q_(value, 'units')`." 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": { 47 | "scrolled": true 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "p_1 = Q_(101325, \"Pa\")" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "We can use whatever units we'd like, Pint supports a wide variety of units." 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 3, 64 | "metadata": { 65 | "scrolled": true 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "p_1 = Q_(1.01325, \"bar\")\n", 70 | "p_1 = Q_(14.7, \"psi\")\n", 71 | "p_1 = Q_(1.0, \"atm\")" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Another way to specify the units is to use the `units` class that we imported. This class has a number of attributes (text following a period) that can be used to create a quantity with units by multiplying a number with the unit. \n", 79 | "\n", 80 | "```python\n", 81 | "units.degR\n", 82 | "# ^^^^\n", 83 | "# This is the attribute\n", 84 | "```\n", 85 | "\n", 86 | "Let's set the temperature now. The available units of temperature are `degF` (`fahrenheit`), `degR` (`rankine`), `degC` (`celsius`), and `K` (`kelvin`)." 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 4, 92 | "metadata": { 93 | "scrolled": true 94 | }, 95 | "outputs": [], 96 | "source": [ 97 | "T_1 = 460 * units.degR\n", 98 | "T_1 = 25 * units.degC\n", 99 | "T_1 = 75 * units.degF\n", 100 | "T_1 = 400 * units.K" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "The two ways of creating the units are equivalent. The following cell should print `True` to demonstrate this." 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": { 114 | "scrolled": true 115 | }, 116 | "outputs": [ 117 | { 118 | "data": { 119 | "text/plain": [ 120 | "True" 121 | ] 122 | }, 123 | "execution_count": 5, 124 | "metadata": {}, 125 | "output_type": "execute_result" 126 | } 127 | ], 128 | "source": [ 129 | "Q_(101325, \"Pa\") == 1.0 * units.atm" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "Note the convention we are using here: the variables are named with the property, followed by an underscore, then the number of the state. In this case, we are setting properties for state 1, hence `T_1` and `p_1`." 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "## ThermoState\n", 144 | "\n", 145 | "Now that we have defined two properties with units, let's define a state. First, we create a variable to hold the `State` and tell ThermoState what substance we want to use with that state. The available substances are:\n", 146 | "\n", 147 | "* `water`\n", 148 | "* `air`\n", 149 | "* `R134a`\n", 150 | "* `R22`\n", 151 | "* `propane`\n", 152 | "* `ammonia`\n", 153 | "* `isobutane`\n", 154 | "* `carbondioxide`\n", 155 | "* `oxygen`\n", 156 | "* `nitrogen`" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "Note that the name of the substance is case-insensitive (it doesn't matter whether you use lower case or upper case). It is often easiest to set the name of the substance in a variable, like:" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 6, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "substance = \"water\"" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "Now we need to create the `State` and assign values for the properties. Properties of the state are set as arguments to the `State` class, and they must always be set in pairs, we cannot set a single property at a time. The syntax is\n", 180 | "\n", 181 | " st = State(substance, property_1=value, property_2=value)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "
\n", 189 | "\n", 190 | "**Warning**\n", 191 | "\n", 192 | "Remember that two independent and intensive properties are required to set the state!\n", 193 | "\n", 194 | "
" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "To demonstrate, we will set the `T` and `p` properties of the state and set them equal to the temperature and pressure we defined above. Note that the capitalization of the properties is important! The `p` is lower case while the `T` is upper case (lower case `t` means time)." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 7, 207 | "metadata": { 208 | "scrolled": true 209 | }, 210 | "outputs": [ 211 | { 212 | "name": "stdout", 213 | "output_type": "stream", 214 | "text": [ 215 | "T = 400 kelvin, p = 1.0 standard_atmosphere\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "print(\"T = {}, p = {}\".format(T_1, p_1))" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 8, 226 | "metadata": { 227 | "scrolled": true 228 | }, 229 | "outputs": [], 230 | "source": [ 231 | "st_1 = State(substance, T=T_1, p=p_1)" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "metadata": {}, 237 | "source": [ 238 | "Note again the convention we are using here: The state is labeled by `st`, then an underscore, then the number of the state.\n", 239 | "\n", 240 | "The variables that we use on the right side of the equal sign in the `State` function can be named anything we want. For instance, the following code is exactly equivalent to what we did before." 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 9, 246 | "metadata": {}, 247 | "outputs": [ 248 | { 249 | "name": "stdout", 250 | "output_type": "stream", 251 | "text": [ 252 | "Does luke equal p_1? True\n", 253 | "Does leia equal T_1? True\n", 254 | "Does st_starwars equal st_1? True\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "luke = Q_(1.0, \"atm\")\n", 260 | "leia = Q_(400.0, \"K\")\n", 261 | "print(\"Does luke equal p_1?\", luke == p_1)\n", 262 | "print(\"Does leia equal T_1?\", leia == T_1)\n", 263 | "st_starwars = State(substance, T=leia, p=luke)\n", 264 | "print(\"Does st_starwars equal st_1?\", st_starwars == st_1)" 265 | ] 266 | }, 267 | { 268 | "cell_type": "markdown", 269 | "metadata": {}, 270 | "source": [ 271 | "
\n", 272 | "\n", 273 | "**Warning**\n", 274 | "\n", 275 | "To avoid confusing yourself, name your variables to something useful. For instance, use the property symbol, then an underscore, then the state number, as in `p_1 = Q_(1.0, 'atm')` to indicate the pressure at state 1. In my notes and solutions, this is the convention that I will follow, and I will use `st_#` to indicate a `State` (e.g., `st_1` is state 1, `st_2` is state 2, and so forth).\n", 276 | "\n", 277 | "
" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "In theory, any two pairs of independent properties can be used to set the state. In reality, the pairs of properties available to set the state is slightly limited because of the way the equation of state is written. The available pairs of properties are\n", 285 | "\n", 286 | "* `Tp`\n", 287 | "* `Ts`\n", 288 | "* `Tv`\n", 289 | "* `Tx`\n", 290 | "* `pu`\n", 291 | "* `ps`\n", 292 | "* `pv`\n", 293 | "* `ph`\n", 294 | "* `px`\n", 295 | "* `uv`\n", 296 | "* `sv`\n", 297 | "* `hs`\n", 298 | "* `hv`\n", 299 | "\n", 300 | "The reverse of any of these pairs is also possible and totally equivalent.\n", 301 | "\n", 302 | "By setting two properties in this way, the `State` class will calculate all the other properties we might be interested in. We can access the value of any property by getting the attribute for that property. The available properties are `T` (temperature), `p` (pressure), `v` (mass-specific volume), `u` (mass-specific internal energy), `h` (mass-specific enthalpy), `s` (mass-specific entropy), `x` (quality), `cp` (specific heat at constant pressure), `cv` (specific heat at constant volume), and `phase` (the phase of this state). The syntax is\n", 303 | "\n", 304 | " State.property\n", 305 | " \n", 306 | "or\n", 307 | "\n", 308 | " st_1.T # Gets the temperature\n", 309 | " st_1.p # Gets the pressure\n", 310 | " st_1.v # Gets the specific volume\n", 311 | " st_1.u # Gets the internal energy\n", 312 | " st_1.h # Gets the enthalpy\n", 313 | " st_1.s # Gets the entropy\n", 314 | " st_1.x # Gets the quality\n", 315 | " st_1.cp # Gets the specific heat at constant pressure\n", 316 | " st_1.cv # Gets the specific heat at constant volume\n", 317 | " st_1.phase # Gets the phase at this state\n", 318 | " " 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "
\n", 326 | "\n", 327 | "**Note**\n", 328 | "\n", 329 | "Capitalization is important! The temperature has upper case `T`, while the other properties are lower case to indicate that they are mass-specific quantities.\n", 330 | "\n", 331 | "
" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 10, 337 | "metadata": { 338 | "scrolled": true 339 | }, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "T_1 = 400.0 kelvin\n", 346 | "p_1 = 101324.99999999953 pascal\n", 347 | "v_1 = 1.801983936953226 meter ** 3 / kilogram\n", 348 | "u_1 = 2547715.3635084038 joule / kilogram\n", 349 | "h_1 = 2730301.3859201893 joule / kilogram\n", 350 | "s_1 = 7496.2021523754065 joule / kelvin / kilogram\n", 351 | "x_1 = None\n", 352 | "cp_1 = 2009.2902478486988 joule / kelvin / kilogram\n", 353 | "cv_1 = 1509.1482452129906 joule / kelvin / kilogram\n", 354 | "phase_1 = gas\n" 355 | ] 356 | } 357 | ], 358 | "source": [ 359 | "print(\"T_1 = {}\".format(st_1.T))\n", 360 | "print(\"p_1 = {}\".format(st_1.p))\n", 361 | "print(\"v_1 = {}\".format(st_1.v))\n", 362 | "print(\"u_1 = {}\".format(st_1.u))\n", 363 | "print(\"h_1 = {}\".format(st_1.h))\n", 364 | "print(\"s_1 = {}\".format(st_1.s))\n", 365 | "print(\"x_1 = {}\".format(st_1.x))\n", 366 | "print(\"cp_1 = {}\".format(st_1.cp))\n", 367 | "print(\"cv_1 = {}\".format(st_1.cv))\n", 368 | "print(\"phase_1 = {}\".format(st_1.phase))" 369 | ] 370 | }, 371 | { 372 | "cell_type": "markdown", 373 | "metadata": {}, 374 | "source": [ 375 | "In this case, the value for the quality is the special Python value `None`. This is because at 400 K and 101325 Pa, the state of water is a **superheated vapor** and the quality is **undefined** except in the vapor dome. To access states in the vapor dome, we cannot use `T` and `p` as independent properties, because they are not independent inside the vapor dome. Instead, we have to use the pairs involving the other properties (possibly including the quality) to set the state. When we define the quality, the units are `dimensionless` or `percent`. For instance:" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": 11, 381 | "metadata": { 382 | "scrolled": true 383 | }, 384 | "outputs": [ 385 | { 386 | "name": "stdout", 387 | "output_type": "stream", 388 | "text": [ 389 | "T_2 = 373.15 kelvin\n", 390 | "p_2 = 101417.99665788242 pascal\n", 391 | "v_2 = 0.16811572834411828 meter ** 3 / kilogram\n", 392 | "u_2 = 627756.5746698529 joule / kilogram\n", 393 | "h_2 = 644806.535045544 joule / kilogram\n", 394 | "s_2 = 1911.9019425021506 joule / kelvin / kilogram\n", 395 | "x_2 = 0.1 dimensionless\n" 396 | ] 397 | } 398 | ], 399 | "source": [ 400 | "T_2 = Q_(100.0, \"degC\")\n", 401 | "x_2 = Q_(0.1, \"dimensionless\")\n", 402 | "st_2 = State(\"water\", T=T_2, x=x_2)\n", 403 | "print(\"T_2 = {}\".format(st_2.T))\n", 404 | "print(\"p_2 = {}\".format(st_2.p))\n", 405 | "print(\"v_2 = {}\".format(st_2.v))\n", 406 | "print(\"u_2 = {}\".format(st_2.u))\n", 407 | "print(\"h_2 = {}\".format(st_2.h))\n", 408 | "print(\"s_2 = {}\".format(st_2.s))\n", 409 | "print(\"x_2 = {}\".format(st_2.x))" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "In addition, whether you use the `'dimensionless'` \"units\" for the quality as above, or use the `'percent'` \"units\", the result is exactly equivalent. The next cell should print `True` to the screen to demonstrate this." 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": 12, 422 | "metadata": { 423 | "scrolled": true 424 | }, 425 | "outputs": [ 426 | { 427 | "data": { 428 | "text/plain": [ 429 | "True" 430 | ] 431 | }, 432 | "execution_count": 12, 433 | "metadata": {}, 434 | "output_type": "execute_result" 435 | } 436 | ], 437 | "source": [ 438 | "x_2 == Q_(10.0, \"percent\")" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "metadata": {}, 444 | "source": [ 445 | "From these results, we can see that the units of the units of the properties stored in the `State` are always SI units - Kelvin, Pascal, m3/kg, J/kg, and J/(kg-Kelvin). We can use the `to` function to convert the units to anything we want, provided the dimensions are compatible. The syntax is `State.property.to('units')`." 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": 13, 451 | "metadata": { 452 | "scrolled": true 453 | }, 454 | "outputs": [ 455 | { 456 | "name": "stdout", 457 | "output_type": "stream", 458 | "text": [ 459 | "211.99999999999991 degree_Fahrenheit\n", 460 | "0.4566498699316826 british_thermal_unit / degree_Rankine / pound\n" 461 | ] 462 | } 463 | ], 464 | "source": [ 465 | "print(st_2.T.to(\"degF\"))\n", 466 | "print(st_2.s.to(\"BTU/(lb*degR)\"))" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": {}, 472 | "source": [ 473 | "
\n", 474 | "\n", 475 | "**Note**\n", 476 | "\n", 477 | "The values are always converted in the `State` to SI units, no matter what the input units are. Therefore, if you want EE units as an output, you have to convert.\n", 478 | "\n", 479 | "
" 480 | ] 481 | }, 482 | { 483 | "cell_type": "markdown", 484 | "metadata": {}, 485 | "source": [ 486 | "If we try to convert to a unit with incompatible dimensions, Pint will raise a `DimenstionalityError` exception.\n", 487 | "\n", 488 | "
\n", 489 | "\n", 490 | "**Warning**\n", 491 | "\n", 492 | "If you get a `DimensionalityError`, examine your conversion very closely. The error message will tell you why the dimensions are incompatible!\n", 493 | "\n", 494 | "
" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": 14, 500 | "metadata": { 501 | "scrolled": false 502 | }, 503 | "outputs": [ 504 | { 505 | "ename": "DimensionalityError", 506 | "evalue": "Cannot convert from 'kelvin' ([temperature]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2)", 507 | "output_type": "error", 508 | "traceback": [ 509 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 510 | "\u001b[1;31mDimensionalityError\u001b[0m Traceback (most recent call last)", 511 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mst_2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mT\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'joule'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 512 | "\u001b[1;31mDimensionalityError\u001b[0m: Cannot convert from 'kelvin' ([temperature]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2)" 513 | ] 514 | } 515 | ], 516 | "source": [ 517 | "print(st_2.T.to(\"joule\"))" 518 | ] 519 | }, 520 | { 521 | "cell_type": "markdown", 522 | "metadata": {}, 523 | "source": [ 524 | "Here we have tried to convert from `'kelvin'` to `'joule'` and the error message **which is the last line** says\n", 525 | " \n", 526 | " DimensionalityError: Cannot convert from 'kelvin' ([temperature]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2)\n", 527 | " \n", 528 | "The dimensions of a temperature are, well, temperature. The formula for energy (Joule) is $m*a*d$ (mass times acceleration times distance), and in terms of dimensions, $M*L/T^2*L = L^2*M/T^2$ (where in dimensions, capital $T$ is time). Clearly, these dimensions are incompatible. A more subtle case might be trying to convert **energy** to **power** (again, not allowed):" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": 15, 534 | "metadata": { 535 | "scrolled": false 536 | }, 537 | "outputs": [ 538 | { 539 | "ename": "DimensionalityError", 540 | "evalue": "Cannot convert from 'joule' ([length] ** 2 * [mass] / [time] ** 2) to 'watt' ([length] ** 2 * [mass] / [time] ** 3)", 541 | "output_type": "error", 542 | "traceback": [ 543 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 544 | "\u001b[1;31mDimensionalityError\u001b[0m Traceback (most recent call last)", 545 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mQ_\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1000.0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'joule'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mto\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'watt'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;31m## Other Common Errors\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 546 | "\u001b[1;31mDimensionalityError\u001b[0m: Cannot convert from 'joule' ([length] ** 2 * [mass] / [time] ** 2) to 'watt' ([length] ** 2 * [mass] / [time] ** 3)" 547 | ] 548 | } 549 | ], 550 | "source": [ 551 | "Q_(1000.0, \"joule\").to(\"watt\") ## Other Common Errors" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "## Default Units" 559 | ] 560 | }, 561 | { 562 | "attachments": {}, 563 | "cell_type": "markdown", 564 | "metadata": {}, 565 | "source": [ 566 | "Default units can be set either through the `set_default_units(\"units\")` function, when creating a state, or after a state has been set to change the units of the state. Units can be set either with `\"SI\"` or `\"EE\"` for the corresponding sets of units, or `None` to reset the default units." 567 | ] 568 | }, 569 | { 570 | "cell_type": "code", 571 | "execution_count": 16, 572 | "metadata": {}, 573 | "outputs": [ 574 | { 575 | "name": "stdout", 576 | "output_type": "stream", 577 | "text": [ 578 | "1.7566087533509436 british_thermal_unit / degree_Rankine / pound\n", 579 | "7.354570555884304 kilojoule / kelvin / kilogram\n", 580 | "7354.570555884259 joule / kelvin / kilogram\n" 581 | ] 582 | } 583 | ], 584 | "source": [ 585 | "from thermostate import set_default_units\n", 586 | "\n", 587 | "set_default_units(\"EE\")\n", 588 | "\n", 589 | "st_3 = State(\"water\", T=Q_(100, \"degC\"), p=Q_(1.0, \"atm\"))\n", 590 | "print(\"These will be EE units because we set the default for all states above:\", st_3.s)\n", 591 | "\n", 592 | "st_4 = State(\"water\", T=Q_(100, \"degC\"), p=Q_(1.0, \"atm\"), units=\"SI\")\n", 593 | "print(\n", 594 | " \"These will be pseudo-SI units that use kJ, which were set as the default when creating this state:\",\n", 595 | " st_4.s,\n", 596 | ")\n", 597 | "\n", 598 | "st_4.units = None\n", 599 | "print(\n", 600 | " \"These will be true SI units, because we reset the default units for this state:\",\n", 601 | " st_4.s,\n", 602 | ")\n", 603 | "\n", 604 | "# Calling this again with None will reset the default units for all created states to true SI\n", 605 | "set_default_units(None)\n", 606 | "\n", 607 | "st_5 = State(\"water\", T=Q_(100.0, \"degC\"), p=Q_(1.0, \"atm\"))\n", 608 | "print(\"These are true SI units, set by the set_default_units:\", st_5.s)" 609 | ] 610 | }, 611 | { 612 | "cell_type": "markdown", 613 | "metadata": {}, 614 | "source": [ 615 | "## Other Common Errors" 616 | ] 617 | }, 618 | { 619 | "cell_type": "markdown", 620 | "metadata": {}, 621 | "source": [ 622 | "Other common errors generated from using ThermoState will raise `StateErrors`. These errors may be due to\n", 623 | "\n", 624 | "1. Not specifying enough properties to fix the state, or specifying too many properties to fix the state\n", 625 | "2. Specifying a pair of properties that are not independent at the desired condtions\n", 626 | "3. Entering an unsupported pair of property inputs (the currently unsupported pairs are `Tu`, `Th`, and `us`, due to limitations in CoolProp)\n", 627 | "4. Specifying a `Quantity` with incorrect dimensions for the property input\n", 628 | "\n", 629 | "An example demonstrating #4 from above:" 630 | ] 631 | }, 632 | { 633 | "cell_type": "code", 634 | "execution_count": 17, 635 | "metadata": {}, 636 | "outputs": [ 637 | { 638 | "ename": "StateError", 639 | "evalue": "The dimensions for v must be [length] ** 3 / [mass]", 640 | "output_type": "error", 641 | "traceback": [ 642 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 643 | "\u001b[1;31mStateError\u001b[0m Traceback (most recent call last)", 644 | "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mState\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'water'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mv\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mQ_\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1000.0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'degC'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mp\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mQ_\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m1.0\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'bar'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", 645 | "\u001b[1;32mc:\\users\\user\\documents\\github\\thermostate\\src\\thermostate\\thermostate.py\u001b[0m in \u001b[0;36m__init__\u001b[1;34m(self, substance, label, units, **kwargs)\u001b[0m\n\u001b[0;32m 317\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 318\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput_props\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 319\u001b[1;33m \u001b[0msetattr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minput_props\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0minput_props\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0minput_props\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 320\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 321\u001b[0m \u001b[1;33m@\u001b[0m\u001b[0mproperty\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 646 | "\u001b[1;32mc:\\users\\user\\documents\\github\\thermostate\\src\\thermostate\\thermostate.py\u001b[0m in \u001b[0;36m__setattr__\u001b[1;34m(self, key, value)\u001b[0m\n\u001b[0;32m 222\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mvalue\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtuple\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;31m# pragma: no cover, for typing\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 223\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Must pass a tuple of Quantities\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 224\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_check_dimensions\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 225\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_check_values\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 226\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_set_properties\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 647 | "\u001b[1;32mc:\\users\\user\\documents\\github\\thermostate\\src\\thermostate\\thermostate.py\u001b[0m in \u001b[0;36m_check_dimensions\u001b[1;34m(self, properties, values)\u001b[0m\n\u001b[0;32m 387\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mvalid\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 388\u001b[0m raise StateError(\n\u001b[1;32m--> 389\u001b[1;33m \u001b[1;34mf\"The dimensions for {p} must be {self._dimensions[p]}\"\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 390\u001b[0m )\n\u001b[0;32m 391\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", 648 | "\u001b[1;31mStateError\u001b[0m: The dimensions for v must be [length] ** 3 / [mass]" 649 | ] 650 | } 651 | ], 652 | "source": [ 653 | "State(\"water\", v=Q_(1000.0, \"degC\"), p=Q_(1.0, \"bar\"))" 654 | ] 655 | }, 656 | { 657 | "cell_type": "markdown", 658 | "metadata": {}, 659 | "source": [ 660 | "## Summary\n", 661 | "\n", 662 | "In summary, we need to use two (2) **independent and intensive** properties to fix the state of any simple compressible system. We need to define these quantities with units using Pint, and then use them to set the conditions of a State. Then, we can access the other properties of the State by using the attributes." 663 | ] 664 | }, 665 | { 666 | "cell_type": "code", 667 | "execution_count": 19, 668 | "metadata": { 669 | "scrolled": true 670 | }, 671 | "outputs": [ 672 | { 673 | "name": "stdout", 674 | "output_type": "stream", 675 | "text": [ 676 | "T_5 = 666.0536816976219 kelvin\n", 677 | "p_5 = 669560197.2594488 pascal\n", 678 | "v_5 = 0.0010136928689306796 meter ** 3 / kilogram\n", 679 | "u_5 = 1321271.6027173044 joule / kilogram\n", 680 | "h_5 = 1999999.9999990265 joule / kilogram\n", 681 | "s_5 = 3099.999999999998 joule / kelvin / kilogram\n", 682 | "x_5 = None\n" 683 | ] 684 | } 685 | ], 686 | "source": [ 687 | "h_5 = Q_(2000.0, \"kJ/kg\")\n", 688 | "s_5 = Q_(3.10, \"kJ/(kg*K)\")\n", 689 | "st_5 = State(\"water\", h=h_5, s=s_5)\n", 690 | "print(\"T_5 = {}\".format(st_5.T))\n", 691 | "print(\"p_5 = {}\".format(st_5.p))\n", 692 | "print(\"v_5 = {}\".format(st_5.v))\n", 693 | "print(\"u_5 = {}\".format(st_5.u))\n", 694 | "print(\"h_5 = {}\".format(st_5.h))\n", 695 | "print(\"s_5 = {}\".format(st_5.s))\n", 696 | "print(\"x_5 = {}\".format(st_5.x))" 697 | ] 698 | } 699 | ], 700 | "metadata": { 701 | "anaconda-cloud": {}, 702 | "kernelspec": { 703 | "display_name": ".venv", 704 | "language": "python", 705 | "name": "python3" 706 | }, 707 | "language_info": { 708 | "codemirror_mode": { 709 | "name": "ipython", 710 | "version": 3 711 | }, 712 | "file_extension": ".py", 713 | "mimetype": "text/x-python", 714 | "name": "python", 715 | "nbconvert_exporter": "python", 716 | "pygments_lexer": "ipython3", 717 | "version": "3.10.7" 718 | }, 719 | "vscode": { 720 | "interpreter": { 721 | "hash": "fb7bc785118bb4360cab2bc3a7c18a4172b91ba838a1a0de23ea670f85cf526f" 722 | } 723 | } 724 | }, 725 | "nbformat": 4, 726 | "nbformat_minor": 2 727 | } 728 | -------------------------------------------------------------------------------- /tests/test_thermostate.py: -------------------------------------------------------------------------------- 1 | """Test module for the main ThermoState code.""" 2 | import numpy as np 3 | import pytest 4 | 5 | from thermostate import Q_, State, set_default_units 6 | from thermostate.thermostate import StateError 7 | 8 | 9 | class TestState(object): 10 | """Test the functions of the State object.""" 11 | 12 | def test_eq(self): 13 | """Test equality comparison of states. 14 | 15 | States are equal when their properties are equal and the substances are the 16 | same. 17 | """ 18 | st_1 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 19 | st_2 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 20 | assert st_1 == st_2 21 | 22 | def test_eq_not_two_states(self): 23 | """Test that comparing a state with something else doesn't work.""" 24 | assert not State(substance="water") == 3 25 | assert not 3 == State(substance="water") 26 | 27 | def test_not_eq(self): 28 | """States are not equal when properties are not equal.""" 29 | st_1 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 30 | st_2 = State(substance="water", T=Q_(300.0, "K"), p=Q_(101325.0, "Pa")) 31 | assert not st_1 == st_2 32 | 33 | def test_not_eq_sub(self): 34 | """States are not equal when substances are not the same.""" 35 | st_1 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 36 | st_2 = State(substance="ammonia", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 37 | assert not st_1 == st_2 38 | 39 | def test_comparison(self): 40 | """Greater/less than comparisons are not supported.""" 41 | st_1 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 42 | st_2 = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 43 | with pytest.raises(TypeError): 44 | st_1 < st_2 45 | with pytest.raises(TypeError): 46 | st_1 <= st_2 47 | with pytest.raises(TypeError): 48 | st_1 > st_2 49 | with pytest.raises(TypeError): 50 | st_1 >= st_2 51 | 52 | def test_unit_definitions(self): 53 | """All of the properties should have units defined.""" 54 | st = State("water") 55 | props = st._all_props.union(st._read_only_props) - {"phase"} # type: ignore 56 | assert all([a in st._SI_units.keys() for a in props]) # type: ignore 57 | 58 | def test_lowercase_input(self): 59 | """Substances should be able to be specified with lowercase letters.""" 60 | State(substance="water") 61 | State(substance="r22") 62 | State(substance="r134a") 63 | State(substance="ammonia") 64 | State(substance="propane") 65 | State(substance="air") 66 | State(substance="isobutane") 67 | State(substance="carbondioxide") 68 | State(substance="oxygen") 69 | State(substance="nitrogen") 70 | 71 | def test_bad_substance(self): 72 | """A substance not in the approved list should raise a ValueError.""" 73 | with pytest.raises(ValueError): 74 | State(substance="bad substance") 75 | 76 | def test_too_many_props(self): 77 | """Specifying too many properties should raise a ValueError.""" 78 | with pytest.raises(ValueError): 79 | State( 80 | substance="water", 81 | T=Q_(300, "K"), 82 | p=Q_(101325, "Pa"), 83 | u=Q_(100, "kJ/kg"), 84 | ) 85 | 86 | def test_too_few_props(self): 87 | """Specifying too few properties should raise a value error.""" 88 | with pytest.raises(ValueError): 89 | State(substance="water", T=Q_(300, "K")) 90 | 91 | def test_negative_temperature(self): 92 | """Negative absolute temperatures should raise a StateError.""" 93 | with pytest.raises(StateError): 94 | State(substance="water", T=Q_(-100, "K"), p=Q_(101325, "Pa")) 95 | 96 | def test_negative_pressure(self): 97 | """Negative absolute pressures should raise a StateError.""" 98 | with pytest.raises(StateError): 99 | State(substance="water", T=Q_(300, "K"), p=Q_(-101325, "Pa")) 100 | 101 | def test_negative_volume(self): 102 | """Negative absolute specific volumes should raise a StateError.""" 103 | with pytest.raises(StateError): 104 | State(substance="water", T=Q_(300, "K"), v=Q_(-10.13, "m**3/kg")) 105 | 106 | def test_quality_lt_zero(self): 107 | """Vapor qualities less than 0.0 should raise a StateError.""" 108 | with pytest.raises(StateError): 109 | State(substance="water", x=Q_(-1.0, "dimensionless"), p=Q_(101325, "Pa")) 110 | 111 | def test_quality_gt_one(self): 112 | """Vapor qualities greater than 1.0 should raise a StateError.""" 113 | with pytest.raises(StateError): 114 | State(substance="water", x=Q_(2.0, "dimensionless"), p=Q_(101325, "Pa")) 115 | 116 | def test_invalid_input_prop(self): 117 | """Invalid input properties should raise a ValueError.""" 118 | with pytest.raises(ValueError): 119 | State( 120 | substance="water", x=Q_(0.5, "dimensionless"), bad_prop=Q_(101325, "Pa") 121 | ) 122 | 123 | @pytest.mark.parametrize("prop", ["T", "p", "v", "u", "s", "h"]) 124 | def test_bad_dimensions(self, prop: str): 125 | """Setting bad dimensions for the input property raises a StateError.""" 126 | kwargs = {prop: Q_(1.0, "dimensionless")} 127 | if prop == "v": 128 | kwargs["T"] = Q_(300.0, "K") 129 | else: 130 | kwargs["v"] = Q_(1.0, "m**3/kg") 131 | with pytest.raises(StateError): 132 | State(substance="water", **kwargs) 133 | 134 | def test_bad_x_dimensions(self): 135 | """Setting bad dimensions for quality raises a StateError. 136 | 137 | Must be done in a separate test because the "dimensionless" 138 | sentinel value used in the other test is actually the correct 139 | dimension for quality. 140 | """ 141 | with pytest.raises(StateError): 142 | State(substance="water", T=Q_(300.0, "K"), x=Q_(1.01325, "K")) 143 | 144 | def test_TP_twophase(self): 145 | """Setting a two-phase mixture with T and p should raise a StateError.""" 146 | with pytest.raises(StateError): 147 | State(substance="water", T=Q_(373.1242958476844, "K"), p=Q_(101325.0, "Pa")) 148 | 149 | def test_bad_get_property(self): 150 | """Accessing attributes that aren't one of the properties or pairs raises.""" 151 | s = State(substance="water", T=Q_(400.0, "K"), p=Q_(101325.0, "Pa")) 152 | with pytest.raises(AttributeError): 153 | s.bad_get 154 | 155 | def test_bad_property_setting(self): 156 | """Regression test that pressure is lowercase p, not uppercase.""" 157 | s = State(substance="water") 158 | with pytest.raises(AttributeError): 159 | # Should be lowercase p 160 | s.TP = Q_(400.0, "K"), Q_(101325.0, "Pa") 161 | 162 | def test_label_cannot_be_converted_to_string(self): 163 | """Trying to set a label that can't be converted to a string is a TypeError.""" 164 | 165 | class NoStr: 166 | def __str__(self) -> str: 167 | raise NotImplementedError 168 | 169 | with pytest.raises(TypeError, match="The given label"): 170 | State("water", label=NoStr()) 171 | 172 | def test_unsupported_pair(self): 173 | """Trying to set with an unsupported property pair raises a StateError.""" 174 | with pytest.raises(StateError, match="The pair of input"): 175 | State("water", T=Q_(100.0, "degC"), u=Q_(1e6, "J/kg")) 176 | 177 | def test_set_Tp(self): 178 | """Set a pair of properties of the State and check the properties. 179 | 180 | Also works as a functional/regression test of CoolProp. 181 | """ 182 | s = State(substance="water") 183 | s.Tp = Q_(400.0, "K"), Q_(101325.0, "Pa") 184 | # Pylance does not support NumPy ufuncs 185 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 186 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 187 | assert np.isclose(s.Tp[0], Q_(400.0, "K")) # type: ignore 188 | assert np.isclose(s.Tp[1], Q_(101325.0, "Pa")) # type: ignore 189 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 190 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 191 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 192 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 193 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 194 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 195 | assert s.x is None 196 | assert s.phase == "gas" 197 | 198 | def test_set_pT(self): 199 | """Set a pair of properties of the State and check the properties. 200 | 201 | Also works as a functional/regression test of CoolProp. 202 | """ 203 | s = State(substance="water") 204 | s.pT = Q_(101325.0, "Pa"), Q_(400.0, "K") 205 | # Pylance does not support NumPy ufuncs 206 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 207 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 208 | assert np.isclose(s.pT[1], Q_(400.0, "K")) # type: ignore 209 | assert np.isclose(s.pT[0], Q_(101325.0, "Pa")) # type: ignore 210 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 211 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 212 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 213 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 214 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 215 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 216 | assert s.x is None 217 | assert s.phase == "gas" 218 | 219 | # This set of tests fails because T and u are not valid inputs for PhaseSI 220 | # in CoolProp 6.1.0 221 | @pytest.mark.xfail(strict=True, raises=StateError) 222 | def test_set_uT(self): 223 | """Set a pair of properties of the State and check the properties. 224 | 225 | Also works as a functional/regression test of CoolProp. 226 | """ 227 | s = State(substance="water") 228 | s.uT = Q_(2547715.3635084038, "J/kg"), Q_(400.0, "K") 229 | # Pylance does not support NumPy ufuncs 230 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 231 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 232 | assert np.isclose(s.uT[1], Q_(400.0, "K")) # type: ignore 233 | assert np.isclose(s.uT[0], Q_(2547715.3635084038, "J/kg")) # type: ignore 234 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 235 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 236 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 237 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 238 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 239 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 240 | assert s.x is None 241 | 242 | @pytest.mark.xfail(strict=True, raises=StateError) 243 | def test_set_Tu(self): 244 | """Set a pair of properties of the State and check the properties. 245 | 246 | Also works as a functional/regression test of CoolProp. 247 | """ 248 | s = State(substance="water") 249 | s.Tu = Q_(400.0, "K"), Q_(2547715.3635084038, "J/kg") 250 | # Pylance does not support NumPy ufuncs 251 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 252 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 253 | assert np.isclose(s.Tu[0], Q_(400.0, "K")) # type: ignore 254 | assert np.isclose(s.Tu[1], Q_(2547715.3635084038, "J/kg")) # type: ignore 255 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 256 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 257 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 258 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 259 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 260 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 261 | assert s.x is None 262 | 263 | # This set of tests fails because T and h are not valid inputs for PhaseSI 264 | # in CoolProp 6.1.0 265 | @pytest.mark.xfail(strict=True, raises=StateError) 266 | def test_set_hT(self): 267 | """Set a pair of properties of the State and check the properties. 268 | 269 | Also works as a functional/regression test of CoolProp. 270 | """ 271 | s = State(substance="water") 272 | s.hT = Q_(2730301.3859201893, "J/kg"), Q_(400.0, "K") 273 | # Pylance does not support NumPy ufuncs 274 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 275 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 276 | assert np.isclose(s.hT[1], Q_(400.0, "K")) # type: ignore 277 | assert np.isclose(s.hT[0], Q_(2730301.3859201893, "J/kg")) # type: ignore 278 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 279 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 280 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 281 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 282 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 283 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 284 | assert s.x is None 285 | 286 | @pytest.mark.xfail(strict=True, raises=StateError) 287 | def test_set_Th(self): 288 | """Set a pair of properties of the State and check the properties. 289 | 290 | Also works as a functional/regression test of CoolProp. 291 | """ 292 | s = State(substance="water") 293 | s.Th = Q_(400.0, "K"), Q_(2730301.3859201893, "J/kg") 294 | # Pylance does not support NumPy ufuncs 295 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 296 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 297 | assert np.isclose(s.Th[0], Q_(400.0, "K")) # type: ignore 298 | assert np.isclose(s.Th[1], Q_(2730301.3859201893, "J/kg")) # type: ignore 299 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 300 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 301 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 302 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 303 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 304 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 305 | assert s.x is None 306 | 307 | # This set of tests fails because x and h are not valid inputs for PhaseSI 308 | # in CoolProp 6.3.0 309 | @pytest.mark.xfail(strict=True, raises=StateError) 310 | def test_set_xh(self): 311 | """Set a pair of properties of the State and check the properties. 312 | 313 | Also works as a functional/regression test of CoolProp. 314 | """ 315 | s = State(substance="water") 316 | s.xh = Q_(0.5, "dimensionless"), Q_(1624328.2430353598, "J/kg") 317 | # Pylance does not support NumPy ufuncs 318 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 319 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 320 | assert np.isclose(s.xT[1], Q_(400.0, "K")) # type: ignore 321 | assert np.isclose(s.xT[0], Q_(0.5, "dimensionless")) # type: ignore 322 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 323 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 324 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 325 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 326 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 327 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 328 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 329 | 330 | # This set of tests fails because x and h are not valid inputs for PhaseSI 331 | # in CoolProp 6.3.0 332 | @pytest.mark.xfail(strict=True, raises=StateError) 333 | def test_set_hx(self): 334 | """Set a pair of properties of the State and check the properties. 335 | 336 | Also works as a functional/regression test of CoolProp. 337 | """ 338 | s = State(substance="water") 339 | s.hx = Q_(1624328.2430353598, "J/kg"), Q_(0.5, "dimensionless") 340 | # Pylance does not support NumPy ufuncs 341 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 342 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 343 | assert np.isclose(s.xT[1], Q_(400.0, "K")) # type: ignore 344 | assert np.isclose(s.xT[0], Q_(0.5, "dimensionless")) # type: ignore 345 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 346 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 347 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 348 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 349 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 350 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 351 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 352 | 353 | def test_set_sT(self): 354 | """Set a pair of properties of the State and check the properties. 355 | 356 | Also works as a functional/regression test of CoolProp. 357 | """ 358 | s = State(substance="water") 359 | s.sT = Q_(7496.2021523754065, "J/(kg*K)"), Q_(400.0, "K") 360 | # Pylance does not support NumPy ufuncs 361 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 362 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 363 | assert np.isclose(s.sT[1], Q_(400.0, "K")) # type: ignore 364 | assert np.isclose(s.sT[0], Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 365 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 366 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 367 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 368 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 369 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 370 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 371 | assert s.x is None 372 | 373 | def test_set_Ts(self): 374 | """Set a pair of properties of the State and check the properties. 375 | 376 | Also works as a functional/regression test of CoolProp. 377 | """ 378 | s = State(substance="water") 379 | s.Ts = Q_(400.0, "K"), Q_(7496.2021523754065, "J/(kg*K)") 380 | # Pylance does not support NumPy ufuncs 381 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 382 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 383 | assert np.isclose(s.Ts[0], Q_(400.0, "K")) # type: ignore 384 | assert np.isclose(s.Ts[1], Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 385 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 386 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 387 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 388 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 389 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 390 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 391 | assert s.x is None 392 | 393 | def test_set_vT(self): 394 | """Set a pair of properties of the State and check the properties. 395 | 396 | Also works as a functional/regression test of CoolProp. 397 | """ 398 | s = State(substance="water") 399 | s.vT = Q_(1.801983936953226, "m**3/kg"), Q_(400.0, "K") 400 | # Pylance does not support NumPy ufuncs 401 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 402 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 403 | assert np.isclose(s.vT[1], Q_(400.0, "K")) # type: ignore 404 | assert np.isclose(s.vT[0], Q_(1.801983936953226, "m**3/kg")) # type: ignore 405 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 406 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 407 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 408 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 409 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 410 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 411 | assert s.x is None 412 | 413 | def test_set_Tv(self): 414 | """Set a pair of properties of the State and check the properties. 415 | 416 | Also works as a functional/regression test of CoolProp. 417 | """ 418 | s = State(substance="water") 419 | s.Tv = Q_(400.0, "K"), Q_(1.801983936953226, "m**3/kg") 420 | # Pylance does not support NumPy ufuncs 421 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 422 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 423 | assert np.isclose(s.Tv[0], Q_(400.0, "K")) # type: ignore 424 | assert np.isclose(s.Tv[1], Q_(1.801983936953226, "m**3/kg")) # type: ignore 425 | assert np.isclose(s.u, Q_(2547715.3635084038, "J/kg")) # type: ignore 426 | assert np.isclose(s.s, Q_(7496.2021523754065, "J/(kg*K)")) # type: ignore 427 | assert np.isclose(s.cp, Q_(2009.2902478486988, "J/(kg*K)")) # type: ignore 428 | assert np.isclose(s.cv, Q_(1509.1482452129906, "J/(kg*K)")) # type: ignore 429 | assert np.isclose(s.v, Q_(1.801983936953226, "m**3/kg")) # type: ignore 430 | assert np.isclose(s.h, Q_(2730301.3859201893, "J/kg")) # type: ignore 431 | assert s.x is None 432 | 433 | def test_set_xT(self): 434 | """Set a pair of properties of the State and check the properties. 435 | 436 | Also works as a functional/regression test of CoolProp. 437 | """ 438 | s = State(substance="water") 439 | s.xT = Q_(0.5, "dimensionless"), Q_(400.0, "K") 440 | # Pylance does not support NumPy ufuncs 441 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 442 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 443 | assert np.isclose(s.xT[1], Q_(400.0, "K")) # type: ignore 444 | assert np.isclose(s.xT[0], Q_(0.5, "dimensionless")) # type: ignore 445 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 446 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 447 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 448 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 449 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 450 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 451 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 452 | s.xT = Q_(50, "percent"), Q_(400.0, "K") 453 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 454 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 455 | assert np.isclose(s.xT[1], Q_(400.0, "K")) # type: ignore 456 | assert np.isclose(s.xT[0], Q_(0.5, "dimensionless")) # type: ignore 457 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 458 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 459 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 460 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 461 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 462 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 463 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 464 | 465 | def test_set_Tx(self): 466 | """Set a pair of properties of the State and check the properties. 467 | 468 | Also works as a functional/regression test of CoolProp. 469 | """ 470 | s = State(substance="water") 471 | s.Tx = Q_(400.0, "K"), Q_(0.5, "dimensionless") 472 | # Pylance does not support NumPy ufuncs 473 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 474 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 475 | assert np.isclose(s.Tx[0], Q_(400.0, "K")) # type: ignore 476 | assert np.isclose(s.Tx[1], Q_(0.5, "dimensionless")) # type: ignore 477 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 478 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 479 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 480 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 481 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 482 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 483 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 484 | s.Tx = Q_(400.0, "K"), Q_(50, "percent") 485 | assert np.isclose(s.T, Q_(400.0, "K")) # type: ignore 486 | assert np.isclose(s.p, Q_(245769.34557103913, "Pa")) # type: ignore 487 | assert np.isclose(s.Tx[0], Q_(400.0, "K")) # type: ignore 488 | assert np.isclose(s.Tx[1], Q_(0.5, "dimensionless")) # type: ignore 489 | assert np.isclose(s.u, Q_(1534461.5163075812, "J/kg")) # type: ignore 490 | assert np.isclose(s.s, Q_(4329.703956664546, "J/(kg*K)")) # type: ignore 491 | assert np.isclose(s.cp, Q_(4056.471547685226, "J/(kg*K)")) # type: ignore 492 | assert np.isclose(s.cv, Q_(2913.7307270395363, "J/(kg*K)")) # type: ignore 493 | assert np.isclose(s.v, Q_(0.3656547423394701, "m**3/kg")) # type: ignore 494 | assert np.isclose(s.h, Q_(1624328.2430353598, "J/kg")) # type: ignore 495 | assert np.isclose(s.x, Q_(0.5, "dimensionless")) # type: ignore 496 | 497 | def test_set_pu(self): 498 | """Set a pair of properties of the State and check the properties. 499 | 500 | Also works as a functional/regression test of CoolProp. 501 | """ 502 | s = State(substance="water") 503 | s.pu = Q_(101325.0, "Pa"), Q_(1013250.0, "J/kg") 504 | # Pylance does not support NumPy ufuncs 505 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 506 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 507 | assert np.isclose(s.pu[0], Q_(101325.0, "Pa")) # type: ignore 508 | assert np.isclose(s.pu[1], Q_(1013250.0, "J/kg")) # type: ignore 509 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 510 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 511 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 512 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 513 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 514 | s.pu = Q_(101325.0, "Pa"), Q_(3013250.0, "J/kg") 515 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 516 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 517 | assert np.isclose(s.pu[0], Q_(101325.0, "Pa")) # type: ignore 518 | assert np.isclose(s.pu[1], Q_(3013250.0, "J/kg")) # type: ignore 519 | assert np.isclose(s.u, Q_(3013250.0, "J/kg")) # type: ignore 520 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 521 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 522 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 523 | assert s.x is None 524 | 525 | def test_set_up(self): 526 | """Set a pair of properties of the State and check the properties. 527 | 528 | Also works as a functional/regression test of CoolProp. 529 | """ 530 | s = State(substance="water") 531 | s.up = Q_(1013250.0, "J/kg"), Q_(101325.0, "Pa") 532 | # Pylance does not support NumPy ufuncs 533 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 534 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 535 | assert np.isclose(s.up[0], Q_(1013250.0, "J/kg")) # type: ignore 536 | assert np.isclose(s.up[1], Q_(101325.0, "Pa")) # type: ignore 537 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 538 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 539 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 540 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 541 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 542 | s.up = Q_(3013250.0, "J/kg"), Q_(101325.0, "Pa") 543 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 544 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 545 | assert np.isclose(s.up[0], Q_(3013250.0, "J/kg")) # type: ignore 546 | assert np.isclose(s.up[1], Q_(101325.0, "Pa")) # type: ignore 547 | assert np.isclose(s.u, Q_(3013250, "J/kg")) # type: ignore 548 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 549 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 550 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 551 | assert s.x is None 552 | 553 | def test_set_ps(self): 554 | """Set a pair of properties of the State and check the properties. 555 | 556 | Also works as a functional/regression test of CoolProp. 557 | """ 558 | s = State(substance="water") 559 | s.ps = Q_(101325.0, "Pa"), Q_(3028.9867985920914, "J/(kg*K)") 560 | # Pylance does not support NumPy ufuncs 561 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 562 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 563 | assert np.isclose(s.ps[0], Q_(101325.0, "Pa")) # type: ignore 564 | assert np.isclose(s.ps[1], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 565 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 566 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 567 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 568 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 569 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 570 | s.ps = Q_(101325.0, "Pa"), Q_(8623.283568815832, "J/(kg*K)") 571 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 572 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 573 | assert np.isclose(s.ps[0], Q_(101325.0, "Pa")) # type: ignore 574 | assert np.isclose(s.ps[1], Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 575 | assert np.isclose(s.u, Q_(3013250.0, "J/kg")) # type: ignore 576 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 577 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 578 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 579 | assert s.x is None 580 | 581 | def test_set_sp(self): 582 | """Set a pair of properties of the State and check the properties. 583 | 584 | Also works as a functional/regression test of CoolProp. 585 | """ 586 | s = State(substance="water") 587 | s.sp = Q_(3028.9867985920914, "J/(kg*K)"), Q_(101325.0, "Pa") 588 | # Pylance does not support NumPy ufuncs 589 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 590 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 591 | assert np.isclose(s.sp[0], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 592 | assert np.isclose(s.sp[1], Q_(101325.0, "Pa")) # type: ignore 593 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 594 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 595 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 596 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 597 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 598 | s.sp = Q_(8623.283568815832, "J/(kg*K)"), Q_(101325.0, "Pa") 599 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 600 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 601 | assert np.isclose(s.sp[0], Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 602 | assert np.isclose(s.sp[1], Q_(101325.0, "Pa")) # type: ignore 603 | assert np.isclose(s.u, Q_(3013250, "J/kg")) # type: ignore 604 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 605 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 606 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 607 | assert s.x is None 608 | 609 | def test_set_pv(self): 610 | """Set a pair of properties of the State and check the properties. 611 | 612 | Also works as a functional/regression test of CoolProp. 613 | """ 614 | s = State(substance="water") 615 | s.pv = Q_(101325.0, "Pa"), Q_(0.4772010021515822, "m**3/kg") 616 | # Pylance does not support NumPy ufuncs 617 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 618 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 619 | assert np.isclose(s.pv[0], Q_(101325.0, "Pa")) # type: ignore 620 | assert np.isclose(s.pv[1], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 621 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 622 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 623 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 624 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 625 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 626 | s.pv = Q_(101325.0, "Pa"), Q_(3.189303132125469, "m**3/kg") 627 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 628 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 629 | assert np.isclose(s.pv[0], Q_(101325.0, "Pa")) # type: ignore 630 | assert np.isclose(s.pv[1], Q_(3.189303132125469, "m**3/kg")) # type: ignore 631 | assert np.isclose(s.u, Q_(3013250.0, "J/kg")) # type: ignore 632 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 633 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 634 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 635 | assert s.x is None 636 | 637 | def test_set_vp(self): 638 | """Set a pair of properties of the State and check the properties. 639 | 640 | Also works as a functional/regression test of CoolProp. 641 | """ 642 | s = State(substance="water") 643 | s.vp = Q_(0.4772010021515822, "m**3/kg"), Q_(101325.0, "Pa") 644 | # Pylance does not support NumPy ufuncs 645 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 646 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 647 | assert np.isclose(s.vp[0], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 648 | assert np.isclose(s.vp[1], Q_(101325.0, "Pa")) # type: ignore 649 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 650 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 651 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 652 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 653 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 654 | s.vp = Q_(3.189303132125469, "m**3/kg"), Q_(101325.0, "Pa") 655 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 656 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 657 | assert np.isclose(s.vp[0], Q_(3.189303132125469, "m**3/kg")) # type: ignore 658 | assert np.isclose(s.vp[1], Q_(101325.0, "Pa")) # type: ignore 659 | assert np.isclose(s.u, Q_(3013250, "J/kg")) # type: ignore 660 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 661 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 662 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 663 | assert s.x is None 664 | 665 | def test_set_ph(self): 666 | """Set a pair of properties of the State and check the properties. 667 | 668 | Also works as a functional/regression test of CoolProp. 669 | """ 670 | s = State(substance="water") 671 | s.ph = Q_(101325.0, "Pa"), Q_(1061602.391543017, "J/kg") 672 | # Pylance does not support NumPy ufuncs 673 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 674 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 675 | assert np.isclose(s.ph[0], Q_(101325.0, "Pa")) # type: ignore 676 | assert np.isclose(s.ph[1], Q_(1061602.391543017, "J/kg")) # type: ignore 677 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 678 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 679 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 680 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 681 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 682 | s.ph = Q_(101325.0, "Pa"), Q_(3336406.139862406, "J/kg") 683 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 684 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 685 | assert np.isclose(s.ph[0], Q_(101325.0, "Pa")) # type: ignore 686 | assert np.isclose(s.ph[1], Q_(3336406.139862406, "J/kg")) # type: ignore 687 | assert np.isclose(s.u, Q_(3013250.0, "J/kg")) # type: ignore 688 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 689 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 690 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 691 | assert s.x is None 692 | 693 | def test_set_hp(self): 694 | """Set a pair of properties of the State and check the properties. 695 | 696 | Also works as a functional/regression test of CoolProp. 697 | """ 698 | s = State(substance="water") 699 | s.hp = Q_(1061602.391543017, "J/kg"), Q_(101325.0, "Pa") 700 | # Pylance does not support NumPy ufuncs 701 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 702 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 703 | assert np.isclose(s.hp[0], Q_(1061602.391543017, "J/kg")) # type: ignore 704 | assert np.isclose(s.hp[1], Q_(101325.0, "Pa")) # type: ignore 705 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 706 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 707 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 708 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 709 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 710 | s.hp = Q_(3336406.139862406, "J/kg"), Q_(101325.0, "Pa") 711 | assert np.isclose(s.T, Q_(700.9882316847855, "K")) # type: ignore 712 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 713 | assert np.isclose(s.hp[0], Q_(3336406.139862406, "J/kg")) # type: ignore 714 | assert np.isclose(s.hp[1], Q_(101325.0, "Pa")) # type: ignore 715 | assert np.isclose(s.u, Q_(3013250, "J/kg")) # type: ignore 716 | assert np.isclose(s.s, Q_(8623.283568815832, "J/(kg*K)")) # type: ignore 717 | assert np.isclose(s.v, Q_(3.189303132125469, "m**3/kg")) # type: ignore 718 | assert np.isclose(s.h, Q_(3336406.139862406, "J/kg")) # type: ignore 719 | assert s.x is None 720 | 721 | def test_set_px(self): 722 | """Set a pair of properties of the State and check the properties. 723 | 724 | Also works as a functional/regression test of CoolProp. 725 | """ 726 | s = State(substance="water") 727 | s.px = Q_(101325.0, "Pa"), Q_(0.28475636946248034, "dimensionless") 728 | # Pylance does not support NumPy ufuncs 729 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 730 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 731 | assert np.isclose(s.px[0], Q_(101325.0, "Pa")) # type: ignore 732 | assert np.isclose(s.px[1], Q_(0.2847563694624, "dimensionless")) # type: ignore 733 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 734 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 735 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 736 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 737 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 738 | 739 | def test_set_xp(self): 740 | """Set a pair of properties of the State and check the properties. 741 | 742 | Also works as a functional/regression test of CoolProp. 743 | """ 744 | s = State(substance="water") 745 | s.xp = Q_(0.28475636946248034, "dimensionless"), Q_(101325.0, "Pa") 746 | # Pylance does not support NumPy ufuncs 747 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 748 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 749 | assert np.isclose(s.xp[0], Q_(0.2847563694624, "dimensionless")) # type: ignore 750 | assert np.isclose(s.xp[1], Q_(101325.0, "Pa")) # type: ignore 751 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 752 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 753 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 754 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 755 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 756 | 757 | # This set of tests fails because s and u are not valid inputs for PhaseSI 758 | # in CoolProp 6.1.0 759 | @pytest.mark.xfail(strict=True, raises=StateError) 760 | def test_set_us(self): 761 | """Set a pair of properties of the State and check the properties. 762 | 763 | Also works as a functional/regression test of CoolProp. 764 | """ 765 | s = State(substance="water") 766 | s.us = Q_(1013250.0, "J/kg"), Q_(3028.9867985920914, "J/(kg*K)") 767 | # Pylance does not support NumPy ufuncs 768 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 769 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 770 | assert np.isclose(s.us[0], Q_(1013250.0, "J/kg")) # type: ignore 771 | assert np.isclose(s.us[1], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 772 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 773 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 774 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 775 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 776 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 777 | 778 | @pytest.mark.xfail(strict=True, raises=StateError) 779 | def test_set_su(self): 780 | """Set a pair of properties of the State and check the properties. 781 | 782 | Also works as a functional/regression test of CoolProp. 783 | """ 784 | s = State(substance="water") 785 | s.su = Q_(3028.9867985920914, "J/(kg*K)"), Q_(1013250.0, "J/kg") 786 | # Pylance does not support NumPy ufuncs 787 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 788 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 789 | assert np.isclose(s.su[0], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 790 | assert np.isclose(s.su[1], Q_(1013250.0, "J/kg")) # type: ignore 791 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 792 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 793 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 794 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 795 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 796 | 797 | def test_set_uv(self): 798 | """Set a pair of properties of the State and check the properties. 799 | 800 | Also works as a functional/regression test of CoolProp. 801 | """ 802 | s = State(substance="water") 803 | s.uv = Q_(1013250.0, "J/kg"), Q_(0.4772010021515822, "m**3/kg") 804 | # Pylance does not support NumPy ufuncs 805 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 806 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 807 | assert np.isclose(s.uv[0], Q_(1013250.0, "J/kg")) # type: ignore 808 | assert np.isclose(s.uv[1], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 809 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 810 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 811 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 812 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 813 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 814 | 815 | def test_set_vu(self): 816 | """Set a pair of properties of the State and check the properties. 817 | 818 | Also works as a functional/regression test of CoolProp. 819 | """ 820 | s = State(substance="water") 821 | s.vu = Q_(0.4772010021515822, "m**3/kg"), Q_(1013250.0, "J/kg") 822 | # Pylance does not support NumPy ufuncs 823 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 824 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 825 | assert np.isclose(s.vu[0], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 826 | assert np.isclose(s.vu[1], Q_(1013250.0, "J/kg")) # type: ignore 827 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 828 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 829 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 830 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 831 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 832 | 833 | def test_set_sv(self): 834 | """Set a pair of properties of the State and check the properties. 835 | 836 | Also works as a functional/regression test of CoolProp. 837 | """ 838 | s = State(substance="water") 839 | s.sv = Q_(3028.9867985920914, "J/(kg*K)"), Q_(0.4772010021515822, "m**3/kg") 840 | # Pylance does not support NumPy ufuncs 841 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 842 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 843 | assert np.isclose(s.sv[0], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 844 | assert np.isclose(s.sv[1], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 845 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 846 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 847 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 848 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 849 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 850 | 851 | def test_set_vs(self): 852 | """Set a pair of properties of the State and check the properties. 853 | 854 | Also works as a functional/regression test of CoolProp. 855 | """ 856 | s = State(substance="water") 857 | s.vs = Q_(0.4772010021515822, "m**3/kg"), Q_(3028.9867985920914, "J/(kg*K)") 858 | # Pylance does not support NumPy ufuncs 859 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 860 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 861 | assert np.isclose(s.vs[0], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 862 | assert np.isclose(s.vs[1], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 863 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 864 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 865 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 866 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 867 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 868 | 869 | def test_set_sh(self): 870 | """Set a pair of properties of the State and check the properties. 871 | 872 | Also works as a functional/regression test of CoolProp. 873 | """ 874 | s = State(substance="water") 875 | s.sh = Q_(3028.9867985920914, "J/(kg*K)"), Q_(1061602.391543017, "J/kg") 876 | # Pylance does not support NumPy ufuncs 877 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 878 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 879 | assert np.isclose(s.sh[0], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 880 | assert np.isclose(s.sh[1], Q_(1061602.391543017, "J/kg")) # type: ignore 881 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 882 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 883 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 884 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 885 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 886 | 887 | def test_set_hs(self): 888 | """Set a pair of properties of the State and check the properties. 889 | 890 | Also works as a functional/regression test of CoolProp. 891 | """ 892 | s = State(substance="water") 893 | s.hs = Q_(1061602.391543017, "J/kg"), Q_(3028.9867985920914, "J/(kg*K)") 894 | # Pylance does not support NumPy ufuncs 895 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 896 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 897 | assert np.isclose(s.hs[0], Q_(1061602.391543017, "J/kg")) # type: ignore 898 | assert np.isclose(s.hs[1], Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 899 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 900 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 901 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 902 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 903 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 904 | 905 | def test_set_vh(self): 906 | """Set a pair of properties of the State and check the properties. 907 | 908 | Also works as a functional/regression test of CoolProp. 909 | """ 910 | s = State(substance="water") 911 | s.vh = Q_(0.4772010021515822, "m**3/kg"), Q_(1061602.391543017, "J/kg") 912 | # Pylance does not support NumPy ufuncs 913 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 914 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 915 | assert np.isclose(s.vh[0], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 916 | assert np.isclose(s.vh[1], Q_(1061602.391543017, "J/kg")) # type: ignore 917 | assert np.isclose(s.u, Q_(1013250.0, "J/kg")) # type: ignore 918 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 919 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 920 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 921 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 922 | 923 | def test_set_hv(self): 924 | """Set a pair of properties of the State and check the properties. 925 | 926 | Also works as a functional/regression test of CoolProp. 927 | """ 928 | s = State(substance="water") 929 | s.hv = Q_(1061602.391543017, "J/kg"), Q_(0.4772010021515822, "m**3/kg") 930 | # Pylance does not support NumPy ufuncs 931 | assert np.isclose(s.T, Q_(373.1242958476843, "K")) # type: ignore 932 | assert np.isclose(s.p, Q_(101325.0, "Pa")) # type: ignore 933 | assert np.isclose(s.hv[0], Q_(1061602.391543017, "J/kg")) # type: ignore 934 | assert np.isclose(s.hv[1], Q_(0.4772010021515822, "m**3/kg")) # type: ignore 935 | assert np.isclose(s.u, Q_(1013250, "J/kg")) # type: ignore 936 | assert np.isclose(s.s, Q_(3028.9867985920914, "J/(kg*K)")) # type: ignore 937 | assert np.isclose(s.v, Q_(0.4772010021515822, "m**3/kg")) # type: ignore 938 | assert np.isclose(s.h, Q_(1061602.391543017, "J/kg")) # type: ignore 939 | assert np.isclose(s.x, Q_(0.28475636946248034, "dimensionless")) # type: ignore 940 | 941 | def test_state_units_EE(self): 942 | """Set a state with EE units and check the properties.""" 943 | s = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm"), units="EE") 944 | assert s.units == "EE" 945 | assert s.cv.units == "british_thermal_unit / degree_Rankine / pound" 946 | assert s.cp.units == "british_thermal_unit / degree_Rankine / pound" 947 | assert s.s.units == "british_thermal_unit / degree_Rankine / pound" 948 | assert s.h.units == "british_thermal_unit / pound" 949 | assert s.T.units == "degree_Fahrenheit" 950 | assert s.u.units == "british_thermal_unit / pound" 951 | assert s.v.units == "foot ** 3 / pound" 952 | assert s.p.units == "pound_force_per_square_inch" 953 | 954 | def test_state_units_SI(self): 955 | """Set a state with SI units and check the properties.""" 956 | s = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm"), units="SI") 957 | assert s.units == "SI" 958 | assert s.cv.units == "kilojoule / kelvin / kilogram" 959 | assert s.cp.units == "kilojoule / kelvin / kilogram" 960 | assert s.s.units == "kilojoule / kelvin / kilogram" 961 | assert s.h.units == "kilojoule / kilogram" 962 | assert s.T.units == "degree_Celsius" 963 | assert s.u.units == "kilojoule / kilogram" 964 | assert s.v.units == "meter ** 3 / kilogram" 965 | assert s.p.units == "bar" 966 | 967 | def test_default_units(self): 968 | """Set default units and check for functionality.""" 969 | s = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm")) 970 | assert s.units is None 971 | set_default_units("SI") 972 | s2 = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm")) 973 | assert s2.units == "SI" 974 | set_default_units("EE") 975 | s3 = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm")) 976 | assert s3.units == "EE" 977 | set_default_units(None) 978 | 979 | def test_unsupported_units(self): 980 | """Unsupported unit names should raise TypeError.""" 981 | with pytest.raises(TypeError): 982 | set_default_units("bad") 983 | with pytest.raises(TypeError): 984 | State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm"), units="bad") 985 | 986 | def test_change_units(self): 987 | """Change state units and check variable units have changed.""" 988 | s = State("water", T=Q_(100, "degC"), p=Q_(1.0, "atm"), units="EE") 989 | assert s.units == "EE" 990 | s.units = "SI" 991 | assert s.units == "SI" 992 | assert s.cv.units == "kilojoule / kelvin / kilogram" 993 | assert s.cp.units == "kilojoule / kelvin / kilogram" 994 | assert s.s.units == "kilojoule / kelvin / kilogram" 995 | assert s.h.units == "kilojoule / kilogram" 996 | assert s.T.units == "degree_Celsius" 997 | assert s.u.units == "kilojoule / kilogram" 998 | assert s.v.units == "meter ** 3 / kilogram" 999 | assert s.p.units == "bar" 1000 | --------------------------------------------------------------------------------