├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── install.rst ├── license.rst ├── mainclasses.rst ├── make.bat ├── pyequion_logo.png ├── reference.rst └── tutorials.rst ├── pyequion2 ├── __init__.py ├── activity │ ├── __init__.py │ ├── debye.py │ ├── extended_debye.py │ ├── ideal.py │ ├── pitzer.py │ ├── pitzer_sanity_assertions.py │ └── py_coo_tensor_ops.py ├── builder.py ├── constants.py ├── converters.py ├── datamods │ ├── README │ ├── __init__.py │ ├── chemical_potentials.py │ ├── density_solids.py │ ├── phreeqc.dat │ ├── pitzer.txt │ ├── pitzer_data.py │ ├── reactions_gases.py │ ├── reactions_irreversible.py │ ├── reactions_solids.py │ ├── reactions_solutions.py │ ├── species.py │ └── temp.py ├── eqsolver │ ├── __init__.py │ ├── eqsolver.py │ ├── residual_functions.py │ └── solvers.py ├── equilibrium_backend.py ├── equilibrium_system.py ├── fugacity │ ├── __init__.py │ ├── peng_robinson.py │ └── solve_cubic.py ├── gaseous_system.py ├── gui │ ├── __init__.py │ ├── images │ │ └── pyequion_logo.png │ ├── initializer.py │ ├── logmaker.py │ ├── main.py │ ├── run.py │ ├── seqsolution.py │ ├── solution.py │ └── solver.py ├── interface │ ├── __init__.py │ ├── diffusion_coefficients.py │ ├── interface_functions.py │ ├── interface_solution.py │ └── interface_system.py ├── logmaker.py ├── sequencer.py ├── solution.py ├── utils.py └── water_properties.py ├── pyinstaller ├── FLAGS.txt ├── hook │ └── hook-lark.py └── pyequiongui.py ├── pyproject_.toml ├── requirements.txt ├── setup.py └── test ├── examples ├── example0.py ├── example1.py ├── example2.py ├── example3.py ├── example4.py ├── example5.py ├── example6.py ├── example7.py └── example8.py ├── suite ├── converters.py ├── eqsys.py ├── gaseous_system.py ├── intsys.py └── pitzer.py └── test_suite_1.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | 141 | #VSCode 142 | .vscode/ 143 | 144 | # others 145 | .old/ 146 | 147 | toybox/ 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, PyEquIon. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyEquion2 2 | 3 | A pure python implementation for electrolytes chemical equilibrium. 4 | 5 | Repository at https://github.com/pyequion/pyequion2 6 | 7 | A heavily update version of the previous package found in https://github.com/caiofcm/pyequion 8 | 9 | Tests of the accuracy of its parameters and equations are underway, so results may change. Its API may change, and modules and functions may be added, removed and renamed. Use at your own peril! 10 | 11 | ## Features 12 | 13 | - Pure python package: hence it is easy to install in any platform 14 | - Calculation of equilibrium of inorganic salts in water solution with precipitation and degassing 15 | - Automatic determination of reactions 16 | - Provides information as: Ionic Strength, pH, Electric Conductivity and the concentrations of each species as well their activity coefficient 17 | - A modular approach for the activity coefficient calculation allows the usage of new thermodynamic models 18 | - Calculation of equilibrium and reaction fluxes at reacting solid interfaces 19 | - An incorporated GUI for ease-of-use 20 | 21 | ## Installation 22 | 23 | The package can be installed either running downloading the 24 | source code and `pip install .` on this folder, or directly by 25 | 26 | ``` 27 | pip install git+https://github.com/pyequion/pyequion2 28 | ``` 29 | 30 | There is an older PyPI version, but is not supported anymore 31 | 32 | 33 | By default, package installed without GUI to omit PyQT dependency. 34 | To install GUI, use commands with extra argument: 35 | ``` 36 | pip install .[gui] 37 | pip install pyequion2[gui] 38 | ``` 39 | 40 | ## GUI 41 | 42 | To run a GUI version of PyEquion2, just run 43 | 44 | ``` 45 | import pyequion2 46 | pyequion2.rungui() 47 | ``` 48 | 49 | 50 | ## Contributors 51 | 52 | - Caio Curitiba Marcellos 53 | - Danilo Naiff 54 | - Gerson Francisco da Silva Junior 55 | - Elvis do Amaral Soares 56 | - Fabio Ramos 57 | - Amaro G. Barreto Jr 58 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'PyEquion2' 21 | copyright = '2021, PyEquion' 22 | author = 'PyEquion' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.0.6.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 34 | 'sphinx.ext.napoleon', 35 | 'sphinx.ext.coverage'] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'alabaster' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PyEquion2's documentation! 2 | ==================================== 3 | 4 | .. automodule:: pyequion2 5 | :noindex: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | install.rst 12 | tutorials.rst 13 | mainclasses.rst 14 | reference.rst 15 | license.rst 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ================================ 3 | To install PyEquion using pip, you can 4 | 5 | ```sh 6 | pip install pyequion2 7 | ``` 8 | 9 | You can also install from the sourse itself, from 10 | 11 | https://github.com/pyequion/pyequion2. 12 | 13 | Requires: \ 14 | numpy \ 15 | auto_mix_prep \ 16 | cloudpickle \ 17 | commentjson \ 18 | Cython \ 19 | matplotlib \ 20 | ordered_set \ 21 | periodictable \ 22 | PyQt5 \ 23 | scipy \ -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ================================ 3 | 4 | BSD 3-Clause License 5 | 6 | Copyright (c) 2021, PyEquIon. 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | * Neither the name of the copyright holder nor the names of its 20 | contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /docs/mainclasses.rst: -------------------------------------------------------------------------------- 1 | Main classes references 2 | ================================ 3 | 4 | .. autoclass:: pyequion2.EquilibriumSystem 5 | :members: 6 | .. autoclass:: pyequion2.InterfaceSystem 7 | :members: -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pyequion_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyequion/pyequion2/9c176d5e2fc5d00617a08e148ae440cf6801c20f/docs/pyequion_logo.png -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Full reference 2 | ================================ 3 | -------------------------------------------------------------------------------- /pyequion2/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A pure python implementation for electrolytes chemical equilibrium. 4 | 5 | Repository can be found at 6 | https://github.com/pyequion/pyequion2 7 | 8 | Example 9 | ------- 10 | >>> import pyequion 11 | >>> #Define equilibrium calculation class 12 | >>> eqsys = pyequion.EquilibriumSystem(['HCO3-','Ca++','Na+','Cl-']) 13 | >>> #Define mass balances and system temperature 14 | >>> elements_balance = {'Ca':0.028, 'Cl':0.056, 'Na':0.075, 'C':0.065} 15 | >>> TK = 298.15 16 | >>> #Solve equilibrium 17 | >>> solution, solution_stats = eqsys.solve_equilibrium_elements_balance(TK, elements_balance, tol=1e-12) 18 | >>> #Show properties 19 | >>> solution.ph 20 | 7.5660993446870854 21 | 22 | GUI Example 23 | ----------- 24 | >>> import pyequion 25 | >>> pyequion.rungui() 26 | """ 27 | 28 | from .equilibrium_system import EquilibriumSystem 29 | from .equilibrium_backend import EquilibriumBackend 30 | from .interface import InterfaceSystem 31 | from .gaseous_system import InertGaseousSystem 32 | # from .gui import run as rungui 33 | from . import converters 34 | from . import water_properties 35 | 36 | #__all__ = ['EquilibriumSystem', 'InterfaceSystem', 'converters', 'water_properties'] -------------------------------------------------------------------------------- /pyequion2/activity/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .debye import setup_debye, setup_debye_limiting 4 | from .extended_debye import setup_extended_debye 5 | from .pitzer import setup_pitzer 6 | from .ideal import setup_ideal -------------------------------------------------------------------------------- /pyequion2/activity/debye.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | import functools 3 | import warnings 4 | 5 | import numpy as np 6 | try: 7 | import jax 8 | import jax.numpy as jnp 9 | except (ImportError, AssertionError): 10 | warnings.warn("JAX not installed, so can't be used as backend") 11 | try: 12 | import torch 13 | except (ImportError, AssertionError): 14 | warnings.warn("PyTorch not installed, so can't be used as backend") 15 | 16 | from .. import utils 17 | from .. import constants 18 | 19 | 20 | def setup_debye(solutes, calculate_osmotic_coefficient=False, backend='numpy'): 21 | assert backend in ['numpy', 'jax', "torch"] 22 | if backend == 'jax': 23 | zarray = jnp.array([utils.charge_number(specie) for specie in solutes], 24 | dtype=jnp.float32) 25 | elif backend == "numpy": 26 | zarray = np.array([utils.charge_number(specie) for specie in solutes], 27 | dtype=np.double) 28 | elif backend == "torch": 29 | zarray = torch.tensor([utils.charge_number(specie) for specie in solutes], 30 | dtype=torch.float) 31 | f = functools.partial(loggamma_and_osmotic, zarray=zarray, 32 | calculate_osmotic_coefficient=calculate_osmotic_coefficient, 33 | backend=backend) 34 | def g(xarray, TK): return constants.LOG10E * \ 35 | f(xarray, TK) # ln(gamma) to log10(gamma) 36 | if backend == 'jax': 37 | g = jax.jit(g) 38 | return g 39 | 40 | 41 | def setup_debye_limiting(solutes, calculate_osmotic_coefficient=False, backend='numpy'): 42 | assert backend in ['numpy', 'jax', "torch"] 43 | if backend == 'jax': 44 | zarray = jnp.array([utils.charge_number(specie) for specie in solutes], 45 | dtype=jnp.float32) 46 | elif backend == "numpy": 47 | zarray = np.array([utils.charge_number(specie) for specie in solutes], 48 | dtype=np.double) 49 | elif backend == "torch": 50 | zarray = torch.tensor([utils.charge_number(specie) for specie in solutes], 51 | dtype=torch.float) 52 | f = functools.partial(loggamma_and_osmotic_limiting, zarray=zarray, 53 | calculate_osmotic_coefficient=calculate_osmotic_coefficient, 54 | backend=backend) 55 | def g(xarray, TK): return constants.LOG10E * \ 56 | f(xarray, TK) # ln(gamma) to log10(gamma) 57 | if backend == 'jax': 58 | g = jax.jit(g) 59 | return g 60 | 61 | 62 | def loggamma_and_osmotic(carray, T, zarray, calculate_osmotic_coefficient, backend="numpy"): 63 | if backend in ["numpy", "torch"]: 64 | np_ = np 65 | else: 66 | np_ = jnp 67 | if backend == "torch": 68 | I = 0.5*torch.sum(carray*zarray**2, dim=-1, keepdim=True) 69 | sqrtI = torch.sqrt(I) 70 | else: 71 | I = 0.5*np_.sum(carray*zarray**2, axis=-1, keepdims=True) 72 | sqrtI = np_.sqrt(I) 73 | A = A_debye(T, backend=backend) 74 | F = A*f_debye(sqrtI, backend=backend) 75 | logg = zarray**2*F 76 | resw = -A*sqrtI**3/(1 + constants.B_DEBYE*sqrtI) 77 | if calculate_osmotic_coefficient: 78 | if backend == "torch": 79 | osmotic_coefficient = (2*resw/torch.sum(carray) + 1) 80 | else: 81 | osmotic_coefficient = (2*resw/np_.sum(carray) + 1) 82 | else: 83 | osmotic_coefficient = 1.0 84 | if backend == "torch": 85 | osmotic_coefficient = torch.ones_like(logg[..., :1])*osmotic_coefficient 86 | logg = torch.cat([osmotic_coefficient, logg], dim=-1) 87 | else: 88 | osmotic_coefficient = np_.ones(logg.shape[:-1])*osmotic_coefficient 89 | logg = np_.hstack([osmotic_coefficient, logg]) 90 | return logg 91 | 92 | 93 | def loggamma_and_osmotic_limiting(carray, T, zarray, calculate_osmotic_coefficient, backend="numpy"): 94 | if backend in ["numpy", "torch"]: 95 | np_ = np 96 | else: 97 | np_ = jnp 98 | if backend == "torch": 99 | I = 0.5*torch.sum(carray*zarray**2, dim=-1, keepdim=True) 100 | sqrtI = torch.sqrt(I) 101 | else: 102 | I = 0.5*np_.sum(carray*zarray**2, axis=-1, keepdims=True) 103 | sqrtI = np_.sqrt(I) 104 | A = A_debye(T, backend=backend) 105 | F = A*f_limiting(sqrtI, backend=backend) 106 | logg = zarray**2*F 107 | resw = -A*sqrtI**3 108 | if calculate_osmotic_coefficient: 109 | if backend == "torch": 110 | osmotic_coefficient = (2*resw/torch.sum(carray) + 1) 111 | else: 112 | osmotic_coefficient = (2*resw/np_.sum(carray) + 1) 113 | else: 114 | osmotic_coefficient = 1.0 115 | if backend == "torch": 116 | osmotic_coefficient = torch.ones_like(logg[..., :1])*osmotic_coefficient 117 | logg = torch.cat([osmotic_coefficient, logg], dim=-1) 118 | else: 119 | osmotic_coefficient = np_.ones(logg.shape[:-1])*osmotic_coefficient 120 | logg = np_.hstack([osmotic_coefficient, logg]) 121 | return logg 122 | 123 | 124 | def A_debye(T, backend="numpy"): 125 | if backend in ["numpy", "torch"]: 126 | np_ = np 127 | else: 128 | np_ = jnp 129 | Na = 6.0232e23 130 | ee = 4.8029e-10 131 | k = 1.38045e-16 132 | ds = -0.0004 * T + 1.1188 133 | eer = 305.7 * np_.exp(-np_.exp(-12.741 + 0.01875 * T) - T / 219.0) 134 | Aphi = 1.0/3.0*(2.0 * np_.pi * Na * ds / 1000) ** 0.5 * \ 135 | (ee / (eer * k * T) ** 0.5) ** 3.0 136 | if backend == "torch": 137 | Aphi = float(Aphi) 138 | return Aphi 139 | 140 | 141 | def f_debye(sqrtI, backend="numpy"): 142 | if backend == "torch": 143 | res = -(sqrtI/(1+constants.B_DEBYE*sqrtI) + 144 | 2/constants.B_DEBYE*torch.log(1 + constants.B_DEBYE*sqrtI)) 145 | else: 146 | if backend == "numpy": 147 | np_ = np 148 | elif backend == "jax": 149 | np_ = jnp 150 | res = -(sqrtI/(1+constants.B_DEBYE*sqrtI) + 151 | 2/constants.B_DEBYE*np_.log(1 + constants.B_DEBYE*sqrtI)) 152 | return res 153 | 154 | 155 | def f_limiting(sqrtI, backend="numpy"): 156 | if backend == "torch": 157 | res = -(sqrtI/(1+constants.B_DEBYE*sqrtI) + 158 | 2/constants.B_DEBYE*torch.log(1 + constants.B_DEBYE*sqrtI)) 159 | else: 160 | if backend == "numpy": 161 | np_ = np 162 | elif backend == "jax": 163 | np_ = jnp 164 | res = -(sqrtI/(1+constants.B_DEBYE*sqrtI) + 165 | 2/constants.B_DEBYE*np_.log(1 + constants.B_DEBYE*sqrtI)) 166 | return res -------------------------------------------------------------------------------- /pyequion2/activity/extended_debye.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | import warnings 4 | 5 | import numpy as np 6 | try: 7 | import jax 8 | import jax.numpy as jnp 9 | except (ImportError, AssertionError): 10 | warnings.warn("JAX not installed, so can't be used as backend") 11 | try: 12 | import torch 13 | except (ImportError, AssertionError): 14 | warnings.warn("PyTorch not installed, so can't be used as backend") 15 | 16 | from .. import datamods 17 | from .. import utils 18 | from .. import constants 19 | 20 | 21 | DB_SPECIES = datamods.species['debye'] 22 | 23 | 24 | def setup_extended_debye(solutes, 25 | calculate_osmotic_coefficient=False, 26 | backend='numpy'): 27 | assert backend in ['numpy', 'jax', "torch"] 28 | if backend == 'jax': 29 | np_ = jnp #Hack 30 | else: 31 | np_ = np 32 | db_species = DB_SPECIES 33 | I_factor = [] 34 | dh_a = [] 35 | dh_b = [] 36 | for sp in solutes: 37 | I_factor_, dh_a_, dh_b_ = _species_definition_dh_model(sp, db_species, backend=backend) 38 | I_factor.append(I_factor_) 39 | dh_a.append(dh_a_) 40 | dh_b.append(dh_b_) 41 | if backend in ["numpy", "jax"]: 42 | dh_a = np_.array(dh_a) 43 | dh_b = np_.array(dh_b) 44 | I_factor = np_.array(I_factor) 45 | zarray = np_.array([utils.charge_number(specie) for specie in solutes]) 46 | else: 47 | dh_a = torch.tensor(dh_a, dtype=torch.float) 48 | dh_b = torch.tensor(dh_b, dtype=torch.float) 49 | I_factor = torch.tensor(I_factor, dtype=torch.float) 50 | zarray = torch.tensor([utils.charge_number(specie) for specie in solutes], dtype=torch.float) 51 | g = functools.partial(_loggamma_and_osmotic, 52 | zarray=zarray, 53 | calculate_osmotic_coefficient=calculate_osmotic_coefficient, 54 | dh_a=dh_a, dh_b=dh_b, 55 | I_factor=I_factor, 56 | backend=backend) #loggamma 57 | if backend == 'jax': 58 | g = jax.jit(g) 59 | return g 60 | 61 | 62 | def _loggamma_and_osmotic(xarray, TK, zarray, calculate_osmotic_coefficient, 63 | dh_a, dh_b, I_factor, 64 | backend = "numpy"): 65 | if backend in ["numpy", "torch"]: 66 | np_ = np 67 | else: 68 | np_ = jnp 69 | if backend in ["numpy", "jax"]: 70 | A, B = _debye_huckel_constant(TK, backend=backend) 71 | I = 0.5*np_.sum(zarray**2*xarray, axis=-1, keepdims=True) 72 | logg1 = -A * zarray ** 2 * \ 73 | np_.sqrt(I) / (1 + B * dh_a * np_.sqrt(I)) + dh_b * I 74 | logg2 = I_factor*I 75 | logg3 = -A * zarray ** 2 * (np_.sqrt(I) / (1.0 + np_.sqrt(I)) - 0.3 * I) 76 | logg = np_.nan_to_num(logg1)*(~np_.isnan(dh_a)) + \ 77 | np_.nan_to_num(logg2)*(np_.isnan(dh_a) & (~np_.isnan(I_factor))) + \ 78 | np_.nan_to_num(logg3)*(np_.isnan(dh_a) & (np_.isnan(I_factor))) 79 | resw = -A*I**(3/2)/(1 + constants.B_DEBYE*I**(1/2)) 80 | if calculate_osmotic_coefficient: 81 | osmotic_coefficient = constants.LOG10E*(2*resw/np_.sum(xarray, axis=-1, keepdims=True)+1) 82 | else: 83 | osmotic_coefficient = constants.LOG10E 84 | osmotic_coefficient = np_.ones(logg.shape[:-1])*osmotic_coefficient 85 | # Insertion of osmotic coefficient 86 | logg = np_.hstack([osmotic_coefficient, logg]) 87 | else: 88 | A, B = _debye_huckel_constant(TK, backend=backend) 89 | I = 0.5*torch.sum(zarray**2*xarray, dim=-1, keepdim=True) 90 | logg1 = -A*zarray**2*torch.sqrt(I)/(1 + B*dh_a*torch.sqrt(I)) + dh_b*I 91 | logg2 = I_factor*I 92 | logg3 = -A*zarray**2*(torch.sqrt(I)/(1.0 + torch.sqrt(I)) - 0.3*I) 93 | logg = torch.nan_to_num(logg1)*(~torch.isnan(dh_a)) + \ 94 | torch.nan_to_num(logg2)*(torch.isnan(dh_a) & (~torch.isnan(I_factor))) + \ 95 | torch.nan_to_num(logg3)*(torch.isnan(dh_a) & (torch.isnan(I_factor))) 96 | resw = -A*I**(3/2)/(1 + constants.B_DEBYE*I**(1/2)) 97 | if calculate_osmotic_coefficient: 98 | osmotic_coefficient = constants.LOG10E*(2*resw/torch.sum(xarray, dim=-1, keepdim=True)+1) 99 | else: 100 | osmotic_coefficient = constants.LOG10E 101 | osmotic_coefficient = torch.ones_like(logg[..., :1])*osmotic_coefficient 102 | logg = torch.cat([osmotic_coefficient, logg], dim=-1) 103 | return logg 104 | 105 | def _debye_huckel_constant(TK, backend="numpy"): 106 | if backend in ["numpy", "torch"]: 107 | np_ = np 108 | else: 109 | np_ = jnp 110 | epsilon = _dieletricconstant_water(TK) 111 | rho = _density_water(TK) 112 | A = 1.82483e6 * np_.sqrt(rho) / (epsilon * TK) ** 1.5 # (L/mol)^1/2 113 | B = 50.2916 * np_.sqrt(rho / (epsilon * TK)) # Angstrom^-1 . (L/mol)^1/2 114 | if backend == "torch": 115 | A = float(A) 116 | B = float(B) 117 | return A, B 118 | 119 | 120 | def _species_definition_dh_model(tag, species_activity_db, backend="numpy"): 121 | if backend in ["numpy", "torch"]: 122 | np_ = np 123 | else: 124 | np_ = jnp 125 | z = utils.charge_number(tag) 126 | if tag not in species_activity_db: 127 | if z == 0: 128 | I_factor = 0.1 129 | dh_a = np_.nan 130 | dh_b = np_.nan 131 | else: # Else should use davies 132 | I_factor = np_.nan 133 | dh_a = np_.nan 134 | dh_b = np_.nan 135 | else: 136 | db_specie = species_activity_db[tag] 137 | try: 138 | if "I_factor" in db_specie: 139 | I_factor = db_specie["I_factor"] 140 | dh_a = np_.nan 141 | dh_b = np_.nan 142 | else: 143 | I_factor = np_.nan 144 | dh_a = db_specie["dh"]["phreeqc"]["a"] 145 | dh_b = db_specie["dh"]["phreeqc"]["b"] 146 | except KeyError as e: 147 | print("Error getting activity of specie = {}".format(tag)) 148 | raise e 149 | if backend == "torch": 150 | I_factor = float(I_factor) 151 | dh_a = float(dh_a) 152 | dh_b = float(dh_b) 153 | return I_factor, dh_a, dh_b 154 | 155 | 156 | def _dieletricconstant_water(TK): 157 | # for TK: 273-372 158 | return 0.24921e3 - 0.79069 * TK + 0.72997e-3 * TK ** 2 159 | 160 | 161 | def _density_water(TK): 162 | # for TK: 273-372 163 | return ( 164 | 0.183652 165 | + 0.00724987 * TK 166 | - 0.203449e-4 * TK ** 2 167 | + 1.73702e-8 * TK ** 3 168 | ) 169 | -------------------------------------------------------------------------------- /pyequion2/activity/ideal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import warnings 3 | 4 | import numpy as np 5 | try: 6 | import jax 7 | import jax.numpy as jnp 8 | except (ImportError, AssertionError): 9 | warnings.warn("JAX not installed, so can't be used as backend") 10 | try: 11 | import torch 12 | except (ImportError, AssertionError): 13 | warnings.warn("PyTorch not installed, so can't be used as backend") 14 | 15 | from .. import constants 16 | 17 | def setup_ideal(solutes, calculate_osmotic_coefficient=False, backend='numpy'): 18 | assert backend in ['numpy', 'jax', "torch"] 19 | if backend == 'numpy': 20 | def g(x, TK): 21 | constant = np.ones(x.shape[:-1])*constants.LOG10E 22 | return np.hstack([constant, np.zeros_like(x)]) 23 | elif backend == "jax": 24 | def g(x, TK): 25 | constant = jnp.ones(x.shape[:-1])*constants.LOG10E 26 | return jnp.hstack([constant, jnp.zeros_like(x)]) 27 | g = jax.jit(g) 28 | elif backend == "torch": 29 | def g(x, TK): 30 | return torch.cat([x[..., :1]*constants.LOG10E, x*0.0], dim=-1) 31 | return g 32 | -------------------------------------------------------------------------------- /pyequion2/activity/pitzer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | import os 3 | import pathlib 4 | import re 5 | import functools 6 | import warnings 7 | 8 | import numpy as np 9 | 10 | from . import py_coo_tensor_ops as coo_tensor_ops 11 | from .. import utils 12 | from .. import constants 13 | from .. import datamods 14 | 15 | 16 | def setup_pitzer(solutes, calculate_osmotic_coefficient=False, backend='numpy'): 17 | assert backend in ['numpy'] 18 | property_dict = make_pitzer_dictionary() 19 | B0, B0_inds = make_parameter_matrix(solutes, 'B0', property_dict) 20 | B1, B1_inds = make_parameter_matrix(solutes, 'B1', property_dict) 21 | B2, B2_inds = make_parameter_matrix(solutes, 'B2', property_dict) 22 | C0, C0_inds = make_parameter_matrix(solutes, 'C0', property_dict) 23 | THETA, THETA_inds = make_parameter_matrix(solutes, 'THETA', property_dict) 24 | LAMBDA, LAMBDA_inds = make_parameter_matrix( 25 | solutes, 'LAMDA', property_dict) 26 | PSI, PSI_inds = make_parameter_3_tensor(solutes, 'PSI', property_dict) 27 | zarray = np.array([utils.charge_number(specie) for specie in solutes], 28 | dtype=np.double) 29 | f = functools.partial(loggamma_and_osmotic, zarray=zarray, 30 | calculate_osmotic_coefficient=calculate_osmotic_coefficient, 31 | B0_=B0, B0_inds=B0_inds, 32 | B1_=B1, B1_inds=B1_inds, 33 | B2_=B2, B2_inds=B2_inds, 34 | C0_=C0, C0_inds=C0_inds, 35 | THETA_=THETA, THETA_inds=THETA_inds, 36 | PSI_=PSI, PSI_inds=PSI_inds, 37 | LAMBDA_=LAMBDA, LAMBDA_inds=LAMBDA_inds) 38 | def g(xarray, TK): 39 | # ln(gamma) to log10(gamma) 40 | return constants.LOG10E * f(xarray, TK) 41 | return g 42 | 43 | 44 | def loggamma_and_osmotic(carray, T, zarray, 45 | calculate_osmotic_coefficient, 46 | B0_, B0_inds, 47 | B1_, B1_inds, 48 | B2_, B2_inds, 49 | C0_, C0_inds, 50 | THETA_, THETA_inds, 51 | PSI_, PSI_inds, 52 | LAMBDA_, LAMBDA_inds): 53 | #print(B0_inds, B1_inds, B2_inds, C0_inds, THETA_inds) 54 | temp_vector = temperature_vector(T) 55 | B0 = np.sum(temp_vector*B0_, axis=-1) 56 | B1 = np.sum(temp_vector*B1_, axis=-1) 57 | B2 = np.sum(temp_vector*B2_, axis=-1) 58 | C0 = np.sum(temp_vector*C0_, axis=-1) 59 | THETA = np.sum(temp_vector*THETA_, axis=-1) 60 | PSI = np.sum(temp_vector*PSI_, axis=-1) 61 | LAMBDA = np.sum(temp_vector*LAMBDA_, axis=-1) 62 | 63 | # We are excluding ZETA here 64 | dim_matrices = np.array([carray.shape[0], carray.shape[0]], dtype=np.intc) 65 | dim_tensors = np.array( 66 | [carray.shape[0], carray.shape[0], carray.shape[0]], dtype=np.intc) 67 | if carray.dtype != np.double: 68 | carray = carray.astype(np.double) 69 | if zarray.dtype != np.double: 70 | zarray = zarray.astype(np.double) 71 | I = 0.5*np.sum(carray*zarray**2) 72 | sqrtI = np.sqrt(I) 73 | Z = np.sum(carray*np.abs(zarray)) 74 | A = A_debye_plummer(T) 75 | 76 | # (1,1),(2,1) < 4, (2,2) = 4, (3,2),(4,2) > 4 77 | valence_prod_1 = -1*zarray[B1_inds[:, 0]]*zarray[B1_inds[:, 1]] 78 | valence_prod_2 = -1*zarray[B2_inds[:, 0]]*zarray[B2_inds[:, 1]] 79 | alpha1 = 2.0*(valence_prod_1 != 4) + 1.4*(valence_prod_1 == 4) 80 | alpha2 = 12.0*(valence_prod_2 <= 4) + 50.0*(valence_prod_2 > 4) 81 | monovalence = ((np.abs(zarray[B2_inds[:, 0]]) <= 1) | \ 82 | (np.abs(zarray[B2_inds[:, 1]]) <= 1)) 83 | 84 | excess_index_matrix = (zarray*zarray.reshape(-1, 1) > 1) & \ 85 | (np.abs(zarray - zarray.reshape(-1, 1)) > 1e-6) 86 | THETA_e_inds = np.transpose(np.nonzero(excess_index_matrix)).astype(int) 87 | #THETA_e_inds = THETA_inds #REMOVE 88 | z_mn = zarray[THETA_e_inds[:, 0]]*zarray[THETA_e_inds[:, 1]] 89 | z_nn = zarray[THETA_e_inds[:, 0]]*zarray[THETA_e_inds[:, 0]] 90 | z_mm = zarray[THETA_e_inds[:, 1]]*zarray[THETA_e_inds[:, 1]] 91 | x_mn = 6*A*sqrtI*z_mn 92 | x_mm = 6*A*sqrtI*z_mm 93 | x_nn = 6*A*sqrtI*z_nn 94 | J = jtheta(x_mn) - 0.5*(jtheta(x_mm) + jtheta(x_nn)) 95 | THETA_e = z_mn/(4*I)*J 96 | J_prime = (x_mn*jprime(x_mn) - \ 97 | 0.5*x_mm*jprime(x_mm) - \ 98 | 0.5*x_nn*jprime(x_nn)) 99 | THETA_e_prime = z_mn/(8*I*I)*J_prime - THETA_e/I 100 | C = C0/(2*np.sqrt(-1*zarray[C0_inds[:, 0]]*zarray[C0_inds[:, 1]])) 101 | 102 | F_1 = A*f_debye(sqrtI) 103 | F_21 = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 104 | B1*gprime(alpha1*sqrtI)/I, 105 | B1_inds, dim_matrices, carray, carray) 106 | F_22 = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 107 | (~monovalence)*B2*gprime(alpha2*sqrtI)/I, 108 | B2_inds, dim_matrices, carray, carray) 109 | F_31 = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 110 | THETA_e_prime, THETA_e_inds, dim_matrices, carray, carray) 111 | F = F_1 + F_21 + F_22 + F_31 112 | res1 = zarray**2*F 113 | 114 | sum_11 = 2*coo_tensor_ops.coo_matrix_vector( 115 | B0, B0_inds, dim_matrices, carray) 116 | sum_12 = 2*coo_tensor_ops.coo_matrix_vector( 117 | B1*gb(alpha1*sqrtI), 118 | B1_inds, 119 | dim_matrices, 120 | carray) 121 | sum_13 = 2*coo_tensor_ops.coo_matrix_vector( 122 | (~monovalence)*B2*gb(alpha2*sqrtI), 123 | B2_inds, dim_matrices, carray) 124 | sum_21 = Z*coo_tensor_ops.coo_matrix_vector( 125 | C, C0_inds, dim_matrices, carray) 126 | sum_22 = 0.5*np.abs(zarray) *\ 127 | coo_tensor_ops.coo_matrix_vector_vector( 128 | C, C0_inds, dim_matrices, carray, carray) 129 | res2 = sum_11 + sum_12 + sum_13 + sum_21 + sum_22 130 | 131 | sum_31 = 2*coo_tensor_ops.coo_matrix_vector( 132 | THETA, THETA_inds, dim_matrices, carray) 133 | sum_32 = 2*coo_tensor_ops.coo_matrix_vector( 134 | THETA_e, THETA_e_inds, dim_matrices, carray) 135 | sum_33 = 0.5*coo_tensor_ops.coo_tensor_vector_vector( 136 | PSI, PSI_inds, dim_tensors, carray, carray) 137 | #print(sum_31) 138 | #print(sum_32) 139 | res3 = sum_31 + sum_32 + sum_33 140 | sum_41 = 2*coo_tensor_ops.coo_matrix_vector( 141 | LAMBDA, LAMBDA_inds, dim_matrices, carray) 142 | res4 = sum_41 143 | logg = res1 + res2 + res3 + res4 # res1 + res2 + res3 + res4 144 | #B + I*Bprime 145 | #B = B0 + B1*g(alpha1*sqrtI) + B2*g(alpha*sqrtI) 146 | #Bprime = (B1*gprime(alpha1*sqrtI) + B2*gprime(alpha2*sqrtI))/I 147 | # B + I*Bprime = B0 + B1*(g(alpha1*sqrtI) + gprime(alpha1*sqrtI)) 148 | # + B2*(g(alpha2*sqrtI) + gprime(alpha2*sqrtI)) 149 | # Water activity 150 | if not calculate_osmotic_coefficient: 151 | osmotic_coefficient = 1.0 152 | else: 153 | raise NotImplementedError 154 | res1w = -A*sqrtI**3/(1 + constants.B_DEBYE*sqrtI) 155 | 156 | sum_11w = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 157 | B0, B0_inds, dim_matrices, carray, carray) 158 | sum_12w = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 159 | B1*(gb(alpha1*sqrtI) + gprime(alpha1*sqrtI)), B0_inds, dim_matrices, carray, carray) 160 | sum_13w = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 161 | B2*(gb(alpha2*sqrtI) + gprime(alpha2*sqrtI)), B0_inds, dim_matrices, carray, carray) 162 | sum_21w = 0.5*Z*coo_tensor_ops.coo_matrix_vector_vector( 163 | C, C0_inds, dim_matrices, carray, carray) 164 | res2w = sum_11w + sum_12w + sum_13w + sum_21w 165 | 166 | sum_31w = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 167 | PHI + I*PHI_prime, THETA_inds, dim_matrices, carray, carray) 168 | sum_32w = 1/6*coo_tensor_ops.coo_tensor_vector_vector_vector( 169 | PSI, PSI_inds, dim_tensors, carray, carray, carray) 170 | res3w = sum_31w + sum_32w 171 | sum_41 = 0.5*coo_tensor_ops.coo_matrix_vector_vector( 172 | LAMBDA, LAMBDA_inds, dim_matrices, carray, carray) 173 | res4w = sum_41 174 | 175 | resw = 2/np.sum(carray)*(res1w + res2w + res3w + res4w) 176 | osmotic_coefficient = (resw + 1) 177 | logg = np.insert(logg, 0, osmotic_coefficient) 178 | return logg 179 | 180 | 181 | def A_debye(T): 182 | Na = 6.0232e23 183 | ee = 4.8029e-10 184 | k = 1.38045e-16 185 | ds = -0.0004 * T + 1.1188 186 | eer = 305.7 * np.exp(-np.exp(-12.741 + 0.01875 * T) - T / 219.0) 187 | Aphi = 1.0/3.0*(2.0 * np.pi * Na * ds / 1000) ** 0.5 * \ 188 | (ee / (eer * k * T) ** 0.5) ** 3.0 189 | return Aphi 190 | 191 | def A_debye_plummer(T): 192 | ds = -0.0004 * T + 1.1188 193 | eer = 305.7 * np.exp(-np.exp(-12.741 + 0.01875 * T) - T / 219.0) 194 | return 1400684*(ds/(eer*T))**(3.0/2) 195 | 196 | def gprime(x): 197 | return -2*(1-(1+x+x**2/2)*np.exp(-x))/(x**2) 198 | 199 | 200 | def jprime(x): 201 | a, b, c, d = 4.581, 0.7237, 0.0120, 0.528 202 | return ((4+a/(x**b)*np.exp(c*x**d)) - 203 | (x**(1-2*b)*a*np.exp(c*x**d)*(c*d*x**(b+d-1)-b*x**(b-1)))) / \ 204 | ((4+a*np.exp(c*x**d)/(x**b))**2) 205 | 206 | 207 | def gb(x): 208 | return 2*(1-(1+x)*np.exp(-x))/(x**2) 209 | 210 | 211 | def jtheta(x): 212 | return x/(4 + 4.581/(x**(0.7237))*np.exp(0.0120*x**(0.528))) 213 | 214 | 215 | def f_debye(sqrtI): 216 | res = -(sqrtI/(1+constants.B_DEBYE*sqrtI) + 217 | 2/constants.B_DEBYE*np.log(1 + constants.B_DEBYE*sqrtI)) 218 | return res 219 | 220 | 221 | def make_pitzer_dictionary(): 222 | # ownpath = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) 223 | # filepath = ownpath.parents[0]/'data'/'pitzer.txt' 224 | # with open(filepath, 'r') as file: 225 | # lines = file.read().split('\n') 226 | lines = datamods.pitzer_data.split('\n') 227 | # Excluding (OH) labeled elements (boron elements, essentialy) and PITZER line 228 | # lines = [line for line in lines[1:] if '(OH)' not in line] 229 | # Excluding PITZER line 230 | lines = lines[1:] 231 | lines_processed = [_process_line_pitzer(line) for line in lines] 232 | property_names = [] 233 | property_indexes = [] 234 | for i, line in enumerate(lines_processed): 235 | if len(line) == 1: 236 | property_names.append(line[0][1:]) 237 | property_indexes.append(i) 238 | property_dict = dict() 239 | i_low = 0 240 | i_max = len(property_names) - 1 241 | for j, name in enumerate(property_names): 242 | if j < i_max: 243 | i_high = property_indexes[j+1] 244 | lines_processed_i = lines_processed[i_low+1:i_high] 245 | i_low = i_high 246 | # property_dict[name] = lines_processed_i 247 | else: 248 | lines_processed_i = lines_processed[i_low+1:] 249 | # property_dict[name] = lines_processed_i 250 | property_dict_i = dict() 251 | for line in lines_processed_i: 252 | value = line[-6:] 253 | key = tuple(sorted(line[:-6])) 254 | property_dict_i[key] = value 255 | property_dict[name] = property_dict_i 256 | return property_dict 257 | 258 | 259 | def temperature_vector(T): 260 | T0 = 298.15 # K 261 | res = np.array([1, 262 | 1/T - 1/T0, 263 | np.log(T/T0), 264 | T - T0, 265 | T**2 - T0**2, 266 | 1/T**2 - 1/T0**2], dtype=np.double) 267 | return res 268 | 269 | 270 | def make_parameter_matrix(species, parameter, property_dict): 271 | indexes = [] 272 | values = [] 273 | 274 | for i, specie1 in enumerate(species): 275 | for j, specie2 in enumerate(species): 276 | key = tuple(sorted([specie1, specie2])) 277 | if key in property_dict[parameter]: 278 | res_ij = np.array(property_dict[parameter][key]) 279 | if (i == j): 280 | res_ij *= 2 281 | values.append(res_ij) 282 | indexes.append((i, j)) 283 | M = np.array(values, dtype=np.double) 284 | M_inds = np.array(indexes, dtype=np.intc) 285 | if M.shape[0] == 0: #Case of an empty matrix 286 | M = M.reshape(0, 6) 287 | M_inds = M_inds.reshape(0, 2) 288 | return M, M_inds 289 | 290 | 291 | def make_parameter_3_tensor(species, parameter, property_dict): 292 | indexes = [] 293 | values = [] 294 | 295 | for i, specie1 in enumerate(species): 296 | for j, specie2 in enumerate(species): 297 | for k, specie3 in enumerate(species): 298 | key = tuple(sorted([specie1, specie2, specie3])) 299 | if key in property_dict[parameter]: 300 | res_ij = np.array(property_dict[parameter][key]) 301 | if (i == j) and (i == k): 302 | res_ij *= 3 303 | elif (i == j) or (i == k) or (j == k): 304 | res_ij *= 2 305 | values.append(res_ij) 306 | indexes.append((i, j, k)) 307 | M = np.array(values, dtype=np.double) 308 | M_inds = np.array(indexes, dtype=np.intc) 309 | if M.shape[0] == 0: #Case of an empty matrix 310 | M = M.reshape(0, 6) 311 | M_inds = M_inds.reshape(0, 3) 312 | return M, M_inds 313 | 314 | 315 | def _find_and_replace_charge_signed(string, sign): 316 | # Cation finding 317 | pattern = r'.*(\%s\d).*'%sign 318 | match = re.search(pattern, string) 319 | if match: 320 | number = int(match.group(1)[-1]) 321 | patternsub = r'\%s\d' % sign 322 | new_string = re.sub(patternsub, sign*number, string) 323 | else: 324 | new_string = string 325 | return new_string 326 | 327 | 328 | def _find_and_replace_charge(string): 329 | string = _find_and_replace_charge_signed(string, '+') 330 | string = _find_and_replace_charge_signed(string, '-') 331 | return string 332 | 333 | 334 | def _remove_after_hash(linestrings): 335 | for i, string in enumerate(linestrings): 336 | if '#' in string: 337 | return linestrings[:i] 338 | return linestrings 339 | 340 | 341 | def _process_line_pitzer(line): 342 | linestrings = line.split() 343 | if len(linestrings) == 1: # Parameter name 344 | return linestrings 345 | linestrings = _remove_after_hash(linestrings) # Remove comments 346 | for i, string in enumerate(linestrings): 347 | try: # Should be a float 348 | linestrings[i] = float(string) 349 | except: # Is a element 350 | linestrings[i] = _find_and_replace_charge(string) 351 | max_size = 8 if type(linestrings[2]) == float else 9 352 | if len(linestrings) < max_size: 353 | linestrings = linestrings + [0.0]*(max_size - len(linestrings)) 354 | return linestrings 355 | -------------------------------------------------------------------------------- /pyequion2/activity/pitzer_sanity_assertions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | 5 | def make_sanity_assertions(): 6 | from .coo_tensor_ops import coo_tensor_ops 7 | from . import py_coo_tensor_ops 8 | 9 | for i in range(2): 10 | n = 50 + 20*i 11 | A_data = np.random.randn(n).astype(np.double) 12 | A_inds = np.random.randint(n, size=(n,2), dtype=np.intc) 13 | A_shape = np.array([n, n], dtype=np.intc) 14 | At_data = np.random.randn(n).astype(np.double) 15 | At_inds = np.random.randint(n, size=(n,3), dtype=np.intc) 16 | At_shape = np.array([n, n, n], dtype=np.intc) 17 | b1 = np.random.randn(n).astype(np.double) 18 | b2 = np.random.randn(n).astype(np.double) 19 | b3 = np.random.randn(n).astype(np.double) 20 | 21 | res_cython_1 = coo_tensor_ops.coo_matrix_vector(A_data, A_inds, A_shape, b1) 22 | res_python_1 = py_coo_tensor_ops.coo_matrix_vector(A_data, A_inds, A_shape, b1) 23 | res_cython_2 = coo_tensor_ops.coo_matrix_vector_vector(A_data, A_inds, A_shape, b1, b2) 24 | res_python_2 = py_coo_tensor_ops.coo_matrix_vector_vector(A_data, A_inds, A_shape, b1, b2) 25 | res_cython_3 = coo_tensor_ops.coo_tensor_vector_vector(At_data, At_inds, At_shape, b1, b2) 26 | res_python_3 = py_coo_tensor_ops.coo_tensor_vector_vector(At_data, At_inds, At_shape, b1, b2) 27 | res_cython_4 = coo_tensor_ops.coo_tensor_vector_vector_vector(At_data, At_inds, At_shape, b1, b2, b3) 28 | res_python_4 = py_coo_tensor_ops.coo_tensor_vector_vector_vector(At_data, At_inds, At_shape, b1, b2, b3) 29 | 30 | 31 | assert(np.max(np.abs(res_cython_1 - res_python_1)) < 1e-12) 32 | assert(np.max(np.abs(res_cython_2 - res_python_2)) < 1e-12) 33 | assert(np.max(np.abs(res_cython_3 - res_python_3)) < 1e-12) 34 | assert(np.max(np.abs(res_cython_4 - res_python_4)) < 1e-12) -------------------------------------------------------------------------------- /pyequion2/activity/py_coo_tensor_ops.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def coo_matrix_vector(A_data, A_inds, A_shape, b): 5 | assert A_shape[1] == b.shape[0] 6 | result = np.zeros(A_shape[0],dtype=np.double) 7 | imax = A_data.shape[0] 8 | for i in range(imax): 9 | ind0 = A_inds[i,0] 10 | ind1 = A_inds[i,1] 11 | result[ind0] += A_data[i]*b[ind1] 12 | return result 13 | 14 | 15 | def coo_matrix_vector_vector(A_data, A_inds, A_shape, 16 | b1, b2): 17 | assert A_shape[0] == b1.shape[0] 18 | assert A_shape[1] == b2.shape[0] 19 | imax = A_data.shape[0] 20 | result = 0.0 21 | for i in range(imax): 22 | ind0 = A_inds[i,0] 23 | ind1 = A_inds[i,1] 24 | result += A_data[i]*b1[ind0]*b2[ind1] 25 | return result 26 | 27 | 28 | def coo_tensor_vector_vector(A_data, A_inds, A_shape, 29 | b1, b2): 30 | assert A_shape[1] == b1.shape[0] 31 | assert A_shape[2] == b2.shape[0] 32 | 33 | result = np.zeros(A_shape[0],dtype=np.double) 34 | imax = A_data.shape[0] 35 | 36 | for i in range(imax): 37 | ind0 = A_inds[i,0] 38 | ind1 = A_inds[i,1] 39 | ind2 = A_inds[i,2] 40 | result[ind0] += A_data[i]*b1[ind1]*b2[ind2] 41 | return result 42 | 43 | # @cython.boundscheck(False) # Deactivate bounds checking 44 | # @cython.wraparound(False) # Deactivate negative indexing. 45 | def coo_tensor_vector_vector_vector(A_data, A_inds, A_shape, 46 | b1, b2, b3): 47 | assert A_shape[0] == b1.shape[0] 48 | assert A_shape[1] == b2.shape[0] 49 | assert A_shape[2] == b3.shape[0] 50 | imax = A_data.shape[0] 51 | result = 0.0 52 | for i in range(imax): 53 | ind0 = A_inds[i,0] 54 | ind1 = A_inds[i,1] 55 | ind2 = A_inds[i,2] 56 | result += A_data[i]*b1[ind0]*b2[ind1]*b3[ind2] 57 | return result 58 | -------------------------------------------------------------------------------- /pyequion2/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | GAS_CONSTANT = 8.31446261815324 4 | MOLAR_WEIGHT_WATER = 18.01528*1e-3 #kg/mol 5 | LOG10E = 0.4342944819032518 6 | LOGE10 = 1.0/LOG10E 7 | B_DEBYE = 1.2 #kg^0.5 mole^-0.5 8 | ALKALINE_COEFFICIENTS = { 9 | 'HCO3-': 1.0, 10 | 'CO3--': 2.0, 11 | 'OH-': 1.0, 12 | 'HPO4--': 1.0, 13 | 'PO4---': 2.0, 14 | 'H3SiO4--': 1.0, 15 | 'NH3': 1.0, 16 | 'HS-': 1.0, 17 | 'H+': -1.0, 18 | 'HSO4--': -1.0, 19 | 'HF': -1.0, 20 | 'H3PO4': -1.0, 21 | 'HNO2': -1.0 22 | } 23 | TURBULENT_VISCOSITY_CONSTANT = 9.5*1e-4 24 | PATM = 101325. 25 | PBAR = 1e5 -------------------------------------------------------------------------------- /pyequion2/converters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import periodictable 3 | import numpy as np 4 | 5 | from . import water_properties 6 | from . import builder 7 | from .datamods import reactions_solids 8 | from .datamods import density_solids 9 | 10 | 11 | ELEMENTS_MOLAR_WEIGHTS = { 12 | el.symbol: el.mass for el in periodictable.elements if el.symbol != 'n'} 13 | 14 | 15 | def mmolar_to_molal(x, TK=298.15): 16 | #mol/m^3 = mol/(1e3*L) = 1e-3*mol/L = mmol/L 17 | return x/water_properties.water_density(TK) 18 | 19 | 20 | def molal_to_mmolar(x, TK=298.15): 21 | return x*water_properties.water_density(TK) #mmol/L = mol/m^3 to mol/kg 22 | 23 | 24 | def mgl_to_molal(x, specie, TK=298.15): 25 | #x : mg/L 26 | #mg/L = (1e-3*g)/(1e-3*m^3) = g/m^3 27 | gkg = x/water_properties.water_density(TK) # g/(kg H2O) 28 | if specie in ELEMENTS_MOLAR_WEIGHTS: 29 | molar_mass = ELEMENTS_MOLAR_WEIGHTS[specie] 30 | else: #Specie 31 | el_and_coefs = builder.get_elements_and_their_coefs([specie])[0] 32 | molar_mass = sum([ELEMENTS_MOLAR_WEIGHTS[el]*coef 33 | for el, coef in el_and_coefs]) # g/mol 34 | molal = gkg/molar_mass #mol/(kg H20) 35 | return molal 36 | 37 | 38 | def molal_to_mgl(x, specie, TK=298.15): 39 | if specie in ELEMENTS_MOLAR_WEIGHTS: 40 | molar_mass = ELEMENTS_MOLAR_WEIGHTS[specie] 41 | else: #Specie 42 | el_and_coefs = builder.get_elements_and_their_coefs([specie])[0] 43 | molar_mass = sum([ELEMENTS_MOLAR_WEIGHTS[el]*coef 44 | for el, coef in el_and_coefs]) # g/mol 45 | y = x*molar_mass #mol/(kg H2O) to g/(kg H2O) 46 | mgl = y*water_properties.water_density(TK) #g/(kg H2O) to g/m^3 = mg/L 47 | return mgl 48 | 49 | 50 | def get_activity_from_fugacity(fugacity, spec, TK=298.15): 51 | gases = builder.DEFAULT_DB_FILES["gases"] 52 | possible = [g for g in gases if spec in g and spec + '(g)' in g] 53 | if possible == []: 54 | return None 55 | else: 56 | logK = builder.get_log_equilibrium_constants(possible, TK, 1.0)[0] 57 | logact = logK + np.log10(fugacity) 58 | return 10**logact 59 | 60 | def get_activity_from_partial_pressure(pp, spec, TK=298.15): 61 | #For now, alias for get_activity_from_fugacity 62 | return get_activity_from_fugacity(pp, spec, TK) 63 | 64 | def phase_to_molar_weight(phase_name): 65 | possible_reactions = list(filter(lambda r : r['phase_name'] == phase_name, 66 | reactions_solids)) 67 | if len(possible_reactions) == 0: 68 | return None 69 | else: 70 | reaction = possible_reactions[0] 71 | specs_and_coefs = [(s, k) for s, k in reaction.items() if k == 1.0] 72 | specs, coefs = zip(*specs_and_coefs) 73 | els_and_coefs = builder.get_elements_and_their_coefs(specs) 74 | spec_masses = [sum([ELEMENTS_MOLAR_WEIGHTS[el]*coef 75 | for el, coef in el_and_coefs]) 76 | for el_and_coefs in els_and_coefs] 77 | molar_mass = sum([sp_mass*coef for sp_mass, coef in zip(spec_masses, coefs)]) 78 | return molar_mass*1e-3 #g/mol to kg/mol 79 | 80 | 81 | def phase_density(phase_name): 82 | return density_solids.densities.get(phase_name, 3000.0) 83 | 84 | -------------------------------------------------------------------------------- /pyequion2/datamods/README: -------------------------------------------------------------------------------- 1 | Data taken from phreeqc.data and pitzer.txt 2 | -------------------------------------------------------------------------------- /pyequion2/datamods/__init__.py: -------------------------------------------------------------------------------- 1 | from .reactions_solids import reactions_solids 2 | from .reactions_solutions import reactions_solutions 3 | from .reactions_irreversible import reactions_irreversible 4 | from .reactions_gases import reactions_gases 5 | from .species import species 6 | from .pitzer_data import pitzer_data 7 | from .chemical_potentials import chemical_potentials -------------------------------------------------------------------------------- /pyequion2/datamods/chemical_potentials.py: -------------------------------------------------------------------------------- 1 | chemical_potentials = { #k J/mol, J/mol/K 2 | 'Ag(CN)2-': {'mu0': 305.43, 'coef0': -192.46}, 3 | 'AgCl': {'mu0': -72.8, 'coef0': -153.97}, 4 | 'AgCl2-': {'mu0': -215.48, 'coef0': -231.38}, 5 | 'Ag(NH3)2+': {'mu0': -17.24, 'coef0': -245.18}, 6 | 'AgOH': {'mu0': -92.05, 'coef0': 0.0}, 7 | 'Ag(OH)--': {'mu0': -260.24, 'coef0': 0.0}, 8 | 'Al+++': {'mu0': -485.34, 'coef0': 321.75}, 9 | 'Al(OH)4-': {'mu0': -1297.88, 'coef0': -117.15}, 10 | 'Ar': {'mu0': 16.32, 'coef0': -59.41}, 11 | 'AsFO3--': {'mu0': -1027.55, 'coef0': 0.0}, 12 | 'AsFO3H-': {'mu0': -1061.06, 'coef0': 0.0}, 13 | 'As(OH)2+': {'mu0': -400.99, 'coef0': 0.0}, 14 | 'AsO3H2-': {'mu0': -587.22, 'coef0': -110.46}, 15 | 'AsO3H3': {'mu0': -639.9, 'coef0': -194.97}, 16 | 'AsO4---': {'mu0': -648.52, 'coef0': 162.76}, 17 | 'AsO4H2-': {'mu0': -753.29, 'coef0': -117.15}, 18 | 'AsO4H3': {'mu0': -766.09, 'coef0': -184.1}, 19 | 'AuBr2-': {'mu0': -115.02, 'coef0': -219.66}, 20 | 'AuBr4-': {'mu0': -167.36, 'coef0': -335.98}, 21 | 'Au(CN)2-': {'mu0': 285.77, 'coef0': -171.54}, 22 | 'AuCl2-': {'mu0': -151.17, 'coef0': 0.0}, 23 | 'AuCl4-': {'mu0': -235.22, 'coef0': -266.94}, 24 | 'Au(OH)3': {'mu0': -283.47, 'coef0': 0.0}, 25 | 'Au(OH)4-': {'mu0': -455.64, 'coef0': 0.0}, 26 | 'Au(OH)5--': {'mu0': -616.72, 'coef0': 0.0}, 27 | 'BF4-': {'mu0': -1486.99, 'coef0': -179.91}, 28 | 'BH4-': {'mu0': 114.27, 'coef0': -110.46}, 29 | 'BO3H3': {'mu0': -968.85, 'coef0': -132.34}, 30 | 'B(OH)4-': {'mu0': -1153.32, 'coef0': -102.51}, 31 | 'B4O7--': {'mu0': -2604.96, 'coef0': 0.0}, 32 | 'Ba++': {'mu0': -560.66, 'coef0': -12.55}, 33 | 'Bi+++': {'mu0': 82.84, 'coef0': 0.0}, 34 | 'Br-': {'mu0': -103.97, 'coef0': -82.42}, 35 | 'BrO-': {'mu0': -33.47, 'coef0': -41.84}, 36 | 'BrOH': {'mu0': -82.42, 'coef0': -142.26}, 37 | 'BrO3-': {'mu0': 1.67, 'coef0': -163.18}, 38 | 'Br2': {'mu0': 3.93, 'coef0': -130.54}, 39 | 'Br3-': {'mu0': -107.07, 'coef0': -215.48}, 40 | 'Br5-': {'mu0': -103.76, 'coef0': -316.73}, 41 | 'CHO2-': {'mu0': -351.04, 'coef0': -92.05}, 42 | 'CH2O2': {'mu0': -372.38, 'coef0': -163.18}, 43 | 'CN-': {'mu0': 172.38, 'coef0': -94.14}, 44 | 'CNH': {'mu0': 119.66, 'coef0': -124.68}, 45 | 'CNO-': {'mu0': -97.49, 'coef0': -106.69}, 46 | 'CNOH': {'mu0': -117.15, 'coef0': -144.77}, 47 | 'CNS-': {'mu0': 92.68, 'coef0': -144.35}, 48 | '': {'mu0': 97.53, 'coef0': 0.0}, 49 | 'CO2': {'mu0': -385.99, 'coef0': 117.53}, 50 | 'CO3--': {'mu0': -527.9, 'coef0': 56.9}, 51 | 'CO3H-': {'mu0': -586.85, 'coef0': -91.21}, 52 | 'CO3H2': {'mu0': -608.25, 'coef0': 0.0}, 53 | 'C2H3O2-': {'mu0': -369.41, 'coef0': -86.61}, 54 | 'C2H4O2': {'mu0': -396.56, 'coef0': -178.66}, 55 | 'C6H10O5': {'mu0': -662.49, 'coef0': 0.0}, 56 | 'C6H12O6': {'mu0': -917.44, 'coef0': 0.0}, 57 | 'C10H15N5O10P2': {'mu0': -2000.0, 'coef0': 0.0}, 58 | 'C10H16N5O13P3': {'mu0': -2890.0, 'coef0': 0.0}, 59 | 'C12H22O11': {'mu0': -1552.22, 'coef0': 0.0}, 60 | 'Ca++': {'mu0': -553.04, 'coef0': 55.23}, 61 | 'Cd++': {'mu0': -77.58, 'coef0': 73.22}, 62 | 'CdBr2': {'mu0': -302.08, 'coef0': 0.0}, 63 | 'CdBr3-': {'mu0': -407.52, 'coef0': 0.0}, 64 | 'Cd(CN)2': {'mu0': 207.94, 'coef0': 0.0}, 65 | 'Cd(CN)3-': {'mu0': 354.8, 'coef0': 0.0}, 66 | 'Cd(CN)42-': {'mu0': 507.52, 'coef0': 0.0}, 67 | 'CdCl2': {'mu0': -359.32, 'coef0': -121.75}, 68 | 'CdCl3-': {'mu0': -487.02, 'coef0': -202.92}, 69 | 'CdJ2': {'mu0': -201.25, 'coef0': 0.0}, 70 | 'CdJ3-': {'mu0': -259.41, 'coef0': 0.0}, 71 | 'CdJ42-': {'mu0': -315.89, 'coef0': -326.35}, 72 | 'Cd(OH)': {'mu0': -261.08, 'coef0': 0.0}, 73 | 'Cd(OH)2': {'mu0': -442.67, 'coef0': 0.0}, 74 | 'Cd(OH)3-': {'mu0': -600.82, 'coef0': 0.0}, 75 | 'Cd(OH)42-': {'mu0': -758.56, 'coef0': 0.0}, 76 | 'Cl-': {'mu0': -131.26, 'coef0': -56.48}, 77 | 'ClH': {'mu0': -96.44, 'coef0': 0.0}, 78 | 'ClH2+': {'mu0': -39.37, 'coef0': 0.0}, 79 | 'ClO-': {'mu0': -36.82, 'coef0': -41.84}, 80 | 'ClOH': {'mu0': -79.91, 'coef0': -142.26}, 81 | 'ClO2-': {'mu0': 17.15, 'coef0': -101.25}, 82 | 'ClO2H': {'mu0': 5.86, 'coef0': -188.28}, 83 | 'ClO3-': {'mu0': -3.35, 'coef0': -162.34}, 84 | 'ClO4-': {'mu0': -8.62, 'coef0': -182.0}, 85 | 'Cl2': {'mu0': 6.9, 'coef0': -121.34}, 86 | 'Co++': {'mu0': -54.39, 'coef0': 112.87}, 87 | 'Co+++': {'mu0': 133.89, 'coef0': 305.43}, 88 | 'Co(NH3)6+++': {'mu0': -162.76, 'coef0': -167.36}, 89 | 'Co(OH)2': {'mu0': -421.75, 'coef0': 0.0}, 90 | 'Co(OH)3-': {'mu0': -644.75, 'coef0': 0.0}, 91 | 'CrO4--': {'mu0': -727.85, 'coef0': -50.21}, 92 | 'CrO4H-': {'mu0': -764.84, 'coef0': -184.1}, 93 | 'Cr2O7--': {'mu0': -1301.22, 'coef0': -261.92}, 94 | 'Cs+': {'mu0': -282.04, 'coef0': -133.05}, 95 | 'Cu+': {'mu0': 50.0, 'coef0': -40.58}, 96 | 'Cu++': {'mu0': 65.52, 'coef0': 99.58}, 97 | 'Cu(CN)2-': {'mu0': 257.73, 'coef0': 0.0}, 98 | 'Cu(CNS)2': {'mu0': 229.87, 'coef0': 0.0}, 99 | 'Cu(CN)3--': {'mu0': 403.76, 'coef0': 0.0}, 100 | 'Cu(CN)4---': {'mu0': 566.51, 'coef0': 0.0}, 101 | 'Cu(CNS)4---': {'mu0': 364.01, 'coef0': -644.34}, 102 | 'CuCl2': {'mu0': -197.9, 'coef0': 0.0}, 103 | 'CuCl2-': {'mu0': -240.16, 'coef0': 0.0}, 104 | 'Cu(NH3)4++': {'mu0': -111.29, 'coef0': -273.63}, 105 | 'Cu(OH)2': {'mu0': -249.07, 'coef0': 120.92}, 106 | 'Cu(OH)3-': {'mu0': -495.8, 'coef0': 0.0}, 107 | 'Cu(OH)4--': {'mu0': -658.14, 'coef0': 0.0}, 108 | 'CuSO4': {'mu0': -692.24, 'coef0': 0.0}, 109 | 'F-': {'mu0': -278.82, 'coef0': 13.81}, 110 | 'FH': {'mu0': -296.85, 'coef0': -88.7}, 111 | 'F2H-': {'mu0': -578.15, 'coef0': -92.47}, 112 | 'Fe+++': {'mu0': -4.6, 'coef0': 315.89}, 113 | 'Fe(CN)6---': {'mu0': 729.27, 'coef0': -270.29}, 114 | 'Fe(CN)6----': {'mu0': 694.92, 'coef0': -94.98}, 115 | 'Fe(OH)+2': {'mu0': -438.06, 'coef0': 0.0}, 116 | 'Fe(OH)3': {'mu0': -659.4, 'coef0': 0.0}, 117 | 'Fe(OH)3-': {'mu0': -615.05, 'coef0': 0.0}, 118 | 'Fe(OH)4--': {'mu0': -929.68, 'coef0': 0.0}, 119 | 'Fe2(OH)2++++': {'mu0': -467.27, 'coef0': 355.64}, 120 | 'Ga+++': {'mu0': -158.99, 'coef0': 330.54}, 121 | 'GaBr4-': {'mu0': -550.2, 'coef0': -35.98}, 122 | 'Ga(OH)+': {'mu0': -380.33, 'coef0': 0.0}, 123 | 'Ga(OH)2+': {'mu0': -597.48, 'coef0': 0.0}, 124 | 'Ga(OH)4-': {'mu0': -983.24, 'coef0': 0.0}, 125 | 'Ga(OH)5--': {'mu0': -1158.97, 'coef0': 0.0}, 126 | 'Ga(OH)6---': {'mu0': -1330.51, 'coef0': 0.0}, 127 | 'H+': {'mu0': 0.0, 'coef0': 0.0}, 128 | 'H2': {'mu0': 18.0, 'coef0': -49.0}, 129 | 'He': {'mu0': 19.25, 'coef0': -55.65}, 130 | 'Hg': {'mu0': 37.23, 'coef0': 0.0}, 131 | 'Hg++': {'mu0': 164.43, 'coef0': 32.22}, 132 | 'Hg(CN)2': {'mu0': 312.13, 'coef0': -156.06}, 133 | 'Hg(CN)3-': {'mu0': 463.17, 'coef0': -215.06}, 134 | 'Hg(CN)4--': {'mu0': 618.4, 'coef0': -297.06}, 135 | 'HgCl2': {'mu0': -173.22, 'coef0': -154.81}, 136 | 'HgCl3-': {'mu0': -309.2, 'coef0': -209.2}, 137 | 'HgCl4--': {'mu0': -446.85, 'coef0': -292.88}, 138 | 'HgJ2': {'mu0': -75.31, 'coef0': -175.73}, 139 | 'HgJ3-': {'mu0': -148.53, 'coef0': -301.25}, 140 | 'HgJ4--': {'mu0': -211.71, 'coef0': -359.82}, 141 | 'Hg(NH3)4++': {'mu0': -51.88, 'coef0': -334.72}, 142 | 'Hg(OH)2': {'mu0': -274.89, 'coef0': -142.26}, 143 | 'Hg(OH)3-': {'mu0': -427.6, 'coef0': 0.0}, 144 | 'In+': {'mu0': -12.13, 'coef0': 0.0}, 145 | 'In3+': {'mu0': -106.27, 'coef0': -150.62}, 146 | 'J-': {'mu0': -51.59, 'coef0': -111.29}, 147 | 'JBr2-': {'mu0': -123.01, 'coef0': 0.0}, 148 | 'JCl2-': {'mu0': -161.08, 'coef0': 0.0}, 149 | 'JO-': {'mu0': -38.49, 'coef0': 5.44}, 150 | 'JOH': {'mu0': -99.16, 'coef0': -95.4}, 151 | 'JO3-': {'mu0': -128.03, 'coef0': -118.41}, 152 | 'JO3H': {'mu0': -132.63, 'coef0': -166.94}, 153 | 'J2': {'mu0': 16.4, 'coef0': -137.24}, 154 | 'J3-': {'mu0': -51.46, 'coef0': -239.32}, 155 | 'K+': {'mu0': -283.26, 'coef0': -102.51}, 156 | 'Kr': {'mu0': 15.06, 'coef0': -61.5}, 157 | 'La+++': {'mu0': -723.41, 'coef0': 184.1}, 158 | 'Li+': {'mu0': -293.8, 'coef0': -14.23}, 159 | 'Mg++': {'mu0': -456.01, 'coef0': 117.99}, 160 | 'Mn++': {'mu0': -228.03, 'coef0': 73.64}, 161 | 'MnCl3-': {'mu0': -620.07, 'coef0': 0.0}, 162 | 'Mn(OH)3-': {'mu0': -744.33, 'coef0': 0.0}, 163 | 'MnO4-': {'mu0': -447.27, 'coef0': -191.21}, 164 | 'MoO4--': {'mu0': -836.38, 'coef0': -27.2}, 165 | 'NH3': {'mu0': -26.57, 'coef0': -111.29}, 166 | 'NH4+': {'mu0': -79.37, 'coef0': -113.39}, 167 | 'NO2-': {'mu0': -37.24, 'coef0': -140.16}, 168 | 'NO2H': {'mu0': -55.65, 'coef0': -152.72}, 169 | 'NO3-': {'mu0': -111.34, 'coef0': -146.44}, 170 | 'N2': {'mu0': 18.19, 'coef0': 0.0}, 171 | 'N2H4': {'mu0': 128.03, 'coef0': -138.07}, 172 | 'N2H5+': {'mu0': 82.42, 'coef0': -150.62}, 173 | 'N3-': {'mu0': 348.11, 'coef0': -107.95}, 174 | 'N3H': {'mu0': 321.75, 'coef0': -146.02}, 175 | 'Na+': {'mu0': -261.89, 'coef0': -58.99}, 176 | 'Nb(OH)5': {'mu0': -1448.5, 'coef0': 0.0}, 177 | 'Ne': {'mu0': 19.25, 'coef0': -66.11}, 178 | 'Ni2+': {'mu0': -45.61, 'coef0': 128.87}, 179 | 'Ni(CN)4--': {'mu0': 471.96, 'coef0': -217.57}, 180 | 'Ni(NH3)6++': {'mu0': -256.06, 'coef0': -394.55}, 181 | 'Ni(OH)1': {'mu0': -227.61, 'coef0': 71.13}, 182 | 'OH-': {'mu0': -157.29, 'coef0': 10.75}, 183 | 'OH3+': {'mu0': -237.18, 'coef0': -69.91}, 184 | 'O2': {'mu0': 16.44, 'coef0': 0.0}, 185 | 'O2H-': {'mu0': -67.36, 'coef0': -23.85}, 186 | 'O2H2': {'mu0': -134.1, 'coef0': -143.93}, 187 | 'OsO5H-': {'mu0': -470.28, 'coef0': 0.0}, 188 | 'OsO5H2': {'mu0': -539.07, 'coef0': 0.0}, 189 | 'PH3': {'mu0': 1.46, 'coef0': -201.67}, 190 | 'PH4+': {'mu0': 67.78, 'coef0': 0.0}, 191 | 'PO4---': {'mu0': -1018.8, 'coef0': 221.75}, 192 | 'PO4H--': {'mu0': -1089.26, 'coef0': 33.47}, 193 | 'PO4H++': {'mu0': -1090.58, 'coef0': 0.0}, 194 | 'PO4H2-': {'mu0': -1130.39, 'coef0': -90.37}, 195 | 'PO4H3': {'mu0': -1170.48, 'coef0': 0.0}, 196 | 'P2O7----': {'mu0': -1919.2, 'coef0': 117.15}, 197 | 'P2O7H---': {'mu0': -1972.34, 'coef0': -46.02}, 198 | 'P2O7---': {'mu0': -1990.2, 'coef0': 0.0}, 199 | 'P2O7H2--': {'mu0': -2010.41, 'coef0': -163.18}, 200 | 'P2O7H3-': {'mu0': -2023.38, 'coef0': -213.38}, 201 | 'P2O7H4': {'mu0': -2032.17, 'coef0': -267.78}, 202 | 'Pb++': {'mu0': -24.39, 'coef0': -10.46}, 203 | 'PbBr2': {'mu0': -240.58, 'coef0': 0.0}, 204 | 'PbBr3-': {'mu0': -343.09, 'coef0': 0.0}, 205 | 'PbCl2': {'mu0': -297.19, 'coef0': 0.0}, 206 | 'PbCl3-': {'mu0': -426.35, 'coef0': 0.0}, 207 | 'PbJ2': {'mu0': -143.51, 'coef0': 0.0}, 208 | 'PbJ3-': {'mu0': -198.74, 'coef0': 0.0}, 209 | 'PbJ4--': {'mu0': -254.81, 'coef0': 0.0}, 210 | 'Pb(OH)': {'mu0': -226.35, 'coef0': 0.0}, 211 | 'Pb(OH)2': {'mu0': -400.83, 'coef0': 0.0}, 212 | 'Pb(OH)3-': {'mu0': -575.72, 'coef0': 0.0}, 213 | 'Pd++': {'mu0': 176.56, 'coef0': 117.15}, 214 | 'Pd(CN)4--': {'mu0': 627.6, 'coef0': 0.0}, 215 | 'PdCl2': {'mu0': -128.87, 'coef0': 0.0}, 216 | 'PdCl3-': {'mu0': -276.14, 'coef0': 0.0}, 217 | 'PdCl4--': {'mu0': -416.73, 'coef0': -259.41}, 218 | 'PdCl6--': {'mu0': -430.12, 'coef0': -271.96}, 219 | 'Pt2+': {'mu0': 185.77, 'coef0': 0.0}, 220 | 'PtCl2': {'mu0': -79.91, 'coef0': 0.0}, 221 | 'PtCl3-': {'mu0': -228.45, 'coef0': 0.0}, 222 | 'PtCl4--': {'mu0': -368.61, 'coef0': -167.36}, 223 | 'PtCl6--': {'mu0': -489.53, 'coef0': -220.08}, 224 | 'Rb-': {'mu0': -282.21, 'coef0': -124.26}, 225 | 'ReCl6--': {'mu0': -589.94, 'coef0': -251.04}, 226 | 'ReO4-': {'mu0': -694.54, 'coef0': -201.25}, 227 | 'RuO4-': {'mu0': -245.6, 'coef0': 0.0}, 228 | 'RuO4--': {'mu0': -303.76, 'coef0': 0.0}, 229 | 'S--': {'mu0': 85.77, 'coef0': 14.64}, 230 | 'SH-': {'mu0': 12.05, 'coef0': -62.76}, 231 | 'SH2': {'mu0': -27.87, 'coef0': -121.34}, 232 | 'SO3--': {'mu0': -486.6, 'coef0': 29.29}, 233 | 'SO3H-': {'mu0': -527.81, 'coef0': -139.75}, 234 | 'SO3H2': {'mu0': -537.9, 'coef0': -232.21}, 235 | 'SO4--': {'mu0': -744.63, 'coef0': -20.08}, 236 | 'SO4H-': {'mu0': -756.01, 'coef0': -131.8}, 237 | 'SO4H2': {'mu0': -738.88, 'coef0': 0.0}, 238 | 'S2--': {'mu0': 79.5, 'coef0': -28.45}, 239 | 'S2O4--': {'mu0': -600.4, 'coef0': -92.05}, 240 | 'S2O4H-': {'mu0': -614.63, 'coef0': 0.0}, 241 | 'S2O4H2': {'mu0': -616.72, 'coef0': 0.0}, 242 | 'S2O8--': {'mu0': -1110.43, 'coef0': -248.11}, 243 | 'S3--': {'mu0': 73.64, 'coef0': -66.11}, 244 | 'S4--': {'mu0': 69.04, 'coef0': -103.34}, 245 | 'S5--': {'mu0': 65.69, 'coef0': -140.58}, 246 | 'SbF(OH)2': {'mu0': -724.67, 'coef0': 0.0}, 247 | 'Sb(OH)2+': {'mu0': -414.3, 'coef0': 0.0}, 248 | 'Sb(OH)3': {'mu0': -644.75, 'coef0': -116.32}, 249 | 'Se--': {'mu0': 129.29, 'coef0': 0.0}, 250 | 'SeO3--': {'mu0': -369.87, 'coef0': -12.55}, 251 | 'SeO3H-': {'mu0': -411.54, 'coef0': -135.14}, 252 | 'SeO3H2': {'mu0': -426.22, 'coef0': -207.94}, 253 | 'SeO4--': {'mu0': -441.41, 'coef0': -53.97}, 254 | 'SeO4H-': {'mu0': -452.29, 'coef0': -149.37}, 255 | 'SiF6--': {'mu0': -2199.53, 'coef0': -122.17}, 256 | 'SnCl2': {'mu0': -299.57, 'coef0': -171.54}, 257 | 'SnCl3-': {'mu0': -430.12, 'coef0': -259.41}, 258 | 'Sr++': {'mu0': -557.31, 'coef0': 39.33}, 259 | 'TeO3H2': {'mu0': -318.82, 'coef0': 0.0}, 260 | 'Ti++': {'mu0': -158.99, 'coef0': 0.0}, 261 | 'Ti+++': {'mu0': 214.64, 'coef0': 192.46}, 262 | 'TiO2+': {'mu0': -594.13, 'coef0': 0.0}, 263 | 'Ti+': {'mu0': -32.38, 'coef0': -125.52}, 264 | 'TiCl3': {'mu0': -274.47, 'coef0': -133.89}, 265 | 'TiCl4-': {'mu0': -421.75, 'coef0': -242.67}, 266 | 'U+++': {'mu0': -520.49, 'coef0': 125.52}, 267 | 'U++++': {'mu0': -579.07, 'coef0': 326.35}, 268 | 'UO2++': {'mu0': -986.82, 'coef0': 72.8}, 269 | 'V2+': {'mu0': -217.57, 'coef0': 129.7}, 270 | 'V3+': {'mu0': -242.25, 'coef0': 230.12}, 271 | 'VO+': {'mu0': -401.66, 'coef0': 0.0}, 272 | 'VO++': {'mu0': -446.43, 'coef0': -133.89}, 273 | 'VO2+': {'mu0': -587.02, 'coef0': 42.26}, 274 | 'VO43-': {'mu0': -899.14, 'coef0': 0.0}, 275 | 'VO4H--': {'mu0': -974.87, 'coef0': -16.74}, 276 | 'VO4H2-': {'mu0': -1020.9, 'coef0': -121.34}, 277 | 'V2O7----': {'mu0': -1719.62, 'coef0': 0.0}, 278 | 'V2O7H---': {'mu0': -1792.43, 'coef0': 0.0}, 279 | 'V2O7H3-': {'mu0': -1863.97, 'coef0': 0.0}, 280 | 'V10O28H2-----': {'mu0': -8096.04, 'coef0': 0.0}, 281 | 'V10O28H2----': {'mu0': -8116.96, 'coef0': 0.0}, 282 | 'Xe': {'mu0': 13.39, 'coef0': -65.69}, 283 | 'Zn++': {'mu0': -147.03, 'coef0': 112.13}, 284 | 'Zn(CN)4--': {'mu0': 446.85, 'coef0': -225.94}, 285 | 'ZnCl2': {'mu0': -403.76, 'coef0': 0.0}, 286 | 'ZnCl4--': {'mu0': -666.09, 'coef0': 0.0}, 287 | 'Zn(OH)2': {'mu0': -522.79, 'coef0': 0.0}, 288 | 'Zn(OH)3-': {'mu0': -694.33, 'coef0': 0.0}, 289 | 'Zn(OH)4--': {'mu0': -858.68, 'coef0': 0.0}} -------------------------------------------------------------------------------- /pyequion2/datamods/density_solids.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #Scrapped from 4 | #https://www.engineeringtoolbox.com/mineral-density-d_1555.html 5 | 6 | densities = \ 7 | {'Acanthite': 7200.0, #kg/m^3 8 | 'Acmite': 3520.0, 9 | 'Actinolite': 3040.0, 10 | 'Alabandite': 4000.0, 11 | 'Alamandine': 4090.0, 12 | 'Albite': 2620.0, 13 | 'Allanite': 3300.0, 14 | 'Allemontite': 6150.0, 15 | 'Allophane': 1900.0, 16 | 'Altaite': 8140.000000000001, 17 | 'Alunite': 2590.0, 18 | 'Amblygonite': 2980.0, 19 | 'Analcime': 2300.0, 20 | 'Anatese': 3900.0, 21 | 'Andalusite': 3150.0, 22 | 'Andesine': 2670.0, 23 | 'Andradite': 3700.0, 24 | 'Anglesite': 6300.0, 25 | 'Ankerite': 3050.0, 26 | 'Annebergite': 3050.0, 27 | 'Anorthite': 2730.0, 28 | 'Anorthoclase': 2580.0, 29 | 'Anthophyllite': 2850.0, 30 | 'Anthydrite': 2970.0, 31 | 'Antimony': 6660.0, 32 | 'Antlerite': 3900.0, 33 | 'Apatite': 3190.0, 34 | 'Apidote': 3300.0, 35 | 'Apophyllite': 2340.0, 36 | 'Aragonite': 2930.0, 37 | 'Arfvedsonite': 3440.0, 38 | 'Argenite': 7300.0, 39 | 'Arsenic': 5700.0, 40 | 'Arsenopyrite': 6070.0, 41 | 'Atacamite': 3760.0, 42 | 'Augite': 3400.0, 43 | 'Aurichalcite': 3640.0, 44 | 'Autonite': 3150.0, 45 | 'Awaruite': 8000.0, 46 | 'Axinite': 3280.0, 47 | 'Azurite': 3830.0, 48 | 'Barite': 4480.0, 49 | 'Bastnaesite': 4950.0, 50 | 'Beidellite': 2150.0, 51 | 'Beryl': 2630.0, 52 | 'Biotite': 2800.0, 53 | 'Bismite': 8500.0, 54 | 'Bismuth': 9750.0, 55 | 'Boehmite': 3030.0, 56 | 'Boracite': 2900.0, 57 | 'Borax': 1710.0, 58 | 'Bornite': 5090.0, 59 | 'Boulangerite': 5700.0, 60 | 'Brannerite': 4500.0, 61 | 'Braunite': 4760.0, 62 | 'Bravoite': 5010.0, 63 | 'Brochantite': 3970.0, 64 | 'Bromargyrite': 5800.0, 65 | 'Bronzite': 3200.0, 66 | 'Brookite': 4110.0, 67 | 'Brucite': 2390.0, 68 | 'Bytownite': 2710.0, 69 | 'Calcite': 2710.0, 70 | 'Calomel': 6450.0, 71 | 'Calvarite': 9040.0, 72 | 'Cancrinite': 2450.0, 73 | 'Carnallite': 1600.0, 74 | 'Carnotite': 3700.0, 75 | 'Cassiterite': 6900.0, 76 | 'Celestite': 3950.0, 77 | 'Celsian': 3250.0, 78 | 'Cerargyrite': 5550.0, 79 | 'Cerussite': 6580.0, 80 | 'Cervanite': 6500.0, 81 | 'Chabazite': 2090.0, 82 | 'Chalcanthite': 2210.0, 83 | 'Chalcocite': 5500.0, 84 | 'Chalcopyrite': 4190.0, 85 | 'Chlorite': 2420.0, 86 | 'Chloritoid': 3510.0, 87 | 'Chondrodite': 3150.0, 88 | 'Chromite': 4500.0, 89 | 'Chrysoberyl': 3500.0, 90 | 'Chrysocolla': 2000.0, 91 | 'Cinnibar': 8100.0, 92 | 'Clinochlore': 2650.0, 93 | 'Clinoclase': 4290.0, 94 | 'Clinoenstatite': 3400.0, 95 | 'Clinoferrosilite': 4100.0, 96 | 'Clinohumite': 3260.0, 97 | 'Clinzoite': 3340.0, 98 | 'Cobaltite': 6330.0, 99 | 'Colemanite': 2420.0, 100 | 'Columbite': 5300.0, 101 | 'Copper': 8940.0, 102 | 'Cordierite': 2550.0, 103 | 'Corundum': 4050.0, 104 | 'Coveillite': 4680.0, 105 | 'Cristobalite': 2270.0, 106 | 'Crocoite': 6000.0, 107 | 'Cruolite': 2970.0, 108 | 'Cubanite': 4700.0, 109 | 'Cummingtonite': 3350.0, 110 | 'Cuprite': 6100.0, 111 | 'Danburite': 2990.0, 112 | 'Datolite': 2900.0, 113 | 'Diamond': 3510.0, 114 | 'Diaspore': 3400.0, 115 | 'Dickite': 2600.0, 116 | 'Digenite': 5600.0, 117 | 'Diopside': 3400.0, 118 | 'Dioptase': 3310.0, 119 | 'Dolmite': 2840.0, 120 | 'Dolomite': 2840.0, 121 | 'Dumortierite': 3340.0, 122 | 'Edenite': 3020.0, 123 | 'Embolite': 5600.0, 124 | 'Enargite': 4450.0, 125 | 'Enstatite': 3200.0, 126 | 'Erythrite': 3120.0, 127 | 'Espomite': 1670.0, 128 | 'Euclase': 3040.0, 129 | 'Euxenite': 4840.0, 130 | 'Fayalite': 4390.0, 131 | 'Feberite': 7450.0, 132 | 'Fergusonite': 4500.0, 133 | 'Ferrimolybdite': 4000.0, 134 | 'Ferrosilite': 3950.0, 135 | 'Fluorite': 3130.0, 136 | 'Forsterite': 3270.0, 137 | 'Franklinite': 5140.0, 138 | 'Gadolinite': 4000.0, 139 | 'Gahnite': 4000.0, 140 | 'Galaxite': 4230.0, 141 | 'Galena': 7200.0, 142 | 'Garnierite': 2410.0, 143 | 'Gaylussite': 1960.0, 144 | 'Geocronite': 6400.0, 145 | 'Gersdorffite': 6110.0, 146 | 'Gibbsite': 2340.0, 147 | 'Glauberite': 2770.0, 148 | 'Glauconite': 2400.0, 149 | 'Glaucophane': 3070.0, 150 | 'Gmelinite': 2090.0, 151 | 'Goethite': 3300.0, 152 | 'Gold': 19320.0, 153 | 'Goslarite': 2000.0, 154 | 'Graphite': 2160.0, 155 | 'Greenocktite': 3980.0, 156 | 'Grossularite': 3420.0, 157 | 'Gypsum': 2300.0, 158 | 'Halite': 2170.0, 159 | 'Halloysite': 2000.0, 160 | 'Harmotome': 2460.0, 161 | 'Hastingsite': 3170.0, 162 | 'Hausmanite': 4760.0, 163 | 'Hauynite': 2450.0, 164 | 'Hectorite': 2000.0, 165 | 'Hedenbergite': 3550.0, 166 | 'Helvite': 3260.0, 167 | 'Hemimorphite': 3450.0, 168 | 'Hercynite': 3950.0, 169 | 'Hermatite': 5300.0, 170 | 'Hessite': 7200.0, 171 | 'Heulandite': 2200.0, 172 | 'Hornblende': 3000.0, 173 | 'Huebnerite': 7150.0, 174 | 'Humite': 3150.0, 175 | 'Hyalophane': 2810.0, 176 | 'Hydrozincite': 3200.0, 177 | 'Hypersthene': 3200.0, 178 | 'Ice': 990.0, 179 | 'Iddingsite': 2500.0, 180 | 'Idocrase': 3400.0, 181 | 'Illemenite': 4720.0, 182 | 'Illite': 2600.0, 183 | 'Ilvaite': 4010.0, 184 | 'Iodobromite': 5700.0, 185 | 'Iodyrite': 5600.0, 186 | 'Irdosmine': 19300.0, 187 | 'Iridium': 22700.0, 188 | 'Jacobsite': 4750.0, 189 | 'Jadeite': 3300.0, 190 | 'Jamesonite': 5560.0, 191 | 'Jarosite': 2900.0, 192 | 'Kainite': 2100.0, 193 | 'Kaliophilite': 2580.0, 194 | 'Kaolinite': 2600.0, 195 | 'Kernite': 1910.0, 196 | 'Krennerite': 8530.0, 197 | 'Kyanite': 3610.0, 198 | 'Langbeinite': 2830.0, 199 | 'Larsenite': 5900.0, 200 | 'Laumonite': 2290.0, 201 | 'Lawsonite': 3090.0, 202 | 'Lazulite': 3050.0, 203 | 'Lazurite': 2400.0, 204 | 'Lechatelierite': 2500.0, 205 | 'Lepidocrocite': 4000.0, 206 | 'Lepidolite': 2840.0, 207 | 'Leucite': 2470.0, 208 | 'Liebthenite': 3800.0, 209 | 'Limonite': 3300.0, 210 | 'Linarite': 5400.0, 211 | 'Linneaeite': 4800.0, 212 | 'Lithiophilite': 3340.0, 213 | 'Loellingite': 7100.0, 214 | 'Magnesite': 3000.0, 215 | 'Magnetite': 5150.0, 216 | 'Malachite': 3600.0, 217 | 'Manganite': 4340.0, 218 | 'Manganosite': 5180.0, 219 | 'Marcasite': 4890.0, 220 | 'Margarite': 3030.0, 221 | 'Marialite': 2560.0, 222 | 'Meionite': 2690.0, 223 | 'Melanterite': 1890.0, 224 | 'Melilite': 2950.0, 225 | 'Menaghinite': 6380.0, 226 | 'Miargyrite': 5190.0, 227 | 'Microcline': 2560.0, 228 | 'Microlite': 4200.0, 229 | 'Millerite': 5500.0, 230 | 'Mimetite': 7170.0, 231 | 'Minium': 8200.0, 232 | 'Molybdenite': 5500.0, 233 | 'Monazite': 4800.0, 234 | 'Monticellite': 3200.0, 235 | 'Montmotillonite': 2000.0, 236 | 'Mullite': 3050.0, 237 | 'Musscovite': 2820.0, 238 | 'Nacrite': 2600.0, 239 | 'Nagyagite': 7500.0, 240 | 'Natrolite': 2250.0, 241 | 'Nephheline': 2550.0, 242 | 'Ni skutterudite': 6500.0, 243 | 'Niccolite': 7790.0, 244 | 'Nitre': 2100.0, 245 | 'Nontronite': 2300.0, 246 | 'Norbergite': 3150.0, 247 | 'Noselite': 2340.0, 248 | 'Oligoclase': 2650.0, 249 | 'Opal': 2090.0, 250 | 'Orpiment': 3520.0, 251 | 'Orthoclase': 2560.0, 252 | 'Ottrelite': 3520.0, 253 | 'Palladium': 11550.0, 254 | 'Paragonite': 2780.0, 255 | 'Pargasite': 3120.0, 256 | 'Pearceite': 6130.0, 257 | 'Pectolite': 2860.0, 258 | 'Pentlandite': 4600.0, 259 | 'Perovskite': 4000.0, 260 | 'Petalite': 2420.0, 261 | 'Petzite': 8700.0, 262 | 'Phenaktite': 2980.0, 263 | 'Phillipsite': 2200.0, 264 | 'Phlogopite': 2700.0, 265 | 'Phosgenite': 6000.0, 266 | 'Phosphuranylite': 4100.0, 267 | 'Pigeonite': 3300.0, 268 | 'Plagionite': 5400.0, 269 | 'Platinum': 21450.0, 270 | 'Polucite': 2900.0, 271 | 'Polybasite': 4600.0, 272 | 'Polycrase': 5000.0, 273 | 'Polyhalite': 2770.0, 274 | 'Potash alum': 1750.0, 275 | 'Powellite': 4340.0, 276 | 'Prehnite': 2870.0, 277 | 'Proustite': 5550.0, 278 | 'Pyrargyrite': 5850.0, 279 | 'Pyrite': 5010.0, 280 | 'Pyrochlore': 4200.0, 281 | 'Pyrolusite': 4400.0, 282 | 'Pyromorphite': 6700.0, 283 | 'Pyrope': 3650.0, 284 | 'Pyrophyllite': 2840.0, 285 | 'Pyrrhotite': 4610.0, 286 | 'Quartz': 2620.0, 287 | 'Rammelsbergite': 7100.0, 288 | 'Realgar': 3560.0, 289 | 'Rhodochrosite': 3690.0, 290 | 'Rhodonite': 3500.0, 291 | 'Riebeckite': 3400.0, 292 | 'Roscoelite': 2970.0, 293 | 'Rutile': 4250.0, 294 | 'Samarskite': 5600.0, 295 | 'Sanidine': 2520.0, 296 | 'Saponite': 2300.0, 297 | 'Scapolite': 2660.0, 298 | 'Scheelite': 6010.0, 299 | 'Scolecite': 2160.0, 300 | 'Scorodite': 3200.0, 301 | 'Scorzalite': 3270.0, 302 | 'Semseyite': 5800.0, 303 | 'Sepiolite': 2000.0, 304 | 'Serpentine': 2530.0, 305 | 'Siderite': 3960.0, 306 | 'Siegenite': 4900.0, 307 | 'Sillimanite': 3240.0, 308 | 'Silver': 10500.0, 309 | 'Sklodowskite': 3540.0, 310 | 'Skutterudite': 6100.0, 311 | 'Smithsonite': 4450.0, 312 | 'Soda nitre': 2260.0, 313 | 'Sodalite': 2290.0, 314 | 'Sperrylite': 10580.0, 315 | 'Spessartine': 4180.0, 316 | 'Sphalerite': 4050.0, 317 | 'Sphene': 3480.0, 318 | 'Spinel group': 3570.0, 319 | 'Spodumene': 3150.0, 320 | 'Stannite': 4300.0, 321 | 'Staurolite': 3710.0, 322 | 'Stephanite': 6250.0, 323 | 'Sternbergite': 4220.0, 324 | 'Stibnite': 4630.0, 325 | 'Stilbite': 2150.0, 326 | 'Stillwellite': 4610.0, 327 | 'Stolzite': 7900.0, 328 | 'Stromeyerite': 6000.0, 329 | 'Strontianite': 3780.0, 330 | 'Sulphur': 2060.0, 331 | 'Sylvanite': 7900.0, 332 | 'Sylvite': 1990.0, 333 | 'Talc': 2750.0, 334 | 'Tantalite': 6200.0, 335 | 'Tennantite': 4600.0, 336 | 'Tenorite': 6500.0, 337 | 'Tephroite': 4110.0, 338 | 'Tetrahedrite': 4600.0, 339 | 'Thenardite': 2680.0, 340 | 'Thomsonite': 2340.0, 341 | 'Thorianite': 10000.0, 342 | 'Thorite': 4000.0, 343 | 'Tin': 7280.0, 344 | 'Topaz': 3550.0, 345 | 'Torbernite': 3200.0, 346 | 'Tourmaline': 3000.0, 347 | 'Tremolite': 2900.0, 348 | 'Tridymite': 2280.0, 349 | 'Triphylite': 3400.0, 350 | 'Troilite': 4610.0, 351 | 'Trona': 2130.0, 352 | 'Tungstite': 5500.0, 353 | 'Turquoise': 2600.0, 354 | 'Tyuyamunite': 3300.0, 355 | 'Ulexite': 1950.0, 356 | 'Uraninite': 6500.0, 357 | 'Uranophane': 3900.0, 358 | 'Uvarovite': 3400.0, 359 | 'Vanadinite': 6800.0, 360 | 'Varicite': 2500.0, 361 | 'Vaterite': 2710.0, #Assume now same as calcite 362 | 'Vermiculite': 2300.0, 363 | 'Violarite': 4500.0, 364 | 'Vivianite': 2650.0, 365 | 'Wavellite': 2340.0, 366 | 'Willemite': 3900.0, 367 | 'Witherite': 4300.0, 368 | 'Wolframite': 7100.0, 369 | 'Wollastonite': 2840.0, 370 | 'Wulfenite': 6500.0, 371 | 'Wurtzite': 4030.0000000000005, 372 | 'Xenotime': 4400.0, 373 | 'Zincite': 5430.0, 374 | 'Zinkenite': 5120.0, 375 | 'Zircon': 4650.0, 376 | 'Zoisite': 3300.0} -------------------------------------------------------------------------------- /pyequion2/datamods/pitzer.txt: -------------------------------------------------------------------------------- 1 | PITZER 2 | -B0 3 | B(OH)4- K+ 0.035 4 | B(OH)4- Na+ -0.0427 5 | B3O3(OH)4- K+ -0.13 6 | B3O3(OH)4- Na+ -0.056 7 | B4O5(OH)4-2 K+ -0.022 8 | B4O5(OH)4-2 Na+ -0.11 9 | Ba+2 Br- 0.31455 0 0 -0.33825E-3 10 | Ba+2 Cl- 0.5268 0 0 0 0 4.75e4 # ref. 3 11 | Ba+2 OH- 0.17175 12 | Br- H+ 0.1960 0 0 -2.049E-4 13 | Br- K+ 0.0569 0 0 7.39E-4 14 | Br- Li+ 0.1748 0 0 -1.819E-4 15 | Br- Mg+2 0.4327 0 0 -5.625E-5 16 | Br- Na+ 0.0973 0 0 7.692E-4 17 | Br- Sr+2 0.331125 0 0 -0.32775E-3 18 | Ca+2 Br- 0.3816 0 0 -5.2275E-4 19 | Ca+2 Cl- 0.3159 0 0 -3.27e-4 1.4e-7 # ref. 3 20 | Ca+2 HCO3- 0.4 21 | Ca+2 HSO4- 0.2145 22 | Ca+2 OH- -0.1747 23 | Ca+2 SO4-2 0 # ref. 3 24 | CaB(OH)4+ Cl- 0.12 25 | Cl- Fe+2 0.335925 26 | Cl- H+ 0.1775 0 0 -3.081E-4 27 | Cl- K+ 0.04808 -758.48 -4.7062 0.010072 -3.7599e-6 # ref. 3 28 | Cl- Li+ 0.1494 0 0 -1.685E-4 29 | Cl- Mg+2 0.351 0 0 -9.32e-4 5.94e-7 # ref. 3 30 | Cl- MgB(OH)4+ 0.16 31 | Cl- MgOH+ -0.1 32 | Cl- Mn+2 0.327225 33 | Cl- Na+ 7.534e-2 9598.4 35.48 -5.8731e-2 1.798e-5 -5e5 # ref. 3 34 | Cl- Sr+2 0.2858 0 0 0.717E-3 35 | CO3-2 K+ 0.1488 0 0 1.788E-3 36 | CO3-2 Na+ 0.0399 0 0 1.79E-3 37 | Fe+2 HSO4- 0.4273 38 | Fe+2 SO4-2 0.2568 39 | H+ HSO4- 0.2065 40 | H+ SO4-2 0.0298 41 | HCO3- K+ 0.0296 0 0 0.996E-3 42 | HCO3- Mg+2 0.329 43 | HCO3- Na+ -0.018 # ref. 3 + new -analytic for calcite 44 | HCO3- Sr+2 0.12 45 | HSO4- K+ -0.0003 46 | HSO4- Mg+2 0.4746 47 | HSO4- Na+ 0.0454 48 | K+ OH- 0.1298 49 | K+ SO4-2 3.17e-2 0 0 9.28e-4 # ref. 3 50 | Li+ OH- 0.015 51 | Li+ SO4-2 0.136275 0 0 0.5055E-3 52 | Mg+2 SO4-2 0.2135 -951 0 -2.34e-2 2.28e-5 # ref. 3 53 | Mn+2 SO4-2 0.2065 54 | Na+ OH- 0.0864 0 0 7.00E-4 55 | Na+ SO4-2 2.73e-2 0 -5.8 9.89e-3 0 -1.563e5 # ref. 3 56 | SO4-2 Sr+2 0.200 0 0 -2.9E-3 57 | -B1 58 | B(OH)4- K+ 0.14 59 | B(OH)4- Na+ 0.089 60 | B3O3(OH)4- Na+ -0.910 61 | B4O5(OH)4-2 Na+ -0.40 62 | Ba+2 Br- 1.56975 0 0 6.78E-3 63 | Ba+2 Cl- 0.687 0 0 1.417e-2 # ref. 3 64 | Ba+2 OH- 1.2 65 | Br- H+ 0.3564 0 0 4.467E-4 66 | Br- K+ 0.2212 0 0 17.40E-4 67 | Br- Li+ 0.2547 0 0 6.636E-4 68 | Br- Mg+2 1.753 0 0 3.8625E-3 69 | Br- Na+ 0.2791 0 0 10.79E-4 70 | Br- Sr+2 1.7115 0 0 6.5325E-3 71 | Ca+2 Br- 1.613 0 0 6.0375E-3 72 | Ca+2 Cl- 1.614 0 0 7.63e-3 -8.19e-7 # ref. 3 73 | Ca+2 HCO3- 2.977 # ref. 3 + new -analytic for calcite 74 | Ca+2 HSO4- 2.53 75 | Ca+2 OH- -0.2303 76 | Ca+2 SO4-2 3.546 0 0 5.77e-3 # ref. 3 77 | Cl- Fe+2 1.53225 78 | Cl- H+ 0.2945 0 0 1.419E-4 79 | Cl- K+ 0.2168 0 -6.895 2.262e-2 -9.293e-6 -1e5 # ref. 3 80 | Cl- Li+ 0.3074 0 0 5.366E-4 81 | Cl- Mg+2 1.65 0 0 -1.09e-2 2.60e-5 # ref. 3 82 | Cl- MgOH+ 1.658 83 | Cl- Mn+2 1.55025 84 | Cl- Na+ 0.2769 1.377e4 46.8 -6.9512e-2 2e-5 -7.4823e5 # ref. 3 85 | Cl- Sr+2 1.667 0 0 2.8425E-3 86 | CO3-2 K+ 1.43 0 0 2.051E-3 87 | CO3-2 Na+ 1.389 0 0 2.05E-3 88 | Fe+2 HSO4- 3.48 89 | Fe+2 SO4-2 3.063 90 | H+ HSO4- 0.5556 91 | HCO3- K+ 0.25 0 0 1.104E-3 # ref. 3 92 | HCO3- Mg+2 0.6072 93 | HCO3- Na+ 0 # ref. 3 + new -analytic for calcite 94 | HSO4- K+ 0.1735 95 | HSO4- Mg+2 1.729 96 | HSO4- Na+ 0.398 97 | K+ OH- 0.32 98 | K+ SO4-2 0.756 -1.514e4 -80.3 0.1091 # ref. 3 99 | Li+ OH- 0.14 100 | Li+ SO4-2 1.2705 0 0 1.41E-3 101 | Mg+2 SO4-2 3.367 -5.78e3 0 -1.48e-1 1.576e-4 # ref. 3 102 | Mn+2 SO4-2 2.9511 103 | Na+ OH- 0.253 0 0 1.34E-4 104 | Na+ SO4-2 0.956 2.663e3 0 1.158e-2 0 -3.194e5 # ref. 3 105 | SO4-2 Sr+2 3.1973 0 0 27e-3 106 | -B2 107 | Ca+2 Cl- -1.13 0 0 -0.0476 # ref. 3 108 | Ca+2 OH- -5.72 109 | Ca+2 SO4-2 -59.3 0 0 -0.443 -3.96e-6 # ref. 3 110 | Fe+2 SO4-2 -42.0 111 | HCO3- Na+ 8.22 0 0 -0.049 # ref. 3 + new -analytic for calcite 112 | Mg+2 SO4-2 -32.45 0 -3.236e3 21.812 -1.8859e-2 # ref. 3 113 | Mn+2 SO4-2 -40.0 114 | SO4-2 Sr+2 -54.24 0 0 -0.42 115 | -C0 116 | B(OH)4- Na+ 0.0114 117 | Ba+2 Br- -0.0159576 118 | Ba+2 Cl- -0.143 -114.5 # ref. 3 119 | Br- Ca+2 -0.00257 120 | Br- H+ 0.00827 0 0 -5.685E-5 121 | Br- K+ -0.00180 0 0 -7.004E-5 122 | Br- Li+ 0.0053 0 0 -2.813E-5 123 | Br- Mg+2 0.00312 124 | Br- Na+ 0.00116 0 0 -9.30E-5 125 | Br- Sr+2 0.00122506 126 | Ca+2 Cl- 1.4e-4 -57 -0.098 -7.83e-4 7.18e-7 # ref. 3 127 | Ca+2 SO4-2 0.114 # ref. 3 128 | Cl- Fe+2 -0.00860725 129 | Cl- H+ 0.0008 0 0 6.213E-5 130 | Cl- K+ -7.88e-4 91.27 0.58643 -1.298e-3 4.9567e-7 # ref. 3 131 | Cl- Li+ 0.00359 0 0 -4.520E-5 132 | Cl- Mg+2 0.00651 0 0 -2.50e-4 2.418e-7 # ref. 3 133 | Cl- Mn+2 -0.0204972 134 | Cl- Na+ 1.48e-3 -120.5 -0.2081 0 1.166e-7 11121 # ref. 3 135 | Cl- Sr+2 -0.00130 136 | CO3-2 K+ -0.0015 137 | CO3-2 Na+ 0.0044 138 | Fe+2 SO4-2 0.0209 139 | H+ SO4-2 0.0438 140 | HCO3- K+ -0.008 141 | K+ OH- 0.0041 142 | K+ SO4-2 8.18e-3 -625 -3.30 4.06e-3 # ref. 3 143 | Li+ SO4-2 -0.00399338 0 0 -2.33345e-4 144 | Mg+2 SO4-2 2.875e-2 0 -2.084 1.1428e-2 -8.228e-6 # ref. 3 145 | Mn+2 SO4-2 0.01636 146 | Na+ OH- 0.0044 0 0 -18.94E-5 147 | Na+ SO4-2 3.418e-3 -384 0 -8.451e-4 0 5.177e4 # ref. 3 148 | -THETA 149 | B(OH)4- Cl- -0.065 150 | B(OH)4- SO4-2 -0.012 151 | B3O3(OH)4- Cl- 0.12 152 | B3O3(OH)4- HCO3- -0.10 153 | B3O3(OH)4- SO4-2 0.10 154 | B4O5(OH)4-2 Cl- 0.074 155 | B4O5(OH)4-2 HCO3- -0.087 156 | B4O5(OH)4-2 SO4-2 0.12 157 | Ba+2 Na+ 0.07 # ref. 3 158 | Br- OH- -0.065 159 | Ca+2 H+ 0.092 160 | Ca+2 K+ -5.35e-3 0 0 3.08e-4 # ref. 3 161 | Ca+2 Mg+2 0.007 162 | Ca+2 Na+ 9.22e-2 0 0 -4.29e-4 1.21e-6 # ref. 3 163 | Cl- CO3-2 -0.02 164 | Cl- HCO3- 0.03 165 | Cl- HSO4- -0.006 166 | Cl- OH- -0.05 167 | Cl- SO4-2 0.03 # ref. 3 168 | CO3-2 OH- 0.1 169 | CO3-2 SO4-2 0.02 170 | H+ K+ 0.005 171 | H+ Mg+2 0.1 172 | H+ Na+ 0.036 173 | HCO3- CO3-2 -0.04 174 | HCO3- SO4-2 0.01 175 | K+ Na+ -0.012 176 | Mg+2 Na+ 0.07 177 | Na+ Sr+2 0.051 178 | OH- SO4-2 -0.013 179 | -LAMDA 180 | B(OH)3 Cl- 0.091 181 | B(OH)3 K+ -0.14 182 | B(OH)3 Na+ -0.097 183 | B(OH)3 SO4-2 0.018 184 | B3O3(OH)4- B(OH)3 -0.20 185 | Ca+2 CO2 0.183 186 | Ca+2 H4SiO4 0.238 # ref. 3 187 | Cl- CO2 -0.005 188 | CO2 CO2 -1.34e-2 348 0.803 # new VM("CO2"), CO2 solubilities at high P, 0 - 150�C 189 | CO2 HSO4- -0.003 190 | CO2 K+ 0.051 191 | CO2 Mg+2 0.183 192 | CO2 Na+ 0.085 193 | CO2 SO4-2 0.075 # Rumpf and Maurer, 1993. 194 | H4SiO4 K+ 0.0298 # ref. 3 195 | H4SiO4 Li+ 0.143 # ref. 3 196 | H4SiO4 Mg+2 0.238 -1788 -9.023 0.0103 # ref. 3 197 | H4SiO4 Na+ 0.0566 75.3 0.115 # ref. 3 198 | H4SiO4 SO4-2 -0.085 0 0.28 -8.25e-4 # ref. 3 199 | -ZETA 200 | B(OH)3 Cl- H+ -0.0102 201 | B(OH)3 Na+ SO4-2 0.046 202 | Cl- H4SiO4 K+ -0.0153 # ref. 3 203 | Cl- H4SiO4 Li+ -0.0196 # ref. 3 204 | CO2 Na+ SO4-2 -0.015 205 | -PSI 206 | B(OH)4- Cl- Na+ -0.0073 207 | B3O3(OH)4- Cl- Na+ -0.024 208 | B4O5(OH)4-2 Cl- Na+ 0.026 209 | Br- K+ Na+ -0.0022 210 | Br- K+ OH- -0.014 211 | Br- Na+ H+ -0.012 212 | Br- Na+ OH- -0.018 213 | Ca+2 Cl- H+ -0.015 214 | Ca+2 Cl- K+ -0.025 215 | Ca+2 Cl- Mg+2 -0.012 216 | Ca+2 Cl- Na+ -1.48e-2 0 0 -5.2e-6 # ref. 3 217 | Ca+2 Cl- OH- -0.025 218 | Ca+2 Cl- SO4-2 -0.122 0 0 -1.21e-3 # ref. 3 219 | Ca+2 K+ SO4-2 -0.0365 # ref. 3 220 | Ca+2 Mg+2 SO4-2 0.024 221 | Ca+2 Na+ SO4-2 -0.055 17.2 # ref. 3 222 | Cl- Br- K+ 0 223 | Cl- CO3-2 K+ 0.004 224 | Cl- CO3-2 Na+ 0.0085 225 | Cl- H+ K+ -0.011 226 | Cl- H+ Mg+2 -0.011 227 | Cl- H+ Na+ -0.004 228 | Cl- HCO3- Mg+2 -0.096 229 | Cl- HCO3- Na+ 0 # ref. 3 + new -analytic for calcite 230 | Cl- HSO4- H+ 0.013 231 | Cl- HSO4- Na+ -0.006 232 | Cl- K+ Mg+2 -0.022 -14.27 # ref. 3 233 | Cl- K+ Na+ -0.0015 0 0 1.8e-5 # ref. 3 234 | Cl- K+ OH- -0.006 235 | Cl- K+ SO4-2 -1e-3 # ref. 3 236 | Cl- Mg+2 MgOH+ 0.028 237 | Cl- Mg+2 Na+ -0.012 -9.51 # ref. 3 238 | Cl- Mg+2 SO4-2 -0.008 32.63 # ref. 3 239 | Cl- Na+ OH- -0.006 240 | Cl- Na+ SO4-2 0 # ref. 3 241 | Cl- Na+ Sr+2 -0.0021 242 | CO3-2 HCO3- K+ 0.012 243 | CO3-2 HCO3- Na+ 0.002 244 | CO3-2 K+ Na+ 0.003 245 | CO3-2 K+ OH- -0.01 246 | CO3-2 K+ SO4-2 -0.009 247 | CO3-2 Na+ OH- -0.017 248 | CO3-2 Na+ SO4-2 -0.005 249 | H+ HSO4- K+ -0.0265 250 | H+ HSO4- Mg+2 -0.0178 251 | H+ HSO4- Na+ -0.0129 252 | H+ K+ Br- -0.021 253 | H+ K+ SO4-2 0.197 254 | HCO3- K+ Na+ -0.003 255 | HCO3- Mg+2 SO4-2 -0.161 256 | HCO3- Na+ SO4-2 -0.005 257 | HSO4- K+ SO4-2 -0.0677 258 | HSO4- Mg+2 SO4-2 -0.0425 259 | HSO4- Na+ SO4-2 -0.0094 260 | K+ Mg+2 SO4-2 -0.048 261 | K+ Na+ SO4-2 -0.010 262 | K+ OH- SO4-2 -0.050 263 | Mg+2 Na+ SO4-2 -0.015 264 | Na+ OH- SO4-2 -0.009 -------------------------------------------------------------------------------- /pyequion2/datamods/pitzer_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | pitzer_data = \ 4 | r"""PITZER 5 | -B0 6 | B(OH)4- K+ 0.035 7 | B(OH)4- Na+ -0.0427 8 | B3O3(OH)4- K+ -0.13 9 | B3O3(OH)4- Na+ -0.056 10 | B4O5(OH)4-2 K+ -0.022 11 | B4O5(OH)4-2 Na+ -0.11 12 | Ba+2 Br- 0.31455 0 0 -0.33825E-3 13 | Ba+2 Cl- 0.5268 0 0 0 0 4.75e4 # ref. 3 14 | Ba+2 OH- 0.17175 15 | Br- H+ 0.1960 0 0 -2.049E-4 16 | Br- K+ 0.0569 0 0 7.39E-4 17 | Br- Li+ 0.1748 0 0 -1.819E-4 18 | Br- Mg+2 0.4327 0 0 -5.625E-5 19 | Br- Na+ 0.0973 0 0 7.692E-4 20 | Br- Sr+2 0.331125 0 0 -0.32775E-3 21 | Ca+2 Br- 0.3816 0 0 -5.2275E-4 22 | Ca+2 Cl- 0.3159 0 0 -3.27e-4 1.4e-7 # ref. 3 23 | Ca+2 HCO3- 0.4 24 | Ca+2 HSO4- 0.2145 25 | Ca+2 OH- -0.1747 26 | Ca+2 SO4-2 0 # ref. 3 27 | CaB(OH)4+ Cl- 0.12 28 | Cl- Fe+2 0.335925 29 | Cl- H+ 0.1775 0 0 -3.081E-4 30 | Cl- K+ 0.04808 -758.48 -4.7062 0.010072 -3.7599e-6 # ref. 3 31 | Cl- Li+ 0.1494 0 0 -1.685E-4 32 | Cl- Mg+2 0.351 0 0 -9.32e-4 5.94e-7 # ref. 3 33 | Cl- MgB(OH)4+ 0.16 34 | Cl- MgOH+ -0.1 35 | Cl- Mn+2 0.327225 36 | Cl- Na+ 7.534e-2 9598.4 35.48 -5.8731e-2 1.798e-5 -5e5 # ref. 3 37 | Cl- Sr+2 0.2858 0 0 0.717E-3 38 | CO3-2 K+ 0.1488 0 0 1.788E-3 39 | CO3-2 Na+ 0.0399 0 0 1.79E-3 40 | Fe+2 HSO4- 0.4273 41 | Fe+2 SO4-2 0.2568 42 | H+ HSO4- 0.2065 43 | H+ SO4-2 0.0298 44 | HCO3- K+ 0.0296 0 0 0.996E-3 45 | HCO3- Mg+2 0.329 46 | HCO3- Na+ -0.018 # ref. 3 + new -analytic for calcite 47 | HCO3- Sr+2 0.12 48 | HSO4- K+ -0.0003 49 | HSO4- Mg+2 0.4746 50 | HSO4- Na+ 0.0454 51 | K+ OH- 0.1298 52 | K+ SO4-2 3.17e-2 0 0 9.28e-4 # ref. 3 53 | Li+ OH- 0.015 54 | Li+ SO4-2 0.136275 0 0 0.5055E-3 55 | Mg+2 SO4-2 0.2135 -951 0 -2.34e-2 2.28e-5 # ref. 3 56 | Mn+2 SO4-2 0.2065 57 | Na+ OH- 0.0864 0 0 7.00E-4 58 | Na+ SO4-2 2.73e-2 0 -5.8 9.89e-3 0 -1.563e5 # ref. 3 59 | SO4-2 Sr+2 0.200 0 0 -2.9E-3 60 | -B1 61 | B(OH)4- K+ 0.14 62 | B(OH)4- Na+ 0.089 63 | B3O3(OH)4- Na+ -0.910 64 | B4O5(OH)4-2 Na+ -0.40 65 | Ba+2 Br- 1.56975 0 0 6.78E-3 66 | Ba+2 Cl- 0.687 0 0 1.417e-2 # ref. 3 67 | Ba+2 OH- 1.2 68 | Br- H+ 0.3564 0 0 4.467E-4 69 | Br- K+ 0.2212 0 0 17.40E-4 70 | Br- Li+ 0.2547 0 0 6.636E-4 71 | Br- Mg+2 1.753 0 0 3.8625E-3 72 | Br- Na+ 0.2791 0 0 10.79E-4 73 | Br- Sr+2 1.7115 0 0 6.5325E-3 74 | Ca+2 Br- 1.613 0 0 6.0375E-3 75 | Ca+2 Cl- 1.614 0 0 7.63e-3 -8.19e-7 # ref. 3 76 | Ca+2 HCO3- 2.977 # ref. 3 + new -analytic for calcite 77 | Ca+2 HSO4- 2.53 78 | Ca+2 OH- -0.2303 79 | Ca+2 SO4-2 3.546 0 0 5.77e-3 # ref. 3 80 | Cl- Fe+2 1.53225 81 | Cl- H+ 0.2945 0 0 1.419E-4 82 | Cl- K+ 0.2168 0 -6.895 2.262e-2 -9.293e-6 -1e5 # ref. 3 83 | Cl- Li+ 0.3074 0 0 5.366E-4 84 | Cl- Mg+2 1.65 0 0 -1.09e-2 2.60e-5 # ref. 3 85 | Cl- MgOH+ 1.658 86 | Cl- Mn+2 1.55025 87 | Cl- Na+ 0.2769 1.377e4 46.8 -6.9512e-2 2e-5 -7.4823e5 # ref. 3 88 | Cl- Sr+2 1.667 0 0 2.8425E-3 89 | CO3-2 K+ 1.43 0 0 2.051E-3 90 | CO3-2 Na+ 1.389 0 0 2.05E-3 91 | Fe+2 HSO4- 3.48 92 | Fe+2 SO4-2 3.063 93 | H+ HSO4- 0.5556 94 | HCO3- K+ 0.25 0 0 1.104E-3 # ref. 3 95 | HCO3- Mg+2 0.6072 96 | HCO3- Na+ 0 # ref. 3 + new -analytic for calcite 97 | HSO4- K+ 0.1735 98 | HSO4- Mg+2 1.729 99 | HSO4- Na+ 0.398 100 | K+ OH- 0.32 101 | K+ SO4-2 0.756 -1.514e4 -80.3 0.1091 # ref. 3 102 | Li+ OH- 0.14 103 | Li+ SO4-2 1.2705 0 0 1.41E-3 104 | Mg+2 SO4-2 3.367 -5.78e3 0 -1.48e-1 1.576e-4 # ref. 3 105 | Mn+2 SO4-2 2.9511 106 | Na+ OH- 0.253 0 0 1.34E-4 107 | Na+ SO4-2 0.956 2.663e3 0 1.158e-2 0 -3.194e5 # ref. 3 108 | SO4-2 Sr+2 3.1973 0 0 27e-3 109 | -B2 110 | Ca+2 Cl- -1.13 0 0 -0.0476 # ref. 3 111 | Ca+2 OH- -5.72 112 | Ca+2 SO4-2 -59.3 0 0 -0.443 -3.96e-6 # ref. 3 113 | Fe+2 SO4-2 -42.0 114 | HCO3- Na+ 8.22 0 0 -0.049 # ref. 3 + new -analytic for calcite 115 | Mg+2 SO4-2 -32.45 0 -3.236e3 21.812 -1.8859e-2 # ref. 3 116 | Mn+2 SO4-2 -40.0 117 | SO4-2 Sr+2 -54.24 0 0 -0.42 118 | -C0 119 | B(OH)4- Na+ 0.0114 120 | Ba+2 Br- -0.0159576 121 | Ba+2 Cl- -0.143 -114.5 # ref. 3 122 | Br- Ca+2 -0.00257 123 | Br- H+ 0.00827 0 0 -5.685E-5 124 | Br- K+ -0.00180 0 0 -7.004E-5 125 | Br- Li+ 0.0053 0 0 -2.813E-5 126 | Br- Mg+2 0.00312 127 | Br- Na+ 0.00116 0 0 -9.30E-5 128 | Br- Sr+2 0.00122506 129 | Ca+2 Cl- 1.4e-4 -57 -0.098 -7.83e-4 7.18e-7 # ref. 3 130 | Ca+2 SO4-2 0.114 # ref. 3 131 | Cl- Fe+2 -0.00860725 132 | Cl- H+ 0.0008 0 0 6.213E-5 133 | Cl- K+ -7.88e-4 91.27 0.58643 -1.298e-3 4.9567e-7 # ref. 3 134 | Cl- Li+ 0.00359 0 0 -4.520E-5 135 | Cl- Mg+2 0.00651 0 0 -2.50e-4 2.418e-7 # ref. 3 136 | Cl- Mn+2 -0.0204972 137 | Cl- Na+ 1.48e-3 -120.5 -0.2081 0 1.166e-7 11121 # ref. 3 138 | Cl- Sr+2 -0.00130 139 | CO3-2 K+ -0.0015 140 | CO3-2 Na+ 0.0044 141 | Fe+2 SO4-2 0.0209 142 | H+ SO4-2 0.0438 143 | HCO3- K+ -0.008 144 | K+ OH- 0.0041 145 | K+ SO4-2 8.18e-3 -625 -3.30 4.06e-3 # ref. 3 146 | Li+ SO4-2 -0.00399338 0 0 -2.33345e-4 147 | Mg+2 SO4-2 2.875e-2 0 -2.084 1.1428e-2 -8.228e-6 # ref. 3 148 | Mn+2 SO4-2 0.01636 149 | Na+ OH- 0.0044 0 0 -18.94E-5 150 | Na+ SO4-2 3.418e-3 -384 0 -8.451e-4 0 5.177e4 # ref. 3 151 | -THETA 152 | B(OH)4- Cl- -0.065 153 | B(OH)4- SO4-2 -0.012 154 | B3O3(OH)4- Cl- 0.12 155 | B3O3(OH)4- HCO3- -0.10 156 | B3O3(OH)4- SO4-2 0.10 157 | B4O5(OH)4-2 Cl- 0.074 158 | B4O5(OH)4-2 HCO3- -0.087 159 | B4O5(OH)4-2 SO4-2 0.12 160 | Ba+2 Na+ 0.07 # ref. 3 161 | Br- OH- -0.065 162 | Ca+2 H+ 0.092 163 | Ca+2 K+ -5.35e-3 0 0 3.08e-4 # ref. 3 164 | Ca+2 Mg+2 0.007 165 | Ca+2 Na+ 9.22e-2 0 0 -4.29e-4 1.21e-6 # ref. 3 166 | Cl- CO3-2 -0.02 167 | Cl- HCO3- 0.03 168 | Cl- HSO4- -0.006 169 | Cl- OH- -0.05 170 | Cl- SO4-2 0.03 # ref. 3 171 | CO3-2 OH- 0.1 172 | CO3-2 SO4-2 0.02 173 | H+ K+ 0.005 174 | H+ Mg+2 0.1 175 | H+ Na+ 0.036 176 | HCO3- CO3-2 -0.04 177 | HCO3- SO4-2 0.01 178 | K+ Na+ -0.012 179 | Mg+2 Na+ 0.07 180 | Na+ Sr+2 0.051 181 | OH- SO4-2 -0.013 182 | -LAMDA 183 | B(OH)3 Cl- 0.091 184 | B(OH)3 K+ -0.14 185 | B(OH)3 Na+ -0.097 186 | B(OH)3 SO4-2 0.018 187 | B3O3(OH)4- B(OH)3 -0.20 188 | Ca+2 CO2 0.183 189 | Ca+2 H4SiO4 0.238 # ref. 3 190 | Cl- CO2 -0.005 191 | CO2 CO2 -1.34e-2 348 0.803 # new VM("CO2"), CO2 solubilities at high P, 0 - 150�C 192 | CO2 HSO4- -0.003 193 | CO2 K+ 0.051 194 | CO2 Mg+2 0.183 195 | CO2 Na+ 0.085 196 | CO2 SO4-2 0.075 # Rumpf and Maurer, 1993. 197 | H4SiO4 K+ 0.0298 # ref. 3 198 | H4SiO4 Li+ 0.143 # ref. 3 199 | H4SiO4 Mg+2 0.238 -1788 -9.023 0.0103 # ref. 3 200 | H4SiO4 Na+ 0.0566 75.3 0.115 # ref. 3 201 | H4SiO4 SO4-2 -0.085 0 0.28 -8.25e-4 # ref. 3 202 | -ZETA 203 | B(OH)3 Cl- H+ -0.0102 204 | B(OH)3 Na+ SO4-2 0.046 205 | Cl- H4SiO4 K+ -0.0153 # ref. 3 206 | Cl- H4SiO4 Li+ -0.0196 # ref. 3 207 | CO2 Na+ SO4-2 -0.015 208 | -PSI 209 | B(OH)4- Cl- Na+ -0.0073 210 | B3O3(OH)4- Cl- Na+ -0.024 211 | B4O5(OH)4-2 Cl- Na+ 0.026 212 | Br- K+ Na+ -0.0022 213 | Br- K+ OH- -0.014 214 | Br- Na+ H+ -0.012 215 | Br- Na+ OH- -0.018 216 | Ca+2 Cl- H+ -0.015 217 | Ca+2 Cl- K+ -0.025 218 | Ca+2 Cl- Mg+2 -0.012 219 | Ca+2 Cl- Na+ -1.48e-2 0 0 -5.2e-6 # ref. 3 220 | Ca+2 Cl- OH- -0.025 221 | Ca+2 Cl- SO4-2 -0.122 0 0 -1.21e-3 # ref. 3 222 | Ca+2 K+ SO4-2 -0.0365 # ref. 3 223 | Ca+2 Mg+2 SO4-2 0.024 224 | Ca+2 Na+ SO4-2 -0.055 17.2 # ref. 3 225 | Cl- Br- K+ 0 226 | Cl- CO3-2 K+ 0.004 227 | Cl- CO3-2 Na+ 0.0085 228 | Cl- H+ K+ -0.011 229 | Cl- H+ Mg+2 -0.011 230 | Cl- H+ Na+ -0.004 231 | Cl- HCO3- Mg+2 -0.096 232 | Cl- HCO3- Na+ 0 # ref. 3 + new -analytic for calcite 233 | Cl- HSO4- H+ 0.013 234 | Cl- HSO4- Na+ -0.006 235 | Cl- K+ Mg+2 -0.022 -14.27 # ref. 3 236 | Cl- K+ Na+ -0.0015 0 0 1.8e-5 # ref. 3 237 | Cl- K+ OH- -0.006 238 | Cl- K+ SO4-2 -1e-3 # ref. 3 239 | Cl- Mg+2 MgOH+ 0.028 240 | Cl- Mg+2 Na+ -0.012 -9.51 # ref. 3 241 | Cl- Mg+2 SO4-2 -0.008 32.63 # ref. 3 242 | Cl- Na+ OH- -0.006 243 | Cl- Na+ SO4-2 0 # ref. 3 244 | Cl- Na+ Sr+2 -0.0021 245 | CO3-2 HCO3- K+ 0.012 246 | CO3-2 HCO3- Na+ 0.002 247 | CO3-2 K+ Na+ 0.003 248 | CO3-2 K+ OH- -0.01 249 | CO3-2 K+ SO4-2 -0.009 250 | CO3-2 Na+ OH- -0.017 251 | CO3-2 Na+ SO4-2 -0.005 252 | H+ HSO4- K+ -0.0265 253 | H+ HSO4- Mg+2 -0.0178 254 | H+ HSO4- Na+ -0.0129 255 | H+ K+ Br- -0.021 256 | H+ K+ SO4-2 0.197 257 | HCO3- K+ Na+ -0.003 258 | HCO3- Mg+2 SO4-2 -0.161 259 | HCO3- Na+ SO4-2 -0.005 260 | HSO4- K+ SO4-2 -0.0677 261 | HSO4- Mg+2 SO4-2 -0.0425 262 | HSO4- Na+ SO4-2 -0.0094 263 | K+ Mg+2 SO4-2 -0.048 264 | K+ Na+ SO4-2 -0.010 265 | K+ OH- SO4-2 -0.050 266 | Mg+2 Na+ SO4-2 -0.015 267 | Na+ OH- SO4-2 -0.009""" -------------------------------------------------------------------------------- /pyequion2/datamods/reactions_gases.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | reactions_gases = [ 4 | {'CO2': 1.0, 5 | 'CO2(g)': -1.0, 6 | 'phase_name': 'CO2(g)', 7 | 'T_c': 304.2, 8 | 'P_c': 72.86, 9 | 'Omega': 0.225, 10 | 'log_K25': -1.468, 11 | 'deltah': -4.776, 12 | 'log_K_coefs': [10.5624, -0.023547, -3972.8, 0.0, 587460.0, 1.9194e-05], 13 | 'type': 'rev'}, 14 | {'H2O': 1.0, 15 | 'H2O(g)': -1.0, 16 | 'phase_name': 'H2O(g)', 17 | 'T_c': 647.3, 18 | 'P_c': 217.6, 19 | 'Omega': 0.344, 20 | 'log_K25': 1.506, 21 | 'deltah': -10.52342256214149, 22 | 'log_K_coefs': [-16.5066, -0.0020013, 2710.7, 3.7646, 0.0, 2.24e-06], 23 | 'type': 'rev'}, 24 | {'O2': 1.0, 25 | 'O2(g)': -1.0, 26 | 'phase_name': 'O2(g)', 27 | 'T_c': 154.6, 28 | 'P_c': 49.8, 29 | 'Omega': 0.021, 30 | 'log_K25': -2.8983, 31 | 'log_K_coefs': [-7.5001, 0.0078981, 0.0, 0.0, 200270.0], 32 | 'type': 'rev'}, 33 | {'H2': 1.0, 34 | 'H2(g)': -1.0, 35 | 'phase_name': 'H2(g)', 36 | 'T_c': 33.2, 37 | 'P_c': 12.8, 38 | 'Omega': -0.225, 39 | 'log_K25': -3.105, 40 | 'deltah': -1.0, 41 | 'log_K_coefs': [-9.3114, 0.0046473, -49.335, 1.4341, 128150.0], 42 | 'type': 'rev'}, 43 | {'N2': 1.0, 44 | 'N2(g)': -1.0, 45 | 'phase_name': 'N2(g)', 46 | 'T_c': 126.2, 47 | 'P_c': 33.5, 48 | 'Omega': 0.039, 49 | 'log_K25': -3.1864, 50 | 'log_K_coefs': [-58.453, 0.001818, 3199.0, 17.909, -27460.0], 51 | 'type': 'rev'}, 52 | {'H+': 1.0, 53 | 'HS-': 1.0, 54 | 'H2S(g)': -1.0, 55 | 'phase_name': 'H2S(g)', 56 | 'T_c': 373.2, 57 | 'P_c': 88.2, 58 | 'Omega': 0.1, 59 | 'log_K25': -7.9759, 60 | 'log_K_coefs': [-97.354, -0.031576, 1828.5, 37.44, 28.56], 61 | 'type': 'rev'}, 62 | {'CH4': 1.0, 63 | 'CH4(g)': -1.0, 64 | 'phase_name': 'CH4(g)', 65 | 'T_c': 190.6, 66 | 'P_c': 45.4, 67 | 'Omega': 0.008, 68 | 'log_K25': -2.8, 69 | 'log_K_coefs': [10.44, -0.00765, -6669.0, 0.0, 1014000.0], 70 | 'type': 'rev'}, 71 | {'NH3': 1.0, 72 | 'NH3(g)': -1.0, 73 | 'phase_name': 'NH3(g)', 74 | 'T_c': 405.6, 75 | 'P_c': 111.3, 76 | 'Omega': 0.25, 77 | 'log_K25': 1.7966, 78 | 'log_K_coefs': [-18.758, 0.0003367, 2511.3, 4.8619, 39.192], 79 | 'type': 'rev'} 80 | ] 81 | -------------------------------------------------------------------------------- /pyequion2/datamods/reactions_irreversible.py: -------------------------------------------------------------------------------- 1 | reactions_irreversible = [ 2 | # FIXME Automatic irreversible for: Cl- 3 | {"CaCl2": -1, "Ca++": 1, "Cl-": 2, "type": "irrev", "id_db": -1}, 4 | {"NaCl": -1, "Na+": 1, "Cl-": 1, "type": "irrev", "id_db": -1}, 5 | {"KCl": -1, "K+": 1, "Cl-": 1, "type": "irrev", "id_db": -1}, 6 | {"KOH": -1, "K+": 1, "OH-": 1, "type": "irrev", "id_db": -1}, 7 | {"MgCl2": -1, "Mg++": 1, "Cl-": 2, "type": "irrev", "id_db": -1}, 8 | {"K2SO4": -1, "K+": 2, "SO4--": 1, "type": "irrev", "id_db": -1}, 9 | {"BaCl2": -1, "Ba++": 1, "Cl-": 2, "type": "irrev", "id_db": -1}, 10 | {"NaHSO4": -1, "Na+": 1, "HSO4-": 1, "type": "irrev", "id_db": -1}, 11 | {"Na2SO4": -1, "Na+": 2, "HSO4-": 1, "type": "irrev", "id_db": -1}, 12 | {"H2SO4": -1, "H+": 2, "SO4-": 1, "type": "irrev", "id_db": -1}, 13 | {"Al2(SO4)3": -1, "Al+++": 2, "SO4--": 3, "type": "irrev", "id_db": -1}, 14 | ] -------------------------------------------------------------------------------- /pyequion2/datamods/reactions_solids.py: -------------------------------------------------------------------------------- 1 | reactions_solids = [ # Reaction: solid allways negative coefficient! 2 | { 3 | "CaCO3(s)": -1.0, 4 | "CO3--": 1.0, 5 | "Ca++": 1.0, 6 | "log_K25": -8.48, 7 | "log_K_coefs": [-171.9065, -0.077993, 2839.319, 71.595], 8 | "type": "rev", 9 | "phase_name": "Calcite", 10 | }, 11 | { 12 | "CaCO3(s)": -1.0, 13 | "CO3--": 1.0, 14 | "Ca++": 1.0, 15 | "log_K25": -8.336, 16 | "log_K_coefs": [-171.9773, -0.077993, 2903.293, 71.595], 17 | "type": "rev", 18 | "phase_name": "Aragonite", 19 | }, 20 | { # Added CM: based on witens.dat (stimela.dat): TODO check log_K25 = aragonite 21 | "CaCO3(s)": -1.0, 22 | "CO3--": 1.0, 23 | "Ca++": 1.0, 24 | "log_K25": -8.336, 25 | "log_K_coefs": [-172.1295, -0.077993, 3074.688, 71.595], 26 | "type": "rev", 27 | "phase_name": "Vaterite", 28 | }, 29 | { 30 | "CaMg(CO3)2(s)": -1.0, 31 | "Ca++": 1.0, 32 | "Mg++": 1.0, 33 | "CO3--": 2.0, 34 | "log_K25": -17.09, 35 | "log_K_coefs": "", 36 | "type": "rev", 37 | "phase_name": "Dolomite", 38 | }, 39 | { 40 | "FeCO3(s)": -1.0, 41 | "Fe++": 1.0, 42 | "CO3--": 1.0, 43 | "log_K25": -10.89, 44 | "log_K_coefs": "", 45 | "type": "rev", 46 | "phase_name": "Siderite", 47 | }, 48 | { 49 | "BaCO3(s)": -1.0, 50 | "Ba++": 1.0, 51 | "CO3--": 1.0, 52 | "log_K25": -8.562, 53 | "log_K_coefs": [607.642, 0.121098, -20011.25, -236.4948], 54 | "type": "rev", 55 | "phase_name": "Witherite", 56 | }, 57 | { 58 | "BaSO4(s)": -1.0, 59 | "Ba++": 1.0, 60 | "SO4--": 1.0, 61 | "log_K25": -9.97, 62 | "log_K_coefs": "", 63 | "type": "rev", 64 | "phase_name": "Barite", 65 | }, 66 | { 67 | "CaSO4(s)": -1.0, 68 | "Ca++": 1.0, 69 | "SO4--": 1.0, 70 | "log_K25": -4.36, 71 | "log_K_coefs": [84.9, 0.0, -3135.12, -31.79], 72 | "type": "rev", 73 | "phase_name": "Anhydrite", 74 | }, 75 | { 76 | "NaCl(s)": -1.0, 77 | "Cl-": 1.0, 78 | "Na+": 1.0, 79 | "log_K25": 1.57, 80 | "log_K_coefs": "", 81 | "type": "rev", 82 | "phase_name": "Halite", 83 | }, 84 | { 85 | "Mn(OH)2(s)": -1.0, 86 | "H+": -2.0, 87 | "Mn++": 1.0, 88 | "H2O": 2.0, 89 | "log_K25": 15.2, 90 | "log_K_coefs": "", 91 | "type": "rev", 92 | "phase_name": "Pyrochroite", 93 | }, 94 | { 95 | "MnCO3(s)": -1.0, 96 | "Mn++": 1.0, 97 | "CO3--": 1.0, 98 | "log_K25": -11.13, # phreeqc 99 | # "log_K25": -10.39, # wateq 100 | "log_K_coefs": "", 101 | "type": "rev", 102 | "phase_name": "Rhodochrosite", 103 | }, 104 | { 105 | "CaSO4:2H2O(s)": -1.0, 106 | "Ca++": 1.0, 107 | "SO4--": 1.0, 108 | "H2O": 2.0, 109 | "log_K25": -4.58, 110 | "log_K_coefs": [68.2401, 0.0, -3221.51, -25.0627], 111 | "type": "rev", 112 | "phase_name": "Gypsum", 113 | }, 114 | { 115 | "SrCO3(s)": -1.0, 116 | "Sr++": 1.0, 117 | "CO3--": 1.0, 118 | "log_K25": -9.271, 119 | "log_K_coefs": [155.0305, 0.0, -7239.594, -56.58638], 120 | "type": "rev", 121 | "phase_name": "Strontianite", 122 | }, 123 | { 124 | "SrSO4(s)": -1.0, 125 | "Sr++": 1.0, 126 | "SO4--": 1.0, 127 | "log_K25": -6.63, 128 | "log_K_coefs": [-7.14, 0.00611, 75.0, 0.0, 0.0, -1.79e-05], 129 | "type": "rev", 130 | "phase_name": "Celestite", 131 | }, 132 | { 133 | "Ca5(PO4)3(OH)(s)": -1.0, 134 | "H+": -4.0, 135 | "H2O": 1.0, 136 | "HPO4--": 3.0, 137 | "Ca++": 5.0, 138 | "log_K25": -3.421, 139 | "log_K_coefs": "", 140 | "type": "rev", 141 | "phase_name": "Hydroxyapatite", 142 | }, 143 | { 144 | "CaF2(s)": -1.0, 145 | "Ca++": 1.0, 146 | "F-": 2.0, 147 | "log_K25": -10.6, 148 | "log_K_coefs": [66.348, 0.0, -4298.2, -25.271], 149 | "type": "rev", 150 | "phase_name": "Fluorite", 151 | }, 152 | { 153 | "SiO2(s)": -1.0, 154 | "H2O": -2.0, 155 | "H4SiO4": 1.0, 156 | "log_K25": -2.71, 157 | "log_K_coefs": [-0.26, 0.0, -731.0], 158 | "type": "rev", 159 | "phase_name": "SiO2(a)", 160 | }, 161 | { 162 | "SiO2(s)": -1.0, 163 | "H2O": -2.0, 164 | "H4SiO4": 1.0, 165 | "log_K25": -3.55, 166 | "log_K_coefs": [-0.09, 0.0, -1032.0], 167 | "type": "rev", 168 | "phase_name": "Chalcedony", 169 | }, 170 | { 171 | "SiO2(s)": -1.0, 172 | "H2O": -2.0, 173 | "H4SiO4": 1.0, 174 | "log_K25": -3.98, 175 | "log_K_coefs": [0.41, 0.0, -1309.0], 176 | "type": "rev", 177 | "phase_name": "Quartz", 178 | }, 179 | { 180 | "Al(OH)3(s)": -1.0, 181 | "H+": -3.0, 182 | "Al+++": 1.0, 183 | "H2O": 3.0, 184 | "log_K25": 8.11, 185 | "log_K_coefs": "", 186 | "type": "rev", 187 | "phase_name": "Gibbsite", 188 | }, 189 | { 190 | "Al(OH)3(s)": -1.0, 191 | "H+": -3.0, 192 | "Al+++": 1.0, 193 | "H2O": 3.0, 194 | "log_K25": 10.8, 195 | "log_K_coefs": "", 196 | "type": "rev", 197 | "phase_name": "Al(OH)3(a)", 198 | }, 199 | { 200 | "Al2Si2O5(OH)4(s)": -1.0, 201 | "H+": -6.0, 202 | "H2O": 1.0, 203 | "H4SiO4": 2.0, 204 | "Al+++": 2.0, 205 | "log_K25": 7.435, 206 | "log_K_coefs": "", 207 | "type": "rev", 208 | "phase_name": "Kaolinite", 209 | }, 210 | { 211 | "NaAlSi3O8(s)": -1.0, 212 | "H2O": -8.0, 213 | "Na+": 1.0, 214 | "Al(OH)4-": 1.0, 215 | "H4SiO4": 3.0, 216 | "log_K25": -18.002, 217 | "log_K_coefs": "", 218 | "type": "rev", 219 | "phase_name": "Albite", 220 | }, 221 | { 222 | "CaAl2Si2O8(s)": -1.0, 223 | "H2O": -8.0, 224 | "Ca++": 1.0, 225 | "Al(OH)4-": 2.0, 226 | "H4SiO4": 2.0, 227 | "log_K25": -19.714, 228 | "log_K_coefs": "", 229 | "type": "rev", 230 | "phase_name": "Anorthite", 231 | }, 232 | { 233 | "Ca0.165Al2.33Si3.67O10(OH)2(s)": -1.0, 234 | "H2O": -12.0, 235 | "Ca++": 0.165, 236 | "Al(OH)4-": 2.33, 237 | "H4SiO4": 3.67, 238 | "H+": 2.0, 239 | "log_K25": -45.027, 240 | "log_K_coefs": "", 241 | "type": "rev", 242 | "phase_name": "Ca-Montmorillonite", 243 | }, 244 | { 245 | "KAlSi3O8(s)": -1.0, 246 | "H2O": -8.0, 247 | "K+": 1.0, 248 | "Al(OH)4-": 1.0, 249 | "H4SiO4": 3.0, 250 | "log_K25": -20.573, 251 | "log_K_coefs": "", 252 | "type": "rev", 253 | "phase_name": "K-feldspar", 254 | }, 255 | { 256 | "KCl(s)": -1.0, 257 | "K+": 1.0, 258 | "Cl-": 1.0, 259 | "log_K25": 0.9, 260 | "log_K_coefs": "", 261 | "type": "rev", 262 | "phase_name": "Sylvite", 263 | }, 264 | { 265 | "KAl3Si3O10(OH)2(s)": -1.0, 266 | "H+": -10.0, 267 | "K+": 1.0, 268 | "Al+++": 3.0, 269 | "H4SiO4": 3.0, 270 | "log_K25": 12.703, 271 | "log_K_coefs": "", 272 | "type": "rev", 273 | "phase_name": "K-mica", 274 | }, 275 | { 276 | "Mg5Al2Si3O10(OH)8(s)": -1.0, 277 | "H+": -16.0, 278 | "Mg++": 5.0, 279 | "Al+++": 2.0, 280 | "H4SiO4": 3.0, 281 | "H2O": 6.0, 282 | "log_K25": 68.38, 283 | "log_K_coefs": "", 284 | "type": "rev", 285 | "phase_name": "Chlorite(14A)", 286 | }, 287 | { 288 | "Mg3Si2O5(OH)4(s)": -1.0, 289 | "H+": -6.0, 290 | "H2O": 1.0, 291 | "H4SiO4": 2.0, 292 | "Mg++": 3.0, 293 | "log_K25": 32.2, 294 | "log_K_coefs": [13.248, 0.0, 10217.1, -6.1894], 295 | "type": "rev", 296 | "phase_name": "Chrysotile", 297 | }, 298 | { 299 | "Mg2Si3O7.5OH:3H2O(s)": -1.0, 300 | "H+": -4.0, 301 | "H2O": -0.5, 302 | "Mg++": 2.0, 303 | "H4SiO4": 3.0, 304 | "log_K25": 15.76, 305 | "log_K_coefs": "", 306 | "type": "rev", 307 | "phase_name": "Sepiolite", 308 | }, 309 | { 310 | "Mg2Si3O7.5OH:3H2O(s)": -1.0, 311 | "H+": -4.0, 312 | "H2O": -0.5, 313 | "Mg++": 2.0, 314 | "H4SiO4": 3.0, 315 | "log_K25": 18.66, 316 | "log_K_coefs": "", 317 | "type": "rev", 318 | "phase_name": "Sepiolite(d)", 319 | }, 320 | { 321 | "Mg3Si4O10(OH)2(s)": -1.0, 322 | "H2O": -4.0, 323 | "H+": -6.0, 324 | "Mg++": 3.0, 325 | "H4SiO4": 4.0, 326 | "log_K25": 21.399, 327 | "log_K_coefs": "", 328 | "type": "rev", 329 | "phase_name": "Talc", 330 | }, 331 | { 332 | "Fe2O3(s)": -1.0, 333 | "H+": -6.0, 334 | "Fe+++": 2.0, 335 | "H2O": 3.0, 336 | "log_K25": -4.008, 337 | "log_K_coefs": "", 338 | "type": "rev", 339 | "phase_name": "Hematite", 340 | }, 341 | { 342 | "KAl3(SO4)2(OH)6(s)": -1.0, 343 | "H+": -6.0, 344 | "K+": 1.0, 345 | "Al+++": 3.0, 346 | "SO4--": 2.0, 347 | "H2O": 6.0, 348 | "log_K25": -1.4, 349 | "log_K_coefs": "", 350 | "type": "rev", 351 | "phase_name": "Alunite", 352 | }, 353 | { 354 | "Fe(OH)3(s)": -1.0, 355 | "H+": -3.0, 356 | "Fe+++": 1.0, 357 | "H2O": 3.0, 358 | "log_K25": 4.891, 359 | "log_K_coefs": "", 360 | "type": "rev", 361 | "phase_name": "Fe(OH)3(a)", 362 | }, 363 | { 364 | "FeS(s)": -1.0, 365 | "H+": -1.0, 366 | "Fe++": 1.0, 367 | "HS-": 1.0, 368 | "log_K25": -3.915, 369 | "log_K_coefs": "", 370 | "type": "rev", 371 | "phase_name": "FeS(ppt)", 372 | }, 373 | { 374 | "FeS(s)": -1.0, 375 | "H+": -1.0, 376 | "Fe++": 1.0, 377 | "HS-": 1.0, 378 | "log_K25": -4.648, 379 | "log_K_coefs": "", 380 | "type": "rev", 381 | "phase_name": "Mackinawite", 382 | }, 383 | { 384 | "S(s)": -1.0, 385 | "H+": -2.0, 386 | "e-": -2.0, 387 | "H2S": 1.0, 388 | "log_K25": 4.882, 389 | "log_K_coefs": "", 390 | "type": "electronic", 391 | "phase_name": "Sulfur", 392 | }, 393 | { 394 | "Fe(OH)3(s)": -1.0, 395 | "H+": -3.0, 396 | "Fe+++": 1.0, 397 | "H2O": 3.0, 398 | "log_K25": 4.891, 399 | "log_K_coefs": "", 400 | "type": "rev", 401 | "phase_name": "Fe(OH)3(a)", 402 | }, 403 | { 404 | "MnO2:H2O(s)": -1.0, 405 | "H+": -4.0, 406 | "e-": -2.0, 407 | "Mn++": 1.0, 408 | "H2O": 3.0, 409 | "log_K25": 41.38, 410 | "log_K_coefs": "", 411 | "type": "electronic", 412 | "phase_name": "Pyrolusite", 413 | }, 414 | { 415 | "FeS2(s)": -1.0, 416 | "H+": -2.0, 417 | "e-": -2.0, 418 | "Fe++": 1.0, 419 | "HS-": 2.0, 420 | "log_K25": -18.479, 421 | "log_K_coefs": "", 422 | "type": "electronic", 423 | "phase_name": "Pyrite", 424 | }, 425 | { 426 | "FeOOH(s)": -1.0, 427 | "H+": -3.0, 428 | "Fe+++": 1.0, 429 | "H2O": 2.0, 430 | "log_K25": -1.0, 431 | "log_K_coefs": "", 432 | "type": "rev", 433 | "phase_name": "Goethite", 434 | }, 435 | { 436 | "K2SO4(s)": -1.0, 437 | "K+": 2.0, 438 | "SO4--": 1.0, 439 | "log_K25": -1.850, 440 | # "log_K_coefs": "", 441 | # "deltah": 5.7552581, # 24.080 kJ/mol -> 5.7552581 kcal/mol 442 | # "log_K_coefs": [2.36863E+0, 0.0, -1.25778E+3, 0.0], #I guess was from sit.dat 443 | "log_K_coefs": [ 444 | 674.142, 445 | 0.30423, 446 | -18037, 447 | -280.236, 448 | 0, 449 | -1.44055e-4, 450 | ], # From Appelo (2015) 451 | # "log_K_coefs": [674.142, 0.30423, 0, 0, 0, 0], 452 | "type": "rev", 453 | "phase_name": "Arcanite", 454 | }, 455 | ] 456 | -------------------------------------------------------------------------------- /pyequion2/datamods/temp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pathlib 4 | 5 | ownpath = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) 6 | filepath = ownpath/'pitzer.txt' 7 | with open(filepath, 'r') as file: 8 | text1 = file.read() 9 | 10 | import pitzer_data 11 | text2 = pitzer_data.pitzer_data -------------------------------------------------------------------------------- /pyequion2/eqsolver/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .eqsolver import * -------------------------------------------------------------------------------- /pyequion2/eqsolver/solvers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | 5 | def solver_constrained_newton(f, x0, maxiter=10000, tol=1e-6, 6 | delta_step=0.9999, 7 | max_step=1.0, 8 | print_frequency=None): 9 | delta_step = 0.9999 10 | control_value = 10**-200 11 | x = x0.copy() 12 | for i in range(maxiter): 13 | res, jac = f(x) 14 | if np.max(np.abs(res)) < tol: 15 | break 16 | try: 17 | delta_x = np.linalg.solve(jac, -res) 18 | except np.linalg.LinAlgError: 19 | print("Solver jacobian is singular. Returning value and residual as it is") 20 | return x, res 21 | control_index = np.abs(delta_x) > control_value 22 | x_step = x[control_index] 23 | delta_x_step = delta_x[control_index] 24 | step_ = -delta_step*x_step/delta_x_step*((x_step + delta_x_step) <= 0) + \ 25 | max_step*(x_step+delta_x_step > 0) 26 | step = np.min(step_) 27 | x_new = x + step*delta_x 28 | x_new[x_new < control_value] = control_value 29 | if print_frequency is not None: 30 | if (i+1) % print_frequency == 0: 31 | print('------') 32 | print(x) 33 | print(res) 34 | print(i) 35 | print('------') 36 | x = x_new 37 | return x, res 38 | -------------------------------------------------------------------------------- /pyequion2/fugacity/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .peng_robinson import make_peng_robinson_fugacity_function -------------------------------------------------------------------------------- /pyequion2/fugacity/peng_robinson.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | 4 | import numpy as np 5 | 6 | from . import solve_cubic 7 | 8 | 9 | _GAS_CONSTANT_ATM = 82.06 #atm cm^3 mol^{-1} K^{-1} 10 | 11 | 12 | def make_peng_robinson_fugacity_function(reactions_gases): 13 | T_c, P_c, Omega = _get_peng_robinson_params(reactions_gases) 14 | f = functools.partial(_peng_robinson_fugacity, T_c=T_c, P_c=P_c, Omega=Omega) 15 | return f 16 | 17 | 18 | def _get_peng_robinson_params(reactions_gases): 19 | T_c = np.array([r.get('T_c', 0.0) for r in reactions_gases]) 20 | P_c = np.array([r.get('P_c', 1.0) for r in reactions_gases]) 21 | Omega = np.array([r.get('Omega', 0.0) for r in reactions_gases]) 22 | return T_c, P_c, Omega 23 | 24 | 25 | def _peng_robinson_a_and_b(x, T, P, T_c, P_c, Omega): 26 | kappa = 0.37464 + 1.54226*Omega - 0.26993*Omega**2 #adimensionless 27 | #Tr = np.clip(T/T_c, a_min=0.0, a_max=1.0) #adimensionless 28 | Tr = T/T_c 29 | alpha_ = (1 + kappa*(1 - np.sqrt(Tr)))**2 #adimensionless 30 | a_ = 0.45723533*_GAS_CONSTANT_ATM**2*T_c**2/P_c #atm cm^6 mol^{-2} 31 | b_ = 0.07779607*_GAS_CONSTANT_ATM*T_c/P_c #cm^3/mol 32 | a_alpha_ = a_*alpha_ 33 | b = np.sum(x*b_) 34 | a_alpha = np.sum((x*x[..., None])*np.sqrt(a_alpha_*a_alpha_[..., None])) 35 | return a_alpha, b 36 | 37 | 38 | def _peng_robinson_comprehensibility(a_alpha, b, T, P): 39 | A = a_alpha*P/(_GAS_CONSTANT_ATM**2*T**2) 40 | B = b*P/(_GAS_CONSTANT_ATM*T) 41 | C3 = 1 42 | C2 = B - 1 43 | C1 = A - 2*B - 3*B**2 44 | C0 = B**3 + B**2 - A*B 45 | roots = solve_cubic.solve_cubic(C3, C2, C1, C0) 46 | Z = np.real(roots[0]) #Surely real (biggest) 47 | return Z 48 | 49 | 50 | def _peng_robinson_fugacity(x, T, P, T_c, P_c, Omega): #x: molal_fractions, T: K, P: atm 51 | a_alpha, b = _peng_robinson_a_and_b(x, T, P, T_c, P_c, Omega) 52 | comprehensibility = _peng_robinson_comprehensibility(a_alpha, b, T, P) #P*V_m/(R*T) 53 | const1 = 2*np.sqrt(2) 54 | const2 = 1 + np.sqrt(2) 55 | const3 = np.sqrt(2) - 1 56 | V_m = comprehensibility*(_GAS_CONSTANT_ATM*T)/P 57 | logphi1 = P*V_m/(_GAS_CONSTANT_ATM*T) - 1 58 | logphi2 = -np.log(P*(V_m-b)/(_GAS_CONSTANT_ATM*T)) 59 | logphi3a = a_alpha/(const1*b*(_GAS_CONSTANT_ATM*T)) 60 | logphi3b = np.log((V_m + const2*b)/(V_m - const3*b)) 61 | logphi3 = -logphi3a*logphi3b 62 | logphi = logphi1 + logphi2 + logphi3 #base3 63 | logphi = logphi/2.302585092994046 #base e to base 10 64 | return logphi -------------------------------------------------------------------------------- /pyequion2/fugacity/solve_cubic.py: -------------------------------------------------------------------------------- 1 | # CUBIC ROOT SOLVER 2 | 3 | # Date Created : 24.05.2017 4 | # Created by : Shril Kumar [(shril.iitdhn@gmail.com),(github.com/shril)] & 5 | # Devojoyti Halder [(devjyoti.itachi@gmail.com),(github.com/devojoyti)] 6 | 7 | # Project : Classified 8 | # Use Case : Instead of using standard numpy.roots() method for finding roots, 9 | # we have implemented our own algorithm which is ~10x faster than 10 | # in-built method. 11 | 12 | # Algorithm Link : www.1728.org/cubic2.htm 13 | 14 | # This script (Cubic Equation Solver) is an independent program for computation of roots of Cubic Polynomials. This script, however, 15 | # has no relation with original project code or calculations. It is to be also made clear that no knowledge of it's original project 16 | # is included or used to device this script. This script is complete freeware developed by above signed users, and may further be 17 | # used or modified for commercial or non-commercial purpose. 18 | 19 | 20 | # Libraries imported for fast mathematical computations. 21 | import math 22 | import numpy as np 23 | 24 | # Main Function takes in the coefficient of the Cubic Polynomial 25 | # as parameters and it returns the roots in form of numpy array. 26 | # Polynomial Structure -> ax^3 + bx^2 + cx + d = 0 27 | 28 | def solve_cubic(a, b, c, d): 29 | 30 | if (a == 0 and b == 0): # Case for handling Liner Equation 31 | return np.array([(-d * 1.0) / c]) # Returning linear root as numpy array. 32 | 33 | elif (a == 0): # Case for handling Quadratic Equations 34 | 35 | D = c * c - 4.0 * b * d # Helper Temporary Variable 36 | if D >= 0: 37 | D = math.sqrt(D) 38 | x1 = (-c + D) / (2.0 * b) 39 | x2 = (-c - D) / (2.0 * b) 40 | else: 41 | D = math.sqrt(-D) 42 | x1 = (-c + D * 1j) / (2.0 * b) 43 | x2 = (-c - D * 1j) / (2.0 * b) 44 | 45 | return np.array([x1, x2]) # Returning Quadratic Roots as numpy array. 46 | 47 | f = findF(a, b, c) # Helper Temporary Variable 48 | g = findG(a, b, c, d) # Helper Temporary Variable 49 | h = findH(g, f) # Helper Temporary Variable 50 | 51 | if f == 0 and g == 0 and h == 0: # All 3 Roots are Real and Equal 52 | if (d / a) >= 0: 53 | x = (d / (1.0 * a)) ** (1 / 3.0) * -1 54 | else: 55 | x = (-d / (1.0 * a)) ** (1 / 3.0) 56 | return np.array([x, x, x]) # Returning Equal Roots as numpy array. 57 | 58 | elif h <= 0: # All 3 roots are Real 59 | 60 | i = math.sqrt(((g ** 2.0) / 4.0) - h) # Helper Temporary Variable 61 | j = i ** (1 / 3.0) # Helper Temporary Variable 62 | k = math.acos(-(g / (2 * i))) # Helper Temporary Variable 63 | L = j * -1 # Helper Temporary Variable 64 | M = math.cos(k / 3.0) # Helper Temporary Variable 65 | N = math.sqrt(3) * math.sin(k / 3.0) # Helper Temporary Variable 66 | P = (b / (3.0 * a)) * -1 # Helper Temporary Variable 67 | 68 | x1 = 2 * j * math.cos(k / 3.0) - (b / (3.0 * a)) 69 | x2 = L * (M + N) + P 70 | x3 = L * (M - N) + P 71 | 72 | return np.array([x1, x2, x3]) # Returning Real Roots as numpy array. 73 | 74 | elif h > 0: # One Real Root and two Complex Roots 75 | R = -(g / 2.0) + math.sqrt(h) # Helper Temporary Variable 76 | if R >= 0: 77 | S = R ** (1 / 3.0) # Helper Temporary Variable 78 | else: 79 | S = (-R) ** (1 / 3.0) * -1 # Helper Temporary Variable 80 | T = -(g / 2.0) - math.sqrt(h) 81 | if T >= 0: 82 | U = (T ** (1 / 3.0)) # Helper Temporary Variable 83 | else: 84 | U = ((-T) ** (1 / 3.0)) * -1 # Helper Temporary Variable 85 | 86 | x1 = (S + U) - (b / (3.0 * a)) 87 | x2 = -(S + U) / 2 - (b / (3.0 * a)) + (S - U) * math.sqrt(3) * 0.5j 88 | x3 = -(S + U) / 2 - (b / (3.0 * a)) - (S - U) * math.sqrt(3) * 0.5j 89 | 90 | return np.array([x1, x2, x3]) # Returning One Real Root and two Complex Roots as numpy array. 91 | 92 | 93 | # Helper function to return float value of f. 94 | def findF(a, b, c): 95 | return ((3.0 * c / a) - ((b ** 2.0) / (a ** 2.0))) / 3.0 96 | 97 | 98 | # Helper function to return float value of g. 99 | def findG(a, b, c, d): 100 | return (((2.0 * (b ** 3.0)) / (a ** 3.0)) - ((9.0 * b * c) / (a **2.0)) + (27.0 * d / a)) /27.0 101 | 102 | 103 | # Helper function to return float value of h. 104 | def findH(g, f): 105 | return ((g ** 2.0) / 4.0 + (f ** 3.0) / 27.0) -------------------------------------------------------------------------------- /pyequion2/gaseous_system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | from . import builder 5 | from . import fugacity 6 | 7 | 8 | class InertGaseousSystem(object): 9 | def __init__(self, possible_gases, database_files=None, 10 | fugacity_model="IDEAL"): 11 | if database_files is None: 12 | database_files = builder.DEFAULT_DB_FILES 13 | possible_gas_reactions = \ 14 | builder.get_all_possible_gas_reactions(database_files) 15 | _, self.gaseous_reactions = builder._get_species_reactions_from_compounds( 16 | set(possible_gases), possible_gas_reactions) 17 | self.gas_names = [r['phase_name'] for r in self.gaseous_reactions] 18 | self.gas_indexes = {g: i for i, g in enumerate(self.gas_names)} 19 | self.set_fugacity_coefficient_function(fugacity_model) 20 | 21 | def get_fugacity(self, molals_gases, TK, P): 22 | molals_gases_array = np.zeros(len(self.gas_names)) 23 | for key, value in molals_gases.items(): 24 | molals_gases_array[self.gas_indexes[key]] = value 25 | activity_array = self.fugacity_function(molals_gases_array, 26 | TK, 27 | P) 28 | activity_map = {phase: activity_array[self.gas_indexes[phase]] 29 | for phase in self.gas_names} 30 | return activity_map 31 | 32 | def fugacity_function(self, molals_gases, TK, P): 33 | """ 34 | Activity function for gaseous species 35 | 36 | Parameters 37 | ---------- 38 | molals_gases: np.ndarray 39 | Array of molals of gases 40 | TK: float 41 | Temperature in Kelvin 42 | P: float 43 | Pressure in atm 44 | 45 | Returns 46 | ------- 47 | ndarray 48 | Log-activity of gases 49 | """ 50 | molal_fractions = molals_gases/np.sum(molals_gases) 51 | fugacity_coefficient_term = self._fugacity_coefficient_function( 52 | molal_fractions, TK, P) 53 | partial_pressure_term = np.log10(P) + np.log10(molal_fractions) 54 | logact = fugacity_coefficient_term + partial_pressure_term 55 | return logact 56 | 57 | def set_fugacity_coefficient_function(self, 58 | fugacity_model="IDEAL"): 59 | """ 60 | Returns 61 | ------- 62 | Callable[np.ndarray, float, float] -> float 63 | Log-fugacity function, accepting molal_fractions, temperature (TK), and pressure (ATM) 64 | """ 65 | gaseous_reactions = self.gaseous_reactions 66 | if fugacity_model == "IDEAL": 67 | self._fugacity_coefficient_function = lambda x, TK, P: 0.0 68 | elif fugacity_model == "PENGROBINSON": 69 | self._fugacity_coefficient_function = \ 70 | fugacity.make_peng_robinson_fugacity_function(gaseous_reactions) 71 | else: 72 | raise NotImplementedError -------------------------------------------------------------------------------- /pyequion2/gui/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .run import run -------------------------------------------------------------------------------- /pyequion2/gui/images/pyequion_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyequion/pyequion2/9c176d5e2fc5d00617a08e148ae440cf6801c20f/pyequion2/gui/images/pyequion_logo.png -------------------------------------------------------------------------------- /pyequion2/gui/initializer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, 5 | QTextEdit, QLineEdit, 6 | QPushButton, QCheckBox, 7 | QGridLayout, QVBoxLayout, 8 | QHBoxLayout, QMessageBox, 9 | QComboBox) 10 | from PyQt5.QtGui import QFont 11 | from PyQt5.QtCore import Qt 12 | 13 | from .. import EquilibriumSystem 14 | from .solver import SolverGUI 15 | 16 | 17 | class InitializerGUI(QWidget): 18 | def __init__(self, parent=None): 19 | super().__init__(parent) 20 | self.parent_ = parent #TODO: There must be some PyQt actual solution 21 | self.initializeUI() 22 | 23 | def initializeUI(self): 24 | if not self.has_parent: 25 | self.setGeometry(100, 100, 300, 300) 26 | self.setWindowTitle('PyEquion GUI Initializer') 27 | self.setupWidgets() 28 | self.show() 29 | 30 | def setupWidgets(self): 31 | main_grid = QGridLayout() 32 | elements_label = QLabel("Base species/elements") 33 | self.elements_inserter = QTextEdit() 34 | self.elements_inserter.setPlaceholderText( 35 | "Put every element in a row. Example: \n"\ 36 | "C\n"\ 37 | "Ca\n"\ 38 | "Na\n" 39 | ) 40 | self.from_elements_checker = QCheckBox("From elements") 41 | self.from_elements_checker.setToolTip("If checked, put elements above. "\ 42 | "If not, put seed ions") 43 | self.from_elements_checker.setChecked(True) 44 | self.create_button = QPushButton("Create equilibrium") 45 | self.create_button.setToolTip("Create the equilibrium system") 46 | self.create_button.clicked.connect(self.create_equilibrium) 47 | equilibrium_type_label = QLabel("Equilibrium type:") 48 | self.equilibrium_type_cb = QComboBox() 49 | self.equilibrium_type_cb.addItems(["Aqueous equilibrium"]) 50 | #self.equilibrium_type_cb.addItems(["Aqueous equilibrium", "Phase equilibrium"]) 51 | 52 | main_grid.addWidget(elements_label, 0, 0, 1, 2) 53 | main_grid.addWidget(self.elements_inserter, 1, 0, 1, 2) 54 | main_grid.addWidget(self.from_elements_checker, 2, 0, 1, 1) 55 | main_grid.addWidget(equilibrium_type_label, 3, 0, 1, 1) 56 | main_grid.addWidget(self.equilibrium_type_cb, 3, 1, 1, 1) 57 | main_grid.addWidget(self.create_button, 4, 0, 1, 2) 58 | 59 | self.setLayout(main_grid) 60 | 61 | def create_equilibrium(self): 62 | from_elements = self.from_elements_checker.isChecked() 63 | base_species = [s.strip() for s in self.elements_inserter.toPlainText().strip('\n').split('\n')] 64 | try: 65 | eqsys = EquilibriumSystem(base_species, from_elements=from_elements, 66 | activity_model="PITZER") 67 | except: 68 | QMessageBox.critical(self, 69 | "Could not create equilibrium", 70 | "Could not create equilibrium. Did you set seeds correctly?", 71 | QMessageBox.Close, 72 | QMessageBox.Close) 73 | return 74 | if self.equilibrium_type_cb.currentText() == "Aqueous equilibrium": 75 | type_eq = "aqueous" 76 | elif self.equilibrium_type_cb.currentText() == "Phase equilibrium": 77 | type_eq = "phase" 78 | 79 | solving_gui = SolverGUI(eqsys, type_eq, self.parent_) 80 | self.create_new_gui(solving_gui) 81 | 82 | def create_new_gui(self, new_gui): 83 | if not self.has_parent: 84 | self.new_gui = new_gui 85 | self.new_gui.show() 86 | else: 87 | self.parent_.display_and_connect(self, new_gui, "Solver") 88 | 89 | @property 90 | def has_parent(self): 91 | return self.parent_ is not None 92 | -------------------------------------------------------------------------------- /pyequion2/gui/logmaker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #DEPRECATED: Use pyequion.logmaker.py 4 | 5 | 6 | def make_solver_log(molal_balance, activity_balance, 7 | molal_balance_log, activity_balance_log, 8 | temperature, pressure, 9 | closing_equation, closing_equation_value): 10 | lines = [] 11 | temperature_line = "T - {0:.2f} atm".format(temperature) 12 | pressure_line = "P - {0:.2f} atm".format(pressure) 13 | lines.append(temperature_line) 14 | lines.append(pressure_line) 15 | for key, value in molal_balance.items(): 16 | balance_line = "[{0}]={1:.2e} molal".format(key, value) 17 | lines.append(balance_line) 18 | for key, value in activity_balance.items(): 19 | balance_line = "{{{0}}}={1:.2e} molal".format(key, value) 20 | lines.append(balance_line) 21 | for key, value in molal_balance_log.items(): 22 | balance_line = "log[{0}]={1:.2e} molal".format(key, value) 23 | lines.append(balance_line) 24 | for key, value in activity_balance_log.items(): 25 | balance_line = "log{{{0}}} - {1:.2e} molal".format(key, value) 26 | lines.append(balance_line) 27 | log = "\n".join(lines) 28 | return log -------------------------------------------------------------------------------- /pyequion2/gui/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import collections 4 | 5 | from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, 6 | QTextEdit, QLineEdit, 7 | QPushButton, QCheckBox, 8 | QGridLayout, QVBoxLayout, 9 | QHBoxLayout, QMessageBox, 10 | QComboBox, QMainWindow, 11 | QTabWidget, QTabBar, 12 | QScrollArea, QAction, 13 | QFileDialog) 14 | from PyQt5.QtGui import QFont, QPixmap 15 | from PyQt5.QtCore import Qt 16 | 17 | import cloudpickle 18 | 19 | from .. import EquilibriumSystem 20 | from .solver import SolverGUI 21 | from .initializer import InitializerGUI 22 | from .solution import SolutionGUI 23 | 24 | 25 | class PyEquionGUI(QMainWindow): 26 | def __init__(self): 27 | super().__init__() 28 | self.initializeUI() 29 | 30 | def initializeUI(self): 31 | self.setGeometry(100, 100, 900, 600) 32 | self.setWindowTitle('PyEquion2 GUI') 33 | self.setCentralWidget(WelcomeWidget(self)) 34 | self.create_menu() 35 | self.tabWidget = None 36 | self.show() 37 | 38 | def create_menu(self): 39 | exit_act = QAction('Exit', self) 40 | exit_act.setShortcut('Ctrl+Q') 41 | exit_act.triggered.connect(self.close) 42 | 43 | open_act = QAction('Open', self) 44 | open_act.setShortcut('Ctrl+O') 45 | open_act.triggered.connect(self.close) 46 | 47 | save_act = QAction('Save', self) 48 | save_act.setShortcut('Ctrl+S') 49 | save_act.triggered.connect(self.saveToFile) 50 | 51 | self.menu_bar = self.menuBar() 52 | self.menu_bar.setNativeMenuBar(False) 53 | 54 | file_menu = self.menu_bar.addMenu('File') 55 | file_menu.addAction(open_act) 56 | file_menu.addAction(save_act) 57 | file_menu.addSeparator() 58 | file_menu.addAction(exit_act) 59 | 60 | 61 | def initialize(self): 62 | self.tabWidget = TabController(self) 63 | self.setCentralWidget(self.tabWidget) 64 | 65 | def load(self, filename): 66 | with open(filename, "rb") as f: 67 | loaded_widget = cloudpickle.load(f) 68 | self.tabWidget = loaded_widget 69 | self.setCentralWidget(self.tabWidget) 70 | 71 | def closeEvent(self, event): 72 | """ 73 | Display a QMessageBox when asking the user if they want to 74 | quit the program. 75 | """ 76 | answer = QMessageBox.question(self, "Quit PyEquion2?", 77 | "Are you sure you want to Quit?", QMessageBox.No | QMessageBox.Yes, 78 | QMessageBox.Yes) 79 | if answer == QMessageBox.Yes: 80 | event.accept() # accept the event and close the application 81 | else: 82 | event.ignore() # ignore the close event 83 | 84 | def saveToFile(self): 85 | if self.tabWidget is None: 86 | QMessageBox.information(self, "Error", 87 | "Nothing to be saved.", QMessageBox.Ok) 88 | return 89 | 90 | #else 91 | file_name, _ = QFileDialog.getSaveFileName(self, 'Save File', 92 | "","Picke File (*.pkl)") 93 | if file_name.endswith('.pkl'): 94 | self.tabWidget.save(file_name) 95 | else: 96 | QMessageBox.information(self, "Error", 97 | "Unable to save file.", QMessageBox.Ok) 98 | 99 | def setupWidgets(self): 100 | pass 101 | 102 | 103 | class TabController(QWidget): 104 | def __init__(self, parent): 105 | super().__init__(parent) 106 | self.nchildren = collections.defaultdict(lambda : 0) 107 | self.tags = collections.defaultdict(lambda : "1") 108 | 109 | self.layout = QVBoxLayout(self) 110 | # Initialize tab screen 111 | self.tabs = QTabWidget() 112 | self.tabs.setTabsClosable(True) 113 | self.tabs.tabCloseRequested.connect(self.close_tab) 114 | 115 | self.initializer_widget = InitializerGUI(self) 116 | self.tabs.addTab(self.initializer_widget, "Initializer 1") 117 | self.tabs.tabBar().setTabButton(0, QTabBar.RightSide, None) 118 | 119 | self.layout.addWidget(self.tabs) 120 | self.setLayout(self.layout) 121 | 122 | def close_tab(self, index): 123 | widget = self.tabs.widget(index) 124 | widget.close() 125 | self.tabs.removeTab(index) 126 | 127 | def display_and_connect(self, creator_widget, 128 | created_widget, 129 | name, 130 | create_scroll=True): 131 | #TODO: Put the connect part 132 | self.nchildren[creator_widget] += 1 133 | tag = self.tags[creator_widget] + \ 134 | ".{0}".format(self.nchildren[creator_widget]) 135 | self.tags[created_widget] = tag 136 | name_ = name + " " + tag #TODO: Put tag in names 137 | if create_scroll: 138 | scroll = QScrollArea() 139 | scroll.setWidgetResizable(True) # CRITICAL 140 | scroll.setWidget(created_widget) # CRITICAL 141 | index = self.tabs.addTab(scroll, name_) 142 | else: 143 | index = self.tabs.addTab(created_widget, name_) 144 | self.tabs.setCurrentIndex(index) 145 | 146 | def save(self, filename): 147 | #Saving and loading are done through TabController 148 | with open(filename, 'wb') as f: 149 | cloudpickle.dump(self, f) 150 | 151 | 152 | class WelcomeWidget(QWidget): 153 | def __init__(self, parent): 154 | super().__init__(parent) 155 | self.initializeUI() 156 | 157 | def initializeUI(self): 158 | self.main_layout = QVBoxLayout(self) 159 | welcome_label = QLabel("Welcome to PyEquion2 GUI.") 160 | welcome_label.setAlignment(Qt.AlignCenter) 161 | logo_label = QLabel() 162 | logo_label.setPixmap(QPixmap("./images/pyequion_logo.png")) 163 | #logo_label.setAlignment(Qt.AlignCenter) 164 | self.start_button = QPushButton("Start") 165 | self.start_button.clicked.connect(self.initialize) 166 | self.load_button = QPushButton("Load") 167 | #self.load_button.clicked.connect(?) 168 | self.license_button = QPushButton("License") 169 | self.license_button.clicked.connect(self.show_license) 170 | 171 | self.main_layout.addWidget(welcome_label) 172 | self.main_layout.addWidget(logo_label) 173 | self.main_layout.addWidget(self.start_button) 174 | self.main_layout.addWidget(self.load_button) 175 | self.main_layout.addWidget(self.license_button) 176 | 177 | self.show_license() 178 | 179 | def initialize(self): 180 | self.parent().initialize() 181 | self.close() 182 | 183 | def show_license(self): 184 | QMessageBox.information(self, "License", 185 | LICENSE_MESSAGE, QMessageBox.Ok) 186 | 187 | 188 | LICENSE_MESSAGE = \ 189 | """\ 190 | BSD 3-Clause License 191 | 192 | Copyright (c) 2021, PyEquion. 193 | All rights reserved. 194 | 195 | Redistribution and use in source and binary forms, with or without 196 | modification, are permitted provided that the following conditions are met: 197 | 198 | * Redistributions of source code must retain the above copyright notice, this 199 | list of conditions and the following disclaimer. 200 | 201 | * Redistributions in binary form must reproduce the above copyright notice, 202 | this list of conditions and the following disclaimer in the documentation 203 | and/or other materials provided with the distribution. 204 | 205 | * Neither the name of the copyright holder nor the names of its 206 | contributors may be used to endorse or promote products derived from 207 | this software without specific prior written permission. 208 | 209 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 210 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 211 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 212 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 213 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 214 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 215 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 216 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 217 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 218 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 219 | """ 220 | -------------------------------------------------------------------------------- /pyequion2/gui/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | try: 5 | from PyQt5.QtWidgets import QApplication 6 | except ModuleNotFoundError: 7 | raise ModuleNotFoundError("PyEquion2 GUI requires PyQt5") 8 | from .main import PyEquionGUI 9 | 10 | 11 | def run(): 12 | app = QApplication(sys.argv) 13 | window = PyEquionGUI() 14 | sys.exit(app.exec_()) 15 | -------------------------------------------------------------------------------- /pyequion2/gui/seqsolution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import matplotlib 4 | matplotlib.use('Qt5Agg') 5 | 6 | import itertools 7 | 8 | from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, 9 | QTextEdit, QLineEdit, 10 | QPushButton, QCheckBox, 11 | QGridLayout, QVBoxLayout, 12 | QHBoxLayout, QMessageBox, 13 | QComboBox, QScrollArea, 14 | QFrame, QFileDialog) 15 | from PyQt5.QtGui import QFont 16 | from PyQt5.QtCore import Qt 17 | 18 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg 19 | from matplotlib.figure import Figure 20 | 21 | import numpy as np 22 | 23 | 24 | HEADER_COLOR = "cyan" 25 | 26 | 27 | class SeqSolutionGUI(QWidget): 28 | def __init__(self, solutions, solver_log, type_eq, pairs, parent=None): 29 | super().__init__(parent) 30 | self.parent_ = parent #TODO: There must be some PyQt actual solution 31 | self.pairs = pairs 32 | self.solutions = solutions 33 | self.base_solution = solutions[0] 34 | self.solver_log = solver_log 35 | self.type_eq = type_eq 36 | self.plot_windows = [] 37 | self.initializeUI() 38 | 39 | def initializeUI(self): 40 | if not self.has_parent: 41 | self.setGeometry(100, 100, 300, 300) 42 | self.setWindowTitle("PyEquion GUI Solver") 43 | self.setupWidgets() 44 | self.show() 45 | 46 | def setupWidgets(self): 47 | self.main_layout = QGridLayout() 48 | 49 | save_log_button = QPushButton("Save log (header)") 50 | save_log_button.clicked.connect(self.save_log_to_file) 51 | 52 | properties_vbox = self.make_properties_vbox() 53 | properties_vbox.setContentsMargins(5, 5, 25, 5) 54 | 55 | species_grid = self.make_species_grid() 56 | species_grid.setContentsMargins(25, 5, 25, 5) 57 | 58 | if self.type_eq == "aqueous": 59 | phases_grid = self.make_saturation_indexes() 60 | elif self.type_eq == "phase": 61 | phases_grid = self.make_phase_molals() 62 | phases_grid.setContentsMargins(25, 5, 5, 5) 63 | 64 | self.main_layout.addLayout(properties_vbox, 0, 0) 65 | self.main_layout.addLayout(species_grid, 0, 1) 66 | self.main_layout.addLayout(phases_grid, 0, 2) 67 | self.main_layout.addWidget(save_log_button, 1, 0) 68 | 69 | self.setLayout(self.main_layout) 70 | 71 | def make_species_grid(self): 72 | sorted_species = sorted(self.base_solution.molals, 73 | key=lambda k : self.base_solution.molals[k], 74 | reverse=True) 75 | species_grid = QGridLayout() 76 | 77 | title_species_label = QLabel("Component") 78 | title_molals_label = QLabel("Molal") 79 | title_act_label = QLabel("Activity") 80 | title_fraction_label = QLabel("Mole fraction") 81 | 82 | species_grid.addWidget(title_species_label, 0, 0) 83 | species_grid.addWidget(title_molals_label, 0, 1) 84 | species_grid.addWidget(title_act_label, 0, 2) 85 | species_grid.addWidget(title_fraction_label, 0, 3) 86 | 87 | for i, specie in enumerate(sorted_species, 1): 88 | # specie_hbox = QHBoxLayout() 89 | specie_label = QLabel(specie) 90 | molals_label = self.make_value_plot(specie, 'molals') 91 | # conc_label = self.show_value_label(specie, self.base_solution.concentrations) 92 | act_label = self.make_value_plot(specie, 'activities') 93 | fraction_label = self.make_value_plot(specie, 'mole_fractions') 94 | species_grid.addWidget(specie_label, i, 0) 95 | species_grid.addWidget(molals_label, i, 1) 96 | species_grid.addWidget(act_label, i, 2) 97 | species_grid.addWidget(fraction_label, i, 3) 98 | 99 | sorted_elements = sorted(self.base_solution.elements_molals, 100 | key=lambda k : self.base_solution.elements_molals[k], 101 | reverse=True) 102 | for j, element in enumerate(sorted_elements, i+1): 103 | specie_label = QLabel(element) 104 | molals_label = self.make_value_plot(element, 'elements_molals') 105 | act_label = QLabel("") 106 | fraction_label = QLabel("") 107 | species_grid.addWidget(specie_label, j, 0) 108 | species_grid.addWidget(molals_label, j, 1) 109 | species_grid.addWidget(act_label, j, 2) 110 | species_grid.addWidget(fraction_label, j, 3) 111 | species_grid.setRowStretch(species_grid.rowCount(), 1) 112 | # species_grid.setSpacing(0) 113 | # items = (species_grid.itemAt(i) for i in range(species_grid.count())) 114 | # for item in items: 115 | # item.widget().setStyleSheet("border: 1px solid black;") 116 | 117 | # for i in range(species_grid.columnCount()): 118 | # species_grid.setColumnStretch(i, 1) 119 | return species_grid 120 | 121 | def make_saturation_indexes(self): 122 | phases = sorted(self.base_solution.saturation_indexes, 123 | key = lambda k : self.base_solution.saturation_indexes[k], 124 | reverse=True) 125 | phases_grid = QGridLayout() 126 | 127 | title_phase = QLabel("Phase") 128 | title_si = QLabel("SI") 129 | title_satur = QLabel("Saturation") 130 | phases_grid.addWidget(title_phase, 0, 0) 131 | phases_grid.addWidget(title_si, 0, 1) 132 | phases_grid.addWidget(title_satur, 0, 2) 133 | 134 | for i, phase in enumerate(phases, 1): 135 | phase_label = QLabel(phase) 136 | si_label = self.make_value_plot(phase, 'saturation_indexes') 137 | satur_label = self.make_value_plot(phase, 'saturations') 138 | phases_grid.addWidget(phase_label, i, 0) 139 | phases_grid.addWidget(si_label, i, 1) 140 | phases_grid.addWidget(satur_label, i, 2) 141 | phases_grid.setRowStretch(phases_grid.rowCount(), 1) 142 | 143 | return phases_grid 144 | 145 | def make_phase_molals(self): 146 | phases_grid = QGridLayout() 147 | solid_phases = sorted(self.base_solution.solid_molals, 148 | key = lambda k : self.base_solution.solid_molals[k], 149 | reverse=True) 150 | gas_phases = sorted(self.base_solution.gas_molals, 151 | key = lambda k : self.base_solution.gas_molals[k], 152 | reverse=True) 153 | 154 | title_phase = QLabel("Phase") 155 | title_molal = QLabel("Molals") 156 | phases_grid.addWidget(title_phase, 0, 0) 157 | phases_grid.addWidget(title_molal, 0, 1) 158 | 159 | i = 0 160 | for i, solid_phase in enumerate(solid_phases, 1): 161 | phase_label = QLabel(solid_phase) 162 | molal_label = self.make_value_plot(solid_phase, 'solid_molals') 163 | phases_grid.addWidget(phase_label, i, 0) 164 | phases_grid.addWidget(molal_label, i, 1) 165 | 166 | for j, gas_phase in enumerate(gas_phases, i+1): 167 | phase_label = QLabel(gas_phase) 168 | molal_label = self.make_value_plot(gas_phase, 'gas_molals') 169 | phases_grid.addWidget(phase_label, j, 0) 170 | phases_grid.addWidget(molal_label, j, 1) 171 | phases_grid.setRowStretch(phases_grid.rowCount(), 1) 172 | 173 | # phases_grid.setSpacing(0) 174 | # items = (phases_grid.itemAt(i) for i in range(phases_grid.count())) 175 | # for item in items: 176 | # item.widget().setStyleSheet("border: 1px solid black;") 177 | 178 | # for i in range(phases_grid.columnCount()): 179 | # phases_grid.setColumnStretch(i, 1) 180 | 181 | return phases_grid 182 | 183 | def make_properties_vbox(self): 184 | properties_vbox = QVBoxLayout() 185 | 186 | ph_button = QPushButton("pH") 187 | ph_button.clicked.connect(lambda : self.plot_single_property("ph")) 188 | ionic_strength_button = QPushButton("I") 189 | ionic_strength_button.clicked.connect(lambda : self.plot_single_property("ionic_strength", "mol/kg H2O")) 190 | conductivity_button = QPushButton("\u03C3") 191 | conductivity_button.clicked.connect(lambda : self.plot_single_property("electrical_conductivity", "S/m")) 192 | 193 | type_equilibrium_marker = "aqueous only" if (self.type_eq == "aqueous") else "phases precipitation" 194 | type_equilibrium_string = "Equilibrium type: {0}".format(type_equilibrium_marker) 195 | 196 | properties_vbox.addWidget(QLabel("Properties:")) 197 | properties_vbox.addWidget(ph_button) 198 | properties_vbox.addWidget(ionic_strength_button) 199 | properties_vbox.addWidget(conductivity_button) 200 | properties_vbox.addWidget(QLabel(" ")) 201 | properties_vbox.addWidget(QLabel("Balance conditions")) 202 | properties_vbox.addWidget(QLabel(self.solver_log)) 203 | properties_vbox.addWidget(QLabel(type_equilibrium_string)) 204 | properties_vbox.addStretch() 205 | 206 | return properties_vbox 207 | 208 | def show_value_label(self, val, d): 209 | if val in d: 210 | label_str = "{:.2e}".format(d[val]) 211 | else: 212 | label_str = "" 213 | label = QLabel(label_str) 214 | return label 215 | 216 | def make_value_plot(self, val, property_name): 217 | if val in getattr(self.base_solution, property_name): 218 | button_str = "Plot" 219 | button = QPushButton(button_str) 220 | button.clicked.connect(lambda : self.plot_dict_property(val, property_name)) 221 | else: 222 | button = QPushButton("") 223 | return button 224 | 225 | def plot_dict_property(self, val, property_name, unit="mol/kg H2O"): 226 | properties = np.array([getattr(solution, property_name)[val] 227 | for solution in self.solutions]) 228 | plot_window = PlotWindow() 229 | plot_window.plot_widget.plot_pairs(properties, self.pairs) 230 | plot_window.plot_widget.axes.set_ylabel("{0}:{1} [{2}]".format(property_name, val, unit)) 231 | plot_window.show() 232 | self.plot_windows.append(plot_window) 233 | 234 | def plot_single_property(self, property_name, unit=" "): 235 | properties = np.array([getattr(solution, property_name) 236 | for solution in self.solutions]) 237 | plot_window = PlotWindow(self) 238 | plot_widget = plot_window.plot_widget 239 | plot_widget.plot_pairs(properties, self.pairs) 240 | plot_widget.axes.set_ylabel("{0} [{1}]".format(property_name, unit)) 241 | plot_window.show() 242 | self.plot_windows.append(plot_window) 243 | 244 | def save_log_to_file(self): 245 | #else 246 | file_name, _ = QFileDialog.getSaveFileName(self, 'Save File', 247 | "","Text File (*.txt)") 248 | try: 249 | with open(file_name, 'w') as f: 250 | f.write(self.solver_log) 251 | except: 252 | QMessageBox.information(self, "Error", 253 | "Unable to save file.", QMessageBox.Ok) 254 | 255 | @property 256 | def has_parent(self): 257 | return self.parent_ is not None 258 | 259 | 260 | class PlotWindow(QWidget): 261 | def __init__(self, width=8, height=8, dpi=100): 262 | super().__init__() 263 | self.plot_widget = PlotWidget(width, height, dpi) 264 | self.initializeUI() 265 | 266 | def initializeUI(self): 267 | self.setGeometry(100, 100, 600, 600) 268 | self.setWindowTitle("Plot") 269 | self.setupWidgets() 270 | 271 | def setupWidgets(self): 272 | layout = QVBoxLayout() 273 | self.save_button = QPushButton("Save figure") 274 | self.save_button.clicked.connect(self.savefig) 275 | self.export_button = QPushButton("Export") 276 | self.export_button.clicked.connect(self.export) 277 | layout.addWidget(self.plot_widget) 278 | layout.addWidget(self.save_button) 279 | layout.addWidget(self.export_button) 280 | self.setLayout(layout) 281 | 282 | def savefig(self): 283 | file_name, _ = QFileDialog.getSaveFileName(self, 'Save File', 284 | "","PNG file (*.png)") 285 | try: 286 | self.plot_widget.fig.savefig(file_name) 287 | except: 288 | QMessageBox.information(self, "Error", 289 | "Unable to save file.", QMessageBox.Ok) 290 | 291 | def export(self): 292 | file_name, _ = QFileDialog.getSaveFileName(self, 'Save File', 293 | "","Text file (*.txt)") 294 | try: 295 | xy = self.plot_widget.axes.lines[0].get_xydata() 296 | header = "{%s} {%s}"%(self.plot_widget.axes.get_xlabel(), self.plot_widget.axes.get_ylabel()) 297 | np.savetxt(file_name, xy, header=header) 298 | except: 299 | QMessageBox.information(self, "Error", 300 | "Unable to save file.", QMessageBox.Ok) 301 | 302 | class PlotWidget(FigureCanvasQTAgg): 303 | def __init__(self, parent, width=8, height=8, dpi=100): 304 | self.parent = parent 305 | self.fig = Figure(figsize=(width, height), dpi=dpi) 306 | self.axes = self.fig.add_subplot(111) 307 | super().__init__(self.fig) 308 | 309 | def plot(self, x, y): 310 | self.axes.cla() 311 | self.axes.plot(x, y) 312 | 313 | def plot_single(self, x): 314 | self.axes.cla() 315 | self.axes.plot(x) 316 | 317 | def plot_pairs(self, x, pairs): 318 | if len(pairs) == 0: 319 | self.plot_single(x) 320 | else: 321 | base_pair, other_pairs = pairs[0], pairs[1:] 322 | n = len(x) 323 | xbase = np.linspace(base_pair.bounds[0], base_pair.bounds[1], n) 324 | self.axes.cla() 325 | self.axes.plot(xbase, x, 'o') 326 | self.axes.set_xlabel("{0} [{1}]".format(base_pair.name, base_pair.unit)) 327 | for i, pair in enumerate(other_pairs, start=2): 328 | name = "{0} [{1}]".format(pair.name, pair.unit) 329 | bounds = pair.bounds 330 | self.add_secondary_axis(bounds, name, i) 331 | 332 | def add_secondary_axis(self, bounds, name=None, n=2): 333 | axnew = self.axes.twiny() 334 | newlabel = np.linspace(bounds[0], bounds[1], len(self.axes.get_xticks())) 335 | newlabel = np.round(newlabel, 5) 336 | axnew.set_xticks(self.axes.get_xticks()) 337 | axnew.set_xticklabels(newlabel) 338 | axnew.xaxis.set_ticks_position('bottom') # set the position of the second x-axis to bottom 339 | axnew.xaxis.set_label_position('bottom') # set the position of the second x-axis to bottom 340 | axnew.spines['bottom'].set_position(('outward', 36*(n-1))) 341 | if name is not None: 342 | axnew.set_xlabel(name) 343 | axnew.set_xlim(self.axes.get_xlim()) 344 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /pyequion2/gui/solution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | 4 | from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, 5 | QTextEdit, QLineEdit, 6 | QPushButton, QCheckBox, 7 | QGridLayout, QVBoxLayout, 8 | QHBoxLayout, QMessageBox, 9 | QComboBox, QScrollArea, 10 | QFrame, QFileDialog) 11 | from PyQt5.QtGui import QFont 12 | from PyQt5.QtCore import Qt 13 | 14 | import numpy as np 15 | 16 | 17 | HEADER_COLOR = "cyan" 18 | 19 | 20 | class SolutionGUI(QWidget): 21 | def __init__(self, solution, solver_log, type_eq, parent=None): 22 | super().__init__(parent) 23 | self.parent_ = parent #TODO: There must be some PyQt actual solution 24 | self.solution = solution 25 | self.solver_log = solver_log 26 | self.type_eq = type_eq 27 | self.initializeUI() 28 | 29 | def initializeUI(self): 30 | if not self.has_parent: 31 | self.setGeometry(100, 100, 300, 300) 32 | self.setWindowTitle("PyEquion GUI Solver") 33 | self.setupWidgets() 34 | self.show() 35 | 36 | def setupWidgets(self): 37 | self.main_layout = QGridLayout() 38 | 39 | save_log_button = QPushButton("Save log") 40 | save_log_button.clicked.connect(self.save_log_to_file) 41 | 42 | properties_vbox = self.make_properties_vbox() 43 | properties_vbox.setContentsMargins(5, 5, 25, 5) 44 | 45 | species_grid = self.make_species_grid() 46 | species_grid.setContentsMargins(25, 5, 25, 5) 47 | 48 | if self.type_eq == "aqueous": 49 | phases_grid = self.make_saturation_indexes() 50 | elif self.type_eq == "phase": 51 | phases_grid = self.make_phase_molals() 52 | phases_grid.setContentsMargins(25, 5, 5, 5) 53 | 54 | self.main_layout.addLayout(properties_vbox, 0, 0) 55 | self.main_layout.addLayout(species_grid, 0, 1) 56 | self.main_layout.addLayout(phases_grid, 0, 2) 57 | self.main_layout.addWidget(save_log_button, 1, 0) 58 | 59 | self.setLayout(self.main_layout) 60 | 61 | def make_species_grid(self): 62 | sorted_species = sorted(self.solution.molals, 63 | key=lambda k : self.solution.molals[k], 64 | reverse=True) 65 | species_grid = QGridLayout() 66 | 67 | title_species_label = QLabel("Component") 68 | title_molals_label = QLabel("Molal") 69 | title_act_label = QLabel("Activity") 70 | title_fraction_label = QLabel("Mole fraction") 71 | title_mgl_label = QLabel("mg/L") 72 | 73 | species_grid.addWidget(title_species_label, 0, 0) 74 | species_grid.addWidget(title_molals_label, 0, 1) 75 | species_grid.addWidget(title_act_label, 0, 2) 76 | species_grid.addWidget(title_fraction_label, 0, 3) 77 | species_grid.addWidget(title_mgl_label, 0, 4) 78 | 79 | for i, specie in enumerate(sorted_species, 1): 80 | # specie_hbox = QHBoxLayout() 81 | specie_label = QLabel(specie) 82 | molals_label = self.show_value_label(specie, self.solution.molals) 83 | # conc_label = self.show_value_label(specie, self.solution.concentrations) 84 | act_label = self.show_value_label(specie, self.solution.activities) 85 | fraction_label = self.show_value_label(specie, self.solution.mole_fractions) 86 | mgl_label = self.show_value_label(specie, self.solution.concentrations_mgl) 87 | species_grid.addWidget(specie_label, i, 0) 88 | species_grid.addWidget(molals_label, i, 1) 89 | species_grid.addWidget(act_label, i, 2) 90 | species_grid.addWidget(fraction_label, i, 3) 91 | species_grid.addWidget(mgl_label, i, 4) 92 | 93 | sorted_elements = sorted(self.solution.elements_molals, 94 | key=lambda k : self.solution.elements_molals[k], 95 | reverse=True) 96 | for j, element in enumerate(sorted_elements, i+1): 97 | specie_label = QLabel(element) 98 | molals_label = self.show_value_label(element, self.solution.elements_molals) 99 | act_label = QLabel("") 100 | fraction_label = QLabel("") 101 | mgl_label = self.show_value_label(element, self.solution.elements_mgl) 102 | species_grid.addWidget(specie_label, j, 0) 103 | species_grid.addWidget(molals_label, j, 1) 104 | species_grid.addWidget(act_label, j, 2) 105 | species_grid.addWidget(fraction_label, j, 3) 106 | species_grid.addWidget(mgl_label, j, 4) 107 | 108 | species_grid.setSpacing(0) 109 | items = (species_grid.itemAt(i) for i in range(species_grid.count())) 110 | for item in items: 111 | item.widget().setStyleSheet("border: 1px solid black;") 112 | 113 | for i in range(species_grid.columnCount()): 114 | species_grid.setColumnStretch(i, 1) 115 | return species_grid 116 | 117 | def make_saturation_indexes(self): 118 | phases = sorted(self.solution.saturation_indexes, 119 | key = lambda k : self.solution.saturation_indexes[k], 120 | reverse=True) 121 | phases_grid = QGridLayout() 122 | 123 | title_phase = QLabel("Phase") 124 | title_si = QLabel("SI") 125 | title_satur = QLabel("Saturation") 126 | phases_grid.addWidget(title_phase, 0, 0) 127 | phases_grid.addWidget(title_si, 0, 1) 128 | phases_grid.addWidget(title_satur, 0, 2) 129 | 130 | for i, phase in enumerate(phases, 1): 131 | phase_label = QLabel(phase) 132 | si_label = self.show_value_label(phase, self.solution.saturation_indexes) 133 | satur_label = self.show_value_label(phase, self.solution.saturations) 134 | phases_grid.addWidget(phase_label, i, 0) 135 | phases_grid.addWidget(si_label, i, 1) 136 | phases_grid.addWidget(satur_label, i, 2) 137 | 138 | phases_grid.setSpacing(0) 139 | items = (phases_grid.itemAt(i) for i in range(phases_grid.count())) 140 | for item in items: 141 | item.widget().setStyleSheet("border: 1px solid black;") 142 | 143 | for i in range(phases_grid.columnCount()): 144 | phases_grid.setColumnStretch(i, 1) 145 | 146 | return phases_grid 147 | 148 | def make_phase_molals(self): 149 | phases_grid = QGridLayout() 150 | solid_phases = sorted(self.solution.solid_molals, 151 | key = lambda k : self.solution.solid_molals[k], 152 | reverse=True) 153 | gas_phases = sorted(self.solution.gas_molals, 154 | key = lambda k : self.solution.gas_molals[k], 155 | reverse=True) 156 | 157 | title_phase = QLabel("Phase") 158 | title_molal = QLabel("Molals") 159 | phases_grid.addWidget(title_phase, 0, 0) 160 | phases_grid.addWidget(title_molal, 0, 1) 161 | 162 | i = 0 163 | for i, solid_phase in enumerate(solid_phases, 1): 164 | phase_label = QLabel(solid_phase) 165 | molal_label = self.show_value_label(solid_phase, 166 | self.solution.solid_molals) 167 | phases_grid.addWidget(phase_label, i, 0) 168 | phases_grid.addWidget(molal_label, i, 1) 169 | 170 | for j, gas_phase in enumerate(gas_phases, i+1): 171 | phase_label = QLabel(gas_phase) 172 | molal_label = self.show_value_label(gas_phase, self.solution.gas_molals) 173 | phases_grid.addWidget(phase_label, j, 0) 174 | phases_grid.addWidget(molal_label, j, 1) 175 | 176 | phases_grid.setSpacing(0) 177 | items = (phases_grid.itemAt(i) for i in range(phases_grid.count())) 178 | for item in items: 179 | item.widget().setStyleSheet("border: 1px solid black;") 180 | 181 | for i in range(phases_grid.columnCount()): 182 | phases_grid.setColumnStretch(i, 1) 183 | 184 | return phases_grid 185 | 186 | def make_properties_vbox(self): 187 | properties_vbox = QVBoxLayout() 188 | 189 | ph_str = "pH = {:.2f}".format(self.solution.ph) 190 | ionic_strength_str = "I = {:.2f} molals".format(self.solution.ionic_strength) 191 | conductivity_str = "\u03C3 = {:.2f} S/m".format(self.solution.electrical_conductivity) 192 | balance_str = self.solver_log 193 | type_equilibrium_marker = "aqueous only" if (self.type_eq == "aqueous") else "phases precipitation" 194 | type_equilibrium_string = "Equilibrium type: {0}".format(type_equilibrium_marker) 195 | 196 | properties_vbox.addWidget(QLabel("Properties:")) 197 | properties_vbox.addWidget(QLabel(ph_str)) 198 | properties_vbox.addWidget(QLabel(ionic_strength_str)) 199 | properties_vbox.addWidget(QLabel(conductivity_str)) 200 | properties_vbox.addWidget(QLabel(" ")) 201 | properties_vbox.addWidget(QLabel("Balance conditions")) 202 | properties_vbox.addWidget(QLabel(balance_str)) 203 | properties_vbox.addWidget(QLabel(type_equilibrium_string)) 204 | properties_vbox.addStretch() 205 | 206 | return properties_vbox 207 | 208 | def show_value_label(self, val, d): 209 | if val in d: 210 | label_str = "{:.2e}".format(d[val]) 211 | else: 212 | label_str = "" 213 | label = QLabel(label_str) 214 | return label 215 | 216 | def save_log_to_file(self): 217 | #else 218 | file_name, _ = QFileDialog.getSaveFileName(self, 'Save File', 219 | "","Text File (*.txt)") 220 | try: 221 | self.solution.savelog(file_name) 222 | except: 223 | QMessageBox.information(self, "Error", 224 | "Unable to save file.", QMessageBox.Ok) 225 | 226 | @property 227 | def has_parent(self): 228 | return self.parent_ is not None -------------------------------------------------------------------------------- /pyequion2/interface/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .interface_system import InterfaceSystem -------------------------------------------------------------------------------- /pyequion2/interface/diffusion_coefficients.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | from ..water_properties import water_dynamic_viscosity, water_density 4 | 5 | 6 | def get_diffusion_coefficients(solutes, TK): 7 | diffusion_coefs_ = {k: COEFFICIENTS[k] 8 | for k in solutes 9 | if k in COEFFICIENTS.keys()} 10 | diffusion_coefs = diffusion_temp(diffusion_coefs_, TK) 11 | diffusion_median = np.median(list(diffusion_coefs.values())) 12 | diffusion_vector = np.array([diffusion_coefs.get(k, diffusion_median) 13 | for k in solutes]) 14 | return diffusion_vector 15 | 16 | 17 | def diff_temp_formula(TK, dw0, dw1): 18 | return dw0*np.exp(dw1/TK - dw1/298.15) *\ 19 | TK*water_dynamic_viscosity(298.15)/(298.15*water_dynamic_viscosity(TK)) 20 | 21 | 22 | def diffusion_temp(coefficients, TK): 23 | coefficients_temp = dict() 24 | for key, value in coefficients.items(): 25 | if len(value) < 2: 26 | coefficients_temp[key] = diff_temp_formula( 27 | TK, coefficients[key][0], 0.0) 28 | else: 29 | coefficients_temp[key] = diff_temp_formula( 30 | TK, coefficients[key][0], coefficients[key][1]) 31 | return coefficients_temp 32 | 33 | 34 | def diffusion_temp_istrength(coefficients, TK, I): 35 | raise NotImplementedError 36 | 37 | 38 | COEFFICIENTS = \ 39 | {'Na+': (1.331e-09, 122.0, 1.52, 3.7), 40 | 'HCO3-': (1.18e-09,), 41 | 'Ca++': (7.931e-10, 97.0, 3.4, 24.6), 42 | 'Cl-': (2.031e-09, 194.0, 1.6, 6.9), 43 | 'OH-': (5.27e-09,), 44 | 'H+': (9.311e-09, 1000.0, 0.46, 1.1e-09), 45 | 'CO3--': (9.551e-10, 0.0, 1.12, 2.84), 46 | 'NaCO3-': (5.85e-10,), 47 | 'CaHCO3+': (5.09e-10,), 48 | 'Mg++': (7.051e-10, 111.0, 2.4, 13.7), 49 | 'K+': (1.961e-09, 395.0, 2.5, 21.0), 50 | 'Fe++': (7.191e-10,), 51 | 'Mn++': (6.881e-10,), 52 | 'Al+++': (5.591e-10,), 53 | 'Ba++': (8.481e-10, 46.0), 54 | 'Sr++': (7.941e-10, 161.0), 55 | 'H4SiO4': (1.101e-09,), 56 | 'SO4--': (1.071e-09, 34.0, 2.08, 13.4), 57 | 'NO3-': (1.91e-09, 184.0, 1.85, 3.85), 58 | 'H3BO3': (1.11e-09,), 59 | 'PO4---': (6.121e-10,), 60 | 'F-': (1.461e-09,), 61 | 'Li+': (1.031e-09, 80.0), 62 | 'Br-': (2.011e-09, 258.0), 63 | 'Zn++': (7.151e-10,), 64 | 'Cd++': (7.171e-10,), 65 | 'Pb++': (9.451e-10,), 66 | 'Cu++': (7.331e-10,)} 67 | -------------------------------------------------------------------------------- /pyequion2/interface/interface_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | 5 | LOGE10 = 2.302585092994046 6 | GAS_CONSTANT = 8.314462618 7 | DEFAULT_TEMPERATURE = 298.15 8 | 9 | 10 | def log_decorator(f): 11 | def g(logsatur, logksp, TK, *args): 12 | satur = 10**logsatur 13 | ksp = 10**logksp 14 | return f(satur, ksp, TK, *args) 15 | return g 16 | 17 | 18 | def log_decorator_deriv(df): 19 | def dg(logsatur, logksp, TK, *args): 20 | satur = 10**logsatur 21 | ksp = 10**logksp 22 | return df(satur, ksp, TK, *args)*satur*LOGE10 23 | return dg 24 | 25 | 26 | def arrhenize(f): 27 | def f_(satur, ksp, TK, preexp, energy): 28 | log_reaction_constant = np.log(preexp) - energy/GAS_CONSTANT*(1/TK - 1/DEFAULT_TEMPERATURE) 29 | reaction_constant = np.exp(log_reaction_constant) 30 | return f(satur, ksp, TK, reaction_constant) 31 | return f_ 32 | 33 | 34 | def linear_ksp(satur, ksp, TK, reaction_constant): 35 | return reaction_constant*ksp*(satur-1)*(satur >= 1) 36 | 37 | 38 | def linear_ksp_deriv(satur, ksp, TK, reaction_constant): 39 | return reaction_constant*ksp*(satur >= 1) 40 | 41 | 42 | def linear(satur, ksp, TK, reaction_constant): 43 | return reaction_constant*(satur-1)*(satur >= 1) 44 | 45 | 46 | def linear_deriv(satur, ksp, TK, reaction_constant): 47 | return reaction_constant*(satur >= 1) 48 | 49 | 50 | def spinoidal(satur, ksp, TK, reaction_constant): 51 | return reaction_constant*(satur**0.5-1)**2*(satur >= 1) 52 | 53 | 54 | def spinoidal_deriv(satur, ksp, TK, reaction_constant): 55 | return reaction_constant/(satur**0.5)*(satur**0.5-1)*(satur > 1) 56 | 57 | 58 | #HINT: should be in beginning according to PEP8, but couldn't 59 | INTERFACE_MAP = \ 60 | {'linear_ksp': (log_decorator(linear_ksp), log_decorator_deriv(linear_ksp_deriv)), 61 | 'linear': (log_decorator(linear), log_decorator_deriv(linear_deriv)), 62 | 'spinoidal': (log_decorator(spinoidal), log_decorator_deriv(spinoidal_deriv)), 63 | 'linear_ksp_temp': (log_decorator(arrhenize(linear_ksp)), log_decorator_deriv(arrhenize(linear_ksp_deriv))), 64 | 'linear_temp': (log_decorator(arrhenize(linear)), log_decorator_deriv(arrhenize(linear_deriv))), 65 | 'spinoidal_temp': (log_decorator(arrhenize(spinoidal)), log_decorator_deriv(arrhenize(spinoidal_deriv)))} 66 | 67 | 68 | SPECIFIC_SOLIDS_MODEL = \ 69 | {'Calcite': ('linear_ksp_temp', (52153.84138874875, 86881.05)), 70 | 'Vaterite': ('linear_ksp_temp', (52153.84138874875, 86881.05)), #Fitted from our data 71 | 'Aragonite': ('linear_ksp_temp', (52153.84138874875, 86881.05))} 72 | -------------------------------------------------------------------------------- /pyequion2/interface/interface_solution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | from .. import solution 5 | 6 | 7 | class InterfaceSolutionResult(solution.SolutionResult): 8 | def __init__(self, equilibrium_system, x, TK, 9 | x_bulk, transport_vector, reaction_imp, relative_diffusion_vector, 10 | *args, **kwargs): 11 | super().__init__(equilibrium_system, x, TK, **kwargs) 12 | if relative_diffusion_vector is None: 13 | self._transport_fluxes = transport_vector*(x_bulk - x) 14 | else: 15 | self._transport_fluxes = transport_vector*(x_bulk - x*relative_diffusion_vector) 16 | self._interface_indexes = equilibrium_system._explicit_interface_indexes + \ 17 | equilibrium_system._implicit_interface_indexes 18 | if reaction_imp is None: 19 | self._reaction_fluxes = self._make_reaction_fluxes_1(equilibrium_system) 20 | else: 21 | self._reaction_fluxes = self._make_reaction_fluxes_2(equilibrium_system, reaction_imp) 22 | 23 | def _make_reaction_fluxes_1(self, equilibrium_system): 24 | #First calculate J^R, whose order will be self._interface_indexes controled 25 | reaction_stoich_matrix = self.solid_stoich_matrix[self._interface_indexes, :] 26 | balance_matrix = self.formula_matrix[2:, :] #Reduced formula matrix 27 | reduced_balance_matrix = balance_matrix[:, 1:] #Only solutes 28 | b = reduced_balance_matrix@self._transport_fluxes 29 | A = balance_matrix@reaction_stoich_matrix.transpose() 30 | jr, _, _, _ = np.linalg.lstsq(A, b, rcond=None) 31 | reaction_fluxes = np.zeros(len(self.solid_phase_names)) 32 | for i, phase in enumerate(equilibrium_system.interface_phases): 33 | reaction_fluxes[equilibrium_system.interface_indexes_dict[phase]] = jr[i] 34 | return reaction_fluxes 35 | 36 | def _make_reaction_fluxes_2(self, equilibrium_system, jr_imp): 37 | #First calculate J^R, whose order will be self._interface_indexes controled 38 | reaction_stoich_matrix_exp = \ 39 | self.solid_stoich_matrix[equilibrium_system._explicit_interface_indexes, :] 40 | reaction_stoich_matrix_imp = \ 41 | self.solid_stoich_matrix[equilibrium_system._implicit_interface_indexes, :] 42 | balance_matrix = self.formula_matrix[2:, :] #Reduced formula matrix 43 | reduced_balance_matrix = balance_matrix[:, 1:] #Only solutes 44 | b1 = reduced_balance_matrix@self._transport_fluxes 45 | b2 = -balance_matrix@reaction_stoich_matrix_imp.transpose()@jr_imp 46 | b = b1 + b2 47 | A = balance_matrix@reaction_stoich_matrix_exp.transpose() 48 | if A.shape[1] == 0: 49 | jr_exp = np.zeros(0) 50 | else: 51 | jr_exp, _, _, _ = np.linalg.lstsq(A, b, rcond=None) 52 | reaction_fluxes = np.zeros(len(self.solid_phase_names)) 53 | for i, phase in enumerate(equilibrium_system.explicit_interface_phases): 54 | reaction_fluxes[equilibrium_system._explicit_interface_indexes_dict[phase]] = jr_exp[i] 55 | for i, phase in enumerate(equilibrium_system.implicit_interface_phases): 56 | reaction_fluxes[equilibrium_system._implicit_interface_indexes_dict[phase]] = jr_imp[i] 57 | return reaction_fluxes 58 | 59 | @property 60 | def reaction_fluxes(self): #mol/m^2 s 61 | return {phase: self._reaction_fluxes[i] for i, phase in enumerate(self.solid_phase_names)} 62 | #Next make reaction fluxes including non-reacting species 63 | 64 | @property 65 | def transport_fluxes(self): #mol/m^2 s 66 | return {solute: self._transport_fluxes[i] for i, solute in enumerate(self.solutes)} 67 | 68 | @property 69 | def elements_reaction_fluxes(self): 70 | return {element: self._elements_reaction_fluxes[i] for i, element in enumerate(self.elements)} 71 | 72 | @property 73 | def _elements_reaction_fluxes(self): 74 | return self.formula_matrix@(self.solid_stoich_matrix.T@self._reaction_fluxes) -------------------------------------------------------------------------------- /pyequion2/logmaker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | 4 | 5 | def make_solver_log(molal_balance, activity_balance, 6 | molal_balance_log, activity_balance_log, 7 | temperature, pressure, 8 | closing_equation, closing_equation_value, 9 | npoints = None): 10 | lines = [] 11 | if not is_sequence(temperature): 12 | temperature_line = "T - {0:.2f} K".format(temperature) 13 | else: 14 | temperature_line = "T - ({0:.2f}, {1:.2f}) K".format(temperature[0], temperature[1]) 15 | if not is_sequence(pressure): 16 | pressure_line = "P - {0:.2f} atm".format(pressure) 17 | else: 18 | pressure_line = "P - ({0:.2f}, {1:.2f}) atm".format(pressure[0], pressure[1]) 19 | lines.append(temperature_line) 20 | lines.append(pressure_line) 21 | for key, value in molal_balance.items(): 22 | if not is_sequence(value): 23 | balance_line = "[{0}]={1:.2e} mol/kg H2O".format(key, value) 24 | else: 25 | balance_line = "[{0}]=({1:.2e}, {2:.2e}) mol/kg H2O".format(key, value[0], value[1]) 26 | lines.append(balance_line) 27 | for key, value in activity_balance.items(): 28 | if not is_sequence(value): 29 | balance_line = "{{{0}}}={1:.2e} mol/kg H2O".format(key, value) 30 | else: 31 | balance_line = "{{{0}}}=({1:.2e}, {2:.2e}) mol/kg H2O".format(key, value[0], value[1]) 32 | lines.append(balance_line) 33 | for key, value in molal_balance_log.items(): 34 | if not is_sequence(value): 35 | balance_line = "log[{0}]={1:.2e} log-molal".format(key, value) 36 | else: 37 | balance_line = "log[{0}]=({1:.2e}, {2:.2e}) log-mol/kg H2O".format(key, value[0], value[1]) 38 | lines.append(balance_line) 39 | for key, value in activity_balance_log.items(): 40 | if not is_sequence(value): 41 | balance_line = "log{{{0}}}={1:.2e} log-molal".format(key, value) 42 | else: 43 | balance_line = "log{{{0}}}=({1:.2e}, {2:.2e}) log-mol/kg H2O".format(key, value[0], value[1]) 44 | lines.append(balance_line) 45 | if npoints is not None: 46 | points_line = "NPOINTS - {0}".format(npoints) 47 | lines.append(points_line) 48 | log = "\n".join(lines) 49 | return log 50 | 51 | 52 | def is_sequence(obj): 53 | try: 54 | len(obj) 55 | except: 56 | return False 57 | else: 58 | return True 59 | 60 | 61 | def is_number(obj): 62 | try: 63 | float(obj) 64 | except: 65 | False 66 | else: 67 | return True -------------------------------------------------------------------------------- /pyequion2/sequencer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import itertools 4 | 5 | 6 | def transform_to_sequence_of_arguments(npoints, *args): 7 | """ 8 | 9 | Parameters 10 | ---------- 11 | npoints : int 12 | *args : float | (float, float) | dict[Any, float | (float, float)] 13 | 14 | Returns 15 | ------- 16 | List[Iterable[float] | Iterable[dict[Any, float]]] 17 | 18 | """ 19 | res = [] 20 | for arg in args: 21 | if _is_number(arg): 22 | res.append(itertools.repeat(arg, npoints)) 23 | elif _is_sequence(arg) and not isinstance(arg, dict): 24 | res.append(_linspace_iterator(arg[0], arg[1], npoints)) 25 | elif isinstance(arg, dict): 26 | res.append(_dict_iterator(arg, npoints)) 27 | return res 28 | 29 | 30 | def _is_sequence(obj): 31 | try: 32 | len(obj) 33 | except: 34 | return False 35 | else: 36 | return True 37 | 38 | 39 | def _is_number(obj): 40 | try: 41 | float(obj) 42 | except: 43 | False 44 | else: 45 | return True 46 | 47 | 48 | def _linspace_iterator(a, b, n): 49 | for i in range(n): 50 | yield a + (b - a)/(n-1)*i 51 | 52 | 53 | def _dict_iterator(d, npoints): 54 | d_copy = copy.copy(d) 55 | for key, val in d_copy.items(): 56 | val_iterator = itertools.repeat(val, npoints) if _is_number(val) \ 57 | else _linspace_iterator(val[0], val[1], npoints) 58 | d_copy[key] = val_iterator 59 | for i in range(npoints): 60 | yield {k: next(v) for k, v in d_copy.items()} 61 | -------------------------------------------------------------------------------- /pyequion2/solution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | 4 | import numpy as np 5 | 6 | from . import builder 7 | from . import converters 8 | 9 | 10 | MOLAL_MASS_WATER = 18.01528 #g/mol 11 | MOLALITY_WATER = 1e3*1/MOLAL_MASS_WATER #mol/kg 12 | 13 | 14 | class SolutionResult(): 15 | """ 16 | Class for solution of equilibria 17 | 18 | Parameters 19 | ---------- 20 | TK: float 21 | Temperature in Kelvin 22 | base_species: List[str] 23 | Base aqueous species in system 24 | elements: List[str] 25 | Elements in system 26 | species: List[str] 27 | Species in system 28 | reactions: List[dict] 29 | Reactions in system 30 | solid_reactions: List[dict] 31 | Solid Reactions in system 32 | formula_matrix: ndarray 33 | Formula matrix of system 34 | stoich_matrix: ndarray 35 | Stoichiometric matrix of system 36 | solid_formula_matrix: ndarray 37 | Solid formula matrix of system 38 | solid_stoich_matrix: ndarray 39 | Solid stoichiometric matrix of system 40 | """ 41 | 42 | def __init__(self, equilibrium_system, x, TK, 43 | molals_solids=None, solid_phases_in=None, 44 | molals_gases=None, gas_phases_in=None, 45 | PATM=1.0): 46 | self.TK = TK 47 | self.PATM = PATM 48 | self.solverlog = equilibrium_system.solverlog 49 | self.solvertype = equilibrium_system.solvertype 50 | self._x_molal = x 51 | self._x_logact = equilibrium_system.activity_function(x, TK) 52 | self._x_act = np.nan_to_num(10**self._x_logact) 53 | self._molals_solids = molals_solids 54 | self._solid_phases_in = solid_phases_in 55 | self._molals_gases = molals_gases 56 | self._gas_phases_in = gas_phases_in 57 | self.base_species = equilibrium_system.base_species 58 | self.species = equilibrium_system.species 59 | self.reactions = equilibrium_system.reactions 60 | self.solid_reactions = equilibrium_system.solid_reactions 61 | self.gas_reactions = equilibrium_system.gas_reactions 62 | self.elements = equilibrium_system.elements 63 | self.formula_matrix = equilibrium_system.formula_matrix 64 | self.stoich_matrix = equilibrium_system.stoich_matrix 65 | self.solid_formula_matrix = equilibrium_system.solid_formula_matrix 66 | self.solid_stoich_matrix = equilibrium_system.solid_stoich_matrix 67 | self.gas_formula_matrix = equilibrium_system.gas_formula_matrix 68 | self.gas_stoich_matrix = equilibrium_system.gas_stoich_matrix 69 | self._logsatur = self._build_saturation_indexes() 70 | 71 | def getlog(self): 72 | separator = "\n" + "-"*40 + "\n" 73 | conditions_block_init = f"CONDITIONS\n{self.solvertype}" 74 | conditions_block = conditions_block_init + "\n" + self.solverlog 75 | species_block = self._make_species_string() 76 | properties_block = self._make_properties_string() 77 | phases_block = self._make_phases_string() 78 | saturation_block = self._make_saturation_string() 79 | log = separator.join((conditions_block, 80 | species_block, 81 | properties_block, 82 | phases_block, 83 | saturation_block)) 84 | return log 85 | 86 | def savelog(self, filename): 87 | with open(filename, "w") as f: 88 | f.write(self.getlog()) 89 | 90 | @property 91 | def molals(self): 92 | """Molals""" 93 | molals_dict = {'H2O': MOLALITY_WATER} 94 | molals_dict.update(self.solute_molals) 95 | return molals_dict 96 | 97 | @property 98 | def solute_molals(self): 99 | """Molals of solutes""" 100 | molals_dict = {self.solutes[i]: self._x_molal[i] 101 | for i in range(len(self._x_molal))} 102 | return molals_dict 103 | 104 | @property 105 | def mole_fractions(self): 106 | molal_sum = sum(self.molals.values()) 107 | return {key: value/molal_sum for key, value in self.molals.items()} 108 | 109 | @property 110 | def concentrations(self): # mM or mol/m^3 111 | """Equilibrium concentrations. Assumes water volue much greater than ionic volumes. 112 | At high ionic concentration one should give preference to molals""" 113 | return {sp: converters.molal_to_mmolar(val, 114 | self.TK) 115 | for sp, val in self.molals.items()} 116 | 117 | @property 118 | def concentrations_mgl(self): 119 | return {sp: converters.molal_to_mgl(val, 120 | sp, 121 | self.TK) 122 | for sp, val in self.molals.items()} 123 | 124 | @property 125 | def elements_mgl(self): 126 | return {el: converters.molal_to_mgl(val, 127 | el, 128 | self.TK) 129 | for el, val in self.elements_molals.items()} 130 | 131 | @property 132 | def activities(self): 133 | """Equilibrium activities""" 134 | return {self.species[i]: self._x_act[i] 135 | for i in range(len(self._x_act))} 136 | 137 | @property 138 | def saturation_indexes(self): 139 | """Saturation indexes for solids""" 140 | return {self.solid_phase_names[i]: self._logsatur[i] 141 | for i in range(len(self._logsatur))} 142 | 143 | @property 144 | def saturations(self): 145 | return {k:10**v for k, v in self.saturation_indexes.items()} 146 | 147 | @property 148 | def ionic_strength(self): 149 | """Ionic strength of system""" 150 | return 0.5*np.sum( 151 | self._charge_vector[1:]**2*self._x_molal) 152 | 153 | @property 154 | def solute_elements(self): # Ignore H and O 155 | """Elements ignoring H and O""" 156 | return self.elements[2:] 157 | 158 | @property 159 | def solutes(self): # Ignore H2O 160 | """Solutes""" 161 | return self.species[1:] 162 | 163 | @property 164 | def solid_phase_names(self): 165 | """Names of solid phases""" 166 | return [sol_reac['phase_name'] for sol_reac in self.solid_reactions] 167 | 168 | @property 169 | def solid_molals(self): 170 | """Solid molals""" 171 | if self._solid_phases_in is None: 172 | solid_molals_ = dict() 173 | else: 174 | solid_molals_ = dict(zip(self._solid_phases_in, self._molals_solids)) 175 | solid_molals = {k: solid_molals_.get(k, 0.0) for k in self.solid_phase_names} 176 | return solid_molals 177 | 178 | @property 179 | def gas_phase_names(self): 180 | """Names of solid phases""" 181 | return [gas_reac['phase_name'] for gas_reac in self.gas_reactions] 182 | 183 | @property 184 | def gas_molals(self): 185 | """Solid molals""" 186 | if self._gas_phases_in is None: 187 | gas_molals_ = dict() 188 | else: 189 | gas_molals_ = dict(zip(self._gas_phases_in, self._molals_gases)) 190 | gas_molals = {k: gas_molals_.get(k, 0.0) for k in self.gas_phase_names} 191 | return gas_molals 192 | 193 | @property 194 | def elements_molals(self): 195 | """Molals for elements""" 196 | balance_vector = self._balance_vector 197 | return {k: balance_vector[i] for i, k in enumerate(self.elements)} 198 | 199 | @property 200 | def charge_density(self): 201 | """Charge density (e/kg)""" 202 | return np.sum(self.formula_matrix[-1, 1:]*self._x_molal) 203 | 204 | @property 205 | def ph(self): 206 | """pH""" 207 | return -np.log10(self.activities['H+']) 208 | 209 | @property 210 | def electrical_conductivity(self): 211 | """Electrical conductivity in S/m""" 212 | #https://www.aqion.de/site/electrical-conductivity 213 | ec = 6.2*self.ionic_strength #muS/cm to S/m 214 | return ec 215 | 216 | def _build_saturation_indexes(self): 217 | logacts = np.log10(self._x_act) 218 | solid_reactions = self.solid_reactions 219 | TK = self.TK 220 | PATM = self.PATM 221 | solid_stoich_matrix = self.solid_stoich_matrix 222 | logiap = solid_stoich_matrix@logacts 223 | logks = builder.get_log_equilibrium_constants(solid_reactions, TK, PATM) 224 | logsatur = logiap - logks 225 | return logsatur 226 | 227 | @property 228 | def _extended_x_molal(self): 229 | return np.hstack([MOLALITY_WATER, self._x_molal]) 230 | 231 | @property 232 | def _charge_vector(self): 233 | return self.formula_matrix[-1, :] 234 | 235 | @property 236 | def _balance_vector(self): 237 | return self.formula_matrix[:-1, :]@self._extended_x_molal 238 | 239 | def _make_species_string(self, which_value="molals", precision=3): 240 | lines = [] 241 | head = "[COMPONENT] [CONCENTRATION (mol/kg H2O)] "\ 242 | "[ACTIVITY (mol/kg H2O)] [MOLE FRACTION]" 243 | lines.append(head) 244 | base_string = "{0} {1:.%ie} {2:.%ie} {3:%ie}"%(precision, precision, precision) 245 | for sp in sorted(self.molals.keys(), 246 | key = lambda sp : self.molals[sp], 247 | reverse=True): 248 | string = base_string.format(sp, self.molals[sp], 249 | self.activities[sp], 250 | self.mole_fractions[sp]) 251 | lines.append(string) 252 | base_element_string = "{0} {1:.%ie}"%precision 253 | for el in sorted(self.elements_molals.keys()): 254 | string = base_element_string.format(el, self.elements_molals[el]) 255 | lines.append(string) 256 | text = '\n'.join(lines) 257 | return text 258 | 259 | def _make_properties_string(self, precision=3): 260 | head = "PROPERTIES" 261 | ph_line = f"pH = {self.ph:.{precision}f}" 262 | ionic_strength_line = f"I = {self.ionic_strength:.{precision}f} mol/kg H2O" 263 | conductivity_line = f"conductivity = {self.electrical_conductivity:.{precision}f} S/m" 264 | lines = [head, ph_line, ionic_strength_line, conductivity_line] 265 | text = '\n'.join(lines) 266 | return text 267 | 268 | def _make_saturation_string(self, precision=3): 269 | lines = [] 270 | head = "[PHASE] [SUPERSATURATION] [SI]" 271 | lines.append(head) 272 | for phase, si in self.saturation_indexes.items(): 273 | string = f"{phase} {10**si} {si}" 274 | lines.append(string) 275 | text = '\n'.join(lines) 276 | return text 277 | 278 | def _make_phases_string(self, precision=3): 279 | lines = [] 280 | head = "[PHASE] [AMOUNT mol/kg H2O]" 281 | lines.append(head) 282 | iterator = itertools.chain(self.solid_molals.items(), 283 | self.gas_molals.items()) 284 | for phase, molal in iterator: 285 | string = f"{phase} {molal}" 286 | lines.append(string) 287 | text = '\n'.join(lines) 288 | return text -------------------------------------------------------------------------------- /pyequion2/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .builder import load_from_db, charge_number, stoich_number #For backward compatibility 3 | 4 | # import copy 5 | # import re 6 | 7 | # import commentjson 8 | 9 | 10 | # def load_from_db(fname): 11 | # if not isinstance(fname, str): 12 | # return copy.deepcopy(fname) 13 | # #Open a json file as dict 14 | # with open(fname, "r") as json_file: 15 | # db = commentjson.load(json_file) 16 | # return db 17 | 18 | 19 | # def charge_number(specie): 20 | # """ 21 | # Get charge number of specie 22 | # """ 23 | # return 1*specie.count('+') - 1*specie.count('-') 24 | 25 | 26 | # def stoich_number(specie, element): 27 | # """ 28 | # Get stoichometric coeficient of element in specie 29 | # """ 30 | # if element == 'e': # Charge number 31 | # return charge_number(specie) 32 | # re_string = r"({0}[(A-Z)(\+\-)\d])|{0}$" 33 | # # If there is element, it will return either 34 | # # {0}{1}.format(element,char), or {0}.format(element) 35 | # # In second case, element is on the end, no number given, 36 | # # so stoich number is 1. In first case, if char is a int, 37 | # # stoich number is char, else, stoich number is 1 38 | # re_group = re.search(re_string.format(element), specie) 39 | # if re_group is None: # No element in this specie 40 | # value = 0 41 | # else: 42 | # res_string = re_group.group(0) 43 | # if res_string == element: 44 | # value = 1 45 | # else: 46 | # try: 47 | # value = int(res_string[-1]) 48 | # except ValueError: 49 | # value = 1 50 | # return value 51 | 52 | 53 | # def _convert_to_extended_format(specie): 54 | # if "(" not in specie: 55 | # return specie 56 | # else: 57 | # return specie -------------------------------------------------------------------------------- /pyequion2/water_properties.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import numpy as np 3 | 4 | from scipy import interpolate 5 | 6 | 7 | WATER_TEMPERATURE_CELSIUS_LIST = [0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 8 | 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 9 | 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 10 | 95.0, 100.0] # degrees Celsius 11 | WATER_SPECIFIC_HEAT_CAPACITY_LIST = [4200, 4200, 4188, 4184, 4183, 4183, 4183, 12 | 4183, 4182, 4182, 4181, 4182, 4183, 13 | 4184, 4187, 4190, 4194, 4199, 4204, 14 | 4210, 4210] # J/(kg K) 100 degrees not this 15 | WATER_THERMAL_CONDUCTIVITY = [0.5516, 0.5516, 0.5674, 0.5769, 0.5861, 0.5948, 0.6030, 16 | 0.6107, 0.6178, 0.6244, 0.6305, 0.6360, 0.6410, 17 | 0.6455, 0.6495, 0.6530, 0.6562, 0.6589, 0.6613, 18 | 0.6634, 0.6634] # W/(m K) 100 degrees not this 19 | 20 | _water_thermal_conductivity_celsius = \ 21 | interpolate.interp1d(WATER_TEMPERATURE_CELSIUS_LIST, WATER_THERMAL_CONDUCTIVITY, 22 | fill_value="extrapolate") 23 | _water_specific_heat_capacity_celsius = \ 24 | interpolate.interp1d(WATER_TEMPERATURE_CELSIUS_LIST, WATER_SPECIFIC_HEAT_CAPACITY_LIST, 25 | fill_value="extrapolate") 26 | 27 | 28 | def water_density(T): # T in Kelvin, density in kg m^3 29 | T = T - 273.15 30 | return 999.99399 + 0.04216485*T - 0.007097451*T**2 + 0.00003509571*T**3 - 9.9037785*1e-8*T**4 31 | 32 | 33 | def water_dynamic_viscosity(T): # T in Kelvin, returns Pa s 34 | return 1.856*1e-14 *\ 35 | np.exp(4209/T + 0.04527*T + (-3.376*1e-5)*T**2) # Pa s 36 | 37 | 38 | def water_thermal_conductivity(T): # T in Kelvin 39 | return _water_thermal_conductivity_celsius(T-273.15)[()] 40 | 41 | 42 | def water_specific_heat_capacity(T): # T in Kelvin 43 | return _water_specific_heat_capacity_celsius(T-273.15)[()] 44 | 45 | 46 | def water_kinematic_viscosity(T): #T in Kelvin, returns m^2/s 47 | return water_dynamic_viscosity(T)/water_density(T) -------------------------------------------------------------------------------- /pyinstaller/FLAGS.txt: -------------------------------------------------------------------------------- 1 | COMPILATION INSTRUCTIONS 2 | 3 | 1 - Install pyinstaller from pip 4 | 2 - Make sure you can run (possibly in an environment) "python pyequiongui.py" on this folder 5 | 3 - Run on this folder: 6 | pyinstaller --noconfirm --onefile --additional-hooks-dir hook pyequiongui.py -------------------------------------------------------------------------------- /pyinstaller/hook/hook-lark.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files('lark') -------------------------------------------------------------------------------- /pyinstaller/pyequiongui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pyequion2 3 | 4 | try: 5 | pyequion2.rungui() 6 | except Exception as e: 7 | with open("ERRORLOG", "a") as f: 8 | f.write(str(e)) 9 | f.write("\n") 10 | -------------------------------------------------------------------------------- /pyproject_.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "Cython>=0.15.1", 5 | "numpy>=1.10" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "pyequion2" 11 | version = "0.0.6.4" 12 | 13 | [project.optional-dependencies] 14 | gui = ["PyQt5>=5.15.6"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.21.4 2 | auto_mix_prep>=0.2.0 3 | cloudpickle>=2.0.0 4 | commentjson>=0.9.0 5 | matplotlib>=3.5.0 6 | ordered_set>=4.0.2 7 | periodictable>=1.6.0 8 | scipy>=1.7.3 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import warnings 3 | from setuptools import setup, find_packages 4 | 5 | packages = ['pyequion2'] + \ 6 | ['pyequion2.' + subpack for subpack in find_packages('pyequion2')] 7 | 8 | REQUIREMENTS = [i.strip() for i in open("requirements.txt").readlines()] 9 | 10 | # Optional dependencies 11 | extras_require = { 12 | "gui": ["PyQt5>=5.15.6"] 13 | } 14 | 15 | # Setup common arguments 16 | common_args = dict( 17 | name="pyequion2", 18 | description="Chemical equilibrium for electrolytes in pure python", 19 | packages=packages, 20 | author="PyEquion", 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: BSD License", 24 | "Operating System :: OS Independent", 25 | ], 26 | url="https://github.com/pyequion/pyequion2/", 27 | python_requires=">=3.6", 28 | install_requires=REQUIREMENTS, 29 | extras_require=extras_require, 30 | ) 31 | 32 | try: 33 | import numpy 34 | import Cython 35 | from Cython.Build import cythonize 36 | 37 | # Define the extension module 38 | from setuptools.extension import Extension 39 | ext = Extension( 40 | "pyequion2.cythonize_module", 41 | ["pyequion2/cythonize_module.pyx"], 42 | ) 43 | 44 | setup( 45 | version="0.0.6.4", 46 | setup_requires=['setuptools>=18.0', 'cython'], 47 | ext_modules=cythonize([ext]), 48 | include_dirs=[numpy.get_include()], 49 | **common_args 50 | ) 51 | except Exception as e: 52 | warnings.warn(f"Could not install with cython module. Installing pure python. Error: {e}") 53 | setup( 54 | version="0.0.6.3", 55 | **common_args 56 | ) 57 | -------------------------------------------------------------------------------- /test/examples/example0.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pyequion2 import rungui 3 | except ImportError: 4 | print('Please install gui extra') 5 | import sys 6 | sys.exit(1) 7 | 8 | rungui() 9 | 10 | #Windows Subsystem for Linux (WSL) 11 | -------------------------------------------------------------------------------- /test/examples/example1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pyequion2 4 | 5 | 6 | eqsys = pyequion2.EquilibriumSystem(['Ca', 'Na', 'Cl', 'C'], from_elements=True) #We set up the feed components of our system 7 | molal_balance = {'Ca':0.028, 'C':0.065, 'Na':0.065, 'Cl':0.056} #Set up the balances 8 | TK = 298.15 #Temperature in Kelvin 9 | PATM = 1.0 #Pressure in atm 10 | #Returns the solution class (the second argument are solver statistics, no need for understanding now) 11 | solution, solution_stats = eqsys.solve_equilibrium_mixed_balance(TK, molal_balance=molal_balance, PATM=PATM) -------------------------------------------------------------------------------- /test/examples/example2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import EquilibriumSystem #Import the necessary module 4 | 5 | eqsys = EquilibriumSystem(['Na', 'C'], from_elements=True) #We set up the feed components of our system 6 | molal_balance = {'Na': 0.150} #Set up the balances 7 | activities_balance_log = {'H+': -9.0} 8 | TK = 35 + 273.15 #Temperature in Kelvin 9 | solution, solution_stats = eqsys.solve_equilibrium_mixed_balance(TK, 10 | molal_balance=molal_balance, 11 | activities_balance_log=activities_balance_log) 12 | -------------------------------------------------------------------------------- /test/examples/example3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import EquilibriumSystem #Import the necessary module 4 | eqsys = EquilibriumSystem(['C', 'Na', 'Ca', 'Cl'], from_elements=True) #We set up the feed components of our system 5 | molal_balance = {'C':0.075, 'Na':0.075, 'Cl':0.056, 'Ca':0.028} #Set up the balances 6 | TK = 298.15 #Temperature in Kelvin 7 | PATM = 1e0 #Pressure in atm 8 | #Returns the solution class (the second argument are solver statistics, no need for understanding now) 9 | solution, solution_stats = eqsys.solve_equilibrium_elements_balance_phases(TK, 10 | molal_balance, 11 | solid_phases=[], 12 | has_gas_phases=True, 13 | PATM=PATM) 14 | print(solution.gas_molals) 15 | 16 | #log K + log CO2(g) = log CO2 17 | -------------------------------------------------------------------------------- /test/examples/example4.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import EquilibriumSystem #Import the necessary module 4 | eqsys = EquilibriumSystem(['CO2']) #We set up the feed components of our system 5 | molal_balance = {'C':0.5} #Set up the balances 6 | TK = 298.15 7 | solution, _ = eqsys.solve_equilibrium_elements_balance_phases(TK, 8 | molal_balance) 9 | PATM = 100.0 10 | solution_high_pressure, _ = eqsys.solve_equilibrium_elements_balance_phases(TK, 11 | molal_balance, 12 | PATM=PATM) 13 | -------------------------------------------------------------------------------- /test/examples/example5.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import InterfaceSystem 4 | 5 | 6 | intsys = InterfaceSystem(['Ca', 'C', 'Na', 'Cl', 'Mg'], from_elements=True) 7 | 8 | elements_balance = {'Ca':0.028, 'C':0.065, 'Na':0.075, 'Cl':0.056, 'Mg':0.02} 9 | TK = 298.15 10 | solution, res = intsys.\ 11 | solve_equilibrium_mixed_balance(TK, molal_balance=elements_balance) 12 | 13 | intsys.set_interface_phases(phases=['Calcite'], fill_defaults=True) 14 | molals_bulk = solution.solute_molals 15 | 16 | transport_params = {'type': 'pipe', 17 | 'shear_velocity': 1e-1} 18 | 19 | solution_int, res = intsys.solve_interface_equilibrium(TK, 20 | molals_bulk, 21 | transport_params, 22 | fully_diffusive=False, 23 | transport_model="B") -------------------------------------------------------------------------------- /test/examples/example6.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | import numpy as np 5 | import scipy.integrate 6 | 7 | from pyequion2 import InterfaceSystem 8 | from pyequion2 import water_properties 9 | 10 | 11 | def reynolds_number(flow_velocity, pipe_diameter, TK=298.15): #Dimensionless 12 | kinematic_viscosity = water_properties.water_kinematic_viscosity(TK) 13 | return flow_velocity*pipe_diameter/kinematic_viscosity 14 | 15 | 16 | def darcy_friction_factor(flow_velocity, pipe_diameter, TK=298.15): 17 | reynolds = reynolds_number(flow_velocity, pipe_diameter, TK) 18 | if reynolds < 2300: 19 | return 64/reynolds 20 | else: #Blasius 21 | return 0.316*reynolds**(-1./4) 22 | 23 | 24 | def shear_velocity(flow_velocity, pipe_diameter, TK=298.15): 25 | f = darcy_friction_factor(flow_velocity, pipe_diameter, TK) 26 | return np.sqrt(f/8.0)*flow_velocity 27 | 28 | 29 | elements = ['Ca', 'C', 'Na', 'Cl'] 30 | intsys = InterfaceSystem(elements, from_elements=True) 31 | intsys.set_interface_phases(['Vaterite'], fill_defaults=True) 32 | index_map = {el: i for i, el in enumerate(elements)} 33 | reverse_index_map = {i: el for i, el in enumerate(elements)} 34 | 35 | TK = 298.15 36 | pipe_diameter = 0.05 #m 37 | flow_velocity = 1.0 38 | pipe_length = 80.0 #m 39 | pipe_time = pipe_length/flow_velocity 40 | 41 | transport_params = {'type': 'pipe', 42 | 'shear_velocity': shear_velocity(flow_velocity, pipe_diameter, TK)} 43 | solution_stats = {'res': None, 'x': 'default'} 44 | solution_stats_int = {'res': None, 'x': 'default'} 45 | 46 | def f(t, y): 47 | global solution_stats 48 | global solution_stats_int 49 | elements_balance = {el: y[index_map[el]] for el in elements} 50 | solution, solution_stats = intsys.solve_equilibrium_mixed_balance(TK, 51 | molal_balance=elements_balance, 52 | tol=1e-6, 53 | initial_guess=solution_stats['x']) 54 | molals_bulk = solution.solute_molals 55 | solution_int, solution_stats_int = intsys.solve_interface_equilibrium(TK, 56 | molals_bulk, 57 | transport_params, 58 | tol=1e-6, 59 | initial_guess=solution_stats_int['x']) 60 | elements_reaction_fluxes = solution_int.elements_reaction_fluxes 61 | wall_scale = 4/(pipe_diameter*water_properties.water_density(TK)) #(4/m*m^3/kg) -> #4*m^2/kg 62 | dy = -wall_scale*np.hstack( 63 | [elements_reaction_fluxes[reverse_index_map[i]] 64 | for i in range(y.shape[0])]) 65 | return dy 66 | 67 | 68 | initial_elements_balance = {'Ca':0.028, 'C':0.065, 'Na':0.075, 'Cl':0.056} 69 | initial_elements_vector = np.hstack([initial_elements_balance[reverse_index_map[i]] 70 | for i in range(len(initial_elements_balance))]) 71 | 72 | start_time = time.time() 73 | sol = scipy.integrate.solve_ivp(f, (0.0, pipe_time), initial_elements_vector) 74 | elapsed_time = time.time() - start_time 75 | -------------------------------------------------------------------------------- /test/examples/example7.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | import numpy as np 5 | import scipy.integrate 6 | import matplotlib.pyplot as plt 7 | 8 | from pyequion2 import InterfaceSystem 9 | from pyequion2 import water_properties 10 | 11 | 12 | def reynolds_number(flow_velocity, pipe_diameter, TK=298.15): #Dimensionless 13 | kinematic_viscosity = water_properties.water_kinematic_viscosity(TK) 14 | return flow_velocity*pipe_diameter/kinematic_viscosity 15 | 16 | 17 | def darcy_friction_factor(flow_velocity, pipe_diameter, TK=298.15): 18 | reynolds = reynolds_number(flow_velocity, pipe_diameter, TK) 19 | if reynolds < 2300: 20 | return 64/reynolds 21 | else: #Blasius 22 | return 0.316*reynolds**(-1./4) 23 | 24 | 25 | def shear_velocity(flow_velocity, pipe_diameter, TK=298.15): 26 | f = darcy_friction_factor(flow_velocity, pipe_diameter, TK) 27 | return np.sqrt(f/8.0)*flow_velocity 28 | 29 | 30 | elements = ['Ca', 'C', 'Mg'] 31 | intsys = InterfaceSystem(elements, from_elements=True) 32 | intsys.set_interface_phases(['Calcite', 'Dolomite']) 33 | 34 | TK = 298.15 35 | pipe_diameter = 0.01 #m 36 | flow_velocity = 1.0 37 | pipe_length = 80.0 #m 38 | pipe_time = pipe_length/flow_velocity 39 | 40 | co2_flash_value = 0.001 41 | initial_ca_value = 0.02 42 | initial_mg_value = 0.01 43 | 44 | transport_params = {'type': 'pipe', 45 | 'shear_velocity': shear_velocity(flow_velocity, pipe_diameter, TK)} 46 | solution_stats = {'res': None, 'x': 'default'} 47 | solution_stats_int = {'res': None, 'x': 'default'} 48 | 49 | def f(t, y): 50 | global solution_stats 51 | global solution_stats_int 52 | molal_balance = {'Ca': y[0], 'Mg': y[1], 'CO2': co2_flash_value} 53 | solution, solution_stats = intsys.solve_equilibrium_mixed_balance(TK, 54 | molal_balance=molal_balance, 55 | tol=1e-6, 56 | initial_guess=solution_stats['x']) 57 | molals_bulk = solution.solute_molals 58 | solution_int, solution_stats_int = intsys.solve_interface_equilibrium(TK, 59 | molals_bulk, 60 | transport_params, 61 | tol=1e-6, 62 | initial_guess=solution_stats_int['x']) 63 | elements_reaction_fluxes = solution_int.elements_reaction_fluxes 64 | wall_scale = 4/(pipe_diameter*water_properties.water_density(TK)) 65 | dy = -wall_scale*np.array( 66 | [elements_reaction_fluxes['Ca'], elements_reaction_fluxes['Mg']]) 67 | return dy 68 | 69 | 70 | initial_vector = np.array([initial_ca_value, initial_mg_value]) 71 | start_time = time.time() 72 | sol = scipy.integrate.solve_ivp(f, (0.0, pipe_time), initial_vector, 73 | t_eval = np.linspace(0.0, pipe_time, 101)) 74 | elapsed_time = time.time() - start_time 75 | 76 | plt.plot(sol.t, sol.y[0], label='Ca') 77 | plt.plot(sol.t, sol.y[1], label='Mg') 78 | plt.xlabel('t') 79 | plt.ylabel('c') 80 | plt.legend() 81 | plt.show() 82 | -------------------------------------------------------------------------------- /test/examples/example8.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import pyequion2 5 | eqsys = pyequion2.EquilibriumSystem(['Na', 'Cl'], from_elements=True) #We set up the feed components of our system 6 | molal_balance = {'Na':0.065, 'Cl':0.065} #Set up the balances 7 | eqsys.update_system(eqsys.reactions[1:]) 8 | TK = 298.15 #Temperature in Kelvin 9 | PATM = 1.0 #Pressure in atm 10 | #Returns the solution class (the second argument are solver statistics, no need for understanding now) 11 | solution, solution_stats = eqsys.solve_equilibrium_mixed_balance(TK, molal_balance=molal_balance, PATM=PATM) -------------------------------------------------------------------------------- /test/suite/converters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import converters 4 | 5 | def test1(): 6 | el = 'CaCO3' # 7 | x = 1.0 #mol/kg H2O \approx mol/L 8 | y = converters.molal_to_mgl(x, el) 9 | CORRECT_VALUE_Y = 99798.8386920672 10 | assert(abs(y-CORRECT_VALUE_Y)/CORRECT_VALUE_Y < 1e-6) 11 | x2 = converters.mgl_to_molal(y, el) 12 | assert(abs(x - x2)/x2 < 1e-6) 13 | 14 | def test2(): 15 | spec = 'CO2' 16 | pp = 1.0 17 | TK = 298.15 18 | act = converters.get_activity_from_partial_pressure(pp, spec, TK) 19 | comparisor = 3.4*1e-2 20 | assert(abs(act - comparisor)/comparisor < 1e-2) 21 | 22 | def test3(): 23 | phase = "Calcite" 24 | molar_mass = converters.phase_to_molar_weight("Calcite") 25 | print(molar_mass) 26 | 27 | test3() -------------------------------------------------------------------------------- /test/suite/eqsys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pyequion2 4 | 5 | eqsys = pyequion2.EquilibriumSystem([]) -------------------------------------------------------------------------------- /test/suite/gaseous_system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pyequion2 4 | from pyequion2 import gaseous_system, converters 5 | 6 | gsys = gaseous_system.InertGaseousSystem(["CO2(g)"], fugacity_model="PENGROBINSON") 7 | logacts = gsys.get_fugacity({"CO2(g)":1.0}, 300, 100) 8 | fugacity = 10**logacts['CO2(g)'] -------------------------------------------------------------------------------- /test/suite/intsys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pyequion2 import InterfaceSystem 4 | 5 | def test1(): 6 | intsys = InterfaceSystem(['Ca', 'C', 'Na', 'Cl', 'Mg'], from_elements=True) 7 | 8 | elements_balance = {'Ca':0.028, 'C':0.065, 'Na':0.075, 'Cl':0.056, 'Mg':0.02} 9 | TK = 298.15 10 | solution, res = intsys.solve_equilibrium_mixed_balance(TK, molal_balance=elements_balance) 11 | 12 | #Run 1a 13 | intsys.set_interface_phases(phases=['Calcite'], fill_defaults=True) 14 | molals_bulk = solution.solute_molals 15 | transport_params = {'type': 'pipe', 16 | 'shear_velocity': 0.01} 17 | solution_int, res = intsys.solve_interface_equilibrium(TK, 18 | molals_bulk, 19 | transport_params, 20 | fully_diffusive=False, 21 | transport_model=None) 22 | 23 | #Run 1b 24 | intsys.set_global_transport_model("B") 25 | solution_int, res = intsys.solve_interface_equilibrium(TK, 26 | molals_bulk, 27 | transport_params, 28 | fully_diffusive=False, 29 | transport_model="A") 30 | 31 | #Run 1c 32 | intsys.set_interface_phases() 33 | transport_params = {'type': 'sphere', 34 | 'radius': 1e-6} 35 | solution_int, res = intsys.solve_interface_equilibrium(TK, 36 | molals_bulk, 37 | transport_params, 38 | fully_diffusive=True, 39 | transport_model=None) 40 | 41 | test1() -------------------------------------------------------------------------------- /test/test_suite_1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | import phreeqpython 5 | 6 | import pyequion2 7 | 8 | temperature = 25 9 | pp = phreeqpython.PhreeqPython() 10 | solution_pp = pp.add_solution_simple({'CaCl2':1.0,'NaHCO3':2.0}, temperature = temperature) 11 | 12 | eqsys = pyequion2.EquilibriumSystem(['CaCl2', 'NaHCO3'], activity_model="PITZER") #We set up the feed components of our system 13 | molal_balance = {'Ca':0.001, 'C':0.002, 'Na':0.002, 'Cl':0.002} #Set up the balances 14 | TK = 273.15 + temperature #Temperature in Kelvin 15 | PATM = 1.0 #Pressure in atm 16 | #Returns the solution class (the second argument are solver statistics, no need for understanding now) 17 | solution, solution_stats = eqsys.solve_equilibrium_mixed_balance(TK, molal_balance=molal_balance, PATM=PATM) --------------------------------------------------------------------------------