├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── basic │ ├── phasepy.component.rst │ └── phasepy.mixture.rst │ ├── conf.py │ ├── equilibrium │ ├── phasepy.elv.rst │ ├── phasepy.flash.rst │ ├── phasepy.multiflash.rst │ └── phasepy.stability.rst │ ├── fit │ ├── binaryfit.jpg │ ├── fit.mixtures.rst │ ├── fit.pure.rst │ └── purefit.jpg │ ├── index.rst │ ├── models │ ├── phasepy.actmodels.rst │ ├── phasepy.cubic.rst │ ├── phasepy.cubicm.rst │ ├── phasepy.cubicp.rst │ ├── phasepy.virial.rst │ └── phasepy.virialgama.rst │ ├── phasepy.equilibrium.rst │ ├── phasepy.fit.rst │ ├── phasepy.rst │ ├── phasepy.sgt.rst │ └── sgt │ ├── ethanolhexane_beta0.jpg │ ├── ethanolwater_beta0.jpg │ ├── sgt.jpg │ ├── sgt.tenbeta0.rst │ ├── sgt.tenpure.rst │ └── sgt.tensgt.rst ├── examples ├── 1. Components and mixtures.ipynb ├── 10. Square Gradient Theory for pure component.ipynb ├── 11. Square Gradient Theory for mixtures and beta = 0.ipynb ├── 12. Square Gradient Theory for mixtures and beta != 0.ipynb ├── 13. Fitting Interaction Parameters for Mixtures.ipynb ├── 14. Fitting Pure Component Data.ipynb ├── 15. Reuse of phasepy's functions.ipynb ├── 2. Property calculation from Cubic EoS.ipynb ├── 3. Phase stability test (tpd).ipynb ├── 4. Two phase flash (TP).ipynb ├── 5. Bubble Point calculation.ipynb ├── 6. Dew Point calculation.ipynb ├── 7. Liquid-Liquid Equilibria.ipynb ├── 8. Three phase equilibria (VLLE).ipynb └── 9. Solid-Fluid Equilibria (SLE and SLLE).ipynb ├── long_description.rst ├── phasepy ├── __init__.py ├── actmodels │ ├── __init__.py │ ├── nrtl.py │ ├── original_unifac.py │ ├── redlichkister.py │ ├── unifac.py │ ├── uniquac.py │ ├── virial.py │ ├── virialgama.py │ └── wilson.py ├── constants.py ├── cubic │ ├── __init__.py │ ├── alphas.py │ ├── cubic.py │ ├── cubicmix.py │ ├── cubicpure.py │ ├── mhv.py │ ├── mhv1.py │ ├── mixingrules.py │ ├── psatpure.py │ ├── qmr.py │ ├── tsatpure.py │ ├── vdwmix.py │ ├── vdwpure.py │ ├── volume_solver.py │ ├── vtcubicmix.py │ ├── vtcubicpure.py │ └── wongsandler.py ├── database │ ├── dortmund-2018.xlsx │ ├── dortmund-2021.xlsx │ ├── original-unifac.xlsx │ └── unifac.xlsx ├── equilibrium │ ├── __init__.py │ ├── bubble.py │ ├── dew.py │ ├── equilibriumresult.py │ ├── flash.py │ ├── hazb.py │ ├── hazt.py │ ├── lle.py │ ├── multiflash.py │ ├── solidequilibria.py │ └── stability.py ├── fit │ ├── __init__.py │ ├── binaryfit.py │ ├── fitcii.py │ ├── fitmulticomponent.py │ ├── fitpsat.py │ ├── fitvt.py │ └── ternaryfit.py ├── math.py ├── mixtures.py ├── saft_forcefield.py ├── sgt │ ├── __init__.py │ ├── coloc_z.py │ ├── coloc_z_ds.py │ ├── linear_spot.py │ ├── path_hk.py │ ├── path_sk.py │ ├── reference_component.py │ ├── sgt_beta0.py │ ├── sgtpure.py │ └── tensionresult.py └── src │ ├── actmodels_cy.pyx │ ├── cijmix_cy.pyx │ └── coloc_cy.pyx ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | #Compiled objects# 7 | *.c 8 | 9 | # C extensions 10 | *.so 11 | *.DS_Store 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | phasepy.egg-info/ 33 | phasepy/saftvrmie/ 34 | phasepy/__pycache__/ 35 | phasepy/actmodels/__pycache__/ 36 | phasepy/cubic/__pycache__/ 37 | phasepy/equilibrium/__pycache__/ 38 | phasepy/fit/__pycache__/ 39 | phasepy/sgt/__pycache__/ 40 | phasepy/src/build/ 41 | phasepy/src/setup.py 42 | 43 | upload_pypi.sh 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .vscode/settings.json 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Phasepy Changelog 2 | 3 | ## v0.0.55 4 | * Changed `cumtrapz` to `cumulative_trapezoid` function in the `path_sk` solver for SGT. (due to scipy deprecation) 5 | 6 | 7 | ## v0.0.54 8 | * Updated the function `multiflash_solid` function used for both `sle` and `slle` solvers. The updated version allows controlings thresholds for the values of phase fractions (beta) and phase stability variables (tetha). The updated version also modified the Gibbs minimization step, to first do some iterations without derivative information. The errors from the minimization step now are consistent with the ASS step (`error_inner` refers to the mass balance and `error_outer` refers to the phase equilibria error). The `full_output` option now returns the method used to compute equilibria. 9 | 10 | ## v0.0.53 11 | * Changed `np.int` to `int` (due to deprecation of `np.int`) 12 | 13 | ## v0.0.52 14 | 15 | * Initial support for perfomning solid-liquid and solid-liquid-liquid equilibria. `component` function accepts now the enthalpy and temperature of fusion. These are needed to compute the solid phase fugacity coefficient. 16 | * Functions `sle` and `slle` to compute solid-liquid and solid-liquid-liquid equilibria. Both function solve a flash that checks for phase stability. 17 | * Fix bug in van der Waals EoS for mixtures. The object didn't have implemented methods for computing properties using `eos.temperature_aux`. 18 | * Added option to have an unbalanced `kij` matrix for QMR. 19 | 20 | ## v0.0.51 21 | 22 | * Now you can create personalized cubic EoS by choosing its alpha function and c1 and c2 parameters. See the `cubiceos` function. 23 | 24 | 25 | ## v0.0.50 26 | 27 | * Now you can create mixtures by adding pure components (`+`) 28 | * Bug in Wong-Sandler mixing rule fixed 29 | * Updated Dortmund-UNIFAC database `(obtained July, 2021) `_. 30 | * MHV-1 mixing rule added for cubic equations of state 31 | 32 | 33 | ## v0.0.49 34 | 35 | * UNIQUAC model added 36 | * First Changelog! 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2016, Gustavo Chaparro , Andrés Mejía 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt README.rst, long_description.rst 2 | recursive-include cython * 3 | recursive-include *.pyx 4 | recursive-include *.c 5 | global-exclude __pycache__ 6 | global-exclude *.py[co] 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | phasepy 3 | ======= 4 | 5 | .. image:: https://mybinder.org/badge_logo.svg 6 | :target: https://mybinder.org/v2/gh/gustavochm/phasepy/master 7 | 8 | Introduction 9 | ------------ 10 | 11 | Phasepy is an open-source scientific Python package for calculation of 12 | `physical properties of phases `_ at 13 | `thermodynamic equilibrium `_. 14 | Main application areas include computation of fluid phase equilibria 15 | and interfacial properties. 16 | 17 | Phasepy includes routines for calculation of vapor-liquid equilibrium (VLE), 18 | liquid-liquid equilibrium (LLE) and vapor-liquid-liquid equilibrium 19 | (VLLE). Phase equilibrium can be modelled either with *the continous 20 | approach*, using a combination of a cubic equation of state (EoS, 21 | e.g. Van der Waals, Peng-Robinson, Redlich-Kwong, or their 22 | derivatives) model and a mixing rule (Quadratic, Modified Huron-Vidal 23 | or Wong-Sandler) for all phases, or *the discontinuous approach* using 24 | a virial equation for the vapor phase and an activity coefficient model 25 | (NRTL, Wilson, Redlich-Kister, UNIQUAC or Dortmund Modified UNIFAC) for the 26 | liquid phase(s). 27 | 28 | Interfacial property estimation using the continuous phase equilibrium 29 | approach allows calculation of density profiles and interfacial 30 | tension using the Square Gradient Theory (SGT). 31 | 32 | Phasepy supports fitting of model parameter values from experimental data. 33 | 34 | Installation Prerequisites 35 | -------------------------- 36 | - numpy 37 | - scipy 38 | - pandas 39 | - openpyxl 40 | - C/C++ Compiler for Cython extension modules 41 | 42 | Installation 43 | ------------ 44 | 45 | Get the latest version of phasepy from 46 | https://pypi.python.org/pypi/phasepy/ 47 | 48 | An easy installation option is to use Python pip: 49 | 50 | $ pip install phasepy 51 | 52 | Alternatively, you can build phasepy yourself using latest source 53 | files: 54 | 55 | $ git clone https://github.com/gustavochm/phasepy 56 | 57 | **Note for Apple Silicon users:** It is recommended to install python and phasepy dependencies (numpy, scipy, cython, pandas) through conda miniforge, then you can install phasepy running ``pip install phasepy``. 58 | 59 | 60 | Documentation 61 | ------------- 62 | 63 | Phasepy's documentation is available on the web: 64 | 65 | https://phasepy.readthedocs.io/en/latest/ 66 | 67 | 68 | Getting Started 69 | --------------- 70 | 71 | Base input variables include temperature [K], pressure [bar] and molar 72 | volume [cm^3/mol]. Specification of a mixture starts with 73 | specification of pure components: 74 | 75 | .. code-block:: python 76 | 77 | >>> from phasepy import component, mixture 78 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, 79 | w=0.344861, GC={'H2O':1}) 80 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, 81 | w=0.643558, GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 82 | >>> mix = mixture(ethanol, water) 83 | 84 | Here is an example how to calculate the bubble point vapor composition 85 | and pressure of saturated 50 mol-% ethanol - 50 mol-% water liquid 86 | mixture at temperature 320 K using Peng Robinson EoS. In this example 87 | the Modified Huron Vidal mixing rule utilizes the Dortmund Modified 88 | UNIFAC activity coefficient model for the solution of the mixture EoS. 89 | 90 | .. code-block:: python 91 | 92 | >>> mix.unifac() 93 | >>> from phasepy import preos 94 | >>> eos = preos(mix, 'mhv_unifac') 95 | >>> from phasepy.equilibrium import bubblePy 96 | >>> y_guess, P_guess = [0.2, 0.8], 1.0 97 | >>> bubblePy(y_guess, P_guess, X=[0.5, 0.5], T=320.0, model=eos) 98 | (array([0.70761727, 0.29238273]), 0.23248584919691206) 99 | 100 | For more examples, please have a look at the Jupyter Notebook files 101 | located in the *examples* folder of the sources or 102 | `view examples in github `_. 103 | 104 | 105 | Bug reports 106 | ----------- 107 | 108 | To report bugs, please use the phasepy's Bug Tracker at: 109 | 110 | https://github.com/gustavochm/phasepy/issues 111 | 112 | 113 | License information 114 | ------------------- 115 | 116 | See ``LICENSE.txt`` for information on the terms & conditions for usage 117 | of this software, and a DISCLAIMER OF ALL WARRANTIES. 118 | 119 | Although not required by the phasepy license, if it is convenient for you, 120 | please cite phasepy if used in your work. Please also consider contributing 121 | any changes you make back, and benefit the community. 122 | 123 | 124 | Chaparro, G., Mejía, A. Phasepy: A Python based framework for fluid phase 125 | equilibria and interfacial properties computation. J Comput Chem. 2020, 41, 29, 126 | 2504-2526. `https://doi.org/10.1002/jcc.26405 `_. 127 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = phasepy 8 | SOURCEDIR = source 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) -------------------------------------------------------------------------------- /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=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=phasepy 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/basic/phasepy.component.rst: -------------------------------------------------------------------------------- 1 | phasepy.component 2 | ================= 3 | 4 | :class:`phasepy.component` object stores pure component information needed for equilibria and interfacial properties computation. 5 | A component can be created as follows: 6 | 7 | .. code-block:: python 8 | 9 | >>> from phasepy import component 10 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 11 | Ant=[11.64785144, 3797.41566067, -46.77830444], 12 | GC={'H2O':1}) 13 | 14 | Besides storing pure component data, the class incorporates basics methods for e.g. saturation pressure evaluation using Antoine equation, and liquid volume estimation with Rackett equation. 15 | 16 | .. code-block:: python 17 | 18 | >>> water.psat(T=373.0) # vapor saturation pressure [bar] 19 | 1.0072796747419537 20 | >>> water.vlrackett(T=310.0) # liquid molar volume [cm3/mol] 21 | 16.46025809309672 22 | 23 | 24 | .. warning:: User is required to supply the necessary parameters for methods 25 | 26 | .. autoclass:: phasepy.component 27 | :members: 28 | 29 | -------------------------------------------------------------------------------- /docs/source/basic/phasepy.mixture.rst: -------------------------------------------------------------------------------- 1 | phasepy.mixture 2 | =============== 3 | 4 | :class:`phasepy.mixture` object stores both pure component and mixture 5 | related information and interaction parameters needed for equilibria 6 | and interfacial properties computation. 7 | Two pure components are required to create a base mixture: 8 | 9 | .. code-block:: python 10 | 11 | >>> import numpy as np 12 | >>> from phasepy import component, mixture 13 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 14 | Ant=[11.64785144, 3797.41566067, -46.77830444], 15 | GC={'H2O':1}) 16 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 17 | Ant=[11.61809279, 3423.0259436, -56.48094263], 18 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 19 | >>> mix = mixture(ethanol, water) 20 | 21 | Additional components can be added to the mixture with 22 | :func:`phasepy.mixture.add_component`. 23 | 24 | .. code-block:: python 25 | 26 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 27 | Ant=[9.16238246, 2541.97883529, -50.40534341], 28 | GC={'CH3':3, 'CH3O':1, 'C':1}) 29 | >>> mix.add_component(mtbe) 30 | 31 | Once all components have been added to the mixture, the interaction 32 | parameters must be supplied using a function depending on which model 33 | will be used: 34 | 35 | For quadratic mixing rule (QMR) used in cubic EoS: 36 | 37 | >>> kij = np.array([[0, k12, k13], 38 | [k21, 0, k23], 39 | [k31, k32, 0]]) 40 | >>> mix.kij_cubic(kij) 41 | 42 | For NRTL model: 43 | 44 | >>> alpha = np.array([[0, alpha12, alpha13], 45 | [alpha21, 0, alpha23], 46 | [alpha31, alpha32, 0]]) 47 | >>> g = np.array([[0, g12, g13], 48 | [g21, 0, g23], 49 | [g31, g32, 0]]) 50 | >>> g1 = np.array([[0, gT12, gT13], 51 | [gT21, 0, gT23], 52 | [gT31, gT32, 0]]) 53 | >>> mix.NRTL(alpha, g, g1) 54 | 55 | For Wilson model: 56 | 57 | >>> A = np.array([[0, A12, A13], 58 | [A21, 0, A23], 59 | [A31, A32, 0]]) 60 | >>> mix.wilson(A) 61 | 62 | For Redlich Kister parameters are set by polynomial by pairs, the 63 | order of the pairs must be the following: 64 | 1-2, 1-3, ..., 1-n, 2-3, ..., 2-n, etc. 65 | 66 | >>> C0 = np.array([poly12], [poly13], [poly23]] 67 | >>> C1 = np.array([polyT12], [polyT13], [polyT23]] 68 | >>> mix.rk(C0, C1) 69 | 70 | For Modified-UNIFAC model, Dortmund public database must be read in: 71 | 72 | >>> mix.unifac() 73 | 74 | .. warning:: User is required to supply the necessary parameters for methods 75 | 76 | 77 | 78 | .. autoclass:: phasepy.mixture 79 | :members: 80 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'phasepy' 23 | copyright = '2019, Gustavo Chaparro M.' 24 | author = 'Gustavo Chaparro M., Andrés Mejía M.' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '0.0.1' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.mathjax', 47 | 'sphinx.ext.ifconfig', 48 | 'sphinx.ext.viewcode', 49 | 'sphinx.ext.githubpages', 50 | 'sphinx.ext.napoleon' 51 | ] 52 | 53 | add_module_names = False 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This pattern also affects html_static_path and html_extra_path . 77 | exclude_patterns = [] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = 'sphinx' 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'sphinx_rtd_theme' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # Custom sidebar templates, must be a dictionary that maps document names 102 | # to template names. 103 | # 104 | # The default sidebars (for documents that don't match any pattern) are 105 | # defined by theme itself. Builtin themes are using these templates by 106 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 107 | # 'searchbox.html']``. 108 | # 109 | # html_sidebars = {} 110 | 111 | 112 | # -- Options for HTMLHelp output --------------------------------------------- 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'phasepydoc' 116 | 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'phasepy.tex', 'phasepy Documentation', 143 | 'Gustavo Chaparro M.', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output ------------------------------------------ 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'phasepy', 'phasepy Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'phasepy', 'phasepy Documentation', 164 | author, 'phasepy', 'One line description of project.', 165 | 'Miscellaneous'), 166 | ] 167 | 168 | 169 | # -- Extension configuration ------------------------------------------------- 170 | 171 | # -- Options for todo extension ---------------------------------------------- 172 | 173 | # If true, `todo` and `todoList` produce output, else they produce nothing. 174 | todo_include_todos = True -------------------------------------------------------------------------------- /docs/source/equilibrium/phasepy.elv.rst: -------------------------------------------------------------------------------- 1 | Bubble Point and Dew Point 2 | ========================== 3 | 4 | Calculation of 5 | `bubble point `_ and 6 | `dew point `_ 7 | for vapor-liquid systems apply simplifications of the Rachford-Rice 8 | mass balance, as the liquid phase fraction (0% or 100%) is already 9 | known. Default solution strategy applies first Accelerated Successive 10 | Substitutions method to update phase compositions in an inner loop, 11 | and a Quasi-Newton method to update pressure or temperature in an 12 | outer loop. If convergence is not reached in 10 iterations, or if user 13 | defines initial estimates to be good, the algorithm switches to a 14 | Phase Envelope method solving the following system of equations: 15 | 16 | .. math:: 17 | 18 | f_i &= \ln K_i + \ln \hat{\phi}_i^v(\underline{y}, T, P) -\ln \hat{\phi}_i^l(\underline{x}, T, P) \quad i = 1,...,c \\ 19 | f_{c+1} &= \sum_{i=1}^c (y_i-x_i) 20 | 21 | Bubble Point 22 | ------------ 23 | 24 | Bubble point is a fluid state where saturated liquid of known 25 | composition and liquid fraction 100% is forming a differential size 26 | bubble. The algorithm finds vapor phase composition and either 27 | temperature or pressure of the bubble point. 28 | 29 | The algorithm solves composition using a simplified Rachford-Rice 30 | equation: 31 | 32 | .. math:: 33 | 34 | FO = \sum_{i=1}^c x_i (K_i-1) = \sum_{i=1}^c y_i -1 = 0 35 | 36 | 37 | >>> import numpy as np 38 | >>> from phasepy import component, mixture, rkseos 39 | >>> from phasepy.equilibrium import bubbleTy, bubblePy 40 | >>> butanol = component(name='butanol', Tc=563.0, Pc=44.14, Zc=0.258, Vc=274.0, w=0.589462, 41 | Ant=[10.20170373, 2939.88668723, -102.28265042]) 42 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 43 | Ant=[9.16238246, 2541.97883529, -50.40534341]) 44 | >>> Kij = np.zeros([2,2]) 45 | >>> mix = mixture(mtbe, butanol) 46 | >>> mix.kij_cubic(Kij) 47 | >>> eos = rkseos(mix, 'qmr') 48 | >>> x = np.array([0.5, 0.5]) 49 | >>> P0, T0 = 1.0, 340.0 50 | >>> y0 = np.array([0.8, 0.2]) 51 | >>> bubbleTy(y0, T0, x, 1.0, eos) # vapor fractions, temperature 52 | (array([0.90411878, 0.09588122]), 343.5331023048577) 53 | >>> bubblePy(y0, P0, x, 343.533, eos) # vapor fractions, pressure 54 | (array([0.90411894, 0.09588106]), 0.9999969450754181) 55 | 56 | .. automodule:: phasepy.equilibrium.bubble 57 | :members: bubbleTy, bubblePy 58 | :undoc-members: 59 | :show-inheritance: 60 | :noindex: 61 | 62 | 63 | Dew Point 64 | --------- 65 | 66 | Dew point is a fluid state where saturated vapor of known composition 67 | and liquid fraction 0% is forming a differential size liquid 68 | droplet. The algorithm finds liquid phase composition and either 69 | temperature or pressure of the dew point. 70 | 71 | The algorithm solves composition using a simplified Rachford-Rice 72 | equation: 73 | 74 | .. math:: 75 | FO = 1 - \sum_{i=1}^c \frac{y_i}{K_i} = 1 - \sum_{i=1}^c x_i = 0 76 | 77 | 78 | >>> import numpy as np 79 | >>> from phasepy import component, mixture, prsveos 80 | >>> from phasepy.equilibrium import dewPx, dewTx 81 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 82 | ... ksv=[1.27092923, 0.0440421], 83 | ... GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 84 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 85 | ... ksv=[0.76429651, 0.04242646], 86 | ... GC={'CH3':3, 'CH3O':1, 'C':1}) 87 | >>> mix = mixture(mtbe, ethanol) 88 | >>> C0 = np.array([0.02635196, -0.02855964, 0.01592515]) 89 | >>> C1 = np.array([312.575789, 50.1476555, 5.13981131]) 90 | >>> mix.rk(C0, C1) 91 | >>> eos = prsveos(mix, mixrule = 'mhv_rk') 92 | >>> y = np.array([0.5, 0.5]) 93 | >>> P0, T0 = 1.0, 340.0 94 | >>> x0 = np.array([0.2, 0.8]) 95 | >>> dewPx(x0, P0, y, 340.0, eos) # liquid fractions, pressure 96 | (array([0.20223477, 0.79776523]), 1.0478247383242278) 97 | >>> dewTx(x0, T0, y, 1.047825, eos) # liquid fractions, temperature 98 | (array([0.20223478, 0.79776522]), 340.0000061757033) 99 | 100 | 101 | .. automodule:: phasepy.equilibrium.dew 102 | :members: dewPx, dewTx 103 | :undoc-members: 104 | :show-inheritance: 105 | :noindex: 106 | -------------------------------------------------------------------------------- /docs/source/equilibrium/phasepy.flash.rst: -------------------------------------------------------------------------------- 1 | Two-Phase Flash 2 | =============== 3 | 4 | Determination of phase composition in 5 | `flash evaporation `_ 6 | is a classical phase equilibrium problem. In the simplest case covered 7 | here, temperature, pressure and overall composition of a system are 8 | known (PT flash). If the system is thermodynamically unstable it will 9 | form two (or more) distinct phases. The flash algorithm first solves 10 | the Rachford-Rice mass balance and then updates composition by the 11 | Accelerated Successive Substitution method. 12 | 13 | .. math:: 14 | FO = \sum_{i=1}^c \left( x_i^\beta - x_i^\alpha \right) = \sum_{i=1}^c \frac{z_i (K_i-1)}{1+\psi (K_i-1)} 15 | 16 | :math:`x` is molar fraction of a component in a phase, 17 | :math:`z` is the overall molar fraction of component in the system, 18 | :math:`K = x^\beta / x^\alpha` is the equilibrium constant and 19 | :math:`\psi` is the phase fraction of phase :math:`\beta` in the system. 20 | Subscript refers to component index and superscript refers to phase 21 | index. 22 | 23 | If convergence is not reached in three iterations, the algorithm 24 | switches to a second order minimization of the Gibbs free energy of 25 | the system: 26 | 27 | .. math:: 28 | min \, {G(\underline{F}^\alpha, \underline{F}^\beta)} = \sum_{i=1}^c (F_i^\alpha \ln \hat{f}_i^\alpha + F_i^\beta \ln \hat{f}_i^\beta) 29 | 30 | :math:`F` is the molar amount of component in a phase and 31 | :math:`\hat{f}` is the effective fugacity. 32 | 33 | .. warning:: 34 | 35 | ``flash()`` routine does not check for the stability of the numerical 36 | solution (see :ref:`stability`). 37 | 38 | Vapor-Liquid Equilibrium 39 | ------------------------ 40 | 41 | This example shows solution of vapor-liquid equilibrium (VLE) using 42 | the ``flash()`` function. 43 | 44 | >>> import numpy as np 45 | >>> from phasepy import component, mixture, preos 46 | >>> from phasepy.equilibrium import flash 47 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 48 | Ant=[11.64785144, 3797.41566067, -46.77830444], 49 | GC={'H2O':1}) 50 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 51 | Ant=[11.61809279, 3423.0259436, -56.48094263], 52 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 53 | >>> mix = mixture(water, ethanol) 54 | >>> mix.unifac() 55 | >>> eos = preos(mix, 'mhv_unifac') 56 | >>> T = 360.0 57 | >>> P = 1.01 58 | >>> Z = np.array([0.8, 0.2]) 59 | >>> x0 = np.array([0.1, 0.9]) 60 | >>> y0 = np.array([0.2, 0.8]) 61 | >>> flash(x0, y0, 'LV', Z, T, P, eos) # phase compositions, vapor phase fraction 62 | (array([0.8979481, 0.1020519]), array([0.53414948, 0.46585052]), 0.26923713078124695) 63 | 64 | 65 | .. automodule:: phasepy.equilibrium.flash 66 | :members: flash 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | 71 | Liquid-Liquid Equilibrium 72 | ------------------------- 73 | 74 | For liquid-liquid equilibrium (LLE), it is important to consider 75 | stability of the phases. ``lle()`` function takes into account both 76 | stability and equilibrium simultaneously for the PT flash. 77 | 78 | >>> import numpy as np 79 | >>> from phasepy import component, mixture, virialgamma 80 | >>> from phasepy.equilibrium import lle 81 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 82 | Ant=[11.64785144, 3797.41566067, -46.77830444], 83 | GC={'H2O':1}) 84 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 85 | Ant=[9.16238246, 2541.97883529, -50.40534341], 86 | GC={'CH3':3, 'CH3O':1, 'C':1}) 87 | >>> mix = mixture(water, mtbe) 88 | >>> mix.unifac() 89 | >>> eos = virialgamma(mix, actmodel = 'unifac') 90 | >>> T = 320.0 91 | >>> P = 1.01 92 | >>> Z = np.array([0.5, 0.5]) 93 | >>> x0 = np.array([0.01, 0.99]) 94 | >>> w0 = np.array([0.99, 0.01]) 95 | >>> lle(x0, w0, Z, T, P, eos) # phase compositions, phase 2 fraction 96 | (array([0.1560131, 0.8439869]), array([0.99289324, 0.00710676]), 0.4110348438873743) 97 | 98 | .. automodule:: phasepy.equilibrium.ell 99 | :members: lle 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | Liquid-liquid flash can be also solved without considering stability by 104 | using the ``flash()`` function, but this is not recommended. 105 | 106 | >>> from phasepy.equilibrium import flash 107 | >>> flash(x0, w0, 'LL', Z, T, P, eos) # phase compositions, phase 2 fraction 108 | (array([0.1560003, 0.8439997]), array([0.99289323, 0.00710677]), 0.41104385845638447) 109 | 110 | -------------------------------------------------------------------------------- /docs/source/equilibrium/phasepy.multiflash.rst: -------------------------------------------------------------------------------- 1 | Vapor-Liquid-Liquid Equilibrium 2 | =============================== 3 | 4 | To avoid meta-stable solutions in vapor-liquid-liquid equilibrium 5 | (VLLE) calculations, phasepy applies a modified Rachford-Rice mass 6 | balance system of equations by Gupta et al, which allows to verify the 7 | stability and equilibria of the phases simultaneously. 8 | 9 | .. math:: 10 | 11 | \sum_{i=1}^c \frac{z_i (K_{ik} \exp{\theta_k}-1)}{1+ \sum\limits^{\pi}_{\substack{j=1 \\ j \neq r}}{\psi_j (K_{ij}} \exp{\theta_j} -1)} = 0 \qquad k = 1,..., \pi, k \neq r 12 | 13 | :math:`\theta` is a stability variable. Positive value indicates 14 | unstable phase and zero value a stable phase. If default solution 15 | using Accelerated Successive Substitution and Newton's method does not 16 | converge fast, the algorith will switch to minimization of the Gibbs 17 | free energy of the system: 18 | 19 | .. math:: 20 | min \, {G} = \sum_{k=1}^\pi \sum_{i=1}^c F_{ik} \ln \hat{f}_{ik} 21 | 22 | 23 | Multicomponent Flash 24 | -------------------- 25 | 26 | Function ``vlle()`` solves VLLE for mixtures with two or more 27 | components given overall molar fractions *Z*, temperature and 28 | pressure. 29 | 30 | >>> import numpy as np 31 | >>> from phasepy import component, mixture, virialgamma 32 | >>> from phasepy.equilibrium import vlle 33 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 34 | Ant=[11.64785144, 3797.41566067, -46.77830444], 35 | GC={'H2O':1}) 36 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 37 | Ant=[9.16238246, 2541.97883529, -50.40534341], 38 | GC={'CH3':3, 'CH3O':1, 'C':1}) 39 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 40 | Ant=[11.61809279, 3423.0259436, -56.48094263], 41 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 42 | >>> mix = mixture(water, mtbe) 43 | >>> mix.add_component(ethanol) 44 | >>> mix.unifac() 45 | >>> eos = virialgamma(mix, actmodel='unifac') 46 | >>> x0 = np.array([0.95, 0.025, 0.025]) 47 | >>> w0 = np.array([0.4, 0.5, 0.1]) 48 | >>> y0 = np.array([0.15, 0.8, 0.05]) 49 | >>> Z = np.array([0.5, 0.44, 0.06]) 50 | >>> T = 328.5 51 | >>> P = 1.01 52 | >>> vlle(x0, w0, y0, Z, T, P, eos, full_output=True) 53 | T: 328.5 54 | P: 1.01 55 | error_outer: 3.985492841236682e-08 56 | error_inner: 3.4482008487377304e-10 57 | iter: 14 58 | beta: array([0.41457868, 0.22479531, 0.36062601]) 59 | tetha: array([0., 0., 0.]) 60 | X: array([[0.946738 , 0.01222701, 0.04103499], 61 | [0.23284911, 0.67121402, 0.09593687], 62 | [0.15295408, 0.78764474, 0.05940118]]) 63 | v: [None, None, None] 64 | states: ['L', 'L', 'V'] 65 | 66 | 67 | .. automodule:: phasepy.equilibrium.hazt 68 | :members: vlle 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | 73 | Binary Mixture Composition 74 | -------------------------- 75 | 76 | The function ``vlleb()`` can solve a special case to find component 77 | molar fractions in each of the three phases in a VLLE system 78 | containing only two components. Given either temperature or pressure, 79 | ``vlleb()`` solves the other, along with the component molar fractions. 80 | This system is fully defined (zero degrees of freedom) according to the 81 | `Gibbs phase rule `_. 82 | 83 | >>> import numpy as np 84 | >>> from phasepy import component, mixture, virialgamma 85 | >>> from phasepy.equilibrium import vlleb 86 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 87 | Ant=[11.64785144, 3797.41566067, -46.77830444], 88 | GC={'H2O':1}) 89 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 90 | Ant=[9.16238246, 2541.97883529, -50.40534341], 91 | GC={'CH3':3, 'CH3O':1, 'C':1}) 92 | >>> mix = mixture(water, mtbe) 93 | >>> mix.unifac() 94 | >>> eos = virialgamma(mix, actmodel='unifac') 95 | >>> x0 = np.array([0.01, 0.99]) 96 | >>> w0 = np.array([0.99, 0.01]) 97 | >>> y0 = (x0 + w0)/2 98 | >>> T0 = 320.0 99 | >>> P = 1.01 100 | >>> vlleb(x0, w0, y0, T0, P, 'P', eos, full_output=True) 101 | T: array([327.60666698]) 102 | P: 1.01 103 | error: 4.142157056965187e-12 104 | nfev: 17 105 | X: array([0.17165659, 0.82834341]) 106 | vx: None 107 | statex: 'Liquid' 108 | W: array([0.99256232, 0.00743768]) 109 | vw: None 110 | statew: 'Liquid' 111 | Y: array([0.15177615, 0.84822385]) 112 | vy: None 113 | statey: 'Vapor' 114 | 115 | .. automodule:: phasepy.equilibrium.hazb 116 | :members: vlleb 117 | :undoc-members: 118 | :show-inheritance: 119 | -------------------------------------------------------------------------------- /docs/source/equilibrium/phasepy.stability.rst: -------------------------------------------------------------------------------- 1 | .. _stability: 2 | 3 | Phase Stability Analysis 4 | ======================== 5 | 6 | Stability tests give a relative estimate of how stable a phase (given 7 | temperature, pressure and composition) is thermodynamically. A stable 8 | phase decreases the Gibbs free energy of the system compared to a 9 | reference phase (e.g. the overall composition of a mixture). To 10 | evaluate the relative stability, phasepy applies the Tangent Plane 11 | Distance (TPD) function introduced by Michelsen. 12 | 13 | .. math:: 14 | 15 | F_{TPD}(w) = \sum_{i=1}^c w_i \left[\ln w_i + \ln \hat{\phi}_i(w) - \ln z_i - \ln \hat{\phi}_i(z) \right] 16 | 17 | First, the TPD function is minimized locally w.r.t. phase composition 18 | :math:`w`, starting from an initial composition. If the TPD value at 19 | the minimum is negative, it implies that the phase is less stable than 20 | the reference. A positive value means more stable than the reference. 21 | 22 | The function ``tpd_min()`` can be applied to find a phase composition 23 | from given initial values, and the TPD value of the minimized 24 | result. Negative TPD value for the first example (trial phase is 25 | liquid) means that the resulting liquid phase composition is 26 | unstable. Positive TPD value for the second example (trial phase is 27 | vapor) means that the resulting vapor phase is more stable than the 28 | reference phase. Therefore the second solution could be used e.g. as 29 | an initial estimate for two-phase flash calculation. 30 | 31 | >>> import numpy as np 32 | >>> from phasepy import component, mixture, preos 33 | >>> from phasepy.equilibrium import tpd_min 34 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 35 | Ant=[11.64785144, 3797.41566067, -46.77830444], 36 | GC={'H2O':1}) 37 | >>> mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059, 38 | Ant=[9.16238246, 2541.97883529, -50.40534341], 39 | GC={'CH3':3, 'CH3O':1, 'C':1}) 40 | >>> mix = mixture(water, mtbe) 41 | >>> mix.unifac() 42 | >>> eos = preos(mix, 'mhv_unifac') 43 | >>> w = np.array([0.01, 0.99]) 44 | >>> z = np.array([0.5, 0.5]) 45 | >>> T = 320.0 46 | >>> P = 1.01 47 | >>> tpd_min(w, z, T, P, eos, stateW='L', stateZ='L') # molar fractions and TPD value 48 | (array([0.30683438, 0.69316562]), -0.005923915138229763) 49 | >>> tpd_min(w, z, T, P, eos, stateW='V', stateZ='L') # molar fractions and TPD value 50 | (array([0.16434188, 0.83565812]), 0.24576563932356765) 51 | 52 | 53 | .. automodule:: phasepy.equilibrium.stability 54 | :members: tpd_min 55 | :undoc-members: 56 | :show-inheritance: 57 | :noindex: 58 | 59 | 60 | Phasepy function ``tpd_minimas()`` can be used to try to find 61 | several TPD minima. The function uses random initial compositions to 62 | search for minima, so it can find the same minimum multiple times. 63 | 64 | >>> from phasepy.equilibrium import tpd_minimas 65 | >>> nmin = 3 66 | >>> tpd_minimas(nmin, z, T, P, eos, 'L', 'L') # minima and TPD values (two unstable minima) 67 | (array([0.99538258, 0.00461742]), array([0.30683414, 0.69316586]), array([0.99538258, 0.00461742])), 68 | array([-0.33722905, -0.00592392, -0.33722905]) 69 | >>> tpd_minimas(nmin, z, T, P, eos, 'V', 'L') # minima and TPD values (one stable minimum) 70 | (array([0.1643422, 0.8356578]), array([0.1643422, 0.8356578]), array([0.1643422, 0.8356578])), 71 | array([0.24576564, 0.24576564, 0.24576564]) 72 | 73 | .. automodule:: phasepy.equilibrium.stability 74 | :members: tpd_minimas 75 | :undoc-members: 76 | :show-inheritance: 77 | :noindex: 78 | 79 | 80 | Function ``lle_init()`` can be used to generate two phase compositions 81 | e.g. for subsequent liquid liquid equilibrium calculations. 82 | Note that the results are not guaranteed to be stable or even different. 83 | 84 | >>> from phasepy.equilibrium import lle_init 85 | >>> lle_init(z, T, P, eos) 86 | array([0.99538258, 0.00461742]), array([0.30683414, 0.69316586]) 87 | 88 | 89 | .. automodule:: phasepy.equilibrium.stability 90 | :members: lle_init 91 | :undoc-members: 92 | :show-inheritance: 93 | :noindex: 94 | -------------------------------------------------------------------------------- /docs/source/fit/binaryfit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/docs/source/fit/binaryfit.jpg -------------------------------------------------------------------------------- /docs/source/fit/fit.pure.rst: -------------------------------------------------------------------------------- 1 | Pure component data 2 | =================== 3 | 4 | Depending on the model that will be used, different information might be needed. For general purposes there might be necessary to fit saturation pressure, liquid density and interfacial tension. Experimental data is needed, in this case water saturation properties is obtanied from NIST database, more experimental data can be found in DIPPR, TDE, Knovel or by your own measurements. 5 | 6 | >>> #Experimental Saturation Data of water obtained from NIST 7 | >>> #Saturation Temperature in Kelvin 8 | >>> Tsat = np.array([290., 300., 310., 320., 330., 340., 350., 9 | ... 360., 370., 380.]) 10 | >>> #Saturation Pressure in bar 11 | >>> Psat = np.array([0.0192 , 0.035368, 0.062311, 0.10546 , 12 | ... 0.17213 , 0.27188 , 0.41682 , 0.62194 , 0.90535 , 1.2885 ]) 13 | >>> #Saturated Liquid density in mol/cm3 14 | >>> rhol = np.array([0.05544 , 0.055315, 0.055139, 0.054919, 15 | ... 0.054662, 0.054371, 0.054049, 0.053698, 0.053321, 0.052918]) 16 | >>> #Interfacial Tension in mN/m 17 | >>> tension = np.array([73.21 , 71.686, 70.106, 68.47 , 66.781, 18 | ... 65.04 , 63.248, 61.406, 59.517, 57.581]) 19 | 20 | Saturation Pressure 21 | ------------------- 22 | 23 | First, Antoine Coefficients can be fitted to the following form: 24 | 25 | .. math:: 26 | \ln P = A - \frac{B}{T + C} 27 | 28 | The model is fitted directly with Phasepy, optionally an initial guess can be passed. 29 | 30 | >>> #Fitting Antoine Coefficients 31 | >>> from phasepy.fit import fit_ant 32 | >>> Ant = fit_ant(Tsat, Psat) 33 | >>> #Objection function value, Antoine Parameters 34 | >>> 5.1205342479858257e-05, [1.34826650e+01, 5.02634690e+03, 9.07664231e-04] 35 | >>> #Optionally an initial guess for the parameters can be passed to the function 36 | >>> Ant = fit_ant(Tsat, Psat, x0 = [11, 3800, -44]) 37 | >>> #Objection function value, Antoine Parameters 38 | >>> 2.423780448316938e-07,[ 11.6573823 , 3800.11357063, -46.77260501] 39 | 40 | .. automodule:: phasepy.fit.fitpsat 41 | :members: fit_ant 42 | :undoc-members: 43 | :show-inheritance: 44 | :noindex: 45 | 46 | 47 | Following with saturation pressure fitting, when using PRSV EoS 48 | it is necessary to fit :math:`\alpha` parameters, for these purpose a component has to be defined and then using the experimental data the parameters can be fitted. 49 | 50 | .. math:: 51 | \alpha = (1 + k_1 [1 - \sqrt{T_r}] + k_2 [1- T_r][0.7 - T_r])^2 52 | 53 | >>> #Fitting ksv for PRSV EoS 54 | >>> from phasepy.fit import fit_ksv 55 | >>> #parameters of pure component obtained from DIPPR 56 | >>> name = 'water' 57 | >>> Tc = 647.13 #K 58 | >>> Pc = 220.55 #bar 59 | >>> Zc = 0.229 60 | >>> Vc = 55.948 #cm3/mol 61 | >>> w = 0.344861 62 | >>> pure = component(name = name, Tc = Tc, Pc = Pc, Zc = Zc, 63 | ... Vc = Vc, w = w) 64 | >>> ksv = fit_ksv(pure, Tsat, Psat) 65 | >>> #Objection function value, ksv Parameters 66 | >>> 1.5233471126821199e-10, [ 0.87185176, -0.06621339] 67 | 68 | 69 | 70 | .. automodule:: phasepy.fit.fitpsat 71 | :members: fit_ksv 72 | :undoc-members: 73 | :show-inheritance: 74 | :noindex: 75 | 76 | 77 | 78 | Volume Translation 79 | ------------------ 80 | 81 | When working with cubic EoS, there might an interest for a better prediction of liquid densities, this can be done by a volume translation. This volume correction doesn't change equilibria results and its parameters is obtained in Phasepy as follows: 82 | 83 | 84 | >>> from phasepy import prsveos 85 | >>> from phasepy.fit import fit_vt 86 | >>> #Defining the component with the optimized alpha parameters 87 | >>> pure = component(name = name, Tc = Tc, Pc = Pc, Zc = Zc, 88 | ... Vc = Vc, w = w, ksv = [ 0.87185176, -0.06621339] ) 89 | >>> vt = fit_vt(pure, prsveos, Tsat, Psat, rhol) 90 | >>> #Objetive function and volume translation 91 | >>> 0.001270834833817397, 3.46862174 92 | 93 | .. automodule:: phasepy.fit.fitvt 94 | :members: fit_vt 95 | :undoc-members: 96 | :show-inheritance: 97 | :noindex: 98 | 99 | 100 | Influence parameter for SGT 101 | --------------------------- 102 | 103 | Finally, influence parameters are necessary to compute interfacial properties, these can be fitted with experimental interfacial tension. 104 | 105 | 106 | >>> from phasepy.fit import fit_cii 107 | >>> #Defining the component with the volume traslation parameter. 108 | >>> pure = component(name = name, Tc = Tc, Pc = Pc, Zc = Zc, 109 | ... Vc = Vc, w = w, ksv = [ 0.87185176, -0.06621339], c = 3.46862174) 110 | >>> eos = prsveos(pure, volume_translation = False) 111 | >>> cii = fit_cii(tension, Tsat, eos, order = 2) 112 | >>> #fitted influence parameter polynomial 113 | >>> [2.06553362e-26, 2.64204784e-23, 4.10320513e-21] 114 | 115 | Beware that influence parameter in cubic EoS absorbs density deviations from experimental data, if there is any volume correction the influence parameter will change. 116 | 117 | >>> eos = prsveos(pure, volume_translation = True) 118 | >>> cii = fit_cii(tension, Tsat, eos, order = 2) 119 | >>> #fitted influence parameter polynomial with volume translation 120 | >>> [2.74008872e-26, 1.23088986e-23, 3.05681188e-21] 121 | 122 | .. automodule:: phasepy.fit.fitcii 123 | :members: fit_cii 124 | :undoc-members: 125 | :show-inheritance: 126 | :noindex: 127 | 128 | 129 | The perfomance of the fitted interaction parameters can be compared against the experimental data (dots), the following figure shows the behavior of the cubic EoS for the saturation pressure, liquid density and interfacial tension (oranges lines). 130 | 131 | .. image:: purefit.jpg 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/source/fit/purefit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/docs/source/fit/purefit.jpg -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | phasepy 3 | ======= 4 | 5 | Introduction 6 | ------------ 7 | 8 | Phasepy is an open-source scientific Python package for calculation of 9 | `physical properties of phases `_ at 10 | `thermodynamic equilibrium `_. 11 | Main application areas include computation of fluid phase equilibria 12 | and interfacial properties. 13 | 14 | Phasepy includes routines for calculation of vapor-liquid equilibrium (VLE), 15 | liquid-liquid equilibrium (LLE) and vapor-liquid-liquid equilibrium 16 | (VLLE). Phase equilibrium can be modelled either with *the continous 17 | approach*, using a combination of a cubic equation of state (EoS, 18 | e.g. Van der Waals, Peng-Robinson, Redlich-Kwong, or their 19 | derivatives) model and a mixing rule (Quadratic, Modified Huron-Vidal 20 | or Wong-Sandler) for all phases, or *the discontinuous approach* using 21 | a virial equation for the vapor phase and an activity coefficient model 22 | (NRTL, Wilson, Redlich-Kister or Dortmund Modified UNIFAC) for the 23 | liquid phase(s). 24 | 25 | Interfacial property estimation using the continuous phase equilibrium 26 | approach allows calculation of density profiles and interfacial 27 | tension using the Square Gradient Theory (SGT). 28 | 29 | Phasepy supports fitting of model parameter values from experimental data. 30 | 31 | Installation Prerequisites 32 | -------------------------- 33 | - numpy 34 | - scipy 35 | - pandas 36 | - openpyxl 37 | - C/C++ Compiler for Cython extension modules 38 | 39 | Installation 40 | ------------ 41 | 42 | Get the latest version of phasepy from 43 | https://pypi.python.org/pypi/phasepy/ 44 | 45 | An easy installation option is to use Python pip: 46 | 47 | $ pip install phasepy 48 | 49 | Alternatively, you can build phasepy yourself using latest source 50 | files: 51 | 52 | $ git clone https://github.com/gustavochm/phasepy 53 | 54 | 55 | Documentation 56 | ------------- 57 | 58 | Phasepy's documentation is available on the web: 59 | 60 | https://phasepy.readthedocs.io/en/latest/ 61 | 62 | 63 | Getting Started 64 | --------------- 65 | 66 | Base input variables include temperature [K], pressure [bar] and molar 67 | volume [cm^3/mol]. Specification of a mixture starts with 68 | specification of pure components: 69 | 70 | .. code-block:: python 71 | 72 | >>> from phasepy import component, mixture 73 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, 74 | w=0.344861, GC={'H2O':1}) 75 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, 76 | w=0.643558, GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 77 | >>> mix = mixture(ethanol, water) 78 | 79 | Here is an example how to calculate the bubble point vapor composition 80 | and pressure of saturated 50 mol-% ethanol - 50 mol-% water liquid 81 | mixture at temperature 320 K using Peng Robinson EoS. In this example 82 | the Modified Huron Vidal mixing rule utilizes the Dortmund Modified 83 | UNIFAC activity coefficient model for the solution of the mixture EoS. 84 | 85 | .. code-block:: python 86 | 87 | >>> mix.unifac() 88 | >>> from phasepy import preos 89 | >>> eos = preos(mix, 'mhv_unifac') 90 | >>> from phasepy.equilibrium import bubblePy 91 | >>> y_guess, P_guess = [0.2, 0.8], 1.0 92 | >>> bubblePy(y_guess, P_guess, X=[0.5, 0.5], T=320.0, model=eos) 93 | (array([0.70761727, 0.29238273]), 0.23248584919691206) 94 | 95 | For more examples, please have a look at the Jupyter Notebook files 96 | located in the *examples* folder of the sources or 97 | `view examples in github `_. 98 | 99 | 100 | Bug reports 101 | ----------- 102 | 103 | To report bugs, please use the phasepy's Bug Tracker at: 104 | 105 | https://github.com/gustavochm/phasepy/issues 106 | 107 | 108 | License information 109 | ------------------- 110 | 111 | See ``LICENSE.txt`` for information on the terms & conditions for usage 112 | of this software, and a DISCLAIMER OF ALL WARRANTIES. 113 | 114 | Although not required by the phasepy license, if it is convenient for you, 115 | please cite phasepy if used in your work. Please also consider contributing 116 | any changes you make back, and benefit the community. 117 | 118 | 119 | Chaparro, G., Mejía, A. Phasepy: A Python based framework for fluid phase 120 | equilibria and interfacial properties computation. 121 | J Comput Chem. 2020; 1– 23. `https://doi.org/10.1002/jcc.26405 `_. 122 | 123 | 124 | .. toctree:: 125 | :maxdepth: 1 126 | :caption: Contents: 127 | 128 | phasepy 129 | phasepy.equilibrium 130 | phasepy.sgt 131 | phasepy.fit 132 | 133 | 134 | Indices and Search 135 | ================== 136 | 137 | * :ref:`genindex` 138 | * :ref:`modindex` 139 | * :ref:`search` 140 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.actmodels.rst: -------------------------------------------------------------------------------- 1 | Activity coefficient models 2 | =========================== 3 | 4 | .. automodule:: phasepy.actmodels.nrtl 5 | :members: nrtl 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | .. automodule:: phasepy.actmodels.wilson 10 | :members: wilson 11 | :undoc-members: 12 | :show-inheritance: 13 | 14 | .. automodule:: phasepy.actmodels.redlichkister 15 | :members: rk 16 | :undoc-members: 17 | :show-inheritance: 18 | 19 | .. automodule:: phasepy.actmodels.unifac 20 | :members: unifac 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.cubic.rst: -------------------------------------------------------------------------------- 1 | Cubic Equation of State Model 2 | ============================= 3 | 4 | This phase equilibrium model class applies `equation of state (EoS) 5 | model `_ for both 6 | vapor and liquid phases. EoS formulation is explicit: 7 | 8 | .. math:: 9 | P = \frac{RT}{v-b} - \frac{a}{(v+c_1b)(v+c_2b)} 10 | 11 | Phasepy includes following cubic EoS: 12 | 13 | - Van der Waals (VdW) 14 | - Peng Robinson (PR) 15 | - Redlich Kwong (RK) 16 | - Redlich Kwong Soave (RKS), a.k.a Soave Redlich Kwong (SRK) 17 | - Peng Robinson Stryjek Vera (PRSV) 18 | 19 | Both pure component EoS and mixture EoS are supported. 20 | 21 | Pure Component EoS 22 | ------------------ 23 | 24 | Pure component example using Peng-Robinson EoS: 25 | 26 | >>> from phasepy import preos, component 27 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 28 | Ant=[11.61809279, 3423.0259436, -56.48094263], 29 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 30 | >>> eos = preos(ethanol) 31 | >>> eos.psat(T=350.0) # saturation pressure, liquid volume and vapor volume 32 | (array([0.98800647]), array([66.75754804]), array([28799.31921623])) 33 | 34 | Density can be computed given the aggregation state (``L`` for liquid, 35 | ``V`` for vapor): 36 | 37 | >>> eos.density(T=350.0, P=1.0, state='L') 38 | 0.01497960198094922 39 | >>> eos.density(T=350.0, P=1.0, state='V') 40 | 3.515440899573752e-05 41 | 42 | 43 | Volume Translation 44 | ------------------ 45 | 46 | Volume translated (VT) versions of EoS are available for PR, RK, RKS 47 | and PRSV models. These models include an additional component specific 48 | volume translation parameter ``c``, which can be used to improve 49 | liquid density predictions without changing phase equilibrium. 50 | EoS property ``volume_translation`` must be ``True`` to enable VT. 51 | 52 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 53 | Ant=[11.61809279, 3423.0259436, -56.48094263], 54 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}, 55 | c=5.35490936) 56 | >>> eos = preos(ethanol, volume_translation=True) 57 | >>> eos.psat(T=350.0) # saturation pressure, liquid volume and vapor volume 58 | (array([0.98800647]), array([61.40263868]), array([28793.96430687])) 59 | >>> eos.density(T=350.0, P=1.0, state='L') 60 | 0.01628597159790686 61 | >>> eos.density(T=350.0, P=1.0, state='V') 62 | 3.5161028012629526e-05 63 | 64 | 65 | Mixture EoS 66 | ----------- 67 | 68 | Mixture EoS utilize one-fluid mixing rules, using parameters for 69 | hypothetical pure fluids, to predict the mixture behavior. The mixing 70 | rules require interaction parameter values as input (zero values are 71 | assumed if no values are specified). 72 | 73 | Classic Quadratic Mixing Rule (QMR) 74 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | .. math:: 77 | a_m = \sum_{i=1}^c \sum_{j=1}^c x_ix_ja_{ij} \quad a_{ij} = 78 | \sqrt{a_ia_j}(1-k_{ij}) \quad b_m = \sum_{i=1}^c x_ib_i 79 | 80 | Example of Peng-Robinson with QMR: 81 | 82 | >>> from phasepy import preos 83 | >>> mix = mixture(ethanol, water) 84 | >>> Kij = np.array([[0, -0.11], [-0.11, 0]]) 85 | >>> mix.kij_cubic(Kij) 86 | >>> eos = preos(mix, mixrule='qmr') 87 | 88 | 89 | Modified Huron Vidal (MHV) Mixing Rule 90 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 91 | 92 | MHV mixing rule is specified in combination with an activity 93 | coefficient model to solve EoS. 94 | In MHV model, the repulsive (covolume) parameter is calcuated same way 95 | as in QMR 96 | 97 | .. math:: 98 | b_m = \sum_{i=1}^c x_ib_i 99 | 100 | while attractive term is an implicit function 101 | 102 | .. math:: 103 | g^e_{EOS} = g^e_{model} 104 | 105 | 106 | Example of Peng-Robinson with MHV and NRTL: 107 | 108 | >>> alpha = np.array([[0.0, 0.5597628], 109 | [0.5597628, 0.0]]) 110 | >>> g = np.array([[0.0, -57.6880881], 111 | [668.682368, 0.0]]) 112 | >>> g1 = np.array([[0.0, 0.46909821], 113 | [-0.37982045, 0.0]]) 114 | >>> mix.NRTL(alpha, g, g1) 115 | >>> eos = preos(mix, mixrule='mhv_nrtl') 116 | 117 | Example of Peng-Robinson with MHV and Modified-UNIFAC: 118 | 119 | >>> mix.unifac() 120 | >>> eos = preos(mix, mixrule='mhv_unifac') 121 | 122 | 123 | Wong Sandler (WS) Mixing Rule 124 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 125 | 126 | WS mixing rule is specified in combination with an activity 127 | coefficient model to solve EoS. 128 | 129 | Example of Peng-Robinson with WS and Redlich Kister: 130 | 131 | >>> C0 = np.array([1.20945699, -0.62209997, 3.18919339]) 132 | >>> C1 = np.array([-13.271128, 101.837857, -1100.29221]) 133 | >>> mix.rk(C0, C1) 134 | >>> eos = preos(mix, mixrule='ws_rk') 135 | 136 | 137 | .. automodule:: phasepy.cubic.cubic 138 | :members: 139 | :undoc-members: 140 | :show-inheritance: 141 | 142 | 143 | EoS classes 144 | ----------- 145 | 146 | .. toctree:: 147 | phasepy.cubicp 148 | phasepy.cubicm 149 | 150 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.cubicm.rst: -------------------------------------------------------------------------------- 1 | Mixture Cubic Equation of State Class 2 | ===================================== 3 | 4 | .. automodule:: phasepy.cubic.cubicmix 5 | :members: cubicm 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.cubicp.rst: -------------------------------------------------------------------------------- 1 | Pure Cubic Equation of State Class 2 | ================================== 3 | 4 | .. automodule:: phasepy.cubic.cubicpure 5 | :members: cpure 6 | :show-inheritance: 7 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.virial.rst: -------------------------------------------------------------------------------- 1 | Virial EoS 2 | ========== 3 | 4 | All virial models use the same argument list: 5 | 6 | T : float 7 | absolute temperature [K] 8 | Tij: array 9 | square matrix of critical temperatures [K] 10 | Pij: array 11 | square matrix of critical pressures [bar] 12 | wij: array 13 | square matrix of acentric factors 14 | 15 | 16 | .. automodule:: phasepy.actmodels.virial 17 | :members: Tsonopoulos, Abbott, ideal_gas 18 | :undoc-members: 19 | :show-inheritance: 20 | -------------------------------------------------------------------------------- /docs/source/models/phasepy.virialgama.rst: -------------------------------------------------------------------------------- 1 | Virial - Activity coefficient Model 2 | =================================== 3 | 4 | This phase equilibrium model class uses a virial correlation for the 5 | vapor phase and an activity coefficient model for the liquid phase. 6 | For the gas phase, the virial coefficient can be estimated 7 | using ideal gas law, Abbott or Tsonopoulos correlations. 8 | For liquid phase, NRTL, Wilson, Redlich-Kister and Modified-UNIFAC 9 | activity coefficient models are available. 10 | 11 | .. toctree:: 12 | phasepy.virial 13 | phasepy.actmodels 14 | 15 | Before creating a phase equilibrium model object, it is necessary to 16 | supply the interactions parameters of the activity coefficient model 17 | to the mixture object, for example using the NRTL model: 18 | 19 | >>> import numpy as np 20 | >>> from phasepy import component, mixture, virialgamma 21 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 22 | Ant=[11.64785144, 3797.41566067, -46.77830444], 23 | GC={'H2O':1}) 24 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 25 | Ant=[11.61809279, 3423.0259436, -56.48094263], 26 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 27 | >>> mix = mixture(ethanol, water) 28 | >>> alpha = np.array([[0.0, 0.5597628], 29 | [0.5597628, 0.0]]) 30 | >>> g = np.array([[0.0, -57.6880881], 31 | [668.682368, 0.0]]) 32 | >>> g1 = np.array([[0.0, 0.46909821], 33 | [-0.37982045, 0.0]]) 34 | >>> mix.NRTL(alpha, g, g1) 35 | >>> model = virialgamma(mix, virialmodel='Abbott', actmodel='nrtl') 36 | 37 | Parameters for Redlich-Kister are specified as follows: 38 | 39 | >>> C0 = np.array([1.20945699, -0.62209997, 3.18919339]) 40 | >>> C1 = np.array([-13.271128, 101.837857, -1100.29221]) 41 | >>> mix.rk(C0, C1) 42 | >>> model = virialgamma(mix, actmodel='rk') 43 | 44 | Modified-UNIFAC with an ideal gas model is set up simply: 45 | 46 | >>> mix.unifac() 47 | >>> model = virialgamma(mix, virialmodel='ideal_gas', actmodel='unifac') 48 | 49 | .. automodule:: phasepy.actmodels.virialgama 50 | :members: virialgamma 51 | :show-inheritance: 52 | -------------------------------------------------------------------------------- /docs/source/phasepy.equilibrium.rst: -------------------------------------------------------------------------------- 1 | phasepy.equilibrium 2 | =================== 3 | 4 | 5 | Phase equilibrium conditions are obtained from a differential entropy balance of a system. The following equationes must be solved: 6 | 7 | .. math:: 8 | T^\alpha = T^\beta = ... &= T^\pi\\ 9 | P^\alpha = P^\beta = ... &= P^\pi\\ 10 | \mu_i^\alpha = \mu_i^\beta = ... &= \mu_i^\pi \quad i = 1,...,c 11 | 12 | Where :math:`T`, :math:`P` and :math:`\mu` are the temperature, 13 | pressure and chemical potencial, :math:`\alpha`, :math:`\beta` and 14 | :math:`\pi` are the phases and :math:`i` is component index. 15 | 16 | For the continuous (:math:`\phi-\phi`) phase equilibrium approach, equilibrium is 17 | defined using fugacity coefficients :math:`\phi`: 18 | 19 | .. math:: 20 | x_i^\alpha\hat{\phi}_i^\alpha = x_i^\beta \hat{\phi}_i^\beta = ... = x_i^\pi \hat{\phi}_i^\pi \quad i = 1,...,c 21 | 22 | For the discontinuous (:math:`\gamma-\phi`) phase equilibrium 23 | approach, the equilibrium is defined with vapor fugacity coefficient 24 | and liquid phase activity coefficients :math:`\gamma`: 25 | 26 | .. math:: 27 | x_i^\alpha\hat{\gamma}_i^\alpha f_i^0 = x_i^\beta \hat{\gamma}_i^\beta f_i^0 = ... = x_i^\pi\hat{\phi}_i^\pi P \quad i = 1,...,c 28 | 29 | where :math:`f_i^0` is standard state fugacity. 30 | 31 | 32 | .. toctree:: 33 | :maxdepth: 1 34 | 35 | ./equilibrium/phasepy.stability 36 | ./equilibrium/phasepy.elv 37 | ./equilibrium/phasepy.flash 38 | ./equilibrium/phasepy.multiflash 39 | -------------------------------------------------------------------------------- /docs/source/phasepy.fit.rst: -------------------------------------------------------------------------------- 1 | phasepy.fit 2 | =========== 3 | 4 | In order to compute phase equilibria and interfacial properties it is necessary to count with pure component parameters as: Antoine correlation parameters, volume translataion constant, influence parameter for SGT, among others. Similarly, when working with mixtures interaction parameters of activity coefficients models or binary correction :math:`k_{ij}` for quadratic mixing rule are necessary to predict phase equilibria. Often those parameters can be found in literature, but for many educational and research purposes there might be necessary to fit them to experimental data. 5 | 6 | Phasepy includes several functions that relies on equilibria routines included in the package and in SciPy optimization tools for fitting models parameters. These functions are explained in the following secctions for pure component and for mixtures. 7 | 8 | 9 | .. toctree:: 10 | ./fit/fit.pure 11 | ./fit/fit.mixtures 12 | -------------------------------------------------------------------------------- /docs/source/phasepy.rst: -------------------------------------------------------------------------------- 1 | phasepy 2 | ======= 3 | 4 | Phasepy aims to require minimum quantity of parameters needed to do phase equilibrium calculations. First it is required to create components and mixtures, and then combine them with a phase equilibrium model to create a final model object, which can be used to carry out fluid phase equilibrium calculations. 5 | 6 | .. toctree:: 7 | ./basic/phasepy.component 8 | ./basic/phasepy.mixture 9 | 10 | With the class component :class:`phasepy.component`, only pure component info is saved. Info includes critical temperature, pressure, volume, acentric factor, Antoine coefficients and group contribution info. 11 | The class :class:`phasepy.mixture` saves pure component data, but also interactions parameters for the activity coeffient models. 12 | 13 | .. code-block:: python 14 | 15 | >>> from phasepy import component 16 | >>> water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861, 17 | Ant=[11.64785144, 3797.41566067, -46.77830444], 18 | GC={'H2O':1}) 19 | >>> ethanol = component(name='ethanol', Tc=514.0, Pc=61.37, Zc=0.241, Vc=168.0, w=0.643558, 20 | Ant=[11.61809279, 3423.0259436, -56.48094263], 21 | GC={'CH3':1, 'CH2':1, 'OH(P)':1}) 22 | >>> water.psat(T=373.0) # vapor saturation pressure [bar] 23 | 1.0072796747419537 24 | >>> ethanol.vlrackett(T=310.0) # liquid molar volume [cm3/mol] 25 | 56.32856995891473 26 | 27 | A mixture can be created from two components: 28 | 29 | .. code-block:: python 30 | 31 | >>> from phasepy import mixture 32 | >>> mix = mixture(ethanol, water) 33 | >>> mix.names 34 | ['ethanol', 'water'] 35 | >>> mix.nc # number of components 36 | 2 37 | >>> mix.psat(T=373.0) # vapor saturation pressures [bar] 38 | array([2.23333531, 1.00727967]) 39 | >>> mix.vlrackett(T=310.0) # liquid molar volumes [cm3/mol] 40 | array([56.32856996, 16.46025809]) 41 | 42 | 43 | Phasepy includes two phase equilibrium models: 44 | 45 | 1. A discontinous (:math:`\gamma - \phi`) Virial - Activity Coefficient Method model where the vapor and liquid deviations are modeled with an virial expansion and an activity coefficient model, respectively, or 46 | 2. A continuous (:math:`\phi - \phi`) Cubic Equation of State model, using the same equation of state for all the phases. 47 | 48 | .. toctree:: 49 | :maxdepth: 1 50 | 51 | ./models/phasepy.virialgama 52 | ./models/phasepy.cubic 53 | 54 | The coded models were tested to pass molar partial property test and Gibbs-Duhem consistency, in the case of activity coefficient model the following equations were tested: 55 | 56 | .. math:: 57 | \frac{G^E}{RT} - \sum_{i=1}^c x_i \ln \gamma_i = 0\\ 58 | \sum_{i=1}^c x_i d\ln \gamma_i = 0 59 | 60 | where, :math:`G^E` refers to the Gibbs excess energy, :math:`R` is the ideal gas constant, :math:`T` is the absolute temperature, and :math:`x_i` and :math:`\gamma_i` are the mole fraction and activity coefficient of component :math:`i`. And in the case of cubic EoS: 61 | 62 | .. math:: 63 | \ln \phi - \sum_{i=1}^c x_i \ln \hat{\phi_i} = 0 \\ 64 | \frac{d \ln \phi}{dP} - \frac{Z - 1}{P} = 0 \\ 65 | \sum_{i=1}^c x_i d \ln \hat{\phi_i} = 0 66 | 67 | Here, :math:`\phi` is the fugacity coefficient of the mixture, :math:`x_i` and :math:`\hat{\phi_i}` are the mole fraction and fugacity coefficient of component :math:`i`, :math:`P` refers to pressure and :math:`Z` to the compressibility factor. 68 | -------------------------------------------------------------------------------- /docs/source/phasepy.sgt.rst: -------------------------------------------------------------------------------- 1 | phasepy.sgt 2 | =========== 3 | 4 | The Square Gradient Theory (SGT) is the reference framework when studying interfacial properties between fluid phases in equilibrium. It was originally proposed by van der Waals and then reformulated by Cahn and Hilliard. SGT proposes that the Helmholtz free energy density at the interface can be described by a homogeneous and a gradient contribution. 5 | 6 | .. math:: 7 | a = a_0 + \frac{1}{2} \sum_i \sum_j c_{ij} \frac{d\rho_i}{dz} \frac{d\rho_j}{dz} + \cdots 8 | 9 | Here :math:`a` is the Helmholtz free energy density, :math:`a_0` is the Helmholtz free energy density bulk contribution, :math:`c_{ij}` is the cross influence parameter between component :math:`i` and :math:`j` , :math:`\rho` is denisty vector and :math:`z` is the lenght coordinate. The cross influence parameter is usually computed using a geometric mean rule and a correction :math:`c_{ij} = (1-\beta_{ij})\sqrt{c_i c_j}`. 10 | 11 | The density profiles between the bulk phases are mean to minimize the energy of the system. This results in the following Euler-Lagrange system: 12 | 13 | .. math:: 14 | \sum_j c_{ij} \frac{d^2 \rho_j}{dz^2} = \mu_i - \mu_i^0 \qquad i = 1,...,c 15 | 16 | .. math:: 17 | \rho(z \rightarrow -\infty) = \rho^\alpha \qquad \rho(z \rightarrow \infty) = \rho^\beta 18 | 19 | 20 | Here :math:`\mu` represent the chemical potential and the superscript indicates its value evaluated at the bulk phase. :math:`\alpha` and :math:`\beta` are the bulk phases index. 21 | 22 | Once the density profiles were solved the interfacial tension, :math:`\sigma` between the phases can be computed as: 23 | 24 | .. math:: 25 | \sigma = \int_{-\infty}^{\infty} \sum_i \sum_j c_{ij} \frac{d\rho_i}{dz} \frac{d\rho_j}{dz} dz 26 | 27 | Solution procedure for SGT strongly depends for if you are working with a pure component or a mixture. In the latter, the correction value of :math:`\beta_{ij}` plays a huge role in the solution procedure. 28 | These cases will be covered. 29 | 30 | .. toctree:: 31 | ./sgt/sgt.tenpure 32 | ./sgt/sgt.tenbeta0 33 | ./sgt/sgt.tensgt 34 | -------------------------------------------------------------------------------- /docs/source/sgt/ethanolhexane_beta0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/docs/source/sgt/ethanolhexane_beta0.jpg -------------------------------------------------------------------------------- /docs/source/sgt/ethanolwater_beta0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/docs/source/sgt/ethanolwater_beta0.jpg -------------------------------------------------------------------------------- /docs/source/sgt/sgt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/docs/source/sgt/sgt.jpg -------------------------------------------------------------------------------- /docs/source/sgt/sgt.tenpure.rst: -------------------------------------------------------------------------------- 1 | SGT for pure components 2 | ======================= 3 | 4 | When working with pure components, SGT implementation is direct as there is a continuous path from the vapor to the liquid phase. SGT can be reformulated with density as indepedent variable. 5 | 6 | .. math:: 7 | \sigma = \sqrt{2} \int_{\rho^\alpha}^{\rho_\beta} \sqrt{c_i \Delta \Omega} d\rho 8 | 9 | Here, :math:`\Delta \Omega` represents the grand thermodynamic potential, obtained from: 10 | 11 | .. math:: 12 | \Delta \Omega = a_0 - \rho \mu^0 + P^0 13 | 14 | Where :math:`P^0` is the equilibrium pressure. 15 | 16 | In phasepy this integration is done using ortoghonal collocation, which reduces the number of nodes needed for a desired error. This calculation is done with the ``sgt_pure`` function and it requires the equilibrium densities, temperature and pressure as inputs. 17 | 18 | >>> #component creation 19 | >>> water = component(name = 'Water', Tc = 647.13, Pc = 220.55, Zc = 0.229, Vc = 55.948, w = 0.344861 20 | ... ksv = [ 0.87185176, -0.06621339], cii = [2.06553362e-26, 2.64204784e-23, 4.10320513e-21]) 21 | >>> #EoS object creation 22 | >>> eos = prsveos(water) 23 | 24 | First vapor-liquid equilibria has to be computed. This is done with the ``psat`` method from the EoS, which returns the pressure and densities at equilibrium. Then the interfacial can be computed as it is shown. 25 | 26 | >>> T = 350 #K 27 | >>> Psat, vl, vv = eos.psat(T) 28 | >>> rhol = 1/vl 29 | >>> rhov = 1/vv 30 | >>> sgt_pure(rhov, rhol, T, Psat, eos, full_output = False) 31 | >>> #Tension in mN/m 32 | ... 63.25083234 33 | 34 | Optionally, ``full_output`` allows to get all the computation information as the density profile, interfacial lenght and grand thermodynamic potential. 35 | 36 | >>> solution = sgt_pure(rhol, rhov, T, Psat, eos, full_output = True) 37 | >>> solution.z #interfacial lenght array 38 | >>> solution.rho #density array 39 | >>> solution.tension #IFT computed value 40 | 41 | 42 | .. automodule:: phasepy.sgt.sgtpuros 43 | :members: sgt_pure 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | -------------------------------------------------------------------------------- /docs/source/sgt/sgt.tensgt.rst: -------------------------------------------------------------------------------- 1 | SGT for mixtures and :math:`\beta_{ij} \neq 0` 2 | ============================================== 3 | 4 | When working with mixtures and at least one :math:`\beta_{ij} \neq 0`, SGT has to be solved as a boundary value problem (BVP)with a finite interfacial lenght. 5 | 6 | .. math:: 7 | \sum_j c_{ij} \frac{d^2 \rho_j}{dz^2} = \mu_i - \mu_i^0 \qquad i = 1,...,c 8 | 9 | .. math:: 10 | \rho(z \rightarrow 0) = \rho^\alpha \qquad \rho(z \rightarrow L) = \rho^\beta 11 | 12 | In phasepy two solution procedure are available for this purpose, both on them relies on orthogonal collocation. The first one, solve the BVP at a given interfacial lenght, then it computes the interfacial tension. After this first iteration the interfacial lenght is increased and the density profiles are solved again using the obtained solution as an initial guess, then the interfacial tension is computed again. This iterative procedure is repeated until the interfacial tension stops decreasing whithin a given tolerance (default value 0.01 mN/m). This procedure is inspired in the work of Liang and Michelsen. 13 | 14 | First, as for any SGT computation, equilibria has to be computed. 15 | 16 | >>> hexane = component(name = 'n-Hexane', Tc = 507.6, Pc = 30.25, Zc = 0.266, Vc = 371.0, w = 0.301261, 17 | ksv = [ 0.81185833, -0.08790848], 18 | cii = [ 5.03377433e-24, -3.41297789e-21, 9.97008208e-19], 19 | GC = {'CH3':2, 'CH2':4}) 20 | >>> ethanol = component(name = 'Ethanol', Tc = 514.0, Pc = 61.37, Zc = 0.241, Vc = 168.0, w = 0.643558, 21 | ksv = [1.27092923, 0.0440421 ], 22 | cii = [ 2.35206942e-24, -1.32498074e-21, 2.31193555e-19], 23 | GC = {'CH3':1, 'CH2':1, 'OH(P)':1}) 24 | >>> mix = mixture(ethanol, hexane) 25 | >>> a12, a21 = np.array([1141.56994427, 125.25729314]) 26 | >>> A = np.array([[0, a12], [a21, 0]]) 27 | >>> mix.wilson(A) 28 | >>> eos = prsveos(mix, 'mhv_wilson') 29 | >>> T = 320 #K 30 | >>> X = np.array([0.3, 0.7]) 31 | >>> P0 = 0.3 #bar 32 | >>> Y0 = np.array([0.7, 0.3]) 33 | >>> sol = bubblePy(Y0, P0, X, T, eos, full_output = True) 34 | >>> Y = sol.Y 35 | >>> P = sol.P 36 | >>> vl = sol.v1 37 | >>> vv = sol.v2 38 | >>> #computing the density vector 39 | >>> rhol = X / vl 40 | >>> rhov = Y / vv 41 | 42 | 43 | The correction :math:`\beta_{ij}` has to be supplied to the eos with ``eos.beta_sgt`` method. Otherwise the influence parameter matrix will be singular and a error will be raised. 44 | 45 | >>> bij = 0.1 46 | >>> beta = np.array([[0, bij], [bij, 0]]) 47 | >>> eos.beta_sgt(beta) 48 | 49 | Then the interfacial tension can be computed as follows: 50 | 51 | 52 | >>> sgt_mix(rhol, rhov, T, P, eos, z0 = 10., rho0 = 'hyperbolic', full_output = False) 53 | >>> #interfacial tension in mN/m 54 | >>> 14.367813285945807 55 | 56 | In the example ``z0`` refers to the initial interfacial langht, ``rho0`` refers to the initial guess to solve the BVP. Available options are ``'linear'`` for linear density profiles, ``hyperbolic`` for density profiles obtained from hyperbolic tangent. Other option is to provide an array with the initial guess, the shape of this array has to be nc x n, where n is the number of collocation points. Finally, a TensionResult can be passed to ``rho0``, this object is usually obtained from another SGT computation, as for example from a calculation with :math:`\beta_{ij} = 0`. 57 | 58 | 59 | If the ``full_output`` option is set to ``True``, all the computated information will be given in a TensionResult object. Atributes are accessed similar as SciPy OptimizationResult. 60 | 61 | >>> sol = sgt_mix(rhol, rhov, T, P, eos, z0 = 10., rho0 = 'hyperbolic', full_output = False) 62 | >>> sol.tension 63 | ... 14.36781328594585 #IFT in mN/m 64 | >>> #density profiles and spatial coordiante access 65 | >>> sol.rho 66 | >>> sol.z 67 | 68 | 69 | 70 | .. automodule:: phasepy.sgt.coloc_z 71 | :members: sgt_mix 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | The second solution method is based on a modified SGT system of equations, proposed by Mu et al. This system introduced a time variable :math:`s` which helps to get linearize the system of equations during the first iterations. 76 | 77 | .. math:: 78 | \sum_j c_{ij} \frac{d^2 \rho_j}{dz^2} = \frac{\delta \rho_i}{\delta s} + \mu_i - \mu_i^0 \qquad i = 1,...,c 79 | 80 | .. math:: 81 | \rho(z \rightarrow 0) = \rho^\alpha \qquad \rho(z \rightarrow L) = \rho^\beta 82 | 83 | This differential equation is advanced in time until no further changes is found on the density profiles. Then the interfacial tension is computed. 84 | Its use is similar as the method described above, with the ``msgt_mix`` function. 85 | 86 | >>> solm = msgt_mix(rhol, rhov, T, P, eos, z = 20, rho0 = sol, full_output = True) 87 | >>> solm.tension 88 | ... 14.367827924919165 #IFT in mN/m 89 | 90 | The density profiles obtained from each method are show in the following figure. The dashed line was computed solving the original BVP with increasing interfacial length and the dots were computed with the modified system. 91 | 92 | .. image:: sgt.jpg 93 | :width: 60 % 94 | :align: center 95 | 96 | 97 | 98 | .. automodule:: phasepy.sgt.coloc_z_ds 99 | :members: msgt_mix 100 | :undoc-members: 101 | :show-inheritance: 102 | -------------------------------------------------------------------------------- /examples/3. Phase stability test (tpd).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Stability testing with Tangent Plane Distance (TPD) function\n", 8 | "\n", 9 | "The [tangent plane distance](https://www.sciencedirect.com/science/article/pii/0378381282850012) ($tpd$) function allows testing the relative stability of a phase of composition $z$ against a trial phase of composition $w$ at fixed temperature and pressure].\n", 10 | "\n", 11 | "\n", 12 | "$$ tpd(\\underline{w}) = \\sum_{i=1}^c w_i (\\ln w_i + \\ln \\hat{\\phi}_i(\\underline{w})\n", 13 | "- \\ln z_i - \\ln \\hat{\\phi}_i(\\underline{z})) $$\n", 14 | "\n", 15 | "Usually, this function is minimized to check the stability of the given phase based on the following criteria:\n", 16 | "- If the minimized $tpd$ is positive, the global phase $z$ is stable.\n", 17 | "- If the minimized $tpd$ is zero, the global phase $z$ and trial phase $w$ are in equilibrium.\n", 18 | "- If the minimized $tpd$ is negative, the global phase $z$ is unstable\n", 19 | "\n", 20 | "\n", 21 | "In this notebook, stability analysis for the mixture of water and mtbe will be performed. To start, the required functions are imported." 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import numpy as np\n", 31 | "from phasepy import component, mixture, preos\n", 32 | "from phasepy.equilibrium import tpd_min, tpd_minimas, lle_init" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "Then, the mixture of water and mtbe and its interaction parameters are set up." 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861,\n", 49 | " Ant=[11.64785144, 3797.41566067, -46.77830444],\n", 50 | " GC={'H2O':1})\n", 51 | "\n", 52 | "mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059,\n", 53 | " Ant=[9.16238246, 2541.97883529, -50.40534341], \n", 54 | " GC={'CH3':3, 'CH3O':1, 'C':1})\n", 55 | "\n", 56 | "mix = mixture(water, mtbe)\n", 57 | "\n", 58 | "# or\n", 59 | "\n", 60 | "mix = water + mtbe\n", 61 | "\n", 62 | "mix.unifac()\n", 63 | "eos = preos(mix, 'mhv_unifac')" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "----\n", 71 | "### tpd_min\n", 72 | "\n", 73 | "The ``tpd_min`` function searches for a phase composition corresponding to a minimum of $tpd$ function given an initial value. The user needs to specify whether the trial (W) and reference (Z) phases are liquids (``L``) or vapors (``V``)." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "Liquid molar fractions and TPD value: (array([0.11779034, 0.88220966]), -0.0859542665231543)\n", 86 | "Vapor molar fractions and TPD value: (array([0.17422237, 0.82577763]), 0.07570203456337987)\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "T = 320.0\n", 92 | "P = 1.01\n", 93 | "z = np.array([0.5, 0.5])\n", 94 | "w = np.array([0.01, 0.99])\n", 95 | "print(\"Liquid molar fractions and TPD value:\", tpd_min(w, z, T, P, eos, stateW='L', stateZ='L'))\n", 96 | "print(\"Vapor molar fractions and TPD value:\", tpd_min(w, z, T, P, eos, stateW='V', stateZ='L'))" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "---\n", 104 | "### tpd_minimas\n", 105 | "The ``tpd_minimas`` function will attempt (but does not guarantee) to search for ``nmin`` minima of the $tpd$ function. As for the ``tpd_min`` function, you need to specify the aggregation state of the global (``z``) and the trial phase (``w``)." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 4, 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "name": "stdout", 115 | "output_type": "stream", 116 | "text": [ 117 | "TPD liquid minima: ((array([0.99868736, 0.00131264]), array([0.11778925, 0.88221075])), array([-0.73704754, -0.08595427]))\n", 118 | "TPD vapor minima: ((array([0.17422234, 0.82577766]), array([0.17422234, 0.82577766])), array([0.07570203, 0.07570203]))\n" 119 | ] 120 | } 121 | ], 122 | "source": [ 123 | "nmin = 2\n", 124 | "print(\"TPD liquid minima:\", tpd_minimas(nmin, z, T, P, eos, stateW='L', stateZ='L'))\n", 125 | "print(\"TPD vapor minima:\", tpd_minimas(nmin, z, T, P, eos, stateW='V', stateZ='L'))" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "---\n", 133 | "### lle_init\n", 134 | "\n", 135 | "Finally, the ``lle_init`` function can be used to find initial guesses for liquid-liquid equilibrium calculation.\n", 136 | "\n", 137 | "This function call ``tpd_minimas`` with ``nmin=2`` and liquid state for trial and global phase." 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 5, 143 | "metadata": {}, 144 | "outputs": [ 145 | { 146 | "data": { 147 | "text/plain": [ 148 | "(array([0.99868736, 0.00131264]), array([0.11778925, 0.88221075]))" 149 | ] 150 | }, 151 | "execution_count": 5, 152 | "metadata": {}, 153 | "output_type": "execute_result" 154 | } 155 | ], 156 | "source": [ 157 | "lle_init(z, T, P, eos)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "For further information please also check [official documentation](https://phasepy.readthedocs.io/), or just try:\n", 165 | "\n", 166 | "```function?```" 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": null, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [] 175 | } 176 | ], 177 | "metadata": { 178 | "kernelspec": { 179 | "display_name": "thermo", 180 | "language": "python", 181 | "name": "thermo" 182 | }, 183 | "language_info": { 184 | "codemirror_mode": { 185 | "name": "ipython", 186 | "version": 3 187 | }, 188 | "file_extension": ".py", 189 | "mimetype": "text/x-python", 190 | "name": "python", 191 | "nbconvert_exporter": "python", 192 | "pygments_lexer": "ipython3", 193 | "version": "3.9.7" 194 | } 195 | }, 196 | "nbformat": 4, 197 | "nbformat_minor": 4 198 | } 199 | -------------------------------------------------------------------------------- /examples/4. Two phase flash (TP).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Two-Phase Flash Calculation (VLE and LLE)\n", 8 | "\n", 9 | "The isothermal-isobaric two-phase flash is carried out by a combination of accelerated successive substitution (ASS) to update phase compositions and Halley's method to solve the Rachford-Rice mass balance. \n", 10 | "\n", 11 | "$$ FO = \\sum_{i=1}^c \\left( x_i^\\beta - x_i^\\alpha \\right) = \\sum_{i=1}^c \\frac{z_i (K_i-1)}{1+\\psi (K_i-1)} $$\n", 12 | "\n", 13 | "Where, $K_i = x_i^\\beta/x_i^\\alpha =\\hat{\\phi}_i^\\alpha /\\hat{\\phi}_i^\\beta $ represents the equilibrium constant and $\\psi$ the fraction of the phase $\\beta$. The described method can be slow at high pressures, for that reason, the number of cycles of ASS is limited to ``nacc`` cycles and if no solution is reached the algorithm changes to a second-order procedure based on Gibbs free energy minimization:\n", 14 | "\n", 15 | "$$ min \\, {G(\\underline{F}^\\alpha, \\underline{F}^\\beta)} = \\sum_{i=1}^c (F_i^\\alpha \\ln \\hat{f}_i^\\alpha + F_i^\\beta \\ln \\hat{f}_i^\\beta) $$\n", 16 | "\n", 17 | "Here, $F$ refers to the number of moles and $\\hat{f}$ to the effective fugacity, the superscript refers to the phase index and the subscript to the specie index. The optimization is performed using SciPy minimization routines.\n", 18 | "\n", 19 | "Phasepy ``flash`` routine solves phase compositions for two-phase flash at constant pressure and temperature (PT-flash). The ```flash``` function requires initial guesses for the phase compositions, the states of the phases, i.e ```LV``` fo liquid-vapor flash or ```LL``` for liquid-liquid flash, the global phase composition (``z``), the temperature (``T``) and pressure (``P``) of the system.\n", 20 | "\n", 21 | "In this notebook, Vapor-Liquid and Liquid-Liquid flash calculation will be exemplified. Examples below apply Peng-Robinson equation of state. The mixing rule applied is Modified Huron Vidal combined with Modified UNIFAC.\n", 22 | "\n", 23 | "To start, the required functions are imported." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import numpy as np\n", 33 | "from phasepy import mixture, component, preos\n", 34 | "from phasepy.equilibrium import flash" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "---\n", 42 | "### Vapor Liquid Equilibrium Flash\n", 43 | "\n", 44 | "This calculation will be exemplified for the mixture of ethanol and water." 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 2, 50 | "metadata": {}, 51 | "outputs": [ 52 | { 53 | "data": { 54 | "text/plain": [ 55 | " T: 350.0\n", 56 | " P: 0.8\n", 57 | " beta: 0.4665703979134844\n", 58 | " error: 9.457065565351011e-09\n", 59 | " iter: 7\n", 60 | " X: array([0.23951597, 0.76048403])\n", 61 | " v1: 32.508879490778675\n", 62 | " state1: 'L'\n", 63 | " Y: array([0.58348128, 0.41651872])\n", 64 | " v2: 35869.13850298779\n", 65 | " state2: 'V'" 66 | ] 67 | }, 68 | "execution_count": 2, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "water = component(name='water', Tc=647.13, Pc=220.55, \n", 75 | " w=0.344861, GC={'H2O':1})\n", 76 | "\n", 77 | "ethanol = component(name='ethanol', Tc=514.0, Pc=61.37,\n", 78 | " w=0.643558, GC={'CH3':1, 'CH2':1, 'OH(P)':1})\n", 79 | "\n", 80 | "mix = mixture(ethanol, water)\n", 81 | "# or\n", 82 | "mix = ethanol + water\n", 83 | "\n", 84 | "mix.unifac() \n", 85 | "eos = preos(mix, 'mhv_unifac')\n", 86 | "T = 350.0\n", 87 | "P = 0.8\n", 88 | "Z = np.array([0.4, 0.6])\n", 89 | "x0 = np.array([0.23512692, 0.76487308])\n", 90 | "y0 = np.array([0.5, 0.5])\n", 91 | "flash(x0, y0, 'LV', Z, T, P, eos, full_output=True)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "---\n", 99 | "### Liquid Liquid Equilibrium Flash\n", 100 | "\n", 101 | "This calculation will be exemplified for the mixture of water and butanol." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 3, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/plain": [ 112 | " T: 320.0\n", 113 | " P: 1.01\n", 114 | " beta: 0.46991089151358123\n", 115 | " error: 2.9082881323704062e-09\n", 116 | " iter: 7\n", 117 | " X: array([0.05876434, 0.94123566])\n", 118 | " v1: 112.09263402948604\n", 119 | " state1: 'L'\n", 120 | " Y: array([0.99774164, 0.00225836])\n", 121 | " v2: 21.756817287953822\n", 122 | " state2: 'L'" 123 | ] 124 | }, 125 | "execution_count": 3, 126 | "metadata": {}, 127 | "output_type": "execute_result" 128 | } 129 | ], 130 | "source": [ 131 | "water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861,\n", 132 | " Ant=[11.64785144, 3797.41566067, -46.77830444],\n", 133 | " GC={'H2O':1})\n", 134 | "\n", 135 | "mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059,\n", 136 | " Ant=[9.16238246, 2541.97883529, -50.40534341],\n", 137 | " GC={'CH3':3, 'CH3O':1, 'C':1})\n", 138 | "\n", 139 | "mix = mixture(water, mtbe)\n", 140 | "# or\n", 141 | "mix = water + mtbe\n", 142 | "mix.unifac()\n", 143 | "eos = preos(mix, 'mhv_unifac')\n", 144 | "T = 320.0\n", 145 | "P = 1.01\n", 146 | "Z = np.array([0.5, 0.5])\n", 147 | "x0 = np.array([0.01, 0.99])\n", 148 | "w0 = np.array([0.99, 0.01])\n", 149 | "flash(x0, w0, 'LL', Z, T, P, eos, full_output=True)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "For further information please also check [official documentation](https://phasepy.readthedocs.io/), or just try:\n", 157 | "\n", 158 | "```function?```" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [] 167 | } 168 | ], 169 | "metadata": { 170 | "kernelspec": { 171 | "display_name": "thermo", 172 | "language": "python", 173 | "name": "thermo" 174 | }, 175 | "language_info": { 176 | "codemirror_mode": { 177 | "name": "ipython", 178 | "version": 3 179 | }, 180 | "file_extension": ".py", 181 | "mimetype": "text/x-python", 182 | "name": "python", 183 | "nbconvert_exporter": "python", 184 | "pygments_lexer": "ipython3", 185 | "version": "3.9.7" 186 | } 187 | }, 188 | "nbformat": 4, 189 | "nbformat_minor": 4 190 | } 191 | -------------------------------------------------------------------------------- /examples/7. Liquid-Liquid Equilibria.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Liquid Liquid Equilibrium (LLE)\n", 8 | "\n", 9 | "Phase stability plays a key role during equilibrium computation when dealing with more than two liquid phases. For this purpose the following modified multiphase Rachford-Rice mass balance has been proposed by [Gupta et al.](https://www.sciencedirect.com/science/article/pii/037838129180021M):\n", 10 | "\n", 11 | "\n", 12 | "$$ \\sum_{i=1}^c \\frac{z_i (K_{ik} \\exp{\\theta_k}-1)}{1+ \\sum\\limits^{\\pi}_{\\substack{j=1 \\\\ j \\neq r}}{\\psi_j (K_{ij}} \\exp{\\theta_j} -1)} = 0 \\qquad k = 1,..., \\pi, k \\neq r $$\n", 13 | "\n", 14 | "Subject to:\n", 15 | "\n", 16 | "$$ \\psi_k \\theta_k = 0 $$\n", 17 | "\n", 18 | "In this system of equations, $z_i$ represents the global composition of the component $i$, $ K_{ij} = x_{ij}/x_{ir} = \\hat{\\phi}_{ir}/\\hat{\\phi}_{ij} $ is the constant equilibrium of component $i$ in phase $j$ respect to the reference phase $r$, and $\\psi_j$ and $\\theta_j$ are the phase fraction and stability variable of the phase $j$. \n", 19 | "\n", 20 | "The solution strategy is similar to the classic isothermal isobaric two-phase flash. First, a reference phase must be selected, this phase is considered stable during the procedure. In an inner loop, the system of equations is solved using multidimensional Newton's method for phase fractions and stability variables and then compositions are updated in an outer loop using accelerated successive substitution (ASS). Once the algorithm has converged, the stability variable gives information about the phase. If it takes a value of zero the phase is stable and if it is positive the phase is not. The proposed successive substitution method can be slow, if that is the case the algorithm attempts to minimize Gibbs Free energy of the system. This procedure also ensures stable solutions and is solved using SciPy's functions.\n", 21 | "\n", 22 | "$$ min \\, {G} = \\sum_{k=1}^\\pi \\sum_{i=1}^c F_{ik} \\ln \\hat{f}_{ik} $$\n", 23 | "\n", 24 | "This notebook shows the solution of liquid-liquid equilibrium using the ``lle`` function. This function incorporates the algorithm described above. To start, the required functions are imported." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "import numpy as np\n", 34 | "from phasepy import component, mixture, virialgamma, unifac\n", 35 | "from phasepy.equilibrium import lle" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "The LLE calculation for the mixture of water and mtbe will be exemplified. First the mixture and its interaction parameters are set up. " 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "data": { 52 | "text/plain": [ 53 | " T: 320.0\n", 54 | " P: 1.01\n", 55 | " error_outer: 2.9035040568173407e-09\n", 56 | " error_inner: 1.0073726451407202e-10\n", 57 | " iter: 7\n", 58 | " beta: array([0.53009228, 0.46990772])\n", 59 | " tetha: array([0.])\n", 60 | " X: array([[0.05876935, 0.94123065],\n", 61 | " [0.99774233, 0.00225767]])\n", 62 | " v: [None, None]\n", 63 | " states: ['L', 'L']" 64 | ] 65 | }, 66 | "execution_count": 2, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "water = component(name='water', Tc=647.13, Pc=220.55, Zc=0.229, Vc=55.948, w=0.344861,\n", 73 | " Ant=[11.64785144, 3797.41566067, -46.77830444],\n", 74 | " GC={'H2O':1})\n", 75 | "\n", 76 | "mtbe = component(name='mtbe', Tc=497.1, Pc=34.3, Zc=0.273, Vc=329.0, w=0.266059,\n", 77 | " Ant=[9.16238246, 2541.97883529, -50.40534341],\n", 78 | " GC={'CH3':3, 'CH3O':1, 'C':1})\n", 79 | "\n", 80 | "mix = mixture(water, mtbe)\n", 81 | "# or\n", 82 | "mix = water + mtbe\n", 83 | "\n", 84 | "mix.unifac()\n", 85 | "eos = virialgamma(mix, actmodel='unifac')\n", 86 | "T = 320.0\n", 87 | "P = 1.01\n", 88 | "Z = np.array([0.5, 0.5])\n", 89 | "x0 = np.array([0.01, 0.99])\n", 90 | "w0 = np.array([0.99, 0.01])\n", 91 | "lle(x0, w0, Z, T, P, eos, full_output=True)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "-----\n", 99 | "Initial guesses for the phase compositions can be obtained using the ``lle_init`` function. This is shown below." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 3, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "data": { 109 | "text/plain": [ 110 | "(array([0.99868736, 0.00131264]), array([0.11778925, 0.88221075]))" 111 | ] 112 | }, 113 | "execution_count": 3, 114 | "metadata": {}, 115 | "output_type": "execute_result" 116 | } 117 | ], 118 | "source": [ 119 | "from phasepy.equilibrium import lle_init\n", 120 | "from phasepy import preos \n", 121 | "eos = preos(mix, 'mhv_unifac')\n", 122 | "T = 320.0\n", 123 | "P = 1.01\n", 124 | "z = np.array([0.5, 0.5])\n", 125 | "lle_init(z, T, P, eos)" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "For further information please also check [official documentation](https://phasepy.readthedocs.io/), or just try:\n", 133 | "\n", 134 | "```function?```" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [] 143 | } 144 | ], 145 | "metadata": { 146 | "kernelspec": { 147 | "display_name": "thermo", 148 | "language": "python", 149 | "name": "thermo" 150 | }, 151 | "language_info": { 152 | "codemirror_mode": { 153 | "name": "ipython", 154 | "version": 3 155 | }, 156 | "file_extension": ".py", 157 | "mimetype": "text/x-python", 158 | "name": "python", 159 | "nbconvert_exporter": "python", 160 | "pygments_lexer": "ipython3", 161 | "version": "3.9.7" 162 | } 163 | }, 164 | "nbformat": 4, 165 | "nbformat_minor": 4 166 | } 167 | -------------------------------------------------------------------------------- /long_description.rst: -------------------------------------------------------------------------------- 1 | Phasepy is an open-source scientific python package for fluid phase equilibria computation. 2 | This package facilitates the calculation of liquid-vapor equilibrium, liquid-liquid equilibrium 3 | and liquid-liquid-vapor equilibrium. Equilibrium calculations can be performed with cubic equations 4 | of state with classic or advances mixing rules or with a discontinuous approach using a virial equation 5 | of state for the vapor phase and an activity coefficients model for the liquid phase. 6 | 7 | Besides computations, with this package is also possible to fit phase equilibria data, functions to fit quadratic 8 | mix rule, NRTL, Wilson and Redlich Kister parameters, are included. 9 | 10 | Phasepy relies on numpy, scipy and cython extension modules, when necessary. -------------------------------------------------------------------------------- /phasepy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | phasepy: package for fluid phase equilibria with Python 3 | ======================================================= 4 | 5 | Contents 6 | ------------------------------------------------------- 7 | 8 | Available EoS 9 | ------------- 10 | Virial Gas: ideal_gas, Tsonopoulos, Abbott 11 | Activity Coefficient Models : NRTL, Wilson, UNIFAC, Redlich-Kister 12 | Cubic EoS: VdW, PR, PRSV, RK, RKS 13 | Mixrules: QMR, MHV 14 | 15 | Available equilibrium routines (phasepy.equilibrium) 16 | VLE : Liquid Vapour Equilibrium 17 | LLE : Liquid Liquid Equilibrium 18 | VLLE : Vapor - Liquid - Liquid - Equilibrium 19 | 20 | Available fitting routines (phasepy.fit) 21 | fit_kij : fit kij for qmr mixrule 22 | fit_nrtl : fit nrtl parameters 23 | fit_wilson : fit Wilson parameters 24 | fit_rk : fit Redlich Kister parameters 25 | 26 | Interfacial properties (phasepy.sgt): 27 | sgt_pure: SGT for pure fluids. 28 | sgt_mix_beta0 : SGT for mixtures with beta = 0 29 | (Reference component, Path functions,linear approximation, 30 | spot approximation) 31 | sgt_mix : SGT for mixtures with beta != 0 32 | msgt_mix : modified SGT for mixtures with beta != 0 33 | 34 | 35 | """ 36 | 37 | from __future__ import division, print_function, absolute_import 38 | 39 | # __all__ = [s for s in dir() if not s.startswith('_')] 40 | 41 | from .mixtures import * 42 | from .cubic.cubic import * 43 | from .actmodels import * 44 | from . import actmodels 45 | from . import cubic 46 | from . import equilibrium 47 | from . import fit 48 | from . import sgt 49 | from .math import * 50 | -------------------------------------------------------------------------------- /phasepy/actmodels/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | from .redlichkister import rk, rkb, drk 4 | from .redlichkister import rk_aux, rkb_aux, drk_aux 5 | 6 | from .nrtl import nrtl, nrtlter, dnrtl, dnrtlter 7 | from .nrtl import nrtl_aux, nrtlter_aux, dnrtl_aux, dnrtlter_aux 8 | 9 | from .wilson import wilson, dwilson 10 | from .wilson import wilson_aux, dwilson_aux 11 | 12 | from .unifac import unifac, dunifac 13 | from .unifac import unifac_aux, dunifac_aux 14 | 15 | from .uniquac import uniquac, duniquac 16 | from .uniquac import uniquac_aux, duniquac_aux 17 | 18 | from .original_unifac import unifac_original, dunifac_original 19 | from .original_unifac import unifac_original_aux, dunifac_original_aux 20 | 21 | from .virial import ideal_gas, Tsonopoulos, Abbott, virial 22 | from .virialgama import virialgamma 23 | -------------------------------------------------------------------------------- /phasepy/actmodels/nrtl.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .actmodels_cy import nrtl_cy, rkter_nrtl_cy 4 | from .actmodels_cy import dnrtl_cy, drkter_nrtl_cy 5 | 6 | 7 | def nrtl_aux(X, tau, G): 8 | X = np.asarray(X, dtype=np.float64) 9 | lngama = nrtl_cy(X, tau, G) 10 | return lngama 11 | 12 | 13 | def dnrtl_aux(X, tau, G): 14 | X = np.asarray(X, dtype=np.float64) 15 | lngama, dlngama = dnrtl_cy(X, tau, G) 16 | return lngama, dlngama 17 | 18 | 19 | def nrtl(X, T, alpha, g, g1): 20 | r''' 21 | The non-random two-liquid (NRTL) activity coefficient model is a a 22 | local composition model, widely used to describe vapor-liquid, 23 | liquid-liquid and vapor-liquid-liquid equilibria. This function 24 | returns array of natural logarithm of the activity coefficients. 25 | 26 | .. math:: 27 | g^e = \sum_{i=1}^c x_i \frac{\sum_{j=1}^c \tau_{ji}G_{ji}x_j}{\sum_{l=1}^c G_{li}x_l} 28 | 29 | .. math:: 30 | \tau = g/T + g_1 31 | 32 | Parameters 33 | ---------- 34 | X: array 35 | Molar fractions 36 | T: float 37 | Absolute temperature [K] 38 | g: array 39 | Matrix of energy interactions [K] 40 | g1: array 41 | Matrix of energy interactions [1/K] 42 | alpha: array 43 | Matrix of aleatory factors 44 | ''' 45 | X = np.asarray(X, dtype=np.float64) 46 | tau = g/T + g1 47 | G = np.exp(-alpha*tau) 48 | lngama = nrtl_cy(X, tau, G) 49 | 50 | return lngama 51 | 52 | 53 | def nrtlter_aux(X, tau, G, D): 54 | X = np.asarray(X, dtype=np.float64) 55 | lngama = nrtl_cy(X, tau, G) 56 | 57 | xd = X*D 58 | lngama += rkter_nrtl_cy(X, xd) 59 | # lngama += nrtl(X, T, alpha, g, g1) 60 | return lngama 61 | 62 | 63 | def dnrtlter_aux(X, tau, G, D): 64 | X = np.asarray(X, dtype=np.float64) 65 | 66 | xd = X*D 67 | lngamaD, dlngamaD = drkter_nrtl_cy(X, xd, D) 68 | lngama, dlngama = dnrtl_cy(X, tau, G) 69 | 70 | lngama += lngamaD 71 | dlngama += dlngamaD 72 | 73 | return lngama, dlngama 74 | 75 | 76 | def nrtlter(X, T, alpha, g, g1, D): 77 | ''' 78 | NRTL activity coefficient model. 79 | 80 | Parameters 81 | ---------- 82 | X: array like 83 | vector of molar fractions 84 | T: float 85 | absolute temperature in K 86 | g: array like 87 | matrix of energy interactions in K 88 | g1: array_like 89 | matrix of energy interactions in 1/K 90 | alpha: array_like 91 | aleatory factor 92 | D : array_like 93 | ternary contribution parameters 94 | 95 | tau = ((g + g1*T)/T) 96 | 97 | Returns 98 | ------- 99 | lngama: array_like 100 | natural logarithm of activify coefficient 101 | ''' 102 | xd = X*D 103 | lngama = rkter_nrtl_cy(X, xd) 104 | lngama += nrtl(X, T, alpha, g, g1) 105 | return lngama 106 | 107 | 108 | def dnrtl(X, T, alpha, g, g1): 109 | ''' 110 | Derivatives of NRTL activity coefficient model. 111 | 112 | Parameters 113 | ---------- 114 | X: array like 115 | vector of molar fractions 116 | T: float 117 | absolute temperature in K 118 | g: array like 119 | matrix of energy interactions in K 120 | g1: array_like 121 | matrix of energy interactions in 1/K 122 | alpha: array_like 123 | aleatory factor. 124 | 125 | Notes 126 | ----- 127 | tau = ((g + g1*T)/T) 128 | 129 | Returns 130 | ------- 131 | lngama: array_like 132 | natural logarithm of activify coefficient 133 | dlngama: array_like 134 | derivative of natural logarithm of activify coefficient 135 | ''' 136 | X = np.asarray(X, dtype=np.float64) 137 | tau = g/T + g1 138 | G = np.exp(-alpha*tau) 139 | lngama, dlngama = dnrtl_cy(X, tau, G) 140 | 141 | return lngama, dlngama 142 | 143 | 144 | def dnrtlter(X, T, alpha, g, g1, D): 145 | ''' 146 | Derivatives of NRTL activity coefficient model with additional ternary 147 | contribution. 148 | 149 | Parameters 150 | ---------- 151 | X: array like 152 | vector of molar fractions 153 | T: float 154 | absolute temperature in K 155 | g: array like 156 | matrix of energy interactions in K 157 | g1: array_like 158 | matrix of energy interactions in 1/K 159 | alpha: array_like 160 | aleatory factor 161 | D : array_like 162 | ternary contribution parameters 163 | 164 | tau = ((g + g1*T)/T) 165 | 166 | Returns 167 | ------- 168 | lngama: array_like 169 | natural logarithm of activify coefficient 170 | dlngama: array_like 171 | derivative of natural logarithm of activify coefficient 172 | ''' 173 | 174 | xd = X*D 175 | lngamaD, dlngamaD = drkter_nrtl_cy(X, xd, D) 176 | lngama, dlngama = dnrtl(X, T, alpha, g, g1) 177 | 178 | lngama += lngamaD 179 | dlngama += dlngamaD 180 | 181 | return lngama, dlngama 182 | -------------------------------------------------------------------------------- /phasepy/actmodels/original_unifac.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | def unifac_original_aux(x, qi, ri, Vk, Qk, tethai, psi): 6 | 7 | # Combinatory part 8 | rx = np.dot(x, ri) 9 | qx = np.dot(x, qi) 10 | phi_x = ri/rx 11 | tetha = x*qi / qx 12 | phi_tetha = (ri*qx) / (qi*rx) 13 | 14 | lngamac = np.log(phi_x) + 1. - phi_x 15 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 16 | 17 | # Residual part 18 | Xm = x@Vk 19 | Xm = Xm/Xm.sum() 20 | tetha = Xm*Qk 21 | tetha /= tetha.sum() 22 | 23 | SumA = tetha@psi 24 | SumB = (psi*tetha)@(1./SumA) 25 | Gm = Qk * (1 - np.log(SumA) - SumB) 26 | 27 | SumAi = tethai@psi 28 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 29 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 30 | 31 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 32 | 33 | lngama = lngamac + lngamar 34 | return lngama 35 | 36 | 37 | def dunifac_original_aux(x, qi, ri, Vk, Qk, tethai, psi): 38 | 39 | # Combinatory part 40 | rx = np.dot(x, ri) 41 | qx = np.dot(x, qi) 42 | phi_x = ri/rx 43 | 44 | phi_tetha = (ri*qx) / (qi*rx) 45 | 46 | dphi_x = - np.outer(ri, ri)/rx**2 47 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 48 | 49 | lngamac = np.log(phi_x) + 1. - phi_x 50 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 51 | 52 | dlngamac = (dphi_x * (1./phi_x - 1.)).T 53 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 54 | 55 | # Residual part 56 | Xm1 = x@Vk 57 | Xm1s = Xm1.sum() 58 | dXm1s = np.sum(Vk, axis=1) 59 | Xm = Xm1 / Xm1s 60 | dXm = (Vk * Xm1s - np.outer(dXm1s, Xm1))/Xm1s**2 61 | 62 | tetha1 = Xm*Qk 63 | tetha1s = tetha1.sum() 64 | tetha = tetha1/tetha1s 65 | dtetha = ((Qk*dXm)*tetha1s-np.outer(dXm@Qk, tetha1))/tetha1s**2 66 | 67 | SumA = tetha@psi 68 | dSumA = dtetha@psi 69 | dter1 = (dSumA / SumA).T 70 | 71 | SumB = (psi*tetha)@(1./SumA) 72 | dSumB = (psi/SumA)@dtetha.T - (tetha*psi/SumA**2)@dSumA.T 73 | 74 | Gm = Qk * (1 - np.log(SumA) - SumB) 75 | dlnGk = (Qk * (- dter1 - dSumB).T).T 76 | 77 | SumAi = tethai@psi 78 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 79 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 80 | 81 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 82 | dlngamar = Vk@dlnGk 83 | 84 | lngama = lngamac + lngamar 85 | dlngama = dlngamac + dlngamar 86 | 87 | return lngama, dlngama 88 | 89 | 90 | def unifac_original(x, T, qi, ri, Vk, Qk, tethai, amn): 91 | ''' 92 | Derivatives of Dortmund UNIFAC activity coefficient model 93 | for multicomponent mixtures. Group definitions and parameter values from 94 | `Dortmund public database `_. 95 | Function returns array of natural logarithm of activity 96 | 97 | Parameters 98 | ---------- 99 | X: array like 100 | vector of molar fractions 101 | T: float 102 | absolute temperature in K 103 | qi: array like 104 | component surface array 105 | ri: array_like 106 | component volumes arrays 107 | Vk : array_like 108 | group volumes array 109 | Qk : array_like 110 | group surface arrays 111 | tethai : array_like 112 | surface fraction array 113 | amn : array_like 114 | energy interactions coefficient 115 | 116 | Returns 117 | ------- 118 | lngama: array_like 119 | natural logarithm of activify coefficient 120 | ''' 121 | # Combinatory part 122 | rx = np.dot(x, ri) 123 | qx = np.dot(x, qi) 124 | phi_x = ri/rx 125 | tetha = x*qi / qx 126 | phi_tetha = (ri*qx) / (qi*rx) 127 | 128 | lngamac = np.log(phi_x) + 1. - phi_x 129 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 130 | 131 | # Residual part 132 | psi = np.exp(-amn/T) 133 | Xm = x@Vk 134 | Xm = Xm/Xm.sum() 135 | tetha = Xm*Qk 136 | tetha /= tetha.sum() 137 | 138 | SumA = tetha@psi 139 | SumB = (psi*tetha)@(1./SumA) 140 | Gm = Qk * (1 - np.log(SumA) - SumB) 141 | 142 | SumAi = tethai@psi 143 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 144 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 145 | 146 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 147 | 148 | lngama = lngamac + lngamar 149 | return lngama 150 | 151 | 152 | def dunifac_original(x, T, qi, ri, Vk, Qk, tethai, amn): 153 | ''' 154 | Derivatives of Dortmund UNIFAC activity coefficient model 155 | for multicomponent mixtures. Group definitions and parameter values from 156 | `Dortmund public database `_. 157 | Function returns array of natural logarithm of activity 158 | 159 | Parameters 160 | ---------- 161 | X: array like 162 | vector of molar fractions 163 | T: float 164 | absolute temperature in K 165 | qi: array like 166 | component surface array 167 | ri: array_like 168 | component volumes arrays 169 | Vk : array_like 170 | group volumes array 171 | Qk : array_like 172 | group surface arrays 173 | tethai : array_like 174 | surface fraction array 175 | amn : array_like 176 | energy interactions coefficient 177 | 178 | Returns 179 | ------- 180 | lngama: array_like 181 | natural logarithm of activify coefficient 182 | dlngama: array_like 183 | derivative of natural logarithm of activify coefficient respect to molar fraction 184 | ''' 185 | 186 | # Combinatory part 187 | rx = np.dot(x, ri) 188 | qx = np.dot(x, qi) 189 | phi_x = ri/rx 190 | 191 | phi_tetha = (ri*qx) / (qi*rx) 192 | 193 | dphi_x = - np.outer(ri, ri)/rx**2 194 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 195 | 196 | lngamac = np.log(phi_x) + 1. - phi_x 197 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 198 | 199 | dlngamac = (dphi_x * (1./phi_x - 1.)).T 200 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 201 | 202 | # Residual part 203 | psi = np.exp(-amn/T) 204 | 205 | Xm1 = x@Vk 206 | Xm1s = Xm1.sum() 207 | dXm1s = np.sum(Vk, axis=1) 208 | Xm = Xm1 / Xm1s 209 | dXm = (Vk * Xm1s - np.outer(dXm1s, Xm1))/Xm1s**2 210 | 211 | tetha1 = Xm*Qk 212 | tetha1s = tetha1.sum() 213 | tetha = tetha1/tetha1s 214 | dtetha = ((Qk*dXm)*tetha1s-np.outer(dXm@Qk, tetha1))/tetha1s**2 215 | 216 | SumA = tetha@psi 217 | dSumA = dtetha@psi 218 | dter1 = (dSumA / SumA).T 219 | 220 | SumB = (psi*tetha)@(1./SumA) 221 | dSumB = (psi/SumA)@dtetha.T - (tetha*psi/SumA**2)@dSumA.T 222 | 223 | Gm = Qk * (1 - np.log(SumA) - SumB) 224 | dlnGk = (Qk * (- dter1 - dSumB).T).T 225 | 226 | SumAi = tethai@psi 227 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 228 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 229 | 230 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 231 | dlngamar = Vk@dlnGk 232 | 233 | lngama = lngamac + lngamar 234 | dlngama = dlngamac + dlngamar 235 | 236 | return lngama, dlngama 237 | -------------------------------------------------------------------------------- /phasepy/actmodels/redlichkister.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .actmodels_cy import rkb_cy, rk_cy, drk_cy 4 | 5 | 6 | def rkb_aux(x, G): 7 | x = np.asarray(x, dtype=np.float64) 8 | Mp = rkb_cy(x, G) 9 | return Mp 10 | 11 | 12 | def rk_aux(x, G, combinatory): 13 | x = np.asarray(x, dtype=np.float64) 14 | ge, dge = rk_cy(x, G, combinatory) 15 | Mp = ge + dge - np.dot(dge, x) 16 | return Mp 17 | 18 | 19 | def drk_aux(x, G, combinatory): 20 | x = np.asarray(x, dtype=np.float64) 21 | ge, dge, d2ge = drk_cy(x, G, combinatory) 22 | Mp = ge + dge - np.dot(dge, x) 23 | dMp = d2ge - d2ge@x 24 | return Mp, dMp 25 | 26 | 27 | def rkb(x, T, C, C1): 28 | ''' 29 | Redlich-Kister activity coefficient model for multicomponent mixtures. 30 | 31 | Parameters 32 | ---------- 33 | X: array_like 34 | vector of molar fractions 35 | T: float 36 | absolute temperature in K 37 | C: array_like 38 | polynomial values adim 39 | C1: array_like 40 | polynomial values in K 41 | 42 | Notes 43 | ----- 44 | 45 | G = C + C1/T 46 | 47 | Returns 48 | ------- 49 | lngama: array_like 50 | natural logarithm of activify coefficient 51 | ''' 52 | x = np.asarray(x, dtype=np.float64) 53 | 54 | G = C + C1 / T 55 | Mp = rkb_cy(x, G) 56 | return Mp 57 | 58 | 59 | def rk(x, T, C, C1, combinatory): 60 | r''' 61 | Redlich-Kister activity coefficient model for multicomponent 62 | mixtures. This method uses a polynomial fit of Gibbs excess 63 | energy. It is not recommended to use more than 5 terms of the 64 | polynomial expansion. Function returns array of natural logarithm 65 | of activity coefficients. 66 | 67 | .. math:: 68 | g^e_{ij} = x_ix_j \sum_{k=0}^m C_k (x_i - x_j)^k 69 | 70 | .. math:: 71 | G = C + C_1/T 72 | 73 | Parameters 74 | ---------- 75 | X: array 76 | Molar fractions 77 | T: float 78 | Absolute temperature [K] 79 | C: array 80 | Polynomial coefficient values adim 81 | C1: array 82 | Polynomial coefficient values [K] 83 | ''' 84 | x = np.asarray(x, dtype=np.float64) 85 | 86 | G = C + C1 / T 87 | ge, dge = rk_cy(x, G, combinatory) 88 | Mp = ge + dge - np.dot(dge, x) 89 | return Mp 90 | 91 | 92 | def drk(x, T, C, C1, combinatory): 93 | ''' 94 | Derivatives of Redlich-Kister activity coefficient model 95 | for multicomponent mixtures. 96 | 97 | Parameters 98 | ---------- 99 | X: array_like 100 | vector of molar fractions 101 | T: float 102 | absolute temperature in K 103 | C: array_like 104 | polynomial values adim 105 | C1: array_like 106 | polynomial values in K 107 | combinatory: array_like 108 | index by pairs of Redlich Kister Expansion 109 | 110 | Notes 111 | ----- 112 | 113 | G = C + C1/T 114 | 115 | Returns 116 | ------- 117 | lngama: array_like 118 | natural logarithm of activify coefficient 119 | dlngama: array_like 120 | derivative of natural logarithm of activify coefficient 121 | ''' 122 | x = np.asarray(x, dtype=np.float64) 123 | 124 | G = C + C1 / T 125 | ge, dge, d2ge = drk_cy(x, G, combinatory) 126 | Mp = ge + dge - np.dot(dge, x) 127 | dMp = d2ge - d2ge@x 128 | return Mp, dMp 129 | -------------------------------------------------------------------------------- /phasepy/actmodels/unifac.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | def unifac_aux(x, qi, ri, ri34, Vk, Qk, tethai, amn, psi): 6 | 7 | # Combinatory part 8 | rx = np.dot(x, ri) 9 | r34x = np.dot(x, ri34) 10 | qx = np.dot(x, qi) 11 | phi = ri34/r34x 12 | phi_tetha = (ri*qx) / (qi*rx) 13 | lngamac = np.log(phi) 14 | lngamac += 1 - phi 15 | lngamac -= 5*qi*(np.log(phi_tetha)+1-phi_tetha) 16 | 17 | # Residual part 18 | Xm = x@Vk 19 | Xm = Xm/Xm.sum() 20 | tetha = Xm*Qk 21 | tetha /= tetha.sum() 22 | 23 | SumA = tetha@psi 24 | SumB = (psi*tetha)@(1./SumA) 25 | Gm = Qk * (1 - np.log(SumA) - SumB) 26 | 27 | SumAi = tethai@psi 28 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 29 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 30 | 31 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 32 | 33 | return lngamac + lngamar 34 | 35 | 36 | def dunifac_aux(x, qi, ri, ri34, Vk, Qk, tethai, amn, psi): 37 | 38 | # nc = len(x) 39 | # ng = len(Qk) 40 | 41 | # Combinatory part 42 | rx = np.dot(x, ri) 43 | r34x = np.dot(x, ri34) 44 | qx = np.dot(x, qi) 45 | phi = ri34/r34x 46 | phi_tetha = (ri*qx) / (qi*rx) 47 | lngamac = np.log(phi) 48 | lngamac += 1 - phi 49 | lngamac -= 5*qi*(np.log(phi_tetha)+1-phi_tetha) 50 | 51 | dphi = - np.outer(ri34, ri34)/r34x**2 52 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 53 | dlngamac = (dphi * (1/phi - 1)).T 54 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 55 | 56 | # Residual part 57 | Xm1 = x@Vk 58 | Xm1s = Xm1.sum() 59 | dXm1s = np.sum(Vk, axis=1) 60 | Xm = Xm1 / Xm1s 61 | dXm = (Vk * Xm1s - np.outer(dXm1s, Xm1))/Xm1s**2 62 | 63 | tetha1 = Xm*Qk 64 | tetha1s = tetha1.sum() 65 | tetha = tetha1/tetha1s 66 | dtetha = ((Qk*dXm)*tetha1s-np.outer(dXm@Qk, tetha1))/tetha1s**2 67 | 68 | SumA = tetha@psi 69 | dSumA = dtetha@psi 70 | dter1 = (dSumA / SumA).T 71 | 72 | SumB = (psi*tetha)@(1./SumA) 73 | dSumB = (psi/SumA)@dtetha.T - (tetha*psi/SumA**2)@dSumA.T 74 | 75 | Gm = Qk * (1 - np.log(SumA) - SumB) 76 | dlnGk = (Qk * (- dter1 - dSumB).T).T 77 | 78 | SumAi = tethai@psi 79 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 80 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 81 | 82 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 83 | dlngamar = Vk@dlnGk 84 | 85 | lngama = lngamac + lngamar 86 | dlngama = dlngamac + dlngamar 87 | 88 | return lngama, dlngama 89 | 90 | 91 | def unifac(x, T, qi, ri, ri34, Vk, Qk, tethai, a0, a1, a2): 92 | r''' 93 | Dortmund Modified-UNIFAC activity coefficient model for 94 | multicomponent mixtures is a group contribution method, which uses 95 | group definitions and parameter values from 96 | `Dortmund public database `_. 97 | Function returns array of natural logarithm of activity 98 | coefficients. 99 | 100 | .. math:: 101 | \ln \gamma_i = \ln \gamma_i^{comb} + \ln \gamma_i^{res} 102 | 103 | Energy interaction equation is 104 | 105 | .. math:: 106 | a_{mn} = a_0 + a_1 T + a_2 T^2 107 | 108 | Parameters 109 | ---------- 110 | X: array 111 | Molar fractions 112 | T: float 113 | Absolute temperature [K] 114 | qi: array 115 | Component surface array 116 | ri: array 117 | Component volumes array 118 | ri34 : array 119 | Component volume array, exponent 3/4 120 | Vk : array 121 | Group volumes 122 | Qk : array 123 | Group surface array 124 | tethai : array 125 | Surface fractions 126 | a0 : array 127 | Energy interactions polynomial coefficients 128 | a1 : array 129 | Energy interactions polynomial coefficients 130 | a2 : array 131 | Energy interactions polynomial coefficients 132 | ''' 133 | 134 | # nc = len(x) 135 | # ng = len(Qk) 136 | 137 | # Combinatory part 138 | rx = np.dot(x, ri) 139 | r34x = np.dot(x, ri34) 140 | qx = np.dot(x, qi) 141 | phi = ri34/r34x 142 | phi_tetha = (ri*qx) / (qi*rx) 143 | lngamac = np.log(phi) 144 | lngamac += 1 - phi 145 | lngamac -= 5*qi*(np.log(phi_tetha)+1-phi_tetha) 146 | 147 | amn = a0 + a1 * T + a2 * T**2 148 | 149 | # Residual part 150 | psi = np.exp(-amn/T) 151 | Xm = x@Vk 152 | Xm = Xm/Xm.sum() 153 | tetha = Xm*Qk 154 | tetha /= tetha.sum() 155 | 156 | SumA = tetha@psi 157 | SumB = (psi*tetha)@(1./SumA) 158 | Gm = Qk * (1 - np.log(SumA) - SumB) 159 | 160 | SumAi = tethai@psi 161 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 162 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 163 | 164 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 165 | 166 | return lngamac + lngamar 167 | 168 | 169 | def dunifac(x, T, qi, ri, ri34, Vk, Qk, tethai, a0, a1, a2): 170 | ''' 171 | Derivatives of Dortmund UNIFAC activity coefficient model 172 | for multicomponent mixtures. 173 | 174 | Parameters 175 | ---------- 176 | X: array like 177 | vector of molar fractions 178 | T: float 179 | absolute temperature in K 180 | qi: array like 181 | component surface array 182 | ri: array_like 183 | component volumes arrays 184 | ri34 : array_like 185 | component volumen arrays power to 3/4 186 | Vk : array_like 187 | group volumes array 188 | Qk : array_like 189 | group surface arrays 190 | tethai : array_like 191 | surface fraction array 192 | a0 : array_like 193 | energy interactions polynomial coefficient 194 | a1 : array_like 195 | energy interactions polynomial coefficient 196 | a2 : array_like 197 | energy interactions polynomial coefficient 198 | 199 | Notes 200 | ----- 201 | Energy interaction arrays: amn = a0 + a1 * T + a2 * T**2 202 | 203 | Returns 204 | ------- 205 | lngama: array_like 206 | natural logarithm of activify coefficient 207 | dlngama: array_like 208 | derivative of natural logarithm of activify coefficient 209 | ''' 210 | 211 | # nc = len(x) 212 | # ng = len(Qk) 213 | 214 | # Combinatory part 215 | rx = np.dot(x, ri) 216 | r34x = np.dot(x, ri34) 217 | qx = np.dot(x, qi) 218 | phi = ri34/r34x 219 | phi_tetha = (ri*qx) / (qi*rx) 220 | lngamac = np.log(phi) 221 | lngamac += 1 - phi 222 | lngamac -= 5*qi*(np.log(phi_tetha)+1-phi_tetha) 223 | 224 | dphi = - np.outer(ri34, ri34)/r34x**2 225 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 226 | dlngamac = (dphi * (1/phi - 1)).T 227 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 228 | 229 | amn = a0 + a1 * T + a2 * T**2 230 | 231 | # Residual part 232 | psi = np.exp(-amn/T) 233 | 234 | Xm1 = x@Vk 235 | Xm1s = Xm1.sum() 236 | dXm1s = np.sum(Vk, axis=1) 237 | Xm = Xm1 / Xm1s 238 | dXm = (Vk * Xm1s - np.outer(dXm1s, Xm1))/Xm1s**2 239 | 240 | tetha1 = Xm*Qk 241 | tetha1s = tetha1.sum() 242 | tetha = tetha1/tetha1s 243 | dtetha = ((Qk*dXm)*tetha1s-np.outer(dXm@Qk, tetha1))/tetha1s**2 244 | 245 | SumA = tetha@psi 246 | dSumA = dtetha@psi 247 | dter1 = (dSumA / SumA).T 248 | 249 | SumB = (psi*tetha)@(1./SumA) 250 | dSumB = (psi/SumA)@dtetha.T - (tetha*psi/SumA**2)@dSumA.T 251 | 252 | Gm = Qk * (1 - np.log(SumA) - SumB) 253 | dlnGk = (Qk * (- dter1 - dSumB).T).T 254 | 255 | SumAi = tethai@psi 256 | SumBi = np.tensordot((tethai/SumAi), psi, axes=(1, 1)) 257 | Gi = Qk * (1 - np.log(SumAi) - SumBi) 258 | 259 | lngamar = (Vk*(Gm - Gi)).sum(axis=1) 260 | dlngamar = Vk@dlnGk 261 | 262 | lngama = lngamac + lngamar 263 | dlngama = dlngamac + dlngamar 264 | 265 | return lngama, dlngama 266 | -------------------------------------------------------------------------------- /phasepy/actmodels/uniquac.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | def uniquac_aux(x, ri, qi, tau): 6 | 7 | # Combinatory part 8 | rx = np.dot(x, ri) 9 | qx = np.dot(x, qi) 10 | phi_x = ri/rx 11 | tetha = x*qi / qx 12 | phi_tetha = (ri*qx) / (qi*rx) 13 | 14 | lngamac = np.log(phi_x) + 1. - phi_x 15 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 16 | 17 | # residual part 18 | Sj = np.matmul(tetha, tau) 19 | SumA = np.matmul(tau, (tetha/Sj)) 20 | lngamar = 1. - np.log(Sj) - SumA 21 | lngamar *= qi 22 | 23 | lngama = lngamac + lngamar 24 | return lngama 25 | 26 | 27 | def duniquac_aux(x, ri, qi, tau): 28 | 29 | # Combinatory part 30 | rx = np.dot(x, ri) 31 | qx = np.dot(x, qi) 32 | phi_x = ri/rx 33 | 34 | phi_tetha = (ri*qx) / (qi*rx) 35 | 36 | dphi_x = - np.outer(ri, ri)/rx**2 37 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 38 | 39 | lngamac = np.log(phi_x) + 1. - phi_x 40 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 41 | 42 | dlngamac = (dphi_x * (1./phi_x - 1.)).T 43 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 44 | 45 | # Residual part 46 | nc = len(x) 47 | dx = np.eye(nc) 48 | tethai = x*qi 49 | dtethai = dx*qi 50 | tetha = tethai / qx 51 | Sj = np.matmul(tetha, tau) 52 | SumA = np.matmul(tau, (tetha/Sj)) 53 | 54 | lngamar = 1. - np.log(Sj) - SumA 55 | lngamar *= qi 56 | 57 | dtetha = ((dtethai*qx - np.outer(tethai, qi)) / qx**2).T 58 | dSj = np.matmul(dtetha, tau) 59 | dSumA_aux = ((dtetha*Sj - tetha*dSj)/Sj**2).T 60 | dSumA = np.matmul(tau, dSumA_aux) 61 | 62 | dlngamar = (-dSj/Sj - dSumA.T) 63 | dlngamar *= qi 64 | 65 | lngama = lngamac + lngamar 66 | dlngama = dlngamac + dlngamar 67 | return lngama, dlngama 68 | 69 | 70 | def uniquac(x, T, ri, qi, a0, a1): 71 | r""" 72 | UNIQUAC activity coefficient model. This function returns array of natural 73 | logarithm of activity coefficients. 74 | 75 | .. math:: 76 | \ln \gamma_i = \ln \gamma_i^{comb} + \ln \gamma_i^{res} 77 | 78 | Energy interaction equation is: 79 | 80 | .. math:: 81 | a_{ij} = a_0 + a_1 T 82 | 83 | Parameters 84 | ---------- 85 | x: array 86 | Molar fractions 87 | T: float 88 | Absolute temperature [K] 89 | ri: array 90 | Component volumes array 91 | qi: array 92 | Component surface array 93 | a0 : array 94 | Energy interactions polynomial coefficients [K] 95 | a1 : array 96 | Energy interactions polynomial coefficients [Adim] 97 | 98 | Returns 99 | ------- 100 | lngama: array 101 | natural logarithm of activity coefficients. 102 | dlngama: array 103 | composition derivatives of activity coefficients natural logarithm. 104 | """ 105 | # Combinatory part 106 | rx = np.dot(x, ri) 107 | qx = np.dot(x, qi) 108 | phi_x = ri/rx 109 | tetha = x*qi / qx 110 | phi_tetha = (ri*qx) / (qi*rx) 111 | 112 | lngamac = np.log(phi_x) + 1. - phi_x 113 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 114 | 115 | # residual part 116 | Aij = a0 + a1 * T 117 | tau = np.exp(-Aij/T) 118 | Sj = np.matmul(tetha, tau) 119 | SumA = np.matmul(tau, (tetha/Sj)) 120 | lngamar = 1. - np.log(Sj) - SumA 121 | lngamar *= qi 122 | 123 | lngama = lngamac + lngamar 124 | return lngama 125 | 126 | 127 | def duniquac(x, T, ri, qi, a0, a1): 128 | r""" 129 | UNIQUAC activity coefficient model. This function returns array of natural 130 | logarithm of activity coefficients and its composition derivates matrix. 131 | 132 | .. math:: 133 | \ln \gamma_i = \ln \gamma_i^{comb} + \ln \gamma_i^{res} 134 | 135 | Energy interaction equation is: 136 | 137 | .. math:: 138 | a_{ij} = a_0 + a_1 T 139 | 140 | Parameters 141 | ---------- 142 | x: array 143 | Molar fractions 144 | T: float 145 | Absolute temperature [K] 146 | ri: array 147 | Component volumes array 148 | qi: array 149 | Component surface array 150 | a0 : array 151 | Energy interactions polynomial coefficients [K] 152 | a1 : array 153 | Energy interactions polynomial coefficients [Adim] 154 | 155 | Returns 156 | ------- 157 | lngama: array 158 | natural logarithm of activity coefficients. 159 | """ 160 | # Combinatory part 161 | rx = np.dot(x, ri) 162 | qx = np.dot(x, qi) 163 | phi_x = ri/rx 164 | 165 | phi_tetha = (ri*qx) / (qi*rx) 166 | 167 | dphi_x = - np.outer(ri, ri)/rx**2 168 | dphi_tetha = np.outer(ri/qi, rx*qi - ri*qx) / rx**2 169 | 170 | lngamac = np.log(phi_x) + 1. - phi_x 171 | lngamac -= 5.*qi*(np.log(phi_tetha) + 1. - phi_tetha) 172 | 173 | dlngamac = (dphi_x * (1./phi_x - 1.)).T 174 | dlngamac -= 5*qi*(dphi_tetha.T * (1/phi_tetha - 1)) 175 | 176 | # Residual part 177 | nc = len(x) 178 | dx = np.eye(nc) 179 | tethai = x*qi 180 | dtethai = dx*qi 181 | tetha = tethai / qx 182 | 183 | Aij = a0 + a1 * T 184 | tau = np.exp(-Aij/T) 185 | 186 | Sj = np.matmul(tetha, tau) 187 | SumA = np.matmul(tau, (tetha/Sj)) 188 | 189 | lngamar = 1. - np.log(Sj) - SumA 190 | lngamar *= qi 191 | 192 | dtetha = ((dtethai*qx - np.outer(tethai, qi)) / qx**2).T 193 | dSj = np.matmul(dtetha, tau) 194 | dSumA_aux = ((dtetha*Sj - tetha*dSj)/Sj**2).T 195 | dSumA = np.matmul(tau, dSumA_aux) 196 | 197 | dlngamar = (-dSj/Sj - dSumA.T) 198 | dlngamar *= qi 199 | 200 | lngama = lngamac + lngamar 201 | dlngama = dlngamac + dlngamar 202 | return lngama, dlngama 203 | -------------------------------------------------------------------------------- /phasepy/actmodels/virial.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..constants import R 4 | 5 | 6 | def Virialmix(mix): 7 | ''' 8 | VirialMix creates the needed arrays to work with multicomponent 9 | virial eos 10 | 11 | Parameters 12 | ---------- 13 | mix: object 14 | Created from two or more components 15 | 16 | Returns 17 | ------- 18 | Tij: array 19 | Square matrix of critical temperatures 20 | Pij: array 21 | Square matrix of critical pressures 22 | Zij: array 23 | Square matrix of critical compressibility factor 24 | wij: array 25 | Square matrix of acentric factor 26 | 27 | ''' 28 | Tc = np.asarray(mix.Tc) 29 | Pc = np.asarray(mix.Pc) 30 | Zc = np.asarray(mix.Zc) 31 | w = np.asarray(mix.w) 32 | Vc = np.asarray(mix.Vc) 33 | 34 | Vc3 = Vc**(1/3) 35 | vij = (np.add.outer(Vc3, Vc3)/2)**3 36 | kij = 1 - np.sqrt(np.outer(Vc, Vc))/vij 37 | Tij = np.sqrt(np.outer(Tc, Tc))*(1-kij) 38 | wij = np.add.outer(w, w)/2 39 | Zij = np.add.outer(Zc, Zc)/2 40 | Pij = Zij*R*Tij/vij 41 | np.fill_diagonal(Pij, Pc) 42 | 43 | return Tij, Pij, Zij, wij 44 | 45 | 46 | def Tsonopoulos(T, Tij, Pij, wij): 47 | r''' 48 | Returns array of virial coefficient for a mixture at given 49 | temperature with Tsonopoulos correlation for the first virial 50 | coefficient, `B`: 51 | 52 | .. math:: 53 | \frac{BP_c}{RT_c} = B^{(0)} + \omega B^{(1)} 54 | 55 | Where :math:`B^{(0)}` and :math:`B^{(1)}` are obtained from: 56 | 57 | .. math:: 58 | B^{(0)} &= 0.1445 - \frac{0.33}{T_r} - \frac{0.1385}{T_r^2} - \frac{0.0121}{T_r^3} - \frac{0.000607}{T_r^8} \\ 59 | B^{(1)} &= 0.0637 + \frac{0.331}{T_r^2} - \frac{0.423}{T_r^3} - \frac{0.008}{T_r^8} 60 | ''' 61 | Tr = T/Tij 62 | B0 = 0.1145-0.330/Tr-0.1385/Tr**2-0.0121/Tr**3-0.000607/Tr**8 63 | B1 = 0.0637+0.331/Tr**2-0.423/Tr**3-0.008/Tr**8 64 | Bij = (B0+wij*B1)*R*Tij/Pij 65 | return Bij 66 | 67 | 68 | def Abbott(T, Tij, Pij, wij): 69 | r''' 70 | Returns array of virial coefficients for a mixture at given 71 | temperature with Abbott-Van Ness correlation for the first virial 72 | coefficient, `B`: 73 | 74 | .. math:: 75 | \frac{BP_c}{RT_c} = B^{(0)} + \omega B^{(1)} 76 | 77 | Where :math:`B^{(0)}` and :math:`B^{(1)}` are obtained from: 78 | 79 | .. math:: 80 | B^{(0)} &= 0.083 - \frac{0.422}{T_r^{1.6}}\\ 81 | B^{(1)} &= 0.139 + \frac{0.179}{T_r^{4.2}} 82 | ''' 83 | Tr = T/Tij 84 | B0 = 0.083-0.422/Tr**1.6 85 | B1 = 0.139-0.172/Tr**4.2 86 | Bij = (B0+wij*B1)*R*Tij/Pij 87 | return Bij 88 | 89 | 90 | def ideal_gas(T, Tij, Pij, wij): 91 | r''' 92 | Returns array of ideal virial coefficients (zeros). The model equation is 93 | 94 | .. math:: 95 | Z = \frac{Pv}{RT} = 1 96 | 97 | Note: Ideal gas model uses only the shape of Tij to produce zeros. 98 | ''' 99 | Bij = np.zeros_like(Tij) 100 | return Bij 101 | 102 | 103 | def virial(x, T, Tij, Pij, wij, virialmodel): 104 | ''' 105 | Computes the virial coefficient and partial virial coefficient for a 106 | mixture at given temperature and composition. 107 | 108 | Parameters 109 | ---------- 110 | x: array 111 | fraction mole array 112 | T : float 113 | absolute temperature in K 114 | Tij: array 115 | Square matrix of critical temperatures 116 | Pij: array 117 | Square matrix of critical pressures 118 | wij: array 119 | Square matrix of acentric 120 | virialmodel : function 121 | Function that computes the virial coefficient. 122 | 123 | Returns 124 | ------- 125 | Bi: array 126 | Array of virial coefficient of pure component 127 | Bp : array 128 | Array of partial virial coefficients 129 | ''' 130 | 131 | Bij = virialmodel(T, Tij, Pij, wij) 132 | 133 | Bx = Bij*x 134 | # Mixture Virial 135 | Bm = np.sum(Bx.T*x) 136 | # Molar partial virial 137 | Bp = 2*np.sum(Bx, axis=1) - Bm 138 | 139 | return Bij, Bp 140 | -------------------------------------------------------------------------------- /phasepy/actmodels/wilson.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .actmodels_cy import wilson_cy, dwilson_cy 4 | 5 | 6 | def wilson_aux(X, M): 7 | X = np.asarray(X, dtype=np.float64) 8 | lngama = wilson_cy(X, M) 9 | return lngama 10 | 11 | 12 | def dwilson_aux(X, M): 13 | X = np.asarray(X, dtype=np.float64) 14 | lngama, dlngama = dwilson_cy(X, M) 15 | return lngama, dlngama 16 | 17 | 18 | def wilson(X, T, A, vl): 19 | r'''Wilson activity coefficient model is a local composition model 20 | recommended for vapor-liquid equilibria calculation. It can't 21 | predict liquid liquid equilibrium. Function returns array of 22 | natural logarithm of activity coefficients. 23 | 24 | .. math:: 25 | g^e = \sum_{i=1}^c x_i \ln ( \sum_{j=1}^c x_j \Lambda_{ij}) 26 | 27 | Parameters 28 | ---------- 29 | X: array 30 | Molar fractions 31 | T: float 32 | Absolute temperature [K] 33 | A: array like 34 | Matrix of energy interactions [K] 35 | vl: function 36 | Returns liquid volume of species [:math:`\mathrm{cm^3/mol}`] 37 | given temperature [K] as argument. 38 | ''' 39 | X = np.asarray(X, dtype=np.float64) 40 | v = vl(T) 41 | M = np.divide.outer(v, v).T * np.exp(-A/T) 42 | lngama = wilson_cy(X, M) 43 | return lngama 44 | 45 | 46 | def dwilson(X, T, A, vl): 47 | ''' 48 | Derivatives of Wilson activity coefficient model 49 | 50 | Parameters 51 | ---------- 52 | X: array like 53 | vector of molar fractions 54 | T: float 55 | absolute temperature in K 56 | A: array like 57 | matrix of energy interactions in K 58 | vl: function 59 | liquid volume of species in cm3/mol 60 | 61 | Returns 62 | ------- 63 | lngama: array_like 64 | natural logarithm of activify coefficient 65 | dlngama: array_like 66 | derivative of natural logarithm of activify coefficient 67 | ''' 68 | X = np.asarray(X, dtype=np.float64) 69 | v = vl(T) 70 | M = np.divide.outer(v, v).T * np.exp(-A/T) 71 | lngama, dlngama = dwilson_cy(X, M) 72 | 73 | return lngama, dlngama 74 | -------------------------------------------------------------------------------- /phasepy/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | # Constants 4 | kb = 1.3806488e-23 # [J/K] Boltzman's constant 5 | Na = 6.022142e23 # [mol-1] Avogrado's Number 6 | 7 | R = 83.14 # [bar cm3 mol-1 K-1] Ideal gas constant 8 | r = 8.314 # [J mol-1 K-1] Ideal gas constant 9 | -------------------------------------------------------------------------------- /phasepy/cubic/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | phasepy.cubic: cubic equation of state with Python 3 | ======================================================= 4 | 5 | Cubic EoS 6 | --------- 7 | vdw : van der Waals EoS 8 | pr : Peng-Robinson EoE 9 | prsv : Peng-Robinson-Stryjec-Vera EoS 10 | rk : Redlich-Kwong EoS 11 | rsv : Redlich-Kwong-Soave EoS 12 | 13 | Available mixrules 14 | ------------------ 15 | 16 | qmr : quadratic mixrule 17 | mhv_nrtl : Modified Huron Vidal mixing rule with NRTL model 18 | mhv_wilson : Modified Huron Vidal mixing rule with Wilson model 19 | mhv_unifac : Modified Huron Vidal mixing rule with Wilson model 20 | mhv_rk : Modified Huron Vidal mixing rule with Redlich-Kister model 21 | 22 | ws_nrtl : Wong-Sandler mixing rule with NRTL model 23 | ws_wilson : Wong-Sandler mixing rule with Wilson model 24 | ws_unifac : Wong-Sandler mixing rule with Wilson model 25 | ws_rk : Wong-Sandler mixing rule with Redlich-Kister model 26 | 27 | Alpha functions 28 | --------------- 29 | alpha_vdw : van der Waals alpha function 30 | alpha_soave : Soave's alpha function 31 | alpha_sv : Stryjek-Vera's alpha function 32 | alpha_aat : AAT's alpha function 33 | """ 34 | 35 | from __future__ import division, print_function, absolute_import 36 | from .cubic import * 37 | from .alphas import alpha_vdw, alpha_rk, alpha_soave, alpha_sv, alpha_aat -------------------------------------------------------------------------------- /phasepy/cubic/alphas.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | def alpha_vdw(): 6 | return 1. 7 | 8 | 9 | # Redlich Kwong's alphas function 10 | def alpha_rk(T, Tc): 11 | return np.sqrt(T/Tc)**-0.5 12 | 13 | 14 | # Soaves's alpha function 15 | def alpha_soave(T, k, Tc): 16 | return (1+k*(1-np.sqrt(T/Tc)))**2 17 | 18 | 19 | # SV's alphas function 20 | def alpha_sv(T, ksv, Tc): 21 | ksv = ksv.T 22 | k0 = ksv[0] 23 | k1 = ksv[1] 24 | Tr = T/Tc 25 | sTr = np.sqrt(Tr) 26 | return (1+(k0+k1*(0.7-Tr)*(1+sTr))*(1-sTr))**2 27 | 28 | 29 | # Almeida-Aznar-Telles alphas function 30 | def alpha_aat(T, AAT, Tc): 31 | """ 32 | Parameters 33 | ---------- 34 | T : float 35 | Temperature [K] 36 | AAT : array_like 37 | AAT parameters, AAT = [AAT1, AAT2, AAT3] 38 | Tc : float 39 | Critical temperature [K] 40 | """ 41 | AAT = AAT.T 42 | AAT1 = AAT[0] 43 | AAT2 = AAT[1] 44 | AAT3 = AAT[2] 45 | Tr = T/Tc 46 | alpha_aux = AAT1 * (1. - Tr) * np.abs(1. - Tr)**(AAT2 - 1.) 47 | alpha_aux += AAT3 * (1/Tr - 1.) 48 | return np.exp(alpha_aux) 49 | -------------------------------------------------------------------------------- /phasepy/cubic/psatpure.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..constants import R 4 | from warnings import warn 5 | 6 | 7 | def psat(T, cubic, P0=None): 8 | """ 9 | Computes saturation pressure with cubic eos 10 | 11 | Parameters 12 | ---------- 13 | T : float, 14 | saturation temperature [K] 15 | cubic : object 16 | cubic eos object 17 | Returns 18 | ------- 19 | P : float 20 | saturation pressure [bar] 21 | vl: float, 22 | saturation liquid volume [cm3/mol] 23 | vv: float, 24 | saturation vapor volume [cm3/mol] 25 | """ 26 | 27 | if T >= cubic.Tc: 28 | warn('Temperature is greater than critical temperature, returning critical point') 29 | vc = 1. / cubic.density(cubic.Tc, cubic.Pc, 'L') 30 | out = cubic.Pc, vc, vc 31 | return out 32 | 33 | a = cubic.a_eos(T) 34 | b = cubic.b 35 | c1 = cubic.c1 36 | c2 = cubic.c2 37 | emin = cubic.emin 38 | e = a/(b*R*T) 39 | 40 | if P0 is None: 41 | if e > emin: # Zero fugacity initiation 42 | U = (e-c1-c2-np.sqrt((e-c1-c2)**2-4*(c1*c2+e)))/2 43 | if c1 == 0 and c2 == 0: 44 | S = -1-np.log(U-1)-e/U 45 | else: 46 | S = -1-np.log(U-1)-e*np.log((U+c1)/(U+c2))/(c1-c2) 47 | P = np.exp(S)*R*T/b # bar 48 | else: # Pmin Pmax initiation 49 | a1 = -R*T 50 | a2 = -2*b*R*T*(c1+c2)+2*a 51 | a3 = -R*T*b**2*(c1**2+4*c1*c2+c2**2)+a*b*(c1+c2-4) 52 | a4 = -R*T*2*b**3*c1*c2*(c1+c2)+2*a*b**2*(1-c1-c2) 53 | a5 = -R*T*b**4*c1*c2+a*b**3*(c1+c2) 54 | V = np.roots([a1, a2, a3, a4, a5]) 55 | V = V[np.isreal(V)] 56 | V = V[V > b] 57 | P = cubic(T, V) 58 | P[P < 0] = 0. 59 | P = P.mean() 60 | else: 61 | P = P0 62 | itmax = 20 63 | for k in range(itmax): 64 | A = a*P/(R*T)**2 65 | B = b*P/(R*T) 66 | Z = cubic._Zroot(A, B) 67 | Zl = min(Z) 68 | Zv = max(Z) 69 | fugL = cubic._logfug_aux(Zl, A, B) 70 | fugV = cubic._logfug_aux(Zv, A, B) 71 | FO = fugV-fugL 72 | dFO = (Zv-Zl)/P 73 | dP = FO/dFO 74 | P -= dP 75 | if abs(dP) < 1e-8: 76 | break 77 | vl = Zl*R*T/P 78 | vv = Zv*R*T/P 79 | return P, vl, vv 80 | -------------------------------------------------------------------------------- /phasepy/cubic/qmr.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | # Quadratic mixrule 6 | def qmr(X, RT, ai, bi, order, Kij): 7 | ''' 8 | Quadratic mixrule QMR 9 | 10 | Inputs 11 | ---------- 12 | X : molar fraction array [x1, x2, ..., xc] 13 | RT: Absolute temperature in K plus R 14 | ai : pure component attrative term in bar cm6/mol2 15 | bi : pure component cohesive term in cm3/mol 16 | Kij : matrix of interaction parameters 17 | 18 | 19 | Out : 20 | D (mixture a term) 21 | Di (mixture a term first derivative) 22 | Dij (mixture a term second derivative) 23 | B (mixture b term) 24 | Bi (mixture b term first derivative) 25 | Bij (mixture a term second derivative) 26 | ''' 27 | 28 | aij = np.sqrt(np.outer(ai, ai))*(1-Kij) 29 | ax = aij * X 30 | # Atractive term of mixture 31 | D = np.sum(ax.T*X) 32 | # Mixture covolume 33 | B = np.dot(bi, X) 34 | 35 | if order == 0: 36 | mixparameters = D, B 37 | elif order == 1: 38 | Di = 2*np.sum(ax, axis=1) 39 | Bi = bi 40 | mixparameters = D, Di, B, Bi 41 | elif order == 2: 42 | Di = 2*np.sum(ax, axis=1) 43 | Dij = 2*aij 44 | Bi = bi 45 | Bij = 0. 46 | mixparameters = D, Di, Dij, B, Bi, Bij 47 | else: 48 | raise Exception('Derivative order not valid') 49 | 50 | return mixparameters 51 | -------------------------------------------------------------------------------- /phasepy/cubic/tsatpure.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | from scipy.optimize import brentq, newton 3 | from ..constants import R 4 | 5 | 6 | def fobj_tsat(T, P, cubic): 7 | 8 | a = cubic.a_eos(T) 9 | b = cubic.b 10 | 11 | A = a*P/(R*T)**2 12 | B = b*P/(R*T) 13 | Z = cubic._Zroot(A, B) 14 | Zl = min(Z) 15 | Zv = max(Z) 16 | fugL = cubic._logfug_aux(Zl, A, B) 17 | fugV = cubic._logfug_aux(Zv, A, B) 18 | FO = fugV-fugL 19 | 20 | return FO 21 | 22 | 23 | def tsat(cubic, P, T0=None, Tbounds=None): 24 | """ 25 | Computes saturation temperature with cubic eos 26 | 27 | Parameters 28 | ---------- 29 | cubic: object 30 | cubic eos object 31 | P: float 32 | saturation pressure [bar] 33 | T0 : float, optional 34 | Temperature to start iterations [K] 35 | Tbounds : tuple, optional 36 | (Tmin, Tmax) Temperature interval to start iterations [K] 37 | 38 | Returns 39 | ------- 40 | T : float 41 | saturation temperature [K] 42 | vl: float 43 | saturation liquid volume [cm3/mol] 44 | vv: float 45 | saturation vapor volume [cm3/mol] 46 | 47 | """ 48 | bool1 = T0 is None 49 | bool2 = Tbounds is None 50 | 51 | if bool1 and bool2: 52 | raise Exception('You must provide either Tbounds or T0') 53 | 54 | if not bool1: 55 | sol = newton(fobj_tsat, x0=T0, args=(P, cubic), 56 | full_output=False) 57 | Tsat = sol[0] 58 | elif not bool2: 59 | sol = brentq(fobj_tsat, Tbounds[0], Tbounds[1], args=(P, cubic), 60 | full_output=False) 61 | Tsat = sol 62 | 63 | vl = 1./cubic.density(Tsat, P, 'L') 64 | vv = 1./cubic.density(Tsat, P, 'V') 65 | out = (Tsat, vl, vv) 66 | return out 67 | -------------------------------------------------------------------------------- /phasepy/cubic/volume_solver.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | 4 | 5 | # Auxiliar function that computes objective function and its derivatives 6 | def _volume_newton_aux(V, B, D_RT, P_RT, c1, c2): 7 | 8 | Vc1B = V + c1*B 9 | Vc2B = V + c2*B 10 | V_B = V - B 11 | 12 | f0 = P_RT - (1./V_B - D_RT / (Vc1B * Vc2B)) 13 | 14 | df0 = 1/V_B**2 - D_RT * (1./(Vc1B * Vc2B**2) + 1./(Vc1B**2 * Vc2B)) 15 | 16 | return f0, df0 17 | 18 | 19 | # Functions that solves volume using Newtons's Method 20 | def volume_newton(v0, P_RT, D_RT, B, c1, c2): 21 | V = 1. * v0 22 | f0, df0 = _volume_newton_aux(V, B, D_RT, P_RT, c1, c2) 23 | error = np.abs(f0) 24 | it = 0 25 | while error > 1e-8 and it < 20: 26 | it += 1 27 | dV = - f0 / df0 28 | V += dV 29 | error = np.abs(f0) 30 | f0, df0 = _volume_newton_aux(V, B, D_RT, P_RT, c1, c2) 31 | return V 32 | -------------------------------------------------------------------------------- /phasepy/database/dortmund-2018.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/phasepy/database/dortmund-2018.xlsx -------------------------------------------------------------------------------- /phasepy/database/dortmund-2021.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/phasepy/database/dortmund-2021.xlsx -------------------------------------------------------------------------------- /phasepy/database/original-unifac.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/phasepy/database/original-unifac.xlsx -------------------------------------------------------------------------------- /phasepy/database/unifac.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavochm/phasepy/0d5115865dfcf3ba4932f5a6ced4ec7d618e6680/phasepy/database/unifac.xlsx -------------------------------------------------------------------------------- /phasepy/equilibrium/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | phasepy.equilibrium: phase equilibria with Python 3 | ======================================================= 4 | 5 | 6 | Functions 7 | --------- 8 | bubbleTy : bubble point P, x -> T, y 9 | bubblePy : bubble point T, x -> P, y 10 | dewTx : dew point P, y -> T, x 11 | dewTy : dew point T, y -> P, x 12 | flash : istohermal isobaric two phase flash z, T, P -> x,y,beta 13 | lle : liquid liquid equilibrium z, T, P -> x, w, beta 14 | lle_init : finds initial guess for ell 15 | multiflash : multiflash algorithm that checks stability of the phases 16 | vlleb : heteroazetropic calculation (VLLE) for binary mixtures 17 | vlle : heteroazetropic calculation (VLLE) for multicomponent mixtures 18 | 19 | tpd : Michelsen tpd function 20 | tpd_mim : finds a minimum of tpd function given a initial guess 21 | tpd_minimas : tries to find n minimas of tpd function 22 | 23 | """ 24 | 25 | from __future__ import division, print_function, absolute_import 26 | 27 | # __all__ = [s for s in dir() if not s.startswith('_')] 28 | 29 | from .bubble import bubblePy, bubbleTy 30 | from .dew import dewPx, dewTx 31 | from .flash import flash 32 | from .multiflash import multiflash 33 | from .hazt import haz, vlle 34 | from .hazb import vlleb 35 | from .stability import tpd_min, tpd_minimas, lle_init 36 | from .stability import tpd_val, gmix 37 | from .lle import lle 38 | from .solidequilibria import slle, sle 39 | -------------------------------------------------------------------------------- /phasepy/equilibrium/dew.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import root 4 | from ..math import gdem 5 | from .equilibriumresult import EquilibriumResult 6 | 7 | 8 | # ELV phi-phi 9 | def dew_sus(P_T, Y, T_P, type, x_guess, eos, vl0, vv0): 10 | 11 | if type == 'T': 12 | P = P_T 13 | temp_aux = T_P 14 | elif type == 'P': 15 | T = P_T 16 | temp_aux = eos.temperature_aux(T) 17 | P = T_P 18 | 19 | # Vapour fugacities 20 | lnphiv, vv0 = eos.logfugef_aux(Y, temp_aux, P, 'V', vv0) 21 | 22 | tol = 1e-8 23 | error = 1 24 | itacc = 0 25 | niter = 0 26 | n = 5 27 | X_calc = x_guess 28 | X = x_guess 29 | 30 | while error > tol and itacc < 3: 31 | niter += 1 32 | 33 | # Liquid fugacitiies 34 | lnphil, vl0 = eos.logfugef_aux(X, temp_aux, P, 'L', vl0) 35 | 36 | lnK = lnphil-lnphiv 37 | K = np.exp(lnK) 38 | X_calc_old = X_calc 39 | X_calc = Y/K 40 | 41 | if niter == (n-3): 42 | X3 = X_calc 43 | elif niter == (n-2): 44 | X2 = X_calc 45 | elif niter == (n-1): 46 | X1 = X_calc 47 | elif niter == n: 48 | niter = 0 49 | itacc += 1 50 | dacc = gdem(X_calc, X1, X2, X3) 51 | X_calc += dacc 52 | error = np.linalg.norm(X_calc - X_calc_old) 53 | X = X_calc/X_calc.sum() 54 | 55 | if type == 'T': 56 | f0 = X_calc.sum() - 1 57 | elif type == 'P': 58 | f0 = np.log(X_calc.sum()) 59 | 60 | return f0, X, lnK, vl0, vv0 61 | 62 | 63 | def dew_newton(inc, Y, T_P, type, eos): 64 | 65 | global vl, vv 66 | 67 | f = np.zeros_like(inc) 68 | lnK = inc[:-1] 69 | K = np.exp(lnK) 70 | 71 | if type == 'T': 72 | P = inc[-1] 73 | temp_aux = T_P 74 | elif type == 'P': 75 | T = inc[-1] 76 | temp_aux = eos.temperature_aux(T) 77 | P = T_P 78 | 79 | X = Y/K 80 | 81 | # Liquid fugacities 82 | lnphil, vl = eos.logfugef_aux(X, temp_aux, P, 'L', vl) 83 | # Vapor fugacities 84 | lnphiv, vv = eos.logfugef_aux(Y, temp_aux, P, 'V', vv) 85 | 86 | f[:-1] = lnK + lnphiv - lnphil 87 | f[-1] = (Y-X).sum() 88 | 89 | return f 90 | 91 | 92 | def dewPx(x_guess, P_guess, y, T, model, good_initial=False, 93 | v0=[None, None], full_output=False): 94 | """ 95 | Dew point (y, T) -> (x, P) 96 | 97 | Solves dew point (liquid phase composition and pressure) at given 98 | temperature and vapor composition. 99 | 100 | Parameters 101 | ---------- 102 | x_guess : array 103 | Initial guess of liquid phase molar fractions 104 | P_guess : float 105 | Initial guess of equilibrium pressure [bar] 106 | y : array 107 | Vapor phase molar fractions 108 | T : float 109 | Temperature [K] 110 | model : object 111 | Phase equilibrium model object 112 | good_initial: bool, optional 113 | If True uses only phase envelope method in solution 114 | v0 : list, optional 115 | Liquid and vapor phase molar volume used as initial values to 116 | compute fugacities 117 | full_output: bool, optional 118 | Flag to return a dictionary of all calculation info 119 | 120 | Returns 121 | ------- 122 | x : array 123 | Liquid molar fractions 124 | P : float 125 | Equilibrium pressure [bar] 126 | """ 127 | nc = model.nc 128 | if len(x_guess) != nc or len(y) != nc: 129 | raise Exception('Composition vector lenght must be equal to nc') 130 | 131 | global vl, vv 132 | vl0, vv0 = v0 133 | 134 | temp_aux = model.temperature_aux(T) 135 | 136 | it = 0 137 | itmax = 10 138 | tol = 1e-8 139 | 140 | P = P_guess 141 | f, X, lnK, vl, vv = dew_sus(P, y, temp_aux, 'T', x_guess, model, vl0, vv0) 142 | error = np.abs(f) 143 | h = 1e-3 144 | 145 | while error > tol and it <= itmax and not good_initial: 146 | it += 1 147 | f1, X1, lnK1, vl, vv = dew_sus(P+h, y, temp_aux, 'T', X, model, vl, vv) 148 | f, X, lnK, vl, vv = dew_sus(P, y, temp_aux, 'T', X, model, vl, vv) 149 | df = (f1-f)/h 150 | dP = f / df 151 | if dP > P: 152 | dP = 0.4 * P 153 | elif np.isnan(dP): 154 | dP = 0.0 155 | it = 1.*itmax 156 | P -= dP 157 | error = np.abs(f) 158 | 159 | if error > tol: 160 | inc0 = np.hstack([lnK, P]) 161 | sol1 = root(dew_newton, inc0, args=(y, temp_aux, 'T', model)) 162 | sol = sol1.x 163 | lnK = sol[:-1] 164 | error = np.linalg.norm(sol1.fun) 165 | it += sol1.nfev 166 | lnK = sol[:-1] 167 | X = y / np.exp(lnK) 168 | P = sol[-1] 169 | 170 | if full_output: 171 | sol = {'T': T, 'P': P, 'error': error, 'iter': it, 172 | 'X': X, 'v1': vl, 'state1': 'Liquid', 173 | 'Y': y, 'v2': vv, 'state2': 'Vapor'} 174 | out = EquilibriumResult(sol) 175 | return out 176 | 177 | return X, P 178 | 179 | 180 | def dewTx(x_guess, T_guess, y, P, model, good_initial=False, 181 | v0=[None, None], full_output=False): 182 | """ 183 | Dew point (y, P) -> (x, T) 184 | 185 | Solves dew point (liquid phase composition and temperature) at given 186 | pressure and vapor phase composition. 187 | 188 | Parameters 189 | ---------- 190 | x_guess : array 191 | Initial guess of liquid phase molar fractions 192 | T_guess : float 193 | Initial guess of equilibrium temperature [K] 194 | y : array 195 | Vapor phase molar fractions 196 | P : float 197 | Pressure [bar] 198 | model : object 199 | Phase equilibrium model object 200 | good_initial: bool, optional 201 | If True uses only phase envelope method in solution 202 | v0 : list, optional 203 | Liquid and vapor phase molar volume used as initial values to 204 | compute fugacities 205 | full_output: bool, optional 206 | Flag to return a dictionary of all calculation info 207 | 208 | Returns 209 | ------- 210 | x : array 211 | Liquid molar fractions 212 | T : float 213 | Equilibrium temperature [K] 214 | """ 215 | 216 | nc = model.nc 217 | if len(x_guess) != nc or len(y) != nc: 218 | raise Exception('Composition vector lenght must be equal to nc') 219 | 220 | global vl, vv 221 | vl0, vv0 = v0 222 | 223 | it = 0 224 | itmax = 10 225 | tol = 1e-8 226 | 227 | T = T_guess 228 | f, X, lnK, vl, vv = dew_sus(T, y, P, 'P', x_guess, model, vl0, vv0) 229 | error = np.abs(f) 230 | h = 1e-4 231 | 232 | while error > tol and it <= itmax and not good_initial: 233 | it += 1 234 | f1, X1, lnK1, vl, vv = dew_sus(T+h, y, P, 'P', X, model, vl, vv) 235 | f, X, lnK, vl, vv = dew_sus(T, y, P, 'P', X, model, vl, vv) 236 | df = (f1-f)/h 237 | if np.isnan(df): 238 | df = 0.0 239 | it = 1.*itmax 240 | T -= f/df 241 | error = np.abs(f) 242 | 243 | if error > tol: 244 | inc0 = np.hstack([lnK, T]) 245 | sol1 = root(dew_newton, inc0, args=(y, P, 'P', model)) 246 | sol = sol1.x 247 | lnK = sol[:-1] 248 | error = np.linalg.norm(sol1.fun) 249 | it += sol1.nfev 250 | lnK = sol[:-1] 251 | X = y / np.exp(lnK) 252 | T = sol[-1] 253 | 254 | if full_output: 255 | sol = {'T': T, 'P': P, 'error': error, 'iter': it, 256 | 'X': X, 'v1': vl, 'state1': 'Liquid', 257 | 'Y': y, 'v2': vv, 'state2': 'Vapor'} 258 | out = EquilibriumResult(sol) 259 | return out 260 | 261 | return X, T 262 | 263 | 264 | __all__ = ['dewTx', 'dewPx'] 265 | -------------------------------------------------------------------------------- /phasepy/equilibrium/equilibriumresult.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | # obtained from scipy optimize result 4 | 5 | class EquilibriumResult(dict): 6 | """ 7 | Extended dictionary class with pretty printing 8 | """ 9 | def __getattr__(self, name): 10 | try: 11 | return self[name] 12 | except KeyError: 13 | raise AttributeError(name) 14 | 15 | __setattr__ = dict.__setitem__ 16 | __delattr__ = dict.__delitem__ 17 | 18 | def __repr__(self): 19 | 20 | if self.keys(): 21 | m = max(map(len, list(self.keys()))) + 1 22 | return '\n'.join([k.rjust(m) + ': ' + repr(v) 23 | for k, v in self.items()]) 24 | else: 25 | return self.__class__.__name__ + "()" 26 | -------------------------------------------------------------------------------- /phasepy/equilibrium/flash.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..math import gdem 4 | from scipy.optimize import minimize 5 | from .equilibriumresult import EquilibriumResult 6 | 7 | 8 | def rachfordrice(beta, K, Z): 9 | ''' 10 | Solves Rachford Rice equation by Halley's method 11 | ''' 12 | K1 = K-1. 13 | g0 = np.dot(Z, K) - 1. 14 | g1 = 1. - np.dot(Z, 1/K) 15 | singlephase = False 16 | 17 | if g0 < 0: 18 | beta = 0. 19 | D = np.ones_like(Z) 20 | singlephase = True 21 | elif g1 > 0: 22 | beta = 1. 23 | D = 1 + K1 24 | singlephase = True 25 | it = 0 26 | e = 1. 27 | while e > 1e-8 and it < 20 and not singlephase: 28 | it += 1 29 | D = 1 + beta*K1 30 | KD = K1/D 31 | fo = np.dot(Z, KD) 32 | dfo = - np.dot(Z, KD**2) 33 | d2fo = 2*np.dot(Z, KD**3) 34 | dbeta = - (2*fo*dfo)/(2*dfo**2-fo*d2fo) 35 | beta += dbeta 36 | e = np.abs(dbeta) 37 | 38 | return beta, D, singlephase 39 | 40 | 41 | def Gibbs_obj(v, phases, Z, temp_aux, P, model): 42 | ''' 43 | Objective function to minimize Gibbs energy in biphasic flash 44 | ''' 45 | l = Z-v 46 | v[v < 1e-8] = 1e-8 47 | l[l < 1e-8] = 1e-8 48 | X = l/l.sum() 49 | Y = v/v.sum() 50 | global v1, v2 51 | lnfugl, v1 = model.logfugef_aux(X, temp_aux, P, phases[0], v1) 52 | lnfugv, v2 = model.logfugef_aux(Y, temp_aux, P, phases[1], v2) 53 | fugl = np.log(X) + lnfugl 54 | fugv = np.log(Y) + lnfugv 55 | fo = v*fugv + l*fugl 56 | f = np.sum(fo) 57 | df = fugv - fugl 58 | return f, df 59 | 60 | 61 | def dGibbs_obj(v, phases, Z, temp_aux, P, model): 62 | ''' 63 | Objective function to minimize Gibbs energy in biphasic flash when second 64 | order derivatives are available 65 | ''' 66 | 67 | l = Z - v 68 | v[v < 1e-8] = 1e-8 69 | l[l < 1e-8] = 1e-8 70 | vt = np.sum(v) 71 | lt = np.sum(l) 72 | X = l/lt 73 | Y = v/vt 74 | nc = len(l) 75 | eye = np.eye(nc) 76 | 77 | global v1, v2 78 | lnfugl, dlnfugl, v1 = model.dlogfugef_aux(X, temp_aux, P, phases[0], v1) 79 | lnfugv, dlnfugv, v2 = model.dlogfugef_aux(Y, temp_aux, P, phases[1], v2) 80 | 81 | fugl = np.log(X) + lnfugl 82 | fugv = np.log(Y) + lnfugv 83 | fo = v*fugv + l*fugl 84 | f = np.sum(fo) 85 | df = fugv - fugl 86 | 87 | global dfugv, dfugl 88 | dfugv = eye/v - 1/vt + dlnfugv/vt 89 | dfugl = eye/l - 1/lt + dlnfugl/lt 90 | 91 | return f, df 92 | 93 | 94 | def dGibbs_hess(v, phases, Z, temp_aux, P, model): 95 | ''' 96 | Hessian to minimize Gibbs energy in biphasic flash when second 97 | order derivatives are available 98 | ''' 99 | global dfugv, dfugl 100 | d2fo = dfugv + dfugl 101 | return d2fo 102 | 103 | 104 | def flash(x_guess, y_guess, equilibrium, Z, T, P, model, 105 | v0=[None, None], K_tol=1e-8, nacc=5, full_output=False): 106 | """ 107 | Isobaric isothermic (PT) flash: (Z, T, P) -> (x, y, beta) 108 | 109 | Parameters 110 | ---------- 111 | x_guess : array 112 | Initial guess for molar fractions of phase 1 (liquid) 113 | y_guess : array 114 | Initial guess for molar fractions of phase 2 (gas or liquid) 115 | equilibrium : string 116 | Two-phase system definition: 'LL' (liquid-liquid) or 117 | 'LV' (liquid-vapor) 118 | Z : array 119 | Overall molar fractions of components 120 | T : float 121 | Absolute temperature [K] 122 | P : float 123 | Pressure [bar] 124 | model : object 125 | Phase equilibrium model object 126 | v0 : list, optional 127 | Liquid and vapor phase molar volume used as initial values to compute 128 | fugacities 129 | K_tol : float, optional 130 | Tolerance for equilibrium constant values 131 | nacc : int, optional 132 | number of accelerated successive substitution cycles to perform 133 | full_output: bool, optional 134 | Flag to return a dictionary of all calculation info 135 | 136 | Returns 137 | ------- 138 | x : array 139 | Phase 1 molar fractions of components 140 | y : array 141 | Phase 2 molar fractions of components 142 | beta : float 143 | Phase 2 phase fraction 144 | """ 145 | nc = model.nc 146 | if len(x_guess) != nc or len(y_guess) != nc or len(Z) != nc: 147 | raise Exception('Composition vector lenght must be equal to nc') 148 | 149 | temp_aux = model.temperature_aux(T) 150 | 151 | v10, v20 = v0 152 | 153 | e1 = 1 154 | itacc = 0 155 | it = 0 156 | it2 = 0 157 | n = 5 158 | X = x_guess 159 | Y = y_guess 160 | 161 | global v1, v2 162 | fugl, v1 = model.logfugef_aux(X, temp_aux, P, equilibrium[0], v10) 163 | fugv, v2 = model.logfugef_aux(Y, temp_aux, P, equilibrium[1], v20) 164 | lnK = fugl - fugv 165 | K = np.exp(lnK) 166 | 167 | bmin = max(np.hstack([((K*Z-1.)/(K-1.))[K > 1], 0.])) 168 | bmax = min(np.hstack([((1.-Z)/(1.-K))[K < 1], 1.])) 169 | beta = (bmin + bmax)/2 170 | 171 | while e1 > K_tol and itacc < nacc: 172 | it += 1 173 | it2 += 1 174 | lnK_old = lnK 175 | beta, D, singlephase = rachfordrice(beta, K, Z) 176 | 177 | X = Z/D 178 | Y = X*K 179 | X /= X.sum() 180 | Y /= Y.sum() 181 | fugl, v1 = model.logfugef_aux(X, temp_aux, P, equilibrium[0], v1) 182 | fugv, v2 = model.logfugef_aux(Y, temp_aux, P, equilibrium[1], v2) 183 | 184 | lnK = fugl-fugv 185 | if it == (n-3): 186 | lnK3 = lnK 187 | elif it == (n-2): 188 | lnK2 = lnK 189 | elif it == (n-1): 190 | lnK1 = lnK 191 | elif it == n: 192 | it = 0 193 | itacc += 1 194 | dacc = gdem(lnK, lnK1, lnK2, lnK3) 195 | lnK += dacc 196 | K = np.exp(lnK) 197 | e1 = ((lnK-lnK_old)**2).sum() 198 | 199 | if e1 > K_tol and itacc == nacc and not singlephase: 200 | if model.secondorder: 201 | fobj = dGibbs_obj 202 | jac = True 203 | hess = dGibbs_hess 204 | method = 'trust-ncg' 205 | else: 206 | fobj = Gibbs_obj 207 | jac = True 208 | hess = None 209 | method = 'BFGS' 210 | 211 | vsol = minimize(fobj, beta*Y, jac=jac, method=method, hess=hess, 212 | tol=K_tol, args=(equilibrium, Z, temp_aux, P, model)) 213 | 214 | it2 += vsol.nit 215 | e1 = np.linalg.norm(vsol.jac) 216 | v = vsol.x 217 | l = Z - v 218 | beta = v.sum() 219 | v[v <= 1e-8] = 0 220 | l[l <= 1e-8] = 0 221 | Y = v / beta 222 | Y /= Y.sum() 223 | X = l/l.sum() 224 | 225 | if beta == 1.0: 226 | X = Y.copy() 227 | elif beta == 0.: 228 | Y = X.copy() 229 | 230 | if full_output: 231 | sol = {'T': T, 'P': P, 'beta': beta, 'error': e1, 'iter': it2, 232 | 'X': X, 'v1': v1, 'state1': equilibrium[0], 233 | 'Y': Y, 'v2': v2, 'state2': equilibrium[1]} 234 | out = EquilibriumResult(sol) 235 | return out 236 | 237 | return X, Y, beta 238 | -------------------------------------------------------------------------------- /phasepy/equilibrium/hazb.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import root 4 | from .equilibriumresult import EquilibriumResult 5 | 6 | ''' 7 | def haz_objb(inc, T_P, type, model): 8 | 9 | X, W, Y, P_T = np.array_split(inc, 4) 10 | 11 | if type == 'T': 12 | P = P_T 13 | T = T_P 14 | elif type == 'P': 15 | T = P_T 16 | P = T_P 17 | 18 | temp_aux = model.temperature_aux(T) 19 | 20 | global vx, vw, vy 21 | 22 | fugX, vx = model.logfugef_aux(X, temp_aux, P, 'L') 23 | fugW, vw = model.logfugef_aux(W, temp_aux, P, 'L') 24 | fugY, vy = model.logfugef_aux(Y, temp_aux, P, 'V') 25 | 26 | K1 = np.exp(fugX-fugY) 27 | K2 = np.exp(fugX-fugW) 28 | return np.hstack([K1*X-Y, K2*X-W, X.sum()-1, Y.sum()-1, W.sum()-1]) 29 | ''' 30 | 31 | def haz_objb(inc, T_P, type, model): 32 | 33 | X, W, Y, P_T = np.array_split(inc, 4) 34 | # P_T = P_T[0] 35 | if type == 'T': 36 | P = P_T 37 | temp_aux = T_P 38 | elif type == 'P': 39 | T = P_T 40 | temp_aux = model.temperature_aux(T) 41 | P = T_P 42 | 43 | global vx, vw, vy 44 | 45 | fugX, vx = model.logfugef_aux(X, temp_aux, P, 'L', vx) 46 | fugW, vw = model.logfugef_aux(W, temp_aux, P, 'L', vw) 47 | fugY, vy = model.logfugef_aux(Y, temp_aux, P, 'V', vy) 48 | 49 | K1 = np.exp(fugX-fugY) 50 | K2 = np.exp(fugX-fugW) 51 | return np.hstack([K1*X-Y, K2*X-W, X.sum()-1, Y.sum()-1, W.sum()-1]) 52 | 53 | 54 | def vlleb(X0, W0, Y0, P_T, T_P, spec, model, v0=[None, None, None], 55 | full_output=False): 56 | ''' 57 | Solves component molar fractions in each phase and either 58 | temperature or pressure in vapor-liquid-liquid equilibrium (VLLE) 59 | of binary (two component) mixture: (T or P) -> (X, W, Y, and P or T) 60 | 61 | Parameters 62 | ---------- 63 | X0 : array 64 | Initial guess molar fractions of liquid phase 1 65 | W0 : array 66 | Initial guess molar fractions of liquid phase 2 67 | Y0 : array 68 | Initial guess molar fractions of vapor phase 69 | P_T : float 70 | Absolute temperature [K] or pressure [bar] (see *spec*) 71 | T_P : float 72 | Absolute temperature [K] or pressure [bar] (see *spec*) 73 | spec: string 74 | 'T' if T_P is temperature or 'P' if T_P is pressure. 75 | model : object 76 | Phase equilibrium model object 77 | v0 : list, optional 78 | Liquid phase 1 and 2 and vapor phase molar volume used as initial 79 | values to compute fugacities 80 | full_output: bool, optional 81 | Flag to return a dictionary of all calculation info 82 | 83 | Returns 84 | ------- 85 | X : array 86 | Liquid phase 1 molar fractions 87 | W : array 88 | Liquid phase 2 molar fractions 89 | Y : array 90 | Vapor phase molar fractions 91 | var: float 92 | Temperature [K] or pressure [bar], opposite of *spec* 93 | 94 | ''' 95 | 96 | nc = model.nc 97 | if nc != 2: 98 | raise Exception('vlleb() requires a binary mixture') 99 | 100 | if len(X0) != nc or len(W0) != nc or len(Y0) != nc: 101 | raise Exception('Composition vector lenght must be equal to nc') 102 | 103 | global vx, vw, vy 104 | vx, vw, vy = v0 105 | ''' 106 | sol1 = root(haz_objb, np.hstack([X0, W0, Y0, P_T]), 107 | args=(T_P, spec, model, v0)) 108 | ''' 109 | if spec == 'T': 110 | temp_aux = model.temperature_aux(T_P) 111 | sol1 = root(haz_objb, np.hstack([X0, W0, Y0, P_T]), 112 | args=(temp_aux, spec, model)) 113 | elif spec == 'P': 114 | sol1 = root(haz_objb, np.hstack([X0, W0, Y0, P_T]), 115 | args=(T_P, spec, model)) 116 | else: 117 | raise Exception('Specification not known') 118 | 119 | error = np.linalg.norm(sol1.fun) 120 | nfev = sol1.nfev 121 | sol = sol1.x 122 | if np.any(sol < 0): 123 | raise Exception('composition, T or P is negative') 124 | X, W, Y, var = np.array_split(sol, 4) 125 | 126 | if full_output: 127 | if spec == 'T': 128 | P = var 129 | T = T_P 130 | elif spec == 'P': 131 | T = var 132 | P = T_P 133 | inc = {'T': T, 'P': P, 'error': error, 'nfev': nfev, 134 | 'X': X, 'vx': vx, 'statex': 'Liquid', 135 | 'W': W, 'vw': vw, 'statew': 'Liquid', 136 | 'Y': Y, 'vy': vy, 'statey': 'Vapor'} 137 | out = EquilibriumResult(inc) 138 | return out 139 | 140 | return X, W, Y, var 141 | -------------------------------------------------------------------------------- /phasepy/equilibrium/lle.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .stability import tpd_minimas 4 | from .multiflash import multiflash 5 | 6 | 7 | def lle(x0, w0, Z, T, P, model, v0=[None, None], 8 | K_tol=1e-8, nacc=5, full_output=False): 9 | """ 10 | Isobaric isothermic (PT) flash for multicomponent liquid-liquid 11 | systems: (Z, T, P) -> (x, w, beta) 12 | 13 | Parameters 14 | ---------- 15 | x0 : array 16 | Initial guess for molar fractions of liquid phase 1 17 | w0 : array 18 | Initial guess for molar fractions of liquid phase 2 19 | Z : array 20 | Overall molar fractions of components 21 | T : float 22 | Absolute temperature [K] 23 | P : float 24 | Pressure [bar] 25 | model : object 26 | Phase equilibrium model object 27 | v0 : list, optional 28 | Liquid phase 1 and 2 molar volumes used as initial values to compute 29 | fugacities 30 | K_tol : float, optional 31 | Tolerance for equilibrium constant values 32 | nacc : int, optional 33 | number of accelerated successive substitution cycles to perform 34 | full_output: bool, optional 35 | Flag to return a dictionary of all calculation info 36 | 37 | Returns 38 | ------- 39 | x : array 40 | Phase 1 molar fractions of components 41 | w : array 42 | Phase 2 molar fractions of components 43 | beta : float 44 | Phase 2 phase fraction 45 | """ 46 | nc = model.nc 47 | if len(x0) != nc or len(w0) != nc or len(Z) != nc: 48 | raise Exception('Composition vector lenght must be equal to nc') 49 | 50 | equilibrium = ['L', 'L'] 51 | 52 | temp_aux = model.temperature_aux(T) 53 | 54 | fugx, v1 = model.logfugef_aux(x0, temp_aux, P, equilibrium[0], v0[0]) 55 | fugw, v2 = model.logfugef_aux(w0, temp_aux, P, equilibrium[1], v0[1]) 56 | lnK = fugx - fugw 57 | K = np.exp(lnK) 58 | 59 | bmin = max(np.hstack([((K*Z-1.)/(K-1.))[K > 1], 0.])) 60 | bmax = min(np.hstack([((1.-Z)/(1.-K))[K < 1], 1.])) 61 | beta = (bmin + bmax)/2 62 | X0 = np.array([x0, w0]) 63 | 64 | beta0 = np.array([1-beta, beta, 0.]) 65 | 66 | out = multiflash(X0, beta0, equilibrium, Z, T, P, model, 67 | [v1, v2], K_tol, nacc, True) 68 | Xm, beta, tetha, v = out.X, out.beta, out.tetha, out.v 69 | 70 | if tetha > 0: 71 | xes, tpd_min2 = tpd_minimas(2, Xm[0], T, P, model, 'L', 'L', 72 | v[0], v[0]) 73 | X0 = np.asarray(xes) 74 | beta0 = np.hstack([beta, 0.]) 75 | out = multiflash(X0, beta0, equilibrium, Z, T, P, model, v, K_tol, 76 | nacc, True) 77 | Xm, beta, tetha, v = out.X, out.beta, out.tetha, out.v 78 | 79 | X, W = Xm 80 | if tetha > 0: 81 | W = X.copy() 82 | 83 | if full_output: 84 | return out 85 | 86 | return X, W, beta[1] 87 | 88 | 89 | __all__ = ['lle'] 90 | -------------------------------------------------------------------------------- /phasepy/fit/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | 4 | __all__ = [s for s in dir() if not s.startswith('_')] 5 | 6 | from .binaryfit import * 7 | from .ternaryfit import * 8 | from .fitpsat import * 9 | from .fitcii import * 10 | from .fitmulticomponent import * 11 | from .fitvt import * 12 | -------------------------------------------------------------------------------- /phasepy/fit/fitcii.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..sgt import ten_fit 4 | from ..math import gauss 5 | 6 | 7 | def fit_cii(tenexp, Texp, model, order=2, n=100, P0=None): 8 | """ 9 | fit influence parameters of SGT 10 | 11 | Parameters 12 | ---------- 13 | tenexp: array_like 14 | experimental surface tension in mN/m 15 | Texp: array_like 16 | experimental temperature in K. 17 | model: object 18 | created from eos and component 19 | order: int, optional 20 | order of cii polynomial 21 | n : int, optional 22 | number of integration points in SGT 23 | P0 : array_like , optional 24 | initial guess for saturation pressure 25 | 26 | Returns 27 | ------- 28 | cii : array_like 29 | polynomial coefficients of influence parameters of SGT 30 | """ 31 | roots, weigths = gauss(n) 32 | tena = np.zeros_like(tenexp) 33 | neq = len(Texp) 34 | if P0 is None: 35 | psat0 = neq * [None] 36 | else: 37 | if len(P0) != neq: 38 | raise Exception('P0 lenght must be the same as Texp') 39 | else: 40 | psat0 = P0 41 | 42 | for i in range(len(Texp)): 43 | tena[i] = ten_fit(Texp[i], model, roots, weigths, psat0[i]) 44 | 45 | cii = (tenexp/tena)**2 46 | 47 | return np.polyfit(Texp, cii, order) 48 | -------------------------------------------------------------------------------- /phasepy/fit/fitmulticomponent.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..equilibrium import bubblePy, lle, tpd_min, vlleb, haz 4 | 5 | 6 | def fobj_vle(model, Xexp, Yexp, Texp, Pexp, weights_vle=[1., 1.]): 7 | """ 8 | Objective function to fit parameters for VLE in multicomponent mixtures 9 | """ 10 | 11 | n = Pexp.shape[0] 12 | n1, n2 = Xexp.shape 13 | if n2 == n: 14 | Xexp = Xexp.T 15 | Yexp = Yexp.T 16 | 17 | P = np.zeros(n) # n, 18 | Y = np.zeros_like(Xexp) # nc,n 19 | 20 | for i in range(n): 21 | Y[i], P[i] = bubblePy(Yexp[i], Pexp[i], Xexp[i], Texp[i], model) 22 | 23 | error = weights_vle[0] * np.sum((Y-Yexp)**2) 24 | error += weights_vle[1] * np.sum((P/Pexp-1)**2) 25 | error /= n 26 | return error 27 | 28 | 29 | def fobj_lle(model, Xexp, Wexp, Texp, Pexp, tpd=True, weights_lle=[1., 1.]): 30 | """ 31 | Objective function to fit parameters for LLE in multicomponent mixtures 32 | """ 33 | n = Pexp.shape[0] 34 | n1, n2 = Xexp.shape 35 | if n2 == n: 36 | Xexp = Xexp.T 37 | Wexp = Wexp.T 38 | 39 | X = np.zeros_like(Xexp) 40 | W = np.zeros_like(Wexp) 41 | Z = (Xexp+Wexp)/2 42 | 43 | for i in range(n): 44 | if tpd: 45 | X0, tpd = tpd_min(Xexp[i], Z[i], Texp[i], Pexp[i], model, 'L', 'L') 46 | W0, tpd = tpd_min(Wexp[i], Z[i], Texp[i], Pexp[i], model, 'L', 'L') 47 | else: 48 | X0 = Xexp[i] 49 | W0 = Wexp[i] 50 | X[i], W[i], beta = lle(X0, W0, Z[i], Texp[i], Pexp[i], model) 51 | 52 | error = weights_lle[0] * np.sum((X-Xexp)**2) 53 | error += weights_lle[1] * np.sum((W-Wexp)**2) 54 | error /= n 55 | return error 56 | 57 | 58 | def fobj_vlleb(model, Xellv, Wellv, Yellv, Tellv, Pellv, 59 | weights_vlleb=[1., 1., 1., 1.]): 60 | """ 61 | Objective function to fit parameters for VLLE in binary mixtures 62 | """ 63 | n = len(Tellv) 64 | n1, n2 = Xellv.shape 65 | if n2 == n: 66 | Xellv = Xellv.T 67 | Wellv = Wellv.T 68 | Yellv = Yellv.T 69 | 70 | X = np.zeros_like(Xellv) 71 | W = np.zeros_like(Wellv) 72 | Y = np.zeros_like(Yellv) 73 | P = np.zeros(n) 74 | Zll = (Xellv + Wellv) / 2 75 | 76 | for i in range(n): 77 | try: 78 | X0, tpd = tpd_min(Xellv[i], Zll[i], Tellv[i], Pellv[i], 79 | model, 'L', 'L') 80 | W0, tpd = tpd_min(Wellv[i], Zll[i], Tellv[i], Pellv[i], 81 | model, 'L', 'L') 82 | X[i], W[i], Y[i], P[i] = vlleb(X0, W0, Yellv[i], Pellv[i], 83 | Tellv[i], 'T', model) 84 | except: 85 | pass 86 | 87 | error = weights_vlleb[0] * np.sum((X-Xellv)**2) 88 | error += weights_vlleb[1] * np.sum((W-Wellv)**2) 89 | error += weights_vlleb[2] * np.sum((Y-Yellv)**2) 90 | error += weights_vlleb[3] * np.sum((P/Pellv-1)**2) 91 | error /= n 92 | 93 | return error 94 | 95 | 96 | def fobj_vllet(model, Xellv, Wellv, Yellv, Tellv, Pellv, 97 | weights_vlle=[1., 1., 1.]): 98 | """ 99 | Objective function to fit parameters for VLLE in multicomponent mixtures 100 | """ 101 | 102 | n = len(Tellv) 103 | n1, n2 = Xellv.shape 104 | if n2 == n: 105 | Xellv = Xellv.T 106 | Wellv = Wellv.T 107 | Yellv = Yellv.T 108 | 109 | X = np.zeros_like(Xellv) 110 | W = np.zeros_like(Wellv) 111 | Y = np.zeros_like(Yellv) 112 | 113 | error = 0 114 | for i in range(n): 115 | try: 116 | X[i], W[i], Y[i] = haz(Xellv[i], Wellv[i], Yellv[i], 117 | Tellv[i], Pellv[i], model, True) 118 | except ValueError: 119 | X[i], W[i], Y[i], T = haz(Xellv[i], Wellv[i], Yellv[i], Tellv[i], 120 | Pellv[i], model, True) 121 | error += (T/Tellv[i]-1)**2 122 | except: 123 | pass 124 | 125 | error += weights_vlle[0] * np.sum((np.nan_to_num(X)-Xellv)**2) 126 | error += weights_vlle[1] * np.sum((np.nan_to_num(Y)-Yellv)**2) 127 | error += weights_vlle[2] * np.sum((np.nan_to_num(W)-Wellv)**2) 128 | error /= n 129 | return error 130 | -------------------------------------------------------------------------------- /phasepy/fit/fitpsat.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | from scipy.optimize import minimize 3 | import numpy as np 4 | from ..cubic import prsveos 5 | 6 | 7 | def psat_obj(C, Pexp, Texp): 8 | pant = np.exp(C[0]-C[1]/(Texp+C[2])) 9 | error = np.sum((pant / Pexp - 1)**2) 10 | return error 11 | 12 | 13 | # Fit Antoine parameters 14 | def fit_ant(Texp, Pexp, x0=[0, 0, 0]): 15 | """ 16 | fit Antoine parameters, base exp 17 | 18 | Parameters 19 | ---------- 20 | Texp: experimental temperature in K. 21 | Pexp : experimental pressure in bar. 22 | x0 : array_like, optional 23 | initial values. 24 | 25 | Returns 26 | ------- 27 | fit : OptimizeResult 28 | Result of SciPy minimize 29 | """ 30 | fit = minimize(psat_obj, x0, args=(Pexp, Texp)) 31 | return fit 32 | 33 | 34 | def fobj_alpha(k, Texp, Pexp, cubic): 35 | cubic.k = k 36 | P = np.zeros_like(Pexp) 37 | for i in range(len(Texp)): 38 | P[i], _, _ = cubic.psat(Texp[i]) 39 | error = np.sum((P / Pexp - 1)**2) 40 | return error 41 | 42 | 43 | # fit SV alpha 44 | def fit_ksv(component, Texp, Pexp, ksv0=[1, 0]): 45 | """ 46 | fit PRSV alpha parameters 47 | 48 | Parameters 49 | ---------- 50 | component : object 51 | created with component class 52 | Texp : array_like 53 | experimental temperature in K. 54 | Pexp : array_like 55 | experimental pressure in bar. 56 | ksv0 : array_like, optional 57 | initial values. 58 | 59 | Returns 60 | ------- 61 | fit : OptimizeResult 62 | Result of SciPy minimize 63 | 64 | """ 65 | cubic = prsveos(component) 66 | fit = minimize(fobj_alpha, ksv0, args=(Texp, Pexp, cubic)) 67 | return fit 68 | -------------------------------------------------------------------------------- /phasepy/fit/fitvt.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | from scipy.optimize import minimize 3 | import numpy as np 4 | 5 | 6 | def fobj_c(c, eos, Texp, Pexp, rhoexp): 7 | eos.c = c 8 | n = len(Texp) 9 | rho = np.zeros_like(rhoexp) 10 | for i in range(n): 11 | rho[i] = eos.density(Texp[i], Pexp[i], 'L') 12 | return np.sum((rho/rhoexp - 1)**2) 13 | 14 | 15 | def fit_vt(component, eos, Texp, Pexp, rhoexp, c0=0.): 16 | """ 17 | fit Volume Translation for cubic EoS 18 | 19 | Parameters 20 | ---------- 21 | component : object 22 | created with component class 23 | eos: function 24 | cubic eos function 25 | Texp : array_like 26 | experimental temperature in K. 27 | Pexp : array_like 28 | experimental pressure in bar. 29 | rhoexp : array_like 30 | experimental liquid density at given temperature and 31 | pressure in mol/cm3. 32 | c0 : float, optional 33 | initial values. 34 | 35 | Returns 36 | ------- 37 | fit : OptimizeResult 38 | Result of SciPy minimize 39 | 40 | """ 41 | cubic = eos(component, volume_translation=True) 42 | fit = minimize(fobj_c, c0, args=(cubic, Texp, Pexp, rhoexp)) 43 | return fit 44 | -------------------------------------------------------------------------------- /phasepy/fit/ternaryfit.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..equilibrium import haz 4 | from ..actmodels import virialgamma 5 | from .fitmulticomponent import fobj_vle, fobj_lle, fobj_vllet 6 | 7 | 8 | def fobj_nrtlrkt(D, Xexp, Wexp, Yexp, Texp, Pexp, mix, good_initial=True): 9 | 10 | n = len(Pexp) 11 | mix.rkt(D) 12 | 13 | vg = virialgamma(mix, actmodel='nrtlt') 14 | x = np.zeros_like(Xexp) 15 | w = np.zeros_like(Wexp) 16 | y = np.zeros_like(Yexp) 17 | 18 | for i in range(n): 19 | x[:, i], w[:, i], y[:, i] = haz(Xexp[:, i], Wexp[:, i], Yexp[:, i], 20 | Texp[i], Pexp[i], vg, good_initial) 21 | 22 | error = ((np.nan_to_num(x)-Xexp)**2).sum() 23 | error += ((np.nan_to_num(w)-Wexp)**2).sum() 24 | error += ((np.nan_to_num(y)-Yexp)**2).sum() 25 | 26 | return error 27 | 28 | 29 | def fobj_nrtlt(inc, mix, datavle=None, datalle=None, datavlle=None, 30 | alpha_fixed=False, Tdep=False): 31 | 32 | if alpha_fixed: 33 | a12 = a13 = a23 = 0.2 34 | if Tdep: 35 | g12, g21, g13, g31, g23, g32, g12T, g21T, g13T, g31T, g23T, g32T = inc 36 | gT = np.array([[0, g12T, g13T], 37 | [g21T, 0, g23T], 38 | [g31T, g32T, 0]]) 39 | else: 40 | g12, g21, g13, g31, g23, g32 = inc 41 | gT = None 42 | else: 43 | if Tdep: 44 | g12, g21, g13, g31, g23, g32, g12T, g21T, g13T, g31T, g23T, g32T, a12, a13, a23 = inc 45 | gT = np.array([[0, g12T, g13T], 46 | [g21T, 0, g23T], 47 | [g31T, g32T, 0]]) 48 | else: 49 | g12, g21, g13, g31, g23, g32, a12, a13, a23 = inc 50 | gT = None 51 | 52 | g = np.array([[0, g12, g13], 53 | [g21, 0, g23], 54 | [g31, g32, 0]]) 55 | 56 | alpha = np.array([[0, a12, a13], 57 | [a12, 0, a23], 58 | [a13, a23, 0]]) 59 | 60 | mix.NRTL(alpha, g, gT) 61 | model = virialgamma(mix) 62 | 63 | error = 0 64 | 65 | if datavle is not None: 66 | error += fobj_vle(model, *datavle) 67 | if datalle is not None: 68 | error += fobj_lle(model, *datalle) 69 | if datavlle is not None: 70 | error += fobj_vllet(model, *datavlle) 71 | return error 72 | 73 | 74 | def fobj_kijt(inc, eos, mix, datavle=None, datalle=None, datavlle=None): 75 | 76 | k12, k13, k23 = inc 77 | Kij = np.array([[0, k12, k13], 78 | [k12, 0, k23], 79 | [k13, k23, 0]]) 80 | mix.kij_cubic(Kij) 81 | model = eos(mix) 82 | 83 | error = 0 84 | 85 | if datavle is not None: 86 | error += fobj_vle(model, *datavle) 87 | if datalle is not None: 88 | error += fobj_lle(model, *datalle) 89 | if datavlle is not None: 90 | error += fobj_vllet(model, *datavlle) 91 | return error 92 | -------------------------------------------------------------------------------- /phasepy/math.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .coloc_cy import jcobi_roots, colocAB, colocA, colocB 4 | 5 | 6 | def gauss(n): 7 | roots, dif1 = jcobi_roots(n, N0=0, N1=0, Al=0, Be=0) 8 | ax = 1 9 | ax /= roots 10 | ax /= (1-roots) 11 | vect = ax/dif1**2 12 | vect /= np.sum(vect) 13 | return roots, vect 14 | 15 | 16 | def lobatto(n): 17 | roots, dif1 = jcobi_roots(n - 2, N0=1, N1=1, Al=1, Be=1) 18 | s0 = 2/(n-1)/n 19 | vect = s0/dif1**2 20 | vect /= np.sum(vect) 21 | return roots, vect 22 | 23 | 24 | def gdem(X, X1, X2, X3): 25 | dX2 = X - X3 26 | dX1 = X - X2 27 | dX = X - X1 28 | b01 = dX.dot(dX1) 29 | b02 = dX.dot(dX2) 30 | b12 = dX1.dot(dX2) 31 | b11 = dX1.dot(dX1) 32 | b22 = dX2.dot(dX2) 33 | den = b11*b22-b12**2 34 | with np.errstate(divide='ignore', invalid='ignore'): 35 | mu1 = (b02*b12 - b01*b22)/den 36 | mu2 = (b01*b12 - b02*b11)/den 37 | dacc = (dX - mu2*dX1)/(1+mu1+mu2) 38 | return np.nan_to_num(dacc) 39 | 40 | 41 | def dem(X, X1, X2): 42 | dX1 = X1 - X2 43 | dX = X - X1 44 | lbda = np.dot(dX1, dX) / np.dot(dX, dX) 45 | dacc = dX / (1. - lbda) 46 | return np.nan_to_num(dacc) 47 | 48 | __all__ = ['gauss', 'lobatto', 'colocAB', 'colocA', 'colocB', 'gdem', 'dem'] 49 | -------------------------------------------------------------------------------- /phasepy/saft_forcefield.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | kb = 1.3806488e-23 # K/J 5 | Na = 6.02214e23 6 | 7 | a = np.array([[14.8359, 22.2019, 7220.9599, 23193.475, -6207.4663, 1732.96], 8 | [8.0034, -22.5111, 3.5750, 60.3129, 0., 0.], 9 | [6.9829, -13.7097, -1.9604, 17.3237, 0, 0], 10 | [6.4159, -34.3656, 59.6108, -21.6579, -35.8210, 27.2358], 11 | [6.1284, -9.1568, -0.2229, 4.5311, 0., 0.], 12 | [5.9217, -8.0711, 0.4264, 2.5600, 0., 0.]]) 13 | 14 | b = np.array([[0, -6.9630, 468.7358, -983.6038, 914.3608, -1383.4441], 15 | [0, -5.2669, 10.2299, -6.4860, 0., 0.], 16 | [0, -3.869, 5.2519, -2.3637, 0., 0.], 17 | [0, -6.9751, 19.2063, -26.0583, 17.4222, -4.5757], 18 | [0., -2.8486, 2.7828, -0.9030, 0., 0.], 19 | [0, -2.5291, 2.1864, -0.6298, 0., 0.]]) 20 | 21 | c = np.array([[0.1284, 1.6772, 0., 0., 0., 0.], 22 | [0.1125, 1.5404, -5.8769, 5.2427, 0., 0.], 23 | [-0.2092, 4.2672, -9.7703, 4.8661, -0.1950, 4.2125], 24 | [0.1350, 1.3115, -10.1437, 24.0729, -24.8084, 9.7109], 25 | [0.1107, 1.9807, -6.6720, 5.4841, 0., 0.], 26 | [0.1302, 1.9357, -6.4591, 5.1864, 0., 0.]]) 27 | 28 | d = np.array([[0., 0.4049, -0.1592, 0., 0., 0.], 29 | [0., -3.1964, 2.5174, 0.3518, -0.1654, 0.], 30 | [0, -1.3778, -2.4836, 3.5280, 0.7918, -0.1246], 31 | [0., -5.8540, 13.3411, -14.3302, 6.7309, -0.7830], 32 | [0, -3.1341, 2.7657, -0.2737, -0.0431, 0.], 33 | [0, -3.1078, 2.8058, -0.4375, 0., 0.]]) 34 | 35 | j = np.array([[1.8966, -6.9808, 10.6330, -9.2041, 4.2503, 0.], 36 | [-0.0696, -1.9440, 6.2575, -5.4431, 0.8731, 0.], 37 | [0.0656, -1.4630, 3.6991, -2.5081, 0., 0.], 38 | [0.1025, -1.1948, 2.8448, -1.9519, 0., 0.], 39 | [0.1108, -0.9900, 2.2187, -1.5027, 0., 0.], 40 | [0.2665, -0.4268, -0.2732, 0.6486, 0., 0.]]) 41 | 42 | k = np.array([[0, -1.6205, -0.8019, 1.7086, -0.5333, 1.0536], 43 | [0, -10.5646, 25.4914, -20.5091, 3.6753, 0.], 44 | [0., -8.9309, 18.9584, -11.6668, -0.2561, 0.], 45 | [0, -8.1077, 16.7865, -9.6354, -1.2390, 0.], 46 | [0., -7.3749, 14.5313, -7.4967, -1.9209, 0.], 47 | [0., 1.7499, -10.1370, 9.4381, 0., 0.]]) 48 | 49 | 50 | # Force Fields for Coarse-Grained Molecular Simulations 51 | # from a Corresponding States Correlation. Mejia et al, 2014 52 | def saft_forcefield(ms, Tc, w, rhol07): 53 | lambda_a = 6. 54 | index = int(ms-1) 55 | ai = a[index] 56 | bi = b[index] 57 | ci = c[index] 58 | di = d[index] 59 | ji = j[index] 60 | ki = k[index] 61 | exponent = np.arange(6) 62 | 63 | # Eq 13 lambda_r 64 | wi = w**exponent 65 | lambda_r = np.dot(ai, wi) / (1 + np.dot(bi, wi)) 66 | 67 | # Eq 14 alpha VdW 68 | alpha = (lambda_r/(lambda_r-6))*(lambda_r/6) 69 | alpha **= (6/(lambda_r-6))*(1/3-1/(lambda_r-3)) 70 | 71 | # Eq 15 T*c 72 | alphai = alpha**exponent 73 | Tc_r = np.dot(ci, alphai) / (1 + np.dot(di, alphai)) 74 | 75 | # Eq 16 calculo eps 76 | eps = Tc / Tc_r * kb 77 | 78 | # Eq 18 rho*c 79 | rhoc_r = np.dot(ji, alphai) / (1 + np.dot(ki, alphai)) 80 | 81 | # Eq 17 calculo sigma 82 | sigma = (rhoc_r / rhol07 / Na)**(1/3) 83 | 84 | return lambda_r, lambda_a, ms, eps, sigma 85 | -------------------------------------------------------------------------------- /phasepy/sgt/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | from .path_sk import ten_beta0_sk 4 | from .path_hk import ten_beta0_hk 5 | from .reference_component import ten_beta0_reference 6 | 7 | from .sgt_beta0 import sgt_mix_beta0 8 | from .sgtpure import ten_fit, sgt_pure 9 | from .linear_spot import sgt_linear, sgt_spot 10 | 11 | from .coloc_z import sgt_mix, sgt_zfixed 12 | from .coloc_z_ds import msgt_mix 13 | 14 | from .tensionresult import TensionResult 15 | -------------------------------------------------------------------------------- /phasepy/sgt/linear_spot.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import root 4 | from ..math import lobatto 5 | from .cijmix_cy import cmix_cy 6 | from .tensionresult import TensionResult 7 | 8 | 9 | def fobj_saddle(ros, mu0, T, eos): 10 | mu = eos.muad(ros, T) 11 | return mu - mu0 12 | 13 | 14 | def sgt_linear(rho1, rho2, Tsat, Psat, model, n=100, full_output=False): 15 | """ 16 | SGT linear for mixtures (rho1, rho2, T, P) -> interfacial tension 17 | 18 | Parameters 19 | ---------- 20 | rho1 : float 21 | phase 1 density vector 22 | rho2 : float 23 | phase 2 density vector 24 | Tsat : float 25 | saturation temperature 26 | Psat : float 27 | saturation pressure 28 | model : object 29 | created with an EoS 30 | n : int, optional 31 | number points to solve density profiles 32 | full_output : bool, optional 33 | wheter to outputs all calculation info 34 | 35 | Returns 36 | ------- 37 | ten : float 38 | interfacial tension between the phases 39 | """ 40 | 41 | # Dimensionless variables 42 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 43 | Pad = Psat*Pfactor 44 | ro1a = rho1*rofactor 45 | ro2a = rho2*rofactor 46 | 47 | cij = model.ci(Tsat) 48 | cij /= cij[0, 0] 49 | 50 | mu0 = model.muad(ro1a, Tsat) 51 | mu02 = model.muad(ro2a, Tsat) 52 | if not np.allclose(mu0, mu02): 53 | raise Exception('Not equilibria compositions, mu1 != mu2') 54 | roots, weights = lobatto(n) 55 | s = 0 56 | ro_s = (ro2a[s]-ro1a[s])*roots + ro1a[s] # integrations nodes 57 | wreal = np.abs(weights*(ro2a[s]-ro1a[s])) # integration weights 58 | 59 | # Linear profile 60 | pend = (ro2a - ro1a) 61 | b = ro1a 62 | ro = (np.outer(roots, pend) + b).T 63 | 64 | # Derivatives respect to component 1 65 | dro = np.gradient(ro, ro_s, edge_order=2, axis=1) 66 | 67 | suma = cmix_cy(dro, cij) 68 | dom = np.zeros(n) 69 | for k in range(1, n - 1): 70 | dom[k] = model.dOm(ro[:, k], Tsat, mu0, Pad) 71 | 72 | integral = np.nan_to_num(np.sqrt(2*dom*suma)) 73 | tension = np.dot(integral, wreal) 74 | tension *= tenfactor 75 | 76 | if full_output: 77 | # Z profile 78 | with np.errstate(divide='ignore'): 79 | intz = (np.sqrt(suma/(2*dom))) 80 | intz[np.isinf(intz)] = 0 81 | z = np.cumsum(intz*wreal) 82 | z /= zfactor 83 | ro /= rofactor 84 | dictresult = {'tension': tension, 'rho': ro, 'z': z, 85 | 'GPT': np.hstack([0, dom, 0])} 86 | out = TensionResult(dictresult) 87 | return out 88 | 89 | return tension 90 | 91 | 92 | def sgt_spot(rho1, rho2, Tsat, Psat, model, n=50, full_output=False): 93 | """ 94 | SGT spot for mixtures (rho1, rho2, T, P) -> interfacial tension 95 | 96 | Parameters 97 | ---------- 98 | rho1 : float 99 | phase 1 density vector 100 | rho2 : float 101 | phase 2 density vector 102 | Tsat : float 103 | saturation temperature 104 | Psat : float 105 | saturation pressure 106 | model : object 107 | created with an EoS 108 | n : int, optional 109 | number points to solve density profiles 110 | full_output : bool, optional 111 | wheter to outputs all calculation info 112 | 113 | Returns 114 | ------- 115 | ten : float 116 | interfacial tension between the phases 117 | """ 118 | 119 | # adimensionalizar variables 120 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 121 | Pad = Psat*Pfactor 122 | ro1a = rho1*rofactor 123 | ro2a = rho2*rofactor 124 | 125 | cij = model.ci(Tsat) 126 | cij /= cij[0, 0] 127 | 128 | mu0 = model.muad(ro1a, Tsat) 129 | mu02 = model.muad(ro2a, Tsat) 130 | if not np.allclose(mu0, mu02): 131 | raise Exception('Not equilibria compositions, mu1 != mu2') 132 | 133 | roots, weights = lobatto(n) 134 | s = 0 135 | try: 136 | ros = (ro1a + ro2a)/2 137 | ros = root(fobj_saddle, ros, args=(mu0, Tsat, model), method='lm') 138 | if ros.success: 139 | ros = ros.x 140 | # segment1 141 | # Linear profile 142 | pend = (ros - ro1a) 143 | b = ro1a 144 | ro1 = (np.outer(roots, pend) + b).T 145 | 146 | ro_s1 = (ros[s]-ro1a[s])*roots + ro1a[s] # integration nodes 147 | wreal1 = np.abs(weights*(ros[s]-ro1a[s])) # integration weights 148 | 149 | dro1 = np.gradient(ro1, ro_s1, edge_order=2, axis=1) 150 | 151 | # segment2 152 | # Linear profile 153 | pend = (ro2a - ros) 154 | b = ros 155 | ro2 = (np.outer(roots, pend) + b).T 156 | 157 | ro_s2 = (ro2a[s]-ro1a[s])*roots + ro1a[s] # integration nodes 158 | wreal2 = np.abs(weights*(ro2a[s]-ro1a[s])) # integration weights 159 | 160 | dro2 = np.gradient(ro2, ro_s2, edge_order=2, axis=1) 161 | 162 | dom1 = np.zeros(n) 163 | dom2 = np.zeros(n) 164 | for i in range(n): 165 | dom1[i] = model.dOm(ro1[:, i], Tsat, mu0, Pad) 166 | dom2[i] = model.dOm(ro2[:, i], Tsat, mu0, Pad) 167 | dom1[0] = 0. 168 | dom2[-1] = 0. 169 | 170 | suma1 = cmix_cy(dro1, cij) 171 | suma2 = cmix_cy(dro2, cij) 172 | 173 | integral1 = np.nan_to_num(np.sqrt(2*dom1*suma1)) 174 | integral2 = np.nan_to_num(np.sqrt(2*dom2*suma2)) 175 | tension = np.dot(integral1, wreal1) 176 | tension += np.dot(integral2, wreal2) 177 | tension *= tenfactor 178 | out = tension 179 | if full_output: 180 | with np.errstate(divide='ignore'): 181 | intz1 = np.sqrt(suma1/(2*dom1)) 182 | intz2 = np.sqrt(suma2/(2*dom2)) 183 | intz1[np.isinf(intz1)] = 0 184 | intz2[np.isinf(intz2)] = 0 185 | z1 = np.cumsum(intz1*wreal1) 186 | z2 = z1[-1] + np.cumsum(intz2*wreal2) 187 | z = np.hstack([z1, z2]) 188 | ro = np.hstack([ro1, ro2]) 189 | z /= zfactor 190 | ro /= rofactor 191 | dictresult = {'tension': tension, 'rho': ro, 'z': z, 192 | 'GPT': np.hstack([dom1, dom2])} 193 | out = TensionResult(dictresult) 194 | else: 195 | out = sgt_linear(ro1, ro2, Tsat, Psat, model, n, full_output) 196 | except: 197 | out = sgt_linear(ro1, ro2, Tsat, Psat, model, n, full_output) 198 | 199 | return out 200 | -------------------------------------------------------------------------------- /phasepy/sgt/path_hk.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import fsolve 4 | from scipy.integrate import cumulative_trapezoid 5 | from .cijmix_cy import cmix_cy 6 | from .tensionresult import TensionResult 7 | 8 | 9 | def fobj_beta0(dro, ro1, dh2, s, temp_aux, mu0, ci, sqrtci, model): 10 | ro = ro1 + dro 11 | dmu = model.muad_aux(ro, temp_aux) - mu0 12 | 13 | f1 = sqrtci[s]*dmu 14 | f2 = sqrtci*dmu[s] 15 | obj = f1 - f2 16 | obj[s] = dh2 - ci.dot(dro**2) 17 | return obj 18 | 19 | 20 | def ten_beta0_hk(rho1, rho2, Tsat, Psat, model, n=1000, full_output=False): 21 | 22 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 23 | Pad = Psat*Pfactor 24 | ro1a = rho1*rofactor 25 | ro2a = rho2*rofactor 26 | 27 | nc = model.nc 28 | 29 | temp_aux = model.temperature_aux(Tsat) 30 | 31 | mu0 = model.muad_aux(ro1a, temp_aux) 32 | mu02 = model.muad_aux(ro2a, temp_aux) 33 | if not np.allclose(mu0, mu02): 34 | raise Exception('Not equilibria compositions, mu1 != mu2') 35 | 36 | cij = model.ci(Tsat) 37 | c1 = cij[0, 0] 38 | cij /= c1 39 | ci = np.diag(cij) 40 | sqrtci = np.sqrt(ci) 41 | 42 | Nsteps = n 43 | deltaro = ro2a-ro1a 44 | Hl0 = ci.dot(deltaro) 45 | dH = Hl0/(Nsteps + 1) 46 | dH2 = dH**2 47 | 48 | dro = [np.zeros(nc)] 49 | ro = [ro1a] 50 | 51 | i = 1 52 | dr0 = deltaro*dH2 53 | dro0 = fsolve(fobj_beta0, dr0, args=(ro[i-1], dH2, 0, temp_aux, mu0, ci, 54 | sqrtci, model)) 55 | ro0 = np.add(ro[i-1], dro0) 56 | ro.append(ro0) 57 | dro.append(dro0) 58 | end = False 59 | 60 | while not end and i < 2*Nsteps: 61 | i += 1 62 | dro0 = fsolve(fobj_beta0, dro[i-1], args=(ro[i-1], dH2, 0, 63 | temp_aux, mu0, ci, sqrtci, model)) 64 | ro0 = ro[i-1] + dro0 65 | ro.append(ro0) 66 | dro.append(dro0) 67 | end = np.allclose(ro0, ro2a, rtol=1e-2) 68 | 69 | ro.append(ro2a) 70 | dro.append(ro2a-ro[-2]) 71 | dro2 = np.asarray(dro).T 72 | 73 | # Path Error 74 | Hl = np.sqrt(ci.dot(dro2**2)).sum() 75 | dH = Hl/(Nsteps + 1) 76 | dH2 = dH**2 77 | error = np.abs(Hl - Hl0) 78 | it = 0 79 | 80 | while error > 1e-3 and it < 5: 81 | it += 1 82 | Hl0 = Hl 83 | dro = [np.zeros(nc)] 84 | ro = [ro1a] 85 | i = 1 86 | dr0 = deltaro*dH2 87 | dro0 = fsolve(fobj_beta0, dr0, args=(ro[i-1], dH2, 0, temp_aux, 88 | mu0, ci, sqrtci, model)) 89 | ro0 = np.add(ro[i-1], dro0) 90 | ro.append(ro0) 91 | dro.append(dro0) 92 | end = np.allclose(ro[i], ro2a, rtol=1e-2) 93 | 94 | while i < Nsteps: 95 | i += 1 96 | dro0 = fsolve(fobj_beta0, dro[i-1], args=(ro[i-1], dH2, 0, 97 | temp_aux, mu0, ci, sqrtci, model)) 98 | ro0 = np.add(ro[i-1], dro0) 99 | ro.append(ro0) 100 | dro.append(dro0) 101 | 102 | ro.append(ro2a) 103 | dro.append(ro2a-ro[-2]) 104 | dro2 = np.asarray(dro).T 105 | 106 | Hl = np.sqrt(ci.dot(dro2**2)).sum() 107 | dH = Hl/(Nsteps + 1) 108 | dH2 = dH**2 109 | error = np.abs(Hl - Hl0) 110 | 111 | ro2 = np.asarray(ro).T 112 | Hi = dH * np.arange(0, Nsteps + 2) 113 | drodh = np.gradient(ro2, Hi, edge_order=2, axis=1) 114 | 115 | suma = cmix_cy(drodh, cij) 116 | dom = np.zeros(Nsteps + 2) 117 | for k in range(1, Nsteps + 1): 118 | dom[k] = model.dOm_aux(ro2[:, k], temp_aux, mu0, Pad) 119 | 120 | # Tension computation 121 | integral = np.nan_to_num(np.sqrt(2*dom*suma)) 122 | ten = np.abs(np.trapz(integral, Hi)) 123 | ten *= tenfactor 124 | 125 | if full_output: 126 | # Z profile 127 | with np.errstate(divide='ignore'): 128 | intz = (np.sqrt(suma/(2*dom))) 129 | intz[np.isinf(intz)] = 0 130 | z = np.abs(cumulative_trapezoid(intz, Hi, initial=0)) 131 | z /= zfactor 132 | ro2 /= rofactor 133 | dictresult = {'tension': ten, 'rho': ro2, 'z': z, 134 | 'GPT': dom, 'path': Hi} 135 | out = TensionResult(dictresult) 136 | return out 137 | return ten 138 | -------------------------------------------------------------------------------- /phasepy/sgt/path_sk.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import root 4 | from scipy.integrate import cumulative_trapezoid 5 | from .cijmix_cy import cmix_cy 6 | from .tensionresult import TensionResult 7 | 8 | 9 | def fobj_sk(inc, spath, temp_aux, mu0, ci, sqrtci, model): 10 | ro = np.abs(inc[:-1]) 11 | alpha = inc[-1] 12 | mu = model.muad_aux(ro, temp_aux) 13 | obj = np.zeros_like(inc) 14 | obj[:-1] = mu - (mu0 + alpha*sqrtci) 15 | obj[-1] = spath - sqrtci.dot(ro) 16 | return obj 17 | 18 | 19 | def ten_beta0_sk(rho1, rho2, Tsat, Psat, model, n=200, full_output=False, 20 | alpha0=None): 21 | 22 | nc = model.nc 23 | 24 | # Dimensionless variables 25 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 26 | Pad = Psat*Pfactor 27 | ro1a = rho1*rofactor 28 | ro2a = rho2*rofactor 29 | 30 | cij = model.ci(Tsat) 31 | cij /= cij[0, 0] 32 | ci = np.diag(cij) 33 | sqrtci = np.sqrt(ci) 34 | 35 | temp_aux = model.temperature_aux(Tsat) 36 | 37 | mu0 = model.muad_aux(ro1a, temp_aux) 38 | mu02 = model.muad_aux(ro2a, temp_aux) 39 | if not np.allclose(mu0, mu02): 40 | raise Exception('Not equilibria compositions, mu1 != mu2') 41 | 42 | # Path function 43 | s0 = sqrtci.dot(ro1a) 44 | s1 = sqrtci.dot(ro2a) 45 | spath = np.linspace(s0, s1, n) 46 | ro = np.zeros([nc, n]) 47 | alphas = np.zeros(n) 48 | ro[:, 0] = ro1a 49 | ro[:, -1] = ro2a 50 | 51 | deltaro = ro2a - ro1a 52 | i = 1 53 | r0 = ro1a + deltaro * (spath[i]-s0) / n 54 | if alpha0 is None: 55 | alpha0 = np.mean((model.muad_aux(r0, temp_aux) - mu0)/sqrtci) 56 | r0 = np.hstack([r0, alpha0]) 57 | ro0 = root(fobj_sk, r0, args=(spath[i], temp_aux, mu0, ci, sqrtci, model), 58 | method='lm') 59 | ro0 = ro0.x 60 | ro[:, i] = np.abs(ro0[:-1]) 61 | 62 | for i in range(1, n): 63 | sol = root(fobj_sk, ro0, args=(spath[i], temp_aux, mu0, ci, 64 | sqrtci, model), method='lm') 65 | ro0 = sol.x 66 | alphas[i] = ro0[-1] 67 | ro[:, i] = ro0[:-1] 68 | 69 | # Derivatives respect to path function 70 | drods = np.gradient(ro, spath, edge_order=2, axis=1) 71 | 72 | suma = cmix_cy(drods, cij) 73 | dom = np.zeros(n) 74 | for k in range(1, n - 1): 75 | dom[k] = model.dOm_aux(ro[:, k], temp_aux, mu0, Pad) 76 | 77 | integral = np.nan_to_num(np.sqrt(2*dom*suma)) 78 | tension = np.abs(np.trapz(integral, spath)) 79 | tension *= tenfactor 80 | 81 | if full_output: 82 | # Zprofile 83 | with np.errstate(divide='ignore'): 84 | intz = (np.sqrt(suma/(2*dom))) 85 | intz[np.isinf(intz)] = 0 86 | z = np.abs(cumulative_trapezoid(intz, spath, initial=0)) 87 | z /= zfactor 88 | ro /= rofactor 89 | dictresult = {'tension': tension, 'rho': ro, 'z': z, 90 | 'GPT': dom, 'path': spath, 'alphas': alphas} 91 | out = TensionResult(dictresult) 92 | return out 93 | 94 | return tension 95 | -------------------------------------------------------------------------------- /phasepy/sgt/reference_component.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from scipy.optimize import fsolve 4 | from ..math import lobatto, colocA 5 | from .cijmix_cy import cmix_cy 6 | from .tensionresult import TensionResult 7 | 8 | 9 | def fobj_beta0(ro, ro_s, s, temp_aux, mu0, sqrtci, model): 10 | nc = model.nc 11 | ro = np.insert(ro, s, ro_s) 12 | dmu = model.muad_aux(ro, temp_aux) - mu0 13 | 14 | f1 = sqrtci[s]*dmu 15 | f2 = sqrtci*dmu[s] 16 | 17 | return (f1-f2)[np.arange(nc) != s] 18 | 19 | 20 | def ten_beta0_reference(rho1, rho2, Tsat, Psat, model, s=0, 21 | n=100, full_output=False): 22 | 23 | nc = model.nc 24 | 25 | # Dimensionless profile 26 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 27 | Pad = Psat*Pfactor 28 | ro1a = rho1*rofactor 29 | ro2a = rho2*rofactor 30 | 31 | cij = model.ci(Tsat) 32 | cij /= cij[0, 0] 33 | ci = np.diag(cij) 34 | sqrtci = np.sqrt(ci) 35 | 36 | temp_aux = model.temperature_aux(Tsat) 37 | 38 | mu0 = model.muad_aux(ro1a, temp_aux) 39 | mu02 = model.muad_aux(ro2a, temp_aux) 40 | if not np.allclose(mu0, mu02): 41 | raise Exception('Not equilibria compositions, mu1 != mu2') 42 | # roots and weights for Lobatto quadrature 43 | roots, weights = lobatto(n) 44 | 45 | ro_s = (ro2a[s]-ro1a[s])*roots+ro1a[s] # Integration nodes 46 | wreal = np.abs(weights*(ro2a[s]-ro1a[s])) # Integration weights 47 | 48 | # A matrix for derivatives with orthogonal collocation 49 | A = colocA(roots) / (ro2a[s]-ro1a[s]) 50 | 51 | rodep = np.zeros([nc-1, n]) 52 | rodep[:, 0] = ro1a[np.arange(nc) != s] 53 | for i in range(1, n): 54 | rodep[:, i] = fsolve(fobj_beta0, rodep[:, i-1], 55 | args=(ro_s[i], s, temp_aux, mu0, sqrtci, model)) 56 | ro = np.insert(rodep, s, ro_s, axis=0) 57 | dro = rodep@A.T 58 | dro = np.insert(dro, s, np.ones(n), axis=0) 59 | 60 | suma = cmix_cy(dro, cij) 61 | 62 | dom = np.zeros(n) 63 | for k in range(1, n-1): 64 | dom[k] = model.dOm_aux(ro[:, k], temp_aux, mu0, Pad) 65 | 66 | intten = np.nan_to_num(np.sqrt(suma*(2*dom))) 67 | ten = np.dot(intten, wreal) 68 | ten *= tenfactor 69 | 70 | if full_output: 71 | # Z profile 72 | with np.errstate(divide='ignore'): 73 | intz = (np.sqrt(suma/(2*dom))) 74 | intz[np.isinf(intz)] = 0 75 | z = np.cumsum(intz*wreal) 76 | z /= zfactor 77 | ro /= rofactor 78 | dictresult = {'tension': ten, 'rho': ro, 'z': z, 79 | 'GPT': dom} 80 | out = TensionResult(dictresult) 81 | return out 82 | 83 | return ten 84 | -------------------------------------------------------------------------------- /phasepy/sgt/sgt_beta0.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from .path_sk import ten_beta0_sk 4 | from .path_hk import ten_beta0_hk 5 | from .reference_component import ten_beta0_reference 6 | 7 | 8 | def sgt_mix_beta0(rho1, rho2, Tsat, Psat, model, n=100, method='reference', 9 | full_output=False, s=0, alpha0=None): 10 | """ 11 | SGT for mixtures and beta = 0 (rho1, rho2, T, P) -> interfacial tension 12 | 13 | Parameters 14 | ---------- 15 | rho1 : float 16 | phase 1 density vector 17 | rho2 : float 18 | phase 2 density vector 19 | Tsat : float 20 | saturation temperature 21 | Psat : float 22 | saturation pressure 23 | model : object 24 | created with an EoS 25 | n : int, optional 26 | number points to solve density profiles 27 | method : string 28 | method used to solve density profiles, available options are 29 | 'reference' for using reference component method, 'cornelisse' for 30 | Cornelisse path function and 'liang' for Liang path function 31 | full_output : bool, optional 32 | wheter to outputs all calculation info 33 | s : int 34 | index of reference component used in refernce component method 35 | alpha0 : float 36 | initial guess for solving Liang path function 37 | 38 | 39 | Returns 40 | ------- 41 | ten : float 42 | interfacial tension between the phases 43 | """ 44 | 45 | cij = model.ci(Tsat) 46 | cij /= cij[0, 0] 47 | dcij = np.linalg.det(cij) 48 | if not np.isclose(dcij, 0): 49 | warning = 'Influece parameter matrix is not singular probably a' 50 | warning += ' beta has been set up.' 51 | raise Exception(warning) 52 | 53 | if method == 'reference': 54 | sol = ten_beta0_reference(rho1, rho2, Tsat, Psat, model, s, n, 55 | full_output) 56 | elif method == 'cornelisse': 57 | sol = ten_beta0_hk(rho1, rho2, Tsat, Psat, model, n, full_output) 58 | elif method == 'liang': 59 | sol = ten_beta0_sk(rho1, rho2, Tsat, Psat, model, n, full_output, 60 | alpha0) 61 | else: 62 | raise Exception('Method not implemented') 63 | 64 | return sol 65 | -------------------------------------------------------------------------------- /phasepy/sgt/sgtpure.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | import numpy as np 3 | from ..math import gauss 4 | from .tensionresult import TensionResult 5 | 6 | 7 | def ten_fit(T, model, roots, weigths, P0=None): 8 | 9 | # LV Equilibrium 10 | Psat, vl, vv = model.psat(T, P0) 11 | rol = 1./vl 12 | rov = 1./vv 13 | 14 | # Dimensionless variables 15 | Tfactor, Pfactor, rofactor, tenfactor = model.sgt_adim_fit(T) 16 | 17 | rola = rol * rofactor 18 | rova = rov * rofactor 19 | Tad = T * Tfactor 20 | Pad = Psat * Pfactor 21 | 22 | # Chemical potentials 23 | mu0 = model.muad(rova, Tad) 24 | 25 | roi = (rola-rova)*roots+rova 26 | wreal = (rola-rova)*weigths 27 | 28 | # dOm = model.dOm(roi, Tad, mu0, Pad) 29 | n = len(weigths) 30 | dOm = np.zeros(n) 31 | for i in range(n): 32 | dOm[i] = model.dOm(roi[i], Tad, mu0, Pad) 33 | 34 | tenint = np.nan_to_num(np.sqrt(2*dOm)) 35 | ten = np.dot(wreal, tenint) 36 | ten *= tenfactor 37 | 38 | return ten 39 | 40 | 41 | def sgt_pure(rhov, rhol, Tsat, Psat, model, n=100, full_output=False): 42 | """ 43 | SGT for pure component (rhol, rhov, T, P) -> interfacial tension 44 | 45 | Parameters 46 | ---------- 47 | rhov : float 48 | vapor phase density 49 | rhol : float 50 | liquid phase density 51 | Tsat : float 52 | saturation temperature 53 | Psat : float 54 | saturation pressure 55 | model : object 56 | created with an EoS 57 | n : int, optional 58 | number of collocation points for IFT integration 59 | full_output : bool, optional 60 | wheter to outputs all calculation info 61 | 62 | Returns 63 | ------- 64 | ten : float 65 | interfacial tension between the phases 66 | """ 67 | 68 | # roots and weights of Gauss quadrature 69 | roots, w = gauss(n) 70 | 71 | Tfactor, Pfactor, rofactor, tenfactor, zfactor = model.sgt_adim(Tsat) 72 | 73 | rola = rhol * rofactor 74 | rova = rhov * rofactor 75 | Tad = Tsat * Tfactor 76 | Pad = Psat * Pfactor 77 | 78 | # Equilibrium chemical potential 79 | mu0 = model.muad(rova, Tad) 80 | mu02 = model.muad(rola, Tad) 81 | if not np.allclose(mu0, mu02): 82 | raise Exception('Not equilibria compositions, mul != muv') 83 | 84 | roi = (rola-rova)*roots+rova 85 | wreal = np.abs((rola-rova)*w) 86 | 87 | dOm = np.zeros(n) 88 | for i in range(n): 89 | dOm[i] = model.dOm(roi[i], Tad, mu0, Pad) 90 | 91 | tenint = np.nan_to_num(np.sqrt(2*dOm)) 92 | ten = np.dot(wreal, tenint) 93 | ten *= tenfactor 94 | 95 | if full_output: 96 | zint = np.sqrt(1/(2*dOm)) 97 | z = np.cumsum(wreal*zint) 98 | z /= zfactor 99 | roi /= rofactor 100 | dictresult = {'tension': ten, 'rho': roi, 'z': z, 101 | 'GPT': dOm} 102 | out = TensionResult(dictresult) 103 | return out 104 | 105 | return ten 106 | -------------------------------------------------------------------------------- /phasepy/sgt/tensionresult.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function, absolute_import 2 | 3 | # obtained from scipy optimize result 4 | 5 | class TensionResult(dict): 6 | 7 | def __getattr__(self, name): 8 | try: 9 | return self[name] 10 | except KeyError: 11 | raise AttributeError(name) 12 | 13 | __setattr__ = dict.__setitem__ 14 | __delattr__ = dict.__delitem__ 15 | 16 | def __repr__(self): 17 | 18 | if self.keys(): 19 | m = max(map(len, list(self.keys()))) + 1 20 | return '\n'.join([k.rjust(m) + ': ' + repr(v) 21 | for k, v in self.items()]) 22 | else: 23 | return self.__class__.__name__ + "()" 24 | -------------------------------------------------------------------------------- /phasepy/src/cijmix_cy.pyx: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | cimport cython 3 | 4 | @cython.boundscheck(False) 5 | @cython.wraparound(False) 6 | def cmix_cy(double [:,:] drodz, double [:, :] cij): 7 | 8 | cdef int n, nc, i, j, k 9 | nc = drodz.shape[0] 10 | n = drodz.shape[1] 11 | cdef double[:] suma = np.zeros(n) 12 | 13 | for k in range(n): 14 | for i in range(nc): 15 | for j in range(nc): 16 | suma[k] += cij[i,j]*drodz[i,k]*drodz[j,k] 17 | return suma.base -------------------------------------------------------------------------------- /phasepy/src/coloc_cy.pyx: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | cimport cython 4 | 5 | 6 | @cython.boundscheck(False) 7 | @cython.wraparound(False) 8 | def jcobi_roots(int n, int N0, int N1, int Al, int Be): 9 | 10 | cdef int i, j, k, Ab, Ad, Ap, nt 11 | cdef double z, zc, z1, y, x, xn, xn1, xd, xd1, xp, xp1 12 | cdef bint success 13 | #vectores derivadas y raices 14 | cdef double[:] dif1 = np.zeros(n) 15 | cdef double[:] dif2 = np.zeros(n) 16 | cdef double[:] root = np.zeros(n) 17 | 18 | #coeficientes polinomio Jacobi 19 | Ab = Al + Be 20 | Ad = Be - Al 21 | Ap = Be * Al 22 | 23 | dif1[0] = (Ad/(Ab+2)+1)/2 24 | dif2[0] = 0. 25 | 26 | if not (n < 2): 27 | for i in range(1,n): 28 | z1 = i 29 | z = Ab + 2*z1 30 | dif1[i] = (Ab*Ad/z/(z+2)+1)/2 31 | if i == 1: 32 | dif2[i] = (Ab + Ap + z1)/z/z/(z+1) 33 | else: 34 | z **= 2 35 | y = z1 * (Ab + z1) 36 | y *= (Ap + y) 37 | dif2[i] = y / z / (z-1) 38 | 39 | #determinacion de las raices del polinomio de Jacobi 40 | 41 | x = 0.0 42 | for i in range(n): 43 | success = False 44 | while not success: 45 | xd = 0.0 46 | xn = 1.0 47 | xd1 = 0.0 48 | xn1 = 0.0 49 | for j in range(n): 50 | xp = (dif1[j]-x)*xn - dif2[j]*xd 51 | xp1 = (dif1[j]-x)*xn1 - dif2[j]*xd1-xn 52 | xd = xn 53 | xd1 = xn1 54 | xn = xp 55 | xn1 = xp1 56 | zc = 1.0 57 | z = xn/xn1 58 | if not (i == 0): 59 | for j in range(1,i+1): 60 | zc -= z / (x - root[j-1]) 61 | z /= zc 62 | x -= z 63 | success = (abs(z) < 1e-9) 64 | root[i] = x 65 | x += 0.0001 66 | 67 | if N0 == 1 and N1 == 1: 68 | root = np.hstack([0,root,1]) 69 | elif N0 == 1: 70 | root = np.hstack([0,root]) 71 | elif N1 == 1: 72 | root = np.hstack([root,1]) 73 | 74 | nt = n + N0 + N1 75 | dif1 = np.ones(nt) 76 | 77 | for k in range(nt): 78 | x=root[k] 79 | for j in range(nt): 80 | if j!=k: 81 | y = x-root[j] 82 | dif1[k] = y*dif1[k] 83 | 84 | return root.base, dif1.base 85 | 86 | 87 | @cython.boundscheck(False) 88 | @cython.wraparound(False) 89 | def colocAB(double [:] roots): 90 | cdef int n, k, j, i 91 | cdef double x, y, gen 92 | n = roots.shape[0] 93 | cdef double [:] dif1 = np.ones(n) 94 | cdef double [:] dif2 = np.zeros(n) 95 | cdef double [:] dif3 = np.zeros(n) 96 | cdef double [:,:] B = np.zeros([n,n]) 97 | cdef double [:,:] A = np.zeros([n,n]) 98 | for k in range(n): 99 | x=roots[k] 100 | for j in range(n): 101 | if j!=k: 102 | y=x-roots[j] 103 | dif3[k]=y*dif3[k]+3.*dif2[k] 104 | dif2[k]=y*dif2[k]+2.*dif1[k] 105 | dif1[k]=y*dif1[k] 106 | for i in range(n): 107 | for j in range(n): 108 | if j!=i: 109 | y=roots[i]-roots[j] 110 | gen=dif1[i]/dif1[j]/y 111 | A[i,j]=gen 112 | B[i,j]=gen*(dif2[i]/dif1[i]-2/y) 113 | else: 114 | A[i,j]=dif2[i]/dif1[i]/2 115 | B[i,j]=dif3[i]/dif1[i]/3 116 | return A.base, B.base 117 | 118 | @cython.boundscheck(False) 119 | @cython.wraparound(False) 120 | @cython.cdivision(True) 121 | def colocA(double [:] roots): 122 | cdef int n, k, j, i 123 | cdef double x, y, gen 124 | n = roots.shape[0] 125 | cdef double [:] dif1 = np.ones(n) 126 | cdef double [:] dif2 = np.zeros(n) 127 | cdef double [:,:] A = np.zeros([n,n]) 128 | for k in range(n): 129 | x=roots[k] 130 | for j in range(n): 131 | if j!=k: 132 | y=x-roots[j] 133 | dif2[k]=y*dif2[k]+2.*dif1[k] 134 | dif1[k]=y*dif1[k] 135 | for i in range(n): 136 | for j in range(n): 137 | if j!=i: 138 | y=roots[i]-roots[j] 139 | gen=dif1[i]/dif1[j]/y 140 | A[i,j]=gen 141 | else: 142 | A[i,j]=dif2[i]/dif1[i]/2 143 | return A.base 144 | 145 | @cython.boundscheck(False) 146 | @cython.wraparound(False) 147 | def colocB(double [:] roots): 148 | cdef int n, k, j, i 149 | cdef double x, y, gen 150 | n = roots.shape[0] 151 | cdef double [:] dif1 = np.ones(n) 152 | cdef double [:] dif2 = np.zeros(n) 153 | cdef double [:] dif3 = np.zeros(n) 154 | cdef double [:,:] B = np.zeros([n,n]) 155 | for k in range(n): 156 | x=roots[k] 157 | for j in range(n): 158 | if j!=k: 159 | y=x-roots[j] 160 | dif3[k]=y*dif3[k]+3.*dif2[k] 161 | dif2[k]=y*dif2[k]+2.*dif1[k] 162 | dif1[k]=y*dif1[k] 163 | for i in range(n): 164 | for j in range(n): 165 | if j!=i: 166 | y=roots[i]-roots[j] 167 | gen=dif1[i]/dif1[j]/y 168 | B[i,j]=gen*(dif2[i]/dif1[i]-2/y) 169 | else: 170 | B[i,j]=dif3[i]/dif1[i]/3 171 | return B.base 172 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | openpyxl 5 | pandas 6 | cython 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | 3 | try: 4 | from Cython.Distutils import build_ext 5 | except ImportError: 6 | use_cython = False 7 | else: 8 | use_cython = True 9 | 10 | """ 11 | if use_cython: 12 | ext_modules += [Extension('phasepy.coloc_cy', 13 | ['phasepy/src/coloc_cy.pyx']), 14 | Extension('phasepy.actmodels.actmodels_cy', 15 | ['phasepy/src/actmodels_cy.pyx']), 16 | Extension('phasepy.sgt.cijmix_cy', 17 | ['phasepy/src/cijmix_cy.pyx'])] 18 | cmdclass.update({'build_ext': build_ext}) 19 | else: 20 | ext_modules += [Extension('phasepy.coloc_cy', ['phasepy/src/coloc_cy.c']), 21 | Extension('phasepy.actmodels.actmodels_cy', 22 | ['phasepy/src/actmodels_cy.c']), 23 | Extension('phasepy.sgt.cijmix_cy', 24 | ['phasepy/src/cijmix_cy.c'])] 25 | """ 26 | cmdclass = {} 27 | ext_modules = [] 28 | 29 | ext_modules += [Extension('phasepy.coloc_cy', 30 | ['phasepy/src/coloc_cy.pyx']), 31 | Extension('phasepy.actmodels.actmodels_cy', 32 | ['phasepy/src/actmodels_cy.pyx']), 33 | Extension('phasepy.sgt.cijmix_cy', 34 | ['phasepy/src/cijmix_cy.pyx'])] 35 | cmdclass.update({'build_ext': build_ext}) 36 | 37 | setup( 38 | name='phasepy', 39 | license='MIT', 40 | version='0.0.55', 41 | description='Multiphase multicomponent Equilibria', 42 | author='Gustavo Chaparro Maldonado, Andres Mejia Matallana', 43 | author_email='gustavochaparro@udec.cl', 44 | url='https://github.com/gustavochm/phasepy', 45 | download_url='https://github.com/gustavochm/phasepy.git', 46 | long_description=open('long_description.rst').read(), 47 | packages=['phasepy', 'phasepy.cubic', 'phasepy.equilibrium', 'phasepy.fit', 48 | 'phasepy.sgt', 'phasepy.actmodels'], 49 | cmdclass=cmdclass, 50 | ext_modules=ext_modules, 51 | install_requires=['numpy', 'scipy', 'pandas', 'openpyxl'], 52 | platforms=["Windows", "Linux", "Mac OS", "Unix"], 53 | keywords=['Phase Equilibrium', 'Cubic EOS', 'QMR', 'MHV', 'WS', 'NRTL', 54 | 'Wilson', 'UNIFAC', 'UNIQUAC', 'Flash', 'VLE', 'LLE', 'VLLE', 55 | 'SGT'], 56 | package_data={'phasepy': ['database/*']}, 57 | zip_safe=False 58 | ) 59 | --------------------------------------------------------------------------------